Compare commits

...

8 Commits
master ... dev

Author SHA1 Message Date
lingling a54e9402ac 修复生成链接时不添加伪装域名和sni问题 2024-12-10 20:24:19 +08:00
lingling d7f4630047 修改打包模式为hasa 2024-12-06 18:37:20 +08:00
lingling c8aeaad963 修改端口解决新后端api对接问题 2024-12-06 18:25:41 +08:00
lingling 4cc3befc07 修改后端api程式 2024-12-05 20:58:06 +08:00
lingling 9c65fb2491 完成 2024-12-01 22:32:34 +08:00
lingling 6f551540f0 创建链接还是有问题 2024-12-01 01:37:10 +08:00
lingling 771d4223bc 代码逻辑分离 2024-11-29 22:44:13 +08:00
lingling a51faba9af 暂存 2024-11-28 22:05:04 +08:00
23 changed files with 8218 additions and 1170 deletions

6385
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,14 +14,19 @@
"@quasar/extras": "^1.0.0", "@quasar/extras": "^1.0.0",
"@waiting/base64": "^4.2.9", "@waiting/base64": "^4.2.9",
"axios": "^1.2.1", "axios": "^1.2.1",
"chai": "^5.1.2",
"js-base64": "^3.7.5", "js-base64": "^3.7.5",
"link-chang": "file:",
"mocha": "^10.8.2",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-router": "^4.0.0", "vue-router": "^4.0.0",
"vuex": "^4.0.1" "vuex": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-typescript": "^7.26.0",
"@quasar/app-vite": "^1.0.0", "@quasar/app-vite": "^1.0.0",
"@quasar/quasar-app-extension-testing": "^2.2.0",
"@types/node": "^12.20.21", "@types/node": "^12.20.21",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0", "@typescript-eslint/parser": "^5.10.0",
@ -33,7 +38,7 @@
"typescript": "^4.5.4" "typescript": "^4.5.4"
}, },
"engines": { "engines": {
"node": "^18 || ^16 || ^14.19", "node": "^22 || ^18 || ^16 || ^14.19",
"npm": ">= 6.13.4", "npm": ">= 6.13.4",
"yarn": ">= 1.21.1" "yarn": ">= 1.21.1"
} }

View File

@ -53,7 +53,7 @@ module.exports = configure(function (/* ctx */) {
node: 'node16', node: 'node16',
}, },
gzip: true, gzip: true,
vueRouterMode: 'history', // available values: 'hash', 'history' vueRouterMode: 'hash', // available values: 'hash', 'history'
// vueRouterBase, // vueRouterBase,
// vueDevtools, // vueDevtools,
// vueOptionsAPI: false, // vueOptionsAPI: false,
@ -80,6 +80,7 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: { devServer: {
// https: true // https: true
port: 9090,
open: true, // opens browser window automatically open: true, // opens browser window automatically
}, },

7
quasar.extensions.json Normal file
View File

@ -0,0 +1,7 @@
{
"@quasar/testing": {
"harnesses": [
"unit-jest@beta"
]
}
}

117
src/Util/Link.ts Normal file
View File

@ -0,0 +1,117 @@
import { decode, encode } from 'js-base64'
import { getdata } from 'src/api/api';
import { VlessLink, vmess } from 'src/models/models';
import { is_ip } from './comm';
import { vmessDefault } from 'src/config';
/**
* vmess
* @param ip IP地址
* @returns vmess链接
*/
async function CreatVmessDirect(ip: string): Promise<string> {
let tmp = ''
const api = new getdata;
const obj = JSON.parse(decode(vmessDefault))
const array = ip.split(/[\s\n]/)
for (let index = 0; index < array.length; index++) {
if (!is_ip(array[index])) {
return ''
}
for (let index = 0; index < array.length; index++) {
const name = await api.get_country(array[index])
obj.ps = name + array[index]
obj.add = array[index]
tmp += 'vmess://' + encode(JSON.stringify(obj)) + '\n'
}
}
return tmp
}
function ChangVmessServer() {
return 0
}
/**
*
* @param type vmess |vless
*/
function CreateLink(type: 'vmess' | 'vless', linkinfo: vmess | VlessLink) {
let tmp = ''
switch (true) {
case type === 'vless':
if ('uuid' in linkinfo) {
tmp = createVlessLink(linkinfo)
}
break;
case type === 'vmess':
break;
default:
break;
}
return tmp
}
function parseVlessLink(link: string): VlessLink | null {
const regex = /^vless:/;
const match = link.match(regex);
if (!match) {
console.error('Invalid VLESS link format.');
return null;
}
const uuid = match[1];
const host = match[2];
const port = parseInt(match[3], 10);
const params = parseParams(match[4]); // 解析参数
const name = match[5] ? decodeURIComponent(match[5].slice(1)) : undefined; // 去掉 "#" 并解码
return {
uuid,
host,
port,
params,
name,
};
}
function parseParams(paramsString?: string): Record<string, string> | undefined {
if (!paramsString || !paramsString.startsWith('?')) return undefined;
return paramsString
.slice(1) // 去掉开头的 "?"
.split('&') // 分割每个键值对
.reduce<Record<string, string>>((acc, pair) => {
const [key, value] = pair.split('=');
if (key) acc[key] = value || ''; // 处理可能的无值参数
return acc;
}, {});
}
/**
* vless链接
* @param param0
* @returns
*/
function createVlessLink({ uuid, host, port, params, name }: VlessLink): string {
// 基本的 VLESS 链接格式
let link = `vless://${uuid}@${host}:${port}`;
// 如果有查询参数,则拼接它们
if (params && Object.keys(params).length > 0) {
const queryString = new URLSearchParams(params).toString();
link += `?${queryString}`;
}
// 如果有名称字段,将名称放在 # 后面
if (name) {
link += `#${encodeURIComponent(name)}`;
}
return link + '\n';
}
export { CreatVmessDirect, ChangVmessServer, CreateLink, parseVlessLink }

