FormDrawer.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <template>
  2. <div>
  3. <el-drawer v-bind="$attrs" v-on="$listeners" @opened="onOpen" @close="onClose">
  4. <div style="height:100%">
  5. <el-row style="height:100%;overflow:auto">
  6. <el-col :md="24" :lg="12" class="left-editor">
  7. <div class="setting" title="资源引用" @click="showResource">
  8. <el-badge :is-dot="!!resources.length" class="item">
  9. <i class="el-icon-setting" />
  10. </el-badge>
  11. </div>
  12. <el-tabs v-model="activeTab" type="card" class="editor-tabs">
  13. <el-tab-pane name="html">
  14. <span slot="label">
  15. <i v-if="activeTab==='html'" class="el-icon-edit" />
  16. <i v-else class="el-icon-document" />
  17. template
  18. </span>
  19. </el-tab-pane>
  20. <el-tab-pane name="js">
  21. <span slot="label">
  22. <i v-if="activeTab==='js'" class="el-icon-edit" />
  23. <i v-else class="el-icon-document" />
  24. script
  25. </span>
  26. </el-tab-pane>
  27. <el-tab-pane name="css">
  28. <span slot="label">
  29. <i v-if="activeTab==='css'" class="el-icon-edit" />
  30. <i v-else class="el-icon-document" />
  31. css
  32. </span>
  33. </el-tab-pane>
  34. </el-tabs>
  35. <div v-show="activeTab==='html'" id="editorHtml" class="tab-editor" />
  36. <div v-show="activeTab==='js'" id="editorJs" class="tab-editor" />
  37. <div v-show="activeTab==='css'" id="editorCss" class="tab-editor" />
  38. </el-col>
  39. <el-col :md="24" :lg="12" class="right-preview">
  40. <div class="action-bar" :style="{'text-align': 'left'}">
  41. <span class="bar-btn" @click="runCode">
  42. <i class="el-icon-refresh" />
  43. 刷新
  44. </span>
  45. <span class="bar-btn" @click="exportFile">
  46. <i class="el-icon-download" />
  47. 导出vue文件
  48. </span>
  49. <span ref="copyBtn" class="bar-btn copy-btn">
  50. <i class="el-icon-document-copy" />
  51. 复制代码
  52. </span>
  53. <span class="bar-btn delete-btn" @click="$emit('update:visible', false)">
  54. <i class="el-icon-circle-close" />
  55. 关闭
  56. </span>
  57. </div>
  58. <iframe
  59. v-show="isIframeLoaded"
  60. ref="previewPage"
  61. class="result-wrapper"
  62. frameborder="0"
  63. src="preview.html"
  64. @load="iframeLoad"
  65. />
  66. <div v-show="!isIframeLoaded" v-loading="true" class="result-wrapper" />
  67. </el-col>
  68. </el-row>
  69. </div>
  70. </el-drawer>
  71. <resource-dialog
  72. :visible.sync="resourceVisible"
  73. :origin-resource="resources"
  74. @save="setResource"
  75. />
  76. </div>
  77. </template>
  78. <script>
  79. import { parse } from '@babel/parser'
  80. import ClipboardJS from 'clipboard'
  81. import { saveAs } from 'file-saver'
  82. import {
  83. makeUpHtml, vueTemplate, vueScript, cssStyle
  84. } from '@/components/generator/html'
  85. import { makeUpJs } from '@/components/generator/js'
  86. import { makeUpCss } from '@/components/generator/css'
  87. import { exportDefault, beautifierConf, titleCase } from '@/utils/index'
  88. import ResourceDialog from './ResourceDialog'
  89. import loadMonaco from '@/utils/loadMonaco'
  90. import loadBeautifier from '@/utils/loadBeautifier'
  91. const editorObj = {
  92. html: null,
  93. js: null,
  94. css: null
  95. }
  96. const mode = {
  97. html: 'html',
  98. js: 'javascript',
  99. css: 'css'
  100. }
  101. let beautifier
  102. let monaco
  103. export default {
  104. components: { ResourceDialog },
  105. props: ['formData', 'generateConf'],
  106. data() {
  107. return {
  108. activeTab: 'html',
  109. htmlCode: '',
  110. jsCode: '',
  111. cssCode: '',
  112. codeFrame: '',
  113. isIframeLoaded: false,
  114. isInitcode: false, // 保证open后两个异步只执行一次runcode
  115. isRefreshCode: false, // 每次打开都需要重新刷新代码
  116. resourceVisible: false,
  117. scripts: [],
  118. links: [],
  119. monaco: null
  120. }
  121. },
  122. computed: {
  123. resources() {
  124. return this.scripts.concat(this.links)
  125. }
  126. },
  127. watch: {},
  128. created() {
  129. },
  130. mounted() {
  131. window.addEventListener('keydown', this.preventDefaultSave)
  132. const clipboard = new ClipboardJS('.copy-btn', {
  133. text: trigger => {
  134. const codeStr = this.generateCode()
  135. this.$notify({
  136. title: '成功',
  137. message: '代码已复制到剪切板,可粘贴。',
  138. type: 'success'
  139. })
  140. return codeStr
  141. }
  142. })
  143. clipboard.on('error', e => {
  144. this.$message.error('代码复制失败')
  145. })
  146. },
  147. beforeDestroy() {
  148. window.removeEventListener('keydown', this.preventDefaultSave)
  149. },
  150. methods: {
  151. preventDefaultSave(e) {
  152. if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
  153. e.preventDefault()
  154. }
  155. },
  156. onOpen() {
  157. const { type } = this.generateConf
  158. this.htmlCode = makeUpHtml(this.formData, type)
  159. this.jsCode = makeUpJs(this.formData, type)
  160. this.cssCode = makeUpCss(this.formData)
  161. loadBeautifier(btf => {
  162. beautifier = btf
  163. this.htmlCode = beautifier.html(this.htmlCode, beautifierConf.html)
  164. this.jsCode = beautifier.js(this.jsCode, beautifierConf.js)
  165. this.cssCode = beautifier.css(this.cssCode, beautifierConf.html)
  166. loadMonaco(val => {
  167. monaco = val
  168. this.setEditorValue('editorHtml', 'html', this.htmlCode)
  169. this.setEditorValue('editorJs', 'js', this.jsCode)
  170. this.setEditorValue('editorCss', 'css', this.cssCode)
  171. if (!this.isInitcode) {
  172. this.isRefreshCode = true
  173. this.isIframeLoaded && (this.isInitcode = true) && this.runCode()
  174. }
  175. })
  176. })
  177. },
  178. onClose() {
  179. this.isInitcode = false
  180. this.isRefreshCode = false
  181. },
  182. iframeLoad() {
  183. if (!this.isInitcode) {
  184. this.isIframeLoaded = true
  185. this.isRefreshCode && (this.isInitcode = true) && this.runCode()
  186. }
  187. },
  188. setEditorValue(id, type, codeStr) {
  189. if (editorObj[type]) {
  190. editorObj[type].setValue(codeStr)
  191. } else {
  192. editorObj[type] = monaco.editor.create(document.getElementById(id), {
  193. value: codeStr,
  194. theme: 'vs-dark',
  195. language: mode[type],
  196. automaticLayout: true
  197. })
  198. }
  199. // ctrl + s 刷新
  200. editorObj[type].onKeyDown(e => {
  201. if (e.keyCode === 49 && (e.metaKey || e.ctrlKey)) {
  202. this.runCode()
  203. }
  204. })
  205. },
  206. runCode() {
  207. const jsCodeStr = editorObj.js.getValue()
  208. try {
  209. const ast = parse(jsCodeStr, { sourceType: 'module' })
  210. const astBody = ast.program.body
  211. if (astBody.length > 1) {
  212. this.$confirm(
  213. 'js格式不能识别,仅支持修改export default的对象内容',
  214. '提示',
  215. {
  216. type: 'warning'
  217. }
  218. )
  219. return
  220. }
  221. if (astBody[0].type === 'ExportDefaultDeclaration') {
  222. const postData = {
  223. type: 'refreshFrame',
  224. data: {
  225. generateConf: this.generateConf,
  226. html: editorObj.html.getValue(),
  227. js: jsCodeStr.replace(exportDefault, ''),
  228. css: editorObj.css.getValue(),
  229. scripts: this.scripts,
  230. links: this.links
  231. }
  232. }
  233. this.$refs.previewPage.contentWindow.postMessage(
  234. postData,
  235. location.origin
  236. )
  237. } else {
  238. this.$message.error('请使用export default')
  239. }
  240. } catch (err) {
  241. this.$message.error(`js错误:${err}`)
  242. console.error(err)
  243. }
  244. },
  245. generateCode() {
  246. const html = vueTemplate(editorObj.html.getValue())
  247. const script = vueScript(editorObj.js.getValue())
  248. const css = cssStyle(editorObj.css.getValue())
  249. return beautifier.html(html + script + css, beautifierConf.html)
  250. },
  251. exportFile() {
  252. this.$prompt('文件名:', '导出文件', {
  253. inputValue: `${+new Date()}.vue`,
  254. closeOnClickModal: false,
  255. inputPlaceholder: '请输入文件名'
  256. }).then(({ value }) => {
  257. if (!value) value = `${+new Date()}.vue`
  258. const codeStr = this.generateCode()
  259. const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
  260. saveAs(blob, value)
  261. })
  262. },
  263. showResource() {
  264. this.resourceVisible = true
  265. },
  266. setResource(arr) {
  267. const scripts = []; const
  268. links = []
  269. if (Array.isArray(arr)) {
  270. arr.forEach(item => {
  271. if (item.endsWith('.css')) {
  272. links.push(item)
  273. } else {
  274. scripts.push(item)
  275. }
  276. })
  277. this.scripts = scripts
  278. this.links = links
  279. } else {
  280. this.scripts = []
  281. this.links = []
  282. }
  283. }
  284. }
  285. }
  286. </script>
  287. <style lang="scss" scoped>
  288. @import '@/styles/mixin.scss';
  289. .tab-editor {
  290. position: absolute;
  291. top: 33px;
  292. bottom: 0;
  293. left: 0;
  294. right: 0;
  295. font-size: 14px;
  296. }
  297. .left-editor {
  298. position: relative;
  299. height: 100%;
  300. background: #1e1e1e;
  301. overflow: hidden;
  302. }
  303. .setting{
  304. position: absolute;
  305. right: 15px;
  306. top: 3px;
  307. color: #a9f122;
  308. font-size: 18px;
  309. cursor: pointer;
  310. z-index: 1;
  311. }
  312. .right-preview {
  313. height: 100%;
  314. .result-wrapper {
  315. height: calc(100vh - 33px);
  316. width: 100%;
  317. overflow: auto;
  318. padding: 12px;
  319. box-sizing: border-box;
  320. }
  321. }
  322. @include action-bar;
  323. ::v-deep .el-drawer__header {
  324. display: none;
  325. }
  326. </style>