Init
This commit is contained in:
commit
d4ce6e6261
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Vue</title>
|
||||||
|
</head>
|
||||||
|
<body style="padding: 0; margin: 0;">
|
||||||
|
<div id="app" style="padding: 0; margin: 0;"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1212
package-lock.json
generated
Normal file
1212
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "proxysubscribehub",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"base-64": "^1.0.0",
|
||||||
|
"tdesign-icons-vue-next": "^0.3.4",
|
||||||
|
"tdesign-vue-next": "^1.10.7",
|
||||||
|
"vue": "^3.5.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"vite": "^6.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
32
src/App.vue
Normal file
32
src/App.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script setup>
|
||||||
|
import HelloWorld from './components/HelloWorld.vue'
|
||||||
|
import Main from './components/Main.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- <div>
|
||||||
|
<a href="https://vite.dev" target="_blank">
|
||||||
|
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
||||||
|
</a>
|
||||||
|
<a href="https://vuejs.org/" target="_blank">
|
||||||
|
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<HelloWorld msg="Vite + Vue" /> -->
|
||||||
|
<Main></Main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.vue:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #42b883aa);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
135
src/components/Agreements/Vmess.vue
Normal file
135
src/components/Agreements/Vmess.vue
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<t-form label-align="left" required-mark>
|
||||||
|
<t-form-item label="名称">
|
||||||
|
<t-input v-model="props.vmess.name" />
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="服务器地址">
|
||||||
|
<t-input v-model="props.vmess.server" />
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="端口">
|
||||||
|
<t-input-number v-model="props.vmess.port" theme="normal" />
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="用户ID">
|
||||||
|
<t-input v-model="props.vmess.userId" />
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="替代ID">
|
||||||
|
<t-input-number v-model="props.vmess.alterId" theme="normal" />
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="加密方式">
|
||||||
|
<t-select v-model="props.vmess.security">
|
||||||
|
<t-option value="auto">auto</t-option>
|
||||||
|
<t-option value="none">none</t-option>
|
||||||
|
<t-option value="aes-128-gcm">aes-128-gcm</t-option>
|
||||||
|
<t-option value="chacha20-poly1305">chacha20-poly1305</t-option>
|
||||||
|
<t-option value="zero">zero</t-option>
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="包编码">
|
||||||
|
<t-select v-model="props.vmess.type">
|
||||||
|
<t-option value="none">none</t-option>
|
||||||
|
<t-option value="packet">packet</t-option>
|
||||||
|
<t-option value="xudp">xudp</t-option>
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="传输协议">
|
||||||
|
<t-select v-model="props.vmess.network">
|
||||||
|
<t-option value="tcp">tcp</t-option>
|
||||||
|
<t-option value="kcp">kcp</t-option>
|
||||||
|
<t-option value="ws">ws</t-option>
|
||||||
|
<t-option value="http">http</t-option>
|
||||||
|
<t-option value="quic">quic</t-option>
|
||||||
|
<t-option value="grpc">grpc</t-option>
|
||||||
|
<t-option value="httpupgrade">httpupgrade</t-option>
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="传输层加密">
|
||||||
|
<t-select v-model="props.vmess.tls">
|
||||||
|
<t-option value="none">none</t-option>
|
||||||
|
<t-option value="tls">tls</t-option>
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="伪装类型">
|
||||||
|
<t-select v-model="props.vmess.mask">
|
||||||
|
<t-option value="none">none</t-option>
|
||||||
|
<t-option value="dtls">dtls</t-option>
|
||||||
|
<t-option value="srtp">srtp</t-option>
|
||||||
|
<t-option value="utp">utp</t-option>
|
||||||
|
<t-option value="wechat-video">wechat-video</t-option>
|
||||||
|
<t-option value="wireguard">wireguard</t-option>
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
vmess: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
type: String,
|
||||||
|
default: "127.0.0.1",
|
||||||
|
},
|
||||||
|
port: {
|
||||||
|
type: Number,
|
||||||
|
default: 1080,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
alterId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
type: String,
|
||||||
|
default: "auto",
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "none",
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
type: String,
|
||||||
|
default: "tcp",
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
type: String,
|
||||||
|
default: "none",
|
||||||
|
},
|
||||||
|
mask: {
|
||||||
|
type: String,
|
||||||
|
default: "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 用于判断编辑节点或新建节点
|
||||||
|
key: {
|
||||||
|
type: Number,
|
||||||
|
default: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// const name = ref('');
|
||||||
|
// const server = ref('127.0.0.1');
|
||||||
|
// const port = ref(1080);
|
||||||
|
// const userId = ref('');
|
||||||
|
// const alterId = ref(0);
|
||||||
|
// const security = ref('auto');
|
||||||
|
// const type = ref('none');
|
||||||
|
// const network = ref('tcp');
|
||||||
|
// const tls = ref('none');
|
||||||
|
</script>
|
||||||
62
src/components/Cards/GeneralCard.vue
Normal file
62
src/components/Cards/GeneralCard.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<t-card :title=props.name hover-shadow>
|
||||||
|
<t-skeleton :loading="urlVisible">
|
||||||
|
<span style="display: inline-block; text-align: left;">{{ server + req + props.name }}</span>
|
||||||
|
</t-skeleton>
|
||||||
|
<template #actions>
|
||||||
|
<div class="actions">
|
||||||
|
<t-button size="small" theme="default" @click="$emit('toSubscribeItem', props.url, props.name)">编辑</t-button>
|
||||||
|
<t-button size="small" @click="urlVisible = !urlVisible">{{
|
||||||
|
urlVisible ? "显示" : "隐藏"
|
||||||
|
}}</t-button>
|
||||||
|
<t-button size="small" theme="danger" @click="dialogVisible = true">
|
||||||
|
<template #icon>
|
||||||
|
<delete-icon />
|
||||||
|
</template>
|
||||||
|
删除
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-card>
|
||||||
|
|
||||||
|
<!-- dialog -->
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
width="40%"
|
||||||
|
header="警告"
|
||||||
|
destroy-on-close
|
||||||
|
:confirm-btn="{
|
||||||
|
content: '删除',
|
||||||
|
theme: 'danger',
|
||||||
|
loading
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<span>确认要删除该订阅吗?</span>
|
||||||
|
</div>
|
||||||
|
</t-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.actions > * {
|
||||||
|
margin-left: 10px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { DeleteIcon } from "tdesign-icons-vue-next";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
url: String,
|
||||||
|
name: String
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示控制
|
||||||
|
const urlVisible = ref(true);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
|
||||||
|
// 地址生成
|
||||||
|
const server = "https://www.cloudy233.top";
|
||||||
|
const req = "/api/?name=";//TODO
|
||||||
|
</script>
|
||||||
150
src/components/General.vue
Normal file
150
src/components/General.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="general-content">
|
||||||
|
<div class="control-bar">
|
||||||
|
<t-button size="large" @click="dialogVisible = true">
|
||||||
|
<!-- icon莫名其妙的小,或许是个bug,也有可能和flex有关 -->
|
||||||
|
<!-- <template #icon>
|
||||||
|
<add-icon />
|
||||||
|
</template> -->
|
||||||
|
+ 添加订阅
|
||||||
|
</t-button>
|
||||||
|
<t-input v-model="searchText" size="large" disabled style="margin: 0 48px 0 16px">
|
||||||
|
<template #prefix-icon>
|
||||||
|
<filter-icon />
|
||||||
|
</template>
|
||||||
|
</t-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card" v-for="i in Url" :key="i">
|
||||||
|
<GeneralCard
|
||||||
|
:url="i.url"
|
||||||
|
:name="i.name"
|
||||||
|
@toSubscribeItem="(url, name) => $emit('toSubscribeItem', url, name)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- total应当由后端提供,显示条件为卡片数量大于9 -->
|
||||||
|
<!-- 暂时禁用 -->
|
||||||
|
<t-pagination
|
||||||
|
v-if="Url.length < 0"
|
||||||
|
:total="Url.length"
|
||||||
|
showPageNumber
|
||||||
|
:default-page-size="9"
|
||||||
|
:showPageSize="false"
|
||||||
|
showPreviousAndNextBtn
|
||||||
|
:totalContent="false"
|
||||||
|
size="small"
|
||||||
|
@current-change="onCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- dialog -->
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
header="添加订阅"
|
||||||
|
width="40%"
|
||||||
|
:confirm-btn="{
|
||||||
|
content: '保存',
|
||||||
|
theme: 'primary',
|
||||||
|
loading,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<t-space direction="vertical" style="width: 100%">
|
||||||
|
<div class="dialog-content">
|
||||||
|
<t-form :data="fromData" label-align="left">
|
||||||
|
|
||||||
|
<t-form-item label="订阅名称" name="name">
|
||||||
|
<t-input></t-input>
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="别名" name="url">
|
||||||
|
<t-input></t-input>
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
</t-space>
|
||||||
|
</t-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.general-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-content: start;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 28%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .t-form__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// icon
|
||||||
|
import { FilterIcon, AddIcon } from "tdesign-icons-vue-next";
|
||||||
|
|
||||||
|
import { computed, onBeforeMount, onMounted, ref } from "vue";
|
||||||
|
import GeneralCard from "./Cards/GeneralCard.vue";
|
||||||
|
|
||||||
|
const searchText = ref("");
|
||||||
|
|
||||||
|
//TODO向后端发请求获取代理数据
|
||||||
|
const Url = ref([]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
//测试数据(Url长度上限应该为9,按当前分页向服务器请求数据)
|
||||||
|
// for (let i = 0; i < 10; i++) {
|
||||||
|
// Url.value.push({
|
||||||
|
// name: "订阅" + i,
|
||||||
|
// url: "vmess://" + i,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
Url.value.push({
|
||||||
|
name: "all",
|
||||||
|
url: "vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogInRrIiwNCiAgImFkZCI6ICI0My4xNTMuMTg0LjkxIiwNCiAgInBvcnQiOiAiNDczNTkiLA0KICAiaWQiOiAiMzAxYzgyMWEtODljOS00ZWI0LTlmYmYtYWIwMjgyNjMxZTJhIiwNCiAgImFpZCI6ICIwIiwNCiAgInNjeSI6ICJhdXRvIiwNCiAgIm5ldCI6ICJrY3AiLA0KICAidHlwZSI6ICJkdGxzIiwNCiAgImhvc3QiOiAiIiwNCiAgInBhdGgiOiAiIiwNCiAgInRscyI6ICIiLA0KICAic25pIjogIiIsDQogICJhbHBuIjogIiIsDQogICJmcCI6ICIiDQp9"
|
||||||
|
})
|
||||||
|
console.log(Url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 过滤器
|
||||||
|
const filterUrl = computed(() => {
|
||||||
|
if (searchText.value.length > 0) {
|
||||||
|
return Url.value.filter((item) => {
|
||||||
|
return item.name.includes(searchText.value);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Url.value;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const fromData = ref({
|
||||||
|
labelAlign: "left",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 换页
|
||||||
|
function onCurrentChange(index, pageInfo) {
|
||||||
|
console.log(index, pageInfo);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
43
src/components/HelloWorld.vue
Normal file
43
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
msg: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button type="button" @click="count++">count is {{ count }}</button>
|
||||||
|
<p>
|
||||||
|
Edit
|
||||||
|
<code>components/HelloWorld.vue</code> to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Check out
|
||||||
|
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||||
|
>create-vue</a
|
||||||
|
>, the official Vue + Vite starter
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Learn more about IDE Support for Vue in the
|
||||||
|
<a
|
||||||
|
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||||
|
target="_blank"
|
||||||
|
>Vue Docs Scaling up Guide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
76
src/components/Main.vue
Normal file
76
src/components/Main.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<t-layout class="main-layout">
|
||||||
|
<t-aside>
|
||||||
|
<t-menu theme="light" default-value="General" :collapsed="collapsed" @change="changeHandler">
|
||||||
|
<template #logo>
|
||||||
|
<span style="font-size: larger">{{ collapsed? "PSH" : "Proxy Subscribe Hub" }}</span>
|
||||||
|
</template>
|
||||||
|
<t-menu-item value="General">
|
||||||
|
<template #icon>
|
||||||
|
<app-icon />
|
||||||
|
</template>
|
||||||
|
总览
|
||||||
|
</t-menu-item>
|
||||||
|
<!-- <t-menu-item value="item2">节点管理</t-menu-item> -->
|
||||||
|
<template #operations>
|
||||||
|
<t-button
|
||||||
|
variant="text"
|
||||||
|
shape="square"
|
||||||
|
theme="default"
|
||||||
|
@click="collapsed = !collapsed;"
|
||||||
|
style="background-color: #fff"
|
||||||
|
>
|
||||||
|
<view-list-icon />
|
||||||
|
</t-button>
|
||||||
|
</template>
|
||||||
|
</t-menu>
|
||||||
|
</t-aside>
|
||||||
|
<t-layout>
|
||||||
|
<t-content style="overflow: auto">
|
||||||
|
<div>
|
||||||
|
<General
|
||||||
|
@toSubscribeItem="(url, name) => toSubscribeItem(url, name)"
|
||||||
|
v-if="menuValue === 'General'"
|
||||||
|
/>
|
||||||
|
<SubscribeItem
|
||||||
|
@backToGeneral="menuValue = 'General'"
|
||||||
|
:propsData="Url"
|
||||||
|
:name="Name"
|
||||||
|
v-if="menuValue === 'SubscribeItem'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</t-content>
|
||||||
|
</t-layout>
|
||||||
|
</t-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main-layout {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import General from "./General.vue";
|
||||||
|
import SubscribeItem from "./SubscribeItem.vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { ViewListIcon, AppIcon } from "tdesign-icons-vue-next";
|
||||||
|
|
||||||
|
const menuValue = ref("General");
|
||||||
|
const collapsed = ref(false);
|
||||||
|
let Url = "";
|
||||||
|
let Name = "";
|
||||||
|
|
||||||
|
function changeHandler(active) {
|
||||||
|
console.log(active);
|
||||||
|
menuValue.value = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSubscribeItem(url, name) {
|
||||||
|
// console.log(url);
|
||||||
|
Url = url;
|
||||||
|
Name = name;
|
||||||
|
menuValue.value = "SubscribeItem";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
219
src/components/SubscribeItem.vue
Normal file
219
src/components/SubscribeItem.vue
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="subscribe-item-container">
|
||||||
|
<t-breadcrumb style="margin-bottom: 16px">
|
||||||
|
<t-breadcrumb-item @click="$emit('backToGeneral')"
|
||||||
|
>总览</t-breadcrumb-item
|
||||||
|
>
|
||||||
|
<t-breadcrumb-item>{{ props.name }}</t-breadcrumb-item>
|
||||||
|
</t-breadcrumb>
|
||||||
|
|
||||||
|
<div class="control-bar">
|
||||||
|
<t-button @click="addNode()">
|
||||||
|
<template #icon>
|
||||||
|
<add-icon />
|
||||||
|
</template>
|
||||||
|
添加节点
|
||||||
|
</t-button>
|
||||||
|
<t-button @click="readByClipboard()" style="margin-left: 16px">
|
||||||
|
<template #icon>
|
||||||
|
<paste-icon />
|
||||||
|
</template>
|
||||||
|
从剪贴板导入(暂不支持)
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
<t-table
|
||||||
|
row-key="key"
|
||||||
|
:data="data"
|
||||||
|
:columns="columns"
|
||||||
|
active-row-type="single"
|
||||||
|
:hover="false"
|
||||||
|
>
|
||||||
|
</t-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- dialog -->
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
header="添加节点"
|
||||||
|
width="40%"
|
||||||
|
:confirm-btn="{
|
||||||
|
content: '保存',
|
||||||
|
theme: 'primary',
|
||||||
|
loading,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<t-select v-model="agreementSelect" :borderless="true" auto-width>
|
||||||
|
<t-option value="vmess">vmess</t-option>
|
||||||
|
</t-select>
|
||||||
|
|
||||||
|
<Vmess :vmess="vemssProps" v-if="agreementSelect === 'vmess'" />
|
||||||
|
</div>
|
||||||
|
</t-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.subscribe-item-container {
|
||||||
|
padding: 16px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-content: start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content > * {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, h, onMounted, onBeforeMount } from "vue";
|
||||||
|
import Vmess from "./Agreements/Vmess.vue";
|
||||||
|
import { AddIcon, PasteIcon } from "tdesign-icons-vue-next";
|
||||||
|
import { MessagePlugin } from "tdesign-vue-next";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
propsData: String,
|
||||||
|
name: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = ref([]);
|
||||||
|
|
||||||
|
// 代理参数
|
||||||
|
let vemssProps = {
|
||||||
|
name: "",
|
||||||
|
server: "127.0.0.1",
|
||||||
|
port: 1080,
|
||||||
|
userId: "",
|
||||||
|
alterId: 0,
|
||||||
|
security: "auto",
|
||||||
|
type: "none",
|
||||||
|
network: "tcp",
|
||||||
|
tls: "tls",
|
||||||
|
mask: "none"
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.propsData) {
|
||||||
|
if (props.propsData.length > 0) {
|
||||||
|
let propsDataSplit = props.propsData.split(",");
|
||||||
|
for (let i in propsDataSplit) {
|
||||||
|
let decodeData = JSON.parse(atob(propsDataSplit[i].split("://")[1])); //解码
|
||||||
|
switch (propsDataSplit[i].split("://")[0]) {
|
||||||
|
case "vmess":
|
||||||
|
data.value.push({
|
||||||
|
key: i,
|
||||||
|
name: decodeData.ps,
|
||||||
|
url: propsDataSplit[i],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
data.value.push({
|
||||||
|
key: i,
|
||||||
|
name: "暂不支持",
|
||||||
|
url: propsDataSplit[i],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示控制
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const agreementSelect = ref("");
|
||||||
|
|
||||||
|
const columns = ref([
|
||||||
|
{ colKey: "name", title: "名称", ellipsis: true },
|
||||||
|
{ colKey: "url", title: "分享链接", ellipsis: true },
|
||||||
|
{
|
||||||
|
colKey: "actions",
|
||||||
|
title: "操作",
|
||||||
|
cell: (h, { row }) => [
|
||||||
|
h("a", {
|
||||||
|
innerHTML: "复制",
|
||||||
|
onClick: () => {
|
||||||
|
copyToClipboard(row.url);
|
||||||
|
},
|
||||||
|
style: { cursor: "pointer" },
|
||||||
|
}),
|
||||||
|
h("span", "\u00A0\u00A0"),
|
||||||
|
h("a", {
|
||||||
|
innerHTML: "编辑",
|
||||||
|
onClick: () => {editNode(row.url.split("://")[0], row.url)},
|
||||||
|
style: { cursor: "pointer" },
|
||||||
|
}),
|
||||||
|
h("span", "\u00A0\u00A0"),
|
||||||
|
h("a", { innerHTML: "删除", style: { cursor: "pointer" } }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
// let data = ref([{ key: "1", name: "tk", url: "vmess://" }]);
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
MessagePlugin.info("复制成功");
|
||||||
|
} catch (e) {
|
||||||
|
MessagePlugin.error("复制失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readByClipboard() {
|
||||||
|
try {
|
||||||
|
navigator.clipboard.readText().then((text) => {
|
||||||
|
MessagePlugin.success("读取成功");
|
||||||
|
//解析链接
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
MessagePlugin.error("读取失败");
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNode() {
|
||||||
|
vemssProps = {
|
||||||
|
name: "",
|
||||||
|
server: "127.0.0.1",
|
||||||
|
port: 1080,
|
||||||
|
userId: "",
|
||||||
|
alterId: 0,
|
||||||
|
security: "auto",
|
||||||
|
type: "none",
|
||||||
|
network: "tcp",
|
||||||
|
tls: "tls",
|
||||||
|
mask: "none"
|
||||||
|
};
|
||||||
|
agreementSelect.value = "";
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editNode(arg, url) {
|
||||||
|
// 测试
|
||||||
|
switch (arg) {
|
||||||
|
case "vmess":
|
||||||
|
let decodeUrl = JSON.parse(atob(url.split("://")[1])); //解码
|
||||||
|
console.log(decodeUrl);
|
||||||
|
vemssProps = {
|
||||||
|
name: decodeUrl.ps,
|
||||||
|
server: decodeUrl.add,
|
||||||
|
port: decodeUrl.port,
|
||||||
|
userId: decodeUrl.id,
|
||||||
|
alterId: decodeUrl.aid,
|
||||||
|
security: decodeUrl.scy,
|
||||||
|
type: "none",
|
||||||
|
network: decodeUrl.net,
|
||||||
|
tls: decodeUrl.tls,
|
||||||
|
mask: decodeUrl.type,
|
||||||
|
};
|
||||||
|
agreementSelect.value = "vmess";
|
||||||
|
dialogVisible.value = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
7
src/main.js
Normal file
7
src/main.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import TDesign from 'tdesign-vue-next'
|
||||||
|
import 'tdesign-vue-next/es/style/index.css';
|
||||||
|
|
||||||
|
createApp(App).use(TDesign).mount('#app')
|
||||||
79
src/style.css
Normal file
79
src/style.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
vite.config.js
Normal file
10
vite.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0'
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user