浏览代码

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

yanglzh 1 月之前
父节点
当前提交
c56ee0499a
共有 100 个文件被更改,包括 12323 次插入0 次删除
  1. 11 0
      package.json
  2. 377 0
      src/api/assist/index.ts
  3. 227 0
      src/api/assist/type.ts
  4. 38 0
      src/api/common/index.ts
  5. 8 0
      src/api/common/type.ts
  6. 45 0
      src/api/flow/flowDemo.ts
  7. 127 0
      src/api/flow/flowForm.ts
  8. 143 0
      src/api/flow/flowModel.ts
  9. 94 0
      src/api/system/report/complaint-resolve-history.ts
  10. 25 0
      src/api/system/report/complaints.ts
  11. 8 0
      src/api/system/report/feedback.ts
  12. 22 0
      src/api/system/report/statistics.ts
  13. 140 0
      src/api/system/report/type.ts
  14. 356 0
      src/components/assistant/ComponentLibrary.vue
  15. 212 0
      src/components/assistant/DashboardDesigner.vue
  16. 118 0
      src/components/assistant/DashboardViewer.vue
  17. 710 0
      src/components/assistant/DraggableCard.vue
  18. 134 0
      src/components/assistant/ViewerCard.vue
  19. 76 0
      src/components/assistant/types.ts
  20. 49 0
      src/components/form-create-designer/components/DragBox.vue
  21. 201 0
      src/components/form-create-designer/components/DragTool.vue
  22. 546 0
      src/components/form-create-designer/components/EventConfig.vue
  23. 206 0
      src/components/form-create-designer/components/FcDesigner.vue
  24. 263 0
      src/components/form-create-designer/components/FetchConfig.vue
  25. 153 0
      src/components/form-create-designer/components/FieldInput.vue
  26. 304 0
      src/components/form-create-designer/components/FnConfig.vue
  27. 251 0
      src/components/form-create-designer/components/FnEditor.vue
  28. 103 0
      src/components/form-create-designer/components/FnInput.vue
  29. 125 0
      src/components/form-create-designer/components/HtmlEditor.vue
  30. 93 0
      src/components/form-create-designer/components/JsonPreview.vue
  31. 72 0
      src/components/form-create-designer/components/PropsInput.vue
  32. 75 0
      src/components/form-create-designer/components/Required.vue
  33. 25 0
      src/components/form-create-designer/components/Row.vue
  34. 142 0
      src/components/form-create-designer/components/Struct.vue
  35. 121 0
      src/components/form-create-designer/components/StructEditor.vue
  36. 162 0
      src/components/form-create-designer/components/TableOptions.vue
  37. 166 0
      src/components/form-create-designer/components/TreeOptions.vue
  38. 140 0
      src/components/form-create-designer/components/TypeSelect.vue
  39. 244 0
      src/components/form-create-designer/components/Validate.vue
  40. 88 0
      src/components/form-create-designer/components/ValueInput.vue
  41. 45 0
      src/components/form-create-designer/components/Warning.vue
  42. 165 0
      src/components/form-create-designer/components/language/LanguageConfig.vue
  43. 188 0
      src/components/form-create-designer/components/language/LanguageInput.vue
  44. 242 0
      src/components/form-create-designer/components/style/BorderInput.vue
  45. 166 0
      src/components/form-create-designer/components/style/BoxSizeInput.vue
  46. 269 0
      src/components/form-create-designer/components/style/BoxSpaceInput.vue
  47. 60 0
      src/components/form-create-designer/components/style/ColorInput.vue
  48. 118 0
      src/components/form-create-designer/components/style/ConfigItem.vue
  49. 174 0
      src/components/form-create-designer/components/style/FontInput.vue
  50. 164 0
      src/components/form-create-designer/components/style/RadiusInput.vue
  51. 329 0
      src/components/form-create-designer/components/style/ShadowContent.vue
  52. 93 0
      src/components/form-create-designer/components/style/ShadowInput.vue
  53. 118 0
      src/components/form-create-designer/components/style/SizeInput.vue
  54. 263 0
      src/components/form-create-designer/components/style/StyleConfig.vue
  55. 210 0
      src/components/form-create-designer/components/table/Table.vue
  56. 658 0
      src/components/form-create-designer/components/table/TableView.vue
  57. 390 0
      src/components/form-create-designer/components/tableForm/TableForm.vue
  58. 101 0
      src/components/form-create-designer/components/tableForm/TableFormColumnView.vue
  59. 45 0
      src/components/form-create-designer/components/tableForm/TableFormView.vue
  60. 43 0
      src/components/form-create-designer/config/base/field.js
  61. 116 0
      src/components/form-create-designer/config/base/form.js
  62. 26 0
      src/components/form-create-designer/config/base/style.js
  63. 15 0
      src/components/form-create-designer/config/base/validate.js
  64. 68 0
      src/components/form-create-designer/config/index.js
  65. 24 0
      src/components/form-create-designer/config/menu.js
  66. 45 0
      src/components/form-create-designer/config/rule/alert.js
  67. 49 0
      src/components/form-create-designer/config/rule/button.js
  68. 40 0
      src/components/form-create-designer/config/rule/card.js
  69. 121 0
      src/components/form-create-designer/config/rule/cascader.js
  70. 68 0
      src/components/form-create-designer/config/rule/checkbox.js
  71. 86 0
      src/components/form-create-designer/config/rule/col.js
  72. 30 0
      src/components/form-create-designer/config/rule/collapse.js
  73. 36 0
      src/components/form-create-designer/config/rule/collapseItem.js
  74. 53 0
      src/components/form-create-designer/config/rule/color.js
  75. 70 0
      src/components/form-create-designer/config/rule/date.js
  76. 64 0
      src/components/form-create-designer/config/rule/dateRange.js
  77. 31 0
      src/components/form-create-designer/config/rule/divider.js
  78. 31 0
      src/components/form-create-designer/config/rule/editor.js
  79. 53 0
      src/components/form-create-designer/config/rule/group.js
  80. 52 0
      src/components/form-create-designer/config/rule/html.js
  81. 32 0
      src/components/form-create-designer/config/rule/image.js
  82. 62 0
      src/components/form-create-designer/config/rule/input.js
  83. 49 0
      src/components/form-create-designer/config/rule/number.js
  84. 52 0
      src/components/form-create-designer/config/rule/password.js
  85. 43 0
      src/components/form-create-designer/config/rule/radio.js
  86. 44 0
      src/components/form-create-designer/config/rule/rate.js
  87. 46 0
      src/components/form-create-designer/config/rule/row.js
  88. 70 0
      src/components/form-create-designer/config/rule/select.js
  89. 53 0
      src/components/form-create-designer/config/rule/slider.js
  90. 44 0
      src/components/form-create-designer/config/rule/space.js
  91. 47 0
      src/components/form-create-designer/config/rule/subForm.js
  92. 46 0
      src/components/form-create-designer/config/rule/switch.js
  93. 29 0
      src/components/form-create-designer/config/rule/tabPane.js
  94. 35 0
      src/components/form-create-designer/config/rule/table.js
  95. 79 0
      src/components/form-create-designer/config/rule/tableForm.js
  96. 43 0
      src/components/form-create-designer/config/rule/tableFormColumn.js
  97. 38 0
      src/components/form-create-designer/config/rule/tabs.js
  98. 79 0
      src/components/form-create-designer/config/rule/tag.js
  99. 50 0
      src/components/form-create-designer/config/rule/text.js
  100. 63 0
      src/components/form-create-designer/config/rule/textarea.js

+ 11 - 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",
@@ -40,10 +46,12 @@
     "element-plus": "2.2.28",
     "event-source-polyfill": "1.0.31",
     "html2canvas": "^1.4.1",
+    "htmlparser2": "^10.0.0",
     "js-cookie": "3.0.5",
     "jsplumb": "2.15.6",
     "jsrsasign": "10.8.6",
     "loadsh": "0.0.4",
+    "markdown-it": "^14.1.0",
     "mitt": "3.0.0",
     "monaco-editor": "^0.52.2",
     "nprogress": "0.2.0",
