Container.vue 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271
  1. <template>
  2. <el-container class="FormMaking fm-form" :class="{
  3. ['fm-'+formKey]: true,
  4. 'is-fullscreen': fullscreen
  5. }" :style="{'z-index': zIndex}">
  6. <el-main class="FormMaking-main">
  7. <el-container>
  8. <el-aside width="250px" class="widget-left-panel" ref="widgetLeftPanel">
  9. <el-tabs v-model="activeLeft" class="left-tabs" v-show="!leftHide">
  10. <el-tab-pane name="field" >
  11. <template #label>
  12. <el-tooltip :content="$t('fm.actions.components')" placement="bottom-start">
  13. <span>&nbsp;&nbsp;<i class="fm-iconfont icon-yuanshuju-zujianku"></i>&nbsp;&nbsp;</span>
  14. </el-tooltip>
  15. </template>
  16. <el-scrollbar>
  17. <div class="components-list">
  18. <el-collapse v-model="activeFields">
  19. <el-collapse-item name="layout" v-if="layoutFields.length" :title="$t('fm.components.layout.title')">
  20. <draggable tag="ul" :list="layoutComponents"
  21. :group="{ name:'people', pull:handlePull,put:false}"
  22. :animation="200"
  23. :sort="false"
  24. ghost-class="ghost"
  25. @end="handleMoveEnd"
  26. @start="handleMoveStart"
  27. :move="handleMove"
  28. item-key="name"
  29. >
  30. <template #item="{element}">
  31. <Transition>
  32. <li @click="handleField(element)"
  33. v-if="layoutFields.indexOf(element.type) >=0 && !(isVantMobile && (element.type == 'report'))"
  34. class="form-edit-widget-label no-put"
  35. :class="{
  36. 'is-disabled': isVantMobile && (element.type == 'report')
  37. }"
  38. >
  39. <a>
  40. <i class="icon fm-iconfont" :class="element.icon"></i>
  41. <span>{{element.name}}</span>
  42. </a>
  43. </li>
  44. </Transition>
  45. </template>
  46. </draggable>
  47. </el-collapse-item>
  48. <el-collapse-item name="collection" v-if="collectionFields.length" :title="$t('fm.components.collection.title')">
  49. <draggable tag="ul" :list="collectionComponents"
  50. :group="{ name:'people', pull:handlePull,put:false}"
  51. :animation="200"
  52. :sort="false"
  53. ghost-class="ghost"
  54. @end="handleMoveEnd"
  55. @start="handleMoveStart"
  56. :move="handleMove"
  57. item-key="name"
  58. >
  59. <template #item="{element}">
  60. <Transition>
  61. <li @click="handleField(element)" v-if="collectionFields.indexOf(element.type) >= 0" class="form-edit-widget-label"
  62. :class="{
  63. 'no-put': true,
  64. 'subform-put': element.type == 'table' || element.type == 'subform',
  65. 'dialog-put': element.type == 'dialog'
  66. }">
  67. <a>
  68. <i class="icon fm-iconfont" :class="element.icon"></i>
  69. <span>{{element.name}}</span>
  70. </a>
  71. </li>
  72. </Transition>
  73. </template>
  74. </draggable>
  75. </el-collapse-item>
  76. <el-collapse-item name="basic" v-if="basicFields.length" :title="$t('fm.components.basic.title')">
  77. <draggable tag="ul"
  78. :list="basicComponents"
  79. :group="{ name:'people', pull:handlePull,put:false}"
  80. :animation="200"
  81. :sort="false"
  82. ghost-class="ghost"
  83. @end="handleMoveEnd"
  84. @start="handleMoveStart"
  85. :move="handleMove"
  86. item-key="name"
  87. >
  88. <template #item="{element}">
  89. <Transition>
  90. <li @click="handleField(element)"
  91. v-if="basicFields.indexOf(element.type)>=0 && !(isVantMobile && (element.type == 'color' || element.type == 'transfer' || element.type == 'link'))"
  92. class="form-edit-widget-label"
  93. :class="{
  94. 'no-put': element.type == 'divider',
  95. 'is-disabled': isVantMobile && (element.type == 'color' || element.type == 'transfer' || element.type == 'link')
  96. }">
  97. <a>
  98. <i class="icon fm-iconfont" :class="element.icon"></i>
  99. <span>{{element.name}}</span>
  100. </a>
  101. </li>
  102. </Transition>
  103. </template>
  104. </draggable>
  105. </el-collapse-item>
  106. <el-collapse-item name="advance" v-if="advanceFields.length" :title="$t('fm.components.advance.title')">
  107. <draggable tag="ul" :list="advanceComponents"
  108. :group="{ name:'people', pull:handlePull,put:false}"
  109. :animation="200"
  110. :sort="false"
  111. ghost-class="ghost"
  112. @end="handleMoveEnd"
  113. @start="handleMoveStart"
  114. :move="handleMove"
  115. item-key="name"
  116. >
  117. <template #item="{element}">
  118. <Transition>
  119. <li @click="handleField(element)"
  120. v-if="advanceFields.indexOf(element.type) >= 0 && !(isVantMobile && (element.type == 'editor' || element.type == 'list'))"
  121. class="form-edit-widget-label"
  122. :class="{
  123. 'no-put': element.type == 'table' || element.type == 'subform' || element.type == 'dialog' || element.type == 'group',
  124. 'subform-put': element.type == 'table' || element.type == 'subform',
  125. 'dialog-put': element.type == 'dialog',
  126. 'is-new': element.type == null,
  127. 'is-disabled': isVantMobile && (element.type == 'editor' || element.type == 'list')
  128. }">
  129. <a>
  130. <i class="icon fm-iconfont" :class="element.icon"></i>
  131. <span>{{element.name}}</span>
  132. </a>
  133. </li>
  134. </Transition>
  135. </template>
  136. </draggable>
  137. </el-collapse-item>
  138. <el-collapse-item name="custom" v-if="customFields.length" :title="$t('fm.components.custom.title')">
  139. <draggable tag="ul" :list="customComponents"
  140. :group="{ name:'people', pull:handlePull,put:false}"
  141. :animation="200"
  142. :sort="false"
  143. ghost-class="ghost"
  144. @end="handleMoveEnd"
  145. @start="handleMoveStart"
  146. :move="handleMove"
  147. item-key="name"
  148. >
  149. <template #item="{element}">
  150. <Transition>
  151. <li @click="handleField(element)" class="form-edit-widget-label" >
  152. <a>
  153. <i class="icon fm-iconfont custom" :class="element.icon ? '' : 'icon-extend'">
  154. <span v-html="element.icon" v-if="element.icon"></span>
  155. </i>
  156. <span>{{element.name}}</span>
  157. </a>
  158. </li>
  159. </Transition>
  160. </template>
  161. </draggable>
  162. </el-collapse-item>
  163. </el-collapse>
  164. </div>
  165. </el-scrollbar>
  166. </el-tab-pane>
  167. <el-tab-pane name="outline">
  168. <template #label>
  169. <el-tooltip :content="$t('fm.actions.outline')" placement="bottom">
  170. <span>&nbsp;&nbsp;<i class="fm-iconfont icon-fuhao-dagangshu"></i>&nbsp;&nbsp;</span>
  171. </el-tooltip>
  172. </template>
  173. <outline ref="outlineRef" :show="activeLeft == 'outline'" :data="widgetForm.list" @select="onSelectWidget"></outline>
  174. </el-tab-pane>
  175. </el-tabs>
  176. <div class="container-left-arrow" @click="handleLeftToggle"></div>
  177. </el-aside>
  178. <el-container class="center-container" direction="vertical">
  179. <el-header class="btn-bar" style="height: 45px;">
  180. <div class="btn-bar-plat">
  181. <a :class="{'active': platform == 'pc'}" @click="handlePlatform('pc')"><i class="fm-iconfont icon-pc"></i></a>
  182. <a :class="{'active': platform == 'pad'}" @click="handlePlatform('pad')"><i class="fm-iconfont icon-pad"></i></a>
  183. <a :class="{'active': platform == 'mobile'}" @click="handlePlatform('mobile')"><i class="fm-iconfont icon-mobile"></i></a>
  184. </div>
  185. <div class="btn-diviler"></div>
  186. <div class="btn-bar-action" v-if="showDo">
  187. <el-tooltip :content="$t('fm.actions.undo')" placement="bottom">
  188. <a @click="handleUndo" :class="{'disabled': !undo}"><i class="fm-iconfont icon-007caozuo_chexiao"></i></a>
  189. </el-tooltip>
  190. <el-tooltip :content="$t('fm.actions.redo')" placement="bottom">
  191. <a @click="handleRedo" :class="{'disabled': !redo}"><i class="fm-iconfont icon-8zhongzuo"></i></a>
  192. </el-tooltip>
  193. </div>
  194. <div class="btn-diviler" v-if="showDo"></div>
  195. <div class="btn-bar-action">
  196. <el-tooltip :content="$t('fm.actions.fullScreen')" placement="bottom" v-if="!fullscreen">
  197. <a @click="handleFullScreen"><i class="fm-iconfont icon-quanping_o"></i></a>
  198. </el-tooltip>
  199. <el-tooltip :content="$t('fm.actions.exitFullScreen')" placement="bottom" v-else>
  200. <a @click="handleExitFullScreen"><i class="fm-iconfont icon-quxiaoquanping_o"></i></a>
  201. </el-tooltip>
  202. </div>
  203. <slot name="action">
  204. </slot>
  205. <el-button v-if="upload" link type="primary" size="default" @click="handleUpload"><i class="fm-iconfont icon-daoru" style="font-size: 18px; font-weight: 600; margin: 5px;" />{{$t('fm.actions.import')}}</el-button>
  206. <el-button v-if="preview" link type="primary" size="default" @click="handlePreview"><i class="fm-iconfont icon-icon_yulan" style="font-size: 18px; font-weight: 600; margin: 5px;" />{{$t('fm.actions.preview')}}</el-button>
  207. <el-button v-if="clearable" link type="primary" size="default" @click="handleClear"><i class="fm-iconfont icon-qingkong" style="font-size: 18px; font-weight: 600; margin: 5px;" />{{$t('fm.actions.clear')}}</el-button>
  208. <el-button v-if="generateJson" link type="primary" size="default" @click="handleGenerateJson"><i class="fm-iconfont icon-json1" style="font-size: 18px; font-weight: 600; margin: 5px;" />{{$t('fm.actions.json')}}</el-button>
  209. <el-button v-if="generateCode" link type="primary" size="default" @click="handleGenerateCode"><i class="fm-iconfont icon-daimakuai" style="font-size: 18px; font-weight: 600; margin: 5px;" />{{$t('fm.actions.code')}}</el-button>
  210. </el-header>
  211. <el-main :class="{'widget-empty': widgetForm.list.length == 0}">
  212. <template v-if="!resetJson && !isVantMobile">
  213. <antdv-widget-form v-if="widgetForm.config.ui == 'antd'">
  214. <widget-form ref="widgetForm" :data="widgetForm" v-model:select="widgetFormSelect" :platform="platform" :form-key="formKey"></widget-form>
  215. </antdv-widget-form>
  216. <widget-form v-else ref="widgetForm" :data="widgetForm" v-model:select="widgetFormSelect" :platform="platform" :form-key="formKey"></widget-form>
  217. </template>
  218. <vant-widget-form v-if="!resetJson && isVantMobile" ref="widgetForm" :data="widgetForm" v-model:select="widgetFormSelect" :platform="platform" :form-key="formKey"></vant-widget-form>
  219. </el-main>
  220. </el-container>
  221. <el-aside class="widget-config-container" ref="widgetConfigContainer">
  222. <el-container v-show="!rightHide">
  223. <el-header height="45px">
  224. <div class="config-tab" :class="{active: configTab=='widget'}" @click="handleConfigSelect('widget')">{{$t('fm.config.widget.title')}}</div>
  225. <div class="config-tab" :class="{active: configTab=='form'}" @click="handleConfigSelect('form')">{{$t('fm.config.form.title')}}</div>
  226. </el-header>
  227. <el-main class="config-content">
  228. <widget-config v-show="configTab=='widget'" ref="widgetConfig"
  229. :platform="platform"
  230. :sheets="styleSheetsArray"
  231. :datasources="dataSourceArray"
  232. :eventscripts="eventScriptArray"
  233. :data="widgetFormSelect"
  234. :key="formConfigKey"
  235. @on-event-add="handleEventAdd"
  236. @on-event-edit="handleEventEdit"
  237. @on-event-remove="handleEventRemove"
  238. @on-datasource-edit="handleDatasourceEdit"
  239. @on-class-edit="handleClassEdit"
  240. :form-key="formKey"
  241. :field-models="fieldModels"
  242. >
  243. <template #widgetconfig="{type, customProps, data}">
  244. <slot name="widgetconfig" :type="type" :customProps="customProps" :data="data"></slot>
  245. </template>
  246. </widget-config>
  247. <form-config v-show="configTab=='form'" ref="formConfig"
  248. :sheets="styleSheetsArray"
  249. :data="widgetForm.config"
  250. @on-style-update="onStyleUpdate"
  251. @on-datasource-update="onDataSourceUpdate"
  252. @on-eventscript-update="onEventScriptUpdate"
  253. @on-eventscript-confirm="onEventScriptConfirm"
  254. :form-key="formKey"
  255. ></form-config>
  256. </el-main>
  257. </el-container>
  258. <div class="container-right-arrow" @click="handleRightToggle"></div>
  259. </el-aside>
  260. <preview-dialog ref="previewDialog" @get-data-success="preivewGetData"></preview-dialog>
  261. <import-json-dialog ref="importJsonDialog" @load-json="handleLoadJson"></import-json-dialog>
  262. <cus-dialog
  263. :visible="jsonVisible"
  264. @on-close="jsonVisible = false"
  265. ref="jsonPreview"
  266. width="800px"
  267. :title="jsonTitle"
  268. >
  269. <code-editor height="400px" mode="json" v-model="jsonTemplate" wrap></code-editor>
  270. <template #action>
  271. <el-button type="primary" class="json-btn" :data-clipboard-text="jsonCopyValue">{{$t('fm.actions.copyData')}}</el-button>
  272. <el-button type="primary" @click="handleExportJSON">{{$t('fm.actions.export')}}</el-button>
  273. </template>
  274. </cus-dialog>
  275. <cus-dialog
  276. :visible="codeVisible"
  277. @on-close="codeVisible = false"
  278. ref="codePreview"
  279. width="800px"
  280. :title="$t('fm.actions.code')"
  281. >
  282. <el-tabs type="border-card" style="box-shadow: none;" v-model="codeActiveName">
  283. <el-tab-pane label="Vue Component" name="vue">
  284. <code-editor height="450px" mode="html" v-model="vueTemplate"></code-editor>
  285. </el-tab-pane>
  286. <el-tab-pane label="HTML" name="html">
  287. <code-editor height="450px" mode="html" v-model="htmlTemplate"></code-editor>
  288. </el-tab-pane>
  289. </el-tabs>
  290. <template #action>
  291. <el-button type="primary" class="code-btn" :data-clipboard-text="codeCopyValue">{{$t('fm.actions.copyData')}}</el-button>
  292. <el-button type="primary" @click="handleExport">{{$t('fm.actions.export')}}</el-button>
  293. </template>
  294. </cus-dialog>
  295. </el-container>
  296. </el-main>
  297. <el-footer height="30px" style="font-weight: 600;"><a target="_blank" href="https://form.making.link">Powered by FormMaking {{version ? `@ ${version}` : ''}}</a></el-footer>
  298. </el-container>
  299. </template>
  300. <script>
  301. import Draggable from 'vuedraggable/src/vuedraggable'
  302. import WidgetConfig from './WidgetConfig.vue'
  303. import FormConfig from './FormConfig.vue'
  304. import WidgetForm from './WidgetForm.vue'
  305. import VantWidgetForm from './VantWidget/WidgetForm.vue'
  306. import AntdvWidgetForm from './AntdvWidget/WidgetForm.vue'
  307. import CusDialog from './CusDialog.vue'
  308. import ClipboardJS from 'clipboard'
  309. import CodeEditor from './CodeEditor/index.vue'
  310. import {basicComponents, layoutComponents, advanceComponents, collectionComponents} from './componentsConfig.js'
  311. import {updateStyleSheets, splitStyleSheets, splitSheetName, addClass, removeClass} from '../util/index.js'
  312. import { EventBus } from '../util/event-bus.js'
  313. import generateCode from './generateCode.js'
  314. import historyManager from '../util/history-manager.js'
  315. import _ from 'lodash'
  316. import { UpgradeData } from '../util/version-upgrade'
  317. import PreviewDialog from './PreviewDialog.vue'
  318. import ImportJsonDialog from './ImportJson/dialog.vue'
  319. import Outline from './Outline.vue'
  320. import { findModelNodeString } from '../util/find-node.js'
  321. import { getModels } from '../util/model-outline.js'
  322. import { ElMessage } from '../util/message.js'
  323. export default {
  324. name: 'fm-making-form',
  325. components: {
  326. Draggable,
  327. WidgetConfig,
  328. FormConfig,
  329. WidgetForm,
  330. VantWidgetForm,
  331. CusDialog,
  332. CodeEditor,
  333. PreviewDialog,
  334. ImportJsonDialog,
  335. Outline,
  336. AntdvWidgetForm
  337. },
  338. props: {
  339. preview: {
  340. type: Boolean,
  341. default: false
  342. },
  343. generateCode: {
  344. type: Boolean,
  345. default: false
  346. },
  347. generateJson: {
  348. type: Boolean,
  349. default: false
  350. },
  351. upload: {
  352. type: Boolean,
  353. default: false
  354. },
  355. clearable: {
  356. type: Boolean,
  357. default: false
  358. },
  359. basicFields: {
  360. type: Array,
  361. default: () => ['input', 'textarea', 'number', 'radio', 'checkbox', 'time', 'date', 'rate', 'color', 'select', 'switch', 'slider', 'text', 'html', 'button', 'link', 'cascader', 'treeselect', 'steps', 'transfer', 'pagination']
  362. },
  363. advanceFields: {
  364. type: Array,
  365. default: () => ['blank', 'component', 'fileupload', 'imgupload', 'editor', 'list']
  366. },
  367. layoutFields: {
  368. type: Array,
  369. default: () => ['grid', 'report', 'tabs', 'collapse', 'inline', 'card', 'divider', 'alert']
  370. },
  371. collectionFields: {
  372. type: Array,
  373. default: () => ['table', 'subform', 'dialog', 'group']
  374. },
  375. customFields: {
  376. type: Array,
  377. default: () => []
  378. },
  379. globalConfig: {
  380. type: Object,
  381. default: () => ({})
  382. },
  383. fieldConfig: {
  384. type: Array,
  385. default: () => []
  386. },
  387. name: {
  388. type: String,
  389. default: ''
  390. },
  391. cache: {
  392. type: Boolean,
  393. default: false
  394. },
  395. jsonTemplates: {
  396. type: Array,
  397. default: () => []
  398. },
  399. initFromTemplate: {
  400. type :Boolean,
  401. default: false
  402. },
  403. fieldModels: {
  404. type: Array,
  405. default: () => []
  406. },
  407. panel: {
  408. type: String,
  409. default: 'field'
  410. },
  411. zIndex: {
  412. type: Number,
  413. default: 2000
  414. },
  415. useAntdForm: {
  416. type: Boolean,
  417. default: false
  418. },
  419. useVantForm: {
  420. type: Boolean,
  421. default: false
  422. }
  423. },
  424. emits: ['ready', 'preview'],
  425. computed: {
  426. isVantMobile () {
  427. return this.widgetForm.config.mobileVant && this.platform === 'mobile'
  428. }
  429. },
  430. data () {
  431. return {
  432. version: window.FormMaking_OPTIONS['version'],
  433. basicComponents,
  434. layoutComponents,
  435. advanceComponents,
  436. collectionComponents,
  437. customComponents: [],
  438. resetJson: false,
  439. widgetForm: {
  440. list: [],
  441. config: {
  442. labelWidth: 100,
  443. labelPosition: 'left',
  444. size: 'default',
  445. customClass: '',
  446. ui: 'element',
  447. layout: 'horizontal',
  448. width: '100%',
  449. hideLabel: false,
  450. hideErrorMessage: false
  451. },
  452. },
  453. configTab: 'form',
  454. widgetFormSelect: null,
  455. previewVisible: false,
  456. jsonVisible: false,
  457. codeVisible: false,
  458. uploadVisible: false,
  459. blank: '',
  460. htmlTemplate: '',
  461. jsonTemplate: '',
  462. vueTemplate: '',
  463. uploadEditor: null,
  464. jsonCopyValue: '',
  465. jsonClipboard: null,
  466. codeCopyValue: '',
  467. codeClipboard: null,
  468. codeActiveName: 'vue',
  469. undo: false,
  470. redo: false,
  471. formKey: Math.random().toString(36).slice(-8),
  472. formConfigKey: Math.random().toString(36).slice(-8),
  473. styleSheetsArray: [],
  474. dataSourceArray: [],
  475. eventScriptArray: [],
  476. platform: 'pc',
  477. activeFields: ['basic', 'advance', 'layout', 'custom', 'collection'],
  478. activeLeft: this.panel,
  479. isScrollTo: true,
  480. jsonTitle: this.$t('fm.actions.json'),
  481. modelNode: '',
  482. rightHide: false,
  483. leftHide: false,
  484. fullscreen: false,
  485. showDo: false,
  486. dragging: false
  487. }
  488. },
  489. created () {
  490. this._loadComponents()
  491. },
  492. provide () {
  493. return {
  494. 'changeConfigTab': this.changeConfigTab,
  495. 'getModelNode': this.getModelNode,
  496. 'isMobile': this.isMobile,
  497. 'getFormModels': this.getFormModels,
  498. 'getFormFields': this.getFormFields,
  499. 'getDataSourceArray': this.getDataSourceArray,
  500. 'useAntdForm': this.useAntdForm,
  501. 'setDragging': this.setDragging,
  502. 'getDragging': this.getDragging,
  503. 'useVantForm': this.useVantForm,
  504. 'getVantMobile': () => this.isVantMobile
  505. }
  506. },
  507. mounted () {
  508. const _this = this
  509. // 添加表单默认事件
  510. this.widgetForm.config.eventScript = [
  511. {key: 'mounted', name: 'mounted', func: ''},
  512. {key: 'refresh', name: 'refresh', func: ''},
  513. {key: 'onFormChange', name: 'onFormChange', type: 'rule'}
  514. ]
  515. // 加载全局配置项
  516. this.widgetForm.config = {
  517. ...this.widgetForm.config,
  518. ...this.globalConfig
  519. }
  520. this.platform = this.widgetForm.config.platform || 'pc'
  521. this.initConfig()
  522. this.cache && this.setJSON(localStorage.getItem('fmjson'+this.name) ?? this.widgetForm)
  523. this.$emit('ready')
  524. window.onbeforeunload = (e) => {
  525. this.saveJsonCache()
  526. }
  527. // 从模板导入
  528. if (this.initFromTemplate) {
  529. this.handleUpload()
  530. }
  531. historyManager.clear().then(() => {
  532. this.showDo = true
  533. EventBus.$on('on-history-add-' + this.formKey, () => {
  534. historyManager.add(this.widgetForm, (this.widgetFormSelect && this.widgetFormSelect.key) ? this.widgetFormSelect.key : '').then(() => {
  535. _this.undo = true
  536. _this.redo = false
  537. })
  538. this.saveJsonCache()
  539. })
  540. })
  541. },
  542. beforeUnmount () {
  543. EventBus.$off('on-history-add-' + this.formKey)
  544. this.saveJsonCache()
  545. },
  546. methods: {
  547. handlePull (a, b, c) {
  548. if (c.className.split(' ').indexOf('is-disabled') >= 0) {
  549. return false
  550. }
  551. return 'clone'
  552. },
  553. setDragging (dragging) {
  554. this.dragging = dragging
  555. },
  556. getDragging () {
  557. return this.dragging
  558. },
  559. generatePreviewQrcode (url) {
  560. this.$refs.previewDialog.generateQrcode(url)
  561. },
  562. handleRightToggle () {
  563. if (this.rightHide) {
  564. removeClass(this.$refs['widgetConfigContainer'].$el, 'hide-status')
  565. this.rightHide = false
  566. } else {
  567. addClass(this.$refs['widgetConfigContainer'].$el, 'hide-status')
  568. this.rightHide = true
  569. }
  570. },
  571. handleLeftToggle () {
  572. if (this.leftHide) {
  573. removeClass(this.$refs['widgetLeftPanel'].$el, 'hide-status')
  574. this.leftHide = false
  575. } else {
  576. addClass(this.$refs['widgetLeftPanel'].$el, 'hide-status')
  577. this.leftHide = true
  578. }
  579. },
  580. saveJsonCache () {
  581. this.cache && localStorage.setItem('fmjson'+this.name, JSON.stringify(this.widgetForm))
  582. },
  583. removeJsonCache () {
  584. localStorage.setItem('fmjson'+this.name, JSON.stringify(this.widgetForm))
  585. },
  586. initConfig () {
  587. this.platform = this.widgetForm.config.platform || 'pc'
  588. this.onStyleUpdate(splitStyleSheets(this.widgetForm.config.styleSheets || ''))
  589. this.onDataSourceUpdate(this.widgetForm.config.dataSource || [])
  590. this.onEventScriptUpdate(this.widgetForm.config.eventScript || [])
  591. this.formConfigKey = Math.random().toString(36).slice(-8)
  592. },
  593. handleGoGithub () {
  594. window.location.href = 'https://github.com/GavinZhuLei/vue-form-making'
  595. },
  596. handleConfigSelect (value) {
  597. this.configTab = value
  598. },
  599. handleMoveEnd (evt) {
  600. },
  601. handleMoveStart ({oldIndex}) {
  602. },
  603. handleMove () {
  604. return true
  605. },
  606. handlePreview () {
  607. this.$emit('preview', _.cloneDeep(this.widgetForm))
  608. this.$refs.previewDialog.preview(_.cloneDeep(this.widgetForm), this.platform)
  609. },
  610. preivewGetData (data) {
  611. this.jsonTitle = this.$t('fm.actions.getData')
  612. this.jsonVisible = true
  613. this.jsonTemplate = data
  614. this.$nextTick(() => {
  615. this.jsonClipboard = new ClipboardJS(document.getElementsByClassName('json-btn')[0])
  616. this.jsonClipboard.on('success', (e) => {
  617. ElMessage({
  618. message: this.$t('fm.message.copySuccess'),
  619. type: 'success'
  620. }, this)
  621. })
  622. this.jsonCopyValue = JSON.stringify(data)
  623. })
  624. },
  625. handleGenerateJson () {
  626. this.jsonTitle = this.$t('fm.actions.json')
  627. this.jsonVisible = true
  628. this.jsonTemplate = this.widgetForm
  629. this.$nextTick(() => {
  630. this.jsonClipboard = new ClipboardJS(document.getElementsByClassName('json-btn')[0])
  631. this.jsonClipboard.on('success', (e) => {
  632. ElMessage({
  633. message: this.$t('fm.message.copySuccess'),
  634. type: 'success'
  635. }, this)
  636. e.clearSelection()
  637. })
  638. this.jsonCopyValue = JSON.stringify(this.widgetForm)
  639. })
  640. },
  641. handleGenerateCode () {
  642. this.codeVisible = true
  643. this.htmlTemplate = generateCode(JSON.stringify(this.widgetForm), 'html', this.widgetForm.config.ui, this.isVantMobile)
  644. this.vueTemplate = generateCode(JSON.stringify(this.widgetForm), 'vue', this.widgetForm.config.ui, this.isVantMobile)
  645. this.$nextTick(() => {
  646. this.codeClipboard = new ClipboardJS(document.getElementsByClassName('code-btn')[0])
  647. this.codeClipboard.on('success', (e) => {
  648. ElMessage({
  649. message: this.$t('fm.message.copySuccess'),
  650. type: 'success'
  651. }, this)
  652. })
  653. this.codeCopyValue = this.codeActiveName == 'vue' ? this.vueTemplate : this.htmlTemplate
  654. })
  655. },
  656. handleUpload () {
  657. this.$refs.importJsonDialog.open(this.jsonTemplates)
  658. },
  659. handleLoadJson (json) {
  660. try {
  661. this.setJSON(json)
  662. } catch (e) {
  663. ElMessage({
  664. message: e.message,
  665. type: 'error'
  666. }, this)
  667. }
  668. },
  669. handleClear () {
  670. this.widgetForm = {
  671. ...this.widgetForm,
  672. list: [],
  673. }
  674. this.widgetFormSelect = {}
  675. this.$nextTick(() => {
  676. EventBus.$emit('on-history-add-' + this.formKey)
  677. })
  678. },
  679. clear () {
  680. this.handleClear()
  681. },
  682. getJSON () {
  683. return JSON.stringify(this.widgetForm)
  684. },
  685. getHtml (type = 'html', ui = 'element') {
  686. return generateCode(JSON.stringify(this.widgetForm), type, ui, this.isVantMobile)
  687. },
  688. setJSON (json) {
  689. if (typeof json === 'string') {
  690. json = JSON.parse(json)
  691. }
  692. this.widgetForm = _.cloneDeep({
  693. ...json,
  694. list: json.list ?? json.list.map(item => UpgradeData(item))
  695. })
  696. if (this.widgetForm.config?.eventScript) {
  697. this.widgetForm.config.eventScript.findIndex(item => item.key === 'onFormChange') < 0
  698. && this.widgetForm.config.eventScript.unshift({key: 'onFormChange', name: 'onFormChange', type: 'rule'})
  699. this.widgetForm.config.eventScript.findIndex(item => item.key === 'refresh') < 0
  700. && this.widgetForm.config.eventScript.unshift({key: 'refresh', name: 'refresh', func: ''})
  701. this.widgetForm.config.eventScript.findIndex(item => item.key === 'mounted') < 0
  702. && this.widgetForm.config.eventScript.unshift({key: 'mounted', name: 'mounted', func: ''})
  703. }
  704. if (json.list.length> 0) {
  705. this.widgetFormSelect = this.widgetForm.list[0]
  706. } else {
  707. this.widgetFormSelect = {}
  708. }
  709. this.initConfig()
  710. this.$nextTick(() => { EventBus.$emit('on-history-add-' + this.formKey) })
  711. },
  712. handleInput (val) {
  713. this.blank = val
  714. },
  715. handleField (item) {
  716. EventBus.$emit('on-field-add-' + this.formKey, item)
  717. },
  718. handleUndo () {
  719. if (this.undo) {
  720. historyManager.updateLatest(this.widgetForm, (this.widgetFormSelect && this.widgetFormSelect.key) ? this.widgetFormSelect.key : '').then(() => {
  721. historyManager.undo().then((data) => {
  722. this.widgetForm = {...data.data}
  723. this.widgetFormSelect = this._findWidgetItem(this.widgetForm.list, data.key)
  724. this.undo = data.undo
  725. this.redo = data.redo
  726. this.initConfig()
  727. })
  728. })
  729. }
  730. },
  731. handleRedo () {
  732. if (this.redo) {
  733. historyManager.redo().then((data) => {
  734. this.widgetForm = {...data.data}
  735. this.widgetFormSelect = this._findWidgetItem(this.widgetForm.list, data.key)
  736. this.undo = data.undo
  737. this.redo = data.redo
  738. this.initConfig()
  739. })
  740. }
  741. },
  742. handleFullScreen() {
  743. this.fullscreen = true
  744. },
  745. handleExitFullScreen () {
  746. this.fullscreen = false
  747. },
  748. _findWidgetItem (list, key, type = 'key') {
  749. const index = list.findIndex(item => item[type] == key)
  750. if (index >= 0) {
  751. return list[index]
  752. } else {
  753. for (let m = 0; m < list.length; m++) {
  754. const item = list[m]
  755. if (item.type === 'grid') {
  756. let findItem = this._findWidgetItem(item.columns, key, type)
  757. if (findItem.key) {
  758. return findItem
  759. }
  760. for (let i = 0; i < item.columns.length; i++) {
  761. let findItem = this._findWidgetItem(item.columns[i].list, key, type)
  762. if (findItem.key) {
  763. return findItem
  764. }
  765. }
  766. }
  767. if (item.type === 'table') {
  768. let findItem = this._findWidgetItem(item.tableColumns, key, type)
  769. if (findItem.key) {
  770. return findItem
  771. }
  772. }
  773. if (item.type === 'subform') {
  774. let findItem = this._findWidgetItem(item.list, key, type)
  775. if (findItem.key) {
  776. return findItem
  777. }
  778. }
  779. if (item.type === 'tabs') {
  780. for (let i = 0; i < item.tabs.length; i++) {
  781. let findItem = this._findWidgetItem(item.tabs[i].list, key, type)
  782. if (findItem.key) {
  783. return findItem
  784. }
  785. }
  786. }
  787. if (item.type === 'collapse') {
  788. for (let i = 0; i < item.tabs.length; i++) {
  789. let findItem = this._findWidgetItem(item.tabs[i].list, key, type)
  790. if (findItem.key) {
  791. return findItem
  792. }
  793. }
  794. }
  795. if (item.type === 'report') {
  796. for (let r = 0; r < item.rows.length; r++) {
  797. let findItem = this._findWidgetItem(item.rows[r].columns, key, type)
  798. if (findItem.key) {
  799. return findItem
  800. }
  801. for (let c = 0; c < item.rows[r].columns.length; c++) {
  802. let findItem = this._findWidgetItem(item.rows[r].columns[c].list, key, type)
  803. if (findItem.key) {
  804. return findItem
  805. }
  806. }
  807. }
  808. }
  809. if (item.type === 'inline') {
  810. let findItem = this._findWidgetItem(item.list, key, type)
  811. if (findItem.key) {
  812. return findItem
  813. }
  814. }
  815. if (item.type === 'dialog') {
  816. let findItem = this._findWidgetItem(item.list, key, type)
  817. if (findItem.key) {
  818. return findItem
  819. }
  820. }
  821. if (item.type === 'card') {
  822. let findItem = this._findWidgetItem(item.list, key, type)
  823. if (findItem.key) {
  824. return findItem
  825. }
  826. }
  827. if (item.type === 'group') {
  828. let findItem = this._findWidgetItem(item.list, key, type)
  829. if (findItem.key) {
  830. return findItem
  831. }
  832. }
  833. }
  834. return {}
  835. }
  836. },
  837. _loadComponents () {
  838. this.basicComponents = this.basicComponents.map(item => {
  839. return {
  840. ...item,
  841. name: this.$t(`fm.components.fields.${item.type}`),
  842. options: (() => {
  843. let newField = this.fieldConfig.find(o => o.type == item.type)
  844. if (newField) {
  845. return {...item.options, ...newField.options}
  846. } else {
  847. return {...item.options}
  848. }
  849. })()
  850. }
  851. })
  852. this.advanceComponents = this.advanceComponents.map(item => {
  853. return {
  854. ...item,
  855. name: this.$t(`fm.components.fields.${item.type}`),
  856. options: (() => {
  857. let newField = this.fieldConfig.find(o => o.type == item.type)
  858. if (newField) {
  859. return {...item.options, ...newField.options}
  860. } else {
  861. return {...item.options}
  862. }
  863. })()
  864. }
  865. })
  866. this.layoutComponents = this.layoutComponents.map(item => {
  867. return {
  868. ...item,
  869. name: this.$t(`fm.components.fields.${item.type}`),
  870. options: (() => {
  871. let newField = this.fieldConfig.find(o => o.type == item.type)
  872. if (newField) {
  873. return {...item.options, ...newField.options}
  874. } else {
  875. return {...item.options}
  876. }
  877. })()
  878. }
  879. })
  880. this.collectionComponents = this.collectionComponents.map(item => {
  881. return {
  882. ...item,
  883. name: this.$t(`fm.components.fields.${item.type}`),
  884. options: (() => {
  885. let newField = this.fieldConfig.find(o => o.type == item.type)
  886. if (newField) {
  887. return {...item.options, ...newField.options}
  888. } else {
  889. return {...item.options}
  890. }
  891. })()
  892. }
  893. })
  894. this.customComponents = this.customFields.map(item => {
  895. return {
  896. ...item,
  897. type: 'custom',
  898. options: (() => {
  899. let newField = this.fieldConfig.find(o => o.type == item.type)
  900. if (newField) {
  901. return {...item.options, ...newField.options}
  902. } else {
  903. return {...item.options}
  904. }
  905. })()
  906. }
  907. })
  908. },
  909. onStyleUpdate (sheets) {
  910. let head = '.fm-' + this.formKey + ' '
  911. updateStyleSheets(sheets, head)
  912. this.styleSheetsArray = splitSheetName(sheets)
  913. },
  914. onDataSourceUpdate (dataSource) {
  915. this.dataSourceArray = dataSource.map(item => ({
  916. value: item.key,
  917. label: item.name,
  918. args: item.args ? Object.fromEntries(item.args.map(o => [o, ''])) : {}
  919. }))
  920. },
  921. onEventScriptUpdate (eventScript) {
  922. this.eventScriptArray = eventScript.map(item => ({
  923. value: item.key,
  924. label: item.name
  925. }))
  926. },
  927. onEventScriptConfirm (eventObj) {
  928. this.$refs.widgetConfig.setEvent(eventObj)
  929. this.$nextTick(() => {
  930. EventBus.$emit('on-history-add-' + this.formKey)
  931. })
  932. },
  933. handlePlatform (platform) {
  934. this.widgetForm.config.platform = this.platform = platform
  935. },
  936. handleExport () {
  937. const fileName = (new Date().getTime()) + '.' + this.codeActiveName
  938. const fileData = this.codeActiveName == 'vue' ? this.vueTemplate : this.htmlTemplate
  939. this._exportFile(fileData, fileName)
  940. },
  941. handleExportJSON () {
  942. this._exportFile(JSON.stringify(this.jsonTemplate), (new Date().getTime()) + '.json')
  943. },
  944. handleClassEdit () {
  945. this.$refs.formConfig.editClass()
  946. },
  947. handleDatasourceEdit (datasource) {
  948. this.$refs.formConfig.editDatasource(datasource)
  949. },
  950. handleEventAdd (name) {
  951. this.$refs.formConfig.editScript(name)
  952. },
  953. handleEventEdit ({eventName, functionKey}) {
  954. this.$refs.formConfig.editScript(eventName, functionKey)
  955. },
  956. handleEventRemove (eventName) {
  957. this.widgetFormSelect.events[eventName] = ''
  958. },
  959. _exportFile (data, fileName) {
  960. let blob = new Blob([data], {
  961. type: 'application/octet-stream'
  962. })
  963. if(navigator.msSaveOrOpenBlob ){
  964. navigator.msSaveOrOpenBlob(blob, fileName);
  965. }else{
  966. // Create download link element
  967. let downloadLink = document.createElement("a");
  968. // Create a link to the file
  969. downloadLink.href = window.URL.createObjectURL(blob);
  970. // Setting the file name
  971. downloadLink.download = fileName;
  972. downloadLink.style.display = 'none'
  973. document.body.appendChild(downloadLink);
  974. //triggering the function
  975. downloadLink.click();
  976. document.body.removeChild(downloadLink);
  977. }
  978. },
  979. setSelect (field) {
  980. let selectWidget = this._findWidgetItem(this.widgetForm.list, field, 'model')
  981. if (selectWidget) {
  982. this.widgetFormSelect = selectWidget
  983. }
  984. },
  985. changeConfigTab (tab) {
  986. this.configTab = tab
  987. },
  988. onSelectWidget (key) {
  989. let selectWidget = this._findWidgetItem(this.widgetForm.list, key, 'key')
  990. if (selectWidget) {
  991. this.isScrollTo = false
  992. this.widgetFormSelect = selectWidget
  993. }
  994. setTimeout(() => {
  995. this.$refs.widgetForm.scrollTo()
  996. }, 200)
  997. },
  998. getModelNode () {
  999. return this.modelNode
  1000. },
  1001. isMobile () {
  1002. return this.platform === 'mobile'
  1003. },
  1004. getFormModels () {
  1005. return getModels(this.widgetForm.list)
  1006. },
  1007. getFormFields () {
  1008. return this._getFormFields(this.widgetForm.list)
  1009. },
  1010. _getFormFields (list, group = '', parent = '') {
  1011. let currentNode = []
  1012. for (let i = 0; i < list.length; i++) {
  1013. if (!list[i].type) continue
  1014. const curLabel = parent
  1015. ? [parent, `<${this.$t('fm.components.fields.' + list[i].type)}>${list[i].model ?? ''}`].join(' / ')
  1016. : `<${this.$t('fm.components.fields.' + list[i].type)}>${list[i].model ?? ''}`
  1017. currentNode.push({
  1018. id: list[i].model ? ( group ? group + '.' + list[i].model : list[i].model ) : list[i].key,
  1019. label: curLabel,
  1020. icon: list[i].icon,
  1021. type: list[i].type,
  1022. model: list[i].model,
  1023. children: [],
  1024. dataBind: list[i].options?.dataBind,
  1025. options: list[i].options,
  1026. disabled: (list[i].model ? false : true) || ['grid', 'report', 'tabs', 'collapse', 'inline', 'card', 'divider', 'alert'].includes(list[i].type),
  1027. hideDisabled: list[i].model ? false : true,
  1028. setdataDisabled: (list[i].model ? false : true) || ['grid', 'report', 'tabs', 'collapse', 'inline', 'card', 'divider', 'alert', 'button', 'link'].includes(list[i].type),
  1029. remoteOptionDisabled: !list[i].options.remote,
  1030. dynamicValueDisabled: !list[i].options.isDynamicValue,
  1031. dialogDisabled: list[i].type !== 'dialog',
  1032. optionDisabled: !(list[i].type == 'steps' || list[i].type == 'transfer' || Object.keys(list[i].options).indexOf('options')>=0 )
  1033. })
  1034. if (list[i].type == 'grid') {
  1035. currentNode[i].children = this._getFormFields(list[i].columns, group, curLabel)
  1036. }
  1037. if (list[i].type == 'col') {
  1038. currentNode[i].children = this._getFormFields(list[i].list, group, curLabel)
  1039. }
  1040. if (list[i].type == 'report') {
  1041. let reportList = []
  1042. for (let r = 0; r < list[i].rows.length; r++) {
  1043. for (let c = 0; c < list[i].rows[r].columns.length; c++) {
  1044. let td = list[i].rows[r].columns[c]
  1045. if (!td.options.invisible) {
  1046. reportList.push(td)
  1047. }
  1048. }
  1049. }
  1050. currentNode[i].children = this._getFormFields(reportList, group, curLabel)
  1051. }
  1052. if (list[i].type == 'td') {
  1053. currentNode[i].children = this._getFormFields(list[i].list, group, curLabel)
  1054. }
  1055. if (list[i].type == 'tabs') {
  1056. for (let t = 0; t < list[i].tabs.length; t++) {
  1057. currentNode[i].children.push({
  1058. label: list[i].tabs[t].label,
  1059. children: [],
  1060. disabled: true,
  1061. id: Math.random().toString(36).slice(-8),
  1062. setdataDisabled: true,
  1063. remoteOptionDisabled: true,
  1064. dialogDisabled: true
  1065. })
  1066. currentNode[i].children[t].children = this._getFormFields(list[i].tabs[t].list, group, curLabel)
  1067. }
  1068. }
  1069. if (list[i].type == 'collapse') {
  1070. for (let t = 0; t < list[i].tabs.length; t++) {
  1071. currentNode[i].children.push({
  1072. label: list[i].tabs[t].title,
  1073. children: [],
  1074. disabled: true,
  1075. id: Math.random().toString(36).slice(-8),
  1076. setdataDisabled: true,
  1077. remoteOptionDisabled: true,
  1078. dialogDisabled: true
  1079. })
  1080. currentNode[i].children[t].children = this._getFormFields(list[i].tabs[t].list, group, curLabel)
  1081. }
  1082. }
  1083. if (list[i].type == 'inline') {
  1084. currentNode[i].children = this._getFormFields(list[i].list, group, curLabel)
  1085. }
  1086. if (list[i].type == 'table') {
  1087. let curGroup = group ? group + '.' + list[i].model : list[i].model
  1088. currentNode[i].children = this._getFormFields(list[i].tableColumns, curGroup, curLabel)
  1089. }
  1090. if (list[i].type == 'subform') {
  1091. let curGroup = group ? group + '.' + list[i].model : list[i].model
  1092. currentNode[i].children = this._getFormFields(list[i].list, curGroup, curLabel)
  1093. }
  1094. if (list[i].type == 'dialog') {
  1095. let curGroup = group ? group + '.' + list[i].model : list[i].model
  1096. currentNode[i].children = this._getFormFields(list[i].list, curGroup, curLabel)
  1097. }
  1098. if (list[i].type == 'card') {
  1099. currentNode[i].children = this._getFormFields(list[i].list, group, curLabel)
  1100. }
  1101. if (list[i].type == 'group') {
  1102. let curGroup = group ? group + '.' + list[i].model : list[i].model
  1103. currentNode[i].children = this._getFormFields(list[i].list, curGroup, curLabel)
  1104. }
  1105. }
  1106. return currentNode
  1107. },
  1108. getDataSourceArray () {
  1109. return _.cloneDeep(this.dataSourceArray)
  1110. },
  1111. },
  1112. watch: {
  1113. '$i18n.locale': function (val) {
  1114. this._loadComponents()
  1115. },
  1116. codeActiveName (val) {
  1117. this.codeCopyValue = this.codeActiveName == 'vue' ? this.vueTemplate : this.htmlTemplate
  1118. },
  1119. widgetFormSelect (val) {
  1120. val.key && this.$refs.outlineRef?.setCurrentKey(val.key, this.isScrollTo)
  1121. this.isScrollTo = true
  1122. this.modelNode = findModelNodeString(this.widgetForm.list, 'key', val.key)
  1123. },
  1124. activeLeft (val) {
  1125. if (val == 'outline') {
  1126. this.widgetFormSelect?.key && this.$refs.outlineRef?.setCurrentKey(this.widgetFormSelect.key, this.isScrollTo)
  1127. }
  1128. },
  1129. 'widgetFormSelect.key': function (val) {
  1130. this.formConfigKey = Math.random().toString(36).slice(-8)
  1131. }
  1132. }
  1133. }
  1134. </script>
  1135. <style lang="scss">
  1136. .widget-empty {
  1137. background-position: 50%;
  1138. }
  1139. // 添加过渡动画样式
  1140. .v-enter-active,
  1141. .v-leave-active {
  1142. transition: opacity .3s ease, transform 0.3s ease;
  1143. }
  1144. .v-enter-from,
  1145. .v-leave-to {
  1146. opacity: 0;
  1147. transform: scale(0);
  1148. }
  1149. .v-enter-to,
  1150. .v-leave-from {
  1151. opacity: 1;
  1152. transform: scale(1);
  1153. }
  1154. </style>