refactor: Add new frontend as submodule

This commit is contained in:
Peng-YM 2022-07-06 15:41:01 +08:00
parent bc58419bb1
commit ffd219abfe
81 changed files with 32 additions and 20460 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "web"]
path = web
url = https://github.com/sub-store-org/Sub-Store-Front-End.git

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.6.4",
"version": "2.7.0",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {

View File

@ -27,36 +27,39 @@ function ConditionalFilter({ rule }) {
return {
name: 'Conditional Filter',
func: (proxies) => {
return proxies.map(proxy => isMatch(rule, proxy));
}
}
return proxies.map((proxy) => isMatch(rule, proxy));
},
};
}
function isMatch(rule, proxy) {
// leaf node
if (!rule.operator) {
switch (rule.proposition) {
case "IN":
case 'IN':
return rule.value.indexOf(proxy[rule.attr]) !== -1;
case "CONTAINS":
if (typeof proxy[rule.attr] !== "string") return false;
case 'CONTAINS':
if (typeof proxy[rule.attr] !== 'string') return false;
return proxy[rule.attr].indexOf(rule.value) !== -1;
case "EQUALS":
case 'EQUALS':
return proxy[rule.attr] === rule.value;
case "EXISTS":
return proxy[rule.attr] !== null || typeof proxy[rule.attr] !== "undefined";
case 'EXISTS':
return (
proxy[rule.attr] !== null ||
typeof proxy[rule.attr] !== 'undefined'
);
default:
throw new Error(`Unknown proposition: ${rule.proposition}`);
}
}
// operator nodes
switch (rule.operator) {
case "AND":
return rule.child.every(child => isMatch(child, proxy));
case "OR":
return rule.child.some(child => isMatch(child, proxy));
case "NOT":
switch (rule.operator) {
case 'AND':
return rule.child.every((child) => isMatch(child, proxy));
case 'OR':
return rule.child.some((child) => isMatch(child, proxy));
case 'NOT':
return !isMatch(rule.child, proxy);
default:
throw new Error(`Unknown operator: ${rule.operator}`);

File diff suppressed because one or more lines are too long

1
web Submodule

@ -0,0 +1 @@
Subproject commit b10b708c3420a1b4cdc71a7f1b845de701a2382b

25
web/.gitignore vendored
View File

@ -1,25 +0,0 @@
.DS_Store
node_modules
/dist
yarn.lock
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vercel

View File

@ -1,3 +0,0 @@
# Ignore artifacts:
build
coverage

View File

@ -1,47 +0,0 @@
printWidth : 80 # 显示宽度
tabWidth : 2 # tab 宽度
useTabs : false # 使用 tab 而不是空格
semi : false # 使用分号
singleQuote : true # 使用单引号
jsxSingleQuote : false # 在 JSX 中使用单引号而不是双引号
bracketSpacing : true # 在对象花括号内打印空格 true { foo: bar } false {foo: bar}
arrowParens : "avoid" # 箭头函数只有一个参数的时候的周围的括号 "always" - (x) => x "avoid" - x => x
embeddedLanguageFormatting : "auto" # "auto" - 嵌入代码如果 Prettier 可以识别则格式化它 "off" - 永远不要自动格式化
bracketSameLine : false # 多行属性的 HTMLHTML、JSX、Vue、Angular标签的 ">" 放在最后一行的末尾,而不是单独在下一行(不适用于自闭合元素)
#htmlWhitespaceSensitivity : "strict"
vueIndentScriptAndStyle : true # 在 Vue 文件中缩进 <script> 和 <style> 标签
insertPragma : false # 是否插入一个特殊 @format 标记指定文件已使用 Prettier 格式化
# 行尾风格
# "lf" 仅换行 ( \n),常见于 Linux 和 macOS 以及 git repos 内部
# "crlf" - 回车 + 换行字符 ( \r\n),常见于 Windows
# "cr" - 仅回车字符 ( \r),很少使用
# "auto" - 保持现有的行尾(一个文件中的混合值通过查看第一行之后使用的内容进行标准化)
endOfLine : "lf"
# 需要提供注释才允许格式化
# /**
# * @prettier 或 @format
# */
requirePragma : false
# 对象属性的引号风格
# "as-needed" 仅在需要时在对象属性周围添加引号
# "consistent" 如果对象中至少一个属性需要引号,则所有属性都使用引号
# "preserve" 尊重对象属性中的引号
quoteProps : "consistent"
# 在多行逗号分隔的句法结构中尽可能打印尾随逗号
# "es5" 在 ES5 中有效的尾随逗号对象、数组等TypeScript 中的类型参数中没有尾随逗号
# "none" 没有尾随逗号。
# "all" 尽可能使用尾随逗号(包括函数参数和调用)。要运行,以这种方式格式化的 JavaScript 代码需要一个支持 ES2017Node.js 8+ 或现代浏览器)或下级编译的引擎。这还可以在 TypeScript 中的类型参数中启用尾随逗号(自 2018 年 1 月发布的 TypeScript 2.7 起支持)
trailingComma : "es5"
# 例外配置覆盖
overrides :
- files :
- "*.ts"
- "*.tsx"
options :
semi : true
arrowParens : "always"

View File

@ -1,24 +0,0 @@
# web
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

12298
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,61 +0,0 @@
{
"name": "web",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"vercel-build": "npm run build"
},
"dependencies": {
"@dzangolab/vue-country-flag-icon": "^0.2.0",
"axios": "^0.20.0",
"chartist": "^0.11.4",
"core-js": "^3.6.5",
"material-design-icons-iconfont": "^5.0.1",
"timeago.js": "^4.0.2",
"v-clipboard": "^2.2.3",
"vee-validate": "^3.4.5",
"vue": "^2.6.12",
"vue-chartist": "^2.3.1",
"vue-i18n": "^8.22.2",
"vue-qrcode-component": "^2.1.1",
"vue-router": "^3.4.3",
"vue-world-map": "^0.1.1",
"vuetify": "^2.3.10",
"vuex": "^3.5.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.6",
"@vue/cli-plugin-eslint": "^4.5.6",
"@vue/cli-service": "^4.5.6",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"sass": "~1.26.11",
"sass-loader": "^8.0.0",
"vue-cli-plugin-vuetify": "~2.0.7",
"vue-template-compiler": "^2.6.12",
"vuetify-loader": "^1.3.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

View File

@ -1,41 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vuetify Md Pro | 404</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// https://github.com/rafrex/spa-github-pages
// Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
// ----------------------------------------------------------------------
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. http://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set segmentCount to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe
// Otherwise, leave segmentCount as 0.
var segmentCount = 1;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
l.pathname.slice(1).split('/').slice(segmentCount).join('/').replace(/&/g, '~and~') +
(l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta charset="utf-8"/>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"/>
<link rel="Bookmark" href="https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"/>
<link rel="shortcut icon" href="https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"/>
<link rel="apple-touch-icon" href="https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"/>
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Sub-Store</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,113 +0,0 @@
<template>
<v-app>
<TopToolbar></TopToolbar>
<v-main>
<router-view></router-view>
</v-main>
<BottomNav ref="bottomNavBar"></BottomNav>
<v-snackbar :value="successMessage" app bottom color="success" elevation="20">
{{ successMessage }}
</v-snackbar>
<v-snackbar :value="errorMessage" app bottom color="error" elevation="20">
{{ errorMessage }}
</v-snackbar>
<v-overlay :value="isLoading">
<v-progress-circular indeterminate size="64" color="primary"></v-progress-circular>
</v-overlay>
</v-app>
</template>
<script>
import TopToolbar from "@/components/TopToolbar";
import BottomNav from "@/components/BottomNav";
import { showError } from "@/utils";
async function initStore(store) {
await store.dispatch('FETCH_SUBSCRIPTIONS').catch(() => {
showError(`无法拉取订阅列表!`);
});
await store.dispatch("FETCH_COLLECTIONS").catch(() => {
showError(`无法拉取组合订阅列表!`);
});
await store.dispatch("FETCH_ARTIFACTS").catch(() => {
showError(`无法拉取配置列表!`);
});
await store.dispatch("FETCH_SETTINGS").catch(() => {
showError(`无法拉取配置列表!`);
});
await store.dispatch("FETCH_ENV").catch(() => {
showError(`无法获取当前运行环境!`);
});
}
export default {
components: {
TopToolbar,
BottomNav,
},
created() {
initStore(this.$store);
const vuetify = this.$vuetify;
if (window.matchMedia) {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
vuetify.theme.dark = true;
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
console.log(`changed to ${e.matches ? "dark" : "light"} mode`)
vuetify.theme.dark = e.matches ? true : false;
});
}
},
mounted (){
const bottomNavBar = this.$refs.bottomNavBar.$el;
const height = bottomNavBar.offsetHeight || bottomNavBar.clientHeight;
this.$store.commit("SET_BOTTOM_NAVBAR_HEIGHT", height);
},
computed: {
successMessage() {
return this.$store.state.successMessage;
},
errorMessage() {
return this.$store.state.errorMessage;
},
isLoading() {
return this.$store.state.isLoading;
}
},
watch: {
successMessage() {
if (this.$store.state.snackbarTimer) {
clearTimeout(this.$store.state.snackbarTimer);
}
const timer = setTimeout(() => {
this.$store.commit("SET_SUCCESS_MESSAGE", "");
}, 3000);
this.$store.commit("SET_SNACK_BAR_TIMER", timer);
},
errorMessage() {
if (this.$store.state.snackbarTimer) {
clearTimeout(this.$store.state.snackbarTimer);
}
const timer = setTimeout(() => {
this.$store.commit("SET_ERROR_MESSAGE", "");
}, 3000);
this.$store.commit("SET_SNACK_BAR_TIMER", timer);
}
}
}
</script>
<style lang="scss">
@import "./assets/css/app";
@import "./assets/css/general.css";
</style>

View File

@ -1 +0,0 @@
module.exports = __webpack_public_path__ + "img/clint-mckoy.36f95307.jpg";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +0,0 @@
[v-cloak] {
display: none;
}
.text-pre-wrap {
white-space: pre-wrap !important;
}
.v-navigation-drawer {
padding-top: constant(safe-area-inset-top) !important;
padding-top: env(safe-area-inset-top) !important;
}
.v-bottom-sheet.v-dialog--fullscreen {
padding-top: constant(safe-area-inset-top) !important;
padding-top: env(safe-area-inset-top) !important;
}
.v-app-bar {
height: auto !important;
padding-top: constant(safe-area-inset-top) !important;
padding-top: env(safe-area-inset-top) !important;
}
.v-toolbar {
height: auto !important;
padding-top: constant(safe-area-inset-top) !important;
padding-top: env(safe-area-inset-top) !important;
}
.v-toolbar__content {
padding-left: 12px !important;
padding-right: 12px !important;
}
.v-main {
margin-top: constant(safe-area-inset-top) !important;
margin-top: env(safe-area-inset-top) !important;
margin-bottom: constant(safe-area-inset-bottom) !important;
margin-bottom: env(safe-area-inset-bottom) !important;
}
.v-main .container {
height: 100%;
}
.v-bottom-navigation,
.v-bottom-sheet {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.v-bottom-navigation {
box-sizing: content-box;
}
.v-bottom-navigation button {
box-sizing: border-box;
}

View File

@ -1 +0,0 @@
module.exports = __webpack_public_path__ + "img/lock.9ae20e99.jpg";

View File

@ -1 +0,0 @@
module.exports = __webpack_public_path__ + "img/login.d6d3bb09.jpg";

View File

@ -1 +0,0 @@
module.exports = __webpack_public_path__ + "img/logo.82b9c7a5.png";

View File

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>

Before

Width:  |  Height:  |  Size: 539 B

View File

@ -1 +0,0 @@
module.exports = __webpack_public_path__ + "img/pricing.f76b550f.jpg";

View File

@ -1 +0,0 @@
module.exports = __webpack_public_path__ + "img/register.85b37874.jpg";

View File

@ -1 +0,0 @@
module.exports = __webpack_public_path__ + "img/vuetify.31b0d032.svg";

View File

@ -1,35 +0,0 @@
<template>
<v-bottom-navigation
v-model="activeItem"
app
color="primary"
fixed
grow
>
<v-btn :to="{path: '/'}" value="subscription">
<span>订阅</span>
<v-icon>flight_takeoff</v-icon>
</v-btn>
<v-btn :to="{path: '/cloud'}" value="artifacts">
<span>同步</span>
<v-icon>mdi-cloud</v-icon>
</v-btn>
<v-btn :to="{path: '/user'}" value="user">
<span>我的</span>
<v-icon>mdi-account</v-icon>
</v-btn>
</v-bottom-navigation>
</template>
<script>
export default {
data: () => {
return {
activeItem: 'subscription'
}
}
}
</script>

View File

@ -1,75 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>flag</v-icon>
国旗
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
国旗
</v-card-title>
<v-card-text>
添加或者删除节点国旗
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
工作模式
<v-radio-group v-model="mode">
<v-row>
<v-col>
<v-radio label="添加" value="ADD"/>
</v-col>
<v-col>
<v-radio label="删除" value="REMOVE"/>
</v-col>
</v-row>
</v-radio-group>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ["args"],
data: function () {
return {
idx: this.$vnode.key,
mode: "ADD"
}
},
created() {
if (typeof this.args !== 'undefined')
this.mode = this.args === true ? "ADD" : "REMOVE";
},
watch: {
mode() {
this.$emit("dataChanged", {
idx: this.idx,
args: this.mode === "ADD"
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,64 +0,0 @@
<template>
<div class = "float-menu-switch-wrapper" ref = "floatMenuSwitch">
<v-speed-dial v-model = "fab" :direction = "direction" :transition = "transition"
>
<template v-slot:activator>
<v-btn v-model = "fab" color = "primary" fab>
<v-icon v-if = "fab"> mdi-close</v-icon>
<v-icon v-else> mdi-gesture-double-tap</v-icon>
</v-btn>
</template>
<slot></slot>
</v-speed-dial>
</div>
</template>
<script>
export default {
name : "FloatMenu",
data (){
return {
direction : "top",
fab : false,
fling : false,
hover : false,
tabs : null,
transition : "scale-transition",
};
},
updated (){
const floatMenuSwitch = this.$refs.floatMenuSwitch;
floatMenuSwitch.style.bottom = 2 * this.bottomNavBarHeight + "px";
},
computed : {
bottomNavBarHeight (){
return this.$store.state.bottomNavBarHeight;
},
},
};
</script>
<style lang = "scss" scoped>
.float-menu-switch-wrapper {
position : fixed;
right : 16px;;
z-index : 99;
.v-speed-dial > button.v-btn.v-btn--round {
margin-right : 0;
width : 40px;
height : 40px;
}
::v-deep .v-speed-dial__list button.theme--light.v-btn {
margin-right : 0;
}
}
/* This is for documentation purposes and will not be needed in your application */
#create .v-speed-dial {
position : absolute;
}
#create .v-btn--floating {
position : relative;
}
</style>

View File

@ -1,120 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>compress</v-icon>
节点去重
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{ on }">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">节点去重</v-card-title>
<v-card-text>
删除或者重命名重复节点提供以下两种选项<br/>
- 删除删除多余重复节点<br/>
- 重命名对重复节点添加序号进行重命名可以定制序号显示的格式
(用空格分割的数字)序号位置 (前缀或者后缀)连接符
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
操作
<v-radio-group v-model="action">
<v-row>
<v-col>
<v-radio label="重命名" value="rename"/>
</v-col>
<v-col>
<v-radio label="删除" value="delete"/>
</v-col>
</v-row>
</v-radio-group>
<v-form v-if="action === 'rename'">
序号位置
<v-radio-group v-model="position" row>
<v-radio label="前缀" value="front"/>
<v-radio label="后缀" value="back"/>
</v-radio-group>
序号格式
<v-text-field
v-model="template"
clear-icon="clear"
clearable
hint="例如:𝟘 𝟙 𝟚 𝟛 𝟜 𝟝 𝟞 𝟟 𝟠 𝟡"
placeholder="序号显示格式,用空格分隔"
/>
连接符
<v-text-field
v-model="link"
clear-icon="clear"
clearable
hint="例如:-"
placeholder="节点名和序号的连接符"
/>
</v-form>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ["args"],
data: function () {
return {
idx: this.$vnode.key,
action: "rename",
position: "back",
template: "0 1 2 3 4 5 6 7 8 9",
link: "-",
};
},
computed: {
attr() {
return `${this.action}/${this.position}/${this.template}${this.link}`;
},
},
methods: {
save() {
this.$emit("dataChanged", {
idx: this.idx,
args: {
action: this.action,
position: this.position,
template: this.template,
link: this.link,
},
});
},
},
watch: {
attr() {
this.save();
},
},
created() {
if (typeof this.args !== 'undefined') {
this.action = this.args.action || this.action;
this.position = this.args.position || this.position;
this.template = this.args.template || this.template;
this.link = typeof this.args.link !== 'undefined' ? this.args.link : this.link;
}
}
};
</script>
<style scoped>
</style>

View File

@ -1,167 +0,0 @@
<template>
<v-list>
<v-list-item v-for="(proxy, idx) in proxies" :key="idx">
<v-list-item-content>
<v-list-item-title class="wrap-text" v-text="proxy.name"></v-list-item-title>
<v-chip-group>
<v-chip color="primary" outlined x-small>
<v-icon left x-small>mdi-server</v-icon>
{{ proxy.type.toUpperCase() }}
</v-chip>
<v-chip v-if="proxy.udp" color="blue" outlined x-small>
<v-icon left x-small>mdi-fire</v-icon>
UDP
</v-chip>
<v-chip v-if="proxy.tfo" color="success" outlined x-small>
<v-icon left x-small>mdi-flash</v-icon>
TFO
</v-chip>
<v-chip v-if="proxy['skip-cert-verify']" color="error" outlined x-small>
<v-icon left x-small>error</v-icon>
SCERT
</v-chip>
</v-chip-group>
</v-list-item-content>
<v-list-item-action>
<v-row>
<v-col>
<v-btn
v-if="proxy.type !== 'http'"
icon
@click="showQRCode(idx)"
>
<v-icon color="grey lighten-1" small>mdi-qrcode</v-icon>
</v-btn>
</v-col>
<v-col>
<v-btn icon @click="showInfo(idx)">
<v-icon color="grey lighten-1" small>mdi-information</v-icon>
</v-btn>
</v-col>
</v-row>
</v-list-item-action>
</v-list-item>
<v-dialog
v-model="dialog"
>
<v-card>
<v-card-title>
{{ info.name }}
</v-card-title>
<v-card-text>
<h4>{{ info.isp }}</h4>
<h4>{{ info.region }}</h4>
<h4>{{ info.ip }}</h4>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog
v-model="showQR"
>
<v-card>
<v-card-title>
{{ info.name }}
<v-btn
icon
@click="copyLink()"
>
<v-icon>content_copy</v-icon>
</v-btn>
</v-card-title>
<v-card-text
align="center"
>
<vue-q-r-code-component
:text="qr"
/>
</v-card-text>
</v-card>
</v-dialog>
</v-list>
</template>
<script>
import {axios} from "@/utils";
import VueQRCodeComponent from 'vue-qrcode-component';
const flags = new Map([["AC", "🇦🇨"], ["AF", "🇦🇫"], ["AI", "🇦🇮"], ["AL", "🇦🇱"], ["AM", "🇦🇲"], ["AQ", "🇦🇶"], ["AR", "🇦🇷"], ["AS", "🇦🇸"], ["AT", "🇦🇹"], ["AU", "🇦🇺"], ["AW", "🇦🇼"], ["AX", "🇦🇽"], ["AZ", "🇦🇿"], ["BB", "🇧🇧"], ["BD", "🇧🇩"], ["BE", "🇧🇪"], ["BF", "🇧🇫"], ["BG", "🇧🇬"], ["BH", "🇧🇭"], ["BI", "🇧🇮"], ["BJ", "🇧🇯"], ["BM", "🇧🇲"], ["BN", "🇧🇳"], ["BO", "🇧🇴"], ["BR", "🇧🇷"], ["BS", "🇧🇸"], ["BT", "🇧🇹"], ["BV", "🇧🇻"], ["BW", "🇧🇼"], ["BY", "🇧🇾"], ["BZ", "🇧🇿"], ["CA", "🇨🇦"], ["CF", "🇨🇫"], ["CH", "🇨🇭"], ["CK", "🇨🇰"], ["CL", "🇨🇱"], ["CM", "🇨🇲"], ["CN", "🇨🇳"], ["CO", "🇨🇴"], ["CP", "🇨🇵"], ["CR", "🇨🇷"], ["CU", "🇨🇺"], ["CV", "🇨🇻"], ["CW", "🇨🇼"], ["CX", "🇨🇽"], ["CY", "🇨🇾"], ["CZ", "🇨🇿"], ["DE", "🇩🇪"], ["DG", "🇩🇬"], ["DJ", "🇩🇯"], ["DK", "🇩🇰"], ["DM", "🇩🇲"], ["DO", "🇩🇴"], ["DZ", "🇩🇿"], ["EA", "🇪🇦"], ["EC", "🇪🇨"], ["EE", "🇪🇪"], ["EG", "🇪🇬"], ["EH", "🇪🇭"], ["ER", "🇪🇷"], ["ES", "🇪🇸"], ["ET", "🇪🇹"], ["EU", "🇪🇺"], ["FI", "🇫🇮"], ["FJ", "🇫🇯"], ["FK", "🇫🇰"], ["FM", "🇫🇲"], ["FO", "🇫🇴"], ["FR", "🇫🇷"], ["GA", "🇬🇦"], ["GB", "🇬🇧"], ["HK", "🇭🇰"], ["HU", "🇭🇺"], ["ID", "🇮🇩"], ["IE", "🇮🇪"], ["IL", "🇮🇱"], ["IM", "🇮🇲"], ["IN", "🇮🇳"], ["IS", "🇮🇸"], ["IT", "🇮🇹"], ["JP", "🇯🇵"], ["KR", "🇰🇷"], ["LU", "🇱🇺"], ["MO", "🇲🇴"], ["MX", "🇲🇽"], ["MY", "🇲🇾"], ["NL", "🇳🇱"], ["PH", "🇵🇭"], ["RO", "🇷🇴"], ["RS", "🇷🇸"], ["RU", "🇷🇺"], ["RW", "🇷🇼"], ["SA", "🇸🇦"], ["SB", "🇸🇧"], ["SC", "🇸🇨"], ["SD", "🇸🇩"], ["SE", "🇸🇪"], ["SG", "🇸🇬"], ["TH", "🇹🇭"], ["TN", "🇹🇳"], ["TO", "🇹🇴"], ["TR", "🇹🇷"], ["TV", "🇹🇻"], ["TW", "🇨🇳"], ["UK", "🇬🇧"], ["UM", "🇺🇲"], ["US", "🇺🇸"], ["UY", "🇺🇾"], ["UZ", "🇺🇿"], ["VA", "🇻🇦"], ["VE", "🇻🇪"], ["VG", "🇻🇬"], ["VI", "🇻🇮"], ["VN", "🇻🇳"], ["ZA", "🇿🇦"]])
export default {
name: "ProxyList",
props: ['url', 'sub', 'raw'],
components: {VueQRCodeComponent},
data: function () {
return {
proxies: [],
uris: [],
dialog: false,
showQR: false,
info: {
name: "",
isp: "",
region: "",
ip: ""
},
qr: "",
link: ""
}
},
methods: {
async fetch() {
try {
await axios.get(this.raw ? `${this.url}?raw=true` : this.url).then(resp => {
let {data} = resp;
// eslint-disable-next-line no-debugger
this.proxies = data;
})
await axios.get(this.raw ? `${this.url}?target=URI&raw=true` : `${this.url}?target=URI`).then(resp => {
const {data} = resp;
this.uris = data.split("\n");
});
// fix http offset
this.proxies.forEach((p, idx) => {
if (p.type === 'http') {
this.uris.splice(idx, 0, null);
}
})
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", err);
}
},
async showInfo(idx) {
const {server, name} = this.proxies[idx];
const res = await axios.get(`/utils/IP_API/${encodeURIComponent(server)}`).then(resp => resp.data);
this.info.name = name;
this.info.isp = `ISP${res.isp}`;
this.info.region = `地区:${flags.get(res.countryCode)} ${res.regionName} ${res.city}`;
this.info.ip = `IP${res.query}`
this.dialog = true
},
copyLink() {
this.$clipboard(this.link);
this.$store.commit("SET_SUCCESS_MESSAGE", `节点链接已复制到剪贴板!`);
},
showQRCode(idx) {
this.qr = this.uris[idx];
this.link = this.uris[idx];
this.info.name = this.proxies[idx].name;
this.showQR = true;
}
},
created() {
this.fetch();
}
}
</script>
<style scoped>
.wrap-text {
white-space: normal;
}
</style>

View File

@ -1,114 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>code</v-icon>
正则删除
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
正则删除
</v-card-title>
<v-card-text>
根据正则表达式删除节点名中的字段注意正则表达式需要注意转义
<br/>这里是一个合法的正则表达式:
<br/>
<b>IEPL|IPLC</b>
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
正则表达式
<v-chip-group
column
>
<v-chip
v-for="(regex, idx) in regexps"
:key="idx"
close
close-icon="mdi-delete"
@click="edit(idx)"
@click:close="remove(idx)"
>
{{ regex }}
</v-chip>
</v-chip-group>
<v-text-field
v-model="form.regex"
append-icon="mdi-send"
clear-icon="clear"
clearable
placeholder="添加新正则表达式"
solo
@click:append="add(form.regex)"
@keyup.enter="add(form.regex)"
/>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ['args'],
data: function () {
return {
mode: "IN",
form: {
regex: ""
},
regexps: [],
idx: this.$vnode.key,
}
},
methods: {
add(keyword) {
if (keyword) {
this.regexps.push(keyword);
this.form.regex = "";
} else {
this.$store.commit("SET_ERROR_MESSAGE", "正则表达式不能为空!");
}
},
edit(idx) {
this.form.regex = this.regexps[idx];
this.remove(idx);
},
remove(idx) {
this.regexps.splice(idx, 1);
},
save() {
this.$emit("dataChanged", {
idx: this.idx,
args: this.regexps
});
}
},
watch: {
regexps() {
this.save();
}
},
created() {
this.regexps = this.args || [];
}
}
</script>
<style scoped>
</style>

View File

@ -1,136 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>filter_list</v-icon>
正则过滤
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
正则过滤
</v-card-title>
<v-card-text>
根据正则表达式过滤节点如果设置为保留模式则匹配<b>任何一个</b>正则表达式的节点会被保留否则会被过滤
正则表达式需要注意转义
<br/>这里是一个合法的正则表达式:
<br/>
<b>IEPL|IPLC</b>
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
工作模式
<v-radio-group v-model="mode">
<v-row>
<v-col>
<v-radio label="保留模式" value="IN"/>
</v-col>
<v-col>
<v-radio label="过滤模式" value="OUT"/>
</v-col>
</v-row>
</v-radio-group>
正则表达式
<v-chip-group
column
>
<v-chip
v-for="(regex, idx) in regexps"
:key="idx"
close
close-icon="mdi-delete"
@click="edit(idx)"
@click:close="remove(idx)"
>
{{ regex }}
</v-chip>
</v-chip-group>
<v-text-field
v-model="form.regex"
append-icon="mdi-send"
clear-icon="clear"
clearable
placeholder="添加新正则表达式"
solo
@click:append="add(form.regex)"
@keyup.enter="add(form.regex)"
/>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ['args'],
data: function () {
return {
mode: "IN",
form: {
regex: ""
},
regexps: [],
idx: this.$vnode.key,
}
},
methods: {
add(keyword) {
if (keyword) {
this.regexps.push(keyword);
this.form.regex = "";
} else {
this.$store.commit("SET_ERROR_MESSAGE", "正则表达式不能为空!");
}
},
edit(idx) {
this.form.regex = this.regexps[idx];
this.remove(idx);
},
remove(idx) {
this.regexps.splice(idx, 1);
},
save() {
this.$emit("dataChanged", {
idx: this.idx,
args: {
regex: this.regexps,
keep: this.mode === 'IN'
}
});
}
},
watch: {
regexps() {
this.save();
},
mode() {
this.save();
}
},
created() {
if (this.args) {
this.regexps = this.args.regex || [];
if (typeof this.args.keep !== 'undefined') this.mode = this.args.keep ? "IN" : "OUT";
else this.mode = "IN";
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,139 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>filter_list</v-icon>
正则重命名
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
正则重命名
</v-card-title>
<v-card-text>
使用替换节点名中的字段
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
正则表达式
<v-chip-group
column
>
<v-chip
v-for="(chip, idx) in chips"
:key="idx"
close
close-icon="mdi-delete"
@click="edit(idx)"
@click:close="remove(idx)"
>
{{ chip }}
</v-chip>
</v-chip-group>
<v-row>
<v-col>
<v-text-field
v-model="form.regex"
clear-icon="clear"
clearable
placeholder="正则表达式"
solo
/>
</v-col>
<v-col>
<v-text-field
v-model="form.replace"
append-icon="mdi-send"
clear-icon="clear"
clearable
placeholder="替换为"
solo
@click:append="add"
@keyup.enter="add"
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ['args'],
data: function () {
return {
idx: this.$vnode.key,
mode: "IN",
form: {
regex: "",
replace: ""
},
regexps: []
}
},
computed: {
chips() {
return this.regexps.map(k => {
const {expr, now} = k;
return `${expr}${now.length === 0 ? "∅" : now}`;
});
}
},
methods: {
add() {
if (this.form.regex) {
this.regexps.push({
expr: this.form.regex,
now: this.form.replace || ""
});
this.form.regex = "";
this.form.replace = "";
} else {
this.$store.commit("SET_ERROR_MESSAGE", "正则表达式不能为空!");
}
},
edit(idx) {
this.form.regex = this.regexps[idx].expr;
this.form.replace = this.regexps[idx].now;
this.remove(idx);
},
remove(idx) {
this.regexps.splice(idx, 1);
},
save() {
this.$emit("dataChanged", {
idx: this.idx,
args: this.regexps
});
}
},
created() {
this.regexps = this.args || [];
},
watch: {
regexps() {
this.save();
}
},
}
</script>
<style scoped>
</style>

View File

@ -1,114 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>sort</v-icon>
正则排序
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
正则排序
</v-card-title>
<v-card-text>
根据给出的正则表达式的顺序对节点进行排序无法匹配的节点将会按照正序排列
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
正则表达式
<v-chip-group
column
>
<v-chip
v-for="(item, idx) in items"
:key="idx"
close
close-icon="mdi-delete"
@click="edit(idx)"
@click:close="remove(idx)"
>
{{ item }}
</v-chip>
</v-chip-group>
<v-text-field
v-model="form.item"
append-icon="mdi-send"
clear-icon="clear"
clearable
placeholder="添加新正则表达式"
solo
@click:append="add(form.item)"
@keyup.enter="add(form.item)"
/>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ['args'],
data: function () {
return {
idx: this.$vnode.key,
selection: null,
currentTag: null,
form: {
item: ""
},
items: []
}
},
created() {
if (this.args) {
this.items = this.args || [];
}
},
watch: {
items() {
this.save();
},
},
methods: {
add(item) {
if (item) {
this.items.push(item);
this.form.item = "";
} else {
this.$store.commit("SET_ERROR_MESSAGE", "正则表达式不能为空!");
}
},
edit(idx) {
this.form.item = this.items[idx];
this.remove(idx);
},
remove(idx) {
this.items.splice(idx, 1);
},
save() {
this.$emit("dataChanged", {
idx: this.idx,
args: this.items
});
},
}
}
</script>
<style scoped>
</style>

View File

@ -1,100 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>flag</v-icon>
区域过滤
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
区域过滤器
</v-card-title>
<v-card-text>
根据区域过滤节点至少需要保留一个区域选中的区域会被保留
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
<v-chip-group v-model="selection" active-class="primary accent-4" column multiple>
<v-chip
v-for="region in regions"
:key="region.name"
:value="region.value"
class="ma-2"
label
>
{{ region.name }}
</v-chip>
</v-chip-group>
</v-card-text>
</v-card>
</template>
<script>
const regions = [
{
name: "🇭🇰 香港",
value: "HK"
},
{
name: "🇨🇳 台湾",
value: "TW"
},
{
name: "🇸🇬 新加坡",
value: "SG"
},
{
name: "🇯🇵 日本",
value: "JP"
},
{
name: "🇺🇸 美国",
value: "US"
},
{
name: "🇬🇧 英国",
value: "UK"
}
];
export default {
props: ["args"],
data: function () {
return {
idx: this.$vnode.key,
regions,
selection: []
}
},
created() {
this.selection = this.args || [];
},
watch: {
selection() {
this.$emit("dataChanged", {
idx: this.idx,
args: this.selection
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,86 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>dns</v-icon>
节点域名解析
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
节点域名解析
</v-card-title>
<v-card-text>
将节点域名解析成 IP 地址
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
服务提供商
<v-radio-group v-model="provider">
<v-row>
<v-col>
<v-radio label="Google" value="Google"/>
</v-col>
<v-col>
<v-radio label="IP-API" value="IP-API"/>
</v-col>
<v-col>
<v-radio label="Cloudflare" value="Cloudflare"/>
</v-col>
</v-row>
</v-radio-group>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ["args"],
data: function () {
return {
idx: this.$vnode.key,
provider: "Google"
}
},
created() {
if (typeof this.args !== "undefined") {
this.provider = this.args.provider || "Google";
}
},
methods: {
save() {
this.$emit("dataChanged", {
idx: this.idx,
args: {
provider: this.provider
}
});
},
},
watch: {
provider() {
this.save();
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,103 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>code</v-icon>
脚本过滤器
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
脚本过滤器
</v-card-title>
<v-card-text>
用一段脚本过滤节点可以提供脚本链接或者直接输入脚本
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
输入
<v-radio-group v-model="mode">
<v-row>
<v-col>
<v-radio label="链接" value="link"/>
</v-col>
<v-col>
<v-radio label="脚本" value="script"/>
</v-col>
</v-row>
</v-radio-group>
<v-textarea
v-model="content"
:label="hint"
auto-grow
clear-icon="clear"
clearable
solo
/>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ["args"],
data: function () {
return {
idx: this.$vnode.key,
mode: "link",
content: ""
}
},
computed: {
hint() {
return this.mode === 'link' ? "请输入链接地址" : "请输入一段脚本"
}
},
created() {
if (typeof this.args !== 'undefined') {
this.mode = this.args.mode;
this.content = this.args.content;
}
},
watch: {
mode() {
this.save();
},
content() {
this.save();
}
},
methods: {
save() {
if (this.content) {
this.$emit("dataChanged", {
idx: this.idx,
args: {
mode: this.mode,
content: this.content
}
});
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,103 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>code</v-icon>
脚本操作
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
脚本操作
</v-card-title>
<v-card-text>
用一段脚本操作节点可以提供脚本链接或者直接输入脚本
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
输入
<v-radio-group v-model="mode">
<v-row>
<v-col>
<v-radio label="链接" value="link"/>
</v-col>
<v-col>
<v-radio label="脚本" value="script"/>
</v-col>
</v-row>
</v-radio-group>
<v-textarea
v-model="content"
:label="hint"
auto-grow
clear-icon="clear"
clearable
solo
/>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ["args"],
data: function () {
return {
idx: this.$vnode.key,
mode: "link",
content: ""
}
},
computed: {
hint() {
return this.mode === 'link' ? "请输入链接地址" : "请输入一段脚本"
}
},
created() {
if (typeof this.args !== 'undefined') {
this.mode = this.args.mode;
this.content = this.args.content;
}
},
watch: {
mode() {
this.save();
},
content() {
this.save();
}
},
methods: {
save() {
if (this.content) {
this.$emit("dataChanged", {
idx: this.idx,
args: {
mode: this.mode,
content: this.content
}
});
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,77 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>sort_by_alpha</v-icon>
节点排序
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
节点排序
</v-card-title>
<v-card-text>
根据节点名排序一共有正序逆序随机三种模式
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
模式
<v-radio-group v-model="mode">
<v-row>
<v-col>
<v-radio label="正序" value="asc"/>
</v-col>
<v-col>
<v-radio label="逆序" value="desc"/>
</v-col>
<v-col>
<v-radio label="随机" value="random"/>
</v-col>
</v-row>
</v-radio-group>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ["args"],
data: function () {
return {
idx: this.$vnode.key,
mode: "asc"
}
},
created() {
this.mode = this.args;
},
watch: {
mode() {
this.$emit("dataChanged", {
idx: this.idx,
args: this.mode
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,46 +0,0 @@
<template>
<v-fab-transition>
<v-speed-dial
v-model="opened"
absolute
bottom
direction="top"
fab
left
small
transition="slide-y-reverse-transition"
>
<template #activator>
<v-btn
fab
>
<v-icon v-if="opened">mdi-close</v-icon>
<v-icon v-else>mdi-plus</v-icon>
</v-btn>
</template>
<v-btn
color="primary"
fab
>
<v-icon>mdi-plus</v-icon>
</v-btn>
<v-btn
color="primary"
fab
>
<v-icon>create_new_folder</v-icon>
</v-btn>
</v-speed-dial>
</v-fab-transition>
</template>
<script>
export default {
data() {
return {
opened: false,
}
},
computed: {}
}
</script>

View File

@ -1,30 +0,0 @@
<template>
<div>
<v-app-bar
:clipped="true"
:mini-variant="false"
app fixed
>
<v-toolbar-title><h3>{{ title }}</h3></v-toolbar-title>
</v-app-bar>
</div>
</template>
<script>
export default {
data: () => {
return {
showMenu: false
}
},
methods: {},
computed: {
title: function () {
return this.$store.state.title;
}
}
}
</script>

View File

@ -1,109 +0,0 @@
<template>
<v-card class="ml-1 mr-1 mb-1 mt-1">
<v-card-title>
<v-icon color="primary" left>cloud_circle</v-icon>
节点类型过滤
<v-spacer></v-spacer>
<v-btn icon @click="$emit('up', idx)">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<v-btn icon @click="$emit('down', idx)">
<v-icon>keyboard_arrow_down</v-icon>
</v-btn>
<v-btn icon @click="$emit('deleteProcess', idx)">
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-dialog>
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon>help</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">
节点类型过滤器
</v-card-title>
<v-card-text>
根据节点类型过滤节点至少需要保留一种类型选中的类型会被保留
</v-card-text>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
<v-chip-group v-model="selection" active-class="primary accent-4" column multiple>
<v-chip
v-for="type in types"
:key="type.name"
:value="type.value"
class="ma-2"
label
>
{{ type.name }}
</v-chip>
</v-chip-group>
</v-card-text>
</v-card>
</template>
<script>
const types = [
{
name: "Shadowsocks",
value: "ss"
},
{
name: "Shadowsocks R",
value: "ssr"
},
{
name: "VMess",
value: "vmess"
},
{
name: "VLess",
value: "vless"
},
{
name: "Trojan",
value: "trojan"
},
{
name: "HTTP(s)",
value: "http"
},
{
name: "Socks5",
value: "socks5"
},
{
name: "Snell",
value: "snell"
}
];
export default {
props: ['args'],
data: function () {
return {
idx: this.$vnode.key,
types,
selection: []
}
},
created() {
this.selection = this.args || [];
},
watch: {
selection() {
this.$emit("dataChanged", {
idx: this.idx,
type: "Type Filter",
args: this.selection
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,9 +0,0 @@
<script>
import {VCard} from 'vuetify/lib'
export default {
name: 'Card',
extends: VCard
}
</script>

View File

@ -1,69 +0,0 @@
<template>
<v-list-item
:active-class="`primary ${!isDark ? 'black' : 'white'}--text`"
:href="href"
:rel="href && href !== '#' ? 'noopener' : undefined"
:target="href && href !== '#' ? '_blank' : undefined"
:to="item.to"
>
<v-list-item-icon
v-if="text"
class="v-list-item__icon--text"
v-text="computedText"
/>
<v-list-item-icon v-else-if="item.icon">
<v-icon v-text="item.icon"/>
</v-list-item-icon>
<v-list-item-content v-if="item.title || item.subtitle">
<v-list-item-title v-text="item.title"/>
<v-list-item-subtitle v-text="item.subtitle"/>
</v-list-item-content>
</v-list-item>
</template>
<script>
import Themeable from 'vuetify/lib/mixins/themeable'
export default {
name: 'Item',
mixins: [Themeable],
props: {
item: {
type: Object,
default: () => ({
href: undefined,
icon: undefined,
subtitle: undefined,
title: undefined,
to: undefined
})
},
text: {
type: Boolean,
default: false
}
},
computed: {
computedText() {
if (!this.item || !this.item.title) return ''
let text = ''
this.item.title.split(' ').forEach(val => {
text += val.substring(0, 1)
})
return text
},
href() {
return this.item.href || (!this.item.to ? '#' : undefined)
}
}
}
</script>

View File

@ -1,123 +0,0 @@
<template>
<v-list-group
:color="barColor !== 'rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.7)' ? 'white' : 'grey darken-1'"
:group="group"
:prepend-icon="item.icon"
:sub-group="subGroup"
append-icon="mdi-menu-down"
>
<template v-slot:activator>
<v-list-item-icon
v-if="text"
class="v-list-item__icon--text"
v-text="computedText"
/>
<v-list-item-avatar
v-else-if="item.avatar"
class="align-self-center"
color="grey"
>
<v-img src="https://demos.creative-tim.com/material-dashboard-pro/assets/img/faces/avatar.jpg"/>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="item.title"/>
</v-list-item-content>
</template>
<template v-for="(child, i) in children">
<base-item-sub-group
v-if="child.children"
:key="`sub-group-${i}`"
:item="child"
/>
<base-item
v-else
:key="`item-${i}`"
:item="child"
text
/>
</template>
</v-list-group>
</template>
<script>
// Utilities
import kebabCase from 'lodash/kebabCase'
import {mapState} from 'vuex'
export default {
name: 'ItemGroup',
inheritAttrs: false,
props: {
item: {
type: Object,
default: () => ({
avatar: undefined,
group: undefined,
title: undefined,
children: []
})
},
subGroup: {
type: Boolean,
default: false
},
text: {
type: Boolean,
default: false
}
},
computed: {
...mapState(['barColor']),
children() {
return this.item.children.map(item => ({
...item,
to: !item.to ? undefined : `${this.item.group}/${item.to}`
}))
},
computedText() {
if (!this.item || !this.item.title) return ''
let text = ''
this.item.title.split(' ').forEach(val => {
text += val.substring(0, 1)
})
return text
},
group() {
return this.genGroup(this.item.children)
}
},
methods: {
genGroup(children) {
return children
.filter(item => item.to)
.map(item => {
const parent = item.group || this.item.group
let group = `${parent}/${kebabCase(item.to)}`
if (item.children) {
group = `${group}|${this.genGroup(item.children)}`
}
return group
}).join('|')
}
}
}
</script>
<style>
.v-list-group__activator p {
margin-bottom: 0;
}
</style>

View File

@ -1,25 +0,0 @@
<template>
<base-item-group
:item="item"
sub-group
text
/>
</template>
<script>
export default {
name: 'ItemSubGroup',
props: {
item: {
type: Object,
default: () => ({
avatar: undefined,
group: undefined,
title: undefined,
children: []
})
}
}
}
</script>

View File

@ -1,64 +0,0 @@
<script>
// Components
import {VAlert, VBtn, VIcon} from 'vuetify/lib'
export default {
name: 'MaterialAlert',
extends: VAlert,
computed: {
__cachedDismissible() {
if (!this.dismissible) return null
const color = 'white'
return this.$createElement(VBtn, {
staticClass: 'v-alert__dismissible',
props: {
color,
icon: true,
small: true
},
attrs: {
'aria-label': this.$vuetify.lang.t(this.closeLabel)
},
on: {
// eslint-disable-next-line
click: () => (this.isActive = false)
}
}, [
this.$createElement(VIcon, {
props: {color}
}, '$vuetify.icons.cancel')
])
},
classes() {
return {
...VAlert.options.computed.classes.call(this),
'v-alert--material': true
}
},
hasColoredIcon() {
return true
}
}
}
</script>
<style lang="sass">
.v-alert--material
margin-top: 32px
.v-alert__icon
background-color: #FFFFFF
height: 44px
min-width: 44px
top: -36px
.v-alert__dismissible
align-self: flex-start
margin: 0 !important
padding: 0 !important
</style>

View File

@ -1,168 +0,0 @@
<template>
<v-card
v-bind="$attrs"
:class="classes"
class="v-card--material pa-3"
>
<div class="d-flex grow flex-wrap">
<v-avatar
v-if="avatar"
class="mx-auto v-card--material__avatar elevation-12"
color="grey"
size="128"
>
<v-img :src="avatar"/>
</v-avatar>
<v-sheet
v-else
:class="{
'pa-7': !$slots.image
}"
:color="color"
:max-height="icon ? 90 : undefined"
:width="inline || icon ? 'auto' : '100%'"
class="text-start v-card--material__heading mb-n6"
dark
>
<slot
v-if="$slots.heading"
name="heading"
/>
<slot
v-else-if="$slots.image"
name="image"
/>
<div
v-else-if="title && !icon"
class="display-1 font-weight-light"
v-text="title"
/>
<v-icon
v-else-if="icon"
size="32"
v-text="icon"
/>
<div
v-if="text"
class="headline font-weight-thin"
v-text="text"
/>
</v-sheet>
<div
v-if="$slots['after-heading']"
class="ml-6"
>
<slot name="after-heading"/>
</div>
<v-col
v-if="hoverReveal"
class="text-center py-0 mt-n12"
cols="12"
>
<slot name="reveal-actions"/>
</v-col>
<div
v-else-if="icon && title"
class="ml-4"
>
<div
class="card-title font-weight-light"
v-text="title"
/>
</div>
</div>
<slot/>
<template v-if="$slots.actions">
<v-divider class="mt-2"/>
<v-card-actions class="pb-0">
<slot name="actions"/>
</v-card-actions>
</template>
</v-card>
</template>
<script>
export default {
name: 'MaterialCard',
props: {
avatar: {
type: String,
default: ''
},
color: {
type: String,
default: 'success'
},
hoverReveal: {
type: Boolean,
default: false
},
icon: {
type: String,
default: undefined
},
image: {
type: Boolean,
default: false
},
inline: {
type: Boolean,
default: false
},
text: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
computed: {
classes() {
return {
'v-card--material--has-heading': this.hasHeading,
'v-card--material--hover-reveal': this.hoverReveal
}
},
hasHeading() {
return Boolean(this.$slots.heading || this.title || this.icon)
},
hasAltHeading() {
return Boolean(this.$slots.heading || (this.title && this.icon))
}
}
}
</script>
<style lang="sass">
.v-card--material
&__avatar
position: relative
top: -64px
margin-bottom: -32px
&__heading
position: relative
top: -40px
transition: .3s ease
z-index: 1
&.v-card--material--hover-reveal:hover
.v-card--material__heading
transform: translateY(-40px)
</style>

View File

@ -1,95 +0,0 @@
<template>
<base-material-card
v-bind="$attrs"
v-on="$listeners"
class="v-card--material-chart"
>
<template v-slot:heading>
<chartist
:data="data"
:event-handlers="eventHandlers"
:options="options"
:ratio="ratio"
:responsive-options="responsiveOptions"
:type="type"
style="max-height: 150px;"
/>
</template>
<slot
slot="reveal-actions"
name="reveal-actions"
/>
<slot/>
<slot
slot="actions"
name="actions"
/>
</base-material-card>
</template>
<script>
export default {
name: 'MaterialChartCard',
inheritAttrs: false,
props: {
data: {
type: Object,
default: () => ({})
},
eventHandlers: {
type: Array,
default: () => ([])
},
options: {
type: Object,
default: () => ({})
},
ratio: {
type: String,
default: undefined
},
responsiveOptions: {
type: Array,
default: () => ([])
},
type: {
type: String,
required: true,
validator: v => ['Bar', 'Line', 'Pie'].includes(v)
}
}
}
</script>
<style lang="sass">
.v-card--material-chart
p
color: #999
.v-card--material__heading
max-height: 185px
.ct-label
color: inherit
opacity: .7
font-size: 0.975rem
font-weight: 100
.ct-grid
stroke: rgba(255, 255, 255, 0.2)
.ct-series-a .ct-point,
.ct-series-a .ct-line,
.ct-series-a .ct-bar,
.ct-series-a .ct-slice-donut
stroke: rgba(255, 255, 255, .8)
.ct-series-a .ct-slice-pie,
.ct-series-a .ct-area
fill: rgba(255, 255, 255, .4)
</style>

View File

@ -1,70 +0,0 @@
<template>
<v-menu
v-model="value"
v-bind="$attrs"
:transition="transition"
offset-y
>
<template v-slot:activator="{ attrs, on }">
<v-btn
v-bind="attrs"
v-on="on"
:color="color"
default
min-width="200"
rounded
>
<slot/>
<v-icon>
mdi-{{ value ? 'menu-up' : 'menu-down' }}
</v-icon>
</v-btn>
</template>
<v-sheet>
<v-list dense>
<v-list-item
v-for="(item, i) in items"
:key="i"
@click="$(`click:action-${item.id}`)"
>
<v-list-item-content>
<v-list-item-title v-text="item.text"/>
</v-list-item-content>
</v-list-item>
</v-list>
</v-sheet>
</v-menu>
</template>
<script>
// Mixins
import Proxyable from 'vuetify/lib/mixins/proxyable'
export default {
name: 'MaterialDropdown',
mixins: [Proxyable],
props: {
color: {
type: String,
default: 'primary'
},
items: {
type: Array,
default: () => ([
{
id: undefined,
text: undefined
}
])
},
transition: {
type: String,
default: 'scale-transition'
}
}
}
</script>

View File

@ -1,66 +0,0 @@
<template>
<v-snackbar
v-bind="{
...$attrs,
...$props,
'color': 'transparent'
}"
:class="classes"
:value="value"
@change="$emit('change', $event)"
>
<base-material-alert
:color="color"
:dismissible="dismissible"
:type="type"
class="ma-0"
dark
>
<slot/>
</base-material-alert>
</v-snackbar>
</template>
<script>
// Components
import {VSnackbar} from 'vuetify/lib'
export default {
name: 'BaseMaterialSnackbar',
extends: VSnackbar,
props: {
dismissible: {
type: Boolean,
default: true
},
type: {
type: String,
default: ''
}
},
computed: {
classes() {
return {
...VSnackbar.options.computed.classes.call(this),
'v-snackbar--material': true
}
}
}
}
</script>
<style lang="sass">
.v-snackbar--material
margin-top: 32px
margin-bottom: 32px
.v-alert--material,
.v-snack__wrapper
border-radius: 4px
.v-snack__content
overflow: visible
padding: 0
</style>

View File

@ -1,113 +0,0 @@
<template>
<base-material-card
v-bind="$attrs"
v-on="$listeners"
:icon="icon"
class="v-card--material-stats"
>
<template v-slot:after-heading>
<div class="ml-auto text-right">
<div
class="body-3 grey--text font-weight-light"
v-text="title"
/>
<h3 class="display-2 font-weight-light text--primary">
{{ value }} <small>{{ smallValue }}</small>
</h3>
</div>
</template>
<v-col
class="px-0"
cols="12"
>
<v-divider/>
</v-col>
<v-icon
:color="subIconColor"
class="ml-2 mr-1"
size="16"
>
{{ subIcon }}
</v-icon>
<span
:class="subTextColor"
class="caption grey--text font-weight-light"
v-text="subText"
/>
</base-material-card>
</template>
<script>
import Card from './Card'
export default {
name: 'MaterialStatsCard',
inheritAttrs: false,
props: {
...Card.props,
icon: {
type: String,
required: true
},
subIcon: {
type: String,
default: undefined
},
subIconColor: {
type: String,
default: undefined
},
subTextColor: {
type: String,
default: undefined
},
subText: {
type: String,
default: undefined
},
title: {
type: String,
default: undefined
},
value: {
type: String,
default: undefined
},
smallValue: {
type: String,
default: undefined
}
}
}
</script>
<style lang="sass">
.v-card--material-stats
display: flex
flex-wrap: wrap
position: relative
> div:first-child
justify-content: space-between
.v-card
border-radius: 4px
flex: 0 1 auto
.v-card__text
display: inline-block
flex: 1 0 calc(100% - 120px)
position: absolute
top: 0
right: 0
width: 100%
.v-card__actions
flex: 1 0 100%
</style>

View File

@ -1,43 +0,0 @@
<template>
<v-tabs
v-model="internalValue"
v-bind="$attrs"
:active-class="`${color} ${$vuetify.theme.dark ? 'black' : 'white'}--text`"
class="v-tabs--pill"
hide-slider
>
<slot/>
<slot name="items"/>
</v-tabs>
</template>
<script>
// Mixins
import Proxyable from 'vuetify/lib/mixins/proxyable'
export default {
name: 'MaterialTabs',
mixins: [Proxyable],
props: {
color: {
type: String,
default: 'primary'
}
}
}
</script>
<style lang="sass">
.v-tabs--pill
.v-tab,
.v-tab:before
border-radius: 24px
&.v-tabs--icons-and-text
.v-tab,
.v-tab:before
border-radius: 4px
</style>

View File

@ -1,75 +0,0 @@
<template>
<v-card class="text-center v-card--testimony">
<div class="pt-6">
<v-icon
color="black"
x-large
>
mdi-format-quote-close
</v-icon>
</div>
<v-card-text
class="display-1 font-weight-light font-italic mb-3"
v-text="blurb"
/>
<div
class="display-2 font-weight-light mb-2"
v-text="author"
/>
<div
class="body-2 text-uppercase grey--text"
v-text="handle"
/>
<v-avatar
color="grey"
size="100"
>
<v-img
:alt="`${author} Testimonial`"
:src="avatar"
/>
</v-avatar>
<div/>
</v-card>
</template>
<script>
export default {
name: 'BaseMaterialTestimony',
props: {
author: {
type: String,
default: ''
},
avatar: {
type: String,
default: 'https://demos.creative-tim.com/material-dashboard-pro/assets/img/faces/card-profile1-square.jpg'
},
blurb: {
type: String,
default: ''
},
handle: {
type: String,
default: ''
}
}
}
</script>
<style lang="sass">
.v-card--testimony
padding-bottom: 72px
margin-bottom: 64px
.v-avatar
position: absolute
left: calc(50% - 64px)
top: calc(100% - 64px)
</style>

View File

@ -1,109 +0,0 @@
<template>
<v-card
class="v-card--wizard"
elevation="12"
max-width="700"
>
<v-card-title class="justify-center display-2 font-weight-light pt-5">
Build your profile
</v-card-title>
<div class="text-center display-1 grey--text font-weight-light mb-6">
This information will let us know more about you.
</div>
<v-tabs
ref="tabs"
v-model="internalValue"
background-color="green lighten-5"
color="white"
grow
slider-size="50"
>
<v-tabs-slider
class="mt-1"
color="success"
/>
<v-tab
v-for="(item, i) in items"
:key="i"
:disabled="!availableSteps.includes(i)"
:ripple="false"
>
{{ item }}
</v-tab>
</v-tabs>
<div class="my-6"/>
<v-card-text>
<v-tabs-items v-model="internalValue">
<slot/>
</v-tabs-items>
</v-card-text>
<v-card-actions class="pb-4 pa-4">
<v-btn
:disabled="internalValue === 0"
class="white--text"
color="grey darken-2"
min-width="125"
@click="$emit('click:prev')"
>
Previous
</v-btn>
<v-spacer/>
<v-btn
:disabled="!availableSteps.includes(internalValue + 1)"
color="success"
min-width="100"
@click="$emit('click:next')"
>
{{ internalValue === items.length - 1 ? 'Finish' : 'Next' }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
// Mixins
import Proxyable from 'vuetify/lib/mixins/proxyable'
export default {
name: 'BaseMaterialWizard',
mixins: [Proxyable],
props: {
availableSteps: {
type: Array,
default: () => ([])
},
items: {
type: Array,
default: () => ([])
}
}
}
</script>
<style lang="sass">
.v-card--wizard
overflow: visible
.v-tabs-bar
height: 56px
padding: 0 8px
.v-slide-group__wrapper
overflow: visible
.v-tabs-slider
border-radius: 4px
.v-slide-group__wrapper
contain: initial
</style>

View File

@ -1,34 +0,0 @@
<template>
<div class="display-2 font-weight-light col col-12 text-left text--primary pa-0 mb-8">
<h5 class="font-weight-light">
{{ subheading }}
<template v-if="text">
<span
class="subtitle-1"
v-text="text"
/>
</template>
</h5>
<div class="pt-2">
<slot/>
</div>
</div>
</template>
<script>
export default {
name: 'Subheading',
props: {
subheading: {
type: String,
default: ''
},
text: {
type: String,
default: ''
}
}
}
</script>

View File

@ -1,42 +0,0 @@
<template>
<section class="mb-12 text-center">
<h1
class="font-weight-light mb-2"
style="color:#3c4858; font-size:24px"
v-text="`Vuetify ${heading}`"
/>
<span
class="font-weight-light"
style="font-size: 16px; color: #3c4858"
>
Please checkout the
<a
:href="`https://vuetifyjs.com/${link}`"
class="secondary--text"
rel="noopener"
style="text-decoration:none;"
target="_blank"
>
full documentation
</a>
</span>
</section>
</template>
<script>
export default {
name: 'VComponent',
props: {
heading: {
type: String,
default: ''
},
link: {
type: String,
default: ''
}
}
}
</script>

View File

@ -1,4 +0,0 @@
const DEBUG = process.env.NODE_ENV === 'development';
const domain = process.env.DOMIAN || 'https://sub.store';
export const BACKEND_BASE = DEBUG ? `http://localhost:3000` : domain;
// export const BACKEND_BASE = DEBUG ? `https://sub.store:9999` : `https://sub.store`;

View File

@ -1,24 +0,0 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import ar from 'vuetify/lib/locale/ar'
import en from 'vuetify/lib/locale/en'
Vue.use(VueI18n)
const messages = {
en: {
...require('@/locales/en.json'),
$vuetify: en
},
ar: {
...require('@/locales/ar.json'),
$vuetify: ar
}
}
export default new VueI18n({
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
messages
})

View File

@ -1,44 +0,0 @@
{
"avatar": "تانيا أندرو",
"buttons": "وصفت",
"calendar": "التقويم",
"charts": "الرسوم البيانية",
"components": "المكونات",
"ct": "CT",
"dashboard": "لوحة القيادة",
"dtables": "جداول البيانات",
"eforms": "أشكال موسعة",
"error": "صفحة الخطأ",
"etables": "الجداول الموسعة",
"example": "مثال",
"forms": "إستمارات",
"fullscreen": "خريطة الشاشة الكاملة",
"google": "خرائط جوجل",
"grid": "نظام الشبكة",
"icons": "الرموز",
"lock": "قفل الشاشة الصفحة",
"login": "صفحة تسجيل الدخول",
"maps": "خرائط",
"multi": "متعدد المستويات انهيار",
"notifications": "إخطارات",
"pages": "صفحات",
"plan": "اختر خطة",
"pricing": "التسعير",
"my-profile": "ملفي",
"edit-profile": "تعديل الملف الشخصي",
"register": "تسجيل الصفحة",
"rforms": "النماذج العادية",
"rtables": "الجداول العادية",
"rtl": "دعم RTL",
"search": "بحث...",
"settings": "الإعدادات",
"tables": "الجداول",
"tabs": "Tabs",
"tim": "تيم الإبداعية",
"timeline": "الجدول الزمني",
"typography": "طباعة",
"user": "ملف تعريفي للمستخدم",
"vforms": "نماذج التحقق من الصحة",
"widgets": "الحاجيات",
"wizard": "ساحر"
}

View File

@ -1,44 +0,0 @@
{
"avatar": "Tania Andrew",
"buttons": "Buttons",
"calendar": "Calendar",
"charts": "Charts",
"components": "Components",
"ct": "CT",
"dashboard": "Dashboard",
"dtables": "Data Tables",
"eforms": "Extended Forms",
"error": "Error Page",
"etables": "Extended Tables",
"example": "Example",
"forms": "Forms",
"fullscreen": "Full Screen Map",
"google": "Google Maps",
"grid": "Grid System",
"icons": "Icons",
"lock": "Lock Screen Page",
"login": "Login Page",
"maps": "Maps",
"multi": "Multi Level Collapse",
"notifications": "Notifications",
"pages": "Pages",
"plan": "Choose Plan",
"pricing": "Pricing",
"my-profile": "My Profile",
"edit-profile": "Edit Profile",
"register": "Register Page",
"rforms": "Regular Forms",
"rtables": "Regular Tables",
"rtl": "RTL Support",
"search": "Search",
"settings": "Settings",
"tables": "Tables",
"tabs": "Tabs",
"tim": "Creative Tim",
"timeline": "Timeline",
"typography": "Typography",
"user": "User Profile",
"vforms": "Validation Forms",
"widgets": "Widgets",
"wizard": "Wizard"
}

View File

@ -1,23 +0,0 @@
import Vue from 'vue'
import vuetify from './plugins/vuetify';
import 'material-design-icons-iconfont/dist/material-design-icons.css'
import './plugins/base';
import './plugins/chartist';
// import './plugins/vee-validate';
import './plugins/vue-world-map';
import i18n from './i18n';
import router from './router';
import store from './store';
import Clipboard from 'v-clipboard';
import App from './App.vue'
Vue.config.productionTip = false
Vue.use(Clipboard);
new Vue({
vuetify,
router,
store,
Clipboard,
i18n,
render: h => h(App)
}).$mount('#app')

View File

@ -1,17 +0,0 @@
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
'@/components/base', true, /\.vue$/
)
requireComponent.keys().forEach(fileName => {
const componentConfig = requireComponent(fileName)
const componentName = upperFirst(
camelCase(fileName.replace(/^\.\//, '').replace(/\.\w+$/, ''))
)
Vue.component(`Base${componentName}`, componentConfig.default || componentConfig)
})

View File

@ -1,4 +0,0 @@
import Vue from 'vue'
import 'chartist/dist/chartist.min.css'
Vue.use(require('vue-chartist'))

View File

@ -1,5 +0,0 @@
import Vue from 'vue'
// import * as VeeValidate from 'vee-validate'
import VeeValidate from 'vee-validate'
Vue.use(VeeValidate)

View File

@ -1,4 +0,0 @@
import Vue from 'vue'
import VueWorldMap from 'vue-world-map'
Vue.component('v-world-map', VueWorldMap)

View File

@ -1,24 +0,0 @@
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import i18n from '@/i18n'
Vue.use(Vuetify)
const theme = {
primary: '#E91E63',
secondary: '#9C27b0',
accent: '#9C27b0',
info: '#00CAE3'
}
export default new Vuetify({
lang: {
t: (key, ...params) => i18n.t(key, params)
},
theme: {
themes: {
dark: theme,
light: theme
}
}
})

View File

@ -1,62 +0,0 @@
import Vue from 'vue';
import Router from 'vue-router';
import store from "../store";
import Subscription from "@/views/Subscription";
import Dashboard from "@/views/Dashboard";
import User from "@/views/User";
import SubEditor from "@/views/SubEditor";
import Cloud from "@/views/Cloud";
Vue.use(Router);
const router = new Router({
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "subscriptions",
component: Subscription,
meta: {title: "订阅", keepAlive: true}
},
{
path: "/dashboard",
name: "dashboard",
component: Dashboard,
meta: {title: "首页", keepAlive: true}
},
{
path: "/cloud",
name: "artifact",
component: Cloud,
meta: {title: "同步", keepAlive: true}
},
{
path: "/user",
name: "user",
component: User,
meta: {title: "我的", keepAlive: true}
},
{
path: "/sub-edit/:name",
name: "sub-editor",
component: SubEditor,
meta: {title: "订阅编辑"}
},
{
path: "/collection-edit/:name",
name: "collection-edit",
component: SubEditor,
props: {isCollection: true},
meta: {title: "组合订阅编辑"}
}
]
});
router.beforeEach((to, from, next) => {
const {meta} = to;
store.commit("SET_NAV_TITLE", meta.title);
next();
})
export default router;

View File

@ -1,137 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import {axios} from "@/utils";
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
title: "Sub-Store",
clipboard: "",
isLoading: false,
bottomNavBarHeight: 0,
successMessage: "",
errorMessage: "",
snackbarTimer: "",
subscriptions: {},
collections: {},
artifacts: {},
env: {},
settings: {}
},
mutations: {
COPY(state, text) {
state.clipboard = text;
},
// UI
SET_NAV_TITLE(state, title) {
state.title = title;
},
SET_BOTTOM_NAVBAR_HEIGHT (state, height){
state.bottomNavBarHeight = height;
},
SET_LOADING(state, loading) {
state.isLoading = loading;
},
SET_SNACK_BAR_TIMER(state, timer) {
state.snackbarTimer = timer;
},
SET_SUCCESS_MESSAGE(state, msg) {
state.successMessage = msg;
},
SET_ERROR_MESSAGE(state, msg) {
state.errorMessage = msg;
}
},
actions: {
// fetch subscriptions
async FETCH_SUBSCRIPTIONS({state}) {
return axios.get("/subs").then(resp => {
const {data} = resp.data;
state.subscriptions = data;
});
},
// fetch collections
async FETCH_COLLECTIONS({state}) {
return axios.get("/collections").then(resp => {
const {data} = resp.data;
state.collections = data;
});
},
async FETCH_ARTIFACTS({state}) {
return axios.get("/artifacts").then(resp => {
const {data} = resp.data;
state.artifacts = data;
});
},
// fetch env
async FETCH_ENV({state}) {
return axios.get("/utils/env").then(resp => {
const {data} = resp.data;
state.env = data;
})
},
async FETCH_SETTINGS({state}) {
return axios.get("/settings").then(resp => {
state.settings = {
theme: {
darkMode: false
},
...resp.data
}
});
},
// update subscriptions
async UPDATE_SUBSCRIPTION({dispatch}, {name, sub}) {
return axios.patch(`/sub/${name}`, sub).then(() => {
dispatch("FETCH_SUBSCRIPTIONS");
dispatch("FETCH_COLLECTIONS");
});
},
// new subscription
async NEW_SUBSCRIPTION({dispatch}, sub) {
return axios.post(`/subs`, sub).then(() => {
dispatch("FETCH_SUBSCRIPTIONS");
});
},
// delete subscription
async DELETE_SUBSCRIPTION({dispatch}, name) {
return axios.delete(`/sub/${name}`).then(() => {
dispatch("FETCH_SUBSCRIPTIONS");
dispatch("FETCH_COLLECTIONS");
});
},
// update collection
async UPDATE_COLLECTION({dispatch}, {name, collection}) {
return axios.patch(`/collection/${name}`, collection).then(() => {
dispatch("FETCH_COLLECTIONS");
});
},
// new collection
async NEW_COLLECTION({dispatch}, collection) {
return axios.post(`/collections`, collection).then(() => {
dispatch("FETCH_COLLECTIONS");
})
},
// delete collection
async DELETE_COLLECTION({dispatch}, name) {
return axios.delete(`/collection/${name}`).then(() => {
dispatch("FETCH_COLLECTIONS");
})
}
},
getters: {}
})
export default store;

View File

@ -1,23 +0,0 @@
import Axios from 'axios';
import Vue from 'vue';
import store from "@/store";
import {BACKEND_BASE} from "@/config";
export const axios = Axios.create({
baseURL: `${BACKEND_BASE}/api`,
timeout: 10000
});
export const EventBus = new Vue();
export function isEmptyObj(obj) {
return Object.keys(obj).length === 0;
}
export function showInfo(msg) {
store.commit("SET_SUCCESS_MESSAGE", msg);
}
export function showError(err) {
store.commit("SET_ERROR_MESSAGE", err);
}

View File

@ -1,345 +0,0 @@
<template>
<v-container fluid>
<v-card>
<v-card-title>
<v-icon left>mdi-cloud</v-icon>
同步配置
<v-spacer></v-spacer>
<v-btn icon @click="syncAllArtifacts()">
<v-icon>backup</v-icon>
</v-btn>
<v-btn icon @click="openGist()">
<v-icon>visibility</v-icon>
</v-btn>
<v-dialog v-model="showArtifactDialog" max-width="400px">
<template #activator="{on}">
<v-btn v-on="on" icon>
<v-icon color="primary">mdi-plus-circle</v-icon>
</v-btn>
</template>
<v-card class="pl-4 pr-4 pb-4 pt-4">
<v-subheader>
<v-icon left>{{ editing ? 'edit_off' : 'mdi-plus-circle' }}</v-icon>
<h3>{{ editing ? '修改' : '添加' }}同步配置</h3>
</v-subheader>
<v-divider></v-divider>
<v-form v-model="formValid" class="pt-4 pl-4 pr-4 pb-0">
<v-text-field v-model="currentArtifact.name" :disabled="editing" clear-icon="clear" clearable label="配置名称"
placeholder="填入生成配置名称名称需唯一如Clash.yaml。" />
<v-text-field v-model="currentArtifact['display-name']" clear-icon="clear" clearable label="配置显示名称"
placeholder="填入生成配置显示名称,名称无需唯一。" />
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-text-field v-on="on" :rules="validations.required" :value="getType(currentArtifact.type)"
label="类型" />
</template>
<v-list dense>
<v-list-item @click="setArtifactType('subscription')">
<v-list-item-icon>
<v-icon>mdi-link</v-icon>
</v-list-item-icon>
<v-list-item-title>订阅</v-list-item-title>
</v-list-item>
<v-list-item @click="setArtifactType('collection')">
<v-list-item-icon>
<v-icon>list</v-icon>
</v-list-item-icon>
<v-list-item-title>组合订阅</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-text-field v-model="currentArtifact.source" v-on="on"
:placeholder="`填入${getType(currentArtifact.type) || '来源'}的名称。`" :rules="validations.required"
label="来源" />
</template>
<v-list dense>
<v-list-item v-for="(sub, idx) in getSources(currentArtifact.type)" :key="idx"
@click="currentArtifact.source = sub.name">
<v-list-item-avatar>
<v-icon v-if="!sub.icon" color="teal darken-1">mdi-cloud</v-icon>
<v-img v-else :class="getIconClass(sub.icon)" :src="sub.icon" />
</v-list-item-avatar>
<v-list-item-title>{{ sub['display-name'] || sub.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-text-field v-on="on" :rules="validations.required" :value="currentArtifact.platform" label="目标" />
</template>
<v-list dense>
<v-list-item v-for="platform in ['Surge', 'Loon', 'QX', 'Clash']" :key="platform"
@click="currentArtifact.platform = platform">
<v-list-item-avatar>
<v-img :class="getIconClass('#invert')" :src="getIcon(platform)"></v-img>
</v-list-item-avatar>
<v-list-item-title>{{ platform }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-form>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn :disabled="!formValid" color="primary" small text @click="doneEditArtifact()">
确认
</v-btn>
<v-btn small text @click="clear()">
取消
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card-title>
<template v-for="(artifact, idx) in artifacts">
<v-list-item :key="artifact.name" dense three-line>
<v-list-item-avatar>
<v-img :class="getIconClass('#invert')" :src="getIcon(artifact.platform)" />
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ artifact['display-name'] || artifact.name }}
</v-list-item-title>
<v-chip-group>
<v-chip label>
<v-icon left>info</v-icon>
{{ getType(artifact.type) }}
</v-chip>
<v-chip label>
<v-icon left>mdi-link</v-icon>
{{ artifact.source }}
</v-chip>
</v-chip-group>
<v-list-item-subtitle>更新于{{ getUpdatedTime(artifact.updated) }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-row>
<v-col>
<v-btn icon @click="toggleSync(artifact)">
<v-icon :color="artifact.sync ? undefined: 'red'">{{ artifact.sync ? "alarm" : "alarm_off" }}</v-icon>
</v-btn>
</v-col>
<v-col>
<v-menu bottom left>
<template #activator="{ on }">
<v-btn v-on="on" icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item v-if="artifact.url" @click="copy(artifact)">
<v-list-item-title>复制</v-list-item-title>
</v-list-item>
<v-list-item @click="editArtifact(artifact)">
<v-list-item-title>编辑</v-list-item-title>
</v-list-item>
<v-list-item @click="preview(artifact.name)">
<v-list-item-title>预览</v-list-item-title>
</v-list-item>
<v-list-item @click="sync(artifact.name)">
<v-list-item-title>同步</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteArtifact(idx, artifact.name)">
<v-list-item-title>删除</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-col>
</v-row>
</v-list-item-action>
</v-list-item>
</template>
</v-card>
</v-container>
</template>
<script>
import {axios} from '@/utils';
import {BACKEND_BASE} from "@/config";
import {format} from 'timeago.js';
export default {
name: "Cloud",
data() {
return {
showArtifactDialog: false,
currentArtifact: {
name: "",
'display-name': "",
type: "subscription",
source: "",
platform: "",
},
editing: null,
formValid: false,
validations: {
required: [
v => !!v || "不能为空!"
]
}
}
},
computed: {
artifacts() {
const items = this.$store.state.artifacts;
return Object.keys(items).map(k => items[k]);
},
settings() {
return this.$store.state.settings;
}
},
methods: {
getIcon(platform) {
const ICONS = {
"Clash": "https://raw.githubusercontent.com/58xinian/icon/master/clash_mini.png",
"QX": "https://raw.githubusercontent.com/Orz-3/mini/none/quanX.png",
"Surge": "https://raw.githubusercontent.com/Orz-3/mini/none/surge.png",
"Loon": "https://raw.githubusercontent.com/Orz-3/mini/none/loon.png",
"ShadowRocket": "https://raw.githubusercontent.com/Orz-3/mini/master/loon.png"
}
return ICONS[platform];
},
getType(type) {
const DESCRIPTIONS = {
"subscription": "订阅",
"collection": "组合订阅"
}
return DESCRIPTIONS[type];
},
getUpdatedTime(time) {
if (!time) {
return "从未更新";
} else {
return format(time, "zh_CN");
}
},
async doneEditArtifact() {
console.log(JSON.stringify(this.currentArtifact, null, 2));
try {
if (this.editing) {
await axios.patch(`/artifact/${this.currentArtifact.name}`, this.currentArtifact);
} else {
await axios.post("/artifacts", this.currentArtifact);
}
await this.$store.dispatch("FETCH_ARTIFACTS");
this.clear();
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `${this.editing ? "更新" : "创建"}配置失败!${err}`);
}
},
async editArtifact(artifact) {
this.editing = true;
Object.assign(this.currentArtifact, artifact);
this.showArtifactDialog = true;
},
async toggleSync(artifact) {
artifact.sync = !artifact.sync;
try {
await axios.patch(`/artifact/${artifact.name}`, artifact);
await this.$store.dispatch("FETCH_ARTIFACTS");
this.$store.commit("SET_SUCCESS_MESSAGE", `${artifact.sync ? '启用' : '禁用'}自动同步配置${artifact.name}`);
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `更改同步配置失败!${err}`);
}
},
async deleteArtifact(idx, name) {
try {
await axios.delete(`/artifact/${name}`);
await this.$store.dispatch("FETCH_ARTIFACTS");
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `删除配置失败!${err}`);
}
},
clear() {
this.currentArtifact = {
name: "",
'display-name': "",
type: "subscription",
source: "",
platform: ""
}
this.showArtifactDialog = false;
this.editing = false;
},
copy(artifact) {
if (artifact.url) {
this.$clipboard(artifact.url);
this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制配置链接");
}
},
preview(name) {
window.open(`${BACKEND_BASE}/api/artifact/${name}?action=preview`);
},
async sync(name) {
this.$store.commit("SET_LOADING", true);
try {
await axios.get(`/artifact/${name}?action=sync`);
await this.$store.dispatch("FETCH_ARTIFACTS");
this.$store.commit("SET_SUCCESS_MESSAGE", `同步配置成功!`);
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `同步配置失败!${err}`);
} finally {
this.$store.commit("SET_LOADING", false);
}
},
async syncAllArtifacts() {
this.$store.commit("SET_LOADING", true);
try {
await axios.get(`/cron/sync-artifacts`);
await this.$store.dispatch("FETCH_ARTIFACTS");
this.$store.commit("SET_SUCCESS_MESSAGE", `Gist 同步生成节点成功!`);
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `Gist 同步生成节点失败!${err}`);
} finally {
this.$store.commit("SET_LOADING", false);
}
},
setArtifactType(type) {
this.currentArtifact.type = type;
this.currentArtifact.source = "";
},
getSources(type) {
let data;
switch (type) {
case "subscription":
data = this.$store.state.subscriptions;
break;
case "collection":
data = this.$store.state.collections;
}
return Object.keys(data).map(k => data[k]);
},
getIconClass(url) {
return url.indexOf('#invert') !== -1 && !this.$vuetify.theme.dark ? 'invert' : ''
},
openGist() {
window.open(`https://gist.github.com${'/' + this.settings.githubUser || ''}`)
}
}
}
</script>
<style scoped>
.invert {
filter: invert(100%);
}
</style>

View File

@ -1,49 +0,0 @@
<template>
<v-container></v-container>
</template>
<script>
export default {
name: "Dashboard",
components: {},
computed: {
pie() {
const total = 400;
const upload = 30;
const download = 200;
const remaining = total - (upload + download);
return {
series: [
{
name: "流量",
type: "pie",
radius: "50%",
data: [
{
name: `剩余量\n${(remaining)} GB`,
value: remaining
},
{
name: `下载量\n${(download)} GB`,
value: download
},
{
name: `上传量\n${(upload)} GB`,
value: upload
}
]
}
],
animationEasing: 'elasticOut'
};
}
}
}
</script>
<style>
.remains {
width: 100%;
height: 200px; /* or e.g. 400px */
}
</style>

View File

@ -1,752 +0,0 @@
<template>
<v-container>
<FloatMenu class="floatActionBtn">
<v-btn fab small @click.stop="dialog = true">
<v-icon color="purple lighten-1">mdi-plus-circle</v-icon>
</v-btn>
<v-btn fab small @click="save">
<v-icon color="teal lighten-1">mdi-content-save</v-icon>
</v-btn>
</FloatMenu>
<v-dialog v-model="dialog" scrollable>
<v-card>
<v-card-title>选择节点操作</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-radio-group v-model="selectedProcess" dense>
<v-radio
v-for="(k, idx) in Object.keys(availableProcessors)"
:key="idx"
:label="availableProcessors[k].name"
:value="k"
></v-radio>
</v-radio-group>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="addProcess(selectedProcess)"
>确认
</v-btn>
<v-btn text @click="dialog = false">取消</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model = "showShareDialog" max-width = "400px">
<v-card class = "pl-4 pr-4 pb-4 pt-4">
<v-card-title>
<v-icon left>mdi-file-import</v-icon>
配置导入
<v-spacer />
</v-card-title>
<v-textarea v-model = "imported" :rules = "validations.importRules"
clear-icon = "clear" clearable label = "粘贴配置以导入" rows = "5"
solo
/>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color = "primary" text @click = "importConf">确认</v-btn>
<v-btn text @click = "showShareDialog = false">取消</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-card class="mb-4">
<v-subheader>订阅配置</v-subheader>
<v-form v-model="formState.basicValid" class="pl-4 pr-4 pb-0">
<v-text-field
v-model="options.name"
class="mt-2"
clear-icon="clear"
clearable
label="订阅名称"
placeholder="填入订阅名称,名称需唯一"
required
/>
<v-text-field
v-model="options['display-name']"
class="mt-2"
clear-icon="clear"
clearable
label="订阅显示名称"
placeholder="填入订阅显示名称,名称无需唯一"
/>
<!--For Subscription-->
<v-radio-group
v-if="!isCollection"
v-model="options.source"
class="mt-0 mb-0"
>
<template v-slot:label>
<div>订阅来源</div>
</template>
<v-row dense>
<v-col>
<v-radio label="远程" value="remote" />
</v-col>
<v-col>
<v-radio label="本地" value="local" />
</v-col>
<v-col></v-col>
</v-row>
</v-radio-group>
<v-textarea
v-if="!isCollection && options.source !== 'local'"
v-model="options.url"
:rules="validations.urlRules"
auto-grow
class="mt-0"
clear-icon="clear"
clearable
label="订阅链接"
placeholder="填入机场原始订阅链接"
required
rows="2"
/>
<v-textarea
v-if="options.source === 'local'"
v-model="options.content"
clear-icon="clear"
clearable
label="订阅内容"
placeholder="填入原始订阅内容"
autogrow
rows="5"
row-height="15"
class="mt-0"
>
</v-textarea>
<v-textarea
v-if="!isCollection && options.source !== 'local'"
v-model="options.ua"
auto-grow
class="mt-2"
clear-icon="clear"
clearable
label="User-Agent"
placeholder="自定义下载订阅使用的User-Agent可选。"
rows="2"
/>
<!--For Collection-->
<v-list v-if="isCollection" dense>
<v-subheader class="pl-0">包含的订阅</v-subheader>
<v-list-item v-for="sub in availableSubs" :key="sub.name">
<v-list-item-avatar>
<v-icon v-if="!sub.icon" color="teal darken-1">mdi-cloud</v-icon>
<v-img v-else :class="getIconClass(sub.icon)" :src="sub.icon" />
</v-list-item-avatar>
<v-list-item-content>
<template v-if="sub['display-name']">
{{ sub['display-name'] }} ({{ sub.name }})
</template>
<template v-else>
{{ sub.name }}
</template>
</v-list-item-content>
<v-spacer></v-spacer>
<v-checkbox v-model="selected" :value="sub.name" class="pr-1" />
</v-list-item>
</v-list>
<v-textarea
v-model="options.icon"
auto-grow
class="mt-2"
clear-icon="clear"
clearable
label="图标链接"
placeholder="填入想要展示的图标链接,可选。"
rows="2"
/>
</v-form>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text small @click = "save" class="fixedActionBtn">
<v-icon>mdi-content-save</v-icon>
保存
</v-btn>
<v-btn text small @click.stop = "showShareDialog = true" class = "fixedActionBtn">
<v-icon>mdi-file-import</v-icon>
导入
</v-btn>
<v-btn text small @click = "share" class = "fixedActionBtn">
<v-icon>mdi-share-circle</v-icon>
分享
</v-btn>
</v-card-actions>
</v-card>
<v-card class="mb-4">
<v-subheader>常用选项</v-subheader>
<v-form class="pl-4 pr-4">
<v-item-group>
<v-radio-group v-model="options.useless" class="mt-0 mb-0" dense>
过滤非法节点
<v-row>
<v-col>
<v-radio label="保留" value="KEEP" />
</v-col>
<v-col>
<v-radio label="删除" value="REMOVE" />
</v-col>
<v-col></v-col>
</v-row>
</v-radio-group>
<v-radio-group v-model="options.udp" class="mt-0 mb-0" dense>
UDP转发
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制开启" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制关闭" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
<v-radio-group
v-model="options['skip-cert-verify']"
class="mt-0 mb-0"
dense
>
跳过证书验证
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制跳过" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制验证" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
<v-radio-group v-model="options.tfo" class="mt-0 mb-0" dense>
TCP Fast Open
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制开启" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制关闭" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
<v-radio-group v-model="options['aead']" class="mt-0 mb-0" dense>
Vmess AEAD
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制开启" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制关闭" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
</v-item-group>
</v-form>
</v-card>
<v-card class="mb-4">
<v-subheader>Surge 选项</v-subheader>
<v-form class="pl-4 pr-4">
<v-radio-group
v-model="options['surge-hybrid']"
class="mt-0 mb-0"
dense
>
Hybrid 策略
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制开启" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制关闭" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
</v-form>
</v-card>
<v-card id="processors" class="mb-4">
<v-subheader> 节点操作
<v-spacer></v-spacer>
<v-btn icon color = "primary" @click.stop = "dialog = true"
>
<v-icon>mdi-plus-circle</v-icon>
</v-btn>
</v-subheader>
<!--<v-divider></v-divider>-->
<component
:is="p.component"
v-for="p in processors"
:key="p.id"
:args="p.args"
@dataChanged="dataChanged"
@deleteProcess="deleteProcess"
@down="moveDown"
@up="moveUp"
>
</component>
</v-card>
</v-container>
</template>
<script>
import { showError, showInfo } from "@/utils";
import FloatMenu from "@/components/FloatMenu.vue";
import TypeFilter from "@/components/TypeFilter";
import RegionFilter from "@/components/RegionFilter";
import RegexFilter from "@/components/RegexFilter";
import SortOperator from "@/components/SortOperator";
import RegexRenameOperator from "@/components/RegexRenameOperator";
import RegexDeleteOperator from "@/components/RegexDeleteOperator";
import FlagOperator from "@/components/FlagOperator";
import ScriptFilter from "@/components/ScriptFilter";
import ScriptOperator from "@/components/ScriptOperator";
import RegexSortOperator from "@/components/RegexSortOperator";
import HandleDuplicateOperator from "@/components/HandleDuplicateOperator";
import ResolveDomainOperator from "@/components/ResolveDomainOperator";
const AVAILABLE_PROCESSORS = {
"Flag Operator": {
component: "FlagOperator",
name: "国旗",
},
"Type Filter": {
component: "TypeFilter",
name: "类型过滤器",
},
"Region Filter": {
component: "RegionFilter",
name: "区域过滤器",
},
"Regex Filter": {
component: "RegexFilter",
name: "正则过滤器",
},
"Sort Operator": {
component: "SortOperator",
name: "节点排序",
},
"Regex Sort Operator": {
component: "RegexSortOperator",
name: "正则排序",
},
"Regex Rename Operator": {
component: "RegexRenameOperator",
name: "正则重命名",
},
"Regex Delete Operator": {
component: "RegexDeleteOperator",
name: "删除正则",
},
"Handle Duplicate Operator": {
component: "HandleDuplicateOperator",
name: "节点去重",
},
"Resolve Domain Operator": {
component: "ResolveDomainOperator",
name: "节点域名解析",
},
"Script Filter": {
component: "ScriptFilter",
name: "脚本过滤器",
},
"Script Operator": {
component: "ScriptOperator",
name: "脚本操作",
},
};
export default {
props: {
isCollection: {
type: Boolean,
default() {
return false;
},
},
},
components: {
FlagOperator,
RegexFilter,
RegionFilter,
TypeFilter,
SortOperator,
RegexSortOperator,
RegexRenameOperator,
RegexDeleteOperator,
ScriptFilter,
ScriptOperator,
HandleDuplicateOperator,
ResolveDomainOperator,
FloatMenu,
},
data: function () {
return {
selectedProcess: null,
showShareDialog: false,
imported: "",
dialog: false,
validations: {
urlRules: [
(v) =>
this.options.source === "remote" && (!!v || "订阅链接不能为空!"),
(v) =>
this.options.source === "remote" &&
(/^https?:\/\//.test(v) || "订阅链接不合法!"),
],
importRules: [(v) => !!v || "不能导入空配置!"],
},
formState: {
basicValid: false,
},
options: {
name: "",
"display-name": "",
source: "",
url: "",
content: "",
icon: "",
ua: "",
useless: "KEEP",
udp: "DEFAULT",
"skip-cert-verify": "DEFAULT",
tfo: "DEFAULT",
"surge-hybrid": "DEFAULT",
aead: "DEFAULT",
},
process: [],
selected: [],
};
},
created() {
const name = decodeURIComponent(this.$route.params.name);
let source;
if (this.isCollection) {
source =
typeof name === "undefined" || name === "UNTITLED"
? {}
: this.$store.state.collections[name];
this.$store.commit(
"SET_NAV_TITLE",
source.name ? `组合订阅编辑 ➤ ${source.name}` : "新建组合订阅"
);
this.selected = source.subscriptions || [];
} else {
source =
typeof name === "undefined" || name === "UNTITLED"
? {}
: this.$store.state.subscriptions[name];
this.$store.commit(
"SET_NAV_TITLE",
source.name ? `订阅编辑 ➤ ${source.name}` : "新建订阅"
);
}
this.name = source.name;
const { options, process } = loadProcess(this.options, source);
this.options = options;
this.process = process;
},
computed: {
availableSubs() {
return this.$store.state.subscriptions;
},
availableProcessors() {
return AVAILABLE_PROCESSORS;
},
processors() {
return this.process
.filter((p) => AVAILABLE_PROCESSORS[p.type])
.map((p) => {
return {
component: AVAILABLE_PROCESSORS[p.type].component,
args: p.args,
id: p.id,
};
});
},
config() {
const output = {
name: this.options.name,
"display-name": this.options["display-name"],
icon: this.options.icon,
process: [],
};
if (this.isCollection) {
output.subscriptions = this.selected;
} else {
output.url = this.options.url;
output.source = this.options.source;
output.content = this.options.content;
}
// assign user-agent, if ua is set
let ua = this.options.ua;
if (typeof ua != "undefined" && ua != null && ua.trim().length > 0) {
output.ua = ua;
} else {
output.ua = "";
}
// useless filter
if (this.options.useless === "REMOVE") {
output.process.push({
type: "Useless Filter",
});
}
// udp, tfo, scert, surge-hybrid, aead
for (const opt of [
"udp",
"tfo",
"skip-cert-verify",
"surge-hybrid",
"aead",
]) {
if (this.options[opt] !== "DEFAULT") {
output.process.push({
type: "Set Property Operator",
args: { key: opt, value: this.options[opt] === "FORCE_OPEN" },
});
}
}
for (const p of this.process) {
output.process.push(p);
}
return output;
},
},
methods: {
getIconClass(url) {
return url.indexOf("#invert") !== -1 && !this.$vuetify.theme.dark
? "invert"
: "";
},
save() {
if (this.isCollection) {
if (this.options.name && this.selected) {
if (this.$route.params.name === "UNTITLED") {
this.$store
.dispatch("NEW_COLLECTION", this.config)
.then(() => {
showInfo(`成功创建组合订阅:${this.name}`);
this.$router.back();
})
.catch(() => {
showError(`发生错误,无法创建组合订阅!`);
});
} else {
this.$store
.dispatch("UPDATE_COLLECTION", {
name: this.$route.params.name,
collection: this.config,
})
.then(() => {
showInfo(`成功保存组合订阅:${this.name}`);
})
.catch(() => {
showError(`发生错误,无法保存组合订阅!`);
});
}
}
} else {
console.log("Saving subscription...");
if (!this.options.name) {
showError(`订阅名字不能为空!`);
return;
}
if (this.options.source === "remote" && !this.options.url) {
showError(`订阅链接不能为空!`);
return;
}
if (this.$route.params.name !== "UNTITLED") {
this.$store
.dispatch("UPDATE_SUBSCRIPTION", {
name: this.$route.params.name,
sub: this.config,
})
.then(() => {
showInfo(`成功保存订阅:${this.options.name}`);
})
.catch(() => {
showError(`发生错误,无法保存订阅!`);
});
} else {
this.$store
.dispatch("NEW_SUBSCRIPTION", this.config)
.then(() => {
showInfo(`成功创建订阅:${this.options.name}`);
this.$router.back();
})
.catch(() => {
showError(`发生错误,无法创建订阅!`);
});
}
}
},
share() {
let config = this.config;
config.name = "「订阅名称」";
if (this.isCollection) {
config.subscriptions = [];
} else {
config.url = "「订阅链接」";
}
config = JSON.stringify(config);
this.$clipboard(config);
this.$store.commit(
"SET_SUCCESS_MESSAGE",
"导出成功,订阅已复制到剪贴板!"
);
// this.showShareDialog = false;
},
importConf() {
if (this.imported) {
const sub = JSON.parse(this.imported);
const { options, process } = loadProcess(this.options, sub);
delete options.name;
delete options.url;
delete options.content;
Object.assign(this.options, options);
this.process = process;
this.$store.commit("SET_SUCCESS_MESSAGE", "成功导入订阅!");
this.showShareDialog = false;
} else {
this.$store.commit("SET_ERROR_MESSAGE", "不能导入空配置!");
}
},
dataChanged(content) {
let index = 0;
for (; index < this.process.length; index++) {
if (this.process[index].id === content.idx) {
break;
}
}
this.process[index].args = content.args;
},
addProcess(type) {
this.process.push({ type, id: uuidv4() });
this.dialog = false;
},
deleteProcess(id) {
let index = 0;
for (; index < this.process.length; index++) {
if (this.process[index].id === id) {
break;
}
}
this.process.splice(index, 1);
},
moveUp(id) {
let index = 0;
for (; index < this.process.length; index++) {
if (this.process[index].id === id) {
break;
}
}
if (index === 0) return;
// otherwise swap with previous one
const prev = this.process[index - 1];
const cur = this.process[index];
this.process.splice(index - 1, 2, cur, prev);
},
moveDown(id) {
let index = 0;
for (; index < this.process.length; index++) {
if (this.process[index].id === id) {
break;
}
}
// otherwise swap with latter one
const cur = this.process[index];
const next = this.process[index + 1];
this.process.splice(index, 2, next, cur);
},
},
};
function loadProcess(options, source, isCollection = false) {
options = {
...options,
name: source.name,
"display-name": source["display-name"],
icon: source.icon,
ua: source.ua,
};
if (isCollection) {
options.subscriptions = source.subscriptions;
} else {
options.url = source.url;
options.source = source.source || "remote";
options.content = source.content;
}
let process = [];
// flag
for (const p of source.process || []) {
switch (p.type) {
case "Useless Filter":
options.useless = "REMOVE";
break;
case "Set Property Operator":
options[p.args.key] = p.args.value ? "FORCE_OPEN" : "FORCE_CLOSE";
break;
default:
p.id = uuidv4();
process.push(p);
}
}
return { options, process };
}
function uuidv4() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
</script>
<style lang="scss" scoped>
.invert {
filter: invert(100%);
}
.fixedActionBtn{
&.theme--dark{
color: #ffffffcc;
}
&.theme--light {
color : #00000099;
}
}
</style>

View File

@ -1,371 +0,0 @@
<template>
<v-container fluid>
<v-dialog v-model = "showPreviewDialog" scrollable>
<v-card>
<v-card-title>预览转换结果</v-card-title>
<v-divider></v-divider>
<v-list flat>
<v-list-item v-for = "platform in platformList" :key = "platform.name"
@click = "previewSpecificPlatform(platform.path)"
>
<v-list-item-avatar>
<v-img :class = "getIconClass('#invert')" :src = "platform.icon"
/>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text = "platform.name"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click = "showPreviewDialog = false">取消</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-card>
<v-card-title>
<v-icon left>local_airport</v-icon>
单个订阅
<v-spacer></v-spacer>
<v-btn icon @click = "createSub">
<v-icon color = "primary">mdi-plus-circle</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-list dense>
<v-list-item v-for = "sub in subscriptions" :key = "sub.name"
@click = "preview(sub)"
>
<v-list-item-avatar>
<v-icon v-if = "!sub.icon" color = "teal darken-1">mdi-cloud
</v-icon>
<v-img v-else :class = "getIconClass(sub.icon)"
:src = "sub.icon"
/>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class = "font-weight-medium"
v-text = "sub['display-name'] || sub.name"
></v-list-item-title>
<v-list-item-title v-text = "sub.url"></v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-menu bottom left>
<template v-slot:activator = "{ on, attrs }">
<v-btn v-bind = "attrs" v-on = "on" icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item v-for = "(menuItem, i) in editMenu" :key = "i"
@click = "subscriptionMenu(menuItem.action, sub)"
>
<v-list-item-content>{{
menuItem.title
}}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<v-card>
<v-card-title>
<v-icon left>work_outline</v-icon>
组合订阅
<v-spacer></v-spacer>
<v-btn icon @click = "createCol">
<v-icon color = "primary">mdi-plus-circle</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-list dense>
<v-list-item v-for = "collection in collections"
:key = "collection.name" dense
@click = "preview(collection, type='collection')"
>
<v-list-item-avatar>
<v-icon v-if = "!collection.icon" color = "teal darken-1">
mdi-cloud
</v-icon>
<v-img v-else :class = "getIconClass(collection.icon)"
:src = "collection.icon"
/>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class = "font-weight-medium"
v-text = "collection['display-name'] || collection.name"
></v-list-item-title>
<v-chip-group column>
<v-chip v-for = "subs in collection.subsInfo" :key = "subs.name"
class = "ma-2 ml-0 mr-1 pa-2" label small
>
{{ subs['display-name'] || subs.name }}
</v-chip>
</v-chip-group>
</v-list-item-content>
<v-list-item-action>
<v-menu bottom left>
<template v-slot:activator = "{ on, attrs }">
<v-btn v-bind = "attrs" v-on = "on" icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item v-for = "(menuItem, i) in editMenu" :key = "i"
@click = "collectionMenu(menuItem.action, collection)"
>
<v-list-item-content>{{
menuItem.title
}}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<v-dialog v-model = "showProxyList" fullscreen hide-overlay scrollable
transition = "dialog-bottom-transition"
>
<v-card fluid>
<v-toolbar class = "flex-grow-0">
<v-icon>mdi-dns</v-icon>
<v-spacer></v-spacer>
<v-toolbar-title>
<h4>节点列表</h4>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<v-btn icon @click = "showProxyList = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar-items>
<template v-slot:extension>
<v-tabs v-model = "tab" centered grow>
<v-tabs-slider color = "primary" />
<v-tab key = "raw">
<h4>原始节点</h4>
</v-tab>
<v-tab key = "processed">
<h4>生成节点</h4>
</v-tab>
</v-tabs>
</template>
</v-toolbar>
<v-card-text>
<v-tabs-items v-model = "tab">
<v-tab-item key = "raw">
<proxy-list :key = "url + 'raw'" ref = "proxyList" :raw = "true"
:sub = "sub" :url = "url"
></proxy-list>
</v-tab-item>
<v-tab-item key = "processed">
<proxy-list :key = "url" ref = "proxyList" :sub = "sub"
:url = "url"
></proxy-list>
</v-tab-item>
</v-tabs-items>
</v-card-text>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import ProxyList from '@/components/ProxyList'
import { BACKEND_BASE } from '@/config'
export default {
components : { ProxyList },
data : () => {
return {
opened : false,
showProxyList : false,
showPreviewDialog : false,
previewSubName : '',
isCollectionPreview : false,
url : '',
sub : [],
tab : 1,
platformList : [
{
name : 'Clash',
path : 'Clash',
icon : 'https://raw.githubusercontent.com/58xinian/icon/master/clash_mini.png',
},
{
name : 'Quantumult X',
path : 'QX',
icon : 'https://raw.githubusercontent.com/Orz-3/mini/none/quanX.png',
},
{
name : 'Surge',
path : 'Surge',
icon : 'https://raw.githubusercontent.com/Orz-3/mini/none/surge.png',
},
{
name : 'Loon',
path : 'Loon',
icon : 'https://raw.githubusercontent.com/Orz-3/mini/none/loon.png',
},
{
name : 'Stash',
path : 'Stash',
icon : 'https://raw.githubusercontent.com/Orz-3/mini/master/Alpha/stash.png',
}
],
editMenu : [
{
title : '链接',
action : 'COPY'
},
{
title : '编辑',
action : 'EDIT'
},
{
title : '预览',
action : 'PREVIEW'
},
{
title : '删除',
action : 'DELETE'
}
]
}
},
computed : {
subscriptionBaseURL (){
return BACKEND_BASE
},
subscriptions : {
get (){
const subs = this.$store.state.subscriptions
return Object.keys(subs).map(k => subs[k])
},
set (){
}
},
collections (){
const cols = this.$store.state.collections
const collections = Object.keys(cols).map(k => cols[k])
const subscriptions = this.$store.state.subscriptions
collections.map(item => {
item.subsInfo = []
item.subscriptions.map(sub => item.subsInfo.push(subscriptions[sub]))
})
return collections
},
},
methods : {
previewSpecificPlatform (path){
window.open(`${this.subscriptionBaseURL}/download/${this.isCollectionPreview ? 'collection/' : ''}${this.previewSubName}?target=${path}`)
this.showPreviewDialog = false
},
subscriptionMenu (action, sub){
console.log(`${action} --> ${sub.name}`)
switch (action){
case 'COPY':
this.$clipboard(
`${this.subscriptionBaseURL}/download/${encodeURIComponent(
sub.name)}`)
this.$store.commit('SET_SUCCESS_MESSAGE', '成功复制订阅链接')
break
case 'EDIT':
this.$router.push(`/sub-edit/${encodeURIComponent(sub.name)}`)
break
case 'PREVIEW':
this.previewSubName = sub.name
this.isCollectionPreview = false
this.showPreviewDialog = true
break
case 'DELETE':
this.$store.dispatch(
'DELETE_SUBSCRIPTION', encodeURIComponent(sub.name))
break
}
},
collectionMenu (action, collection){
console.log(`${action} --> ${collection.name}`)
switch (action){
case 'COPY':
this.$clipboard(
`${this.subscriptionBaseURL}/download/collection/${encodeURIComponent(
collection.name)}`)
this.$store.commit('SET_SUCCESS_MESSAGE', '成功复制订阅链接')
break
case 'EDIT':
this.$router.push(`/collection-edit/${collection.name}`)
break
case 'PREVIEW':
this.previewSubName = collection.name
this.isCollectionPreview = true
this.showPreviewDialog = true
break
case 'DELETE':
this.$store.dispatch('DELETE_COLLECTION', collection.name)
break
}
},
preview (item, type = 'sub'){
if (type === 'sub'){
this.url = `${BACKEND_BASE}/download/${encodeURIComponent(
item.name)}`
this.sub = item.url
} else{
this.url = `${BACKEND_BASE}/download/collection/${encodeURIComponent(
item.name)}`
}
this.showProxyList = true
},
createSub (){
this.$router.push('/sub-edit/UNTITLED')
},
createCol (){
this.$router.push('/collection-edit/UNTITLED')
},
async refreshProxyList (){
try{
await this.$refs.proxyList.refresh()
this.$store.commit('SET_SUCCESS_MESSAGE', '刷新成功!')
} catch (err){
this.$store.commit('SET_ERROR_MESSAGE', err.response.data.message)
}
},
getIconClass (url){
return url.indexOf(
'#invert') !== - 1 && !this.$vuetify.theme.dark ? 'invert' : ''
}
}
}
</script>
<style scoped>
.invert {
filter : invert(100%);
}
.v-dialog > .v-card > .v-toolbar {
position : sticky;
top : 0;
z-index : 999;
}
</style>

View File

@ -1,170 +0,0 @@
<template>
<v-container fluid>
<v-card>
<v-card-title>
<v-icon left>mdi-cloud</v-icon>
数据同步
<v-spacer></v-spacer>
<v-btn icon @click="openGist()">
<v-icon color="primary" small>visibility</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
最近同步于{{ syncTime }}
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn :loading="status.uploading"
class="ma-2 white--text"
color="blue-grey"
small
@click="sync('upload')">
上传
<v-icon right>
mdi-cloud-upload
</v-icon>
</v-btn>
<v-btn :loading="status.downloading"
class="ma-2 white--text"
color="blue-grey"
small
@click="sync('download')">
恢复
<v-icon right>
mdi-cloud-download
</v-icon>
</v-btn>
</v-card-actions>
</v-card>
<v-card>
<v-card-title>
<v-icon left>settings</v-icon>
设置
<v-spacer></v-spacer>
</v-card-title>
<v-card-text>
<v-list dense>
<v-subheader>GitHub配置</v-subheader>
<v-list-item>
<v-col>
<v-row>
<v-text-field
v-model="settings.githubUser"
clear-icon="clear"
clearable
hint="填入GitHub用户名" label="GitHub 用户名"
/>
</v-row>
<v-row>
<v-text-field
v-model="settings.gistToken"
clear-icon="clear"
clearable
hint="填入GitHub Token" label="GitHub Token"
/>
</v-row>
</v-col>
</v-list-item>
</v-list>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" small text @click="save()">保存</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script>
import {axios, showError} from "@/utils";
import {format} from "timeago.js";
export default {
data() {
return {
status: {
uploading: false,
downloading: false
}
}
},
computed: {
settings: {
get() {
return this.$store.state.settings;
},
set(value) {
this.$store.state.settings = value;
}
},
syncTime() {
if (this.settings.syncTime) {
return format(this.settings.syncTime, "zh_CN");
} else {
return "从未同步";
}
}
},
methods: {
async save() {
await axios.patch(`/settings`, this.settings);
await this.$store.dispatch("FETCH_SETTINGS");
this.$store.commit("SET_SUCCESS_MESSAGE", `保存成功!`);
},
// eslint-disable-next-line no-unused-vars
async sync(action) {
const setLoading = (status) => {
if (action === 'upload') {
this.status.uploading = status;
} else if (action === 'download') {
this.status.downloading = status;
}
}
if (!this.settings.gistToken) {
this.$store.commit("SET_ERROR_MESSAGE", "未设置GitHub Token");
return;
}
setLoading(true);
axios.get(`/utils/backup?action=${action}`).then(resp => {
if (resp.data.status === 'success') {
this.$store.commit("SET_SUCCESS_MESSAGE", `${action === 'upload' ? "备份" : "还原"}成功!`);
this.updateStore(this.$store);
}
}).catch(err => {
this.$store.commit("SET_ERROR_MESSAGE", `备份失败!${err}`);
}).finally(() => {
setLoading(false);
});
},
updateStore(store) {
store.dispatch('FETCH_SUBSCRIPTIONS').catch(() => {
showError(`无法拉取订阅列表!`);
});
store.dispatch("FETCH_COLLECTIONS").catch(() => {
showError(`无法拉取组合订阅列表!`);
});
store.dispatch("FETCH_SETTINGS").catch(() => {
showError(`无法拉取设置!`);
});
store.dispatch("FETCH_ARTIFACTS").catch(() => {
showError(`无法拉取同步配置!`);
});
},
openGist() {
window.open(`https://gist.github.com${'/' + this.settings.githubUser || ''}`)
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,18 +0,0 @@
{
"name": "sub-store",
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build"
}
],
"routes": [
{
"src": "/(js|css|img)/.*",
"headers": { "cache-control": "max-age=31536000, immutable" }
},
{ "handle": "filesystem" },
{ "src": ".*", "dest": "/" }
]
}

View File

@ -1,7 +0,0 @@
module.exports = {
"transpileDependencies": [
"vuetify",
'vue-echarts',
'resize-detector'
]
}