@@ -65,6 +73,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,7 +83,9 @@
     "xlsx-with-styles": "0.17.2"
   },
   "devDependencies": {
+    "@types/downloadjs": "^1.4.6",
     "@types/js-cookie": "3.0.6",
+    "@types/markdown-it": "^14.1.2",
     "@types/node": "17.0.21",
     "@types/nprogress": "0.2.0",
     "@types/sortablejs": "1.10.7",

+ 377 - 0
src/api/assist/index.ts

@@ -0,0 +1,377 @@
+import {
+	ChatRequest,
+	ChatResponse,
+	ErrorResponse,
+	LmConfigListParams,
+	LmConfigAddReq,
+	LmConfigEditReq,
+	LmConfigStatusReq,
+	LmConfigDeleteParams,
+	LmConfigGetParams,
+	SessionMessagesListParams,
+	SessionMessagesSaveReq,
+	LmDashboard,
+	Prompt,
+	PromptListParams,
+	PromptAddReq,
+	PromptEditReq,
+	PromptDeleteParams,
+	PromptGetParams,
+} from '/@/api/assist/type'
+import { get, post, del, put } from '/@/utils/request'
+import getOrigin from '/@/utils/origin'
+import { getToken } from '/@/utils/auth'
+
+export default {
+	model: {
+		// 大语言模型配置列表
+		getList: (params?: LmConfigListParams) => get('/ai/lmconfig/list', params),
+		// 获取大语言模型配置信息
+		detail: (params: LmConfigGetParams) => get('/ai/lmconfig/get', params),
+		// 添加大语言模型配置
+		add: (data: LmConfigAddReq) => post('/ai/lmconfig/add', data),
+		// 修改大语言模型配置
+		edit: (data: LmConfigEditReq) => put('/ai/lmconfig/edit', data),
+		// 删除大语言模型配置
+		del: (params: LmConfigDeleteParams) => del('/ai/lmconfig/delete', params),
+		// 设置大语言模型配置状态
+		setStatus: (data: LmConfigStatusReq) => put('/ai/lmconfig/status', data),
+	},
+
+	dashboard: {
+		// 获取仪表盘列表
+		list: (params: any) => get('/ai/lmdashboards/list', params),
+		// 获取仪表盘详情
+		detail: (id: number) => get('/ai/lmdashboards/get', { id }),
+		// 添加仪表盘
+		add: (data: Omit<LmDashboard, 'id'>) => post('/ai/lmdashboards/add', data),
+		// 编辑仪表盘
+		edit: (data: LmDashboard) => put('/ai/lmdashboards/edit', data),
+		// 删除仪表盘
+		del: (ids: number[]) => del('/ai/lmdashboards/delete', { ids }),
+	},
+
+	session: {
+		list: (params: any) => get('/ai/lmsessions/list', params),
+		// 添加大模型会话
+		add: (title: string): Promise<{ id: number }> => post('/ai/lmsessions/add', { title }),
+		// 删除大模型会话
+		del: (ids: number[]) => del('/ai/lmsessions/delete', { ids }),
+
+		edit: (id: number, title: string): Promise<void> => put('/ai/lmsessions/edit', { sessionId: id, title }),
+
+		message: {
+			// 获取会话消息列表
+			list: (params: SessionMessagesListParams) => get('/ai/lmsessions/messages/list', params),
+			// 保存会话消息
+			save: (data: SessionMessagesSaveReq) => post('/ai/lmsessions/messages/save', data),
+
+			bookmark: (sessionId: number, messageId: number, like: boolean) =>
+				put('/ai/lmsessions/messages/like', {
+					sessionId,
+					messageId,
+					like,
+				}),
+			bookmark_list: (param: { keyWord?: string; pageNum?: number; pageSize?: number } = {}) => get('/ai/lmsessions/messages/like/list', param),
+		},
+	},
+
+
+	chat: {
+		prompt: {
+			// Prompt列表
+			list: (params?: PromptListParams) => get('/ai/lmprompt/list', params),
+			// 获取Prompt详情
+			detail: (params: PromptGetParams) => get('/ai/lmprompt/get', params),
+			// 添加Prompt
+			add: (data: PromptAddReq) => post('/ai/lmprompt/add', data),
+			// 编辑Prompt
+			edit: (data: PromptEditReq) => put('/ai/lmprompt/edit', data),
+			// 删除Prompt
+			del: (params: PromptDeleteParams) => del('/ai/lmprompt/delete', params),
+		},
+		struct: (uuid: string, refresh?: boolean) => post('/ai/chat/structdata', { uuid,refresh }),
+		sse: (data: {
+			chatRequest: ChatRequest
+			// eslint-disable-next-line no-unused-vars
+			onReceive: (resp: ChatResponse) => void
+
+			// eslint-disable-next-line no-unused-vars
+			onComplete?: (error: Error | undefined) => void
+		}) => {
+			const { chatRequest, onReceive, onComplete } = data
+
+			//FIXME 需要抹掉
+			chatRequest.modelMcpId = 1 as unknown as number[]
+			chatRequest['UserId'] = 10
+
+			//FIXME 后端没有处理id的能力
+			chatRequest.message.map((value,index)=>{
+				value.id = index
+			})
+
+			// 构建SSE URL
+			const baseURL = getOrigin()
+			const url = `${baseURL}/ai/chat`
+
+			// 使用fetch API实现SSE POST请求(EventSource不支持POST和自定义headers)
+			const controller = new AbortController()
+
+			// 使用fetch API实现SSE POST请求
+			const startSSE = async () => {
+				try {
+					const headers: Record<string, string> = {
+						'Content-Type': 'application/json',
+						Accept: 'text/event-stream',
+						'Cache-Control': 'no-cache',
+					}
+
+					// 添加认证token
+					const token = getToken()
+					if (token) {
+						headers['Authorization'] = `Bearer ${token}`
+					}
+
+					const response = await fetch(url, {
+						method: 'POST',
+						headers,
+						body: JSON.stringify(chatRequest),
+						signal: controller.signal,
+					}).catch((e: Error) => e)
+
+					if (response instanceof Error) {
+						throw new Error('无法连接到服务器')
+					}
+
+					if (!response.ok) {
+						throw new Error(`HTTP error! status: ${response.status}`)
+					}
+
+					const reader = response.body?.getReader()
+					const decoder = new TextDecoder()
+
+					if (!reader) {
+						throw new Error('Response body is not readable')
+					}
+
+					// 读取SSE流
+					let currentEvent = ''
+					let buffer = ''
+
+					while (true) {
+						const { done, value } = await reader.read()
+
+						if (done) {
+							break
+						}
+
+						buffer += decoder.decode(value, { stream: true })
+
+						// 按双换行符分割事件块
+						const eventBlocks = buffer.split('\n\n')
+
+						// 保留最后一个可能不完整的事件块
+						buffer = eventBlocks.pop() || ''
+
+						for (const eventBlock of eventBlocks) {
+							if (!eventBlock.trim()) continue
+
+							const lines = eventBlock.split('\n')
+							let eventType = ''
+							let dataLines: string[] = []
+
+							// 解析事件块中的每一行
+							for (const line of lines) {
+								const trimmedLine = line.trim()
+
+								if (trimmedLine.startsWith('event:')) {
+									eventType = trimmedLine.substring(6).trim()
+								} else if (trimmedLine.startsWith('data:')) {
+									// 提取data后的内容,保留原始格式(包括可能的空格)
+									const dataContent = line.substring(line.indexOf('data:') + 5)
+									dataLines.push(dataContent)
+								}
+							}
+
+							// 如果有事件类型,更新当前事件
+							if (eventType) {
+								currentEvent = eventType
+							}
+
+							// 如果有数据行,处理数据
+							if (dataLines.length > 0) {
+								// 将多行data合并,用换行符连接
+								const data = dataLines.join('\n')
+
+								// 根据事件类型处理数据
+								switch (currentEvent) {
+									case 'message': {
+										const messageResponse: ChatResponse = {
+											type: 'message',
+											message: data,
+										}
+										onReceive(messageResponse)
+										break
+									}
+
+									case 'toolcall': {
+										const tools = JSON.parse(data)
+										const toolcallResponse: ChatResponse = {
+											type: 'toolcall',
+											request: {
+												id: tools['tool_call_id'],
+												name: tools['name'],
+												data: tools['arguments'],
+											},
+										}
+										onReceive(toolcallResponse)
+										break
+									}
+
+									case 'structdata': {
+										const structDataResponse: ChatResponse = {
+											type: 'structdata',
+											uuid: data,
+										}
+										onReceive(structDataResponse)
+										break
+									}
+
+									case 'toolres': {
+										const tools = JSON.parse(data)
+										const toolresResponse: ChatResponse = {
+											type: 'toolres',
+											response: {
+												id: tools['tool_call_id'],
+												name: tools['name'],
+												data: tools['response'],
+											},
+										}
+										onReceive(toolresResponse)
+										break
+									}
+
+									case 'error': {
+										const errorResponse: ErrorResponse = {
+											type: 'error',
+											error: data,
+										}
+										onReceive(errorResponse)
+										break
+									}
+
+									case 'meta': {
+										break
+									}
+
+									default: {
+										// 如果没有明确的事件类型,默认作为消息处理
+										const defaultResponse: ChatResponse = {
+											type: 'message',
+											message: data,
+										}
+										onReceive(defaultResponse)
+										break
+									}
+								}
+							}
+						}
+
+						// 处理buffer中剩余的可能完整的单行事件
+						const remainingLines = buffer.split('\n')
+						const completeLines = remainingLines.slice(0, -1)
+						buffer = remainingLines[remainingLines.length - 1] || ''
+
+						for (const line of completeLines) {
+							const trimmedLine = line.trim()
+
+							if (trimmedLine === '') {
+								continue
+							}
+
+							if (trimmedLine.startsWith('event:')) {
+								currentEvent = trimmedLine.substring(6).trim()
+								continue
+							}
+
+							if (trimmedLine.startsWith('data:')) {
+								const data = line.substring(line.indexOf('data:') + 5)
+
+								// 根据事件类型处理数据
+								switch (currentEvent) {
+									case 'message': {
+										const messageResponse: ChatResponse = {
+											type: 'message',
+											message: data,
+										}
+										onReceive(messageResponse)
+										break
+									}
+
+									case 'toolcall': {
+										try {
+											const tools = JSON.parse(data)
+											const toolcallResponse: ChatResponse = {
+												type: 'toolcall',
+												request: {
+													name: tools['name'],
+													data: tools['arguments'],
+												},
+											}
+											onReceive(toolcallResponse)
+										} catch (error) {
+											console.error('解析toolcall数据失败:', error, '原始数据:', data)
+										}
+										break
+									}
+
+									case 'toolres': {
+										try {
+											const tools = JSON.parse(data)
+											const toolresResponse: ChatResponse = {
+												type: 'toolres',
+												response: {
+													name: tools['name'],
+													data: tools['response'],
+												},
+											}
+											onReceive(toolresResponse)
+										} catch (error) {
+											console.error('解析toolres数据失败:', error, '原始数据:', data)
+										}
+										break
+									}
+
+									default: {
+										// 如果没有明确的事件类型,默认作为消息处理
+										const defaultResponse: ChatResponse = {
+											type: 'message',
+											message: data,
+										}
+										onReceive(defaultResponse)
+										break
+									}
+								}
+							}
+						}
+					}
+
+					onComplete?.(undefined)
+				} catch (error: any) {
+					if (error.name !== 'AbortError') {
+						console.error('SSE连接错误:', error)
+					}
+					onComplete?.(error)
+				}
+			}
+
+			// 启动SSE连接
+			startSSE()
+
+			// 返回关闭连接的函数
+			return () => {
+				controller.abort()
+			}
+		}
+	},
+}

+ 227 - 0
src/api/assist/type.ts

@@ -0,0 +1,227 @@
+/**
+ * {
+ *     "modelClassId": 41,
+ *     "modelMcpId": 56,
+ *     "message": [
+ *         {
+ *             "role": "user",
+ *             "timestamp": 1719242380731,
+ *             "id": "88",
+ *             "content": "分析一下最近10条登录日志"
+ *         }
+ *     ],
+ *     "UserId": 10
+ * }
+ */
+import { UploadFile } from '/@/api/common/type'
+
+// 消息类型定义
+export type Message = {
+	id: number
+	role: 'user' | 'assistant' | 'system' | 'meta' | 'tool'
+	//仅markdown渲染器需要
+	render_content: string
+	//实际传给AI的内容
+	content: string
+	timestamp: number
+
+	//仅role为tool时需要
+	name?: string
+	tool_call_id?: string
+
+	//仅role为assistant时需要
+	like?: boolean
+	tool_calls?: FunctionCall[]
+
+	//仅role为user时需要
+	files?: Array<UploadFile> | undefined
+}
+
+export type FunctionCall = {
+	id: string
+	type: 'function'
+	function: {
+		name: string
+		arguments: string
+	}
+}
+
+export type ChatRequest = {
+	session_id: number
+	modelClassId?: number
+	modelEmbeddingId?: number
+	modelMcpId?: number[]
+	message: Message[]
+}
+
+export type ChatResponseType = 'message' | 'toolcall' | 'toolres' | 'error' | 'datamsg' | 'structdata'
+
+export type ChatResponseBase<T = ChatResponseType> = {
+	type: T
+}
+
+export type Text = ChatResponseBase<'message'> & {
+	message: string
+}
+
+export type ToolCallRequest = ChatResponseBase<'toolcall'> & {
+	request: {
+		id: string
+		name: string
+		data: string
+	}
+}
+
+export type ToolCallResponse = ChatResponseBase<'toolres'> & {
+	response: {
+		id: string
+		name: string
+		data: string
+	}
+}
+
+export type ErrorResponse = ChatResponseBase<'error'> & {
+	error: string
+}
+
+export type MetaResponse = ChatResponseBase<'meta'> & {
+	meta: string
+}
+
+export type DataResponse = ChatResponseBase<'datamsg'> & {
+	data: string
+}
+
+export type StructDataResponse = ChatResponseBase<'structdata'> & {
+	uuid: string
+}
+
+export type ChatResponse = Text | ToolCallRequest | ToolCallResponse | ErrorResponse | DataResponse | StructDataResponse
+
+export type ModelType = 'embedding' | 'chat'
+// 大语言模型配置相关类型定义
+
+// 大语言模型配置列表查询参数
+export type LmConfigListParams = {
+	keyWord?: string // 搜索关键字
+	dateRange?: string[] // 日期范围
+	OrderBy?: string // 排序
+	pageNum?: number // 分页号码,默认1
+	pageSize?: number // 分页数量,最大500,默认10
+	modelClass?: string // 模型分类
+	modelName?: string // 模型名称
+	modelType?: ModelType // 模型类型
+	status?: string // 是否启用
+	createdAt?: string // 创建时间
+}
+
+// 大语言模型配置基础信息
+export type LmConfigInfo = {
+	id?: number
+	modelClass?: string // 模型分类
+	modelName?: string // 模型名称
+	apiKey?: string // API密钥
+	baseUrl?: string // 基础URL
+	modelType?: ModelType // 模型类型
+	isCallFun?: boolean // 是否调用函数
+	maxToken?: number // 最大令牌数
+	status: boolean // 是否启用
+	createdAt?: string // 创建时间
+	updatedAt?: string // 更新时间
+	createdBy?: number // 创建者ID
+	updatedBy?: number // 更新者ID
+	createdUser?: any // 创建用户信息
+	actionBtn?: any // 操作按钮
+	[key: string]: any // 允许其他字段
+}
+
+// 大语言模型配置添加请求
+export type LmConfigAddReq = Omit<LmConfigInfo, 'id' | 'createdAt' | 'updatedAt'>
+
+// 大语言模型配置编辑请求
+export type LmConfigEditReq = LmConfigInfo & {
+	id: number // 编辑时ID必须
+}
+
+// 大语言模型配置状态设置请求
+export type LmConfigStatusReq = {
+	id: number
+	status: string
+}
+
+// 删除请求参数
+export type LmConfigDeleteParams = {
+	ids: number[]
+}
+
+// 获取单个配置参数
+export type LmConfigGetParams = {
+	id: number
+}
+
+export type LmSession = {
+	session_id: number
+	title: string
+}
+
+// 会话消息列表查询参数
+export type SessionMessagesListParams = {
+	sessionId: number
+}
+
+// 保存会话消息请求
+export type SessionMessagesSaveReq = {
+	sessionId: number
+	messages: Message[]
+}
+
+export type LmDashboard = {
+	id: number
+	title: string
+	data: string
+	remark?: string // 备注
+	createdAt?: string // 创建时间
+	updatedAt?: string // 更新时间
+	createdBy?: number // 创建者ID
+	updatedBy?: number // 更新者ID
+}
+
+export type Prompt = {
+	id: number
+	title: string
+	prompt: string
+	placeholder?: string
+	createdAt?: string // 创建时间
+	updatedAt?: string // 更新时间
+	createdBy?: number // 创建者ID
+	updatedBy?: number // 更新者ID
+}
+
+// Prompt列表查询参数
+export type PromptListParams = {
+	keyWord?: string // 搜索关键字
+	dateRange?: string[] // 日期范围
+	OrderBy?: string // 排序
+	pageNum?: number // 分页号码,默认1
+	pageSize?: number // 分页数量,最大500,默认10
+	title?: string // 标题
+	createdAt?: string // 创建时间
+}
+
+// Prompt添加请求
+export type PromptAddReq = Omit<Prompt, 'id' | 'createdAt' | 'updatedAt'>
+
+// Prompt编辑请求
+export type PromptEditReq = Prompt & {
+	id: number // 编辑时ID必须
+}
+
+// Prompt删除请求参数
+export type PromptDeleteParams = {
+	ids: number[]
+}
+
+// 获取单个Prompt参数
+export type PromptGetParams = {
+	id: number
+}

+ 38 - 0
src/api/common/index.ts

@@ -0,0 +1,38 @@
+import service from '/@/utils/request'
+import { UploadFile } from '/@/api/common/type'
+
+export default {
+	upload: {
+		// eslint-disable-next-line no-unused-vars
+		single: (blob: File,progress: ((progress: number) => void) | undefined = undefined):Promise<UploadFile> => {
+			const data = new FormData()
+			data.append('file', blob)
+			return service({
+				url: '/common/singleFile',
+				method: "post",
+				data,
+				onUploadProgress: (progressEvent) => {
+					if (progress) {
+						progress(progressEvent.loaded / progressEvent.total)
+					}
+				}
+			}) as unknown as Promise<UploadFile>
+		},
+		// eslint-disable-next-line no-unused-vars
+		multi: (blob: File[],progress: ((progress: number) => void) | undefined = undefined):Promise<UploadFile[]> => {
+			const data = new FormData()
+			blob.forEach(file => data.append('file', file))
+			// return post('/common/multipleFile', data)
+			return service({
+				url: '/common/multipleFile',
+				method: "post",
+				data,
+				onUploadProgress: (progressEvent) => {
+					if (progress) {
+						progress(progressEvent.loaded / progressEvent.total)
+					}
+				}
+			}) as unknown as Promise<UploadFile[]>
+		}
+	}
+}

+ 8 - 0
src/api/common/type.ts

@@ -0,0 +1,8 @@
+
+export type UploadFile = {
+	size: number
+	path: string
+	name: string
+	type: string
+	full_path: string
+}

+ 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'>

+ 356 - 0
src/components/assistant/ComponentLibrary.vue

@@ -0,0 +1,356 @@
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import { Plus } from '@element-plus/icons-vue'
+import Markdown from '/@/components/markdown/Markdown.vue'
+import type { ComponentLibraryItem, Content } from './types'
+import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
+import EChartsPlugin from '/@/components/markdown/plugins/echarts'
+import VueCharts from '/@/components/markdown/plugins/impl/VueCharts.vue'
+import VueStructData from '/@/components/markdown/plugins/impl/VueStructData.vue'
+
+const plugin: Array<MarkdownPlugin<any>> = [EChartsPlugin()]
+
+const props = defineProps<{
+	library?: ComponentLibraryItem[]
+}>()
+
+const componentLibrary = computed(() => props.library || [])
+
+const emit = defineEmits<{
+	// eslint-disable-next-line no-unused-vars
+	(e: 'addCard', cardData: Content): void
+}>()
+
+// 拖拽开始
+const handleDragStart = (event: DragEvent, component: any) => {
+	if (!event.dataTransfer) return
+
+	event.dataTransfer.setData('text/plain', JSON.stringify({
+		title: component.title,
+		data: component.data,
+		type: component.type
+	}))
+
+	event.dataTransfer.effectAllowed = 'copy'
+}
+
+// 添加组件到画布
+const addComponent = (component: any) => {
+	emit('addCard', {
+		title: component.title,
+		data: component.data,
+		type: component.type
+	})
+}
+
+// 预览组件
+const previewComponent = ref<any>(null)
+const showPreview = ref(false)
+
+const openPreview = (component: any) => {
+	previewComponent.value = component
+	showPreview.value = true
+}
+</script>
+
+<template>
+	<div class="component-library">
+		<div class="library-header">
+			<h3>组件库</h3>
+			<div class="library-info">
+				<span>{{ componentLibrary.length }} 个组件</span>
+			</div>
+		</div>
+
+		<div class="library-content">
+			<div class="component-list">
+				<div
+					v-for="component in componentLibrary"
+					:key="component.id"
+					class="component-item"
+					draggable="true"
+					@dragstart="handleDragStart($event, component)"
+				>
+					<div class="component-header">
+						<div class="component-icon">
+							<el-icon :size="20">
+								<component :is="component.icon" />
+							</el-icon>
+						</div>
+						<div class="component-info">
+							<h4 class="component-title">{{ component.title }}</h4>
+							<p class="component-description">{{ component.description }}</p>
+						</div>
+					</div>
+
+					<div class="component-preview">
+
+						<Markdown
+							v-if="component.type === 'markdown'"
+							:content="component.preview"
+							:plugins="plugin"
+							class="preview-content"
+						/>
+						<vue-charts style="width: 100%;height: 200px" :data="component.data" v-if="component.type === 'echarts'"/>
+						<vue-struct-data :uuid="component.data" v-if="component.type === 'structdata'" refresh/>
+					</div>
+
+					<div class="component-actions">
+						<el-button
+							type="primary"
+							size="small"
+							:icon="Plus"
+							@click="addComponent(component)"
+							class="add-btn"
+						>
+							添加到画布
+						</el-button>
+						<el-button
+							type="text"
+							size="small"
+							@click="openPreview(component)"
+							class="preview-btn"
+						>
+							预览
+						</el-button>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<!-- 使用说明 -->
+		<div class="library-footer">
+			<div class="usage-tips">
+				<h4>使用说明</h4>
+				<ul>
+					<li>拖拽组件到左侧画布</li>
+					<li>或点击"添加到画布"按钮</li>
+					<li>在画布中拖拽标题栏调整位置</li>
+				</ul>
+			</div>
+		</div>
+
+		<!-- 预览对话框 -->
+		<el-dialog
+			v-model="showPreview"
+			:title="`预览 - ${previewComponent?.title}`"
+			width="600px"
+			v-if="previewComponent"
+		>
+			<div class="preview-dialog-content">
+				<vue-charts style="width: 100%;height: 400px" :data="previewComponent.data" v-if="previewComponent.type === 'echarts'"/>
+				<Markdown
+					v-if="previewComponent.type === 'markdown'"
+					:content="previewComponent.data"
+					:plugins="plugin"
+					class="full-preview-content"
+				/>
+				<vue-struct-data :uuid="previewComponent.data" v-if="previewComponent.type === 'structdata'" refresh/>
+			</div>
+			<template #footer>
+				<div class="dialog-footer">
+					<el-button @click="showPreview = false">关闭</el-button>
+					<el-button
+						type="primary"
+						@click="addComponent(previewComponent); showPreview = false"
+					>
+						添加到画布
+					</el-button>
+				</div>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.component-library {
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+}
+
+.library-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 16px 20px;
+	border-bottom: 1px solid var(--el-border-color-light);
+	background: var(--el-fill-color-extra-light);
+
+	h3 {
+		margin: 0;
+		color: var(--el-text-color-primary);
+		font-size: 16px;
+		font-weight: 600;
+	}
+
+	.library-info {
+		font-size: 14px;
+		color: var(--el-text-color-regular);
+	}
+}
+
+.library-content {
+	flex: 1;
+	overflow-y: auto;
+	padding: 16px;
+}
+
+.component-list {
+	display: flex;
+	flex-direction: column;
+	gap: 16px;
+}
+
+.component-item {
+	border: 1px solid var(--el-border-color-light);
+	border-radius: 8px;
+	background: var(--el-bg-color);
+	transition: all 0.2s ease;
+	cursor: grab;
+
+	&:hover {
+		border-color: var(--el-color-primary-light-7);
+		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+	}
+
+	&:active {
+		cursor: grabbing;
+	}
+}
+
+.component-header {
+	display: flex;
+	align-items: flex-start;
+	gap: 12px;
+	padding: 16px 16px 12px 16px;
+}
+
+.component-icon {
+	flex-shrink: 0;
+	width: 40px;
+	height: 40px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	background: var(--el-color-primary-light-9);
+	border-radius: 8px;
+	color: var(--el-color-primary);
+}
+
+.component-info {
+	flex: 1;
+	min-width: 0;
+}
+
+.component-title {
+	margin: 0 0 4px 0;
+	font-size: 14px;
+	font-weight: 600;
+	color: var(--el-text-color-primary);
+}
+
+.component-description {
+	margin: 0;
+	font-size: 12px;
+	color: var(--el-text-color-secondary);
+	line-height: 1.4;
+}
+
+.component-preview {
+	padding: 0 16px 12px 16px;
+	border-top: 1px solid var(--el-border-color-extra-light);
+	margin-top: 8px;
+	padding-top: 12px;
+}
+
+.preview-content {
+	font-size: 12px;
+	line-height: 1.4;
+	max-height: 80px;
+	overflow: hidden;
+
+	:deep(blockquote) {
+		margin: 4px 0;
+		padding: 6px 10px;
+		font-size: 11px;
+	}
+
+	:deep(table) {
+		font-size: 10px;
+		margin: 4px 0;
+	}
+
+	:deep(th), :deep(td) {
+		padding: 2px 6px;
+	}
+}
+
+.component-actions {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 12px 16px;
+	border-top: 1px solid var(--el-border-color-extra-light);
+	background: var(--el-fill-color-extra-light);
+	border-radius: 0 0 8px 8px;
+}
+
+.add-btn {
+	flex: 1;
+	margin-right: 8px;
+}
+
+.preview-btn {
+	color: var(--el-text-color-regular);
+
+	&:hover {
+		color: var(--el-color-primary);
+	}
+}
+
+.library-footer {
+	border-top: 1px solid var(--el-border-color-light);
+	padding: 16px 20px;
+	background: var(--el-fill-color-extra-light);
+}
+
+.usage-tips {
+	h4 {
+		margin: 0 0 8px 0;
+		font-size: 14px;
+		font-weight: 600;
+		color: var(--el-text-color-primary);
+	}
+
+	ul {
+		margin: 0;
+		padding-left: 16px;
+
+		li {
+			font-size: 12px;
+			color: var(--el-text-color-secondary);
+			line-height: 1.5;
+			margin-bottom: 4px;
+		}
+	}
+}
+
+.preview-dialog-content {
+	max-height: 400px;
+	overflow-y: auto;
+	padding: 16px;
+	border: 1px solid var(--el-border-color-light);
+	border-radius: 6px;
+	background: var(--el-fill-color-extra-light);
+}
+
+.full-preview-content {
+	font-size: 14px;
+	line-height: 1.6;
+}
+
+.dialog-footer {
+	text-align: right;
+}
+</style>

