Files
vue3-element-admin/src/views/codegen/components/PreviewStep.vue
Ray.Hao 7cfcba3635 feat(codegen): 默认生成 CRUD 页面并限制不适用的查询方式
生成配置默认页面类型改为 crud,普通页面标注为兼容选项
IN 查询不适用于 byte[],BETWEEN 不适用于 Boolean 与 byte[]
2026-11-04 11:22:08 +08:00

541 lines
14 KiB
Vue

<template>
<div class="preview-step">
<!-- 工具栏 -->
<div class="preview-toolbar">
<div class="toolbar-left">
<div class="filter-group">
<span class="filter-label">
<el-icon><View /></el-icon>
预览范围
</span>
<el-radio-group
:model-value="previewScope"
size="small"
@update:model-value="onScopeChange"
>
<el-radio-button value="all">
<el-icon><Grid /></el-icon>
全部
</el-radio-button>
<el-radio-button value="frontend">
<el-icon><Monitor /></el-icon>
前端
</el-radio-button>
<el-radio-button value="backend">
<el-icon><Cpu /></el-icon>
后端
</el-radio-button>
</el-radio-group>
</div>
<el-divider direction="vertical" />
<div class="filter-group">
<span class="filter-label">
<el-icon><Files /></el-icon>
文件类型
</span>
<el-checkbox-group
:model-value="previewTypes"
size="small"
@update:model-value="onTypesChange"
>
<el-checkbox-button v-for="t in previewTypeOptions" :key="t" :value="t">
{{ t }}
</el-checkbox-button>
</el-checkbox-group>
</div>
</div>
<div class="toolbar-right">
<el-button size="small" type="primary" plain @click="emit('copy')">
<template #icon><CopyDocument /></template>
复制代码
</el-button>
<el-button size="small" type="success" plain @click="handleDownload">
<template #icon><Download /></template>
下载 ZIP
</el-button>
</div>
</div>
<!-- 主区域 -->
<div class="preview-container">
<!-- 文件树 -->
<div class="file-tree-panel" :style="{ width: fileTreeWidth + 'px' }">
<div class="panel-header">
<div class="header-left">
<el-icon class="header-icon"><FolderOpened /></el-icon>
<span class="header-title">文件列表</span>
<el-tag size="small" type="info" effect="plain">{{ fileCount }} 个文件</el-tag>
</div>
</div>
<el-scrollbar class="panel-body">
<el-tree
ref="fileTreeRef"
:data="filteredTreeData"
node-key="key"
default-expand-all
highlight-current
:current-node-key="currentFileKey"
@node-click="(data: any) => emit('file-click', data)"
>
<template #default="{ data }">
<div class="tree-node" :class="{ 'is-active': data.key === currentFileKey }">
<el-icon class="file-icon" :class="`icon-${getFileIcon(data)}`">
<Document />
</el-icon>
<span class="node-label">{{ data.label }}</span>
<el-tag
v-if="data.scope"
size="small"
:type="data.scope === 'frontend' ? 'success' : 'warning'"
effect="plain"
class="scope-tag"
>
{{ data.scope === "frontend" ? "前端" : "后端" }}
</el-tag>
</div>
</template>
</el-tree>
</el-scrollbar>
<div class="resize-handle" @mousedown="startResize">
<div class="resize-line" />
</div>
</div>
<!-- 代码预览 -->
<div class="code-preview-panel">
<div class="panel-header">
<div class="header-left">
<el-icon class="header-icon"><Document /></el-icon>
<span class="file-path">{{ currentFilePath || "请选择文件预览" }}</span>
</div>
<div class="header-right">
<el-tag
v-if="currentLanguage"
size="small"
type="primary"
effect="dark"
class="lang-tag"
>
{{ currentLanguage }}
</el-tag>
</div>
</div>
<el-scrollbar class="panel-body">
<div v-if="!code" class="empty-code">
<el-icon class="empty-icon"><DocumentCopy /></el-icon>
<div class="empty-text">点击左侧文件预览代码</div>
</div>
<Codemirror
v-else
ref="cmRef"
:value="code"
:options="cmOptions"
border
:readonly="true"
height="100%"
width="100%"
/>
</el-scrollbar>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import "codemirror/mode/javascript/javascript.js";
import Codemirror from "codemirror-editor-vue3";
import type { CmComponentRef } from "codemirror-editor-vue3";
import type { EditorConfiguration } from "codemirror";
import type { TreeInstance } from "element-plus";
import type { TreeNode } from "../utils/tree-builder";
import GeneratorAPI from "@/api/codegen";
import type { GenConfigForm } from "@/api/codegen";
import { getFileIcon } from "../utils/tree-builder";
/**
* 代码预览步骤
*/
const props = defineProps<{
genConfigFormData: GenConfigForm;
previewScope: "all" | "frontend" | "backend";
previewTypes: string[];
previewTypeOptions: string[];
filteredTreeData: TreeNode[];
code: string;
currentFileKey: string;
tableName: string;
}>();
/**
* 上抛预览范围变化与文件点击
*/
const emit = defineEmits<{
(_e: "update:previewScope", _val: "all" | "frontend" | "backend"): void;
(_e: "update:previewTypes", _val: string[]): void;
(_e: "file-click", _data: TreeNode): void;
(_e: "copy"): void;
}>();
const cmRef = ref<CmComponentRef>();
const cmOptions: EditorConfiguration = { mode: "text/javascript" };
const fileTreeRef = ref<TreeInstance>();
const fileTreeWidth = ref(280);
const currentFilePath = computed(() => {
const key = props.currentFileKey;
if (!key) return "";
const idx = key.indexOf(":");
return idx > -1 ? key.slice(idx + 1) : key;
});
/**
* 切换预览范围
*/
function onScopeChange(val: unknown) {
if (val === "all" || val === "frontend" || val === "backend") emit("update:previewScope", val);
}
/**
* 切换预览文件类型
*/
function onTypesChange(val: unknown) {
if (Array.isArray(val) && val.every((item) => typeof item === "string"))
emit("update:previewTypes", val);
}
const currentLanguage = computed(() => {
if (!props.currentFileKey) return "";
const parts = currentFilePath.value.split(".");
return parts.length > 1 ? parts.pop() : "";
});
const fileCount = computed(() => {
let count = 0;
/**
* 递归统计文件数量
*/
function walk(nodes: TreeNode[]) {
nodes.forEach((n) => {
if (!n.children || !n.children.length) count++;
else walk(n.children);
});
}
walk(props.filteredTreeData);
return count;
});
/**
* 下载生成的代码
*/
function handleDownload() {
const pageType = props.genConfigFormData.pageType || "crud";
return GeneratorAPI.download(props.tableName, pageType as "classic" | "crud", "ts");
}
/**
* 刷新代码编辑器布局
*/
function refreshEditor() {
cmRef.value?.refresh();
}
// 拖拽结束、再次开始或组件卸载时,共用同一份监听清理函数。
let stopResize: (() => void) | undefined;
/**
* 开始拖动文件树宽度
*/
function startResize(e: MouseEvent) {
stopResize?.();
const startX = e.clientX;
const startWidth = fileTreeWidth.value;
/**
* 按鼠标位移调整预览区域宽度。
*/
const onMove = (ev: MouseEvent) => {
fileTreeWidth.value = Math.max(200, Math.min(500, startWidth + ev.clientX - startX));
};
/**
* 结束拖动并移除鼠标监听。
*/
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
stopResize = undefined;
};
stopResize = onUp;
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
}
/**
* 父组件切换到预览步骤后刷新编辑器布局。
*/
defineExpose({ refreshEditor });
onBeforeUnmount(() => {
stopResize?.();
cmRef.value?.destroy();
});
</script>
<style scoped lang="scss">
.preview-step {
display: flex;
flex-direction: column;
height: 100%;
.preview-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
margin-bottom: 12px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
.toolbar-left {
display: flex;
gap: 12px;
align-items: center;
.filter-group {
display: flex;
gap: 8px;
align-items: center;
.filter-label {
display: inline-flex;
gap: 4px;
align-items: center;
font-size: 13px;
font-weight: 500;
color: var(--el-text-color-secondary);
}
}
}
.toolbar-right {
display: flex;
gap: 8px;
align-items: center;
}
}
.preview-container {
display: flex;
flex: 1;
gap: 12px;
min-height: 0;
.file-tree-panel,
.code-preview-panel {
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-lighter);
.header-left,
.header-right {
display: flex;
gap: 8px;
align-items: center;
}
.header-icon {
font-size: 16px;
color: var(--el-color-primary);
}
.header-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.file-path {
font-family: "JetBrains Mono", "Fira Code", Consolas, Monaco, monospace;
font-size: 13px;
color: var(--el-text-color-regular);
}
.lang-tag {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
text-transform: uppercase;
}
}
.panel-body {
flex: 1;
min-height: 0;
}
}
.file-tree-panel {
position: relative;
min-width: 200px;
max-width: 500px;
:deep(.el-tree) {
padding: 4px;
.el-tree-node__content {
height: 32px;
padding: 0 8px;
margin: 1px 0;
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
background: var(--el-fill-color-light);
}
}
.el-tree-node.is-current > .el-tree-node__content {
font-weight: 500;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
// 文件夹图标颜色
.el-tree-node__expand-icon {
color: var(--el-color-warning);
}
}
.tree-node {
display: flex;
flex: 1;
gap: 6px;
align-items: center;
.file-icon {
font-size: 15px;
transition: transform 0.2s ease;
&.icon-java {
color: #b07219;
}
&.icon-vue {
color: #42b883;
}
&.icon-typescript {
color: #3178c6;
}
&.icon-xml {
color: #f60;
}
&.icon-html {
color: #e34c26;
}
&.icon-javascript {
color: #f7df1e;
}
&.icon-scss {
color: #c6538c;
}
&.icon-sql {
color: #336791;
}
&.icon-folder {
color: var(--el-color-warning);
}
}
.node-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
white-space: nowrap;
}
.scope-tag {
height: 20px;
padding: 0 6px;
margin-left: auto;
font-size: 11px;
}
&:hover .file-icon {
transform: scale(1.15);
}
// 当前选中的行标签样式调整
&.is-active .scope-tag {
color: #fff;
background: var(--el-color-primary);
border-color: var(--el-color-primary);
}
}
.resize-handle {
position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 12px;
cursor: col-resize;
.resize-line {
width: 2px;
height: 40px;
background: var(--el-border-color);
border-radius: 1px;
transition: background 0.2s ease;
}
&:hover .resize-line {
background: var(--el-color-primary);
}
}
}
.code-preview-panel {
flex: 1;
min-width: 0;
.empty-code {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
justify-content: center;
height: 100%;
color: var(--el-text-color-secondary);
.empty-icon {
font-size: 48px;
opacity: 0.3;
}
.empty-text {
font-size: 14px;
}
}
}
}
}
</style>