82
src/Util/comm.ts Normal file
View File

@ -0,0 +1,82 @@
import { copyToClipboard, QVueGlobals } from 'quasar'
/**
*
* @param string
*/
function copy(string: string, $q: QVueGlobals) {
copyToClipboard(string)
.then(() => {
// success!
$q.notify({
message: '复制成功',
color: 'positive',
position: 'top'
})
})
.catch(() => {
// fail
$q.notify({
message: '复制失败',
color: 'negative'
})
})
}
/**
*
* @param bili ms
* @returns
*/
function getColorForDelay(delay: number): string {
// 限制延迟在200到2000之间
const clampedDelay = Math.min(Math.max(delay, 200), 2000);
// 将延迟从[200, 2000]映射到[0, 1]
const normalizedDelay = (clampedDelay - 200) / (2000 - 200);
// 计算从绿色到红色的颜色变化
const green = 255 - normalizedDelay * 255;
const red = normalizedDelay * 255;
// 返回 RGB 颜色格式
return `rgb(${Math.round(red)}, ${Math.round(green)}, 0)`;
}
/**
*
* @param bili
* @returns
*/
function signal_style(bili: number) {
let tmp = ''
if (bili >= 0 && bili <= 700) {
tmp = 'green'
} else if (bili > 700 && bili < 1400) {
tmp = '#FF9800'
} else {
tmp = 'red'
}
return tmp
}
const is_ip = (ip: string) => {
const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
return reg.test(ip);
}
function extractIPv4(ip: string): string | null {
const ipv4Regex = /(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)/;
const matches = ip.match(ipv4Regex);
// 如果没有匹配到任何IPv4地址返回 null
if (!matches) {
return null;
}
// 返回第一个匹配到的IPv4地址
return matches[0];
}
export { copy, getColorForDelay, signal_style, is_ip, extractIPv4 }

152
src/Util/node/Vless.ts Normal file
View File

