mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-13 05:08:59 +08:00
refactor: Add new frontend as submodule
This commit is contained in:
parent
bc58419bb1
commit
ffd219abfe
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "web"]
|
||||
path = web
|
||||
url = https://github.com/sub-store-org/Sub-Store-Front-End.git
|
6
backend/dist/cron-sync-artifacts.min.js
vendored
6
backend/dist/cron-sync-artifacts.min.js
vendored
File diff suppressed because one or more lines are too long
6
backend/dist/sub-store-parser.loon.min.js
vendored
6
backend/dist/sub-store-parser.loon.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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": {
|
||||
|
@ -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}`);
|
||||
|
6
backend/sub-store.min.js
vendored
6
backend/sub-store.min.js
vendored
File diff suppressed because one or more lines are too long
1
web
Submodule
1
web
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit b10b708c3420a1b4cdc71a7f1b845de701a2382b
|
25
web/.gitignore
vendored
25
web/.gitignore
vendored
@ -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
|
@ -1,3 +0,0 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
@ -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 # 多行属性的 HTML(HTML、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 代码需要一个支持 ES2017(Node.js 8+ 或现代浏览器)或下级编译的引擎。这还可以在 TypeScript 中的类型参数中启用尾随逗号(自 2018 年 1 月发布的 TypeScript 2.7 起支持)
|
||||
trailingComma : "es5"
|
||||
|
||||
# 例外配置覆盖
|
||||
overrides :
|
||||
- files :
|
||||
- "*.ts"
|
||||
- "*.tsx"
|
||||
options :
|
||||
semi : true
|
||||
arrowParens : "always"
|
@ -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/).
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
12298
web/package-lock.json
generated
12298
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
]
|
||||
}
|
@ -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 |
@ -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>
|
113
web/src/App.vue
113
web/src/App.vue
@ -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>
|
@ -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
@ -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;
|
||||
}
|
@ -1 +0,0 @@
|
||||
module.exports = __webpack_public_path__ + "img/lock.9ae20e99.jpg";
|
@ -1 +0,0 @@
|
||||
module.exports = __webpack_public_path__ + "img/login.d6d3bb09.jpg";
|
@ -1 +0,0 @@
|
||||
module.exports = __webpack_public_path__ + "img/logo.82b9c7a5.png";
|
@ -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 |
@ -1 +0,0 @@
|
||||
module.exports = __webpack_public_path__ + "img/pricing.f76b550f.jpg";
|
@ -1 +0,0 @@
|
||||
module.exports = __webpack_public_path__ + "img/register.85b37874.jpg";
|
@ -1 +0,0 @@
|
||||
module.exports = __webpack_public_path__ + "img/vuetify.31b0d032.svg";
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -1,9 +0,0 @@
|
||||
<script>
|
||||
import {VCard} from 'vuetify/lib'
|
||||
|
||||
export default {
|
||||
name: 'Card',
|
||||
|
||||
extends: VCard
|
||||
}
|
||||
</script>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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`;
|
@ -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
|
||||
})
|
@ -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": "ساحر"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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')
|
@ -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)
|
||||
})
|
@ -1,4 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import 'chartist/dist/chartist.min.css'
|
||||
|
||||
Vue.use(require('vue-chartist'))
|
@ -1,5 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
// import * as VeeValidate from 'vee-validate'
|
||||
import VeeValidate from 'vee-validate'
|
||||
|
||||
Vue.use(VeeValidate)
|
@ -1,4 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import VueWorldMap from 'vue-world-map'
|
||||
|
||||
Vue.component('v-world-map', VueWorldMap)
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
@ -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;
|
@ -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;
|
@ -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);
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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": "/" }
|
||||
]
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
"transpileDependencies": [
|
||||
"vuetify",
|
||||
'vue-echarts',
|
||||
'resize-detector'
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user