├── .gitignore ├── README.md ├── index.html ├── package.json ├── postcss.config.cjs ├── public ├── assets │ ├── audios │ │ ├── Popcorn.ogg │ │ ├── alert.mp3 │ │ ├── empty.mp3 │ │ ├── error_1.wav │ │ ├── lock.mp3 │ │ ├── success_1.wav │ │ └── unlock.mp3 │ ├── images │ │ ├── about-1.jpg │ │ ├── bg-dark.jpg │ │ ├── i7.jpeg │ │ ├── i9.jpeg │ │ ├── lae-fav.png │ │ └── logo-lae-2023.png │ ├── js │ │ ├── decoder │ │ │ ├── draco_decoder.js │ │ │ ├── draco_decoder.wasm │ │ │ └── draco_wasm_wrapper.js │ │ └── paint.js │ ├── lae-dark.png │ ├── lae-dark.svg │ ├── lae-white.png │ ├── lae-white.svg │ ├── lottie │ │ ├── Balloon.json │ │ ├── Birthday-cake.json │ │ ├── Cluster-Ready.json │ │ ├── Confetti-ball.json │ │ ├── Dizzy-face.json │ │ ├── Eyes.json │ │ ├── Flushed.json │ │ ├── Ghost.json │ │ ├── Glowing-star.json │ │ ├── Logo-dark.json │ │ ├── Logo-white.json │ │ ├── Party-popper.json │ │ ├── Partying-face.json │ │ ├── Scrunched-mouth.json │ │ ├── Thinking-face.json │ │ ├── Wave.json │ │ ├── lae-jump-black.json │ │ ├── lae-jump.json │ │ ├── lae-mouse-leave-2.json │ │ └── lae-mouse-leave.json │ └── models │ │ └── model.gltf ├── manifest.json └── scripts │ └── mefrp.sh ├── src ├── App.vue ├── components │ ├── LaeLogo.vue │ ├── Layout.vue │ ├── Loading.vue │ ├── LoginButton.vue │ ├── Lottie.vue │ ├── Maintenance.vue │ ├── Markdown │ │ └── Preview.vue │ ├── Menu.vue │ ├── Notifications.vue │ ├── SimpleMenuIcon.vue │ ├── Tasks.vue │ ├── Terminal.vue │ ├── WorkOrderStatus.vue │ ├── headers │ │ ├── Charge.vue │ │ ├── ClusterReady.vue │ │ ├── Header.vue │ │ ├── User.vue │ │ └── Username.vue │ ├── icons │ │ ├── HostMenuIcon.vue │ │ └── TextMenuIcon.vue │ └── menus │ │ └── IndexLayout.vue ├── config │ ├── api.js │ ├── app.js │ └── menus.js ├── main.js ├── plugins │ ├── audio.js │ ├── direct.js │ ├── echo.js │ ├── gateway.js │ ├── gateway.js.bak │ ├── http.js │ ├── httpInterceptors.js │ ├── lyric.js │ ├── menuOptions.js │ ├── persistedstate.js │ ├── router.js │ ├── site.js │ ├── spinner.js │ └── stores │ │ ├── app.js │ │ ├── gct.js │ │ ├── hosts.js │ │ ├── http.js │ │ ├── ips.js │ │ ├── navs.js │ │ ├── red-packets.js │ │ ├── tasks.js │ │ ├── tunnels.js │ │ └── user.js ├── style.css ├── utils │ ├── composables.js │ ├── layout.js │ └── route.js └── views │ ├── About.vue │ ├── Api.vue │ ├── Hosts.vue │ ├── Index.vue │ ├── Nav.vue │ ├── Stars.vue │ ├── Status.vue │ ├── errors │ ├── 401.vue │ ├── 404.vue │ ├── 500.vue │ └── Base.vue │ ├── forum │ ├── Announcements.vue │ ├── Partner.vue │ ├── Pinned.vue │ └── components │ │ └── Topic.vue │ ├── home │ ├── Portal.vue │ └── Portal1.vue │ ├── modules │ ├── Base.vue │ ├── forbidden-forest │ │ ├── Base.vue │ │ ├── Index.vue │ │ ├── My.vue │ │ └── Show.vue │ ├── gct │ │ ├── Base.vue │ │ ├── Create.vue │ │ ├── Create.vue.bak │ │ ├── Index.vue │ │ ├── Show.vue │ │ └── components │ │ │ ├── Containers.vue │ │ │ ├── MenuIcon.vue │ │ │ └── Terminal.vue │ ├── ip-manager │ │ ├── Base.vue │ │ ├── Create.vue │ │ ├── Forward.vue │ │ └── Index.vue │ ├── red-packets │ │ ├── Base.vue │ │ ├── Create.vue │ │ ├── History.vue │ │ ├── Index.vue │ │ └── Show.vue │ └── tunnels │ │ ├── Base.vue │ │ ├── Concat.vue │ │ ├── Create.vue │ │ ├── Downloads.vue │ │ ├── Index.vue │ │ ├── Show.vue │ │ ├── Sign.vue │ │ ├── Sponsor.vue │ │ ├── Status.vue │ │ └── components │ │ └── Tunnels.vue │ ├── users │ ├── Login.vue │ ├── QrCode.vue │ └── User.vue │ └── work-orders │ ├── Create.vue │ ├── Index.vue │ └── Show.vue ├── tailwind.config.cjs ├── vite.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.njsproj 2 | *.sln 3 | *.sw? 4 | ### VisualStudioCode template 5 | .vscode/* 6 | !.vscode/settings.json 7 | !.vscode/tasks.json 8 | !.vscode/launch.json 9 | !.vscode/extensions.json 10 | !.vscode/*.code-snippets 11 | 12 | # Local History for Visual Studio Code 13 | .history/ 14 | 15 | # Built Visual Studio Code Extensions 16 | *.vsix 17 | 18 | ### JetBrains template 19 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 20 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 21 | 22 | # User-specific stuff 23 | .idea/**/workspace.xml 24 | .idea/**/tasks.xml 25 | .idea/**/usage.statistics.xml 26 | .idea/**/dictionaries 27 | .idea/**/shelf 28 | 29 | # AWS User-specific 30 | .idea/**/aws.xml 31 | 32 | # Generated files 33 | .idea/**/contentModel.xml 34 | 35 | # Sensitive or high-churn files 36 | .idea/**/dataSources/ 37 | .idea/**/dataSources.ids 38 | .idea/**/dataSources.local.xml 39 | .idea/**/sqlDataSources.xml 40 | .idea/**/dynamic.xml 41 | .idea/**/uiDesigner.xml 42 | .idea/**/dbnavigator.xml 43 | 44 | # Gradle 45 | .idea/**/gradle.xml 46 | .idea/**/libraries 47 | 48 | # Gradle and Maven with auto-import 49 | # When using Gradle or Maven with auto-import, you should exclude module files, 50 | # since they will be recreated, and may cause churn. Uncomment if using 51 | # auto-import. 52 | # .idea/artifacts 53 | # .idea/compiler.xml 54 | # .idea/jarRepositories.xml 55 | # .idea/modules.xml 56 | # .idea/*.iml 57 | # .idea/modules 58 | # *.iml 59 | # *.ipr 60 | 61 | # CMake 62 | cmake-build-*/ 63 | 64 | # Mongo Explorer plugin 65 | .idea/**/mongoSettings.xml 66 | 67 | # File-based project format 68 | *.iws 69 | 70 | # IntelliJ 71 | out/ 72 | 73 | # mpeltonen/sbt-idea plugin 74 | .idea_modules/ 75 | 76 | # JIRA plugin 77 | atlassian-ide-plugin.xml 78 | 79 | # Cursive Clojure plugin 80 | .idea/replstate.xml 81 | 82 | # SonarLint plugin 83 | .idea/sonarlint/ 84 | 85 | # Crashlytics plugin (for Android Studio and IntelliJ) 86 | com_crashlytics_export_strings.xml 87 | crashlytics.properties 88 | crashlytics-build.properties 89 | fabric.properties 90 | 91 | # Editor-based Rest Client 92 | .idea/httpRequests 93 | 94 | # Android studio 3.1+ serialized cache file 95 | .idea/caches/build_file_checksums.ser 96 | 97 | ### Vue template 98 | # gitignore template for Vue.js projects 99 | # 100 | # Recommended template: Node.gitignore 101 | 102 | docs/_book 103 | 104 | test/ 105 | 106 | ### Node template 107 | # Logs 108 | logs 109 | *.log 110 | npm-debug.log* 111 | yarn-debug.log* 112 | yarn-error.log* 113 | lerna-debug.log* 114 | .pnpm-debug.log* 115 | 116 | # Diagnostic reports (https://nodejs.org/api/report.html) 117 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 118 | 119 | # Runtime data 120 | pids 121 | *.pid 122 | *.seed 123 | *.pid.lock 124 | 125 | # Directory for instrumented libs generated by jscoverage/JSCover 126 | lib-cov 127 | 128 | # Coverage directory used by tools like istanbul 129 | coverage 130 | *.lcov 131 | 132 | # nyc test coverage 133 | .nyc_output 134 | 135 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 136 | .grunt 137 | 138 | # Bower dependency directory (https://bower.io/) 139 | bower_components 140 | 141 | # node-waf configuration 142 | .lock-wscript 143 | 144 | # Compiled binary addons (https://nodejs.org/api/addons.html) 145 | build/Release 146 | 147 | # Dependency directories 148 | node_modules/ 149 | jspm_packages/ 150 | 151 | # Snowpack dependency directory (https://snowpack.dev/) 152 | web_modules/ 153 | 154 | # TypeScript cache 155 | *.tsbuildinfo 156 | 157 | # Optional npm cache directory 158 | .npm 159 | 160 | # Optional eslint cache 161 | .eslintcache 162 | 163 | # Optional stylelint cache 164 | .stylelintcache 165 | 166 | # Microbundle cache 167 | .rpt2_cache/ 168 | .rts2_cache_cjs/ 169 | .rts2_cache_es/ 170 | .rts2_cache_umd/ 171 | 172 | # Optional REPL history 173 | .node_repl_history 174 | 175 | # Output of 'npm pack' 176 | *.tgz 177 | 178 | # Yarn Integrity file 179 | .yarn-integrity 180 | 181 | # dotenv environment variable files 182 | .env 183 | .env.development.local 184 | .env.test.local 185 | .env.production.local 186 | .env.local 187 | 188 | # parcel-bundler cache (https://parceljs.org/) 189 | .cache 190 | .parcel-cache 191 | 192 | # Next.js build output 193 | .next 194 | out 195 | 196 | # Nuxt.js build / generate output 197 | .nuxt 198 | dist 199 | 200 | # Gatsby files 201 | .cache/ 202 | # Comment in the public line in if your project uses Gatsby and not Next.js 203 | # https://nextjs.org/blog/next-9-1#public-directory-support 204 | # public 205 | 206 | # vuepress build output 207 | .vuepress/dist 208 | 209 | # vuepress v2.x temp and cache directory 210 | .temp 211 | .cache 212 | 213 | # Docusaurus cache and generated files 214 | .docusaurus 215 | 216 | # Serverless directories 217 | .serverless/ 218 | 219 | # FuseBox cache 220 | .fusebox/ 221 | 222 | # DynamoDB Local files 223 | .dynamodb/ 224 | 225 | # TernJS port file 226 | .tern-port 227 | 228 | # Stores VSCode versions used for testing VSCode extensions 229 | .vscode-test 230 | 231 | # yarn v2 232 | .yarn/cache 233 | .yarn/unplugged 234 | .yarn/build-state.yml 235 | .yarn/install-state.gz 236 | .pnp.* 237 | 238 | ### Windows template 239 | # Windows thumbnail cache files 240 | Thumbs.db 241 | Thumbs.db:encryptable 242 | ehthumbs.db 243 | ehthumbs_vista.db 244 | 245 | # Dump file 246 | *.stackdump 247 | 248 | # Folder config file 249 | [Dd]esktop.ini 250 | 251 | # Recycle Bin used on file shares 252 | $RECYCLE.BIN/ 253 | 254 | # Windows Installer files 255 | *.cab 256 | *.msi 257 | *.msix 258 | *.msm 259 | *.msp 260 | 261 | # Windows shortcuts 262 | *.lnk 263 | 264 | ### macOS template 265 | # General 266 | .DS_Store 267 | .AppleDouble 268 | .LSOverride 269 | 270 | # Icon must end with two \r 271 | Icon 272 | 273 | # Thumbnails 274 | ._* 275 | 276 | # Files that might appear in the root of a volume 277 | .DocumentRevisions-V100 278 | .fseventsd 279 | .Spotlight-V100 280 | .TemporaryItems 281 | .Trashes 282 | .VolumeIcon.icns 283 | .com.apple.timemachine.donotpresent 284 | 285 | # Directories potentially created on remote AFP share 286 | .AppleDB 287 | .AppleDesktop 288 | Network Trash Folder 289 | Temporary Items 290 | .apdisk 291 | 292 | .vscode/ 293 | .idea/ 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 30 | 41 | 42 | 莱云 43 | 44 | 45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lae-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@kangc/v-md-editor": "^2.3.15", 13 | "@vicons/ionicons5": "^0.12.0", 14 | "@vicons/material": "^0.12.0", 15 | "autoprefixer": "^10.4.13", 16 | "axios": "^1.2.2", 17 | "echarts": "^5.4.1", 18 | "highlight.js": "^11.7.0", 19 | "humanize-plus": "^1.8.2", 20 | "js-md5": "^0.7.3", 21 | "laravel-echo": "^1.14.0", 22 | "lottie-web": "^5.10.1", 23 | "naive-ui": "^2.34.3", 24 | "postcss": "^8.4.20", 25 | "prismjs": "^1.29.0", 26 | "pusher-js": "^7.4.0", 27 | "qrcode-vue3": "^1.4.17", 28 | "tailwindcss": "^3.2.4", 29 | "three": "^0.149.0", 30 | "ts-node": "^10.9.1", 31 | "vfonts": "^0.0.3", 32 | "vooks": "^0.2.12", 33 | "vue": "^3.2.45", 34 | "vue-axios": "^3.5.2", 35 | "vue-router": "^4.0.13", 36 | "vuex": "^4.0.2", 37 | "xterm": "^5.1.0", 38 | "xterm-addon-fit": "^0.7.0", 39 | "xterm-addon-search": "^0.11.0", 40 | "xterm-addon-search-bar": "^0.2.0" 41 | }, 42 | "devDependencies": { 43 | "@types/axios": "^0.14.0", 44 | "@types/vue": "^2.0.0", 45 | "@types/vue-router": "^2.0.0", 46 | "@types/xterm": "^3.0.0", 47 | "@vitejs/plugin-vue": "^4.0.0", 48 | "vite": "^4.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/assets/audios/Popcorn.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/audios/Popcorn.ogg -------------------------------------------------------------------------------- /public/assets/audios/alert.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/audios/alert.mp3 -------------------------------------------------------------------------------- /public/assets/audios/empty.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/audios/empty.mp3 -------------------------------------------------------------------------------- /public/assets/audios/error_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/audios/error_1.wav -------------------------------------------------------------------------------- /public/assets/audios/lock.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/audios/lock.mp3 -------------------------------------------------------------------------------- /public/assets/audios/success_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/audios/success_1.wav -------------------------------------------------------------------------------- /public/assets/audios/unlock.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/audios/unlock.mp3 -------------------------------------------------------------------------------- /public/assets/images/about-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/images/about-1.jpg -------------------------------------------------------------------------------- /public/assets/images/bg-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/images/bg-dark.jpg -------------------------------------------------------------------------------- /public/assets/images/i7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/images/i7.jpeg -------------------------------------------------------------------------------- /public/assets/images/i9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/images/i9.jpeg -------------------------------------------------------------------------------- /public/assets/images/lae-fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/images/lae-fav.png -------------------------------------------------------------------------------- /public/assets/images/logo-lae-2023.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/images/logo-lae-2023.png -------------------------------------------------------------------------------- /public/assets/js/decoder/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/js/decoder/draco_decoder.wasm -------------------------------------------------------------------------------- /public/assets/js/paint.js: -------------------------------------------------------------------------------- 1 | registerPaint( 2 | 'smooth-corners', 3 | class { 4 | static get inputProperties() { 5 | return ['--smooth-corners']; 6 | } 7 | superellipse(t, e, s = 4, r) { 8 | Number.isNaN(s) && (s = 4), 9 | (void 0 === r || Number.isNaN(r)) && (r = s), 10 | s > 100 && (s = 100), 11 | r > 100 && (r = 100), 12 | s < 1e-11 && (s = 1e-11), 13 | r < 1e-11 && (r = 1e-11); 14 | const o = 2 / s, 15 | a = r ? 2 / r : o, 16 | i = (2 * Math.PI) / 360; 17 | return Array.from({ length: 360 }, (s, r) => 18 | ((s) => { 19 | const r = Math.cos(s), 20 | i = Math.sin(s); 21 | return { 22 | x: Math.abs(r) ** o * t * Math.sign(r), 23 | y: Math.abs(i) ** a * e * Math.sign(i), 24 | }; 25 | })(r * i) 26 | ); 27 | } 28 | paint(t, e, s) { 29 | const [r, o] = s 30 | .get('--smooth-corners') 31 | .toString() 32 | .replace(/ /g, '') 33 | .split(','), 34 | a = e.width / 2, 35 | i = e.height / 2, 36 | n = this.superellipse(a, i, parseFloat(r, 10), parseFloat(o, 10)); 37 | (t.fillStyle = '#000'), t.setTransform(1, 0, 0, 1, a, i), t.beginPath(); 38 | for (let e = 0; e < n.length; e++) { 39 | const { x: s, y: r } = n[e]; 40 | 0 === e ? t.moveTo(s, r) : t.lineTo(s, r); 41 | } 42 | t.closePath(), t.fill(); 43 | } 44 | } 45 | ); 46 | -------------------------------------------------------------------------------- /public/assets/lae-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/lae-dark.png -------------------------------------------------------------------------------- /public/assets/lae-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/assets/lae-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVampireSP/lae-ui/5dbf6e28aaea753ba9bcbb84f7d9632de3d7f4ce/public/assets/lae-white.png -------------------------------------------------------------------------------- /public/assets/lae-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "莱云 控制面板", 3 | "short_name" : "LAE Dashboard", 4 | "description" : "莱云 镜缘映射 游戏容器 CDN 等服务的控制面板", 5 | "start_url" : "/", 6 | "display" : "standalone", 7 | "background_color" : "#F0F0F0", 8 | "theme_color" : "#F0F0F0", 9 | "orientation" : "any", 10 | "icons": [ 11 | { 12 | "src" : "/assets/lae-dark.png", 13 | "type" : "image/png", 14 | "sizes" : "320x192" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /public/scripts/mefrp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | check_root() { 4 | if [ $EUID -ne 0 ]; then 5 | echo "请使用 root 权限运行" 6 | # exit 1 7 | fi 8 | 9 | } 10 | 11 | install_frpc() { 12 | # 获取 CPU 架构 13 | arch=$(uname -m) 14 | 15 | # 获取内核 16 | kernel=$(uname -s) 17 | # kernel 转换为小写 18 | # shellcheck disable=SC2021 19 | kernel=$(echo "$kernel" | tr '[A-Z]' '[a-z]') 20 | 21 | # if arch is x86_64, then arch is amd64 22 | if [ "$arch" == "x86_64" ]; then 23 | arch="amd64" 24 | fi 25 | 26 | 27 | file_name="frp_MirrorEdgeFrp_0.46.1_beta_${kernel}_$arch" 28 | 29 | link="https://r2.laecloud.com/MEFrpRelease/$file_name.tar.gz" 30 | 31 | # 下载 frp 32 | wget -O /tmp/frp.tar.gz "$link" 33 | 34 | # 解压 frp,解压后的文件夹名字是 frpc 35 | tar -zxvf /tmp/frp.tar.gz -C /tmp 36 | 37 | # 将 frpc 复制到 /usr/bin/frpc 38 | cp /tmp/"$file_name"/frpc /usr/bin/frpc 39 | # cp /tmp/$file_name/frpc ./frpc 40 | 41 | rm -rf /tmp/"$file_name" 42 | } 43 | 44 | check_root 45 | 46 | # 检测 /usr/bin/frpc 是否存在 47 | if [ ! -f /usr/bin/frpc ]; then 48 | echo "frpc 未安装" 49 | install_frpc 50 | fi 51 | 52 | # 获取参数 53 | token=$1 54 | id=$2 55 | 56 | # 检测参数是否为空 57 | if [ -z "$token" ] || [ -z "$id" ]; then 58 | echo "参数错误" 59 | exit 1 60 | fi 61 | 62 | # 将 token 放到 /etc/laecloud/token 文件中 63 | 64 | if [ ! -d /etc/laecloud ]; then 65 | mkdir /etc/laecloud 66 | fi 67 | 68 | echo "$token" > /etc/laecloud/token 69 | 70 | # systemd 71 | cat > /etc/systemd/system/frpc@.service << EOF 72 | [Unit] 73 | Description=LAE Frp Client Service of %i 74 | After=network.target 75 | 76 | 77 | [Service] 78 | Type=simple 79 | User=nobody 80 | Restart=on-failure 81 | RestartSec=5s 82 | ExecStart=/bin/bash -c '/usr/bin/frpc -t \"$(cat /etc/laecloud/token)\" -i %i' 83 | LimitNOFILE=1048576 84 | 85 | [Install] 86 | WantedBy=multi-user.target 87 | EOF 88 | 89 | systemctl daemon-reload 90 | systemctl enable --now frpc@"$id" 91 | 92 | echo "安装完成,如果需要停止这一条隧道,请运行:" 93 | echo "systemctl disable --now frpc@$id" -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/LaeLogo.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 38 | -------------------------------------------------------------------------------- /src/components/Layout.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 73 | -------------------------------------------------------------------------------- /src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/LoginButton.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/Lottie.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 94 | 95 | -------------------------------------------------------------------------------- /src/components/Maintenance.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 64 | -------------------------------------------------------------------------------- /src/components/Markdown/Preview.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Notifications.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 158 | -------------------------------------------------------------------------------- /src/components/SimpleMenuIcon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/Tasks.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/Terminal.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 128 | 129 | -------------------------------------------------------------------------------- /src/components/WorkOrderStatus.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/headers/Charge.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 113 | -------------------------------------------------------------------------------- /src/components/headers/ClusterReady.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 44 | -------------------------------------------------------------------------------- /src/components/headers/Header.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 97 | 98 | 103 | -------------------------------------------------------------------------------- /src/components/headers/User.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /src/components/headers/Username.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 73 | 74 | -------------------------------------------------------------------------------- /src/components/icons/HostMenuIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 56 | 57 | -------------------------------------------------------------------------------- /src/components/icons/TextMenuIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/menus/IndexLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 42 | -------------------------------------------------------------------------------- /src/config/api.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | dev: { 3 | // api: 'http://www.lae.test/api/', 4 | // auth: 'http://www.lae.test', 5 | 6 | api: 'http://www.lae.test/api/', 7 | auth: 'http://www.lae.test', 8 | 9 | ws_gateway: 'ws://www.lae.test:8282', 10 | // gateway: 'http://www.lae.test:3000/', 11 | gateway: 'http://www.lae.test/api/modules', 12 | 13 | ws_host: 'www.lae.test', 14 | ws_port: 6001, 15 | ws_auth_endpoint: 'http://www.lae.test/broadcasting/auth', 16 | pusher_key: 'app-key', 17 | 18 | avatar: 'https://cravatar.cn/avatar/', 19 | 20 | status: 'https://api.laecloud.com/ngx_status', 21 | 22 | modules: { 23 | gct: { 24 | panel: 'http://192.168.81.107', 25 | }, 26 | }, 27 | }, 28 | prod: { 29 | api: 'https://api.laecloud.com/api/', 30 | auth: 'https://api.laecloud.com', 31 | 32 | ws_gateway: 'wss://ws.gateway.laecloud.com', 33 | // gateway: 'https://gateway.laecloud.com/', 34 | gateway: 'https://api.laecloud.com/api/modules', 35 | 36 | ws_host: 'socket.lae.yistars.net', 37 | ws_port: 443, 38 | ws_auth_endpoint: 'https://api.laecloud.com/broadcasting/auth', 39 | pusher_key: 'Q6SEgerhsgMVz', 40 | 41 | avatar: 'https://cravatar.cn/avatar/', 42 | 43 | status: 'https://api.laecloud.com/ngx_status', 44 | 45 | modules: { 46 | gct: { 47 | panel: 'https://ptero.laecloud.com', 48 | }, 49 | }, 50 | }, 51 | } 52 | 53 | let current = config.dev; 54 | 55 | if (process.env.NODE_ENV === 'production') { 56 | current = config.prod; 57 | } 58 | 59 | console.log('api endpoint: ' + current.api); 60 | 61 | export default current; -------------------------------------------------------------------------------- /src/config/app.js: -------------------------------------------------------------------------------- 1 | import {ref} from "vue"; 2 | 3 | export default { 4 | name: "莱云", 5 | 6 | // isLogin: false, 7 | 8 | loading: ref(false), 9 | } -------------------------------------------------------------------------------- /src/config/menus.js: -------------------------------------------------------------------------------- 1 | import {GameControllerOutline} from "@vicons/ionicons5"; 2 | import {addMenuOptions} from "../plugins/menuOptions"; 3 | 4 | 5 | const modules = [ 6 | { 7 | id: "gct", 8 | name: "游戏容器", 9 | route: "modules.gct.index", 10 | icon: GameControllerOutline 11 | }, 12 | // { 13 | // id: "ip-manager", 14 | // name: "IP 管理", 15 | // route: "modules.ip-manager.index", 16 | // icon: GameControllerOutline 17 | // }, 18 | // { 19 | // id: "user-mqtt", 20 | // name: "消息队列", 21 | // route: "modules.user-mqtt" 22 | // } 23 | // { 24 | // id: "forbidden-forest", 25 | // name: "禁林", 26 | // route: "modules.forbidden-forest.index", 27 | // icon: ArticleOutlined 28 | // }, 29 | ] 30 | 31 | for (let i = 0; i < modules.length; i++) { 32 | addMenuOptions('top', modules[i].route, modules[i].name, modules[i].icon); 33 | } 34 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | 3 | import axios from 'axios'; 4 | import VueAxios from 'vue-axios'; 5 | 6 | import UserStore from './plugins/stores/user' 7 | import HttpStore from './plugins/stores/user' 8 | import router from './plugins/router'; 9 | 10 | import './plugins/audio'; 11 | import './plugins/gateway'; 12 | 13 | import App from './App.vue' 14 | 15 | /** Styles **/ 16 | import './style.css' 17 | // 通用字体 18 | import 'vfonts/Lato.css' 19 | // 等宽字体 20 | import 'vfonts/FiraCode.css' 21 | 22 | // 注册菜单项目 23 | import './config/menus' 24 | 25 | 26 | // 解决样式冲突 27 | const meta = document.createElement('meta') 28 | meta.name = 'naive-ui-style' 29 | document.head.appendChild(meta) 30 | 31 | const app = createApp(App) 32 | 33 | app.use(UserStore, HttpStore) 34 | app.use(router, axios, VueAxios) 35 | 36 | 37 | app.mount("#app"); 38 | -------------------------------------------------------------------------------- /src/plugins/audio.js: -------------------------------------------------------------------------------- 1 | // 不触发一次点击事件是没办法自动播放背景音乐的 2 | // 不过目前看来就先这样吧 3 | 4 | let firstClick = false 5 | 6 | const play = (name, volume = 0.5) => { 7 | if (firstClick) { 8 | let filename = '/assets/audios/' + name 9 | let audio = new Audio(filename) 10 | audio.volume = volume 11 | audio.load() 12 | audio.play().then(r => { 13 | console.log('play audio: ' + name, r) 14 | 15 | }).catch(e => { 16 | console.log('play audio error: ' + name, e) 17 | }) 18 | } 19 | } 20 | 21 | window.addEventListener('click', () => { 22 | // let isSafari = /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor); 23 | // if (!isSafari) { 24 | // 25 | // } 26 | 27 | if (!firstClick) { 28 | firstClick = true 29 | 30 | play('empty.mp3', 0) 31 | play('Popcorn.ogg', 0) 32 | } 33 | }) 34 | 35 | export default play -------------------------------------------------------------------------------- /src/plugins/direct.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import {request, response} from './httpInterceptors' 4 | 5 | // axios.defaults.withCredentials = true; 6 | 7 | // 实例 8 | let instance = axios.create({ 9 | timeout: 10000, 10 | }) 11 | 12 | instance.interceptors.request.use(request.onFulfilled, request.onRejected) 13 | 14 | instance.interceptors.response.use(response.onFulfilled, response.onRejected) 15 | 16 | export default instance 17 | -------------------------------------------------------------------------------- /src/plugins/echo.js: -------------------------------------------------------------------------------- 1 | import PusherJS from 'pusher-js' 2 | import LaravelEcho from 'laravel-echo' 3 | 4 | import userStore from './stores/user' 5 | 6 | import api from '../config/api.js' 7 | 8 | PusherJS.logToConsole = false 9 | 10 | let echo = new LaravelEcho({ 11 | broadcaster: 'pusher', 12 | key: api.pusher_key, 13 | wsHost: api.ws_host, 14 | wsPort: api.ws_port, 15 | wssPort: api.ws_port, 16 | forceTLS: false, 17 | encrypted: true, 18 | disableStats: true, 19 | enableLogging: true, 20 | enabledTransports: ['ws', 'wss'], 21 | 22 | authEndpoint: api.ws_auth_endpoint, 23 | auth: { 24 | headers: { 25 | Accept: 'application/json', 26 | Authorization: 'Bearer ' + userStore.state.token, 27 | }, 28 | }, 29 | }) 30 | 31 | let events = [] 32 | 33 | echo.private(`users.${userStore.state.user.id}`).listen('.messages', (e) => { 34 | console.log(e) 35 | 36 | let event = e['event'] 37 | 38 | for (let i = 0; i < events.length; i++) { 39 | if (events[i].event === event) { 40 | events[i].callback(e) 41 | } 42 | } 43 | 44 | 45 | }) 46 | 47 | const listen = (event, callback) => { 48 | events.push({event, callback}) 49 | } 50 | 51 | // 取消监听 52 | const leave = (event, callback) => { 53 | // 根据事件名和回调函数删除 54 | events = events.filter((item) => { 55 | return item.event !== event && item.callback !== callback 56 | }) 57 | } 58 | 59 | 60 | export { 61 | echo, 62 | listen, 63 | leave, 64 | } 65 | -------------------------------------------------------------------------------- /src/plugins/gateway.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import api from '../config/api.js' 4 | import {request, response} from "./httpInterceptors.js"; 5 | import app from '../config/app' 6 | 7 | const baseURL = api.gateway 8 | // axios.defaults.withCredentials = true; 9 | 10 | // 实例 11 | let instance = axios.create({ 12 | baseURL: baseURL, 13 | timeout: 10000, 14 | }) 15 | 16 | const requestOnFulfilled = (config) => { 17 | // app.loading.value = true 18 | return request.onFulfilled(config) 19 | } 20 | 21 | const requestOnRejected = (config) => { 22 | // app.loading.value = true 23 | return request.onRejected(config) 24 | } 25 | 26 | const responseOnFulfilled = (res) => { 27 | // app.loading.value = false 28 | return response.onFulfilled(res) 29 | } 30 | 31 | const responseOnRejected = (error) => { 32 | // app.loading.value = false 33 | return response.onRejected(error) 34 | } 35 | 36 | instance.interceptors.request.use(requestOnFulfilled, requestOnRejected) 37 | 38 | instance.interceptors.response.use(responseOnFulfilled, responseOnRejected) 39 | export default instance 40 | -------------------------------------------------------------------------------- /src/plugins/gateway.js.bak: -------------------------------------------------------------------------------- 1 | import user from "./stores/user.js"; 2 | import {dialog} from "../utils/layout.js"; 3 | import api from "../config/api.js"; 4 | import {ref} from "vue"; 5 | 6 | let requests = [] 7 | 8 | let ws 9 | 10 | let authed = false 11 | const closed = ref(false) 12 | 13 | // 创建 ws,增加 header 14 | if (user.state.token) { 15 | ws = new WebSocket(api.ws_gateway + '?token=' + user.state.token); 16 | 17 | // ws.onclose = function () { 18 | // dialog.warning({ 19 | // title: '网关连接关闭', 20 | // content: '请尝试刷新页面。', 21 | // positiveText: '好吧' 22 | // }) 23 | // 24 | // closed.value = true 25 | // }; 26 | ws.onclose = function () { 27 | dialog.warning({ 28 | title: '无法连接到网关', 29 | content: '请联系我们获得帮助,或者尝试刷新页面。', 30 | positiveText: '好' 31 | }) 32 | 33 | closed.value = true 34 | }; 35 | 36 | ws.onmessage = function (event) { 37 | const data = JSON.parse(event.data) 38 | 39 | if (data['code'] === 200 || data['msg'] === 'authed') { 40 | authed = true 41 | } 42 | 43 | // if dev, show message 44 | if (process.env.NODE_ENV === 'development') { 45 | console.log('gateway', data) 46 | } 47 | 48 | if (data['data']) { 49 | // if it has request_id, then it's a response 50 | if (data['request_id']) { 51 | const request = requests.find(request => request.request_id === data['request_id']) 52 | 53 | if (data.code !== 200 && data.data.message) { 54 | dialog.error({ 55 | title: '错误', 56 | content: data['data']['message'], 57 | }) 58 | } 59 | 60 | if (request) { 61 | request.callback(data) 62 | 63 | // remove request 64 | requests = requests.filter(request => request.request_id !== data['request_id']) 65 | } 66 | } 67 | } 68 | } 69 | 70 | } 71 | 72 | const wsSend = function (module_id, method, path, data = [], callback = null) { 73 | if (user.state.token) { 74 | 75 | if (authed) { 76 | // 生成 request_id 77 | const request_id = Math.random().toString(36).substring(2, 9) 78 | 79 | const payload = { 80 | request_id: request_id, 81 | module_id: module_id, 82 | method: method, 83 | path: path, 84 | data: data 85 | } 86 | 87 | ws.send(JSON.stringify(payload)) 88 | 89 | if (callback) { 90 | requests.push({ 91 | request_id: request_id, 92 | callback: callback 93 | }) 94 | } 95 | } else { 96 | if (!close) { 97 | console.warn('正在重试请求') 98 | setTimeout(function () { 99 | wsSend(module_id, method, path, data, callback) 100 | }, 500) 101 | } 102 | } 103 | 104 | 105 | } 106 | } 107 | 108 | 109 | const get = function (module_id, url, data) { 110 | return new Promise((resolve, reject) => { 111 | wsSend(module_id, 'GET', url, data, (data) => { 112 | if (data.code >= 200 && data.code < 300) { 113 | resolve(data) 114 | } else { 115 | reject(data) 116 | } 117 | }) 118 | }) 119 | } 120 | 121 | const post = function (module_id, url, data) { 122 | return new Promise((resolve, reject) => { 123 | wsSend(module_id, 'POST', url, data, (data) => { 124 | if (data.code >= 200 && data.code < 300) { 125 | resolve(data) 126 | } else { 127 | reject(data) 128 | } 129 | }) 130 | }) 131 | } 132 | 133 | const put = function (module_id, url, data) { 134 | return new Promise((resolve, reject) => { 135 | wsSend(module_id, 'PUT', url, data, (data) => { 136 | if (data.code >= 200 && data.code < 300) { 137 | resolve(data) 138 | } else { 139 | reject(data) 140 | } 141 | }) 142 | }) 143 | } 144 | 145 | const patch = function (module_id, url, data) { 146 | return new Promise((resolve, reject) => { 147 | wsSend(module_id, 'PATCH', url, data, (data) => { 148 | if (data.code >= 200 && data.code < 300) { 149 | resolve(data) 150 | } else { 151 | reject(data) 152 | } 153 | }) 154 | }) 155 | } 156 | 157 | const del = function (module_id, url, data) { 158 | return new Promise((resolve, reject) => { 159 | wsSend(module_id, 'DELETE', url, data, (data) => { 160 | if (data.code >= 200 && data.code < 300) { 161 | resolve(data) 162 | } else { 163 | reject(data) 164 | } 165 | }) 166 | }) 167 | } 168 | 169 | export default { 170 | get: get, 171 | post: post, 172 | put: put, 173 | patch: patch, 174 | delete: del, 175 | closed: closed 176 | } -------------------------------------------------------------------------------- /src/plugins/http.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {request, response} from "./httpInterceptors"; 3 | import api from '../config/api' 4 | 5 | 6 | const baseURL = api.api 7 | // axios.defaults.withCredentials = true; 8 | 9 | // 实例 10 | let instance = axios.create({ 11 | baseURL: baseURL, 12 | timeout: 10000, 13 | }) 14 | 15 | instance.interceptors.request.use(request.onFulfilled, request.onRejected) 16 | 17 | instance.interceptors.response.use(response.onFulfilled, response.onRejected) 18 | export default instance 19 | -------------------------------------------------------------------------------- /src/plugins/httpInterceptors.js: -------------------------------------------------------------------------------- 1 | import user from './stores/user' 2 | // import {dialog, loadingBar} from "../utils/layout"; 3 | import {dialog} from '../utils/layout' 4 | // import {dialog} from "../utils/layout"; 5 | import http from './stores/http' 6 | import router from './router' 7 | import {h} from 'vue' 8 | // import loadingBar from './spinner' 9 | import error401 from '../views/errors/401.vue' 10 | import error404 from '../views/errors/404.vue' 11 | import error500 from '../views/errors/500.vue' 12 | 13 | 14 | const request = { 15 | onFulfilled: (config) => { 16 | if (config.headers === undefined) { 17 | config.headers = {} 18 | } 19 | 20 | config.headers['Accept'] = 'application/json' 21 | config.headers['Authorization'] = 'Bearer ' + user.state.token 22 | 23 | // loadingBar.start() 24 | 25 | return Promise.resolve(config) 26 | }, 27 | onRejected: (error) => { 28 | console.error(error) 29 | 30 | // if timeout, retry 31 | // if (error.code === 'ECONNABORTED') { 32 | // return request.onFulfilled(error.config) 33 | // } 34 | 35 | // loadingBar.error() 36 | 37 | return Promise.reject(error) 38 | }, 39 | } 40 | 41 | const response = { 42 | onFulfilled: (res) => { 43 | // if 20x 44 | // if (res.status >= 200 && res.status < 300) { 45 | // // loadingBar.finish() 46 | // } else if (res.status >= 400 && res.status < 600) { 47 | // // loadingBar.error() 48 | // } 49 | 50 | return Promise.resolve(res) 51 | }, 52 | onRejected: (error) => { 53 | // loadingBar.error() 54 | 55 | console.error('axios error', error) 56 | 57 | let data = [] 58 | 59 | if (error.response.data.data) { 60 | data = error.response.data.data 61 | } 62 | 63 | if (error.response.data.message) { 64 | data = error.response.data.message 65 | } 66 | 67 | if (error.response.data.error) { 68 | data = error.response.data.error.message 69 | } 70 | 71 | if (error.response.status === 429) { 72 | if (!http.state.isAlertedTooManyRequests) { 73 | dialog.warning({ 74 | title: '太频繁啦', 75 | content: '休息一会吧,服务器也累了。', 76 | positiveText: '好吧', 77 | onPositiveClick: () => { 78 | http.state.isAlertedTooManyRequests = false 79 | }, 80 | }) 81 | http.isAlertedTooManyRequests = true 82 | } 83 | } else if (error.response.status === 401) { 84 | if (router.currentRoute.value.name !== 'auth.login') { 85 | if (!http.state.isAlertedToken) { 86 | // 87 | // dialog.error({ 88 | // title: '提示', 89 | // content: 'Token 鉴权失败,请重新登录。', 90 | // positiveText: '好', 91 | // onPositiveClick: () => { 92 | // router.push({name: 'auth.login'}); 93 | // }, 94 | // 95 | // }) 96 | 97 | dialog.error({ 98 | title: '访问被禁止', 99 | content: () => { 100 | return h(error401) 101 | }, 102 | positiveText: '重新登录', 103 | negativeText: '先这样', 104 | onPositiveClick: () => { 105 | router.push({name: 'auth.login'}) 106 | }, 107 | }) 108 | 109 | http.state.isAlertedToken = true 110 | } 111 | } 112 | } else if (error.response.status === 404) { 113 | dialog.error({ 114 | title: '404 未找到', 115 | content: () => { 116 | return h(error404, { 117 | show_footer: false, 118 | }) 119 | }, 120 | }) 121 | } else if (error.response.status === 500) { 122 | dialog.error({ 123 | title: '500 服务器错误', 124 | content: () => { 125 | return h(error500) 126 | }, 127 | }) 128 | } else { 129 | if (data.length !== 0) { 130 | dialog.error({ 131 | title: '错误', 132 | content: data, 133 | }) 134 | } 135 | } 136 | 137 | return Promise.reject(error) 138 | }, 139 | } 140 | 141 | export {request, response} 142 | -------------------------------------------------------------------------------- /src/plugins/lyric.js: -------------------------------------------------------------------------------- 1 | const lyrics = [ 2 | '正在点咖啡', 3 | '正在制作卡布奇诺', 4 | '正在拉花', 5 | 'Make LAE great again!', 6 | '正在制作拿铁', 7 | '正在制作摩卡', 8 | '正在回忆童年', 9 | 10 | 11 | // 哈哈 12 | '!!我为什么会在加载条下面!!', 13 | ] 14 | 15 | 16 | // 导出函数 17 | export default function () { 18 | // 随机 lyrics 中的内容,如果没有就返回 '' 19 | return lyrics[Math.floor(Math.random() * lyrics.length)] || '' 20 | } -------------------------------------------------------------------------------- /src/plugins/menuOptions.js: -------------------------------------------------------------------------------- 1 | import {h, ref} from "vue"; 2 | import {RouterLink} from "vue-router"; 3 | import {NIcon} from 'naive-ui' 4 | import router from "./router.js"; 5 | import LaeLogo from "../components/LaeLogo.vue"; 6 | import SimpleMenuIcon from "../components/SimpleMenuIcon.vue"; 7 | 8 | // function renderIcon(icon) { 9 | // return () => h(NIcon, null, {default: () => h(icon)}); 10 | // } 11 | 12 | const selectedKey = ref(""); 13 | const menuInst = ref(null); 14 | const selectAndExpand = (route) => { 15 | if (route.params) { 16 | selectedKey.value = route.name + JSON.stringify(route.params); 17 | } else { 18 | selectedKey.value = route.name; 19 | } 20 | menuInst.value?.showOption(selectedKey.value); 21 | }; 22 | 23 | // listen to route change 24 | router.afterEach((to) => { 25 | selectAndExpand(to); 26 | 27 | // 将所有 menuCollapsed.value 置为 false 28 | // for (let key in menuCollapsed.value) { 29 | // menuCollapsed.value[key] = false; 30 | // } 31 | 32 | if (to.meta) { 33 | if (to.meta['collapses']) { 34 | for (let key in to.meta['collapses']) { 35 | menuCollapsed.value[to.meta['collapses'][key]] = true; 36 | } 37 | } 38 | } 39 | }) 40 | 41 | 42 | const menuCollapsed = ref({ 43 | top: false, 44 | left: false, 45 | }) 46 | 47 | 48 | const menuOptions = ref({ 49 | top: [], 50 | left: [], 51 | menu: [] 52 | }) 53 | 54 | const validateIfDuplicate = (type, route_name) => { 55 | return menuOptions.value[type].find((option) => option.key === route_name); 56 | } 57 | 58 | const addMenuOptions = (type, route_options, text, icon = null, icon_props = {}) => { 59 | 60 | // if it is a string, convert it to object 61 | if (typeof route_options === 'string') { 62 | route_options = {name: route_options} 63 | } 64 | 65 | if (validateIfDuplicate(type, route_options.name)) { 66 | console.warn(`[menuOptions] 忽略 ${type} 菜单项目,因为有重复的名称: ${route_options.name}`); 67 | return; 68 | } 69 | 70 | // let data = { 71 | // label: () => h( 72 | // RouterLink, 73 | // { 74 | // to: route_options, 75 | // }, 76 | // {default: () => h('span', { 77 | // class: 'relative', 78 | // style: { 79 | // top: "0.5px" 80 | // } 81 | // }, text)} 82 | // ), 83 | // } 84 | 85 | 86 | let data = { 87 | label: () => h( 88 | RouterLink, 89 | { 90 | to: route_options, 91 | }, 92 | {default: () => h('span', {}, text)} 93 | ), 94 | } 95 | 96 | if (route_options.params) { 97 | 98 | let params = route_options.params; 99 | 100 | // 如果有数字,就转换成字符串 101 | for (let key in params) { 102 | if (typeof params[key] === 'number') { 103 | params[key] = params[key].toString(); 104 | } 105 | } 106 | 107 | data.key = route_options.name + JSON.stringify(params); 108 | 109 | // console.log('注册 key', data.key) 110 | 111 | } else { 112 | data.key = route_options.name + "{}"; 113 | } 114 | 115 | if (!icon) { 116 | data.icon = () => { 117 | return h(SimpleMenuIcon, { 118 | title: text, 119 | }) 120 | } 121 | } else { 122 | data.icon = () => { 123 | return h(NIcon, icon_props, {default: () => h(icon, icon_props)}); 124 | } 125 | } 126 | 127 | menuOptions.value[type].push(data); 128 | } 129 | 130 | 131 | const addMultiMenuOptions = (type, options) => { 132 | options.forEach((option) => { 133 | // if it has children, add children 134 | if (option.children) { 135 | // add multi 136 | addMultiMenuOptions(type, option.children); 137 | } else { 138 | addMenuOptions(type, option.route_name, option.text, option.icon) 139 | } 140 | }) 141 | } 142 | 143 | const addMenuDivider = (type) => { 144 | menuOptions.value[type].push({ 145 | type: 'divider', 146 | }) 147 | } 148 | 149 | const removeAllMenuOptions = (type) => { 150 | menuOptions.value[type] = []; 151 | } 152 | 153 | function removeAllMenuOptionsThen(type, func) { 154 | removeAllMenuOptions(type); 155 | func(); 156 | } 157 | 158 | const removeMenuOption = (type, route_name) => { 159 | // 删除指定 key 的菜单项 160 | menuOptions.value[type] = menuOptions.value[type].filter((option) => option.key !== route_name + "{}"); 161 | } 162 | 163 | 164 | menuOptions.value['menu'].push({ 165 | label: () => h( 166 | RouterLink, 167 | { 168 | to: {name: 'index'}, 169 | }, 170 | {default: () => h(LaeLogo, {class: 'lae-logo', width: 40, height: 25})}, 171 | ), 172 | key: 'index', 173 | }); 174 | 175 | 176 | export { 177 | menuOptions, 178 | selectAndExpand, 179 | selectedKey, 180 | menuInst, 181 | addMenuOptions, 182 | addMultiMenuOptions, 183 | removeAllMenuOptions, 184 | addMenuDivider, 185 | removeMenuOption, menuCollapsed, removeAllMenuOptionsThen 186 | 187 | } -------------------------------------------------------------------------------- /src/plugins/persistedstate.js: -------------------------------------------------------------------------------- 1 | export default (options = {storage: null, key: null}) => { 2 | const storage = options.storage || (window && window.localStorage); 3 | const key = options.key || "vuex"; 4 | 5 | // 获取state的值 6 | const getState = (key, storage) => { 7 | const value = storage.getItem(key); 8 | try { 9 | return typeof value !== "undefined" ? JSON.parse(value) : undefined; 10 | } catch (err) { 11 | console.log(err); 12 | } 13 | return undefined; 14 | }; 15 | 16 | // 设置state的值 17 | const setState = ( 18 | key, 19 | state, 20 | storage 21 | ) => storage.setItem(key, JSON.stringify(state)); 22 | 23 | return (store) => { 24 | // 初始化时获取数据,如果有的话,把原来的vuex的state替换掉 25 | const data = getState(key, storage); 26 | if (data) { 27 | store.replaceState(data); 28 | } 29 | 30 | // 订阅 store 的 mutation。handler 会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态作为参数 31 | store.subscribe((mutation, state) => { 32 | setState(key, state, storage); 33 | }); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/plugins/site.js: -------------------------------------------------------------------------------- 1 | import app from '../config/app'; 2 | 3 | function title(str) { 4 | document.title = str + ' - ' + app.name; 5 | } 6 | 7 | export { 8 | title, 9 | } -------------------------------------------------------------------------------- /src/plugins/spinner.js: -------------------------------------------------------------------------------- 1 | import {ref} from 'vue' 2 | 3 | const loading = ref(false) 4 | 5 | export default { 6 | loading, 7 | start: () => { 8 | loading.value = true 9 | }, 10 | finish: () => { 11 | loading.value = false 12 | }, 13 | error: () => { 14 | loading.value = false 15 | } 16 | } -------------------------------------------------------------------------------- /src/plugins/stores/app.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'vuex' 2 | import createPersistedState from '../persistedstate' 3 | 4 | export default createStore({ 5 | plugins: [ 6 | createPersistedState({ 7 | key: 'app', 8 | }), 9 | ], 10 | state: { 11 | display_feedback: true, 12 | }, 13 | getters: { 14 | display_feedback: state => state.display_feedback, 15 | }, 16 | mutations: { 17 | set_display_feedback(state, value) { 18 | state.display_feedback = value 19 | } 20 | }, 21 | actions: {} 22 | }) 23 | -------------------------------------------------------------------------------- /src/plugins/stores/gct.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'vuex' 2 | import gateway from "../gateway.js"; 3 | 4 | export default createStore({ 5 | state: { 6 | containers: [], 7 | }, 8 | actions: { 9 | fetchGct({commit}) { 10 | // http.get('/modules/gct/hosts').then((response) => { 11 | // commit('setGct', response.data) 12 | // }) 13 | gateway.get('gct/hosts').then(res => { 14 | commit('setGct', res.data); 15 | }) 16 | }, 17 | }, 18 | mutations: { 19 | addGct(state, gct) { 20 | state.containers.push(gct) 21 | }, 22 | removeGct(state, gct) { 23 | state.containers.splice(state.containers.indexOf(gct), 1) 24 | }, 25 | setGct(state, gct) { 26 | state.containers = gct 27 | }, 28 | updateGct(state, gct) { 29 | let index = state.containers.findIndex((item) => item.id === gct.id) 30 | state.containers.splice(index, 1, gct) 31 | }, 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /src/plugins/stores/hosts.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'vuex' 2 | import http from "../http.js"; 3 | 4 | export default createStore({ 5 | state: { 6 | client: '', 7 | config: { 8 | client: '', 9 | server: '', 10 | }, 11 | created_at: '', 12 | custom_domain: '', 13 | deleted_at: '', 14 | free_traffic: 0, 15 | host_id: 0, 16 | id: 0, 17 | last_add_free_traffic_at: '', 18 | last_bytes: 0, 19 | local_address: '', 20 | name: '', 21 | price: 0, 22 | protocol: '', 23 | remote_port: 0, 24 | review_at: '', 25 | run_id: '', 26 | server: { 27 | allow_http: 0, 28 | allow_https: 0, 29 | allow_stcp: 0, 30 | allow_tcp: 0, 31 | allow_udp: 0, 32 | free_traffic: 0, 33 | id: 0, 34 | is_china_mainland: 0, 35 | max_port: 0, 36 | max_tunnels: 0, 37 | min_port: 0, 38 | name: '', 39 | price_per_gb: 0, 40 | server_address: '', 41 | server_port: '', 42 | status: '', 43 | token: '', 44 | tunnels: 0, 45 | }, 46 | server_id: 0, 47 | sk: '', 48 | status: '', 49 | suspended_at: '', 50 | traffic: { 51 | name: '', 52 | traffic_in: [], 53 | traffic_out: [], 54 | }, 55 | tunnel: { 56 | conf: { 57 | HealthCheckAddr: '', 58 | PluginParams: '', 59 | bandwidth_limit: '', 60 | group: '', 61 | group_key: '', 62 | health_check_interval_s: 0, 63 | health_check_max_failed: 0, 64 | health_check_timeout_s: 0, 65 | health_check_type: '', 66 | health_check_url: '', 67 | local_ip: '', 68 | local_port: 0, 69 | metas: '', 70 | name: '', 71 | plugin: '', 72 | proxy_protocol_version: "", 73 | remote_port: 0, 74 | type: '', 75 | use_compression: false, 76 | use_encryption: false, 77 | }, 78 | cur_conns: 0, 79 | last_close_time: '', 80 | last_start_time: '', 81 | name: '', 82 | status: '', 83 | today_traffic_in: 0, 84 | today_traffic_out: 0, 85 | }, 86 | updated_at: '', 87 | use_compression: 0, 88 | use_encryption: 0, 89 | user_id: 0, 90 | }, 91 | actions: { 92 | updateHosts({commit}, id) { 93 | http.get('/modules/frp/hosts/' + id).then(response => { 94 | commit('updateHosts', response.data); 95 | }) 96 | }, 97 | }, 98 | mutations: { 99 | updateHosts(state, hosts) { 100 | state = hosts; 101 | } 102 | }, 103 | }) 104 | -------------------------------------------------------------------------------- /src/plugins/stores/http.js: -------------------------------------------------------------------------------- 1 | import {createStore} from "vuex"; 2 | 3 | export default createStore({ 4 | state: { 5 | isAlertedToken: false, 6 | isAlertedTooManyRequests: false, 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /src/plugins/stores/ips.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'vuex' 2 | import gateway from "../gateway.js"; 3 | 4 | export default createStore({ 5 | state: { 6 | ips: [], 7 | }, 8 | actions: { 9 | fetch({commit}) { 10 | gateway.get('ip-manager/hosts').then(res => { 11 | commit('set', res.data); 12 | }) 13 | }, 14 | }, 15 | mutations: { 16 | set(state, data) { 17 | state.ips = data 18 | } 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /src/plugins/stores/navs.js: -------------------------------------------------------------------------------- 1 | import {createStore} from "vuex"; 2 | import createPersistedState from "../persistedstate.js"; 3 | 4 | export default createStore({ 5 | plugins: [ 6 | createPersistedState({ 7 | key: 'navs', 8 | }), 9 | ], 10 | state: { 11 | navs: [], 12 | }, 13 | mutations: { 14 | addNav(state, nav) { 15 | state.navs.push(nav); 16 | }, 17 | removeNav(state, nav) { 18 | state.navs.splice(state.navs.indexOf(nav), 1); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/plugins/stores/red-packets.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'vuex' 2 | import gateway from "../gateway.js"; 3 | 4 | export default createStore({ 5 | state: { 6 | redPackets: [], 7 | }, 8 | actions: { 9 | fetch({commit}) { 10 | gateway.get('red-packets').then(res => { 11 | commit('set', res.data); 12 | }) 13 | }, 14 | }, 15 | mutations: { 16 | set(state, gct) { 17 | state.redPackets = gct 18 | } 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /src/plugins/stores/tasks.js: -------------------------------------------------------------------------------- 1 | import {createStore} from "vuex"; 2 | import http from "../http.js"; 3 | 4 | function filterStatus(task_status) { 5 | if (task_status === 'error') { 6 | return 'error'; 7 | } else if (task_status === 'success') { 8 | return 'success'; 9 | } else if (task_status === 'failed') { 10 | return 'warning'; 11 | } else { 12 | return 'info'; 13 | } 14 | } 15 | 16 | export default createStore({ 17 | state: { 18 | tasks: [], 19 | last_status: 'info', 20 | processing: false, 21 | }, 22 | actions: { 23 | fetch({commit}) { 24 | http.get('/tasks').then(response => { 25 | 26 | let tasks = response.data; 27 | // 只保留 status 为 pending, processing, error, success, failed 的任务 28 | tasks = tasks.filter(task => { 29 | return ['pending', 'processing',].includes(task.status); 30 | }) 31 | 32 | commit('set', tasks); 33 | }); 34 | }, 35 | }, 36 | mutations: { 37 | set(state, tasks) { 38 | state.tasks = tasks; 39 | }, 40 | deleteTask(state, task_id) { 41 | console.log('deleteTask', task_id); 42 | state.tasks = state.tasks.filter(t => t.id !== task_id); 43 | 44 | state.processing = false; 45 | 46 | 47 | // 如果没有任务了,就设置为 info 48 | if (state.tasks.length === 0) { 49 | state.last_status = 'info'; 50 | } 51 | 52 | }, 53 | addTask(state, task) { 54 | state.tasks.push(task); 55 | 56 | state.last_status = filterStatus(task.status); 57 | 58 | }, 59 | updateTask(state, task) { 60 | let index = state.tasks.findIndex(t => t.id === task.id); 61 | state.tasks[index] = task; 62 | 63 | state.last_status = filterStatus(task.status); 64 | // 65 | // console.log(task.status) 66 | // console.log('current status', state.last_status) 67 | // console.log('processing', state.processing) 68 | 69 | state.processing = task.status === 'processing'; 70 | 71 | } 72 | } 73 | }); -------------------------------------------------------------------------------- /src/plugins/stores/tunnels.js: -------------------------------------------------------------------------------- 1 | import {createStore} from "vuex"; 2 | import gateway from "../gateway.js"; 3 | 4 | export default createStore({ 5 | state: { 6 | tunnels: [], 7 | }, 8 | actions: { 9 | fetchTunnels({commit}) { 10 | // http.get('/modules/frp/hosts').then(response => { 11 | // commit('setTunnels', response.data); 12 | // }); 13 | 14 | gateway.get('frp/hosts').then(res => { 15 | commit('setTunnels', res.data); 16 | }) 17 | }, 18 | }, 19 | mutations: { 20 | addTunnel(state, tunnel) { 21 | state.tunnels.push(tunnel); 22 | }, 23 | removeTunnel(state, tunnel) { 24 | state.tunnels.splice(state.tunnels.indexOf(tunnel), 1); 25 | }, 26 | setTunnels(state, tunnels) { 27 | state.tunnels = tunnels; 28 | }, 29 | updateTunnel(state, tunnel) { 30 | let index = state.tunnels.findIndex(t => t.id === tunnel.id); 31 | state.tunnels[index] = tunnel; 32 | } 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/plugins/stores/user.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'vuex' 2 | import createPersistedState from '../persistedstate' 3 | import http from "../http.js"; 4 | 5 | export default createStore({ 6 | plugins: [ 7 | createPersistedState({ 8 | key: 'user', 9 | }), 10 | ], 11 | state: { 12 | token: null, 13 | user: { 14 | id: '', 15 | uuid: '', 16 | name: '', 17 | email: '', 18 | email_md5: '', 19 | real_name: '', 20 | birthday_at: '', 21 | balance: 0, 22 | banned_reason: '', 23 | user_group_id: '', 24 | user_group: { 25 | name: '', 26 | exempt: false, 27 | discount: 0, 28 | }, 29 | created_at: '', 30 | updated_at: '', 31 | }, 32 | }, 33 | actions: { 34 | updateToken({commit}, token) { 35 | commit('updateToken', token) 36 | }, 37 | updateUser({commit}, user) { 38 | commit('updateUser', user) 39 | }, 40 | fetch({commit}) { 41 | http.get('/user').then(response => { 42 | commit('updateUser', response.data); 43 | }); 44 | }, 45 | }, 46 | mutations: { 47 | updateToken(state, token) { 48 | state.token = token 49 | }, 50 | updateUser(state, user) { 51 | state.user = user 52 | }, 53 | }, 54 | }) 55 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | overflow: hidden; 7 | --header-height: 64px; 8 | } 9 | 10 | .menu-item { 11 | display: inline; 12 | position: relative; 13 | top: 3px 14 | } 15 | 16 | .fade-enter-active, 17 | .fade-leave-active { 18 | transition: opacity 0.15s ease-in-out; 19 | } 20 | 21 | .fade-enter-from, 22 | .fade-leave-to { 23 | opacity: 0; 24 | } 25 | 26 | .markdown-preview .github-markdown-body { 27 | /*padding: 0;*/ 28 | background-color: unset !important; 29 | } 30 | 31 | .vmd .v-md-editor-preview .github-markdown-body { 32 | padding: 0 !important; 33 | } 34 | 35 | .vmd .github-markdown-body p { 36 | padding: 0 !important; 37 | margin-bottom: 0 !important; 38 | } 39 | 40 | .h-full { 41 | height: 80vh; 42 | } 43 | 44 | @media only screen and (max-width: 639px) { 45 | body { 46 | overflow: visible; 47 | } 48 | } 49 | 50 | 51 | /*noinspection ALL*/ 52 | .lae-logo { 53 | background-image: url('/assets/lae-dark.png'); 54 | background-size: 40px; 55 | background-repeat: no-repeat; 56 | width: 40px; 57 | height: 25px; 58 | display: block; 59 | } 60 | 61 | @media (prefers-color-scheme: dark) { 62 | /* .background-container { 63 | background: url('/assets/images/bg-dark.jpg'); 64 | filter: brightness(0.6); 65 | } */ 66 | /*noinspection ALL*/ 67 | .lae-logo { 68 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0s; 69 | background-image: url('/assets/lae-white.png'); 70 | } 71 | 72 | 73 | .v-md-editor { 74 | background-color: #3b3b3b !important; 75 | } 76 | 77 | .v-md-editor__toolbar-item--active, 78 | .v-md-editor__toolbar-item--active:hover { 79 | background: #323232 !important; 80 | } 81 | 82 | .v-md-editor__toolbar { 83 | border-bottom: 1px solid #606060 !important; 84 | } 85 | 86 | .v-md-editor__toolbar-divider::before { 87 | border-left: 1px solid #606060 !important; 88 | } 89 | 90 | .v-md-editor--editable .v-md-editor__editor-wrapper { 91 | border-right: 1px solid #606060 !important; 92 | } 93 | 94 | .v-md-textarea-editor pre, 95 | .v-md-textarea-editor textarea { 96 | color: #cacaca !important; 97 | } 98 | 99 | 100 | } -------------------------------------------------------------------------------- /src/utils/composables.js: -------------------------------------------------------------------------------- 1 | import {inject, provide, reactive, toRef, watchEffect} from 'vue' 2 | import {useBreakpoint, useMemo} from 'vooks' 3 | 4 | export function useIsMobile() { 5 | const breakpointRef = useBreakpoint() 6 | return useMemo(() => { 7 | return breakpointRef.value === 'xs' 8 | }) 9 | } 10 | 11 | export function useIsTablet() { 12 | const breakpointRef = useBreakpoint() 13 | return useMemo(() => { 14 | return breakpointRef.value === 's' 15 | }) 16 | } 17 | 18 | export function useIsSmallDesktop() { 19 | const breakpointRef = useBreakpoint() 20 | return useMemo(() => { 21 | return breakpointRef.value === 'm' 22 | }) 23 | } 24 | 25 | export const i18n = function (data) { 26 | const localeReactive = inject('i18n', null) 27 | return { 28 | locale: toRef(localeReactive, 'locale'), 29 | t(key) { 30 | const {locale} = localeReactive 31 | return data[locale][key] 32 | } 33 | } 34 | } 35 | 36 | i18n.provide = function (localeRef) { 37 | const localeReactive = reactive({}) 38 | watchEffect(() => { 39 | localeReactive.locale = localeRef.value 40 | }) 41 | provide('i18n', localeReactive) 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/layout.js: -------------------------------------------------------------------------------- 1 | import {createDiscreteApi, darkTheme, lightTheme, useOsTheme} from "naive-ui"; 2 | 3 | import {computed} from "vue"; 4 | 5 | const osTheme = useOsTheme(); 6 | const configProviderProps = computed(() => ({ 7 | theme: osTheme.value === 'light' ? lightTheme : darkTheme, 8 | })) 9 | 10 | const {message, notification, dialog, loadingBar} = createDiscreteApi( 11 | ["message", "dialog", "notification", "loadingBar"], { 12 | configProviderProps: configProviderProps 13 | } 14 | ); 15 | 16 | export {message, notification, dialog, loadingBar, osTheme} -------------------------------------------------------------------------------- /src/utils/route.js: -------------------------------------------------------------------------------- 1 | export function findMenuValue(options, path) { 2 | for (const option of options) { 3 | if (option.children) { 4 | const value = findMenuValue(option.children, path) 5 | if (value) return value 6 | } 7 | if (option.path === path) { 8 | return option.key 9 | } 10 | } 11 | return undefined 12 | } 13 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 161 | 162 | -------------------------------------------------------------------------------- /src/views/Api.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 39 | 40 | -------------------------------------------------------------------------------- /src/views/Hosts.vue: -------------------------------------------------------------------------------- 1 | 118 | 119 | -------------------------------------------------------------------------------- /src/views/Index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 38 | -------------------------------------------------------------------------------- /src/views/Stars.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 76 | -------------------------------------------------------------------------------- /src/views/Status.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | -------------------------------------------------------------------------------- /src/views/errors/401.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /src/views/errors/404.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | -------------------------------------------------------------------------------- /src/views/errors/500.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | 25 | -------------------------------------------------------------------------------- /src/views/errors/Base.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /src/views/forum/Announcements.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/views/forum/Partner.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 36 | -------------------------------------------------------------------------------- /src/views/forum/Pinned.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/views/forum/components/Topic.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 48 | -------------------------------------------------------------------------------- /src/views/home/Portal.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 73 | -------------------------------------------------------------------------------- /src/views/home/Portal1.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 110 | -------------------------------------------------------------------------------- /src/views/modules/Base.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/modules/forbidden-forest/Base.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/views/modules/forbidden-forest/Index.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 70 | 139 | 140 | -------------------------------------------------------------------------------- /src/views/modules/forbidden-forest/My.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 56 | 98 | 99 | -------------------------------------------------------------------------------- /src/views/modules/forbidden-forest/Show.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 | 133 | 134 | -------------------------------------------------------------------------------- /src/views/modules/gct/Base.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 55 | -------------------------------------------------------------------------------- /src/views/modules/gct/Create.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /src/views/modules/gct/Index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 40 | 41 | -------------------------------------------------------------------------------- /src/views/modules/gct/components/Containers.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | -------------------------------------------------------------------------------- /src/views/modules/gct/components/MenuIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /src/views/modules/gct/components/Terminal.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 227 | 228 | 238 | -------------------------------------------------------------------------------- /src/views/modules/ip-manager/Base.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/views/modules/ip-manager/Create.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 26 | 27 | -------------------------------------------------------------------------------- /src/views/modules/ip-manager/Forward.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 15 | 16 | -------------------------------------------------------------------------------- /src/views/modules/ip-manager/Index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 16 | 17 | -------------------------------------------------------------------------------- /src/views/modules/red-packets/Base.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/views/modules/red-packets/Create.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 87 | 88 | -------------------------------------------------------------------------------- /src/views/modules/red-packets/History.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /src/views/modules/red-packets/Index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /src/views/modules/red-packets/Show.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 111 | 112 | -------------------------------------------------------------------------------- /src/views/modules/tunnels/Base.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/modules/tunnels/Concat.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 147 | -------------------------------------------------------------------------------- /src/views/modules/tunnels/Downloads.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/views/modules/tunnels/Index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/views/modules/tunnels/Sign.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | -------------------------------------------------------------------------------- /src/views/modules/tunnels/Sponsor.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 79 | 80 | 82 | -------------------------------------------------------------------------------- /src/views/modules/tunnels/Status.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | -------------------------------------------------------------------------------- /src/views/modules/tunnels/components/Tunnels.vue: -------------------------------------------------------------------------------- 1 | 121 | 122 | -------------------------------------------------------------------------------- /src/views/users/Login.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 104 | -------------------------------------------------------------------------------- /src/views/users/User.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 106 | -------------------------------------------------------------------------------- /src/views/work-orders/Index.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 74 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | build: { 8 | rollupOptions: { 9 | output: { 10 | manualChunks(id) { 11 | if (id.includes('node_modules')) { 12 | return id 13 | .toString() 14 | .split('node_modules/')[1] 15 | .split('/')[0] 16 | .toString() 17 | } 18 | }, 19 | }, 20 | }, 21 | }, 22 | }) 23 | --------------------------------------------------------------------------------