@ -0,0 +1,152 @@
import { Base64 } from 'js-base64';
class VLESSQuery {
constructor(
public security: string,
public alpn: string[],
public sni: string,
public fp: string,
public sid: string,
public pbk: string,
public flow: string,
public encryption: string,
public type: string,
public headerType: string,
public path: string,
public host: string
) { }
}
class VLESS {
constructor(
public name: string,
public uuid: string,
public server: string,
public port: number,
public query: VLESSQuery
) { }
// Base64 解码函数
// private static base64Decode(str: string): string {
// return Buffer.from(str, 'base64').toString('utf-8');
// }
// 编码 VLESS URL
encodeURL(): string {
const url = new URL(`vless://${this.uuid}@${this.server}:${this.port}`);
// 设置 Query 参数
const query = url.searchParams;
query.set('security', this.query.security);
query.set('sni', this.query.sni);
query.set('fp', this.query.fp);
query.set('sid', this.query.sid);
query.set('pbk', this.query.pbk);
query.set('flow', this.query.flow);
query.set('encryption', this.query.encryption);
query.set('type', this.query.type);
query.set('headerType', this.query.headerType);
query.set('path', this.query.path);
query.set('host', this.query.host);
// 移除空值参数
for (const [key, value] of query) {
if (value === '') {
query.delete(key);
}
}
// 如果有 name设置为 Fragment
if (this.name) {
url.hash = this.name;
} else {
url.hash = `${this.server}:${this.port}`;
}
return url.toString();
}
// 解码 VLESS URL
static decodeURL(urlStr: string): VLESS {
if (!urlStr.startsWith('vless://')) {
throw new Error(`Invalid vless URL: ${urlStr}`);
}
const decodedUrl = urlStr.split('@');
const url = new URL(`http://${decodedUrl[1]}`);
const regex = /[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/;
const match = urlStr.match(regex);
const uuid = match[0];
const hostname = url.hostname;
const port = parseInt(url.port, 10);
const query = url.searchParams;
const encryption = query.get('encryption') || '';
const security = query.get('security') || '';
const type = query.get('type') || '';
const flow = query.get('flow') || '';
const headerType = query.get('headerType') || '';
const pbk = query.get('pbk') || '';
const sid = query.get('sid') || '';
const fp = query.get('fp') || '';
const alpns = query.get('alpn') || '';
const alpn = alpns ? alpns.split(',') : [];
const sni = query.get('sni') || '';
const path = query.get('path') || '';
const host = query.get('host') || '';
// 获取 name若为空则使用 `hostname:port`
const name = url.hash ? decodeURIComponent(url.hash.slice(1)) : `${hostname}:${port}`;
const queryObj = new VLESSQuery(
security,
alpn,
sni,
fp,
sid,
pbk,
flow,
encryption,
type,
headerType,
path,
host
);
return new VLESS(name, uuid, hostname, port, queryObj);
}
}
// // 示例用法
// function callVLESS() {
// const vless = new VLESS(
// 'Sharon-香港',
// '6adb4f43-9813-45f4-abf8-772be7db08sd',
// 'ss.com',
// 443,
// new VLESSQuery(
// 'reality',
// ['http/1.1'],
// 'ss.com',
// 'chrome',
// '',
// 'g-oxbqigzCaXqARxuyD2_vbTYeMD9zn8wnTo02S69QM',
// 'xtls-rprx-vision',
// 'none',
// 'tcp',
// 'none',
// '',
// ''
// )
// );
// const encoded = vless.encodeURL();
// console.log('Encoded VLESS URL:', encoded);
// const decoded = VLESS.decodeURL(encoded);
// console.log('Decoded VLESS:', decoded);
// }
// callVLESS();
export { VLESS }

122
src/Util/node/Vmess.ts Normal file
View File

@ -0,0 +1,122 @@
class Vmess {
add?: string;
aid?: any; // This could be number or string depending on usage
alpn?: string;
fp?: string;
host?: string;
id?: string;
net?: string;
path?: string;
port?: any; // This could be a number or string
ps?: string;
scy?: string;
sni?: string;
tls?: string;
type?: string;
v?: string;
constructor(data: {
add?: string;
aid?: any;
alpn?: string;
fp?: string;
host?: string;
id?: string;
net?: string;
path?: string;
port?: any;
ps?: string;
scy?: string;
sni?: string;
tls?: string;
type?: string;
v?: string;
}) {
this.add = data.add;
this.aid = data.aid;
this.alpn = data.alpn;
this.fp = data.fp;
this.host = data.host;
this.id = data.id;
this.net = data.net;
this.path = data.path;
this.port = data.port;
this.ps = data.ps;
this.scy = data.scy;
this.sni = data.sni;
this.tls = data.tls;
this.type = data.type;
this.v = data.v;
}
// 编码 Vmess URL
encodeURL(): string {
// 如果备注为空,则使用服务器地址 + 端口
if (!this.ps) {
this.ps = `${this.add}:${this.port}`;
}
// 如果版本为空,则默认为 2
if (!this.v) {
this.v = '2';
}
const param = JSON.stringify(this);
return 'vmess://' + this.base64Encode(param);
}
// 解码 Vmess URL
static decodeURL(url: string): Vmess {
if (!url.startsWith('vmess://')) {
throw new Error(`Invalid vmess URL: ${url}`);
}
const param = url.slice(8); // Remove "vmess://"
const decoded = Vmess.base64Decode(param.trim());
try {
const parsed = JSON.parse(decoded);
if (parsed.scy === '') {
parsed.scy = 'auto';
}
if (!parsed.ps) {
parsed.ps = parsed.add + ':' + parsed.port;
}
return new Vmess(parsed);
} catch (error) {
throw new Error(`Failed to parse VMESS URL: ${error}`);
}
}
// Base64 编码
private base64Encode(str: string): string {
return Buffer.from(str, 'utf-8').toString('base64');
}
// Base64 解码
private static base64Decode(str: string): string {
return Buffer.from(str, 'base64').toString('utf-8');
}
}
// 开发者测试
function callVmessURL() {
const vmess = new Vmess({
add: 'xx.xxx.ru',
port: '2095',
aid: 0,
scy: 'auto',
net: 'ws',
type: 'none',
id: '7a737f41-b792-4260-94ff-3d864da67380',
host: 'xx.xxx.ru',
path: '/',
tls: ''
});
const encoded = vmess.encodeURL();
console.log('Encoded VMESS URL:', encoded);
const decoded = Vmess.decodeURL(encoded);
console.log('Decoded VMESS:', decoded);
}
callVmessURL();

View File

@ -1,13 +1,32 @@
import { api } from 'src/boot/axios'; import { api } from 'src/boot/axios';
class getdata { class getdata {
async get_server() { async get_server() {
return await api.get('https://api.shagain.club/api.php?type=list'); return await api.get('https://api.giaogiao.uk/server/getlist');
} }
async text_server(url: string) { async text_server(url: string) {
return await api.post('https://api.shagain.club/textserver', { url }); const res = await api.post(
'https://service-7hslob28-1258902677.nj.apigw.tencentcs.com/release/',
{ url }
);
const tmp = { 'is_online': res.data.sataus == 400, 'time': res.data.time }
return tmp
} }
async get_country(ip: string) { async get_country(ip: string) {
return await api.post('https://api.shagain.club/api.php?type=searchip', { ip }); const res = await api.post('https://api.giaogiao.uk/server/searchip', {
ip,
});
// res = res.data.country
// if (typeof (res) == 'string') { return res }
// console.log(res.data.data.country)
return res.data.data.country
} }
async get_server_ms(ip: string, port: number, to_ip: string, istls: number) {
const http = istls == 0 ? 'http' : 'https'
return this.text_server(`${http}://${ip}:${port}/${to_ip}`)
}
} }
export { getdata }; export { getdata };

View File

@ -1,49 +0,0 @@
<template>
<q-item
clickable
tag="a"
target="_blank"
:href="link"
>
<q-item-section
v-if="icon"
avatar
>
<q-icon :name="icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
<q-item-label caption>{{ caption }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'EssentialLink',
props: {
title: {
type: String,
required: true
},
caption: {
type: String,
default: ''
},
link: {
type: String,
default: '#'
},
icon: {
type: String,
default: ''
}
}
});
</script>

View File

