star7th 5 年之前
父节点
当前提交
baf79eaa29

+ 86 - 0
server/Application/Api/Controller/MockController.class.php

@@ -0,0 +1,86 @@
+<?php
+namespace Api\Controller;
+use Think\Controller;
+class MockController extends BaseController {
+
+    //添加
+    public function add(){
+        $page_id = I("page_id/d");  
+        $template = I("template");  
+        $login_user = $this->checkLogin();
+        $uid = $login_user['uid'] ;
+        $page = M("Page")->where(" page_id = '$page_id' ")->find();
+        if(!$this->checkItemCreator($uid , $page['item_id'])){
+            $this->sendError(10303);
+            return ;
+        }
+
+        $json = json_decode(htmlspecialchars_decode($template)) ;
+        if(!$json){
+            $this->sendError(10101,'为了服务器安全,只允许写符合json语法的字符串');
+            return ;
+        }
+        $unique_key = md5(time().rand()."gbgdhbdgtfgfK3@bv45342asfsdfjhyfgkj54fofgfbv45342asfsdg");
+        //假如已经该页面存在mock
+        $mock_page = D("Mock")->where(" page_id = '$page_id' ")->find();
+        if($mock_page){
+            $unique_key = $mock_page['unique_key'] ;
+            D("Mock")->where("page_id = '$page_id' ")->save(array(
+                "uid"=>$uid ,
+                "template"=>$template ,
+                "last_update_time" => date("Y-m-d H:i:s"),
+            ));
+        }else{
+            $id = D("Mock")->add(array(
+                "unique_key"=>$unique_key ,
+                "uid"=>$uid ,
+                "page_id"=>$page_id ,
+                "item_id"=> $page['item_id'] ,
+                "template"=>$template ,
+                "addtime" => date("Y-m-d H:i:s"),
+                "last_update_time" => date("Y-m-d H:i:s"),
+                "view_times"=>0 
+            ));
+        }
+
+        $this->sendResult(array(
+            "page_id"=>$page_id ,
+            "unique_key"=>$unique_key 
+        ));
+    }
+
+    // 根据页面id获取mock信息
+    public function infoByPageId(){
+        $page_id = I("page_id/d");  
+        $uid = $login_user['uid'] ;
+        $page = D("Mock")->where(" page_id = '$page_id' ")->find();
+        $login_user = $this->checkLogin(false);
+        if (!$this->checkItemVisit($login_user['uid'] , $page['item_id'])) {
+            $this->sendError(10103);
+            return;
+        }
+        $this->sendResult($page);
+    }
+
+    // 根据唯一key获取mock的响应数据
+    public function infoByKey(){
+        $unique_key = I("unique_key");  
+        $page = D("Mock")->where(" unique_key = '%s' ",array($unique_key))->find();
+        $template = $page['template'] ;
+        $res = http_post("http://127.0.0.1:7123/mock",array(
+            "template"=> htmlspecialchars_decode($page['template']) 
+        ));
+        if($res){
+            $json = json_decode($res) ;
+            if(!$json){
+                $this->sendError(10101,'为了服务器安全,只允许写符合json语法的字符串');
+                return ;
+            }
+            echo $res ;
+        }else{
+            echo "mock服务暂时不可用。网站管理员安装完showdoc后需要另行安装mock服务,详情请打开https://www.showdoc.com.cn/help";
+        }
+        
+    }
+
+}

+ 14 - 1
server/Application/Api/Controller/UpdateController.class.php

