mirror of
				https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
				synced 2025-10-31 08:51:04 +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,24 +27,27 @@ 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}`); | ||||
|         } | ||||
| @ -52,11 +55,11 @@ function isMatch(rule, proxy) { | ||||
| 
 | ||||
|     // 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": | ||||
|         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
	 Peng-YM
						Peng-YM