houzhongjian
2024-07-11 759b1c71011abd6b58c37d2566f3f3c208c2f1b2
提交 | 用户 | 时间
759b1c 1 <template>
H 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 } from '@/utils'
88 import ResourceDialog from './ResourceDialog'
89 import loadMonaco from '@/utils/loadMonaco'
90 import loadBeautifier from '@/utils/loadBeautifier'
91
92 const editorObj = {
93   html: null,
94   js: null,
95   css: null
96 }
97 const mode = {
98   html: 'html',
99   js: 'javascript',
100   css: 'css'
101 }
102 let beautifier
103 let monaco
104
105 export default {
106   components: { ResourceDialog },
107   props: ['formData', 'generateConf'],
108   data() {
109     return {
110       activeTab: 'html',
111       htmlCode: '',
112       jsCode: '',
113       cssCode: '',
114       codeFrame: '',
115       isIframeLoaded: false,
116       isInitcode: false, // 保证open后两个异步只执行一次runcode
117       isRefreshCode: false, // 每次打开都需要重新刷新代码
118       resourceVisible: false,
119       scripts: [],
120       links: [],
121       monaco: null
122     }
123   },
124   computed: {
125     resources() {
126       return this.scripts.concat(this.links)
127     }
128   },
129   watch: {},
130   created() {
131   },
132   mounted() {
133     window.addEventListener('keydown', this.preventDefaultSave)
134     const clipboard = new ClipboardJS('.copy-btn', {
135       text: trigger => {
136         const codeStr = this.generateCode()
137         this.$notify({
138           title: '成功',
139           message: '代码已复制到剪切板,可粘贴。',
140           type: 'success'
141         })
142         return codeStr
143       }
144     })
145     clipboard.on('error', e => {
146       this.$message.error('代码复制失败')
147     })
148   },
149   beforeDestroy() {
150     window.removeEventListener('keydown', this.preventDefaultSave)
151   },
152   methods: {
153     preventDefaultSave(e) {
154       if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
155         e.preventDefault()
156       }
157     },
158     onOpen() {
159       const { type } = this.generateConf
160       this.htmlCode = makeUpHtml(this.formData, type)
161       this.jsCode = makeUpJs(this.formData, type)
162       this.cssCode = makeUpCss(this.formData)
163
164       loadBeautifier(btf => {
165         beautifier = btf
166         this.htmlCode = beautifier.html(this.htmlCode, beautifierConf.html)
167         this.jsCode = beautifier.js(this.jsCode, beautifierConf.js)
168         this.cssCode = beautifier.css(this.cssCode, beautifierConf.html)
169
170         loadMonaco(val => {
171           monaco = val
172           this.setEditorValue('editorHtml', 'html', this.htmlCode)
173           this.setEditorValue('editorJs', 'js', this.jsCode)
174           this.setEditorValue('editorCss', 'css', this.cssCode)
175           if (!this.isInitcode) {
176             this.isRefreshCode = true
177             this.isIframeLoaded && (this.isInitcode = true) && this.runCode()
178           }
179         })
180       })
181     },
182     onClose() {
183       this.isInitcode = false
184       this.isRefreshCode = false
185     },
186     iframeLoad() {
187       if (!this.isInitcode) {
188         this.isIframeLoaded = true
189         this.isRefreshCode && (this.isInitcode = true) && this.runCode()
190       }
191     },
192     setEditorValue(id, type, codeStr) {
193       if (editorObj[type]) {
194         editorObj[type].setValue(codeStr)
195       } else {
196         editorObj[type] = monaco.editor.create(document.getElementById(id), {
197           value: codeStr,
198           theme: 'vs-dark',
199           language: mode[type],
200           automaticLayout: true
201         })
202       }
203       // ctrl + s 刷新
204       editorObj[type].onKeyDown(e => {
205         if (e.keyCode === 49 && (e.metaKey || e.ctrlKey)) {
206           this.runCode()
207         }
208       })
209     },
210     runCode() {
211       const jsCodeStr = editorObj.js.getValue()
212       try {
213         const ast = parse(jsCodeStr, { sourceType: 'module' })
214         const astBody = ast.program.body
215         if (astBody.length > 1) {
216           this.$confirm(
217             'js格式不能识别,仅支持修改export default的对象内容',
218             '提示',
219             {
220               type: 'warning'
221             }).catch(() => {});
222           return
223         }
224         if (astBody[0].type === 'ExportDefaultDeclaration') {
225           const postData = {
226             type: 'refreshFrame',
227             data: {
228               generateConf: this.generateConf,
229               html: editorObj.html.getValue(),
230               js: jsCodeStr.replace(exportDefault, ''),
231               css: editorObj.css.getValue(),
232               scripts: this.scripts,
233               links: this.links
234             }
235           }
236
237           this.$refs.previewPage.contentWindow.postMessage(
238             postData,
239             location.origin
240           )
241         } else {
242           this.$message.error('请使用export default')
243         }
244       } catch (err) {
245         this.$message.error(`js错误:${err}`)
246         console.error(err)
247       }
248     },
249     generateCode() {
250       const html = vueTemplate(editorObj.html.getValue())
251       const script = vueScript(editorObj.js.getValue())
252       const css = cssStyle(editorObj.css.getValue())
253       return beautifier.html(html + script + css, beautifierConf.html)
254     },
255     exportFile() {
256       this.$prompt('文件名:', '导出文件', {
257         inputValue: `${+new Date()}.vue`,
258         closeOnClickModal: false,
259         inputPlaceholder: '请输入文件名'
260       }).then(({ value }) => {
261         if (!value) value = `${+new Date()}.vue`
262         const codeStr = this.generateCode()
263         const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
264         saveAs(blob, value)
265       })
266     },
267     showResource() {
268       this.resourceVisible = true
269     },
270     setResource(arr) {
271       const scripts = []; const
272         links = []
273       if (Array.isArray(arr)) {
274         arr.forEach(item => {
275           if (item.endsWith('.css')) {
276             links.push(item)
277           } else {
278             scripts.push(item)
279           }
280         })
281         this.scripts = scripts
282         this.links = links
283       } else {
284         this.scripts = []
285         this.links = []
286       }
287     }
288   }
289 }
290 </script>
291
292 <style lang="scss" scoped>
293 @import '@/styles/mixin.scss';
294 .tab-editor {
295   position: absolute;
296   top: 33px;
297   bottom: 0;
298   left: 0;
299   right: 0;
300   font-size: 14px;
301 }
302 .left-editor {
303   position: relative;
304   height: 100%;
305   background: #1e1e1e;
306   overflow: hidden;
307 }
308 .setting{
309   position: absolute;
310   right: 15px;
311   top: 3px;
312   color: #a9f122;
313   font-size: 18px;
314   cursor: pointer;
315   z-index: 1;
316 }
317 .right-preview {
318   height: 100%;
319   .result-wrapper {
320     height: calc(100vh - 33px);
321     width: 100%;
322     overflow: auto;
323     padding: 12px;
324     box-sizing: border-box;
325   }
326 }
327 @include action-bar;
328 :deep(.el-drawer__header) {
329   display: none;
330 }
331 </style>