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
@@ -197,7 +197,7 @@ const messagesContainer = ref<HTMLElement>()
// WebSocket 连接
let ws: WebSocket | null = null
const WS_URL = import.meta.env.VITE_APP_WS_ENDPOINT + '/api/v1/ai/mcp/ws/chat'
const WS_URL = import.meta.env.VITE_APP_WS_ENDPOINT + '/api/v1/application/ai/ws/chat'
// 计算属性
const connectionStatusText = computed(() => {
@@ -150,7 +150,7 @@ const props = defineProps({
}
})
import JobAPI, { JobLogPageQuery, JobLogTable } from "@/api/monitor/job";
import JobAPI, { JobLogPageQuery, JobLogTable } from "@/api/application/job";
import { useAppStore } from "@/store/modules/app.store";
import { DeviceEnum } from "@/enums/settings/device.enum";
@@ -36,10 +36,10 @@
</el-form-item>
<!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery" v-hasPerm="['monitor:job:query']"
<el-button type="primary" icon="search" @click="handleQuery" v-hasPerm="['app:job:query']"
>查询</el-button
>
<el-button icon="refresh" @click="handleResetQuery" v-hasPerm="['monitor:job:query']">重置</el-button>
<el-button icon="refresh" @click="handleResetQuery" v-hasPerm="['app:job:query']">重置</el-button>
<!-- 展开/收起 -->
<template v-if="isExpandable">
<el-link
@@ -83,7 +83,7 @@
type="success"
icon="plus"
@click="handleOpenDialog('create')"
v-hasPerm="['monitor:job:create']"
v-hasPerm="['app:job:create']"
>新增</el-button
>
<el-button
@@ -91,7 +91,7 @@
icon="delete"
:disabled="selectIds.length === 0"
@click="handleDelete(selectIds)"
v-hasPerm="['monitor:job:delete']"
v-hasPerm="['app:job:delete']"
>批量删除</el-button
>
</div>
@@ -102,11 +102,11 @@
icon="download"
circle
@click="handleExport"
v-hasPerm="['monitor:job:export']"
v-hasPerm="['app:job:export']"
/>
</el-tooltip>
<el-tooltip content="清除">
<el-button type="danger" icon="delete" circle @click="handleClear" v-hasPerm="['monitor:job:clear']" />
<el-button type="danger" icon="delete" circle @click="handleClear" v-hasPerm="['app:job:clear']" />
</el-tooltip>
<el-tooltip content="刷新">
<el-button
@@ -114,7 +114,7 @@
icon="refresh"
circle
@click="handleRefresh"
v-hasPerm="['monitor:job:refresh']"
v-hasPerm="['app:job:refresh']"
/>
</el-tooltip>
</div>
@@ -217,7 +217,7 @@
link
icon="edit"
@click="handleOpenDialog('update', scope.row.id)"
v-hasPerm="['monitor:job:update']"
v-hasPerm="['app:job:update']"
>
编辑
</el-button>
@@ -227,11 +227,11 @@
link
icon="delete"
@click="handleDelete([scope.row.id])"
v-hasPerm="['monitor:job:delete']"
v-hasPerm="['app:job:delete']"
>
删除
</el-button>
<el-dropdown trigger="click" v-hasPerm="['monitor:job:status']">
<el-dropdown trigger="click" v-hasPerm="['app:job:status']">
<el-button type="warning" size="small" link icon="ArrowDown"
>更多</el-button
>
@@ -566,7 +566,7 @@
@click="handleSubmit"
>确定</el-button
>
<el-button v-else type="primary" @click="handleCloseDialog" v-hasPerm="['monitor:job:detail']"
<el-button v-else type="primary" @click="handleCloseDialog" v-hasPerm="['app:job:detail']"
>确定</el-button
>
</div>
@@ -585,12 +585,12 @@ defineOptions({
inheritAttrs: false,
});
import JobAPI, { JobTable, JobForm, JobPageQuery } from "@/api/monitor/job";
import JobAPI, { JobTable, JobForm, JobPageQuery } from "@/api/application/job";
import IntervalTab from "@/components/IntervalTab/index.vue";
import { useDictStore } from "@/store/index";
import { vue3CronPlus } from "vue3-cron-plus";
import "vue3-cron-plus/dist/index.css"; // 引入样式
import JobLogDrawer from "@/views/monitor/job/components/JobLogDrawer.vue"
import JobLogDrawer from "@/views/application/job/components/JobLogDrawer.vue"
const dictStore = useDictStore();
@@ -51,10 +51,10 @@
<template #header>
<div class="card-header">
<span>
<el-tooltip content="流程列表">
<el-tooltip content="流程管理列表">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
演示示例列表
流程管理列表
</span>
</div>
</template>
@@ -103,7 +103,7 @@
</div>
</div>
<!-- 表格区域:系统配置列表 -->
<!-- 表格区域:工作流列表 -->
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row
class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
<template #empty>
@@ -234,7 +234,7 @@ defineOptions({
import { ref, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { ResultEnum } from "@/enums/api/result.enum";
import ExampleAPI, { ExampleTable, ExampleForm, ExamplePageQuery } from "@/api/demo/example";
import ExampleAPI, { ExampleTable, ExampleForm, ExamplePageQuery } from "@/api/generator/demo";
import ImportModal from "@/components/Upload/ImportModal.vue";
import DatePicker from "@/components/DatePicker/index.vue";
+263 -175
View File
@@ -2,46 +2,7 @@
<div class="app-container">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":">
<el-form-item prop="table_name" label="表名称">
<el-input v-model="queryFormData.table_name" placeholder="请输入表名称" clearable />
</el-form-item>
<el-form-item prop="status" label="状态">
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
<el-option value="true" label="启用" />
<el-option value="false" label="停用" />
</el-select>
</el-form-item>
<!-- 时间范围,收起状态下隐藏 -->
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
<DatePicker v-model="dateRange" @update:model-value="handleDateRangeChange"/>
</el-form-item>
<!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery" v-hasPerm="['demo:example:query']">
查询
</el-button>
<el-button icon="refresh" @click="handleResetQuery" v-hasPerm="['demo:example:query']">
重置
</el-button>
<!-- 展开/收起 -->
<template v-if="isExpandable">
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
{{ isExpand ? "收起" : "展开" }}
<el-icon>
<template v-if="isExpand">
<ArrowUp />
</template>
<template v-else>
<ArrowDown />
</template>
</el-icon>
</el-link>
</template>
</el-form-item>
</el-form>
<el-form v-show="showSearch" ref="queryRef" :model="queryFormData" :inline="true" >
<el-form v-show="showSearch" ref="queryRef" :model="queryFormData" :inline="true" label-suffix=":">
<el-form-item label="表名称" prop="table_name">
<el-input
v-model="queryFormData.table_name"
@@ -53,7 +14,7 @@
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input
v-model="queryFormData.tableComment"
v-model="queryFormData.table_comment"
placeholder="请输入表描述"
clearable
style="width: 200px"
@@ -71,120 +32,193 @@
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['generator:gencode:query']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery" v-hasPermi="['generator:gencode:query']">重置</el-button>
<el-button v-hasPermi="['generator:gencode:query']" type="primary" icon="Sesearcharch" @click="handleQuery">搜索</el-button>
<el-button v-hasPermi="['generator:gencode:query']" icon="refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Download"
:disabled="multiple"
@click="handleGenTable"
v-hasPermi="['generator:gencode:code']"
>生成</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="openCreateTable"
v-hasRole="['admin']"
>创建</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="Upload"
@click="openImportTable"
v-hasPermi="['generator:gencode:import']"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleEditTable"
v-hasPermi="['generator:gencode:update']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['generator:gencode:delete']"
>删除</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 内容区域 -->
<el-card shadow="hover" class="data-table">
<template #header>
<div class="card-header">
<span>
<el-tooltip content="生成代码">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
生成代码
</span>
</div>
</template>
<el-table v-loading="loading" :data="tableList" @selection-change="handleSelectionChange">
<el-table-column type="selection" align="center" width="55"></el-table-column>
<el-table-column label="序号" type="index" width="50" align="center">
<template #default="scope">
<span>{{(queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1}}</span>
<!-- 功能区域 -->
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-row :gutter="10">
<el-col :span="1.5">
<el-button
v-hasPermi="['generator:gencode:code']"
type="primary"
plain
icon="Download"
:disabled="multiple"
@click="handleGenTable"
>生成</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="openCreateTable"
v-hasRole="['admin']"
>创建</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="Upload"
@click="openImportTable"
v-hasPermi="['generator:gencode:import']"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleEditTable"
v-hasPermi="['generator:gencode:update']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['generator:gencode:delete']"
>删除</el-button>
</el-col>
</el-row>
</div>
<div class="data-table__toolbar--tools">
<el-row :gutter="10">
<el-col :span="1.5">
<el-tooltip content="导入">
<el-button type="info" icon="upload" circle @click="handleOpenImportDialog" v-hasPerm="['demo:example:import']" />
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="导出">
<el-button type="warning" icon="download" circle @click="handleExport" v-hasPerm="['demo:example:export']" />
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="刷新">
<el-button type="primary" icon="refresh" circle @click="handleRefresh" v-hasPerm="['demo:example:refresh']" />
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="列表筛选">
<el-dropdown trigger="click">
<el-button type="default" icon="operation" circle v-hasPerm="['demo:example:filter']" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="column in tableColumns" :key="column.prop"
:command="column">
<el-checkbox v-model="column.show">
{{ column.label }}
</el-checkbox>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</el-col>
</el-row>
</div>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
:data="tableList"
highlight-current-row
class="data-table__content"
height="450"
border
stripe
@selection-change="handleSelectionChange"
>
<template #empty>
<el-empty :image-size="80" description="暂无数据" />
</template>
</el-table-column>
<el-table-column
label="表名称"
align="center"
prop="table_name"
:show-overflow-tooltip="true"
/>
<el-table-column
label="表描述"
align="center"
prop="tableComment"
:show-overflow-tooltip="true"
/>
<el-table-column
label="实体"
align="center"
prop="className"
:show-overflow-tooltip="true"
/>
<el-table-column label="创建时间" align="center" prop="createTime" width="160" />
<el-table-column label="更新时间" align="center" prop="updateTime" width="160" />
<el-table-column label="操作" align="center" width="330" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="预览" placement="top">
<el-button link type="primary" icon="View" @click="handlePreview(scope.row)" v-hasPermi="['generator:gencode:query']"></el-button>
</el-tooltip>
<el-tooltip content="编辑" placement="top">
<el-button link type="primary" icon="Edit" @click="handleEditTable(scope.row)" v-hasPermi="['generator:gencode:update']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['generator:gencode:delete']"></el-button>
</el-tooltip>
<el-tooltip content="同步" placement="top">
<el-button link type="primary" icon="Refresh" @click="handleSynchDb(scope.row)" v-hasPermi="['generator:gencode:edit']"></el-button>
</el-tooltip>
<el-tooltip content="生成代码" placement="top">
<el-button link type="primary" icon="Download" @click="handleGenTable(scope.row)" v-hasPermi="['generator:gencode:code']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryFormData.page_no"
v-model:limit="queryFormData.page_size"
@pagination="getList"
/>
<el-table-column type="selection" align="center" width="55"></el-table-column>
<el-table-column label="序号" type="index" min-width="55" align="center" fixed>
<template #default="scope">
<span>{{(queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1}}</span>
</template>
</el-table-column>
<el-table-column
label="表名称"
align="center"
prop="table_name"
:show-overflow-tooltip="true"
/>
<el-table-column
label="表描述"
align="center"
prop="tableComment"
:show-overflow-tooltip="true"
/>
<el-table-column
label="实体"
align="center"
prop="className"
:show-overflow-tooltip="true"
/>
<el-table-column label="创建时间" align="center" prop="createTime" width="160" />
<el-table-column label="更新时间" align="center" prop="updateTime" width="160" />
<el-table-column label="操作" align="center" width="330" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="预览" placement="top">
<el-button link type="primary" icon="View" @click="handlePreview(scope.row)" v-hasPermi="['generator:gencode:query']"></el-button>
</el-tooltip>
<el-tooltip content="编辑" placement="top">
<el-button link type="primary" icon="Edit" @click="handleEditTable(scope.row)" v-hasPermi="['generator:gencode:update']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['generator:gencode:delete']"></el-button>
</el-tooltip>
<el-tooltip content="同步" placement="top">
<el-button link type="primary" icon="Refresh" @click="handleSynchDb(scope.row)" v-hasPermi="['generator:gencode:edit']"></el-button>
</el-tooltip>
<el-tooltip content="生成代码" placement="top">
<el-button link type="primary" icon="Download" @click="handleGenTable(scope.row)" v-hasPermi="['generator:gencode:code']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<template #footer>
<pagination
v-show="total>0"
v-model:total="total"
v-model:page="queryFormData.page_no"
v-model:limit="queryFormData.page_size"
@pagination="loadingData"
/>
</template>
</el-card>
<!-- 预览界面 -->
<el-dialog :title="preview.title" v-model="preview.open" width="80%" top="5vh" append-to-body class="scrollbar">
<el-dialog v-model="preview.open" :title="preview.title" width="80%" top="5vh" append-to-body class="scrollbar">
<el-tabs v-model="preview.activeName">
<el-tab-pane
v-for="(value, key) in preview.data"
@@ -192,7 +226,7 @@
:name="key.substring(key.lastIndexOf('/')+1,key.indexOf('.jinja2'))"
:key="value"
>
<el-link :underline="false" icon="DocumentCopy" v-copyText="value" v-copyText:callback="copyTextSuccess" style="float:right">&nbsp;复制</el-link>
<el-link :underline="false" icon="DocumentCopy" @click="() => navigator.clipboard.writeText(value).then(() => copyTextSuccess())" style="float:right">&nbsp;复制</el-link>
<pre>{{ value }}</pre>
</el-tab-pane>
</el-tabs>
@@ -202,7 +236,7 @@
</div>
</template>
<script setup name="Gen">
<script setup lang="ts">
defineOptions({
name: "GenCode",
inheritAttrs: false,
@@ -212,9 +246,12 @@ import GencodeAPI from "@/api/generator/gencode";
import router from "@/router";
import importTable from "@/views/gencode/backcode/components/importTable.vue";
import createTable from "@/views/gencode/backcode/components/createTable.vue";
import { ElMessage, ElMessageBox } from 'element-plus';
const route = useRoute();
const { proxy } = getCurrentInstance();
// 创建组件引用
const importRef = ref();
const createRef = ref();
const tableList = ref([]);
const loading = ref(true);
@@ -233,33 +270,57 @@ const data = reactive({
page_no: 1,
page_size: 10,
table_name: undefined,
tableComment: undefined
table_comment: undefined
},
preview: {
open: false,
title: "代码预览",
data: {},
activeName: "do.py"
active_name: "model.py",
activeName: "model.py" // 添加正确的 activeName 属性
}
});
// 表格列配置
const tableColumns = ref([
{ prop: 'selection', label: '选择框', show: true },
{ prop: 'index', label: '序号', show: true },
{ prop: 'name', label: '名称', show: true },
{ prop: 'status', label: '状态', show: true },
{ prop: 'description', label: '描述', show: true },
{ prop: 'created_at', label: '创建时间', show: true },
{ prop: 'updated_at', label: '更新时间', show: true },
{ prop: 'creator', label: '创建人', show: true },
{ prop: 'operation', label: '操作', show: true }
])
const { queryFormData, preview } = toRefs(data);
onActivated(() => {
const time = route.query.t;
if (time != null && time != uniqueId.value) {
uniqueId.value = time;
queryFormData.value.page_no = Number(route.query.page_no);
queryFormData.value.page_no = Number(route.query.page_no || 1);
dateRange.value = [];
proxy.resetForm("queryForm");
getList();
loadingData();
}
})
/** 查询表集合 */
function getList() {
function loadingData() {
loading.value = true;
GencodeAPI.listTable(proxy.addDateRange(queryFormData.value, dateRange.value)).then(response => {
// 创建包含日期范围的查询参数
const queryParams = {
...queryFormData.value
};
// 如果有日期范围,添加到查询参数中
if (dateRange.value && dateRange.value.length === 2) {
queryParams.start_time = dateRange.value[0];
queryParams.end_time = dateRange.value[1];
}
GencodeAPI.listTable(queryParams).then(response => {
tableList.value = response.rows;
total.value = response.total;
loading.value = false;
@@ -269,19 +330,20 @@ function getList() {
/** 搜索按钮操作 */
function handleQuery() {
queryFormData.value.page_no = 1;
getList();
loadingData();
}
/** 生成代码操作 */
function handleGenTable(row) {
const tbNames = row.table_name || tableNames.value;
if (tbNames == "") {
proxy.$modal.msgError("请选择要生成的数据");
const tbNames = row?.table_name || tableNames.value;
if (!tbNames || (Array.isArray(tbNames) && tbNames.length === 0)) {
ElMessage.error("请选择要生成的数据");
return;
}
if (row.genType === "1") {
if (row?.genType === "1") {
GencodeAPI.genCodeToPath(row.tableName).then(() => {
proxy.$modal.msgSuccess("成功生成到自定义路径:" + row.genPath);
ElMessage.success("成功生成到自定义路径:" + row.genPath);
});
} else {
GencodeAPI.batchGenCode(tbNames).then(response => {
@@ -299,27 +361,45 @@ function handleGenTable(row) {
/** 同步数据库操作 */
function handleSynchDb(row) {
const tableName = row.table_name;
proxy.$modal.confirm('确认要强制同步"' + tableName + '"表结构吗?').then(function () {
ElMessageBox.confirm(
'确认要强制同步"' + tableName + '"表结构吗?',
'确认操作',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
return GencodeAPI.syncDb(tableName);
}).then(() => {
proxy.$modal.msgSuccess("同步成功");
ElMessage.success("同步成功");
}).catch(() => {});
}
/** 打开导入表弹窗 */
function openImportTable() {
proxy.$refs["importRef"].show();
if (importRef.value && typeof importRef.value.show === 'function') {
importRef.value.show();
}
}
/** 打开创建表弹窗 */
function openCreateTable() {
proxy.$refs["createRef"].show();
if (createRef.value && typeof createRef.value.show === 'function') {
createRef.value.show();
}
}
/** 重置按钮操作 */
function resetQuery() {
dateRange.value = [];
proxy.resetForm("queryRef");
// 手动重置表单
queryFormData.value = {
page_no: 1,
page_size: 10,
table_name: undefined,
table_comment: undefined
};
handleQuery();
}
@@ -334,7 +414,7 @@ function handlePreview(row) {
/** 复制代码成功 */
function copyTextSuccess() {
proxy.$modal.msgSuccess("复制成功");
ElMessage.success("复制成功");
}
// 多选框选中数据
@@ -353,14 +433,22 @@ function handleEditTable(row) {
/** 删除按钮操作 */
function handleDelete(row) {
const tableIds = row.tableId ? [row.tableId] : ids.value;
proxy.$modal.confirm('是否确认删除表编号为"' + tableIds + '"的数据项?').then(function () {
const tableIds = row?.tableId ? [row.tableId] : ids.value;
ElMessageBox.confirm(
'是否确认删除表编号为"' + tableIds + '"的数据项?',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
return GencodeAPI.deleteTable(tableIds);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
loadingData();
ElMessage.success("删除成功");
}).catch(() => {});
}
getList();
loadingData();
</script>
@@ -22,10 +22,10 @@
</el-form-item>
<!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery" v-hasPerm="['demo:example:query']">
<el-button type="primary" icon="search" @click="handleQuery" v-hasPerm="['generator:demo:query']">
查询
</el-button>
<el-button icon="refresh" @click="handleResetQuery" v-hasPerm="['demo:example:query']">
<el-button icon="refresh" @click="handleResetQuery" v-hasPerm="['generator:demo:query']">
重置
</el-button>
<!-- 展开/收起 -->
@@ -62,33 +62,33 @@
<!-- 功能区域 -->
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button type="success" icon="plus" @click="handleOpenDialog('create')" v-hasPerm="['demo:example:create']">新增</el-button>
<el-button type="success" icon="plus" @click="handleOpenDialog('create')" v-hasPerm="['generator:demo:create']">新增</el-button>
<el-button type="danger" icon="delete" :disabled="selectIds.length === 0"
@click="handleDelete(selectIds)" v-hasPerm="['demo:example:delete']">批量删除</el-button>
@click="handleDelete(selectIds)" v-hasPerm="['generator:demo:delete']">批量删除</el-button>
<el-dropdown trigger="click">
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Check" @click="handleMoreClick(true)" v-hasPerm="['demo:example:batch_available']">批量启用</el-dropdown-item>
<el-dropdown-item icon="Check" @click="handleMoreClick(true)" v-hasPerm="['generator:demo:batch_available']">批量启用</el-dropdown-item>
<el-dropdown-item icon="CircleClose"
@click="handleMoreClick(false)" v-hasPerm="['demo:example:batch_available']">批量停用</el-dropdown-item>
@click="handleMoreClick(false)" v-hasPerm="['generator:demo:batch_available']">批量停用</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="data-table__toolbar--tools">
<el-tooltip content="导入">
<el-button type="info" icon="upload" circle @click="handleOpenImportDialog" v-hasPerm="['demo:example:import']" />
<el-button type="info" icon="upload" circle @click="handleOpenImportDialog" v-hasPerm="['generator:demo:import']" />
</el-tooltip>
<el-tooltip content="导出">
<el-button type="warning" icon="download" circle @click="handleExport" v-hasPerm="['demo:example:export']" />
<el-button type="warning" icon="download" circle @click="handleExport" v-hasPerm="['generator:demo:export']" />
</el-tooltip>
<el-tooltip content="刷新">
<el-button type="primary" icon="refresh" circle @click="handleRefresh" v-hasPerm="['demo:example:refresh']" />
<el-button type="primary" icon="refresh" circle @click="handleRefresh" v-hasPerm="['generator:demo:refresh']" />
</el-tooltip>
<el-tooltip content="列表筛选">
<el-dropdown trigger="click">
<el-button type="default" icon="operation" circle v-hasPerm="['demo:example:filter']" />
<el-button type="default" icon="operation" circle v-hasPerm="['generator:demo:filter']" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="column in tableColumns" :key="column.prop"
@@ -145,11 +145,11 @@
label="操作" align="center" min-width="200">
<template #default="scope">
<el-button type="info" size="small" link icon="document"
@click="handleOpenDialog('detail', scope.row.id)" v-hasPerm="['demo:example:detail']">详情</el-button>
@click="handleOpenDialog('detail', scope.row.id)" v-hasPerm="['generator:demo:detail']">详情</el-button>
<el-button type="primary" size="small" link icon="edit"
@click="handleOpenDialog('update', scope.row.id)" v-hasPerm="['demo:example:update']">编辑</el-button>
@click="handleOpenDialog('update', scope.row.id)" v-hasPerm="['generator:demo:update']">编辑</el-button>
<el-button type="danger" size="small" link icon="delete"
@click="handleDelete([scope.row.id])" v-hasPerm="['demo:example:delete']">删除</el-button>
@click="handleDelete([scope.row.id])" v-hasPerm="['generator:demo:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -217,7 +217,7 @@
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
<el-button @click="handleCloseDialog">取消</el-button>
<el-button v-if="dialogVisible.type !== 'detail'" type="primary"
@click="handleSubmit" v-hasPerm="['demo:example:submit']">确定</el-button>
@click="handleSubmit" v-hasPerm="['generator:demo:submit']">确定</el-button>
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
</div>
</template>
@@ -238,7 +238,7 @@ defineOptions({
import { ref, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { ResultEnum } from "@/enums/api/result.enum";
import ExampleAPI, { ExampleTable, ExampleForm, ExamplePageQuery } from "@/api/demo/example";
import ExampleAPI, { ExampleTable, ExampleForm, ExamplePageQuery } from "@/api/generator/demo";
import ImportModal from "@/components/Upload/ImportModal.vue";
import DatePicker from "@/components/DatePicker/index.vue";
@@ -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>