@ -1,64 +0,0 @@
<template>
<div>
<p>{{ title }}</p>
<ul>
<li v-for="todo in todos" :key="todo.id" @click="increment">
{{ todo.id }} - {{ todo.content }}
</li>
</ul>
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
<p>Active: {{ active ? 'yes' : 'no' }}</p>
<p>Clicks on todos: {{ clickCount }}</p>
</div>
</template>
<script lang="ts">
import {
defineComponent,
PropType,
computed,
ref,
toRef,
Ref,
} from 'vue';
import { Todo, Meta } from './models';
function useClickCount() {
const clickCount = ref(0);
function increment() {
clickCount.value += 1
return clickCount.value;
}
return { clickCount, increment };
}
function useDisplayTodo(todos: Ref<Todo[]>) {
const todoCount = computed(() => todos.value.length);
return { todoCount };
}
export default defineComponent({
name: 'ExampleComponent',
props: {
title: {
type: String,
required: true
},
todos: {
type: Array as PropType<Todo[]>,
default: () => []
},
meta: {
type: Object as PropType<Meta>,
required: true
},
active: {
type: Boolean
}
},
setup (props) {
return { ...useClickCount(), ...useDisplayTodo(toRef(props, 'todos')) };
},
});
</script>

View File

@ -8,8 +8,7 @@
:style="{ color: signal_style }" /><span>{{ time }}ms</span></div> :style="{ color: signal_style }" /><span>{{ time }}ms</span></div>
<q-input v-model="outtext" filled autogrow readonly /> <q-input v-model="outtext" filled autogrow readonly />
<div style="text-align: center;margin-top: 1rem;"> <div style="text-align: center;margin-top: 1rem;">
<q-btn color="white" text-color="black" @click="copy('link')" label="复制" /> <q-btn color="white" text-color="black" @click="copy_link()" label="复制" />
<!-- <q-btn color="white" text-color="black" @click="copy('sub')" label="复制订阅" /> -->
</div> </div>
</div> </div>
</div> </div>
@ -20,11 +19,13 @@ import {
defineComponent, defineComponent,
computed, computed,
ref, ref,
onMounted,
} from 'vue'; } from 'vue';
import { copyToClipboard } from 'quasar'
import { encode, decode } from 'js-base64';
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { getdata } from 'src/api/api' import { copy, extractIPv4, getColorForDelay } from 'src/Util/comm';
import { vlessLink } from 'src/config';
import { getdata } from 'src/api/api';
import { CreateLink } from 'src/Util/Link';
export default defineComponent({ export default defineComponent({
name: 'LinkItem', name: 'LinkItem',
props: { props: {
@ -32,88 +33,46 @@ export default defineComponent({
type: Object, type: Object,
required: true required: true
}, },
text: { names: {
type: String type: Array as () => string[],
required: true
} }
}, },
setup(props) { setup(props) {
const online = ref(false); const online = ref(false);
const isonline = (ip: string) => {
let http = props.serve.istls == 0 ? 'http' : 'https'
api.text_server(`${http}://${props.serve.ip}:${props.serve.port}/${ip}`).then(res => {
online.value = res.data.sataus == 400
time.value = res.data.time
})
}
const api = new getdata;
const $q = useQuasar() const $q = useQuasar()
const time = ref(0) const time = ref(0)
const outtext = computed(() => { const outtext = ref('')
let tmp = '' const api = new getdata;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let text = props.text!.replace(/\ +/g, '');
text = text.replace(/[\r\n]/g, '');
let arr = text.split('vmess://');
for (let iterator of arr) {
if (!(iterator.length > 0)) continue
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
online.value = false
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let obj = JSON.parse(decode(iterator));
obj.add = props.serve.ip
obj.port = props.serve.port
obj.host = props.serve.host
obj.tls = props.serve.istls == 0 ? '' : 'tls'
let reg = new RegExp(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/);
obj.path = '/' + obj.ps.match(reg)[0];
let ip = obj.ps.match(reg)[0]
isonline(ip)
tmp += 'vmess://' + encode(JSON.stringify(obj)) + '\n'
}
return tmp
})
const signal_style = computed(() => { const signal_style = computed(() => {
let tmp = '' return getColorForDelay(time.value)
if (time.value >= 0 && time.value <= 700) { })
tmp = 'green'
} else if (time.value > 700 && time.value < 1400) { onMounted(() => {
tmp = '#FF9800' for (let index = 0; index < props.names.length; index++) {
} else { let ip = extractIPv4(props.names[index])
tmp = 'red' if (ip == null) { continue }
let vless = vlessLink
vless.name = props.names[index]
vless.host = props.serve.host
vless.port = props.serve.port
if (props.serve.istls == 1 && vless.params) {
vless.params.security = 'tls'
vless.params.path = `/${ip}`
vless.params.host = props.serve.host
vless.params.sni = props.serve.host
} }
return tmp api.get_server_ms(props.serve.host, props.serve.port, ip, props.serve.istls).then((res) => {
console.log(res)
time.value = res.time
online.value = res.is_online
}) })
const copy = (type: string) => {
let tmp = '' outtext.value += CreateLink('vless', vless)
switch (type) {
case 'sub':
tmp = `http://149.129.107.38/link.php?link=${outtext.value}`
break;
case 'link':
tmp = outtext.value
break;
default:
break;
} }
copyToClipboard(tmp)
.then(() => {
// success!
$q.notify({
message: '复制成功',
color: 'positive',
position: 'top'
}) })
}) const copy_link = () => { copy(outtext.value, $q) }
.catch(() => { return { props, online, time, signal_style, outtext, copy_link };
// fail
$q.notify({
message: '复制失败',
color: 'negative'
})
})
}
return { props, outtext, copy, online, time, signal_style };
}, },
}); });
</script> </script>