+ 212 - 0
src/components/assistant/DashboardDesigner.vue

@@ -0,0 +1,212 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import DraggableCard from './DraggableCard.vue'
+import { Document } from '@element-plus/icons-vue'
+import type { MarkdownDashBoard, Position, Size, Content, AddCardData } from './types'
+
+defineProps<{
+	cards: MarkdownDashBoard[]
+}>()
+
+const emit = defineEmits<{
+	// eslint-disable-next-line no-unused-vars
+	(e: 'updatePosition', id: string, position: Position): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'updateSize', id: string, size: Size): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'updateContent', id: string, content: Content): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'removeCard', id: string): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'addCard', cardData: AddCardData): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'moveCardUp', id: string): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'moveCardDown', id: string): void
+}>()
+
+const designerContainer = ref<HTMLElement>()
+
+// 处理卡片位置更新
+const handleCardPositionUpdate = (id: string, position: Position) => {
+	emit('updatePosition', id, position)
+}
+
+// 处理卡片大小更新
+const handleCardSizeUpdate = (id: string, size: Size) => {
+	emit('updateSize', id, size)
+}
+
+// 处理卡片内容更新
+const handleCardContentUpdate = (id: string, content: Content) => {
+	emit('updateContent', id, content)
+}
+
+// 处理卡片删除
+const handleCardRemove = (id: string) => {
+	emit('removeCard', id)
+}
+
+// 处理卡片上移
+const handleCardMoveUp = (id: string) => {
+	emit('moveCardUp', id)
+}
+
+// 处理卡片下移
+const handleCardMoveDown = (id: string) => {
+	emit('moveCardDown', id)
+}
+
+// 处理拖拽放置
+const handleDrop = (event: DragEvent) => {
+	event.preventDefault()
+
+	if (!designerContainer.value) return
+
+	const rect = designerContainer.value.getBoundingClientRect()
+	const x = ((event.clientX - rect.left) / rect.width) * 100
+	const y = ((event.clientY - rect.top) / rect.height) * 100
+
+	// 从拖拽数据中获取组件信息
+	const dragData = event.dataTransfer?.getData('text/plain')
+	if (dragData) {
+		try {
+			const componentData = JSON.parse(dragData)
+			// 限制在画布范围内
+			const constrainedX = Math.max(0, Math.min(70, x)) // 预留30%宽度
+			const constrainedY = Math.max(0, Math.min(75, y)) // 预留25%高度
+			emit('addCard', {
+				...componentData,
+				x: constrainedX,
+				y: constrainedY
+			})
+		} catch (error) {
+			console.error('Invalid drag data:', error)
+		}
+	}
+}
+
+const handleDragOver = (event: DragEvent) => {
+	event.preventDefault()
+}
+</script>
+
+<template>
+	<div class="dashboard-designer">
+		<div
+			ref="designerContainer"
+			class="designer-canvas"
+			@drop="handleDrop"
+			@dragover="handleDragOver"
+		>
+			<!-- 网格背景 -->
+			<div class="grid-background"></div>
+
+			<!-- 渲染所有卡片 -->
+			<DraggableCard
+				v-for="card in cards"
+				:key="card.id"
+				:card="card"
+				@update-position="handleCardPositionUpdate"
+				@update-size="handleCardSizeUpdate"
+				@update-content="handleCardContentUpdate"
+				@remove="handleCardRemove"
+				@move-up="handleCardMoveUp"
+				@move-down="handleCardMoveDown"
+			/>
+
+			<!-- 空状态提示 -->
+			<div v-if="cards.length === 0" class="empty-canvas">
+				<div class="empty-icon">
+					<el-icon :size="60" color="#d1d5db">
+						<Document />
+					</el-icon>
+				</div>
+				<div class="empty-text">
+					<h3>开始设计您的仪表板</h3>
+					<p>从右侧组件库拖拽组件到此处</p>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.dashboard-designer {
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+}
+
+.designer-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 16px 20px;
+	border-bottom: 1px solid var(--el-border-color-light);
+	background: var(--el-fill-color-extra-light);
+
+	h3 {
+		margin: 0;
+		color: var(--el-text-color-primary);
+		font-size: 16px;
+		font-weight: 600;
+	}
+
+	.designer-info {
+		font-size: 14px;
+		color: var(--el-text-color-regular);
+	}
+}
+
+.designer-canvas {
+	flex: 1;
+	position: relative;
+	overflow: hidden;
+	background: var(--el-bg-color-page);
+	min-height: 500px;
+}
+
+.grid-background {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-image:
+		linear-gradient(to right, var(--el-border-color-extra-light) 1px, transparent 1px),
+		linear-gradient(to bottom, var(--el-border-color-extra-light) 1px, transparent 1px);
+	background-size: 20px 20px;
+	pointer-events: none;
+	opacity: 0.5;
+}
+
+.empty-canvas {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
+	text-align: center;
+	color: var(--el-text-color-secondary);
+
+	.empty-icon {
+		margin-bottom: 16px;
+		opacity: 0.6;
+	}
+
+	.empty-text {
+		h3 {
+			margin: 0 0 8px 0;
+			font-size: 18px;
+			font-weight: 500;
+			color: var(--el-text-color-regular);
+		}
+
+		p {
+			margin: 0;
+			font-size: 14px;
+			color: var(--el-text-color-secondary);
+		}
+	}
+}
+</style>

