feat: 代码生成适配多语言后端

This commit is contained in:
Ray.Hao
2026-01-27 20:44:00 +08:00
parent b381f03633
commit 6ff4a65ec9
2 changed files with 62 additions and 88 deletions

View File

@@ -12,6 +12,10 @@ export interface GeneratorPreviewItem {
fileName: string; fileName: string;
/** 文件内容 */ /** 文件内容 */
content: string; content: string;
/** 文件范围(frontend/backend) */
scope: "frontend" | "backend";
/** 文件语言(扩展名) */
language: string;
} }
/** 数据表分页查询参数 */ /** 数据表分页查询参数 */

View File

@@ -388,7 +388,7 @@
@node-click="handleFileTreeNodeClick" @node-click="handleFileTreeNodeClick"
> >
<template #default="{ data }"> <template #default="{ data }">
<div :class="`i-svg:${getFileTreeNodeIcon(data.label)}`" /> <div :class="`i-svg:${getFileTreeNodeIcon(data)}`" />
<span class="ml-1">{{ data.label }}</span> <span class="ml-1">{{ data.label }}</span>
</template> </template>
</el-tree> </el-tree>
@@ -525,7 +525,13 @@ import type { EditorConfiguration } from "codemirror";
import { FormTypeEnum, QueryTypeEnum } from "@/enums/codegen"; import { FormTypeEnum, QueryTypeEnum } from "@/enums/codegen";
import GeneratorAPI from "@/api/codegen"; import GeneratorAPI from "@/api/codegen";
import type { FieldConfig, GenConfigForm, TableQueryParams, TableItem } from "@/api/types"; import type {
FieldConfig,
GenConfigForm,
TableQueryParams,
TableItem,
GeneratorPreviewItem,
} from "@/api/types";
import { ElLoading } from "element-plus"; import { ElLoading } from "element-plus";
import DictAPI from "@/api/system/dict"; import DictAPI from "@/api/system/dict";
@@ -535,43 +541,37 @@ interface TreeNode {
label: string; label: string;
content?: string; content?: string;
children?: TreeNode[]; children?: TreeNode[];
scope?: "frontend" | "backend";
language?: string;
} }
const treeData = ref<TreeNode[]>([]); const treeData = ref<TreeNode[]>([]);
const previewScope = ref<"all" | "frontend" | "backend">("all"); const previewScope = ref<"all" | "frontend" | "backend">("all");
const previewTypeOptions = ["ts", "vue", "java", "xml"]; const previewTypeOptions = ref<string[]>([]);
const previewTypes = ref<string[]>([...previewTypeOptions]); const previewTypes = ref<string[]>([]);
const frontendType = "ts"; const frontendType = "ts";
const filteredTreeData = computed<TreeNode[]>(() => { const filteredTreeData = computed<TreeNode[]>(() => {
if (!treeData.value.length) return []; if (!treeData.value.length) return [];
// 基于原树 scope/types 过滤叶子节点 // 基于原树 scope/types 过滤叶子节点
const match = (label: string, parentPath: string[]): boolean => { const match = (node: TreeNode): boolean => {
// scope 过滤:根据路径初步判断
const pathStr = parentPath.join("/");
if (previewScope.value !== "all") { if (previewScope.value !== "all") {
const isBackend = /(^|\/)src\/main\//.test(pathStr) || /(^|\/)java\//.test(pathStr); if (node.scope !== previewScope.value) return false;
const scopeOfNode = isBackend ? "backend" : "frontend";
if (scopeOfNode !== previewScope.value) return false;
} }
// 类型过滤:根据后缀 if (!previewTypes.value.length) return true;
const ext = label.split(".").pop() || ""; const language = node.language || node.label.split(".").pop() || "";
return previewTypes.value.includes(ext); return previewTypes.value.includes(language);
}; };
const cloneFilter = (node: TreeNode, parents: string[] = []): TreeNode | null => { const cloneFilter = (node: TreeNode): TreeNode | null => {
if (!node.children || node.children.length === 0) { if (!node.children || node.children.length === 0) {
return match(node.label, parents) ? { ...node } : null; return match(node) ? { ...node } : null;
} }
const nextParents = [...parents, node.label]; const children = (node.children || []).map((c) => cloneFilter(c)).filter(Boolean) as TreeNode[];
const children = (node.children || [])
.map((c) => cloneFilter(c, nextParents))
.filter(Boolean) as TreeNode[];
if (!children.length) return null; if (!children.length) return null;
return { label: node.label, children }; return { label: node.label, children };
}; };
const filtered = treeData.value.map((n) => cloneFilter(n)).filter(Boolean) as TreeNode[]; return treeData.value.map((n) => cloneFilter(n)).filter(Boolean) as TreeNode[];
return filtered;
}); });
const queryFormRef = ref(); const queryFormRef = ref();
@@ -647,12 +647,12 @@ const backendDirHandle = ref<any>(null);
const frontendDirName = ref(""); const frontendDirName = ref("");
const backendDirName = ref(""); const backendDirName = ref("");
// 预览的原始文件列表(用于写盘) // 预览的原始文件列表(用于写盘)
const lastPreviewFiles = ref<{ path: string; fileName: string; content: string }[]>([]); const lastPreviewFiles = ref<GeneratorPreviewItem[]>([]);
const needFrontend = computed(() => const needFrontend = computed(() =>
lastPreviewFiles.value.some((f) => resolveRootForPath(f.path) === "frontend") lastPreviewFiles.value.some((f) => resolveRootForItem(f) === "frontend")
); );
const needBackend = computed(() => const needBackend = computed(() =>
lastPreviewFiles.value.some((f) => resolveRootForPath(f.path) === "backend") lastPreviewFiles.value.some((f) => resolveRootForItem(f) === "backend")
); );
const canWriteToLocal = computed(() => { const canWriteToLocal = computed(() => {
if (!lastPreviewFiles.value.length) return false; if (!lastPreviewFiles.value.length) return false;
@@ -913,8 +913,19 @@ async function handlePreview(tableName: string) {
frontendType frontendType
); );
dialog.title = `代码生成 ${tableName}`; dialog.title = `代码生成 ${tableName}`;
const tree = buildTree(data); const previewList = data || [];
lastPreviewFiles.value = data || []; const typeOptions = Array.from(
new Set(
previewList
.map((item) => item.language || item.fileName.split(".").pop() || "")
.filter(Boolean)
)
);
previewTypeOptions.value = typeOptions;
previewTypes.value = [...typeOptions];
const tree = buildTree(previewList);
lastPreviewFiles.value = previewList;
treeData.value = tree?.children ? [...tree.children] : []; treeData.value = tree?.children ? [...tree.children] : [];
const firstLeafNode = findFirstLeafNode(tree); const firstLeafNode = findFirstLeafNode(tree);
@@ -933,52 +944,17 @@ async function handlePreview(tableName: string) {
* @param data - 数据数组 * @param data - 数据数组
* @returns 树形结构根节点 * @returns 树形结构根节点
*/ */
function buildTree(data: { path: string; fileName: string; content: string }[]): TreeNode { function buildTree(data: GeneratorPreviewItem[]): TreeNode {
// 动态获取根节点 // 动态获取根节点
const root: TreeNode = { label: "前后端代码", children: [] }; const root: TreeNode = { label: "前后端代码", children: [] };
data.forEach((item) => { data.forEach((item) => {
// 将路径分成数组 const normalizedPath = item.path.replace(/\\/g, "/");
const separator = item.path.includes("/") ? "/" : "\\"; const parts = normalizedPath.split("/").filter(Boolean);
const parts = item.path.split(separator);
// 定义特殊路径
const specialPaths = [
"src" + separator + "main",
"java",
genConfigFormData.value.backendAppName,
genConfigFormData.value.frontendAppName,
(genConfigFormData.value.packageName + "." + genConfigFormData.value.moduleName).replace(
/\./g,
separator
),
];
// 检查路径中的特殊部分并合并它们
const mergedParts: string[] = [];
let buffer: string[] = [];
parts.forEach((part) => {
buffer.push(part);
const currentPath = buffer.join(separator);
if (specialPaths.includes(currentPath)) {
mergedParts.push(currentPath);
buffer = [];
}
});
// 将 mergedParts 路径中的分隔符 \ 替换为 /
mergedParts.forEach((part, index) => {
mergedParts[index] = part.replace(/\\/g, "/");
});
if (buffer.length > 0) {
mergedParts.push(...buffer);
}
let currentNode = root; let currentNode = root;
mergedParts.forEach((part) => { parts.forEach((part) => {
// 查找或创建当前部分的子节点 // 查找或创建当前部分的子节点
let node = currentNode.children?.find((child) => child.label === part); let node = currentNode.children?.find((child) => child.label === part);
if (!node) { if (!node) {
@@ -992,6 +968,8 @@ function buildTree(data: { path: string; fileName: string; content: string }[]):
currentNode.children?.push({ currentNode.children?.push({
label: item.fileName, label: item.fileName,
content: item?.content, content: item?.content,
scope: item.scope,
language: item.language,
}); });
}); });
@@ -1024,22 +1002,26 @@ function handleFileTreeNodeClick(data: TreeNode) {
} }
/** 获取文件树节点图标 */ /** 获取文件树节点图标 */
function getFileTreeNodeIcon(label: string) { function getFileTreeNodeIcon(node: TreeNode) {
if (label.endsWith(".java")) { const ext = (node.language || node.label.split(".").pop() || "").toLowerCase();
if (ext === "java") {
return "java"; return "java";
} }
if (label.endsWith(".html")) { if (ext === "html") {
return "html"; return "html";
} }
if (label.endsWith(".vue")) { if (ext === "vue") {
return "vue"; return "vue";
} }
if (label.endsWith(".ts")) { if (ext === "ts") {
return "typescript"; return "typescript";
} }
if (label.endsWith(".xml")) { if (ext === "xml") {
return "xml"; return "xml";
} }
if (["cs", "go", "py", "php", "js"].includes(ext)) {
return "code";
}
return "file"; return "file";
} }
@@ -1138,23 +1120,11 @@ async function isSameFile(dirHandle: any, filePath: string, content: string): Pr
} }
} }
// 将模板中 path 映射到前/后端根目录 // 将预览条目映射到前/后端根目录(由后端给出 scope
function resolveRootForPath(p: string) { function resolveRootForItem(item: GeneratorPreviewItem) {
const normalized = p.replace(/\\/g, "/"); if (item.scope === "backend") {
const frontApp = genConfigFormData.value.frontendAppName;
const backApp = genConfigFormData.value.backendAppName;
if (
(backApp && normalized.startsWith(`${backApp}/`)) ||
normalized.includes("/src/main/") ||
normalized.startsWith("src/main/") ||
normalized.startsWith("java/")
) {
return "backend" as const; return "backend" as const;
} }
if ((frontApp && normalized.startsWith(`${frontApp}/`)) || normalized.startsWith("src/")) {
return "frontend" as const;
}
// 默认前端
return "frontend" as const; return "frontend" as const;
} }
@@ -1204,7 +1174,7 @@ const writeGeneratedCode = async () => {
let backCount = 0; let backCount = 0;
const failed: string[] = []; const failed: string[] = [];
const files = lastPreviewFiles.value.filter((f) => { const files = lastPreviewFiles.value.filter((f) => {
const root = resolveRootForPath(f.path); const root = resolveRootForItem(f);
return writeScope.value === "all" || root === writeScope.value; return writeScope.value === "all" || root === writeScope.value;
}); });
writeProgress.total = files.length; writeProgress.total = files.length;
@@ -1220,7 +1190,7 @@ const writeGeneratedCode = async () => {
while (queue.length) { while (queue.length) {
const item = queue.shift()!; const item = queue.shift()!;
try { try {
const root = resolveRootForPath(item.path); const root = resolveRootForItem(item);
const relativePath = stripProjectRoot(`${item.path}/${item.fileName}`); const relativePath = stripProjectRoot(`${item.path}/${item.fileName}`);
writeProgress.current = relativePath; writeProgress.current = relativePath;
if (overwriteMode.value === "ifChanged") { if (overwriteMode.value === "ifChanged") {