View File

@ -6,9 +6,8 @@
<div v-show="online" style="text-align: center;"><q-icon title="在线" name="signal_cellular_alt" <div v-show="online" style="text-align: center;"><q-icon title="在线" name="signal_cellular_alt"
:style="{ color: signal_style }" /><span>{{ time }}ms</span></div> :style="{ color: signal_style }" /><span>{{ time }}ms</span></div>
<q-input v-model="outtext" filled autogrow readonly /> <q-input v-model="outtext" filled autogrow readonly />
<div style="text-align: center;margin-top: 1rem;display: flex;justify-content: space-around;"> <div style="text-align: center;margin-top: 1rem;">
<q-btn color="white" text-color="black" @click="copy('link')" label="复制" /> <q-btn color="white" text-color="black" @click="copy_link()" label="复制" />
<!-- <q-btn color="white" text-color="black" @click="copy('sub')" label="复制订阅" /> -->
</div> </div>
</div> </div>
</div> </div>
@ -19,99 +18,54 @@ import {
defineComponent, defineComponent,
computed, computed,
ref, ref,
onMounted,
} from 'vue'; } from 'vue';
import { copyToClipboard } from 'quasar'
import { encode, decode } from 'js-base64';
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { getdata } from 'src/api/api' import { getdata } from 'src/api/api'
import { vlessLink } from 'src/config';
import { copy, extractIPv4, getColorForDelay } from 'src/Util/comm';
import { CreateLink } from 'src/Util/Link';
export default defineComponent({ export default defineComponent({
name: 'LinkItem', name: 'LinkItem',
props: { props: {
text: { names: {
type: String type: Array as () => string[],
required: true
} }
}, },
setup(props) { setup(props) {
const time = ref(0);
const online = ref(false); const online = ref(false);
const isonline = (ip: string) => {
let http = 'http'
api.text_server(`${http}://${ip}:9000/`).then(res => {
online.value = res.data.sataus == 400
time.value = res.data.time
})
}
const def_link = 'ew0KICAidiI6ICIyIiwNCiAgInBzIjogIjAiLA0KICAiYWRkIjogIjE4NS4yMTguNi4xMDgiLA0KICAicG9ydCI6ICI5MDAwIiwNCiAgImlkIjogIjJlZTU3ODA2LWY2ZTQtNDgyYS1lZjA4LTczNjBjMDRjZDNlNSIsDQogICJhaWQiOiAiMCIsDQogICJzY3kiOiAiYXV0byIsDQogICJuZXQiOiAid3MiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiIiwNCiAgInBhdGgiOiAiLyIsDQogICJ0bHMiOiAiIiwNCiAgInNuaSI6ICIiLA0KICAiYWxwbiI6ICIiDQp9'
const api = new getdata; const api = new getdata;
const $q = useQuasar() const $q = useQuasar()
const outtext = computed(() => { const time = ref(0)
let tmp = '' const outtext = ref('')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let text = props.text!.replace(/\ +/g, '');
text = text.replace(/[\r\n]/g, '');
let arr = text.split('vmess://');
for (let iterator of arr) {
if (!(iterator.length > 0)) continue
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
online.value = false
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let obj = JSON.parse(decode(def_link));
let obj_old = JSON.parse(decode(iterator));
let reg = new RegExp(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/);
let ip = obj_old.ps.match(reg)[0]
obj.ps = obj_old.ps
obj.add = ip
obj.port = 9000
obj.host = ''
obj.tls = ''
obj.path = '/';
isonline(ip)
tmp += 'vmess://' + encode(JSON.stringify(obj)) + '\n'
}
return tmp
})
const signal_style = computed(() => { const signal_style = computed(() => {
let tmp = '' return getColorForDelay(time.value)
if (time.value >= 0 && time.value <= 700) { })
tmp = 'green'
} else if (time.value > 700 && time.value < 1400) { onMounted(() => {
tmp = '#FF9800' for (let index = 0; index < props.names.length; index++) {
} else { let ip = extractIPv4(props.names[index])
tmp = 'red' if (ip == null) { continue }
let vless = vlessLink
vless.name = props.names[index]
vless.host = ip
vless.port = 9000
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
vless.params!.path = '/'
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
vless.params!.security = 'none'
api.get_server_ms(ip, 9000, '', 0).then((res) => {
time.value = res.time
online.value = res.is_online
})
outtext.value += CreateLink('vless', vless)
} }
return tmp
}) })
const copy = (type: string) => { const copy_link = () => { copy(outtext.value, $q) }
let tmp = '' return { props, online, time, signal_style, outtext, copy_link };
switch (type) {
case 'sub':
tmp = `http://149.129.107.38/link.php?link=${outtext.value}`
break;
case 'link':
tmp = outtext.value
break;
default:
break;
}
copyToClipboard(tmp)
.then(() => {
// success!
$q.notify({
message: '复制成功',
color: 'positive',
position: 'top'
})
})
.catch(() => {
// fail
$q.notify({
message: '复制失败',
color: 'negative'
})
})
}
return { props, outtext, copy, online, signal_style, time };
}, },
}); });
</script> </script>