+ 118 - 0
src/components/assistant/DashboardViewer.vue

@@ -0,0 +1,118 @@
+<script setup lang="ts">
+import ViewerCard from './ViewerCard.vue'
+import { Document } from '@element-plus/icons-vue'
+import type { MarkdownDashBoard } from './types'
+
+defineProps<{
+	cards: MarkdownDashBoard[]
+}>()
+</script>
+
+<template>
+	<div class="dashboard-viewer">
+		<div class="viewer-canvas">
+			<!-- 网格背景 -->
+			<div class="grid-background"></div>
+
+			<!-- 渲染所有卡片 -->
+			<ViewerCard
+				v-for="card in cards"
+				:key="card.id"
+				:card="card"
+			/>
+
+			<!-- 空状态提示 -->
+			<div v-if="cards.length === 0" class="empty-canvas">
+				<div class="empty-icon">
+					<el-icon :size="60" color="#d1d5db">
+						<Document />
+					</el-icon>
+				</div>
+				<div class="empty-text">
+					<h3>暂无内容</h3>
+					<p>仪表板中还没有任何组件</p>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.dashboard-viewer {
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+}
+
+.viewer-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 16px 20px;
+	border-bottom: 1px solid var(--el-border-color-light);
+	background: var(--el-fill-color-extra-light);
+
+	h3 {
+		margin: 0;
+		color: var(--el-text-color-primary);
+		font-size: 16px;
+		font-weight: 600;
+	}
+
+	.viewer-info {
+		font-size: 14px;
+		color: var(--el-text-color-regular);
+	}
+}
+
+.viewer-canvas {
+	flex: 1;
+	position: relative;
+	overflow: hidden;
+	background: var(--el-bg-color-page);
+	min-height: 500px;
+}
+
+.grid-background {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-image:
+		linear-gradient(to right, var(--el-border-color-extra-light) 1px, transparent 1px),
+		linear-gradient(to bottom, var(--el-border-color-extra-light) 1px, transparent 1px);
+	background-size: 20px 20px;
+	pointer-events: none;
+	opacity: 0.5;
+}
+
+.empty-canvas {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
+	text-align: center;
+	color: var(--el-text-color-secondary);
+
+	.empty-icon {
+		margin-bottom: 16px;
+		opacity: 0.6;
+	}
+
+	.empty-text {
+		h3 {
+			margin: 0 0 8px 0;
+			font-size: 18px;
+			font-weight: 500;
+			color: var(--el-text-color-regular);
+		}
+
+		p {
+			margin: 0;
+			font-size: 14px;
+			color: var(--el-text-color-secondary);
+		}
+	}
+}
+</style>

