UNPKG

mock-service-plugin

Version:
797 lines (711 loc) 23.9 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta name="renderer" content="webkit" /> <title>API Document List - Express Mock Server</title> <link href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css" rel="stylesheet" /> <link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css" rel="stylesheet" /> <style> body, html { height: 100%; background-color: #1a1a1a; color: #f0f0f0; } .main { height: calc(100% - 50px); } .hero.is-dark { padding: 0; height: 50px; } .hero-body { padding: 0px 100px; width: 100%; display: flex; justify-content: space-between; align-items: center; } .hero .title { font-size: 1.5rem; margin: 0; } .hero .title_sub { font-size: 1rem; margin: 0; } .main > .columns { height: 100%; } .sidebar { overflow: auto; background-color: #2c2c2c; border-right: 1px solid #444; } .search-bar { border-bottom: 1px solid #444; background-color: #333; } .docs { overflow: auto; background-color: #1e1e1e; position: relative; } .notification { margin-bottom: 0; border-radius: 0; background-color: #2a2a2a; color: #e0e0e0; border-bottom: 1px solid #444; } .docs pre { margin: 0; padding: 15px; background-color: inherit; max-height: calc(100vh - 200px); overflow: auto; white-space: pre-wrap; word-wrap: break-word; } .hljs { background: transparent !important; padding: 0 !important; display: block; overflow-x: auto; } .hljs-ln-numbers { padding-right: 1em; text-align: right; color: #666; user-select: none; } .file-info { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; } .file-info-item { flex: 1; min-width: 200px; } .file-info strong { color: #88c0d0; } .file-info a { color: #81a1c1; text-decoration: underline; } .file-info a:hover { color: #5e81ac; } .content-view { padding: 20px; } .image-preview { max-width: 100%; max-height: 500px; display: block; margin: 20px auto; box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); border: 1px solid #444; } .download-btn { display: inline-block; margin-top: 15px; padding: 8px 15px; background: #4c566a; color: white; border-radius: 4px; text-decoration: none; transition: background 0.3s; } .download-btn:hover { background: #5e81ac; } /* 添加弹窗样式 */ .modal { display: none; } .modal.is-active { display: flex; } .modal-background { background-color: rgba(0, 0, 0, 0.86); } .modal-content { background-color: #2a2a2a; } .modal-card { background-color: #2a2a2a; } .modal-card-head { background-color: #333; border-bottom: 1px solid #444; } .modal-card-body { background-color: #2a2a2a; } .modal-card-foot { background-color: #333; border-top: 1px solid #444; display: flex; justify-content: flex-end; align-items: center; } .modal-card-title { color: #f0f0f0; } .textarea { background-color: #333; color: #f0f0f0; border-color: #444; } .textarea:focus { border-color: #5e81ac; box-shadow: 0 0 0 0.125em rgba(94, 129, 172, 0.25); } .button.is-primary { background-color: #5e81ac; } .button.is-primary:hover { background-color: #81a1c1; } .button.is-light { background-color: #4c566a; color: #f0f0f0; } .button.is-light:hover { background-color: #5e81ac; } .content-type-tag { display: inline-block; padding: 3px 8px; background: #5e81ac; border-radius: 4px; font-size: 0.8em; margin-left: 10px; } .menu-list { display: flex; flex-direction: column; gap: 5px; } .menu-list li { display: flex; justify-content: space-between; align-items: center; padding-right: 5px; } .menu-list a { color: #d8dee9; transition: all 0.2s; border-left: 3px solid transparent; padding-left: 15px; } .menu-list li:hover { background: #3b4252; color: #88c0d0; border-left: 3px solid #81a1c1; } .menu-list li.is-active { background: #3b4252; color: #88c0d0; border-left: 3px solid #81a1c1; font-weight: bold; } [v-cloak] { display: none; } </style> </head> <body> <section class="hero is-dark"> <div class="hero-body"> <p class="title">Express Mock 服务器</p> <p class="title_sub">API 文档管理平台</p> </div> </section> <section id="app" class="main" v-cloak> <div class="columns is-mobile is-gapless"> <div class="column is-one-quarter sidebar"> <p class="notification search-bar"> <input class="input is-dark" type="text" placeholder="输入关键字搜索..." v-model="searchQuery" /> </p> <aside class="menu"> <ul class="menu-list"> <li v-for="it of filteredDocs" :key="it.routeKey" :class="{'is-active': menuActiveItem.routeKey === it.routeKey}" > <a @click="showContent(it)"> <span>{{it.restfulTemplateUrl}}</span> </a> <div style=" display: flex; justify-content: center; align-items: center; gap: 5px; " > <span class="content-type-tag">{{it.method}}</span> <button class="button is-small is-light" @click="openEditModal(it)" > 编辑 </button> </div> </li> </ul> </aside> </div> <!-- 添加编辑弹窗 --> <div class="modal" :class="{'is-active': showEditModal}"> <div class="modal-background" @click="closeEditModal"></div> <div class="modal-content"> <div class="modal-card"> <header class="modal-card-head"> <p class="modal-card-title">编辑内容</p> <button class="delete" aria-label="close" @click="closeEditModal" ></button> </header> <section class="modal-card-body"> <div class="field"> <div class="control"> <input class="textarea" v-model="editingItemContent.restfulTemplateUrl" rows="10" ></> </div> </div> </section> <footer class="modal-card-foot"> <button class="button is-primary" @click="saveEdit"> 确认 </button> <button class="button is-light" @click="closeEditModal"> 取消 </button> </footer> </div> </div> </div> <div class="column docs"> <div v-if="menuActiveItem.routeKey" class="notification"> <div class="file-info"> <div class="file-info-item"> <p><strong>文件名:</strong> {{menuActiveItem.fileName}}</p> </div> <div v-if="menuActiveItem.describe" class="file-info-item"> <p><strong>描述:</strong> {{menuActiveItem.describe}}</p> </div> <div class="file-info-item"> <p> <strong>类型:</strong> {{menuActiveItem.contentType || 'application/json'}} </p> </div> <div class="file-info-item"> <p> <strong>接口地址:</strong> <a :href="menuActiveItem.path" target="_blank" >{{menuActiveItem.path}}</a > </p> </div> </div> </div> <div v-else class="notification"> <p>请从左侧菜单选择要查看的接口文档</p> </div> <div class="content-view"> <!-- 文本内容展示 --> <pre v-if="isTextContent"><code ref="docsContent"></code></pre> <!-- 图片预览 --> <div v-if="isImageContent"> <img :src="menuActiveItem.restfulTemplateUrl" class="image-preview" alt="图片预览" /> <a :href="menuActiveItem.restfulTemplateUrl" target="_blank" class="download-btn" >下载图片</a > </div> <!-- PDF预览 --> <div v-if="isPdfContent"> <iframe :src="menuActiveItem.restfulTemplateUrl" width="100%" height="600px" frameborder="0" ></iframe> <a :href="menuActiveItem.restfulTemplateUrl" target="_blank" class="download-btn" >下载PDF</a > </div> <!-- 其他二进制文件 --> <div v-if="isBinaryContent && !isImageContent && !isPdfContent"> <p>该文件类型为二进制格式,无法直接预览</p> <a :href="menuActiveItem.restfulTemplateUrl" target="_blank" class="download-btn" >下载文件</a > </div> <!-- 加载状态 --> <div v-if="loading" class="has-text-centered"> <p>加载中...</p> </div> </div> </div> </div> </section> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/json.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/xml.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/css.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/javascript.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/markdown.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/yaml.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/plaintext.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script> <script> // filepath, // fileName, // fileExt, // routeKey, // method, // path, // query, // restfulTemplateUrl, // describe, // contentType, // content, (function () { const temp = { restfulTemplateUrl: '', path: '', fileName: '', contentType: '', describe: '', method: '' } /** * @typedef {Object} MenuItem * @property {string} filepath * @property {string} routeKey * @property {string} fileName * @property {string} fileExt * @property {string} method * @property {string} path * @property {string} query * @property {string} restfulTemplateUrl * @property {string} describe * @property {string} contentType * @property {string} content */ /** * @typedef {Object} VueInstance * @property {string} searchQuery * @property {Object} menuActiveItem * @property {MenuItem[]} menuList * @property {boolean} isTextContent * @property {boolean} isImageContent * @property {boolean} isPdfContent * @property {boolean} isBinaryContent * @property {boolean} loading * @property {boolean} showEditModal * @property {MenuItem|null} editingItemContent */ /** @type {VueInstance} */ var app = new Vue({ el: '#app', data: { // search key searchQuery: '', // select item menuActiveItem: temp, menuList: [], // content type flags isTextContent: false, isImageContent: false, isPdfContent: false, isBinaryContent: false, // loading state loading: false, // 编辑弹窗相关 showEditModal: false, editingItemContent: temp }, computed: { /** * @this {VueInstance} */ filteredDocs: function () { var self = this; if (self.searchQuery === '') { return self.menuList; } var searchRegex = new RegExp(self.searchQuery, 'i'); return self.menuList.filter(function (it) { return ( searchRegex.test(it.describe) || searchRegex.test(it.restfulTemplateUrl) || searchRegex.test(it.path) || searchRegex.test(it.method) || searchRegex.test(it.contentType || '') ); }); }, }, methods: { /** * @this {VueInstance} */ getContentTypeTag: function(contentType) { if (!contentType) return 'JSON'; const typeMap = { 'application/json': 'JSON', 'text/plain': 'TEXT', 'text/html': 'HTML', 'application/xml': 'XML', 'text/csv': 'CSV', 'application/pdf': 'PDF', 'image/png': 'PNG', 'image/jpeg': 'JPG', 'image/gif': 'GIF', 'image/svg+xml': 'SVG', 'text/css': 'CSS', 'application/javascript': 'JS', 'text/markdown': 'MD', 'application/x-yaml': 'YAML', 'text/event-stream': 'SSE' }; for (const [key, value] of Object.entries(typeMap)) { if (contentType.includes(key)) { return value; } } return contentType.split('/').pop().toUpperCase(); }, /** * @this {VueInstance} */ getHighlightLanguage: function(contentType) { if (!contentType) return 'json'; const langMap = { 'application/json': 'json', 'text/plain': 'plaintext', 'text/html': 'html', 'application/xml': 'xml', 'text/csv': 'plaintext', 'text/css': 'css', 'application/javascript': 'javascript', 'text/markdown': 'markdown', 'application/x-yaml': 'yaml', 'text/event-stream': 'plaintext' }; for (const [key, value] of Object.entries(langMap)) { if (contentType.includes(key)) { return value; } } return 'plaintext'; }, /** * @this {VueInstance} */ setContentFlags: function(contentType) { this.isTextContent = false; this.isImageContent = false; this.isPdfContent = false; this.isBinaryContent = false; if (!contentType) { this.isTextContent = true; return; } // 文本类型 if (contentType.startsWith('text/') || contentType.includes('json') || contentType.includes('xml') || contentType.includes('yaml') || contentType.includes('javascript') || contentType.includes('css')) { this.isTextContent = true; } // 图片类型 else if (contentType.startsWith('image/')) { this.isImageContent = true; this.isBinaryContent = true; } // PDF类型 else if (contentType.includes('pdf')) { this.isPdfContent = true; this.isBinaryContent = true; } // 其他二进制类型 else { this.isBinaryContent = true; } }, /** * @this {VueInstance} */ highlightContent: function(content, contentType) { const lang = this.getHighlightLanguage(contentType); // 确保内容被正确格式化 if (lang === 'json') { try { const jsonObj = JSON.parse(content); content = JSON.stringify(jsonObj, null, 2); } catch (e) { console.warn('JSON 格式化失败:', e); } } const result = hljs.highlight(content, { language: lang }).value; const docsContent = this.$refs.docsContent; docsContent.innerHTML = result; docsContent.className = `hljs language-${lang}`; // 添加行号 hljs.lineNumbersBlock(docsContent); // 确保代码块正确显示 this.$nextTick(() => { const pre = docsContent.parentElement; if (pre) { pre.style.whiteSpace = 'pre-wrap'; pre.style.wordWrap = 'break-word'; } }); }, /** * @this {VueInstance} */ showContent: function (it) { var that = this; that.menuActiveItem = it; that.loading = true; // 设置内容类型标志 that.setContentFlags(it.contentType); // 对于二进制内容,不需要请求数据 if (that.isBinaryContent) { that.loading = false; return; } // 对于文本内容,获取内容并高亮显示 $.ajax({ url: it.restfulTemplateUrl, method: it.method ?? 'GET', dataType: 'text', success: function (data) { // 如果是 JSON 内容,进行格式化 if (it.contentType && it.contentType.includes('json')) { try { // 尝试直接解析 const jsonObj = JSON.parse(data); data = JSON.stringify(jsonObj, null, 2); } catch (e) { // 尝试去除注释后再解析 try { const noComment = data.replace(/\/\*[\s\S]*?\*\//g, '').replace(/([^:]|^)\/\/.*$/gm, '$1'); const jsonObj = JSON.parse(noComment); data = JSON.stringify(jsonObj, null, 2); } catch (e2) { console.warn('JSON 格式化失败:', e2); // 保持原样 } } } that.highlightContent(data, it.contentType); that.loading = false; }, error: function (xhr) { // 如果请求失败,尝试直接显示内容 try { let content = xhr.responseText; // 如果是 JSON 内容,进行格式化 if (it.contentType && it.contentType.includes('json')) { try { const jsonObj = JSON.parse(content); content = JSON.stringify(jsonObj, null, 2); } catch (e) { try { const noComment = content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/([^:]|^)\/\/.*$/gm, '$1'); const jsonObj = JSON.parse(noComment); content = JSON.stringify(jsonObj, null, 2); } catch (e2) { console.warn('JSON 格式化失败:', e2); } } } that.highlightContent(content, it.contentType); } catch (e) { const docsContent = that.$refs.docsContent; docsContent.textContent = '无法解析内容: ' + e.message; docsContent.className = 'hljs language-plaintext'; } that.loading = false; } }); }, /** * @this {VueInstance} */ openEditModal: function(it) { this.editingItemContent = it; this.showEditModal = true; }, /** * @this {VueInstance} */ closeEditModal: function() { this.showEditModal = false; this.editingItemContent = temp; }, /** * @this {VueInstance} */ saveEdit: function() { try { this.showContent(this.editingItemContent); this.closeEditModal(); } catch (e) { console.log(e) alert('JSON 格式错误,请检查输入内容'); } } } }); // ejs output app.menuList = @menuList; // 默认显示第一个文档 if (app.menuList && app.menuList.length > 0) { app.$nextTick(() => { app.showContent(app.menuList[0]); }); } })(); </script> </body> </html>