refactor(module): 重构模块结构,将示例模块和AI模块迁移至应用模块

- 删除原有的示例模块(module_example)和AI模块(module_ai)
- 在应用模块(module_application)下新增AI子模块和示例子模块
- 更新相关路由配置,移除不再使用的模块路由
- 调整定时任务模块从监控模块迁移至应用模块
- 更新前端API路径和组件文件结构
- 修正数据库表名前缀,统一使用'app_'作为应用模块表前缀
This commit is contained in:
zhangtao
2025-10-05 12:24:40 +08:00
parent 055e054173
commit d943d295d0
62 changed files with 1566 additions and 1571 deletions
@@ -1,173 +0,0 @@
<!--
低代码页面生成器 - 主界面
整合 Palette、Canvas、Inspector 三个核心组件
-->
<template>
<div class="container">
<!-- 左侧组件库 -->
<Palette @add-component="handleAddComponent" />
<!-- 中间画布 -->
<Canvas
:components="components"
:selected-component-id="selectedComponentId || undefined"
@update-components="handleUpdateComponents"
@select-component="handleSelectComponent"
@update-component="handleUpdateComponent"
@delete-component="handleDeleteComponent"
@move-component="handleMoveComponent"
/>
<!-- 右侧属性面板 -->
<Inspector
:selected-component="selectedComponent"
@update-component="handleUpdateComponent"
@delete-component="handleDeleteComponent"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import Palette from './components/Palette.vue';
import Canvas from './components/Canvas.vue';
import Inspector from './components/Inspector.vue';
import { ComponentSchema, PageSchema, generateId } from './utils/schema';
import { generateVueFile, exportAsFile, copyToClipboard } from './utils/serializer';
// 响应式数据
const components = ref<ComponentSchema[]>([]);
const selectedComponentId = ref<string | null>(null);
// 计算属性
const selectedComponent = computed(() => {
if (!selectedComponentId.value) return null;
return findComponentById(components.value, selectedComponentId.value);
});
// 查找组件
function findComponentById(components: ComponentSchema[], id: string): ComponentSchema | null {
for (const component of components) {
if (component.id === id) {
return component;
}
if (component.children) {
const found = findComponentById(component.children, id);
if (found) return found;
}
}
return null;
}
// 处理添加组件
function handleAddComponent(component: ComponentSchema) {
components.value.push(component);
selectedComponentId.value = component.id;
ElMessage.success(`已添加 ${component.type} 组件`);
}
// 处理更新组件列表
function handleUpdateComponents(newComponents: ComponentSchema[]) {
components.value = newComponents;
}
// 处理选择组件
function handleSelectComponent(component: ComponentSchema | null) {
selectedComponentId.value = component?.id || null;
}
// 处理更新组件
function handleUpdateComponent(component: ComponentSchema) {
updateComponentById(components.value, component);
}
// 更新组件
function updateComponentById(components: ComponentSchema[], updatedComponent: ComponentSchema) {
for (let i = 0; i < components.length; i++) {
if (components[i].id === updatedComponent.id) {
components[i] = updatedComponent;
return;
}
if (components[i].children) {
updateComponentById(components[i].children!, updatedComponent);
}
}
}
// 处理删除组件
function handleDeleteComponent(componentId: string) {
deleteComponentById(components.value, componentId);
if (selectedComponentId.value === componentId) {
selectedComponentId.value = null;
}
}
// 删除组件
function deleteComponentById(components: ComponentSchema[], id: string): boolean {
for (let i = 0; i < components.length; i++) {
if (components[i].id === id) {
components.splice(i, 1);
return true;
}
if (components[i].children) {
if (deleteComponentById(components[i].children!, id)) {
return true;
}
}
}
return false;
}
// 处理移动组件
function handleMoveComponent(fromIndex: number, toIndex: number) {
const component = components.value.splice(fromIndex, 1)[0];
components.value.splice(toIndex, 0, component);
}
// 组件挂载时加载本地数据
onMounted(() => {
try {
const savedComponents = localStorage.getItem('lowcode-components');
if (savedComponents) {
components.value = JSON.parse(savedComponents);
}
} catch (error) {
console.warn('加载本地数据失败:', error);
}
});
// 监听组件变化,自动保存到本地存储
watch(() => components.value, (newComponents) => {
try {
localStorage.setItem('lowcode-components', JSON.stringify(newComponents));
} catch (error) {
console.warn('保存到本地存储失败:', error);
}
}, { deep: true });
</script>
<style scoped>
.container {
position: relative;
width: 100%;
background-color: var(--el-bg-color-overlay);
height: calc(100vh - 50px - 40px);
overflow: hidden;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.container {
height: calc(100vh - 40px);
}
}
@media (max-width: 768px) {
.container {
height: calc(100vh - 30px);
}
}
</style>
+139 -631
View File
@@ -1,665 +1,173 @@
<!--
低代码页面生成器 - 主界面
整合 Palette、Canvas、Inspector 三个核心组件
-->
<template>
<div class="container">
<div class="left-board">
<div class="logo-wrapper">
<div class="logo">
<img :src="logo" alt="logo"> Form Generator
</div>
</div>
<el-scrollbar class="left-scrollbar">
<div class="components-list">
<div class="components-title">
<svg-icon icon-class="component" />输入型组件
</div>
<draggable
class="components-draggable" :list="inputComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" item-key="label" @end="onEnd">
<template #item="{ element, index }">
<div :key="index" class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</template>
</draggable>
<div class="components-title">
<svg-icon icon-class="component" />选择型组件
</div>
<draggable
class="components-draggable" :list="selectComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" item-key="label" @end="onEnd">
<template #item="{ element, index }">
<div :key="index" class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</template>
</draggable>
<div class="components-title">
<svg-icon icon-class="component" /> 布局型组件
</div>
<draggable
class="components-draggable" :list="layoutComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" item-key="label" @end="onEnd">
<template #item="{ element, index }">
<div :key="index" class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</template>
</draggable>
</div>
</el-scrollbar>
</div>
<div class="center-board">
<div class="action-bar">
<el-button icon="Download" type="primary" text @click="download" v-hasPermi="['generator:webcode:download']">
导出vue文件
</el-button>
<el-button class="copy-btn-main" icon="DocumentCopy" type="primary" text @click="copy" v-hasPermi="['generator:webcode:copy']">
复制代码
</el-button>
<el-button class="delete-btn" icon="Delete" text type="danger" @click="empty" v-hasPermi="['generator:webcode:empty']">
清空
</el-button>
</div>
<el-scrollbar class="center-scrollbar">
<el-row class="center-board-row" :gutter="formConf.gutter">
<el-form :size="formConf.size" :label-position="formConf.labelPosition" :disabled="formConf.disabled"
:label-width="formConf.labelWidth + 'px'">
<draggable
class="drawing-board" :list="drawingList" :animation="340" group="componentsGroup"
item-key="label">
<template #item="{ element, index }">
<DraggableItem
:key="element.renderKey" :drawing-list="drawingList" :element="element" :index="index"
:active-id="activeId" :form-conf="formConf" @active-item="activeFormItem" @copy-item="drawingItemCopy"
@delete-item="drawingItemDelete" />
</template>
</draggable>
<div v-show="!drawingList.length" class="empty-info">
从左侧拖入或点选组件进行表单设计
</div>
</el-form>
</el-row>
</el-scrollbar>
</div>
<RightPanel
:active-data="activeData" :form-conf="formConf" :show-field="!!drawingList.length"
@tag-change="tagChange" />
<CodeTypeDialog v-model="dialogVisible" title="选择生成类型" :show-file-name="showFileName" @confirm="generate" />
<input id="copyNode" type="hidden">
<!-- 左侧组件库 -->
<Palette @add-component="handleAddComponent" />
<!-- 中间画布 -->
<Canvas
:components="components"
:selected-component-id="selectedComponentId || undefined"
@update-components="handleUpdateComponents"
@select-component="handleSelectComponent"
@update-component="handleUpdateComponent"
@delete-component="handleDeleteComponent"
@move-component="handleMoveComponent"
/>
<!-- 右侧属性面板 -->
<Inspector
:selected-component="selectedComponent"
@update-component="handleUpdateComponent"
@delete-component="handleDeleteComponent"
/>
</div>
</template>
<script setup>
import draggable from "vuedraggable"
import ClipboardJS from 'clipboard'
import beautifier from 'js-beautify'
import logo from '@/assets/logo/logo.png'
import { inputComponents, selectComponents, layoutComponents, formConf as formConfData } from '@/utils/generator/config'
import { beautifierConf } from '@/utils/index'
import drawingDefalut from '@/utils/generator/drawingDefalut'
import { makeUpHtml, vueTemplate, vueScript, cssStyle } from '@/utils/generator/html'
import { makeUpJs } from '@/utils/generator/js'
import { makeUpCss } from '@/utils/generator/css'
import Download from '@/plugins/download.ts'
import { ElNotification, ElMessageBox, ElMessage } from 'element-plus'
import DraggableItem from './components/DraggableItem.vue'
import RightPanel from './components/RightPanel.vue'
import CodeTypeDialog from './components/CodeTypeDialog.vue'
import { onMounted, watch, ref, nextTick } from 'vue'
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import Palette from './components/Palette.vue';
import Canvas from './components/Canvas.vue';
import Inspector from './components/Inspector.vue';
import { ComponentSchema, PageSchema, generateId } from './utils/schema';
import { generateVueFile, exportAsFile, copyToClipboard } from './utils/serializer';
const drawingList = ref(drawingDefalut)
const dialogVisible = ref(false)
const showFileName = ref(false)
const operationType = ref('')
const idGlobal = ref(100)
const activeData = ref(drawingDefalut[0])
const activeId = ref(drawingDefalut[0].formId)
const generateConf = ref(null)
const formData = ref({})
const formConf = ref(formConfData)
let oldActiveId
let tempActiveData
// 响应式数据
const components = ref<ComponentSchema[]>([]);
const selectedComponentId = ref<string | null>(null);
function activeFormItem(element) {
activeData.value = element
activeId.value = element.formId
}
function copy() {
dialogVisible.value = true
showFileName.value = false
operationType.value = 'copy'
}
function download() {
dialogVisible.value = true
showFileName.value = true
operationType.value = 'download'
}
function empty() {
ElMessageBox.confirm('确定要清空所有组件吗?', '提示', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
idGlobal.value = 100
drawingList.value = []
// 计算属性
const selectedComponent = computed(() => {
if (!selectedComponentId.value) return null;
return findComponentById(components.value, selectedComponentId.value);
});
// 查找组件
function findComponentById(components: ComponentSchema[], id: string): ComponentSchema | null {
for (const component of components) {
if (component.id === id) {
return component;
}
).catch(() => {
// 用户取消操作
})
}
function onEnd(obj, a) {
if (obj.from !== obj.to) {
activeData.value = tempActiveData
activeId.value = idGlobal.value
}
}
function addComponent(item) {
const clone = cloneComponent(item)
drawingList.value.push(clone)
activeFormItem(clone)
}
function cloneComponent(origin) {
const clone = JSON.parse(JSON.stringify(origin))
clone.formId = ++idGlobal.value
clone.span = formConf.value.span
clone.renderKey = +new Date() // 改变renderKey后可以实现强制更新组件
if (!clone.layout) clone.layout = 'colFormItem'
if (clone.layout === 'colFormItem') {
clone.vModel = `field${idGlobal.value}`
clone.placeholder !== undefined && (clone.placeholder += clone.label)
tempActiveData = clone
} else if (clone.layout === 'rowFormItem') {
delete clone.label
clone.componentName = `row${idGlobal.value}`
clone.gutter = formConf.value.gutter
tempActiveData = clone
}
return tempActiveData
}
function drawingItemCopy(item, parent) {
let clone = JSON.parse(JSON.stringify(item))
clone = createIdAndKey(clone)
parent.push(clone)
activeFormItem(clone)
}
function createIdAndKey(item) {
item.formId = ++idGlobal.value
item.renderKey = +new Date()
if (item.layout === 'colFormItem') {
item.vModel = `field${idGlobal.value}`
} else if (item.layout === 'rowFormItem') {
item.componentName = `row${idGlobal.value}`
}
if (Array.isArray(item.children)) {
item.children = item.children.map(childItem => createIdAndKey(childItem))
}
return item
}
function drawingItemDelete(index, parent) {
parent.splice(index, 1)
nextTick(() => {
const len = drawingList.value.length
if (len) {
activeFormItem(drawingList.value[len - 1])
if (component.children) {
const found = findComponentById(component.children, id);
if (found) return found;
}
})
}
return null;
}
function tagChange(newTag) {
newTag = cloneComponent(newTag)
newTag.vModel = activeData.value.vModel
newTag.formId = activeId.value
newTag.span = activeData.value.span
delete activeData.value.tag
delete activeData.value.tagIcon
delete activeData.value.document
Object.keys(newTag).forEach(key => {
if (activeData.value[key] !== undefined
&& typeof activeData.value[key] === typeof newTag[key]) {
newTag[key] = activeData.value[key]
// 处理添加组件
function handleAddComponent(component: ComponentSchema) {
components.value.push(component);
selectedComponentId.value = component.id;
ElMessage.success(`已添加 ${component.type} 组件`);
}
// 处理更新组件列表
function handleUpdateComponents(newComponents: ComponentSchema[]) {
components.value = newComponents;
}
// 处理选择组件
function handleSelectComponent(component: ComponentSchema | null) {
selectedComponentId.value = component?.id || null;
}
// 处理更新组件
function handleUpdateComponent(component: ComponentSchema) {
updateComponentById(components.value, component);
}
// 更新组件
function updateComponentById(components: ComponentSchema[], updatedComponent: ComponentSchema) {
for (let i = 0; i < components.length; i++) {
if (components[i].id === updatedComponent.id) {
components[i] = updatedComponent;
return;
}
if (components[i].children) {
updateComponentById(components[i].children!, updatedComponent);
}
})
activeData.value = newTag
updateDrawingList(newTag, drawingList.value)
}
function updateDrawingList(newTag, list) {
const index = list.findIndex(item => item.formId === activeId.value)
if (index > -1) {
list.splice(index, 1, newTag)
} else {
list.forEach(item => {
if (Array.isArray(item.children)) updateDrawingList(newTag, item.children)
})
}
}
function generate(data) {
generateConf.value = data
nextTick(() => {
switch (operationType.value) {
case 'copy':
execCopy(data)
break
case 'download':
execDownload(data)
break
default:
break
}
})
}
function execDownload(data) {
const codeStr = generateCode()
const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
Download.saveAs(blob, data.fileName)
}
function execCopy(data) {
document.getElementById('copyNode').click()
}
function AssembleFormData() {
formData.value = { fields: JSON.parse(JSON.stringify(drawingList.value)), ...formConf.value }
}
function generateCode() {
const { type } = generateConf.value
AssembleFormData()
const script = vueScript(makeUpJs(formData.value, type))
const html = vueTemplate(makeUpHtml(formData.value, type))
const css = cssStyle(makeUpCss(formData.value))
return beautifier.html(html + script + css, beautifierConf.html)
}
watch(() => activeData.value.label, (val, oldVal) => {
if (
activeData.value.placeholder === undefined
|| !activeData.value.tag
|| oldActiveId !== activeId.value
) {
return
// 处理删除组件
function handleDeleteComponent(componentId: string) {
deleteComponentById(components.value, componentId);
if (selectedComponentId.value === componentId) {
selectedComponentId.value = null;
}
activeData.value.placeholder = activeData.value.placeholder.replace(oldVal, '') + val
})
watch(activeId, (val) => {
oldActiveId = val
}, { immediate: true })
}
// 删除组件
function deleteComponentById(components: ComponentSchema[], id: string): boolean {
for (let i = 0; i < components.length; i++) {
if (components[i].id === id) {
components.splice(i, 1);
return true;
}
if (components[i].children) {
if (deleteComponentById(components[i].children!, id)) {
return true;
}
}
}
return false;
}
// 处理移动组件
function handleMoveComponent(fromIndex: number, toIndex: number) {
const component = components.value.splice(fromIndex, 1)[0];
components.value.splice(toIndex, 0, component);
}
// 组件挂载时加载本地数据
onMounted(() => {
const clipboard = new ClipboardJS('#copyNode', {
text: trigger => {
const codeStr = generateCode()
ElNotification({ title: '成功', message: '代码已复制到剪切板,可粘贴。', type: 'success' })
return codeStr
try {
const savedComponents = localStorage.getItem('lowcode-components');
if (savedComponents) {
components.value = JSON.parse(savedComponents);
}
})
clipboard.on('error', e => {
ElMessage.error('代码复制失败')
})
})
} catch (error) {
console.warn('加载本地数据失败:', error);
}
});
// 监听组件变化,自动保存到本地存储
watch(() => components.value, (newComponents) => {
try {
localStorage.setItem('lowcode-components', JSON.stringify(newComponents));
} catch (error) {
console.warn('保存到本地存储失败:', error);
}
}, { deep: true });
</script>
<style lang='scss'>
// 使用Element Plus系统颜色变量
<style scoped>
.container {
position: relative;
width: 100%;
background-color: var(--el-bg-color-overlay);
height: calc(100vh - 50px - 40px);
overflow: hidden;
}
.left-board {
width: 260px;
position: absolute;
left: 0;
top: 0;
height: calc(100vh - 50px - 40px);
.logo-wrapper {
position: relative;
height: 42px;
border-bottom: 1px solid var(--el-border-color-extra-light);
box-sizing: border-box;
.logo {
position: absolute;
left: 12px;
top: 6px;
line-height: 30px;
color: var(--el-color-primary);
font-weight: 600;
font-size: 17px;
white-space: nowrap;
>img {
width: 30px;
height: 30px;
vertical-align: top;
}
.github {
display: inline-block;
vertical-align: sub;
margin-left: 15px;
>img {
height: 22px;
}
}
}
}
.left-scrollbar {
.el-scrollbar__wrap {
box-sizing: border-box;
overflow-x: hidden !important;
margin-bottom: 0 !important;
.components-list {
padding: 8px;
box-sizing: border-box;
height: 100%;
.components-title {
font-size: 14px;
// color: #222;
margin: 6px 2px;
.svg-icon {
// color: #666;
font-size: 18px;
margin-right: 5px;
}
}
.components-draggable {
padding-bottom: 20px;
.components-item {
display: inline-block;
width: 48%;
margin: 1%;
transition: transform 0ms !important;
.components-body {
padding: 8px 10px;
background: var(--el-border-color-extra-light);
font-size: 12px;
cursor: move;
border: 1px dashed var(--el-border-color-extra-light);
border-radius: 3px;
.svg-icon {
// color: #777;
font-size: 15px;
margin-right: 5px;
}
&:hover {
border: 1px dashed var(--el-color-primary);
color: var(--el-color-primary);
.svg-icon {
color: var(--el-color-primary);
}
}
}
}
}
}
}
}
/* 响应式设计 */
@media (max-width: 1200px) {
.container {
height: calc(100vh - 40px);
}
}
.center-board {
height: calc(100vh - 50px - 40px);
width: auto;
margin: 0 350px 0 260px;
box-sizing: border-box;
.action-bar {
position: relative;
height: 42px;
padding: 0 15px;
box-sizing: border-box;
;
border: 1px solid var(--el-border-color-extra-light);
border-top: none;
border-left: none;
display: flex;
align-items: center;
justify-content: flex-end;
.delete-btn {
color: var(--el-color-danger);
}
}
.center-scrollbar {
height: calc(100vh - 50px - 40px - 42px);
overflow: hidden;
border-left: 1px solid var(--el-border-color-extra-light);
border-right: 1px solid var(--el-border-color-extra-light);
box-sizing: border-box;
.el-scrollbar__view {
overflow-x: hidden;
}
.center-board-row {
padding: 12px 12px 15px 12px;
box-sizing: border-box;
&>.el-form {
// 69 = 12+15+42
height: calc(100vh - 50px - 40px - 69px);
flex: 1;
.drawing-board {
height: 100%;
position: relative;
.components-body {
padding: 0;
margin: 0;
font-size: 0;
}
.sortable-ghost {
position: relative;
display: block;
overflow: hidden;
&::before {
content: " ";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: var(--el-color-primary);
z-index: 2;
}
}
.components-item.sortable-ghost {
width: 100%;
height: 60px;
background: var(--el-border-color-extra-light);
}
.active-from-item {
&>.el-form-item {
background: var(--el-border-color-extra-light);
border-radius: 6px;
}
&>.drawing-item-copy,
&>.drawing-item-delete {
display: initial;
}
&>.component-name {
color: var(--el-color-primary);
}
.el-input__wrapper {
box-shadow: 0 0 0 1px var(--el-input-hover-border-color) inset;
}
}
.el-form-item {
margin-bottom: 15px;
}
}
.drawing-item {
position: relative;
cursor: move;
&.unfocus-bordered:not(.activeFromItem)>div:first-child {
border: 1px dashed var(--el-border-color);
}
.el-form-item {
padding: 12px 10px;
}
}
.drawing-row-item {
position: relative;
cursor: move;
box-sizing: border-box;
border: 1px dashed var(--el-border-color);
border-radius: 3px;
padding: 0 2px;
margin-bottom: 15px;
.drawing-row-item {
margin-bottom: 2px;
}
.el-col {
margin-top: 22px;
}
.el-form-item {
margin-bottom: 0;
}
.drag-wrapper {
min-height: 80px;
flex: 1;
display: flex;
flex-wrap: wrap;
}
&.active-from-item {
border: 1px dashed var(--el-color-primary);
}
.component-name {
position: absolute;
top: 0;
left: 0;
font-size: 12px;
color: var(--el-text-color-placeholder);
display: inline-block;
padding: 0 6px;
}
}
.drawing-item,
.drawing-row-item {
&:hover {
&>.el-form-item {
background: var(--el-border-color-extra-light);
border-radius: 6px;
}
&>.drawing-item-copy,
&>.drawing-item-delete {
display: initial;
}
}
&>.drawing-item-copy,
&>.drawing-item-delete {
display: none;
position: absolute;
top: -10px;
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
border-radius: 50%;
font-size: 12px;
border: 1px solid;
cursor: pointer;
z-index: 1;
}
&>.drawing-item-copy {
right: 56px;
border-color: var(--el-color-primary);
color: var(--el-color-primary);
background: var(--el-bg-color);
&:hover {
background: var(--el-color-primary);
color: var(--el-color-white);
}
}
&>.drawing-item-delete {
right: 24px;
border-color: var(--el-color-danger);
color: var(--el-color-danger);
background: var(--el-bg-color);
&:hover {
background: var(--el-color-danger);
color: var(--el-color-white);
}
}
}
.empty-info {
position: absolute;
top: 46%;
left: 0;
right: 0;
text-align: center;
font-size: 18px;
color: var(--el-text-color-placeholder);
letter-spacing: 4px;
}
}
}
}
@media (max-width: 768px) {
.container {
height: calc(100vh - 30px);
}
}
</style>
@@ -0,0 +1,669 @@
<template>
<div class="app-container">
<div class="container">
<div class="left-board">
<div class="logo-wrapper">
<div class="logo">
<img :src="logo" alt="logo"> Form Generator
</div>
</div>
<el-scrollbar class="left-scrollbar">
<div class="components-list">
<div class="components-title">
<svg-icon icon-class="component" />输入型组件
</div>
<draggable
class="components-draggable" :list="inputComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" item-key="label" @end="onEnd">
<template #item="{ element, index }">
<div :key="index" class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</template>
</draggable>
<div class="components-title">
<svg-icon icon-class="component" />选择型组件
</div>
<draggable
class="components-draggable" :list="selectComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" item-key="label" @end="onEnd">
<template #item="{ element, index }">
<div :key="index" class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</template>
</draggable>
<div class="components-title">
<svg-icon icon-class="component" /> 布局型组件
</div>
<draggable
class="components-draggable" :list="layoutComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" item-key="label" @end="onEnd">
<template #item="{ element, index }">
<div :key="index" class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</template>
</draggable>
</div>
</el-scrollbar>
</div>
<div class="center-board">
<div class="action-bar">
<el-button icon="Download" type="primary" text @click="download" v-hasPermi="['generator:webcode:download']">
导出vue文件
</el-button>
<el-button class="copy-btn-main" icon="DocumentCopy" type="primary" text @click="copy" v-hasPermi="['generator:webcode:copy']">
复制代码
</el-button>
<el-button class="delete-btn" icon="Delete" text type="danger" @click="empty" v-hasPermi="['generator:webcode:empty']">
清空
</el-button>
</div>
<el-scrollbar class="center-scrollbar">
<el-row class="center-board-row" :gutter="formConf.gutter">
<el-form :size="formConf.size" :label-position="formConf.labelPosition" :disabled="formConf.disabled"
:label-width="formConf.labelWidth + 'px'">
<draggable
class="drawing-board" :list="drawingList" :animation="340" group="componentsGroup"
item-key="label">
<template #item="{ element, index }">
<DraggableItem
:key="element.renderKey" :drawing-list="drawingList" :element="element" :index="index"
:active-id="activeId" :form-conf="formConf" @active-item="activeFormItem" @copy-item="drawingItemCopy"
@delete-item="drawingItemDelete" />
</template>
</draggable>
<div v-show="!drawingList.length" class="empty-info">
从左侧拖入或点选组件进行表单设计
</div>
</el-form>
</el-row>
</el-scrollbar>
</div>
</div>
<RightPanel
:active-data="activeData" :form-conf="formConf" :show-field="!!drawingList.length"
@tag-change="tagChange" />
<CodeTypeDialog v-model="dialogVisible" title="选择生成类型" :show-file-name="showFileName" @confirm="generate" />
<input id="copyNode" type="hidden">
</div>
</template>
<script setup>
import draggable from "vuedraggable"
import ClipboardJS from 'clipboard'
import beautifier from 'js-beautify'
import logo from '@/assets/logo/logo.png'
import { inputComponents, selectComponents, layoutComponents, formConf as formConfData } from '@/utils/generator/config'
import { beautifierConf } from '@/utils/index'
import drawingDefalut from '@/utils/generator/drawingDefalut'
import { makeUpHtml, vueTemplate, vueScript, cssStyle } from '@/utils/generator/html'
import { makeUpJs } from '@/utils/generator/js'
import { makeUpCss } from '@/utils/generator/css'
import Download from '@/plugins/download.ts'
import { ElNotification, ElMessageBox, ElMessage } from 'element-plus'
import DraggableItem from './components/DraggableItem.vue'
import RightPanel from './components/RightPanel.vue'
import CodeTypeDialog from './components/CodeTypeDialog.vue'
import { onMounted, watch, ref, nextTick } from 'vue'
const drawingList = ref(drawingDefalut)
const dialogVisible = ref(false)
const showFileName = ref(false)
const operationType = ref('')
const idGlobal = ref(100)
const activeData = ref(drawingDefalut[0])
const activeId = ref(drawingDefalut[0].formId)
const generateConf = ref(null)
const formData = ref({})
const formConf = ref(formConfData)
let oldActiveId
let tempActiveData
function activeFormItem(element) {
activeData.value = element
activeId.value = element.formId
}
function copy() {
dialogVisible.value = true
showFileName.value = false
operationType.value = 'copy'
}
function download() {
dialogVisible.value = true
showFileName.value = true
operationType.value = 'download'
}
function empty() {
ElMessageBox.confirm('确定要清空所有组件吗?', '提示', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
idGlobal.value = 100
drawingList.value = []
}
).catch(() => {
// 用户取消操作
})
}
function onEnd(obj, a) {
if (obj.from !== obj.to) {
activeData.value = tempActiveData
activeId.value = idGlobal.value
}
}
function addComponent(item) {
const clone = cloneComponent(item)
drawingList.value.push(clone)
activeFormItem(clone)
}
function cloneComponent(origin) {
const clone = JSON.parse(JSON.stringify(origin))
clone.formId = ++idGlobal.value
clone.span = formConf.value.span
clone.renderKey = +new Date() // 改变renderKey后可以实现强制更新组件
if (!clone.layout) clone.layout = 'colFormItem'
if (clone.layout === 'colFormItem') {
clone.vModel = `field${idGlobal.value}`
clone.placeholder !== undefined && (clone.placeholder += clone.label)
tempActiveData = clone
} else if (clone.layout === 'rowFormItem') {
delete clone.label
clone.componentName = `row${idGlobal.value}`
clone.gutter = formConf.value.gutter
tempActiveData = clone
}
return tempActiveData
}
function drawingItemCopy(item, parent) {
let clone = JSON.parse(JSON.stringify(item))
clone = createIdAndKey(clone)
parent.push(clone)
activeFormItem(clone)
}
function createIdAndKey(item) {
item.formId = ++idGlobal.value
item.renderKey = +new Date()
if (item.layout === 'colFormItem') {
item.vModel = `field${idGlobal.value}`
} else if (item.layout === 'rowFormItem') {
item.componentName = `row${idGlobal.value}`
}
if (Array.isArray(item.children)) {
item.children = item.children.map(childItem => createIdAndKey(childItem))
}
return item
}
function drawingItemDelete(index, parent) {
parent.splice(index, 1)
nextTick(() => {
const len = drawingList.value.length
if (len) {
activeFormItem(drawingList.value[len - 1])
}
})
}
function tagChange(newTag) {
newTag = cloneComponent(newTag)
newTag.vModel = activeData.value.vModel
newTag.formId = activeId.value
newTag.span = activeData.value.span
delete activeData.value.tag
delete activeData.value.tagIcon
delete activeData.value.document
Object.keys(newTag).forEach(key => {
if (activeData.value[key] !== undefined
&& typeof activeData.value[key] === typeof newTag[key]) {
newTag[key] = activeData.value[key]
}
})
activeData.value = newTag
updateDrawingList(newTag, drawingList.value)
}
function updateDrawingList(newTag, list) {
const index = list.findIndex(item => item.formId === activeId.value)
if (index > -1) {
list.splice(index, 1, newTag)
} else {
list.forEach(item => {
if (Array.isArray(item.children)) updateDrawingList(newTag, item.children)
})
}
}
function generate(data) {
generateConf.value = data
nextTick(() => {
switch (operationType.value) {
case 'copy':
execCopy(data)
break
case 'download':
execDownload(data)
break
default:
break
}
})
}
function execDownload(data) {
const codeStr = generateCode()
const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
Download.saveAs(blob, data.fileName)
}
function execCopy(data) {
document.getElementById('copyNode').click()
}
function AssembleFormData() {
formData.value = { fields: JSON.parse(JSON.stringify(drawingList.value)), ...formConf.value }
}
function generateCode() {
const { type } = generateConf.value
AssembleFormData()
const script = vueScript(makeUpJs(formData.value, type))
const html = vueTemplate(makeUpHtml(formData.value, type))
const css = cssStyle(makeUpCss(formData.value))
return beautifier.html(html + script + css, beautifierConf.html)
}
watch(() => activeData.value.label, (val, oldVal) => {
if (
activeData.value.placeholder === undefined
|| !activeData.value.tag
|| oldActiveId !== activeId.value
) {
return
}
activeData.value.placeholder = activeData.value.placeholder.replace(oldVal, '') + val
})
watch(activeId, (val) => {
oldActiveId = val
}, { immediate: true })
onMounted(() => {
const clipboard = new ClipboardJS('#copyNode', {
text: trigger => {
const codeStr = generateCode()
ElNotification({ title: '成功', message: '代码已复制到剪切板,可粘贴。', type: 'success' })
return codeStr
}
})
clipboard.on('error', e => {
ElMessage.error('代码复制失败')
})
})
</script>
<style lang='scss'>
// 使用Element Plus系统颜色变量
.container {
position: relative;
width: 100%;
background-color: var(--el-bg-color-overlay);
height: calc(100vh - 50px - 40px);
overflow: hidden;
.left-board {
width: 260px;
position: absolute;
left: 0;
top: 0;
height: calc(100vh - 50px - 40px);
.logo-wrapper {
position: relative;
height: 42px;
border-bottom: 1px solid var(--el-border-color-extra-light);
box-sizing: border-box;
.logo {
position: absolute;
left: 12px;
top: 6px;
line-height: 30px;
color: var(--el-color-primary);
font-weight: 600;
font-size: 17px;
white-space: nowrap;
>img {
width: 30px;
height: 30px;
vertical-align: top;
}
.github {
display: inline-block;
vertical-align: sub;
margin-left: 15px;
>img {
height: 22px;
}
}
}
}
.left-scrollbar {
.el-scrollbar__wrap {
box-sizing: border-box;
overflow-x: hidden !important;
margin-bottom: 0 !important;
.components-list {
padding: 8px;
box-sizing: border-box;
height: 100%;
.components-title {
font-size: 14px;
// color: #222;
margin: 6px 2px;
.svg-icon {
// color: #666;
font-size: 18px;
margin-right: 5px;
}
}
.components-draggable {
padding-bottom: 20px;
.components-item {
display: inline-block;
width: 48%;
margin: 1%;
transition: transform 0ms !important;
.components-body {
padding: 8px 10px;
background: var(--el-border-color-extra-light);
font-size: 12px;
cursor: move;
border: 1px dashed var(--el-border-color-extra-light);
border-radius: 3px;
.svg-icon {
// color: #777;
font-size: 15px;
margin-right: 5px;
}
&:hover {
border: 1px dashed var(--el-color-primary);
color: var(--el-color-primary);
.svg-icon {
color: var(--el-color-primary);
}
}
}
}
}
}
}
}
}
.center-board {
height: calc(100vh - 50px - 40px);
width: auto;
margin: 0 350px 0 260px;
box-sizing: border-box;
.action-bar {
position: relative;
height: 42px;
padding: 0 15px;
box-sizing: border-box;
;
border: 1px solid var(--el-border-color-extra-light);
border-top: none;
border-left: none;
display: flex;
align-items: center;
justify-content: flex-end;
.delete-btn {
color: var(--el-color-danger);
}
}
.center-scrollbar {
height: calc(100vh - 50px - 40px - 42px);
overflow: hidden;
border-left: 1px solid var(--el-border-color-extra-light);
border-right: 1px solid var(--el-border-color-extra-light);
box-sizing: border-box;
.el-scrollbar__view {
overflow-x: hidden;
}
.center-board-row {
padding: 12px 12px 15px 12px;
box-sizing: border-box;
&>.el-form {
// 69 = 12+15+42
height: calc(100vh - 50px - 40px - 69px);
flex: 1;
.drawing-board {
height: 100%;
position: relative;
.components-body {
padding: 0;
margin: 0;
font-size: 0;
}
.sortable-ghost {
position: relative;
display: block;
overflow: hidden;
&::before {
content: " ";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: var(--el-color-primary);
z-index: 2;
}
}
.components-item.sortable-ghost {
width: 100%;
height: 60px;
background: var(--el-border-color-extra-light);
}
.active-from-item {
&>.el-form-item {
background: var(--el-border-color-extra-light);
border-radius: 6px;
}
&>.drawing-item-copy,
&>.drawing-item-delete {
display: initial;
}
&>.component-name {
color: var(--el-color-primary);
}
.el-input__wrapper {
box-shadow: 0 0 0 1px var(--el-input-hover-border-color) inset;
}
}
.el-form-item {
margin-bottom: 15px;
}
}
.drawing-item {
position: relative;
cursor: move;
&.unfocus-bordered:not(.activeFromItem)>div:first-child {
border: 1px dashed var(--el-border-color);
}
.el-form-item {
padding: 12px 10px;
}
}
.drawing-row-item {
position: relative;
cursor: move;
box-sizing: border-box;
border: 1px dashed var(--el-border-color);
border-radius: 3px;
padding: 0 2px;
margin-bottom: 15px;
.drawing-row-item {
margin-bottom: 2px;
}
.el-col {
margin-top: 22px;
}
.el-form-item {
margin-bottom: 0;
}
.drag-wrapper {
min-height: 80px;
flex: 1;
display: flex;
flex-wrap: wrap;
}
&.active-from-item {
border: 1px dashed var(--el-color-primary);
}
.component-name {
position: absolute;
top: 0;
left: 0;
font-size: 12px;
color: var(--el-text-color-placeholder);
display: inline-block;
padding: 0 6px;
}
}
.drawing-item,
.drawing-row-item {
&:hover {
&>.el-form-item {
background: var(--el-border-color-extra-light);
border-radius: 6px;
}
&>.drawing-item-copy,
&>.drawing-item-delete {
display: initial;
}
}
&>.drawing-item-copy,
&>.drawing-item-delete {
display: none;
position: absolute;
top: -10px;
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
border-radius: 50%;
font-size: 12px;
border: 1px solid;
cursor: pointer;
z-index: 1;
}
&>.drawing-item-copy {
right: 56px;
border-color: var(--el-color-primary);
color: var(--el-color-primary);
background: var(--el-bg-color);
&:hover {
background: var(--el-color-primary);
color: var(--el-color-white);
}
}
&>.drawing-item-delete {
right: 24px;
border-color: var(--el-color-danger);
color: var(--el-color-danger);
background: var(--el-bg-color);
&:hover {
background: var(--el-color-danger);
color: var(--el-color-white);
}
}
}
.empty-info {
position: absolute;
top: 46%;
left: 0;
right: 0;
text-align: center;
font-size: 18px;
color: var(--el-text-color-placeholder);
letter-spacing: 4px;
}
}
}
}
}
}
</style>