+ 710 - 0
src/components/assistant/DraggableCard.vue

@@ -0,0 +1,710 @@
+<script setup lang="ts">
+import { ref, computed, onUnmounted, watch, nextTick } from 'vue'
+import { Delete, Edit, Top, Bottom } from '@element-plus/icons-vue'
+import Markdown from '/@/components/markdown/Markdown.vue'
+import type { MarkdownDashBoard, Position, Size, Content, ResizeType } from './types'
+import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
+import EChartsPlugin from '/@/components/markdown/plugins/echarts'
+import VueCharts from '/@/components/markdown/plugins/impl/VueCharts.vue'
+import VueStructData from '/@/components/markdown/plugins/impl/VueStructData.vue'
+
+const plugin: Array<MarkdownPlugin<any>> = [EChartsPlugin()]
+
+const props = defineProps<{
+	card: MarkdownDashBoard
+}>()
+
+const emit = defineEmits<{
+	// eslint-disable-next-line no-unused-vars
+	(e: 'updatePosition', id: string, position: Position): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'updateSize', id: string, size: Size): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'updateContent', id: string, content: Content): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'remove', id: string): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'moveUp', id: string): void
+	// eslint-disable-next-line no-unused-vars
+	(e: 'moveDown', id: string): void
+}>()
+
+const cardRef = ref<HTMLElement>()
+const isDragging = ref(false)
+const isResizing = ref(false)
+const dragOffset = ref({ x: 0, y: 0 })
+const resizeType = ref<ResizeType>('')
+const initialSize = ref({ w: 0, h: 0 })
+const initialPosition = ref({ x: 0, y: 0 })
+
+// 编辑相关状态
+const showEditDialog = ref(false)
+const editTitle = ref<string>()
+const editType = ref<MarkdownDashBoard['type']>()
+const editData = ref('')
+const editCharts = ref()
+
+watch(editData,()=>{
+	editCharts.value?.resize()
+})
+
+// 右键菜单相关状态
+const showContextMenu = ref(false)
+const contextMenuPosition = ref({ x: 0, y: 0 })
+
+// 计算卡片样式
+const cardStyle = computed(() => ({
+	position: 'absolute' as const,
+	left: `${props.card.x}%`,
+	top: `${props.card.y}%`,
+	width: `${props.card.w}%`,
+	height: `${props.card.h}%`,
+	zIndex: props.card.z + 100
+}))
+
+// 开始拖拽移动
+const startDrag = (event: MouseEvent) => {
+	if (!cardRef.value || isResizing.value) return
+
+	isDragging.value = true
+
+	const rect = cardRef.value.getBoundingClientRect()
+	const parentRect = cardRef.value.parentElement?.getBoundingClientRect()
+
+	if (!parentRect) return
+
+	dragOffset.value = {
+		x: event.clientX - rect.left,
+		y: event.clientY - rect.top
+	}
+
+	document.addEventListener('mousemove', handleDrag)
+	document.addEventListener('mouseup', stopDrag)
+
+	// 防止文本选择
+	event.preventDefault()
+}
+
+// 开始拖拽调整大小
+const startResize = (event: MouseEvent, type: 'se' | 'e' | 's') => {
+	if (!cardRef.value) return
+
+	isResizing.value = true
+	resizeType.value = type
+
+	initialSize.value = { w: props.card.w, h: props.card.h }
+	initialPosition.value = { x: event.clientX, y: event.clientY }
+
+	document.addEventListener('mousemove', handleResize)
+	document.addEventListener('mouseup', stopResize)
+
+	// 防止文本选择和事件冒泡
+	event.preventDefault()
+	event.stopPropagation()
+}
+
+// 处理拖拽移动
+const handleDrag = (event: MouseEvent) => {
+	if (!isDragging.value || !cardRef.value) return
+
+	const parentRect = cardRef.value.parentElement?.getBoundingClientRect()
+	if (!parentRect) return
+
+	const newX = ((event.clientX - dragOffset.value.x - parentRect.left) / parentRect.width) * 100
+	const newY = ((event.clientY - dragOffset.value.y - parentRect.top) / parentRect.height) * 100
+
+	// 限制在画布范围内
+	const constrainedX = Math.max(0, Math.min(100 - props.card.w, newX))
+	const constrainedY = Math.max(0, Math.min(100 - props.card.h, newY))
+
+	emit('updatePosition', props.card.id, { x: constrainedX, y: constrainedY })
+}
+
+// 处理拖拽调整大小
+const handleResize = (event: MouseEvent) => {
+	if (!isResizing.value || !cardRef.value) return
+
+	const parentRect = cardRef.value.parentElement?.getBoundingClientRect()
+	if (!parentRect) return
+
+	const deltaX = event.clientX - initialPosition.value.x
+	const deltaY = event.clientY - initialPosition.value.y
+
+	const deltaXPercent = (deltaX / parentRect.width) * 100
+	const deltaYPercent = (deltaY / parentRect.height) * 100
+
+	let newW = initialSize.value.w
+	let newH = initialSize.value.h
+
+	if (resizeType.value === 'se' || resizeType.value === 'e') {
+		newW = Math.max(15, Math.min(100 - props.card.x, initialSize.value.w + deltaXPercent))
+	}
+
+	if (resizeType.value === 'se' || resizeType.value === 's') {
+		newH = Math.max(10, Math.min(100 - props.card.y, initialSize.value.h + deltaYPercent))
+	}
+
+	emit('updateSize', props.card.id, { w: newW, h: newH })
+}
+
+// 停止拖拽移动
+const stopDrag = () => {
+	isDragging.value = false
+	document.removeEventListener('mousemove', handleDrag)
+	document.removeEventListener('mouseup', stopDrag)
+}
+
+// 停止拖拽调整大小
+const stopResize = () => {
+	isResizing.value = false
+	resizeType.value = ''
+	document.removeEventListener('mousemove', handleResize)
+	document.removeEventListener('mouseup', stopResize)
+}
+
+// 编辑卡片
+const handleEdit = async () => {
+	editTitle.value = props.card.title
+	editData.value = props.card.data
+	editType.value = props.card.type
+	showEditDialog.value = true
+	await nextTick()
+	editCharts.value?.resize()
+}
+
+// 确认编辑
+const confirmEdit = () => {
+	if ((editTitle.value === undefined && editData.value.trim()) || (editTitle.value !== undefined && (editTitle.value.trim() && editData.value.trim()))) {
+		emit('updateContent', props.card.id, {
+			title: editTitle.value?.trim(),
+			data: editData.value.trim()
+		})
+		showEditDialog.value = false
+	}
+}
+
+// 取消编辑
+const cancelEdit = () => {
+	showEditDialog.value = false
+	editTitle.value = ''
+	editData.value = ''
+}
+
+// 删除卡片
+const handleRemove = () => {
+	emit('remove', props.card.id)
+	showContextMenu.value = false
+}
+
+// 处理右键菜单
+const handleContextMenu = (event: MouseEvent) => {
+	event.preventDefault()
+	event.stopPropagation()
+
+	contextMenuPosition.value = {
+		x: event.clientX,
+		y: event.clientY
+	}
+	showContextMenu.value = true
+
+	// 点击其他地方关闭菜单
+	const closeMenu = () => {
+		showContextMenu.value = false
+		document.removeEventListener('click', closeMenu)
+	}
+
+	// 延迟添加事件监听器,避免立即触发
+	setTimeout(() => {
+		document.addEventListener('click', closeMenu)
+	}, 0)
+}
+
+// 处理右键菜单编辑
+const handleContextEdit = () => {
+	handleEdit()
+	showContextMenu.value = false
+}
+
+// 处理上移一层
+const handleMoveUp = () => {
+	emit('moveUp', props.card.id)
+	showContextMenu.value = false
+}
+
+// 处理下移一层
+const handleMoveDown = () => {
+	emit('moveDown', props.card.id)
+	showContextMenu.value = false
+}
+
+// 清理事件监听器
+onUnmounted(() => {
+	document.removeEventListener('mousemove', handleDrag)
+	document.removeEventListener('mouseup', stopDrag)
+	document.removeEventListener('mousemove', handleResize)
+	document.removeEventListener('mouseup', stopResize)
+})
+
+const charts = ref()
+
+watch(()=> props.card.w, () => {
+	charts.value?.resize()
+})
+watch(()=> props.card.h, () => {
+	charts.value?.resize()
+})
+
+</script>
+
+<template>
+	<div
+		ref="cardRef"
+		:style="cardStyle"
+		:class="['draggable-card', { 'is-dragging': isDragging, 'is-resizing': isResizing }]"
+		@contextmenu="handleContextMenu"
+	>
+		<el-card class="card-content" shadow="hover" @mousedown="startDrag">
+			<!-- 卡片标题栏 - 可拖拽区域 -->
+			<template #header v-if="card.title !== undefined">
+				<div class="card-header">
+					<span class="card-title">{{ card.title }}</span>
+				</div>
+			</template>
+
+			<!-- 卡片内容 -->
+			<div class="card-body">
+				<Markdown
+					v-if="card.type === 'markdown'"
+					:content="card.data"
+					:plugins="plugin"
+					class="markdown-content"
+				/>
+				<vue-charts ref="charts" style="width: 100%;height: 100%" :data="card.data" v-if="card.type === 'echarts'"/>
+				<vue-struct-data :uuid="card.data" v-if="card.type === 'structdata'" refresh/>
+			</div>
+		</el-card>
+
+		<!-- 调整大小的拖拽手柄 -->
+		<div class="resize-handles">
+			<!-- 右边缘 -->
+			<div
+				class="resize-handle resize-handle-e"
+				@mousedown="startResize($event, 'e')"
+			></div>
+
+			<!-- 底边缘 -->
+			<div
+				class="resize-handle resize-handle-s"
+				@mousedown="startResize($event, 's')"
+			></div>
+
+			<!-- 右下角 -->
+			<div
+				class="resize-handle resize-handle-se"
+				@mousedown="startResize($event, 'se')"
+			></div>
+		</div>
+
+	<!-- 右键菜单 -->
+	<Teleport to="body">
+		<div
+			v-if="showContextMenu"
+			class="context-menu"
+			:style="{
+				left: contextMenuPosition.x + 'px',
+				top: contextMenuPosition.y + 'px'
+			}"
+		>
+			<div class="context-menu-item" @click="handleContextEdit">
+				<el-icon><Edit /></el-icon>
+				<span>编辑</span>
+			</div>
+			<div class="context-menu-divider"></div>
+			<div class="context-menu-item" @click="handleMoveUp">
+				<el-icon><Top /></el-icon>
+				<span>上移一层</span>
+			</div>
+			<div class="context-menu-item" @click="handleMoveDown">
+				<el-icon><Bottom /></el-icon>
+				<span>下移一层</span>
+			</div>
+			<div class="context-menu-divider"></div>
+			<div class="context-menu-item context-menu-item-danger" @click="handleRemove">
+				<el-icon><Delete /></el-icon>
+				<span>删除</span>
+			</div>
+		</div>
+	</Teleport>
+
+	<!-- 编辑对话框 -->
+	<el-dialog
+		v-model="showEditDialog"
+		title="编辑卡片内容"
+		width="900px"
+		:before-close="cancelEdit"
+		append-to-body
+	>
+		<div class="edit-dialog-content">
+			<!-- 左侧编辑区 -->
+			<div class="edit-panel">
+				<div class="form-item" v-if="editTitle !== undefined">
+					<label class="form-label">卡片标题</label>
+					<el-input
+						v-model="editTitle"
+						placeholder="请输入卡片标题"
+						maxlength="50"
+						show-word-limit
+					/>
+				</div>
+
+				<div class="form-item">
+					<label class="form-label">卡片内容 (支持Markdown)</label>
+					<el-input
+						v-model="editData"
+						type="textarea"
+						placeholder="请输入卡片内容,支持Markdown语法"
+						:rows="16"
+						resize="none"
+						class="edit-textarea"
+					/>
+				</div>
+			</div>
+
+			<!-- 右侧预览区 -->
+			<div class="preview-panel">
+				<div class="preview-header">
+					<label class="form-label">预览效果</label>
+				</div>
+				<div class="preview-container">
+					<Markdown
+						v-if="editType === 'markdown'"
+						:content="editData"
+						:plugins="plugin"
+						class="preview-content"
+					/>
+					<vue-charts ref="editCharts" style="width: 100%;height: 100%" :data="editData" v-if="editType === 'echarts'"/>
+				</div>
+			</div>
+		</div>
+
+		<template #footer>
+			<div class="dialog-footer">
+				<el-button @click="cancelEdit">取消</el-button>
+				<el-button
+					type="primary"
+					@click="confirmEdit"
+					:disabled="(editTitle === undefined && !editData.trim()) || (editTitle !== undefined && (!editTitle.trim() || !editData.trim()))"
+				>
+					确定
+				</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</div>
+</template>
+
+<style scoped lang="scss">
+.draggable-card {
+	transition: box-shadow 0.2s ease;
+
+	&.is-dragging {
+		.card-content {
+			box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+		}
+	}
+
+	&.is-resizing {
+		.card-content {
+			box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+		}
+	}
+
+	&:hover {
+		.resize-handles {
+			opacity: 1;
+		}
+	}
+}
+
+.card-content {
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+
+	:deep(.el-card__header) {
+		padding: 12px 16px;
+		border-bottom: 1px solid var(--el-border-color-lighter);
+		background: var(--el-fill-color-extra-light);
+	}
+
+	:deep(.el-card__body) {
+		flex: 1;
+		padding: 16px;
+		overflow: auto;
+	}
+}
+
+.card-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	cursor: move;
+	user-select: none;
+
+	&:hover {
+		background: var(--el-fill-color-light);
+		margin: -12px -16px;
+		padding: 12px 16px;
+		border-radius: 4px;
+	}
+}
+
+.card-title {
+	font-weight: 600;
+	color: var(--el-text-color-primary);
+	font-size: 14px;
+}
+
+.card-body {
+	height: 100%;
+	overflow: auto;
+}
+
+.markdown-content {
+	font-size: 13px;
+	line-height: 1.5;
+
+	:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
+		margin-top: 12px;
+		margin-bottom: 8px;
+		font-size: 14px;
+	}
+
+	:deep(p) {
+		margin-bottom: 8px;
+	}
+
+	:deep(blockquote) {
+		margin: 8px 0;
+		padding: 8px 12px;
+		font-size: 12px;
+	}
+
+	:deep(table) {
+		font-size: 12px;
+		margin: 8px 0;
+	}
+
+	:deep(th), :deep(td) {
+		padding: 4px 8px;
+	}
+}
+
+/* 调整大小手柄样式 */
+.resize-handles {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	pointer-events: none;
+	opacity: 0;
+	transition: opacity 0.2s ease;
+}
+
+.resize-handle {
+	position: absolute;
+	pointer-events: all;
+	background: var(--el-color-primary);
+	transition: all 0.2s ease;
+
+	&:hover {
+		background: var(--el-color-primary-light-3);
+	}
+}
+
+.resize-handle-e {
+	top: 0;
+	right: -2px;
+	bottom: 0;
+	width: 4px;
+	cursor: e-resize;
+}
+
+.resize-handle-s {
+	left: 0;
+	right: 0;
+	bottom: -2px;
+	height: 4px;
+	cursor: s-resize;
+}
+
+.resize-handle-se {
+	right: -2px;
+	bottom: -2px;
+	width: 8px;
+	height: 8px;
+	cursor: se-resize;
+	border-radius: 0 0 4px 0;
+}
+
+/* 编辑对话框样式 */
+.edit-dialog-content {
+	display: flex;
+	gap: 20px;
+	height: 500px;
+}
+
+.edit-panel {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+
+	.form-item {
+		margin-bottom: 16px;
+
+		&:last-child {
+			flex: 1;
+			display: flex;
+			flex-direction: column;
+		}
+	}
+
+	.form-label {
+		display: block;
+		margin-bottom: 8px;
+		font-size: 14px;
+		font-weight: 600;
+		color: var(--el-text-color-primary);
+	}
+
+	.edit-textarea {
+		flex: 1;
+
+		:deep(.el-textarea__inner) {
+			height: 100% !important;
+			resize: none;
+			font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+			font-size: 13px;
+			line-height: 1.5;
+		}
+	}
+}
+
+.preview-panel {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	border-left: 1px solid var(--el-border-color-lighter);
+	padding-left: 20px;
+
+	.preview-header {
+		margin-bottom: 8px;
+
+		.form-label {
+			display: block;
+			font-size: 14px;
+			font-weight: 600;
+			color: var(--el-text-color-primary);
+		}
+	}
+
+	.preview-container {
+		flex: 1;
+		border: 1px solid var(--el-border-color-light);
+		border-radius: 6px;
+		padding: 16px;
+		background: var(--el-fill-color-extra-light);
+		overflow-y: auto;
+	}
+
+	.preview-content {
+		font-size: 14px;
+		line-height: 1.6;
+
+		:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
+			margin-top: 16px;
+			margin-bottom: 12px;
+
+			&:first-child {
+				margin-top: 0;
+			}
+		}
+
+		:deep(p) {
+			margin-bottom: 12px;
+
+			&:last-child {
+				margin-bottom: 0;
+			}
+		}
+
+		:deep(blockquote) {
+			margin: 12px 0;
+			padding: 12px 16px;
+		}
+
+		:deep(table) {
+			margin: 12px 0;
+		}
+	}
+}
+
+.dialog-footer {
+	text-align: right;
+}
+
+/* 右键菜单样式 */
+.context-menu {
+	position: fixed;
+	z-index: 9999;
+	background: var(--el-bg-color-overlay);
+	border: 1px solid var(--el-border-color-light);
+	border-radius: 6px;
+	box-shadow: var(--el-box-shadow-light);
+	padding: 4px 0;
+	min-width: 120px;
+	backdrop-filter: blur(12px);
+}
+
+.context-menu-item {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+	padding: 8px 16px;
+	cursor: pointer;
+	transition: all 0.2s ease;
+	font-size: 14px;
+	color: var(--el-text-color-primary);
+
+	.el-icon {
+		font-size: 14px;
+		color: var(--el-text-color-regular);
+	}
+
+	&:hover {
+		background: var(--el-fill-color-light);
+
+		.el-icon {
+			color: var(--el-color-primary);
+		}
+	}
+
+	&.context-menu-item-danger {
+		&:hover {
+			background: var(--el-color-danger-light-9);
+			color: var(--el-color-danger);
+
+			.el-icon {
+				color: var(--el-color-danger);
+			}
+		}
+	}
+}
+
+.context-menu-divider {
+	height: 1px;
+	background: var(--el-border-color-lighter);
+	margin: 4px 0;
+}
+</style>

