refactor: 代码生成页面布局重构优化
This commit is contained in:
122
src/views/codegen/composables/useCodePreview.ts
Normal file
122
src/views/codegen/composables/useCodePreview.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import GeneratorAPI from "@/api/codegen";
|
||||
import type { GenConfigForm } from "@/api/codegen";
|
||||
import {
|
||||
buildFileTree,
|
||||
filterTree,
|
||||
findFirstLeaf,
|
||||
findLeafByKey,
|
||||
getFileIcon,
|
||||
} from "../utils/tree-builder";
|
||||
import type { TreeNode } from "../utils/tree-builder";
|
||||
|
||||
export function useCodePreview(genConfigFormData: Ref<GenConfigForm>) {
|
||||
const treeData = ref<TreeNode[]>([]);
|
||||
const previewScope = ref<"all" | "frontend" | "backend">("all");
|
||||
const previewTypeOptions = ref<string[]>([]);
|
||||
const previewTypes = ref<string[]>([]);
|
||||
|
||||
const filteredTreeData = computed(() => {
|
||||
if (!treeData.value.length) return [];
|
||||
return filterTree(treeData.value, previewScope.value, previewTypes.value);
|
||||
});
|
||||
|
||||
const code = ref("");
|
||||
const currentFileKey = ref("");
|
||||
const fileTreeRef = ref();
|
||||
|
||||
const { copy, copied } = useClipboard();
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) ElMessage.success("复制成功");
|
||||
});
|
||||
|
||||
/** 获取预览数据并构建文件树 */
|
||||
async function handlePreview(tableName: string) {
|
||||
treeData.value = [];
|
||||
const pageType = genConfigFormData.value.pageType || "classic";
|
||||
const data = await GeneratorAPI.getPreviewData(tableName, pageType as "classic" | "curd", "ts");
|
||||
const previewList = 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 = buildFileTree(previewList);
|
||||
treeData.value = tree?.children ? [...tree.children] : [];
|
||||
|
||||
// 选中第一个叶子节点
|
||||
const firstLeaf = findFirstLeaf(tree);
|
||||
if (firstLeaf) {
|
||||
code.value = firstLeaf.content || "";
|
||||
currentFileKey.value = firstLeaf.key || "";
|
||||
await nextTick();
|
||||
fileTreeRef.value?.setCurrentKey?.(currentFileKey.value);
|
||||
}
|
||||
|
||||
return previewList;
|
||||
}
|
||||
|
||||
/** 点击文件树节点 */
|
||||
function handleFileTreeNodeClick(data: TreeNode) {
|
||||
if (!data.children || data.children.length === 0) {
|
||||
code.value = data.content || "";
|
||||
currentFileKey.value = data.key || "";
|
||||
}
|
||||
}
|
||||
|
||||
/** 复制代码 */
|
||||
function handleCopyCode() {
|
||||
if (code.value) copy(code.value);
|
||||
}
|
||||
|
||||
// 过滤条件变了之后,如果当前选中的文件还在就继续高亮,否则选第一个
|
||||
watch(
|
||||
() => filteredTreeData.value,
|
||||
async (nodes) => {
|
||||
if (!nodes.length) {
|
||||
currentFileKey.value = "";
|
||||
code.value = "";
|
||||
return;
|
||||
}
|
||||
if (currentFileKey.value) {
|
||||
const leaf = findLeafByKey(nodes, currentFileKey.value);
|
||||
if (leaf) {
|
||||
await nextTick();
|
||||
fileTreeRef.value?.setCurrentKey?.(currentFileKey.value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const first = findFirstLeaf({ label: "root", key: "root", children: nodes });
|
||||
if (first) {
|
||||
code.value = first.content || "";
|
||||
currentFileKey.value = first.key || "";
|
||||
await nextTick();
|
||||
fileTreeRef.value?.setCurrentKey?.(currentFileKey.value);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return {
|
||||
treeData,
|
||||
filteredTreeData,
|
||||
previewScope,
|
||||
previewTypes,
|
||||
previewTypeOptions,
|
||||
code,
|
||||
currentFileKey,
|
||||
fileTreeRef,
|
||||
handlePreview,
|
||||
handleFileTreeNodeClick,
|
||||
handleCopyCode,
|
||||
getFileIcon,
|
||||
};
|
||||
}
|
||||
106
src/views/codegen/composables/useGenConfig.ts
Normal file
106
src/views/codegen/composables/useGenConfig.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import GeneratorAPI from "@/api/codegen";
|
||||
import type { GenConfigForm } from "@/api/codegen";
|
||||
import DictAPI from "@/api/system/dict";
|
||||
import MenuAPI from "@/api/system/menu";
|
||||
import { QueryTypeEnum } from "@/enums/codegen";
|
||||
|
||||
export function useGenConfig() {
|
||||
const genConfigFormData = ref<GenConfigForm>({
|
||||
fieldConfigs: [],
|
||||
pageType: "classic",
|
||||
});
|
||||
|
||||
const genConfigFormRules = {
|
||||
tableName: [{ required: true, message: "请输入表名", trigger: "blur" }],
|
||||
businessName: [{ required: true, message: "请输入业务名", trigger: "blur" }],
|
||||
packageName: [{ required: true, message: "请输入主包名", trigger: "blur" }],
|
||||
moduleName: [{ required: true, message: "请输入模块名", trigger: "blur" }],
|
||||
entityName: [{ required: true, message: "请输入实体名", trigger: "blur" }],
|
||||
};
|
||||
|
||||
const menuOptions = ref<OptionItem[]>([]);
|
||||
const dictOptions = ref<OptionItem[]>([]);
|
||||
|
||||
/** 自动根据表前缀推导实体名 */
|
||||
watch(
|
||||
() => genConfigFormData.value.removeTablePrefix,
|
||||
(prefix) => {
|
||||
const table = genConfigFormData.value.tableName;
|
||||
if (!table) return;
|
||||
const p = prefix || "";
|
||||
const base = table.startsWith(p) ? table.slice(p.length) : table;
|
||||
const camel = base
|
||||
.split("_")
|
||||
.filter(Boolean)
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join("");
|
||||
genConfigFormData.value.entityName = camel;
|
||||
}
|
||||
);
|
||||
|
||||
// Date 类型字段默认用范围查询,比较符合直觉
|
||||
watch(
|
||||
() => genConfigFormData.value.fieldConfigs,
|
||||
(newVal) => {
|
||||
if (!newVal) return;
|
||||
newVal.forEach((fieldConfig) => {
|
||||
if (
|
||||
fieldConfig.fieldType?.includes("Date") &&
|
||||
fieldConfig.isShowInQuery === 1 &&
|
||||
fieldConfig.queryType == null
|
||||
) {
|
||||
fieldConfig.queryType = QueryTypeEnum.BETWEEN.value as number;
|
||||
}
|
||||
});
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
/** 加载配置:并行获取菜单、字典、生成配置 */
|
||||
async function loadConfig(tableName: string) {
|
||||
const [menuList, dictList, config] = await Promise.all([
|
||||
MenuAPI.getOptions(true),
|
||||
DictAPI.getList(),
|
||||
GeneratorAPI.getGenConfig(tableName),
|
||||
]);
|
||||
menuOptions.value = menuList;
|
||||
dictOptions.value = dictList;
|
||||
genConfigFormData.value = config;
|
||||
return config;
|
||||
}
|
||||
|
||||
/** 保存配置 */
|
||||
async function saveConfig(tableName: string) {
|
||||
await GeneratorAPI.saveGenConfig(tableName, genConfigFormData.value);
|
||||
}
|
||||
|
||||
/** 校验基础配置必填项 */
|
||||
function validateBasic(): boolean {
|
||||
const { tableName, packageName, businessName, moduleName, entityName } =
|
||||
genConfigFormData.value;
|
||||
if (!tableName || !packageName || !businessName || !moduleName || !entityName) {
|
||||
ElMessage.error("表名、业务名、包名、模块名、实体名不能为空");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 批量设置字段属性 */
|
||||
function bulkSet(key: "isShowInQuery" | "isShowInList" | "isShowInForm", value: 0 | 1) {
|
||||
const list = genConfigFormData.value?.fieldConfigs || [];
|
||||
list.forEach((row) => {
|
||||
row[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
genConfigFormData,
|
||||
genConfigFormRules,
|
||||
menuOptions,
|
||||
dictOptions,
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
validateBasic,
|
||||
bulkSet,
|
||||
};
|
||||
}
|
||||
237
src/views/codegen/composables/useLocalWrite.ts
Normal file
237
src/views/codegen/composables/useLocalWrite.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import type { GeneratorPreviewItem, GenConfigForm } from "@/api/codegen";
|
||||
import { ElLoading } from "element-plus";
|
||||
|
||||
export function useLocalWrite(genConfigFormData: Ref<GenConfigForm>) {
|
||||
const supportsFSAccess = typeof (window as any).showDirectoryPicker === "function";
|
||||
|
||||
const writeDialog = reactive({ visible: false });
|
||||
const frontendDirHandle = ref<FileSystemDirectoryHandle | null>(null);
|
||||
const backendDirHandle = ref<FileSystemDirectoryHandle | null>(null);
|
||||
const frontendDirPath = ref("");
|
||||
const backendDirPath = ref("");
|
||||
const writeScope = ref<"all" | "frontend" | "backend">("all");
|
||||
const overwriteMode = ref<"overwrite" | "skip" | "ifChanged">("overwrite");
|
||||
const writeProgress = reactive({ total: 0, done: 0, percent: 0, current: "" });
|
||||
const writeRunning = ref(false);
|
||||
const lastPreviewFiles = ref<GeneratorPreviewItem[]>([]);
|
||||
|
||||
const needFrontend = computed(() =>
|
||||
lastPreviewFiles.value.some((f) => resolveRootForItem(f) === "frontend")
|
||||
);
|
||||
const needBackend = computed(() =>
|
||||
lastPreviewFiles.value.some((f) => resolveRootForItem(f) === "backend")
|
||||
);
|
||||
// 只要有预览文件就可以点写入按钮,目录在弹窗里选
|
||||
const canWriteToLocal = computed(() => lastPreviewFiles.value.length > 0);
|
||||
|
||||
function openWriteDialog() {
|
||||
writeDialog.visible = true;
|
||||
}
|
||||
|
||||
function setPreviewFiles(files: GeneratorPreviewItem[]) {
|
||||
lastPreviewFiles.value = files;
|
||||
}
|
||||
|
||||
async function pickFrontendDir() {
|
||||
try {
|
||||
frontendDirHandle.value = await (window as any).showDirectoryPicker();
|
||||
frontendDirPath.value = frontendDirHandle.value?.name || "";
|
||||
ElMessage.success("前端目录选择成功");
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
|
||||
async function pickBackendDir() {
|
||||
try {
|
||||
backendDirHandle.value = await (window as any).showDirectoryPicker();
|
||||
backendDirPath.value = backendDirHandle.value?.name || "";
|
||||
ElMessage.success("后端目录选择成功");
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmWrite() {
|
||||
await writeGeneratedCode();
|
||||
writeDialog.visible = false;
|
||||
}
|
||||
|
||||
// ---- 内部工具函数 ----
|
||||
|
||||
function resolveRootForItem(item: GeneratorPreviewItem): "frontend" | "backend" {
|
||||
return item.scope === "backend" ? "backend" : "frontend";
|
||||
}
|
||||
|
||||
function stripProjectRoot(p: string): string {
|
||||
const normalized = p.replace(/\\/g, "/");
|
||||
const frontApp = genConfigFormData.value.frontendAppName;
|
||||
const backApp = genConfigFormData.value.backendAppName;
|
||||
if (frontApp && normalized.startsWith(`${frontApp}/`))
|
||||
return normalized.slice(frontApp.length + 1);
|
||||
if (backApp && normalized.startsWith(`${backApp}/`))
|
||||
return normalized.slice(backApp.length + 1);
|
||||
const idx = normalized.indexOf("/src/");
|
||||
if (idx > -1) return normalized.slice(idx + 1);
|
||||
if (normalized.startsWith("src/")) return normalized;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// 逐级创建/获取目录句柄
|
||||
async function ensureDir(root: FileSystemDirectoryHandle, path: string[], create = true) {
|
||||
let current = root;
|
||||
for (const segment of path) {
|
||||
current = await current.getDirectoryHandle(segment, { create });
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
async function writeFileToDir(
|
||||
dirHandle: FileSystemDirectoryHandle,
|
||||
filePath: string,
|
||||
content: string
|
||||
) {
|
||||
const normalized = filePath.replace(/\\/g, "/");
|
||||
const parts = normalized.split("/").filter(Boolean);
|
||||
const fileName = parts.pop()!;
|
||||
const targetDir = await ensureDir(dirHandle, parts, true);
|
||||
const fileHandle = await targetDir.getFileHandle(fileName, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(content ?? "");
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
async function pathExists(
|
||||
dirHandle: FileSystemDirectoryHandle,
|
||||
filePath: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const normalized = filePath.replace(/\\/g, "/");
|
||||
const parts = normalized.split("/").filter(Boolean);
|
||||
const fileName = parts.pop()!;
|
||||
const targetDir = await ensureDir(dirHandle, parts, false);
|
||||
await targetDir.getFileHandle(fileName, { create: false });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function isSameFile(
|
||||
dirHandle: FileSystemDirectoryHandle,
|
||||
filePath: string,
|
||||
content: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const normalized = filePath.replace(/\\/g, "/");
|
||||
const parts = normalized.split("/").filter(Boolean);
|
||||
const fileName = parts.pop()!;
|
||||
const targetDir = await ensureDir(dirHandle, parts, false);
|
||||
const fileHandle = await targetDir.getFileHandle(fileName, { create: false });
|
||||
const file = await fileHandle.getFile();
|
||||
const text = await file.text();
|
||||
return text === (content ?? "");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeGeneratedCode() {
|
||||
if (!supportsFSAccess) {
|
||||
ElMessage.warning("当前浏览器不支持本地写入,请选择下载ZIP");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(needFrontend.value && !frontendDirHandle.value) ||
|
||||
(needBackend.value && !backendDirHandle.value)
|
||||
) {
|
||||
ElMessage.warning("请先选择所需的前/后端目录");
|
||||
return;
|
||||
}
|
||||
if (!lastPreviewFiles.value.length) {
|
||||
ElMessage.warning("请先生成预览");
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingSvc = ElLoading.service({ lock: true, text: "正在写入代码..." });
|
||||
writeRunning.value = true;
|
||||
let frontCount = 0;
|
||||
let backCount = 0;
|
||||
const failed: string[] = [];
|
||||
|
||||
const files = lastPreviewFiles.value.filter(
|
||||
(f) => writeScope.value === "all" || resolveRootForItem(f) === writeScope.value
|
||||
);
|
||||
writeProgress.total = files.length;
|
||||
writeProgress.done = 0;
|
||||
writeProgress.percent = 0;
|
||||
writeProgress.current = "";
|
||||
|
||||
const concurrency = 4;
|
||||
const queue = files.slice();
|
||||
|
||||
async function worker() {
|
||||
while (queue.length) {
|
||||
const item = queue.shift()!;
|
||||
try {
|
||||
await (async () => {
|
||||
const root = resolveRootForItem(item);
|
||||
const relativePath = stripProjectRoot(`${item.path}/${item.fileName}`);
|
||||
writeProgress.current = relativePath;
|
||||
|
||||
const targetRoot =
|
||||
root === "frontend" ? frontendDirHandle.value! : backendDirHandle.value!;
|
||||
|
||||
if (overwriteMode.value === "ifChanged") {
|
||||
const same = await isSameFile(targetRoot, relativePath, item.content || "");
|
||||
if (same) return;
|
||||
}
|
||||
if (overwriteMode.value === "skip") {
|
||||
const exists = await pathExists(targetRoot, relativePath);
|
||||
if (exists) return;
|
||||
}
|
||||
|
||||
await writeFileToDir(targetRoot, relativePath, item.content || "");
|
||||
if (root === "frontend") frontCount++;
|
||||
else backCount++;
|
||||
})().catch((err) => {
|
||||
console.error("写入失败:", item.path, err);
|
||||
failed.push(item.path);
|
||||
});
|
||||
} finally {
|
||||
writeProgress.done++;
|
||||
writeProgress.percent = Math.round((writeProgress.done / writeProgress.total) * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
||||
loadingSvc.close();
|
||||
writeRunning.value = false;
|
||||
|
||||
if (failed.length) {
|
||||
ElMessage.warning(
|
||||
`部分文件写入失败 ${failed.length} 个,成功 前端 ${frontCount} 个,后端 ${backCount} 个`
|
||||
);
|
||||
} else {
|
||||
ElMessage.success(`写入完成:前端 ${frontCount} 个文件,后端 ${backCount} 个文件`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
supportsFSAccess,
|
||||
writeDialog,
|
||||
frontendDirPath,
|
||||
backendDirPath,
|
||||
writeScope,
|
||||
overwriteMode,
|
||||
writeProgress,
|
||||
writeRunning,
|
||||
canWriteToLocal,
|
||||
openWriteDialog,
|
||||
setPreviewFiles,
|
||||
pickFrontendDir,
|
||||
pickBackendDir,
|
||||
confirmWrite,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user