| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270 |
- <template>
- <div class="app-container">
- <PageSearch
- ref="searchRef"
- :search-config="searchConfig"
- @query-click="handleQueryClick"
- @reset-click="handleResetClick"
- />
- <PageContent ref="contentRef" :content-config="contentConfig">
- <template #toolbar="{ toolbarRight, onToolbar, removeIds, cols }">
- <CrudToolbarLeft :remove-ids="removeIds">
- <el-row :gutter="10">
- <el-col :span="1.5">
- <el-button
- v-hasPerm="['module_generator:gencode:create']"
- type="primary"
- icon="Plus"
- @click="createTableVisible = true"
- >
- 创建
- </el-button>
- </el-col>
- <el-col :span="1.5">
- <el-button
- v-hasPerm="['module_generator:gencode:import']"
- type="success"
- icon="Upload"
- @click="handleImportClick"
- >
- 导入
- </el-button>
- </el-col>
- <el-col :span="1.5">
- <el-button
- v-hasPerm="['module_generator:gencode:delete']"
- type="danger"
- icon="Delete"
- :disabled="removeIds.length === 0"
- @click="handleDelete()"
- >
- 批量删除
- </el-button>
- </el-col>
- <el-col :span="1.5">
- <el-button
- v-hasPerm="['module_generator:gencode:operate']"
- type="warning"
- icon="Download"
- :disabled="removeIds.length === 0"
- @click="handleGenTable('0')"
- >
- 批量生成
- </el-button>
- </el-col>
- </el-row>
- </CrudToolbarLeft>
- <div class="data-table__toolbar--right">
- <CrudToolbarRight :buttons="toolbarRight" :cols="cols" :on-toolbar="onToolbar" />
- </div>
- </template>
- <template #table="{ data, loading: tableLoading, tableRef, onSelectionChange, pagination }">
- <div class="data-table__content">
- <el-table
- :ref="tableRef as any"
- v-loading="tableLoading"
- row-key="id"
- :data="data"
- height="100%"
- border
- stripe
- @selection-change="
- (s) => {
- handleTableSelectionChange(s as GenTableSchema[]);
- onSelectionChange(s);
- }
- "
- >
- <template #empty>
- <el-empty :image-size="80" description="暂无数据" />
- </template>
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'selection')?.show"
- type="selection"
- align="center"
- width="55"
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'index')?.show"
- label="序号"
- type="index"
- min-width="30"
- align="center"
- fixed
- >
- <template #default="scope">
- <span>
- {{ (pagination.currentPage - 1) * pagination.pageSize + scope.$index + 1 }}
- </span>
- </template>
- </el-table-column>
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'table_name')?.show"
- label="表名称"
- prop="table_name"
- :show-overflow-tooltip="true"
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'table_comment')?.show"
- label="表描述"
- prop="table_comment"
- :show-overflow-tooltip="true"
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'class_name')?.show"
- label="实体"
- prop="class_name"
- :show-overflow-tooltip="true"
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'created_time')?.show"
- label="创建时间"
- prop="created_time"
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'updated_time')?.show"
- label="更新时间"
- prop="updated_time"
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'operation')?.show"
- label="操作"
- align="center"
- min-width="120"
- class-name="small-padding fixed-width"
- >
- <template #default="scope">
- <el-button
- v-hasPerm="['module_generator:gencode:update']"
- link
- type="primary"
- :icon="MagicStick"
- @click="handlePreviewTable(scope.row)"
- >
- 代码生成
- </el-button>
- <el-button
- v-hasPerm="['module_generator:gencode:delete']"
- link
- type="danger"
- icon="Delete"
- @click="handleDelete(scope.row)"
- >
- 删除
- </el-button>
- <el-button
- v-hasPerm="['module_generator:db:sync']"
- link
- type="success"
- icon="Refresh"
- @click="handleSynchDb(scope.row)"
- >
- 同步
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </template>
- </PageContent>
- <CreateTableDialog
- v-model="createTableVisible"
- :loading="loading"
- :link-from-gen="createTableLinkFromGen"
- @submit="handleCreateTableSubmit"
- />
- <ImportDbTableDialog
- ref="importDbDialogRef"
- v-model="importVisible"
- v-model:query="importQueryFormData"
- :data="dbTableList"
- :total="importTotal"
- :confirm-loading="importLoading"
- @query="handleImportQuery"
- @reset="handleImportReset"
- @confirm="handleImportTable"
- @fetch="getDbList"
- @selection-change="handleImportTableSelectionChange"
- />
- <GenCodeDrawer
- v-model="editVisible"
- v-model:preview-scope="previewScope"
- v-model:preview-types="previewTypes"
- v-model:code="code"
- :info="info"
- :rules="rules"
- :active-step="activeStep"
- :menu-options="menuOptions"
- :dict-options="dictOptions"
- :loading="loading"
- :next-step-loading="nextStepLoading"
- :preview-loading="previewLoading"
- :preview-type-options="previewTypeOptions"
- :filtered-tree-data="filteredTreeData"
- :cm-options="cmOptions"
- :bulk-set="bulkSet"
- @close="handleClose"
- @prev-step="prevStep"
- @next-step="nextStep"
- @gen-download="handleGenTable('0', info)"
- @gen-write="handleGenTable('1', info)"
- @clear-master-sub="clearMasterSub"
- @master-sub-blur="onMasterSubFieldBlur"
- @file-click="handleFileTreeNodeClick"
- @copy-code="handleCopyCode"
- />
- </div>
- </template>
- <script setup lang="ts">
- defineOptions({
- name: "GenCode",
- inheritAttrs: false,
- });
- import { ref, reactive, computed, onActivated, watch, nextTick, unref, provide } from "vue";
- import { useClipboard } from "@vueuse/core";
- import { useRoute } from "vue-router";
- import type { EditorConfiguration } from "codemirror";
- import type { CmComponentRef } from "codemirror-editor-vue3";
- import { ElMessage, ElMessageBox, type FormInstance } from "element-plus";
- import { MagicStick } from "@element-plus/icons-vue";
- import GencodeAPI, {
- type GenTableSchema,
- type DBTableSchema,
- type GenTablePageQuery,
- } from "@/api/module_generator/gencode";
- import MenuAPI, { MenuTable } from "@/api/module_system/menu";
- import DictAPI, { DictTable } from "@/api/module_system/dict";
- import { MenuTypeEnum } from "@/enums";
- import { useSettingsStore } from "@/store";
- import { ThemeMode } from "@/enums/settings/theme.enum";
- import CrudToolbarLeft from "@/components/CURD/CrudToolbarLeft.vue";
- import CrudToolbarRight from "@/components/CURD/CrudToolbarRight.vue";
- import PageSearch from "@/components/CURD/PageSearch.vue";
- import PageContent from "@/components/CURD/PageContent.vue";
- import CreateTableDialog, { type CreateTableSubmitMeta } from "./components/CreateTableDialog.vue";
- import GenCodeDrawer from "./components/GenCodeDrawer.vue";
- import ImportDbTableDialog from "./components/ImportDbTableDialog.vue";
- import { GENCODE_BASIC_FORM_KEY, GENCODE_CM_KEY } from "./gencodeInjectionKeys";
- import type { TreeNode } from "./types";
- import { useCrudList } from "@/components/CURD/useCrudList";
- import type { IContentConfig, ISearchConfig } from "@/components/CURD/types";
- // 表格列配置接口
- interface TableColumn {
- prop: string;
- label: string;
- show: boolean;
- minWidth?: string;
- formatter?: (row: any, column: any) => any;
- }
- // 文件数据接口
- interface FileData {
- path: string;
- file_name: string;
- content: string;
- full_path: string;
- }
- const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } = useCrudList();
- // 组件引用(与子组件 inject 同步,供校验 / CodeMirror 主题)
- const cmRef = ref<CmComponentRef>();
- const basicInfo = ref<FormInstance>();
- const importDbDialogRef = ref<InstanceType<typeof ImportDbTableDialog>>();
- provide(GENCODE_BASIC_FORM_KEY, basicInfo);
- provide(GENCODE_CM_KEY, cmRef);
- // 状态管理
- const loading = ref(false);
- const nextStepLoading = ref(false);
- const uniqueId = ref("");
- const editVisible = ref(false);
- const activeStep = ref(0);
- // UI状态
- const createTableVisible = ref(false);
- const importVisible = ref(false);
- // 表单和列表数据
- const dbTableList = ref<DBTableSchema[]>([]);
- const ids = ref<number[]>([]);
- const tableNames = ref<string[]>([]);
- // 导入弹窗专用状态
- const importLoading = ref(false);
- const importTotal = ref<number>(0);
- const importQueryFormData = reactive<GenTablePageQuery>({
- page_no: 1,
- page_size: 10,
- table_name: undefined,
- table_comment: undefined,
- });
- // 下拉选项数据
- const dictOptions = ref<DictTable[]>([]);
- const menuOptions = ref<OptionType[]>([]);
- // 表格数据
- type TableItem = {
- table_name: string;
- table_comment: string;
- };
- const tables = ref<TableItem[]>([]);
- // 导入按钮点击事件
- async function handleImportClick() {
- importVisible.value = true;
- await getDbList();
- }
- // 预览相关数据
- const preview = reactive({
- open: false,
- title: "代码预览",
- data: {},
- active_name: "controller.py",
- });
- /** 预览接口加载中(第三步) */
- const previewLoading = ref(false);
- const previewScope = ref<"all" | "frontend" | "backend">("all");
- const previewTypeOptions = ["ts", "vue", "python"];
- const previewTypes = ref<string[]>([...previewTypeOptions]);
- const code = ref<string>("");
- const treeData = ref<TreeNode[]>([]);
- const searchConfig = reactive<ISearchConfig>({
- permPrefix: "module_generator:gencode",
- colon: true,
- isExpandable: false,
- showNumber: 3,
- form: { labelWidth: "auto" },
- searchButtonPerm: "module_generator:gencode:query",
- resetButtonPerm: "module_generator:gencode:query",
- formItems: [
- {
- prop: "table_name",
- label: "表名称",
- type: "input",
- attrs: { placeholder: "请输入表名称", clearable: true, style: { width: "200px" } },
- },
- {
- prop: "table_comment",
- label: "表描述",
- type: "input",
- attrs: { placeholder: "请输入表描述", clearable: true, style: { width: "200px" } },
- },
- ],
- });
- const contentCols = reactive<TableColumn[]>([
- { prop: "selection", label: "选择框", show: true },
- { prop: "index", label: "序号", show: true },
- { prop: "table_name", label: "表名称", show: true },
- { prop: "table_comment", label: "表描述", show: true },
- { prop: "class_name", label: "实体", show: true },
- { prop: "created_time", label: "创建时间", show: true },
- { prop: "updated_time", label: "更新时间", show: true },
- { prop: "operation", label: "操作", show: true },
- ]);
- const contentConfig = reactive<IContentConfig<GenTablePageQuery>>({
- permPrefix: "module_generator:gencode",
- cols: contentCols as IContentConfig["cols"],
- hideColumnFilter: false,
- toolbar: [],
- defaultToolbar: ["refresh", "filter"],
- pagination: {
- pageSize: 10,
- pageSizes: [10, 20, 30, 50],
- },
- request: { page_no: "page_no", page_size: "page_size" },
- indexAction: async (params) => {
- const res = await GencodeAPI.listTable(params as GenTablePageQuery);
- return {
- total: res.data.data?.total || 0,
- list: res.data.data?.items || res.data.data?.list || [],
- };
- },
- });
- const settingsStore = useSettingsStore();
- // 主题计算属性
- const codeTheme = computed(() => (settingsStore.theme === ThemeMode.DARK ? "dracula" : "default"));
- // 监听主题变化并更新CodeMirror实例
- watch(codeTheme, (newTheme) => {
- if (cmRef.value && cmRef.value.cminstance) {
- cmRef.value.cminstance.setOption("theme", newTheme);
- }
- });
- // CodeMirror配置
- const cmOptions: EditorConfiguration = {
- mode: "text/javascript",
- lineNumbers: true,
- smartIndent: true,
- indentUnit: 2,
- tabSize: 2,
- readOnly: false,
- theme: codeTheme.value,
- lineWrapping: true,
- autofocus: false,
- };
- // 工具函数
- const { copy } = useClipboard();
- // ===== 计算属性 =====
- // 过滤后的文件树数据
- const filteredTreeData = computed<TreeNode[]>(() => {
- if (!treeData.value.length) return [];
- // 基于原树按 scope/types 过滤叶子节点
- const match = (label: string, parentPath: string[]): boolean => {
- // scope 过滤:根据路径初步判断
- if (previewScope.value !== "all") {
- // 根据后端返回的格式,检查路径或文件名特征
- const isPythonBackend =
- parentPath.some((part) => part === "backend" || part === "python") || label.includes(".py");
- const isVueFrontend =
- parentPath.some((part) => part === "frontend" || part === "vue") ||
- label.includes(".vue") ||
- label.includes(".ts");
- if (previewScope.value === "backend" && !isPythonBackend) return false;
- if (previewScope.value === "frontend" && !isVueFrontend) return false;
- }
- // 类型过滤:根据文件内容特征判断类型
- if (label.endsWith(".py")) return previewTypes.value.includes("python");
- if (label.endsWith(".vue")) return previewTypes.value.includes("vue");
- if (label.endsWith(".ts")) return previewTypes.value.includes("ts");
- return true;
- };
- const cloneFilter = (node: TreeNode, parents: string[] = []): TreeNode | null => {
- if (!node.children || node.children.length === 0) {
- return match(node.label, parents) ? { ...node } : null;
- }
- const nextParents = [...parents, node.label];
- const children = (node.children || [])
- .map((c) => cloneFilter(c, nextParents))
- .filter(Boolean) as TreeNode[];
- if (!children.length) return null;
- return { label: node.label, children };
- };
- const filtered = treeData.value.map((n) => cloneFilter(n)).filter(Boolean) as TreeNode[];
- return filtered;
- });
- // ===== 功能函数 =====
- /** 一键复制代码 */
- const handleCopyCode = () => {
- const content = code.value;
- if (content) {
- copy(content);
- ElMessage.success("代码复制成功");
- } else {
- ElMessage.warning("没有可复制的代码");
- }
- };
- /** 文件树节点点击事件 */
- function handleFileTreeNodeClick(data: TreeNode): void {
- if (data && (!data.children || data.children.length === 0)) {
- code.value = data.content || "";
- void nextTick(() => applyPreviewEditorMode(data.label));
- }
- }
- /** 递归构建树形结构 */
- function buildTree(data: FileData[]): TreeNode {
- // 创建根节点
- const root: TreeNode = { label: "前后端代码", children: [] };
- data.forEach((item) => {
- // 将路径分成数组(确保使用正斜杠)
- const parts = item.path.split("/").filter((part) => part !== "");
- let currentNode = root;
- // 遍历路径部分,创建对应的文件夹节点
- parts.forEach((part) => {
- // 查找或创建当前部分的子节点
- let node = currentNode.children?.find((child) => child.label === part);
- if (!node) {
- node = { label: part, children: [] };
- currentNode.children?.push(node);
- }
- currentNode = node;
- });
- // 添加文件节点(保持原有目录树展示:文件节点仅显示文件名)
- currentNode.children?.push({
- label: item.file_name,
- full_path: item.full_path,
- content: item?.content,
- });
- });
- return root;
- }
- /** 深度优先取第一个文件节点 */
- function findFirstLeafInTree(nodes: TreeNode[]): TreeNode | null {
- for (const node of nodes) {
- if (!node.children || node.children.length === 0) {
- return node;
- }
- const leaf = findFirstLeafInTree(node.children);
- if (leaf) return leaf;
- }
- return null;
- }
- /** 按文件名切换预览区语法高亮 */
- function applyPreviewEditorMode(fileLabel: string) {
- const inst = cmRef.value?.cminstance;
- if (!inst) return;
- let mode = "text/javascript";
- if (fileLabel.endsWith(".py")) mode = "text/x-python";
- else if (fileLabel.endsWith(".vue")) mode = "text/html";
- else if (fileLabel.endsWith(".ts")) mode = "text/typescript";
- inst.setOption("mode", mode);
- }
- /** 获取生成预览 */
- async function handlePreview(row: GenTableSchema): Promise<void> {
- if (!row.id) {
- ElMessage.warning("无效的表ID");
- return;
- }
- previewLoading.value = true;
- try {
- const response = await GencodeAPI.previewTable(row.id!);
- const raw = response.data?.data;
- if (!raw || typeof raw !== "object" || Object.keys(raw).length === 0) {
- ElMessage.warning("预览内容为空,请先保存配置并检查字段与主子表设置");
- treeData.value = [];
- code.value = "";
- preview.data = {};
- return;
- }
- preview.data = raw;
- const filesData = Object.entries(raw).map(([key, content]) => {
- const pathParts = key.split("/");
- let fileName = pathParts.pop() || "";
- const path = pathParts.join("/");
- if (fileName.endsWith(".j2")) {
- fileName = fileName.substring(0, fileName.lastIndexOf(".j2"));
- }
- const contentStr = typeof content === "string" ? content : JSON.stringify(content);
- return {
- path,
- file_name: fileName,
- content: contentStr,
- full_path: key,
- } as FileData;
- });
- const treeRoot = buildTree(filesData);
- // 预览树仅展示生成文件树(不额外展示“上级目录:xxx”)
- treeData.value = [treeRoot];
- await nextTick();
- let firstLeaf: TreeNode | null = null;
- for (const r of filteredTreeData.value) {
- firstLeaf = findFirstLeafInTree([r]);
- if (firstLeaf) break;
- }
- if (!firstLeaf) {
- firstLeaf = findFirstLeafInTree(treeData.value);
- }
- code.value = firstLeaf?.content || "";
- await nextTick();
- if (firstLeaf?.label) {
- applyPreviewEditorMode(firstLeaf.label);
- }
- preview.open = true;
- preview.active_name = "model.py";
- } catch (error) {
- console.error("预览代码失败:", error);
- } finally {
- previewLoading.value = false;
- }
- }
- /** 表格行内生成代码操作 */
- async function handleGenTable(targetGenType: string, row?: GenTableSchema): Promise<void> {
- let tbNames: string | string[] = [];
- // 判断是单条还是批量操作
- if (row) {
- tbNames = [row.table_name || ""];
- } else if (tableNames.value.length > 0) {
- tbNames = tableNames.value;
- } else {
- ElMessage.error("请选择要生成的数据");
- return;
- }
- loading.value = true;
- try {
- if (targetGenType === "1") {
- if (!Array.isArray(tbNames) || tbNames.length !== 1 || !tbNames[0]) {
- ElMessage.error("自定义路径只能生成单表代码");
- loading.value = false;
- return;
- }
- if (row?.id) await confirmWritePaths(row.id);
- await GencodeAPI.genCodeToPath(tbNames[0]);
- ElMessage.success("已写入项目目录并创建菜单(若尚未存在)");
- } else {
- // ZIP压缩包下载
- const tableNamesArray = Array.isArray(tbNames) ? tbNames : [tbNames];
- const response = await GencodeAPI.batchGenCode(tableNamesArray);
- const raw = response.data as Blob;
- if (raw.size < 100 && raw.type.includes("json")) {
- const text = await raw.text();
- try {
- const json = JSON.parse(text) as { msg?: string };
- ElMessage.error(json.msg || "批量生成失败");
- return;
- } catch {
- /* 非 JSON 小文件仍尝试下载 */
- }
- }
- const blob = new Blob([raw], { type: "application/zip" });
- const url = URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.download = "code.zip";
- link.click();
- URL.revokeObjectURL(url);
- ElMessage.success("已开始下载 code.zip");
- }
- } catch (error) {
- console.error("生成代码失败:", error);
- } finally {
- loading.value = false;
- }
- }
- function escapeHtml(s: string) {
- return s
- .replaceAll("&", "&")
- .replaceAll("<", "<")
- .replaceAll(">", ">")
- .replaceAll('"', """)
- .replaceAll("'", "'");
- }
- async function confirmWritePaths(tableId: number) {
- // 先保存当前抽屉配置,否则 preview 仍基于旧配置,回显会不准确
- await GencodeAPI.updateTable(info as GenTableSchema, tableId);
- const previewRes = await GencodeAPI.previewTable(tableId);
- const raw = previewRes.data?.data as Record<string, unknown> | undefined;
- const keys = raw && typeof raw === "object" ? Object.keys(raw) : [];
- const shown = keys.slice(0, 80);
- const more =
- keys.length > shown.length
- ? `<div style="margin-top:10px;padding:8px 12px;border-radius:6px;background:var(--el-fill-color-light);font-size:12px;color:var(--el-text-color-secondary);text-align:center">还有 <b style="color:var(--el-text-color-primary)">${keys.length - shown.length}</b> 个文件未列出</div>`
- : "";
- const listRows = shown
- .map((p, i) => {
- const bg = i % 2 === 0 ? "var(--el-fill-color-blank)" : "var(--el-fill-color-light)";
- return `<div class="gencode-write-path-row" style="padding:9px 14px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;line-height:1.45;white-space:nowrap;color:var(--el-text-color-primary);background:${bg};border-bottom:1px solid var(--el-border-color-lighter)">${escapeHtml(p)}</div>`;
- })
- .join("");
- const listHtml = shown.length
- ? `<div class="gencode-write-path-list-wrap">${listRows}</div>${more}`
- : `<div style="padding:16px;border-radius:8px;background:var(--el-fill-color-light);color:var(--el-text-color-secondary);font-size:13px;text-align:center">未获取到预览路径,仍将继续写入。</div>`;
- const tipHtml = `<div style="margin-top:12px;padding-top:10px;border-top:1px solid var(--el-border-color-lighter);font-size:12px;line-height:1.5;color:var(--el-text-color-secondary)">与「代码预览」同源;路径为相对项目根的落盘位置。</div>`;
- await ElMessageBox.confirm(
- `<div class="gencode-write-confirm-body" style="font-family:var(--el-font-family);line-height:1.5;color:var(--el-text-color-primary)">
- <div style="margin-bottom:12px">
- <div style="font-size:15px;font-weight:600;letter-spacing:0.02em">将写入以下文件</div>
- <div style="margin-top:4px;font-size:12px;color:var(--el-text-color-secondary)">共 ${keys.length} 项 · 相对项目根目录</div>
- </div>
- ${listHtml}
- ${shown.length ? tipHtml : ""}
- </div>`,
- "写入本地确认",
- {
- confirmButtonText: "确认写入",
- cancelButtonText: "取消",
- type: "warning",
- dangerouslyUseHTMLString: true,
- customClass: "gencode-write-confirm-box",
- }
- );
- }
- /** 同步数据库操作 */
- async function handleSynchDb(row: GenTableSchema): Promise<void> {
- const tableName = row.table_name || "";
- if (!tableName) {
- ElMessage.error("表名不能为空");
- return;
- }
- const renderSummary = (p: any) => {
- const added = p.added?.length ?? 0;
- const removed = p.removed?.length ?? 0;
- const changed = p.changed?.length ?? 0;
- const unchanged = p.unchanged ?? 0;
- return { added, removed, changed, unchanged };
- };
- const renderHtml = (title: string, p: any) => {
- const s = renderSummary(p);
- const list = (xs: string[]) => (xs?.length ? xs.slice(0, 20).join(", ") : "无");
- return `
- <div style="line-height:1.6">
- <div style="font-weight:600;margin-bottom:6px">${title}</div>
- <div>新增:<b>${s.added}</b>;删除:<b>${s.removed}</b>;变更:<b>${s.changed}</b>;未变:${s.unchanged}</div>
- <div style="margin-top:6px">新增列:${list(p.added || [])}</div>
- <div>删除列:${list(p.removed || [])}</div>
- <div>变更列:${list((p.changed || []).map((c: any) => c.column_name))}</div>
- <div style="margin-top:8px;color:var(--el-text-color-secondary)">提示:同步会尽量保留你已配置的 dict/html/query 等生成项,仅以数据库结构为准更新元信息。</div>
- </div>
- `;
- };
- try {
- loading.value = true;
- const previewRes = await GencodeAPI.syncDbPreview(tableName);
- const preview = previewRes.data?.data as any;
- const mainHtml = renderHtml(`主表:${tableName}`, preview);
- const subHtml =
- preview?.sub_table_name && preview?.sub
- ? renderHtml(`子表:${preview.sub_table_name}`, preview.sub)
- : "";
- await ElMessageBox.confirm(`${mainHtml}${subHtml}`, "同步差异预览", {
- confirmButtonText: "确认同步",
- cancelButtonText: "取消",
- type: "warning",
- dangerouslyUseHTMLString: true,
- });
- await GencodeAPI.syncDb(tableName);
- ElMessage.success("表结构已同步到代码生成配置");
- refreshList();
- } catch (error) {
- if (error !== "cancel") console.error("同步表结构失败:", error);
- } finally {
- loading.value = false;
- }
- }
- /** 多选框选中数据 - 主表格 */
- function handleTableSelectionChange(selection: GenTableSchema[]): void {
- ids.value = selection.map((item) => item.id!);
- tableNames.value = selection.map((item) => item.table_name || "").filter(Boolean);
- }
- type ImportTableSelectionRow = { table_name: string; table_comment: string };
- /** 多选框选中数据 - 导入表格 */
- function handleImportTableSelectionChange(rows: ImportTableSelectionRow[]): void {
- tables.value = rows;
- }
- /** 代码生成「上级菜单」仅展示目录节点,便于挂到目录下生成新菜单(不选菜单/按钮作为父级) */
- const filterMenuTypes = (nodes: MenuTable[]) => {
- return nodes
- .filter((node) => node.type === MenuTypeEnum.CATALOG)
- .map((node: any): any => ({
- ...node,
- children: node.children ? filterMenuTypes(node.children) : [],
- }));
- };
- /** 代码生成专用:保留 route_path,便于实时推断分系统 module_xxx */
- function formatMenuTreeWithMeta(nodes: any[]): any[] {
- return nodes.map((node) => {
- const formattedNode: any = {
- value: node.id,
- label: node.name,
- disabled: node.status === false || String(node.status) === "false",
- route_path: node.route_path,
- };
- if (node.children && node.children.length > 0) {
- formattedNode.children = formatMenuTreeWithMeta(node.children);
- }
- return formattedNode;
- });
- }
- /** 表格行内「代码生成」:先打开抽屉再拉数据,避免接口慢时误以为点不动 */
- async function handlePreviewTable(row?: GenTableSchema): Promise<void> {
- const selectedTableId = row?.id ?? ids.value[0];
- if (selectedTableId === undefined || selectedTableId === null) {
- ElMessage.error("请选择要修改的数据");
- return;
- }
- // 先用“列表行数据”把基础信息回显出来(接口慢时不至于看到空表单/旧数据闪烁)
- Object.assign(info, {
- id: row?.id ?? selectedTableId,
- table_name: row?.table_name || info.table_name || "",
- table_comment: row?.table_comment ?? info.table_comment ?? "",
- class_name: row?.class_name ?? info.class_name ?? "",
- package_name: row?.package_name ?? info.package_name ?? "",
- module_name: row?.module_name ?? info.module_name ?? "",
- business_name: row?.business_name ?? info.business_name ?? "",
- function_name: row?.function_name ?? info.function_name ?? "",
- description: row?.description ?? info.description ?? "",
- parent_menu_id: row?.parent_menu_id ?? info.parent_menu_id ?? undefined,
- sub_table_name: row?.sub_table_name ?? info.sub_table_name ?? "",
- sub_table_fk_name: row?.sub_table_fk_name ?? info.sub_table_fk_name ?? "",
- } as Partial<GenTableSchema>);
- // 字段列表以 detail 接口为准,避免上一张表的 columns 残留
- info.columns = [];
- activeStep.value = 0;
- editVisible.value = true;
- try {
- await loadTableDetail(selectedTableId);
- } catch (e) {
- console.error("获取表详情失败:", e);
- ElMessage.error("获取表详情失败,请稍后重试");
- // 保持抽屉打开,便于重试或关闭;勿因接口失败整抽屉被关掉像「点不动」
- return;
- }
- try {
- const [menu_response, dict_response] = await Promise.all([
- MenuAPI.listMenu(),
- DictAPI.listDictType({ page_no: 1, page_size: 100 }),
- ]);
- // 使用代码生成专用格式化:保留 route_path 供「分系统」实时回显
- menuOptions.value = formatMenuTreeWithMeta(filterMenuTypes(menu_response.data.data));
- dictOptions.value = dict_response.data.data?.items || dict_response.data.data?.list || [];
- } catch (e) {
- console.error("菜单或字典加载失败:", e);
- ElMessage.warning("菜单或字典选项加载失败,部分下拉可能为空");
- }
- }
- /** 删除按钮操作 */
- async function handleDelete(row?: GenTableSchema): Promise<void> {
- const tableIds = row?.id ? [row.id] : ids.value;
- if (tableIds.length === 0) {
- ElMessage.error("请选择要删除的数据");
- return;
- }
- try {
- await ElMessageBox.confirm(`是否确认删除选中的${tableIds.length}条数据?`, "删除确认", {
- confirmButtonText: "确定",
- cancelButtonText: "取消",
- type: "warning",
- });
- await GencodeAPI.deleteTable(tableIds);
- refreshList();
- } catch (error) {
- if (error !== "cancel") {
- console.error("删除表数据失败:", error);
- }
- }
- }
- /** 创建表(由 CreateTableDialog 提交 SQL;表结构模式成功后可回写第三步主子表配置) */
- async function handleCreateTableSubmit(sql: string, meta?: CreateTableSubmitMeta): Promise<void> {
- if (!sql || sql.trim() === "") {
- ElMessage.error("请输入创建表SQL语句");
- return;
- }
- loading.value = true;
- try {
- await GencodeAPI.createTable(sql);
- createTableVisible.value = false;
- if (editVisible.value && activeStep.value === 2 && meta?.fromVisual && meta.visualSnapshot) {
- const v = meta.visualSnapshot;
- info.table_name = (v.mainTableName || "").trim();
- const mc = (v.mainComment || "").trim();
- if (mc) info.table_comment = mc;
- if (v.subEnabled) {
- info.sub_table_name = (v.subTableName || "").trim();
- info.sub_table_fk_name = (v.fkColumn || "").trim();
- } else {
- info.sub_table_name = "";
- info.sub_table_fk_name = "";
- }
- info.master_sub_hint = undefined;
- void nextTick(() => {
- basicInfo.value?.clearValidate?.(["table_name", "sub_table_name", "sub_table_fk_name"]);
- });
- }
- refreshList();
- importVisible.value = true;
- await getDbList();
- } catch (error) {
- console.error("创建表数据失败:", error);
- } finally {
- loading.value = false;
- }
- }
- /** 导入表操作 */
- async function handleImportTable(): Promise<void> {
- if (tables.value.length === 0) {
- ElMessage.error("请选择要导入的表");
- return;
- }
- importLoading.value = true;
- try {
- // 提取表名数组
- const tableNames = tables.value.map((table) => table.table_name || "");
- await GencodeAPI.importTable(tableNames);
- importVisible.value = false;
- refreshList();
- // 导入成功后自动打开代码生成抽屉
- if (tables.value.length === 1) {
- await nextTick();
- const list = (unref(contentRef.value?.pageData) ?? []) as GenTableSchema[];
- const importedTable = list.find((t) => t.table_name === tables.value[0].table_name);
- if (importedTable) {
- await handlePreviewTable(importedTable);
- }
- } else {
- // 导入了多个表,刷新列表
- ElMessage.success(`成功导入 ${tables.value.length} 个表`);
- }
- } catch (error) {
- console.error("导入表失败:", error);
- } finally {
- importLoading.value = false;
- }
- }
- /** 查询数据库表数据 */
- async function getDbList(): Promise<void> {
- importLoading.value = true;
- try {
- const res = await GencodeAPI.listDbTable(importQueryFormData);
- if (res.data && res.data.data) {
- dbTableList.value = res.data.data?.items || res.data.data?.list || [];
- importTotal.value = res.data.data.total;
- }
- } catch (error) {
- console.error("获取数据库表列表失败:", error);
- } finally {
- importLoading.value = false;
- }
- }
- /** 导入弹窗搜索按钮操作 */
- async function handleImportQuery(): Promise<void> {
- importQueryFormData.page_no = 1;
- await getDbList();
- }
- /** 导入弹窗重置按钮操作 */
- async function handleImportReset(): Promise<void> {
- importDbDialogRef.value?.resetQueryForm();
- await handleImportQuery();
- }
- // 路由和导航
- const route = useRoute();
- // ===== 生命周期和初始化 =====
- /** 页面激活时执行 */
- onActivated(async () => {
- const time = route.query.t;
- if (time != null && String(time) !== uniqueId.value) {
- uniqueId.value = String(time);
- const pageNo = Number(route.query.page_no || 1);
- await nextTick();
- if (contentRef.value) {
- contentRef.value.pagination.currentPage = pageNo;
- const q = searchRef.value?.getQueryParams() ?? {};
- const f = contentRef.value.getFilterParams?.() ?? {};
- contentRef.value.fetchPageData({ ...q, ...f }, false);
- }
- }
- });
- // 表单数据(后端返回字段可能含 null,这里做更宽松的承载,避免 TS 因类型收窄报错)
- const info = reactive<
- GenTableSchema & {
- sub_table_name?: string | null;
- sub_table_fk_name?: string | null;
- }
- >({
- id: undefined,
- table_name: "",
- table_comment: "",
- sub_table_name: "",
- sub_table_fk_name: "",
- class_name: "",
- package_name: "",
- module_name: "",
- business_name: "",
- function_name: "",
- description: "",
- parent_menu_id: undefined,
- pk_column: undefined,
- sub_table: undefined,
- columns: [],
- sub: false,
- master_sub_hint: undefined,
- });
- /** 代码生成抽屉第三步打开时,创建表弹窗从当前表单预填主/子表名(表结构模式) */
- const createTableLinkFromGen = computed(() => {
- if (!editVisible.value || activeStep.value !== 2) return null;
- return {
- table_name: info.table_name,
- table_comment: info.table_comment,
- sub_table_name: info.sub_table_name ?? undefined,
- sub_table_fk_name: info.sub_table_fk_name ?? undefined,
- };
- });
- /** 主子表两项同填或同空,且子表名不得与主表相同 */
- function validateMasterSubPair(_rule: unknown, _value: unknown, callback: (e?: Error) => void) {
- const sn = (info.sub_table_name || "").trim();
- const fk = (info.sub_table_fk_name || "").trim();
- if (Boolean(sn) !== Boolean(fk)) {
- callback(new Error("子表表名与外键列须同时填写或同时留空"));
- return;
- }
- if (sn && fk && sn === (info.table_name || "").trim()) {
- callback(new Error("子表表名不能与主表表名相同"));
- return;
- }
- callback();
- }
- function onMasterSubFieldBlur() {
- void nextTick(() => {
- basicInfo.value?.validateField("sub_table_name").catch(() => {});
- basicInfo.value?.validateField("sub_table_fk_name").catch(() => {});
- });
- }
- function clearMasterSub() {
- info.sub_table_name = "";
- info.sub_table_fk_name = "";
- info.master_sub_hint = undefined;
- info.sub = false;
- info.sub_table = undefined;
- void nextTick(() => {
- basicInfo.value?.clearValidate(["sub_table_name", "sub_table_fk_name"]);
- });
- }
- /** module_example 风格下业务名可空;模块名示例见 demo、gen_demo02 */
- function validateBusinessName(_rule: unknown, value: unknown, callback: (e?: Error) => void) {
- const pkg = (info.package_name || "").trim();
- const mod = (info.module_name || "").trim();
- const isExampleStyle = pkg.startsWith("module_") && Boolean(mod) && !mod.startsWith("module_");
- if (isExampleStyle) {
- callback();
- return;
- }
- if (value == null || !String(value).trim()) {
- callback(new Error("业务名不能为空"));
- return;
- }
- callback();
- }
- // 校验规则
- const rules = {
- table_name: [{ required: true, message: "表名称不能为空", trigger: "blur" }],
- class_name: [{ required: true, message: "实体名称不能为空", trigger: "blur" }],
- package_name: [{ required: true, message: "生成包路径不能为空", trigger: "blur" }],
- module_name: [{ required: true, message: "生成模块名不能为空", trigger: "blur" }],
- business_name: [{ validator: validateBusinessName, trigger: "blur" }],
- function_name: [{ required: true, message: "生成功能名不能为空", trigger: "blur" }],
- /** 与后端一致:可选;不选时写入本地会按包名自动建目录菜单 */
- sub_table_name: [{ validator: validateMasterSubPair, trigger: "blur" }],
- sub_table_fk_name: [{ validator: validateMasterSubPair, trigger: "blur" }],
- };
- // ===== 工具函数
- /** 提交表单 - 保存配置(从基础配置进入字段配置时允许尚无列,便于先保存主表信息) */
- async function submitForm(options?: { requireColumns?: boolean }) {
- const requireColumns = options?.requireColumns !== false;
- // 检查是否有表ID
- if (!info.id) {
- ElMessage.error("无效的表ID");
- return;
- }
- try {
- loading.value = true;
- if (requireColumns && (!info.columns || info.columns.length === 0)) {
- ElMessage.error("请配置字段信息");
- return;
- }
- // 提交表单数据,确保columns是必需的,并且parent_menu_id总是被包含
- const tableData = {
- ...info,
- parent_menu_id: info.parent_menu_id ?? null, // 将undefined转换为null,确保属性被传输
- columns: info.columns || [], // 确保columns存在
- };
- delete (tableData as Record<string, unknown>).sub_table;
- delete (tableData as Record<string, unknown>).sub;
- delete (tableData as Record<string, unknown>).pk_column;
- delete (tableData as Record<string, unknown>).master_sub_hint;
- const savedColumns = info.columns;
- const res = await GencodeAPI.updateTable(tableData as GenTableSchema, info.id || 0);
- if (res.data?.data) {
- Object.assign(info, res.data.data as GenTableSchema);
- if (savedColumns && savedColumns.length > 0) {
- info.columns = savedColumns;
- }
- }
- return true;
- } catch (error) {
- console.error("保存表单失败:", error);
- } finally {
- loading.value = false;
- }
- }
- // 下一步
- async function nextStep(): Promise<void> {
- if (activeStep.value < 3) {
- nextStepLoading.value = true;
- try {
- // 在进入下一步前先保存当前配置
- if (activeStep.value === 0) {
- // 第一步:基础配置
- const basicInfoValid = await basicInfo.value?.validate().catch(() => false);
- if (!basicInfoValid) return;
- } else if (activeStep.value === 1) {
- // 第二步:字段配置
- if (!info.columns || info.columns.length === 0) {
- ElMessage.error("请配置字段信息");
- return;
- }
- }
- // 保存配置:从第 1 步离开时要求已配置列;从第 0 步进入字段配置时允许仅保存基础信息
- const saved = await submitForm({ requireColumns: activeStep.value !== 0 });
- if (!saved) return;
- activeStep.value++;
- // 当从字段配置进入预览步骤时,自动加载预览数据
- if (activeStep.value === 2 && info.id) {
- await handlePreview({ id: info.id, table_name: info.table_name } as GenTableSchema);
- }
- } finally {
- nextStepLoading.value = false;
- }
- }
- }
- // 上一步
- function prevStep(): void {
- if (activeStep.value > 0) {
- activeStep.value--;
- }
- }
- // 批量设置字段属性
- function bulkSet(field: string | string[], value: any): void {
- if (!info.columns || !Array.isArray(info.columns)) return;
- const fieldsToUpdate = Array.isArray(field) ? field : [field];
- info.columns.forEach((column) => {
- if (column && typeof column === "object") {
- fieldsToUpdate.forEach((f) => {
- (column as any)[f] = value;
- });
- }
- });
- }
- function close(): void {
- editVisible.value = false;
- activeStep.value = 0; // 重置步骤
- // 清除表单验证状态
- setTimeout(() => {
- basicInfo.value?.resetFields();
- }, 300);
- }
- /** 处理抽屉关闭事件 */
- function handleClose(): void {
- close();
- }
- /** 加载表详情 */
- async function loadTableDetail(id: number | string) {
- try {
- loading.value = true;
- const response = await GencodeAPI.detailTable(Number(id));
- if (response?.data?.data) {
- const data = response.data.data;
- // 填充表单数据
- Object.assign(info, { ...data });
- // 处理列数据
- if (data && data.columns && Array.isArray(data.columns)) {
- // 深拷贝确保数据独立性
- info.columns = JSON.parse(JSON.stringify(data.columns));
- // 设置列的选中状态
- (info.columns ?? []).forEach((item: any) => {
- item.select = true;
- });
- }
- // 重置当前步骤为第一步
- activeStep.value = 0;
- }
- } catch (error) {
- console.error("获取表详情失败:", error);
- throw error;
- } finally {
- loading.value = false;
- }
- }
- </script>
|