소스 검색

增加多节点选择平台

feix0518 3 일 전
부모
커밋
35390888e9
14개의 변경된 파일720개의 추가작업 그리고 14개의 파일을 삭제
  1. 6 0
      .gitignore
  2. 1 0
      client/package.json
  3. 52 13
      client/src/App.vue
  4. 5 0
      client/vue.config.js
  5. 15 0
      platform/package.json
  6. BIN
      platform/public/advich.jpg
  7. 11 0
      platform/public/index.html
  8. BIN
      platform/public/logo.png
  9. 296 0
      platform/src/App.vue
  10. 179 0
      platform/src/Layout.vue
  11. 126 0
      platform/src/Menu.vue
  12. 8 0
      platform/src/main.js
  13. 20 0
      platform/vue.config.js
  14. 1 1
      server/package.json

+ 6 - 0
.gitignore

@@ -9,3 +9,9 @@ server/node_modules
 server/package-lock.json
 
 server/screenshots/*
+
+platform/node_modules
+
+platform/package-lock.json
+
+platform/screenshots/*

+ 1 - 0
client/package.json

@@ -7,6 +7,7 @@
     "build": "vue-cli-service build"
   },
   "dependencies": {
+    "element-ui": "^2.15.14",
     "vue": "^2.6.14"
   },
   "devDependencies": {

+ 52 - 13
client/src/App.vue

@@ -1,15 +1,15 @@
 <template>
   <div class="site-test">
-    <h1>未迟 【站点测试工具】</h1>
+    <h1>当前测试节点【{{name}}】</h1>
 
     <!-- 新增的下拉选择框 -->
-    <select v-model="testType" class="test-type-select">
+    <!-- <select v-model="testType" class="test-type-select">
       <option value="1">国内节点</option>
       <option value="2">中亚节点</option>
       <option value="3">日本节点</option>
       <option value="4">欧洲节点</option>
       <option value="4">北美节点</option>
-    </select>
+    </select> -->
 
     <select v-model="protocal" class="test-type-select1">
       <option value="http://">http://</option>
@@ -17,15 +17,19 @@
     </select>
     <input type="text" v-model="targetUrl" placeholder="请输入网站地址">
     <button @click="fetchData">测试</button>
-    <p style="white-space: pre-wrap;" v-show="!loading" >{{ title1 }}</p>
-    <p style="white-space: pre-wrap;">{{ message }}</p>
+    <!-- <p style="white-space: pre-wrap; font-size: 20px; " v-show="!loading" >{{ title1 }}</p>
+     <p style="white-space: pre-wrap;">{{ message }}</p>
 
-    <p style="white-space: pre-wrap;">{{ title2 }}</p>
+    <p style="white-space: pre-wrap;">{{ title2 }}</p> -->
+    <div v-if="errorMessage" style="color: red;">{{ errorMessage }}</div>
     <div v-if="loading">获取测试结果中...</div>
     <!-- App.vue 里渲染 -->
-    <div class="table-content">
+    <div  v-show="resultTableDisplay" class="table-content">
       <table class="k6-table" border="1" cellpadding="6" cellspacing="0">
         <thead>
+          <tr  >
+            <th colSpan="3" style="font-size: 17px; color: white; background-color: #0091D8;">{{ title1 }}</th>
+          </tr>
           <tr>
             <th>指标名称</th>
             <th>指标项</th>
@@ -39,7 +43,7 @@
 
     </div>
 
-    <img :src="imgUrl"></img>
+    <!-- <img :src="imgUrl"></img> -->
     <!-- <div>
       <div ref="terminalContainer" class="terminal-container"></div>
     </div> -->
@@ -49,6 +53,7 @@
 <script>
 // import { Terminal } from 'xterm';
 // import 'xterm/css/xterm.css';
+import { Message } from "element-ui";
 const metricLabelMap = {
   checks_total: "总检查数",
   checks_succeeded: "检查通过率",
@@ -83,24 +88,39 @@ export default {
   data() {
     return {
       targetUrl: "",
-      title1: "",
+      title1: "测试结果",
       title2: "",
       message: "",
       testType: "1", // 新增的下拉框绑定值,默认选择负载测试
       protocal: "http://",
       imgUrl: "",
-      loading: false
+      loading: false,
+      resultTableDisplay: false,
+      name: "",
+      errorMessage: ""
     };
   },
+  created() {
+    const searchParams = new URLSearchParams(window.location.search);
+    this.name = searchParams.get('name');
+  },
   methods: {
     fetchData() {
+      if (!this.targetUrl) {
+        this.errorMessage ="请输入目标地址";
+        return;
+      } else {
+        this.errorMessage = "";
+      }
       // const term = new Terminal();
       // term.open(this.$refs.terminalContainer);
       this.title2 = "";
       this.imgUrl = "";
-      this.title1 = this.targetUrl + "测试结果:";
+      this.resultTableDisplay = false;
+      this.title1 = this.targetUrl + "测试结果";
       const timeStamp = new Date().getTime();
       this.loading = true;
+      
       // 修改请求URL,包含测试类型参数
       fetch(
         `/api/k6?targetUrl=${this.protocal + this.targetUrl}&timeStamp=` +
@@ -108,6 +128,7 @@ export default {
       )
         .then((res) => res.json())
         .then((data) => {
+          this.resultTableDisplay = true;
           this.imgUrl = "/static/screenshot_" + timeStamp + ".png";
           const tbody = document.getElementById("k6-metrics-tbody");
           tbody.innerHTML = "";
@@ -153,6 +174,24 @@ export default {
               tbody.appendChild(tr);
             }
           });
+            const trImgTitle = document.createElement("tr");
+            const thImgTitle = document.createElement("th");
+            thImgTitle.colSpan = 3;
+            thImgTitle.textContent = "站点截图";
+            trImgTitle.appendChild(thImgTitle);
+            tbody.appendChild(trImgTitle);
+
+            const trImg = document.createElement("tr");
+            const tdImg = document.createElement("td");
+            tdImg.colSpan = 3;
+            const img = document.createElement("img");
+            img.src = this.imgUrl;
+            // 将img添加到td中
+            tdImg.appendChild(img);
+            // 将td添加到tr中
+            trImg.appendChild(tdImg);
+            // 最后将tr添加到tbody中
+            tbody.appendChild(trImg);
         }).finally(() => {
           this.loading = false;
         })
@@ -204,7 +243,7 @@ input {
 button {
   margin-left: 10px;
   padding: 8px 16px;
-  background-color: #42b983;
+  background-color: #0091D8;
   color: white;
   border: none;
   border-radius: 4px;
@@ -212,7 +251,7 @@ button {
 }
 
 button:hover {
-  background-color: #3aa876;
+  background-color: #0091D8;
 }
 
 .table-content {

+ 5 - 0
client/vue.config.js

@@ -10,6 +10,11 @@ module.exports = {
         changeOrigin: true,
         // pathRewrite: { '^/api': '/api' },
       },
+      '/static': {
+        target: 'http://localhost:9001', // 后端接口地址(开发阶段)
+        changeOrigin: true,
+        // pathRewrite: { '^/api': '/api' },
+      }
     },
   },
 };

+ 15 - 0
platform/package.json

@@ -0,0 +1,15 @@
+{
+  "name": "client",
+  "version": "1.0.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build"
+  },
+  "dependencies": {
+    "vue": "^2.6.14"
+  },
+  "devDependencies": {
+    "@vue/cli-service": "^4.5.0"
+  }
+}

BIN
platform/public/advich.jpg


+ 11 - 0
platform/public/index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>未迟站点测试工具</title>
+  <link rel="icon" href="/advich.jpg" />
+</head>
+<body>
+  <div id="app"></div>
+</body>
+</html>

BIN
platform/public/logo.png


+ 296 - 0
platform/src/App.vue

@@ -0,0 +1,296 @@
+<template>
+  <div class="site-test">
+    <h1>未迟 【站点测试工具】</h1>
+
+    <!-- 新增的下拉选择框 -->
+    <select v-model="testType" class="test-type-select">
+      <option value="1">国内节点</option>
+      <option value="2">中亚节点</option>
+      <option value="3">日本节点</option>
+      <option value="4">欧洲节点</option>
+      <option value="4">北美节点</option>
+    </select>
+
+    <select v-model="protocal" class="test-type-select1">
+      <option value="http://">http://</option>
+      <option value="https://">https://</option>
+    </select>
+    <input type="text" v-model="targetUrl" placeholder="请输入网站地址">
+    <button @click="fetchData">测试</button>
+    <!-- <p style="white-space: pre-wrap; font-size: 20px; " v-show="!loading" >{{ title1 }}</p>
+     <p style="white-space: pre-wrap;">{{ message }}</p>
+
+    <p style="white-space: pre-wrap;">{{ title2 }}</p> -->
+    <div v-if="loading">获取测试结果中...</div>
+    <!-- App.vue 里渲染 -->
+    <div  v-show="resultTableDisplay" class="table-content">
+      <table class="k6-table" border="1" cellpadding="6" cellspacing="0">
+        <thead>
+          <tr  >
+            <th colSpan="3" style="font-size: 17px; color: white; background-color: #42b983;">{{ title1 }}</th>
+          </tr>
+          <tr>
+            <th>指标名称</th>
+            <th>指标项</th>
+            <th>数值</th>
+          </tr>
+        </thead>
+        <tbody id="k6-metrics-tbody">
+          <!-- 动态填充 -->
+        </tbody>
+      </table>
+
+    </div>
+
+    <!-- <img :src="imgUrl"></img> -->
+    <!-- <div>
+      <div ref="terminalContainer" class="terminal-container"></div>
+    </div> -->
+  </div>
+</template>
+
+<script>
+// import { Terminal } from 'xterm';
+// import 'xterm/css/xterm.css';
+const metricLabelMap = {
+  checks_total: "总检查数",
+  checks_succeeded: "检查通过率",
+  checks_failed: "检查失败率",
+  browser_data_received: "浏览器接收数据量",
+  browser_data_sent: "浏览器发送数据量",
+  browser_http_req_duration: "浏览器 HTTP 请求耗时",
+  browser_http_req_failed: "浏览器 HTTP 请求失败率",
+  browser_web_vital_cls: "浏览器内容布局偏移(CLS)",
+  browser_web_vital_fcp: "浏览器首次内容绘制(FCP)",
+  browser_web_vital_ttfb: "浏览器首字节时间(TTFB)",
+  checks: "检查通过率",
+  data_received: "接收数据量",
+  data_sent: "发送数据量",
+  iteration_duration: "单次迭代耗时",
+  iterations: "总迭代次数",
+  vus: "虚拟用户数",
+  vus_max: "最大虚拟用户数",
+};
+
+const fieldKeyMap = {
+  avg: "平均",
+  min: "最小",
+  med: "中位数",
+  max: "最大",
+  "p(90)": "P90",
+  "p(95)": "P95",
+  value: "数值",
+  rate: "速率",
+};
+export default {
+  data() {
+    return {
+      targetUrl: "",
+      title1: "测试结果",
+      title2: "",
+      message: "",
+      testType: "1", // 新增的下拉框绑定值,默认选择负载测试
+      protocal: "http://",
+      imgUrl: "",
+      loading: false,
+      resultTableDisplay: false
+    };
+  },
+  methods: {
+    fetchData() {
+      // const term = new Terminal();
+      // term.open(this.$refs.terminalContainer);
+      this.title2 = "";
+      this.imgUrl = "";
+      this.resultTableDisplay = false;
+      this.title1 = this.targetUrl + "测试结果";
+      const timeStamp = new Date().getTime();
+      this.loading = true;
+      
+      // 修改请求URL,包含测试类型参数
+      fetch(
+        `/api/k6?targetUrl=${this.protocal + this.targetUrl}&timeStamp=` +
+          timeStamp
+      )
+        .then((res) => res.json())
+        .then((data) => {
+          this.resultTableDisplay = true;
+          this.imgUrl = "/static/screenshot_" + timeStamp + ".png";
+          const tbody = document.getElementById("k6-metrics-tbody");
+          tbody.innerHTML = "";
+
+          Object.entries(data).forEach(([metricKey, metricVal]) => {
+            const metricLabel = metricLabelMap[metricKey] || metricKey;
+
+            if (typeof metricVal === "object" && !Array.isArray(metricVal)) {
+              const subKeys = Object.keys(metricVal);
+              subKeys.forEach((subKey, idx) => {
+                const tr = document.createElement("tr");
+
+                if (idx === 0) {
+                  const nameTd = document.createElement("td");
+                  nameTd.textContent = metricLabel;
+                  nameTd.rowSpan = subKeys.length;
+                  tr.appendChild(nameTd);
+                }
+
+                const subKeyTd = document.createElement("td");
+                subKeyTd.textContent = fieldKeyMap[subKey] || subKey;
+
+                const valTd = document.createElement("td");
+                valTd.textContent = metricVal[subKey];
+
+                tr.appendChild(subKeyTd);
+                tr.appendChild(valTd);
+
+                tbody.appendChild(tr);
+              });
+            } else {
+              const tr = document.createElement("tr");
+              const nameTd = document.createElement("td");
+              nameTd.textContent = metricLabel;
+              const subKeyTd = document.createElement("td");
+              subKeyTd.textContent = "";
+              const valTd = document.createElement("td");
+              valTd.textContent = metricVal;
+
+              tr.appendChild(nameTd);
+              tr.appendChild(subKeyTd);
+              tr.appendChild(valTd);
+              tbody.appendChild(tr);
+            }
+          });
+            const trImgTitle = document.createElement("tr");
+            const thImgTitle = document.createElement("th");
+            thImgTitle.colSpan = 3;
+            thImgTitle.textContent = "站点截图";
+            trImgTitle.appendChild(thImgTitle);
+            tbody.appendChild(trImgTitle);
+
+            const trImg = document.createElement("tr");
+            const tdImg = document.createElement("td");
+            tdImg.colSpan = 3;
+            const img = document.createElement("img");
+            img.src = this.imgUrl;
+            // 将img添加到td中
+            tdImg.appendChild(img);
+            // 将td添加到tr中
+            trImg.appendChild(tdImg);
+            // 最后将tr添加到tbody中
+            tbody.appendChild(trImg);
+        }).finally(() => {
+          this.loading = false;
+        })
+    },
+  },
+};
+</script>
+
+<style>
+#app {
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.site-test {
+  text-align: center;
+  color: #2c3e50;
+  margin-top: 60px;
+}
+.test-type-select {
+  padding: 8px;
+  margin-bottom: 10px;
+  width: 120px;
+  height: 40px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+}
+
+.test-type-select1 {
+  padding: 8px;
+  margin-bottom: 10px;
+  width: 80px;
+  height: 40px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  margin-left: 20px;
+}
+
+input {
+  width: 400px;
+  height: 23px;
+  padding: 8px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  margin-left: 5px;
+}
+
+button {
+  margin-left: 10px;
+  padding: 8px 16px;
+  background-color: #42b983;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+button:hover {
+  background-color: #3aa876;
+}
+
+.table-content {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+table.k6-table {
+  width: 60%;
+  border-collapse: collapse;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+    "Helvetica Neue", Arial;
+  font-size: 14px;
+  margin-top: 20px;
+}
+
+table.k6-table thead {
+  background-color: #f7f7f7;
+}
+
+table.k6-table th,
+table.k6-table td {
+  border: 1px solid #ddd;
+  padding: 10px 12px;
+  color: #000;
+  text-align: center;
+}
+
+/* table.k6-table th {
+    text-align: center;
+    font-weight: 600;
+  }
+
+  table.k6-table td:first-child {
+    font-weight: 600;
+    color: #333;
+    text-align: center;
+    background-color: #fafafa;
+  }
+
+  table.k6-table td:nth-child(2) {
+    text-align: center;
+    color: #333;
+  }
+
+  table.k6-table td:nth-child(3) {
+    text-align: center;
+    color: #000;
+  } */
+
+table.k6-table tbody tr:hover {
+  background-color: #f0f8ff;
+}
+</style>