View File

@ -1,33 +0,0 @@
export interface Todo {
id: number;
content: string;
}
export interface Meta {
totalCount: number;
}
export interface server {
host: string;
id: number;
ip: string;
istls: number;
port: number;
tips: string;
}
export interface vmess {
v: string;
ps: string;
add: string;
port: number;
id: string;
aid: string;
scy: string;
net: string;
type: string;
host: string;
path: string;
tls: string;
sni: string;
alpn: string
}

23
src/config/index.ts Normal file
View File

@ -0,0 +1,23 @@
import { VlessLink } from 'src/models/models';
/**
* vmess模板链接
*/
const vmessDefault = 'ew0KICAidiI6ICIyIiwNCiAgInBzIjogIjAiLA0KICAiYWRkIjogIjE4NS4yMTguNi4xMDgiLA0KICAicG9ydCI6ICI5MDAwIiwNCiAgImlkIjogIjJlZTU3ODA2LWY2ZTQtNDgyYS1lZjA4LTczNjBjMDRjZDNlNSIsDQogICJhaWQiOiAiMCIsDQogICJzY3kiOiAiYXV0byIsDQogICJuZXQiOiAid3MiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiIiwNCiAgInBhdGgiOiAiLyIsDQogICJ0bHMiOiAiIiwNCiAgInNuaSI6ICIiLA0KICAiYWxwbiI6ICIiDQp9'
const vlessDefault = 'vless://2ee57806-f6e4-482a-ef08-7360c04cd3e5@1.1.1.1:9000?encryption=none&security=none&type=ws&path=%2F#%E9%9F%A9%E5%9B%BD1.1.1.1'
/**
* vless对象模板
*/
const vlessLink: VlessLink = {
uuid: '2ee57806-f6e4-482a-ef08-7360c04cd3e5',
host: '150.109.81.208',
port: 9000,
params: {
encryption: 'none',
security: 'none',
type: 'ws',
path: '/'
},
name: '韩国150.109.81.208'
};
export { vmessDefault, vlessDefault, vlessLink }

67
src/models/models.ts Normal file
View File

@ -0,0 +1,67 @@
export interface Todo {
id: number;
content: string;
}
export interface Meta {
totalCount: number;
}
/**
*
*/
export interface server {
host: string;
id: number;
ip: string;
istls: number;
port: number;
tips: string;
}
/**
* vmess结构体
*/
export interface vmess {
v: string;
ps: string;
add: string;
port: number;
id: string;
aid: string;
scy: string;
net: string;
type: string;
host: string;
path: string;
tls: string;
sni: string;
alpn: string
}
/**
* vless结构体
*/
export interface vless {
add: string;
port: number;
uuid: string;
encryption: string;
/**传输协议 */
net: string;
/**伪装类型 */
type: string;
/**地址 */
host: string;
/**路径 */
path: string;
/**tls */
tls: string;
/**sni */
sni: string;
name: string
}
export interface VlessLink {
uuid: string;
host: string;
port: number;
params?: Record<string, string>;
name?: string; // 允许传入名称
}

View File

