Vue 3版本初次提交,继续测试中。

This commit is contained in:
vdpAdmin 2021-12-25 19:36:55 +08:00
commit d3c60c5b8a
294 changed files with 22068 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/variant-form3-vite.iml" filepath="$PROJECT_DIR$/variant-form3-vite.iml" />
</modules>
</component>
</project>

7
.idea/vcs.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/tmp" vcs="Git" />
</component>
</project>

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

10
jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"######": "本文件用于解决IDEA无法识别Vue项目@符号的问题",
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

3154
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "variant-form3",
"version": "3.0.0",
"private": false,
"scripts": {
"serve": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^0.24.0",
"clipboard": "^2.0.8",
"core-js": "^3.6.5",
"element-plus": "^1.2.0-beta.5",
"file-saver": "^2.0.5",
"mitt": "^3.0.0",
"vue": "^3.0.0",
"vue-i18n": "^9.2.0-beta.23",
"vue3-quill": "^0.2.6",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@types/node": "^17.0.0",
"@vitejs/plugin-vue": "^2.0.0",
"@vitejs/plugin-vue-jsx": "^1.3.3",
"ace-builds": "^1.4.12",
"babel-eslint": "^10.1.0",
"mvdir": "^1.0.21",
"sass": "^1.45.0",
"vite": "^2.7.2",
"vite-plugin-svg-icons": "^1.0.5"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

40
src/App.vue Normal file
View File

@ -0,0 +1,40 @@
<template>
<div id="app">
<VFormDesigner />
</div>
</template>
<script>
import VFormDesigner from './components/form-designer/index.vue'
export default {
name: 'App',
components: {
VFormDesigner,
},
data() {
return {
formJson: {"widgetList":[],"formConfig":{"modelName":"formData","refName":"vForm","rulesName":"rules","labelWidth":80,"labelPosition":"left","size":"","labelAlign":"label-left-align","cssCode":"","customClass":"","functions":"","layoutType":"PC","onFormCreated":"","onFormMounted":"","onFormDataChange":"","onFormValidate":""}},
formData: {},
optionData: {}
}
},
methods: {
submitForm() {
this.$refs.vFormRef.getFormData().then(formData => {
// Form Validation OK
alert( JSON.stringify(formData) )
}).catch(error => {
// Form Validation failed
this.$message.error(error)
})
}
}
}
</script>
<style lang="scss">
#app {
height: 100%;
}
</style>

BIN
src/assets/ft-images/t1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
src/assets/ft-images/t2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
src/assets/ft-images/t3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
src/assets/ft-images/t4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src/assets/ft-images/t5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
src/assets/ft-images/t6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
src/assets/ft-images/t7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
src/assets/ft-images/t8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
src/assets/vform-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

View File

@ -0,0 +1,135 @@
<template>
<div class="ace-container">
<!-- 官方文档中使用id这里禁止使用在后期打包后容易出现问题使用 ref 或者 DOM 就行 -->
<div class="ace-editor" ref="ace"></div>
</div>
</template>
<script>
import ace from 'ace-builds'
/* webpackjs便
特别提示禁用此行后需要调用ace.config.set('basePath', 'path...')指定动态js加载URL
*/
//import 'ace-builds/webpack-resolver'
//import 'ace-builds/src-min-noconflict/theme-monokai' //
import 'ace-builds/src-min-noconflict/theme-sqlserver' //
import 'ace-builds/src-min-noconflict/mode-javascript' //
import 'ace-builds/src-min-noconflict/mode-json' //
import 'ace-builds/src-min-noconflict/mode-css' //
import 'ace-builds/src-min-noconflict/ext-language_tools'
import {ACE_BASE_PATH} from "@/utils/config";
export default {
name: 'CodeEditor',
props: {
modelValue: {
type: String,
//required: true
},
readonly: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'javascript'
},
userWorker: { //
type: Boolean,
default: true
},
},
emits: ['update:modelValue'],
mounted() {
//ace.config.set('basePath', 'https://ks3-cn-beijing.ksyun.com/vform2021/ace')
ace.config.set('basePath', ACE_BASE_PATH)
this.addAutoCompletion(ace) //
this.aceEditor = ace.edit(this.$refs.ace, {
maxLines: 20, //
minLines: 5, //
fontSize: 12, //
theme: this.themePath, //
mode: this.modePath, //
tabSize: 2, // 2
readOnly: this.readonly,
highlightActiveLine: true,
value: this.codeValue
})
this.aceEditor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true, //
enableLiveAutocompletion: true, //
})
if (this.mode === 'json') {
this.setJsonMode()
} else if (this.mode === 'css') {
this.setCssMode()
}
if (!this.userWorker) {
this.aceEditor.getSession().setUseWorker(false)
}
//
this.aceEditor.getSession().on('change',(ev)=>{
/*
//this.$emit('update:value', this.aceEditor.getValue()) // , .sync
this.$emit('input', this.aceEditor.getValue())
*/
this.$emit('update:modelValue', this.aceEditor.getValue())
})
},
data() {
return {
aceEditor: null,
themePath: 'ace/theme/sqlserver', // webpack-resolver
modePath: 'ace/mode/javascript', //
codeValue: this.modelValue
}
},
watch: {
//
},
methods: {
addAutoCompletion(ace) {
let acData = [
{meta: 'VForm API', caption: 'getWidgetRef', value: 'getWidgetRef()', score: 1},
{meta: 'VForm API', caption: 'getFormRef', value: 'getFormRef()', score: 1},
//TODO:
]
let langTools = ace.require('ace/ext/language_tools')
langTools.addCompleter({
getCompletions: function(editor, session, pos, prefix, callback) {
if (prefix.length === 0) {
return callback(null, []);
}else {
return callback(null, acData);
}
}
})
},
setJsonMode() {
this.aceEditor.getSession().setMode('ace/mode/json')
},
setCssMode() {
this.aceEditor.getSession().setMode('ace/mode/css')
},
}
}
</script>
<style lang="scss" scoped>
.ace-editor {
min-height: 300px;
}
</style>

View File

@ -0,0 +1,929 @@
/**
* author: vformAdmin
* email: vdpadmin@163.com
* website: https://www.vform666.com
* date: 2021.08.18
* remark: 如果要分发VForm源码需在本文件顶部保留此文件头信息
*/
import {deepClone, generateId, overwriteObj} from "@/utils/util"
import {containers, advancedFields, basicFields, customFields} from "@/components/form-designer/widget-panel/widgetsConfig.js"
import {VARIANT_FORM_VERSION} from "@/utils/config"
import eventBus from "@/utils/event-bus"
export function createDesigner(vueInstance) {
let defaultFormConfig = {
modelName: 'formData',
refName: 'vForm',
rulesName: 'rules',
labelWidth: 80,
labelPosition: 'left',
size: '',
labelAlign: 'label-left-align',
cssCode: '',
customClass: '',
functions: '',
layoutType: 'PC',
onFormCreated: '',
onFormMounted: '',
onFormDataChange: '',
}
return {
widgetList: [],
formConfig: {cssCode: ''},
selectedId: null,
selectedWidget: null,
selectedWidgetName: null, //选中组件名称(唯一)
vueInstance: vueInstance,
formWidget: null, //表单设计容器
cssClassList: [], //自定义样式列表
historyData: {
index: -1, //index: 0,
maxStep: 20,
steps: [],
},
initDesigner() {
this.widgetList = []
this.formConfig = deepClone(defaultFormConfig)
//输出版本信息和语雀链接
console.info(`%cVariantForm %cVer${VARIANT_FORM_VERSION} %chttps://www.yuque.com/variantdev/vform`,
"color:#409EFF;font-size: 22px;font-weight:bolder",
"color:#999;font-size: 12px",
"color:#333"
)
this.initHistoryData()
},
clearDesigner(skipHistoryChange) {
let emptyWidgetListFlag = (this.widgetList.length === 0)
this.widgetList = []
this.selectedId = null
this.selectedWidgetName = null
this.selectedWidget = {} //this.selectedWidget = null
overwriteObj(this.formConfig, defaultFormConfig) //
if (!!skipHistoryChange) {
//什么也不做!!
} else if (!emptyWidgetListFlag) {
this.emitHistoryChange()
} else {
this.saveCurrentHistoryStep()
}
},
loadPresetCssCode(preCssCode) {
if ((this.formConfig.cssCode === '') && !!preCssCode) {
this.formConfig.cssCode = preCssCode
}
},
getLayoutType() {
return this.formConfig.layoutType || 'PC'
},
changeLayoutType(newType) {
this.formConfig.layoutType = newType
},
getImportTemplate() {
return {
widgetList: [],
formConfig: deepClone(this.formConfig)
}
},
loadFormJson(formJson) {
let modifiedFlag = false
if (!!formJson && !!formJson.widgetList) {
this.widgetList = formJson.widgetList
modifiedFlag = true
}
if (!!formJson && !!formJson.formConfig) {
//this.formConfig = importObj.formConfig
overwriteObj(this.formConfig, formJson.formConfig) /* 用=赋值会导致inject依赖注入的formConfig属性变成非响应式 */
modifiedFlag = true
}
return modifiedFlag
},
setSelected(selected) {
if (!selected) {
this.clearSelected()
return
}
this.selectedWidget = selected
if (!!selected.id) {
this.selectedId = selected.id
this.selectedWidgetName = selected.options.name
}
},
updateSelectedWidgetNameAndRef(selectedWidget, newName, newLabel) {
this.selectedWidgetName = newName
//selectedWidget.options.name = newName //此行多余
if (!!newLabel && (Object.keys(selectedWidget.options).indexOf('label') > -1)) {
selectedWidget.options.label = newLabel
}
},
clearSelected() {
this.selectedId = null
this.selectedWidgetName = null
this.selectedWidget = {} //this.selectedWidget = null
},
checkWidgetMove(evt) { /* Only field widget can be dragged into sub-form */
if (!!evt.draggedContext && !!evt.draggedContext.element) {
let wgCategory = evt.draggedContext.element.category
if (!!evt.to) {
if ((evt.to.className === 'sub-form-table') && (wgCategory === 'container')) {
//this.$message.info(this.vueInstance.i18nt('designer.hint.onlyFieldWidgetAcceptable'))
return false
}
}
}
return true
},
/**
* 追加表格新行
* @param widget
*/
appendTableRow(widget) {
let rowIdx = widget.rows.length//确定插入行位置
let newRow = deepClone(widget.rows[widget.rows.length - 1])
newRow.id = 'table-row-' + generateId()
newRow.merged = false
newRow.cols.forEach(col => {
col.id = 'table-cell-' + generateId()
col.options.name = col.id
col.merged = false
col.options.colspan = 1
col.options.rowspan = 1
col.widgetList.length = 0
})
widget.rows.splice(rowIdx, 0, newRow)
this.emitHistoryChange()
},
/**
* 追加表格新列
* @param widget
*/
appendTableCol(widget) {
let colIdx = widget.rows[0].cols.length //确定插入列位置
widget.rows.forEach(row => {
let newCol = deepClone(this.getContainerByType('table-cell'))
newCol.id = 'table-cell-' + generateId()
newCol.options.name = newCol.id
newCol.merged = false
newCol.options.colspan = 1
newCol.options.rowspan = 1
newCol.widgetList.length = 0
row.cols.splice(colIdx, 0, newCol)
})
this.emitHistoryChange()
},
insertTableRow(widget, insertPos, cloneRowIdx, curCol, aboveFlag) {
let newRowIdx = !!aboveFlag ? insertPos : (insertPos + 1) //初步确定插入行位置
if (!aboveFlag) { //继续向下寻找同列第一个未被合并的单元格
let tmpRowIdx = newRowIdx
let rowFoundFlag = false
while (tmpRowIdx < widget.rows.length) {
if (!widget.rows[tmpRowIdx].cols[curCol].merged) {
newRowIdx = tmpRowIdx
rowFoundFlag = true
break
} else {
tmpRowIdx++
}
}
if (!rowFoundFlag) {
newRowIdx = widget.rows.length
}
}
let newRow = deepClone( widget.rows[cloneRowIdx] )
newRow.id = 'table-row-' + generateId()
newRow.merged = false
newRow.cols.forEach(col => {
col.id = 'table-cell-' + generateId()
col.options.name = col.id
col.merged = false
col.options.colspan = 1
col.options.rowspan = 1
col.widgetList.length = 0
})
widget.rows.splice(newRowIdx, 0, newRow)
let colNo = 0
while ((newRowIdx < widget.rows.length - 1) && (colNo < widget.rows[0].cols.length)) { //越界判断
const cellOfNextRow = widget.rows[newRowIdx + 1].cols[colNo]
const rowMerged = cellOfNextRow.merged //确定插入位置下一行的单元格是否为合并单元格
if (!!rowMerged) {
let rowArray = widget.rows
let unMergedCell = {}
let startRowIndex = null
for (let i = newRowIdx; i >= 0; i--) { //查找该行已合并的主单元格
if (!rowArray[i].cols[colNo].merged && (rowArray[i].cols[colNo].options.rowspan > 1)) {
startRowIndex = i
unMergedCell = rowArray[i].cols[colNo]
break
}
}
if (!!unMergedCell.options) { //如果有符合条件的unMergedCell
let newRowspan = unMergedCell.options.rowspan + 1
this.setPropsOfMergedRows(widget.rows, startRowIndex, colNo, unMergedCell.options.colspan, newRowspan)
colNo += unMergedCell.options.colspan
} else {
colNo += 1
}
} else {
//colNo += 1
colNo += cellOfNextRow.options.colspan || 1
}
}
this.emitHistoryChange()
},
insertTableCol(widget, insertPos, curRow, leftFlag) {
let newColIdx = !!leftFlag ? insertPos : (insertPos + 1) //初步确定插入列位置
if (!leftFlag) { //继续向右寻找同行第一个未被合并的单元格
let tmpColIdx = newColIdx
let colFoundFlag = false
while (tmpColIdx < widget.rows[curRow].cols.length) {
if (!widget.rows[curRow].cols[tmpColIdx].merged) {
newColIdx = tmpColIdx
colFoundFlag = true
break
} else {
tmpColIdx++
}
if (!colFoundFlag) {
newColIdx = widget.rows[curRow].cols.length
}
}
}
widget.rows.forEach(row => {
let newCol = deepClone(this.getContainerByType('table-cell'))
newCol.id = 'table-cell-' + generateId()
newCol.options.name = newCol.id
newCol.merged = false
newCol.options.colspan = 1
newCol.options.rowspan = 1
newCol.widgetList.length = 0
row.cols.splice(newColIdx, 0, newCol)
})
let rowNo = 0
while((newColIdx < widget.rows[0].cols.length - 1) && (rowNo < widget.rows.length)) { //越界判断
const cellOfNextCol = widget.rows[rowNo].cols[newColIdx + 1]
const colMerged = cellOfNextCol.merged //确定插入位置右侧列的单元格是否为合并单元格
if (!!colMerged) {
let colArray = widget.rows[rowNo].cols
let unMergedCell = {}
let startColIndex = null
for (let i = newColIdx; i >= 0; i--) { //查找该行已合并的主单元格
if (!colArray[i].merged && (colArray[i].options.colspan > 1)) {
startColIndex = i
unMergedCell = colArray[i]
break
}
}
if (!!unMergedCell.options) { //如果有符合条件的unMergedCell
let newColspan = unMergedCell.options.colspan + 1
this.setPropsOfMergedCols(widget.rows, rowNo, startColIndex, newColspan, unMergedCell.options.rowspan)
rowNo += unMergedCell.options.rowspan
} else {
rowNo += 1
}
} else {
//rowNo += 1
rowNo += cellOfNextCol.options.rowspan || 1
}
}
this.emitHistoryChange()
},
setPropsOfMergedCols(rowArray, startRowIndex, startColIndex, newColspan, rowspan) {
for (let i = startRowIndex; i < startRowIndex + rowspan; i++) {
for (let j = startColIndex; j < startColIndex + newColspan; j++) {
if ((i === startRowIndex) && (j === startColIndex)) {
rowArray[i].cols[j].options.colspan = newColspan //合并后的主单元格
continue
}
rowArray[i].cols[j].merged = true
rowArray[i].cols[j].options.colspan = newColspan
rowArray[i].cols[j].widgetList = []
}
}
},
setPropsOfMergedRows(rowArray, startRowIndex, startColIndex, colspan, newRowspan) {
for (let i = startRowIndex; i < startRowIndex + newRowspan; i++) {
for (let j = startColIndex; j < startColIndex + colspan; j++) {
if ((i === startRowIndex) && (j === startColIndex)) {
rowArray[i].cols[j].options.rowspan = newRowspan
continue
}
rowArray[i].cols[j].merged = true
rowArray[i].cols[j].options.rowspan = newRowspan
rowArray[i].cols[j].widgetList = []
}
}
},
setPropsOfSplitCol(rowArray, startRowIndex, startColIndex, colspan, rowspan) {
for (let i = startRowIndex; i < startRowIndex + rowspan; i++) {
for (let j = startColIndex; j < startColIndex + colspan; j++) {
rowArray[i].cols[j].merged = false;
rowArray[i].cols[j].options.rowspan = 1
rowArray[i].cols[j].options.colspan = 1
}
}
},
setPropsOfSplitRow(rowArray, startRowIndex, startColIndex, colspan, rowspan) {
for (let i = startRowIndex; i < startRowIndex + rowspan; i++) {
for (let j = startColIndex; j < startColIndex + colspan; j++) {
rowArray[i].cols[j].merged = false;
rowArray[i].cols[j].options.rowspan = 1
rowArray[i].cols[j].options.colspan = 1
}
}
},
mergeTableCol(rowArray, colArray, curRow, curCol, leftFlag, cellWidget) {
let mergedColIdx = !!leftFlag ? curCol : curCol + colArray[curCol].options.colspan
// let remainedColIdx = !!leftFlag ? curCol - colArray[curCol - 1].options.colspan : curCol
let remainedColIdx = !!leftFlag ? curCol - 1 : curCol
if (!!leftFlag) { //继续向左寻找同行未被合并的第一个单元格
let tmpColIdx = remainedColIdx
while (tmpColIdx >= 0) {
if (!rowArray[curRow].cols[tmpColIdx].merged) {
remainedColIdx = tmpColIdx
break;
} else {
tmpColIdx--
}
}
}
if (!!colArray[mergedColIdx].widgetList && (colArray[mergedColIdx].widgetList.length > 0)) { //保留widgetList
if (!colArray[remainedColIdx].widgetList || (colArray[remainedColIdx].widgetList.length === 0)) {
colArray[remainedColIdx].widgetList = deepClone(colArray[mergedColIdx].widgetList)
}
}
let newColspan = colArray[mergedColIdx].options.colspan * 1 + colArray[remainedColIdx].options.colspan * 1
this.setPropsOfMergedCols(rowArray, curRow, remainedColIdx, newColspan, cellWidget.options.rowspan)
this.emitHistoryChange()
},
mergeTableWholeRow(rowArray, colArray, rowIndex, colIndex) { //需要考虑操作的行存在已合并的单元格!!
//整行所有单元格行高不一致不可合并!!
let startRowspan = rowArray[rowIndex].cols[0].options.rowspan
let unmatchedFlag = false
for (let i = 1; i < rowArray[rowIndex].cols.length; i++) {
if (rowArray[rowIndex].cols[i].options.rowspan !== startRowspan) {
unmatchedFlag = true
break;
}
}
if (unmatchedFlag) {
this.vueInstance.$message.info(this.vueInstance.i18nt('designer.hint.rowspanNotConsistentForMergeEntireRow'))
return
}
let widgetListCols = colArray.filter((colItem) => {
return !colItem.merged && !!colItem.widgetList && (colItem.widgetList.length > 0)
})
if (!!widgetListCols && (widgetListCols.length > 0)) { //保留widgetList
if ((widgetListCols[0].id !== colArray[0].id) && (!colArray[0].widgetList ||
colArray[0].widgetList.length <= 0)) {
colArray[0].widgetList = deepClone( widgetListCols[0].widgetList )
}
}
this.setPropsOfMergedCols(rowArray, rowIndex, 0, colArray.length, colArray[colIndex].options.rowspan)
this.emitHistoryChange()
},
mergeTableRow(rowArray, curRow, curCol, aboveFlag, cellWidget) {
let mergedRowIdx = !!aboveFlag ? curRow : curRow + cellWidget.options.rowspan
//let remainedRowIdx = !!aboveFlag ? curRow - cellWidget.options.rowspan : curRow
let remainedRowIdx = !!aboveFlag ? curRow - 1 : curRow
if (!!aboveFlag) { //继续向上寻找同列未被合并的第一个单元格
let tmpRowIdx = remainedRowIdx
while (tmpRowIdx >= 0) {
if (!rowArray[tmpRowIdx].cols[curCol].merged) {
remainedRowIdx = tmpRowIdx
break;
} else {
tmpRowIdx--
}
}
}
if (!!rowArray[mergedRowIdx].cols[curCol].widgetList && (rowArray[mergedRowIdx].cols[curCol].widgetList.length > 0)) { //保留widgetList
if (!rowArray[remainedRowIdx].cols[curCol].widgetList || (rowArray[remainedRowIdx].cols[curCol].widgetList.length === 0)) {
rowArray[remainedRowIdx].cols[curCol].widgetList = deepClone(rowArray[mergedRowIdx].cols[curCol].widgetList)
}
}
let newRowspan = rowArray[mergedRowIdx].cols[curCol].options.rowspan * 1 + rowArray[remainedRowIdx].cols[curCol].options.rowspan * 1
this.setPropsOfMergedRows(rowArray, remainedRowIdx, curCol, cellWidget.options.colspan, newRowspan)
this.emitHistoryChange()
},
mergeTableWholeCol(rowArray, colArray, rowIndex, colIndex) { //需要考虑操作的列存在已合并的单元格!!
//整列所有单元格列宽不一致不可合并!!
let startColspan = rowArray[0].cols[colIndex].options.colspan
let unmatchedFlag = false
for (let i = 1; i < rowArray.length; i++) {
if (rowArray[i].cols[colIndex].options.colspan !== startColspan) {
unmatchedFlag = true
break;
}
}
if (unmatchedFlag) {
this.vueInstance.$message.info(this.vueInstance.i18nt('designer.hint.colspanNotConsistentForMergeEntireColumn'))
return
}
let widgetListCols = []
rowArray.forEach(rowItem => {
let tempCell = rowItem.cols[colIndex]
if (!tempCell.merged && !!tempCell.widgetList && (tempCell.widgetList.length > 0)) {
widgetListCols.push(tempCell)
}
})
let firstCellOfCol = rowArray[0].cols[colIndex]
if (!!widgetListCols && (widgetListCols.length > 0)) { //保留widgetList
if ((widgetListCols[0].id !== firstCellOfCol.id) && (!firstCellOfCol.widgetList ||
firstCellOfCol.widgetList.length <= 0)) {
firstCellOfCol.widgetList = deepClone( widgetListCols[0].widgetList )
}
}
this.setPropsOfMergedRows(rowArray, 0, colIndex, firstCellOfCol.options.colspan, rowArray.length)
this.emitHistoryChange()
},
undoMergeTableCol(rowArray, rowIndex, colIndex, colspan, rowspan) {
this.setPropsOfSplitCol(rowArray, rowIndex, colIndex, colspan, rowspan)
this.emitHistoryChange()
},
undoMergeTableRow(rowArray, rowIndex, colIndex, colspan, rowspan) {
this.setPropsOfSplitRow(rowArray, rowIndex, colIndex, colspan, rowspan)
this.emitHistoryChange()
},
deleteTableWholeCol(rowArray, colIndex) { //需考虑删除的是合并列!!
let onlyOneColFlag = true
rowArray.forEach(ri => {
if (ri.cols[0].options.colspan !== rowArray[0].cols.length) {
onlyOneColFlag = false
}
})
//仅剩一列则不可删除!!
if (onlyOneColFlag) {
this.vueInstance.$message.info(this.vueInstance.i18nt('designer.hint.lastColCannotBeDeleted'))
return
}
//整列所有单元格列宽不一致不可删除!!
let startColspan = rowArray[0].cols[colIndex].options.colspan
let unmatchedFlag = false
for (let i = 1; i < rowArray.length; i++) {
if (rowArray[i].cols[colIndex].options.colspan !== startColspan) {
unmatchedFlag = true
break;
}
}
if (unmatchedFlag) {
this.vueInstance.$message.info(this.vueInstance.i18nt('designer.hint.colspanNotConsistentForDeleteEntireColumn'))
return
}
rowArray.forEach((rItem) => {
rItem.cols.splice(colIndex, startColspan)
})
this.emitHistoryChange()
},
deleteTableWholeRow(rowArray, rowIndex) { //需考虑删除的是合并行!!
let onlyOneRowFlag = true
rowArray[0].cols.forEach(ci => {
if (ci.options.rowspan !== rowArray.length) {
onlyOneRowFlag = false
}
})
//仅剩一行则不可删除!!
if (onlyOneRowFlag) {
this.vueInstance.$message.info(this.vueInstance.i18nt('designer.hint.lastRowCannotBeDeleted'))
return
}
//整行所有单元格行高不一致不可删除!!
let startRowspan = rowArray[rowIndex].cols[0].options.rowspan
let unmatchedFlag = false
for (let i = 1; i < rowArray[rowIndex].cols.length; i++) {
if (rowArray[rowIndex].cols[i].options.rowspan !== startRowspan) {
unmatchedFlag = true
break;
}
}
if (unmatchedFlag) {
this.vueInstance.$message.info(this.vueInstance.i18nt('designer.hint.rowspanNotConsistentForDeleteEntireRow'))
return
}
rowArray.splice(rowIndex, startRowspan)
this.emitHistoryChange()
},
getContainerByType(typeName) {
let allWidgets = [...containers, ...basicFields, ...advancedFields, ...customFields]
let foundCon = null
allWidgets.forEach(con => {
if (!!con.category && !!con.type && (con.type === typeName)) {
foundCon = con
}
})
return foundCon
},
getFieldWidgetByType(typeName) {
let allWidgets = [...containers, ...basicFields, ...advancedFields, ...customFields]
let foundWidget = null
allWidgets.forEach(widget => {
if (!!!widget.category && !!widget.type && (widget.type === typeName)) {
foundWidget = widget
}
})
return foundWidget
},
hasConfig(widget, configName) {
let originalWidget = null
if (!!widget.category) {
originalWidget = this.getContainerByType(widget.type)
} else {
originalWidget = this.getFieldWidgetByType(widget.type)
}
if (!originalWidget || !originalWidget.options) {
return false
}
return Object.keys(originalWidget.options).indexOf(configName) > -1
},
cloneGridCol(widget, parentWidget) {
let newGridCol = deepClone(this.getContainerByType('grid-col'))
newGridCol.options.span = widget.options.span
let tmpId = generateId()
newGridCol.id = 'grid-col-' + tmpId
newGridCol.options.name = 'gridCol' + tmpId
parentWidget.cols.push(newGridCol)
},
cloneContainer(containWidget) {
if (containWidget.type === 'grid') {
let newGrid = deepClone(this.getContainerByType('grid'))
newGrid.id = newGrid.type + generateId()
newGrid.options.name = newGrid.id
containWidget.cols.forEach(gridCol => {
let newGridCol = deepClone(this.getContainerByType('grid-col'))
let tmpId = generateId()
newGridCol.id = 'grid-col-' + tmpId
newGridCol.options.name = 'gridCol' + tmpId
newGridCol.options.span = gridCol.options.span
newGrid.cols.push(newGridCol)
})
return newGrid
} else if (containWidget.type === 'table') {
let newTable = deepClone(this.getContainerByType('table'))
newTable.id = newTable.type + generateId()
newTable.options.name = newTable.id
containWidget.rows.forEach(tRow => {
let newRow = deepClone(tRow)
newRow.id = 'table-row-' + generateId()
newRow.cols.forEach(col => {
col.id = 'table-cell-' + generateId()
col.options.name = col.id
col.widgetList = [] //清空组件列表
})
newTable.rows.push(newRow)
})
return newTable
} else { //其他容器组件不支持clone操作
return null
}
},
moveUpWidget(parentList, indexOfParentList) {
if (!!parentList) {
if (indexOfParentList === 0) {
this.vueInstance.$message(this.vueInstance.i18nt('designer.hint.moveUpFirstChildHint'))
return
}
let tempWidget = parentList[indexOfParentList]
parentList.splice(indexOfParentList, 1)
parentList.splice(indexOfParentList - 1, 0, tempWidget)
}
},
moveDownWidget(parentList, indexOfParentList) {
if (!!parentList) {
if (indexOfParentList === parentList.length - 1) {
this.vueInstance.$message(this.vueInstance.i18nt('designer.hint.moveDownLastChildHint'))
return
}
let tempWidget = parentList[indexOfParentList]
parentList.splice(indexOfParentList, 1)
parentList.splice(indexOfParentList + 1, 0, tempWidget)
}
},
copyNewFieldWidget(origin) {
let newWidget = deepClone(origin)
let tempId = generateId()
newWidget.id = newWidget.type.replace(/-/g, '') + tempId
newWidget.options.name = newWidget.id
newWidget.options.label = newWidget.type.toLowerCase()
delete newWidget.displayName
return newWidget
},
copyNewContainerWidget(origin) {
let newCon = deepClone(origin)
newCon.id = newCon.type.replace(/-/g, '') + generateId()
newCon.options.name = newCon.id
if (newCon.type === 'grid') {
let newCol = deepClone( this.getContainerByType('grid-col') )
let tmpId = generateId()
newCol.id = 'grid-col-' + tmpId
newCol.options.name = 'gridCol' + tmpId
newCon.cols.push(newCol)
//
newCol = deepClone(newCol)
tmpId = generateId()
newCol.id = 'grid-col-' + tmpId
newCol.options.name = 'gridCol' + tmpId
newCon.cols.push(newCol)
} else if (newCon.type === 'table') {
let newRow = {cols: []}
newRow.id = 'table-row-' + generateId()
newRow.merged = false
let newCell = deepClone( this.getContainerByType('table-cell') )
newCell.id = 'table-cell-' + generateId()
newCell.options.name = newCell.id
newCell.merged = false
newCell.options.colspan = 1
newCell.options.rowspan = 1
newRow.cols.push(newCell)
newCon.rows.push(newRow)
} else if (newCon.type === 'tab') {
let newTabPane = deepClone( this.getContainerByType('tab-pane') )
newTabPane.id = 'tab-pane-' + generateId()
newTabPane.options.name = 'tab1'
newTabPane.options.label = 'tab 1'
newCon.tabs.push(newTabPane)
}
//newCon.options.customClass = []
delete newCon.displayName
return newCon
},
addContainerByDbClick(container) {
let newCon = this.copyNewContainerWidget(container)
this.widgetList.push(newCon)
this.setSelected(newCon)
},
addFieldByDbClick(widget) {
let newWidget = this.copyNewFieldWidget(widget)
if (!!this.selectedWidget && this.selectedWidget.type === 'tab') {
//获取当前激活的tabPane
let activeTab = this.selectedWidget.tabs[0]
this.selectedWidget.tabs.forEach(tabPane => {
if (!!tabPane.options.active) {
activeTab = tabPane
}
})
!!activeTab && activeTab.widgetList.push(newWidget)
} else if (!!this.selectedWidget && !!this.selectedWidget.widgetList) {
this.selectedWidget.widgetList.push(newWidget)
} else {
this.widgetList.push(newWidget)
}
this.setSelected(newWidget)
this.emitHistoryChange()
},
deleteColOfGrid(gridWidget, colIdx) {
if (!!gridWidget && !!gridWidget.cols) {
gridWidget.cols.splice(colIdx, 1)
}
},
addNewColOfGrid(gridWidget) {
const cols = gridWidget.cols
let newGridCol = deepClone(this.getContainerByType('grid-col'))
let tmpId = generateId()
newGridCol.id = 'grid-col-' + tmpId
newGridCol.options.name = 'gridCol' + tmpId
if ((!!cols) && (cols.length > 0)) {
let spanSum = 0
cols.forEach((col) => {
spanSum += col.options.span
})
if (spanSum >= 24) {
//this.$message.info('列栅格之和超出24')
console.log('列栅格之和超出24')
gridWidget.cols.push(newGridCol)
} else {
newGridCol.options.span = (24 - spanSum) > 12 ? 12 : (24 - spanSum)
gridWidget.cols.push(newGridCol)
}
} else {
gridWidget.cols = [newGridCol]
}
},
addTabPaneOfTabs(tabsWidget) {
const tabPanes = tabsWidget.tabs
let newTabPane = deepClone( this.getContainerByType('tab-pane') )
newTabPane.id = 'tab-pane-' + generateId()
newTabPane.options.name = newTabPane.id
newTabPane.options.label = 'tab ' + (tabPanes.length + 1)
tabPanes.push(newTabPane)
},
deleteTabPaneOfTabs(tabsWidget, tpIdx) {
tabsWidget.tabs.splice(tpIdx, 1)
},
emitEvent(evtName, evtData) { //用于兄弟组件发射事件
//this.vueInstance.$emit(evtName, evtData)
eventBus.$emit(evtName, evtData)
},
handleEvent(evtName, callback) { //用于兄弟组件接收事件
//this.vueInstance.$on(evtName, (data) => callback(data))
eventBus.$on(evtName, (data) => callback(data))
},
setCssClassList(cssClassList) {
this.cssClassList = cssClassList
},
getCssClassList() {
return this.cssClassList
},
registerFormWidget(formWidget) {
this.formWidget = formWidget
},
initHistoryData() {
this.loadFormContentFromStorage()
this.historyData.index++
this.historyData.steps[this.historyData.index] = ({
widgetList: deepClone(this.widgetList),
formConfig: deepClone(this.formConfig)
})
},
emitHistoryChange() {
//console.log('------------', 'Form history changed!')
if (this.historyData.index === this.historyData.maxStep - 1) {
this.historyData.steps.shift()
} else {
this.historyData.index++
}
this.historyData.steps[this.historyData.index] = ({
widgetList: deepClone(this.widgetList),
formConfig: deepClone(this.formConfig)
})
this.saveFormContentToStorage()
if (this.historyData.index < this.historyData.steps.length - 1) {
this.historyData.steps = this.historyData.steps.slice(0, this.historyData.index + 1)
}
console.log('history', this.historyData.index)
},
saveCurrentHistoryStep() {
this.historyData.steps[this.historyData.index] = deepClone({
widgetList: this.widgetList,
formConfig: this.formConfig
})
this.saveFormContentToStorage()
},
undoHistoryStep() {
if (this.historyData.index !== 0) {
this.historyData.index--
}
console.log('undo', this.historyData.index)
this.widgetList = deepClone(this.historyData.steps[this.historyData.index].widgetList)
this.formConfig = deepClone(this.historyData.steps[this.historyData.index].formConfig)
},
redoHistoryStep() {
if (this.historyData.index !== (this.historyData.steps.length - 1)) {
this.historyData.index++
}
console.log('redo', this.historyData.index)
this.widgetList = deepClone(this.historyData.steps[this.historyData.index].widgetList)
this.formConfig = deepClone(this.historyData.steps[this.historyData.index].formConfig)
},
undoEnabled() {
return (this.historyData.index > 0) && (this.historyData.steps.length > 0)
},
redoEnabled() {
return this.historyData.index < (this.historyData.steps.length - 1)
},
saveFormContentToStorage() {
window.localStorage.setItem('widget__list__backup', JSON.stringify(this.widgetList))
window.localStorage.setItem('form__config__backup', JSON.stringify(this.formConfig))
},
loadFormContentFromStorage() {
let widgetListBackup = window.localStorage.getItem('widget__list__backup')
if (!!widgetListBackup) {
this.widgetList = JSON.parse(widgetListBackup)
}
let formConfigBackup = window.localStorage.getItem('form__config__backup')
if (!!formConfigBackup) {
//this.formConfig = JSON.parse(formConfigBackup)
overwriteObj(this.formConfig, JSON.parse(formConfigBackup)) /* 用=赋值会导致inject依赖注入的formConfig属性变成非响应式 */
}
},
}
}