+ 179 - 0
platform/src/Layout.vue

@@ -0,0 +1,179 @@
+<template>
+  <div class="layout" :class="`theme-${theme}`">
+    <!-- 顶部菜单 -->
+    <header class="header">
+      <div class="header-left">
+        
+        <span class="logo"><img src="../public/logo.png" class="img"/></span>
+        <span class="logo">   【站点测试工具】</span>
+      </div>
+      <div class="header-center">
+        <ul class="top-menu">
+          <li v-for="item in topMenuData" :key="item.label" class="top-menu-item">
+            {{ item.label }}
+          </li>
+        </ul>
+      </div>
+      <!-- <div class="header-right">
+        <button class="btn">注册</button>
+        <button class="btn">登录</button>
+      </div> -->
+    </header>
+
+    <div class="main-container">
+      <!-- 左侧菜单 -->
+      <aside class="sidebar">
+        <Menu :menuData="leftMenuData" @update:currentUrl="updateUrl" />
+      </aside>
+
+      <!-- 主体内容 -->
+      <main class="content">
+        <iframe 
+            v-show="currentUrl"
+            :src="currentUrl"
+            frameborder="0"
+            width="100%"
+            height="100%"
+        ></iframe>
+        <div v-show="!currentUrl" style="text-align: center;"><h1>【请选择测试节点】</h1></div>
+      </main>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import Menu from './Menu.vue'; // 导入菜单组件
+
+const props = defineProps({
+  theme: {
+    type: String,
+    default: 'light',
+    validator: (value) => ['light', 'dark'].includes(value),
+  }
+});
+
+const currentUrl = ref('')
+function updateUrl(newUrl) {
+  currentUrl.value  = newUrl.value + "?name=" + newUrl.label;
+}
+
+// 顶部菜单数据
+const topMenuData = ref([
+//   { label: '首页' },
+//   { label: '关于我们' },
+//   { label: '服务' },
+//   { label: '联系我们' },
+]);
+
+// 左侧菜单数据
+const leftMenuData = ref([
+  {
+    label: '节点选择',
+    icon: 'fas fa-cogs',
+    children: [
+      { label: '国内节点', value: 'http://192.168.0.203:8080', icon: 'fas fa-wrench' },
+      { label: '香港节点', value: 'http://192.168.0.203:8080', icon: 'fas fa-wrench' },
+      { label: '日本节点', value: '', icon: 'fas fa-wrench' },
+    ],
+  },
+]);
+</script>
+
+<style scoped>
+.layout {
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+  background-color: white;
+  color: rgb(87, 86, 86);
+  height: 60px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.header-left .logo {
+  vertical-align: middle;
+  font-size: 20px;
+  color: rgb(87, 86, 86);
+  /* font-weight: bold; */
+}
+.header-left .logo .img {
+  width: 15%;
+  height: 15%;
+}
+
+.header-center .top-menu {
+  display: flex;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+.top-menu-item {
+  margin: 0 15px;
+  cursor: pointer;
+}
+
+.top-menu-item:hover {
+  text-decoration: underline;
+}
+
+.header-right .btn {
+  margin-left: 10px;
+  padding: 5px 10px;
+  border: none;
+  border-radius: 4px;
+  background-color: white;
+  color: #007bff;
+  cursor: pointer;
+}
+
+.header-right .btn:hover {
+  background-color: #f0f0f0;
+}
+
+.main-container {
+  display: flex;
+  flex: 1;
+}
+
+.sidebar {
+  width: 250px;
+  background-color: #f8f9fa;
+  border-right: 1px solid #ddd;
+  overflow-y: auto;
+}
+
+.content {
+  flex: 1;
+  padding: 20px;
+  overflow-y: auto;
+}
+
+/* 浅色主题 */
+.theme-light {
+  background-color: #ffffff;
+  color: #333333;
+}
+
+.theme-light .sidebar {
+  background-color: #f8f9fa;
+}
+
+/* 深色主题 */
+.theme-dark {
+  background-color: #1a1a1a;
+  color: #ffffff;
+}
+
+.theme-dark .sidebar {
+  background-color: #2d2d2d;
+}
+</style>

+ 126 - 0
platform/src/Menu.vue

@@ -0,0 +1,126 @@
+<template>
+  <ul class="menu">
+    <li v-for="item in menuData" :key="item.label" class="menu-item">
+      <!-- 菜单项 -->
+      <div class="menu-label" @click="toggleSubMenu(item)">
+        <span v-if="item.icon" class="menu-icon">
+          <i :class="item.icon"></i>
+        </span>
+        <span class="menu-text">{{ item.label }}</span>
+        <span v-if="item.children" class="menu-arrow">
+          {{ isSubMenuOpen(item) ? '▼' : '▶' }}
+        </span>
+      </div>
+
+      <!-- 子菜单 -->
+      <ul v-if="item.children && isSubMenuOpen(item)" class="sub-menu">
+        <li
+          v-for="child in item.children"
+          :key="child.label"
+          class="sub-menu-item"
+        >
+          <span v-if="child.icon" class="menu-icon">
+            <i :class="child.icon"></i>
+          </span>
+          <span class="menu-text" @click="onClickMenuNode(child)" >{{ child.label }}</span>
+        </li>
+      </ul>
+    </li>
+  </ul>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+
+const props = defineProps({
+  menuData: {
+    type: Array,
+    required: true,
+  }
+});
+
+// const { props1 } = toRefs(props);
+
+const openMenus = ref([]);
+
+// 切换子菜单展开/收起
+const toggleSubMenu = (item) => {
+  if (item.children) {
+    const index = openMenus.value.indexOf(item.label);
+    if (index === -1) {
+      openMenus.value.push(item.label);
+    } else {
+      openMenus.value.splice(index, 1);
+    }
+  }
+};
+
+// 判断子菜单是否展开
+const isSubMenuOpen = (item) => {
+  return openMenus.value.includes(item.label);
+};
+
+const emit = defineEmits(['update:currentUrl'])
+
+// 点击菜单项处理
+const onClickMenuNode = (item) => {
+//   props.currentUrl = item;
+  emit('update:currentUrl', item);
+  console.log("item====",item);
+};
+
+toggleSubMenu(props.menuData[0]);
+
+</script>
+
+<style scoped>
+.menu {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.menu-item {
+  margin: 8px 0;
+}
+
+.menu-label {
+  display: flex;
+  align-items: center;
+  padding: 8px 16px;
+  cursor: pointer;
+  transition: background-color 0.3s ease;
+}
+
+.menu-label:hover {
+  background-color: rgba(0, 0, 0, 0.1);
+}
+
+.menu-icon {
+  margin-right: 8px;
+}
+
+.menu-text {
+  flex: 1;
+}
+
+.menu-arrow {
+  margin-left: 8px;
+}
+
+.sub-menu {
+  list-style: none;
+  padding-left: 24px;
+  margin: 0;
+}
+
+.sub-menu-item {
+  padding: 8px 16px;
+  cursor: pointer;
+  transition: background-color 0.3s ease;
+}
+
+.sub-menu-item:hover {
+  background-color: rgba(0, 0, 0, 0.1);
+}
+</style>

+ 8 - 0
platform/src/main.js

@@ -0,0 +1,8 @@
+import Vue from 'vue';
+import App from './Layout.vue';
+
+Vue.config.productionTip = false;
+
+new Vue({
+  render: h => h(App),
+}).$mount('#app');

+ 20 - 0
platform/vue.config.js

@@ -0,0 +1,20 @@
+// client/vue.config.js
+const path = require('path');
+module.exports = {
+  outputDir: path.resolve(__dirname, 'dist'),
+  publicPath: '/',
+  devServer: {
+    proxy: {
+      '/api': {
+        target: 'http://localhost:9001', // 后端接口地址(开发阶段)
+        changeOrigin: true,
+        // pathRewrite: { '^/api': '/api' },
+      },
+      '/static': {
+        target: 'http://localhost:9001', // 后端接口地址(开发阶段)
+        changeOrigin: true,
+        // pathRewrite: { '^/api': '/api' },
+      }
+    },
+  },
+};

+ 1 - 1
server/package.json

@@ -4,7 +4,7 @@
   "scripts": {
     "start": "node index.js",
     "build-client": "cd ../client && npm run build",
-    "serve": "NODE_ENV=development nodemon index.js",
+    "serve": "set NODE_ENV=development && nodemon index.js",
     "dev": "npm run build-client && npm run serve"
   },
   "dependencies": {