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