View File

@ -0,0 +1,108 @@
<!--
/**
* author: vformAdmin
* email: vdpadmin@163.com
* website: https://www.vform666.com
* date: 2021.08.18
* remark: 如果要分发VForm源码需在本文件顶部保留此文件头信息
*/
-->
<template>
<div class="container-wrapper" :class="[customClass]">
<slot></slot>
<div class="container-action" v-if="designer.selectedId === widget.id && !widget.internal">
<i :title="i18nt('designer.hint.selectParentWidget')" @click.stop="selectParentWidget(widget)">
<svg-icon icon-class="el-back" />
</i>
<i class="el-icon-top" v-if="!!parentList && (parentList.length > 1)" :title="i18nt('designer.hint.moveUpWidget')"
@click.stop="moveUpWidget()"></i>
<i class="el-icon-bottom" v-if="!!parentList && (parentList.length > 1)" :title="i18nt('designer.hint.moveDownWidget')"
@click.stop="moveDownWidget()"></i>
<i v-if="widget.type === 'table'" class="iconfont icon-insertrow" :title="i18nt('designer.hint.insertRow')"
@click.stop="appendTableRow(widget)"></i>
<i v-if="widget.type === 'table'" class="iconfont icon-insertcolumn" :title="i18nt('designer.hint.insertColumn')"
@click.stop="appendTableCol(widget)"></i>
<i class="el-icon-copy-document" v-if="(widget.type === 'grid') || (widget.type === 'table')"
:title="i18nt('designer.hint.cloneWidget')" @click.stop="cloneContainer(widget)"></i>
<i class="el-icon-delete" :title="i18nt('designer.hint.remove')" @click.stop="removeWidget"></i>
</div>
<div class="drag-handler" v-if="designer.selectedId === widget.id && !widget.internal">
<i class="el-icon-rank" :title="i18nt('designer.hint.dragHandler')"></i>
<i>{{i18n2t(`designer.widgetLabel.${widget.type}`, `extension.widgetLabel.${widget.type}`)}}</i>
<i v-if="widget.options.hidden === true" class="iconfont icon-hide"></i>
</div>
</div>
</template>
<script>
import i18n from "@/utils/i18n";
import containerMixin from "@/components/form-designer/form-widget/container-widget/containerMixin";
export default {
name: "container-wrapper",
mixins: [i18n, containerMixin],
props: {
widget: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
},
computed: {
customClass() {
return !!this.widget.options.customClass ? this.widget.options.customClass.join(' ') : ''
},
}
}
</script>
<style lang="scss" scoped>
.container-wrapper {
position: relative;
margin-bottom: 5px;
.container-action{
position: absolute;
//bottom: -30px;
bottom: 0;
right: -2px;
height: 28px;
line-height: 28px;
background: $--color-primary;
z-index: 999;
i {
font-size: 14px;
color: #fff;
margin: 0 5px;
cursor: pointer;
}
}
.drag-handler {
position: absolute;
top: -2px;
//bottom: -24px; /* */
left: -2px;
height: 22px;
line-height: 22px;
background: $--color-primary;
z-index: 9;
i {
font-size: 14px;
font-style: normal;
color: #fff;
margin: 4px;
cursor: move;
}
}
}
</style>

View File

@ -0,0 +1,85 @@
export default {
methods: {
appendTableRow(widget) {
this.designer.appendTableRow(widget)
},
appendTableCol(widget) {
this.designer.appendTableCol(widget)
},
onContainerDragAdd(evt, subList) {
const newIndex = evt.newIndex
if (!!subList[newIndex]) {
this.designer.setSelected( subList[newIndex] )
}
this.designer.emitHistoryChange()
},
onContainerDragUpdate() {
this.designer.emitHistoryChange()
},
checkContainerMove(evt) {
return this.designer.checkWidgetMove(evt)
},
selectWidget(widget) {
this.designer.setSelected(widget)
},
selectParentWidget() {
if (this.parentWidget) {
this.designer.setSelected(this.parentWidget)
} else {
this.designer.clearSelected()
}
},
moveUpWidget() {
this.designer.moveUpWidget(this.parentList, this.indexOfParentList)
this.designer.emitHistoryChange()
},
moveDownWidget() {
this.designer.moveDownWidget(this.parentList, this.indexOfParentList)
this.designer.emitHistoryChange()
},
cloneContainer(widget) {
if (!!this.parentList) {
let newCon = this.designer.cloneContainer(widget)
this.parentList.splice(this.indexOfParentList + 1, 0, newCon)
this.designer.setSelected(newCon)
this.designer.emitHistoryChange()
}
},
removeWidget() {
if (!!this.parentList) {
let nextSelected = null
if (this.parentList.length === 1) {
if (!!this.parentWidget) {
nextSelected = this.parentWidget
}
} else if (this.parentList.length === (1 + this.indexOfParentList)) {
nextSelected = this.parentList[this.indexOfParentList - 1]
} else {
nextSelected = this.parentList[this.indexOfParentList + 1]
}
this.$nextTick(() => {
this.parentList.splice(this.indexOfParentList, 1)
//if (!!nextSelected) {
this.designer.setSelected(nextSelected)
//}
this.designer.emitHistoryChange()
})
}
},
}
}

View File

@ -0,0 +1,311 @@
<template>
<el-col v-if="widget.type === 'grid-col'" class="grid-cell" v-bind="layoutProps"
:class="[selected ? 'selected' : '', customClass]" :style="colHeightStyle"
:key="widget.id" @click.stop="selectWidget(widget)">
<draggable :list="widget.widgetList" item-key="id" v-bind="{group:'dragGroup', ghostClass: 'ghost',animation: 200}"
tag="transition-group" :component-data="{name: 'fade'}"
handle=".drag-handler" @end="(evt) => onGridDragEnd(evt, widget.widgetList)"
@add="(evt) => onGridDragAdd(evt, widget.widgetList)"
@update="onGridDragUpdate" :move="checkContainerMove">
<template #item="{ element: subWidget, index: swIdx }">
<div class="form-widget-list">
<template v-if="'container' === subWidget.category">
<component :is="subWidget.type + '-widget'" :widget="subWidget" :designer="designer" :key="subWidget.id" :parent-list="widget.widgetList"
:index-of-parent-list="swIdx" :parent-widget="widget"></component>
</template>
<template v-else>
<component :is="subWidget.type + '-widget'" :field="subWidget" :designer="designer" :key="subWidget.id" :parent-list="widget.widgetList"
:index-of-parent-list="swIdx" :parent-widget="widget" :design-state="true"></component>
</template>
</div>
</template>
</draggable>
<div class="grid-col-action" v-if="designer.selectedId === widget.id && widget.type === 'grid-col'">
<i class="el-icon-back" :title="i18nt('designer.hint.selectParentWidget')"
@click.stop="selectParentWidget(widget)"></i>
<i class="el-icon-top" v-if="!!parentList && (parentList.length > 1)" :title="i18nt('designer.hint.moveUpWidget')"
@click.stop="moveUpWidget()"></i>
<i class="el-icon-bottom" v-if="!!parentList && (parentList.length > 1)" :title="i18nt('designer.hint.moveDownWidget')"
@click.stop="moveDownWidget()"></i>
<i class="el-icon-copy-document" :title="i18nt('designer.hint.cloneWidget')" @click.stop="cloneGridCol(widget)"></i>
<i class="el-icon-delete" :title="i18nt('designer.hint.remove')" @click.stop="removeWidget"></i>
</div>
<div class="grid-col-handler" v-if="designer.selectedId === widget.id && widget.type === 'grid-col'">
<i>{{i18nt('designer.widgetLabel.' + widget.type)}}</i>
</div>
</el-col>
</template>
<script>
import Draggable from 'vuedraggable'
import i18n from "@/utils/i18n";
import FieldComponents from '@/components/form-designer/form-widget/field-widget/index'
export default {
name: "GridColWidget",
componentName: "GridColWidget",
mixins: [i18n],
components: {
Draggable,
...FieldComponents,
},
props: {
widget: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
colHeight: {
type: String,
default: null
},
},
data() {
return {
layoutProps: {
span: this.widget.options.span || 12,
// md: this.widget.options.md || 12,
// sm: this.widget.options.sm || 12,
// xs: this.widget.options.xs || 12,
offset: this.widget.options.offset || 0,
push: this.widget.options.push || 0,
pull: this.widget.options.pull || 0,
}
}
},
computed: {
selected() {
return this.widget.id === this.designer.selectedId
},
customClass() {
return this.widget.options.customClass || ''
},
colHeightStyle() {
return !!this.colHeight ? {height: this.colHeight + 'px'} : {}
},
},
watch: {
'designer.formConfig.layoutType': {
handler(val) {
if (!!this.widget.options.responsive) {
if (val === 'H5') {
this.layoutProps.span = this.widget.options.xs || 12
} else if (val === 'Pad') {
this.layoutProps.span = this.widget.options.sm || 12
} else {
this.layoutProps.span = this.widget.options.md || 12
}
} else {
this.layoutProps.span = this.widget.options.span || 12
}
}
},
'widget.options.responsive': {
handler(val) {
let lyType = this.designer.formConfig.layoutType
if (!!val) {
if (lyType === 'H5') {
this.layoutProps.span = this.widget.options.xs || 12
} else if (lyType === 'Pad') {
this.layoutProps.span = this.widget.options.sm || 12
} else {
this.layoutProps.span = this.widget.options.md || 12
}
} else {
this.layoutProps.span = this.widget.options.span || 12
}
}
},
'widget.options.span': {
handler(val) {
this.layoutProps.span = val
}
},
'widget.options.md': {
handler(val) {
this.layoutProps.span = val
}
},
'widget.options.sm': {
handler(val) {
this.layoutProps.span = val
}
},
'widget.options.xs': {
handler(val) {
this.layoutProps.span = val
}
},
'widget.options.offset': {
handler(val) {
this.layoutProps.offset = val
}
},
'widget.options.push': {
handler(val) {
this.layoutProps.push = val
}
},
'widget.options.pull': {
handler(val) {
this.layoutProps.pull = val
}
},
},
created() {
this.initLayoutProps()
},
methods: {
initLayoutProps() {
if (!!this.widget.options.responsive) {
let lyType = this.designer.formConfig.layoutType
if (lyType === 'H5') {
this.layoutProps.span = this.widget.options.xs || 12
} else if (lyType === 'Pad') {
this.layoutProps.span = this.widget.options.sm || 12
} else {
this.layoutProps.span = this.widget.options.md || 12
}
} else {
this.layoutProps.spn = this.widget.options.span
}
},
onGridDragEnd(evt, subList) {
//
},
onGridDragAdd(evt, subList) {
const newIndex = evt.newIndex
if (!!subList[newIndex]) {
this.designer.setSelected( subList[newIndex] )
}
this.designer.emitHistoryChange()
},
onGridDragUpdate() {
this.designer.emitHistoryChange()
},
selectWidget(widget) {
console.log('id: ' + widget.id)
this.designer.setSelected(widget)
},
checkContainerMove(evt) {
return this.designer.checkWidgetMove(evt)
},
selectParentWidget() {
if (this.parentWidget) {
this.designer.setSelected(this.parentWidget)
} else {
this.designer.clearSelected()
}
},
moveUpWidget() {
this.designer.moveUpWidget(this.parentList, this.indexOfParentList)
},
moveDownWidget() {
this.designer.moveDownWidget(this.parentList, this.indexOfParentList)
},
cloneGridCol(widget) {
this.designer.cloneGridCol(widget, this.parentWidget)
},
removeWidget() {
if (!!this.parentList) {
let nextSelected = null
if (this.parentList.length === 1) {
if (!!this.parentWidget) {
nextSelected = this.parentWidget
}
} else if (this.parentList.length === (1 + this.indexOfParentList)) {
nextSelected = this.parentList[this.indexOfParentList - 1]
} else {
nextSelected = this.parentList[this.indexOfParentList + 1]
}
this.$nextTick(() => {
this.parentList.splice(this.indexOfParentList, 1)
//if (!!nextSelected) {
this.designer.setSelected(nextSelected)
//}
this.designer.emitHistoryChange()
})
}
},
}
}
</script>
<style lang="scss" scoped>
.grid-cell {
min-height: 38px;
//margin: 6px 0; /* marginoffsetpushpull */
padding: 3px;
outline: 1px dashed #336699;
position: relative;
.form-widget-list {
min-height: 28px;
}
.grid-col-action{
position: absolute;
bottom: 0;
right: -2px;
height: 28px;
line-height: 28px;
background: $--color-primary;
z-index: 999;
i {
font-size: 14px;
color: #fff;
margin: 0 5px;
cursor: pointer;
}
}
.grid-col-handler {
position: absolute;
top: -2px;
left: -2px;
height: 22px;
line-height: 22px;
background: $--color-primary;
z-index: 9;
i {
font-size: 14px;
font-style: normal;
color: #fff;
margin: 4px;
cursor: default;
}
}
}
</style>

