捕梦者基础前端框架
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

389 lines
10 KiB

<template>
<ele-page>
<div class="form-builder">
<div class="form-builder-side">
<ele-card
header="表单配置"
:body-style="{ minHeight: '360px', padding: '16px' }"
>
<div style="display: flex; align-items: center">
<div style="flex-shrink: 0">标题宽度</div>
<div style="flex: 1">
<el-select v-model="config.labelWidth" class="ele-fluid">
<el-option
v-for="item in labelWidthOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div style="flex-shrink: 0">&emsp;显示列数:</div>
<div style="flex: 1">
<el-select v-model="config.grid" class="ele-fluid">
<el-option
v-for="item in gridOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
<div style="margin: 12px 0">
<el-button
type="primary"
class="ele-btn-icon"
:icon="PlusOutlined"
@click="add"
>
添加表单项
</el-button>
<el-button
type="primary"
class="ele-btn-icon"
:icon="CodeOutlined"
@click="showPreview = true"
>
生成代码
</el-button>
<ele-popconfirm
:width="220"
placement="top-end"
content="确定要清空所有表单项吗?"
:popper-options="{
modifiers: [{ name: 'offset', options: { offset: [20, 6] } }]
}"
@confirm="items = []"
>
<template #reference>
<el-button
type="danger"
class="ele-btn-icon"
:icon="DeleteOutlined"
>
清空
</el-button>
</template>
</ele-popconfirm>
</div>
<ele-table class="items-table">
<thead style="position: sticky; top: 0; z-index: 2">
<tr>
<th style="width: 38px"></th>
<th style="text-align: center">字段名</th>
<th style="text-align: center">标题</th>
<th style="width: 98px"></th>
</tr>
</thead>
<vue-draggable
tag="tbody"
item-key="key"
v-model="items"
:animation="300"
handle=".sort-handle"
:set-data="() => void 0"
:force-fallback="true"
>
<template #item="{ element }">
<tr>
<td
:style="{
textAlign: 'center',
paddingLeft: 0,
paddingRight: 0,
width: '38px'
}"
>
<ele-text
size="md"
:icon="DragOutlined"
type="placeholder"
class="sort-handle"
/>
</td>
<td>{{ element.prop }}</td>
<td>{{ element.label }}</td>
<td style="text-align: center; width: 98px">
<el-link
type="primary"
:underline="false"
@click="edit(element)"
>
配置
</el-link>
<el-divider direction="vertical" style="margin: 0 2px" />
<ele-popconfirm
:width="220"
placement="top-end"
:content="`确定要移除“${element.label}”吗?`"
:popper-options="{
modifiers: [
{ name: 'offset', options: { offset: [20, 6] } }
]
}"
@confirm="remove(element)"
>
<template #reference>
<el-link type="danger" :underline="false">
移除
</el-link>
</template>
</ele-popconfirm>
</td>
</tr>
</template>
</vue-draggable>
</ele-table>
</ele-card>
</div>
<div class="form-builder-body">
<form-preview
ref="formPreviewRef"
:key="formKey"
:labelWidth="config.labelWidth"
:grid="config.grid"
:items="items"
:data="formState"
@unmount="onFormUnmount"
/>
</div>
</div>
<item-add v-model="showAdd" @done="save" />
<item-edit
v-model="showEdit"
:data="current"
:grid="config.grid"
@done="save"
/>
<code-preview
v-model="showPreview"
:labelWidth="config.labelWidth"
:grid="config.grid"
:items="items"
/>
</ele-page>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import VueDraggable from 'vuedraggable';
import { EleMessage } from 'ele-admin-plus/es';
import {
PlusOutlined,
CodeOutlined,
DeleteOutlined,
DragOutlined
} from '@/components/icons';
import type { ProFormItemProps } from '@/components/ProForm/types';
import {
labelWidthOptions,
gridOptions,
getColProps,
getColSpan
} from './components/util';
import ItemAdd from './components/item-add.vue';
import ItemEdit from './components/item-edit.vue';
import CodePreview from './components/code-preview.vue';
import FormPreview from './components/form-preview.vue';
/** 表单配置 */
const config = reactive({
labelWidth: 80,
grid: 2
});
/** 表单项 */
const items = ref<ProFormItemProps[]>(
[
{ label: '用户账号', prop: 'username', type: 'input', required: true },
{ label: '用户名', prop: 'nickname', type: 'input', required: true },
{
label: '性别',
prop: 'sex',
type: 'dictSelect',
props: { code: 'sex' },
required: true
},
{
label: '角色',
prop: 'roles',
type: 'multipleSelect',
options: [
{ label: '管理员', value: 1 },
{ label: '普通用户', value: 2 },
{ label: '游客', value: 3 }
],
required: true
},
{ label: '邮箱', prop: 'email', type: 'input', required: true },
{ label: '手机号', prop: 'phone', type: 'input', required: true },
{ label: '出生日期', prop: 'birthday', type: 'date', required: true },
{
label: '登录密码',
prop: 'password',
type: 'input',
props: { clearable: false, showPassword: true, type: 'password' },
required: true
},
{
label: '个人简介',
prop: 'introduction',
type: 'textarea',
colProps: { span: 24 }
}
].map((d, i) => {
return { ...d, key: `i-${i}` } as any;
})
);
/** 表单预览组件实例 */
const formPreviewRef = ref<InstanceType<typeof FormPreview> | null>(null);
/** 用于更新表单组件 */
const formKey = ref(0);
/** 表单数据缓存 */
const formState = ref({});
/** 添加弹窗 */
const showAdd = ref(false);
/** 配置弹窗 */
const showEdit = ref(false);
/** 代码预览弹窗 */
const showPreview = ref(false);
/** 当前编辑数据 */
const current = ref<ProFormItemProps | null>(null);
/** 添加表单项 */
const add = () => {
showAdd.value = true;
};
/** 移除表单项 */
const remove = (item: ProFormItemProps) => {
const index = items.value.indexOf(item);
items.value.splice(index, 1);
};
/** 打开表单项配置弹窗 */
const edit = (item: ProFormItemProps) => {
current.value = item;
showEdit.value = true;
};
/** 保存表单项配置 */
const save = (data: ProFormItemProps) => {
const index = items.value.findIndex((item) => item.key === data.key);
const old = items.value.find((item) => item.prop === data.prop);
if (index === -1) {
if (old != null) {
EleMessage.error(`字段名${data.prop}已经存在`);
return;
}
formPreviewRef.value?.resetFields?.(data.prop);
items.value.push(data);
showAdd.value = false;
formKey.value++;
return;
}
if (old != null && old.key !== data.key) {
EleMessage.error(`字段名${data.prop}已经存在`);
return;
}
formPreviewRef.value?.resetFields?.(data.prop);
items.value[index] = data;
showEdit.value = false;
formKey.value++;
};
/** 表单组件销毁事件 */
const onFormUnmount = (form: Record<string, any>) => {
formState.value = form;
};
/** 更新表单项跨列的值 */
watch(
() => config.grid,
(grid, old) => {
items.value.forEach((item) => {
if (item.colProps && item.colProps.span) {
const span = getColSpan(old, item.colProps.span);
if (span > grid) {
item.colProps = void 0;
} else {
const colProps = getColProps(grid, span);
if (colProps == null) {
item.colProps = void 0;
} else {
Object.assign(item.colProps, colProps);
}
}
}
});
}
);
</script>
<script lang="ts">
export default {
name: 'FormBuild'
};
</script>
<style lang="scss" scoped>
.form-builder {
display: flex;
align-items: flex-start;
}
.form-builder-side {
flex-shrink: 0;
width: 360px;
}
.form-builder-body {
flex: 1;
margin-left: 16px;
}
@media screen and (max-width: 1200px) {
.form-builder {
display: block;
}
.form-builder-side {
width: 100%;
}
.form-builder-body {
margin-left: 0;
}
}
//
.items-table {
table-layout: fixed;
.sort-handle {
cursor: move;
}
tr.sortable-ghost {
opacity: 0;
}
tr.sortable-fallback {
opacity: 1 !important;
display: table !important;
table-layout: fixed !important;
td {
background: var(--el-color-primary-light-8);
}
}
}
</style>