mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-29 23:44:41 +00:00
refactor(module): 重构模块结构,将示例模块和AI模块迁移至应用模块
- 删除原有的示例模块(module_example)和AI模块(module_ai) - 在应用模块(module_application)下新增AI子模块和示例子模块 - 更新相关路由配置,移除不再使用的模块路由 - 调整定时任务模块从监控模块迁移至应用模块 - 更新前端API路径和组件文件结构 - 修正数据库表名前缀,统一使用'app_'作为应用模块表前缀
This commit is contained in:
@@ -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(() => {
|
||||
+1
-1
@@ -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";
|
||||
|
||||
+13
-13
@@ -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();
|
||||
|
||||
+4
-4
@@ -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";
|
||||
|
||||
@@ -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"> 复制</el-link>
|
||||
<el-link :underline="false" icon="DocumentCopy" @click="() => navigator.clipboard.writeText(value).then(() => copyTextSuccess())" style="float:right"> 复制</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>
|
||||
|
||||
+15
-15
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user