mock-service-plugin
Version:
Simulated backend interface service
797 lines (711 loc) • 23.9 kB
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 ;
padding: 0 ;
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>