Explorar o código

Merge branch 'feature-workflow' into feature-workflow-and-mcp

# Conflicts:
#	src/i18n/index.ts
kagg886 hai 1 mes
pai
achega
8e75417609
Modificáronse 100 ficheiros con 13257 adicións e 0 borrados
  1. 8 0
      package.json
  2. 45 0
      src/api/flow/flowDemo.ts
  3. 127 0
      src/api/flow/flowForm.ts
  4. 143 0
      src/api/flow/flowModel.ts
  5. 94 0
      src/api/system/report/complaint-resolve-history.ts
  6. 25 0
      src/api/system/report/complaints.ts
  7. 8 0
      src/api/system/report/feedback.ts
  8. 22 0
      src/api/system/report/statistics.ts
  9. 140 0
      src/api/system/report/type.ts
  10. 49 0
      src/components/form-create-designer/components/DragBox.vue
  11. 201 0
      src/components/form-create-designer/components/DragTool.vue
  12. 546 0
      src/components/form-create-designer/components/EventConfig.vue
  13. 206 0
      src/components/form-create-designer/components/FcDesigner.vue
  14. 263 0
      src/components/form-create-designer/components/FetchConfig.vue
  15. 153 0
      src/components/form-create-designer/components/FieldInput.vue
  16. 304 0
      src/components/form-create-designer/components/FnConfig.vue
  17. 251 0
      src/components/form-create-designer/components/FnEditor.vue
  18. 103 0
      src/components/form-create-designer/components/FnInput.vue
  19. 125 0
      src/components/form-create-designer/components/HtmlEditor.vue
  20. 93 0
      src/components/form-create-designer/components/JsonPreview.vue
  21. 72 0
      src/components/form-create-designer/components/PropsInput.vue
  22. 75 0
      src/components/form-create-designer/components/Required.vue
  23. 25 0
      src/components/form-create-designer/components/Row.vue
  24. 142 0
      src/components/form-create-designer/components/Struct.vue
  25. 121 0
      src/components/form-create-designer/components/StructEditor.vue
  26. 162 0
      src/components/form-create-designer/components/TableOptions.vue
  27. 166 0
      src/components/form-create-designer/components/TreeOptions.vue
  28. 140 0
      src/components/form-create-designer/components/TypeSelect.vue
  29. 244 0
      src/components/form-create-designer/components/Validate.vue
  30. 88 0
      src/components/form-create-designer/components/ValueInput.vue
  31. 45 0
      src/components/form-create-designer/components/Warning.vue
  32. 165 0
      src/components/form-create-designer/components/language/LanguageConfig.vue
  33. 188 0
      src/components/form-create-designer/components/language/LanguageInput.vue
  34. 242 0
      src/components/form-create-designer/components/style/BorderInput.vue
  35. 166 0
      src/components/form-create-designer/components/style/BoxSizeInput.vue
  36. 269 0
      src/components/form-create-designer/components/style/BoxSpaceInput.vue
  37. 60 0
      src/components/form-create-designer/components/style/ColorInput.vue
  38. 118 0
      src/components/form-create-designer/components/style/ConfigItem.vue
  39. 174 0
      src/components/form-create-designer/components/style/FontInput.vue
  40. 164 0
      src/components/form-create-designer/components/style/RadiusInput.vue
  41. 329 0
      src/components/form-create-designer/components/style/ShadowContent.vue
  42. 93 0
      src/components/form-create-designer/components/style/ShadowInput.vue
  43. 118 0
      src/components/form-create-designer/components/style/SizeInput.vue
  44. 263 0
      src/components/form-create-designer/components/style/StyleConfig.vue
  45. 210 0
      src/components/form-create-designer/components/table/Table.vue
  46. 658 0
      src/components/form-create-designer/components/table/TableView.vue
  47. 390 0
      src/components/form-create-designer/components/tableForm/TableForm.vue
  48. 101 0
      src/components/form-create-designer/components/tableForm/TableFormColumnView.vue
  49. 45 0
      src/components/form-create-designer/components/tableForm/TableFormView.vue
  50. 43 0
      src/components/form-create-designer/config/base/field.js
  51. 116 0
      src/components/form-create-designer/config/base/form.js
  52. 26 0
      src/components/form-create-designer/config/base/style.js
  53. 15 0
      src/components/form-create-designer/config/base/validate.js
  54. 68 0
      src/components/form-create-designer/config/index.js
  55. 24 0
      src/components/form-create-designer/config/menu.js
  56. 45 0
      src/components/form-create-designer/config/rule/alert.js
  57. 49 0
      src/components/form-create-designer/config/rule/button.js
  58. 40 0
      src/components/form-create-designer/config/rule/card.js
  59. 121 0
      src/components/form-create-designer/config/rule/cascader.js
  60. 68 0
      src/components/form-create-designer/config/rule/checkbox.js
  61. 86 0
      src/components/form-create-designer/config/rule/col.js
  62. 30 0
      src/components/form-create-designer/config/rule/collapse.js
  63. 36 0
      src/components/form-create-designer/config/rule/collapseItem.js
  64. 53 0
      src/components/form-create-designer/config/rule/color.js
  65. 70 0
      src/components/form-create-designer/config/rule/date.js
  66. 64 0
      src/components/form-create-designer/config/rule/dateRange.js
  67. 31 0
      src/components/form-create-designer/config/rule/divider.js
  68. 31 0
      src/components/form-create-designer/config/rule/editor.js
  69. 53 0
      src/components/form-create-designer/config/rule/group.js
  70. 52 0
      src/components/form-create-designer/config/rule/html.js
  71. 32 0
      src/components/form-create-designer/config/rule/image.js
  72. 62 0
      src/components/form-create-designer/config/rule/input.js
  73. 49 0
      src/components/form-create-designer/config/rule/number.js
  74. 52 0
      src/components/form-create-designer/config/rule/password.js
  75. 43 0
      src/components/form-create-designer/config/rule/radio.js
  76. 44 0
      src/components/form-create-designer/config/rule/rate.js
  77. 46 0
      src/components/form-create-designer/config/rule/row.js
  78. 70 0
      src/components/form-create-designer/config/rule/select.js
  79. 53 0
      src/components/form-create-designer/config/rule/slider.js
  80. 44 0
      src/components/form-create-designer/config/rule/space.js
  81. 47 0
      src/components/form-create-designer/config/rule/subForm.js
  82. 46 0
      src/components/form-create-designer/config/rule/switch.js
  83. 29 0
      src/components/form-create-designer/config/rule/tabPane.js
  84. 35 0
      src/components/form-create-designer/config/rule/table.js
  85. 79 0
      src/components/form-create-designer/config/rule/tableForm.js
  86. 43 0
      src/components/form-create-designer/config/rule/tableFormColumn.js
  87. 38 0
      src/components/form-create-designer/config/rule/tabs.js
  88. 79 0
      src/components/form-create-designer/config/rule/tag.js
  89. 50 0
      src/components/form-create-designer/config/rule/text.js
  90. 63 0
      src/components/form-create-designer/config/rule/textarea.js
  91. 62 0
      src/components/form-create-designer/config/rule/time.js
  92. 53 0
      src/components/form-create-designer/config/rule/timeRange.js
  93. 59 0
      src/components/form-create-designer/config/rule/transfer.js
  94. 70 0
      src/components/form-create-designer/config/rule/tree.js
  95. 77 0
      src/components/form-create-designer/config/rule/treeSelect.js
  96. 108 0
      src/components/form-create-designer/config/rule/upload.js
  97. 136 0
      src/components/form-create-designer/index.js
  98. 867 0
      src/components/form-create-designer/locale/en.js
  99. 881 0
      src/components/form-create-designer/locale/zh-cn.js
  100. 880 0
      src/components/form-create-designer/locale/zh-tw.js

+ 8 - 0
package.json

@@ -25,7 +25,13 @@
   "dependencies": {
     "@antv/g2plot": "2.4.20",
     "@element-plus/icons-vue": "2.0.9",
+    "@form-create/component-wangeditor": "^2.6.2",
+    "@form-create/designer": "3.2.11",
+    "@form-create/element-ui": "^3",
     "@guolao/vue-monaco-editor": "^1.5.5",
+    "@logicflow/core": "^2.0.16",
+    "@logicflow/extension": "^2.0.21",
+    "@vueuse/core": "9.0.1",
     "axios": "0.26.0",
     "clipboard": "2.0.11",
     "codemirror": "5.65.16",
@@ -65,6 +71,7 @@
     "vue-data-ui": "^2.4.17",
     "vue-grid-layout": "3.0.0-beta1",
     "vue-i18n": "9.1.10",
+    "vue-json-pretty": "^2.5.0",
     "vue-router": "4.0.13",
     "vue3-clipboard": "1.0.0",
     "vue3-cron": "1.1.8",
@@ -74,6 +81,7 @@
     "xlsx-with-styles": "0.17.2"
   },
   "devDependencies": {
+    "@types/downloadjs": "^1.4.6",
     "@types/js-cookie": "3.0.6",
     "@types/node": "17.0.21",
     "@types/nprogress": "0.2.0",

+ 45 - 0
src/api/flow/flowDemo.ts

@@ -0,0 +1,45 @@
+import request from '/@/utils/request'
+// 查询流程审批测试列表
+export function listFlowDemo(query:object) {
+  return request({
+    url: '/flow/flowDemo/list',
+    method: 'get',
+    params: query
+  })
+}
+// 查询流程审批测试详细
+export function getFlowDemo(id:number) {
+  return request({
+    url: '/flow/flowDemo/get',
+    method: 'get',
+    params: {
+      id: id.toString()
+    }
+  })
+}
+// 新增流程审批测试
+export function addFlowDemo(data:object) {
+  return request({
+    url: '/flow/flowDemo/add',
+    method: 'post',
+    data: data
+  })
+}
+// 修改流程审批测试
+export function updateFlowDemo(data:object) {
+  return request({
+    url: '/flow/flowDemo/edit',
+    method: 'put',
+    data: data
+  })
+}
+// 删除流程审批测试
+export function delFlowDemo(ids:number[]) {
+  return request({
+    url: '/flow/flowDemo/delete',
+    method: 'delete',
+    data:{
+      ids:ids
+    }
+  })
+}

+ 127 - 0
src/api/flow/flowForm.ts

@@ -0,0 +1,127 @@
+import request from '/@/utils/request'
+// 查询流程表单列表
+export function listFlowForm(query:object) {
+  return request({
+    url: '/flow/flowForm/list',
+    method: 'get',
+    params: query
+  })
+}
+
+//查询流程统计信息
+export function staticFlowForm() {
+  return request({
+    url: '/flow/flowForm/counts',
+    method: 'get'
+  })
+}
+
+// 查询所有待办
+export function listFlowFormTodo(query:object) {
+  return request({
+    url: '/flow/flowForm/prepareDataList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询流程表单详细
+export function getFlowForm(id:number) {
+  return request({
+    url: '/flow/flowForm/get',
+    method: 'get',
+    params: {
+      id: id.toString()
+    }
+  })
+}
+// 新增流程表单
+export function addFlowForm(data:object) {
+  return request({
+    url: '/flow/flowForm/add',
+    method: 'post',
+    data: data
+  })
+}
+// 修改流程表单
+export function updateFlowForm(data:object) {
+  return request({
+    url: '/flow/flowForm/edit',
+    method: 'put',
+    data: data
+  })
+}
+// 删除流程表单
+export function delFlowForm(ids:number[]) {
+  return request({
+    url: '/flow/flowForm/delete',
+    method: 'delete',
+    data:{
+      ids:ids
+    }
+  })
+}
+//部署表单
+export function genFlowForm(id:number) {
+  return request({
+    url: '/flow/flowForm/gen',
+    method: 'post',
+    data:{id}
+  })
+}
+
+// 新增流程表单数据
+export function addFlowFormData(data:object) {
+  return request({
+    url: '/flow/flowForm/addFormData',
+    method: 'post',
+    data: data
+  })
+}
+
+// 新增流程表单数据
+export function editFlowFormData(data:object) {
+  return request({
+    url: '/flow/flowForm/editFormData',
+    method: 'put',
+    data: data
+  })
+}
+
+// 获取流程表单列表数据
+export function ListFlowFormData(data:object) {
+  return request({
+    url: '/flow/flowForm/dataList',
+    method: 'get',
+    params: data
+  })
+}
+
+// 获取流程表单数据
+export function getFlowFormData(data:object) {
+  return request({
+    url: '/flow/flowForm/getFormData',
+    method: 'get',
+    params: data
+  })
+}
+
+// 删除流程表单数据
+export function delFlowFormData(data:object) {
+  return request({
+    url: '/flow/flowForm/delFormData',
+    method: 'delete',
+    data: data
+  })
+}
+
+
+export function getFlowFields(name: string) {
+	return request({
+		url: '/flow/flowForm/fields',
+		method: 'get',
+		params: {
+			name: name
+		}
+	})
+}

+ 143 - 0
src/api/flow/flowModel.ts

@@ -0,0 +1,143 @@
+import request from '/@/utils/request'
+// 查询流程模型列表
+export function listFlowModel(query:object) {
+  return request({
+    url: '/flow/flowModel/list',
+    method: 'get',
+    params: query
+  })
+}
+// 查询流程模型详细
+export function getFlowModel(id:number) {
+  return request({
+    url: '/flow/flowModel/get',
+    method: 'get',
+    params: {
+      id: id.toString()
+    }
+  })
+}
+// 新增流程模型
+export function addFlowModel(data:object) {
+  return request({
+    url: '/flow/flowModel/add',
+    method: 'post',
+    data: data
+  })
+}
+// 修改流程模型
+export function updateFlowModel(data:object) {
+  return request({
+    url: '/flow/flowModel/edit',
+    method: 'put',
+    data: data
+  })
+}
+// 删除流程模型
+export function delFlowModel(ids:number[]) {
+  return request({
+    url: '/flow/flowModel/delete',
+    method: 'delete',
+    data:{
+      ids:ids
+    }
+  })
+}
+//相关连表查询数据
+export function linkedDataSearch(){
+  return request({
+    url: '/flow/flowModel/linkedData',
+    method: 'get'
+  })
+}
+
+export function saveModeNode(data:object){
+  return request({
+    url: '/flow/flowModel/saveModelNode',
+    method: 'post',
+    data: data
+  })
+}
+
+export function getNodeData(modelId:number){
+  return request({
+    url: '/flow/flowModel/getModelNode',
+    method: 'get',
+    params:{modelId:modelId}
+  })
+}
+
+//发起流程
+export function wfStart(query:Object){
+  return request({
+    url: '/flow/flowModel/wfStart',
+    method: 'get',
+    params:query
+  })
+}
+
+//保存发起流程信息
+export function saveStartWf(data:Object){
+  return request({
+    url: '/flow/flowModel/wfSaveStart',
+    method: 'post',
+    data:data
+  })
+}
+
+
+export function getCheckData(query:Object){
+  return request({
+    url: '/flow/flowModel/checkData',
+    method: 'get',
+    params:query
+  })
+}
+
+//保存审批信息
+export function saveCheckWf(data:any){
+  if (data.isProxy){
+    return request({
+      url: '/flow/flowModel/proxy',
+      method: 'post',
+      data:data
+    })
+  }
+  return request({
+    url: '/flow/flowModel/checkSave',
+    method: 'post',
+    data:data
+  })
+}
+
+// 获取审批日志列表
+export function checkLog(params:Object){
+  return request({
+    url: '/flow/flowModel/log',
+    method: 'get',
+    params:params
+  })
+}
+
+export function getRunStep(params:Object){
+  return request({
+    url: '/flow/flowModel/runStep',
+    method: 'get',
+    params:params
+  })
+}
+//获取流程监控信息
+export function getMonitor(params:Object){
+  return request({
+    url: '/flow/flowModel/monitor',
+    method: 'get',
+    params:params
+  })
+}
+export function stopRun(ids:number[]){
+  return request({
+    url: '/flow/flowModel/stopRun',
+    method: 'put',
+    data:{ids:ids}
+  })
+}

+ 94 - 0
src/api/system/report/complaint-resolve-history.ts

@@ -0,0 +1,94 @@
+import { Complaint, ComplaintResolveHistory, ComplaintResolveHistoryInsertRequest, ComplaintStatus } from '/@/api/system/report/type'
+import { post } from '/@/utils/request'
+import { checkLog } from '/@/api/flow/flowModel'
+
+export default {
+	// list: (id: number): Promise<ComplaintResolveHistory[]> => get('/system/complaint/records',{ticketNo: id}).then((res: {data: ComplaintResolveHistory[]}) => res.data),
+	list: async (resolve: Complaint['actionBtn']): Promise<ComplaintResolveHistory[]> => {
+		// formId 19
+		// formTable sys_complaints
+		// pageNum 1
+		// pageSize 10
+		const data = await checkLog({
+			formId: resolve.wfFid,
+			formTable: resolve.wfType,
+			pageNum: 1,
+			pageSize: 500,
+			//@ts-ignore
+		})
+			.then((resp) => {
+				return (resp as unknown as { list: Array<WorkflowLog> }).list
+			})
+			.catch(() => undefined)
+
+		if (data === undefined) {
+			return [] as ComplaintResolveHistory[]
+		}
+
+		const rtn = data.map((data) => {
+			let status: ComplaintStatus | undefined = undefined
+			switch (data.btn) {
+				case '发起流程':
+					status = ComplaintStatus.PENDING
+					break
+				case '流程审核':
+					status = ComplaintStatus.PROCESSING
+					break
+				case '强制终止':
+					status = ComplaintStatus.COMPLETED
+					break
+			}
+			const history: ComplaintResolveHistory = {
+				id: data.id,
+				ticketNo: data.id,
+				status: status,
+				operator: data.approvalUser.userNickname,
+				description: data.content ?? '',
+				createdAt: data.createdAt,
+				updatedAt: data.createdAt,
+				// id: number;
+				// ticketNo: number;
+				// status: ComplaintStatus;
+				// operator: string;
+				// description: string
+				// createdAt: string;
+				// updatedAt: string
+			}
+
+			return history
+		})
+
+		const last = rtn.at(-1)
+
+		//最后一个流程审核标记为已完成。
+		if (resolve.title === '' && last?.status === ComplaintStatus.PROCESSING) {
+			last.status = ComplaintStatus.COMPLETED
+		}
+
+		return rtn
+	},
+
+	update: (params: ComplaintResolveHistoryInsertRequest) => post('/system/complaint/records/add', params),
+}
+
+//"id": 63,
+//                 "uid": 1,
+//                 "nodeName": "流程开始",
+//                 "content": "1",
+//                 "createdAt": "2025-08-01 11:58:31",
+//                 "approvalUser": {
+//                     "id": 1,
+//                     "userNickname": "超级管理员"
+//                 },
+//                 "btn": "发起流程"
+type WorkflowLog = {
+	id: number
+	uid: number
+	btn: '发起流程' | '流程审核' | '强制终止'
+	content?: string
+	createdAt: string
+	approvalUser: {
+		id: number
+		userNickname: string
+	}
+}

+ 25 - 0
src/api/system/report/complaints.ts

@@ -0,0 +1,25 @@
+import { get, post, del, put } from '/@/utils/request'
+import type {
+  Complaint,
+  CreateComplaintRequest,
+  UpdateComplaintRequest,
+  ComplaintQueryParams
+} from './type';
+
+export default {
+  // 获取投诉列表
+  getList: (params?: ComplaintQueryParams): Promise<{ list: Complaint[], total: number }> =>
+    get('/system/complaint/list', params),
+
+  // 创建投诉
+  add: (data: CreateComplaintRequest) => post('/system/complaint/add', data),
+
+  // 获取投诉详情
+  detail: (id: number): Promise<Complaint> => get(`/system/complaint/info`, {id}),
+
+  // 更新投诉
+  edit: (data: UpdateComplaintRequest) => put(`/system/complaint/edit`,data),
+
+  // 删除投诉
+  del: (ids: number[]) => del(`/system/complaint/delete`, {ids})
+};

+ 8 - 0
src/api/system/report/feedback.ts

@@ -0,0 +1,8 @@
+import { Feedback, FeedbackCreateParams, FeedbackQueryParams } from '/@/api/system/report/type'
+import { del, get, post } from '/@/utils/request'
+
+export default {
+	list: (params?: FeedbackQueryParams): Promise<{list: Feedback[],total: number}> => get('/system/complaintFeedback/list', params),
+	del: (ids: number[]): Promise<void> => del('/system/complaintFeedback/batch', {ids}),
+	create: (data: FeedbackCreateParams): Promise<void> => post('/system/complaintFeedback', data),
+}

+ 22 - 0
src/api/system/report/statistics.ts

@@ -0,0 +1,22 @@
+import { get } from '/@/utils/request';
+import type {
+  OverviewStatistics,
+  ComplaintTypeDistribution,
+  MonthlyTrend,
+  AreaDistribution,
+  StatisticsQueryParams
+} from './type';
+
+export default {
+  // 获取概要统计
+  overview: (params?: StatisticsQueryParams): Promise<OverviewStatistics> => get('/system/complaint/overview', params),
+
+  // 获取投诉类型分布
+  types: (): Promise<ComplaintTypeDistribution[]> => get('/system/complaint/types').then((res: {data: ComplaintTypeDistribution[]}) => res.data),
+
+  // 获取月度趋势
+  monthlyTrends: (): Promise<MonthlyTrend[]> => get('/system/complaint/monthly-trends').then((res: {data: MonthlyTrend[]}) => res.data),
+
+  // 获取区域分布
+  areas: (): Promise<AreaDistribution[]> => get('/system/complaint/areas').then((res: {data: AreaDistribution[]}) => res.data)
+};

+ 140 - 0
src/api/system/report/type.ts

@@ -0,0 +1,140 @@
+// 投诉区域枚举
+export type ComplaintArea = 'A区' | 'B区'
+
+export enum ComplaintStatus {
+	PENDING,
+	PROCESSING,
+	COMPLETED
+	// 'pending' | 'processing' | 'completed'
+}
+
+// 投诉实体类型
+export interface Complaint {
+  id: number;
+  title: string;
+  category: string;
+  source: string;
+	area: ComplaintArea;
+	complainantName: string;
+	contact?: string;
+	level: string;
+	content: string;
+	assignee: number;
+	status: ComplaintStatus;
+
+  createdAt: string;
+  updatedAt: string;
+
+	//FIXME: 流程控制需要
+	actionBtn: {
+		//"title": "",
+		//                     "type": "disabled",
+		//                     "wfFid": 17,
+		//                     "wfModelType": 2,
+		//                     "wfStatusField": "status",
+		//                     "wfTitle": "title",
+		//                     "wfType": "sys_complaints"
+		wfFid: number
+		wfType: string
+		type: string
+		title: string
+	};
+}
+
+export type CreateComplaintRequest = Pick<Complaint, 'title' | 'category' | 'source' | 'area' | 'complainantName' | 'contact' | 'level' | 'content'> & {
+	assignee?: number | null;
+}
+// 更新投诉请求类型
+export type UpdateComplaintRequest = CreateComplaintRequest & {
+  id: number;
+}
+
+export type BasePageQuery = {
+	pageNum?: number;
+	pageSize?: number;
+}
+
+// 投诉查询参数类型
+export type ComplaintQueryParams = BasePageQuery & {
+
+	dateRange?: [string, string]; //时间范围
+
+  name?: string; //关键词
+  status?: string; //状态
+  category?: string; //类型
+  level?: string; //等级
+
+	orderBy?: 'asc' | 'desc'; //排序方式
+}
+
+// 概要统计数据类型
+export interface OverviewStatistics {
+  totalComplaints: number;
+  pendingComplaints: number;
+  completedComplaints: number;
+  urgentComplaints: number;
+  averageProcessingTime: number;
+  completionRate: number;
+  satisfactionScore: number;
+  satisfactionTotal: number;
+}
+
+// 投诉类型分布数据类型
+export type ComplaintTypeDistribution = {
+  type: string;
+  count: number;
+  percentage: number;
+  trend: string;
+}
+
+// 月度趋势数据类型
+export type MonthlyTrend = {
+  month: string;
+  completionRate: number;
+  totalCount: number;
+  completedCount: number;
+}
+
+// 区域分布数据类型
+export type AreaDistribution = {
+  area: ComplaintArea;
+  count: number;
+  percentage: number;
+}
+
+// 统计查询参数类型
+export type StatisticsQueryParams = {
+  timeRange?: 'week' | 'month' | 'quarter' | 'year';
+}
+
+export type Feedback = {
+  id: number; // 反馈ID
+	surveyCode: string; // 问卷编号
+	ticketNo: number // 投诉编号
+	investigatorName: string; // 调查者姓名
+	contactInfo: string; // 联系信息
+	processingSpeed: string; // 处理速度(字典related_level)
+	staffAttitude: string; // 工作人员态度(字典related_level)
+	resolutionEffect: string // 解决效果(字典related_level)
+	otherSuggestions: string; // 其他建议
+	createdAt: string; // 创建时间
+}
+
+export type FeedbackQueryParams = BasePageQuery & {
+	investigatorName?: string;
+}
+
+export type FeedbackCreateParams = Omit<Feedback, 'id' | 'createdAt'>
+
+export type ComplaintResolveHistory = {
+	id: number;
+	ticketNo: number;
+	status: ComplaintStatus;
+	operator: string;
+	description: string
+	createdAt: string;
+	updatedAt: string
+}
+
+
+export type ComplaintResolveHistoryInsertRequest = Pick<ComplaintResolveHistory, 'status' | 'description' | 'ticketNo'>

+ 49 - 0
src/components/form-create-designer/components/DragBox.vue

@@ -0,0 +1,49 @@
+<script>
+import {defineComponent, h} from 'vue';
+import draggable from 'vuedraggable/src/vuedraggable';
+
+export default defineComponent({
+    name: 'DragBox',
+    props: ['rule', 'tag', 'formCreateInject', 'list'],
+    render(ctx) {
+        const attrs = {...ctx.$props.rule.props, ...ctx.$attrs};
+        let _class = '_fd-' + ctx.$props.tag + '-drag _fd-drag-box';
+        if (!Object.keys(ctx.$slots).length) {
+            _class += ' drag-holder';
+        }
+        attrs.class = _class;
+        attrs.modelValue = ctx.$props.list || [...ctx.$props.formCreateInject.children];
+
+        const keys = {};
+        if (ctx.$slots.default) {
+            const children = ctx.$slots.default();
+            children.forEach(v => {
+                if (v.key) {
+                    keys[v.key] = v;
+                }
+            })
+        }
+        return h(draggable, attrs, {
+            item: ({element, index}) => {
+                const key = element?.__fc__?.key;
+                if (key) {
+                    let vnode = keys['_' + element.slot];
+                    if (vnode) {
+                        vnode.children.forEach(v => {
+                            if (v.key === key + 'fc') {
+                                vnode = v
+                            }
+                        });
+                    } else {
+                        vnode = keys[key + 'fc'];
+                    }
+                    if (vnode) {
+                        return h('div', {class: '_fc-' + ctx.$props.tag + '-item _fd-drag-item', key}, vnode);
+                    }
+                }
+                return h('div', {class: '_fc-' + ctx.$props.tag + '-item _fd-drag-item', key: index}, null);
+            }
+        });
+    }
+});
+</script>

+ 201 - 0
src/components/form-create-designer/components/DragTool.vue

@@ -0,0 +1,201 @@
+<template>
+    <div class="_fd-drag-tool" @click.stop="active" :class="{active: fcx.active === id}">
+        <div class="_fd-drag-mask" v-if="mask"></div>
+        <div class="_fd-drag-l" v-if="!hiddenBtn" @click.stop>
+            <div class="_fd-drag-btn" v-if="dragBtn !== false" v-show="fcx.active === id" style="cursor: move;">
+                <i class="fc-icon icon-move"></i>
+            </div>
+        </div>
+        <div class="_fd-drag-r" v-if="btns !== false && !hiddenMenu">
+            <slot name="handle">
+                <div class="_fd-drag-btn" v-if="isCreate && (btns === true || btns.indexOf('create') > -1)"
+                     @click.stop="$emit('create')">
+                    <i class="fc-icon icon-add"></i>
+                </div>
+                <div class="_fd-drag-btn" v-if="!only && (btns === true || btns.indexOf('copy') > -1)"
+                     @click.stop="$emit('copy')">
+                    <i class="fc-icon icon-copy"></i>
+                </div>
+                <div class="_fd-drag-btn" v-if="children && (btns === true || btns.indexOf('addChild') > -1)"
+                     @click.stop="$emit('addChild')">
+                    <i class="fc-icon icon-add-child"></i>
+                </div>
+                <div class="_fd-drag-btn _fd-drag-danger" v-if="btns === true || btns.indexOf('delete') > -1"
+                     @click.stop="$emit('delete')">
+                    <i class="fc-icon icon-delete"></i>
+                </div>
+            </slot>
+        </div>
+        <slot name="default"></slot>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'DragTool',
+    emits: ['create', 'copy', 'addChild', 'delete', 'active', 'fc.el'],
+    props: {
+        dragBtn: Boolean,
+        children: String,
+        mask: Boolean,
+        handleBtn: [Boolean, Array],
+        formCreateInject: Object,
+        unique: String,
+        only: Boolean
+    },
+    inject: {
+        fcx: {
+            default: null
+        },
+        designer: {
+            default: null
+        },
+        dragTool: {
+            default: null
+        },
+    },
+    provide() {
+        return {
+            dragTool: this
+        }
+    },
+    computed: {
+        isCreate() {
+            return this.dragTool ? !!this.dragTool.children : false;
+        },
+        btns() {
+            if (Array.isArray(this.handleBtn)) {
+                return this.handleBtn.length ? this.handleBtn : false;
+            }
+            return this.handleBtn !== false;
+        },
+        id() {
+            return this.unique || this.formCreateInject.id;
+        },
+        hiddenMenu() {
+            return this.designer.ctx.hiddenDragMenu;
+        },
+        hiddenBtn() {
+            return this.designer.ctx.hiddenDragBtn;
+        },
+    },
+    methods: {
+        active() {
+            if (this.fcx.active === this.id) return;
+            this.fcx.active = this.id;
+            this.$emit('active');
+        }
+    },
+    mounted() {
+        this.$emit('fc.el', this);
+    },
+});
+</script>
+
+<style>
+._fd-drag-tool {
+    position: relative;
+    display: block;
+    min-height: 20px;
+    box-sizing: border-box;
+    padding: 2px;
+    outline: 1px dashed var(--fc-tool-border-color);
+    overflow: hidden;
+    word-wrap: break-word;
+    word-break: break-all;
+    transition: outline-color 0.3s ease;
+    z-index: 0;
+}
+
+._fd-drag-tool ._fd-drag-tool {
+    height: calc(100% - 6px);
+    margin: 3px;
+}
+
+._fd-drag-tool + ._fd-drag-tool {
+    margin-top: 5px;
+}
+
+._fd-drag-tool.active {
+    outline: 2px solid #2E73FF;
+}
+
+._fd-drag-tool.active > div > ._fd-drag-btn {
+    display: flex;
+}
+
+._fd-drag-tool:not(.active):hover > div > ._fd-drag-btn {
+    display: flex !important;
+    opacity: 0.7;
+}
+
+._fd-drag-tool._fd-drop-hover ._fd-drag-box {
+    padding-top: 15px !important;
+    padding-bottom: 15px !important;
+}
+
+._fd-drag-tool ._fd-drag-btn {
+    display: none;
+}
+
+._fd-drag-r {
+    position: absolute;
+    right: 2px;
+    top: calc(100% - 20px);
+    z-index: 1904;
+}
+
+._fd-drag-l {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 1904
+
+}
+
+._fd-drag-btn {
+    height: 18px;
+    width: 18px;
+    color: #fff;
+    background-color: #2E73FF;
+    text-align: center;
+    line-height: 20px;
+    padding-bottom: 1px;
+    float: left;
+    cursor: pointer;
+    justify-content: center;
+}
+
+._fd-drag-btn + ._fd-drag-btn {
+    margin-left: 2px;
+}
+
+._fd-drag-danger {
+    background-color: #FF2E2E;
+}
+
+._fd-drag-btn i {
+    font-size: 14px;
+}
+
+._fd-drag-mask {
+    z-index: 1900;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;;
+}
+
+._fd-drag-tool:hover {
+    outline-color: #2E73FF;
+    outline-style: solid;
+    z-index: 1;
+}
+
+._fd-drag-tool:has(._fd-drag-tool:not(.active):hover, ._fd-drag-tool.active:hover) > div > ._fd-drag-btn {
+    display: none !important;
+}
+</style>

+ 546 - 0
src/components/form-create-designer/components/EventConfig.vue

@@ -0,0 +1,546 @@
+<template>
+    <div class="_fd-event">
+        <el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
+            <el-button size="small" @click="visible=true">{{ t('event.title') }}</el-button>
+        </el-badge>
+        <el-dialog class="_fd-event-dialog" :title="t('event.title')" v-model="visible" destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body
+                   width="980px">
+            <el-container class="_fd-event-con" style="height: 600px">
+                <el-aside style="width:300px;">
+                    <el-container class="_fd-event-l">
+                        <el-header class="_fd-event-head" height="40px">
+                            <el-dropdown popper-class="_fd-event-dropdown" trigger="click" size="default"
+                                         :placement="'bottom-start'">
+                              <span class="el-dropdown-link">
+                                <el-button link type="primary" size="default">
+                                    {{ t('event.create') }}<i class="el-icon-arrow-down el-icon--right"></i>
+                                </el-button>
+                              </span>
+                                <template #dropdown>
+                                    <el-dropdown-menu>
+                                        <el-dropdown-item v-for="name in eventName" :key="name" @click="add(name)"
+                                                          :disabled="Object.keys(event).indexOf(name) > -1">
+                                            <div class="_fd-event-item">
+                                                <span>{{ name }}</span>
+                                                <span class="_fd-label" v-if="eventInfo[name]">
+                                                    {{ eventInfo[name] }}
+                                                </span>
+                                            </div>
+                                        </el-dropdown-item>
+                                        <template v-for="(hook, idx) in hookList">
+                                            <el-dropdown-item :divided="eventName.length > 0 && !idx"
+                                                              @click="add(hook)"
+                                                              :disabled="Object.keys(event).indexOf(hook) > -1">
+                                                <div class="_fd-event-item">
+                                                    <div> {{ hook }}</div>
+                                                    <span class="_fd-label">
+                                                    {{ eventInfo[hook] }}
+                                                </span>
+                                                </div>
+                                            </el-dropdown-item>
+                                        </template>
+                                        <el-dropdown-item :divided="eventName.length > 0" @click="cusEvent">
+                                            <div>{{ t('props.custom') }}</div>
+                                        </el-dropdown-item>
+                                    </el-dropdown-menu>
+                                </template>
+                            </el-dropdown>
+                        </el-header>
+                        <el-main>
+                            <el-menu
+                                :default-active="defActive"
+                                v-model="activeData">
+                                <template v-for="(item, name) in event">
+                                    <template v-if="Array.isArray(item)">
+                                        <template v-for="(event, index) in item" :key="name + index">
+                                            <el-menu-item :index="name + index">
+                                                <div class="_fd-event-title"
+                                                     @click.stop="edit({name, item, index})">
+                                                    <div class="_fd-event-method">
+                                                        <span>function<span>{{
+                                                                name
+                                                            }}</span></span>
+                                                        <span class="_fd-label"
+                                                              v-if="eventInfo[name]">{{ eventInfo[name] }}</span>
+                                                    </div>
+                                                    <i class="fc-icon icon-delete"
+                                                       @click.stop="rm({name, item, index})"></i>
+                                                </div>
+                                            </el-menu-item>
+                                        </template>
+                                    </template>
+                                    <el-menu-item v-else :index="name + 0">
+                                        <div class="_fd-event-title" @click.stop="edit({name})">
+                                            <div class="_fd-event-method">
+                                                <span>function<span>{{
+                                                        name
+                                                    }}</span></span>
+                                                <span class="_fd-label"
+                                                      v-if="eventInfo[name]">{{ eventInfo[name] }}</span>
+                                            </div>
+                                            <i class="fc-icon icon-delete" @click.stop="rm({name})"></i>
+                                        </div>
+                                    </el-menu-item>
+                                </template>
+                                <el-menu-item v-if="cus" style="padding-left: 10px;" index="custom">
+                                    <div class="_fd-event-title" @click.stop>
+                                        <el-input type="text" v-model="cusValue" size="default"
+                                                  @keydown.enter="addCus"
+                                                  :placeholder="t('event.placeholder')">
+                                        </el-input>
+                                        <div>
+                                            <i class="fc-icon icon-add" @click.stop="addCus"></i>
+                                            <i class="fc-icon icon-delete" @click.stop="closeCus"></i>
+                                        </div>
+                                    </div>
+                                </el-menu-item>
+                            </el-menu>
+                        </el-main>
+                    </el-container>
+                </el-aside>
+                <el-main>
+                    <el-container class="_fd-event-r">
+                        <el-header class="_fd-event-head" height="40px" v-if="activeData">
+                            <div><a target="_blank" href="https://form-create.com/v3/instance/">{{t('form.document')}}</a></div>
+                            <div>
+                                <el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
+                                <el-button size="small" type="primary" @click="save" color="#2f73ff">{{
+                                        t('props.save')
+                                    }}
+                                </el-button>
+                            </div>
+                        </el-header>
+                        <el-main v-if="activeData">
+                            <FnEditor ref="fn" v-model="eventStr" body :name="activeData.name"
+                                      :args="fnArgs"
+                                      style="height: 519px;"/>
+                        </el-main>
+                    </el-container>
+                </el-main>
+            </el-container>
+            <template #footer>
+                <div>
+                    <el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" size="default" @click="submit" color="#2f73ff">{{
+                            t('props.ok')
+                        }}
+                    </el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import unique from '@form-create/utils/lib/unique';
+import deepExtend from '@form-create/utils/lib/deepextend';
+import is from '@form-create/utils/lib/type';
+import {defineComponent} from 'vue';
+import FnEditor from './FnEditor.vue';
+import errorMessage from '../utils/message';
+import {getInjectArg} from '../utils';
+
+const $T = '$FNX:';
+
+const isFNX = v => {
+    return is.String(v) && v.indexOf($T) === 0;
+};
+
+export default defineComponent({
+    name: 'EventConfig',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: [Object, undefined, null],
+        componentName: '',
+        eventName: {
+            type: Array,
+            default: () => []
+        }
+    },
+    inject: ['designer'],
+    components: {
+        FnEditor,
+    },
+    data() {
+        return {
+            visible: false,
+            activeData: null,
+            val: null,
+            defActive: 'no',
+            hookList: ['hook_load', 'hook_mounted', 'hook_deleted', 'hook_watch', 'hook_value', 'hook_hidden'],
+            event: {},
+            cus: false,
+            cusValue: '',
+            eventStr: '',
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        activeRule() {
+            return this.designer.setupState.activeRule;
+        },
+        eventInfo() {
+            const info = {};
+            this.eventName.forEach(v => {
+                info[v] = this.t('com.' + this.componentName + '.event.' + v) || this.t('eventInfo.' + v) || '';
+            })
+            this.hookList.forEach(v => {
+                info[v] = this.t('eventInfo.' + v) || '';
+            })
+            return info;
+        },
+        eventNum() {
+            let num = 0;
+            Object.keys(this.modelValue || {}).forEach(k => {
+                num += Array.isArray(this.modelValue[k]) ? this.modelValue[k].length : 1;
+            });
+            const hooks = this.activeRule ? {...this.activeRule.hook || {}} : {};
+            Object.keys(hooks).forEach(k => {
+                num += Array.isArray(this.activeRule.hook[k]) ? this.activeRule.hook[k].length : 1;
+            });
+            return num;
+        },
+        fnArgs() {
+            return [getInjectArg(this.t)];
+        }
+    },
+    watch: {
+        visible(v) {
+            this.event = v ? this.loadFN() : {};
+            if (!v) {
+                this.destroy();
+                this.closeCus();
+            }
+        },
+    },
+    methods: {
+        addCus() {
+            const val = this.cusValue && this.cusValue.trim();
+            if (val) {
+                this.closeCus();
+                this.add(val);
+            }
+        },
+        closeCus() {
+            this.cus = false;
+            this.cusValue = '';
+        },
+        cusEvent() {
+            this.cus = true;
+        },
+        loadFN() {
+            const e = deepExtend({}, this.modelValue || {});
+            const hooks = this.activeRule ? {...this.activeRule.hook || {}} : {};
+            Object.keys(hooks).forEach(k => {
+                e['hook_' + k] = hooks[k];
+            })
+            const val = {};
+            Object.keys(e).forEach(k => {
+                if (Array.isArray(e[k])) {
+                    const data = [];
+                    e[k].forEach(v => {
+                        if (isFNX(v)) {
+                            data.push(v.replace($T, ''));
+                        } else if (is.Function(v) && isFNX(v.__json)) {
+                            data.push(v.__json.replace($T, ''));
+                        } else if (v && v.indexOf('$GLOBAL:') === 0) {
+                            data.push(v);
+                        }
+                    });
+                    val[k] = data;
+                } else if (isFNX(e[k])) {
+                    val[k] = [e[k].replace($T, '')];
+                } else if (is.Function(e[k]) && isFNX(e[k].__json)) {
+                    val[k] = [e[k].__json.replace($T, '')];
+                } else if (e[k] && e[k].indexOf('$GLOBAL:') === 0) {
+                    val[k] = [e[k]];
+                }
+            });
+            return val;
+        },
+        parseFN(e) {
+            const on = {};
+            const hooks = {};
+            Object.keys(e).forEach(k => {
+                const lst = [];
+                e[k].forEach((v, i) => {
+                    lst[i] = v.indexOf('$GLOBAL:') !== 0 ? ($T + v) : v;
+                });
+                if (lst.length > 0) {
+                    if (k.indexOf('hook_') > -1) {
+                        hooks[k.replace('hook_', '')] = lst.length === 1 ? lst[0] : lst;
+                    } else {
+                        on[k] = lst.length === 1 ? lst[0] : lst;
+                    }
+                }
+            });
+            return {hooks, on};
+        },
+        add(name) {
+            let data = {};
+            if (Array.isArray(this.event[name])) {
+                this.event[name].push('');
+                data = {
+                    name,
+                    item: this.event[name],
+                    index: this.event[name].length - 1,
+                };
+            } else if (this.event[name]) {
+                const arr = [this.event[name], ''];
+                this.event[name] = arr;
+                data = {
+                    name,
+                    item: arr,
+                    index: 1,
+                };
+            } else {
+                const arr = [''];
+                this.event[name] = arr;
+                data = {
+                    name,
+                    item: arr,
+                    index: 0,
+                };
+            }
+            if (!this.activeData) {
+                this.edit(data);
+            }
+        },
+        edit(data) {
+            data.key = unique();
+            if (data.item) {
+                this.val = data.item[data.index];
+            } else {
+                this.val = this.event[data.name];
+            }
+            this.activeData = data;
+            this.eventStr = this.val;
+            this.defActive = data.name + (data.index || 0);
+        },
+        save() {
+            if (!this.$refs.fn.save()) {
+                return;
+            }
+            const str = this.eventStr;
+
+            if (this.activeData.item) {
+                this.activeData.item[this.activeData.index] = str;
+            } else {
+                this.event[this.activeData.name] = str;
+            }
+            this.destroy();
+        },
+        rm(data) {
+            if (data.index !== undefined) {
+                data.item.splice(data.index, 1);
+            } else {
+                this.$delete(this.event, data.name);
+            }
+            if (this.defActive === (data.name + (data.index || 0))) {
+                this.destroy();
+            }
+        },
+        destroy() {
+            this.activeData = null;
+            this.val = null;
+            this.defActive = 'no';
+        },
+        close() {
+            this.destroy();
+        },
+        submit() {
+            if (this.activeData) {
+                return errorMessage(this.t('event.saveMsg'));
+            }
+            const {on, hooks} = this.parseFN(this.event);
+            this.$emit('update:modelValue', on);
+            this.activeRule.hook = hooks;
+            this.visible = false;
+            this.destroy();
+            this.closeCus();
+        },
+    },
+    beforeCreate() {
+        window.$inject = {
+            $f: {},
+            rule: [],
+            self: {},
+            option: {},
+            inject: {},
+            args: [],
+        };
+    }
+});
+</script>
+
+<style>
+
+._fd-event .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-event .el-badge {
+    width: 100%;
+}
+
+._fd-event-dialog .el-dialog__body {
+    padding: 10px 20px;
+}
+
+._fd-event-con .el-main {
+    padding: 0;
+}
+
+._fd-event-l, ._fd-event-r {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+    height: 100%;
+    border: 1px solid #ececec;
+}
+
+._fd-event-dropdown .el-dropdown-menu {
+    max-height: 500px;
+    overflow: auto;
+}
+
+._fd-event-head {
+    display: flex;
+    padding: 5px 15px;
+    border-bottom: 1px solid #eee;
+    background: #f8f9ff;
+    align-items: center;
+}
+
+._fd-event-head .el-button.is-link {
+    color: #2f73ff;
+}
+
+._fd-event-r {
+    border-left: 0 none;
+}
+
+._fd-event-r ._fd-event-head {
+    justify-content: space-between;
+}
+
+._fd-event-l > .el-main, ._fd-event-r > .el-main {
+    display: flex;
+    flex-direction: row;
+    flex: 1;
+    flex-basis: auto;
+    box-sizing: border-box;
+    min-width: 0;
+    width: 100%;
+}
+
+._fd-event-r > .el-main {
+    flex-direction: column;
+}
+
+._fd-event-item {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    max-width: 250px;
+    font-size: 14px;
+    overflow: hidden;
+    white-space: pre-wrap;
+}
+
+._fd-event-item ._fd-label {
+    font-size: 12px;
+    color: #AAAAAA;
+}
+
+._fd-event-l .el-menu {
+    padding: 0 10px 5px;
+    border-right: 0 none;
+    width: 100%;
+    border-top: 0 none;
+    overflow: auto;
+}
+
+._fd-event-l .el-menu-item.is-active {
+    background: #e4e7ed;
+    color: #303133;
+}
+
+._fd-event-l .el-menu-item {
+    height: auto;
+    line-height: 1em;
+    border: 1px solid #ECECEC;
+    border-radius: 5px;
+    padding: 0;
+    margin-top: 5px;
+}
+
+._fd-event-method {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    width: 225px;
+    font-size: 14px;
+    font-family: monospace;
+    color: #9D238C;
+    overflow: hidden;
+    white-space: pre-wrap;
+}
+
+._fd-event-method ._fd-label {
+    margin-top: 4px;
+    color: #AAAAAA;
+    font-size: 12px;
+}
+
+._fd-event-method > span:first-child, ._fd-fn-list-method > span:first-child {
+    color: #9D238C;
+}
+
+._fd-event-method > span:first-child > span, ._fd-fn-list-method > span:first-child > span {
+    color: #000;
+    margin-left: 10px;
+}
+
+._fd-event-title {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    padding: 10px 0;
+}
+
+._fd-event-title .fc-icon {
+    margin-right: 6px;
+    font-size: 18px;
+    color: #282828;
+}
+
+._fd-event-title .el-input {
+    width: 200px;
+}
+
+._fd-event-title .el-input__wrapper {
+    box-shadow: none;
+}
+
+._fd-event-title .el-menu-item.is-active i {
+    color: #282828;
+}
+
+._fd-event-con .CodeMirror {
+    height: 100%;
+    width: 100%;
+}
+
+._fd-event-con .CodeMirror-wrap pre.CodeMirror-line {
+    padding-left: 20px;
+}
+</style>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 206 - 0
src/components/form-create-designer/components/FcDesigner.vue


+ 263 - 0
src/components/form-create-designer/components/FetchConfig.vue

@@ -0,0 +1,263 @@
+<template>
+    <div class="_fd-gfc">
+        <el-badge type="warning" is-dot :hidden="!configured">
+            <el-button @click="visible=true" size="small">{{ t('struct.title') }}</el-button>
+        </el-badge>
+        <el-dialog class="_fd-gfc-dialog" v-model="visible" destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body
+                   width="980px">
+            <template #header>
+                {{ t('fetch.optionsType.fetch') }}
+                <Warning :tooltip="t('warning.fetch')"></Warning>
+            </template>
+            <el-container class="_fd-gfc-con" style="height: 450px;">
+                <el-tabs model-value="first" class="_fc-tabs" style="width: 100%">
+                    <el-tab-pane :label="t('fetch.config')" name="first">
+                        <DragForm v-model:api="form.api" v-model="form.formData" :rule="form.rule"
+                                  :option="form.options">
+                            <template #title="scope">
+                                <template v-if="scope.rule.warning">
+                                    <Warning :tooltip="scope.rule.warning">
+                                        {{ scope.rule.title }}
+                                    </Warning>
+                                </template>
+                                <template v-else>
+                                    {{ scope.rule.title }}
+                                </template>
+                            </template>
+                        </DragForm>
+                    </el-tab-pane>
+                    <el-tab-pane lazy name="second">
+                        <template #label>
+                            {{ t('fetch.parse') }}
+                            <Warning :tooltip="t('warning.fetchParse')"></Warning>
+                        </template>
+                        <FnEditor style="height: 415px;" v-model="form.parse" name="parse"
+                                  :args="[{name:'res', info: t('fetch.response')}, 'rule', 'api']"
+                                  ref="parse"></FnEditor>
+                    </el-tab-pane>
+                    <el-tab-pane lazy :label="t('fetch.onError')" name="third">
+                        <FnEditor style="height: 415px;" v-model="form.onError" name="onError"
+                                  :args="['e']"
+                                  ref="error"></FnEditor>
+                    </el-tab-pane>
+                </el-tabs>
+            </el-container>
+            <template #footer>
+                <div>
+                    <el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" size="default" @click="save" color="#2f73ff">{{
+                            t('props.ok')
+                        }}
+                    </el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import {deepCopy} from '@form-create/utils/lib/deepextend';
+import FnEditor from './FnEditor.vue';
+import StructEditor from './StructEditor.vue';
+import {defineComponent} from 'vue';
+import {designerForm} from '../utils/form';
+import errorMessage from '../utils/message';
+import is from '@form-create/utils/lib/type';
+import Warning from './Warning.vue';
+
+const makeRule = (t) => {
+    return [
+        {
+            type: 'input',
+            field: 'action',
+            title: t('fetch.action'),
+            value: '',
+            props: {size: 'default'},
+            validate: [{required: true, message: t('fetch.actionRequired'), trigger: 'blur'}]
+        },
+        {
+            type: 'radio',
+            field: 'method',
+            title: t('fetch.method'),
+            value: 'GET',
+            props: {
+                size: 'default'
+            },
+            options: [
+                {label: 'GET', value: 'GET'},
+                {label: 'POST', value: 'POST'},
+            ],
+            $required: true,
+        },
+        {
+            type: 'radio',
+            field: 'dataType',
+            title: t('fetch.dataType'),
+            warning: t('warning.fetchDataType'),
+            value: 'json',
+            props: {
+                size: 'default'
+            },
+            options: [
+                {label: 'JSON', value: 'json'},
+                {label: 'FormData', value: 'formData'},
+            ],
+            $required: true,
+        },
+        {
+            type: 'TableOptions',
+            field: 'headers',
+            title: t('fetch.headers'),
+            value: {},
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object',
+                size: 'default'
+            },
+        },
+        {
+            type: 'TableOptions',
+            field: 'query',
+            title: t('fetch.query'),
+            warning: t('warning.fetchQuery'),
+            value: {},
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object',
+                size: 'default'
+            },
+        },
+        {
+            type: 'TableOptions',
+            field: 'data',
+            title: t('fetch.data'),
+            warning: t('warning.fetchData'),
+            value: {},
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object',
+                size: 'default'
+            },
+        }];
+}
+
+export default defineComponent({
+    name: 'FetchConfig',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: [Object, String],
+        to: String,
+    },
+    components: {
+        Warning,
+        DragForm: designerForm.$form(),
+        FnEditor,
+        StructEditor
+    },
+    inject: ['designer'],
+    data() {
+        return {
+            visible: false,
+            value: deepCopy(this.modelValue || {}),
+            form: {
+                api: {},
+                formData: {},
+                rule: [],
+                options: {
+                    form: {
+                        labelWidth: '90px',
+                        size: 'default'
+                    },
+                    submitBtn: false,
+                    resetBtn: false,
+                }
+            }
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        configured() {
+            return !is.empty(this.modelValue);
+        },
+    },
+    watch: {
+        visible(v) {
+            if (v) {
+                this.value = deepCopy(this.modelValue || {});
+                this.active();
+            }
+        },
+    },
+    methods: {
+        open() {
+            this.visible = true;
+        },
+        active() {
+            const formData = this.value;
+            this.form.rule = formData.type === 'static' ? [] : makeRule(this.t);
+            this.form.formData = {...formData};
+            this.form.label = formData.label;
+            this.form.type = formData.type;
+            this.form.data = formData.data;
+            this.form.dataType = formData.dataType;
+            this.form.parse = formData.parse || '';
+            this.form.onError = formData.onError || '';
+        },
+        save() {
+            this.form.api.validate().then(() => {
+                const formData = {...this.form.formData};
+                if ((this.$refs.parse && !this.$refs.parse.save()) || (this.$refs.error && !this.$refs.error.save())) {
+                    return;
+                }
+                formData.parse = this.form.parse;
+                formData.onError = this.form.onError;
+                formData.label = this.form.label;
+                formData.type = this.form.type;
+                formData.to = this.to || 'options';
+                this.$emit('update:modelValue', formData);
+                this.visible = false;
+            }).catch(err => {
+                console.error(err);
+                errorMessage(err[Object.keys(err)[0]][0].message);
+            });
+        },
+    },
+    created() {
+        this.active();
+    }
+});
+</script>
+
+<style>
+._fd-gfc, ._fd-gfc .el-badge {
+    width: 100%;
+}
+
+._fd-gfc .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-gfc-dialog .el-tabs__header {
+    margin-bottom: 0;
+}
+
+._fd-gfc-dialog .form-create {
+    margin-top: 15px;
+}
+
+._fd-gfc-con .CodeMirror {
+    height: 100%;
+    width: 100%;
+}
+
+._fd-gfc-con .CodeMirror-wrap pre.CodeMirror-line {
+    padding-left: 20px;
+}
+</style>

+ 153 - 0
src/components/form-create-designer/components/FieldInput.vue

@@ -0,0 +1,153 @@
+<template>
+    <div class="_fd-field-input">
+        <i class="fc-icon icon-group" @click.stop="copy"></i>
+        <el-input
+            v-model="value"
+            :readonly="fieldReadonly || disabled"
+            :disabled="fieldReadonly || disabled"
+            @focus="onFocus"
+            @blur="onInput"
+        >
+            <template #append v-if="!fieldReadonly">
+                <i class="fc-icon icon-auto" @click="makeField"></i>
+            </template>
+        </el-input>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import uniqueId from '@form-create/utils/lib/unique';
+import errorMessage from '../utils/message';
+import {copyTextToClipboard} from '../utils/index';
+import is from '@form-create/utils/lib/type';
+
+export default defineComponent({
+    name: 'FieldInput',
+    inject: ['designer'],
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: String,
+        disabled: Boolean,
+    },
+    computed: {
+        fieldReadonly() {
+            return this.designer.setupState.fieldReadonly;
+        },
+        activeRule() {
+            return this.designer.setupState.activeRule;
+        },
+        t() {
+            return this.designer.setupState.t;
+        }
+    },
+    data() {
+        return {
+            value: this.modelValue || '',
+            oldValue: '',
+        }
+    },
+    watch: {
+        modelValue(n) {
+            this.value = n;
+        }
+    },
+    methods: {
+        copy() {
+            copyTextToClipboard(this.modelValue);
+        },
+        getSubChildren() {
+            let subChildren = this.designer.setupState.getSubFormChildren(this.activeRule) || [];
+            subChildren = is.trueArray(subChildren) ? subChildren : this.designer.setupState.children;
+            return subChildren;
+        },
+        getSubFieldChildren() {
+            const subChildren = this.getSubChildren();
+            const list = [];
+            const getRule = (children) => {
+                children && children.forEach(rule => {
+                    if (rule && rule._fc_drag_tag && rule.field) {
+                        list.push({...rule, children: []});
+                    } else if (rule && rule.children) {
+                        getRule(rule.children);
+                    }
+                });
+                return list;
+            }
+            return getRule(subChildren);
+        },
+        checkValue() {
+            const oldField = this.oldValue;
+            let field = (this.value || '').replace(/[\s\ ]/g, '');
+            if (!field) {
+                errorMessage(this.t('computed.fieldEmpty'));
+                return oldField;
+            } else if (!/^[a-zA-Z]/.test(field)) {
+                errorMessage(this.t('computed.fieldChar'));
+                return oldField;
+            } else if (oldField !== field) {
+                const flag = field.indexOf('.') > -1;
+                if (flag) {
+                    field = field.replaceAll('.', '_');
+                }
+                if (this.getSubFieldChildren().filter(v => v.field === field).length > 0) {
+                    errorMessage(this.t('computed.fieldExist', {label: field}));
+                    return oldField;
+                }
+                if (flag) {
+                    return field;
+                }
+            }
+            this.oldValue = '';
+            return field;
+        },
+        onFocus() {
+            this.oldValue = this.value;
+        },
+        makeField() {
+            this.oldValue = this.value;
+            this.value = uniqueId();
+            this.onInput();
+        },
+        onInput() {
+            if (this.value !== this.modelValue) {
+                this.value = this.checkValue();
+                this.oldValue = this.value;
+                if (this.value !== this.modelValue) {
+                    this.$emit('update:modelValue', this.value);
+                }
+            }
+        },
+    },
+});
+</script>
+
+<style>
+._fd-field-input {
+    width: 100%;
+}
+
+._fd-field-input > .fc-icon {
+    position: absolute;
+    right: 28px;
+    top: 1px;
+    z-index: 3;
+    color: #a8abb2;
+    cursor: pointer;
+    width: 24px;
+    height: 24px;
+    text-align: center;
+}
+
+._fd-field-input > .fc-icon:hover {
+    color: #2E73FF;
+}
+
+._fd-field-input .el-input-group__append {
+    width: 25px;
+    padding: 0;
+    margin: 0;
+    color: #606266;
+    cursor: pointer;
+}
+</style>

+ 304 - 0
src/components/form-create-designer/components/FnConfig.vue

@@ -0,0 +1,304 @@
+<template>
+    <div class="_fd-fn-list">
+        <el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
+            <el-button @click="visible=true" size="small">{{ t('event.title') }}</el-button>
+        </el-badge>
+        <el-dialog class="_fd-fn-list-dialog" :title="t('event.title')" v-model="visible" destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body
+                   width="980px">
+            <el-container class="_fd-fn-list-con" style="height: 600px">
+                <el-aside style="width:300px;">
+                    <el-container class="_fd-fn-list-l">
+                        <el-header class="_fd-fn-list-head" height="40px">
+                            <el-text type="primary" size="default">
+                                {{ t('event.list') }}
+                            </el-text>
+                        </el-header>
+                        <el-main>
+                            <el-menu
+                                :default-active="defActive"
+                                v-model="activeData">
+                                <template v-for="(item, name) in event">
+                                    <el-menu-item :index="name">
+                                        <div class="_fd-fn-list-method" @click.stop="edit(item)">
+                                            <span>function<span>{{ name }}</span></span>
+                                            <span class="_fd-label" v-if="eventInfo[name]">{{ eventInfo[name] }}</span>
+                                        </div>
+                                    </el-menu-item>
+                                </template>
+                            </el-menu>
+                        </el-main>
+                    </el-container>
+                </el-aside>
+                <el-main>
+                    <el-container class="_fd-fn-list-r">
+                        <el-header class="_fd-fn-list-head" height="40px" v-if="activeData">
+                            <el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
+                            <el-button size="small" type="primary" @click="save" color="#2f73ff">{{
+                                    t('props.save')
+                                }}
+                            </el-button>
+                        </el-header>
+                        <el-main v-if="activeData">
+                            <FnEditor ref="fn" v-model="eventStr" :name="activeData.item.name"
+                                      :args="activeData.item.args"/>
+                        </el-main>
+                    </el-container>
+                </el-main>
+            </el-container>
+            <template #footer>
+                <div>
+                    <el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" size="default" @click="submit" color="#2f73ff">{{
+                            t('props.ok')
+                        }}
+                    </el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import unique from '@form-create/utils/lib/unique';
+import deepExtend from '@form-create/utils/lib/deepextend';
+import {defineComponent} from 'vue';
+import FnEditor from './FnEditor.vue';
+import errorMessage from '../utils/message';
+
+const PREFIX = '[[FORM-CREATE-PREFIX-';
+const SUFFIX = '-FORM-CREATE-SUFFIX]]';
+
+export default defineComponent({
+    name: 'FnConfig',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: [Object, undefined, null],
+        eventConfig: {
+            type: Array,
+            default: () => []
+        },
+    },
+    inject: ['designer'],
+    components: {
+        FnEditor,
+    },
+    data() {
+        return {
+            visible: false,
+            activeData: null,
+            defActive: 'no',
+            event: {},
+            cus: false,
+            eventStr: '',
+        };
+    },
+    computed: {
+        eventInfo() {
+            const info = {};
+            this.eventConfig.forEach(v => {
+                info[v.name] = v.info;
+            });
+            return info;
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+        eventNum() {
+            let num = 0;
+            Object.keys(this.modelValue || {}).forEach(k => {
+                if (this.modelValue[k]) {
+                    num++;
+                }
+            });
+            return num;
+        },
+    },
+    watch: {
+        visible(v) {
+            this.event = v ? this.loadFN(deepExtend({}, this.modelValue || {})) : {};
+            if (!v) {
+                this.destroy();
+            }
+        },
+    },
+    methods: {
+        getArgs(item) {
+            return item.args.join(', ');
+        },
+        loadFN(e) {
+            const val = {};
+            this.eventConfig.forEach(item => {
+                const k = item.name;
+                const fn = e[k] || '';
+                val[k] = {
+                    item, fn
+                }
+            });
+            return val;
+        },
+        parseFN(e) {
+            const on = {};
+            Object.keys(e).forEach(k => {
+                if (e[k].fn) {
+                    on[k] = e[k].fn;
+                }
+            });
+            return on;
+        },
+        edit(data) {
+            data.key = unique();
+            this.activeData = data;
+            this.eventStr = data.fn || (PREFIX + `function ${data.item.name}(${this.getArgs(data.item)}){}` + SUFFIX);
+            this.defActive = data.item.name;
+        },
+        save() {
+            if (this.$refs.fn.save()) {
+                this.activeData.fn = this.eventStr;
+                this.destroy();
+                return true;
+            }
+            return false;
+        },
+        destroy() {
+            this.activeData = null;
+            this.defActive = 'no';
+        },
+        close() {
+            this.destroy();
+        },
+        submit() {
+            if (this.activeData && !this.save()) {
+                return;
+            }
+            this.$emit('update:modelValue', this.parseFN(this.event));
+            this.visible = false;
+            this.destroy();
+        },
+    }
+});
+</script>
+
+<style>
+._fd-fn-list, ._fd-fn-list .el-badge {
+    width: 100%;
+}
+
+._fd-fn-list .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-fn-list-dialog .el-dialog__body {
+    padding: 10px 20px;
+}
+
+._fd-fn-list-con .el-main {
+    padding: 0;
+}
+
+._fd-fn-list-l, ._fd-fn-list-r {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+    height: 100%;
+    border: 1px solid #ececec;
+}
+
+._fd-fn-list-head {
+    display: flex;
+    padding: 5px 15px;
+    border-bottom: 1px solid #eee;
+    background: #f8f9ff;
+    align-items: center;
+}
+
+._fd-fn-list-head .el-button.is-link {
+    color: #2f73ff;
+}
+
+._fd-fn-list-r {
+    border-left: 0 none;
+}
+
+._fd-fn-list-r ._fd-fn-list-head {
+    justify-content: flex-end;
+}
+
+._fd-fn-list-l > .el-main, ._fd-fn-list-r > .el-main {
+    display: flex;
+    flex-direction: row;
+    flex: 1;
+    flex-basis: auto;
+    box-sizing: border-box;
+    min-width: 0;
+    width: 100%;
+}
+
+._fd-fn-list-r > .el-main {
+    flex-direction: column;
+}
+
+._fd-fn-list-l .el-menu {
+    padding: 0 10px 5px;
+    border-right: 0 none;
+    width: 100%;
+    border-top: 0 none;
+    overflow: auto;
+}
+
+._fd-fn-list-l .el-menu-item.is-active {
+    background: #e4e7ed;
+    color: #303133;
+}
+
+._fd-fn-list-l .el-menu-item {
+    height: auto;
+    line-height: 1em;
+    border: 1px solid #ECECEC;
+    border-radius: 5px;
+    padding: 0;
+    margin-top: 5px;
+}
+
+._fd-fn-list-method {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    padding: 10px 0;
+    font-size: 14px;
+    line-height: 1em;
+    font-family: monospace;
+    width: 100%;
+    overflow: hidden;
+    white-space: pre-wrap;
+}
+
+
+._fd-fn-list-method ._fd-label {
+    margin-top: 4px;
+    color: #AAAAAA;
+    font-size: 12px;
+}
+
+._fd-fn-list-method-info > span:first-child, ._fd-fn-list-method > span:first-child {
+    color: #9D238C;
+}
+
+._fd-fn-list-method-info > span:first-child > span, ._fd-fn-list-method > span:first-child > span {
+    color: #000;
+    margin-left: 10px;
+}
+
+._fd-fn-list-con .CodeMirror {
+    height: 100%;
+    width: 100%;
+}
+
+._fd-fn-list-con .CodeMirror-wrap pre.CodeMirror-line {
+    padding-left: 20px;
+}
+</style>

+ 251 - 0
src/components/form-create-designer/components/FnEditor.vue

@@ -0,0 +1,251 @@
+<template>
+    <div class="_fd-fn">
+        <div class="_fd-fn-tip">
+            <div class="_fd-fn-ind"></div>
+            <div class="cm-keyword"><span>function {{ name }}(<template
+                v-for="(item, idx) in argList">{{ idx > 0 ? ', ' : '' }}<template v-if="item.type === 'string'">
+<span>{{ item.name }}</span>
+</template><template v-else><el-popover placement="top-start" :width="400" :hide-after="0" trigger="click"
+                                        :title="item.name"
+                                        :content="item.info || ''"
+            ><template #reference><span class="_fd-fn-arg">{{ item.name }}<i
+                class="fc-icon icon-question"></i></span></template>
+                            <template v-if="item.columns">
+                                <el-table :data="item.columns" border>
+                            <el-table-column width="120" property="label" :label="t('event.label')"/>
+                            <el-table-column property="info" :label="t('event.info')"/>
+                            <el-table-column width="80" property="type" :label="t('event.type')"/>
+                          </el-table>
+                            </template>
+                        </el-popover>
+                    </template>
+                    </template>) {</span></div>
+        </div>
+        <div ref="editor" class="_fd-fn-editor"></div>
+        <div class="_fd-fn-tip">
+            <div class="_fd-fn-ind"></div>
+            <div class="cm-keyword">}</div>
+        </div>
+        <el-button v-if="visible && button" type="primary" size="small" @click="save">{{ t('props.save') }}</el-button>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import 'codemirror/addon/hint/show-hint.css';
+import CodeMirror from 'codemirror/lib/codemirror';
+import 'codemirror/mode/javascript/javascript';
+import 'codemirror/addon/hint/show-hint';
+import 'codemirror/addon/hint/javascript-hint';
+import {defineComponent, markRaw} from 'vue';
+import {addAutoKeyMap, toJSON} from '../utils';
+import errorMessage from '../utils/message';
+
+const PREFIX = '[[FORM-CREATE-PREFIX-';
+const SUFFIX = '-FORM-CREATE-SUFFIX]]';
+
+export default defineComponent({
+    name: 'FnEditor',
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: [String, Function],
+        name: String,
+        args: Array,
+        body: Boolean,
+        button: Boolean,
+        fnx: Boolean,
+    },
+    inject: ['designer'],
+    data() {
+        return {
+            editor: null,
+            fn: '',
+            visible: false,
+            value: '',
+        };
+    },
+    watch: {
+        modelValue(n) {
+            if (n != this.value && (!n || !n.__json || (n.__json && n.__json != this.value))) {
+                this.editor && this.editor.setValue(this.tidyValue());
+            }
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        argStr() {
+            return (this.args || []).map(arg => {
+                if (typeof arg === 'string') {
+                    return arg;
+                }
+                return arg.name;
+            }).join(', ');
+        },
+        argList() {
+            return this.args.map(arg => {
+                if (typeof arg === 'string') {
+                    return {
+                        name: arg,
+                        type: 'string'
+                    }
+                }
+                return arg;
+            });
+        },
+    },
+    mounted() {
+        this.$nextTick(() => {
+            this.load();
+        });
+    },
+    methods: {
+        save() {
+            const str = this.editor.getValue() || '';
+            if (str.trim() === '') {
+                this.fn = '';
+            } else {
+                let fn;
+                try {
+                    fn = (new Function('return function ' + this.name + '(' + this.argStr + '){' + str + '}'))();
+                } catch (e) {
+                    console.error(e);
+                    errorMessage(this.t('struct.errorMsg'));
+                    return false;
+                }
+                if (this.body) {
+                    this.fn = (this.fnx ? '$FNX:' : '') + str;
+                } else {
+                    this.fn = PREFIX + fn + SUFFIX;
+                }
+            }
+            this.submit();
+            return true;
+        },
+        submit() {
+            this.$emit('update:modelValue', this.fn);
+            this.$emit('change', this.fn);
+            this.value = this.fn;
+            this.visible = false;
+        },
+        trimString(input) {
+            const firstIndex = input.indexOf('{');
+            const lastIndex = input.lastIndexOf('}');
+            if (firstIndex === -1 || lastIndex === -1 || firstIndex >= lastIndex) {
+                return input;
+            }
+            return input.slice(firstIndex + 1, lastIndex).replace(/^\n+|\n+$/g, '');
+        },
+        tidyValue() {
+            let value = this.modelValue || '';
+            if (value.__json) {
+                value = value.__json;
+            }
+            if (this.fnx && typeof value === 'string' && value.indexOf('$FNX:') === 0) {
+                value = value.slice(5);
+            }
+            if (typeof value === 'function') {
+                value = this.trimString(toJSON(value)).trim();
+            } else if (!this.body) {
+                value = this.trimString(value).trim();
+            }
+            this.value = value;
+            return value;
+        },
+        load() {
+            this.$nextTick(() => {
+                let value = this.tidyValue();
+                this.editor = markRaw(CodeMirror(this.$refs.editor, {
+                    lineNumbers: true,
+                    mode: {name: 'javascript', globalVars: true},
+                    extraKeys: {'Ctrl-Space': 'autocomplete'},
+                    line: true,
+                    tabSize: 2,
+                    lineWrapping: true,
+                    value,
+                }));
+                this.editor.on('inputRead', (cm, event) => {
+                    if (event.keyCode === 32 && event.ctrlKey) { // 检测 Ctrl + Space 快捷键
+                        CodeMirror.showHint(cm, CodeMirror.hint.javascript); // 触发代码提示
+                    }
+                });
+                this.editor.on('change', () => {
+                    this.visible = true;
+                });
+                addAutoKeyMap(this.editor);
+            });
+        },
+    }
+});
+</script>
+
+<style>
+
+._fd-fn {
+    display: flex;
+    flex-direction: column;
+    position: relative;
+    width: 100%;
+    height: 100%;
+}
+
+._fd-fn .el-button {
+    position: absolute;
+    bottom: 3px;
+    right: 5px;
+    box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
+}
+
+._fd-fn-editor {
+    display: flex;
+    flex: 1;
+    width: 100%;
+    overflow: auto;
+}
+
+._fd-fn-editor .CodeMirror {
+    height: 100%;
+    width: 100%;
+}
+
+._fd-fn-tip {
+    color: #000;
+    font-family: monospace;
+    direction: ltr;
+}
+
+._fd-fn-tip .cm-keyword {
+    color: #708;
+    line-height: 24px;
+    white-space: nowrap;
+    overflow-x: auto;
+}
+
+._fd-fn-tip .cm-keyword::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+    background-color: transparent;
+}
+
+._fd-fn-ind {
+    background-color: #f7f7f7;
+    width: 29px;
+    height: 24px;
+    display: inline-block;
+    margin-right: 4px;
+    border-right: 1px solid #ddd;
+    float: left;
+}
+
+._fd-fn-arg {
+    text-decoration: underline;
+    cursor: pointer;
+}
+
+._fd-fn-arg i {
+    font-size: 12px;
+    color: #3073ff;
+}
+
+</style>

+ 103 - 0
src/components/form-create-designer/components/FnInput.vue

@@ -0,0 +1,103 @@
+<template>
+    <div class="_fd-fn-input">
+        <el-badge type="warning" is-dot :hidden="!configured">
+            <el-button @click="visible=true" size="small">
+                <slot>
+                    {{t('event.title')}}
+                </slot>
+            </el-button>
+        </el-badge>
+        <el-dialog class="_fd-fn-input-dialog _fd-config-dialog" :title="title || t('struct.title')" v-model="visible"
+                   destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body width="800px">
+            <FnEditor ref="editor" v-model="value" :name="name" :args="args" :body="body" :fnx="fnx"></FnEditor>
+            <template #footer>
+                <div>
+                    <el-button @click="visible = false" size="default">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" @click="onOk" size="default">{{ t('props.ok') }}</el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import 'codemirror/mode/javascript/javascript';
+import {defineComponent} from 'vue';
+import FnEditor from './FnEditor.vue';
+
+export default defineComponent({
+    name: 'FnInput',
+    components: {FnEditor},
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: [String, Function],
+        name: String,
+        args: Array,
+        title: String,
+        body: Boolean,
+        fnx: Boolean,
+        defaultValue: {
+            require: false
+        },
+        validate: Function,
+    },
+    inject: ['designer'],
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        configured() {
+            return !!this.modelValue;
+        },
+    },
+    data() {
+        return {
+            visible: false,
+            value: this.modelValue
+        };
+    },
+    watch: {
+        modelValue(n){
+            this.value = n;
+        }
+    },
+    methods: {
+        onOk() {
+            if(this.$refs.editor.save()) {
+                this.$emit('update:modelValue', this.value);
+                this.$emit('change', this.value);
+                this.visible = false;
+            }
+        },
+    }
+});
+</script>
+
+<style>
+._fd-fn-input {
+    width: 100%;
+}
+
+._fd-fn-input .el-badge {
+    width: 100%;
+}
+
+._fd-fn-input .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-fn-input-dialog .CodeMirror-lint-tooltip {
+    z-index: 2021 !important;
+}
+
+._fd-fn-input-dialog .el-dialog__body {
+    padding: 0px;
+    height: 500px;
+}
+</style>

+ 125 - 0
src/components/form-create-designer/components/HtmlEditor.vue

@@ -0,0 +1,125 @@
+<template>
+    <div class="_fd-html-editor">
+        <el-button @click="visible=true" style="width: 100%;">{{ title || t('struct.title') }}</el-button>
+        <el-dialog class="_fd-html-editor-con" :title="title || t('struct.title')" v-model="visible"
+                  :close-on-click-modal="false" append-to-body>
+            <div ref="editor" v-if="visible"></div>
+            <template #footer>
+                <div>
+                    <el-button @click="visible = false" size="default">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" @click="onOk" size="default">{{ t('props.ok') }}</el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import CodeMirror from 'codemirror/lib/codemirror';
+import {defineComponent, markRaw} from 'vue';
+import errorMessage from '../utils/message';
+
+export default defineComponent({
+    name: 'HtmlEditor',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: String,
+        title: String,
+        defaultValue: {
+            require: false
+        },
+    },
+    inject: ['designer'],
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        return {
+            editor: null,
+            visible: false,
+            oldVal: null,
+        };
+    },
+    watch: {
+        modelValue() {
+            this.load();
+        },
+        visible(n) {
+            if (n) {
+                this.load();
+            }
+        }
+    },
+    methods: {
+        validateXML(xmlString) {
+            const parser = new DOMParser();
+            const xmlDoc = parser.parseFromString(xmlString, 'application/xml');
+            const parseErrors = xmlDoc.getElementsByTagName('parsererror');
+            if (parseErrors.length > 0) {
+                return parseErrors[0].innerText.split('\n')[0] ?? '';
+            } else {
+                return '';
+            }
+        },
+        load() {
+            this.oldVal = this.modelValue;
+            this.$nextTick(() => {
+                this.editor = markRaw(CodeMirror(this.$refs.editor, {
+                    lineNumbers: true,
+                    mode: 'xml',
+                    lint: true,
+                    line: true,
+                    tabSize: 2,
+                    lineWrapping: true,
+                    value: this.modelValue || ''
+                }));
+            });
+        },
+        onOk() {
+            const str = this.editor.getValue();
+            if (this.validateXML(str)) {
+                errorMessage(this.t('struct.errorMsg'));
+                return false;
+            }
+            this.visible = false;
+            if (str !== this.oldVal) {
+                this.$emit('update:modelValue', str);
+            }
+            return true;
+        },
+    }
+});
+</script>
+
+<style>
+._fd-html-editor {
+    width: 100%;
+}
+
+._fd-html-editor .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-html-editor-con .CodeMirror {
+    height: 450px;
+}
+
+._fd-html-editor-con .CodeMirror-line {
+    line-height: 16px !important;
+    font-size: 13px !important;
+}
+
+._fd-html-editor-con .CodeMirror-lint-tooltip {
+    z-index: 2021 !important;
+}
+
+._fd-html-editor-con .el-dialog__body {
+    padding: 0px 20px;
+}
+</style>

+ 93 - 0
src/components/form-create-designer/components/JsonPreview.vue

@@ -0,0 +1,93 @@
+<template>
+    <el-container class="_fc-json-preview">
+        <el-header height="40px" class="_fc-l-tabs">
+            <div class="_fc-l-tab"
+                 :class="{active: active==='rule'}"
+                 @click="active='rule'"> {{ t('designer.json') }}
+            </div>
+            <div class="_fc-l-tab"
+                 :class="{active: active==='options'}"
+                 @click="active='options'"> {{ t('designer.form') }}
+            </div>
+        </el-header>
+        <el-main style="padding: 8px;">
+            <StructEditor ref="editor" v-model="value" @blur="handleBlur" @focus="handleFocus" format
+                          style="height:100%;"></StructEditor>
+        </el-main>
+    </el-container>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import StructEditor from './StructEditor.vue';
+import {designerForm} from '../utils/form';
+
+export default defineComponent({
+    name: 'JsonPreview',
+    components: {StructEditor},
+    inject: ['designer'],
+    data() {
+        return {
+            active: 'rule',
+            value: this.designer.setupState.getRule(),
+            oldValue: '',
+        }
+    },
+    watch: {
+        active() {
+            this.updateValue();
+        }
+    },
+    computed: {
+        change() {
+            if (this.active === 'rule') {
+                return this.designer.setupState.children;
+            } else {
+                return this.designer.setupState.formOptions;
+            }
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    methods: {
+        updateValue() {
+            if (this.active === 'rule') {
+                this.value = this.designer.setupState.getRule();
+            } else {
+                this.value = this.designer.setupState.getOptions();
+            }
+        },
+        handleFocus() {
+            this.oldValue = designerForm.toJson(this.value);
+        },
+        handleBlur() {
+            if (this.$refs.editor.save() && designerForm.toJson(this.value) !== this.oldValue) {
+                if (this.active === 'rule') {
+                    this.designer.setupState.setRule(this.value || []);
+                } else {
+                    this.designer.setupState.setOptions(this.value || {});
+                }
+            }
+        }
+    },
+    mounted() {
+        this.$watch(() => this.change, () => {
+            this.updateValue();
+        }, {deep: true});
+    }
+});
+</script>
+
+<style>
+._fc-json-preview {
+    display: flex;
+    width: 100%;
+    color: #262626;
+}
+
+._fc-json-preview .CodeMirror {
+    height: 100%;
+    font-size: 12px;
+}
+</style>

+ 72 - 0
src/components/form-create-designer/components/PropsInput.vue

@@ -0,0 +1,72 @@
+<template>
+    <Struct class="_fd-props-input" :modelValue="props" @update:modelValue="onInput" :title="t('designer.customProps')">
+        <i class="fc-icon icon-edit"></i>
+    </Struct>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import Struct from './Struct.vue';
+import extend from '@form-create/utils/lib/extend';
+
+export default defineComponent({
+    name: 'PropsInput',
+    components: {Struct},
+    inject: ['designer'],
+    data() {
+        return {}
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        activeRule() {
+            return this.designer.setupState.activeRule;
+        },
+        props() {
+            const propsKeys = this.activeRule._fc_store?.props_keys || [];
+            const props = {};
+            propsKeys.forEach(k => {
+                if (this.activeRule.props && this.activeRule.props[k]) {
+                    props[k] = this.activeRule.props[k];
+                }
+            });
+            return props;
+        },
+    },
+    methods: {
+        onInput(props) {
+            if (!this.activeRule.props) {
+                this.activeRule.props = {};
+            }
+            if (!this.activeRule._fc_store) {
+                this.activeRule._fc_store = {};
+            }
+            Object.keys(this.props).forEach(k => {
+                if ((props || {})[k] == null) {
+                    delete this.activeRule.props[k];
+                }
+            });
+            extend(this.activeRule.props, props || {});
+            const keys = Object.keys(props || {});
+            if (keys.length) {
+                this.activeRule._fc_store.props_keys = keys;
+            } else {
+                delete this.activeRule._fc_store.props_keys;
+            }
+        }
+    }
+
+});
+</script>
+
+<style>
+._fd-props-input {
+    display: inline-block;
+    width: 16px;
+}
+
+._fd-props-input .fc-icon {
+    cursor: pointer;
+}
+</style>

+ 75 - 0
src/components/form-create-designer/components/Required.vue

@@ -0,0 +1,75 @@
+<template>
+    <div class="_fd-required">
+        <el-switch v-model="required"></el-switch>
+        <LanguageInput v-model="requiredMsg" v-if="required"
+                       :placeholder="t('validate.requiredPlaceholder')"></LanguageInput>
+    </div>
+</template>
+
+<script>
+import is from '@form-create/utils/lib/type';
+import {defineComponent} from 'vue';
+import LanguageInput from './language/LanguageInput.vue';
+
+export default defineComponent({
+    name: 'Required',
+    components: {LanguageInput},
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: {}
+    },
+    inject: ['designer'],
+    watch: {
+        required() {
+            this.update();
+        },
+        requiredMsg() {
+            this.update();
+        },
+        modelValue(n) {
+            const flag = is.String(n);
+            this.required = n === undefined ? false : (flag ? true : !!n);
+            this.requiredMsg = flag ? n : '';
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        const flag = is.String(this.modelValue);
+        return {
+            required: this.modelValue === undefined ? false : (flag ? true : !!this.modelValue),
+            requiredMsg: flag ? this.modelValue : ''
+        };
+    },
+    methods: {
+        update() {
+            let val;
+            if (this.required === false) {
+                val = false;
+            } else {
+                val = this.requiredMsg || true;
+            }
+            this.$emit('update:modelValue', val);
+        },
+    }
+});
+</script>
+
+<style>
+._fd-required {
+    display: flex;
+    align-items: center;
+    width: 100%;
+}
+
+._fd-required .el-input {
+    margin-left: 15px;
+}
+
+._fd-required .el-switch {
+    height: 28px;
+}
+</style>

+ 25 - 0
src/components/form-create-designer/components/Row.vue

@@ -0,0 +1,25 @@
+<template>
+    <el-col :span="24">
+        <div class="_fd-row el-row" :class="{'_fc-child-empty' : !$slots.default}" v-bind="$attrs">
+            <slot name="default"></slot>
+        </div>
+    </el-col>
+
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'fcRow',
+    mounted() {
+    }
+
+});
+</script>
+
+<style>
+._fd-row {
+    width: 100%;
+}
+</style>

+ 142 - 0
src/components/form-create-designer/components/Struct.vue

@@ -0,0 +1,142 @@
+<template>
+    <div class="_fd-struct">
+        <el-badge type="warning" is-dot :hidden="!configured">
+            <div @click="visible=true">
+                <slot>
+                    <el-button class="_fd-plain-button" plain size="small">
+                        {{ title || t('struct.title') }}
+                    </el-button>
+                </slot>
+            </div>
+        </el-badge>
+        <el-dialog class="_fd-struct-con" :title="title || t('struct.title')" v-model="visible" destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body width="800px">
+            <div ref="editor" v-if="visible"></div>
+            <template #footer>
+                <div>
+                    <el-button @click="visible = false" size="default">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" @click="onOk" size="default" color="#2f73ff">{{ t('props.ok') }}</el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import CodeMirror from 'codemirror/lib/codemirror';
+import 'codemirror/mode/javascript/javascript';
+import {deepParseFn, toJSON} from '../utils/index';
+import {deepCopy} from '@form-create/utils/lib/deepextend';
+import {defineComponent, markRaw} from 'vue';
+import is from '@form-create/utils/lib/type';
+import errorMessage from '../utils/message';
+import beautify from 'js-beautify';
+
+export default defineComponent({
+    name: 'Struct',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: [Object, Array, Function],
+        title: String,
+        defaultValue: {
+            require: false
+        },
+        validate: Function,
+    },
+    inject: ['designer'],
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        configured() {
+            return !is.empty(this.modelValue) && Object.keys(this.modelValue).length > 0;
+        },
+    },
+    data() {
+        return {
+            editor: null,
+            visible: false,
+            oldVal: null,
+        };
+    },
+    watch: {
+        modelValue() {
+            this.load();
+        },
+        visible(n) {
+            if (n) {
+                this.load();
+            }
+        },
+    },
+    methods: {
+        load() {
+            const val = toJSON(deepParseFn(this.modelValue ? deepCopy(this.modelValue) : this.defaultValue));
+            this.oldVal = val;
+            this.$nextTick(() => {
+                this.editor = markRaw(CodeMirror(this.$refs.editor, {
+                    lineNumbers: true,
+                    mode: 'javascript',
+                    lint: true,
+                    line: true,
+                    tabSize: 2,
+                    lineWrapping: true,
+                    value: val ? beautify.js(val, {
+                        indent_size: '2',
+                        indent_char: ' ',
+                        max_preserve_newlines: '5',
+                        indent_scripts: 'separate',
+                    }) : '',
+                }));
+            });
+        },
+        onOk() {
+            const str = (this.editor.getValue() || '').trim();
+            let val;
+            try {
+                val = (new Function('return ' + str))();
+            } catch (e) {
+                console.error(e);
+                errorMessage(this.t('struct.errorMsg'));
+                return false;
+            }
+            if (this.validate && false === this.validate(val)) {
+                errorMessage(this.t('struct.errorMsg'));
+                return false;
+            }
+            this.visible = false;
+            if (toJSON(val, null, 2) !== this.oldVal) {
+                this.$emit('update:modelValue', val);
+            }
+            return true;
+        },
+    }
+});
+</script>
+
+<style>
+._fd-struct {
+    width: 100%;
+}
+
+._fd-struct .el-badge {
+    width: 100%;
+}
+
+._fd-struct .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-struct-con .CodeMirror {
+    height: 500px;
+}
+
+._fd-struct-con .el-dialog__body {
+    padding: 0px;
+}
+</style>

+ 121 - 0
src/components/form-create-designer/components/StructEditor.vue

@@ -0,0 +1,121 @@
+<template>
+    <div class="_fd-struct-editor">
+        <div ref="editor"></div>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import CodeMirror from 'codemirror/lib/codemirror';
+import 'codemirror/mode/javascript/javascript';
+import {toJSON} from '../utils/index';
+import {defineComponent, markRaw} from 'vue';
+import errorMessage from '../utils/message';
+import {designerForm} from '../utils/form';
+import beautify from 'js-beautify';
+
+export default defineComponent({
+    name: 'StructEditor',
+    props: {
+        modelValue: [Object, Array, Function],
+        format: Boolean,
+        defaultValue: {
+            require: false
+        }
+    },
+    emits: ['blur', 'focus', 'update:modelValue'],
+    inject: ['designer'],
+    data() {
+        return {
+            editor: null,
+            visible: false,
+            err: false,
+            oldVal: null,
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    watch: {
+        modelValue(n) {
+            if (this.editor) {
+                const val = n ? this.toJson(n) : '';
+                this.oldVal = val;
+                const scrollInfo = this.editor.getScrollInfo();
+                const scrollTop = scrollInfo.top;
+                this.editor.setValue(val);
+                this.editor.scrollTo(0, scrollTop);
+            }
+        }
+    },
+    mounted() {
+        this.$nextTick(() => {
+            this.load();
+        });
+    },
+    methods: {
+        toJson(val) {
+            return this.format ? designerForm.toJson(val, 2) : toJSON(val);
+        },
+        load() {
+            const val = this.modelValue ? this.toJson(this.modelValue) : '';
+            this.oldVal = val;
+            this.$nextTick(() => {
+                this.editor = markRaw(CodeMirror(this.$refs.editor, {
+                    lineNumbers: true,
+                    mode: 'javascript',
+                    lint: true,
+                    line: true,
+                    tabSize: 2,
+                    lineWrapping: true,
+                    value: val ? beautify.js(val, {
+                        indent_size: '2',
+                        indent_char: ' ',
+                        max_preserve_newlines: '5',
+                        indent_scripts: 'separate',
+                    }) : '',
+                }));
+                this.editor.on('blur', () => {
+                    this.$emit('blur');
+                });
+                this.editor.on('focus', () => {
+                    this.$emit('focus');
+                });
+            });
+        },
+        save() {
+            const str = (this.editor.getValue() || '').trim();
+            let val;
+            try {
+                val = (new Function('return ' + str))();
+            } catch (e) {
+                console.error(e);
+                errorMessage(this.t('struct.errorMsg'));
+                return false;
+            }
+            if (this.validate && false === this.validate(val)) {
+                this.err = true;
+                return false;
+            }
+            this.visible = false;
+            if (this.toJson(val) !== this.oldVal) {
+                this.$emit('update:modelValue', val);
+            }
+            return true;
+        },
+    }
+});
+</script>
+
+<style>
+._fd-struct-editor {
+    flex: 1;
+    width: 100%;
+}
+
+._fd-struct-editor > div {
+    height: 100%;
+}
+</style>

+ 162 - 0
src/components/form-create-designer/components/TableOptions.vue

@@ -0,0 +1,162 @@
+<template>
+    <div class="_td-table-opt">
+        <el-table
+            :data="value"
+            border
+            :size="size || 'small'"
+            style="width: 100%">
+            <template v-for="(col,idx) in column" :key="col.label + idx">
+                <el-table-column :label="col.label">
+                    <template #default="scope">
+                        <template v-if="col.value">
+                            <ValueInput :size="size || 'small'" :modelValue="scope.row[col.key]"
+                                        @update:modelValue="(n)=>(scope.row[col.key] = n)"
+                                        @blur="onInput(scope.row)" @change-type="onInput(scope.row)"></ValueInput>
+                        </template>
+                        <template v-else>
+                            <el-input :size="size || 'small'" :modelValue="scope.row[col.key] || ''"
+                                      @Update:modelValue="(n)=>(scope.row[col.key] = n)"
+                                      @blur="onInput(scope.row)"></el-input>
+                        </template>
+                    </template>
+                </el-table-column>
+            </template>
+            <el-table-column width="45" align="center" fixed="right">
+                <template #default="scope">
+                    <i class="fc-icon icon-delete" @click="del(scope.$index)"></i>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="_td-table-opt-handle">
+            <el-button link type="primary" @click="add" v-if="!max || max > value.length">
+                <i class="fc-icon icon-add"></i> {{ t('tableOptions.add') }}
+            </el-button>
+        </div>
+
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import {copy} from '@form-create/utils/lib/extend';
+import ValueInput from './ValueInput.vue';
+
+export default defineComponent({
+    name: 'TableOptions',
+    emits: ['update:modelValue', 'change'],
+    components: {
+        ValueInput
+    },
+    props: {
+        modelValue: [Array, Object],
+        column: {
+            type: Array,
+            default: () => [{label: 'label', key: 'label'}, {label: 'value', key: 'value'}]
+        },
+        valueType: String,
+        max: Number,
+        size: String,
+    },
+    inject: ['designer'],
+    watch: {
+        modelValue() {
+            this.value = this.tidyModelValue();
+        }
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        return {
+            value: this.tidyModelValue(),
+        };
+    },
+    methods: {
+        tidyModelValue() {
+            const modelValue = this.modelValue;
+            if (this.valueType === 'string') {
+                return (modelValue || []).map(value => {
+                    return {value: '' + value}
+                })
+            } else if (this.valueType === 'object') {
+                return Object.keys((modelValue || {})).map(label => {
+                    return {label, value: modelValue[label]}
+                })
+            } else {
+                return [...modelValue || []].map(v => {
+                    return copy(v);
+                });
+            }
+        },
+        tidyValue() {
+            if (this.valueType === 'object') {
+                const obj = {};
+                this.value.forEach(v => {
+                    if (v.label && v.value) {
+                        obj[v.label] = v.value;
+                    }
+                })
+                return obj;
+            } else {
+                return this.value.map(v => {
+                    if (this.valueType === 'string') {
+                        return v.value;
+                    }
+                    return {...v}
+                });
+            }
+        },
+        onInput(item) {
+            if (this.column.length === 1 && '' === item[this.column[0].key]) {
+                return;
+            }
+            const flag = this.column.every(v => {
+                if (v.required === false) {
+                    return true;
+                }
+                if (['object', 'string'].indexOf(this.valueType) > -1) {
+                    return item[v.key] !== undefined && item[v.key] !== '' && item[v.key] !== null;
+                }
+                return item[v.key] !== undefined;
+            })
+            if (flag) {
+                this.input();
+            }
+        },
+        input() {
+            const value = this.tidyValue();
+            this.$emit('update:modelValue', value);
+            this.$emit('change', value);
+        },
+        add() {
+            this.value.push(this.column.reduce((initial, v) => {
+                initial[v.key] = '';
+                return initial;
+            }, {}));
+        },
+        del(idx) {
+            this.value.splice(idx, 1);
+            this.input();
+        }
+    }
+});
+</script>
+
+<style scoped>
+._td-table-opt {
+    width: 100%;
+}
+
+._td-table-opt .el-table {
+    z-index: 1;
+}
+
+._td-table-opt-handle {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-right: 5px;
+}
+</style>

+ 166 - 0
src/components/form-create-designer/components/TreeOptions.vue

@@ -0,0 +1,166 @@
+<template>
+    <div class="_fd-tree-opt">
+        <el-tree
+            :data="value"
+            node-key="index"
+            :expand-on-click-node="false">
+            <template #default="{ node, data }">
+                <div class="_fd-tree-opt-node">
+                    <el-input class="_fd-tree-opt-first" v-model="data[overColumns.label]"
+                              @blur="change"/>
+                    <ValueInput class="_fd-tree-opt-last" v-model="data[overColumns.value]" @blur="change"
+                                @change-type="change">
+                        <template #append>
+                            <div class="_fd-tree-opt-btn" @click="add(node, data)">
+                                <i class="fc-icon icon-add"></i>
+                            </div>
+                            <div class="_fd-tree-opt-btn" @click="append(data)">
+                                <i class="fc-icon icon-add-child"></i>
+                            </div>
+                            <div class="_fd-tree-opt-btn _fd-tree-opt-danger" @click="remove(node, data)">
+                                <i class="fc-icon icon-delete"></i>
+                            </div>
+                        </template>
+                    </ValueInput>
+                </div>
+            </template>
+        </el-tree>
+    </div>
+
+</template>
+
+<script>
+
+import {defineComponent} from 'vue';
+import {deepCopy} from '@form-create/utils/lib/deepextend';
+import ValueInput from './ValueInput.vue';
+
+export default defineComponent({
+    name: 'TreeOptions',
+    emits: ['update:modelValue'],
+    components: {
+        ValueInput
+    },
+    props: {
+        modelValue: Array,
+        columns: Object,
+    },
+    inject: ['designer'],
+    data() {
+        return {
+            value: [...deepCopy(this.modelValue || [])],
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        overColumns() {
+            if (!this.columns) {
+                return {
+                    label: 'label',
+                    value: 'value',
+                };
+            }
+            return {
+                label: this.columns.label || 'label',
+                value: this.columns.value || 'value',
+            }
+        }
+    },
+    created() {
+        if (!this.value.length) {
+            this.value = [{}]
+        }
+    },
+    methods: {
+        tidyValue() {
+            return deepCopy(this.value);
+        },
+        change() {
+            this.$emit('update:modelValue', this.tidyValue());
+        },
+        add(node) {
+            const parent = node.parent;
+            const children = parent.data.children || parent.data;
+            children.push({});
+        },
+        append(data) {
+            if (!data.children) {
+                data.children = [];
+            }
+            data.children.push({});
+        },
+        remove(node, data) {
+            const parent = node.parent;
+            if (parent.data.children) {
+                parent.data.children.splice(parent.data.children.indexOf(data), 1);
+                if (!parent.data.children.length) {
+                    delete parent.data.children;
+                }
+            } else {
+                parent.data.splice(parent.data.indexOf(data), 1);
+            }
+            this.change();
+        },
+    }
+});
+</script>
+
+<style>
+._fd-tree-opt ._fd-tree-opt-btn {
+    height: 19px;
+    width: 18px;
+    color: #fff;
+    text-align: center;
+    line-height: 20px;
+    padding-bottom: 1px;
+    float: left;
+    cursor: pointer;
+    justify-content: center;
+    background-color: #2f73ff;
+}
+
+._fd-tree-opt-node {
+    display: flex;
+    align-items: center;
+}
+
+._fd-tree-opt-first {
+    width: 60px;
+    margin-right: 5px;
+}
+
+._fd-tree-opt-last {
+    width: 165px;
+}
+
+._fd-tree-opt-last._label {
+    width: 175px;
+}
+
+._fd-tree-opt-last._label .el-input-group__append {
+    width: 65px;
+}
+
+._fd-tree-opt ._fd-tree-opt-danger {
+    background-color: #ff2d2e;
+    border-radius: 0 2px 2px 0;
+}
+
+._fd-tree-opt .el-tree-node__content {
+    margin-bottom: 3px;
+    height: 28px;
+}
+
+._fd-tree-opt .el-input__inner {
+    border-right: 0 none;
+}
+
+._fd-tree-opt .el-input-group__append {
+    width: 90px;
+    padding-right: 2px;
+    padding-left: 1px;
+    background: #fff;
+}
+</style>

+ 140 - 0
src/components/form-create-designer/components/TypeSelect.vue

@@ -0,0 +1,140 @@
+<template>
+    <el-dropdown class="_fd-type-select" trigger="click" size="default" popper-class="_fd-type-select-pop"
+                 :disabled="!menus.length" @command="handleCommand">
+        <el-tag type="success" effect="plain" disable-transitions>
+            <template v-if="activeRule">
+                {{ t('com.' + (activeRule._menu.name) + '.name') || activeRule._menu.label }} <i
+                class="fc-icon icon-down" v-if="menus.length"></i>
+            </template>
+            <template v-else>
+                {{
+                    t('com.' + (customForm.config.name) + '.name') || customForm.config.label || customForm.config.name
+                }}
+            </template>
+        </el-tag>
+        <template #dropdown>
+            <el-dropdown-menu>
+                <el-dropdown-item :command="item" v-for="item in menus" :key="item.name">
+                    <div><i class="fc-icon" :class="item.icon || 'icon-input'"></i>{{ t('com.' + (item.name) + '.name') || item.label }}</div>
+                </el-dropdown-item>
+            </el-dropdown-menu>
+        </template>
+    </el-dropdown>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'TypeSelect',
+    inject: ['designer'],
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        activeRule() {
+            return this.designer.setupState.activeRule;
+        },
+        customForm() {
+            return this.designer.setupState.customForm;
+        },
+        menus() {
+            let menus = [];
+            const designer = this.designer.setupState;
+            if (this.activeRule) {
+                const name = this.activeRule._menu.name;
+                const switchConfig = designer.getConfig('switchType', []);
+                if (switchConfig === false) {
+                    return menus;
+                }
+                let switchs = [];
+                switchConfig.forEach(lst => {
+                    if (lst.indexOf(name) > -1) {
+                        switchs.push(...lst);
+                    }
+                });
+                switchs = switchs.filter((key, idx) => {
+                    return key !== name && switchs.indexOf(key) === idx;
+                });
+                if (switchs.length) {
+                    designer.menuList.forEach(item => {
+                        item.list.forEach(menu => {
+                            if (switchs.indexOf(menu.name) > -1) {
+                                menus.push(menu);
+                            }
+                        });
+                    });
+                } else {
+                    designer.menuList.forEach(item => {
+                        if (item.name === this.activeRule._menu.menu) {
+                            item.list.forEach(menu => {
+                                if (menu.name !== name) {
+                                    menus.push(menu);
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+            return menus.filter(menu => this.designer.setupState.hiddenItem.indexOf(menu.name) === -1);
+        }
+    },
+    methods: {
+        handleCommand(item) {
+            let activeRule = this.activeRule;
+            let rule = this.activeRule;
+            if (!rule._menu.inside) {
+                rule = rule.__fc__.parent.rule;
+            }
+            const children = rule.__fc__.parent.rule.children;
+            const replaceRule = this.designer.setupState.makeRule(item);
+            let newRule = replaceRule;
+            if (replaceRule.type === 'DragTool') {
+                newRule = replaceRule.children[0];
+            }
+            if (newRule.field && activeRule.field) {
+                ['title', 'info', 'field', 'validate', 'control', '$required'].forEach(k => {
+                    newRule[k] = activeRule[k];
+                });
+            } else if (activeRule?.computed?.hidden) {
+                newRule.computed = {hidden: activeRule.computed.hidden}
+            }
+            if (activeRule.name) {
+                newRule.name = activeRule.name;
+            }
+            ['name', 'id', 'on'].forEach(k => {
+                if (activeRule[k]) {
+                    newRule[k] = activeRule[k];
+                }
+            })
+            children.splice(children.indexOf(rule), 1, replaceRule);
+            this.$nextTick(() => {
+                this.designer.setupState.triggerActive(newRule);
+            });
+        }
+    }
+});
+</script>
+
+<style>
+._fd-type-select {
+    cursor: pointer;
+}
+
+._fd-type-select.is-disabled {
+    cursor: default;
+}
+
+._fd-type-select .fc-icon {
+    font-size: 14px;
+}
+
+._fd-type-select-pop {
+    max-height: 500px;
+    overflow: auto;
+}
+
+._fd-type-select-pop .fc-icon {
+    font-size: 14px;
+}
+</style>

+ 244 - 0
src/components/form-create-designer/components/Validate.vue

@@ -0,0 +1,244 @@
+<template>
+    <div class="_fd-validate">
+        <template v-for="(item, idx) in validate">
+            <div class="_fd-validate-item">
+                <div class="_fd-validate-title">
+                    <div>
+                        <span>{{ idx + 1 }}</span>
+                        {{ modes[item.mode] }}
+                    </div>
+                    <i class="fc-icon icon-delete2" @click="remove(idx)"></i>
+                </div>
+                <el-row>
+                    <el-col :span="getSpan(item)">
+                        <el-form-item :label="t('validate.mode')">
+                            <el-select v-model="item.trigger" @change="onInput">
+                                <el-option
+                                    v-for="item in triggers"
+                                    :key="item.value"
+                                    :label="item.label"
+                                    :value="item.value"
+                                />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="getSpan(item)">
+                        <el-form-item :label="modes[item.mode]">
+                            <template v-if="item.mode === 'pattern'">
+                                <elInput v-model="item[item.mode]" @change="onInput"></elInput>
+                            </template>
+                            <template v-else-if="item.mode === 'validator'">
+                                <FnInput v-model="item[item.mode]" name="name" :args="['rule', 'value', 'callback']"
+                                         @change="onInput">{{ t('validate.modes.validator') }}
+                                </FnInput>
+                            </template>
+                            <template v-else>
+                                <el-input-number v-model="item[item.mode]" @change="onInput"></el-input-number>
+                            </template>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="24">
+                        <el-form-item :label="t('validate.message')">
+                            <LanguageInput v-model="item.message" :placeholder="t('validate.requiredPlaceholder')"
+                                           @change="onInput">
+                            </LanguageInput>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </div>
+        </template>
+
+        <el-dropdown trigger="click" size="default" popper-class="_fd-validate-pop" @command="handleCommand">
+            <el-button class="_fd-validate-btn" size="small">{{ t('validate.rule') }} +</el-button>
+            <template #dropdown>
+                <el-dropdown-menu>
+                    <el-dropdown-item :command="value" v-for="(label, value) in modes" :key="value">
+                        <div>{{ label }}</div>
+                    </el-dropdown-item>
+                </el-dropdown-menu>
+            </template>
+        </el-dropdown>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import {localeOptions} from '../utils';
+import FnInput from './FnInput.vue';
+import {deepCopy} from '@form-create/utils/lib/deepextend';
+import LanguageInput from './language/LanguageInput.vue';
+
+export default defineComponent({
+    name: 'Validate',
+    inject: ['designer'],
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: Array,
+    },
+    components: {
+        LanguageInput,
+        FnInput,
+    },
+    watch: {
+        modelValue(n) {
+            this.validate = this.parseValue(n || []);
+        }
+    },
+    data() {
+        return {
+            validate: this.parseValue(this.modelValue || []),
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        modes() {
+            const activeRule = this.designer.setupState.activeRule;
+            if (activeRule && activeRule._menu.subForm === 'object') {
+                return {
+                    validator: this.t('validate.modes.validator'),
+                }
+            } else {
+                return {
+                    min: this.t('validate.modes.min'),
+                    max: this.t('validate.modes.max'),
+                    len: this.t('validate.modes.len'),
+                    pattern: this.t('validate.modes.pattern'),
+                    validator: this.t('validate.modes.validator'),
+                }
+            }
+        },
+        triggers() {
+            return localeOptions(this.t, [
+                {label: 'blur', value: 'blur'},
+                {label: 'change', value: 'change'},
+                {label: 'submit', value: 'submit'},
+            ]);
+        }
+    },
+    methods: {
+        handleCommand(mode) {
+            this.validate.push({
+                transform: new Function('val', 'this.type = val == null ? \'string\' : (Array.isArray(val) ? \'array\' : (typeof val)); return val;'),
+                mode,
+                trigger: 'blur'
+            });
+        },
+        autoMessage(item) {
+            const title = this.designer.setupState.activeRule.title;
+            if (this.designer.setupState.activeRule) {
+                item.message = this.t('validate.autoRequired', {title})
+                this.onInput();
+            }
+        },
+        getSpan(item) {
+            return ['pattern', 'validator', 'required'].indexOf(item.mode) > -1 ? 24 : 12;
+        },
+        onInput: function () {
+            this.$emit('update:modelValue', this.validate.map(item => {
+                item = {...item};
+                if (!item.message) {
+                    delete item.message;
+                }
+                return item;
+            }));
+        },
+        remove(idx) {
+            this.validate.splice(idx, 1);
+            this.onInput();
+        },
+        parseValue(val) {
+            return deepCopy(val.map(v => {
+                if (v.validator) {
+                    v.mode = 'validator';
+                }
+                if (!v.mode) {
+                    Object.keys(v).forEach(k => {
+                        if (['message', 'type', 'trigger', 'mode'].indexOf(k) < 0) {
+                            v.mode = k;
+                        }
+                    });
+                }
+                return v;
+            }));
+        }
+    }
+});
+</script>
+
+<style>
+
+._fd-validate {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+}
+
+._fd-validate-btn {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-validate-pop .el-dropdown-menu__item {
+    width: 248px;
+}
+
+._fd-validate-item {
+    border-bottom: 1px dashed #ECECEC;
+    margin-bottom: 10px;
+}
+
+._fd-validate-item .el-col-12:first-child {
+    padding-right: 5px;
+}
+
+._fd-validate-item .el-col-12 + .el-col-12 {
+    padding-left: 5px;
+}
+
+._fd-validate-item .el-input-number {
+    width: 100%;
+}
+
+._fd-validate-title {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    margin-bottom: 10px;
+}
+
+._fd-validate-title > div {
+    display: flex;
+    align-items: center;
+}
+
+._fd-validate-title > div > span {
+    width: 16px;
+    height: 16px;
+    background: #ECECEC;
+    text-align: center;
+    font-size: 12px;
+    line-height: 16px;
+    border-radius: 15px;
+    margin-right: 5px;
+}
+
+._fd-validate-title i {
+    cursor: pointer;
+}
+
+._fd-validate-title i:hover {
+    color: #FF2E2E;
+}
+
+._fd-validate .append-msg {
+    cursor: pointer;
+}
+
+._fd-validate .el-input-group__append {
+    padding: 0 10px;
+}
+</style>

+ 88 - 0
src/components/form-create-designer/components/ValueInput.vue

@@ -0,0 +1,88 @@
+<template>
+    <el-input class="_fd-value-input" v-model="value" @blur="onBlur" v-bind="$attrs">
+        <template #prepend>
+            <el-select v-model="type" style="width: 60px">
+                <el-option :label="t('validate.types.string')" value="1"/>
+                <el-option :label="t('validate.types.number')" value="2"/>
+                <el-option :label="t('validate.types.boolean')" value="3"/>
+            </el-select>
+        </template>
+        <template #append v-if="$slots.append">
+            <slot name="append"></slot>
+        </template>
+    </el-input>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'ValueInput',
+    emits: ['update:modelValue', 'change', 'change-type', 'blur'],
+    inject: ['designer'],
+    props: {
+        modelValue: [String, Number, Boolean],
+    },
+    data() {
+        return {
+            type: '1',
+            value: '',
+        }
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        }
+    },
+    watch: {
+        modelValue: {
+            handler: function (val) {
+                if (typeof val === 'number') {
+                    this.type = '2';
+                } else if (typeof val === 'boolean') {
+                    this.type = '3';
+                } else {
+                    this.type = '1';
+                }
+                this.value = null == val ? '' : ('' + val);
+            },
+            immediate: true,
+        },
+        type() {
+            this.updateValue(this.value);
+            this.$emit('change-type', this.type);
+        }
+    },
+    methods: {
+        onBlur(...args) {
+            if (this.value !== this.toValue(this.modelValue)) {
+                this.updateValue(this.value);
+            }
+            this.$emit('blur', ...args);
+        },
+        updateValue(val) {
+            const value = this.toValue(val);
+            this.$emit('update:modelValue', value);
+            this.$emit('change', value);
+        },
+        toValue(val) {
+            if (this.type === '1') {
+                return '' + val;
+            } else if (this.type === '2') {
+                return parseFloat(val) || 0;
+            }
+            return val === 'true';
+        }
+    }
+});
+</script>
+
+<style>
+._fd-value-input .el-input__validateIcon {
+    display: none;
+}
+
+._fd-value-input .el-select, ._fd-value-input .el-select__wrapper {
+    height: 100%;
+}
+</style>

+ 45 - 0
src/components/form-create-designer/components/Warning.vue

@@ -0,0 +1,45 @@
+<template>
+    <el-tooltip
+        effect="dark"
+        placement="top-start"
+        popper-class="_fd-warning-pop"
+    >
+        <template #content>
+            <span v-html="tooltip"></span>
+        </template>
+        <template v-if="$slots.default">
+            <span class="_fd-warning-text">
+                <slot></slot>
+            </span>
+        </template>
+        <template v-else>
+            <i class="fc-icon icon-question"></i>
+        </template>
+    </el-tooltip>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'Warning',
+    props: {
+        tooltip: String,
+    },
+    data() {
+        return {}
+    },
+});
+</script>
+
+<style>
+._fd-warning-pop {
+    max-width: 400px;
+}
+
+._fd-warning-text {
+    text-decoration: underline;
+    text-decoration-style: dashed;
+    cursor: help;
+}
+</style>

+ 165 - 0
src/components/form-create-designer/components/language/LanguageConfig.vue

@@ -0,0 +1,165 @@
+<template>
+    <div class="_fd-language-config">
+        <div class="_fc-l-label">{{ t('language.name') }}</div>
+        <div class="_fc-l-info">
+            {{ t('warning.language') }}
+        </div>
+        <div class="_fd-lc-header">
+            <el-button size="small" @click="addColumn">{{ t('language.add') }}</el-button>
+            <el-button size="small" type="danger" plain :disabled="!selected.length" @click="batchRmColumn">
+                {{ t('language.batchRemove') }}
+            </el-button>
+        </div>
+        <div class="_fd-lc-body">
+            <el-table :data="column" size="small" ref="table"
+                      @selection-change="selectionChange" row-key="key">
+                <el-table-column type="selection" width="30px"></el-table-column>
+                <el-table-column prop="key" label="Key" width="90px"></el-table-column>
+                <template v-for="item in localeOptions" :key="item.value">
+                    <el-table-column :prop="item.value" :label="item.label" min-width="100px">
+                        <template #default="scope">
+                            <template v-if="scope.row.input">
+                                <el-input size="small" v-model="scope.row[item.value]" @blur="saveColumn(scope.row, true)"></el-input>
+                            </template>
+                            <template v-else>
+                                {{ scope.row[item.value] || '-' }}
+                            </template>
+                        </template>
+                    </el-table-column>
+                </template>
+                <el-table-column width="75px" :label="t('tableOptions.handle')" fixed="right">
+                    <template #default="scope">
+                        <div class="_fd-lc-handle">
+                            <i class="fc-icon icon-edit" v-if="!scope.row.input" @click="scope.row.input = true"></i>
+                            <i class="fc-icon icon-check" v-else @click="saveColumn(scope.row)"></i>
+                            <i class="fc-icon icon-group" @click="copy(scope.row.key)"></i>
+                            <i class="fc-icon icon-delete-circle" @click="rmColumn(scope.$index)"></i>
+                        </div>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </div>
+    </div>
+
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import {copyTextToClipboard} from '../../utils';
+
+export default defineComponent({
+    name: 'LanguageConfig',
+    inject: ['designer'],
+    computed: {
+        localeOptions() {
+            return this.designer.setupState.getConfig('localeOptions', [
+                {value: 'zh-cn', label: '简体中文'},
+                {value: 'en', label: 'English'},
+            ]);
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        return {
+            column: [],
+            uni: 0,
+            selected: [],
+        }
+    },
+    methods: {
+        copy(key) {
+            copyTextToClipboard(key);
+        },
+        addColumn() {
+            this.column.unshift({
+                key: this.randomString(),
+                input: true,
+            })
+        },
+        saveColumn(row, input) {
+            row.input = input || false;
+            const language = this.designer.setupState.formOptions.language;
+            this.localeOptions.forEach(item => {
+                if (!language[item.value]) {
+                    language[item.value] = {};
+                }
+                language[item.value][row.key] = row[item.value];
+            })
+        },
+        rmColumn(idx) {
+            const row = this.column[idx];
+            this.column.splice(idx, 1);
+            const language = this.designer.setupState.formOptions.language;
+            this.localeOptions.forEach(item => {
+                if (language[item.value]) {
+                    delete language[item.value][row.key]
+                }
+            })
+        },
+        batchRmColumn() {
+            this.selected.forEach(item => {
+                this.rmColumn(this.column.indexOf(item));
+            });
+            this.selected = [];
+        },
+        selectionChange(list) {
+            this.selected = list;
+        },
+        randomString() {
+            const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+            let result = '';
+            const charactersLength = characters.length;
+
+            for (let i = 0; i < 7; i++) {
+                result += characters.charAt(Math.floor(Math.random() * charactersLength));
+            }
+            return characters.charAt((this.uni++) % 26) + result;
+        }
+    },
+    mounted() {
+        const language = this.designer.setupState.formOptions.language || {};
+        const column = {};
+        Object.keys(language).forEach(lang => {
+            Object.keys(language[lang]).forEach(key => {
+                if (!column[key]) {
+                    column[key] = {
+                        key: key,
+                    }
+                }
+                column[key][lang] = language[lang][key];
+            })
+        });
+        this.column = Object.values(column);
+    }
+
+});
+</script>
+
+<style>
+._fd-lc-body, ._fd-lc-header {
+    padding: 0 12px;
+}
+
+._fd-lc-body {
+    overflow: auto;
+}
+
+._fd-lc-header {
+    display: flex;
+    justify-content: flex-end;
+    margin-bottom: 12px;
+}
+
+._fd-language-config .el-table__cell {
+    height: 34px;
+}
+
+._fd-lc-handle {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    cursor: pointer;
+}
+</style>

+ 188 - 0
src/components/form-create-designer/components/language/LanguageInput.vue

@@ -0,0 +1,188 @@
+<template>
+    <el-input class="_fd-language-input" :class="{'is-variable': isVar}" :placeholder="placeholder" :disabled="disabled"
+              :modelValue="modelValue"
+              @update:modelValue="onInput"
+              @blur="$emit('blur')"
+              :size="size || 'small'">
+        <template #append>
+            <el-popover placement="bottom-end" :width="300" :hide-after="0" trigger="click" ref="pop"
+                        popper-class="_fd-language-popover">
+                <template #reference>
+                    <i class="fc-icon icon-language"></i>
+                </template>
+                <div class="_fd-language-list">
+                    <div class="_fd-language-header">
+                        <div class="_fd-language-title">
+                            {{ t('language.select') }}<i class="fc-icon icon-setting" @click="openConfig"></i>
+                        </div>
+                        <div class="_fd-language-name">
+                            <template v-for="item in localeList" :key="item.value">
+                                <div>{{ item.label }}</div>
+                            </template>
+                        </div>
+                    </div>
+                    <template v-for="lang in language" :key="lang.key">
+                        <div class="_fd-language-item" @click="clickLang(lang.key)">
+                            <template v-for="item in localeList" :key="item.value">
+                                <div>{{ lang[item.value] || '-' }}</div>
+                            </template>
+                        </div>
+                    </template>
+                </div>
+            </el-popover>
+        </template>
+    </el-input>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'LanguageInput',
+    inject: ['designer'],
+    emits: ['update:modelValue', 'blur', 'change'],
+    props: {
+        size: String,
+        placeholder: String,
+        modelValue: String,
+        disabled: Boolean,
+    },
+    computed: {
+        isVar() {
+            return !!(this.modelValue || '').match(/^\{\{\s*\$t\.(.+)\s*\}\}$/);
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+        localeList() {
+            const localeOptions = this.designer.setupState.getConfig('localeOptions', [
+                {value: 'zh-cn', label: '简体中文'},
+                {value: 'en', label: 'English'},
+            ]);
+            const localeList = [];
+            const locale = this.designer.props?.locale?.name || 'zh-cn';
+            localeOptions.forEach((item) => {
+                if (item.value === locale) {
+                    localeList.unshift(item);
+                } else if (localeList.length < 2) {
+                    localeList.push(item);
+                }
+            });
+            if (localeList.length > 2) {
+                localeList.pop();
+            }
+            return localeList;
+        },
+        language() {
+            const language = this.designer.setupState.formOptions.language || {};
+            const column = {};
+            Object.keys(language).forEach(lang => {
+                Object.keys(language[lang]).forEach(key => {
+                    if (!column[key]) {
+                        column[key] = {
+                            key: key,
+                        }
+                    }
+                    column[key][lang] = language[lang][key];
+                })
+            });
+            return Object.values(column);
+        }
+    },
+    methods: {
+        openConfig() {
+            this.designer.setupState.activeModule = 'language';
+        },
+        clickLang(key) {
+            this.onInput(`{{$t.${key}}}`);
+            this.$refs.pop.hide();
+        },
+        onInput(val) {
+            this.$emit('update:modelValue', val);
+            this.$emit('change', val);
+        }
+    },
+    mounted() {
+    }
+
+});
+</script>
+
+<style>
+._fd-language-list {
+    max-height: 320px;
+    padding-top: 70px;
+    overflow: auto;
+}
+
+._fd-language-input .el-input-group__append {
+    width: 25px;
+    padding: 0;
+    margin: 0;
+    color: #AAAAAA;
+    cursor: pointer;
+}
+
+._fd-language-input.is-variable input {
+    color: #2E73FF;
+}
+
+._fd-language-header, ._fd-language-item {
+    display: flex;
+    border-bottom: 1px solid #ECECEC;
+    padding: 0 12px;
+}
+
+._fd-language-header {
+    font-weight: 500;
+    padding-top: 10px;
+    overflow: auto;
+    color: #262626;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    background-color: #FFFFFF;
+    flex-direction: column;
+}
+
+._fd-language-name > div, ._fd-language-item > div {
+    flex: 1;
+    font-size: 12px;
+    padding: 5px;
+    min-width: 70px;
+}
+
+._fd-language-title {
+    margin: 6px 0;
+}
+
+._fd-language-title .fc-icon {
+    color: #2E73FF;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+._fd-language-name {
+    display: flex;
+}
+
+._fd-language-name > div {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+._fd-language-item {
+    cursor: pointer;
+}
+
+._fd-language-item:hover {
+    color: #2E73FF;
+    background-color: #CCDFFF;
+}
+
+._fd-language-popover {
+    padding: 0 !important;
+}
+</style>

+ 242 - 0
src/components/form-create-designer/components/style/BorderInput.vue

@@ -0,0 +1,242 @@
+<template>
+    <ConfigItem :label="t('style.border')">
+        <div class="line-box" :style="borderStyleStr">
+            <div class="line-box-con"></div>
+        </div>
+        <template #append>
+            <div class="_fd-border-input">
+                <div class="_fd-bi-left">
+                    <div class="_fd-bil-row">
+                        <div class="_fd-bil-col" :class="active === 'Top' ? 'active' : ''" @click="active = 'Top'">┳
+                        </div>
+                    </div>
+                    <div class="_fd-bil-row">
+                        <div class="_fd-bil-col" :class="active === 'Left' ? 'active' : ''" @click="active = 'Left'">┣
+                        </div>
+                        <div class="_fd-bil-col" :class="active === '' ? 'active' : ''" @click="active = ''">╋</div>
+                        <div class="_fd-bil-col" :class="active === 'Right' ? 'active' : ''" @click="active = 'Right'">
+                            ┫
+                        </div>
+                    </div>
+                    <div class="_fd-bil-row">
+                        <div class="_fd-bil-col" :class="active === 'Bottom' ? 'active' : ''"
+                             @click="active = 'Bottom'">┻
+                        </div>
+                    </div>
+                </div>
+                <div class="_fd-bi-right">
+                    <el-select v-model="curStyle" clearable>
+                        <el-option
+                            v-for="item in lineType"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value"
+                        >
+                            <div class="_fd-bi-opt">
+                                <div class="_line" :class="item.value"></div>
+                            </div>
+                        </el-option>
+                    </el-select>
+                    <SizeInput v-model="curWidth"/>
+                    <ColorInput v-model="curColor"/>
+                </div>
+            </div>
+        </template>
+    </ConfigItem>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import SizeInput from './SizeInput.vue';
+import ColorInput from './ColorInput.vue';
+import ConfigItem from './ConfigItem.vue';
+import {toLine} from '@form-create/utils';
+
+export default defineComponent({
+    name: 'BorderInput',
+    components: {ColorInput, SizeInput, ConfigItem},
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({}),
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyValue();
+            this.initCur();
+        },
+        active() {
+            this.initCur();
+        },
+    },
+    computed: {
+        borderStyleStr() {
+            let str = '';
+            Object.keys(this.borderStyle).forEach((key) => {
+                if (this.borderStyle[key] !== '') {
+                    str += toLine(key) + ': ' + this.borderStyle[key] + ';';
+                }
+            }, {})
+            return str;
+        },
+    },
+    data() {
+        const t = this.designer.setupState.t;
+        return {
+            t,
+            active: '',
+            borderStyle: {},
+            curStyle: '',
+            curColor: '',
+            curWidth: '',
+            lineType: ['solid', 'dashed', 'dotted', 'double'].map(k => {
+                return {value: k, label: t('style.' + k)}
+            }),
+            position: ['Top', 'Left', 'Bottom', 'Right'],
+            type: ['Style', 'Color', 'Width'],
+            unwatch: null,
+        }
+    },
+    methods: {
+        tidyValue() {
+            const key = [];
+            this.borderStyle = {};
+            ['', ...this.position].forEach(k => {
+                this.type.forEach(t => {
+                    key.push('border' + k + t);
+                });
+            });
+            key.forEach(k => {
+                this.borderStyle[k] = this.modelValue[k] || '';
+            });
+        },
+        onInput() {
+            const style = Object.keys(this.borderStyle).reduce((acc, key) => {
+                if (this.borderStyle[key] !== '') {
+                    acc[key] = this.borderStyle[key]
+                }
+                return acc
+            }, {})
+            this.$emit('update:modelValue', style)
+            this.$emit('change', style)
+        },
+        pushCur() {
+            this.borderStyle['border' + this.active + 'Style'] = this.curStyle || '';
+            this.borderStyle['border' + this.active + 'Color'] = this.curColor || '';
+            this.borderStyle['border' + this.active + 'Width'] = this.curWidth || '';
+            this.onInput();
+        },
+        initCur() {
+            this.unwatch && this.unwatch();
+            this.curStyle = this.borderStyle['border' + this.active + 'Style'] || '';
+            this.curColor = this.borderStyle['border' + this.active + 'Color'] || '';
+            this.curWidth = this.borderStyle['border' + this.active + 'Width'] || '';
+            this.unwatch = this.$watch(() => [this.curStyle, this.curColor, this.curWidth], () => {
+                this.pushCur();
+            });
+        },
+    },
+    created() {
+        this.tidyValue();
+        this.initCur();
+    }
+
+});
+</script>
+
+<style>
+._fd-border-input {
+    width: 100%;
+    height: 110px;
+    display: flex;
+    justify-content: center;
+}
+
+._fd-border-input ._fd-bi-left {
+    width: 115px;
+    height: 115px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+._fd-border-input ._fd-bi-right {
+    width: 140px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    padding: 5px;
+}
+
+._fd-border-input ._fd-bi-right ._fd-color-input {
+    width: 140px;
+}
+
+._fd-bi-opt {
+    width: 100%;
+    display: flex;
+    height: 100%;
+    align-items: center;
+}
+
+._fd-bi-opt ._line {
+    width: 100%;
+}
+
+._fd-bi-opt .solid {
+    border: 1px solid #000;
+}
+
+._fd-bi-opt .dashed {
+    border: 1px dashed #000;
+}
+
+._fd-bi-opt .dotted {
+    border: 1px dotted #000;
+}
+
+._fd-bi-opt .double {
+    border: 1px double #000;
+}
+
+._fd-border-input ._fd-bil-row {
+    height: 38px;
+    display: flex;
+    justify-content: center;
+}
+
+._fd-border-input ._fd-bil-col {
+    width: 22px;
+    height: 22px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    margin: 8px;
+    font-size: 16px;
+}
+
+._fd-border-input ._fd-bil-col.active {
+    outline: 1px dashed #2E73FF;
+    color: #2E73FF;
+}
+
+.line-box {
+    width: 150px;
+    height: 20px;
+    padding: 1px;
+    box-sizing: border-box;
+}
+
+.line-box-con {
+    width: 100%;
+    height: 100%;
+    background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAD5JREFUOE9jZGBg+M+AChjR+HjlQYqHgQFoXibNS+gBBjKMpDAZHAaQ5GQGBgYUV4+mA7QAgaYokgJ14NMBAK1TIAlUJpxYAAAAAElFTkSuQmCC");
+    opacity: 0.3;
+}
+
+</style>

+ 166 - 0
src/components/form-create-designer/components/style/BoxSizeInput.vue

@@ -0,0 +1,166 @@
+<template>
+    <div class="_fd-box-size-input">
+        <ConfigItem :label="t('props.size')" :info="Object.keys(modelValue).length > 0 ? t('struct.configured') : ''">
+            <template #append>
+                <el-form label-position="top" size="small">
+                    <el-form-item :label="t('style.' + key)" v-for="key in keys" :key="key">
+                        <SizeInput v-model="boxStyle[key]" @change="onInput"></SizeInput>
+                    </el-form-item>
+                    <el-form-item :label="t('style.overflow.name')" style="grid-column: span 2;">
+                        <el-radio-group :modelValue="boxStyle.overflow">
+                            <el-tooltip
+                                effect="dark"
+                                :content="t('style.overflow.' + item.value)"
+                                placement="top"
+                                persistent
+                                :hide-after="0"
+                                v-for="item in overflow"
+                                :key="item.value"
+                            >
+                                <el-radio-button :label="item.value" :value="item.value"
+                                                 @click="changeOverflow(item.value)">
+                                    <template v-if="item.text">
+                                        <span style="font-size: 12px;line-height: 16px;">Auto
+                                        </span>
+                                    </template>
+                                    <template v-else>
+                                        <i class="fc-icon" :class="item.icon"></i>
+                                    </template>
+                                </el-radio-button>
+                            </el-tooltip>
+                        </el-radio-group>
+                    </el-form-item>
+                </el-form>
+            </template>
+        </ConfigItem>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import ConfigItem from './ConfigItem.vue';
+import SizeInput from './SizeInput.vue';
+
+export default defineComponent({
+    name: 'BoxSizeInput',
+    components: {SizeInput, ConfigItem},
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({}),
+        }
+    },
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    data() {
+        return {
+            overflow: [
+                {
+                    value: 'visible',
+                    icon: 'icon-eye',
+                },
+                {
+                    value: 'hidden',
+                    icon: 'icon-eye-close',
+                },
+                {
+                    value: 'scroll',
+                    icon: 'icon-scroll',
+                },
+                {
+                    value: 'auto',
+                    text: 'Auto',
+                },
+            ],
+            keys: ['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'],
+            boxStyle: {
+                width: '',
+                minWidth: '',
+                maxWidth: '',
+                height: '',
+                minHeight: '',
+                maxHeight: '',
+                overflow: '',
+            },
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyValue();
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        }
+    },
+    methods: {
+        tidyValue() {
+            this.boxStyle = {
+                width: '',
+                minWidth: '',
+                maxWidth: '',
+                height: '',
+                minHeight: '',
+                maxHeight: '',
+                overflow: '',
+            };
+            if (!this.modelValue) {
+                return;
+            }
+            Object.keys(this.boxStyle).forEach(k => {
+                if (this.modelValue[k]) {
+                    this.boxStyle[k] = this.modelValue[k];
+                }
+            });
+        },
+        onInput() {
+            const style = Object.keys(this.boxStyle).reduce((acc, key) => {
+                if (this.boxStyle[key] !== '') {
+                    acc[key] = this.boxStyle[key]
+                }
+                return acc
+            }, {})
+            this.$emit('update:modelValue', style)
+            this.$emit('change', style)
+        },
+        changeOverflow(val) {
+            this.boxStyle.overflow = this.boxStyle.overflow === val ? '' : val;
+            this.onInput();
+        },
+        change(type, e) {
+            this.boxStyle[type] = e.target.value;
+        },
+    },
+    created() {
+        this.tidyValue();
+    }
+});
+
+</script>
+
+<style>
+._fd-box-size-input .el-form {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    width: 100%;
+    grid-column-gap: 10px;
+}
+
+._fd-box-size-input .el-radio-group {
+    width: 100%;
+}
+
+._fd-box-size-input .el-radio-button__inner {
+    width: 100%;
+    padding: 4px;
+}
+
+._fd-box-size-input .el-radio-button {
+    flex: 1;
+}
+
+._fd-box-size-input ._fd-size-input .el-input-number--small {
+    width: 100%;
+}
+</style>

+ 269 - 0
src/components/form-create-designer/components/style/BoxSpaceInput.vue

@@ -0,0 +1,269 @@
+<template>
+    <div class="_fd-box-space-input">
+        <div class="_padding">
+            <span class="_padding-title">
+                {{ t('style.margin') }}
+            </span>
+            <input class="_fd-input _fd-top" placeholder="        " :value="boxStyle.marginTop" type="text"
+                   @blur="(e)=>setValue('margin','Top', e)" @input="(e)=>change('marginTop', e)">
+            <input class="_fd-input _fd-right" placeholder="        " :value="boxStyle.marginRight" type="text"
+                   @blur="(e)=>setValue('margin','Right', e)" @input="(e)=>change('marginRight', e)">
+            <input class="_fd-input _fd-bottom" placeholder="        " :value="boxStyle.marginBottom" type="text"
+                   @blur="(e)=>setValue('margin','Bottom', e)" @input="(e)=>change('marginBottom', e)">
+            <input class="_fd-input _fd-left" placeholder="        " :value="boxStyle.marginLeft" type="text"
+                   @blur="(e)=>setValue('margin','Left', e)" @input="(e)=>change('marginLeft', e)">
+            <div class="_fd-help">
+                <i class="fc-icon icon-link2" title="lock" :class="marginLock ? 'active' : ''"
+                   @click="lock('margin')"></i>
+                <i class="fc-icon icon-delete-circle" title="clear" @click="clear('margin')"></i>
+            </div>
+            <div class="_margin">
+                <span class="_margin-title">
+                    {{ t('style.padding') }}
+                </span>
+                <div class="_fd-help">
+                    <i class="fc-icon icon-link2" title="lock" :class="paddingLock ? 'active' : ''"
+                       @click="lock('padding')"></i>
+                    <i class="fc-icon icon-delete-circle" title="clear" @click="clear('padding')"></i>
+                </div>
+                <input class="_fd-input _fd-top" placeholder="        " :value="boxStyle.paddingTop" type="text"
+                       @blur="(e)=>setValue('padding','Top', e)" @input="(e)=>change('paddingTop', e)">
+                <input class="_fd-input _fd-right" placeholder="        " :value="boxStyle.paddingRight" type="text"
+                       @blur="(e)=>setValue('padding','Right', e)" @input="(e)=>change('paddingRight', e)">
+                <input class="_fd-input _fd-bottom" placeholder="        " :value="boxStyle.paddingBottom" type="text"
+                       @blur="(e)=>setValue('padding','Bottom', e)" @input="(e)=>change('paddingBottom', e)">
+                <input class="_fd-input _fd-left" placeholder="        " :value="boxStyle.paddingLeft" type="text"
+                       @blur="(e)=>setValue('padding','Left', e)" @input="(e)=>change('paddingLeft', e)">
+                <div class="_box">
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import ConfigItem from './ConfigItem.vue';
+
+export default defineComponent({
+    name: 'BoxSpaceInput',
+    components: {ConfigItem},
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({}),
+        }
+    },
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    data() {
+        return {
+            position: ['Top', 'Right', 'Bottom', 'Left'],
+            boxStyle: {
+                margin: '',
+                padding: '',
+                marginLeft: '',
+                marginRight: '',
+                marginTop: '',
+                marginBottom: '',
+                paddingLeft: '',
+                paddingRight: '',
+                paddingTop: '',
+                paddingBottom: '',
+            },
+            marginLock: false,
+            paddingLock: false,
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyValue();
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        }
+    },
+    methods: {
+        tidyValue() {
+            this.boxStyle = {};
+            ['margin', 'padding'].forEach(k => {
+                this.boxStyle[k] = this.modelValue[k] || '';
+                this.position.forEach(p => {
+                    this.boxStyle[k + p] = this.tidySize(this.modelValue[k + p] || this.modelValue[k] || '');
+                });
+            })
+        },
+        onInput() {
+            const style = Object.keys(this.boxStyle).reduce((acc, key) => {
+                if (this.boxStyle[key] !== '') {
+                    acc[key] = this.boxStyle[key]
+                }
+                return acc
+            }, {})
+            this.$emit('update:modelValue', style)
+            this.$emit('change', style)
+        },
+        tidySize(val) {
+            const regex = /^(\d*\.?\d+)(px|rem|%|vh|vw|em)$/
+            if (!regex.test(val)) {
+                if (val === 'auto') {
+                    return val;
+                }
+                const number = parseInt(val);
+                if (isNaN(number)) {
+                    return '';
+                } else {
+                    return number + 'px';
+                }
+            }
+            return val;
+        },
+        setValue(type, key, e) {
+            const value = this.tidySize(e.target.value);
+            if (!type) {
+                this.boxStyle[key] = value;
+            } else if (this[type + 'Lock']) {
+                this.position.forEach(v => {
+                    this.boxStyle[type + v] = value;
+                })
+            } else {
+                this.boxStyle[type + key] = value;
+            }
+            this.onInput();
+        },
+        change(type, e) {
+            this.boxStyle[type] = e.target.value;
+        },
+        clear(type) {
+            this.position.forEach(v => {
+                this.boxStyle[type + v] = '';
+            })
+            this.onInput();
+        },
+        lock(type) {
+            const key = type + 'Lock';
+            this[key] = !this[key];
+        },
+
+    },
+    created() {
+        this.tidyValue();
+    }
+});
+
+</script>
+
+<style>
+
+._fd-box-space-input {
+    color: #000000;
+}
+
+._fd-box-space-input ._padding, ._fd-box-space-input ._margin {
+    width: 100%;
+    height: 180px;
+    background-color: #F2CEA5;
+    padding: 40px 55px;
+    box-sizing: border-box;
+    position: relative;
+}
+
+html.dark ._fd-box-space-input ._padding, ._fd-box-space-input ._margin {
+    background-color: #A9855C;
+}
+
+._fd-box-space-input ._margin {
+    width: 100%;
+    height: 100px;
+    background-color: #C6CF92;
+}
+
+._fd-box-space-input ._fd-input {
+    display: inline-block;
+    width: 30%;
+    max-width: 40px;
+    height: 20px;
+    border: 0 none;
+    padding: 0;
+    margin: 0;
+    outline: 0 none;
+    text-align: center;
+    font-size: 12px;
+    background-color: unset;
+    text-decoration: underline;
+}
+
+._fd-box-space-input ._fd-input:hover, ._fd-box-space-input ._fd-input:focus {
+    background-color: #ECECEC;
+    opacity: 0.9;
+    color: #262626;
+}
+
+._fd-box-space-input ._fd-left, ._fd-box-space-input ._fd-right {
+    position: absolute;
+    left: 7px;
+    top: 50%;
+    margin-top: -10px;
+}
+
+._fd-box-space-input ._fd-top, ._fd-box-space-input ._fd-bottom {
+    position: absolute;
+    left: 50%;
+    top: 5px;
+    margin-left: -20px;
+}
+
+._fd-box-space-input ._fd-bottom {
+    top: unset;
+    bottom: 15px;
+}
+
+._fd-box-space-input ._fd-right {
+    left: unset;
+    right: 2px;
+}
+
+._fd-box-space-input ._box {
+    width: 100%;
+    height: 100%;
+    background-color: #94B5C0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+._fd-box-space-input ._padding-title, ._fd-box-space-input ._margin-title {
+    position: absolute;
+    top: 2px;
+    left: 4px;
+}
+
+._fd-box-space-input ._fd-help {
+    display: flex;
+    align-items: center;
+    position: absolute;
+    right: 5px;
+    top: 5px;
+    color: #AAAAAA;
+}
+
+._fd-box-space-input ._padding .fc-icon {
+    cursor: pointer;
+    color: #262626;
+    font-size: 12px;
+}
+
+._fd-box-space-input ._padding .fc-icon + .fc-icon {
+    margin-left: 2px;
+}
+
+._fd-box-space-input .fc-icon.active {
+    color: #2E73FF;
+}
+
+._fd-box-space-input ._fd-x {
+    margin: 0 5px;
+}
+</style>

+ 60 - 0
src/components/form-create-designer/components/style/ColorInput.vue

@@ -0,0 +1,60 @@
+<template>
+    <div class="_fd-color-input">
+        <el-input clearable v-model="value">
+            <template #append>
+                <el-color-picker show-alpha color-format="hex" v-model="value"/>
+            </template>
+        </el-input>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'ColorInput',
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: String,
+    },
+    watch: {
+        modelValue() {
+            this.value = this.modelValue || '';
+        },
+        value(n) {
+            this.$emit('update:modelValue', n);
+            this.$emit('change', n);
+        },
+    },
+    data() {
+        return {
+            value: this.modelValue || ''
+        }
+    },
+    methods: {},
+    created() {
+    }
+
+});
+</script>
+
+<style>
+._fd-color-input {
+    width: 150px;
+}
+
+._fd-color-input .el-input .el-color-picker {
+    margin: 0;
+}
+
+._fd-color-input .el-input .el-input-group__append {
+    padding: 0;
+    width: 24px;
+}
+
+._fd-color-input .el-input .el-color-picker__trigger {
+    border-left: 0 none;
+    border-radius: 0px 3px 3px 0px;
+}
+</style>

+ 118 - 0
src/components/form-create-designer/components/style/ConfigItem.vue

@@ -0,0 +1,118 @@
+<template>
+    <div class="_fd-config-item">
+        <div class="_fd-ci-head">
+            <div class="_fd-ci-label" :class="$slots.append && arrow !== false ? 'is-arrow' : ''"
+                 @click="visit = $slots.append && arrow !== false && !visit">
+                <template v-if="warning">
+                    <Warning :tooltip="warning">
+                        <slot name="label">
+                            <span>{{ label }}</span>
+                        </slot>
+                    </Warning>
+                </template>
+                <template v-else>
+                    <slot name="label">
+                        <span>{{ label }}</span>
+                    </slot>
+                </template>
+                <i class="fc-icon icon-down" v-if="$slots.append && arrow !== false"
+                   :class="(showAppend || visit) ? 'down' : ''"></i>
+            </div>
+            <div class="_fd-ci-con" v-if="$slots.default || info">
+                <template v-if="$slots.default">
+                    <slot></slot>
+                </template>
+                <span class="_fd-ci-info" v-else>{{ info }}</span>
+            </div>
+        </div>
+        <div class="_fd-ci-append" v-if="showAppend || visit" :style="'background:' + appendBackground">
+            <slot name="append"></slot>
+        </div>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import Warning from '../Warning.vue';
+
+export default defineComponent({
+    name: 'ConfigItem',
+    components: {Warning},
+    props: {
+        label: String,
+        info: String,
+        warning: String,
+        appendBackground: String,
+        arrow: {
+            type: Boolean,
+            default: true
+        },
+        showAppend: Boolean,
+    },
+    data() {
+        return {
+            visit: false,
+        }
+    }
+
+
+});
+</script>
+
+<style>
+._fd-config-item {
+    display: flex;
+    width: 100%;
+    flex-direction: column;
+    font-size: 12px;
+    color: #666666;
+    margin-bottom: 10px;
+}
+
+._fd-ci-head {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+._fd-ci-label {
+    display: flex;
+    align-items: center;
+    font-size: 12px;
+    color: #262626;
+}
+
+._fd-ci-con {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    min-width: 150px;
+}
+
+._fd-ci-label.is-arrow {
+    cursor: pointer;
+}
+
+._fd-ci-append {
+    display: flex;
+    flex-direction: column;
+    background: #F5F5F5;
+    margin: 5px 3px 3px;
+    padding: 4px;
+}
+
+
+._fd-ci-label i {
+    font-size: 12px;
+    font-weight: 600;
+}
+
+._fd-ci-label i.down {
+    transform: rotate(-180deg);
+}
+
+._fd-ci-info {
+    font-size: 12px;
+    padding-right: 5px;
+}
+</style>

+ 174 - 0
src/components/form-create-designer/components/style/FontInput.vue

@@ -0,0 +1,174 @@
+<template>
+    <ConfigItem :label="t('style.font.name')">
+        <div class="_fd-fi-box" :style="fontStyle">
+            {{ t('style.font.preview') }}
+        </div>
+        <template #append>
+            <div class="_fd-font-input">
+                <el-form label-width="50px" label-position="top" inline size="small">
+                    <el-form-item :label="t('style.font.size')">
+                        <SizeInput v-model="fontStyle.fontSize" @change="onInput"/>
+                    </el-form-item>
+                    <el-form-item :label="t('style.weight.name')">
+                        <el-select v-model="fontStyle.fontWeight" clearable @change="onInput">
+                            <el-option
+                                v-for="item in weightType"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                                <span :style="{fontWeight: item.value}">{{ item.label }}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item :label="t('style.decoration.name')">
+                        <el-select v-model="fontStyle.textDecoration" clearable @change="onInput">
+                            <el-option
+                                v-for="item in decorationType"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                                <span :style="{textDecoration: item.value}">{{ item.label }}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item :label="t('style.font.align')">
+                        <el-select v-model="fontStyle.textAlign" clearable @change="onInput">
+                            <el-option
+                                v-for="item in alignType"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            />
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item :label="t('style.font.height')">
+                        <SizeInput v-model="fontStyle.lineHeight" @change="onInput"/>
+                    </el-form-item>
+                    <el-form-item :label="t('style.font.spacing')">
+                        <SizeInput v-model="fontStyle.letterSpacing" @change="onInput"/>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </template>
+    </ConfigItem>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import SizeInput from './SizeInput.vue';
+import ColorInput from './ColorInput.vue';
+import ConfigItem from './ConfigItem.vue';
+import {toLine} from '@form-create/utils';
+
+export default defineComponent({
+    name: 'BorderInput',
+    components: {ColorInput, SizeInput, ConfigItem},
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({}),
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyValue();
+        }
+    },
+    computed: {
+        borderStyleStr() {
+            let str = '';
+            Object.keys(this.borderStyle).forEach((key) => {
+                if (this.borderStyle[key] !== '') {
+                    str += toLine(key) + ': ' + this.borderStyle[key] + ';';
+                }
+            }, {})
+            return str;
+        },
+        alignType() {
+            return ['left', 'center', 'right'].map(v => {
+                return {label: this.t('props.' + v), value: v};
+            })
+        },
+        decorationType() {
+            return ['underline', 'line-through', 'overline'].map(v => {
+                return {label: this.t('style.decoration.' + v), value: v};
+            });
+        },
+        weightType() {
+            return [300, 400, 500, 700].map(v => {
+                return {label: this.t('style.weight.' + v), value: v};
+            });
+        },
+    },
+    data() {
+        const t = this.designer.setupState.t;
+        return {
+            t,
+            fontStyle: {
+                fontSize: '',
+                fontWeight: '',
+                fontStyle: '',
+                textDecoration: '',
+                textAlign: '',
+                lineHeight: '',
+                letterSpacing: '',
+            },
+        }
+    },
+    methods: {
+        tidyValue() {
+            Object.keys(this.fontStyle).forEach(k => {
+                this.fontStyle[k] = this.modelValue[k] || '';
+            });
+        },
+        onInput() {
+            const style = Object.keys(this.fontStyle).reduce((acc, key) => {
+                if (this.fontStyle[key] !== '') {
+                    acc[key] = this.fontStyle[key]
+                }
+                return acc
+            }, {})
+            this.$emit('update:modelValue', style)
+            this.$emit('change', style)
+        },
+    },
+    created() {
+        this.tidyValue();
+    }
+
+});
+</script>
+
+<style>
+._fd-font-input {
+    display: flex;
+    justify-content: center;
+    padding: 0 5px;
+}
+
+._fd-fi-box {
+    width: 150px;
+    overflow: hidden;
+}
+
+._fd-font-input .el-form {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    width: 100%;
+    grid-column-gap: 10px;
+}
+
+._fd-font-input .el-form--inline .el-form-item {
+    margin: 0;
+    padding: 0;
+}
+
+._fd-font-input ._fd-size-input .el-input-number--small {
+    width: 100%;
+}
+
+</style>

+ 164 - 0
src/components/form-create-designer/components/style/RadiusInput.vue

@@ -0,0 +1,164 @@
+<template>
+    <div class="_fd-radius-input">
+        <ConfigItem :label="t('style.borderRadius')">
+            <SizeInput :unit="unit" v-model="style.com" @change="batch"/>
+            <template #append>
+                <div class="_fd-radius-con">
+                    <div class="_fd-radius-item">
+                        <div class="_fd-radius-icon" style="transform: rotate(180deg);">
+                            <i class="fc-icon icon-radius"></i>
+                        </div>
+                        <SizeInput :unit="unit" v-model="style.left" @change="onInput"/>
+                    </div>
+                    <div class="_fd-radius-item">
+                        <div class="_fd-radius-icon" style="transform: rotate(-90deg);">
+                            <i class="fc-icon icon-radius"></i>
+                        </div>
+                        <SizeInput :unit="unit" v-model="style.top" @change="onInput"/>
+                    </div>
+                    <div class="_fd-radius-item">
+                        <div class="_fd-radius-icon" style="transform: rotate(90deg);">
+                            <i class="fc-icon icon-radius"></i>
+                        </div>
+                        <SizeInput :unit="unit" v-model="style.bottom" @change="onInput"/>
+                    </div>
+                    <div class="_fd-radius-item">
+                        <div class="_fd-radius-icon">
+                            <i class="fc-icon icon-radius"></i>
+                        </div>
+                        <SizeInput :unit="unit" v-model="style.right" @change="onInput"/>
+                    </div>
+                </div>
+            </template>
+        </ConfigItem>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import SizeInput from './SizeInput.vue';
+import ColorInput from './ColorInput.vue';
+import ConfigItem from './ConfigItem.vue';
+
+export default defineComponent({
+    name: 'RadiusInput',
+    components: {ConfigItem, ColorInput, SizeInput},
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: String
+    },
+    watch: {
+        modelValue(n) {
+            n !== this.oldValue && this.tidyValue();
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        return {
+            visit: false,
+            active: '',
+            style: {
+                com: '',
+                left: '',
+                right: '',
+                top: '',
+                bottom: '',
+            },
+            unit: ['px', '%'],
+            oldValue: '',
+        }
+    },
+    methods: {
+        batch() {
+            this.style.left = this.style.com;
+            this.style.right = this.style.com;
+            this.style.top = this.style.com;
+            this.style.bottom = this.style.com;
+            this.onInput();
+        },
+        tidyValue() {
+            this.style = {
+                com: '',
+                left: '',
+                right: '',
+                top: '',
+                bottom: '',
+            };
+            if (!this.modelValue) {
+                return;
+            }
+            let split = (this.modelValue || '').split(' ').filter(v => v !== '');
+            if (split.length === 1) {
+                split = [split[0], split[0], split[0], split[0]];
+            } else if (split.length === 2) {
+                split = [split[0], split[1], split[0], split[1]];
+            } else if (split.length === 3) {
+                split = [split[0], split[1], split[2], split[1]];
+            }
+            this.style.left = split[0];
+            this.style.top = split[1];
+            this.style.right = split[2];
+            this.style.bottom = split[3];
+            this.updateCom();
+        },
+        updateCom() {
+            let value = `${this.style.left || '0px'} ${this.style.top || '0px'} ${this.style.right || '0px'} ${this.style.bottom || '0px'}`;
+            this.style.com = value.replaceAll(this.style.left, '').trim() === '' ? this.style.left : '';
+        },
+        onInput() {
+            let value = `${this.style.left || '0px'} ${this.style.top || '0px'} ${this.style.right || '0px'} ${this.style.bottom || '0px'}`;
+            if ('' === `${this.style.left}${this.style.top}${this.style.right}${this.style.bottom}`.trim()) {
+                value = '';
+            } else {
+                this.updateCom();
+            }
+            this.oldValue = value;
+            this.$emit('update:modelValue', value);
+            this.$emit('change', value);
+        },
+    },
+    created() {
+        this.tidyValue();
+    }
+
+});
+</script>
+
+<style>
+._fd-radius-input {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+._fd-radius-con {
+    display: flex;
+    flex-wrap: wrap;
+}
+
+._fd-radius-item {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 50%;
+    padding: 5px 0;
+    box-sizing: border-box;
+}
+
+._fd-radius-item ._fd-radius-icon {
+    width: 24px;
+    height: 24px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+._fd-radius-item ._fd-size-input .el-input-number--small {
+    width: 70px;
+}
+</style>

+ 329 - 0
src/components/form-create-designer/components/style/ShadowContent.vue

@@ -0,0 +1,329 @@
+<template>
+    <div class="_fd-shadow-content">
+        <el-form label-width="50px" label-position="top" inline class="_fd-sc-form" size="small">
+            <el-form-item :label="t('style.shadow.mode')">
+                <el-radio-group v-model="form.type" @change="onInput" size="small" class="_fd-sc-radio">
+                    <template v-for="item in options" :key="item.key">
+                        <el-tooltip
+                            effect="dark"
+                            :content="t('style.shadow.' + item.key)"
+                            placement="top"
+                            :hide-after="0"
+                            persistent
+                        >
+                            <el-radio-button :label="item.key" :value="item.key">
+                                <i class="fc-icon" :class="'icon-' + item.icon"></i>
+                            </el-radio-button>
+                        </el-tooltip>
+                    </template>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item :label="t('style.color')">
+                <ColorInput v-model="form.color" @change="onInput"></ColorInput>
+            </el-form-item>
+            <el-form-item :label="t('style.shadow.x')">
+                <el-input v-model="form.x" type="number" @change="onInput">
+                    <template #append>
+                        <el-select v-model="form.x_unit" @change="onInput">
+                            <el-option v-for="item in units" :key="item" :label="item" :value="item"/>
+                        </el-select>
+                    </template>
+                </el-input>
+            </el-form-item>
+            <el-form-item :label="t('style.shadow.y')">
+                <el-input v-model="form.y" type="number" @change="onInput">
+                    <template #append>
+                        <el-select v-model="form.y_unit" @change="onInput">
+                            <el-option v-for="item in units" :key="item" :label="item" :value="item"/>
+                        </el-select>
+                    </template>
+                </el-input>
+            </el-form-item>
+            <el-form-item :label="t('style.shadow.vague')">
+                <el-input v-model="form.vague" type="number" @change="onInput">
+                    <template #append>
+                        <el-select v-model="form.vague_unit" @change="onInput">
+                            <el-option v-for="item in units" :key="item" :label="item" :value="item"/>
+                        </el-select>
+                    </template>
+                </el-input>
+            </el-form-item>
+            <el-form-item :label="t('style.shadow.extend')">
+                <el-input v-model="form.extend" type="number" @change="onInput">
+                    <template #append>
+                        <el-select v-model="form.extend_unit" @change="onInput">
+                            <el-option v-for="item in units" :key="item" :label="item" :value="item"/>
+                        </el-select>
+                    </template>
+                </el-input>
+            </el-form-item>
+        </el-form>
+        <div class="_fd-sc-right">
+            <div
+                ref="box"
+                class="_fd-sc-box"
+                :class="down ? 'down' : ''"
+                @click="getMouseXY($event, 1)"
+                @mousedown="onMousedown"
+                @mouseup="onMouseup"
+                @mousemove="getMouseXY($event, 0)"
+            >
+                <span class="spot" :style="spotStyle">
+                  <i class="spot-id"/>
+                </span>
+                <span class="center-spot"/>
+                <div class="x-hr"/>
+                <div class="y-hr"/>
+            </div>
+        </div>
+    </div>
+</template>
+
+
+<script>
+import {defineComponent} from 'vue';
+import ColorInput from './ColorInput.vue';
+
+export default defineComponent({
+    name: 'ShadowContent',
+    components: {ColorInput},
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: {
+            default: '0px 0px 0px rgba(0, 0, 0, 0)',
+            type: String,
+        },
+    },
+    data() {
+        return {
+            max: 24,
+            boxSize: 250,
+            options: [
+                {key: 'external', icon: 'shadow'},
+                {key: 'inset', icon: 'shadow-inset'},
+            ],
+            form: {
+                color: '',
+                type: 'external',
+                x: 0,
+                y: 0,
+                vague: 0,
+                extend: 0,
+                x_unit: 'px',
+                y_unit: 'px',
+                vague_unit: 'px',
+                extend_unit: 'px',
+            },
+            units: ['px', '%', 'rem', 'em', 'vw', 'vh'],
+            down: false,
+            position: {
+                left: 0,
+                top: 0,
+            },
+        }
+    },
+    computed: {
+        spotStyle() {
+            return {
+                left: this.position.left + 'px',
+                top: this.position.top + 'px',
+            }
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    watch: {
+        position(n) {
+            this.form.x = parseInt(String(((n.left - this.boxSize / 2) / this.boxSize) * this.max));
+            this.form.y = parseInt(String(((n.top - this.boxSize / 2) / this.boxSize) * this.max));
+            const i = this.max / 2;
+            this.form.x = this.form.x < 0 ? Math.max(this.form.x, i * -1) : Math.min(this.form.x, i);
+            this.form.y = this.form.y < 0 ? Math.max(this.form.y, i * -1) : Math.min(this.form.y, i);
+        },
+        modelValue(n) {
+            this.initStyle(n);
+        },
+    },
+    methods: {
+        getMouseXY(e, force) {
+            if (this.down || force) {
+                const _box = this.$refs.box.getBoundingClientRect()
+                this.position = {
+                    left: parseInt(String(e.clientX - _box.x)),
+                    top: parseInt(String(e.clientY - _box.y)),
+                }
+            }
+        },
+        onMouseup() {
+            this.down = false;
+            this.onInput();
+        },
+        onMousedown(e) {
+            this.getMouseXY(e, true);
+            this.down = true;
+        },
+        onInput() {
+            const n = this.form;
+            let value = `${n.x}${n.x_unit} ${n.y}${n.y_unit} ${n.vague}${n.vague_unit} ${n.extend}${n.extend_unit} ${n.color}`
+            if (`${n.x}${n.y}${n.vague}${n.extend}`.replaceAll('0', '') === '') {
+                value = '';
+            } else if (n.type === 'inset') {
+                value += ' inset'
+            }
+            this.$emit('update:modelValue', value);
+            this.$emit('change', value);
+        },
+        initStyle(modelValue) {
+            if ((this.modelValue || '').indexOf(' inset') > -1) {
+                this.form.type = 'inset'
+                modelValue = modelValue.replace(' inset', '')
+            }
+            const shadowData = modelValue.split('rgba')
+            let color, shadowValues;
+
+            if (shadowData.length > 1) {
+                // 将颜色值和其他阴影值进行分离
+                color = 'rgba' + shadowData[1].trim()
+                shadowValues = shadowData[0].trim().split(' ')
+            } else {
+                shadowValues = shadowData[0].trim().split(' ')
+                color = shadowValues.pop()
+            }
+            this.form.color = color || '#000'
+            this.form.x = parseInt(shadowValues[0]) || 0
+            this.form.y = parseInt(shadowValues[1]) || 0
+            this.form.vague = parseInt(shadowValues[2]) || 0
+            this.form.extend = parseInt(shadowValues[3]) || 0
+
+            const getUnit = (value) => {
+                return value?.replace(/[-\d.]/g, '') || 'px'
+            }
+            this.form.x_unit = getUnit(shadowValues[0])
+            this.form.y_unit = getUnit(shadowValues[1])
+            this.form.vague_unit = getUnit(shadowValues[2])
+            this.form.extend_unit = getUnit(shadowValues[3])
+            this.position.left = this.boxSize / 2 + (this.form.x / this.max) * this.boxSize || 0
+            this.position.top = this.boxSize / 2 + (this.form.y / this.max) * this.boxSize || 0
+        }
+    },
+    mounted() {
+        this.initStyle(this.modelValue);
+    }
+
+});
+</script>
+
+<style>
+._fd-shadow-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-direction: column;
+}
+
+._fd-sc-form .fc-icon {
+    font-size: 12px;
+}
+
+._fd-shadow-content .el-form-item {
+    width: 50%;
+    padding: 0 0 0 10px;
+    margin: 0 0 5px !important;
+    box-sizing: border-box;
+}
+
+._fd-shadow-content .el-input__wrapper {
+    flex: 1;
+}
+
+._fd-shadow-content ._fd-sc-box {
+    width: 250px;
+    height: 250px;
+    border: 1px solid #ccc;
+    border-radius: 5px;
+    position: relative;
+    cursor: pointer;
+    overflow: hidden
+}
+
+._fd-shadow-content ._fd-sc-box .spot {
+    position: absolute;
+    width: 0;
+    height: 0;
+    border-radius: 100%
+}
+
+._fd-shadow-content ._fd-sc-box .spot-id {
+    position: absolute;
+    top: -5px;
+    left: -5px;
+    width: 10px;
+    height: 10px;
+    background: #1989fa;
+    border-radius: 100%;
+    z-index: 9
+}
+
+._fd-shadow-content ._fd-sc-box.down .spot-id {
+    box-shadow: 1px 1px 10px 2px #1989fa
+}
+
+._fd-shadow-content ._fd-sc-box .center-spot {
+    position: absolute;
+    width: 0;
+    height: 0;
+    top: 125px;
+    left: 125px;
+    border-radius: 100%;
+    background: #1989fa
+}
+
+._fd-shadow-content ._fd-sc-box .x-hr {
+    width: 100%;
+    position: absolute;
+    height: 1px;
+    top: 125px;
+    background: #ccc
+}
+
+._fd-shadow-content ._fd-sc-box .y-hr {
+    height: 100%;
+    position: absolute;
+    width: 1px;
+    left: 125px;
+    background: #ccc
+}
+
+._fd-shadow-content .el-select__placeholder {
+    text-align: center;
+}
+
+._fd-shadow-content .el-input-group__append {
+    width: 55px;
+    padding: 0;
+}
+
+._fd-shadow-content .el-input {
+    width: 105px;
+}
+
+._fd-shadow-content ._fd-sc-right {
+    margin-top: 10px;
+}
+
+._fd-shadow-content ._fd-sc-radio {
+    width: 105px;
+}
+
+._fd-shadow-content ._fd-sc-radio .el-radio-button {
+    display: flex;
+    flex: 1;
+}
+
+._fd-shadow-content ._fd-sc-radio .el-radio-button__inner {
+    width: 100%;
+}
+
+</style>

+ 93 - 0
src/components/form-create-designer/components/style/ShadowInput.vue

@@ -0,0 +1,93 @@
+<template>
+    <div class="_fd-shadow-input">
+        <ConfigItem :label="t('style.shadow.name')">
+            <el-input clearable v-model="value" class="_fd-si-input">
+                <template #append>
+                    <el-dropdown>
+                        <i class="fc-icon icon-setting"></i>
+                        <template #dropdown>
+                            <el-dropdown-menu>
+                                <el-dropdown-item v-for="item in options" @click="changeValue(item.value)">
+                                    {{ item.label }}
+                                </el-dropdown-item>
+                            </el-dropdown-menu>
+                        </template>
+                    </el-dropdown>
+                </template>
+            </el-input>
+            <template #append>
+                <ShadowContent v-model="value"></ShadowContent>
+            </template>
+        </ConfigItem>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import ShadowContent from './ShadowContent.vue';
+import ConfigItem from './ConfigItem.vue';
+
+export default defineComponent({
+    name: 'ShadowInput',
+    emits: ['update:modelValue', 'change'],
+    components: {ConfigItem, ShadowContent},
+    inject: ['designer'],
+    props: {
+        modelValue: String,
+    },
+    watch: {
+        modelValue() {
+            this.value = this.modelValue || '';
+        },
+        value(n) {
+            this.$emit('update:modelValue', n);
+            this.$emit('change', n);
+        },
+    },
+    data() {
+        const t = this.designer.setupState.t;
+        return {
+            t,
+            options: [
+                {label: t('style.shadow.classic'), value: '3px 5px 7px 2px #CBCBCBFF'},
+                {label: t('style.shadow.flat'), value: '4px 4px 3px -2px #E7E5E5FF'},
+                {label: t('style.shadow.solid'), value: '1px 2px 4px 2px #979797FF'}
+            ],
+            value: this.modelValue || ''
+        }
+    },
+    methods: {
+        changeValue(val) {
+            this.value = val;
+        },
+    },
+    created() {
+    }
+
+});
+</script>
+
+<style>
+._fd-shadow-input ._fd-ci-con {
+    width: 150px;
+}
+
+._fd-shadow-input :focus-visible {
+    outline: 0 none;
+}
+
+._fd-si-input .el-input-group__append {
+    display: inline-flex;
+    width: 24px;
+    padding: 0;
+}
+
+._fd-si-input .el-input__wrapper {
+    flex: 1;
+}
+
+._fd-shadow-input ._fd-ci-con .fc-icon {
+    cursor: pointer;
+}
+
+</style>

+ 118 - 0
src/components/form-create-designer/components/style/SizeInput.vue

@@ -0,0 +1,118 @@
+<template>
+    <div class="_fd-size-input">
+        <template v-if="unit[idx] === 'auto'">
+            <el-button :size="size" style="width: 150px;" @click="changeType()">{{ unit[idx] }}</el-button>
+        </template>
+        <template v-else>
+            <el-inputNumber :size="size" v-model="num" @change="submit" controls-position="right"/>
+            <el-dropdown trigger="click" size="small">
+                <el-button :size="size">{{ unit[idx] }}</el-button>
+                <template #dropdown>
+                    <el-dropdown-menu>
+                        <el-dropdown-item v-for="(name, idx) in unit" :key="name" @click="changeType(idx)">
+                            <div>{{ name }}</div>
+                        </el-dropdown-item>
+                    </el-dropdown-menu>
+                </template>
+            </el-dropdown>
+        </template>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import {isNull} from '../../utils/index';
+
+export default defineComponent({
+    name: 'SizeInput',
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: String,
+        size: String,
+        unit: {
+            type: Array,
+            default: () => ['auto', 'px', '%', 'vh', 'vw', 'em', 'rem']
+        },
+        defaultUnit: {
+            type: String,
+            default: 'px'
+        }
+    },
+    watch: {
+        modelValue() {
+            this.parseValue();
+        }
+    },
+    data() {
+        return {
+            idx: 1,
+            num: 0,
+            oldValue: this.modelValue || '',
+        }
+    },
+    methods: {
+        parseValue() {
+            if (this.modelValue !== 'auto') {
+                this.idx = Math.max(this.unit.indexOf(this.defaultUnit), 0);
+                this.unit.forEach((v, i) => {
+                    if ((this.modelValue || '').indexOf(v) > -1) {
+                        this.idx = i;
+                    }
+                });
+                this.num = isNull(this.modelValue) ? null : parseFloat(this.modelValue || 0);
+            } else {
+                this.idx = 0;
+                this.num = 0;
+            }
+        },
+        submit() {
+            this.oldValue = !isNull(this.num) ? '' + this.num + this.unit[this.idx] : '';
+            this.$emit('update:modelValue', this.oldValue);
+            this.$emit('change', this.oldValue);
+        },
+        changeType(idx) {
+            if (idx !== undefined) {
+                if (this.idx === idx) {
+                    return;
+                }
+                this.idx = idx;
+            } else {
+                this.idx++;
+                if (this.idx > 4) {
+                    this.idx = 0;
+                }
+            }
+            if (this.unit[this.idx] === 'auto') {
+                this.oldValue = 'auto';
+                this.$emit('update:modelValue', 'auto');
+                this.$emit('change', 'auto');
+            } else {
+                this.submit();
+            }
+        },
+    },
+    created() {
+        this.parseValue();
+    }
+
+});
+</script>
+
+<style>
+._fd-size-input {
+    display: flex;
+    align-items: center;
+}
+
+._fd-size-input .el-input-number--small {
+    width: 122px;
+}
+
+._fd-size-input .el-button {
+    font-size: 14px;
+    padding: 5px;
+    margin-left: 3px;
+    width: 25px;
+}
+</style>

+ 263 - 0
src/components/form-create-designer/components/style/StyleConfig.vue

@@ -0,0 +1,263 @@
+<template>
+    <div class="_fd-style-config">
+        <BoxSpaceInput v-model="space" @change="onInput" style="margin-bottom: 10px;"></BoxSpaceInput>
+        <BoxSizeInput v-model="size" @change="onInput"></BoxSizeInput>
+        <ConfigItem :label="t('style.color')">
+            <ColorInput v-model="color" @change="onInput"></ColorInput>
+        </ConfigItem>
+        <ConfigItem :label="t('style.backgroundColor')">
+            <ColorInput v-model="backgroundColor" @change="onInput"></ColorInput>
+        </ConfigItem>
+        <BorderInput v-model="border" @change="onInput"></BorderInput>
+        <RadiusInput v-model="radius" @change="onInput"/>
+        <FontInput v-model="font" @change="onInput"/>
+        <ShadowInput v-model="boxShadow" @change="onInput"></ShadowInput>
+        <ConfigItem :label="t('style.opacity')" class="_fd-opacity-input">
+            <el-slider :show-tooltip="false" v-model="opacity"
+                       @change="onInput"></el-slider>
+            <span>{{ opacity }}%</span>
+        </ConfigItem>
+        <ConfigItem :label="t('style.scale')" class="_fd-opacity-input">
+            <el-slider :min="80" :max="120" :show-tooltip="false" v-model="scale"
+                       @change="onInput"></el-slider>
+            <span>{{ scale }}%</span>
+        </ConfigItem>
+        <ConfigItem :label="t('props.custom')" :info="Object.keys(formData).length > 0 ? t('struct.configured') : ''">
+            <template #append>
+                <TableOptions v-model="formData" @change="onInput" v-bind="{
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object'
+            }"></TableOptions>
+            </template>
+        </ConfigItem>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import BoxSizeInput from './BoxSizeInput.vue';
+import BoxSpaceInput from './BoxSpaceInput.vue';
+import BorderInput from './BorderInput.vue';
+import RadiusInput from './RadiusInput.vue';
+import FontInput from './FontInput.vue';
+import ConfigItem from './ConfigItem.vue';
+import ColorInput from './ColorInput.vue';
+import ShadowInput from './ShadowInput.vue';
+import {isNull} from '../../utils/index';
+import TableOptions from '../TableOptions.vue';
+
+const fontKey = [
+    'fontSize',
+    'fontWeight',
+    'fontStyle',
+    'textDecoration',
+    'textAlign',
+    'lineHeight',
+    'letterSpacing',
+];
+
+const sizeKey = [
+    'height',
+    'width',
+    'minWidth',
+    'minHeight',
+    'maxWidth',
+    'maxHeight',
+    'overflow'
+];
+
+const styleKey = [
+    'color',
+    'backgroundColor',
+    'scale',
+    'borderRadius',
+    'boxShadow',
+    'marginTop',
+    'marginRight',
+    'marginBottom',
+    'marginLeft',
+    'paddingTop',
+    'paddingRight',
+    'paddingBottom',
+    'paddingLeft',
+    'margin',
+    'padding',
+    'opacity',
+    'borderStyle',
+    'borderColor',
+    'borderWidth',
+    'borderTopStyle',
+    'borderTopColor',
+    'borderTopWidth',
+    'borderLeftStyle',
+    'borderLeftColor',
+    'borderLeftWidth',
+    'borderBottomStyle',
+    'borderBottomColor',
+    'borderBottomWidth',
+    'borderRightStyle',
+    'borderRightColor',
+    'borderRightWidth',
+    ...fontKey,
+    ...sizeKey
+];
+
+export default defineComponent({
+    name: 'StyleConfig',
+    inject: ['designer'],
+    emits: ['update:modelValue'],
+    components: {
+        TableOptions,
+        ColorInput,
+        ConfigItem,
+        RadiusInput,
+        BoxSizeInput,
+        BoxSpaceInput,
+        BorderInput,
+        ShadowInput,
+        FontInput,
+    },
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({})
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyStyle();
+        },
+    },
+    data() {
+        const t = this.designer.setupState.t;
+        return {
+            t,
+            formData: {},
+            size: {},
+            space: {},
+            border: {},
+            font: {},
+            radius: '',
+            backgroundColor: '',
+            color: '',
+            boxShadow: '',
+            opacity: 100,
+            scale: 100,
+        }
+    },
+    methods: {
+        tidyStyle() {
+            const style = {...this.modelValue || {}};
+            const space = {};
+            Object.keys(style).forEach(k => {
+                if (['margin', 'padding'].indexOf(k) > -1) {
+                    space[k] = style[k];
+                } else if (k.indexOf('margin') > -1 || k.indexOf('padding') > -1) {
+                    space[k] = style[k];
+                }
+            });
+
+            const size = {};
+            sizeKey.forEach(k => {
+                if (style[k]) {
+                    size[k] = style[k];
+                }
+            });
+
+            this.radius = style.borderRadius || '';
+            delete style.borderRadius;
+
+            const border = {};
+            Object.keys(style).forEach(k => {
+                if (k.indexOf('border') === 0) {
+                    border[k] = style[k];
+                }
+            });
+
+            let opacity = isNull(style.opacity) ? 100 : (parseFloat(style.opacity) || 0);
+            if (opacity && opacity < 1) {
+                opacity = opacity * 100;
+            }
+
+            let scale = style.scale;
+            if (isNull(style.scale)) {
+                scale = 100
+            } else if (isNaN(Number(scale))) {
+                scale = parseFloat(scale) || 100;
+            } else {
+                scale = scale > 0 ? scale * 100 : 0;
+            }
+
+            const font = {};
+            fontKey.forEach(k => {
+                if (style[k]) {
+                    font[k] = style[k];
+                }
+            });
+            this.opacity = opacity;
+            this.scale = scale;
+            this.size = size;
+            this.space = space;
+            this.border = border;
+            this.font = font;
+            this.boxShadow = style.boxShadow || '';
+            this.color = style.color || '';
+            this.backgroundColor = style.backgroundColor || '';
+            styleKey.forEach(k => {
+                delete style[k];
+            })
+            this.formData = style;
+        },
+        onInput() {
+            let temp = {...this.formData};
+            styleKey.forEach(k => {
+                delete temp[k];
+            })
+            const style = {
+                ...temp,
+                color: this.color || '',
+                backgroundColor: this.backgroundColor || '',
+                opacity: (this.opacity >= 0 && this.opacity < 100) ? (this.opacity + '%') : '',
+                borderRadius: this.radius || '',
+                boxShadow: this.boxShadow || '',
+                scale: (this.scale >= 0 && this.scale !== 100) ? (this.scale + '%') : '',
+                ...this.space, ...this.size, ...this.border, ...this.font
+            }
+            Object.keys(style).forEach(k => {
+                if (isNull(style[k])) {
+                    delete style[k];
+                }
+            })
+            this.$emit('update:modelValue', style);
+        },
+    },
+    created() {
+        this.tidyStyle();
+    }
+
+});
+</script>
+
+<style>
+._fd-style-config {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+._fd-opacity-input ._fd-ci-con {
+    display: flex;
+    justify-content: space-between;
+    width: 150px;
+    align-items: center;
+}
+
+._fd-opacity-input ._fd-ci-con > span {
+    width: 32px;
+}
+
+._fd-opacity-input .el-slider {
+    flex: 1;
+    margin-right: 15px;
+}
+</style>

+ 210 - 0
src/components/form-create-designer/components/table/Table.vue

@@ -0,0 +1,210 @@
+<template>
+    <el-col :span="24">
+        <div class="_fc-table">
+            <table border="1" cellspacing="0" cellpadding="0" :style="tableColor">
+                <template v-for="(_,pid) in rule.row" :key="pid">
+                    <tr>
+                        <template v-for="(_, idx) in rule.col" :key="`${pid}${idx}`">
+                            <td v-if="lattice[pid][idx].show"
+                                v-bind="lattice[pid][idx] ? {colspan:lattice[pid][idx].colspan, rowspan:lattice[pid][idx].rowspan} : {}"
+                                valign="top"
+                                :class="(tdClass && tdClass[`${pid}:${idx}`]) || ''"
+                                :style="[tableColor, (tdStyle && tdStyle[`${pid}:${idx}`]) || {}]">
+                                <slot :name="`${pid}:${idx}`"></slot>
+                                <template v-for="slot in lattice[pid][idx].slot">
+                                    <slot :name="`${slot}`"></slot>
+                                </template>
+                            </td>
+                        </template>
+                    </tr>
+                </template>
+            </table>
+        </div>
+    </el-col>
+</template>
+
+<script>
+
+export default {
+    name: 'FcTable',
+    props: {
+        label: String,
+        width: [Number, String],
+        border: {
+            type: Boolean,
+            default: true
+        },
+        borderWidth: String,
+        borderColor: String,
+        rule: {
+            type: Object,
+            default: () => ({row: 1, col: 1})
+        },
+    },
+    watch: {
+        rule: {
+            handler() {
+                this.initRule();
+                this.loadRule();
+                this.tdStyle = this.rule.style || {};
+                this.tdClass = this.rule.class || {};
+            },
+            immediate: true,
+            deep: true,
+        }
+    },
+    data() {
+        return {
+            tdStyle: {},
+            tdClass: {},
+            lattice: {},
+        };
+    },
+    computed: {
+        tableColor() {
+            const border = {};
+            if (this.border === false) {
+                border['border'] = '0 none';
+            } else {
+                if (this.borderColor) {
+                    border['borderColor'] = this.borderColor;
+                }
+                if (this.borderWidth) {
+                    border['borderWidth'] = this.borderWidth;
+                }
+            }
+            return border;
+        },
+    },
+    methods: {
+        initRule() {
+            const rule = this.rule;
+            if (!rule.style) {
+                rule.style = {};
+            }
+            if (!rule.layout) {
+                rule.layout = [];
+            }
+            if (!rule.row) {
+                rule.row = 1;
+            }
+            if (!rule.col) {
+                rule.col = 1;
+            }
+        },
+        loadRule() {
+            const lattice = [];
+            const rule = this.rule || {row: 1, col: 1};
+            for (let index = 0; index < rule.row; index++) {
+                const sub = [];
+                lattice.push(sub);
+                for (let idx = 0; idx < rule.col; idx++) {
+                    sub.push({rowspan: 1, colspan: 1, slot: [], show: true});
+                }
+            }
+            [...(rule.layout || [])].forEach((v, i) => {
+                if (((!v.row || v.row <= 0) && (!v.col || v.col <= 0)) || !lattice[v.top] || !lattice[v.top][v.left] || !lattice[v.top][v.left].show) {
+                    rule.layout.splice(i, 1);
+                    return;
+                }
+                const data = lattice[v.top][v.left];
+                data.layout = v;
+                let col = 1;
+                let row = 1;
+                if (v.col) {
+                    col = (v.col + v.left) > rule.col ? rule.col - v.left : v.col;
+                    data.colspan = col;
+                }
+                if (v.row) {
+                    row = (v.row + v.top) > rule.row ? rule.row - v.top : v.row;
+                    data.rowspan = row;
+                }
+                if (row && col) {
+                    for (let index = 0; index < row; index++) {
+                        const row = lattice[v.top + index];
+                        if (row) {
+                            for (let idx = 0; idx < col; idx++) {
+                                if (!idx && !index)
+                                    continue;
+
+                                if (row[v.left + idx]) {
+                                    row[v.left + idx].show = false;
+                                }
+                                data.slot.push(`${v.top + index}:${v.left + idx}`);
+                            }
+                        }
+                    }
+                }
+            });
+
+            const checkCol = (col) => {
+                return !!(!col || col.layout || !col.show);
+            };
+
+            lattice.forEach((v, index) => {
+                v.forEach((item, idx) => {
+                    let right = false;
+                    let bottom = false;
+                    if (item.layout) {
+                        const col = item.layout.col || 1;
+                        const row = item.layout.row || 1;
+                        for (let i = 0; i < col; i++) {
+                            if (!lattice[index + row] || checkCol(lattice[index + row][idx + i])) {
+                                bottom = true;
+                                continue;
+                            }
+                        }
+                        for (let i = 0; i < row; i++) {
+                            if (!lattice[index + i] || checkCol(lattice[index + i][idx + col])) {
+                                right = true;
+                                continue;
+                            }
+                        }
+                    } else {
+                        right = checkCol(v[idx + 1]);
+                        bottom = lattice[index + 1] ? checkCol(lattice[index + 1][idx]) : true;
+                    }
+                    item.right = right;
+                    item.bottom = bottom;
+                });
+            });
+            this.lattice = lattice;
+        },
+    }
+};
+</script>
+
+<style>
+
+._fc-table {
+    overflow: auto;
+}
+
+._fc-table > table {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    table-layout: fixed;
+    border: 1px solid #EBEEF5;
+    border-bottom: 0 none;
+    border-right: 0 none;
+}
+
+._fc-table tr {
+    min-height: 50px;
+}
+
+._fc-table td {
+    padding: 5px;
+    min-height: 50px;
+    min-width: 80px;
+    position: relative;
+    box-sizing: border-box;
+    overflow-wrap: break-word;
+    /*white-space: nowrap;*/
+    overflow: hidden;
+    border: 0 none;
+    border-right: 1px solid #EBEEF5;
+    border-bottom: 1px solid #EBEEF5;
+}
+</style>

+ 658 - 0
src/components/form-create-designer/components/table/TableView.vue

@@ -0,0 +1,658 @@
+<template>
+    <div class="_fd-table-view">
+        <table border="1" cellspacing="0" cellpadding="0" :style="tableColor">
+            <template v-for="(_,pid) in rule.row" :key="pid">
+                <tr>
+                    <template v-for="(_, idx) in rule.col">
+                        <td v-if="lattice[pid][idx].show" :key="`${pid}${idx}`"
+                            v-bind="lattice[pid][idx] ? {colspan:lattice[pid][idx].colspan, rowspan:lattice[pid][idx].rowspan} : {}"
+                            :style="[tableColor, (style && style[`${pid}:${idx}`]) || {}]"
+                            :class="(rule.class && rule.class[`${pid}:${idx}`]) || ''">
+                            <div class="_fd-table-view-cell">
+                                <DragTool :drag-btn="false" :handle-btn="true" @active="active({pid, idx})"
+                                          :unique="lattice[pid][idx].id">
+                                    <DragBox v-bind="dragProp" @add="e=>dragAdd(e, {pid, idx})"
+                                             :ref="'drag' + pid + idx"
+                                             @end="e=>dragEnd(e, {pid, idx})" @start="e=>dragStart(e)"
+                                             @unchoose="e=>dragUnchoose(e)"
+                                             :list="getSlotChildren([`${pid}:${idx}`, ...lattice[pid][idx].slot])">
+                                        <slot :name="`${pid}:${idx}`"></slot>
+                                    </DragBox>
+                                    <template #handle>
+                                        <div class="_fd-drag-btn _fd-table-view-btn"
+                                             @click.stop="addRow({pid,idx,data: lattice[pid][idx]}, 0)">
+                                            <i class="fc-icon icon-add-col"></i>
+                                        </div>
+                                        <div class="_fd-drag-btn _fd-table-view-btn"
+                                             @click.stop="addCol({pid,idx,data: lattice[pid][idx]}, 0)">
+                                            <i class="fc-icon icon-add-col"
+                                               style="transform: rotate(90deg);"></i>
+                                        </div>
+                                        <div class="_fd-drag-btn _fd-table-view-btn" @click.stop>
+                                            <el-dropdown trigger="click" @command="command">
+                                                <i class="fc-icon icon-setting"></i>
+                                                <template #dropdown>
+                                                    <el-dropdown-menu>
+                                                        <el-dropdown-item
+                                                            :command="['addCol', [{pid,idx,data: lattice[pid][idx]}, 1]]">
+                                                            {{ t('tableOptions.addLeft') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item
+                                                            :command="['addCol', [{pid,idx,data: lattice[pid][idx]}, 0]]">
+                                                            {{ t('tableOptions.addRight') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item
+                                                            :command="['addRow', [{pid,idx,data: lattice[pid][idx]}, 1]]">
+                                                            {{ t('tableOptions.addTop') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item
+                                                            :command="['addRow', [{pid,idx,data: lattice[pid][idx]}, 0]]">
+                                                            {{ t('tableOptions.addBottom') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item divided :disabled="lattice[pid][idx].right"
+                                                                          :command="['mergeRight', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.mergeRight') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item :disabled="lattice[pid][idx].bottom"
+                                                                          :command="['mergeBottom', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.mergeBottom') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item divided
+                                                                          :disabled="!(lattice[pid][idx].layout && lattice[pid][idx].layout.col > 1)"
+                                                                          :command="['splitCol', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.splitCol') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item
+                                                            :disabled="!(lattice[pid][idx].layout && lattice[pid][idx].layout.row > 1)"
+                                                            :command="['splitRow', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.splitRow') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item divided :disabled="rule.col < 2"
+                                                                          :command="['rmCol', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.rmCol') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item :disabled="rule.row < 2"
+                                                                          :command="['rmRow', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.rmRow') }}
+                                                        </el-dropdown-item>
+                                                    </el-dropdown-menu>
+                                                </template>
+                                            </el-dropdown>
+                                        </div>
+
+                                    </template>
+                                </DragTool>
+                            </div>
+                        </td>
+                    </template>
+                </tr>
+            </template>
+        </table>
+    </div>
+</template>
+
+<script>
+
+import DragTool from '../DragTool.vue';
+import DragBox from '../DragBox.vue';
+import {defineComponent} from 'vue';
+import uniqueId from '@form-create/utils/lib/unique';
+
+
+export default defineComponent({
+    name: 'FcTableView',
+    props: {
+        label: String,
+        width: [Number, String],
+        formCreateInject: Object,
+        border: {
+            type: Boolean,
+            default: true
+        },
+        borderWidth: String,
+        borderColor: String,
+        rule: {
+            type: Object,
+            default: () => ({row: 1, col: 1})
+        },
+    },
+    inject: ['designer'],
+    components: {
+        DragTool,
+        DragBox,
+    },
+    watch: {
+        rule: {
+            handler() {
+                this.initRule();
+                this.style = this.rule.style;
+            },
+            immediate: true,
+        }
+    },
+    data() {
+        return {
+            unique: {},
+            style: {},
+            dragProp: {
+                rule: {
+                    props: {
+                        tag: 'el-col',
+                        group: 'default',
+                        ghostClass: 'ghost',
+                        animation: 150,
+                        handle: '._fd-drag-btn',
+                        emptyInsertThreshold: 0,
+                        direction: 'vertical',
+                        itemKey: 'type',
+                    }
+                },
+                tag: 'tableCell',
+            },
+            lattice: {},
+            uni: {},
+
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        tableColor() {
+            const border = {};
+            if (this.border === false) {
+                border['border'] = '0 none';
+            } else {
+                if (this.borderColor) {
+                    border['borderColor'] = this.borderColor;
+                }
+                if (this.borderWidth) {
+                    border['borderWidth'] = this.borderWidth;
+                }
+            }
+            return border;
+        },
+    },
+    methods: {
+        getUnique(key) {
+            if (!this.unique[key]) {
+                this.unique[key] = uniqueId();
+            }
+            return this.unique[key];
+        },
+        getSlotChildren(slots) {
+            const children = [];
+            this.formCreateInject.children.forEach(child => {
+                if (slots.indexOf(child.slot) > -1) {
+                    children.push(child);
+                }
+            });
+            return children;
+        },
+        dragAdd(e, item) {
+            // console.log('dragAdd');
+            const designer = this.designer.setupState;
+            const children = this.formCreateInject.children;
+            const slot = `${item.pid}:${item.idx}`;
+            const rule = e.item._underlying_vm_;
+            const flag = designer.addRule && designer.addRule.children === designer.moveRule;
+            if (flag) {
+                designer.moveRule.splice(designer.moveRule.indexOf(rule), 1);
+            }
+            let idx = 0;
+            const refKey = 'drag' + item.pid + item.idx;
+            if (this.$refs[refKey][0].list.length) {
+                let beforeRule = this.$refs[refKey][0].list[!e.newIndex ? 0 : e.newIndex - 1];
+                idx = children.indexOf(beforeRule) + (e.newIndex ? 1 : 0);
+            } else if (children.length) {
+                const dragSlotKeys = Object.keys(this.$refs);
+                for (let i = dragSlotKeys.indexOf(refKey) - 1; i >= 0; i--) {
+                    if (!this.$refs[dragSlotKeys[i]] || !this.$refs[dragSlotKeys[i]].length) {
+                        continue;
+                    }
+                    const list = this.$refs[dragSlotKeys[i]][0].list || [];
+                    if (list.length) {
+                        idx = children.indexOf(list[list.length - 1]) + 1;
+                        break;
+                    }
+                }
+            }
+            e.newIndex = idx;
+            if (flag) {
+                rule.slot = slot;
+                children.splice(e.newIndex, 0, rule);
+                designer.added = true;
+                designer.handleSortAfter({rule});
+            } else {
+                designer.dragAdd(children, e, `${item.pid}:${item.idx}`);
+            }
+        },
+        dragEnd(e, item) {
+            // console.log('dragEnd');
+            const designer = this.designer.setupState;
+            const children = this.formCreateInject.children;
+            const rule = e.item._underlying_vm_;
+            const oldIdx = children.indexOf(rule);
+            e.newIndex = oldIdx + (e.newIndex - e.oldIndex);
+            e.oldIndex = oldIdx;
+            designer.dragEnd(this.formCreateInject.children, e, `${item.pid}:${item.idx}`);
+        },
+        dragStart() {
+            // console.log('dragStart');
+            this.designer.setupState.dragStart(this.formCreateInject.children);
+        },
+        dragUnchoose(e) {
+            // console.log('dragUnchoose');
+            this.designer.setupState.dragUnchoose(this.formCreateInject.children, e);
+        },
+        initRule() {
+            const rule = this.rule;
+            if (!rule.style) {
+                rule.style = {};
+            }
+            if (!rule.class) {
+                rule.class = {};
+            }
+            if (!rule.layout) {
+                rule.layout = [];
+            }
+            if (!rule.row) {
+                rule.row = 1;
+            }
+            if (!rule.col) {
+                rule.col = 1;
+            }
+        },
+        active(item) {
+            const key = `${item.pid}:${item.idx}`;
+            this.designer.setupState.customActive({
+                name: 'fcTableGrid',
+                onPaste: (rule) => {
+                    rule.slot = key;
+                    this.formCreateInject.children.push(rule);
+                },
+                style: {
+                    formData: {
+                        style: this.rule.style[key] || {},
+                        class: this.rule.class[key] || '',
+                    },
+                    change: (field, value) => {
+                        this.rule[field][key] = value || {};
+                    },
+                }
+            });
+        },
+        command(type) {
+            this[type[0]](...type[1]);
+        },
+        rmSlot(slot, rmSlot) {
+            const slotKey = Object.keys(slot);
+            const children = this.formCreateInject.children;
+            let del = 0;
+            [...children].forEach((child, index) => {
+                if (!child.slot) {
+                    return;
+                }
+                let idx;
+                if (rmSlot.indexOf(child.slot) > -1) {
+                    children.splice(index - del, 1);
+                    del++;
+                } else if (((idx = slotKey.indexOf(child.slot)) > -1)) {
+                    child.slot = slot[slotKey[idx]];
+                }
+            });
+            rmSlot.forEach(v => {
+                delete this.style[v];
+            });
+            this.loadRule();
+        },
+        rmRow(row) {
+            this.rule.row--;
+            const slot = {};
+            const rmSlot = [];
+            for (let index = row.pid; index < this.rule.row + 1; index++) {
+                for (let idx = 0; idx < this.rule.col; idx++) {
+                    if (index === row.pid) {
+                        rmSlot.push(`${row.pid}:${idx}`);
+                    } else {
+                        slot[`${index}:${idx}`] = `${index - 1}:${idx}`;
+                    }
+                }
+            }
+            let del = 0;
+            const layout = this.rule.layout;
+            [...layout].forEach((v, i) => {
+                if (v.top === row.pid) {
+                    layout.splice(i - del, 1);
+                    del++;
+                }
+            });
+            layout.forEach(v => {
+                if (v.top > row.pid) {
+                    v.top--;
+                }
+            });
+            this.rmSlot(slot, rmSlot);
+        },
+        rmCol(row) {
+            this.rule.col--;
+            const slot = {};
+            const rmSlot = [];
+            for (let index = 0; index < this.rule.row; index++) {
+                for (let idx = row.idx + 1; idx < this.rule.col + 1; idx++) {
+                    slot[`${index}:${idx}`] = `${index}:${idx - 1}`;
+                }
+                rmSlot.push(`${index}:${row.idx}`);
+            }
+            let del = 0;
+            const layout = this.rule.layout;
+            [...layout].forEach((v, i) => {
+                if (v.left === row.idx) {
+                    layout.splice(i - del, 1);
+                    del++;
+                }
+            });
+            layout.forEach(v => {
+                if (v.left > row.idx) {
+                    v.left--;
+                }
+            });
+            this.rmSlot(slot, rmSlot);
+        },
+        splitRow(item) {
+            const layout = item.data.layout;
+            const row = layout.row;
+            layout.row = 0;
+            if (row > 1) {
+                for (let i = 1; i < row; i++) {
+                    this.rule.layout.push({
+                        ...layout, top: layout.top + i
+                    });
+                }
+            }
+            this.loadRule();
+        },
+        splitCol(item) {
+            const layout = item.data.layout;
+            const col = layout.col;
+            layout.col = 0;
+            if (col > 1) {
+                for (let i = 1; i < col; i++) {
+                    this.rule.layout.push({
+                        ...layout, left: layout.left + i
+                    });
+                }
+            }
+            this.loadRule();
+        },
+        makeMap(layout) {
+            let map = [];
+            for (let x = layout.top; x < (layout.row || layout.top + 1); x++) {
+                for (let y = layout.left; y < (layout.col || layout.left + 1); y++) {
+                    map.push(`${x}:${y}`);
+                }
+            }
+            return map;
+        },
+        mergeRight(item) {
+            let layout;
+            if (item.data.layout) {
+                const col = (item.data.layout.col || 1) + 1;
+                item.data.layout.col = (col + item.idx) > this.rule.col ? this.rule.col - item.idx : col;
+                layout = item.data.layout;
+            } else {
+                layout = {
+                    top: item.pid,
+                    left: item.idx,
+                    col: 2,
+                };
+                this.rule.layout.push(layout);
+            }
+            const map = this.makeMap(layout);
+            this.formCreateInject.children.forEach(child => {
+                if (!child.slot) return;
+                if (map.indexOf(child.slot) > -1) {
+                    child.slot = `${item.pid}:${item.idx}`;
+                }
+            });
+            this.loadRule();
+        },
+        mergeBottom(item) {
+            let layout;
+            if (item.data.layout) {
+                const row = (item.data.layout.row || 1) + 1;
+                item.data.layout.row = (row + row.pid) > this.rule.col ? this.rule.col - item.pid : row;
+                layout = item.data.layout;
+            } else {
+                layout = {
+                    top: item.pid,
+                    left: item.idx,
+                    row: 2,
+                };
+                this.rule.layout.push(layout);
+            }
+            const map = this.makeMap(layout);
+            this.formCreateInject.children.forEach(child => {
+                if (!child.slot) return;
+                if (map.indexOf(child.slot) > -1) {
+                    child.slot = `${item.pid}:${item.idx}`;
+                }
+            });
+            this.loadRule();
+        },
+        addCol(row, type) {
+            this.rule.col++;
+            this.rule.layout.forEach(v => {
+                if (v.left > (type ? row.idx - 1 : row.idx)) {
+                    v.left++;
+                }
+            });
+            if (type || row.idx < this.rule.col - 2) {
+                const slot = {};
+                for (let index = 0; index < this.rule.row; index++) {
+                    for (let idx = type ? row.idx - 1 : row.idx + 1; idx < this.rule.col - 1; idx++) {
+                        slot[`${index}:${idx}`] = `${index}:${idx + 1}`;
+                    }
+                }
+                const slotKey = Object.keys(slot);
+                this.formCreateInject.children.forEach(child => {
+                    let idx;
+                    if (child.slot && ((idx = slotKey.indexOf(child.slot)) > -1)) {
+                        child.slot = slot[slotKey[idx]];
+                    }
+                });
+                slotKey.forEach(v => {
+                    if (this.style[v]) {
+                        this.style[slot[v]] = this.style[v];
+                        delete this.style[v];
+                    }
+                });
+            }
+            this.loadRule();
+        },
+        addRow(row, type) {
+            this.rule.row++;
+            this.rule.layout.forEach(v => {
+                if (v.top > (type ? row.pid - 1 : row.pid)) {
+                    v.top++;
+                }
+            });
+            if (type || row.pid < this.rule.row - 2) {
+                const slot = {};
+                for (let index = type ? row.pid - 1 : row.pid + 1; index < this.rule.row; index++) {
+                    for (let idx = 0; idx < this.rule.col; idx++) {
+                        slot[`${index}:${idx}`] = `${index + 1}:${idx}`;
+                    }
+                }
+                const slotKey = Object.keys(slot);
+                this.formCreateInject.children.forEach(child => {
+                    let idx;
+                    if (child.slot && ((idx = slotKey.indexOf(child.slot)) > -1)) {
+                        child.slot = slot[slotKey[idx]];
+                    }
+                });
+                slotKey.reverse().forEach(v => {
+                    if (this.style[v]) {
+                        this.style[slot[v]] = this.style[v];
+                        delete this.style[v];
+                    }
+                });
+            }
+            this.loadRule();
+        },
+        loadRule() {
+            const lattice = [];
+            const rule = this.rule || {row: 1, col: 1};
+            for (let index = 0; index < rule.row; index++) {
+                const sub = [];
+                lattice.push(sub);
+                for (let idx = 0; idx < rule.col; idx++) {
+                    sub.push({rowspan: 1, colspan: 1, slot: [], show: true, id: this.getUnique(`${index}${idx}`)});
+                }
+            }
+            [...(rule.layout || [])].forEach((v, i) => {
+                if (((!v.row || v.row <= 0) && (!v.col || v.col <= 0)) || !lattice[v.top] || !lattice[v.top][v.left] || !lattice[v.top][v.left].show) {
+                    rule.layout.splice(i, 1);
+                    return;
+                }
+                const data = lattice[v.top][v.left];
+                data.layout = v;
+                let col = 1;
+                let row = 1;
+                if (v.col) {
+                    col = (v.col + v.left) > rule.col ? rule.col - v.left : v.col;
+                    data.colspan = col;
+                }
+                if (v.row) {
+                    row = (v.row + v.top) > rule.row ? rule.row - v.top : v.row;
+                    data.rowspan = row;
+                }
+                if (row && col) {
+                    for (let index = 0; index < row; index++) {
+                        const row = lattice[v.top + index];
+                        if (row) {
+                            for (let idx = 0; idx < col; idx++) {
+                                if (!idx && !index)
+                                    continue;
+
+                                if (row[v.left + idx]) {
+                                    row[v.left + idx].show = false;
+                                }
+                                data.slot.push(`${v.top + index}:${v.left + idx}`);
+                            }
+                        }
+                    }
+                }
+            });
+
+            const checkCol = (col) => {
+                return !!(!col || col.layout || !col.show);
+            };
+
+            lattice.forEach((v, index) => {
+                v.forEach((item, idx) => {
+                    let right = false;
+                    let bottom = false;
+                    if (item.layout) {
+                        const col = item.layout.col || 1;
+                        const row = item.layout.row || 1;
+                        for (let i = 0; i < col; i++) {
+                            if (!lattice[index + row] || checkCol(lattice[index + row][idx + i])) {
+                                bottom = true;
+                                continue;
+                            }
+                        }
+                        for (let i = 0; i < row; i++) {
+                            if (!lattice[index + i] || checkCol(lattice[index + i][idx + col])) {
+                                right = true;
+                                continue;
+                            }
+                        }
+                    } else {
+                        right = checkCol(v[idx + 1]);
+                        bottom = lattice[index + 1] ? checkCol(lattice[index + 1][idx]) : true;
+                    }
+                    item.right = right;
+                    item.bottom = bottom;
+                });
+            });
+            this.lattice = lattice;
+            this.formCreateInject.rule.props.rule = rule;
+        },
+    },
+    beforeMount() {
+        this.loadRule();
+    }
+});
+</script>
+
+<style>
+
+._fd-table-view {
+    overflow: auto;
+}
+
+._fd-table-view-cell {
+    min-height: 50px;
+    height: 100%;
+    border: 1px inset rgba(0, 0, 0, .1);
+    background: #fff;
+}
+
+._fd-table-view-cell > ._fd-drag-tool {
+    height: 100%;
+    border: 0px;
+    margin: 0px;
+}
+
+._fd-table-view-btn {
+    flex-direction: column;
+    padding: 0;
+}
+
+._fd-table-view-btn .fc-icon {
+    width: 18px;
+    color: #fff;
+    font-size: 16px;
+}
+
+._fd-table-view-icon {
+    color: #FFFFFF;
+    display: flex;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    margin-top: 1px;
+}
+
+._fd-table-view > table {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    table-layout: fixed;
+    border: 1px solid #EBEEF5;
+    border-bottom: 0 none;
+    border-right: 0 none;
+}
+
+._fd-table-view tr {
+    min-height: 50px;
+}
+
+._fd-table-view td {
+    padding: 0;
+    min-height: 50px;
+    min-width: 80px;
+    position: relative;
+    box-sizing: border-box;
+    overflow-wrap: break-word;
+    white-space: nowrap;
+    border: 0 none;
+    border-right: 1px solid #EBEEF5;
+    border-bottom: 1px solid #EBEEF5;
+}
+
+._fd-tableCell-drag {
+    height: 100%;
+}
+</style>

+ 390 - 0
src/components/form-create-designer/components/tableForm/TableForm.vue

@@ -0,0 +1,390 @@
+<template>
+    <div class="_fc-table-form" :class="{'_fc-disabled': disabled}">
+        <component :is="Form" :option="options" :rule="rule" :extendOption="true"
+                   :disabled="disabled"
+                   @change="formChange"
+                   v-model:api="fapi"
+                   @emit-event="$emit"></component>
+        <el-button link type="primary" class="fc-clock" v-if="!max || max > this.trs.length"
+                   @click="addRaw(true)"><i class="fc-icon icon-add-circle" style="font-weight: 700;"></i>
+            {{ formCreateInject.t('add') || '添加' }}
+        </el-button>
+    </div>
+</template>
+
+<script>
+import {markRaw, reactive} from 'vue';
+
+export default {
+    name: 'TableForm',
+    emits: ['change', 'add', 'delete', 'update:modelValue'],
+    props: {
+        formCreateInject: Object,
+        modelValue: {
+            type: Array,
+            default: () => [],
+        },
+        columns: {
+            type: Array,
+            required: true,
+            default: () => []
+        },
+        filterEmptyColumn: {
+            type: Boolean,
+            default: true,
+        },
+        options: {
+            type: Object,
+            default: () => reactive(({
+                submitBtn: false,
+                resetBtn: false,
+            }))
+        },
+        max: Number,
+        disabled: Boolean,
+    },
+    watch: {
+        modelValue: {
+            handler() {
+                this.updateTable()
+            },
+            deep: true
+        },
+        'formCreateInject.preview': function (n) {
+            this.emptyRule.children[0].props.colspan = this.columns.length + (n ? 1 : 2);
+        },
+    },
+    data() {
+        return {
+            rule: [],
+            trs: [],
+            fapi: {},
+            Form: markRaw(this.formCreateInject.form.$form()),
+            copyTrs: '',
+            oldValue: '',
+            emptyRule: {
+                type: 'tr',
+                _isEmpty: true,
+                native: true,
+                subRule: true,
+                children: [
+                    {
+                        type: 'td',
+                        style: {
+                            textAlign: 'center',
+                        },
+                        native: true,
+                        subRule: true,
+                        props: {
+                            colspan: this.columns.length + (this.formCreateInject.preview ? 1 : 2),
+                        },
+                        children: [this.formCreateInject.t('dataEmpty') || '暂无数据']
+                    }
+                ]
+            },
+        };
+    },
+    methods: {
+        formChange() {
+            this.updateValue();
+        },
+        updateValue() {
+            const value = this.trs.map((tr, idx) => {
+                return {
+                    ...(this.modelValue[idx] || {}),
+                    ...this.fapi.getChildrenFormData(tr)
+                }
+            }).filter(v => {
+                if (!this.filterEmptyColumn) {
+                    return true;
+                }
+                if (v === undefined || v === null) {
+                    return false;
+                }
+                let flag = false;
+                Object.keys(v).forEach(k => {
+                    flag = flag || (v[k] !== undefined && v[k] !== '' && v[k] !== null)
+                })
+                return flag;
+            });
+            const str = JSON.stringify(value);
+            if (str !== this.oldValue) {
+                this.oldValue = str;
+                this.$emit('update:modelValue', value);
+                this.$emit('change', value);
+            }
+        },
+        setRawData(idx, formData) {
+            const raw = this.trs[idx];
+            this.fapi.setChildrenFormData(raw, formData, true);
+        },
+        updateTable() {
+            const str = JSON.stringify(this.modelValue);
+            if (this.oldValue === str) {
+                return;
+            }
+            this.oldValue = str;
+            this.trs = this.trs.splice(0, this.modelValue.length);
+            if (!this.modelValue.length) {
+                this.addEmpty();
+            } else {
+                this.clearEmpty();
+            }
+            this.modelValue.forEach((data, idx) => {
+                if (!this.trs[idx]) {
+                    this.addRaw();
+                }
+                this.setRawData(idx, data || {});
+            });
+            this.rule[0].children[1].children = this.trs;
+        },
+        addEmpty() {
+            if (this.trs.length) {
+                this.trs.splice(0, this.trs.length);
+            }
+            this.trs.push(this.emptyRule);
+        },
+        clearEmpty() {
+            if (this.trs[0] && this.trs[0]._isEmpty) {
+                this.trs.splice(0, 1);
+            }
+        },
+        delRaw(idx) {
+            if (this.disabled) {
+                return;
+            }
+            this.trs.splice(idx, 1);
+            this.updateValue();
+            if (this.trs.length) {
+                this.trs.forEach(tr => this.updateRaw(tr));
+            } else {
+                this.addEmpty();
+            }
+            this.$emit('delete', idx);
+        },
+        addRaw(flag) {
+            if (flag && this.disabled) {
+                return;
+            }
+            const tr = this.formCreateInject.form.parseJson(this.copyTrs)[0];
+            if (this.trs.length === 1 && this.trs[0]._isEmpty) {
+                this.trs.splice(0, 1);
+            }
+            this.trs.push(tr);
+            this.updateRaw(tr);
+            if (flag) {
+                this.$emit('add', this.trs.length);
+                this.updateValue();
+            }
+        },
+        updateRaw(tr) {
+            const idx = this.trs.indexOf(tr);
+            tr.children[0].props.innerText = idx + 1;
+            tr.children[tr.children.length - 1].children[0].props.onClick = () => {
+                this.delRaw(idx);
+            };
+        },
+        loadRule() {
+            const header = [{
+                type: 'th',
+                native: true,
+                class: '_fc-tf-head-idx',
+                props: {
+                    innerText: '#'
+                }
+            }];
+            let body = [{
+                type: 'td',
+                class: '_fc-tf-idx',
+                native: true,
+                props: {
+                    innerText: '0'
+                }
+            }];
+            this.columns.forEach((column) => {
+                header.push({
+                    type: 'th',
+                    native: true,
+                    style: column.style,
+                    class: column.required ? '_fc-tf-head-required' : '',
+                    props: {
+                        innerText: column.label || ''
+                    }
+                });
+                body.push({
+                    type: 'td',
+                    native: true,
+                    children: [...(column.rule || [])]
+                });
+            });
+            header.push({
+                type: 'th',
+                native: true,
+                class: '_fc-tf-edit fc-clock',
+                props: {
+                    innerText: this.formCreateInject.t('operation') || '操作'
+                }
+            });
+            body.push({
+                type: 'td',
+                native: true,
+                class: '_fc-tf-btn fc-clock',
+                children: [
+                    {
+                        type: 'i',
+                        native: true,
+                        class: 'fc-icon icon-delete',
+                        props: {},
+                    }
+                ],
+            });
+            this.copyTrs = this.formCreateInject.form.toJson([
+                {
+                    type: 'tr',
+                    native: true,
+                    subRule: true,
+                    children: body
+                }
+            ]);
+            this.rule = [
+                {
+                    type: 'table',
+                    native: true,
+                    class: '_fc-tf-table',
+                    props: {
+                        border: '1',
+                        cellspacing: '0',
+                        cellpadding: '0',
+                    },
+                    children: [
+                        {
+                            type: 'thead',
+                            native: true,
+                            children: [
+                                {
+                                    type: 'tr',
+                                    native: true,
+                                    children: header
+                                }
+                            ]
+                        },
+                        {
+                            type: 'tbody',
+                            native: true,
+                            children: this.trs
+                        }
+                    ]
+                }
+            ]
+        },
+    },
+    created() {
+        this.loadRule();
+    },
+    mounted() {
+        this.updateTable();
+    }
+};
+</script>
+
+<style>
+._fc-table-form {
+    overflow: auto;
+    color: #666666;
+}
+
+._fc-table-form .form-create .el-form-item {
+    margin-bottom: 1px;
+}
+
+._fc-table-form .form-create .el-form-item.is-error {
+    margin-bottom: 22px;
+}
+
+._fc-table-form .el-form-item__label, ._fc-table-form .van-field__label {
+    display: none !important;
+}
+
+._fc-table-form .el-form-item__content {
+    display: flex;
+    margin-left: 0px !important;
+    width: 100% !important;
+}
+
+._fc-tf-head-idx, ._fc-tf-idx {
+    width: 40px;
+    min-width: 40px;
+    font-weight: 500;
+    text-align: center;
+}
+
+._fc-tf-edit, ._fc-tf-btn {
+    width: 70px;
+    min-width: 70px;
+    text-align: center;
+}
+
+._fc-tf-btn .fc-icon {
+    cursor: pointer;
+}
+
+._fc-table-form._fc-disabled ._fc-tf-btn .fc-icon, ._fc-table-form._fc-disabled > .el-button {
+    cursor: not-allowed;
+}
+
+._fc-tf-table {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    table-layout: fixed;
+    border: 1px solid #EBEEF5;
+    border-bottom: 0 none;
+}
+
+._fc-table-form ._fc-tf-table > thead > tr > th {
+    border: 0 none;
+    border-bottom: 1px solid #EBEEF5;
+    height: 40px;
+    font-weight: 500;
+}
+
+._fc-table-form ._fc-tf-table > thead > tr > th + th {
+    border-left: 1px solid #EBEEF5;
+}
+
+._fc-table-form tr {
+    min-height: 50px;
+}
+
+._fc-table-form ._fc-read-view {
+    text-align: center;
+    width: 100%;
+}
+
+._fc-table-form td {
+    padding: 5px;
+    min-height: 50px;
+    min-width: 80px;
+    position: relative;
+    box-sizing: border-box;
+    overflow-wrap: break-word;
+    /*white-space: nowrap;*/
+    overflow: hidden;
+    border: 0 none;
+    border-bottom: 1px solid #EBEEF5;
+}
+
+._fc-table-form td + td {
+    border-left: 1px solid #EBEEF5;
+}
+
+._fc-tf-table .el-input-number, ._fc-tf-table .el-select, ._fc-tf-table .el-slider, ._fc-tf-table .el-cascader, ._fc-tf-table .el-date-editor {
+    width: 100%;
+}
+
+._fc-tf-head-required:before {
+    content: '*';
+    color: #f56c6c;
+    margin-right: 4px;
+}
+</style>

+ 101 - 0
src/components/form-create-designer/components/tableForm/TableFormColumnView.vue

@@ -0,0 +1,101 @@
+<template>
+    <div class="_fd-tf-col" :style="colStyle">
+        <div class="_fd-tf-title">
+            <span v-if="required" class="_fd-tf-required">*</span>{{ label || '' }}
+        </div>
+        <div class="_fd-tf-con">
+            <slot></slot>
+        </div>
+    </div>
+</template>
+
+<script>
+import is from '@form-create/utils/lib/type';
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'TableFormColumnView',
+    props: {
+        label: String,
+        width: [Number, String],
+        color: String,
+        required: Boolean,
+    },
+    computed: {
+        colStyle() {
+            const w = this.width;
+            const style = {width: is.Number(w) ? `${w}px` : ((!w || w === 'auto') ? '180px' : w)};
+            if (this.color) {
+                style.color = this.color;
+            }
+            return style;
+        }
+    },
+    data() {
+        return {};
+    }
+});
+</script>
+
+<style>
+
+._fd-tf-col ._fd-tf-con .el-form-item {
+    margin-bottom: 1px !important;
+}
+
+._fd-tf-col {
+    display: flex;
+    flex-wrap: wrap;
+    flex-direction: column;
+    width: 180px;
+    flex-shrink: 0;
+}
+
+._fd-tf-con .el-form-item__label {
+    display: none !important;
+}
+
+._fd-tf-con {
+    display: flex;
+    flex: 1;
+    width: 100%;
+}
+
+._fd-tf-con .el-form-item__content {
+    display: flex;
+    margin-left: 0px !important;
+    width: 100% !important;
+}
+
+
+._fd-tf-title {
+    display: flex;
+    height: 40px;
+    width: 100% !important;
+    border-bottom: 1px solid #ebeef5;
+    align-items: center;
+    margin-bottom: 0px;
+    padding-left: 5px;
+}
+
+._fd-tf-required {
+    color: #f56c6c;
+    margin-right: 4px;
+}
+
+._fd-tf-con ._fc-l-item {
+    display: flex;
+    width: 100%;
+    margin-top: 4px;
+    flex-shrink: 0;
+}
+
+._fd-tf-con ._fc-l-item > * {
+    display: none !important;
+}
+
+._fd-tf-con .el-input-number, ._fd-tf-con .el-select, ._fd-tf-con .el-slider, ._fd-tf-con .el-cascader, ._fd-tf-con .el-date-editor {
+    width: 100%;
+}
+
+</style>

+ 45 - 0
src/components/form-create-designer/components/tableForm/TableFormView.vue

@@ -0,0 +1,45 @@
+<template>
+    <div class="_fd-table-form">
+        <div class="_fd-tf-wrap" v-if="$slots.default">
+            <slot></slot>
+        </div>
+        <div class="_fc-child-empty" v-else></div>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'TableFormView',
+    data() {
+        return {};
+    },
+});
+</script>
+
+<style>
+._fd-table-form {
+    min-height: 130px;
+    width: 100%;
+    border: 1px solid #ECECEC;
+    background: #fff;
+}
+
+._fc-child-empty {
+    min-height: 130px;
+}
+
+._fd-tf-wrap {
+    display: flex;
+    overflow: auto;
+}
+
+._fd-tf-wrap > ._fd-drag-tool {
+    flex-shrink: 0;
+    display: flex;
+    margin: 2px;
+    height: auto;
+}
+
+</style>

+ 43 - 0
src/components/form-create-designer/config/base/field.js

@@ -0,0 +1,43 @@
+export default function field({t}) {
+    return [
+        {
+            type: 'FieldInput',
+            field: 'field',
+            value: '',
+            title: t('form.field'),
+            warning: t('warning.field'),
+        }, {
+            type: 'LanguageInput',
+            field: 'title',
+            value: '',
+            title: t('form.title'),
+        }, {
+            type: 'LanguageInput',
+            field: 'info',
+            value: '',
+            title: t('form.info'),
+        }, {
+            type: 'SizeInput',
+            field: 'formCreateWrap>labelWidth',
+            value: '',
+            title: t('form.labelWidth'),
+        }, {
+            type: 'Struct',
+            field: '_control',
+            name: 'control',
+            value: [],
+            title: t('form.control'),
+            warning: t('form.controlDocument', {doc: '<a target="_blank" href="https://form-create.com/v3/guide/control" style="color: inherit;text-decoration: underline;">' + t('form.document') + '</a>'}),
+            props: {
+                defaultValue: [],
+                validate(val) {
+                    if (!Array.isArray(val)) return false;
+                    if (!val.length) return true;
+                    return !val.some(({rule}) => {
+                        return !Array.isArray(rule);
+                    });
+                }
+            }
+        },
+    ];
+}

+ 116 - 0
src/components/form-create-designer/config/base/form.js

@@ -0,0 +1,116 @@
+import {localeOptions} from '../../utils';
+
+export default function form({t}) {
+    return [
+        {
+            type: 'input',
+            field: '>formName',
+            value: '',
+            title: t('form.formName'),
+        }, {
+            type: 'radio',
+            field: 'labelPosition',
+            value: 'left',
+            title: t('form.labelPosition'),
+            options: localeOptions(t, [
+                {value: 'left', label: 'left'},
+                {value: 'right', label: 'right'},
+                {value: 'top', label: 'top'},
+            ])
+        }, {
+            type: 'radio',
+            field: 'size',
+            value: 'small',
+            title: t('form.size'),
+            options: localeOptions(t, [
+                {value: 'large', label: 'large'},
+                {value: 'default', label: 'default'},
+                {value: 'small', label: 'small'},
+            ])
+        }, {
+            type: 'input',
+            field: 'labelSuffix',
+            value: '',
+            title: t('form.labelSuffix'),
+            style: {
+                width: '150px'
+            }
+        }, {
+            type: 'SizeInput',
+            field: 'labelWidth',
+            value: '125px',
+            title: t('form.labelWidth'),
+        }, {
+            type: 'switch',
+            field: 'hideRequiredAsterisk',
+            value: false,
+            title: t('form.hideRequiredAsterisk'),
+        }, {
+            type: 'switch',
+            field: 'showMessage',
+            value: true,
+            title: t('form.showMessage'),
+        }, {
+            type: 'switch',
+            field: 'inlineMessage',
+            value: false,
+            title: t('form.inlineMessage'),
+        }, {
+            type: 'switch',
+            field: '_submitBtn>show',
+            value: true,
+            title: t('form.submitBtn'),
+        }, {
+            type: 'switch',
+            field: '_resetBtn>show',
+            value: false,
+            title: t('form.resetBtn'),
+        }, {
+            type: 'FnConfig',
+            field: '>_event',
+            warning: t('form.controlDocument', {doc: '<a target="_blank" href="https://form-create.com/v3/guide/global-event" style="color: inherit;text-decoration: underline;">' + t('form.document') + '</a>'}),
+            value: {},
+            col: {show: true},
+            props: {
+                eventConfig: [
+                    {
+                        name: 'onSubmit',
+                        info: t('form.onSubmit'),
+                        args: ['formData', 'api'],
+                    },
+                    {
+                        name: 'onReset',
+                        info: t('form.onReset'),
+                        args: ['api'],
+                    },
+                    {
+                        name: 'onCreated',
+                        info: t('form.onCreated'),
+                        args: ['api'],
+                    },
+                    {
+                        name: 'onMounted',
+                        info: t('form.onMounted'),
+                        args: ['api'],
+                    },
+                    {
+                        name: 'onReload',
+                        info: t('form.onReload'),
+                        args: ['api'],
+                    },
+                    {
+                        name: 'onChange',
+                        info: t('form.onChange'),
+                        args: ['field', 'value', 'options'],
+                    },
+                    {
+                        name: 'beforeFetch',
+                        info: t('form.beforeFetch'),
+                        args: ['config', 'data'],
+                    },
+                ]
+            },
+            title: t('form.event'),
+        },
+    ];
+}

+ 26 - 0
src/components/form-create-designer/config/base/style.js

@@ -0,0 +1,26 @@
+export default function field({t}) {
+    return [
+        {
+            type: 'input',
+            title: 'ID',
+            field: 'id',
+            wrap: {
+                labelWidth: '45px'
+            }
+        },
+        {
+            type: 'input',
+            title: 'Class',
+            field: 'class',
+            wrap: {
+                labelWidth: '45px'
+            }
+        },
+        {
+            type: 'StyleConfig',
+            field: 'style',
+            title: '',
+            value: {},
+        }
+    ];
+}

+ 15 - 0
src/components/form-create-designer/config/base/validate.js

@@ -0,0 +1,15 @@
+export default function validate({t}) {
+    return [
+        {
+            type: 'Required',
+            field: '$required',
+            title: t('validate.required')
+        },
+        {
+            type: 'validate',
+            field: 'validate',
+            title: t('validate.rule'),
+            value: []
+        },
+    ];
+}

+ 68 - 0
src/components/form-create-designer/config/index.js

@@ -0,0 +1,68 @@
+import radio from './rule/radio';
+import checkbox from './rule/checkbox';
+import input from './rule/input';
+import textarea from './rule/textarea';
+import password from './rule/password';
+import number from './rule/number';
+import select from './rule/select';
+import _switch from './rule/switch';
+import slider from './rule/slider';
+import time from './rule/time';
+import timeRange from './rule/timeRange';
+import date from './rule/date';
+import dateRange from './rule/dateRange';
+import rate from './rule/rate';
+import color from './rule/color';
+import row from './rule/row';
+import col from './rule/col';
+import tabPane from './rule/tabPane';
+import divider from './rule/divider';
+import cascader from './rule/cascader';
+import upload from './rule/upload';
+import transfer from './rule/transfer';
+import tree from './rule/tree';
+import alert from './rule/alert';
+import text from './rule/text';
+import space from './rule/space';
+import tabs from './rule/tabs';
+import button from './rule/button';
+import editor from './rule/editor';
+import group from './rule/group';
+import subForm from './rule/subForm';
+import card from './rule/card';
+import collapse from './rule/collapse';
+import collapseItem from './rule/collapseItem';
+import treeSelect from './rule/treeSelect';
+import tag from './rule/tag';
+import html from './rule/html';
+import table from './rule/table';
+import tableForm from './rule/tableForm';
+import tableFormColumn from './rule/tableFormColumn';
+import image from './rule/image';
+
+
+const ruleList = [
+    input, textarea, password, number, radio, checkbox, select, _switch, rate, time, timeRange, slider, date, dateRange, color, cascader, upload, transfer, tree, treeSelect, editor,
+    group, subForm, tableForm, tableFormColumn,
+    alert, button, text, html, divider, tag, image,
+    row, table, tabs, space, card, collapse,
+    col, tabPane, collapseItem,
+];
+
+export default ruleList;
+
+export function defaultDrag(rule) {
+    return {
+        icon: rule.field ? 'icon-input' : 'icon-cell',
+        label: rule.field || rule.type,
+        name: '_',
+        mask: true,
+        handleBtn: ['delete'],
+        rule() {
+            return rule;
+        },
+        props() {
+            return [];
+        }
+    }
+}

+ 24 - 0
src/components/form-create-designer/config/menu.js

@@ -0,0 +1,24 @@
+export default function createMenu() {
+    return [
+        {
+            name: 'main',
+            title: '基础组件',
+            list: []
+        },
+        {
+            name: 'subform',
+            title: '子表单组件',
+            list: []
+        },
+        {
+            name: 'aide',
+            title: '辅助组件',
+            list: []
+        },
+        {
+            name: 'layout',
+            title: '布局组件',
+            list: []
+        },
+    ];
+}

+ 45 - 0
src/components/form-create-designer/config/rule/alert.js

@@ -0,0 +1,45 @@
+import {localeProps} from '../../utils';
+
+const label = '提示';
+const name = 'elAlert';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-alert',
+    label,
+    name,
+    event: ['close'],
+    rule({t}) {
+        return {
+            type: name,
+            props: {
+                title: t('com.elAlert.name'),
+                description: t('com.elAlert.description'),
+                type: 'success',
+                effect: 'dark',
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'input', field: 'title'}, {
+            type: 'select',
+            field: 'type',
+            options: [{label: 'success', value: 'success'}, {label: 'warning', value: 'warning'}, {
+                label: 'info',
+                value: 'info'
+            }, {label: 'error', value: 'error'}]
+        }, {type: 'input', field: 'description'}, {
+            type: 'switch',
+            field: 'closable',
+            value: true
+        }, {type: 'switch', field: 'center', value: true}, {
+            type: 'input',
+            field: 'closeText'
+        }, {type: 'switch', field: 'showIcon'}, {
+            type: 'select',
+            field: 'effect',
+            options: [{label: 'light', value: 'light'}, {label: 'dark', value: 'dark'}]
+        }]);
+    }
+};

+ 49 - 0
src/components/form-create-designer/config/rule/button.js

@@ -0,0 +1,49 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '按钮';
+const name = 'elButton';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-button',
+    label,
+    name,
+    mask: true,
+    event: ['click'],
+    rule({t}) {
+        return {
+            type: name,
+            props: {},
+            children: [t('com.elButton.name')],
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'input',
+            field: 'formCreateChild',
+        }, {
+            type: 'select',
+            field: 'size',
+            options: localeOptions(t, [{label: 'large', value: 'large'}, {label: 'default', value: 'default'}, {
+                label: 'small',
+                value: 'small'
+            }])
+        }, {
+            type: 'select',
+            field: 'type',
+            options: [{label: 'primary', value: 'primary'}, {
+                label: 'success',
+                value: 'success'
+            }, {label: 'warning', value: 'warning'}, {label: 'danger', value: 'danger'}, {
+                label: 'info',
+                value: 'info'
+            }]
+        }, {type: 'switch', field: 'plain'}, {
+            type: 'switch',
+            field: 'round'
+        }, {type: 'switch', field: 'circle'}, {
+            type: 'switch',
+            field: 'loading'
+        }, {type: 'switch', field: 'disabled'}]);
+    }
+};

+ 40 - 0
src/components/form-create-designer/config/rule/card.js

@@ -0,0 +1,40 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '卡片';
+const name = 'elCard';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-card',
+    label,
+    name,
+    drag: true,
+    inside: false,
+    mask: false,
+    rule({t}) {
+        return {
+            type: name,
+            props: {
+                header: t('com.elCard.props.header')
+            },
+            style: {
+                width: '100%'
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'input',
+            field: 'header',
+        }, {
+            type: 'select',
+            field: 'shadow',
+            value: 'always',
+            options: localeOptions(t, [{label: 'always', value: 'always'}, {label: 'never', value: 'never'}, {
+                label: 'hover',
+                value: 'hover'
+            }])
+        }]);
+    }
+};

+ 121 - 0
src/components/form-create-designer/config/rule/cascader.js

@@ -0,0 +1,121 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps, makeTreeOptions, makeTreeOptionsRule} from '../../utils/index';
+
+const label = '级联选择器';
+const name = 'cascader';
+
+export default {
+    menu: 'main',
+    icon: 'icon-cascader',
+    label,
+    name,
+    input: true,
+    event: ['change', 'expandChange', 'blur', 'focus', 'visibleChange', 'removeTag'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.cascader.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {
+                options: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 3)
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeTreeOptionsRule(t, 'props.options'),
+            ...[
+
+                {
+                    type: 'switch',
+                    field: 'disabled'
+                },
+                {
+                    type: 'switch',
+                    field: 'clearable'
+                },
+                {
+                    type: 'input',
+                    field: 'placeholder'
+                },
+                {
+                    type: 'Object',
+                    field: 'props',
+                    props: {
+                        rule: localeProps(t, name + '.propsOpt', [{
+                            type: 'switch',
+                            field: 'multiple'
+                        }, {
+                            type: 'select',
+                            field: 'expandTrigger',
+                            options: localeOptions(t, [{label: 'click', value: 'click'}, {
+                                label: 'hover',
+                                value: 'hover'
+                            }])
+                        }, {
+                            type: 'switch',
+                            field: 'checkStrictly'
+                        }, {
+                            type: 'switch',
+                            field: 'emitPath',
+                            value: true
+                        }, {
+                            type: 'input',
+                            field: 'value',
+                            value: 'value'
+                        }, {
+                            type: 'input',
+                            field: 'label',
+                            value: 'label'
+                        }, {
+                            type: 'input',
+                            field: 'children',
+                            value: 'children'
+                        }, {
+                            type: 'input',
+                            field: 'disabled',
+                            value: 'disabled'
+                        }, {type: 'input', field: 'leaf'}])
+                    }
+                },
+                {
+                    type: 'switch',
+                    field: 'showAllLevels',
+                    value: true
+                },
+                {
+                    type: 'switch',
+                    field: 'collapseTags'
+                },
+                {
+                    type: 'switch',
+                    field: 'collapseTagsTooltip'
+                },
+                {
+                    type: 'input',
+                    field: 'separator'
+                },
+                {
+                    type: 'switch',
+                    field: 'filterable'
+                },
+                {
+                    type: 'select',
+                    field: 'tagType',
+                    options: [
+                        {label: 'success', value: 'success'},
+                        {label: 'info', value: 'info'},
+                        {label: 'warning', value: 'warning'},
+                        {label: 'danger', value: 'danger'},
+                    ]
+                },
+            ]
+        ]);
+    }
+};

+ 68 - 0
src/components/form-create-designer/config/rule/checkbox.js

@@ -0,0 +1,68 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeOptionsRule, makeTreeOptions} from '../../utils/index';
+
+const label = '多选框';
+const name = 'checkbox';
+
+export default {
+    menu: 'main',
+    icon: 'icon-checkbox',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    validate: ['array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.checkbox.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {},
+            options: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 1)
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeOptionsRule(t, 'options'),
+            ...[
+                {
+                    type: 'switch',
+                    field: 'disabled'
+                },
+                {type: 'switch', field: 'input'},
+                {
+                    type: 'switch',
+                    field: 'type',
+                    props: {activeValue: 'button', inactiveValue: 'default'}
+                },
+                {
+                    field: 'min',
+                    type: 'inputNumber',
+                    props: {
+                        min: 0
+                    }
+                },
+                {
+                    field: 'max',
+                    type: 'inputNumber',
+                    props: {
+                        min: 0
+                    }
+                },
+                {
+                    type: 'ColorInput',
+                    field: 'textColor'
+                },
+                {
+                    type: 'ColorInput',
+                    field: 'fill'
+                }
+            ]
+        ]);
+    }
+};

+ 86 - 0
src/components/form-create-designer/config/rule/col.js

@@ -0,0 +1,86 @@
+import {localeProps} from '../../utils';
+
+const name = 'col';
+
+const devices = {
+    xs: '<768px',
+    sm: '≥768px',
+    md: '≥992px',
+    lg: '≥1200px',
+    xl: '≥1920px',
+};
+
+export default {
+    name,
+    label: '格子',
+    drag: true,
+    dragBtn: false,
+    inside: true,
+    mask: false,
+    rule() {
+        return {
+            type: name,
+            props: {span: 12},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {type: 'slider', field: 'span', value: 12, props: {min: 0, max: 24}},
+            {type: 'slider', field: 'offset', props: {min: 0, max: 24}},
+            {type: 'slider', field: 'push', props: {min: 0, max: 24}},
+            {type: 'slider', field: 'pull', props: {min: 0, max: 24}},
+            {
+                type: 'ConfigItem',
+                props: {
+                    label: t('props.reactive')
+                },
+                children: [
+                    {
+                        type: 'elTabs',
+                        style: {
+                            width: '100%'
+                        },
+                        slot: 'append',
+                        children: Object.keys(devices).map(k => {
+                            return {
+                                type: 'elTabPane',
+                                props: {
+                                    label: devices[k]
+                                },
+                                style: 'padding:0 10px;',
+                                children: [
+                                    {
+                                        type: 'slider',
+                                        field: k + '>span',
+                                        title: t('com.col.props.span'),
+                                        value: 12,
+                                        props: {min: 0, max: 24},
+                                    },
+                                    {
+                                        type: 'slider',
+                                        field: k + '>offset',
+                                        title: t('com.col.props.offset'),
+                                        props: {min: 0, max: 24},
+                                    },
+                                    {
+                                        type: 'slider',
+                                        field: k + '>push',
+                                        title: t('com.col.props.push'),
+                                        props: {min: 0, max: 24},
+                                    },
+                                    {
+                                        type: 'slider',
+                                        field: k + '>pull',
+                                        title: t('com.col.props.pull'),
+                                        props: {min: 0, max: 24},
+                                    }
+                                ]
+                            };
+                        })
+                    }
+                ]
+            },
+        ]);
+    }
+};

+ 30 - 0
src/components/form-create-designer/config/rule/collapse.js

@@ -0,0 +1,30 @@
+import {localeProps} from '../../utils';
+
+const label = '折叠面板';
+const name = 'elCollapse';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-collapse',
+    label,
+    name,
+    mask: false,
+    children: 'elCollapseItem',
+    event: ['change'],
+    rule() {
+        return {
+            type: name,
+            props: {},
+            style: {
+                width: '100%',
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'accordion'
+        }]);
+    }
+};

+ 36 - 0
src/components/form-create-designer/config/rule/collapseItem.js

@@ -0,0 +1,36 @@
+import {localeProps} from '../../utils';
+
+const label = '面板';
+const name = 'elCollapseItem';
+
+export default {
+    icon: 'icon-cell',
+    label,
+    name,
+    drag: true,
+    dragBtn: false,
+    inside: true,
+    mask: false,
+    rule({t}) {
+        return {
+            type: name,
+            props: {
+                title: t('com.elCollapseItem.name')
+            },
+            style: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'input',
+            field: 'title',
+        }, {
+            type: 'input',
+            field: 'name',
+        }, {
+            type: 'switch',
+            field: 'disabled'
+        }]);
+    }
+};

+ 53 - 0
src/components/form-create-designer/config/rule/color.js

@@ -0,0 +1,53 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '颜色选择器';
+const name = 'colorPicker';
+
+export default {
+    menu: 'main',
+    icon: 'icon-color',
+    label,
+    name,
+    input: true,
+    event: ['change', 'activeChange', 'focus', 'blur'],
+    validate: ['string'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.colorPicker.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'showAlpha'
+            },
+            {
+                type: 'select',
+                field: 'colorFormat',
+                options: [{label: 'hsl', value: 'hsl'}, {label: 'hsv', value: 'hsv'}, {
+                    label: 'hex',
+                    value: 'hex'
+                }, {label: 'rgb', value: 'rgb'}]
+            },
+            {
+                type: 'tableOptions',
+                field: 'predefine',
+                props: {
+                    column: [{label: t('props.value'), key: 'value'}],
+                    valueType: 'string'
+                }
+            },
+        ]);
+    }
+};

+ 70 - 0
src/components/form-create-designer/config/rule/date.js

@@ -0,0 +1,70 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '日期';
+const name = 'datePicker';
+
+export default {
+    menu: 'main',
+    icon: 'icon-date',
+    label,
+    name,
+    input: true,
+    event: ['change', 'blur', 'focus', 'calendarChange', 'panelChange', 'visibleChange'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.datePicker.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'switch', field: 'readonly'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'select',
+            field: 'type',
+            options: localeOptions(t, [{label: 'year', value: 'year'}, {label: 'month', value: 'month'}, {
+                label: 'date',
+                value: 'date'
+            }, {label: 'dates', value: 'dates'}, {label: 'week', value: 'week'}, {
+                label: 'datetime',
+                value: 'datetime'
+            }, {label: 'datetimerange', value: 'datetimerange'}, {
+                label: 'daterange',
+                value: 'daterange'
+            }, {label: 'monthrange', value: 'monthrange'}])
+        }, {
+            type: 'switch',
+            field: 'clearable',
+            value: true
+        }, {
+            type: 'Struct',
+            field: 'pickerOptions',
+            props: {defaultValue: {}}
+        }, {type: 'switch', field: 'editable', value: true}, {
+            type: 'input',
+            field: 'placeholder'
+        }, {
+            type: 'input',
+            field: 'startPlaceholder'
+        }, {type: 'input', field: 'endPlaceholder'}, {
+            type: 'input',
+            field: 'format'
+        }, {
+            type: 'select',
+            field: 'align',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'center', value: 'center'}, {
+                label: 'right',
+                value: 'right'
+            }])
+        }, {type: 'input', field: 'rangeSeparator'}, {
+            type: 'switch',
+            field: 'unlinkPanels'
+        }]);
+    }
+};

+ 64 - 0
src/components/form-create-designer/config/rule/dateRange.js

@@ -0,0 +1,64 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '日期区间';
+const name = 'dateRange';
+
+export default {
+    menu: 'main',
+    icon: 'icon-date-range',
+    label,
+    name,
+    input: true,
+    event: ['change', 'blur', 'focus', 'calendarChange', 'panelChange', 'visibleChange'],
+    rule({t}) {
+        return {
+            type: 'datePicker',
+            field: uniqueId(),
+            title: t('com.dateRange.name'),
+            info: '',
+            $required: false,
+            props: {
+                type: 'datetimerange',
+            },
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, 'datePicker.props', [{type: 'switch', field: 'readonly'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'select',
+            field: 'type',
+            options: localeOptions(t, [
+                {label: 'datetimerange', value: 'datetimerange'},
+                {label: 'daterange', value: 'daterange'},
+                {label: 'monthrange', value: 'monthrange'}
+            ])
+        }, {
+            type: 'switch',
+            field: 'clearable',
+            value: true
+        }, {
+            type: 'Struct',
+            field: 'pickerOptions',
+            props: {defaultValue: {}}
+        }, {type: 'switch', field: 'editable', value: true}, {
+            type: 'input',
+            field: 'startPlaceholder'
+        }, {type: 'input', field: 'endPlaceholder'}, {
+            type: 'input',
+            field: 'format'
+        }, {
+            type: 'select',
+            field: 'align',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'center', value: 'center'}, {
+                label: 'right',
+                value: 'right'
+            }])
+        }, {type: 'input', field: 'rangeSeparator'}, {
+            type: 'switch',
+            field: 'unlinkPanels'
+        }]);
+    }
+};

+ 31 - 0
src/components/form-create-designer/config/rule/divider.js

@@ -0,0 +1,31 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '分割线';
+const name = 'elDivider';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-divider',
+    label,
+    name,
+    rule({t}) {
+        return {
+            type: name,
+            props: {},
+            children: [t('com.elDivider.name')],
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'input',
+            field: 'formCreateChild',
+        }, {
+            type: 'select',
+            field: 'contentPosition',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'right', value: 'right'}, {
+                label: 'center',
+                value: 'center'
+            }])
+        }]);
+    }
+};

+ 31 - 0
src/components/form-create-designer/config/rule/editor.js

@@ -0,0 +1,31 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '富文本框';
+const name = 'fcEditor';
+
+export default {
+    menu: 'main',
+    icon: 'icon-editor',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    validate: ['string'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.fcEditor.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }]);
+    }
+};

+ 53 - 0
src/components/form-create-designer/config/rule/group.js

@@ -0,0 +1,53 @@
+import {localeProps} from '../../utils';
+import uniqueId from '@form-create/utils/lib/unique';
+
+const label = '子表单';
+const name = 'group';
+
+export default {
+    menu: 'subform',
+    icon: 'icon-subform',
+    label,
+    name,
+    inside: false,
+    drag: true,
+    dragBtn: true,
+    mask: false,
+    input: true,
+    event: ['change'],
+    subForm: 'array',
+    loadRule(rule) {
+        rule.children = rule.props.rule || [];
+        rule.type = 'FcRow';
+        delete rule.props.rule;
+    },
+    parseRule(rule) {
+        rule.props.rule = rule.children;
+        rule.type = 'group';
+        delete rule.children;
+        delete rule.props.mode;
+    },
+    rule({t}) {
+        return {
+            type: 'fcRow',
+            field: uniqueId(),
+            title: t('com.group.name'),
+            info: '',
+            $required: false,
+            props: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }, {type: 'switch', field: 'syncDisabled', value: true},
+        {type: 'switch', field: 'button', value: true},
+        {type: 'switch', field: 'sortBtn', value: true},
+        {type: 'inputNumber', field: 'expand'},
+        {type: 'inputNumber', field: 'min'},
+        {type: 'inputNumber', field: 'max'},
+        ]);
+    }
+};

+ 52 - 0
src/components/form-create-designer/config/rule/html.js

@@ -0,0 +1,52 @@
+import {localeProps} from '../../utils';
+
+const label = 'HTML';
+const name = 'html';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-html',
+    label,
+    name,
+    rule() {
+        return {
+            type: name,
+            title: '',
+            native: true,
+            attrs: {
+                innerHTML: ''
+            },
+            style: {
+                display: 'block',
+                width: '100%',
+            },
+            children: ['<div style="color:blue;">\n' +
+            ' html html html html html html html html html\n' +
+            '  </div>'],
+        };
+    },
+    watch: {
+        formCreateNative({value, rule}) {
+            if (value) {
+                rule.title = '';
+            }
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch', field: 'formCreateNative', props: {
+                    activeValue: false,
+                    inactiveValue: true,
+                },
+                control: [{value: false, rule: ['formCreateTitle']}]
+            }, {
+                type: 'input',
+                field: 'formCreateTitle',
+            }, {
+                type: 'HtmlEditor',
+                field: 'formCreateChild',
+            }
+        ]);
+    }
+};

+ 32 - 0
src/components/form-create-designer/config/rule/image.js

@@ -0,0 +1,32 @@
+import {localeProps} from '../../utils';
+
+const label = '图片';
+const name = 'elImage';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-image',
+    label,
+    name,
+    rule() {
+        return {
+            type: name,
+            title: '',
+            style: {
+                width: '100px',
+                height: '100px',
+            },
+            props: {
+                src: 'https://static.form-create.com/example.png',
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'input',
+                field: 'src',
+            }
+        ]);
+    }
+};

+ 62 - 0
src/components/form-create-designer/config/rule/input.js

@@ -0,0 +1,62 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '输入框';
+const name = 'input';
+
+export default {
+    menu: 'main',
+    icon: 'icon-input',
+    label,
+    name,
+    input: true,
+    event: ['blur', 'focus', 'change', 'input', 'clear'],
+    validate: ['string', 'url', 'email'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.input.name'),
+            info: '',
+            $required: false,
+            props: {}
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'readonly'
+            },
+            {
+                type: 'select',
+                field: 'type',
+                options: localeOptions(t, [
+                    {label: 'text', value: 'text'},
+                    {label: 'number', value: 'number'},
+                    {label: 'time', value: 'time'},
+                    {label: 'date', value: 'date'},
+                    {label: 'month', value: 'month'},
+                    {label: 'datetime-local', value: 'datetime-local'},
+                ])
+            },
+            {
+                type: 'inputNumber',
+                field: 'maxlength',
+                props: {min: 0}
+            },
+            {
+                type: 'input',
+                field: 'placeholder'
+            },
+            {
+                type: 'switch',
+                field: 'clearable'
+            },
+        ]);
+    }
+};

+ 49 - 0
src/components/form-create-designer/config/rule/number.js

@@ -0,0 +1,49 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '计数器';
+const name = 'inputNumber';
+
+export default {
+    menu: 'main',
+    icon: 'icon-number',
+    label,
+    name,
+    input: true,
+    event: ['blur', 'focus', 'change'],
+    validate: ['number', 'integer', 'float'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.inputNumber.name'),
+            info: '',
+            $required: false,
+            props: {}
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'switch', field: 'disabled'}, {
+            type: 'inputNumber',
+            field: 'min'
+        }, {
+            type: 'inputNumber',
+            field: 'max',
+        },  {
+            type: 'inputNumber',
+            title: 'precision',
+            field: 'precision',
+        }, {type: 'inputNumber', field: 'step', props: {min: 0}}, {
+            type: 'switch',
+            field: 'stepStrictly'
+        }, {
+            type: 'switch',
+            field: 'controls',
+            value: true
+        }, {
+            type: 'select',
+            field: 'controlsPosition',
+            options: localeOptions(t, [{label: 'default', value: ''}, {label: 'right', value: 'right'}])
+        }, {type: 'input', field: 'placeholder'}]);
+    }
+};

+ 52 - 0
src/components/form-create-designer/config/rule/password.js

@@ -0,0 +1,52 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '密码输入框';
+const name = 'password';
+
+export default {
+    menu: 'main',
+    icon: 'icon-password',
+    label,
+    name,
+    input: true,
+    event: ['blur', 'focus', 'change', 'input', 'clear'],
+    validate: ['string'],
+    rule({t}) {
+        return {
+            type: 'input',
+            field: uniqueId(),
+            title: t('com.password.name'),
+            info: '',
+            $required: false,
+            props: {
+                type: 'password'
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'readonly'
+            },
+            {
+                type: 'inputNumber',
+                field: 'maxlength',
+                props: {min: 0}
+            },
+            {
+                type: 'input',
+                field: 'placeholder'
+            },
+            {
+                type: 'switch',
+                field: 'clearable'
+            },
+        ]);
+    }
+};

+ 43 - 0
src/components/form-create-designer/config/rule/radio.js

@@ -0,0 +1,43 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeOptionsRule, makeTreeOptions} from '../../utils/index';
+
+const label = '单选框';
+const name = 'radio';
+
+export default {
+    menu: 'main',
+    icon: 'icon-radio',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    validate: ['string', 'number'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.radio.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {},
+            options: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 1)
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeOptionsRule(t, 'options'),
+            {type: 'switch', field: 'disabled'},
+            {type: 'switch', field: 'input'},
+            {
+                type: 'switch',
+                field: 'type',
+                props: {activeValue: 'button', inactiveValue: 'default'}
+            }, {type: 'ColorInput', field: 'textColor'}, {
+                type: 'ColorInput',
+                field: 'fill'
+            }]);
+    }
+};

+ 44 - 0
src/components/form-create-designer/config/rule/rate.js

@@ -0,0 +1,44 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '评分';
+const name = 'rate';
+
+export default {
+    menu: 'main',
+    icon: 'icon-rate',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    validate: ['number'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.rate.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {type: 'inputNumber', field: 'max', props: {min: 0}}, {
+                type: 'switch',
+                field: 'disabled'
+            }, {type: 'switch', field: 'allowHalf'}, {
+                type: 'ColorInput',
+                field: 'voidColor'
+            }, {type: 'ColorInput', field: 'disabledVoidColor'}, {
+                type: 'input',
+                field: 'voidIconClass'
+            }, {type: 'input', field: 'disabledVoidIconClass'}, {
+                type: 'switch',
+                field: 'showScore'
+            }, {type: 'ColorInput', field: 'textColor'}, {
+                type: 'input',
+                field: 'scoreTemplate'
+            }]);
+    }
+};

+ 46 - 0
src/components/form-create-designer/config/rule/row.js

@@ -0,0 +1,46 @@
+import {localeProps} from '../../utils';
+
+const label = '栅格布局';
+const name = 'fcRow';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-row',
+    label,
+    name,
+    mask: false,
+    children: 'col',
+    childrenLen: 2,
+    rule() {
+        return {
+            type: name,
+            props: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'inputNumber',
+            field: 'gutter',
+            props: {min: 0}
+        }, {
+            type: 'switch',
+            field: 'type',
+            props: {activeValue: 'flex', inactiveValue: 'default'}
+        }, {
+            type: 'select',
+            field: 'justify',
+            options: [{label: 'start', value: 'start'}, {label: 'end', value: 'end'}, {
+                label: 'center',
+                value: 'center'
+            }, {label: 'space-around', value: 'space-around'}, {label: 'space-between', value: 'space-between'}]
+        }, {
+            type: 'select',
+            field: 'align',
+            options: [{label: 'top', value: 'top'}, {label: 'middle', value: 'middle'}, {
+                label: 'bottom',
+                value: 'bottom'
+            }]
+        }]);
+    }
+};

+ 70 - 0
src/components/form-create-designer/config/rule/select.js

@@ -0,0 +1,70 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {getInjectArg, localeProps, makeOptionsRule, makeTreeOptions} from '../../utils/index';
+
+const label = '选择器';
+const name = 'select';
+
+export default {
+    menu: 'main',
+    icon: 'icon-select',
+    label,
+    name,
+    input: true,
+    event: ['change', 'visibleChange', 'removeTag', 'clear', 'blur', 'focus'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.select.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {},
+            options: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 1)
+        };
+    },
+    watch: {
+        multiple({rule}) {
+            rule.key = uniqueId();
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeOptionsRule(t, 'options'),
+            {type: 'switch', field: 'multiple'}, {
+                type: 'switch',
+                field: 'disabled'
+            }, {type: 'switch', field: 'clearable'}, {
+                type: 'switch',
+                field: 'collapseTags'
+            }, {
+                type: 'inputNumber',
+                field: 'multipleLimit',
+                props: {min: 0}
+            }, {type: 'input', field: 'placeholder'}, {
+                type: 'switch',
+                field: 'filterable'
+            }, {
+                type: 'switch',
+                field: 'remote',
+            }, {
+                type: 'FnInput',
+                field: 'remoteMethod',
+                props: {
+                    body: true,
+                    fnx: true,
+                    name: 'remoteMethod',
+                    args: [getInjectArg(t)],
+                },
+            }, {type: 'switch', field: 'allowCreate'}, {
+                type: 'input',
+                field: 'noMatchText'
+            }, {type: 'input', field: 'noDataText'}, {
+                type: 'switch',
+                field: 'reserveKeyword'
+            }, {type: 'switch', field: 'defaultFirstOption'}])
+    }
+};

+ 53 - 0
src/components/form-create-designer/config/rule/slider.js

@@ -0,0 +1,53 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '滑块';
+const name = 'slider';
+
+export default {
+    menu: 'main',
+    icon: 'icon-slider',
+    label,
+    name,
+    input: true,
+    event: ['change', 'input'],
+    validate: ['number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.slider.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'switch', field: 'disabled'}, {
+            type: 'switch',
+            field: 'range'
+        }, {
+            type: 'inputNumber',
+            field: 'min',
+            props: {min: 0}
+        }, {
+            type: 'inputNumber',
+            field: 'max',
+            props: {min: 0},
+        }, {
+            type: 'inputNumber',
+            field: 'step',
+            props: {min: 0},
+        }, {type: 'switch', field: 'showInput'}, {
+            type: 'switch',
+            field: 'showInputControls',
+            value: true
+        }, {type: 'switch', field: 'showStops'}, {
+            type: 'switch',
+            field: 'vertical'
+        }, {
+            type: 'input',
+            field: 'height'
+        }]);
+    }
+};

+ 44 - 0
src/components/form-create-designer/config/rule/space.js

@@ -0,0 +1,44 @@
+import {localeProps} from '../../utils';
+
+const label = '间距';
+const name = 'space';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-space',
+    label,
+    name,
+    rule() {
+        return {
+            type: 'div',
+            wrap: {
+                show: false
+            },
+            native: true,
+            style: {
+                width: '100%',
+                height: '20px',
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return [
+            {
+                type: 'object',
+                field: 'formCreateStyle',
+                native: true,
+                props: {
+                    rule: localeProps(t, name + '.props', [
+                        {
+                            type: 'input',
+                            field: 'height',
+                            title: 'height',
+                        },
+                    ])
+                }
+            }
+
+        ];
+    }
+};

+ 47 - 0
src/components/form-create-designer/config/rule/subForm.js

@@ -0,0 +1,47 @@
+import {localeProps} from '../../utils';
+import uniqueId from '@form-create/utils/lib/unique';
+
+const label = '分组';
+const name = 'subForm';
+
+export default {
+    menu: 'subform',
+    icon: 'icon-group',
+    label,
+    name,
+    inside: false,
+    drag: true,
+    dragBtn: true,
+    mask: false,
+    input: true,
+    subForm: 'object',
+    event: ['change'],
+    loadRule(rule) {
+        rule.children = rule.props.rule || [];
+        rule.type = 'FcRow';
+        delete rule.props.rule;
+    },
+    parseRule(rule) {
+        rule.props.rule = rule.children;
+        rule.type = 'subForm';
+        delete rule.children;
+    },
+    rule({t}) {
+        return {
+            type: 'fcRow',
+            field: uniqueId(),
+            title: t('com.subForm.name'),
+            info: '',
+            $required: false,
+            props: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }, {type: 'switch', field: 'syncDisabled', value: true},
+        ]);
+    }
+};

+ 46 - 0
src/components/form-create-designer/config/rule/switch.js

@@ -0,0 +1,46 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '开关';
+const name = 'switch';
+
+export default {
+    menu: 'main',
+    icon: 'icon-switch',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.switch.name'),
+            info: '',
+            $required: false,
+            props: {
+                activeValue: true,
+                inactiveValue: false,
+            },
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'inputNumber',
+            field: 'width',
+            props: {min: 0},
+        }, {type: 'input', field: 'activeText'}, {
+            type: 'input',
+            field: 'inactiveText'
+        }, {type: 'ValueInput', field: 'activeValue'}, {
+            type: 'ValueInput',
+            field: 'inactiveValue'
+        }, {type: 'ColorInput', field: 'activeColor'}, {
+            type: 'ColorInput',
+            field: 'inactiveColor'
+        }]);
+    }
+};

+ 29 - 0
src/components/form-create-designer/config/rule/tabPane.js

@@ -0,0 +1,29 @@
+import {localeProps} from '../../utils';
+
+const label = '选项卡';
+const name = 'elTabPane';
+
+export default {
+    label,
+    name,
+    inside: true,
+    drag: true,
+    dragBtn: false,
+    mask: false,
+    rule({t}) {
+        return {
+            type: name,
+            props: {label: t('com.elTabPane.name')},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'input', field: 'label'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {type: 'input', field: 'name'}, {
+            type: 'switch',
+            field: 'lazy'
+        }]);
+    }
+};

+ 35 - 0
src/components/form-create-designer/config/rule/table.js

@@ -0,0 +1,35 @@
+import {localeProps} from '../../utils';
+
+const label = '表格布局';
+const name = 'fcTable';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-table',
+    label,
+    name,
+    inside: false,
+    mask: false,
+    rule() {
+        return {
+            type: name,
+            props: {
+                rule: {
+                    row: 3,
+                    col: 4,
+                    style: {},
+                    class: {},
+                    layout: []
+                }
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {type: 'switch', field: 'border', value: true},
+            {type: 'ColorInput', field: 'borderColor'},
+            {type: 'input', field: 'borderWidth'},
+        ]);
+    }
+};

+ 79 - 0
src/components/form-create-designer/config/rule/tableForm.js

@@ -0,0 +1,79 @@
+import unique from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '表格表单';
+const name = 'tableForm';
+
+export default {
+    menu: 'subform',
+    icon: 'icon-table-form',
+    label,
+    name,
+    mask: false,
+    input: true,
+    subForm: 'array',
+    event: ['change', 'add', 'delete'],
+    languageKey: ['add', 'operation', 'dataEmpty'],
+    children: 'tableFormColumn',
+    loadRule(rule) {
+        if (!rule.props) rule.props = {};
+        const columns = rule.props.columns || [];
+        rule.children = columns.map(column => {
+            return {
+                type: 'tableFormColumn',
+                _fc_drag_tag: 'tableFormColumn',
+                props: {
+                    label: column.label,
+                    required: column.required || false,
+                    width: column.style.width || '',
+                    color: column.style.color || '',
+                },
+                children: column.rule || []
+            }
+        });
+        delete rule.props.columns;
+    },
+    parseRule(rule) {
+        const children = rule.children || [];
+        rule.props.columns = children.map(column => {
+            return {
+                label: column.props.label,
+                required: column.props.required,
+                style: {
+                    width: column.props.width,
+                    color: column.props.color,
+                },
+                rule: column.children || []
+            };
+        })
+        rule.children = [];
+    },
+    rule({t}) {
+        return {
+            type: name,
+            field: unique(),
+            title: t('com.tableForm.name'),
+            info: '',
+            props: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'filterEmptyColumn',
+                value: true,
+            },
+            {
+                type: 'inputNumber',
+                field: 'max',
+                props: {min: 0}
+            },
+        ]);
+    }
+};

+ 43 - 0
src/components/form-create-designer/config/rule/tableFormColumn.js

@@ -0,0 +1,43 @@
+import {localeProps} from '../../utils';
+
+const name = 'tableFormColumn';
+
+export default {
+    icon: 'icon-cell',
+    name,
+    aide: true,
+    drag: true,
+    dragBtn: false,
+    mask: false,
+    style: false,
+    rule({t}) {
+        return {
+            type: name,
+            props: {
+                label: t('com.tableFormColumn.label'),
+                width: 'auto'
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'input',
+                field: 'label',
+            },
+            {
+                type: 'switch',
+                field: 'required',
+            },
+            {
+                type: 'input',
+                field: 'width',
+            },
+            {
+                type: 'ColorInput',
+                field: 'color',
+            }
+        ]);
+    }
+};

+ 38 - 0
src/components/form-create-designer/config/rule/tabs.js

@@ -0,0 +1,38 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '标签页';
+const name = 'elTabs';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-tab',
+    label,
+    name,
+    mask: false,
+    event: ['tabClick', 'tabChange', 'tabRemove', 'tabAdd', 'edit'],
+    children: 'elTabPane',
+    rule() {
+        return {
+            type: name,
+            style: {width: '100%'},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'select',
+            field: 'type',
+            options: [{
+                label: 'card',
+                value: 'card'
+            }, {label: 'border-card', value: 'border-card'}]
+        }, {type: 'switch', field: 'closable'}, {
+            type: 'select',
+            field: 'tabPosition',
+            options: localeOptions(t, [{label: 'top', value: 'top'}, {label: 'right', value: 'right'}, {
+                label: 'left',
+                value: 'left'
+            }])
+        }, {type: 'switch', field: 'stretch'}]);
+    }
+};

+ 79 - 0
src/components/form-create-designer/config/rule/tag.js

@@ -0,0 +1,79 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '标签';
+const name = 'elTag';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-tag',
+    label,
+    name,
+    mask: true,
+    event: ['click', 'close'],
+    rule({t}) {
+        return {
+            type: name,
+            title: '',
+            native: true,
+            children: [t('com.elTag.name')]
+        };
+    },
+    watch: {
+        formCreateNative({value, rule}) {
+            if (value) {
+                rule.title = '';
+            }
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch', field: 'formCreateNative', props: {
+                    activeValue: false,
+                    inactiveValue: true,
+                },
+                control: [{value: false, rule: ['formCreateTitle']}]
+            },
+            {
+                type: 'input',
+                field: 'formCreateTitle',
+            }, {
+                type: 'input',
+                field: 'formCreateChild'
+            }, {
+                type: 'select',
+                field: 'type',
+                options: [{label: 'primary', value: 'primary'}, {
+                    label: 'success',
+                    value: 'success'
+                }, {label: 'warning', value: 'warning'}, {label: 'danger', value: 'danger'}, {
+                    label: 'info',
+                    value: 'info'
+                }]
+            }, {
+                type: 'select',
+                field: 'size',
+                options: localeOptions(t, [{label: 'large', value: 'large'}, {
+                    label: 'default',
+                    value: 'default'
+                }, {label: 'small', value: 'small'}])
+            }, {
+                type: 'select',
+                field: 'effect',
+                options: [{label: 'dark', value: 'dark'}, {
+                    label: 'light',
+                    value: 'light'
+                }, {label: 'plain', value: 'plain'}]
+            }, {
+                type: 'switch', field: 'closable'
+            }, {
+                type: 'switch', field: 'disableTransitions'
+            }, {
+                type: 'switch', field: 'hit'
+            }, {
+                type: 'switch', field: 'round'
+            }, {
+                type: 'ColorInput', field: 'color'
+            }]);
+    }
+};

+ 50 - 0
src/components/form-create-designer/config/rule/text.js

@@ -0,0 +1,50 @@
+import {localeProps} from '../../utils';
+
+const label = '文字';
+const name = 'text';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-span',
+    label,
+    name,
+    rule({t}) {
+        return {
+            type: 'div',
+            title: '',
+            native: true,
+            style: {
+                whiteSpace: 'pre-line',
+                width: '100%',
+            },
+            children: [t('com.text.name')],
+        };
+    },
+    watch: {
+        formCreateNative({value, rule}) {
+            if (value) {
+                rule.title = '';
+            }
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch', field: 'formCreateNative', props: {
+                    activeValue: false,
+                    inactiveValue: true,
+                },
+                control: [{value: false, rule: ['formCreateTitle']}]
+            }, {
+                type: 'input',
+                field: 'formCreateTitle',
+            }, {
+                type: 'input',
+                field: 'formCreateChild',
+                props: {
+                    type: 'textarea'
+                }
+            }
+        ]);
+    }
+};

+ 63 - 0
src/components/form-create-designer/config/rule/textarea.js

@@ -0,0 +1,63 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '多行输入框';
+const name = 'textarea';
+
+export default {
+    menu: 'main',
+    icon: 'icon-textarea',
+    label,
+    name,
+    input: true,
+    event: ['blur', 'focus', 'change', 'input'],
+    validate: ['string'],
+    rule({t}) {
+        return {
+            type: 'input',
+            field: uniqueId(),
+            title: t('com.textarea.name'),
+            info: '',
+            $required: false,
+            props: {
+                type: 'textarea'
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'readonly'
+            },
+            {
+                type: 'inputNumber',
+                field: 'maxlength',
+                props: {min: 0}
+            },
+            {
+                type: 'switch',
+                field: 'showWordLimit'
+            },
+            {
+                type: 'input',
+                field: 'placeholder'
+            },
+            {
+                type: 'inputNumber',
+                field: 'rows',
+                props: {
+                    min: 0
+                }
+            },
+            {
+                type: 'switch',
+                field: 'autosize'
+            },
+        ]);
+    }
+};

+ 62 - 0
src/components/form-create-designer/config/rule/time.js

@@ -0,0 +1,62 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '时间';
+const name = 'timePicker';
+
+export default {
+    menu: 'main',
+    icon: 'icon-time',
+    label,
+    name,
+    input: true,
+    event: ['change', 'blur', 'focus', 'visibleChange'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.timePicker.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    watch: {
+        isRange({rule}) {
+            rule.key = uniqueId();
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'switch', field: 'readonly'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'switch',
+            field: 'isRange'
+        }, {
+            type: 'switch',
+            field: 'clearable',
+            value: true
+        }, {
+            type: 'Struct',
+            field: 'pickerOptions',
+            props: {defaultValue: {}}
+        }, {type: 'switch', field: 'editable', value: true}, {
+            type: 'input',
+            field: 'placeholder'
+        }, {
+            type: 'input',
+            field: 'startPlaceholder'
+        }, {type: 'input', field: 'endPlaceholder'}, {
+            type: 'switch',
+            field: 'arrowControl'
+        }, {
+            type: 'select',
+            field: 'align',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'center', value: 'center'}, {
+                label: 'right',
+                value: 'right'
+            }])
+        }]);
+    }
+};

+ 53 - 0
src/components/form-create-designer/config/rule/timeRange.js

@@ -0,0 +1,53 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '时间区间';
+const name = 'timeRange';
+
+export default {
+    menu: 'main',
+    icon: 'icon-time-range',
+    label,
+    name,
+    input: true,
+    event: ['change', 'blur', 'focus', 'visibleChange'],
+    rule({t}) {
+        return {
+            type: 'timePicker',
+            field: uniqueId(),
+            title: t('com.timeRange.name'),
+            info: '',
+            $required: false,
+            props: {
+                isRange: true,
+            },
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, 'timePicker.props', [{type: 'switch', field: 'readonly'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'switch',
+            field: 'clearable',
+            value: true
+        }, {
+            type: 'Struct',
+            field: 'pickerOptions',
+            props: {defaultValue: {}}
+        }, {type: 'switch', field: 'editable', value: true}, {
+            type: 'input',
+            field: 'startPlaceholder'
+        }, {type: 'input', field: 'endPlaceholder'}, {
+            type: 'switch',
+            field: 'arrowControl'
+        }, {
+            type: 'select',
+            field: 'align',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'center', value: 'center'}, {
+                label: 'right',
+                value: 'right'
+            }])
+        }]);
+    }
+};

+ 59 - 0
src/components/form-create-designer/config/rule/transfer.js

@@ -0,0 +1,59 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeTreeOptions, makeTreeOptionsRule} from '../../utils';
+
+const label = '穿梭框';
+const name = 'elTransfer';
+
+export default {
+    menu: 'main',
+    icon: 'icon-transfer',
+    label,
+    name,
+    input: true,
+    event: ['change', 'leftCheckChange', 'rightCheckChange'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.elTransfer.name'),
+            info: '',
+            $required: false,
+            props: {
+                data: makeTreeOptions(t('props.option'), {label: 'label', value: 'key'}, 1)
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeTreeOptionsRule(t, 'props.data', 'label', 'key'),
+            {type: 'switch', field: 'filterable'}, {
+                type: 'input',
+                field: 'filterPlaceholder'
+            }, {
+                type: 'select',
+                field: 'targetOrder',
+                warning: t('com.elTransfer.props.targetOrderInfo'),
+                options: [{label: 'original', value: 'original'}, {
+                    label: 'push',
+                    value: 'push'
+                }, {label: 'unshift', value: 'unshift'}]
+            }, {
+                type: 'TableOptions',
+                field: 'titles',
+                props: {
+                    column: [{label: t('props.value'), key: 'value'}],
+                    valueType: 'string',
+                    max: 2,
+                }
+            }, {
+                type: 'TableOptions',
+                field: 'buttonTexts',
+                props: {
+                    column: [{label: t('props.value'), key: 'value'}],
+                    valueType: 'string',
+                    max: 2,
+                }
+            }]);
+    }
+};

+ 70 - 0
src/components/form-create-designer/config/rule/tree.js

@@ -0,0 +1,70 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeTreeOptions, makeTreeOptionsRule} from '../../utils/index';
+
+const label = '树形控件';
+const name = 'tree';
+
+export default {
+    menu: 'main',
+    icon: 'icon-tree',
+    label,
+    name,
+    input: true,
+    event: ['nodeClick', 'nodeContextmenu', 'checkChange', 'check', 'currentChange', 'nodeExpand', 'nodeCollapse', 'nodeDragStart', 'nodeDragEnter', 'nodeDragLeave', 'nodeDragOver', 'nodeDragEnd', 'nodeDrop'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.tree.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {
+                props: {
+                    label: 'label',
+                },
+                showCheckbox: true,
+                nodeKey: 'id',
+                data: makeTreeOptions(t('props.option'), {label: 'label', value: 'id'}, 3),
+            },
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeTreeOptionsRule(t, 'props.data', 'label', 'id'),
+            {type: 'input', field: 'emptyText'}, {
+                type: 'TableOptions',
+                field: 'props',
+                props: {
+                    column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                    valueType: 'object'
+                }
+            }, {
+                type: 'switch',
+                field: 'renderAfterExpand',
+                value: true
+            }, {
+                type: 'switch',
+                field: 'defaultExpandAll',
+            }, {
+                type: 'switch',
+                field: 'expandOnClickNode',
+                value: true
+            }, {
+                type: 'switch',
+                field: 'checkOnClickNode'
+            }, {type: 'switch', field: 'autoExpandParent', value: true}, {
+                type: 'switch',
+                field: 'checkStrictly'
+            }, {type: 'switch', field: 'accordion'}, {
+                type: 'inputNumber',
+                field: 'indent'
+            }, {
+                type: 'input',
+                field: 'nodeKey'
+            }]);
+    }
+};

+ 77 - 0
src/components/form-create-designer/config/rule/treeSelect.js

@@ -0,0 +1,77 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeTreeOptions, makeTreeOptionsRule} from '../../utils/index';
+
+const label = '树形选择';
+const name = 'elTreeSelect';
+
+export default {
+    menu: 'main',
+    icon: 'icon-tree-select',
+    label,
+    name,
+    input: true,
+    event: ['change', 'visibleChange', 'removeTag', 'clear', 'blur', 'focus'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.elTreeSelect.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {
+                nodeKey: 'value',
+                showCheckbox: true,
+                data: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 3),
+            },
+        };
+    },
+    watch: {
+        multiple({rule}) {
+            rule.key = uniqueId();
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeTreeOptionsRule(t, 'props.data', 'label', 'value'),
+            {type: 'switch', field: 'multiple'}, {
+                type: 'switch',
+                field: 'disabled'
+            }, {type: 'switch', field: 'clearable'}, {
+                type: 'switch',
+                field: 'collapseTags'
+            }, {
+                type: 'inputNumber',
+                field: 'multipleLimit',
+                props: {min: 0}
+            }, {type: 'input', field: 'placeholder'},
+            {
+                type: 'TableOptions',
+                field: 'props',
+                props: {
+                    column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                    valueType: 'object'
+                }
+            }, {
+                type: 'switch',
+                field: 'renderAfterExpand',
+                value: true
+            }, {
+                type: 'switch',
+                field: 'defaultExpandAll',
+            }, {
+                type: 'switch',
+                field: 'expandOnClickNode',
+                value: true
+            }, {
+                type: 'switch',
+                field: 'checkOnClickNode'
+            }, {
+                type: 'input',
+                field: 'nodeKey'
+            }]);
+    }
+};

+ 108 - 0
src/components/form-create-designer/config/rule/upload.js

@@ -0,0 +1,108 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { getInjectArg, getOptionArg, localeOptions, localeProps } from '../../utils'
+
+const label = '上传'
+const name = 'upload';
+
+export default {
+    menu: 'main',
+    icon: 'icon-upload',
+    label,
+    name,
+    input: true,
+    event: ['change', 'remove', 'preview', 'error', 'progress', 'exceed'],
+    languageKey: ['clickToUpload'],
+    validate: ['array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.upload.name'),
+            info: '',
+            $required: false,
+            props: {
+                action: '/',
+                onSuccess: new Function('res', 'file', 'file.url = res.data.url;')
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'select',
+            field: 'listType',
+            options: localeOptions(t, [{label: 'text', value: 'text'}, {
+                label: 'picture',
+                value: 'picture'
+            }, {
+                label: 'picture-card',
+                value: 'picture-card'
+            }]),
+        }, {type: 'switch', field: 'multiple'}, {
+            type: 'input',
+            field: 'action'
+        }, {
+            type: 'FnInput',
+            field: 'beforeUpload',
+            props: {
+                args: ['file'],
+                name: 'beforeUpload',
+            }
+        }, {
+            type: 'FnInput',
+            field: 'beforeRemove',
+            props: {
+                body: true,
+                button: true,
+                fnx: true,
+                args: [getInjectArg(t)],
+                name: 'beforeRemove',
+            }
+        }, {
+					type: 'FnInput',
+					field: 'httpRequest',
+					props: {
+						body: true,
+						button: true,
+						fnx: true,
+						args: [getOptionArg(t)],
+						name: 'httpRequest',
+					}
+				}, {
+            type: 'FnInput',
+            field: 'onSuccess',
+            warning: t('com.upload.info'),
+            props: {
+                args: ['res', 'file'],
+                name: 'onSuccess',
+            }
+        }, {
+            type: 'TableOptions',
+            field: 'headers',
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object'
+            }
+        }, {
+            type: 'TableOptions',
+            field: 'data',
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object'
+            }
+        }, {type: 'input', field: 'name'}, {
+            type: 'switch',
+            field: 'withCredentials'
+        }, {type: 'input', field: 'accept'}, {
+            type: 'switch',
+            field: 'autoUpload',
+            value: true
+        }, {
+            type: 'inputNumber',
+            field: 'limit',
+            props: {min: 0},
+        }]);
+    }
+};

+ 136 - 0
src/components/form-create-designer/index.js

@@ -0,0 +1,136 @@
+import FcDesigner from './components/FcDesigner.vue';
+import DragTool from './components/DragTool.vue';
+import Struct from './components/Struct.vue';
+import Row from './components/Row.vue';
+import HtmlEditor from './components/HtmlEditor.vue';
+import FnEditor from './components/FnEditor.vue';
+import FnInput from './components/FnInput.vue';
+import FetchConfig from './components/FetchConfig.vue';
+import ConfigItem from './components/style/ConfigItem.vue';
+import FieldInput from './components/FieldInput.vue';
+import EventConfig from './components/EventConfig.vue';
+import FnConfig from './components/FnConfig.vue';
+import TableView from './components/table/TableView.vue';
+import Table from './components/table/Table.vue';
+import Validate from './components/Validate.vue';
+import DragBox from './components/DragBox.vue';
+import Required from './components/Required.vue';
+import TableOptions from './components/TableOptions.vue';
+import TreeOptions from './components/TreeOptions.vue';
+import TableFormView from './components/tableForm/TableFormView.vue';
+import TableForm from './components/tableForm/TableForm.vue';
+import TableFormColumnView from './components/tableForm/TableFormColumnView.vue';
+import SizeInput from './components/style/SizeInput.vue';
+import ColorInput from './components/style/ColorInput.vue';
+import StyleConfig from './components/style/StyleConfig.vue';
+import LanguageInput from './components/language/LanguageInput.vue';
+import ValueInput from './components/ValueInput.vue';
+import formCreate, {designerForm} from './utils/form';
+import FcEditor from '@form-create/component-wangeditor';
+import draggable from 'vuedraggable/src/vuedraggable';
+import {
+    compareVersion,
+    copyTextToClipboard,
+    getInjectArg,
+    localeOptions,
+    localeProps,
+    makeOptionsRule,
+    makeRequiredRule,
+    makeTreeOptions,
+    makeTreeOptionsRule,
+    toJSON
+} from './utils/index';
+import globalUseLocale, {t} from './utils/locale';
+import './style/index.css';
+import './style/icon.css';
+import './utils/highlight/style.css';
+
+/**FIXME:在node_modules引用此组件会导致无法加载,暂时排查不出来原因。
+ * 需要复制粘贴组件
+ * */
+const addComponent = (id, component, previewComponent) => {
+    designerForm.component(id, previewComponent || component);
+    formCreate.component(id, component);
+}
+
+designerForm.component('draggable', draggable);
+designerForm.component('DragTool', DragTool);
+designerForm.component('DragBox', DragBox);
+designerForm.component('Validate', Validate);
+designerForm.component('Struct', Struct);
+designerForm.component('HtmlEditor', HtmlEditor);
+designerForm.component('FetchConfig', FetchConfig);
+designerForm.component('FnEditor', FnEditor);
+designerForm.component('FnInput', FnInput);
+designerForm.component('Required', Required);
+designerForm.component('TableOptions', TableOptions);
+designerForm.component('TreeOptions', TreeOptions);
+designerForm.component('TableFormColumn', TableFormColumnView);
+designerForm.component('EventConfig', EventConfig);
+designerForm.component('ColorInput', ColorInput);
+designerForm.component('SizeInput', SizeInput);
+designerForm.component('StyleConfig', StyleConfig);
+designerForm.component('LanguageInput', LanguageInput);
+designerForm.component('ConfigItem', ConfigItem);
+designerForm.component('FieldInput', FieldInput);
+designerForm.component('FnConfig', FnConfig);
+designerForm.component('FcRow', Row);
+designerForm.component('ValueInput', ValueInput);
+addComponent('FcEditor', FcEditor);
+addComponent('TableForm', TableForm, TableFormView);
+addComponent('FcTable', Table, TableView);
+
+const install = function (Vue) {
+    Vue.component('FcDesigner', FcDesigner);
+};
+
+FcDesigner.install = install;
+FcDesigner.makeOptionsRule = makeOptionsRule;
+FcDesigner.copyTextToClipboard = copyTextToClipboard;
+FcDesigner.getInjectArg = getInjectArg;
+FcDesigner.localeOptions = localeOptions;
+FcDesigner.localeProps = localeProps;
+FcDesigner.makeRequiredRule = makeRequiredRule;
+FcDesigner.makeTreeOptions = makeTreeOptions;
+FcDesigner.makeTreeOptionsRule = makeTreeOptionsRule;
+FcDesigner.toJSON = toJSON;
+FcDesigner.formCreate = formCreate;
+FcDesigner.designerForm = designerForm;
+FcDesigner.component = addComponent;
+FcDesigner.useLocale = globalUseLocale;
+FcDesigner.t = t;
+
+FcDesigner.utils = {
+    copyTextToClipboard,
+    getInjectArg,
+    localeOptions,
+    localeProps,
+    makeOptionsRule,
+    makeRequiredRule,
+    makeTreeOptions,
+    makeTreeOptionsRule,
+    toJSON
+}
+
+const minVersion = '3.2.18';
+
+if (compareVersion(minVersion, formCreate.version) === 1) {
+    console.warn('Please use FormCreate version ' + minVersion + ' or greater, see https://github.com/xaboy/form-create.');
+}
+
+export default FcDesigner;
+
+export {
+    formCreate,
+    designerForm,
+    install,
+    copyTextToClipboard,
+    getInjectArg,
+    localeOptions,
+    localeProps,
+    makeOptionsRule,
+    makeRequiredRule,
+    makeTreeOptions,
+    makeTreeOptionsRule,
+    toJSON
+};

+ 867 - 0
src/components/form-create-designer/locale/en.js

@@ -0,0 +1,867 @@
+const En = {
+    name: 'en',
+    form: {
+        field: 'Field',
+        title: 'Title',
+        info: 'Info',
+        control: 'Control',
+        labelPosition: 'Label position',
+        labelStyle: 'Label style',
+        labelSuffix: 'Label suffix',
+        size: 'Form size',
+        event: 'Form event',
+        labelWidth: 'Label width',
+        hideRequiredAsterisk: 'Hide the red asterisk next to the label of a required field',
+        showMessage: 'Display verification error message',
+        inlineMessage: 'Display validation information inline',
+        submitBtn: 'Whether to display the form submit button',
+        resetBtn: 'Whether to display the form reset button',
+        appendChild: 'Insert child',
+        formMode: 'Form mode',
+        formName: 'Form name',
+        componentMode: 'Component',
+        htmlMode: 'HTML',
+        document: 'Document',
+        controlDocument: 'Need more detailed configuration methods? Please view {doc}',
+        onSubmit: 'Triggered when form is submitted',
+        onReset: 'Triggered after form is reset',
+        onCreated: 'Triggered after the form component is initialized',
+        onMounted: 'Triggered after the form component is mounted',
+        onReload: 'Triggered after the form rendering rule is reloaded',
+        onChange: 'Triggered when the component value changes',
+        beforeFetch: 'Triggered before remote data request is sent',
+    },
+    warning: {
+        name: 'Unique identifier for the component, used to access and modify its configuration rules.',
+        field: 'Field name for binding data to the component. Must start with a letter for proper recognition.',
+        fetch: 'Loads remote data through requests, updating the component based on the returned result.',
+        fetchQuery: 'Defines GET parameters for requests, passed via the URL.',
+        fetchData: 'Defines POST parameters for requests, passed in the request body.',
+        fetchDataType: 'Selects the data type for the request body to ensure correct format.',
+        fetchParse: 'Processes the response data after the request and converts it into the required structure.',
+        language: 'Manages multilingual data, allowing easy language switching for content display.',
+    },
+    computed: {
+        fieldUsed: '[{label}] Is used in the calculation formula, please modify the corresponding formula first',
+        fieldExist: '[{label}] Field already exists',
+        fieldEmpty: 'Field is required',
+        fieldChar: 'Field must begin with a letter',
+    },
+    validate: {
+        type: 'Type',
+        typePlaceholder: 'Please select',
+        trigger: 'Trigger',
+        mode: 'Verification method',
+        modes: {
+            required: 'required',
+            pattern: 'pattern',
+            validator: 'validator',
+            min: 'min',
+            max: 'max',
+            len: 'length',
+        },
+        types: {
+            string: 'String',
+            boolean: 'Boolean',
+            array: 'Multiple',
+            number: 'Number',
+            integer: 'Integer',
+            float: 'Float',
+            object: 'Collection',
+            date: 'Date',
+            url: 'Url',
+            email: 'Email',
+        },
+        message: 'Error',
+        auto: 'Automatic',
+        autoRequired: 'Please enter {title}',
+        autoMode: 'Please enter the correct {title}',
+        requiredPlaceholder: 'Please enter',
+        required: 'Is it required',
+        rule: 'Validation',
+    },
+    tableOptions: {
+        handle: 'Operation',
+        add: 'Add',
+        empty1: 'Click the lower right corner',
+        empty2: 'Button to add a column',
+        rmCol: 'Delete current column',
+        rmRow: 'Delete current row',
+        splitRow: 'Split into rows',
+        splitCol: 'Split into columns',
+        mergeBottom: 'Merge downward',
+        mergeRight: 'Merge right',
+        addTop: 'Add top column',
+        addBottom: 'Add the following',
+        addLeft: 'Add left column',
+        addRight: 'Add right column',
+        keyValue: 'key-value',
+    },
+    struct: {
+        title: 'Edit',
+        only: '[{label}] Only one allowed to be added',
+        errorMsg: 'The input content is syntactically incorrect',
+        configured: 'Configured',
+    },
+    event: {
+        title: 'Edit',
+        create: 'Create',
+        list: 'List',
+        placeholder: 'Please enter the name of the event',
+        saveMsg: 'Please save the event currently being edited',
+        type: 'Type',
+        info: 'Info',
+        label: 'Field',
+        inject: {
+            api: 'API of current form',
+            rule: 'Generate rules for the current form',
+            self: 'Component generation rule',
+            option: 'Form configuration',
+            args: 'Original parameters of event',
+        }
+    },
+    eventInfo: {
+        blur: 'Triggered when focus is lost',
+        focus: 'Triggered when focus is obtained',
+        change: 'Triggered when the binding value changes',
+        input: 'Trigger when value changes',
+        clear: 'Triggered when the clear button is clicked',
+        close: 'Triggered when the component is closed',
+        click: 'Fires when the component is clicked',
+        add: 'Trigger when added',
+        delete: 'Triggered when deleted',
+        visibleChange: 'Triggered when the drop-down box appears/hides',
+        calendarChange: 'Triggered when the selected date in the calendar changes',
+        panelChange: 'Fires when the date panel changes',
+        open: 'Triggered when opening',
+        opened: 'Triggered when opening animation ends',
+        closed: 'Triggered when closing animation ends',
+        openAutoFocus: 'Triggered when entering focus on content',
+        closeAutoFocus: 'Triggered when entering focus from content',
+        submit: 'Triggered when submitting table',
+        confirm: 'Triggered when clicking confirm',
+        validateFail: 'Triggered when table verification fails',
+        hook_load: 'Triggered after component rules are loaded',
+        hook_mounted: 'Triggered after component is mounted',
+        hook_deleted: 'Triggered after component rules are removed',
+        hook_watch: 'Triggered after component rules change',
+        hook_value: 'Triggered after component value changes',
+        hook_hidden: 'Triggered after component display status changes',
+    },
+    fetch: {
+        title: 'Set data',
+        create: 'Create data',
+        config: 'Request',
+        action: 'Action',
+        actionRequired: 'Please enter the correct link',
+        placeholder: 'Please enter the name of the data source',
+        method: 'Method',
+        data: 'Attached',
+        dataType: 'DataType',
+        headers: 'Headers',
+        query: 'Query',
+        parse: 'Processing',
+        response: 'Data returned by the interface',
+        onError: 'onError',
+        remote: 'Remote',
+        static: 'Static',
+        optionsType: {
+            fetch: 'Fetch',
+            struct: 'Static',
+        }
+    },
+    style: {
+        width: 'Width',
+        height: 'Height',
+        color: 'Color',
+        backgroundColor: 'Background color',
+        margin: 'Margin',
+        padding: 'Padding',
+        borderRadius: 'Border radius',
+        border: 'Border',
+        solid: 'Solid',
+        dashed: 'Dashed',
+        dotted: 'Dotted',
+        double: 'Double',
+        opacity: 'Opacity',
+        scale: 'Scale',
+        minWidth: 'Min Width',
+        minHeight: 'Min Height',
+        maxWidth: 'Max Width',
+        maxHeight: 'Max Height',
+        overflow: {
+            name: 'Overflow',
+            visible: 'Visible',
+            hidden: 'Hidden',
+            scroll: 'Scroll',
+            auto: 'Auto scroll after overflow',
+        },
+        shadow: {
+            name: 'Shadow',
+            x: 'x-axis offset',
+            y: 'y-axis offset',
+            vague: 'blurred radius',
+            extend: 'diffusion radius',
+            inset: 'inward',
+            external: 'outward',
+            mode: 'Mode',
+            classic: 'Classic',
+            flat: 'Flat',
+            solid: 'Stereoscopic',
+        },
+        font: {
+            name: 'Font',
+            size: 'Size',
+            align: 'Align',
+            height: 'line-height',
+            spacing: 'letter-spacing',
+            preview: 'Preview',
+        },
+        decoration: {
+            name: 'Decoration',
+            underline: 'underline',
+            'line-through': 'line-through',
+            overline: 'overline',
+        },
+        weight: {
+            name: 'font-weight',
+            300: 'Fine',
+            400: 'Default',
+            500: 'Medium',
+            700: 'Bold',
+        }
+    },
+    designer: {
+        component: 'Component',
+        id: 'Unique id',
+        name: 'Serial number',
+        type: 'Type',
+        form: 'Form',
+        json: 'Rule',
+        style: 'Style',
+        rule: 'Basis',
+        advanced: 'Advanced',
+        props: 'Props',
+        customProps: 'Custom props',
+        validate: 'Validate',
+        event: 'Event',
+        clearWarn: 'It cannot be restored after clearing it. Are you sure you want to clear it? ',
+        childEmpty: 'Click the \\e789  button in the lower right corner to add a column',
+        dragEmpty: 'Drag the components from the list on the left here',
+        unload: 'Are you sure you want to leave the current page?',
+        comList: 'Component',
+    },
+    language: {
+        name: 'Language',
+        add: 'Add',
+        batchRemove: 'Batch Deletion',
+        select: 'Select language',
+    },
+    menu: {
+        main: 'Basic',
+        aide: 'Auxiliary',
+        layout: 'Layout',
+        component: 'Component',
+        subform: 'Subform',
+        tree: 'Structure'
+    },
+    props: {
+        disabled: 'disabled',
+        time: 'time',
+        size: 'Size',
+        email: 'email',
+        number: 'number',
+        globalData: 'Global data',
+        mobile: 'Mobile',
+        pc: 'Pc',
+        reactive: 'Reactive',
+        title: 'Title',
+        content: 'Content',
+        collection: 'Collection',
+        group: 'Group',
+        custom: 'Custom',
+        change: 'Change',
+        blur: 'Blur',
+        preview: 'Preview',
+        clear: 'Clear',
+        cancel: 'Cancel',
+        close: 'Close',
+        ok: 'Ok',
+        save: 'Save',
+        refresh: 'Refresh',
+        submit: 'Submit',
+        reset: 'Reset',
+        copy: 'Copy',
+        delete: 'Delete',
+        hide: 'Hidden',
+        show: 'Show',
+        position: 'Position',
+        render: 'Render',
+        large: 'large',
+        default: 'default',
+        small: 'small',
+        always: 'always',
+        never: 'never',
+        hover: 'hover',
+        click: 'click',
+        button: 'button',
+        year: 'year',
+        month: 'month',
+        date: 'date',
+        dates: 'dates',
+        week: 'week',
+        datetime: 'datetime',
+        'datetime-local': 'datetime',
+        datetimerange: 'datetimerange',
+        daterange: 'daterange',
+        monthrange: 'monthrange',
+        left: 'left',
+        right: 'right',
+        top: 'top',
+        text: 'text',
+        picture: 'picture',
+        'picture-card': 'picture-card',
+        center: 'center',
+        vertical: 'vertical',
+        horizontal: 'horizontal',
+        manage: 'Manage',
+        key: 'key',
+        name: 'Name',
+        value: 'value',
+        inputData: 'Default value',
+        append: 'Append',
+        options: 'Options',
+        option: 'Option',
+        callback: 'Callback',
+        _self: 'Current Window',
+        _blank: 'New Window',
+        _parent: 'Parent Window',
+        _top: 'Top Window',
+    },
+    com: {
+        cascader: {
+            name: 'Cascader',
+            event: {
+                expandChange: 'Triggered when the expanded node changes',
+                removeTag: 'In multi-select mode, triggered when Tag is removed'
+            },
+            props: {
+                props: 'Options',
+                placeholder: 'Placeholder',
+                disabled: 'Disabled',
+                clearable: 'Whether clearing options are supported',
+                showAllLevels: 'Whether the full path of the selected value is displayed in the input box',
+                collapseTags: 'Whether to collapse Tags in multi-select mode',
+                collapseTagsTooltip: 'Whether to display all selected tags when the mouse hovers over the text of a collapsed tag',
+                separator: 'Separator',
+                filterable: 'Whether this option can be searched',
+                tagType: 'Type',
+            },
+            propsOpt: {
+                multiple: 'Whether there are multiple selections',
+                expandTrigger: 'How to expand the secondary menu',
+                checkStrictly: 'Whether it is strictly observed that parent and child nodes are not related to each other',
+                emitPath: 'When the selected node changes, whether to return an array consisting of the values of the menus at each level where the node is located',
+                value: 'The value of the specified option is an attribute value of the option object',
+                label: 'Specify the option label as a certain attribute value of the option object',
+                children: 'The child option of the specified option is a certain attribute value of the option object',
+                disabled: 'The specified option is disabled as a certain attribute value of the option object',
+                leaf: 'The flag bit of the leaf node of the specified option is an attribute value of the option object',
+            }
+        },
+        checkbox: {
+            name: 'Checkbox',
+            props: {
+                input: 'Whether to fill in',
+                type: 'Type',
+                disabled: 'Disabled',
+                min: 'Minimum number that can be checked',
+                max: 'The maximum number that can be checked',
+                textColor: 'Font color when the button is active',
+                fill: 'Border and background color when the button is active'
+            }
+        },
+        col: {
+            name: 'Col',
+            props: {
+                span: 'Number of columns occupied by grid',
+                offset: 'Number of spaces on the left side of the grid',
+                push: 'Move the grid to the right by the number of cells',
+                pull: 'Move the grid to the left by the number of cells'
+            }
+        },
+        colorPicker: {
+            name: 'ColorPicker',
+            event: {
+                activeChange: 'Triggered when the color currently displayed in the panel changes'
+            },
+            props: {
+                disabled: 'Disabled',
+                showAlpha: 'Whether transparency selection is supported',
+                colorFormat: 'Color format',
+                predefine: 'Predefined color',
+            }
+        },
+        datePicker: {
+            name: 'Date',
+            props: {
+                pickerOptions: 'Options specific to the current time and date picker',
+                readonly: 'Readonly',
+                disabled: 'Disabled',
+                type: 'Type',
+                editable: 'Text box can be input',
+                clearable: 'Whether to display the clear button',
+                placeholder: 'Placeholder content for non-range selection',
+                startPlaceholder: 'Placeholder content for the start date when selecting the range',
+                endPlaceholder: 'Placeholder content for the end date when selecting a range',
+                format: 'Format displayed in the input box',
+                align: 'Alignment',
+                rangeSeparator: 'Separator when selecting range',
+                unlinkPanels: 'Unlink the two date panels in the range selector',
+            }
+        },
+        dateRange: {
+            name: 'DateRange',
+        },
+        timeRange: {
+            name: 'TimeRange',
+        },
+        elAlert: {
+            name: 'Alert',
+            description: 'Description',
+            props: {
+                title: 'Title',
+                type: 'Type',
+                description: 'Supporting text',
+                closable: 'Whether it can be closed',
+                center: 'Whether the text is centered',
+                closeText: 'Close button custom text',
+                showIcon: 'Whether to display the icon',
+                effect: 'Select a provided theme'
+            }
+        },
+        elButton: {
+            name: 'Button',
+            props: {
+                formCreateChild: 'Content',
+                size: 'Size',
+                type: 'Type',
+                plain: 'Whether the button is plain',
+                round: 'Whether the button has rounded corners',
+                circle: 'Whether the button is round',
+                loading: 'Whether it is loading status',
+                disabled: 'Disabled',
+            }
+        },
+        elCard: {
+            name: 'Card',
+            props: {
+                header: 'Title',
+                shadow: 'Shadow display timing',
+            }
+        },
+        elCollapse: {
+            name: 'Collapse',
+            event: {
+                change: 'Switch the currently active panel, its type is string in accordion mode and array in other modes',
+            },
+            props: {
+                accordion: 'Whether it is in accordion mode'
+            }
+        },
+        elCollapseItem: {
+            name: 'CollapseItem',
+            props: {
+                title: 'Panel title',
+                name: 'Identifier',
+                disabled: 'Disabled',
+            }
+        },
+        elDivider: {
+            name: 'Divider',
+            props: {
+                formCreateChild: 'Set Content',
+                contentPosition: 'Set content position'
+            }
+        },
+        elTabPane: {
+            name: 'TabPane',
+            props: {
+                label: 'Title',
+                disabled: 'Disabled',
+                name: 'Identifier of the tab',
+                lazy: 'Whether the label is delayed in rendering'
+            }
+        },
+        elTabs: {
+            name: 'Tabs',
+            event: {
+                tabClick: 'Triggered when tab is selected',
+                tabChange: 'Triggered when activeName changes',
+                tabRemove: 'Triggered when the tab remove button is clicked',
+                tabAdd: 'Triggered when a new tab button is clicked',
+                edit: 'Triggered after clicking the add or remove button of the tab',
+            },
+            props: {
+                type: 'Type',
+                closable: 'Whether the label can be closed',
+                tabPosition: 'Tab position',
+                stretch: 'Whether the width of the label is self-stretching'
+            }
+        },
+        elTag: {
+            name: 'Tag',
+            props: {
+                formCreateNative: 'Whether to display title',
+                formCreateTitle: 'Title',
+                formCreateChild: 'Content',
+                type: 'Type',
+                size: 'Label size',
+                effect: 'Label theme',
+                closable: 'Whether it can be closed',
+                disableTransitions: 'Whether to disable gradient animation',
+                hit: 'Whether there is a border stroke',
+                round: 'Whether it is round',
+                color: 'Background color'
+            }
+        },
+        elTransfer: {
+            name: 'Transfer',
+            event: {
+                leftCheckChange: 'Triggered when the left list element is selected/unselected by the user',
+                rightCheckChange: 'Triggered when the right list element is selected/unselected by the user'
+            },
+            props: {
+                filterable: 'Is it searchable',
+                filterPlaceholder: 'Search box placeholder',
+                targetOrder: 'Sort strategy of list elements on the right',
+                targetOrderInfo: 'If it is original, keep the same order as the data; if it is push, the newly added elements will be ranked last; if it is unshift, the newly added elements will be ranked first',
+                titles: 'Title',
+                buttonTexts: 'Set button content',
+                props: 'Field alias of data source'
+            }
+        },
+        elTreeSelect: {
+            name: 'TreeSelect',
+            event: {
+                removeTag: 'Triggered when tag is removed in multi-select mode'
+            },
+            props: {
+                multiple: 'Whether there are multiple selections',
+                disabled: 'Disabled',
+                clearable: 'Whether the option can be cleared',
+                collapseTags: 'Whether to display the selected value as text during multi-selection',
+                multipleLimit: 'The maximum number of items that the user can select during multiple selection, if it is 0, there is no limit',
+                placeholder: 'Placeholder',
+                props: 'Options',
+                renderAfterExpand: 'Whether to render its child nodes after expanding a tree node for the first time',
+                defaultExpandAll: 'Whether to expand all nodes by default',
+                expandOnClickNode: 'Whether to expand or shrink nodes when clicking on them',
+                checkOnClickNode: 'Whether to select the node when clicking the node',
+                nodeKey: 'Each tree node is used as an attribute for unique identification, and the entire tree should be unique'
+            }
+        },
+        elImage: {
+            name: 'Image',
+            props: {
+                src: 'Image path'
+            }
+        },
+        fcEditor: {
+            name: 'Editor',
+            props: {
+                disabled: 'Disabled'
+            }
+        },
+        fcRow: {
+            name: 'Row',
+            props: {
+                gutter: 'Grid interval',
+                type: 'Flex layout mode',
+                justify: 'Horizontal arrangement under flex layout',
+                align: 'Vertical arrangement under flex layout'
+            }
+        },
+        fcTable: {
+            name: 'Table',
+            props: {
+                border: 'Whether to display border',
+                borderColor: 'Border color',
+                borderWidth: 'Border width'
+            }
+        },
+        fcTableGrid: {
+            name: 'Grid',
+        },
+        group: {
+            name: 'Subform',
+            props: {
+                disabled: 'Disabled',
+                syncDisabled: 'Whether to force synchronization of the disabled state with the subform',
+                expand: 'Set the default expansion items',
+                button: 'Whether to display the operation button',
+                sortBtn: 'Whether to display the sort button',
+                min: 'Set the minimum number of items to add',
+                max: 'Set the maximum number of items to add',
+            }
+        },
+        html: {
+            name: 'HTML',
+            props: {
+                formCreateNative: 'Whether to display title',
+                formCreateTitle: 'Title',
+                formCreateChild: 'Content',
+            }
+        },
+        input: {
+            name: 'Input',
+            event: {
+                change: 'Triggered when the value changes, when the component loses focus or the user presses Enter',
+            },
+            props: {
+                type: 'Type',
+                maxlength: 'Maximum input length',
+                minlength: 'Minimum input length',
+                placeholder: 'Placeholder',
+                clearable: 'Whether to display the clear button',
+                disabled: 'Disabled',
+                readonly: 'Readonly',
+            }
+        },
+        inputNumber: {
+            name: 'InputNumber',
+            props: {
+                precision: 'Precision of input value',
+                min: 'Set the minimum value allowed for the counter',
+                max: 'Set the maximum allowed value of the counter',
+                step: 'Step',
+                stepStrictly: 'Whether only multiples of step can be entered',
+                disabled: 'Disabled',
+                controls: 'Whether to use control buttons',
+                controlsPosition: 'Control button position',
+                placeholder: 'Placeholder'
+            }
+        },
+        password: {
+            name: 'Password',
+            event: {
+                change: 'Triggered when the value changes, when the component loses focus or the user presses Enter',
+            },
+            props: {
+                disabled: 'Disabled',
+                readonly: 'Readonly',
+                maxlength: 'Maximum input length',
+                minlength: 'Minimum input length',
+                placeholder: 'Placeholder',
+                clearable: 'Whether to display the clear button'
+            }
+        },
+        radio: {
+            name: 'Radio',
+            props: {
+                input: 'Whether to fill in',
+                disabled: 'Disabled',
+                type: 'Type',
+                textColor: 'Text color when button form is activated',
+                fill: 'Fill color and border color when the button form is activated'
+            }
+        },
+        rate: {
+            name: 'Rate',
+            props: {
+                max: 'Maximum score',
+                disabled: 'Disabled',
+                allowHalf: 'Whether to allow half selection',
+                voidColor: 'Color of the icon when not selected',
+                disabledVoidColor: 'The color of the icon when it is not selected when read-only',
+                voidIconClass: 'Class name of the icon when not selected',
+                disabledVoidIconClass: 'The class name of the icon when it is not selected when read-only',
+                showScore: 'Whether to display the current score',
+                textColor: 'Color of auxiliary text',
+                scoreTemplate: 'Score display template'
+            }
+        },
+        select: {
+            name: 'Select',
+            event: {
+                removeTag: 'Triggered when tag is removed in multi-select mode'
+            },
+            props: {
+                multiple: 'Whether there are multiple selections',
+                disabled: 'Disabled',
+                clearable: 'Whether the option can be cleared',
+                collapseTags: 'Whether to display the selected value as text during multi-selection',
+                multipleLimit: 'The maximum number of items that the user can select when multiple-selecting, if it is 0, there is no limit',
+                placeholder: 'Placeholder',
+                filterable: 'Is it searchable',
+                allowCreate: 'Whether users are allowed to create new entries',
+                noMatchText: 'Text displayed when no search conditions match',
+                noDataText: 'Text displayed when option is empty',
+                reserveKeyword: 'When multiple selections are searchable, whether to retain the current search keyword after selecting an option',
+                defaultFirstOption: 'Press Enter in the input box and select the first matching item',
+                remote: 'Whether the options are loaded remotely from the server',
+                remoteMethod: 'Custom remote search methods',
+            }
+        },
+        slider: {
+            name: 'Slider',
+            props: {
+                min: 'Minimum value',
+                max: 'Maximum value',
+                disabled: 'Disabled',
+                step: 'Step',
+                showInput: 'Whether to display the input box, it is only valid during non-range selection',
+                showInputControls: 'Whether to display the control buttons of the input box when the input box is displayed',
+                showStops: 'Whether to display discontinuities',
+                range: 'Whether it is a range selection',
+                vertical: 'Whether portrait mode',
+                height: 'Slider height, required in portrait mode'
+            }
+        },
+        space: {
+            name: 'Space',
+            props: {
+                height: 'Height',
+            }
+        },
+        subForm: {
+            name: 'Group',
+            props: {
+                disabled: 'Disabled',
+                syncDisabled: 'Whether to force synchronization of the disabled state with the subform'
+            }
+        },
+        switch: {
+            name: 'Switch',
+            props: {
+                disabled: 'Disabled',
+                width: 'Width (px)',
+                activeText: 'Text description when opening',
+                inactiveText: 'Text description when closing',
+                activeValue: 'Value when opening',
+                inactiveValue: 'Value when closed',
+                activeColor: 'Background color when opening',
+                inactiveColor: 'Background color when closed'
+            }
+        },
+        tableForm: {
+            name: 'TableForm',
+            props: {
+                disabled: 'Disabled',
+                filterEmptyColumn: 'Whether to filter empty rows',
+                max: 'Maximum number of rows to add, if 0, there is no limit',
+            }
+        },
+        tableFormColumn: {
+            name: 'TableFormColumn',
+            label: 'TableFormColumn',
+            props: {
+                label: 'Title',
+                width: 'Width',
+                color: 'Color',
+                required: 'Whether to display required asterisks',
+            }
+        },
+        text: {
+            name: 'Text',
+            props: {
+                formCreateNative: 'Whether to display title',
+                formCreateTitle: 'Title',
+                formCreateChild: 'Content'
+            }
+        },
+        textarea: {
+            name: 'Textarea',
+            event: {
+                change: 'Triggered when the value changes, when the component loses focus or the user presses Enter',
+            },
+            props: {
+                disabled: 'Disabled',
+                readonly: 'Readonly',
+                maxlength: 'Maximum input length',
+                minlength: 'Minimum input length',
+                showWordLimit: 'Whether to display word count statistics',
+                placeholder: 'Placeholder',
+                rows: 'Number of input box rows',
+                autosize: 'Whether the height is adaptive'
+            }
+        },
+        timePicker: {
+            name: 'Time',
+            props: {
+                pickerOptions: 'Options specific to the current time and date picker',
+                readonly: 'Readonly',
+                disabled: 'Disabled',
+                editable: 'Text box can be input',
+                clearable: 'Whether to display the clear button',
+                placeholder: 'Placeholder content for non-range selection',
+                startPlaceholder: 'Placeholder content for the start date when selecting the range',
+                endPlaceholder: 'Placeholder content for the start date when selecting the range',
+                isRange: 'Whether to select a time range',
+                arrowControl: 'Whether to use arrows for time selection',
+                align: 'Align'
+            }
+        },
+        tree: {
+            name: 'Tree',
+            event: {
+                nodeClick: 'Triggered when the node is clicked',
+                nodeContextmenu: 'This event will be triggered when a node is right-clicked',
+                checkChange: 'Triggered when the check box is clicked',
+                check: 'Triggered after clicking the node checkbox',
+                currentChange: 'Event triggered when the currently selected node changes',
+                nodeExpand: 'Event triggered when a node is expanded',
+                nodeCollapse: 'Event triggered when a node is closed',
+                nodeDragStart: 'Event triggered when a node starts dragging',
+                nodeDragEnter: 'Event triggered when dragging into other nodes',
+                nodeDragLeave: 'Event triggered when dragging leaves a node',
+                nodeDragOver: 'Event triggered when dragging a node',
+                nodeDragEnd: 'Event triggered when drag ends',
+                nodeDrop: 'Event triggered when drag and drop is successfully completed'
+            },
+            props: {
+                emptyText: 'Text displayed when the content is empty',
+                props: 'Options',
+                renderAfterExpand: 'Whether to render its child nodes after expanding a tree node for the first time',
+                defaultExpandAll: 'Whether to expand all nodes by default',
+                expandOnClickNode: 'Whether to expand or contract the node when clicking the node, if it is false, the node will only be expanded or contracted when the arrow icon is clicked. ',
+                checkOnClickNode: 'Whether to select the node when clicking the node',
+                autoExpandParent: 'Whether to automatically expand the parent node when expanding the child node',
+                checkStrictly: 'When the check box is displayed, whether the parent and child are strictly not related to each other should be strictly followed',
+                accordion: 'Whether to open only one sibling tree node for expansion at a time',
+                indent: 'Horizontal indent (px) between adjacent level nodes',
+                nodeKey: 'Each tree node is used as an attribute for unique identification, and the entire tree should be unique'
+            }
+        },
+        upload: {
+            name: 'Upload',
+            info: 'After a successful upload, assign the returned URL to file.url or the result to file.value for use in subsequent form submissions.',
+            event: {
+                remove: 'Triggered when a file is removed from the file list',
+                preview: 'Triggered when clicking an uploaded file in the file list',
+                error: 'Triggered when file upload fails',
+                progress: 'Triggered when file is uploaded',
+                exceed: 'Triggered when the limit is exceeded'
+            },
+            props: {
+                listType: 'Upload type',
+                multiple: 'Whether multiple selection of files is supported',
+                action: 'Upload address (required)',
+                beforeUpload: 'Triggered before uploading a file',
+                onSuccess: 'Triggered when the upload is successful',
+                beforeRemove: 'Triggered before deleting a file',
+                headers: 'Set upload request headers',
+                data: 'Extra parameters attached when uploading',
+                name: 'Uploaded file field name',
+                withCredentials: 'Support sending cookie credential information',
+                accept: 'Accept uploaded file types',
+                autoUpload: 'Whether to upload the file immediately after selecting it',
+                disabled: 'Disabled',
+                limit: 'Maximum number of uploads allowed'
+            }
+        }
+    },
+};
+
+export default En;
+

+ 881 - 0
src/components/form-create-designer/locale/zh-cn.js

@@ -0,0 +1,881 @@
+const ZhCn = {
+    name: 'zh-cn',
+    form: {
+        field: '字段 ID',
+        title: '字段名称',
+        info: '提示信息',
+        control: '联动数据',
+        labelPosition: '标签的位置',
+        labelStyle: '标签的样式',
+        labelSuffix: '标签的后缀',
+        size: '表单的尺寸',
+        event: '表单事件',
+        labelWidth: '标签的宽度',
+        hideRequiredAsterisk: '隐藏必填字段的标签旁边的红色星号',
+        showMessage: '显示校验错误信息',
+        inlineMessage: '以行内形式展示校验信息',
+        submitBtn: '是否显示表单提交按钮',
+        resetBtn: '是否显示表单重置按钮',
+        appendChild: '添加子级',
+        formMode: '表单模式',
+        formName: '表单名称',
+        componentMode: '生成组件',
+        htmlMode: '生成HTML',
+        document: '帮助文档',
+        controlDocument: '需要更详细的配置方法?请查看{doc}',
+        onSubmit: '表单提交时触发',
+        onReset: '表单重置后触发',
+        onCreated: '表单组件初始化完毕后触发',
+        onMounted: '表单组件渲染完毕后触发',
+        onReload: '表单渲染规则重载后触发',
+        onChange: '表单组件的值发生变化时触发',
+        beforeFetch: '远程数据请求发送前触发',
+    },
+    warning: {
+        name: '组件的唯一标识,用于获取和修改该组件的配置规则。通过该标识可以精确定位组件,实现对组件属性和行为的控制。',
+        field: '组件对应的字段名用于与组件的数据进行绑定。字段名需以字母开头,以确保能够正确识别。',
+        fetch: '远程数据通过远程请求加载组件的配置项。配置请求参数后,组件会自动发起请求,获取远程数据并根据返回的结果更新组件。',
+        fetchQuery: '定义请求的 GET 参数,通过 URL 传递数据。',
+        fetchData: '定义请求的 POST 参数,通过请求体传递数据。',
+        fetchDataType: '选择请求体的数据类型,确保数据格式正确。',
+        fetchParse: '请求返回后,可以通过处理函数对返回的结果进行处理,将结果转换为组件所需的数据和结构。',
+        language: '管理页面的多语言数据,在组件中配置不同语言的文本,支持一键切换语言体系,便于在多语言环境下使用和展示内容。',
+    },
+    computed: {
+        fieldUsed: '【{label}】在计算公式中被使用,请先修改对应公式',
+        fieldExist: '【{label}】字段已存在',
+        fieldEmpty: '字段名称不能为空',
+        fieldChar: '字段名称必须以字母开头',
+    },
+    validate: {
+        type: '字段类型',
+        typePlaceholder: '请选择',
+        trigger: '触发方式',
+        mode: '验证方式',
+        modes: {
+            required: '必填',
+            pattern: '正则表达式',
+            validator: '自定义验证',
+            min: '最小值',
+            max: '最大值',
+            len: '长度',
+        },
+        types: {
+            string: '文本',
+            boolean: '布尔',
+            array: '多选',
+            number: '数字',
+            integer: '整数',
+            float: '小数',
+            object: '合集',
+            date: '日期',
+            url: 'URL链接',
+            email: '邮箱地址',
+        },
+        message: '错误信息',
+        auto: '自动获取',
+        autoRequired: '请输入{title}',
+        autoMode: '请输入正确的{title}',
+        requiredPlaceholder: '请输入提示语',
+        required: '是否必填',
+        rule: '验证规则',
+    },
+    tableOptions: {
+        handle: '操作',
+        add: '添加',
+        empty1: '点击右下角',
+        empty2: '按钮添加一列',
+        rmCol: '删除当前列',
+        rmRow: '删除当前行',
+        splitRow: '拆分成行',
+        splitCol: '拆分成列',
+        mergeBottom: '向下合并',
+        mergeRight: '向右合并',
+        addTop: '添加上列',
+        addBottom: '添加下列',
+        addLeft: '添加左列',
+        addRight: '添加右列',
+        keyValue: '键值对',
+    },
+    struct: {
+        title: '编辑数据',
+        only: '【{label}】只允许添加一个',
+        errorMsg: '输入的内容语法错误',
+        configured: '已配置',
+    },
+    event: {
+        title: '设置事件',
+        create: '创建事件',
+        list: '事件列表',
+        placeholder: '请输入事件的名称',
+        saveMsg: '请先保存当前正在编辑的事件',
+        type: '类型',
+        info: '说明',
+        label: '字段',
+        inject: {
+            api: '当前表单的api',
+            rule: '当前表单的生成规则',
+            self: '组件的生成规则',
+            option: '表单的配置',
+            args: '事件的原始参数',
+        },
+				option: {
+					action: '请求链接',
+					method: '请求方式',
+					data: '请求数据',
+					filename: '文件名',
+					file: '文件',
+					headers: '请求头',
+					onError: '错误回调',
+					onProgress: '进度回调',
+					onSuccess: '成功回调',
+					withCredentials: '是否携带凭证',
+				}
+    },
+    eventInfo: {
+        blur: '失去焦点时触发',
+        focus: '获得焦点时触发',
+        change: '当绑定值变化时触发',
+        input: '在值改变时触发',
+        clear: '在点击清空按钮时触发',
+        close: '关闭组件时触发',
+        click: '点击组件时触发',
+        add: '增加时触发',
+        delete: '删除时触发',
+        visibleChange: '下拉框出现/隐藏时触发',
+        calendarChange: '在日历所选日期更改时触发',
+        panelChange: '当日期面板改变时触发',
+        open: '打开的回调',
+        opened: '打开动画结束时的回调',
+        closed: '关闭动画结束时的回调',
+        openAutoFocus: '输入焦点聚焦在内容时的回调',
+        closeAutoFocus: '输入焦点从内容失焦时的回调',
+        submit: '表单提交时触发',
+        confirm: '点击确认按钮时触发',
+        validateFail: '表单验证失败时触发',
+        hook_load: '组件规则加载后触发',
+        hook_mounted: '组件挂载后触发',
+        hook_deleted: '组件规则被移除后触发',
+        hook_watch: '组件规则发生变化后触发',
+        hook_value: '组件的值发生变化后触发',
+        hook_hidden: '组件显示状态发生变化后触发',
+    },
+    fetch: {
+        title: '设置数据源',
+        create: '创建数据源',
+        config: '请求配置',
+        action: '请求链接',
+        actionRequired: '请输入正确的链接',
+        placeholder: '请输入数据源的名称',
+        method: '请求方式',
+        data: '附带数据',
+        dataType: '数据类型',
+        headers: '请求头部',
+        query: '请求参数',
+        parse: '数据处理',
+        response: '接口返回的数据',
+        onError: '错误处理',
+        remote: '远程数据',
+        static: '静态数据',
+        optionsType: {
+            fetch: '远程数据',
+            struct: '静态数据',
+        }
+    },
+    style: {
+        width: '宽度',
+        height: '高度',
+        color: '颜色',
+        backgroundColor: '背景色',
+        margin: '外边距',
+        padding: '内边距',
+        borderRadius: '圆角',
+        border: '边框',
+        solid: '实线',
+        dashed: '虚线',
+        dotted: '点状虚线',
+        double: '双实线',
+        opacity: '透明度',
+        scale: '缩放',
+        minWidth: '最小宽',
+        minHeight: '最小高',
+        maxWidth: '最大宽',
+        maxHeight: '最大高',
+        overflow: {
+            name: '溢出',
+            visible: '可见',
+            hidden: '隐藏',
+            scroll: '滚动',
+            auto: '溢出后自动滚动',
+        },
+        shadow: {
+            name: '阴影',
+            x: 'x轴偏移量',
+            y: 'y轴偏移量',
+            vague: '模糊半径',
+            extend: '扩散半径',
+            inset: '向内',
+            external: '向外',
+            mode: '模式',
+            classic: '经典',
+            flat: '扁平',
+            solid: '立体',
+        },
+        font: {
+            name: '字体',
+            size: '大小',
+            align: '对齐方式',
+            height: '行高',
+            spacing: '字间距',
+            preview: '样式预览',
+        },
+        decoration: {
+            name: '修饰',
+            underline: '下划线',
+            'line-through': '删除线',
+            overline: '上划线',
+        },
+        weight: {
+            name: '粗细',
+            300: '细体',
+            400: '常规体',
+            500: '中黑体',
+            700: '中粗体',
+        }
+    },
+    designer: {
+        component: '组件配置',
+        id: '唯一值',
+        name: '编号',
+        type: '组件类型',
+        form: '表单配置',
+        json: '渲染规则',
+        style: '组件样式配置',
+        rule: '基础配置',
+        advanced: '高级配置',
+        props: '属性配置',
+        customProps: '自定义属性配置',
+        validate: '验证配置',
+        event: '事件配置',
+        clearWarn: '清空后将不能恢复,确定要清空吗?',
+        childEmpty: '点击右下角 \\e789  按钮添加一列',
+        dragEmpty: '拖拽左侧列表中的组件到此处',
+        unload: '确定离开当前页面吗?',
+        comList: '组件列表',
+    },
+    language: {
+        name: '国际化配置',
+        add: '新增词条',
+        batchRemove: '批量删除',
+        select: '选择多语言',
+    },
+    menu: {
+        main: '基础组件',
+        aide: '辅助组件',
+        layout: '布局组件',
+        component: '组件',
+        subform: '子表单组件',
+        tree: '大纲'
+    },
+    props: {
+        disabled: '禁用',
+        time: '时间',
+        size: '尺寸',
+        email: '邮箱',
+        number: '数字',
+        globalData: '全局数据',
+        mobile: '移动端',
+        reactive: '响应式',
+        pc: '电脑端',
+        title: '标题',
+        content: '内容',
+        collection: '合集',
+        group: '分组',
+        custom: '自定义',
+        change: '改变',
+        blur: '失去焦点',
+        preview: '预览',
+        clear: '清空',
+        cancel: '取消',
+        close: '关闭',
+        ok: '确定',
+        save: '保存',
+        refresh: '刷新',
+        submit: '提交',
+        reset: '重置',
+        copy: '复制',
+        delete: '删除',
+        hide: '隐藏',
+        show: '显示',
+        position: '位置',
+        render: '渲染',
+        large: '大',
+        default: '默认',
+        small: '小',
+        always: '常显',
+        never: '不显示',
+        hover: '悬浮',
+        click: '点击',
+        button: '按钮',
+        year: '年份',
+        month: '月份',
+        date: '日期',
+        dates: '日期多选',
+        week: '一周',
+        datetime: '日期时间',
+        'datetime-local': '日期时间',
+        datetimerange: '日期时间区间',
+        daterange: '日期区间',
+        monthrange: '月份区间',
+        left: '左对齐',
+        right: '右对齐',
+        top: '顶部',
+        text: '文字',
+        picture: '图片',
+        'picture-card': '卡片',
+        center: '居中',
+        vertical: '竖向',
+        horizontal: '横向',
+        manage: '管理',
+        key: '键名',
+        name: '名称',
+        value: '值',
+        inputData: '默认值',
+        append: '插入',
+        options: '选项数据',
+        option: '选项',
+        callback: '回调',
+        _self: '当前窗口',
+        _blank: '新的窗口',
+        _parent: '父级窗口',
+        _top: '顶级窗口',
+    },
+    com: {
+        cascader: {
+            name: '级联选择器',
+            event: {
+                expandChange: '当展开节点发生变化时触发',
+                removeTag: '在多选模式下,移除Tag时触发'
+            },
+            props: {
+                props: '配置选项',
+                placeholder: '输入框占位文本',
+                disabled: '是否禁用',
+                clearable: '是否支持清空选项',
+                showAllLevels: '输入框中是否显示选中值的完整路径',
+                collapseTags: '多选模式下是否折叠Tag',
+                collapseTagsTooltip: '当鼠标悬停于折叠标签的文本时,是否显示所有选中的标签',
+                separator: '选项分隔符',
+                filterable: '该选项是否可以被搜索',
+                tagType: '标签类型',
+            },
+            propsOpt: {
+                multiple: '是否多选',
+                expandTrigger: '次级菜单的展开方式',
+                checkStrictly: '是否严格的遵守父子节点不互相关联',
+                emitPath: '在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组',
+                value: '指定选项的值为选项对象的某个属性值',
+                label: '指定选项标签为选项对象的某个属性值',
+                children: '指定选项的子选项为选项对象的某个属性值',
+                disabled: '指定选项的禁用为选项对象的某个属性值',
+                leaf: '指定选项的叶子节点的标志位为选项对象的某个属性值',
+            }
+        },
+        checkbox: {
+            name: '多选框',
+            props: {
+                input: '是否可以填写',
+                type: '按钮类型',
+                disabled: '是否禁用',
+                min: '可被勾选的最小数量',
+                max: '可被勾选的最大数量',
+                textColor: '当按钮为活跃状态时的字体颜色',
+                fill: '当按钮为活跃状态时的边框和背景颜色'
+            }
+        },
+        col: {
+            name: '布局格子',
+            props: {
+                span: '栅格占据的列数',
+                offset: '栅格左侧的间隔格数',
+                push: '栅格向右移动格数',
+                pull: '栅格向左移动格数'
+            }
+        },
+        colorPicker: {
+            name: '颜色选择器',
+            event: {
+                activeChange: '面板中当前显示的颜色发生改变时触发'
+            },
+            props: {
+                disabled: '是否禁用',
+                showAlpha: '是否支持透明度选择',
+                colorFormat: '颜色的格式',
+                predefine: '预定义颜色',
+            }
+        },
+        datePicker: {
+            name: '日期',
+            props: {
+                pickerOptions: '当前时间日期选择器特有的选项',
+                readonly: '完全只读',
+                disabled: '禁用',
+                type: '显示类型',
+                editable: '文本框可输入',
+                clearable: '是否显示清除按钮',
+                placeholder: '非范围选择时的占位内容',
+                startPlaceholder: '范围选择时开始日期的占位内容',
+                endPlaceholder: '范围选择时结束日期的占位内容',
+                format: '显示在输入框中的格式',
+                align: '对齐方式',
+                rangeSeparator: '选择范围时的分隔符',
+                unlinkPanels: '在范围选择器里取消两个日期面板之间的联动',
+            }
+        },
+        dateRange: {
+            name: '日期区间',
+        },
+        timeRange: {
+            name: '时间区间',
+        },
+        elAlert: {
+            name: '提示',
+            description: '说明文字',
+            props: {
+                title: '标题',
+                type: '主题',
+                description: '辅助性文字',
+                closable: '是否可关闭',
+                center: '文字是否居中',
+                closeText: '关闭按钮自定义文本',
+                showIcon: '是否显示图标',
+                effect: '选择提供的主题'
+            }
+        },
+        elButton: {
+            name: '按钮',
+            props: {
+                formCreateChild: '内容',
+                size: '尺寸',
+                type: '类型',
+                plain: '是否朴素按钮',
+                round: '是否圆角按钮',
+                circle: '是否圆形按钮',
+                loading: '是否加载中状态',
+                disabled: '是否禁用状态',
+            }
+        },
+        elCard: {
+            name: '卡片',
+            props: {
+                header: '标题',
+                shadow: '阴影显示时机',
+            }
+        },
+        elCollapse: {
+            name: '折叠面板',
+            event: {
+                change: '切换当前活动面板,在手风琴模式下其类型是string,在其他模式下是array',
+            },
+            props: {
+                accordion: '是否手风琴模式'
+            }
+        },
+        elCollapseItem: {
+            name: '面板',
+            props: {
+                title: '面板标题',
+                name: '唯一标志符',
+                disabled: '是否禁用',
+            }
+        },
+        elDivider: {
+            name: '分割线',
+            props: {
+                formCreateChild: '设置分割线文案',
+                contentPosition: '设置分割线文案的位置'
+            }
+        },
+        elTabPane: {
+            name: '选项卡',
+            props: {
+                label: '选项卡标题',
+                disabled: '是否禁用',
+                name: '选项卡的标识符',
+                lazy: '标签是否延迟渲染'
+            }
+        },
+        elTabs: {
+            name: '标签页',
+            event: {
+                tabClick: 'tab 被选中时触发',
+                tabChange: 'activeName 改变时触发',
+                tabRemove: '点击 tab 移除按钮时触发',
+                tabAdd: '点击 tab 新增按钮时触发',
+                edit: '点击 tab 的新增或移除按钮后触发',
+            },
+            props: {
+                type: '风格类型',
+                closable: '标签是否可关闭',
+                tabPosition: '选项卡所在位置',
+                stretch: '标签的宽度是否自撑开'
+            }
+        },
+        elTag: {
+            name: '标签',
+            props: {
+                formCreateNative: '是否显示标题',
+                formCreateTitle: '标题',
+                formCreateChild: '标签内容',
+                type: '标签的类型',
+                size: '标签的尺寸',
+                effect: '标签的主题',
+                closable: '是否可关闭',
+                disableTransitions: '是否禁用渐变动画',
+                hit: '是否有边框描边',
+                round: '是否为圆形',
+                color: '背景色'
+            }
+        },
+        elTransfer: {
+            name: '穿梭框',
+            event: {
+                leftCheckChange: '左侧列表元素被用户选中 / 取消选中时触发',
+                rightCheckChange: '右侧列表元素被用户选中 / 取消选中时触发'
+            },
+            props: {
+                filterable: '是否可搜索',
+                filterPlaceholder: '搜索框占位符',
+                targetOrder: '右侧列表元素的排序策略',
+                targetOrderInfo: '若为 original,则保持与数据相同的顺序;若为 push,则新加入的元素排在最后;若为 unshift,则新加入的元素排在最前',
+                titles: '自定义列表标题',
+                buttonTexts: '自定义按钮文案',
+                props: '数据源的字段别名'
+            }
+        },
+        elTreeSelect: {
+            name: '树形选择',
+            event: {
+                removeTag: '多选模式下移除tag时触发'
+            },
+            props: {
+                multiple: '是否多选',
+                disabled: '是否禁用',
+                clearable: '是否可以清空选项',
+                collapseTags: '多选时是否将选中值按文字的形式展示',
+                multipleLimit: '多选时用户最多可以选择的项目数,为 0 则不限制',
+                placeholder: '占位符',
+                props: '配置选项',
+                renderAfterExpand: '是否在第一次展开某个树节点后才渲染其子节点',
+                defaultExpandAll: '是否默认展开所有节点',
+                expandOnClickNode: '是否在点击节点的时候展开或者收缩节点',
+                checkOnClickNode: '是否在点击节点的时候选中节点',
+                nodeKey: '每个树节点用来作为唯一标识的属性,整棵树应该是唯一的'
+            }
+        },
+        elImage: {
+            name: '图片',
+            props: {
+                src: '图片链接'
+            }
+        },
+        fcEditor: {
+            name: '富文本框',
+            props: {
+                disabled: '是否禁用'
+            }
+        },
+        fcRow: {
+            name: '栅格布局',
+            props: {
+                gutter: '栅格间隔',
+                type: 'flex布局模式',
+                justify: 'flex布局下的水平排列方式',
+                align: 'flex布局下的垂直排列方式'
+            }
+        },
+        fcTable: {
+            name: '表格布局',
+            props: {
+                border: '是否显示边框',
+                borderColor: '边框颜色',
+                borderWidth: '边框宽度'
+            }
+        },
+        fcTableGrid: {
+            name: '格子',
+        },
+        group: {
+            name: '子表单',
+            props: {
+                disabled: '是否禁用',
+                syncDisabled: '是否与子表单强制同步禁用状态',
+                expand: '设置默认展开几项',
+                button: '是否显示操作按钮',
+                sortBtn: '是否显示排序按钮',
+                min: '设置最小添加几项',
+                max: '设置最多添加几项',
+            }
+        },
+        html: {
+            name: 'HTML',
+            props: {
+                formCreateNative: '是否显示标题',
+                formCreateTitle: '标题',
+                formCreateChild: '内容',
+            }
+        },
+        input: {
+            name: '输入框',
+            event: {
+                change: '当值改变时,当组件失去焦点或用户按Enter时触发',
+            },
+            props: {
+                type: '类型',
+                maxlength: '最大输入长度',
+                minlength: '最小输入长度',
+                placeholder: '输入框占位文本',
+                clearable: '是否显示清除按钮',
+                disabled: '是否禁用',
+                readonly: '是否只读',
+            }
+        },
+        inputNumber: {
+            name: '计数器',
+            props: {
+                precision: '数值精度',
+                min: '设置计数器允许的最小值',
+                max: '设置计数器允许的最大值',
+                step: '计数器步长',
+                stepStrictly: '是否只能输入 step 的倍数',
+                disabled: '是否禁用计数器',
+                controls: '是否使用控制按钮',
+                controlsPosition: '控制按钮位置',
+                placeholder: '输入框占位文本'
+            }
+        },
+        password: {
+            name: '密码输入框',
+            event: {
+                change: '当值改变时,当组件失去焦点或用户按Enter时触发',
+            },
+            props: {
+                disabled: '是否禁用',
+                readonly: '是否只读',
+                maxlength: '最大输入长度',
+                minlength: '最小输入长度',
+                placeholder: '输入框占位文本',
+                clearable: '是否显示清除按钮'
+            }
+        },
+        radio: {
+            name: '单选框',
+            props: {
+                input: '是否可以填写',
+                disabled: '是否禁用',
+                type: '按钮形式',
+                textColor: '按钮形式激活时的文本颜色',
+                fill: '按钮形式激活时的填充色和边框色'
+            }
+        },
+        rate: {
+            name: '评分',
+            props: {
+                max: '最大分值',
+                disabled: '是否禁用',
+                allowHalf: '是否允许半选',
+                voidColor: '未选中时图标的颜色',
+                disabledVoidColor: '只读时未选中时图标的颜色',
+                voidIconClass: '未选中时图标的类名',
+                disabledVoidIconClass: '只读时未选中时图标的类名',
+                showScore: '是否显示当前分数',
+                textColor: '辅助文字的颜色',
+                scoreTemplate: '分数显示模板'
+            }
+        },
+        select: {
+            name: '选择器',
+            event: {
+                removeTag: '多选模式下移除tag时触发'
+            },
+            props: {
+
+                multiple: '是否多选',
+                disabled: '是否禁用',
+                clearable: '是否可以清空选项',
+                collapseTags: '多选时是否将选中值按文字的形式展示',
+                multipleLimit: '多选时用户最多可以选择的项目数,为 0 则不限制',
+                placeholder: '占位符',
+                filterable: '是否可搜索',
+                allowCreate: '是否允许用户创建新条目',
+                noMatchText: '搜索条件无匹配时显示的文字',
+                noDataText: '选项为空时显示的文字',
+                reserveKeyword: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词',
+                defaultFirstOption: '在输入框按下回车,选择第一个匹配项',
+                remote: '其中的选项是否从服务器远程加载',
+                remoteMethod: '自定义远程搜索方法',
+            }
+        },
+        slider: {
+            name: '滑块',
+            props: {
+                min: '最小值',
+                max: '最大值',
+                disabled: '是否禁用',
+                step: '步长',
+                showInput: '是否显示输入框,仅在非范围选择时有效',
+                showInputControls: '在显示输入框的情况下,是否显示输入框的控制按钮',
+                showStops: '是否显示间断点',
+                range: '是否为范围选择',
+                vertical: '是否竖向模式',
+                height: 'Slider 高度,竖向模式时必填'
+            }
+        },
+        space: {
+            name: '间距',
+            props: {
+                height: '高度',
+            }
+        },
+        subForm: {
+            name: '分组',
+            props: {
+                disabled: '是否禁用',
+                syncDisabled: '是否与子表单强制同步禁用状态'
+            }
+        },
+        switch: {
+            name: '开关',
+            props: {
+                disabled: '是否禁用',
+                width: '宽度(px)',
+                activeText: '打开时的文字描述',
+                inactiveText: '关闭时的文字描述',
+                activeValue: '打开时的值',
+                inactiveValue: '关闭时的值',
+                activeColor: '打开时的背景色',
+                inactiveColor: '关闭时的背景色'
+            }
+        },
+        tableForm: {
+            name: '表格表单',
+            props: {
+                disabled: '是否禁用',
+                filterEmptyColumn: '是否过滤空行的数据',
+                max: '最多添加几行,为 0 则不限制',
+            }
+        },
+        tableFormColumn: {
+            name: '表格格子',
+            label: '自定义名称',
+            props: {
+                label: '标题',
+                width: '宽度',
+                color: '颜色',
+                required: '是否显示必填星号',
+            }
+        },
+        text: {
+            name: '文字',
+            props: {
+                formCreateNative: '是否显示标题',
+                formCreateTitle: '标题',
+                formCreateChild: '内容'
+            }
+        },
+        textarea: {
+            name: '多行输入框',
+            event: {
+                change: '当值改变时,当组件失去焦点或用户按Enter时触发',
+            },
+            props: {
+                disabled: '是否禁用',
+                readonly: '是否只读',
+                maxlength: '最大输入长度',
+                minlength: '最小输入长度',
+                showWordLimit: '是否显示统计字数',
+                placeholder: '输入框占位文本',
+                rows: '输入框行数',
+                autosize: '高度是否自适应'
+            }
+        },
+        timePicker: {
+            name: '时间',
+            props: {
+                pickerOptions: '当前时间日期选择器特有的选项',
+                readonly: '完全只读',
+                disabled: '禁用',
+                editable: '文本框可输入',
+                clearable: '是否显示清除按钮',
+                placeholder: '非范围选择时的占位内容',
+                startPlaceholder: '范围选择时开始日期的占位内容',
+                endPlaceholder: '范围选择时开始日期的占位内容',
+                isRange: '是否为时间范围选择',
+                arrowControl: '是否使用箭头进行时间选择',
+                align: '对齐方式'
+            }
+        },
+        tree: {
+            name: '树形控件',
+            event: {
+                nodeClick: '当节点被点击的时候触发',
+                nodeContextmenu: '当某一节点被鼠标右键点击时会触发该事件',
+                checkChange: '当复选框被点击的时候触发',
+                check: '点击节点复选框之后触发',
+                currentChange: '当前选中节点变化时触发的事件',
+                nodeExpand: '节点被展开时触发的事件',
+                nodeCollapse: '节点被关闭时触发的事件',
+                nodeDragStart: '节点开始拖拽时触发的事件',
+                nodeDragEnter: '拖拽进入其他节点时触发的事件',
+                nodeDragLeave: '拖拽离开某个节点时触发的事件',
+                nodeDragOver: '在拖拽节点时触发的事件(类似浏览器的 mouseover 事件)',
+                nodeDragEnd: '拖拽结束时(可能未成功)触发的事件',
+                nodeDrop: '拖拽成功完成时触发的事件'
+            },
+            props: {
+                emptyText: '内容为空的时候展示的文本',
+                props: '配置选项',
+                renderAfterExpand: '是否在第一次展开某个树节点后才渲染其子节点',
+                defaultExpandAll: '是否默认展开所有节点',
+                expandOnClickNode: '是否在点击节点的时候展开或者收缩节点,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。',
+                checkOnClickNode: '是否在点击节点的时候选中节点',
+                autoExpandParent: '展开子节点的时候是否自动展开父节点',
+                checkStrictly: '在显示复选框的情况下,是否严格的遵循父子不互相关联的做法',
+                accordion: '是否每次只打开一个同级树节点展开',
+                indent: '相邻级节点间的水平缩进(px)',
+                nodeKey: '每个树节点用来作为唯一标识的属性,整棵树应该是唯一的'
+            }
+        },
+        upload: {
+            name: '上传',
+            info: '上传成功后,将接口返回的 URL 赋值给 file.url,或将返回结果赋值给 file.value,以便在后续的表单提交时获取这些数据。',
+            event: {
+                remove: '文件列表移除文件时触发',
+                preview: '点击文件列表中已上传的文件时触发',
+                error: '文件上传失败时触发',
+                progress: '文件上传时触发',
+                exceed:'当超出限制时触发'
+            },
+            props: {
+                listType: '上传类型',
+                multiple: '是否支持多选文件',
+                action: '上传的地址(必填)',
+                beforeUpload: '上传文件之前触发',
+                onSuccess: '上传成功时触发',
+                beforeRemove: '删除文件之前触发',
+								httpRequest: '自定义XHR拦截器',
+                headers: '设置上传的请求头部',
+                data: '上传时附带的额外参数',
+                name: '上传的文件字段名',
+                withCredentials: '支持发送 cookie 凭证信息',
+                accept: '接受上传的文件类型',
+                autoUpload: '是否在选取文件后立即进行上传',
+                disabled: '是否禁用',
+                limit: '最大允许上传个数'
+            }
+        }
+    },
+};
+
+export default ZhCn;
+

+ 880 - 0
src/components/form-create-designer/locale/zh-tw.js

@@ -0,0 +1,880 @@
+const ZhTw = {
+    name: 'zh-tw',
+    form: {
+        field: '欄位 ID',
+        title: '欄位名稱',
+        info: '提示資訊',
+        control: '聯動資料',
+        labelPosition: '標籤的位置',
+        labelStyle: '標籤的樣式',
+        labelSuffix: '標籤的後綴',
+        size: '表單的尺寸',
+        event: '表單事件',
+        labelWidth: '標籤的寬度',
+        hideRequiredAsterisk: '隱藏必填欄位的標籤旁邊的紅色星號',
+        showMessage: '顯示校驗錯誤資訊',
+        inlineMessage: '以行內形式展示校驗資訊',
+        submitBtn: '是否顯示表單提交按鈕',
+        resetBtn: '是否顯示表單重置按鈕',
+        appendChild: '添加子級',
+        formMode: '表單模式',
+        formName: '表單名稱',
+        componentMode: '生成元件',
+        htmlMode: '生成HTML',
+        document: '幫助文件',
+        controlDocument: '需要更詳細的配置方法?請查看{doc}',
+        onSubmit: '表單提交時觸發',
+        onReset: '表單重置後觸發',
+        onCreated: '表單元件初始化完畢後觸發',
+        onMounted: '表單元件渲染完畢後觸發',
+        onReload: '表單渲染規則重載後觸發',
+        onChange: '表單元件的值發生變化時觸發',
+        beforeFetch: '遠端資料請求發送前觸發',
+    },
+    warning: {
+        name: '元件的唯一標識,用於獲取和修改該元件的配置規則。通過該標識可以精確定位元件,實現對元件屬性和行為的控制。',
+        field: '元件對應的欄位名用於與元件的資料進行綁定。欄位名需以字母開頭,以確保能夠正確識別。',
+        fetch: '遠端資料通過遠端請求載入元件的配置項。配置請求參數後,元件會自動發起請求,獲取遠端資料並根據返回的結果更新元件。',
+        fetchQuery: '定義請求的 GET 參數,通過 URL 傳遞資料。',
+        fetchData: '定義請求的 POST 參數,通過請求體傳遞資料。',
+        fetchDataType: '選擇請求體的資料類型,確保資料格式正確。',
+        fetchParse: '請求返回後,可以通過處理函數對返回的結果進行處理,將結果轉換為元件所需的資料和結構。',
+        language: '管理頁面的多語言資料,在元件中配置不同語言的文字,支援一鍵切換語言體系,便於在多語言環境下使用和展示內容。',
+    },
+    computed: {
+        fieldUsed: '【{label}】在計算公式中被使用,請先修改對應公式',
+        fieldExist: '【{label}】欄位已存在',
+        fieldEmpty: '欄位名稱不能為空',
+        fieldChar: '欄位名稱必須以字母開頭',
+    },
+    validate: {
+        type: '欄位類型',
+        typePlaceholder: '請選擇',
+        trigger: '觸發方式',
+        mode: '驗證方式',
+        modes: {
+            required: '必填',
+            pattern: '正則表達式',
+            validator: '自定義驗證',
+            min: '最小值',
+            max: '最大值',
+            len: '長度',
+        },
+        types: {
+            string: '文字',
+            boolean: '布林',
+            array: '多選',
+            number: '數字',
+            integer: '整數',
+            float: '小數',
+            object: '合集',
+            date: '日期',
+            url: 'URL連結',
+            email: '郵箱地址',
+        },
+        message: '錯誤資訊',
+        auto: '自動獲取',
+        autoRequired: '請輸入{title}',
+        autoMode: '請輸入正確的{title}',
+        requiredPlaceholder: '請輸入提示語',
+        required: '是否必填',
+        rule: '驗證規則',
+    },
+    tableOptions: {
+        handle: '操作',
+        add: '添加',
+        empty1: '點擊右下角',
+        empty2: '按鈕添加一列',
+        rmCol: '刪除當前列',
+        rmRow: '刪除當前行',
+        splitRow: '拆分成行',
+        splitCol: '拆分成列',
+        mergeBottom: '向下合併',
+        mergeRight: '向右合併',
+        addTop: '添加上列',
+        addBottom: '添加下列',
+        addLeft: '添加左列',
+        addRight: '添加右列',
+        keyValue: '鍵值對',
+    },
+    struct: {
+        title: '編輯資料',
+        only: '【{label}】只允許添加一個',
+        errorMsg: '輸入的內容語法錯誤',
+        configured: '已配置',
+    },
+    event: {
+        title: '設置事件',
+        create: '創建事件',
+        list: '事件列表',
+        placeholder: '請輸入事件的名稱',
+        saveMsg: '請先保存當前正在編輯的事件',
+        type: '類型',
+        info: '說明',
+        label: '欄位',
+        inject: {
+            api: '當前表單的api',
+            rule: '當前表單的生成規則',
+            self: '元件的生成規則',
+            option: '表單的配置',
+            args: '事件的原始參數',
+        },
+        option: {
+            action: '請求連結',
+            method: '請求方式',
+            data: '請求資料',
+            filename: '檔案名',
+            file: '檔案',
+            headers: '請求頭',
+            onError: '錯誤回調',
+            onProgress: '進度回調',
+            onSuccess: '成功回調',
+            withCredentials: '是否攜帶憑證',
+        }
+    },
+    eventInfo: {
+        blur: '失去焦點時觸發',
+        focus: '獲得焦點時觸發',
+        change: '當綁定值變化時觸發',
+        input: '在值改變時觸發',
+        clear: '在點擊清空按鈕時觸發',
+        close: '關閉元件時觸發',
+        click: '點擊元件時觸發',
+        add: '增加時觸發',
+        delete: '刪除時觸發',
+        visibleChange: '下拉框出現/隱藏時觸發',
+        calendarChange: '在日曆所選日期更改時觸發',
+        panelChange: '當日期面板改變時觸發',
+        open: '打開的回調',
+        opened: '打開動畫結束時的回調',
+        closed: '關閉動畫結束時的回調',
+        openAutoFocus: '輸入焦點聚焦在內容時的回調',
+        closeAutoFocus: '輸入焦點從內容失焦時的回調',
+        submit: '表單提交時觸發',
+        confirm: '點擊確認按鈕時觸發',
+        validateFail: '表單驗證失敗時觸發',
+        hook_load: '元件規則載入後觸發',
+        hook_mounted: '元件掛載後觸發',
+        hook_deleted: '元件規則被移除後觸發',
+        hook_watch: '元件規則發生變化後觸發',
+        hook_value: '元件的值發生變化後觸發',
+        hook_hidden: '元件顯示狀態發生變化後觸發',
+    },
+    fetch: {
+        title: '設置資料源',
+        create: '創建資料源',
+        config: '請求配置',
+        action: '請求連結',
+        actionRequired: '請輸入正確的連結',
+        placeholder: '請輸入資料源的名稱',
+        method: '請求方式',
+        data: '附帶資料',
+        dataType: '資料類型',
+        headers: '請求頭部',
+        query: '請求參數',
+        parse: '資料處理',
+        response: '介面返回的資料',
+        onError: '錯誤處理',
+        remote: '遠端資料',
+        static: '靜態資料',
+        optionsType: {
+            fetch: '遠端資料',
+            struct: '靜態資料',
+        }
+    },
+    style: {
+        width: '寬度',
+        height: '高度',
+        color: '顏色',
+        backgroundColor: '背景色',
+        margin: '外邊距',
+        padding: '內邊距',
+        borderRadius: '圓角',
+        border: '邊框',
+        solid: '實線',
+        dashed: '虛線',
+        dotted: '點狀虛線',
+        double: '雙實線',
+        opacity: '透明度',
+        scale: '縮放',
+        minWidth: '最小寬',
+        minHeight: '最小高',
+        maxWidth: '最大寬',
+        maxHeight: '最大高',
+        overflow: {
+            name: '溢出',
+            visible: '可見',
+            hidden: '隱藏',
+            scroll: '滾動',
+            auto: '溢出後自動滾動',
+        },
+        shadow: {
+            name: '陰影',
+            x: 'x軸偏移量',
+            y: 'y軸偏移量',
+            vague: '模糊半徑',
+            extend: '擴散半徑',
+            inset: '向內',
+            external: '向外',
+            mode: '模式',
+            classic: '經典',
+            flat: '扁平',
+            solid: '立體',
+        },
+        font: {
+            name: '字體',
+            size: '大小',
+            align: '對齊方式',
+            height: '行高',
+            spacing: '字間距',
+            preview: '樣式預覽',
+        },
+        decoration: {
+            name: '修飾',
+            underline: '下劃線',
+            'line-through': '刪除線',
+            overline: '上劃線',
+        },
+        weight: {
+            name: '粗細',
+            300: '細體',
+            400: '常規體',
+            500: '中黑體',
+            700: '中粗體',
+        }
+    },
+    designer: {
+        component: '元件配置',
+        id: '唯一值',
+        name: '編號',
+        type: '元件類型',
+        form: '表單配置',
+        json: '渲染規則',
+        style: '元件樣式配置',
+        rule: '基礎配置',
+        advanced: '高級配置',
+        props: '屬性配置',
+        customProps: '自訂屬性配置',
+        validate: '驗證配置',
+        event: '事件配置',
+        clearWarn: '清空後將不能恢復,確定要清空嗎?',
+        childEmpty: '點擊右下角 \\e789  按鈕添加一列',
+        dragEmpty: '拖拽左側列表中的元件到此處',
+        unload: '確定離開當前頁面嗎?',
+        comList: '元件列表',
+    },
+    language: {
+        name: '國際化配置',
+        add: '新增詞條',
+        batchRemove: '批量刪除',
+        select: '選擇多語言',
+    },
+    menu: {
+        main: '基礎元件',
+        aide: '輔助元件',
+        layout: '佈局元件',
+        component: '元件',
+        subform: '子表單元件',
+        tree: '大綱'
+    },
+    props: {
+        disabled: '禁用',
+        time: '時間',
+        size: '尺寸',
+        email: '郵箱',
+        number: '數字',
+        globalData: '全域資料',
+        mobile: '行動端',
+        reactive: '響應式',
+        pc: '電腦端',
+        title: '標題',
+        content: '內容',
+        collection: '合集',
+        group: '分組',
+        custom: '自定義',
+        change: '改變',
+        blur: '失去焦點',
+        preview: '預覽',
+        clear: '清空',
+        cancel: '取消',
+        close: '關閉',
+        ok: '確定',
+        save: '保存',
+        refresh: '刷新',
+        submit: '提交',
+        reset: '重置',
+        copy: '複製',
+        delete: '刪除',
+        hide: '隱藏',
+        show: '顯示',
+        position: '位置',
+        render: '渲染',
+        large: '大',
+        default: '默認',
+        small: '小',
+        always: '常顯',
+        never: '不顯示',
+        hover: '懸浮',
+        click: '點擊',
+        button: '按鈕',
+        year: '年份',
+        month: '月份',
+        date: '日期',
+        dates: '日期多選',
+        week: '一週',
+        datetime: '日期時間',
+        'datetime-local': '日期時間',
+        datetimerange: '日期時間區間',
+        daterange: '日期區間',
+        monthrange: '月份區間',
+        left: '左對齊',
+        right: '右對齊',
+        top: '頂部',
+        text: '文字',
+        picture: '圖片',
+        'picture-card': '卡片',
+        center: '居中',
+        vertical: '豎向',
+        horizontal: '橫向',
+        manage: '管理',
+        key: '鍵名',
+        name: '名稱',
+        value: '值',
+        inputData: '默認值',
+        append: '插入',
+        options: '選項資料',
+        option: '選項',
+        callback: '回調',
+        _self: '當前視窗',
+        _blank: '新的視窗',
+        _parent: '父級視窗',
+        _top: '頂級視窗',
+    },
+    com: {
+        cascader: {
+            name: '級聯選擇器',
+            event: {
+                expandChange: '當展開節點發生變化時觸發',
+                removeTag: '在多選模式下,移除Tag時觸發'
+            },
+            props: {
+                props: '配置選項',
+                placeholder: '輸入框佔位文字',
+                disabled: '是否禁用',
+                clearable: '是否支援清空選項',
+                showAllLevels: '輸入框中是否顯示選中值的完整路徑',
+                collapseTags: '多選模式下是否折疊Tag',
+                collapseTagsTooltip: '當滑鼠懸停於折疊標籤的文字時,是否顯示所有選中的標籤',
+                separator: '選項分隔符',
+                filterable: '該選項是否可以被搜尋',
+                tagType: '標籤類型',
+            },
+            propsOpt: {
+                multiple: '是否多選',
+                expandTrigger: '次級選單的展開方式',
+                checkStrictly: '是否嚴格的遵守父子節點不互相關聯',
+                emitPath: '在選中節點改變時,是否返回由該節點所在的各級選單的值所組成的陣列',
+                value: '指定選項的值為選項物件的某個屬性值',
+                label: '指定選項標籤為選項物件的某個屬性值',
+                children: '指定選項的子選項為選項物件的某個屬性值',
+                disabled: '指定選項的禁用為選項物件的某個屬性值',
+                leaf: '指定選項的葉子節點的標誌位為選項物件的某個屬性值',
+            }
+        },
+        checkbox: {
+            name: '多選框',
+            props: {
+                input: '是否可以填寫',
+                type: '按鈕類型',
+                disabled: '是否禁用',
+                min: '可被勾選的最小數量',
+                max: '可被勾選的最大數量',
+                textColor: '當按鈕為活躍狀態時的字體顏色',
+                fill: '當按鈕為活躍狀態時的邊框和背景顏色'
+            }
+        },
+        col: {
+            name: '佈局格子',
+            props: {
+                span: '柵格佔據的列數',
+                offset: '柵格左側的間隔格數',
+                push: '柵格向右移動格數',
+                pull: '柵格向左移動格數'
+            }
+        },
+        colorPicker: {
+            name: '顏色選擇器',
+            event: {
+                activeChange: '面板中當前顯示的顏色發生改變時觸發'
+            },
+            props: {
+                disabled: '是否禁用',
+                showAlpha: '是否支援透明度選擇',
+                colorFormat: '顏色的格式',
+                predefine: '預定義顏色',
+            }
+        },
+        datePicker: {
+            name: '日期',
+            props: {
+                pickerOptions: '當前時間日期選擇器特有的選項',
+                readonly: '完全唯讀',
+                disabled: '禁用',
+                type: '顯示類型',
+                editable: '文字框可輸入',
+                clearable: '是否顯示清除按鈕',
+                placeholder: '非範圍選擇時的佔位內容',
+                startPlaceholder: '範圍選擇時開始日期的佔位內容',
+                endPlaceholder: '範圍選擇時結束日期的佔位內容',
+                format: '顯示在輸入框中的格式',
+                align: '對齊方式',
+                rangeSeparator: '選擇範圍時的分隔符',
+                unlinkPanels: '在範圍選擇器裡取消兩個日期面板之間的聯動',
+            }
+        },
+        dateRange: {
+            name: '日期區間',
+        },
+        timeRange: {
+            name: '時間區間',
+        },
+        elAlert: {
+            name: '提示',
+            description: '說明文字',
+            props: {
+                title: '標題',
+                type: '主題',
+                description: '輔助性文字',
+                closable: '是否可關閉',
+                center: '文字是否居中',
+                closeText: '關閉按鈕自定義文字',
+                showIcon: '是否顯示圖示',
+                effect: '選擇提供的主題'
+            }
+        },
+        elButton: {
+            name: '按鈕',
+            props: {
+                formCreateChild: '內容',
+                size: '尺寸',
+                type: '類型',
+                plain: '是否樸素按鈕',
+                round: '是否圓角按鈕',
+                circle: '是否圓形按鈕',
+                loading: '是否載入中狀態',
+                disabled: '是否禁用狀態',
+            }
+        },
+        elCard: {
+            name: '卡片',
+            props: {
+                header: '標題',
+                shadow: '陰影顯示時機',
+            }
+        },
+        elCollapse: {
+            name: '摺疊面板',
+            event: {
+                change: '切換當前活動面板,在手風琴模式下其類型是string,在其他模式下是array',
+            },
+            props: {
+                accordion: '是否手風琴模式'
+            }
+        },
+        elCollapseItem: {
+            name: '面板',
+            props: {
+                title: '面板標題',
+                name: '唯一標誌符',
+                disabled: '是否禁用',
+            }
+        },
+        elDivider: {
+            name: '分割線',
+            props: {
+                formCreateChild: '設置分割線文案',
+                contentPosition: '設置分割線文案的位置'
+            }
+        },
+        elTabPane: {
+            name: '選項卡',
+            props: {
+                label: '選項卡標題',
+                disabled: '是否禁用',
+                name: '選項卡的標識符',
+                lazy: '標籤是否延遲渲染'
+            }
+        },
+        elTabs: {
+            name: '標籤頁',
+            event: {
+                tabClick: 'tab 被選中時觸發',
+                tabChange: 'activeName 改變時觸發',
+                tabRemove: '點擊 tab 移除按鈕時觸發',
+                tabAdd: '點擊 tab 新增按鈕時觸發',
+                edit: '點擊 tab 的新增或移除按鈕後觸發',
+            },
+            props: {
+                type: '風格類型',
+                closable: '標籤是否可關閉',
+                tabPosition: '選項卡所在位置',
+                stretch: '標籤的寬度是否自撐開'
+            }
+        },
+        elTag: {
+            name: '標籤',
+            props: {
+                formCreateNative: '是否顯示標題',
+                formCreateTitle: '標題',
+                formCreateChild: '標籤內容',
+                type: '標籤的類型',
+                size: '標籤的尺寸',
+                effect: '標籤的主題',
+                closable: '是否可關閉',
+                disableTransitions: '是否禁用漸變動畫',
+                hit: '是否有邊框描邊',
+                round: '是否為圓形',
+                color: '背景色'
+            }
+        },
+        elTransfer: {
+            name: '穿梭框',
+            event: {
+                leftCheckChange: '左側列表元素被使用者選中 / 取消選中時觸發',
+                rightCheckChange: '右側列表元素被使用者選中 / 取消選中時觸發'
+            },
+            props: {
+                filterable: '是否可搜尋',
+                filterPlaceholder: '搜尋框佔位符',
+                targetOrder: '右側列表元素的排序策略',
+                targetOrderInfo: '若為 original,則保持與資料相同的順序;若為 push,則新加入的元素排在最後;若為 unshift,則新加入的元素排在最前',
+                titles: '自定義列表標題',
+                buttonTexts: '自定義按鈕文案',
+                props: '資料源的欄位別名'
+            }
+        },
+        elTreeSelect: {
+            name: '樹形選擇',
+            event: {
+                removeTag: '多選模式下移除tag時觸發'
+            },
+            props: {
+                multiple: '是否多選',
+                disabled: '是否禁用',
+                clearable: '是否可以清空選項',
+                collapseTags: '多選時是否將選中值按文字的形式展示',
+                multipleLimit: '多選時使用者最多可以選擇的項目數,為 0 則不限制',
+                placeholder: '佔位符',
+                props: '配置選項',
+                renderAfterExpand: '是否在第一次展開某個樹節點後才渲染其子節點',
+                defaultExpandAll: '是否默認展開所有節點',
+                expandOnClickNode: '是否在點擊節點的時候展開或者收縮節點',
+                checkOnClickNode: '是否在點擊節點的時候選中節點',
+                nodeKey: '每個樹節點用來作為唯一標識的屬性,整棵樹應該是唯一的'
+            }
+        },
+        elImage: {
+            name: '圖片',
+            props: {
+                src: '圖片連結'
+            }
+        },
+        fcEditor: {
+            name: '富文字框',
+            props: {
+                disabled: '是否禁用'
+            }
+        },
+        fcRow: {
+            name: '柵格佈局',
+            props: {
+                gutter: '柵格間隔',
+                type: 'flex佈局模式',
+                justify: 'flex佈局下的水平排列方式',
+                align: 'flex佈局下的垂直排列方式'
+            }
+        },
+        fcTable: {
+            name: '表格佈局',
+            props: {
+                border: '是否顯示邊框',
+                borderColor: '邊框顏色',
+                borderWidth: '邊框寬度'
+            }
+        },
+        fcTableGrid: {
+            name: '格子',
+        },
+        group: {
+            name: '子表單',
+            props: {
+                disabled: '是否禁用',
+                syncDisabled: '是否與子表單強制同步禁用狀態',
+                expand: '設置默認展開幾項',
+                button: '是否顯示操作按鈕',
+                sortBtn: '是否顯示排序按鈕',
+                min: '設置最小添加幾項',
+                max: '設置最多添加幾項',
+            }
+        },
+        html: {
+            name: 'HTML',
+            props: {
+                formCreateNative: '是否顯示標題',
+                formCreateTitle: '標題',
+                formCreateChild: '內容',
+            }
+        },
+        input: {
+            name: '輸入框',
+            event: {
+                change: '當值改變時,當元件失去焦點或使用者按Enter時觸發',
+            },
+            props: {
+                type: '類型',
+                maxlength: '最大輸入長度',
+                minlength: '最小輸入長度',
+                placeholder: '輸入框佔位文字',
+                clearable: '是否顯示清除按鈕',
+                disabled: '是否禁用',
+                readonly: '是否唯讀',
+            }
+        },
+        inputNumber: {
+            name: '計數器',
+            props: {
+                precision: '數值精度',
+                min: '設置計數器允許的最小值',
+                max: '設置計數器允許的最大值',
+                step: '計數器步長',
+                stepStrictly: '是否只能輸入 step 的倍數',
+                disabled: '是否禁用計數器',
+                controls: '是否使用控制按鈕',
+                controlsPosition: '控制按鈕位置',
+                placeholder: '輸入框佔位文字'
+            }
+        },
+        password: {
+            name: '密碼輸入框',
+            event: {
+                change: '當值改變時,當元件失去焦點或使用者按Enter時觸發',
+            },
+            props: {
+                disabled: '是否禁用',
+                readonly: '是否唯讀',
+                maxlength: '最大輸入長度',
+                minlength: '最小輸入長度',
+                placeholder: '輸入框佔位文字',
+                clearable: '是否顯示清除按鈕'
+            }
+        },
+        radio: {
+            name: '單選框',
+            props: {
+                input: '是否可以填寫',
+                disabled: '是否禁用',
+                type: '按鈕形式',
+                textColor: '按鈕形式啟用時的文字顏色',
+                fill: '按鈕形式啟用時的填充色和邊框色'
+            }
+        },
+        rate: {
+            name: '評分',
+            props: {
+                max: '最大分值',
+                disabled: '是否禁用',
+                allowHalf: '是否允許半選',
+                voidColor: '未選中時圖示的顏色',
+                disabledVoidColor: '唯讀時未選中時圖示的顏色',
+                voidIconClass: '未選中時圖示的類名',
+                disabledVoidIconClass: '唯讀時未選中時圖示的類名',
+                showScore: '是否顯示當前分數',
+                textColor: '輔助文字的顏色',
+                scoreTemplate: '分數顯示模板'
+            }
+        },
+        select: {
+            name: '選擇器',
+            event: {
+                removeTag: '多選模式下移除tag時觸發'
+            },
+            props: {
+
+                multiple: '是否多選',
+                disabled: '是否禁用',
+                clearable: '是否可以清空選項',
+                collapseTags: '多選時是否將選中值按文字的形式展示',
+                multipleLimit: '多選時使用者最多可以選擇的項目數,為 0 則不限制',
+                placeholder: '佔位符',
+                filterable: '是否可搜尋',
+                allowCreate: '是否允許使用者創建新條目',
+                noMatchText: '搜尋條件無匹配時顯示的文字',
+                noDataText: '選項為空時顯示的文字',
+                reserveKeyword: '多選且可搜尋時,是否在選中一個選項後保留當前的搜尋關鍵詞',
+                defaultFirstOption: '在輸入框按下回車,選擇第一個匹配項',
+                remote: '其中的選項是否從伺服器遠端載入',
+                remoteMethod: '自定義遠端搜尋方法',
+            }
+        },
+        slider: {
+            name: '滑塊',
+            props: {
+                min: '最小值',
+                max: '最大值',
+                disabled: '是否禁用',
+                step: '步長',
+                showInput: '是否顯示輸入框,僅在非範圍選擇時有效',
+                showInputControls: '在顯示輸入框的情況下,是否顯示輸入框的控制按鈕',
+                showStops: '是否顯示間斷點',
+                range: '是否為範圍選擇',
+                vertical: '是否豎向模式',
+                height: 'Slider 高度,豎向模式時必填'
+            }
+        },
+        space: {
+            name: '間距',
+            props: {
+                height: '高度',
+            }
+        },
+        subForm: {
+            name: '分組',
+            props: {
+                disabled: '是否禁用',
+                syncDisabled: '是否與子表單強制同步禁用狀態'
+            }
+        },
+        switch: {
+            name: '開關',
+            props: {
+                disabled: '是否禁用',
+                width: '寬度(px)',
+                activeText: '打開時的文字描述',
+                inactiveText: '關閉時的文字描述',
+                activeValue: '打開時的值',
+                inactiveValue: '關閉時的值',
+                activeColor: '打開時的背景色',
+                inactiveColor: '關閉時的背景色'
+            }
+        },
+        tableForm: {
+            name: '表格表單',
+            props: {
+                disabled: '是否禁用',
+                filterEmptyColumn: '是否過濾空行的資料',
+                max: '最多添加幾行,為 0 則不限制',
+            }
+        },
+        tableFormColumn: {
+            name: '表格格子',
+            label: '自定義名稱',
+            props: {
+                label: '標題',
+                width: '寬度',
+                color: '顏色',
+                required: '是否顯示必填星號',
+            }
+        },
+        text: {
+            name: '文字',
+            props: {
+                formCreateNative: '是否顯示標題',
+                formCreateTitle: '標題',
+                formCreateChild: '內容'
+            }
+        },
+        textarea: {
+            name: '多行輸入框',
+            event: {
+                change: '當值改變時,當元件失去焦點或使用者按Enter時觸發',
+            },
+            props: {
+                disabled: '是否禁用',
+                readonly: '是否唯讀',
+                maxlength: '最大輸入長度',
+                minlength: '最小輸入長度',
+                showWordLimit: '是否顯示統計字數',
+                placeholder: '輸入框佔位文字',
+                rows: '輸入框行數',
+                autosize: '高度是否自適應'
+            }
+        },
+        timePicker: {
+            name: '時間',
+            props: {
+                pickerOptions: '當前時間日期選擇器特有的選項',
+                readonly: '完全唯讀',
+                disabled: '禁用',
+                editable: '文字框可輸入',
+                clearable: '是否顯示清除按鈕',
+                placeholder: '非範圍選擇時的佔位內容',
+                startPlaceholder: '範圍選擇時開始日期的佔位內容',
+                endPlaceholder: '範圍選擇時開始日期的佔位內容',
+                isRange: '是否為時間範圍選擇',
+                arrowControl: '是否使用箭頭進行時間選擇',
+                align: '對齊方式'
+            }
+        },
+        tree: {
+            name: '樹形控制',
+            event: {
+                nodeClick: '當節點被點擊的時候觸發',
+                nodeContextmenu: '當某一節點被滑鼠右鍵點擊時會觸發該事件',
+                checkChange: '當複選框被點擊的時候觸發',
+                check: '點擊節點複選框之後觸發',
+                currentChange: '當前選中節點變化時觸發的事件',
+                nodeExpand: '節點被展開時觸發的事件',
+                nodeCollapse: '節點被關閉時觸發的事件',
+                nodeDragStart: '節點開始拖拽時觸發的事件',
+                nodeDragEnter: '拖拽進入其他節點時觸發的事件',
+                nodeDragLeave: '拖拽離開某個節點時觸發的事件',
+                nodeDragOver: '在拖拽節點時觸發的事件(類似瀏覽器的 mouseover 事件)',
+                nodeDragEnd: '拖拽結束時(可能未成功)觸發的事件',
+                nodeDrop: '拖拽成功完成時觸發的事件'
+            },
+            props: {
+                emptyText: '內容為空的時候展示的文字',
+                props: '配置選項',
+                renderAfterExpand: '是否在第一次展開某個樹節點後才渲染其子節點',
+                defaultExpandAll: '是否默認展開所有節點',
+                expandOnClickNode: '是否在點擊節點的時候展開或者收縮節點,如果為 false,則只有點箭頭圖示的時候才會展開或者收縮節點。',
+                checkOnClickNode: '是否在點擊節點的時候選中節點',
+                autoExpandParent: '展開子節點的時候是否自動展開父節點',
+                checkStrictly: '在顯示複選框的情況下,是否嚴格的遵循父子不互相關聯的做法',
+                accordion: '是否每次只打開一個同級樹節點展開',
+                indent: '相鄰級節點間的水平縮進(px)',
+                nodeKey: '每個樹節點用來作為唯一標識的屬性,整棵樹應該是唯一的'
+            }
+        },
+        upload: {
+            name: '上傳',
+            info: '上傳成功後,將介面返回的 URL 賦值給 file.url,或將返回結果賦值給 file.value,以便在後續的表單提交時獲取這些資料。',
+            event: {
+                remove: '檔案列表移除檔案時觸發',
+                preview: '點擊檔案列表中已上傳的檔案時觸發',
+                error: '檔案上傳失敗時觸發',
+                progress: '檔案上傳時觸發',
+                exceed:'當超出限制時觸發'
+            },
+            props: {
+                listType: '上傳類型',
+                multiple: '是否支援多選檔案',
+                action: '上傳的地址(必填)',
+                beforeUpload: '上傳檔案之前觸發',
+                onSuccess: '上傳成功時觸發',
+                beforeRemove: '刪除檔案之前觸發',
+                httpRequest: '自定義XHR攔截器',
+                headers: '設置上傳的請求頭部',
+                data: '上傳時附帶的額外參數',
+                name: '上傳的檔案欄位名',
+                withCredentials: '支援發送 cookie 憑證資訊',
+                accept: '接受上傳的檔案類型',
+                autoUpload: '是否在選取檔案後立即進行上傳',
+                disabled: '是否禁用',
+                limit: '最大允許上傳個數'
+            }
+        }
+    }
+}
+
+export default ZhTw;

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio