github.com/laof/lite-speed-test@v0.0.0-20230930011949-1f39b7037845/web/index.html (about) 1 <!DOCTYPE html> 2 <html> 3 4 <head> 5 <link rel="stylesheet" type="text/css" href="style.css" media="screen"> 6 </head> 7 <meta charset="utf-8" /> 8 <!-- 引入 vue --> 9 <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.45/vue.global.prod.js"></script> 10 11 <!-- 引入样式 --> 12 <!-- <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> --> 13 <link href="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.2.25/theme-chalk/index.min.css" rel="stylesheet"> 14 15 <!-- 引入组件库 --> 16 <!-- <script src="https://unpkg.com/element-ui/lib/index.js"></script> --> 17 <script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.2.25/index.full.js"></script> 18 <!-- <script src="https://cdn.jsdelivr.net/npm/@element-plus/icons-vue"></script> --> 19 <script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus-icons-vue/2.0.10/global.iife.js"></script> 20 <!-- qrcode --> 21 <!-- <script src="https://cdn.jsdelivr.net/npm/qrcodejs2@0.0.2/qrcode.min.js"></script> --> 22 <script src="wasm_exec.js"></script> 23 24 25 <body> 26 <div id="app"> 27 <el-row> 28 <el-col :span="22" :offset="1"> 29 <el-card> 30 <div slot="header">订阅信息</div> 31 <el-container :element-loading-text="loadingContent"> 32 <el-form label-width="120px" size="large"> 33 <el-form-item label="设置:"> 34 <el-radio-group v-model="option" :disabled="loading"> 35 <el-radio :label="0">基础</el-radio> 36 <el-radio :label="1">高级</el-radio> 37 <el-radio :label="2">导出</el-radio> 38 <el-radio :label="3">手动生成</el-radio> 39 </el-radio-group> 40 </el-form-item> 41 42 <el-form-item label="链接:" v-if="option<2"> 43 <el-col :span="8"> 44 <el-input v-model="subscription" style="width: 800px" 45 @keyup.enter.native="submit" placeholder="支持 V2Ray/Trojan/SS/SSR/Clash 订阅链接,多个节点链接及文件路径的批量测速" 46 :disabled="loading||upload" clearable></el-input> 47 </el-col> 48 <el-col :span="12" ></el-col> 49 <el-col :span="12" style="margin-top: 6px;"> 50 <el-upload :drag="checkUploadStatus('drag')" :v-if="checkUploadStatus('if')" 51 action="" :show-file-list="false" ref="upload" :http-request="handleFileChange" 52 :auto-upload="true" :before-upload="beforeUpload"> 53 <i class="el-icon-upload" v-if="!subscription.length"></i> 54 <!--<el-button slot="trigger" type="primary" icon="el-icon-files" :disabled="loading" v-if="!upload">选择配置文件</el-button>--> 55 <el-button slot="tip" type="danger" icon="el-icon-close" :disabled="loading" 56 v-if="upload" @click="cancelFileUpload">取消文件选择</el-button> 57 <div class="el-upload__text" v-if="!subscription.length"> 58 还可以将配置文件拖到此处,或<em>点击上传</em></div> 59 </el-upload> 60 </el-col> 61 62 </el-form-item> 63 64 <el-form-item label="并发数:" v-if="option<2"> 65 <el-input v-model="concurrency" style="width: 235px" type="number" min="1" max="50" 66 @keyup.enter.native="submit" :disabled="loading"></el-input> 67 </el-form-item> 68 <el-form-item label="测试时长(秒):" v-if="option===1"> 69 <el-input v-model="timeout" style="width: 235px" type="number" min="5" max="60" 70 @keyup.enter.native="submit" :disabled="loading"></el-input> 71 </el-form-item> 72 <el-form-item label="去除重复节点:" v-if="option===1"> 73 <el-checkbox v-model="unique">去重</el-checkbox> 74 </el-form-item> 75 <el-form-item label="测试项:" v-if="option<2"> 76 <el-select v-model="speedtestMode" :disabled="loading"> 77 <el-option v-for="(item, key, index) in init.speedtestModes" :key="index" 78 :label="item" :value="key"> 79 </el-option> 80 </el-select> 81 </el-form-item> 82 <el-form-item label="自定义组名:" v-if="option<2"> 83 <el-input v-model="groupname" style="width: 235px" 84 @keyup.enter.native="submit" :disabled="loading" clearable></el-input> 85 <el-button type="primary" @click="submit" style="margin-left: 20px" v-if="!option" 86 :disabled="loading" :loading="loading"><el-icon v-if="!loading" class="el-icon--left"><Check /></el-icon>提 交</el-button> 87 <el-button type="danger" @click="terminate" :icon="Close" v-if="!option" 88 :disabled="!loading"><el-icon class="el-icon--left"><Close /></el-icon>终 止</el-button> 89 </el-form-item> 90 91 <el-form-item label="Ping方式:" v-if="option===1" :disabled="loading"> 92 <el-select v-model="pingMethod" > 93 <el-option v-for="(item, key, index) in init.pingMethods" :key="index" :label="item" :value="key"> 94 </el-option> 95 </el-select> 96 <el-button type="primary" @click="submit" style="margin-left: 20px" :icon="Check" :disabled="loading" 97 :loading="loading">提 交</el-button> 98 <el-button type="danger" @click="terminate" :icon="Close" :disabled="!loading">终 止</el-button> 99 </el-form-item> 100 101 <!-- export --> 102 <el-form-item label="语言:" v-if="option===2" :disabled="loading"> 103 <el-select v-model="language"> 104 <el-option key="1" label="EN" value="en"></el-option> 105 <el-option key="2" label="中文" value="cn"></el-option> 106 </el-select> 107 </el-form-item> 108 <el-form-item label="字体大小:" v-if="option===2"> 109 <el-input v-model="fontSize" style="width: 235px" type="number" min="12" max="36" 110 @keyup.enter.native="submit" :disabled="loading"></el-input> 111 </el-form-item> 112 <el-form-item label="排序方式:" v-if="option===2" :disabled="loading"> 113 <el-select v-model="sortMethod" > 114 <el-option v-for="(item, key, index) in init.sortMethods" :key="index" 115 :label="item" :value="key"> 116 </el-option> 117 </el-select> 118 </el-form-item> 119 <el-form-item label="主题:" v-if="option===2" :disabled="loading"> 120 <el-select v-model="theme" > 121 <el-option v-for="(item, key, index) in init.themes" :key="index" 122 :label="item" :value="key"> 123 </el-option> 124 </el-select> 125 </el-form-item> 126 <el-form-item label="结果数据:" v-if="option===3" :disabled="loading"> 127 <el-input 128 type="textarea" 129 :autosize="{ minRows: 5, maxRows: 18}" 130 placeholder="input data" 131 style="width: 800px" 132 v-model="generateResultJSON"> 133 </el-input> 134 <el-button type="primary" @click="generateResult" style="margin-left: 20px" :icon="Check" :disabled="loading" 135 :loading="loading">生 成</el-button> 136 </el-form-item> 137 </el-form> 138 </el-container> 139 </el-card> 140 141 <br> 142 <!-- 143 <el-card> 144 <div slot="header">操作</div> 145 <el-container> 146 <el-form :inline="true"> 147 <el-form-item> 148 <el-button type="primary" @click="submit">提交</el-button> 149 </el-form-item> 150 <el-form-item> 151 <el-button type="secondary">终止</el-button> 152 </el-form-item> 153 </el-form> 154 </el-container> 155 </el-card> 156 157 <br> 158 --> 159 <el-card> 160 <el-row style="display: flex;"> 161 <el-col style="display: flex;align-items: center;" :span="1"> 162 <div>结果</div> 163 </el-col> 164 <el-col v-if="result.length" :span="8"> 165 <el-dropdown trigger="click"> 166 <el-button size="large" type="primary"> 167 Actions<el-icon class="el-icon--right"><arrow-down /></el-icon> 168 </el-button> 169 <template #dropdown> 170 <el-dropdown-menu slot="dropdown"> 171 <el-dropdown-item @click.native="handleCopySub()">复制订阅链接</el-dropdown-item> 172 <el-dropdown-item v-if="!loading && result.length" @click.native="handleCopyAvailable()">复制可用节点</el-dropdown-item> 173 <el-dropdown-item v-if="multipleSelection.length" @click.native="handleCopy()">复制节点</el-dropdown-item> 174 <el-dropdown-item v-if="multipleSelection.length" @click.native="handleSave()">导出节点</el-dropdown-item> 175 <!-- <el-dropdown-item @click.native="handleRetest()">重新测试</el-dropdown-item> --> 176 <el-dropdown-item v-if="multipleSelection.length" @click.native="handleQRCode()">显示二维码</el-dropdown-item> 177 <el-dropdown-item v-if="multipleSelection.length" @click.native="handleExportResult()">导出结果</el-dropdown-item> 178 </el-dropdown-menu> 179 </template> 180 </el-dropdown> 181 </el-col> 182 </el-row> 183 <el-container> 184 <el-table :data="result" :cell-style="colorCell" ref="result" 185 :row-key="row => `${row.server}${row.protocol}${row.ping}${row.speed}${row.maxspeed}`" 186 @selection-change="handleSelectionChange" @sort-change="handleSortChange"> 187 <el-table-column type="selection" width="55" :selectable="checkSelectable"> 188 </el-table-column> 189 <!-- <el-table-column label="Group" align="center" prop="group" width="300" sortable> 190 </el-table-column> --> 191 <el-table-column label="Remark" align="center" prop="remark" min-width="400" sortable> 192 </el-table-column> 193 <el-table-column label="Server" align="center" prop="server" min-width="160" sortable> 194 </el-table-column> 195 <el-table-column label="Protocol" align="center" prop="protocol" width="120" sortable 196 :filters="[{ text: 'V2Ray', value: 'vmess' }, { text: 'Trojan', value: 'trojan' }, { text: 'ShadowsocksR', value: 'ssr' }, { text: 'Shadowsocks', value: 'ss' }]" 197 :filter-method="filterProtocol"> 198 </el-table-column> 199 <!-- <el-table-column label="Loss" align="center" prop="loss" width="100" sortable> 200 </el-table-column> --> 201 <el-table-column label="Ping" align="center" prop="ping" width="100" sortable="custom" 202 :filters="[{ text: 'Available ', value: 'available' }]" 203 :filter-method="filterPing"> 204 </el-table-column> 205 <el-table-column label="AvgSpeed" align="center" prop="speed" min-width="150" sortable 206 :filters="[{ text: '200KB', value: 204800 }, { text: '500KB', value: 512000 }, { text: '1MB', value: 1048576 }, { text: '2MB', value: 2097152 }, { text: '5MB', value: 5242880 }, { text: '10MB', value: 10485760 },{ text: '15MB', value: 15728640 }, { text: '20MB', value: 20971520 }]" 207 :filter-multiple="false" 208 :filter-method="filterAvgSpeed" 209 :sort-method="speedSort"> 210 </el-table-column> 211 <el-table-column label="MaxSpeed" align="center" prop="maxspeed" min-width="150" sortable 212 :filters="[{ text: '200KB', value: 204800 }, { text: '500KB', value: 512000 }, { text: '1MB', value: 1048576 }, { text: '2MB', value: 2097152 }, { text: '5MB', value: 5242880 }, { text: '10MB', value: 10485760 },{ text: '15MB', value: 15728640 }, { text: '20MB', value: 20971520 }]" 213 :filter-multiple="false" 214 :filter-method="filterMaxSpeed" 215 :sort-method="maxSpeedSort"> 216 </el-table-column> 217 </el-table> 218 </el-container> 219 </el-card> 220 221 <br> 222 223 <div :class="['dashboard', dashboardCollapsed ? 'collapsed' : '']"> 224 <el-card class="progress"> 225 <div class="progress-bar" :style="{ 'width': testProgress(result, testCount) + '%' }"></div> 226 <div class="progress-inner"> 227 <div class="progress-item"> 228 <span>{{ testProgress(result, testCount) }}%</span> 229 <div>Progress</div> 230 </div> 231 <div v-if="!dashboardCollapsed" class="progress-item"> 232 <span>{{ testOkCount }}/{{ result.length }}</span> 233 <div>Ratio</div> 234 </div> 235 <div v-if="!dashboardCollapsed" class="traffic"> 236 <span> {{ bytesToSize(totalTraffic) }} </span> 237 <div>Traffic</div> 238 </div> 239 <div v-if="!dashboardCollapsed" class="time"> 240 <span>{{ formatSeconds(totalTime) }}</span> 241 <div>Time</div> 242 </div> 243 </div> 244 </el-card> 245 <el-card class="category" v-memo="[result]"> 246 <ul> 247 <li v-if="dashboardCollapsed"> 248 <span>{{ result.filter(item => item.ping > 0).length }}/{{ result.length }}</span> 249 <div>Ratio</div> 250 </li> 251 <li v-if="!dashboardCollapsed"> 252 <span>{{ result.filter(item => item.protocol.startsWith("vmess")).length }}</span> 253 <div>Vmess</div> 254 </li> 255 <li v-if="!dashboardCollapsed"> 256 <span>{{ result.filter(item => item.protocol === "trojan").length }}</span> 257 <div>Trojan</div> 258 </li> 259 <li v-if="!dashboardCollapsed"> 260 <span>{{ result.filter(item => item.protocol === "ssr").length }}</span> 261 <div>SSR</div> 262 </li> 263 <li v-if="!dashboardCollapsed"> 264 <span>{{ result.filter(item => item.protocol === "ss").length }}</span> 265 <div>SS</div> 266 </li> 267 </ul> 268 </el-card> 269 <div class="icon" @click="handleDashboardCollapsed()"> 270 <el-icon v-if="!dashboardCollapsed"><Right /></el-icon> 271 <el-icon v-if="dashboardCollapsed"><Back /></el-icon> 272 <!-- <i v-if="!dashboardCollapsed" class="el-icon-right" ></i> 273 <i v-if="dashboardCollapsed" class="el-icon-back"></i> --> 274 </div> 275 </div> 276 277 <br> 278 279 <el-card v-if="picdata.length"> 280 <div slot="header">导出图片</div> 281 <el-container> 282 <el-image :src="picdata" fit="true" placeholder="未加载图片" id="result_png"></el-image> 283 </el-container> 284 </el-card> 285 </el-col> 286 </el-row> 287 <el-dialog title="Share Links with QRcode" center v-model="qrCodeDialogVisible" width="40%" 288 @opened="handleQRCodeCreate" :before-close="qrCodeHandleClose"> 289 <el-scrollbar style="height:360px;"> 290 <el-row> 291 <el-col v-for="(item, index) of multipleSelection" :key="index" :span="12"> 292 <el-card :body-style="{ padding: '0px', height:'400px'}" shadow="hover" 293 style="width: 320px;height: 330px;text-align: center;"> 294 <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; margin-top: 15px;"> 295 <div :id="'qrcode_' + item.id" style="margin-left: 20px;"></div> 296 <div class="truncate_remark">{{ item.remark }}</div> 297 <div>{{ `${item.ping}ms ${item.speed} ${item.maxspeed}` }}</div> 298 </div> 299 </el-card> 300 </el-col> 301 </el-row> 302 </el-scrollbar> 303 </el-dialog> 304 </div> 305 <script> 306 307 const go = new Go(); 308 WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => { 309 go.run(result.instance); 310 }); 311 312 let themes = { 313 "original": { 314 colorgroup: [ 315 [255, 255, 255], 316 [128, 255, 0], 317 [255, 255, 0], 318 [255, 128, 192], 319 [255, 0, 0] 320 ], 321 bounds: [0, 64 * 1024, 512 * 1024, 4 * 1024 * 1024, 16 * 1024 * 1024], 322 }, 323 "rainbow": { 324 colorgroup: [ 325 [255, 255, 255], 326 [102, 255, 102], 327 [255, 255, 102], 328 [255, 178, 102], 329 [255, 102, 102], 330 [226, 140, 255], 331 [102, 204, 255], 332 [102, 102, 255] 333 ], 334 bounds: [0, 64 * 1024, 512 * 1024, 4 * 1024 * 1024, 16 * 1024 * 1024, 24 * 1024 * 1024, 32 * 1024 * 1024, 40 * 1024 * 1024 ] 335 } 336 } 337 338 let interval = 0; 339 340 let resultjson = []; 341 342 const { createApp } = Vue 343 344 const app = createApp({ 345 data() { 346 return { 347 title: "Lite Speedtest Web GUI", 348 upload: false, 349 filecontent: "", 350 loading: false, 351 subscription: "", 352 concurrency: 2, 353 timeout: 15, 354 unique: true, 355 groupname: "", 356 loadingContent: "", 357 speedtestMode: "all", 358 pingMethod: "googleping", 359 sortMethod: "rspeed", 360 exportMaxSpeed: true, 361 method: "SOCKET", 362 picdata: "", 363 option: 0, 364 multipleSelection: [], 365 qrCodeDialogVisible: false, 366 totalTraffic: 0, 367 totalTime: 0, 368 language: "en", 369 fontSize: 24, 370 theme: "rainbow", 371 generateResultJSON: "", 372 dashboardCollapsed: true, 373 testCount: 0, 374 testOkCount: 0, 375 376 init: { 377 speedtestModes: { 378 all: "全部", 379 pingonly: "Ping only", 380 speedonly: "Speed only", 381 }, 382 pingMethods: { 383 googleping: "Google", 384 tcping: "TCP", 385 }, 386 sortMethods: { 387 rspeed: "speed 倒序", 388 speed: "speed 顺序", 389 ping: "ping 顺序", 390 rping: "ping 倒序", 391 none: "默认", 392 }, 393 themes: { 394 rainbow: "Rainbow", 395 original: "Original", 396 } 397 }, 398 result: [] 399 } 400 }, 401 created() { 402 document.title = this.title; 403 }, 404 methods: { 405 bytesToSize: function (bytes) { 406 const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 407 if (!bytes || bytes === 0) return '0 B'; 408 const i = parseInt(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), 10); 409 if (i === 0) return `${bytes} ${sizes[i]})`; 410 return `${(bytes / (1024 ** i)).toFixed(1)} ${sizes[i]}`; 411 }, 412 testProgress: function (result, testCount) { 413 return result.length ? Math.floor(testCount/result.length*100) : 0 414 }, 415 formatSeconds: function (seconds) { 416 let totalTime = seconds > 0 ? seconds : 0 417 const hours = Math.floor(totalTime / 3600); 418 totalTime %= 3600; 419 const minutes = Math.floor(totalTime / 60); 420 const secs = totalTime % 60; 421 let result = `${secs}s` 422 result = minutes > 0 ? `${minutes}m ${result}` : result 423 result = hours > 0 ? `${hours}h ${result}` : result 424 return result 425 }, 426 incrTotalTime: function () { 427 if (this.totalTime >= 0 && this.loading) { 428 this.$nextTick(() => { 429 setTimeout(() => { 430 this.totalTime++; 431 this.incrTotalTime() 432 }, 1000); 433 }) 434 } 435 }, 436 cancelFileUpload: function () { 437 let self = this; 438 this.file = null; 439 this.filecontent = ''; 440 this.subscription = ''; 441 self.upload = false; 442 }, 443 handleFileChange(e) { 444 let self = this; 445 this.file = e.file; 446 this.errText = ''; 447 if (!this.file || !window.FileReader) return; 448 let reader = new FileReader(); 449 reader.readAsText(this.file); 450 reader.onloadend = function () { 451 self.filecontent = this.result; 452 self.subscription = self.file.name; 453 self.upload = true; 454 } 455 }, 456 beforeUpload(file) { 457 // const isType = file.type === 'application/json' || file.type === 'application/octet-stream' 458 const fsize = file.size / 1024 / 1024 <= 10; 459 // if (!isType) { 460 // this.$message.error('选择的文件格式有误!'); 461 // } 462 if (!fsize) { 463 this.$message.error('上传的文件不能超过10MB!'); 464 } 465 return fsize; 466 }, 467 checkUploadStatus(type) { 468 if (!this.upload) { 469 if (this.subscription.length) 470 return false; 471 else 472 return true; 473 } 474 else { 475 if (type === "if") 476 return true; 477 else if (type === "drag") 478 return false; 479 } 480 }, 481 submit: function () { 482 this.testCount = 0; 483 this.testOkCount = 0; 484 if (!this.subscription.length) { 485 this.$alert("请先输入链接或选择文件!", "错误", { 486 type: "error", 487 }); 488 } else { 489 this.$refs.result.clearSelection(); 490 this.$refs.result.clearFilter(); 491 this.$refs.result.clearSort(); 492 this.loading = true; 493 this.totalTraffic = 0; 494 this.totalTime = 0; 495 this.picdata = ""; 496 this.result = []; 497 this.incrTotalTime() 498 this.loadingContent = "等待后端响应……"; 499 this.starttest(); 500 } 501 }, 502 generateResult: function (params) { 503 if (!this.generateResultJSON) { 504 return 505 } 506 const requestOptions = { 507 method: "POST", 508 headers: { "Content-Type": "application/json" }, 509 body: this.generateResultJSON 510 }; 511 const url = `${window.location.protocol}//${window.location.host}/generateResult` 512 fetch(url, requestOptions) 513 .then(resp => resp.text()) 514 .then(data => { 515 this.picdata = data 516 }) 517 }, 518 terminate: function () { 519 this.loading = false; 520 this.loadingContent = "等待后端响应……"; 521 this.result = []; 522 this.disconnect(); 523 }, 524 handleSelectionChange(val) { 525 // console.log(`select: ${JSON.stringify(val)}`) 526 this.multipleSelection = val; 527 }, 528 handleSortChange(val) { 529 // console.log(`handleSortChange: ${JSON.stringify(val)}`) 530 if (val.prop === "ping") { 531 if (val.order === "ascending") { 532 this.result.sort((obj1, obj2) => { 533 let ping1 = parseFloat(obj1.ping); 534 if (ping1 < 1) { ping1 = 99999 } 535 let ping2 = parseFloat(obj2.ping); 536 if (ping2 < 1) { ping2 = 99999 } 537 return ping1 - ping2 538 }) 539 } else if (val.order === "descending") { 540 this.result.sort((obj1, obj2) => parseFloat(obj2.ping) - parseFloat(obj1.ping)) 541 } else { 542 this.result.sort((obj1, obj2) => obj1.id - obj2.id) 543 } 544 } 545 }, 546 copyToClipboard: async function (data) { 547 if (navigator.clipboard) { 548 await navigator.clipboard.writeText(data) 549 } else { 550 let textArea = document.createElement("textarea"); 551 textArea.value = data; 552 // make the textarea out of viewport 553 textArea.style.position = "fixed"; 554 textArea.style.left = "-999999px"; 555 textArea.style.top = "-999999px"; 556 document.body.appendChild(textArea); 557 textArea.focus(); 558 textArea.select(); 559 document.execCommand('copy'); 560 textArea.remove(); 561 } 562 }, 563 handleCopySub: async function () { 564 // url 565 if (this.subscription.trim().startsWith("http") && !this.subscription.trim().endsWith(".yaml") && !this.subscription.trim().endsWith(".yml")) { 566 await navigator.clipboard.writeText(this.subscription.trim()) 567 this.$message.success("Copy Subscription succeed!"); 568 return 569 } 570 const host = window.location.host; 571 if (host.startsWith("127.0.0.1")) { 572 const url = `${window.location.protocol}//${window.location.host}/getSubscriptionLink` 573 const groupname = this.groupname.trim() || "Default" 574 const requestOptions = { 575 method: "POST", 576 headers: { "Content-Type": "application/json" }, 577 body: JSON.stringify({"filePath": this.subscription.trim(), "group": groupname}) 578 }; 579 fetch(url, requestOptions) 580 .then(resp => resp.text()) 581 .then(data => { 582 navigator.clipboard.writeText(data) 583 this.$message.success("Copy Subscription succeed!"); 584 }) 585 return 586 } 587 this.$message.error("Copy Subscription failed!"); 588 }, 589 handleCopy: async function () { 590 try { 591 links = this.multipleSelection.map(elem => elem.link).join("\n") 592 await this.copyToClipboard(links) 593 this.$message.success("Copy link succeed!"); 594 } catch (err) { 595 this.$message.error("Copy link failed!"); 596 } 597 }, 598 handleCopyAvailable: async function () { 599 try { 600 links = this.result.filter(elem => elem.ping > 0).map(elem => elem.link) 601 await this.copyToClipboard(links.join("\n")) 602 this.$message.success(`Copy ${links.length} link${links.length>1 ? "s" : ""} succeed!`); 603 } catch (err) { 604 this.$message.error("Copy link failed!"); 605 } 606 }, 607 qrCodeHandleClose() { 608 this.qrCodeDialogVisible = false; 609 this.multipleSelection.forEach(item => { 610 document.getElementById('qrcode_' + item.id).innerHTML = ''; 611 }); 612 }, 613 handleQRCode() { 614 this.qrCodeDialogVisible = true 615 }, 616 handleQRCodeCreate: function () { 617 this.$nextTick(() => { 618 const items = this.multipleSelection.map(item => { 619 return { 620 gid: 'qrcode_' + item.id, 621 link: item.link, 622 size: 260 623 } 624 }) 625 wasmQRcode(JSON.stringify(items)) 626 // this.multipleSelection.forEach(item => { 627 // const gid = 'qrcode_' + item.id; 628 // wasmQRcode(gid, item.link, 260, 260) 629 // }) 630 }) 631 }, 632 handleRetest: function () { 633 // const data = { testid: id, testMode: 3, links: [link], ...this.getJSONOptions() } 634 const testids = this.multipleSelection.map(elem => elem.id) 635 const links = this.multipleSelection.map(elem => elem.link) 636 const data = { testMode: 3, ...this.getJSONOptions(), testids, links } 637 this.$refs.result.clearSelection(); 638 this.$refs.result.clearFilter(); 639 this.$refs.result.clearSort(); 640 console.log(`handleRetest: ${JSON.stringify(data)}`) 641 this.send(JSON.stringify(data)); 642 }, 643 saveData: function (data, name) { 644 const blob = new Blob([data], { type: 'text/plain;charset=utf-8;' }) 645 const link = document.createElement('a') 646 if (link == null || link.download == null || link.download == undefined) { 647 return 648 } 649 var event = new Date(); 650 event.setMinutes(event.getMinutes() - event.getTimezoneOffset()); 651 let jsonDate = event.toJSON().slice(0, 19); 652 jsonDate = jsonDate.replaceAll("-", "") 653 jsonDate = jsonDate.replaceAll("T", "") 654 jsonDate = jsonDate.replaceAll(":", "") 655 let url = URL.createObjectURL(blob) 656 link.setAttribute('href', url) 657 link.setAttribute('download', `${name}_${jsonDate}`) 658 link.style.visibility = 'hidden' 659 document.body.appendChild(link) 660 link.click() 661 document.body.removeChild(link) 662 663 }, 664 handleDashboardCollapsed: function () { 665 console.log(this.dashboardCollapsed) 666 this.dashboardCollapsed = !this.dashboardCollapsed 667 }, 668 handleSave: function () { 669 const links = this.multipleSelection.map(elem => { 670 return `# ${elem.remark}\t${elem.ping}\t${elem.speed}\t${elem.maxspeed}\n${elem.link}` 671 }) 672 if (this.subscription.match(/^https?:\/\//g)) { 673 links.unshift(`# ${this.subscription}`) 674 } 675 this.saveData(links.join("\n"), "profile") 676 }, 677 handleExportResult: function (params) { 678 const nodes = this.result.map(item => { 679 const avg_speed = Math.floor(this.getSpeed(item.speed)) || 0 680 const max_speed = Math.floor(this.getSpeed(item.maxspeed)) || 0 681 return { 682 id: item.id, 683 group: item.group, 684 remarks: item.remark, 685 protocol: item.protocol, 686 ping: `${item.ping}`, 687 avg_speed, 688 max_speed, 689 isok: item.ping > 0, 690 } 691 }) 692 const data = { 693 totalTraffic: this.bytesToSize(this.totalTraffic), 694 totalTime: this.formatSeconds(this.totalTime), 695 language: this.language, 696 fontSize: this.fontSize, 697 theme: this.theme, 698 sortMethod: this.sortMethod, 699 nodes, 700 } 701 this.saveData(JSON.stringify(data, null, 2), "result") 702 }, 703 colorCell: function ({ 704 row, 705 column, 706 rowIndex, 707 columnIndex 708 }) { 709 let style = {color: "black", "font-weight": 600}; 710 let speed = 0; 711 switch (columnIndex) { 712 case 5: 713 speed = this.getSpeed(row.speed); 714 break; 715 case 6: 716 speed = this.getSpeed(row.maxspeed); 717 break; 718 default: 719 return style; 720 } 721 if (isNaN(parseFloat(speed))) return style; 722 let color = this.getSpeedColor(speed); 723 // console.log(`speed: ${speed}, row.speed: ${row.speed}, row.maxspeed: ${row.maxspeed} color: ${color}`); 724 style.background = color 725 return style; 726 }, 727 // useNewPalette() { 728 // colorgroup = [ 729 // [255, 255, 255], 730 // [102, 255, 102], 731 // [255, 255, 102], 732 // [255, 178, 102], 733 // [255, 102, 102], 734 // [226, 140, 255], 735 // [102, 204, 255], 736 // [102, 102, 255] 737 // ]; 738 // bounds = [ 739 // 0, 740 // 64 * 1024, 741 // 512 * 1024, 742 // 4 * 1024 * 1024, 743 // 16 * 1024 * 1024, 744 // 24 * 1024 * 1024, 745 // 32 * 1024 * 1024, 746 // 40 * 1024 * 1024 747 // ]; 748 // }, 749 getSpeed(speed) { 750 let value = parseFloat(speed.toString().slice(0, -2)); 751 if (speed.toString().slice(-2) == "MB") { 752 value *= 1048576; 753 } else if (speed.toString().slice(-2) == "KB") { 754 value *= 1024; 755 } else value = parseFloat(speed.toString().slice(0, -1)); 756 return value; 757 }, 758 getColor(lc, rc, level) { 759 let colors = []; 760 let r, g, b; 761 colors.push(parseInt(lc[0] * (1 - level) + rc[0] * level)); 762 colors.push(parseInt(lc[1] * (1 - level) + rc[1] * level)); 763 colors.push(parseInt(lc[2] * (1 - level) + rc[2] * level)); 764 return colors; 765 }, 766 getSpeedColor(speed) { 767 const {colorgroup, bounds} = themes[this.theme]; 768 for (let i = 0; i < bounds.length - 1; i++) { 769 if (speed >= bounds[i] && speed <= bounds[i + 1]) { 770 let color = this.getColor( 771 colorgroup[i], 772 colorgroup[i + 1], 773 (speed - bounds[i]) / (bounds[i + 1] - bounds[i]) 774 ); 775 return "rgb(" + color[0] + "," + color[1] + "," + color[2] + ")"; 776 } 777 } 778 return ( 779 "rgb(" + 780 colorgroup[colorgroup.length - 1][0] + 781 "," + 782 colorgroup[colorgroup.length - 1][1] + 783 "," + 784 colorgroup[colorgroup.length - 1][2] + 785 ")" 786 ); 787 }, 788 connect(url) { 789 try { 790 ws = new WebSocket(url); 791 } catch (ex) { 792 this.loading = false; 793 //this.$message.error('Cannot connect: ' + ex) 794 this.$alert("后端连接错误!请检查后端运行情况!原因:" + ex, "错误"); 795 return; 796 } 797 }, 798 disconnect() { 799 if (ws) { 800 ws.close(); 801 } 802 }, 803 send(msg) { 804 if (ws) { 805 try { 806 ws.send(msg); 807 } catch (ex) { 808 this.$message.error("Cannot send: " + ex); 809 } 810 } else { 811 this.loading = false; 812 //this.$message.error('Cannot send: Not connected') 813 this.$alert("后端连接错误!请检查后端运行情况!", "错误"); 814 } 815 }, 816 getJSONOptions() { 817 let self = this; 818 let groupstr = self.groupname == "" ? "?empty?" : self.groupname; 819 // const options = `^${groupstr}^${self.speedtestMode}^${self.pingMethod}^${self.sortMethod}^${self.exportMaxSpeed}^${self.concurrency}^${self.timeout}` 820 return { 821 group: groupstr, 822 speedtestMode: self.speedtestMode, 823 pingMethod: self.pingMethod, 824 sortMethod: self.sortMethod, 825 unique: self.unique, 826 concurrency: parseInt(self.concurrency), 827 timeout: parseInt(self.timeout), 828 language: self.language, 829 fontSize: parseInt(self.fontSize), 830 theme: self.theme, 831 } 832 }, 833 getOptions() { 834 let self = this; 835 let groupstr = self.groupname == "" ? "?empty?" : self.groupname; 836 const options = `^${groupstr}^${self.speedtestMode}^${self.pingMethod}^${self.sortMethod}^${self.exportMaxSpeed}^${self.concurrency}^${self.timeout}` 837 return options 838 }, 839 starttest() { 840 let self = this; 841 let groupstr = self.groupname == "" ? "?empty?" : self.groupname; 842 this.result = []; 843 this.connect(`ws://${window.location.host}/test`); 844 if (ws) { 845 ws.addEventListener("open", function (ev) { 846 const data = self.getJSONOptions() 847 data.testMode = 2 848 data.subscription = self.upload ? self.filecontent : self.subscription; 849 this.send(JSON.stringify(data)); 850 }); 851 ws.addEventListener("message", this.MessageEvent); 852 } else { 853 this.loading = false; 854 this.$alert("后端连接错误!请检查后端运行情况!", "错误"); 855 } 856 }, 857 loopevent(id, tester) { 858 item = this.result[id]; 859 switch (tester) { 860 case "ping": 861 item.ping = "测试中..."; 862 item.loss = "测试中..."; 863 item.testing = true 864 this.result[id]=item; 865 break; 866 case "speed": 867 item.speed = "测试中..."; 868 item.maxspeed = "测试中..."; 869 item.testing = true 870 this.result[id]=item; 871 break; 872 } 873 }, 874 MessageEvent(ev) { 875 console.log(ev.data); 876 let json = JSON.parse(ev.data); 877 let id = parseInt(json.id); 878 879 let item = {}; 880 switch (json.info) { 881 case "started": 882 this.loadingContent = "后端已启动……"; 883 break; 884 case "fetchingsub": 885 this.loadingContent = "正在获取节点,若节点较多将需要一些时间……"; 886 break; 887 case "begintest": 888 this.loadingContent = "疯狂测速中……"; 889 break; 890 case "gotserver": 891 item = { 892 id: id, 893 group: this.groupname == "" ? json.group : this.groupname, 894 remark: json.remarks, 895 server: json.server, 896 protocol: json.protocol, 897 link: json.link, 898 loss: "0.00%", 899 ping: "0.00", 900 speed: "0.00B", 901 maxspeed: "0.00B" 902 }; 903 this.result[id] = item; 904 break; 905 case "gotservers": 906 json.servers.forEach(json => { 907 item = { 908 id: json.id, 909 group: this.groupname == "" ? json.group : this.groupname, 910 remark: json.remarks, 911 server: json.server, 912 protocol: json.protocol, 913 link: json.link, 914 loss: "0.00%", 915 ping: "0.00", 916 speed: "0.00B", 917 maxspeed: "0.00B" 918 }; 919 this.result[json.id] = item; 920 }) 921 break; 922 case "endone": 923 item = this.result[id]; 924 item.testing = false 925 this.result[id] = item; 926 break; 927 case "startping": 928 //inverval=setInterval("app.loopevent("+id+",\"ping\")",300) 929 this.loopevent(id, "ping"); 930 break; 931 case "gotping": 932 //clearInterval(interval) 933 item = this.result[id]; 934 item.loss = json.loss; 935 item.ping = json.ping || 0; 936 this.testCount += 1 937 if (item.ping > 0) { 938 this.testOkCount += 1 939 } 940 /* 941 item = { 942 "group": json.group, 943 "remark": json.remarks, 944 "loss": json.loss, 945 "ping": json.ping, 946 "speed": "0.00KB" 947 } 948 */ 949 this.result[id] = item; 950 break; 951 case "startspeed": 952 //inverval=setInterval("app.loopevent("+id+",\"speed\")",300) 953 this.loopevent(id, "speed"); 954 break; 955 case "gotspeed": 956 //clearInterval(interval) 957 item = this.result[id]; 958 item.speed = json.speed; 959 item.maxspeed = json.maxspeed; 960 this.totalTraffic += json.traffic 961 this.result[id] = item; 962 break; 963 case "picsaving": 964 this.$notify.info("保存结果图片中……"); 965 break; 966 case "picsaved": 967 this.$notify.success("图片已保存!路径:" + json.path); 968 break; 969 case "picdata": 970 this.picdata = json.data; 971 break; 972 case "eof": 973 this.loading = false; 974 console.log(this.result) 975 break; 976 case "retest": 977 item = this.result[id]; 978 this.$notify.error( 979 "节点 " + item.group + " - " + item.remark + " 第一次测试无速度,将重新测试。" 980 ); 981 break; 982 case "nospeed": 983 item = this.result[id]; 984 this.$notify.error( 985 "节点 " + item.group + " - " + item.remark + " 第二次测试无速度。" 986 ); 987 item.speed = "0.00B"; 988 item.maxspeed = "0.00B"; 989 this.result[id] = item; 990 break; 991 case "error": 992 switch (json.reason) { 993 case "noconnection": 994 item = this.result[id]; 995 item.ping = "0.00"; 996 item.loss = "100.00%"; 997 this.$notify.error( 998 "节点 " + item.group + " - " + item.remark + " 无法连接。" 999 ); 1000 this.result[id] = item; 1001 break; 1002 case "noresolve": 1003 item = this.result[id]; 1004 item.ping = "0.00"; 1005 item.loss = "100.00%"; 1006 this.$notify.error( 1007 "节点 " + item.group + " - " + item.remark + " 无法解析到 IP 地址。" 1008 ); 1009 this.result[id] = item; 1010 break; 1011 case "nonodes": 1012 this.$alert("找不到任何节点。请检查订阅链接。", "错误"); 1013 break; 1014 case "invalidsub": 1015 this.$alert("订阅获取异常。请检查订阅链接。", "错误"); 1016 this.terminate() 1017 break; 1018 case "norecoglink": 1019 this.$alert("找不到任何链接。请检查提供的链接格式。", "错误"); 1020 break; 1021 case "unhandled": 1022 this.$alert("程序异常退出!", "错误"); 1023 break; 1024 } 1025 console.log("error:" + json.reason); 1026 break; 1027 } 1028 }, 1029 floatSort: function (obj1, obj2) { 1030 return parseFloat(obj1.ping) - parseFloat(obj2.ping); 1031 }, 1032 speedSort: function (obj1, obj2) { 1033 const speed1 = isNaN(this.getSpeed(obj1.speed)) ? -1 : this.getSpeed(obj1.speed); 1034 const speed2 = isNaN(this.getSpeed(obj2.speed)) ? -1 : this.getSpeed(obj2.speed); 1035 return speed1 - speed2; 1036 }, 1037 maxSpeedSort: function (obj1, obj2) { 1038 const speed1 = isNaN(this.getSpeed(obj1.maxspeed)) ? -1 : this.getSpeed(obj1.maxspeed); 1039 const speed2 = isNaN(this.getSpeed(obj2.maxspeed)) ? -1 : this.getSpeed(obj2.maxspeed); 1040 return speed1 - speed2; 1041 }, 1042 filterPing: function (value, row) { 1043 return value === "available" ? row.ping > 0 : true; 1044 }, 1045 filterAvgSpeed: function (value, row) { 1046 const speed = isNaN(this.getSpeed(row.speed)) ? -1 : this.getSpeed(row.speed); 1047 return speed >= value; 1048 }, 1049 filterMaxSpeed: function (value, row) { 1050 const speed = isNaN(this.getSpeed(row.maxspeed)) ? -1 : this.getSpeed(row.maxspeed); 1051 return speed >= value; 1052 }, 1053 filterProtocol: function (value, row) { 1054 if (value === "vmess") { 1055 return row.protocol.startsWith("vmess") 1056 } 1057 if (value === "trojan") { 1058 return row.protocol.startsWith("trojan") 1059 } 1060 return value === row.protocol 1061 }, 1062 checkSelectable: function (row, index) { 1063 return !!row.link && row.hasOwnProperty("id") && row.testing !== true 1064 }, 1065 } 1066 }); 1067 for ([name, comp] of Object.entries(ElementPlusIconsVue)) { 1068 app.component(name, comp); 1069 } 1070 app.use(ElementPlus); 1071 app.mount('#app') 1072 </script> 1073 </body> 1074 1075 </html>