@ -7,7 +7,7 @@
<q-input v-model="text" filled autogrow placeholder="IP" /> <q-input v-model="text" filled autogrow placeholder="IP" />
<div style="height:1rem"></div> <div style="height:1rem"></div>
<q-input v-model="outlink" filled autogrow /> <q-input v-model="outlink" filled autogrow />
<q-btn color="white" text-color="black" @click="copy" label="复制" /> <q-btn color="white" text-color="black" @click="copy_link" label="复制" />
</div> </div>
</div> </div>
</div> </div>
@ -19,69 +19,46 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, watch } from 'vue'; import { defineComponent, ref, watch } from 'vue';
import { getdata } from 'src/api/api' import { copy, is_ip } from 'src/Util/comm';
import { decode, encode } from 'js-base64'; import { CreateLink, CreatVmessDirect } from 'src/Util/Link';
import { copyToClipboard, useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { vlessLink } from 'src/config';
import { getdata } from 'src/api/api';
import { VLESS } from 'src/Util/node/Vless';
export default defineComponent({ export default defineComponent({
name: 'CreateLink', name: 'CreateLink',
setup() { setup() {
const $q = useQuasar()
const api = new getdata;
const text = ref(''); const text = ref('');
const outlink = ref(''); const outlink = ref('');
const def_link = 'ew0KICAidiI6ICIyIiwNCiAgInBzIjogIjAiLA0KICAiYWRkIjogIjE4NS4yMTguNi4xMDgiLA0KICAicG9ydCI6ICI5MDAwIiwNCiAgImlkIjogIjJlZTU3ODA2LWY2ZTQtNDgyYS1lZjA4LTczNjBjMDRjZDNlNSIsDQogICJhaWQiOiAiMCIsDQogICJzY3kiOiAiYXV0byIsDQogICJuZXQiOiAid3MiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiIiwNCiAgInBhdGgiOiAiLyIsDQogICJ0bHMiOiAiIiwNCiAgInNuaSI6ICIiLA0KICAiYWxwbiI6ICIiDQp9' const $q = useQuasar()
const is_ip = (ip: string) => { const api = new getdata();
var reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
return reg.test(ip);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
watch(() => text.value, (newValue, oldValue) => { // watch(() => text.value, async (newValue, oldValue) => { //
let obj = JSON.parse(decode(def_link))
let array = newValue.split(/[\s\n]/) let array = newValue.split(/[\s\n]/)
for (let index = 0; index < array.length; index++) { for (let index = 0; index < array.length; index++) {
if (!is_ip(array[index])) { if (!is_ip(array[index])) {
outlink.value = 'erroe ip' outlink.value = 'erroe ip'
return return
} }
} }
outlink.value = '' outlink.value = ''
for (let index = 0; index < array.length; index++) { for (let index = 0; index < array.length; index++) {
api.get_country(array[index]).then(res => { // outlink.value += await CreatVmessDirect(array[index])
// let name = res.data.country == '' ? res.data.province : res.data.country let vless = vlessLink
let name = res.data.country let country = await api.get_country(array[index])
obj.ps = name + array[index] console.log(country)
obj.add = array[index] vless.name = country + array[index]
outlink.value += 'vmess://' + encode(JSON.stringify(obj)) + '\n' vless.host = array[index]
}) outlink.value += CreateLink('vless', vless) + '\n'
} }
}) })
const copy_link = () => { copy(outlink.value, $q) }
return { return {
text, text,
outlink, outlink,
copy() { copy_link
copyToClipboard(outlink.value)
.then(() => {
// success!
$q.notify({
message: '复制成功',
color: 'positive',
position: 'top'
})
})
.catch(() => {
// fail
$q.notify({
message: '复制失败',
color: 'negative'
})
})
}
}; };
} }

View File

@ -24,30 +24,6 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </div>
<!-- <div class="col-sm-8 col-md-4 col-lg-2 col-xl-1 q-pa-md">
<q-card class="my-card">
<img class="rounded-full q-pa-md" src="img/v2rayng.png">
<q-card-section class="text-center">
<div class="text-h6 text-center">v2ray</div>
<q-btn color="positive">
<q-icon left name="file_download" />
<div>安卓手机</div>
</q-btn>
<q-btn class="q-ma-md" color="positive">
<q-icon left name="file_download" />
<div>模拟器</div>
</q-btn>
<q-btn color="positive">
<q-icon left name="file_download" />
<div>PC</div>
</q-btn>
</q-card-section>
<q-card-section class="q-pt-none">
</q-card-section>
</q-card>
</div> -->
</div> </div>
</template> </template>
@ -72,9 +48,6 @@ export default defineComponent({
// eslint-disable-next-line vue/multi-word-component-names // eslint-disable-next-line vue/multi-word-component-names
name: 'Download', name: 'Download',
setup() { setup() {
return { return {
array, array,
dow(url: string) { dow(url: string) {

View File

@ -17,67 +17,12 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, watch } from 'vue'; import { defineComponent } from 'vue';
import { getdata } from 'src/api/api'
import { decode, encode } from 'js-base64';
import { copyToClipboard, useQuasar } from 'quasar';
export default defineComponent({ export default defineComponent({
// eslint-disable-next-line vue/multi-word-component-names // eslint-disable-next-line vue/multi-word-component-names
name: 'Help', name: 'Help',
setup() {
const $q = useQuasar()
const api = new getdata;
const text = ref('');
const outlink = ref('');
const def_link = 'ew0KICAidiI6ICIyIiwNCiAgInBzIjogIjAiLA0KICAiYWRkIjogIjE4NS4yMTguNi4xMDgiLA0KICAicG9ydCI6ICI5MDAwIiwNCiAgImlkIjogIjJlZTU3ODA2LWY2ZTQtNDgyYS1lZjA4LTczNjBjMDRjZDNlNSIsDQogICJhaWQiOiAiMCIsDQogICJzY3kiOiAiYXV0byIsDQogICJuZXQiOiAid3MiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiIiwNCiAgInBhdGgiOiAiLyIsDQogICJ0bHMiOiAiIiwNCiAgInNuaSI6ICIiLA0KICAiYWxwbiI6ICIiDQp9'
const is_ip = (ip: string) => {
var reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
return reg.test(ip);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
watch(() => text.value, (newValue, oldValue) => { //
let obj = JSON.parse(decode(def_link))
console.log(newValue)
if (!is_ip(newValue)) {
outlink.value = 'erroe ip'
return
}
api.get_country(newValue).then(res => {
let name = res.data.country == '中国' ? res.data.province : res.data.country
obj.ps = name + newValue
obj.add = newValue
outlink.value = 'vmess://' + encode(JSON.stringify(obj))
})
})
return {
text,
outlink,
copy() {
copyToClipboard(outlink.value)
.then(() => {
// success!
$q.notify({
message: '复制成功',
color: 'positive',
position: 'top'
})
})
.catch(() => {
// fail
$q.notify({
message: '复制失败',
color: 'negative'
})
})
}
};
}
}); });
</script> </script>

View File

@ -4,7 +4,8 @@
<div class="col-xs-1 col-md-4"></div> <div class="col-xs-1 col-md-4"></div>
<div class="col-xs-10 col-md-4"> <div class="col-xs-10 col-md-4">
<h6 style="text-align: center;margin: 1rem;">节点<q-icon v-show="online" title="在线" name="check_circle_outline" <h6 style="text-align: center;margin: 1rem;">节点<q-icon v-show="online" title="在线" name="check_circle_outline"
style="color: green;" /> <q-icon v-show="!online" title="离线" name="highlight_off" style="color: red;" /></h6> style="color: green;" /> <q-icon v-show="!online" title="离线" name="highlight_off" style="color: red;" />
</h6>
<q-input v-model="text" filled autogrow /> <q-input v-model="text" filled autogrow />
</div> </div>
</div> </div>
@ -12,13 +13,13 @@
<div class="row"> <div class="row">
<div class="col-xs-1 col-md-4"></div> <div class="col-xs-1 col-md-4"></div>
<div class="col-xs-10 col-md-4"> <div class="col-xs-10 col-md-4">
<OriginalLinkItem v-model:text="text"></OriginalLinkItem> <OriginalLinkItem :names="names"></OriginalLinkItem>
</div> </div>
</div> </div>
<div class="row" v-for="(item, index) in serve" :key="index"> <div class="row" v-for="(item, index) in serves" :key="index">
<div class="col-xs-1 col-md-4"></div> <div class="col-xs-1 col-md-4"></div>
<div class="col-xs-10 col-md-4"> <div class="col-xs-10 col-md-4">
<LinkItem :serve="item" v-model:text="text"></LinkItem> <LinkItem :serve="item" v-model:names="names"></LinkItem>
</div> </div>
</div> </div>
</div> </div>
@ -28,47 +29,56 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { server } from 'components/models'; import { server } from 'src/models/models';
import { computed, defineComponent, onMounted, ref } from 'vue'; import { computed, defineComponent, onMounted, ref } from 'vue';
import { getdata } from 'src/api/api' import { getdata } from 'src/api/api'
import LinkItem from 'components/LinkItem.vue'; import LinkItem from 'components/LinkItem.vue';
import OriginalLinkItem from 'components/OriginalLinkItem.vue'; import OriginalLinkItem from 'components/OriginalLinkItem.vue';
import { decode } from 'js-base64'; import { VLESS } from 'src/Util/node/Vless';
export default defineComponent({ export default defineComponent({
name: 'IndexPage', name: 'IndexPage',
components: { LinkItem, OriginalLinkItem }, components: { LinkItem, OriginalLinkItem },
setup() { setup() {
const serve = ref(<server[]>[]) const serves = ref(<server[]>[])
const api = new getdata; const api = new getdata;
const text = ref(''); const text = ref('');
const online = ref(false); const online = ref(false);
const names = ref(<string[]>[])
onMounted(() => { onMounted(() => {
api.get_server().then((res) => { api.get_server().then((res) => {
for (let index = 0; index < res.data.length; index++) { for (let index = 0; index < res.data.data.length; index++) {
serve.value.push(res.data[index]); serves.value.push(res.data.data[index]);
} }
}) })
}) })
const isobj = computed(() => { const isobj = computed(() => {
try { try {
if (text.value.length <= 0) { if (text.value.length <= 0) {
return false return false
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let texttmp = text.value!.replace(/\ +/g, ''); let texttmp = text.value!.replace(/\ +/g, '');
texttmp = texttmp.replace(/[\r\n]/g, ''); texttmp = texttmp.replace(/[\r\n]/g, '');
let arr = texttmp.split('vmess://'); let arr = texttmp.split('vless://');
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
names.value = [];
for (let iterator of arr) { for (let iterator of arr) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties // eslint-disable-next-line vue/no-side-effects-in-computed-properties
online.value = false online.value = false
if (iterator.length > 0) { if (iterator.length > 0) {
let obj = JSON.parse(decode(iterator)); // let tmp = parseVlessLink(iterator)
if (obj.id != '2ee57806-f6e4-482a-ef08-7360c04cd3e5' || obj.net != 'ws') { let tmp = VLESS.decodeURL('vless://' + iterator);
if (!(tmp != null && tmp.uuid == '2ee57806-f6e4-482a-ef08-7360c04cd3e5')) {
return false return false
} }
let reg = new RegExp(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/); // eslint-disable-next-line vue/no-side-effects-in-computed-properties
let ip = obj.ps.match(reg)[0]; if (tmp.name) { names.value.push(tmp.name) }
isonline(ip) // eslint-disable-next-line vue/no-async-in-computed-properties
api.get_server_ms(tmp.server, tmp.port, tmp.query.path, 0).then((res) => {
console.log(res)
online.value = res.is_online
})
} }
} }
@ -79,16 +89,12 @@ export default defineComponent({
return true return true
}) })
const isonline = (ip: string) => {
api.text_server(`http://${ip}:9000/`).then(res => {
online.value = res.data.sataus == 400
})
}
return { return {
serve, serves,
text, text,
isobj, isobj,
online online,
names
}; };
} }
}); });

View File

@ -2,9 +2,13 @@ import { MutationTree } from 'vuex';
import { ExampleStateInterface } from './state'; import { ExampleStateInterface } from './state';
const mutation: MutationTree<ExampleStateInterface> = { const mutation: MutationTree<ExampleStateInterface> = {
someMutation (/* state: ExampleStateInterface */) { someMutation(/* state: ExampleStateInterface */) {
// your code // your code
} },
//改动url
set_text_server_url(state, url) {
state.text_server_url = url;
},
}; };
export default mutation; export default mutation;

View File

@ -1,11 +1,13 @@
export interface ExampleStateInterface { export interface ExampleStateInterface {
prop: boolean; prop: boolean;
text_server_url: string;
} }
function state(): ExampleStateInterface { function state(): ExampleStateInterface {
return { return {
prop: false prop: false,
} text_server_url: '',
};
} }
export default state; export default state;

1812
yarn.lock

File diff suppressed because it is too large Load Diff