+ 134 - 0
src/components/assistant/ViewerCard.vue

@@ -0,0 +1,134 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue'
+import Markdown from '/@/components/markdown/Markdown.vue'
+import type { MarkdownDashBoard } from './types'
+import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
+import EChartsPlugin from '/@/components/markdown/plugins/echarts'
+import VueCharts from '/@/components/markdown/plugins/impl/VueCharts.vue'
+import VueStructData from '/@/components/markdown/plugins/impl/VueStructData.vue'
+
+const plugin: Array<MarkdownPlugin<any>> = [EChartsPlugin()]
+
+const props = defineProps<{
+	card: MarkdownDashBoard
+}>()
+
+// 计算卡片样式
+const cardStyle = computed(() => ({
+	position: 'absolute' as const,
+	left: `${props.card.x}%`,
+	top: `${props.card.y}%`,
+	width: `${props.card.w}%`,
+	height: `${props.card.h}%`,
+	zIndex: props.card.z + 100
+}))
+
+const charts = ref()
+
+watch(()=> props.card.w, () => {
+	charts.value?.resize()
+})
+watch(()=> props.card.h, () => {
+	charts.value?.resize()
+})
+</script>
+
+<template>
+	<div
+		:style="cardStyle"
+		class="viewer-card"
+	>
+		<el-card class="card-content" shadow="hover">
+			<!-- 卡片标题栏 -->
+			<template #header v-if="card.title !== undefined">
+				<div class="card-header">
+					<span class="card-title">{{ card.title }}</span>
+				</div>
+			</template>
+
+			<!-- 卡片内容 -->
+			<div class="card-body">
+				<Markdown
+					v-if="card.type === 'markdown'"
+					:content="card.data"
+					:plugins="plugin"
+					class="markdown-content"
+				/>
+				<vue-charts ref="charts" style="width: 100%;height: 100%" :data="card.data" v-if="card.type === 'echarts'"/>
+				<vue-struct-data :uuid="card.data" v-if="card.type === 'structdata'" refresh/>
+			</div>
+		</el-card>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.viewer-card {
+	transition: none;
+}
+
+.card-content {
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+
+	:deep(.el-card__header) {
+		padding: 12px 16px;
+		border-bottom: 1px solid var(--el-border-color-lighter);
+		background: var(--el-fill-color-extra-light);
+	}
+
+	:deep(.el-card__body) {
+		flex: 1;
+		padding: 16px;
+		overflow: auto;
+	}
+}
+
+.card-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	user-select: none;
+}
+
+.card-title {
+	font-weight: 600;
+	color: var(--el-text-color-primary);
+	font-size: 14px;
+}
+
+.card-body {
+	height: 100%;
+	overflow: auto;
+}
+
+.markdown-content {
+	font-size: 13px;
+	line-height: 1.5;
+
+	:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
+		margin-top: 12px;
+		margin-bottom: 8px;
+		font-size: 14px;
+	}
+
+	:deep(p) {
+		margin-bottom: 8px;
+	}
+
+	:deep(blockquote) {
+		margin: 8px 0;
+		padding: 8px 12px;
+		font-size: 12px;
+	}
+
+	:deep(table) {
+		font-size: 12px;
+		margin: 8px 0;
+	}
+
+	:deep(th), :deep(td) {
+		padding: 4px 8px;
+	}
+}
+</style>

