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>