previewCode.vue 9.4 KB

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