@@ -5,7 +5,7 @@ class UpdateController extends BaseController {
 
     //检测数据库并更新
     public function checkDb(){
-        $version_num = 3 ;
+        $version_num = 4 ;
         $db_version_num = D("Options")->get("db_version_num");
         if(!$db_version_num || $db_version_num < $version_num ){
             $r = $this->updateSqlite();
@@ -316,6 +316,19 @@ class UpdateController extends BaseController {
             `last_update_time` CHAR(2000) NOT NULL DEFAULT ''
             )";
         D("User")->execute($sql);
+        //创建mock表
+        $sql = "CREATE TABLE IF NOT EXISTS `mock` (
+            `id`  INTEGER PRIMARY KEY ,
+            `unique_key` CHAR(2000) NOT NULL DEFAULT '',
+            `uid` int(11) NOT NULL DEFAULT '0',
+            `page_id` int(11) NOT NULL DEFAULT '0',
+            `item_id` int(11) NOT NULL DEFAULT '0',
+            `view_times` int(11) NOT NULL DEFAULT '0',
+            `template` CHAR(2000) NOT NULL DEFAULT '',
+            `addtime` CHAR(2000) NOT NULL DEFAULT '',
+            `last_update_time` CHAR(2000) NOT NULL DEFAULT ''
+            )";
+        D("User")->execute($sql);
         //留个注释提醒自己,如果更新数据库结构,务必更改上面的$version_num
         //留个注释提醒自己,如果更新数据库结构,务必更改上面的$version_num
         //留个注释提醒自己,如果更新数据库结构,务必更改上面的$version_num

+ 1 - 0
server/Application/Common/Conf/config.php

@@ -20,6 +20,7 @@ return array(
         ':domain\s$'               => 'Home/Item/show?item_domain=:1',//item的个性域名
         'uid/:id\d'               => 'Home/Item/showByUid?uid=:1',
         'page/:id\d'               => 'Home/Page/single?page_id=:1',
+        'mock-data/:unique_key\s'               => 'Api/Mock/infoByKey?unique_key=:1',
     ),
     'URL_CASE_INSENSITIVE'=>true,
     'SHOW_ERROR_MSG'        =>  true,    // 显示错误信息,这样在部署模式下也能显示错误

+ 118 - 0
web_src/src/components/common/Mock.vue

@@ -0,0 +1,118 @@
+<template>
+  <div>
+    <el-dialog title="Mock" :visible="true" :close-on-click-modal="false" @close="callback()">
+      <el-form>
+        <p v-if="mock_url" style=" margin-bottom:20px;font-size: 16px">
+          Mock地址 :
+          <code>{{mock_url}}</code>
+          <i class="el-icon-document-copy" v-clipboard:copy="mock_url" v-clipboard:success="onCopy"></i>
+          &nbsp;
+          <el-button @click="callback(mock_url)" type="text">把地址插入文档中</el-button>
+        </p>
+        <el-input
+          type="textarea"
+          class="dialoContent"
+          placeholder="这里填写的是Mock接口的返回结果。你可以直接编辑/粘贴一段json字符串,支持使用MockJs语法(关于MockJs语法,可以查看下方的帮助说明按钮)。输入完毕后,点击保存,就会自动生成Mock地址"
+          :rows="20"
+          v-model="content"
+        ></el-input>
+        <p>
+          <el-button type="primary" @click="handleClick">{{$t('save')}}</el-button>&nbsp;
+          <el-tooltip
+            class="item"
+            effect="dark"
+            content="假如上面填写的是一段符合json语法的字符串,点此按钮可以对json字符串进行快速格式化(美化)"
+            placement="top"
+          >
+            <el-button @click="beautifyJson">json快速美化</el-button>
+          </el-tooltip>&nbsp;
+          &nbsp;
+          <a
+            href="https://www.showdoc.com.cn/p/d952ed6b7b5fb454df13dce74d1b41f8"
+            target="_blank"
+          >帮助说明</a>
+        </p>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="callback()">{{$t('goback')}}</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { unescapeHTML } from '@/models/page'
+export default {
+  name: 'JsonBeautify',
+  props: {
+    formLabelWidth: '120px',
+    callback: '',
+    page_id: ''
+  },
+  data() {
+    return {
+      content: '',
+      json_table_data: '',
+      mock_url: ''
+    }
+  },
+  methods: {
+    add() {
+      this.request('/api/mock/add', {
+        'page_id': this.page_id,
+        'template': this.content
+      }).then((data) => {
+        this.$message({
+          showClose: true,
+          message: '保存成功',
+          type: 'success'
+        })
+        this.mock_url = this.getUrl(data.data.unique_key)
+      })
+    },
+    infoByPageId() {
+      if (this.page_id <= 0) {
+        this.$alert('请先保存页面')
+        this.callback()
+        return
+      }
+      this.request('/api/mock/infoByPageId', {
+        'page_id': this.page_id
+      }).then((data) => {
+        if (data.data && data.data.unique_key && data.data.template) {
+          this.mock_url = this.getUrl(data.data.unique_key)
+          this.content = unescapeHTML(data.data.template)
+        }
+      })
+    },
+    getUrl(unique_key) {
+      if (DocConfig.server.indexOf('web') > -1) {
+        let server = window.location.protocol + '//' + window.location.host + window.location.pathname + 'index.php?s='
+        server = server.replace(/\/web/g, '/server')
+        return server + '/mock-data/' + unique_key
+      } else {
+        return window.location.protocol + '//' + window.location.host + '/server/index.php?s=' + '/mock-data/' + unique_key
+      }
+    },
+    handleClick() {
+      this.add()
+    },
+    beautifyJson() {
+      this.content = this.formatJson(this.content)
+    },
+    onCopy() {
+      this.$message(this.$t('copy_success'))
+    }
+  },
+  mounted() {
+    this.infoByPageId()
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.el-icon-document-copy {
+  cursor: pointer;
+}
+</style>

+ 26 - 3
web_src/src/components/page/edit/Index.vue

@@ -82,7 +82,13 @@
               <el-dropdown-item @click.native="ShowPasteTable">{{$t('paste_insert_table')}}</el-dropdown-item>
             </el-dropdown-menu>
           </el-dropdown>
-          <el-button type size="medium" @click="ShowRunApi">{{$t('http_test_api')}}</el-button>
+          <el-button v-if="lang =='zh-cn'" type size="medium" @click="showMockDialog = true">Mock</el-button>
+          <el-button
+            v-if="lang =='zh-cn'"
+            type
+            size="medium"
+            @click="ShowRunApi"
+          >{{$t('http_test_api')}}</el-button>
 
           <el-badge :value="attachment_count" class="item">
             <el-button type size="medium" @click="ShowAttachment">{{$t('attachment')}}</el-button>
@@ -131,6 +137,18 @@
         :cat_id="cat_id"
         ref="SortPage"
       ></SortPage>
+      <!-- mock -->
+      <Mock
+        :page_id="page_id"
+        v-if="showMockDialog"
+        :callback="(data)=>{
+        if(data){
+          insertValue(data);
+        }
+        showMockDialog = false;
+        }"
+        ref="Mock"
+      ></Mock>
     </el-container>
     <Footer></Footer>
     <div class></div>
@@ -165,6 +183,7 @@
 import Editormd from '@/components/common/Editormd'
 import JsonToTable from '@/components/common/JsonToTable'
 import JsonBeautify from '@/components/common/JsonBeautify'
+import Mock from '@/components/common/Mock'
 import TemplateList from '@/components/page/edit/TemplateList'
 import HistoryVersion from '@/components/page/edit/HistoryVersion'
 import AttachmentList from '@/components/page/edit/AttachmentList'
@@ -195,7 +214,9 @@ export default {
       catalogs: [],
       isLock: 0,
       intervalId: 0,
-      saving: false
+      saving: false,
+      showMockDialog: false,
+      lang: ''
     }
   },
   computed: {
@@ -241,7 +262,8 @@ export default {
     HistoryVersion,
     AttachmentList,
     PasteTable,
-    SortPage
+    SortPage,
+    Mock
   },
   methods: {
     // 获取页面内容
@@ -683,6 +705,7 @@ export default {
     this.remoteIsLock()
     /** 监听粘贴上传图片 **/
     document.addEventListener('paste', this.upload_paste_img)
+    this.lang = DocConfig.lang
   },
 
   beforeDestroy() {

+ 28 - 26
web_src/src/models/page.js

@@ -1,21 +1,23 @@
 // 处理页面数据相关的逻辑
 
+// 定义一个html反转义的函数
+const unescapeHTML = str =>
+  str.replace(
+    /&amp;|&lt;|&gt;|&#39;|&quot;/g,
+    tag =>
+      ({
+        '&amp;': '&',
+        '&lt;': '<',
+        '&gt;': '>',
+        '&#39;': "'",
+        '&quot;': '"'
+      }[tag] || tag)
+  )
+
 // 渲染来自runapi的文档
 const rederPageContent = (page_content, globalParams = {}) => {
   let obj
-  // 先定义一个html反转义的函数
-  const unescapeHTML = str =>
-    str.replace(
-      /&amp;|&lt;|&gt;|&#39;|&quot;/g,
-      tag =>
-        ({
-          '&amp;': '&',
-          '&lt;': '<',
-          '&gt;': '>',
-          '&#39;': "'",
-          '&quot;': '"'
-        }[tag] || tag)
-    )
+
   page_content = unescapeHTML(page_content)
   try {
     obj = JSON.parse(page_content)
@@ -79,18 +81,18 @@ const rederPageContent = (page_content, globalParams = {}) => {
     obj.request.headers[0].name
   ) {
     newContent += `
-##### Header 
+##### Header
 
 |header|必选|类型|说明|
 |:-----  |:-----|-----|
 `
     const headers = obj.request.headers
     headers.map(one => {
-      //如果名字为空,或者存在禁用的key且禁用状态生效中,则终止本条参数
-      if (!one.name || (one.disable && one.disable >= 1 ) ) return ;
+      // 如果名字为空,或者存在禁用的key且禁用状态生效中,则终止本条参数
+      if (!one.name || (one.disable && one.disable >= 1)) return
       newContent += `|${one.name} |${one.require > 0 ? '是' : '否'} |${
         one.type
-      } |${one.remark ? one.remark : '无'}   |
+        } |${one.remark ? one.remark : '无'}   |
 `
     })
   }
@@ -104,18 +106,18 @@ const rederPageContent = (page_content, globalParams = {}) => {
 |:-----  |:-----|-----|
 `
     params.map(one => {
-      //如果名字为空,或者存在禁用的key且禁用状态生效中,则终止本条参数
-      if (!one.name || (one.disable && one.disable >= 1 ) ) return ;
+      // 如果名字为空,或者存在禁用的key且禁用状态生效中,则终止本条参数
+      if (!one.name || (one.disable && one.disable >= 1)) return
       newContent += `|${one.name} |${one.require > 0 ? '是' : '否'} |${
         one.type
-      } |${one.remark ? one.remark : '无'}   |
+        } |${one.remark ? one.remark : '无'}   |
 `
     })
   }
 
   if (obj.request.params.mode == 'json' && params) {
     newContent += `
-##### 请求参数示例 
+##### 请求参数示例
 \`\`\`
 ${params}
 \`\`\`
@@ -136,14 +138,14 @@ ${params}
       if (!one.name) return
       newContent += `|${one.name} |${one.require > 0 ? '是' : '否'} |${
         one.type
-      } |${one.remark ? one.remark : '无'}   |
+        } |${one.remark ? one.remark : '无'}   |
 `
     })
   }
 
   if (obj.response.responseExample) {
     newContent += `
-##### 返回示例 
+##### 返回示例
 \`\`\`
 ${obj.response.responseExample}
    \`\`\`
@@ -156,7 +158,7 @@ ${obj.response.responseExample}
     obj.response.responseParamsDesc[0].name
   ) {
     newContent += `
-##### 返回参数说明 
+##### 返回参数说明
 
 |参数名|类型|说明|
 |:-----  |:-----|-----|
@@ -166,7 +168,7 @@ ${obj.response.responseExample}
       if (!one.name) return
       newContent += `|${one.name} |${one.type} |${
         one.remark ? one.remark : '无'
-      }   |
+        }   |
 `
     })
   }
@@ -181,4 +183,4 @@ ${obj.response.responseExample}
   return newContent
 }
 
-export { rederPageContent }
+export { rederPageContent, unescapeHTML }

+ 101 - 42
web_src/src/util.js

@@ -1,60 +1,119 @@
-//全局函数/变量
-export default{
-  install(Vue,options)
-  {
+// 全局函数/变量
+export default {
+  install(Vue, options) {
     Vue.prototype.getData = function () {
-      console.log('我是插件中的方法');
+      console.log('我是插件中的方法')
     }
 
-    //Vue.prototype.DocConfig = {
-     // "server":'http://127.0.0.1/showdoc.cc/server/index.php?s=',
-      //"server":'../server/index.php?s=',
-    //}
-    Vue.prototype.request = function(){
-    	
+    // Vue.prototype.DocConfig = {
+    // "server":'http://127.0.0.1/showdoc.cc/server/index.php?s=',
+    // "server":'../server/index.php?s=',
+    // }
+    Vue.prototype.request = function () {
+
     }
 
-    Vue.prototype.getRootPath = function(){
-        return window.location.protocol +'//' +window.location.host + window.location.pathname
+    Vue.prototype.getRootPath = function () {
+      return window.location.protocol + '//' + window.location.host + window.location.pathname
     }
 
-    /*判断是否是移动设备*/
-    Vue.prototype.isMobile = function (){
-      return navigator.userAgent.match(/iPhone|iPad|iPod|Android|android|BlackBerry|IEMobile/i) ? true : false; 
+    /* 判断是否是移动设备 */
+    Vue.prototype.isMobile = function () {
+      return !!navigator.userAgent.match(/iPhone|iPad|iPod|Android|android|BlackBerry|IEMobile/i)
     }
 
-    Vue.prototype.get_user_info = function(callback){
-        var that = this ;
-        var url = DocConfig.server+'/api/user/info';
-        var params = new URLSearchParams();
-        params.append('redirect_login', false);
-        that.axios.post(url, params)
-          .then(function (response) {
-            if (callback) {callback(response);};
-          });
+    Vue.prototype.get_user_info = function (callback) {
+      var that = this
+      var url = DocConfig.server + '/api/user/info'
+      var params = new URLSearchParams()
+      params.append('redirect_login', false)
+      that.axios.post(url, params)
+        .then(function (response) {
+          if (callback) { callback(response) };
+        })
     }
 
-    Vue.prototype.get_notice = function(callback){
-        var that = this ;
-        var url = DocConfig.server+'/api/notice/getList';
-        var params = new URLSearchParams();
-        params.append('notice_type', 'unread');
-        params.append('count', '1');
-        that.axios.post(url, params)
-          .then(function (response) {
-            if (callback) {callback(response);};
-          });
+    Vue.prototype.get_notice = function (callback) {
+      var that = this
+      var url = DocConfig.server + '/api/notice/getList'
+      var params = new URLSearchParams()
+      params.append('notice_type', 'unread')
+      params.append('count', '1')
+      that.axios.post(url, params)
+        .then(function (response) {
+          if (callback) { callback(response) };
+        })
     }
 
-    Vue.prototype.set_bg_grey = function(){
-        /*给body添加类,设置背景色*/
-        document.getElementsByTagName("body")[0].className="grey-bg";
+    Vue.prototype.set_bg_grey = function () {
+      /* 给body添加类,设置背景色 */
+      document.getElementsByTagName('body')[0].className = 'grey-bg'
     }
 
-    Vue.prototype.unset_bg_grey = function(){
-      /*去掉添加的背景色*/
-      document.body.removeAttribute("class","grey-bg");
+    Vue.prototype.unset_bg_grey = function () {
+      /* 去掉添加的背景色 */
+      document.body.removeAttribute('class', 'grey-bg')
     }
 
+    // json格式化与压缩
+    // compress=false的时候表示美化json,compress=true的时候表示将美化过的json压缩还原
+    Vue.prototype.formatJson = function (txt, compress = false) {
+      if (compress === false) {
+        try {
+          if (typeof txt === 'string') {
+            txt = JSON.parse(txt)
+          }
+          return JSON.stringify(txt, null, 2)
+        } catch (e) {
+          // 非json数据直接显示
+          return txt
+        }
+      }
+      const indentChar = ' '
+      if (/^\s*$/.test(txt)) {
+        // alert('数据为空,无法格式化! ');
+        return txt
+      }
+      try {
+        var data = eval(`(${txt})`)
+      } catch (e) {
+        // alert(`数据源语法错误,格式化失败! 错误信息: ${e.description}`, 'err');
+        return txt
+      }
+      const draw = []
+      const last = false
+      const This = this
+      const line = compress ? '' : '\n'
+      let nodeCount = 0
+      let maxDepth = 0
+
+      const notify = function (name, value, isLast, indent, formObj) {
+        nodeCount++ /* 节点计数 */
+        for (var i = 0, tab = ''; i < indent; i++) tab += indentChar /* 缩进HTML */
+        tab = compress ? '' : tab /* 压缩模式忽略缩进 */
+        maxDepth = ++indent /* 缩进递增并记录 */
+        if (value && value.constructor == Array) {
+          /* 处理数组 */
+          draw.push(`${tab + (formObj ? `"${name}":` : '')}[${line}`) /* 缩进'[' 然后换行 */
+          for (var i = 0; i < value.length; i++) { notify(i, value[i], i == value.length - 1, indent, false) }
+          draw.push(`${tab}]${isLast ? line : `,${line}`}`) /* 缩进']'换行,若非尾元素则添加逗号 */
+        } else if (value && typeof value === 'object') {
+          /* 处理对象 */
+          draw.push(`${tab + (formObj ? `"${name}":` : '')}{${line}`) /* 缩进'{' 然后换行 */
+          let len = 0
+          var i = 0
+          for (var key in value) len++
+          for (var key in value) notify(key, value[key], ++i == len, indent, true)
+          draw.push(`${tab}}${isLast ? line : `,${line}`}`) /* 缩进'}'换行,若非尾元素则添加逗号 */
+        } else {
+          if (typeof value === 'string') value = `"${value}"`
+          draw.push(tab + (formObj ? `"${name}":` : '') + value + (isLast ? '' : ',') + line)
+        }
+      }
+      const isLast = true
+      const indent = 0
+      notify('', data, isLast, indent, false)
+      return draw.join('')
+    }
   }
-}
+}