View File

@ -0,0 +1,89 @@
<!--
/**
* author: vformAdmin
* email: vdpadmin@163.com
* website: https://www.vform666.com
* date: 2021.08.18
* remark: 如果要分发VForm源码需在本文件顶部保留此文件头信息
*/
-->
<template>
<container-wrapper :designer="designer" :widget="widget" :parent-widget="parentWidget" :parent-list="parentList"
:index-of-parent-list="indexOfParentList">
<el-row :key="widget.id" :gutter="widget.options.gutter" class="grid-container"
:class="[selected ? 'selected' : '', customClass]"
@click.stop="selectWidget(widget)">
<template v-for="(colWidget, colIdx) in widget.cols" :key="colWidget.id">
<grid-col-widget :widget="colWidget" :designer="designer" :parent-list="widget.cols"
:index-of-parent-list="colIdx" :parent-widget="widget"
:col-height="widget.options.colHeight"></grid-col-widget>
</template>
</el-row>
</container-wrapper>
</template>
<script>
import i18n from "@/utils/i18n"
import GridColWidget from "@/components/form-designer/form-widget/container-widget/grid-col-widget"
import containerMixin from "@/components/form-designer/form-widget/container-widget/containerMixin";
import ContainerWrapper from "@/components/form-designer/form-widget/container-widget/container-wrapper";
export default {
name: "grid-widget",
componentName: 'ContainerWidget',
mixins: [i18n, containerMixin],
components: {
ContainerWrapper,
GridColWidget
},
props: {
widget: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
},
computed: {
selected() {
return this.widget.id === this.designer.selectedId
},
customClass() {
return this.widget.options.customClass || ''
},
},
watch: {
//
},
mounted() {
//
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.el-row.grid-container {
min-height: 50px;
//line-height: 48px;
//padding: 6px;
outline: 1px dashed #336699;
.form-widget-list {
min-height: 28px;
}
}
.grid-container.selected, .grid-cell.selected {
outline: 2px solid $--color-primary !important;
}
</style>

View File

@ -0,0 +1,10 @@
const modules = import.meta.globEager('./*.vue')
export default {
install(app) {
for (const path in modules) {
let cname = modules[path].default.name
app.component(cname, modules[path].default)
}
}
}

View File

@ -0,0 +1,119 @@
<!--
/**
* author: vformAdmin
* email: vdpadmin@163.com
* website: https://www.vform666.com
* date: 2021.08.18
* remark: 如果要分发VForm源码需在本文件顶部保留此文件头信息
*/
-->
<template>
<container-wrapper :designer="designer" :widget="widget" :parent-widget="parentWidget" :parent-list="parentList"
:index-of-parent-list="indexOfParentList">
<div :key="widget.id" class="tab-container"
:class="{'selected': selected}" @click.stop="selectWidget(widget)">
<el-tabs :type="widget.displayType" v-model="activeTab" @tab-click="onTabClick">
<el-tab-pane v-for="(tab, index) in widget.tabs" :key="index" :label="tab.options.label" :name="tab.options.name"
@click.stop="selectWidget(widget)">
<draggable :list="tab.widgetList" item-key="id" v-bind="{group:'dragGroup', ghostClass: 'ghost',animation: 200}"
handle=".drag-handler" tag="transition-group" :component-data="{name: 'fade'}"
@add="(evt) => onContainerDragAdd(evt, tab.widgetList)"
@update="onContainerDragUpdate" :move="checkContainerMove">
<template #item="{ element: subWidget, index: swIdx }">
<div class="form-widget-list">
<template v-if="'container' === subWidget.category">
<component :is="subWidget.type + '-widget'" :widget="subWidget" :designer="designer" :key="subWidget.id" :parent-list="tab.widgetList"
:index-of-parent-list="swIdx" :parent-widget="widget"></component>
</template>
<template v-else>
<component :is="subWidget.type + '-widget'" :field="subWidget" :designer="designer" :key="subWidget.id" :parent-list="tab.widgetList"
:index-of-parent-list="swIdx" :parent-widget="widget" :design-state="true"></component>
</template>
</div>
</template>
</draggable>
</el-tab-pane>
</el-tabs>
</div>
</container-wrapper>
</template>
<script>
import Draggable from 'vuedraggable'
import i18n from "@/utils/i18n"
import containerMixin from "@/components/form-designer/form-widget/container-widget/containerMixin"
import ContainerWrapper from "@/components/form-designer/form-widget/container-widget/container-wrapper"
import FieldComponents from '@/components/form-designer/form-widget/field-widget/index'
export default {
name: "tab-widget",
componentName: 'ContainerWidget',
mixins: [i18n, containerMixin],
components: {
ContainerWrapper,
Draggable,
...FieldComponents,
},
props: {
widget: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
},
data() {
return {
activeTab: 'tab1',
//
}
},
computed: {
selected() {
return this.widget.id === this.designer.selectedId
},
customClass() {
return this.widget.options.customClass || ''
},
},
watch: {
//
},
mounted() {
//
},
methods: {
onTabClick(evt) {
console.log('onTabClick', evt)
let paneName = evt.name
this.widget.tabs.forEach((tp) => {
tp.options.active = tp.options.name === paneName;
})
},
}
}
</script>
<style lang="scss" scoped>
.tab-container {
//padding: 5px;
margin: 2px;
.form-widget-list {
min-height: 28px;
}
}
.tab-container.selected {
outline: 2px solid $--color-primary !important;
}
</style>

View File

@ -0,0 +1,344 @@
<template>
<td class="table-cell" :class="[selected ? 'selected' : '', customClass]"
:style="{width: widget.options.cellWidth + '!important' || '', height: widget.options.cellHeight + '!important' || ''}"
:colspan="widget.options.colspan || 1" :rowspan="widget.options.rowspan || 1"
@click.stop="selectWidget(widget)">
<draggable :list="widget.widgetList" item-key="id" class="draggable-div" v-bind="{group:'dragGroup', ghostClass: 'ghost',animation: 200}"
tag="transition-group" :component-data="{name: 'fade'}"
handle=".drag-handler" @end="(evt) => onTableDragEnd(evt, widget.widgetList)"
@add="(evt) => onTableDragAdd(evt, widget.widgetList)"
@update="onTableDragUpdate" :move="checkContainerMove">
<template #item="{ element: subWidget, index: swIdx }">
<div class="form-widget-list">
<template v-if="'container' === subWidget.category">
<component :is="subWidget.type + '-widget'" :widget="subWidget" :designer="designer" :key="subWidget.id" :parent-list="widget.widgetList"
:index-of-parent-list="swIdx" :parent-widget="widget"></component>
</template>
<template v-else>
<component :is="subWidget.type + '-widget'" :field="subWidget" :designer="designer" :key="subWidget.id" :parent-list="widget.widgetList"
:index-of-parent-list="swIdx" :parent-widget="widget" :design-state="true"></component>
</template>
</div>
</template>
</draggable>
<div class="table-cell-action" v-if="designer.selectedId === widget.id && widget.type === 'table-cell'">
<i class="el-icon-back" :title="i18nt('designer.hint.selectParentWidget')"
@click.stop="selectParentWidget()"></i>
<el-dropdown trigger="click" @command="handleTableCellCommand" size="small">
<i class="el-icon-menu" :title="i18nt('designer.hint.cellSetting')"></i>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="insertLeftCol">{{i18nt('designer.setting.insertColumnToLeft')}}</el-dropdown-item>
<el-dropdown-item command="insertRightCol">{{i18nt('designer.setting.insertColumnToRight')}}</el-dropdown-item>
<el-dropdown-item command="insertAboveRow">{{i18nt('designer.setting.insertRowAbove')}}</el-dropdown-item>
<el-dropdown-item command="insertBelowRow">{{i18nt('designer.setting.insertRowBelow')}}</el-dropdown-item>
<el-dropdown-item command="mergeLeftCol" :disabled="mergeLeftColDisabled" divided>{{i18nt('designer.setting.mergeLeftColumn')}}</el-dropdown-item>
<el-dropdown-item command="mergeRightCol" :disabled="mergeRightColDisabled">{{i18nt('designer.setting.mergeRightColumn')}}</el-dropdown-item>
<el-dropdown-item command="mergeWholeRow" :disabled="mergeWholeRowDisabled">{{i18nt('designer.setting.mergeEntireRow')}}</el-dropdown-item>
<el-dropdown-item command="mergeAboveRow" :disabled="mergeAboveRowDisabled" divided>{{i18nt('designer.setting.mergeRowAbove')}}</el-dropdown-item>
<el-dropdown-item command="mergeBelowRow" :disabled="mergeBelowRowDisabled">{{i18nt('designer.setting.mergeRowBelow')}}</el-dropdown-item>
<el-dropdown-item command="mergeWholeCol" :disabled="mergeWholeColDisabled">{{i18nt('designer.setting.mergeEntireColumn')}}</el-dropdown-item>
<el-dropdown-item command="undoMergeRow" :disabled="undoMergeRowDisabled" divided>{{i18nt('designer.setting.undoMergeRow')}}</el-dropdown-item>
<el-dropdown-item command="undoMergeCol" :disabled="undoMergeColDisabled">{{i18nt('designer.setting.undoMergeCol')}}</el-dropdown-item>
<el-dropdown-item command="deleteWholeCol" :disabled="deleteWholeColDisabled" divided>{{i18nt('designer.setting.deleteEntireCol')}}</el-dropdown-item>
<el-dropdown-item command="deleteWholeRow" :disabled="deleteWholeRowDisabled">{{i18nt('designer.setting.deleteEntireRow')}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="table-cell-handler" v-if="designer.selectedId === widget.id && widget.type === 'table-cell'">
<i>{{i18nt('designer.widgetLabel.' + widget.type)}}</i>
</div>
</td>
</template>
<script>
import Draggable from 'vuedraggable'
import i18n from "@/utils/i18n"
import FieldComponents from '@/components/form-designer/form-widget/field-widget/index'
export default {
name: "TableCellWidget",
componentName: "TableCellWidget",
mixins: [i18n],
components: {
Draggable,
...FieldComponents,
},
props: {
widget: Object,
parentWidget: Object,
parentList: Array,
rowIndex: Number,
colIndex: Number,
rowLength: Number,
colLength: Number,
colArray: Array,
rowArray: Array,
designer: Object,
},
computed: {
selected() {
return this.widget.id === this.designer.selectedId
},
customClass() {
return this.widget.options.customClass || ''
},
mergeLeftColDisabled() {
return (this.colIndex <= 0) || (this.colArray[this.colIndex - 1].options.rowspan !== this.widget.options.rowspan)
},
mergeRightColDisabled() {
let rightColIndex = this.colIndex + this.widget.options.colspan
return (this.colIndex >= this.colLength - 1) || (rightColIndex > this.colLength -1)
|| (this.colArray[rightColIndex].options.rowspan !== this.widget.options.rowspan)
},
mergeWholeRowDisabled() {
return (this.colLength <= 1) || (this.colLength === this.widget.options.colspan)
},
mergeAboveRowDisabled() {
return (this.rowIndex <= 0) || (this.rowArray[this.rowIndex - 1].cols[this.colIndex].options.colspan
!== this.widget.options.colspan)
//return this.rowIndex <= 0
//return (this.rowIndex <= 0) || (this.widget.options.colspan !== this.rowArray) //TODO
},
mergeBelowRowDisabled() {
let belowRowIndex = this.rowIndex + this.widget.options.rowspan
return (this.rowIndex >= this.rowLength - 1) || (belowRowIndex > this.rowLength -1)
|| (this.rowArray[belowRowIndex].cols[this.colIndex].options.colspan !== this.widget.options.colspan)
},
mergeWholeColDisabled() {
return (this.rowLength <= 1) || (this.rowLength === this.widget.options.rowspan)
},
undoMergeColDisabled() {
return this.widget.merged || (this.widget.options.colspan <= 1)
},
undoMergeRowDisabled() {
return this.widget.merged || (this.widget.options.rowspan <= 1)
},
deleteWholeColDisabled() {
//return this.colLength === 1
return (this.colLength === 1) || (this.widget.options.colspan === this.colLength)
},
deleteWholeRowDisabled() {
return (this.rowLength === 1) || (this.widget.options.rowspan === this.rowLength)
},
},
watch: {
//
},
methods: {
selectWidget(widget) {
this.designer.setSelected(widget)
},
checkContainerMove(evt) {
return this.designer.checkWidgetMove(evt)
},
onTableDragEnd(obj, subList) {
//
},
onTableDragAdd(evt, subList) { //
const newIndex = evt.newIndex
if (!!subList[newIndex]) {
this.designer.setSelected( subList[newIndex] )
}
this.designer.emitHistoryChange()
},
onTableDragUpdate() {
this.designer.emitHistoryChange()
},
selectParentWidget() {
if (this.parentWidget) {
this.designer.setSelected(this.parentWidget)
} else {
this.designer.clearSelected()
}
},
handleTableCellCommand(command) {
if (command === 'insertLeftCol') {
this.insertLeftCol()
} else if (command === 'insertRightCol') {
this.insertRightCol()
} else if (command === 'insertAboveRow') {
this.insertAboveRow()
} else if (command === 'insertBelowRow') {
this.insertBelowRow()
} else if (command === 'mergeLeftCol') {
this.mergeLeftCol()
} else if (command === 'mergeRightCol') {
this.mergeRightCol()
} else if (command === 'mergeWholeCol') {
this.mergeWholeCol()
} else if (command === 'mergeAboveRow') {
this.mergeAboveRow()
} else if (command === 'mergeBelowRow') {
this.mergeBelowRow()
} else if (command === 'mergeWholeRow') {
this.mergeWholeRow()
} else if (command === 'undoMergeCol') {
this.undoMergeCol()
} else if (command === 'undoMergeRow') {
this.undoMergeRow()
} else if (command === 'deleteWholeCol') {
this.deleteWholeCol()
} else if (command === 'deleteWholeRow') {
this.deleteWholeRow()
}
},
insertLeftCol() {
this.designer.insertTableCol(this.parentWidget, this.colIndex, this.rowIndex, true)
},
insertRightCol() {
this.designer.insertTableCol(this.parentWidget, this.colIndex, this.rowIndex, false)
},
insertAboveRow() {
this.designer.insertTableRow(this.parentWidget, this.rowIndex, this.rowIndex, this.colIndex, true)
},
insertBelowRow() {
this.designer.insertTableRow(this.parentWidget, this.rowIndex, this.rowIndex, this.colIndex, false)
},
mergeLeftCol() {
//this.designer.mergeTableColumn(this.colArray, this.colIndex, true)
this.designer.mergeTableCol(this.rowArray, this.colArray, this.rowIndex, this.colIndex, true, this.widget)
},
mergeRightCol() {
//this.designer.mergeTableColumn(this.colArray, this.colIndex, false)
this.designer.mergeTableCol(this.rowArray, this.colArray, this.rowIndex, this.colIndex, false, this.widget)
},
mergeWholeRow() {
this.designer.mergeTableWholeRow(this.rowArray, this.colArray, this.rowIndex, this.colIndex)
},
mergeAboveRow() {
this.designer.mergeTableRow(this.rowArray, this.rowIndex, this.colIndex, true, this.widget)
},
mergeBelowRow() {
this.designer.mergeTableRow(this.rowArray, this.rowIndex, this.colIndex, false, this.widget)
},
mergeWholeCol() {
this.designer.mergeTableWholeCol(this.rowArray, this.colArray, this.rowIndex, this.colIndex)
},
undoMergeCol() {
this.designer.undoMergeTableCol(this.rowArray, this.rowIndex, this.colIndex,
this.widget.options.colspan, this.widget.options.rowspan)
},
undoMergeRow() {
this.designer.undoMergeTableRow(this.rowArray, this.rowIndex, this.colIndex,
this.widget.options.colspan, this.widget.options.rowspan)
},
deleteWholeCol() {
this.designer.deleteTableWholeCol(this.rowArray, this.colIndex)
},
deleteWholeRow() {
this.designer.deleteTableWholeRow(this.rowArray, this.rowIndex)
},
}
}
</script>
<style lang="scss" scoped>
.table-cell {
//padding: 3px;
border: 1px dashed #336699;
display: table-cell;
position: relative;
.draggable-div {
position: relative;
height: 100%;
}
.form-widget-list {
border: 1px dashed #336699;
margin: 3px;
//min-height: 36px;
height: 100%;
/*position: absolute;*/
/*top: 0;*/
/*right: 0;*/
/*bottom: 0;*/
/*left: 0;*/
}
.table-cell-action{
position: absolute;
//bottom: -30px;
bottom: 0;
right: -2px;
height: 28px;
line-height: 28px;
background: $--color-primary;
z-index: 999;
i {
font-size: 14px;
color: #fff;
margin: 0 5px;
cursor: pointer;
}
}
.table-cell-handler {
position: absolute;
top: -2px;
//bottom: -24px; /* */
left: -2px;
height: 22px;
line-height: 22px;
background: $--color-primary;
z-index: 9;
i {
font-size: 14px;
font-style: normal;
color: #fff;
margin: 4px;
cursor: default; //cursor: move;
}
}
}
.table-cell.selected {
outline: 2px solid $--color-primary !important;
}
</style>

View File

@ -0,0 +1,111 @@
<!--
/**
* author: vformAdmin
* email: vdpadmin@163.com
* website: https://www.vform666.com
* date: 2021.08.18
* remark: 如果要分发VForm源码需在本文件顶部保留此文件头信息
*/
-->
<template>
<container-wrapper :designer="designer" :widget="widget" :parent-widget="parentWidget" :parent-list="parentList"
:index-of-parent-list="indexOfParentList">
<div :key="widget.id" class="table-container"
:class="[selected ? 'selected' : '', customClass]" @click.stop="selectWidget(widget)">
<table class="table-layout">
<tbody>
<tr v-for="(row, rowIdx) in widget.rows" :key="row.id">
<template v-for="(colWidget, colIdx) in row.cols">
<table-cell-widget v-if="!colWidget.merged" :widget="colWidget" :designer="designer"
:key="colWidget.id" :parent-list="widget.cols"
:row-index="rowIdx" :row-length="widget.rows.length"
:col-index="colIdx" :col-length="row.cols.length"
:col-array="row.cols" :row-array="widget.rows"
:parent-widget="widget"></table-cell-widget>
</template>
</tr>
</tbody>
</table>
</div>
</container-wrapper>
</template>
<script>
import i18n from "@/utils/i18n"
import containerMixin from "@/components/form-designer/form-widget/container-widget/containerMixin"
import ContainerWrapper from "@/components/form-designer/form-widget/container-widget/container-wrapper"
import TableCellWidget from "@/components/form-designer/form-widget/container-widget/table-cell-widget"
export default {
name: "table-widget",
componentName: 'ContainerWidget',
mixins: [i18n, containerMixin],
components: {
ContainerWrapper,
TableCellWidget,
},
props: {
widget: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
},
computed: {
selected() {
return this.widget.id === this.designer.selectedId
},
customClass() {
return this.widget.options.customClass || ''
},
},
watch: {
//
},
mounted() {
//
},
methods: {
}
}
</script>
<style lang="scss" scoped>
div.table-container {
padding: 5px;
border: 1px dashed #336699;
box-sizing: border-box;
table.table-layout {
width: 100%;
text-align: center;
//border: 1px solid #c8ebfb;
border-collapse: collapse;
table-layout: fixed;
:deep(td) {
height: 48px;
border: 1px dashed #336699;
padding: 3px;
display: table-cell;
}
.form-widget-list {
border: 1px dashed #336699;
min-height: 36px;
}
}
}
.table-container.selected {
outline: 2px solid $--color-primary !important;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<static-content-wrapper :designer="designer" :field="field" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-button ref="fieldEditor" :type="field.options.type" :size="field.options.size"
:plain="field.options.plain" :round="field.options.round"
:circle="field.options.circle" :icon="field.options.icon"
:disabled="field.options.disabled"
@click="handleButtonWidgetClick">
{{field.options.label}}</el-button>
</static-content-wrapper>
</template>
<script>
import StaticContentWrapper from './static-content-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "button-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
StaticContentWrapper,
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.registerToRefList()
this.initEventHandler()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; //* static-content-wrapper *//
</style>

View File

@ -0,0 +1,104 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-cascader ref="fieldEditor" :options="field.options.optionItems" v-model="fieldModel" class="full-width-input"
:disabled="field.options.disabled"
:size="field.options.size"
:clearable="field.options.clearable"
:filterable="field.options.filterable"
:placeholder="field.options.placeholder || i18nt('render.hint.selectPlaceholder')"
@focus="handleFocusCustomEvent" @blur="handleBlurCustomEvent"
@change="handleChangeEvent">
</el-cascader>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "cascader-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initOptionItems()
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-checkbox-group ref="fieldEditor" v-model="fieldModel"
:disabled="field.options.disabled" :size="field.options.size"
@change="handleChangeEvent">
<template v-if="!!field.options.buttonStyle">
<el-checkbox-button v-for="(item, index) in field.options.optionItems" :key="index" :label="item.value"
:disabled="item.disabled" :border="field.options.border"
:style="{display: field.options.displayStyle}">{{item.label}}</el-checkbox-button>
</template>
<template v-else>
<el-checkbox v-for="(item, index) in field.options.optionItems" :key="index" :label="item.value"
:disabled="item.disabled" :border="field.options.border"
:style="{display: field.options.displayStyle}">{{item.label}}</el-checkbox>
</template>
</el-checkbox-group>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "checkbox-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initOptionItems()
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
</style>

View File

@ -0,0 +1,99 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-color-picker ref="fieldEditor" v-model="fieldModel"
:size="field.options.size"
:disabled="field.options.disabled"
@change="handleChangeEvent">
</el-color-picker>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "color-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-date-picker ref="fieldEditor" :type="field.options.type" v-model="fieldModel" class="full-width-input"
:disabled="field.options.disabled" :readonly="field.options.readonly"
:size="field.options.size"
:clearable="field.options.clearable" :editable="field.options.editable"
:format="field.options.format" :value-format="field.options.valueFormat"
:start-placeholder="field.options.startPlaceholder || i18nt('render.hint.startDatePlaceholder')"
:end-placeholder="field.options.endPlaceholder || i18nt('render.hint.endDatePlaceholder')"
@focus="handleFocusCustomEvent" @blur="handleBlurCustomEvent"
@change="handleChangeEvent">
</el-date-picker>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "date-range-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-date-picker ref="fieldEditor" :type="field.options.type" v-model="fieldModel" class="full-width-input"
:readonly="field.options.readonly" :disabled="field.options.disabled"
:size="field.options.size"
:clearable="field.options.clearable" :editable="field.options.editable"
:format="field.options.format" :value-format="field.options.valueFormat"
:placeholder="field.options.placeholder || i18nt('render.hint.datePlaceholder')"
@focus="handleFocusCustomEvent" @blur="handleBlurCustomEvent"
@change="handleChangeEvent">
</el-date-picker>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "date-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<static-content-wrapper :designer="designer" :field="field" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-divider ref="fieldEditor" direction="horizontal" :content-position="field.options.contentPosition">
{{field.options.label}}</el-divider>
</static-content-wrapper>
</template>
<script>
import StaticContentWrapper from './static-content-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "divider-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
StaticContentWrapper,
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.registerToRefList()
this.initEventHandler()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; //* static-content-wrapper *//
</style>

View File

@ -0,0 +1,546 @@
import {deepClone} from "@/utils/util"
import FormValidators from '@/utils/validators'
import eventBus from "@/utils/event-bus"
export default {
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel', 'getOptionData'],
computed: {
subFormName() {
return !!this.parentWidget ? this.parentWidget.options.name : ''
},
subFormItemFlag() {
return !!this.parentWidget ? this.parentWidget.type === 'sub-form' : false
},
formModel: {
cache: false,
get() {
return this.globalModel.formModel
}
},
},
methods: {
//--------------------- 组件内部方法 begin ------------------//
initFieldModel() {
if (!this.field.formItemFlag) {
return
}
if (!!this.subFormItemFlag && !this.designState) { //SubForm子表单组件需要特殊处理
let subFormData = this.formModel[this.subFormName]
if (((subFormData === undefined) || (subFormData[this.subFormRowIndex] === undefined) ||
(subFormData[this.subFormRowIndex][this.field.options.name] === undefined)) &&
(this.field.options.defaultValue !== undefined)) {
this.fieldModel = this.field.options.defaultValue
subFormData[this.subFormRowIndex][this.field.options.name] = this.field.options.defaultValue
} else if (subFormData[this.subFormRowIndex][this.field.options.name] === undefined) {
this.fieldModel = null
subFormData[this.subFormRowIndex][this.field.options.name] = null
} else {
this.fieldModel = subFormData[this.subFormRowIndex][this.field.options.name]
}
/* 主动触发子表单内field-widget的onChange事件 */
setTimeout(() => { //延时触发onChange事件, 便于更新计算字段!!
this.handleOnChangeForSubForm(this.fieldModel, this.oldFieldValue, subFormData, this.subFormRowId)
}, 800)
this.oldFieldValue = deepClone(this.fieldModel)
this.initFileList() //处理图片上传、文件上传字段
return
}
if ((this.formModel[this.field.options.name] === undefined) &&
(this.field.options.defaultValue !== undefined)) {
this.fieldModel = this.field.options.defaultValue
} else if (this.formModel[this.field.options.name] === undefined) { //如果formModel为空对象则初始化字段值为null!!
this.formModel[this.field.options.name] = null
} else {
this.fieldModel = this.formModel[this.field.options.name]
}
this.oldFieldValue = deepClone(this.fieldModel)
this.initFileList() //处理图片上传、文件上传字段
},
initFileList() { //初始化上传组件的已上传文件列表
if ( ((this.field.type !== 'picture-upload') && (this.field.type !== 'file-upload')) || (this.designState === true) ) {
return
}
if (!!this.fieldModel) {
if (Array.isArray(this.fieldModel)) {
this.fileList = deepClone(this.fieldModel)
} else {
this.fileList.splice(0, 0, deepClone(this.fieldModel))
}
}
},
initEventHandler() {
eventBus.$on('setFormData', function (newFormData) {
console.log('formModel of globalModel----------', this.globalModel.formModel)
if (!this.subFormItemFlag) {
this.setValue(newFormData[this.field.options.name])
}
})
eventBus.$on('field-value-changed', function (values) {
if (!!this.subFormItemFlag) {
let subFormData = this.formModel[this.subFormName]
this.handleOnChangeForSubForm(values[0], values[1], subFormData, this.subFormRowId)
} else {
this.handleOnChange(values[0], values[1])
}
})
/* 监听重新加载选项事件 */
eventBus.$on('reloadOptionItems', function (widgetNames) {
if ((widgetNames.length === 0) || (widgetNames.indexOf(this.field.options.name) > -1)) {
this.initOptionItems(true)
}
})
},
handleOnCreated() {
if (!!this.field.options.onCreated) {
let customFunc = new Function(this.field.options.onCreated)
customFunc.call(this)
}
},
handleOnMounted() {
if (!!this.field.options.onMounted) {
let mountFunc = new Function(this.field.options.onMounted)
mountFunc.call(this)
}
},
registerToRefList(oldRefName) {
if ((this.refList !== null) && !!this.field.options.name) {
if (this.subFormItemFlag && !this.designState) { //处理子表单元素(且非设计状态)
if (!!oldRefName) {
delete this.refList[oldRefName + '@row' + this.subFormRowId]
}
this.refList[this.field.options.name + '@row' + this.subFormRowId] = this
} else {
if (!!oldRefName) {
delete this.refList[oldRefName]
}
this.refList[this.field.options.name] = this
}
}
},
unregisterFromRefList() { //销毁组件时注销组件ref
if ((this.refList !== null) && !!this.field.options.name) {
let oldRefName = this.field.options.name
if (this.subFormItemFlag && !this.designState) { //处理子表单元素(且非设计状态)
delete this.refList[oldRefName + '@row' + this.subFormRowId]
} else {
delete this.refList[oldRefName]
}
}
},
initOptionItems(keepSelected) {
if (this.designState) {
return
}
if ((this.field.type === 'radio') || (this.field.type === 'checkbox')
|| (this.field.type === 'select') || (this.field.type === 'cascader')) {
if (!!this.globalOptionData && this.globalOptionData.hasOwnProperty(this.field.options.name)) {
if (!!keepSelected) {
//this.reloadOptions(this.globalOptionData[this.field.options.name]) /* 异步更新option-data之后不能获取到最新值
// 以下改用provide的getOptionData()方法 */
const newOptionItems = this.getOptionData()
this.reloadOptions(newOptionItems[this.field.options.name])
} else {
this.loadOptions( this.globalOptionData[this.field.options.name] )
}
}
}
},
refreshDefaultValue() {
if ((this.designState === true) && (this.field.options.defaultValue !== undefined)) {
this.fieldModel = this.field.options.defaultValue
}
},
clearFieldRules() {
if (!this.field.formItemFlag) {
return
}
this.rules.splice(0, this.rules.length) //清空已有
},
buildFieldRules() {
if (!this.field.formItemFlag) {
return
}
this.rules.splice(0, this.rules.length) //清空已有
if (!!this.field.options.required) {
this.rules.push({
required: true,
//trigger: ['blur', 'change'],
trigger: ['blur'], /* 去掉change事件触发校验change事件触发时formModel数据尚未更新导致radio/checkbox必填校验出错 */
message: this.i18nt('render.hint.fieldRequired'),
})
}
if (!!this.field.options.validation) {
let vldName = this.field.options.validation
if (!!FormValidators[vldName]) {
this.rules.push({
validator: FormValidators[vldName],
trigger: ['blur', 'change'],
label: this.field.options.label,
errorMsg: this.field.options.validationHint
})
} else {
this.rules.push({
validator: FormValidators['regExp'],
trigger: ['blur', 'change'],
regExp: vldName,
label: this.field.options.label,
errorMsg: this.field.options.validationHint
})
}
}
if (!!this.field.options.onValidate) {
let customFn = new Function('rule', 'value', 'callback', this.field.options.onValidate)
this.rules.push({
validator: customFn,
trigger: ['blur', 'change'],
label: this.field.options.label
})
}
},
/**
* 禁用字段值变动触发表单校验
*/
disableChangeValidate() {
if (!this.rules) {
return
}
this.rules.forEach(rule => {
if (!!rule.trigger) {
rule.trigger.splice(0, rule.trigger.length)
}
})
},
/**
* 启用字段值变动触发表单校验
*/
enableChangeValidate() {
if (!this.rules) {
return
}
this.rules.forEach(rule => {
if (!!rule.trigger) {
rule.trigger.push('blur')
rule.trigger.push('change')
}
})
},
disableOptionOfList(optionList, optionValue) {
if (!!optionList && (optionList.length > 0)) {
optionList.forEach(opt => {
if (opt.value === optionValue) {
opt.disabled = true
}
})
}
},
enableOptionOfList(optionList, optionValue) {
if (!!optionList && (optionList.length > 0)) {
optionList.forEach(opt => {
if (opt.value === optionValue) {
opt.disabled = false
}
})
}
},
//--------------------- 组件内部方法 end ------------------//
//--------------------- 事件处理 begin ------------------//
emitFieldDataChange(newValue, oldValue) {
this.$emit('field-value-changed', [newValue, oldValue])
/* 必须用dispatch向指定父组件派发消息 */
this.dispatch('VFormRender', 'fieldChange',
[this.field.options.name, newValue, oldValue, this.subFormName, this.subFormRowIndex])
},
syncUpdateFormModel(value) {
if (!!this.designState) {
return
}
if (!!this.subFormItemFlag) {
let subFormData = this.formModel[this.subFormName] || [{}]
let subFormDataRow = subFormData[this.subFormRowIndex]
subFormDataRow[this.field.options.name] = value
} else {
this.formModel[this.field.options.name] = value
}
},
handleChangeEvent(value) {
this.syncUpdateFormModel(value)
this.emitFieldDataChange(value, this.oldFieldValue)
//number组件一般不会触发focus事件故此处需要手工赋值oldFieldValue
this.oldFieldValue = deepClone(value) /* oldFieldValue需要在initFieldModel()方法中赋初值!! */
/* 主动触发表单的单个字段校验,用于清除字段可能存在的校验错误提示 */
this.dispatch('VFormRender', 'fieldValidation', [this.field.options.name])
},
handleFocusCustomEvent(event) {
this.oldFieldValue = deepClone(this.fieldModel) //保存修改change之前的值
if (!!this.field.options.onFocus) {
let customFn = new Function('event', this.field.options.onFocus)
customFn.call(this, event)
}
},
handleBlurCustomEvent(event) {
if (!!this.field.options.onBlur) {
let customFn = new Function('event', this.field.options.onBlur)
customFn.call(this, event)
}
},
handleInputCustomEvent(value) {
this.syncUpdateFormModel(value)
if (!!this.field.options.onInput) {
let customFn = new Function('value', this.field.options.onInput)
customFn.call(this, value)
}
},
emitAppendButtonClick() {
/* 必须调用mixins中的dispatch方法逐级向父组件发送消息 */
this.dispatch('VFormRender', 'appendButtonClick', [this]);
},
handleOnChange(val, oldVal) { //自定义onChange事件
if (!!this.field.options.onChange) {
let changeFn = new Function('value', 'oldValue', this.field.options.onChange)
changeFn.call(this, val, oldVal)
}
},
handleOnChangeForSubForm(val, oldVal, subFormData, rowId) { //子表单自定义onChange事件
if (!!this.field.options.onChange) {
let changeFn = new Function('value', 'oldValue', 'subFormData', 'rowId', this.field.options.onChange)
changeFn.call(this, val, oldVal, subFormData, rowId)
}
},
handleButtonWidgetClick() {
if (!!this.designState) { //设计状态不触发点击事件
return
}
if (!!this.field.options.onClick) {
let changeFn = new Function(this.field.options.onClick)
changeFn.call(this)
} else {
this.dispatch('VFormRender', 'buttonClick', [this]);
}
},
remoteQuery(keyword) {
if (!!this.field.options.onRemoteQuery) {
let remoteFn = new Function('keyword', this.field.options.onRemoteQuery)
remoteFn.call(this, keyword)
}
},
//--------------------- 事件处理 end ------------------//
//--------------------- 以下为组件支持外部调用的API方法 begin ------------------//
/* 提示:用户可自行扩充这些方法!!! */
getFormRef() { /* 获取VFrom引用必须在VForm组件created之后方可调用 */
return this.refList['v_form_ref']
},
getWidgetRef(widgetName, showError) {
let foundRef = this.refList[widgetName]
if (!foundRef && !!showError) {
this.$message.error(this.i18nt('render.hint.refNotFound') + widgetName)
}
return foundRef
},
getFieldEditor() { //获取内置的el表单组件
return this.$refs['fieldEditor']
},
/*
注意VFormRender的setFormData方法不会触发子表单内field-widget的setValue方法
因为setFormData方法调用后子表单内所有field-widget组件已被清空接收不到setFormData事件
* */
setValue(newValue) {
/* if ((this.field.type === 'picture-upload') || (this.field.type === 'file-upload')) {
this.fileList = newValue
} else */ if (!!this.field.formItemFlag) {
let oldValue = deepClone(this.fieldModel)
this.fieldModel = newValue
this.initFileList()
this.syncUpdateFormModel(newValue)
this.emitFieldDataChange(newValue, oldValue)
}
},
getValue() {
/* if ((this.field.type === 'picture-upload') || (this.field.type === 'file-upload')) {
return this.fileList
} else */ {
return this.fieldModel
}
},
resetField() {
let defaultValue = this.field.options.defaultValue
this.setValue(defaultValue)
this.$nextTick(() => {
//
})
//清空上传组件文件列表
if ((this.field.type === 'picture-upload') || (this.field.type === 'file-upload')) {
this.$refs['fieldEditor'].clearFiles()
this.fileList.splice(0, this.fileList.length)
}
},
setWidgetOption(optionName, optionValue) { //通用组件选项修改API
if (this.field.options.hasOwnProperty(optionName)) {
this.field.options[optionName] = optionValue
//TODO: 是否重新构建组件??有些属性修改后必须重新构建组件才能生效,比如字段校验规则。
}
},
setReadonly(flag) {
this.field.options.readonly = flag
},
setDisabled(flag) {
this.field.options.disabled = flag
},
setAppendButtonVisible(flag) {
this.field.options.appendButton = flag
},
setAppendButtonDisabled(flag) {
this.field.options.appendButtonDisabled = flag
},
setHidden(flag) {
this.field.options.hidden = flag
if (!!flag) { //清除组件校验规则
this.clearFieldRules()
} else { //重建组件校验规则
this.buildFieldRules()
}
},
setRequired(flag) {
this.field.options.required = flag
this.buildFieldRules()
},
setLabel(newLabel) {
this.field.options.label = newLabel
},
focus() {
if (!!this.getFieldEditor() && !!this.getFieldEditor().focus) {
this.getFieldEditor().focus()
}
},
clearSelectedOptions() { //清空已选选项
if ((this.field.type !== 'checkbox') && (this.field.type !== 'radio') && (this.field.type !== 'select')) {
return
}
if ((this.field.type === 'checkbox') ||
((this.field.type === 'select') && this.field.options.multiple)) {
this.fieldModel = []
} else {
this.fieldModel = ''
}
},
/**
* 加载选项并清空字段值
* @param options
*/
loadOptions(options) {
this.field.options.optionItems = deepClone(options)
//this.clearSelectedOptions() //清空已选选项
},
/**
* 重新加载选项不清空字段值
* @param options
*/
reloadOptions(options) {
this.field.options.optionItems = deepClone(options)
},
disableOption(optionValue) {
this.disableOptionOfList(this.field.options.optionItems, optionValue)
},
enableOption(optionValue) {
this.enableOptionOfList(this.field.options.optionItems, optionValue)
},
setUploadHeader(name, value) {
//this.$set(this.uploadHeaders, name, value)
this.uploadHeaders[name] = value
},
setUploadData(name, value) {
//this.$set(this.uploadData, name, value)
this.uploadData[name] = value
},
setToolbar(customToolbar) {
this.customToolbar = customToolbar
},
//--------------------- 以上为组件支持外部调用的API方法 end ------------------//
}
}

View File

@ -0,0 +1,278 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<!-- el-upload增加:name="field.options.name"会导致又拍云上传失败故删除之 -->
<el-upload ref="fieldEditor" :disabled="field.options.disabled"
:style="styleVariables" class="dynamicPseudoAfter"
:action="field.options.uploadURL" :headers="uploadHeaders" :data="uploadData"
:with-credentials="field.options.withCredentials"
:multiple="field.options.multipleSelect" :file-list="fileList"
:show-file-list="field.options.showFileList" :class="{'hideUploadDiv': uploadBtnHidden}"
:limit="field.options.limit" :on-exceed="handleFileExceed" :before-upload="beforeFileUpload"
:on-success="handleFileUpload" :on-error="handelUploadError" :on-remove="handleFileRemove">
<template #tip>
<div class="el-upload__tip"
v-if="!!field.options.uploadTip">{{field.options.uploadTip}}</div>
</template>
<template #default>
<i class="el-icon-plus avatar-uploader-icon"></i>
</template>
<template #file="{ file }">
<div class="upload-file-list">
<span class="upload-file-name" :title="file.name">{{file.name}}</span>
<a :href="file.url" download="">
<i class="el-icon-download file-action" title="i18nt('render.hint.downloadFile')"></i></a>
<i class="el-icon-delete file-action" title="i18nt('render.hint.removeFile')" v-if="!field.options.disabled"
@click="removeUploadFile(file.name)"></i>
</div>
</template>
</el-upload>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import {deepClone} from "@/utils/util";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
let selectFileText = "'" + translate('render.hint.selectFile') + "'"
export default {
name: "file-upload-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
uploadHeaders: {},
uploadData: {
key: '', //
//token: '', //token
//policy: '', //policy
//authorization: '', //
},
fileList: [], //
uploadBtnHidden: false,
styleVariables: {
'--select-file-action': selectFileText,
},
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
handleFileExceed() {
let uploadLimit = this.field.options.limit /* 此行不能注释下一行ES6模板字符串需要用到 */
this.$message.warning(eval('`' + this.i18nt('render.hint.uploadExceed') + '`'));
},
updateUploadFieldModelAndEmitDataChange(fileList) {
let oldValue = deepClone(this.fieldModel)
this.fieldModel = deepClone(fileList)
this.syncUpdateFormModel(this.fieldModel)
this.emitFieldDataChange(this.fieldModel, oldValue)
},
beforeFileUpload(file) {
let fileTypeCheckResult = false
let extFileName = file.name.substring(file.name.lastIndexOf('.') + 1)
if (!!this.field.options && !!this.field.options.fileTypes) {
let uploadFileTypes = this.field.options.fileTypes
if (uploadFileTypes.length > 0) {
fileTypeCheckResult = uploadFileTypes.some( (ft) => {
return extFileName.toLowerCase() === ft.toLowerCase()
})
}
}
if (!fileTypeCheckResult) {
this.$message.error(this.i18nt('render.hint.unsupportedFileType') + extFileName)
return false;
}
let fileSizeCheckResult = false
let uploadFileMaxSize = 5 //5MB
if (!!this.field.options && !!this.field.options.fileMaxSize) {
uploadFileMaxSize = this.field.options.fileMaxSize
}
fileSizeCheckResult = file.size / 1024 / 1024 <= uploadFileMaxSize
if (!fileSizeCheckResult) {
this.$message.error(this.i18nt('render.hint.fileSizeExceed') + uploadFileMaxSize + 'MB')
return false;
}
this.uploadData.key = file.name
return this.handleOnBeforeUpload(file)
},
handleOnBeforeUpload(file) {
if (!!this.field.options.onBeforeUpload) {
let bfFunc = new Function('file', this.field.options.onBeforeUpload)
let result = bfFunc.call(this, file)
if (typeof result === 'boolean') {
return result
} else {
return true
}
}
return true
},
handleFileUpload(res, file, fileList) {
if (!!this.field.options.onUploadSuccess) {
let mountFunc = new Function('result', 'file', 'fileList', this.field.options.onUploadSuccess)
mountFunc.call(this, res, file, fileList)
} else {
if (file.status === 'success') {
//this.fileList.push(file) /* this.fileList!! */
this.updateUploadFieldModelAndEmitDataChange(fileList)
this.fileList = deepClone(fileList)
this.uploadBtnHidden = fileList.length >= this.field.options.limit
}
}
},
handleFileRemove(file, fileList) {
this.fileList = deepClone(fileList) //this.fileList = fileList
this.updateUploadFieldModelAndEmitDataChange(fileList)
this.uploadBtnHidden = fileList.length >= this.field.options.limit
},
removeUploadFile(fileName) {
let foundIdx = -1
this.fileList.forEach((file,idx) => {
if (file.name === fileName) {
foundIdx = idx
}
})
if (foundIdx >= 0) {
this.fileList.splice(foundIdx, 1)
this.updateUploadFieldModelAndEmitDataChange(this.fileList)
this.uploadBtnHidden = this.fileList.length >= this.field.options.limit
}
},
handelUploadError(err, file, fileList) {
if (!!this.field.options.onUploadError) {
let customFn = new Function('error', 'file', 'fileList', this.field.options.onUploadError)
customFn.call(this, err, file, fileList)
} else {
this.$message({
message: this.i18nt('render.hint.uploadError') + err,
duration: 3000,
type: 'error',
})
}
},
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
.dynamicPseudoAfter :deep(.el-upload.el-upload--text) {
color: $--color-primary;
font-size: 12px;
.el-icon-plus:after {
content: var(--select-file-action);
}
}
.hideUploadDiv {
:deep(div.el-upload--picture-card) { /* 隐藏最后的图片上传按钮 */
display: none;
}
:deep(div.el-upload--text) { /* 隐藏最后的文件上传按钮 */
display: none;
}
:deep(div.el-upload__tip) { /* 隐藏最后的文件上传按钮 */
display: none;
}
}
.upload-file-list {
font-size: 12px;
.file-action {
color: $--color-primary;
margin-left: 5px;
margin-right: 5px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,333 @@
<!--
/**
* author: vformAdmin
* email: vdpadmin@163.com
* website: https://www.vform666.com
* date: 2021.08.18
* remark: 如果要分发VForm源码需在本文件顶部保留此文件头信息
*/
-->
<template>
<div class="field-wrapper" :class="{'design-time-bottom-margin': !!this.designer}">
<el-form-item v-if="!!field.formItemFlag" :label="label" :label-width="labelWidth + 'px'"
:title="field.options.labelTooltip"
v-show="!field.options.hidden || (designState === true)"
:rules="rules" :prop="getPropName()"
:class="[selected ? 'selected' : '', labelAlign, customClass, field.options.required ? 'required' : '']"
@click.stop="selectField(field)">
<template #label>
<span v-if="!!field.options.labelIconClass" class="custom-label">
<template v-if="field.options.labelIconPosition === 'front'">
<template v-if="!!field.options.labelTooltip">
<el-tooltip :content="field.options.labelTooltip" effect="light">
<i :class="field.options.labelIconClass"></i></el-tooltip>{{label}}</template>
<template v-else>
<i :class="field.options.labelIconClass"></i>{{label}}</template>
</template>
<template v-else-if="field.options.labelIconPosition === 'rear'">
<template v-if="!!field.options.labelTooltip">
{{label}}<el-tooltip :content="field.options.labelTooltip" effect="light">
<i :class="field.options.labelIconClass"></i></el-tooltip></template>
<template v-else>
{{label}}<i :class="field.options.labelIconClass"></i></template>
</template>
</span>
</template>
<slot></slot>
</el-form-item>
<template v-if="!!this.designer">
<div class="field-action" v-if="designer.selectedId === field.id">
<i class="el-icon-back" :title="i18nt('designer.hint.selectParentWidget')" @click.stop="selectParentWidget(field)"></i>
<i class="el-icon-top" v-if="!!parentList && (parentList.length > 1)" :title="i18nt('designer.hint.moveUpWidget')"
@click.stop="moveUpWidget(field)"></i>
<i class="el-icon-bottom" v-if="!!parentList && (parentList.length > 1)" :title="i18nt('designer.hint.moveDownWidget')"
@click.stop="moveDownWidget(field)"></i>
<i class="el-icon-delete" :title="i18nt('designer.hint.remove')" @click.stop="removeFieldWidget"></i>
</div>
<div class="drag-handler background-opacity" v-if="designer.selectedId === field.id">
<i class="el-icon-rank" :title="i18nt('designer.hint.dragHandler')"></i>
<i>{{i18n2t(`designer.widgetLabel.${field.type}`, `extension.widgetLabel.${field.type}`)}}</i>
<i v-if="field.options.hidden === true" class="iconfont icon-hide"></i>
</div>
</template>
</div>
</template>
<script>
import i18n from "@/utils/i18n";
export default {
name: "form-item-wrapper",
mixins: [i18n],
props: {
field: Object,
designer: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
rules: Array,
},
inject: ['formConfig'],
computed: {
selected() {
return !!this.designer && this.field.id === this.designer.selectedId
},
label() {
if (!!this.field.options.labelHidden) {
return ''
}
return this.field.options.label
},
labelWidth() {
if (!!this.field.options.labelHidden) {
return 0
}
if (!!this.field.options.labelWidth) {
return this.field.options.labelWidth
}
if (!!this.designer) {
return this.designer.formConfig.labelWidth
} else {
return this.formConfig.labelWidth
}
},
labelAlign() {
if (!!this.field.options.labelAlign) {
return this.field.options.labelAlign
}
if (!!this.designer) {
return this.designer.formConfig.labelAlign || 'label-left-align'
} else {
return this.formConfig.labelAlign || 'label-left-align'
}
},
customClass() {
return !!this.field.options.customClass ? this.field.options.customClass.join(' ') : ''
},
subFormName() {
return !!this.parentWidget ? this.parentWidget.options.name : ''
},
subFormItemFlag() {
return !!this.parentWidget ? this.parentWidget.type === 'sub-form' : false
},
},
created() {
//
},
methods: {
selectField(field) {
if (!!this.designer) {
this.designer.setSelected(field)
this.designer.emitEvent('field-selected', this.parentWidget) //
}
},
selectParentWidget() {
if (this.parentWidget) {
this.designer.setSelected(this.parentWidget)
} else {
this.designer.clearSelected()
}
},
moveUpWidget() {
this.designer.moveUpWidget(this.parentList, this.indexOfParentList)
this.designer.emitHistoryChange()
},
moveDownWidget() {
this.designer.moveDownWidget(this.parentList, this.indexOfParentList)
this.designer.emitHistoryChange()
},
removeFieldWidget() {
if (!!this.parentList) {
let nextSelected = null
if (this.parentList.length === 1) {
if (!!this.parentWidget) {
nextSelected = this.parentWidget
}
} else if (this.parentList.length === (1 + this.indexOfParentList)) {
nextSelected = this.parentList[this.indexOfParentList - 1]
} else {
nextSelected = this.parentList[this.indexOfParentList + 1]
}
this.$nextTick(() => {
this.parentList.splice(this.indexOfParentList, 1)
//if (!!nextSelected) {
this.designer.setSelected(nextSelected)
//}
this.designer.emitHistoryChange()
})
}
},
getPropName() {
if (this.subFormItemFlag && !this.designState) {
return this.subFormName + "." + this.subFormRowIndex + "." + this.field.options.name + ""
} else {
return this.field.options.name
}
},
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss";
.design-time-bottom-margin {
margin-bottom: 5px;
}
.field-wrapper {
position: relative;
.field-action{
position: absolute;
//bottom: -24px;
bottom: 0;
right: -2px;
height: 22px;
line-height: 22px;
background: $--color-primary;
z-index: 9;
i {
font-size: 14px;
color: #fff;
margin: 0 5px;
cursor: pointer;
}
}
.drag-handler {
position: absolute;
top: 0;
//bottom: -22px; /* */
left: -1px;
height: 20px;
line-height: 20px;
//background: $--color-primary;
z-index: 9;
i {
font-size: 12px;
font-style: normal;
color: #fff;
margin: 4px;
cursor: move;
}
&:hover {
//opacity: 1;
background: $--color-primary;
}
}
}
.el-form-item {
//margin-bottom: 0 !important;
//margin-bottom: 6px;
//margin-top: 2px;
position: relative;
:deep(.el-form-item__label) {
white-space: nowrap;
text-overflow: ellipsis;
}
:deep(.el-form-item__content) {
//position: unset; /* TODO: */
}
span.custom-label i {
margin: 0 3px;
}
/* 隐藏Chrome浏览器中el-input数字输入框右侧的上下调整小箭头 */
:deep(.hide-spin-button) input::-webkit-outer-spin-button,
:deep(.hide-spin-button) input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
}
/* 隐藏Firefox浏览器中el-input数字输入框右侧的上下调整小箭头 */
:deep(.hide-spin-button) input[type="number"] {
-moz-appearance: textfield;
}
}
.required :deep(.el-form-item__label)::before {
content: '*';
color: #F56C6C;
margin-right: 4px;
}
.static-content-item {
min-height: 20px;
display: flex; /* 垂直居中 */
align-items: center; /* 垂直居中 */
:deep(.el-divider--horizontal) {
margin: 0;
}
}
.el-form-item.selected, .static-content-item.selected {
outline: 2px solid $--color-primary;
}
:deep(.label-left-align) .el-form-item__label {
text-align: left;
}
:deep(.label-center-align) .el-form-item__label {
text-align: center;
}
:deep(.label-right-align) .el-form-item__label {
text-align: right;
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<static-content-wrapper :designer="designer" :field="field" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<div ref="fieldEditor" v-html="field.options.htmlContent"></div>
</static-content-wrapper>
</template>
<script>
import StaticContentWrapper from './static-content-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "html-text-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
StaticContentWrapper,
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.registerToRefList()
this.initEventHandler()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; //* static-content-wrapper *//
</style>

View File

@ -0,0 +1,9 @@
let comps = {}
const modules = import.meta.globEager('./*.vue')
for (const path in modules) {
let cname = modules[path].default.name
comps[cname] = modules[path].default
}
export default comps

View File

@ -0,0 +1,114 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-input ref="fieldEditor" v-model="fieldModel"
:disabled="field.options.disabled" :readonly="field.options.readonly"
:size="field.options.size" class="hide-spin-button"
:type="inputType"
:show-password="field.options.showPassword"
:placeholder="field.options.placeholder"
:clearable="field.options.clearable"
:minlength="field.options.minLength" :maxlength="field.options.maxLength"
:show-word-limit="field.options.showWordLimit"
:prefix-icon="field.options.prefixIcon" :suffix-icon="field.options.suffixIcon"
@focus="handleFocusCustomEvent" @blur="handleBlurCustomEvent" @input="handleInputCustomEvent"
@change="handleChangeEvent">
<template #append>
<el-button v-if="field.options.appendButton" :disabled="field.options.disabled || field.options.appendButtonDisabled"
:class="field.options.buttonIcon" @click="emitAppendButtonClick"></el-button>
</template>
</el-input>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "input-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
inputType() {
if (this.field.options.type === 'number') {
return 'text' //inputtypenumberv-model
}
return this.field.options.type
},
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
</style>

View File

@ -0,0 +1,103 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-input-number ref="fieldEditor" v-model="fieldModel" class="full-width-input"
:disabled="field.options.disabled"
:size="field.options.size" :controls-position="field.options.controlsPosition"
:placeholder="field.options.placeholder"
:min="field.options.min" :max="field.options.max"
:precision="field.options.precision" :step="field.options.step"
@focus="handleFocusCustomEvent" @blur="handleBlurCustomEvent"
@change="handleChangeEvent">
</el-input-number>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "number-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,225 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<!-- el-upload增加:name="field.options.name"会导致又拍云上传失败故删除之 -->
<el-upload ref="fieldEditor" :disabled="field.options.disabled"
:action="field.options.uploadURL" :headers="uploadHeaders" :data="uploadData"
:with-credentials="field.options.withCredentials"
:multiple="field.options.multipleSelect" :file-list="fileList" :show-file-list="field.options.showFileList"
list-type="picture-card" :class="{'hideUploadDiv': uploadBtnHidden}"
:limit="field.options.limit" :on-exceed="handlePictureExceed"
:before-upload="beforePictureUpload"
:on-success="handlePictureUpload" :on-error="handelUploadError" :on-remove="handlePictureRemove">
<template #tip>
<div class="el-upload__tip"
v-if="!!field.options.uploadTip">{{field.options.uploadTip}}</div>
</template>
<i class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import {deepClone} from "@/utils/util";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "picture-upload-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
uploadHeaders: {},
uploadData: {
key: '', //
//token: '', //token
//policy: '', //policy
//authorization: '', //
},
fileList: [], //
uploadBtnHidden: false,
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
handlePictureExceed() {
let uploadLimit = this.field.options.limit /* 此行不能注释下一行ES6模板字符串需要用到 */
this.$message.warning(eval('`' + this.i18nt('render.hint.uploadExceed') + '`'));
},
updateUploadFieldModelAndEmitDataChange(fileList) {
let oldValue = deepClone(this.fieldModel)
this.fieldModel = deepClone(fileList)
this.syncUpdateFormModel(this.fieldModel)
this.emitFieldDataChange(this.fieldModel, oldValue)
},
beforePictureUpload(file) {
let fileTypeCheckResult = false
if (!!this.field.options && !!this.field.options.fileTypes) {
let uploadFileTypes = this.field.options.fileTypes
if (uploadFileTypes.length > 0) {
fileTypeCheckResult = uploadFileTypes.some( (ft) => {
return file.type === 'image/' + ft
})
}
}
if (!fileTypeCheckResult) {
this.$message.error(this.i18nt('render.hint.unsupportedFileType') + file.type)
return false;
}
let fileSizeCheckResult = false
let uploadFileMaxSize = 5 //5MB
if (!!this.field.options && !!this.field.options.fileMaxSize) {
uploadFileMaxSize = this.field.options.fileMaxSize
}
fileSizeCheckResult = file.size / 1024 / 1024 <= uploadFileMaxSize
if (!fileSizeCheckResult) {
this.$message.error(this.$('render.hint.fileSizeExceed') + uploadFileMaxSize + 'MB')
return false;
}
this.uploadData.key = file.name
return this.handleOnBeforeUpload(file)
},
handleOnBeforeUpload(file) {
if (!!this.field.options.onBeforeUpload) {
let bfFunc = new Function('file', this.field.options.onBeforeUpload)
let result = bfFunc.call(this, file)
if (typeof result === 'boolean') {
return result
} else {
return true
}
}
return true
},
handlePictureUpload(res, file, fileList) {
if (!!this.field.options.onUploadSuccess) {
let customFn = new Function('result', 'file', 'fileList', this.field.options.onUploadSuccess)
customFn.call(this, res, file, fileList)
} else {
if (file.status === 'success') {
//this.fileList.push(file) /* this.fileList!! */
this.updateUploadFieldModelAndEmitDataChange(fileList)
this.uploadBtnHidden = fileList.length >= this.field.options.limit
}
}
},
handlePictureRemove(file, fileList) {
this.fileList = deepClone(fileList) //this.fileList = fileList
this.updateUploadFieldModelAndEmitDataChange(fileList)
this.uploadBtnHidden = fileList.length >= this.field.options.limit
},
handelUploadError(err, file, fileList) {
if (!!this.field.options.onUploadError) {
let customFn = new Function('error', 'file', 'fileList', this.field.options.onUploadError)
customFn.call(this, err, file, fileList)
} else {
this.$message({
message: this.i18nt('render.hint.uploadError') + err,
duration: 3000,
type: 'error',
})
}
},
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
.hideUploadDiv {
:deep(div.el-upload--picture-card) { /* 隐藏最后的图片上传按钮 */
display: none;
}
:deep(div.el-upload--text) { /* 隐藏最后的文件上传按钮 */
display: none;
}
:deep(div.el-upload__tip) { /* 隐藏最后的文件上传按钮提示 */
display: none;
}
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-radio-group ref="fieldEditor" v-model="fieldModel"
:disabled="field.options.disabled" :size="field.options.size"
@change="handleChangeEvent">
<template v-if="!!field.options.buttonStyle">
<el-radio-button v-for="(item, index) in field.options.optionItems" :key="index" :label="item.value"
:disabled="item.disabled" :border="field.options.border"
:style="{display: field.options.displayStyle}">{{item.label}}</el-radio-button>
</template>
<template v-else>
<el-radio v-for="(item, index) in field.options.optionItems" :key="index" :label="item.value"
:disabled="item.disabled" :border="field.options.border"
:style="{display: field.options.displayStyle}">{{item.label}}</el-radio>
</template>
</el-radio-group>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "radio-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initOptionItems()
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
</style>

View File

@ -0,0 +1,102 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-rate ref="fieldEditor" v-model="fieldModel"
:disabled="field.options.disabled"
:max="field.options.max"
:low-threshold="field.options.lowThreshold" :high-threshold="field.options.highThreshold"
:allow-half="field.options.allowHalf"
:show-text="field.options.showText" :show-score="field.options.showScore"
@change="handleChangeEvent">
</el-rate>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "rate-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,124 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<!--
<vue-editor ref="fieldEditor" v-model="fieldModel" :editor-toolbar="customToolbar"
:disabled="field.options.disabled" :placeholder="field.options.placeholder"
@text-change="handleRichEditorChangeEvent"
@focus="handleRichEditorFocusEvent" @blur="handleRichEditorBlurEvent">
</vue-editor>
-->
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import {deepClone} from "@/utils/util";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "rich-editor-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
// VueEditor: resolve => { //
// require(['vue2-editor'], ({VueEditor}) => resolve(VueEditor))
// }
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
customToolbar: [], //
valueChangedFlag: false, //vue2-editor
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
handleRichEditorChangeEvent() {
this.valueChangedFlag = true
this.syncUpdateFormModel(this.fieldModel)
},
handleRichEditorFocusEvent() {
this.oldFieldValue = deepClone(this.fieldModel)
},
handleRichEditorBlurEvent() {
if (this.valueChangedFlag) {
this.emitFieldDataChange(this.fieldModel, this.oldFieldValue)
this.valueChangedFlag = false
}
},
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-select ref="fieldEditor" v-model="fieldModel" class="full-width-input"
:disabled="field.options.disabled"
:size="field.options.size"
:clearable="field.options.clearable"
:filterable="field.options.filterable"
:allow-create="field.options.allowCreate"
:default-first-option="allowDefaultFirstOption"
:automatic-dropdown="field.options.automaticDropdown"
:multiple="field.options.multiple" :multiple-limit="field.options.multipleLimit"
:placeholder="field.options.placeholder || i18nt('render.hint.selectPlaceholder')"
:remote="this.field.options.remote" :remote-method="remoteQuery"
@focus="handleFocusCustomEvent" @blur="handleBlurCustomEvent"
@change="handleChangeEvent">
<el-option v-for="item in field.options.optionItems" :key="item.value" :label="item.label"
:value="item.value" :disabled="item.disabled">
</el-option>
</el-select>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "select-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
allowDefaultFirstOption() {
return (!!this.field.options.filterable && !!this.field.options.allowCreate)
},
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initOptionItems()
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,100 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-slider ref="fieldEditor" v-model="fieldModel"
:disabled="field.options.disabled" :show-stops="field.options.showStops"
:min="field.options.min" :max="field.options.max" :step="field.options.step"
:range="field.options.range" :vertical="field.options.vertical"
@change="handleChangeEvent">
</el-slider>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "slider-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,102 @@
<template>
<static-content-wrapper :designer="designer" :field="field" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<div :class="[!!designState ? 'slot-wrapper-design' : 'slot-wrapper-render']">
<!-- -->
<slot :name="field.options.name" v-bind:formModel="formModel"></slot>
<!-- -->
<!-- slot :name="field.options.name"></slot -->
<div v-if="!!designState" class="slot-title">{{field.options.label}}</div>
</div>
</static-content-wrapper>
</template>
<script>
import StaticContentWrapper from './static-content-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "slot-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
StaticContentWrapper,
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.registerToRefList()
this.initEventHandler()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.slot-wrapper-design {
width: 100%;
min-height: 26px;
background: linear-gradient(45deg, #ccc 25%, #eee 0, #eee 50%, #ccc 0, #ccc 75%, #eee 0);
background-size: 20px 20px;
text-align: center;
.slot-title {
font-size: 13px;
}
}
.slot-wrapper-render {
}
</style>

View File

@ -0,0 +1,189 @@
<template>
<div class="field-wrapper" :class="{'design-time-bottom-margin': !!this.designer}">
<div class="static-content-item" v-show="!field.options.hidden || (designState === true)"
:class="[selected ? 'selected' : '', customClass]" @click.stop="selectField(field)">
<slot></slot>
</div>
<template v-if="!!this.designer">
<div class="field-action" v-if="designer.selectedId === field.id">
<i class="el-icon-back" :title="i18nt('designer.hint.selectParentWidget')" @click.stop="selectParentWidget(field)"></i>
<i class="el-icon-top" v-if="!!parentList && (parentList.length > 1)" :title="i18nt('designer.hint.moveUpWidget')"
@click.stop="moveUpWidget(field)"></i>
<i class="el-icon-bottom" v-if="!!parentList && (parentList.length > 1)" :title="i18nt('designer.hint.moveDownWidget')"
@click.stop="moveDownWidget(field)"></i>
<i class="el-icon-delete" :title="i18nt('designer.hint.remove')" @click.stop="removeFieldWidget"></i>
</div>
<div class="drag-handler background-opacity" v-if="designer.selectedId === field.id">
<i class="el-icon-rank" :title="i18nt('designer.hint.dragHandler')"></i>
<i>{{i18n2t(`designer.widgetLabel.${field.type}`, `extension.widgetLabel.${field.type}`)}}</i>
<i v-if="field.options.hidden === true" class="iconfont icon-hide"></i>
</div>
</template>
</div>
</template>
<script>
import i18n from "@/utils/i18n";
export default {
name: "static-content-wrapper",
mixins: [i18n],
props: {
field: Object,
designer: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
computed: {
selected() {
return !!this.designer && this.field.id === this.designer.selectedId
},
customClass() {
return !!this.field.options.customClass ? this.field.options.customClass.join(' ') : ''
},
},
methods: {
selectField(field) {
if (!!this.designer) {
this.designer.setSelected(field)
this.designer.emitEvent('field-selected', this.parentWidget) //
}
},
selectParentWidget() {
if (this.parentWidget) {
this.designer.setSelected(this.parentWidget)
} else {
this.designer.clearSelected()
}
},
moveUpWidget() {
this.designer.moveUpWidget(this.parentList, this.indexOfParentList)
this.designer.emitHistoryChange()
},
moveDownWidget() {
this.designer.moveDownWidget(this.parentList, this.indexOfParentList)
this.designer.emitHistoryChange()
},
removeFieldWidget() {
if (!!this.parentList) {
let nextSelected = null
if (this.parentList.length === 1) {
if (!!this.parentWidget) {
nextSelected = this.parentWidget
}
} else if (this.parentList.length === (1 + this.indexOfParentList)) {
nextSelected = this.parentList[this.indexOfParentList - 1]
} else {
nextSelected = this.parentList[this.indexOfParentList + 1]
}
this.$nextTick(() => {
this.parentList.splice(this.indexOfParentList, 1)
//if (!!nextSelected) {
this.designer.setSelected(nextSelected)
//}
this.designer.emitHistoryChange()
})
}
},
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss";
.design-time-bottom-margin {
margin-bottom: 5px;
}
.field-wrapper {
position: relative;
.field-action{
position: absolute;
//bottom: -24px;
bottom: 0;
right: -2px;
height: 22px;
line-height: 22px;
background: $--color-primary;
z-index: 9;
i {
font-size: 14px;
color: #fff;
margin: 0 5px;
cursor: pointer;
}
}
.drag-handler {
position: absolute;
top: 0;
//bottom: -22px; /* */
left: -1px;
height: 20px;
line-height: 20px;
//background: $--color-primary;
z-index: 9;
i {
font-size: 12px;
font-style: normal;
color: #fff;
margin: 4px;
cursor: move;
}
&:hover {
//opacity: 1;
background: $--color-primary;
}
}
}
.static-content-item {
min-height: 20px;
display: flex; /* 垂直居中 */
align-items: center; /* 垂直居中 */
:deep(.el-divider--horizontal) {
margin: 0;
}
}
.el-form-item.selected, .static-content-item.selected {
outline: 2px solid $--color-primary;
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<static-content-wrapper :designer="designer" :field="field" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<div ref="fieldEditor">{{field.options.textContent}}</div>
</static-content-wrapper>
</template>
<script>
import StaticContentWrapper from './static-content-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "static-text-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
StaticContentWrapper,
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.registerToRefList()
this.initEventHandler()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; //* static-content-wrapper *//
</style>

View File

@ -0,0 +1,101 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-switch ref="fieldEditor" v-model="fieldModel"
:disabled="field.options.disabled"
:active-text="field.options.activeText" :inactive-text="field.options.inactiveText"
:active-color="field.options.activeColor" :inactive-color="field.options.inactiveColor"
:width="field.options.switchWidth"
@change="handleChangeEvent">
</el-switch>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "switch-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-input type="textarea" ref="fieldEditor" v-model="fieldModel"
:disabled="field.options.disabled" :readonly="field.options.readonly"
:size="field.options.size"
:placeholder="field.options.placeholder" :rows="field.options.rows"
:minlength="field.options.minLength" :maxlength="field.options.maxLength"
:show-word-limit="field.options.showWordLimit"
@focus="handleFocusCustomEvent" @blur="handleBlurCustomEvent" @input="handleInputCustomEvent"
@change="handleChangeEvent">
</el-input>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "textarea-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
</style>

View File

@ -0,0 +1,104 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-time-picker ref="fieldEditor" is-range v-model="fieldModel" class="full-width-input"
:disabled="field.options.disabled" :readonly="field.options.readonly"
:size="field.options.size"
:clearable="field.options.clearable" :editable="field.options.editable"
:format="field.options.format" value-format="HH:mm:ss"
:start-placeholder="field.options.startPlaceholder || i18nt('render.hint.startTimePlaceholder')"
:end-placeholder="field.options.endPlaceholder || i18nt('render.hint.endTimePlaceholder')"
@focus="handleFocusCustomEvent" @blur="handleBlurCustomEvent"
@change="handleChangeEvent">
</el-time-picker>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "time-range-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
:parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
:sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex" :sub-form-row-id="subFormRowId">
<el-time-picker ref="fieldEditor" v-model="fieldModel" class="full-width-input"
:disabled="field.options.disabled" :readonly="field.options.readonly"
:size="field.options.size"
:clearable="field.options.clearable" :editable="field.options.editable"
:format="field.options.format" value-format="HH:mm:ss"
:placeholder="field.options.placeholder || i18nt('render.hint.timePlaceholder')"
@focus="handleFocusCustomEvent" @blur="handleBlurCustomEvent"
@change="handleChangeEvent">
</el-time-picker>
</form-item-wrapper>
</template>
<script>
import FormItemWrapper from './form-item-wrapper'
import emitter from '@/utils/emitter'
import i18n, {translate} from "@/utils/i18n";
import fieldMixin from "@/components/form-designer/form-widget/field-widget/fieldMixin";
export default {
name: "time-widget",
componentName: 'FieldWidget', //FieldWidgetbroadcast
mixins: [emitter, fieldMixin, i18n],
props: {
field: Object,
parentWidget: Object,
parentList: Array,
indexOfParentList: Number,
designer: Object,
designState: {
type: Boolean,
default: false
},
subFormRowIndex: { /* 子表单组件行索引从0开始计数 */
type: Number,
default: -1
},
subFormColIndex: { /* 子表单组件列索引从0开始计数 */
type: Number,
default: -1
},
subFormRowId: { /* 子表单组件行Id唯一id且不可变 */
type: String,
default: ''
},
},
components: {
FormItemWrapper,
},
inject: ['refList', 'formConfig', 'globalOptionData', 'globalModel'],
data() {
return {
oldFieldValue: null, //fieldchange
fieldModel: null,
rules: [],
}
},
computed: {
},
beforeCreate() {
/* 这里不能访问方法和属性!! */
},
created() {
/* mountedcreatedmountedmountedprop
需要在父组件created中初始化 */
this.initFieldModel()
this.registerToRefList()
this.initEventHandler()
this.buildFieldRules()
this.handleOnCreated()
},
mounted() {
this.handleOnMounted()
},
beforeUnmount() {
this.unregisterFromRefList()
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../../../styles/global.scss"; /* form-item-wrapper已引入还需要重复引入吗 */
.full-width-input {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<div class="form-widget-container">
<el-form class="full-height-width widget-form" :label-position="labelPosition"
:class="[customClass, layoutType + '-layout']" :size="size" :validate-on-rule-change="false">
<template v-if="designer.widgetList.length === 0">
<div class="no-widget-hint">{{i18nt('designer.noWidgetHint')}}</div>
<!-- 下面的div用于把父容器高度撑开
<div class="form-widget-list"></div>
-->
</template>
<div class="form-widget-list">
<draggable :list="designer.widgetList" item-key="id" v-bind="{group:'dragGroup', ghostClass: 'ghost',animation: 300}"
tag="transition-group" :component-data="{name: 'fade'}"
handle=".drag-handler" @end="onDragEnd" @add="onDragAdd" @update="onDragUpdate" :move="checkMove">
<template #item="{ element: widget, index }">
<template v-if="'container' === widget.category">
<component :is="getWidgetName(widget)" :widget="widget" :designer="designer" :key="widget.id" :parent-list="designer.widgetList"
:index-of-parent-list="index" :parent-widget="null"></component>
</template>
<template v-else>
<component :is="getWidgetName(widget)" :field="widget" :designer="designer" :key="widget.id" :parent-list="designer.widgetList"
:index-of-parent-list="index" :parent-widget="null" :design-state="true"></component>
</template>
</template>
</draggable>
</div>
</el-form>
</div>
</template>
<script>
import Draggable from 'vuedraggable'
import '@/components/form-designer/form-widget/container-widget/index'
import FieldComponents from '@/components/form-designer/form-widget/field-widget/index'
import i18n from "@/utils/i18n"
export default {
name: "VFormWidget",
componentName: "VFormWidget",
mixins: [i18n],
components: {
Draggable,
...FieldComponents,
},
props: {
designer: Object,
formConfig: Object,
optionData: { //prop
type: Object,
default: () => {}
},
},
provide() {
return {
refList: this.widgetRefList,
formConfig: this.formConfig,
globalOptionData: this.optionData,
getOptionData: () => this.optionData,
globalModel: {
formModel: this.formModel,
}
}
},
inject: ['getDesignerConfig'],
data() {
return {
formModel: {},
widgetRefList: {},
}
},
computed: {
labelPosition() {
if (!!this.designer.formConfig && !!this.designer.formConfig.labelPosition) {
return this.designer.formConfig.labelPosition
}
return 'left'
},
size() {
if (!!this.designer.formConfig && !!this.designer.formConfig.size) {
return this.designer.formConfig.size
}
return 'medium'
},
customClass() {
return this.designer.formConfig.customClass || ''
},
layoutType() {
return this.designer.getLayoutType()
},
},
watch: {
'designer.widgetList': {
deep: true,
handler(val) {
//
}
},
'designer.formConfig': {
deep: true,
handler(val) {
//
}
},
},
created() {
this.designer.initDesigner();
this.designer.loadPresetCssCode( this.getDesignerConfig().presetCssCode )
},
mounted() {
this.disableFirefoxDefaultDrop() /* 禁用Firefox默认拖拽搜索功能!! */
this.designer.registerFormWidget(this)
},
methods: {
getWidgetName(widget) {
return widget.type + '-widget'
},
disableFirefoxDefaultDrop() {
let isFirefox = (navigator.userAgent.toLowerCase().indexOf("firefox") !== -1)
if (isFirefox) {
document.body.ondrop = function (event) {
event.stopPropagation();
event.preventDefault();
}
}
},
onDragEnd(evt) {
//console.log('drag end000', evt)
},
onDragAdd(evt) {
const newIndex = evt.newIndex
if (!!this.designer.widgetList[newIndex]) {
this.designer.setSelected( this.designer.widgetList[newIndex] )
}
this.designer.emitHistoryChange()
},
onDragUpdate() { /* 在VueDraggable内拖拽组件发生位置变化时会触发update未发生组件位置变化不会触发 */
this.designer.emitHistoryChange()
},
checkMove(evt) {
return this.designer.checkWidgetMove(evt)
},
getFormData() {
return this.formModel
},
getWidgetRef(widgetName, showError) {
let foundRef = this.widgetRefList[widgetName]
if (!foundRef && !!showError) {
this.$message.error(this.i18nt('designer.hint.refNotFound') + widgetName)
}
return foundRef
},
}
}
</script>
<style lang="scss" scoped>
.container-scroll-bar {
:deep(.el-scrollbar__wrap), :deep(.el-scrollbar__view) {
overflow-x: hidden;
}
}
.form-widget-container {
padding: 10px;
background: #f1f2f3;
overflow-x: hidden;
overflow-y: auto;
.el-form.full-height-width {
/*
margin: 0 auto;
width: 420px;
border-radius: 15px;
//border-width: 10px;
box-shadow: 0 0 1px 10px #495060;
*/
height: 100%;
padding: 3px;
background: #ffffff;
.no-widget-hint {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: 18px;
color: #999999;
}
.form-widget-list {
min-height: calc(100vh - 56px - 68px);
padding: 3px;
}
}
.el-form.PC-layout {
//
}
.el-form.Pad-layout {
margin: 0 auto;
max-width: 960px;
border-radius: 15px;
box-shadow: 0 0 1px 10px #495060;
}
.el-form.H5-layout {
margin: 0 auto;
width: 420px;
border-radius: 15px;
//border-width: 10px;
box-shadow: 0 0 1px 10px #495060;
}
.el-form.widget-form :deep(.el-row) {
padding: 2px;
border: 1px dashed rgba(170, 170, 170, 0.75);
}
}
.grid-cell {
min-height: 30px;
border-right: 1px dotted #cccccc;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,426 @@
<!--
/**
* author: vformAdmin
* email: vdpadmin@163.com
* website: https://www.vform666.com
* date: 2021.08.18
* remark: 如果要分发VForm源码需在本文件顶部保留此文件头信息
*/
-->
<template>
<el-container class="main-container full-height">
<el-header class="main-header">
<div class="float-left main-title">
<img src="../../assets/vform-logo.png" @click="openHome">
<span class="bold">VForm</span> {{i18nt('application.productTitle')}} <span class="version-span">Ver {{vFormVersion}}</span></div>
<div class="float-right external-link">
<el-dropdown v-if="showLink('languageMenu')" @command="handleLanguageChanged">
<span class="el-dropdown-link">{{curLangName}}<i class="el-icon-arrow-down el-icon--right"></i></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="zh-CN">{{i18nt('application.zh-CN')}}</el-dropdown-item>
<el-dropdown-item command="en-US">{{i18nt('application.en-US')}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<a v-if="showLink('externalLink')" href="javascript:void(0)" @click="(ev) => openUrl(ev, gitUrl)" target="_blank"><svg-icon icon-class="github" />{{i18nt('application.github')}}</a>
<a v-if="showLink('externalLink')" href="javascript:void(0)" @click="(ev) => openUrl(ev, docUrl)" target="_blank"><svg-icon icon-class="document" />{{i18nt('application.document')}}</a>
<a v-if="showLink('externalLink')" href="javascript:void(0)" @click="(ev) => openUrl(ev, chatUrl)" target="_blank">{{i18nt('application.qqGroup')}}</a>
<a v-if="showLink('externalLink')" href="javascript:void(0)" @click="(ev) => openUrl(ev, subScribeUrl)" target="_blank">
{{i18nt('application.subscription')}}<i class="el-icon-top-right"></i></a>
</div>
</el-header>
<el-container>
<el-aside class="side-panel">
<widget-panel :designer="designer" />
</el-aside>
<el-container class="center-layout-container">
<el-header class="toolbar-header">
<toolbar-panel :designer="designer" ref="toolbarRef">
<template v-for="(idx, slotName) in $slots" #[slotName]>
<slot :name="slotName"></slot>
</template>
</toolbar-panel>
</el-header>
<el-main class="form-widget-main">
<el-scrollbar class="container-scroll-bar" :style="{height: scrollerHeight}">
<v-form-widget :designer="designer" :form-config="designer.formConfig">
</v-form-widget>
</el-scrollbar>
</el-main>
</el-container>
<el-aside>
<setting-panel :designer="designer" :selected-widget="designer.selectedWidget" :form-config="designer.formConfig" />
</el-aside>
</el-container>
</el-container>
</template>
<script>
import WidgetPanel from './widget-panel/index'
import ToolbarPanel from './toolbar-panel/index'
import SettingPanel from './setting-panel/index'
import VFormWidget from './form-widget/index'
import {createDesigner} from "@/components/form-designer/designer"
import {addWindowResizeHandler, deepClone, getQueryParam} from "@/utils/util"
import {MOCK_CASE_URL, VARIANT_FORM_VERSION} from "@/utils/config"
import i18n, { changeLocale } from "@/utils/i18n"
export default {
name: "VFormDesigner",
componentName: "VFormDesigner",
mixins: [i18n],
components: {
WidgetPanel,
ToolbarPanel,
SettingPanel,
VFormWidget,
},
props: {
/* 后端字段列表API */
fieldListApi: {
type: Object,
default: null,
},
/* 禁止显示的组件名称数组 */
bannedWidgets: {
type: Array,
default: () => []
},
designerConfig: {
type: Object,
default: () => {
return {
languageMenu: true, //
externalLink: true, //GitHub
formTemplates: true, //
eventCollapse: true, //
clearDesignerButton: true, //
previewFormButton: true, //
importJsonButton: true, //JSON
exportJsonButton: true, //JSON
exportCodeButton: true, //
generateSFCButton: true, //SFC
presetCssCode: '', //CSS
}
}
},
},
data() {
return {
vFormVersion: VARIANT_FORM_VERSION,
curLangName: '',
vsCodeFlag: false,
caseName: '',
docUrl: 'https://www.vform666.com/document.html',
gitUrl: 'https://github.com/vform666/variant-form',
chatUrl: 'https://www.vform666.com/chat-group.html',
subScribeUrl: 'https://www.vform666.com/subscribe.html',
scrollerHeight: 0,
designer: createDesigner(this),
fieldList: []
}
},
provide() {
return {
serverFieldList: this.fieldList,
getDesignerConfig: () => this.designerConfig,
getBannedWidgets: () => this.bannedWidgets,
}
},
created() {
this.vsCodeFlag = getQueryParam('vscode') == 1
this.caseName = getQueryParam('case')
},
mounted() {
this.initLocale()
this.scrollerHeight = window.innerHeight - 56 - 36 + 'px'
addWindowResizeHandler(() => {
this.$nextTick(() => {
this.scrollerHeight = window.innerHeight - 56 - 36 + 'px'
})
})
this.loadCase()
this.loadFieldListFromServer()
},
methods: {
showLink(configName) {
if (this.designerConfig[configName] === undefined) {
return true
}
return !!this.designerConfig[configName]
},
openHome() {
if (!!this.vsCodeFlag) {
const msgObj = {
cmd: 'openUrl',
data: {
url: 'https://www.vform666.com/'
}
}
window.parent.postMessage(msgObj, '*')
}
},
openUrl(event, url) {
if (!!this.vsCodeFlag) {
const msgObj = {
cmd: 'openUrl',
data: {
url
}
}
window.parent.postMessage(msgObj, '*')
} else {
let aDom = event.currentTarget
aDom.href = url
//window.open(url, '_blank') //
}
},
loadCase() {
if (!this.caseName) {
return
}
axios.get(MOCK_CASE_URL + this.caseName + '.txt').then(res => {
if (!!res.data.code) {
this.$message.error(this.i18nt('designer.hint.sampleLoadedFail'))
return
}
this.setFormJson(res.data)
this.$message.success(this.i18nt('designer.hint.sampleLoadedSuccess'))
}).catch(error => {
this.$message.error(this.i18nt('designer.hint.sampleLoadedFail') + ':' + error)
})
},
initLocale() {
let curLocale = localStorage.getItem('v_form_locale')
if (!!this.vsCodeFlag) {
curLocale = curLocale || 'en-US'
} else {
curLocale = curLocale || 'zh-CN'
}
this.curLangName = this.i18nt('application.' + curLocale)
this.changeLanguage(curLocale)
},
loadFieldListFromServer() {
if (!this.fieldListApi) {
return
}
axios.get(this.fieldListApi.URL).then(res => {
let labelKey = this.fieldListApi.labelKey || 'label'
let nameKey = this.fieldListApi.nameKey || 'name'
res.data.forEach(fieldItem => {
this.fieldList.push({
label: fieldItem[labelKey],
name: fieldItem[nameKey]
})
})
}).catch(error => {
this.$message.error(error)
})
},
handleLanguageChanged(command) {
this.changeLanguage(command)
this.curLangName = this.i18nt('application.' + command)
},
changeLanguage(langName) {
changeLocale(langName)
},
setFormJson(formJson) {
let modifiedFlag = false
if (!!formJson) {
if (typeof formJson === 'string') {
modifiedFlag = this.designer.loadFormJson( JSON.parse(formJson) )
} else if (formJson.constructor === Object) {
modifiedFlag = this.designer.loadFormJson(formJson)
}
if (modifiedFlag) {
this.designer.emitHistoryChange()
}
}
},
getFormJson() {
return {
widgetList: deepClone(this.designer.widgetList),
formConfig: deepClone(this.designer.formConfig)
}
},
clearDesigner() {
this.$refs.toolbarRef.clearFormWidget()
},
/**
* 刷新表单设计器
*/
refreshDesigner() {
//this.designer.loadFormJson( this.getFormJson() ) //
let fJson = this.getFormJson()
this.designer.clearDesigner(true) //
this.designer.loadFormJson(fJson)
},
/**
* 预览表单
*/
previewForm() {
this.$refs.toolbarRef.previewForm()
},
/**
* 导入表单JSON
*/
importJson() {
this.$refs.toolbarRef.importJson()
},
/**
* 导出表单JSON
*/
exportJson() {
this.$refs.toolbarRef.exportJson()
},
/**
* 导出Vue/HTML代码
*/
exportCode() {
this.$refs.toolbarRef.exportCode()
},
/**
* 生成SFC代码
*/
generateSFC() {
this.$refs.toolbarRef.generateSFC()
},
//TODO:
}
}
</script>
<style lang="scss" scoped>
.el-container.full-height {
height: 100%;
overflow-y: hidden;
}
.el-container.center-layout-container {
min-width: 680px;
border-left: 2px dotted #EBEEF5;
border-right: 2px dotted #EBEEF5;
}
.el-header.main-header {
border-bottom: 2px dotted #EBEEF5;
height: 48px !important;
line-height: 48px !important;
min-width: 800px;
}
div.main-title {
font-size: 18px;
color: #242424;
display: flex;
align-items: center;
justify-items: center;
img {
cursor: pointer;
width: 36px;
height: 36px;
}
span.bold {
font-size: 20px;
font-weight: bold;
margin: 0 6px 0 6px;
}
span.version-span {
font-size: 14px;
color: #101F1C;
margin-left: 6px;
}
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.el-dropdown-link {
margin-right: 12px;
cursor: pointer;
}
div.external-link a {
font-size: 13px;
text-decoration: none;
margin-right: 10px;
color: #606266;
}
.el-header.toolbar-header {
font-size: 14px;
border-bottom: 1px dotted #CCCCCC;
height: 42px !important;
//line-height: 42px !important;
}
.el-aside.side-panel {
width: 260px !important;
overflow-y: hidden;
}
.el-main.form-widget-main {
padding: 0;
position: relative;
overflow-x: hidden;
}
.container-scroll-bar {
:deep(.el-scrollbar__wrap), :deep(.el-scrollbar__view) {
overflow-x: hidden;
}
}
</style>

View File

@ -0,0 +1,330 @@
<template>
<div>
<el-form :model="formConfig" size="mini" label-position="left" label-width="120px"
class="setting-form" @submit.prevent>
<el-collapse v-model="formActiveCollapseNames" class="setting-collapse">
<el-collapse-item name="1" :title="i18nt('designer.setting.basicSetting')">
<el-form-item :label="i18nt('designer.setting.formSize')">
<el-select v-model="formConfig.size">
<el-option v-for="item in formSizes" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.labelPosition')">
<el-radio-group v-model="formConfig.labelPosition" class="radio-group-custom">
<el-radio-button label="left">{{i18nt('designer.setting.leftPosition')}}</el-radio-button>
<el-radio-button label="top">{{i18nt('designer.setting.topPosition')}}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.labelAlign')">
<el-radio-group v-model="formConfig.labelAlign" class="radio-group-custom">
<el-radio-button label="label-left-align">{{i18nt('designer.setting.leftAlign')}}</el-radio-button>
<el-radio-button label="label-center-align">{{i18nt('designer.setting.centerAlign')}}</el-radio-button>
<el-radio-button label="label-right-align">{{i18nt('designer.setting.rightAlign')}}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.labelWidth')">
<el-input-number v-model="formConfig.labelWidth" :min="0" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.formCss')">
<el-button type="info" icon="el-icon-edit" plain round @click="editFormCss">{{i18nt('designer.setting.addCss')}}</el-button>
</el-form-item>
<!-- -->
<el-form-item :label="i18nt('designer.setting.customClass')">
<el-select v-model="formConfig.customClass" multiple filterable allow-create
default-first-option>
<el-option v-for="(item, idx) in cssClassList" :key="idx" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
<!-- -->
<el-form-item :label="i18nt('designer.setting.globalFunctions')">
<el-button type="info" icon="el-icon-edit" plain round @click="editGlobalFunctions">{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
<el-form-item label-width="0">
<el-divider class="custom-divider">{{i18nt('designer.setting.formSFCSetting')}}</el-divider>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.formModelName')">
<el-input type="text" v-model="formConfig.modelName"></el-input>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.formRefName')">
<el-input type="text" v-model="formConfig.refName"></el-input>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.formRulesName')">
<el-input type="text" v-model="formConfig.rulesName"></el-input>
</el-form-item>
</el-collapse-item>
<el-collapse-item v-if="showEventCollapse()" name="2" :title="i18nt('designer.setting.eventSetting')">
<el-form-item label="onFormCreated" label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round @click="editFormEventHandler('onFormCreated')">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
<el-form-item label="onFormMounted" label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round @click="editFormEventHandler('onFormMounted')">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
<!-- -->
<el-form-item label="onFormDataChange" label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round @click="editFormEventHandler('onFormDataChange')">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
<!-- -->
<!--
<el-form-item label="onFormValidate">
<el-button type="info" icon="el-icon-edit" plain round @click="editFormEventHandler('onFormValidate')">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
-->
</el-collapse-item>
</el-collapse>
</el-form>
<el-dialog :title="i18nt('designer.setting.editFormEventHandler')" v-model="showFormEventDialogFlag"
v-if="showFormEventDialogFlag" :show-close="true" custom-class="small-padding-dialog"
:close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="true">
<el-alert type="info" :closable="false" :title="'form.' + eventParamsMap[curEventName]"></el-alert>
<code-editor :mode="'javascript'" :readonly="false" v-model="formEventHandlerCode"></code-editor>
<el-alert type="info" :closable="false" title="}"></el-alert>
<template #footer>
<div class="dialog-footer">
<el-button @click="showFormEventDialogFlag = false">
{{i18nt('designer.hint.cancel')}}</el-button>
<el-button type="primary" @click="saveFormEventHandler">
{{i18nt('designer.hint.confirm')}}</el-button>
</div>
</template>
</el-dialog>
<el-dialog :title="i18nt('designer.setting.formCss')" v-model="showEditFormCssDialogFlag"
v-if="showEditFormCssDialogFlag" :show-close="true" custom-class="small-padding-dialog"
:close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="true">
<code-editor :mode="'css'" :readonly="false" v-model="formCssCode"></code-editor>
<template #footer>
<div class="dialog-footer">
<el-button @click="showEditFormCssDialogFlag = false">
{{i18nt('designer.hint.cancel')}}</el-button>
<el-button type="primary" @click="saveFormCss">
{{i18nt('designer.hint.confirm')}}</el-button>
</div>
</template>
</el-dialog>
<el-dialog :title="i18nt('designer.setting.globalFunctions')" v-model="showEditFunctionsDialogFlag"
v-if="showEditFunctionsDialogFlag" :show-close="true" custom-class="small-padding-dialog"
:close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="true">
<code-editor :mode="'javascript'" :readonly="false" v-model="functionsCode"></code-editor>
<template #footer>
<div class="dialog-footer">
<el-button @click="showEditFunctionsDialogFlag = false">
{{i18nt('designer.hint.cancel')}}</el-button>
<el-button type="primary" @click="saveGlobalFunctions">
{{i18nt('designer.hint.confirm')}}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import i18n from "@/utils/i18n"
import CodeEditor from '@/components/code-editor/index'
import {deepClone, insertCustomCssToHead, insertGlobalFunctionsToHtml} from "@/utils/util"
export default {
name: "form-setting",
mixins: [i18n],
components: {
CodeEditor,
},
props: {
designer: Object,
formConfig: Object,
},
inject: ['getDesignerConfig'],
data() {
return {
designerConfig: this.getDesignerConfig(),
formActiveCollapseNames: ['1', '2'],
formSizes: [
{label: 'default', value: ''},
{label: 'large', value: 'large'},
{label: 'medium', value: 'medium'},
{label: 'small', value: 'small'},
{label: 'mini', value: 'mini'},
],
showEditFormCssDialogFlag: false,
formCssCode: '',
cssClassList: [],
showEditFunctionsDialogFlag: false,
functionsCode: '',
showFormEventDialogFlag: false,
formEventHandlerCode: '',
curEventName: '',
eventParamsMap: {
'onFormCreated': 'onFormCreated() {',
'onFormMounted': 'onFormMounted() {',
'onFormDataChange': 'onFormDataChange(fieldName, newValue, oldValue, formModel, subFormName, subFormRowIndex) {',
//'onFormValidate': 'onFormValidate() {',
},
}
},
created() {
//JSONCSS
this.designer.handleEvent('form-json-imported', () => {
this.formCssCode = this.formConfig.cssCode
insertCustomCssToHead(this.formCssCode)
this.extractCssClass()
this.designer.emitEvent('form-css-updated', deepClone(this.cssClassList))
})
},
mounted() {
/* SettingPanelFormWidget, FormWidgetformConfig
此处SettingPanel可能无法获取到formConfig.cssCode, 故加个延时函数 */
setTimeout(() => {
this.formCssCode = this.formConfig.cssCode
insertCustomCssToHead(this.formCssCode)
this.extractCssClass()
this.designer.emitEvent('form-css-updated', deepClone(this.cssClassList))
}, 1200)
},
methods: {
showEventCollapse() {
if (this.designerConfig['eventCollapse'] === undefined) {
return true
}
return !!this.designerConfig['eventCollapse']
},
editFormCss() {
this.formCssCode = this.designer.formConfig.cssCode
this.showEditFormCssDialogFlag = true
},
extractCssClass() {
let regExp = /\..*{/g
let result = this.formCssCode.match(regExp)
let cssNameArray = []
if (!!result && result.length > 0) {
result.forEach((rItem) => {
let classArray = rItem.split(',') //class
if (classArray.length > 0) {
classArray.forEach((cItem) => {
let caItem = cItem.trim()
if (caItem.indexOf('.', 1) !== -1) { //.
let newClass = caItem.substring(caItem.indexOf('.') + 1, caItem.indexOf('.', 1)) //.class
if (!!newClass) {
cssNameArray.push(newClass.trim())
}
} else if (caItem.indexOf(' ') !== -1) { //
let newClass = caItem.substring(caItem.indexOf('.') + 1, caItem.indexOf(' ')) //.class
if (!!newClass) {
cssNameArray.push(newClass.trim())
}
} else {
if (caItem.indexOf('{') !== -1) { //{
let newClass = caItem.substring(caItem.indexOf('.') + 1, caItem.indexOf('{'))
cssNameArray.push( newClass.trim() )
} else {
let newClass = caItem.substring(caItem.indexOf('.') + 1)
cssNameArray.push( newClass.trim() )
}
}
})
}
})
}
//this.cssClassList.length = 0
this.cssClassList.splice(0, this.cssClassList.length) //splicelength=0
this.cssClassList = Array.from( new Set(cssNameArray) ) //
},
saveFormCss() {
this.extractCssClass()
this.designer.formConfig.cssCode = this.formCssCode
insertCustomCssToHead(this.formCssCode)
this.showEditFormCssDialogFlag = false
this.designer.emitEvent('form-css-updated', deepClone(this.cssClassList))
},
editGlobalFunctions() {
this.functionsCode = this.designer.formConfig.functions
this.showEditFunctionsDialogFlag = true
},
saveGlobalFunctions() {
this.designer.formConfig.functions = this.functionsCode
insertGlobalFunctionsToHtml(this.functionsCode)
this.showEditFunctionsDialogFlag = false
},
editFormEventHandler(eventName) {
this.curEventName = eventName
this.formEventHandlerCode = this.formConfig[eventName]
this.showFormEventDialogFlag = true
},
saveFormEventHandler() {
this.formConfig[this.curEventName] = this.formEventHandlerCode
this.showFormEventDialogFlag = false
},
}
}
</script>
<style lang="scss" scoped>
.setting-form {
:deep(.el-form-item__label) {
font-size: 13px;
//text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
:deep(.el-form-item--mini.el-form-item) {
margin-bottom: 6px;
}
.radio-group-custom {
:deep(.el-radio-button__inner) {
padding-left: 12px;
padding-right: 12px;
}
}
.custom-divider.el-divider--horizontal {
margin: 10px 0;
}
}
.setting-collapse {
:deep(.el-collapse-item__content) {
padding-bottom: 6px;
}
:deep(.el-collapse-item__header) {
font-style: italic;
font-weight: bold;
}
}
.small-padding-dialog {
:deep(.el-dialog__body) {
padding: 6px 15px 12px 15px;
}
}
</style>

View File

@ -0,0 +1,331 @@
<template>
<el-container class="panel-container">
<el-tabs :active-name="activeTab" style="height: 100%; overflow: hidden">
<el-tab-pane :label="i18nt('designer.hint.widgetSetting')" name="1">
<el-scrollbar class="setting-scrollbar" :style="{height: scrollerHeight}">
<template v-if="!!designer.selectedWidget && !designer.selectedWidget.category">
<el-form :model="optionModel" size="mini" label-position="left" label-width="120px" class="setting-form"
@submit.prevent>
<el-collapse v-model="widgetActiveCollapseNames" class="setting-collapse">
<el-collapse-item name="1" v-if="showCollapse(commonProps)" :title="i18nt('designer.setting.commonSetting')">
<template v-for="(editorName, propName) in commonProps">
<component v-if="hasPropEditor(propName, editorName)" :is="getPropEditor(propName, editorName)"
:designer="designer" :selected-widget="selectedWidget" :option-model="optionModel"></component>
</template>
</el-collapse-item>
<el-collapse-item name="2" v-if="showCollapse(advProps)" :title="i18nt('designer.setting.advancedSetting')">
<template v-for="(editorName, propName) in advProps">
<component v-if="hasPropEditor(propName, editorName)" :is="getPropEditor(propName, editorName)"
:designer="designer" :selected-widget="selectedWidget" :option-model="optionModel"></component>
</template>
</el-collapse-item>
<el-collapse-item name="3" v-if="showEventCollapse() && showCollapse(eventProps)" :title="i18nt('designer.setting.eventSetting')">
<template v-for="(editorName, propName) in eventProps">
<component v-if="hasPropEditor(propName, editorName)" :is="getPropEditor(propName, editorName)"
:designer="designer" :selected-widget="selectedWidget" :option-model="optionModel"></component>
</template>
</el-collapse-item>
</el-collapse>
</el-form>
</template>
<template v-if="(!!designer.selectedWidget && !!designer.selectedWidget.category)">
<el-form :model="optionModel" size="mini" label-position="left" label-width="120px" class="setting-form"
@submit.prevent>
<el-collapse v-model="widgetActiveCollapseNames" class="setting-collapse">
<el-collapse-item name="1" v-if="showCollapse(commonProps)" :title="i18nt('designer.setting.commonSetting')">
<template v-for="(editorName, propName) in commonProps">
<component v-if="hasPropEditor(propName, editorName)" :is="getPropEditor(propName, editorName)"
:designer="designer" :selected-widget="selectedWidget" :option-model="optionModel"></component>
</template>
</el-collapse-item>
<el-collapse-item name="2" v-if="showCollapse(advProps)" :title="i18nt('designer.setting.advancedSetting')">
<template v-for="(editorName, propName) in advProps">
<component v-if="hasPropEditor(propName, editorName)" :is="getPropEditor(propName, editorName)"
:designer="designer" :selected-widget="selectedWidget" :option-model="optionModel"></component>
</template>
</el-collapse-item>
<el-collapse-item name="3" v-if="showEventCollapse() && showCollapse(eventProps)" :title="i18nt('designer.setting.eventSetting')">
<template v-for="(editorName, propName) in eventProps">
<component v-if="hasPropEditor(propName, editorName)" :is="getPropEditor(propName, editorName)"
:designer="designer" :selected-widget="selectedWidget" :option-model="optionModel"></component>
</template>
</el-collapse-item>
</el-collapse>
</el-form>
</template>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane v-if="!!designer" :label="i18nt('designer.hint.formSetting')" name="2">
<el-scrollbar class="setting-scrollbar" :style="{height: scrollerHeight}">
<form-setting :designer="designer" :form-config="formConfig"></form-setting>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
<el-dialog :title="i18nt('designer.setting.editWidgetEventHandler')" v-model="showWidgetEventDialogFlag"
v-if="showWidgetEventDialogFlag" :show-close="true" custom-class="small-padding-dialog"
:close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="true">
<el-alert type="info" :closable="false" :title="eventHeader"></el-alert>
<code-editor :mode="'javascript'" :readonly="false" v-model="eventHandlerCode"></code-editor>
<el-alert type="info" :closable="false" title="}"></el-alert>
<template #footer>
<div class="dialog-footer">
<el-button @click="showWidgetEventDialogFlag = false">
{{i18nt('designer.hint.cancel')}}</el-button>
<el-button type="primary" @click="saveEventHandler">
{{i18nt('designer.hint.confirm')}}</el-button>
</div>
</template>
</el-dialog>
</el-container>
</template>
<script>
import CodeEditor from '@/components/code-editor/index'
import PropertyEditors from './property-editor/index'
import FormSetting from './form-setting'
import WidgetProperties from './propertyRegister'
import {
addWindowResizeHandler,
} from "@/utils/util"
import i18n from "@/utils/i18n"
import eventBus from "@/utils/event-bus"
const {COMMON_PROPERTIES, ADVANCED_PROPERTIES, EVENT_PROPERTIES} = WidgetProperties
export default {
name: "SettingPanel",
componentName: "SettingPanel",
mixins: [i18n],
components: {
CodeEditor,
FormSetting,
...PropertyEditors,
},
props: {
designer: Object,
selectedWidget: Object,
formConfig: Object,
},
inject: ['getDesignerConfig'],
data() {
return {
designerConfig: this.getDesignerConfig(),
scrollerHeight: 0,
activeTab: "2",
widgetActiveCollapseNames: ['1', '3'], //['1', '2', '3'],
formActiveCollapseNames: ['1', '2'],
commonProps: COMMON_PROPERTIES,
advProps: ADVANCED_PROPERTIES,
eventProps: EVENT_PROPERTIES,
showWidgetEventDialogFlag: false,
eventHandlerCode: '',
curEventName: '',
eventHeader: '',
subFormChildWidgetFlag: false,
}
},
computed: {
optionModel: {
get() {
return this.selectedWidget.options
},
set(newValue) {
this.selectedWidget.options = newValue
}
},
},
watch: {
'designer.selectedWidget': {
handler(val) {
if (!!val) {
this.activeTab = "1"
}
}
},
'selectedWidget.options': { //JSON
deep: true,
handler() {
this.designer.saveCurrentHistoryStep()
}
},
formConfig: {
deep: true,
handler() {
this.designer.saveCurrentHistoryStep()
}
},
},
created() {
eventBus.$on('editEventHandler', function (eventName, eventParams) {
this.editEventHandler(eventName, eventParams)
})
this.designer.handleEvent('form-css-updated', (cssClassList) => {
this.designer.setCssClassList(cssClassList)
})
},
mounted() {
if (!this.designer.selectedWidget) {
this.activeTab = "2"
} else {
this.activeTab = "1"
}
this.scrollerHeight = window.innerHeight - 56 - 48 + 'px'
addWindowResizeHandler(() => {
this.$nextTick(() => {
this.scrollerHeight = window.innerHeight - 56 - 48 + 'px'
//console.log(this.scrollerHeight)
})
})
},
methods: {
showEventCollapse() {
if (this.designerConfig['eventCollapse'] === undefined) {
return true
}
return !!this.designerConfig['eventCollapse']
},
hasPropEditor(propName, editorName) {
if (!editorName) {
return false
}
let originalPropName = propName.replace(this.selectedWidget.type + '-', '') //-
return this.designer.hasConfig(this.selectedWidget, originalPropName)
},
getPropEditor(propName, editorName) {
let originalPropName = propName.replace(this.selectedWidget.type + '-', '') //-
let ownPropEditorName = `${this.selectedWidget.type}-${originalPropName}-editor`
//console.log(ownPropEditorName, this.$options.components[ownPropEditorName])
if (!!this.$options.components[ownPropEditorName]) { //
return ownPropEditorName
}
return !!this.$root.$options.components[ownPropEditorName] ? ownPropEditorName : editorName //
},
showCollapse(propsObj) {
let result = false
for (let propName in propsObj) {
if (!propsObj.hasOwnProperty(propName)) {
continue
}
if (this.hasPropEditor(propName, propsObj[propName])) {
result = true
break
}
}
return result
},
editEventHandler(eventName, eventParams) {
this.curEventName = eventName
this.eventHeader = `${this.optionModel.name}.${eventName}(${eventParams.join(', ')}) {`
this.eventHandlerCode = this.selectedWidget.options[eventName] || ''
//
if ((eventName === 'onValidate') && (!this.optionModel['onValidate'])) {
this.eventHandlerCode = " /* sample code */\n /*\n if ((value > 100) || (value < 0)) {\n callback(new Error('error message')) //fail\n } else {\n callback(); //pass\n }\n */"
}
this.showWidgetEventDialogFlag = true
},
saveEventHandler() {
this.selectedWidget.options[this.curEventName] = this.eventHandlerCode
this.showWidgetEventDialogFlag = false
},
}
}
</script>
<style lang="scss" scoped>
.panel-container {
padding: 0 8px;
}
.setting-scrollbar {
:deep(.el-scrollbar__wrap) {
overflow-x: hidden; /* IE浏览器隐藏水平滚动条箭头 */
}
}
.setting-collapse {
:deep(.el-collapse-item__content) {
padding-bottom: 6px;
}
:deep(.el-collapse-item__header) {
font-style: italic;
font-weight: bold;
}
}
.setting-form {
:deep(.el-form-item__label) {
font-size: 13px;
//text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
:deep(.el-form-item--mini.el-form-item) {
margin-bottom: 6px;
}
}
/* 隐藏Chrome浏览器中el-input数字输入框右侧的上下调整小箭头 */
:deep(.hide-spin-button) input::-webkit-outer-spin-button,
:deep(.hide-spin-button) input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
}
/* 隐藏Firefox浏览器中el-input数字输入框右侧的上下调整小箭头 */
:deep(.hide-spin-button) input[type="number"] {
-moz-appearance: textfield;
}
:deep(.custom-divider.el-divider--horizontal) {
margin: 10px 0;
}
:deep(.custom-divider-margin-top.el-divider--horizontal) {
margin: 20px 0;
}
.small-padding-dialog {
:deep(.el-dialog__body) {
padding: 6px 15px 12px 15px;
}
}
</style>

View File

@ -0,0 +1,242 @@
<template>
<div class="option-items-pane">
<el-radio-group v-if="(selectedWidget.type === 'radio') || ((selectedWidget.type === 'select') && !selectedWidget.options.multiple)"
v-model="optionModel.defaultValue" @change="emitDefaultValueChange">
<draggable tag="ul" :list="optionModel.optionItems"
v-bind="{group:'optionsGroup', ghostClass: 'ghost', handle: '.drag-option'}">
<template #item="{ element: option, index: idx }">
<li>
<el-radio :label="option.value">
<el-input v-model="option.value" size="mini" style="width: 100px"></el-input>
<el-input v-model="option.label" size="mini" style="width: 100px"></el-input>
<i class="iconfont icon-drag drag-option"></i>
<el-button circle plain size="mini" type="danger" @click="deleteOption(option, idx)"
icon="el-icon-minus" class="col-delete-button"></el-button>
</el-radio>
</li>
</template>
</draggable>
</el-radio-group>
<el-checkbox-group v-else-if="(selectedWidget.type === 'checkbox') || ((selectedWidget.type === 'select') && selectedWidget.options.multiple)"
v-model="optionModel.defaultValue" @change="emitDefaultValueChange">
<draggable tag="ul" :list="optionModel.optionItems"
v-bind="{group:'optionsGroup', ghostClass: 'ghost', handle: '.drag-option'}">
<template #item="{ element: option, index: idx }">
<li>
<el-checkbox :label="option.value">
<el-input v-model="option.value" size="mini" style="width: 100px"></el-input>
<el-input v-model="option.label" size="mini" style="width: 100px"></el-input>
<i class="iconfont icon-drag drag-option"></i>
<el-button circle plain size="mini" type="danger" @click="deleteOption(option, idx)"
icon="el-icon-minus" class="col-delete-button"></el-button>
</el-checkbox>
</li>
</template>
</draggable>
</el-checkbox-group>
<el-cascader v-else-if="(selectedWidget.type === 'cascader')"
v-model="optionModel.defaultValue" :options="optionModel.optionItems"
@change="emitDefaultValueChange"
:placeholder="i18nt('render.hint.selectPlaceholder')" style="width: 100%">
</el-cascader>
<div v-if="(selectedWidget.type === 'cascader')">
<el-button type="text" @click="importCascaderOptions">{{i18nt('designer.setting.importOptions')}}</el-button>
<el-button type="text" @click="resetDefault">{{i18nt('designer.setting.resetDefault')}}</el-button>
</div>
<div v-if="(selectedWidget.type === 'radio') || (selectedWidget.type === 'checkbox') || (selectedWidget.type === 'select')">
<el-button type="text" @click="addOption">{{i18nt('designer.setting.addOption')}}</el-button>
<el-button type="text" @click="importOptions">{{i18nt('designer.setting.importOptions')}}</el-button>
<el-button type="text" @click="resetDefault">{{i18nt('designer.setting.resetDefault')}}</el-button>
</div>
<el-dialog :title="i18nt('designer.setting.importOptions')" v-model="showImportDialogFlag"
v-if="showImportDialogFlag" :show-close="true" custom-class="small-padding-dialog"
:close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="true">
<el-form-item>
<el-input type="textarea" rows="10" v-model="optionLines"></el-input>
</el-form-item>
<template #footer>
<div class="dialog-footer">
<el-button size="large" type="primary" @click="saveOptions">{{i18nt('designer.hint.confirm')}}</el-button>
<el-button size="large" type="" @click="showImportDialogFlag = false">{{i18nt('designer.hint.cancel')}}</el-button>
</div>
</template>
</el-dialog>
<el-dialog :title="i18nt('designer.setting.importOptions')" v-model="showImportCascaderDialogFlag"
v-if="showImportCascaderDialogFlag" :show-close="true" custom-class="small-padding-dialog"
:close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="true">
<code-editor v-model="cascaderOptions" mode="json" :readonly="false"></code-editor>
<template #footer>
<div class="dialog-footer">
<el-button size="large" type="primary" @click="saveCascaderOptions">{{i18nt('designer.hint.confirm')}}</el-button>
<el-button size="large" type="" @click="showImportCascaderDialogFlag = false">{{i18nt('designer.hint.cancel')}}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import Draggable from 'vuedraggable'
import CodeEditor from '@/components/code-editor/index'
import i18n from "@/utils/i18n";
export default {
name: "OptionItemsSetting",
mixins: [i18n],
components: {
Draggable,
//CodeEditor: () => import('@/components/code-editor/index'),
CodeEditor,
},
props: {
designer: Object,
selectedWidget: Object,
},
data() {
return {
showImportDialogFlag: false,
optionLines: '',
cascaderOptions: '',
showImportCascaderDialogFlag: false,
//separator: '||',
separator: ',',
}
},
computed: {
optionModel() {
return this.selectedWidget.options
},
},
watch: {
'selectedWidget.options': {
deep: true,
handler(val) {
//console.log('888888', 'Options change!')
}
},
},
methods: {
emitDefaultValueChange() {
if (!!this.designer && !!this.designer.formWidget) {
let fieldWidget = this.designer.formWidget.getWidgetRef(this.selectedWidget.options.name)
if (!!fieldWidget && !!fieldWidget.refreshDefaultValue) {
fieldWidget.refreshDefaultValue()
}
}
},
deleteOption(option, index) {
this.optionModel.optionItems.splice(index, 1)
},
addOption() {
let newValue = this.optionModel.optionItems.length + 1
this.optionModel.optionItems.push({
value: newValue,
label: 'new option'
})
},
importOptions() {
this.optionLines = ''
if (this.optionModel.optionItems.length > 0) {
this.optionModel.optionItems.forEach((opt) => {
if (opt.value === opt.label) {
this.optionLines += opt.value + '\n'
} else {
this.optionLines += opt.value + this.separator + opt.label + '\n'
}
})
}
this.showImportDialogFlag = true
},
saveOptions() {
let lineArray = this.optionLines.split('\n')
//console.log('test', lineArray)
if (lineArray.length > 0) {
this.optionModel.optionItems = []
lineArray.forEach((optLine) => {
if (!!optLine && !!optLine.trim()) {
if (optLine.indexOf(this.separator) !== -1) {
this.optionModel.optionItems.push({
value: optLine.split(this.separator)[0],
label: optLine.split(this.separator)[1]
})
} else {
this.optionModel.optionItems.push({
value: optLine,
label: optLine
})
}
}
})
} else {
this.optionModel.optionItems = []
}
this.showImportDialogFlag = false
},
resetDefault() {
if ((this.selectedWidget.type === 'checkbox') ||
((this.selectedWidget.type === 'select') && this.selectedWidget.options.multiple)) {
this.optionModel.defaultValue = []
} else {
this.optionModel.defaultValue = ''
}
this.emitDefaultValueChange()
},
importCascaderOptions() {
this.cascaderOptions = JSON.stringify(this.optionModel.optionItems, null, ' ')
this.showImportCascaderDialogFlag = true
},
saveCascaderOptions() {
try {
let newOptions = JSON.parse(this.cascaderOptions)
this.optionModel.optionItems = newOptions
//TODO:
this.showImportCascaderDialogFlag = false
} catch (ex) {
this.$message.error(this.i18nt('designer.hint.invalidOptionsData') + ex.message)
}
},
}
}
</script>
<style lang="scss" scoped>
.option-items-pane ul {
padding-inline-start: 6px;
padding-left: 6px; /* 重置IE11默认样式 */
}
li.ghost{
background: #fff;
border: 2px dotted $--color-primary;
}
.drag-option {
cursor: move;
}
.small-padding-dialog :deep(.el-dialog__body) {
padding: 10px 15px;
}
.dialog-footer .el-button {
width: 100px;
}
</style>

View File

@ -0,0 +1,173 @@
import {translate} from "@/utils/i18n"
import emitter from '@/utils/emitter'
export const createInputTextEditor = function (propName, propLabelKey) {
return {
props: {
optionModel: Object,
},
render(h) {
return (
<el-form-item label={translate(propLabelKey)}>
<el-input type="text" v-model={this.optionModel[propName]} />
</el-form-item>
)
}
}
}
export const createInputNumberEditor = function (propName, propLabelKey) {
return {
props: {
optionModel: Object,
},
methods: {
updateValue(newValue) {
if ((newValue === undefined) || (newValue === null) || isNaN(newValue)) {
this.optionModel[propName] = null
} else {
this.optionModel[propName] = Number(newValue)
}
},
},
render(h) {
return (
<el-form-item label={translate(propLabelKey)}>
<el-input-number type="text" v-model={this.optionModel[propName]}
onChange={this.updateValue} style="width: 100%" />
</el-form-item>
)
}
}
}
export const createBooleanEditor = function (propName, propLabelKey) {
return {
props: {
optionModel: Object,
},
render(h) {
return (
<el-form-item label={translate(propLabelKey)}>
<el-checkbox v-model={this.optionModel[propName]} />
</el-form-item>
)
}
}
}
export const createCheckboxGroupEditor = function (propName, propLabelKey, configs) {
return {
props: {
optionModel: Object,
},
render(h) {
return (
<el-form-item label={translate(propLabelKey)}>
<el-checkbox-group v-model={this.optionModel[propName]}>
{
configs.optionItems.map(item => {
return <el-checkbox label={item.value}>{item.label}</el-checkbox>
})
}
</el-checkbox-group>
</el-form-item>
)
}
}
}
export const createRadioGroupEditor = function (propName, propLabelKey, configs) {
return {
props: {
optionModel: Object,
},
render(h) {
return (
<el-form-item label={translate(propLabelKey)}>
<el-radio-group v-model={this.optionModel[propName]}>
{
configs.optionItems.map(item => {
return <el-radio label={item.value}>{item.label}</el-radio>
})
}
</el-radio-group>
</el-form-item>
)
}
}
}
export const createRadioButtonGroupEditor = function (propName, propLabelKey, configs) {
return {
props: {
optionModel: Object,
},
render(h) {
return (
<el-form-item label={translate(propLabelKey)}>
<el-radio-group v-model={this.optionModel[propName]}>
{
configs.optionItems.map(item => {
return <el-radio-button label={item.value}>{item.label}</el-radio-button>
})
}
</el-radio-group>
</el-form-item>
)
}
}
}
export const createSelectEditor = function (propName, propLabelKey, configs) {
return {
props: {
optionModel: Object,
},
render(h) {
return (
<el-form-item label={translate(propLabelKey)}>
<el-select v-model={this.optionModel[propName]}>
{
configs.optionItems.map(item => {
return <el-option label={item.label} value={item.value} />
})
}
</el-select>
</el-form-item>
)
}
}
}
export const createEventHandlerEditor = function (eventPropName, eventParams) {
return {
props: {
optionModel: Object,
},
mixins: [emitter],
methods: {
editEventHandler() {
this.dispatch('SettingPanel', 'editEventHandler', [eventPropName, [...eventParams]])
},
},
render(h) {
return (
<el-form-item label={eventPropName} label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round onClick={this.editEventHandler}>
{translate('designer.setting.addEventHandler')}</el-button>
</el-form-item>
)
}
}
}
export const createEmptyEditor = function () {
return {
render() {
return <div style="display: none" />
}
}
}

View File

@ -0,0 +1,24 @@
<template>
<el-form-item :label="i18nt('designer.setting.allowCreate')">
<el-checkbox v-model="optionModel.allowCreate"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "allowCreate-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,28 @@
<template>
<div>
<el-form-item label-width="0">
<el-divider class="custom-divider">{{i18nt('designer.setting.inputButton')}}</el-divider>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.appendButton')">
<el-checkbox v-model="optionModel.appendButton"></el-checkbox>
</el-form-item>
</div>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "appendButton-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.appendButtonDisabled')">
<el-checkbox v-model="optionModel.appendButtonDisabled"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "appendButtonDisabled-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.automaticDropdown')">
<el-checkbox v-model="optionModel.automaticDropdown"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "automaticDropdown-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.border')">
<el-checkbox v-model="optionModel.border"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "border-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.appendButtonIcon')">
<el-input type="text" v-model="optionModel.buttonIcon"></el-input>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "buttonIcon-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.buttonStyle')">
<el-checkbox v-model="optionModel.buttonStyle"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "buttonStyle-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.clearable')">
<el-checkbox v-model="optionModel.clearable"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "clearable-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,40 @@
<template>
<div>
<el-form-item :label="i18nt('designer.setting.widgetColumnWidth')" v-show="!!subFormChildWidgetFlag">
<el-input type="text" v-model="optionModel.columnWidth"></el-input>
</el-form-item>
</div>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "columnWidth-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
data() {
return {
//subFormChildWidgetFlag: false,
subFormChildWidgetFlag: true,
}
},
created() {
},
mounted() {
this.designer.handleEvent('field-selected', (parentWidget) => {
this.subFormChildWidgetFlag = !!parentWidget && (parentWidget.type === 'sub-form');
//console.log('subFormChildWidgetFlag', this.subFormChildWidgetFlag)
})
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,24 @@
<template>
<el-form-item :label="i18nt('designer.setting.colOffsetTitle')">
<el-input-number v-model.number="optionModel.offset" :min="0" :max="24"
style="width: 100%"></el-input-number>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n";
export default {
name: "grid-col-offset-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,24 @@
<template>
<el-form-item :label="i18nt('designer.setting.colPullTitle')">
<el-input-number v-model.number="optionModel.pull" :min="0" :max="24"
style="width: 100%"></el-input-number>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n";
export default {
name: "grid-col-pull-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,24 @@
<template>
<el-form-item :label="i18nt('designer.setting.colPushTitle')">
<el-input-number v-model.number="optionModel.push" :min="0" :max="24"
style="width: 100%"></el-input-number>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n";
export default {
name: "grid-col-push-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.responsive')">
<el-checkbox v-model="optionModel.responsive"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "grid-col-responsive-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,48 @@
<template>
<div>
<el-form-item :label="i18nt('designer.setting.colSpanTitle')" v-if="!optionModel.responsive">
<el-input-number v-model.number="optionModel.span" :min="1" :max="24"
style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.colSpanTitle') + '(PC)'"
v-if="!!optionModel.responsive && (formConfig.layoutType === 'PC')">
<el-input-number v-model.number="optionModel.md" :min="1" :max="24"
style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.colSpanTitle') + '(Pad)'"
v-if="!!optionModel.responsive && (formConfig.layoutType === 'Pad')">
<el-input-number v-model.number="optionModel.sm" :min="1" :max="24"
style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.colSpanTitle') + '(H5)'"
v-if="!!optionModel.responsive && (formConfig.layoutType === 'H5')">
<el-input-number v-model.number="optionModel.xs" :min="1" :max="24"
style="width: 100%"></el-input-number>
</el-form-item>
</div>
</template>
<script>
import i18n from "@/utils/i18n";
export default {
name: "grid-col-span-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
computed: {
formConfig() {
return this.designer.formConfig
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,28 @@
<template>
<div>
<el-form-item :label="i18nt('designer.setting.gridColHeight')">
<el-input type="number" v-model="optionModel.colHeight" @input="inputNumberHandler"
min="0" class="hide-spin-button"></el-input>
</el-form-item>
</div>
</template>
<script>
import i18n from "@/utils/i18n"
import propertyMixin from "@/components/form-designer/setting-panel/property-editor/propertyMixin"
export default {
name: "colHeight-editor",
mixins: [i18n, propertyMixin],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,81 @@
<template>
<div>
<el-form-item label-width="0">
<el-divider class="custom-divider">{{i18nt('designer.setting.columnSetting')}}</el-divider>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.gutter')">
<el-input-number v-model="optionModel.gutter" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.colsOfGrid')"></el-form-item>
<el-form-item label-width="0">
<li v-for="(colItem, colIdx) in selectedWidget.cols" :key="colIdx" class="col-item">
<span class="col-span-title">{{i18nt('designer.setting.colSpanTitle')}}{{colIdx + 1}}</span>
<el-input-number v-model.number="colItem.options.span" :min="1" :max="24"
@change="(newValue, oldValue) => spanChanged(selectedWidget, colItem, colIdx, newValue, oldValue)"
class="cell-span-input"></el-input-number>
<el-button circle plain size="mini" type="danger" @click="deleteCol(selectedWidget, colIdx)"
icon="el-icon-minus" class="col-delete-button"></el-button>
</li>
<div>
<el-button type="text" @click="addNewCol(selectedWidget)">{{i18nt('designer.setting.addColumn')}}</el-button>
</div>
</el-form-item>
</div>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "gutter-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
methods: {
spanChanged(curGrid) {
let spanSum = 0
curGrid.cols.forEach((colItem) => {
spanSum += colItem.options.span
})
if (spanSum > 24) {
//this.$message.info('24')
console.log('列栅格之和超出24')
//TODO:
}
this.designer.saveCurrentHistoryStep()
},
deleteCol(curGrid, colIdx) {
this.designer.deleteColOfGrid(curGrid, colIdx)
this.designer.emitHistoryChange()
},
addNewCol(curGrid) {
this.designer.addNewColOfGrid(curGrid)
this.designer.emitHistoryChange()
},
}
}
</script>
<style lang="scss" scoped>
li.col-item {
list-style: none;
span.col-span-title {
display: inline-block;
font-size: 13px;
width: 120px;
}
.col-delete-button {
margin-left: 6px;
}
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.showBlankRow')">
<el-checkbox v-model="optionModel.showBlankRow"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "showBlankRow-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.showRowNumber')">
<el-checkbox v-model="optionModel.showRowNumber"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "showRowNumber-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,36 @@
<template>
<el-form-item :label="i18nt('designer.setting.labelAlign')">
<el-radio-group v-model="optionModel.labelAlign" class="radio-group-custom">
<el-radio-button label="label-left-align">
{{i18nt('designer.setting.leftAlign')}}</el-radio-button>
<el-radio-button label="label-center-align">
{{i18nt('designer.setting.centerAlign')}}</el-radio-button>
<el-radio-button label="label-right-align">
{{i18nt('designer.setting.rightAlign')}}</el-radio-button>
</el-radio-group>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n";
export default {
name: "sub-form-labelAlign-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style lang="scss" scoped>
.radio-group-custom {
:deep(.el-radio-button__inner) {
padding-left: 12px;
padding-right: 12px;
}
}
</style>

View File

@ -0,0 +1,121 @@
<!--
因tabs属性并不包含于options属性之中故只能跟其他options属性之内的属性值合并设置此处选择与customClass合并
-->
<template>
<div>
<el-form-item :label="i18nt('designer.setting.customClass')">
<el-select v-model="optionModel.customClass" multiple filterable allow-create
default-first-option>
<el-option v-for="(item, idx) in cssClassList" :key="idx" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="i18nt('designer.setting.tabPaneSetting')"></el-form-item>
<el-form-item label-width="0" class="panes-setting">
<draggable tag="ul" :list="selectedWidget.tabs" item-key="id"
v-bind="{group:'panesGroup', ghostClass: 'ghost', handle: '.drag-option'}">
<template #item="{ element: tpItem, index: tpIdx }">
<li class="col-item">
<!-- span style="margin-right: 12px">{{tpIdx + 1}}</span -->
<el-checkbox v-model="tpItem.options.active" disabled @change="(evt) => onTabPaneActiveChange(evt, tpItem)"
style="margin-right: 8px">{{i18nt('designer.setting.paneActive')}}</el-checkbox>
<el-input type="text" v-model="tpItem.options.label" style="width: 155px"></el-input>
<i class="iconfont icon-drag drag-option"></i>
<el-button circle plain size="mini" type="danger" @click="deleteTabPane(selectedWidget, tpIdx)"
icon="el-icon-minus" class="col-delete-button"></el-button>
</li>
</template>
<div>
<el-button type="text" @click="addTabPane(selectedWidget)">{{i18nt('designer.setting.addTabPane')}}</el-button>
</div>
</draggable>
</el-form-item>
</div>
</template>
<script>
import i18n from "@/utils/i18n"
import Draggable from 'vuedraggable'
import {deepClone} from "@/utils/util";
export default {
name: "tab-customClass-editor",
componentName: 'PropertyEditor',
mixins: [i18n],
components: {
Draggable,
},
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
data() {
return {
cssClassList: [],
}
},
created() {
this.cssClassList = deepClone(this.designer.getCssClassList())
//css
this.designer.handleEvent('form-css-updated', (cssClassList) => {
this.cssClassList = cssClassList
})
},
methods: {
onTabPaneActiveChange(evt, tpItem) {
//TODO: !!!
},
addTabPane(curTabs) {
this.designer.addTabPaneOfTabs(curTabs)
this.designer.emitHistoryChange()
},
deleteTabPane(curTabs, tpIdx) {
if (curTabs.tabs.length === 1) {
this.$message.info(this.i18nt('designer.hint.lastPaneCannotBeDeleted'))
return
}
this.designer.deleteTabPaneOfTabs(curTabs, tpIdx)
this.designer.emitHistoryChange()
},
}
}
</script>
<style lang="scss" scoped>
li.col-item {
list-style: none;
span.col-span-title {
display: inline-block;
font-size: 13px;
width: 120px;
}
.col-delete-button {
margin-left: 6px;
}
}
.panes-setting {
ul {
padding-inline-start: 0;
padding-left: 0; /* 重置IE11默认样式 */
margin: 0;
}
.drag-option {
cursor: move;
}
li.ghost {
background: #fff;
border: 2px dotted $--color-primary;
}
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.cellHeight')">
<el-input type="text" v-model="optionModel.cellHeight"></el-input>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "cellHeight-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.cellWidth')">
<el-input type="text" v-model="optionModel.cellWidth"></el-input>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "cellWidth-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,40 @@
<template>
<el-form-item :label="i18nt('designer.setting.customClass')">
<el-select v-model="optionModel.customClass" multiple filterable allow-create
default-first-option>
<el-option v-for="(item, idx) in cssClassList" :key="idx" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n";
import {deepClone} from "@/utils/util";
export default {
name: "customClass-editor",
componentName: 'PropertyEditor',
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
data() {
return {
cssClassList: [],
}
},
created() {
this.cssClassList = deepClone(this.designer.getCssClassList())
//css
this.designer.handleEvent('form-css-updated', (cssClassList) => {
this.cssClassList = cssClassList
})
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,25 @@
<template>
<el-form-item :label="i18nt('designer.setting.defaultValue')">
<el-input v-if="!hasConfig('optionItems')" type="text" v-model="optionModel.defaultValue"
@change="emitDefaultValueChange"></el-input>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
import propertyMixin from "@/components/form-designer/setting-panel/property-editor/propertyMixin"
export default {
name: "defaultValue-editor",
mixins: [i18n, propertyMixin],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.disabled')">
<el-checkbox v-model="optionModel.disabled"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "disabled-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,26 @@
<template>
<el-form-item :label="i18nt('designer.setting.displayStyle')">
<el-radio-group v-model="optionModel.displayStyle">
<el-radio label="inline">{{i18nt('designer.setting.inlineLayout')}}</el-radio>
<el-radio label="block">{{i18nt('designer.setting.blockLayout')}}</el-radio>
</el-radio-group>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "displayStyle-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,24 @@
<template>
<el-form-item :label="i18nt('designer.setting.editable')">
<el-checkbox v-model="optionModel.editable"></el-checkbox>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "editable-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<el-form-item :label="i18nt('designer.setting.endPlaceholder')">
<el-input type="text" v-model="optionModel.endPlaceholder"></el-input>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
export default {
name: "endPlaceholder-editor",
mixins: [i18n],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,12 @@
import emitter from '@/utils/emitter'
export default {
mixins: [emitter],
created() {},
methods: {
editEventHandler(eventName, eventParams) {
this.dispatch('SettingPanel', 'editEventHandler', [eventName, [...eventParams]])
},
}
}

View File

@ -0,0 +1,30 @@
<template>
<el-form-item label="onBeforeUpload" label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round @click="editEventHandler('onBeforeUpload', eventParams)">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
import eventMixin from "@/components/form-designer/setting-panel/property-editor/event-handler/eventMixin"
export default {
name: "onBeforeUpload-editor",
mixins: [i18n, eventMixin],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
data() {
return {
eventParams: ['file'],
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,30 @@
<template>
<el-form-item label="onBlur" label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round @click="editEventHandler('onBlur', eventParams)">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
import eventMixin from "@/components/form-designer/setting-panel/property-editor/event-handler/eventMixin"
export default {
name: "onBlur-editor",
mixins: [i18n, eventMixin],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
data() {
return {
eventParams: ['event'],
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,30 @@
<template>
<el-form-item label="onChange" label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round @click="editEventHandler('onChange', eventParams)">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
import eventMixin from "@/components/form-designer/setting-panel/property-editor/event-handler/eventMixin"
export default {
name: "onChange-editor",
mixins: [i18n, eventMixin],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
data() {
return {
eventParams: ['value', 'oldValue', 'subFormData', 'rowId'],
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,30 @@
<template>
<el-form-item label="onClick" label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round @click="editEventHandler('onClick', eventParams)">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
import eventMixin from "@/components/form-designer/setting-panel/property-editor/event-handler/eventMixin"
export default {
name: "onClick-editor",
mixins: [i18n, eventMixin],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
data() {
return {
eventParams: [],
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,31 @@
<template>
<el-form-item label="onCreated" label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round @click="editEventHandler('onCreated', eventParams)">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
import eventMixin from "@/components/form-designer/setting-panel/property-editor/event-handler/eventMixin"
export default {
name: "onCreated-editor",
mixins: [i18n, eventMixin],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
data() {
return {
eventParams: [],
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,30 @@
<template>
<el-form-item label="onFocus" label-width="150px">
<el-button type="info" icon="el-icon-edit" plain round @click="editEventHandler('onFocus', eventParams)">
{{i18nt('designer.setting.addEventHandler')}}</el-button>
</el-form-item>
</template>
<script>
import i18n from "@/utils/i18n"
import eventMixin from "@/components/form-designer/setting-panel/property-editor/event-handler/eventMixin"
export default {
name: "onFocus-editor",
mixins: [i18n, eventMixin],
props: {
designer: Object,
selectedWidget: Object,
optionModel: Object,
},
data() {
return {
eventParams: ['event'],
}
}
}
</script>
<style scoped>
</style>

Some files were not shown because too many files have changed in this diff Show More