+ 76 - 0
src/components/assistant/types.ts

@@ -0,0 +1,76 @@
+/**
+ * 仪表板助手相关类型定义
+ */
+
+export type MarkdownDashBoardType = 'echarts' | 'markdown' | 'structdata'
+
+// 仪表板卡片类型
+export interface MarkdownDashBoard {
+	/** 卡片唯一标识 */
+	id: string
+	/** X坐标 (百分比 0-100) */
+	x: number
+	/** Y坐标 (百分比 0-100) */
+	y: number
+	/** 宽度 (百分比 0-100) */
+	w: number
+	/** 高度 (百分比 0-100) */
+	h: number
+	/** 层级 (z-index) */
+	z: number
+	/** 卡片标题 */
+	title?: string
+	type: MarkdownDashBoardType
+	/** 卡片内容 (Markdown格式) */
+	data: string
+}
+
+// 位置信息类型
+export interface Position {
+	x: number
+	y: number
+}
+
+// 尺寸信息类型
+export interface Size {
+	w: number
+	h: number
+}
+
+// 内容信息类型
+export interface Content {
+	title?: string
+	data: string
+}
+
+// 组件库项目类型
+export interface ComponentLibraryItem {
+	/** 组件唯一标识 */
+	id: string
+	/** 组件标题 */
+	title?: string
+	/** 组件图标 */
+	icon: any
+	/** 组件描述 */
+	description: string
+
+	type: MarkdownDashBoardType
+	/** 组件完整数据 */
+	data: string
+	/** 组件预览数据 */
+	preview: string
+}
+
+// 拖拽调整大小类型
+export type ResizeType = 'se' | 'e' | 's' | ''
+
+// 卡片添加数据类型
+export interface AddCardData {
+	title?: string
+	type: MarkdownDashBoardType
+	data: string
+	x?: number
+	y?: number
+}
+
+

+ 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>

文件差异内容过多而无法显示
+ 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'
+            },
+        ]);
+    }
+};

部分文件因为文件数量过多而无法显示