mirror of
https://github.com/MCSManager/MCSManager.git
synced 2025-04-18 17:30:25 +08:00
Compare commits
247 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d59aa5392c | ||
|
fe7992c698 | ||
|
c614b59445 | ||
|
079f046d03 | ||
|
6c039af16f | ||
|
cb767de61f | ||
|
1bd34c70e7 | ||
|
4d44129424 | ||
|
660c42e4d7 | ||
|
23e5eded55 | ||
|
39ac65a425 | ||
|
bace05284b | ||
|
dee299bcf8 | ||
|
e17fb9e2e5 | ||
|
5a002fc504 | ||
|
52888d25ff | ||
|
4d78bb756a | ||
|
588beda158 | ||
|
572a91c247 | ||
|
bb0632fc1a | ||
|
32968947ae | ||
|
fcd1808b73 | ||
|
245669f8b8 | ||
|
2055a1dd85 | ||
|
d0a65fe1a1 | ||
|
ff17c03894 | ||
|
ac3c30939a | ||
|
703a951ffb | ||
|
09a8885d68 | ||
|
d00d97cf97 | ||
|
e875650842 | ||
|
903c1ae415 | ||
|
05accb3a91 | ||
|
7d86d1129c | ||
|
77fd7876cc | ||
|
3175897a6f | ||
|
2e7bf36de8 | ||
|
2ff0f7e29c | ||
|
6962066605 | ||
|
e57cd413ce | ||
|
176c8ad02e | ||
|
a22621a436 | ||
|
186a8fe134 | ||
|
3579fe529f | ||
|
311455c532 | ||
|
38744a40ef | ||
|
921c01dd2c | ||
|
0dd75ac7a7 | ||
|
96ecc0ce77 | ||
|
1e3b99d5c6 | ||
|
e61f5b3a6e | ||
|
d801a46e1c | ||
|
3fea34745a | ||
|
f757a27738 | ||
|
83dbf56ace | ||
|
ed9af31a97 | ||
|
ef818a876d | ||
|
8982aebc83 | ||
|
91aa370a2d | ||
|
165b337584 | ||
|
aad44fa7cb | ||
|
73ad4a79a5 | ||
|
c96601b8a8 | ||
|
d6fab115c9 | ||
|
2e4b1a979c | ||
|
42e6019427 | ||
|
77316b25ce | ||
|
ec69be678f | ||
|
8d68116d97 | ||
|
483ee5e6b4 | ||
|
f99cab3f0a | ||
|
c1285cdb10 | ||
|
02d986d25e | ||
|
1fcef8866d | ||
|
e4abb18302 | ||
|
57cc3ddd4f | ||
|
8fc6cec023 | ||
|
b5aae32f08 | ||
|
27c0a9b75c | ||
|
e0cdf74da3 | ||
|
0efa85a162 | ||
|
e2ebd62100 | ||
|
ec36b86175 | ||
|
b7b6835ff1 | ||
|
ad45502f56 | ||
|
42a8e8dbfe | ||
|
58e06b5221 | ||
|
f0a44a4686 | ||
|
0b19bd7fc1 | ||
|
c3929d61b8 | ||
|
fb0693903e | ||
|
3f28f79368 | ||
|
76fa65ce25 | ||
|
61450c41cb | ||
|
954f5e3241 | ||
|
d49e800508 | ||
|
37c2ee2bcf | ||
|
2f974e8728 | ||
|
1748fcd751 | ||
|
386388822d | ||
|
0ba3e32abc | ||
|
d6cf7323de | ||
|
f0060c3c69 | ||
|
51f2d02eaf | ||
|
cbe03d0074 | ||
|
60d3a4a17c | ||
|
ec5beae2a9 | ||
|
2e79b2dcfe | ||
|
bd3e2bb711 | ||
|
47c86b2bc5 | ||
|
c66c880eb3 | ||
|
f9e6702557 | ||
|
19f2284c2d | ||
|
89ad69b21f | ||
|
56eb8c1640 | ||
|
763943a007 | ||
|
a060540995 | ||
|
8be2efca4c | ||
|
79f69eba4b | ||
|
457e7d499f | ||
|
3b8017e24c | ||
|
9938a8590e | ||
|
52c8ac4ff9 | ||
|
d562cb0448 | ||
|
04031cf2db | ||
|
d2be793eea | ||
|
853793a964 | ||
|
3146352d85 | ||
|
0fbc373c94 | ||
|
3bbd01a0ae | ||
|
b9aff22c5a | ||
|
ba42e1f6b3 | ||
|
7e174a3f71 | ||
|
69459c82d5 | ||
|
96f145acdc | ||
|
0b61eb408e | ||
|
3a22884898 | ||
|
412f5132fe | ||
|
fca4690f21 | ||
|
5c0d98ee74 | ||
|
3af2c1437e | ||
|
d78deac2a4 | ||
|
8a027fb2f1 | ||
|
b3c2a4d19c | ||
|
c6222a5f85 | ||
|
92e63ee44a | ||
|
55a39c2b7b | ||
|
2c24401b4a | ||
|
681da12893 | ||
|
2dd1dc384c | ||
|
086a831cd2 | ||
|
3195753e0e | ||
|
cfe70b6be4 | ||
|
4f61f3e5d0 | ||
|
e521258b9d | ||
|
5fd3564b47 | ||
|
46ff718a8f | ||
|
d63914eb0b | ||
|
1bb3ce33f1 | ||
|
ed4f68f670 | ||
|
629e5fc2e6 | ||
|
3336b49695 | ||
|
6a6bdeb220 | ||
|
8941f34624 | ||
|
e17cddf9da | ||
|
5d6e13f478 | ||
|
c0f39d6950 | ||
|
8522fc261e | ||
|
c0e0e04105 | ||
|
ec79de28df | ||
|
9ade8e9462 | ||
|
a4f871b8fd | ||
|
586bc4a9ef | ||
|
137808048d | ||
|
0bd617d5ed | ||
|
39a662eb52 | ||
|
d7a5da9261 | ||
|
1572a6104f | ||
|
cfd962c159 | ||
|
46a2192f4a | ||
|
a7ec8a12b1 | ||
|
15ac453054 | ||
|
6b7d42e61f | ||
|
8e1c54abef | ||
|
17d8ee3b79 | ||
|
aa33f228a0 | ||
|
c74811898b | ||
|
38322c38c8 | ||
|
6967bb736c | ||
|
d71592b766 | ||
|
470d569fec | ||
|
547d1d6c40 | ||
|
8799afb02c | ||
|
76e0bc023c | ||
|
1dcafd9128 | ||
|
a141de7cc7 | ||
|
16d88f768b | ||
|
7c824a419d | ||
|
c05038c1f1 | ||
|
e2a7e20a49 | ||
|
1dd417f05c | ||
|
ab7bb109e0 | ||
|
d6f0691828 | ||
|
3822a7bfb4 | ||
|
78dcc1de4f | ||
|
4d9959f666 | ||
|
b3caf3aa05 | ||
|
8b21e5feb7 | ||
|
019ccf04d1 | ||
|
781bd32c48 | ||
|
ea49d3f36d | ||
|
6a4145ae72 | ||
|
264b8facaf | ||
|
c3e6fb7f0e | ||
|
abb20ad0f1 | ||
|
92438f876a | ||
|
95973a6c77 | ||
|
d260e737d8 | ||
|
0a97ad9d70 | ||
|
dd6cc84461 | ||
|
3934e26e21 | ||
|
a6f6f34e37 | ||
|
0e7da93735 | ||
|
d95b952c77 | ||
|
bb825ec931 | ||
|
f0ce2f3bd0 | ||
|
573fb80f65 | ||
|
d277497861 | ||
|
3fa9c1e784 | ||
|
358c7f4402 | ||
|
d3aaeb39f1 | ||
|
ef91fdb2a7 | ||
|
336dd6b19f | ||
|
b3d6c04b23 | ||
|
c11910a14e | ||
|
adf7c50981 | ||
|
90bedcd985 | ||
|
50c85cc8e3 | ||
|
903417f7b9 | ||
|
aa28a14ed5 | ||
|
bf2b3ed439 | ||
|
b14d594fc2 | ||
|
6c97db269f | ||
|
fcc082b71d | ||
|
3bb4eff36b | ||
|
cb36d76685 | ||
|
4348d72e1c |
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
@ -0,0 +1 @@
|
||||
.gitignore
|
BIN
.github/panel-custom-layout.gif
vendored
Normal file
BIN
.github/panel-custom-layout.gif
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 MiB |
102
.github/workflows/docker.yml
vendored
Normal file
102
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
name: Release Docker Build
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-web:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta web
|
||||
id: meta_web
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }}-web
|
||||
name=githubyumao/mcsmanager-web,enable=${{ github.repository == 'MCSManager/MCSManager' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{ github.repository == 'MCSManager/MCSManager' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and Push Web
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: dockerfile/web.dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta_web.outputs.tags }}
|
||||
labels: ${{ steps.meta_web.outputs.labels }}
|
||||
build-args: |
|
||||
BUILDPLATFORM=linux/amd64
|
||||
|
||||
build-daemon:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java_version: [8, 11, 17, 21]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta daemon
|
||||
id: meta_daemon
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }}-daemon,enable=${{ matrix.java_version == 21 }}
|
||||
name=githubyumao/mcsmanager-daemon,enable=${{ github.repository == 'MCSManager/MCSManager' && matrix.java_version == 21 }}
|
||||
name=ghcr.io/${{ github.repository }}-daemon-jdk${{ matrix.java_version }}
|
||||
name=githubyumao/mcsmanager-daemon-jdk${{ matrix.java_version }},enable=${{ github.repository == 'MCSManager/MCSManager' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{ github.repository == 'MCSManager/MCSManager' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and Push Daemon
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: dockerfile/daemon.dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta_daemon.outputs.tags }}
|
||||
labels: ${{ steps.meta_daemon.outputs.labels }}
|
||||
build-args: |
|
||||
BUILDPLATFORM=linux/amd64
|
||||
JAVA_RUNTIME=${{ matrix.java_version }}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,8 +7,6 @@ yarn-error.log*
|
||||
lerna-debug.log*
|
||||
panel/public/
|
||||
*.code-workspace
|
||||
vscode/
|
||||
.vscode/
|
||||
data/
|
||||
dist/
|
||||
out/
|
||||
|
19
.vscode/settings.json
vendored
Normal file
19
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
// Please do not commit changes to this file.
|
||||
// You need to install the "i18n ally" VSCode plugin.
|
||||
{
|
||||
// Change this to your language, available options: "languages/*.json"
|
||||
// "i18n-ally.displayLanguage": "en_US"
|
||||
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.extract.keyMaxLength": 10000,
|
||||
"cSpell.words": ["BUKKIT", "BUNGEECORD", "MINECRAFT"],
|
||||
"i18n-ally.localesPaths": [
|
||||
"languages",
|
||||
"frontend/src/lang",
|
||||
"daemon/src/i18n",
|
||||
"panel/src/app/i18n"
|
||||
],
|
||||
"i18n-ally.keystyle": "flat",
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
"editor.wordSeparators": "`~!@#$%^&*()=+[{]}\\|;:'\",.<>/?"
|
||||
}
|
2
LICENSE
2
LICENSE
@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
Copyright 2025 MCSManager
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
63
README.md
63
README.md
@ -16,36 +16,42 @@
|
||||
|
||||
[Official Website](http://mcsmanager.com/) | [Docs](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md)
|
||||
[简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md) | [Spanish](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
|
||||
## What is MCSManager?
|
||||
|
||||
**MCSManager Panel** (MCSM) is a **modern, secure, and distributed control panel** designed for managing Minecraft and Steam game servers.
|
||||
|
||||
MCSManager has already gained a certain level of popularity within the community, specifically Minecraft. MCSManager excels in offering a centralized management solution for multiple server instances and provides a secure and reliable multi-user permission system. In addition, We are committed to supporting server administrators not only for Minecraft but also for Terraria and various Steam games. Our goal is to foster a thriving and supportive community for game server management.
|
||||
MCSManager has already gained a certain level of popularity within the community, specifically because of Minecraft. MCSManager excels in offering a centralized management solution for multiple server instances and provides a secure and reliable multi-user permission system. In addition, we are committed to support server administrators, not only for Minecraft but also for Terraria and various Steam games. Our goal is to foster a thriving and supportive community for game server management.
|
||||
|
||||
MCSManager **supports English, French, German, Italian, Japanese, Portuguese, Simplified Chinese, and Traditional Chinese**, with plans to add support for more languages in the future!
|
||||
|
||||
**Terminal**
|
||||
|
||||

|
||||
|
||||
**Instance List**
|
||||
|
||||

|
||||
|
||||
**Custom Layout**
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
1. One-click deployment of `Minecraft` Java/Bedrock Server
|
||||
2. Compatible with most `Steam` game servers. (e.g. `Palworld`, `Squad`, `Project Zomboid`, `Terraria`, etc.)
|
||||
3. Customizable UI, create your own layout
|
||||
4. Support `Docker` virtualization, multiuser, and commercial services
|
||||
|
||||
|
||||
4. Supports all images on `Docker Hub`, supports multiple users and supports commercial services!
|
||||
5. Manage multiple servers with a single web interface
|
||||
6. More...
|
||||
6. The technology stack is simple, and you only need to be good at Typescript to complete the entire MCSManager development.
|
||||
7. And More!
|
||||
|
||||
<br />
|
||||
|
||||
@ -53,7 +59,7 @@ MCSManager **supports English, French, German, Italian, Japanese, Portuguese, Si
|
||||
|
||||
MCSM supports both `Windows` and `Linux`. The only requirement is `Node.js` and some libraries **for decompression**.
|
||||
|
||||
Require [Node.js 16.20.2](https://nodejs.org/en) or above.
|
||||
Requires [Node.js 16.20.2](https://nodejs.org/en) or above.
|
||||
|
||||
<br />
|
||||
|
||||
@ -61,7 +67,7 @@ Require [Node.js 16.20.2](https://nodejs.org/en) or above.
|
||||
|
||||
### Windows
|
||||
|
||||
For Windows, We provides packaged executable files:
|
||||
For Windows, we provide packaged executable files:
|
||||
|
||||
Go to: [https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
@ -71,7 +77,7 @@ Go to: [https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
**One-Command Deployment**
|
||||
|
||||
> Script needs to register system services, it requires root permissions.
|
||||
> The script needs to register system services and requires root permissions because of that.
|
||||
|
||||
```bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
|
||||
@ -91,7 +97,7 @@ systemctl stop mcsm-{web,daemon}
|
||||
|
||||
**Linux Manual Installation**
|
||||
|
||||
- If the installation script failed to execute correctly, you can try install it manually.
|
||||
- If the installation script fails to execute correctly, you can try to install it manually.
|
||||
|
||||
```bash
|
||||
# Create /opt directory if not already
|
||||
@ -133,17 +139,19 @@ This installation approach does not automatically set up MCSManager as a system
|
||||
|
||||
<br />
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- Supported on modern browsers including `Chrome`, `Firefox`, and `Safari`.
|
||||
- Support for `IE` has been discontinued.
|
||||
|
||||
<br />
|
||||
|
||||
## Development
|
||||
|
||||
This section is specifically designed for developers. General users may disregard this portion without concern.
|
||||
|
||||
### Plugins
|
||||
|
||||
We use "VS Code" to develop MCSManager. You may need to install these plugins:
|
||||
|
||||
- i18n display support (I18n Ally)
|
||||
- Code formatter (Prettier)
|
||||
- Vue - Offcial
|
||||
- ESLint
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
@ -160,6 +168,10 @@ git clone https://github.com/MCSManager/MCSManager.git
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
### Dependency Files
|
||||
|
||||
You'll need to go to the [PTY](https://github.com/MCSManager/PTY) and [Zip-Tools](https://github.com/MCSManager/Zip-Tools) projects to download the corresponding binary files and place them in the `daemon/lib` directory to ensure the proper functioning of the `Emulation Terminal` and `File Decompression`.
|
||||
|
||||
### Build Production Version
|
||||
|
||||
```bash
|
||||
@ -167,7 +179,7 @@ git clone https://github.com/MCSManager/MCSManager.git
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
Next, you'll need to go to the [PTY](https://github.com/MCSManager/PTY) and [Zip-Tools](https://github.com/MCSManager/Zip-Tools) projects to download the corresponding binary files and place them in the `daemon/lib` directory to ensure the proper functioning of the `Emulation Terminal` and `File Decompression`.
|
||||
Output Directory: "production-code"
|
||||
|
||||
<br />
|
||||
|
||||
@ -179,6 +191,13 @@ Please ensure that any submitted code adheres to our existing coding style. For
|
||||
|
||||
<br />
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- Supported on modern browsers including `Chrome`, `Firefox`, and `Safari`.
|
||||
- Support for `IE` has been discontinued.
|
||||
|
||||
<br />
|
||||
|
||||
## BUG Reporting
|
||||
|
||||
**Open Issue:** [Click here](https://github.com/MCSManager/MCSManager/issues/new/choose)
|
||||
@ -202,4 +221,4 @@ Thanks to these contributors for providing a substantial amount of translation:
|
||||
|
||||
The source code of MCSManager is licensed under the [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) License.
|
||||
|
||||
Copyright ©2024 MCSManager.
|
||||
Copyright ©2025 MCSManager.
|
||||
|
224
README_DE.md
Normal file
224
README_DE.md
Normal file
@ -0,0 +1,224 @@
|
||||
<div align="center">
|
||||
<a href="https://mcsmanager.com/" target="_blank">
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 id="mcsmanager">
|
||||
<a href="https://mcsmanager.com/" target="_blank">MCSManager Panel</a>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/MCSManager)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[Offizielle Website](http://mcsmanager.com/) | [Dokumentation](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[Englisch](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md) | [Spanisch](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## Was ist MCSManager?
|
||||
|
||||
**MCSManager Panel** (MCSM) ist ein **modernes, sicheres und verbreitetes Verwaltungspanel**, designt für die Verwaltung von Minecraft und Steam Spielserver.
|
||||
|
||||
MCSManager hat bereits eine gewisse Popularität in der Community erlangt, insbesondere durch Minecraft. MCSManager zeichnet sich durch eine zentralisierte Verwaltungslösung für mehrere Serverinstanzen aus und bietet ein sicheres und zuverlässiges Berechtigungssystem für mehrere Benutzer. Darüber hinaus engagieren wir uns für die Unterstützung von Serveradministratoren, nicht nur für Minecraft, sondern auch für Terraria und verschiedene Steam Spiele. Unser Ziel ist es, eine florierende und unterstützende Community für die Verwaltung von Spielservern zu fördern.
|
||||
|
||||
MCSManager **unterstützt Englisch, Französisch, Deutsch, Italienisch, Japanisch, Portugiesisch, Chinesisch (Vereinfacht) und Chinesisch (Traditionell)**, mit Plänen mehr Sprachen in der Zukunft zu unterstützen!
|
||||
|
||||
**Terminal**
|
||||
|
||||

|
||||
|
||||
**Instanzliste**
|
||||
|
||||

|
||||
|
||||
**Benutzerdefiniertes Layout**
|
||||
|
||||

|
||||
|
||||
## Funktionen
|
||||
|
||||
1. Ein-Klick-Bereitstellung von `Minecraft` Java/Bedrock Servers
|
||||
2. Kompatibel mit den meisten `Steam` Spielservern. (z.B. `Palworld`, `Squad`, `Project Zomboid`, `Terraria`, etc.)
|
||||
3. Anpassbare Benutzeroberfläche, erstellen Sie Ihr eigenes Layout
|
||||
4. Unterstützt alle Images auf `Docker Hub`, unterstützt mehrere Benutzer und unterstützt kommerzielle Dienste!
|
||||
5. Verwalten Sie mehrere Server mit einer einzigen Weboberfläche
|
||||
6. Der Technologie-Stack ist einfach, und Sie müssen nur gut in Typescript sein, um die gesamte MCSManager-Entwicklung abzuschließen.
|
||||
7. Und mehr!
|
||||
|
||||
<br />
|
||||
|
||||
## Laufzeitumgebung
|
||||
|
||||
MCSM unterstützt `Windows` und `Linux`. Die einzige Anforderung ist `Node.js` und ein Paar andere Bibliotheken **für die Dekomprimierung**.
|
||||
|
||||
Benötigt [Node.js 16.20.2](https://nodejs.org/en) oder höher.
|
||||
|
||||
<br />
|
||||
|
||||
## Installation
|
||||
|
||||
### Windows
|
||||
|
||||
Für Windows, stellen wir gepackte ausführbare Dateien zur Verfügung:
|
||||
|
||||
Gehe zu: [https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
### Linux
|
||||
|
||||
**Bereitstellung mit einem Befehl**
|
||||
|
||||
> Das Skript muss Systemdienste registrieren und benötigt daher Root-Berechtigungen.
|
||||
|
||||
```bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
|
||||
```
|
||||
|
||||
**Verwendung**
|
||||
|
||||
```bash
|
||||
systemctl start mcsm-{web,daemon}
|
||||
systemctl stop mcsm-{web,daemon}
|
||||
```
|
||||
|
||||
- Unterstützt nur Ubuntu/Centos/Debian/Archlinux.
|
||||
- Installationsort: `/opt/mcsmanager/`.
|
||||
|
||||
<br />
|
||||
|
||||
**Linux - Manuelle Installation**
|
||||
|
||||
- Wenn das Installationsskript nicht ordnungsgemäß ausgeführt wird, können Sie versuchen, es manuell zu installieren.
|
||||
|
||||
```bash
|
||||
# Erstellen des /opt Verzeichnisses, wenn es nicht bereits existiert
|
||||
mkdir /opt
|
||||
# Wechseln zu /opt/
|
||||
cd /opt/
|
||||
# Node.js 20.11 herunterladen. Wenn Sie bereits Node.js 16+ installiert haben, können sie diesen Schritt ignorieren.
|
||||
wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
|
||||
# Node.js entpacken
|
||||
tar -xvf node-v20.11.0-linux-x64.tar.xz
|
||||
# Node.js zum System PATH hinzufügen
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# MCSM's Installationsort vorbereiten
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# MCSManager herunterladen
|
||||
wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
|
||||
tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
./install.sh
|
||||
|
||||
# Bitte öffnen Sie zwei Terminals oder Screens.
|
||||
|
||||
# Starten Sie den Daemon zuerst.
|
||||
./start-daemon.sh
|
||||
|
||||
# Starten Sie anschließend das Web-Interface im zweiten Terminal oder Screen.
|
||||
./start-web.sh
|
||||
|
||||
# Gehen Sie für den Webzugriff zu http://localhost:23333/
|
||||
# Im Allgemeinen scannt das Webinterface automatisch den lokalen Daemon und fügt ihn hinzu.
|
||||
```
|
||||
|
||||
Bei diesem Installationsansatz wird MCSManager nicht automatisch als Systemdienst eingerichtet. Daher ist es notwendig, "Screen" für die Verwaltung zu verwenden. Wenn Sie daran interessiert sind, MCSManager über einen Systemdienst zu verwalten, lesen Sie bitte unser Wiki/unsere Dokumentation.
|
||||
|
||||
<br />
|
||||
|
||||
## Entwicklung
|
||||
|
||||
Dieser Abschnitt wurde speziell für Entwickler entwickelt. Allgemeine Benutzer können diesen Teil ohne Bedenken ignorieren.
|
||||
|
||||
### Plugins
|
||||
|
||||
Wir verwenden "VS Code", um MCSManager zu entwickeln. Möglicherweise müssen Sie diese Plugins installieren:
|
||||
|
||||
- i18n display support (I18n Ally)
|
||||
- Code formatter (Prettier)
|
||||
- Vue - Offcial
|
||||
- ESLint
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.sh
|
||||
./npm-dev-macos.sh
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.bat
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
Sie müssen zu den Projekten [PTY](https://github.com/MCSManager/PTY) und [Zip-Tools](https://github.com/MCSManager/Zip-Tools) gehen, um die entsprechenden Binärdateien herunterzuladen und sie im Verzeichnis 'daemon/lib' abzulegen, um das ordnungsgemäße Funktionieren des `Emulation Terminals` und der `File Decompression` sicherzustellen.
|
||||
|
||||
### Produktionsversion erstellen
|
||||
|
||||
```bash
|
||||
./build.bat # Windows
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
Ausgabe-Verzeichnis: "production-code"
|
||||
|
||||
<br />
|
||||
|
||||
## Code beitragen
|
||||
|
||||
Sollten Sie Probleme bei der Nutzung von MCSManager haben, können Sie gerne [ein Issue](https://github.com/MCSManager/MCSManager/issues/new/choose) einreichen. Alternativ können Sie das Projekt forken und direkt beitragen, indem Sie einen Pull Request einreichen.
|
||||
|
||||
Bitte stellen Sie sicher, dass der eingereichte Code unserem bestehenden Codierungsstil entspricht. Weitere Informationen finden Sie in den Richtlinien in [diesem Issue](https://github.com/MCSManager/MCSManager/issues/544).
|
||||
|
||||
<br />
|
||||
|
||||
## Browser Kompatibilität
|
||||
|
||||
- Wird in modernen Browsern wie `Chrome`, `Firefox` und `Safari` unterstützt.
|
||||
- Die Unterstützung für `IE` wurde eingestellt.
|
||||
|
||||
<br />
|
||||
|
||||
## BUG Reporting
|
||||
|
||||
**Issue erstellen:** [Hier drücken](https://github.com/MCSManager/MCSManager/issues/new/choose)
|
||||
|
||||
**Bericht über Sicherheitslücken:** [SECURITY.md](SECURITY.md)
|
||||
|
||||
<br />
|
||||
|
||||
## Internationalisierung
|
||||
|
||||
Vielen Dank an diese Mitwirkenden für die Bereitstellung einer beträchtlichen Menge an Übersetzungen:
|
||||
|
||||
- [KevinLu2000](https://github.com/KevinLu2000)
|
||||
- [Unitwk](https://github.com/unitwk)
|
||||
- [JianyueLab](https://github.com/JianyueLab)
|
||||
- [IceBrick](https://github.com/IceBrick01)
|
||||
|
||||
<br />
|
||||
|
||||
## Lizens
|
||||
|
||||
Der Quellcode von MCSManager ist unter der [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) Lizenz lizenziert.
|
||||
|
||||
Copyright ©2025 MCSManager.
|
199
README_ES.md
Normal file
199
README_ES.md
Normal file
@ -0,0 +1,199 @@
|
||||
<div align="center">
|
||||
<a href="https://mcsmanager.com/" target="_blank">
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 id="mcsmanager">
|
||||
<a href="https://mcsmanager.com/" target="_blank">Panel de MCSManager</a>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/MCSManager)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[Sitio Oficial](http://mcsmanager.com/) | [Documentación](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[Inglés](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) | [日本語](README_JP.md)
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## ¿Qué es MCSManager?
|
||||
|
||||
**Panel de MCSManager** (MCSM) es un **panel de control moderno, seguro y distribuido** diseñado para gestionar servidores de juego de Minecraft y Steam.
|
||||
|
||||
MCSManager ha ganado popularidad en la comunidad, especialmente en Minecraft. MCSManager ofrece una solución centralizada para gestionar múltiples instancias de servidor y proporciona un sistema de permisos multiusuario seguro y confiable. Nos comprometemos a apoyar a los administradores de servidores no solo para Minecraft, sino también para Terraria y varios juegos de Steam. Nuestro objetivo es fomentar una comunidad próspera y de apoyo en la gestión de servidores de juego.
|
||||
|
||||
MCSManager **admite inglés, francés, alemán, italiano, japonés, portugués, chino simplificado y chino tradicional**, ¡y planea agregar más idiomas en el futuro!
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Características
|
||||
|
||||
1. Implementación con un clic de servidor `Minecraft` Java/Bedrock.
|
||||
2. Compatible con la mayoría de servidores de juegos de `Steam` (p. ej., `Palworld`, `Squad`, `Project Zomboid`, `Terraria`, etc.).
|
||||
3. Interfaz personalizable; crea tu propio diseño.
|
||||
4. Soporte para virtualización con `Docker`, multiusuario y servicios comerciales.
|
||||
5. Gestiona múltiples servidores desde una sola interfaz web.
|
||||
6. ¡Y más!
|
||||
|
||||
<br />
|
||||
|
||||
## Entorno de Ejecución
|
||||
|
||||
MCSM es compatible con `Windows` y `Linux`. El único requisito es `Node.js` y algunas librerías **para descompresión**.
|
||||
|
||||
Requiere [Node.js 16.20.2](https://nodejs.org/en) o superior.
|
||||
|
||||
<br />
|
||||
|
||||
## Instalación
|
||||
|
||||
### Windows
|
||||
|
||||
Para Windows, ofrecemos archivos ejecutables empaquetados:
|
||||
|
||||
Ir a: [https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
### Linux
|
||||
|
||||
**Despliegue con un solo comando**
|
||||
|
||||
> El script necesita registrar servicios del sistema, requiere permisos de root.
|
||||
|
||||
```bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
|
||||
```
|
||||
|
||||
**Uso**
|
||||
|
||||
```bash
|
||||
systemctl start mcsm-{web,daemon}
|
||||
systemctl stop mcsm-{web,daemon}
|
||||
```
|
||||
|
||||
- Solo compatible con Ubuntu/Centos/Debian/Archlinux.
|
||||
- Directorio de instalación: `/opt/mcsmanager/`.
|
||||
|
||||
<br />
|
||||
|
||||
**Instalación Manual en Linux**
|
||||
|
||||
- Si el script de instalación falla, puedes intentar instalarlo manualmente.
|
||||
|
||||
```bash
|
||||
# Crear directorio /opt si no existe
|
||||
mkdir /opt
|
||||
# Cambiar a /opt
|
||||
cd /opt/
|
||||
# Descargar Node.js 20.11. Si ya tienes Node.js 16+ instalado, omite este paso.
|
||||
wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
|
||||
# Descomprimir Node.js
|
||||
tar -xvf node-v20.11.0-linux-x64.tar.xz
|
||||
# Agregar Node.js al PATH del sistema
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# Preparar el directorio de instalación de MCSM
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# Descargar MCSManager
|
||||
wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
|
||||
tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
# Instalar dependencias
|
||||
./install.sh
|
||||
|
||||
# Abrir dos terminales o pantallas.
|
||||
|
||||
# Iniciar el daemon primero.
|
||||
./start-daemon.sh
|
||||
|
||||
# Iniciar la interfaz web en la segunda terminal o pantalla.
|
||||
./start-web.sh
|
||||
|
||||
# Para acceder a la web, ir a http://localhost:23333/
|
||||
# En general, la interfaz web escaneará y añadirá automáticamente el daemon local.
|
||||
```
|
||||
|
||||
Este método de instalación no configura automáticamente MCSManager como un servicio del sistema. Por lo tanto, es necesario usar `screen` para la administración. Para quienes quieran administrar MCSManager a través de un servicio del sistema, por favor consulta nuestra wiki/documentación.
|
||||
|
||||
<br />
|
||||
|
||||
## Compatibilidad del Navegador
|
||||
|
||||
- Compatible con navegadores modernos como `Chrome`, `Firefox` y `Safari`.
|
||||
- El soporte para `IE` ha sido discontinuado.
|
||||
|
||||
<br />
|
||||
|
||||
## Desarrollo
|
||||
|
||||
Esta sección está dirigida específicamente a desarrolladores. Los usuarios generales pueden ignorarla sin problema.
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.sh
|
||||
./npm-dev-macos.sh
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.bat
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
### Construir Versión de Producción
|
||||
|
||||
```bash
|
||||
./build.bat # Windows
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
Luego, deberás ir a los proyectos [PTY](https://github.com/MCSManager/PTY) y [Zip-Tools](https://github.com/MCSManager/Zip-Tools) para descargar los archivos binarios correspondientes y colocarlos en el directorio `daemon/lib` para asegurar el funcionamiento adecuado del `Terminal de Emulación` y la `Descompresión de Archivos`.
|
||||
|
||||
<br />
|
||||
|
||||
## Contribución de Código
|
||||
|
||||
Si experimentas problemas al usar MCSManager, puedes [enviar un Issue](https://github.com/MCSManager/MCSManager/issues/new/choose). Alternativamente, puedes hacer un fork del proyecto y contribuir directamente enviando un Pull Request.
|
||||
|
||||
Asegúrate de que el código enviado siga nuestro estilo de codificación existente. Para más detalles, consulta las pautas en [este issue](https://github.com/MCSManager/MCSManager/issues/544).
|
||||
|
||||
<br />
|
||||
|
||||
## Reporte de Errores
|
||||
|
||||
**Abrir Issue:** [Haz clic aquí](https://github.com/MCSManager/MCSManager/issues/new/choose)
|
||||
|
||||
**Reporte de Vulnerabilidades de Seguridad:** [SECURITY.md](SECURITY.md) (en ingles)
|
||||
|
||||
<br />
|
||||
|
||||
## Internacionalización
|
||||
|
||||
Gracias a estos colaboradores por proporcionar una gran cantidad de traducciones:
|
||||
|
||||
- [KevinLu2000](https://github.com/KevinLu2000)
|
||||
- [Unitwk](https://github.com/unitwk)
|
||||
- [JianyueLab](https://github.com/JianyueLab)
|
||||
- [IceBrick](https://github.com/IceBrick01)
|
||||
|
||||
<br />
|
||||
|
||||
## Licencia
|
||||
El código fuente de MCSManager está licenciado bajo la [Licencia Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
Copyright ©2025 MCSManager.
|
@ -17,7 +17,7 @@
|
||||
|
||||
[HP](http://mcsmanager.com/) | [ドキュメント](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md)
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) | [Spanish](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
@ -202,4 +202,4 @@ git clone https://github.com/MCSManager/MCSManager.git
|
||||
|
||||
ソースコードは、 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)ライセンス使っています。
|
||||
|
||||
Copyright ©2024 MCSManager.x
|
||||
Copyright ©2025 MCSManager.
|
||||
|
@ -16,8 +16,8 @@
|
||||
|
||||
[Website Oficial](http://mcsmanager.com/) | [Documentação](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md)
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) |
|
||||
[日本語](README_JP.md) | [Spanish](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
@ -201,4 +201,4 @@ Agradecemos aos seguintes colaboradores por fornecerem uma quantidade substancia
|
||||
|
||||
O código-fonte do MCSManager é licenciado sob a Licença [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
Copyright ©2024 MCSManager.
|
||||
Copyright ©2025 MCSManager.
|
||||
|
@ -16,8 +16,8 @@
|
||||
|
||||
[官方網站](http://mcsmanager.com/) | [教學說明](https://docs.mcsmanager.com/#/zh-cn/) | [TG 群組](https://t.me/MCSManager_dev) | [成為贊助者](https://afdian.net/a/mcsmanager)
|
||||
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md)
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md) | [Spanish](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
@ -215,4 +215,4 @@ git clone https://github.com/MCSManager/MCSManager.git
|
||||
|
||||
原始碼遵循 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 協定。
|
||||
|
||||
Copyright ©2024 MCSManager.
|
||||
Copyright ©2025 MCSManager.
|
||||
|
41
README_ZH.md
41
README_ZH.md
@ -16,8 +16,8 @@
|
||||
|
||||
[官方网站](http://mcsmanager.com/) | [使用文档](https://docs.mcsmanager.com/#/zh-cn/) | [QQ 群](https://jq.qq.com/?_wv=1027&k=Pgl9ScGw) | [TG 群](https://t.me/MCSManager_dev) | [成为赞助者](https://afdian.net/a/mcsmanager)
|
||||
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md)
|
||||
[English](README.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md) | [Spanish](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
@ -40,9 +40,9 @@
|
||||
1. 支持一键开服!轻松部署 `Minecraft` Java 版/基岩版游戏服务器。
|
||||
2. 兼容大部分 `Steam` 游戏服务器,列如 `幻兽帕鲁`,`战术小队`,`僵尸毁灭工程` 和 `泰拉瑞亚` 等。
|
||||
3. 网页支持拖拽式的小卡片布局,打造自己喜欢的界面布局。
|
||||
4. 支持 `Docker` 虚拟化,支持多用户,支持商业出租行为。
|
||||
5. 支持所有 `Docker` 镜像,轻松打造预设!
|
||||
6. 支持分布式,一个网页即可同时管理数台机器。
|
||||
4. 支持 `Docker Hub` 上的所有镜像,支持多用户,支持商业服务。
|
||||
5. 支持分布式,一个网页即可同时管理数台机器。
|
||||
6. 技术栈简单,仅需擅长 Typescript 即可完成整个 MCSManager 开发!
|
||||
7. 更多...
|
||||
|
||||
<br />
|
||||
@ -127,17 +127,19 @@ tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
<br />
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- 支持 `Chrome` `Firefox` `Safari` `Opera` 等现代主流浏览器。
|
||||
- 已放弃支持 `IE` 浏览器。
|
||||
|
||||
<br />
|
||||
|
||||
## 搭建开发环境
|
||||
|
||||
此段落面向开发人员,普通用户无需关注也无需执行。
|
||||
|
||||
### 必备插件
|
||||
|
||||
我们使用 “VS Code” 开发 MCSManager,你可能需要安装这些插件:
|
||||
|
||||
- i18n 文案显示支持(I18n Ally)
|
||||
- 代码格式化(Prettier)
|
||||
- Vue - Offcial
|
||||
- ESLint
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
@ -154,6 +156,10 @@ git clone https://github.com/MCSManager/MCSManager.git
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
### 依赖文件
|
||||
|
||||
接下来你还需要前往 [PTY](https://github.com/MCSManager/PTY) 和 [Zip-Tools](https://github.com/MCSManager/Zip-Tools) 两个项目下载对应的二进制文件,将他们存放到 `daemon/lib` 目录下,以确保 `仿真终端` 和 `文件解压缩` 的正常工作。
|
||||
|
||||
### 构建生产环境版本
|
||||
|
||||
```bash
|
||||
@ -161,7 +167,7 @@ git clone https://github.com/MCSManager/MCSManager.git
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
接下来你还需要前往 [PTY](https://github.com/MCSManager/PTY) 和 [Zip-Tools](https://github.com/MCSManager/Zip-Tools) 两个项目下载对应的二进制文件,将他们存放到 `daemon/lib` 目录下,以确保 `仿真终端` 和 `文件解压缩` 的正常工作。
|
||||
最终产物目录: "production-code"
|
||||
|
||||
<br />
|
||||
|
||||
@ -173,6 +179,13 @@ git clone https://github.com/MCSManager/MCSManager.git
|
||||
|
||||
<br />
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- 支持 `Chrome` `Firefox` `Safari` `Opera` 等现代主流浏览器。
|
||||
- 已放弃支持 `IE` 浏览器。
|
||||
|
||||
<br />
|
||||
|
||||
## BUG 报告
|
||||
|
||||
欢迎发现的任何问题进行反馈,必当及时修复。
|
||||
@ -196,4 +209,4 @@ git clone https://github.com/MCSManager/MCSManager.git
|
||||
|
||||
源代码遵循 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 协议。
|
||||
|
||||
Copyright ©2024 MCSManager.
|
||||
Copyright ©2025 MCSManager.
|
||||
|
@ -11,13 +11,11 @@ cd daemon
|
||||
call npm run build
|
||||
|
||||
echo "Build panel..."
|
||||
cd ..
|
||||
cd panel
|
||||
cd ../panel
|
||||
call npm run build
|
||||
|
||||
echo "Build frontend..."
|
||||
cd ..
|
||||
cd frontend
|
||||
cd ../frontend
|
||||
call npm run build
|
||||
|
||||
echo "Collecting files..."
|
||||
|
1
common/global.d.ts
vendored
1
common/global.d.ts
vendored
@ -74,6 +74,7 @@ export interface IGlobalInstanceDockerConfig {
|
||||
cpuUsage?: number;
|
||||
workingDir?: string;
|
||||
env?: string[];
|
||||
changeWorkdir?: boolean;
|
||||
}
|
||||
|
||||
export interface IPanelResponseProtocol {
|
||||
|
1080
daemon/package-lock.json
generated
1080
daemon/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mcsmanager-daemon",
|
||||
"version": "4.5.2",
|
||||
"version": "4.6.0",
|
||||
"description": "Provides remote control capability for MCSManager to manage processes, scheduled tasks, I/O streams, and more",
|
||||
"scripts": {
|
||||
"dev": "ts-node --project tsconfig.json src/app.ts",
|
||||
@ -20,15 +20,14 @@
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@koa/router": "^10.0.0",
|
||||
"archiver": "^5.3.1",
|
||||
"axios": "^1.1.3",
|
||||
"axios": "^1.8.2",
|
||||
"compressing": "^1.10.0",
|
||||
"crypto": "^1.0.1",
|
||||
"dockerode": "3.1.0",
|
||||
"dockerode": "4.0.5",
|
||||
"formidable": "^3.5.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"i18next": "^21.8.14",
|
||||
"iconv-lite": "^0.6.2",
|
||||
"koa": "^2.13.1",
|
||||
"koa": "^2.16.1",
|
||||
"koa-body-patch": "^6.0.1",
|
||||
"koa-send": "^5.0.1",
|
||||
"log4js": "^6.4.0",
|
||||
@ -46,15 +45,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archiver": "^5.3.1",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/dockerode": "^3.2.7",
|
||||
"@types/formidable": "^3.4.5",
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"@types/iconv-lite": "0.0.1",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@types/koa__router": "^8.0.7",
|
||||
"@types/koa-send": "^4.1.3",
|
||||
"@types/log4js": "^2.3.5",
|
||||
"@types/mocha": "^8.2.2",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
|
@ -1,12 +0,0 @@
|
||||
import Instance from "../instance/instance";
|
||||
import InstanceCommand from "./base/command";
|
||||
|
||||
export default class SendCommand extends InstanceCommand {
|
||||
constructor(public readonly cmd: string) {
|
||||
super("SendCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
return await instance.execPreset("command", this.cmd);
|
||||
}
|
||||
}
|
@ -10,8 +10,6 @@ import DockerStartCommand from "./docker/docker_start";
|
||||
import TimeCheck from "./task/time";
|
||||
import GeneralUpdateCommand from "./general/general_update";
|
||||
import PtyStartCommand from "./pty/pty_start";
|
||||
import PtyStopCommand from "./pty/pty_stop";
|
||||
import OpenFrpTask from "./task/openfrp";
|
||||
import RconCommand from "./steam/rcon_command";
|
||||
import DockerResizeCommand from "./docker/docker_pty_resize";
|
||||
import PtyResizeCommand from "./pty/pty_resize";
|
||||
@ -45,7 +43,7 @@ export default class FunctionDispatcher extends InstanceCommand {
|
||||
|
||||
// the component that the instance must mount
|
||||
instance.lifeCycleTaskManager.registerLifeCycleTask(new TimeCheck());
|
||||
instance.lifeCycleTaskManager.registerLifeCycleTask(new OpenFrpTask());
|
||||
// instance.lifeCycleTaskManager.registerLifeCycleTask(new OpenFrpTask());
|
||||
|
||||
// Instance general preset capabilities
|
||||
instance.setPreset("command", new GeneralSendCommand());
|
||||
@ -64,7 +62,6 @@ export default class FunctionDispatcher extends InstanceCommand {
|
||||
// Enable emulated terminal mode
|
||||
if (instance.config.terminalOption.pty && instance.config.processType === "general") {
|
||||
instance.setPreset("start", new PtyStartCommand());
|
||||
instance.setPreset("stop", new PtyStopCommand());
|
||||
instance.setPreset("resize", new PtyResizeCommand());
|
||||
}
|
||||
// Whether to enable Docker PTY mode
|
||||
|
@ -9,17 +9,13 @@ import {
|
||||
SetupDockerContainer,
|
||||
StartupDockerProcessError
|
||||
} from "../../../service/docker_process_service";
|
||||
import AbsStartCommand from "../start";
|
||||
|
||||
export default class DockerStartCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("DockerStartCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance, source = "Unknown") {
|
||||
export default class DockerStartCommand extends AbsStartCommand {
|
||||
protected async createProcess(instance: Instance) {
|
||||
if (!instance.hasCwdPath() || !instance.config.ie || !instance.config.oe)
|
||||
throw new StartupDockerProcessError($t("TXT_CODE_a6424dcc"));
|
||||
if (!fs.existsSync(instance.absoluteCwdPath()))
|
||||
throw new StartupDockerProcessError($t("TXT_CODE_instance.dirNoE"));
|
||||
if (!fs.existsSync(instance.absoluteCwdPath())) fs.mkdirpSync(instance.absoluteCwdPath());
|
||||
|
||||
// Docker Image check
|
||||
try {
|
||||
|
@ -4,16 +4,31 @@ import Instance from "../../instance/instance";
|
||||
import { encode } from "iconv-lite";
|
||||
import InstanceCommand from "../base/command";
|
||||
|
||||
export const CTRL_C = "\x03";
|
||||
|
||||
export function isExitCommand(instance: Instance, buf: any) {
|
||||
if (String(buf).toLowerCase() === "^c") {
|
||||
instance.process?.kill("SIGINT");
|
||||
return true;
|
||||
}
|
||||
if (buf == CTRL_C) {
|
||||
instance.process?.write(CTRL_C);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default class GeneralSendCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("SendCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance, buf?: any): Promise<any> {
|
||||
if (isExitCommand(instance, buf)) return;
|
||||
// The server shutdown command needs to send a command, but before the server shutdown command is executed, the status will be set to the shutdown state.
|
||||
// So here the command can only be executed by whether the process exists or not
|
||||
if (instance?.process) {
|
||||
instance.process.write(encode(buf, instance.config.oe));
|
||||
instance.process.write(encode(buf, instance.config.ie));
|
||||
if (instance.config.crlf === 2) return instance.process.write("\r\n");
|
||||
return instance.process.write("\n");
|
||||
} else {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import logger from "../../../service/log";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
@ -8,6 +9,14 @@ export default class GeneralKillCommand extends InstanceCommand {
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
if (instance.status() === Instance.STATUS_STOP) return;
|
||||
|
||||
if (instance.startTimestamp && instance.startTimestamp + 6 * 1000 > Date.now()) {
|
||||
return instance.failure(new Error($t("TXT_CODE_6259357c")));
|
||||
}
|
||||
|
||||
instance.ignoreEventTaskOnce();
|
||||
|
||||
const task = instance?.asynchronousTask;
|
||||
if (task && task.stop) {
|
||||
task
|
||||
@ -17,9 +26,9 @@ export default class GeneralKillCommand extends InstanceCommand {
|
||||
logger.error(`Instance ${instance.config.nickname} asynchronousTask stop error:`, err);
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.process) {
|
||||
await instance.process.kill("SIGKILL");
|
||||
}
|
||||
instance.setLock(false);
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,10 @@ export default class GeneralRestartCommand extends InstanceCommand {
|
||||
|
||||
async exec(instance: Instance) {
|
||||
try {
|
||||
instance.ignoreEventTaskOnce();
|
||||
instance.println("INFO", $t("TXT_CODE_restart.start"));
|
||||
await instance.execPreset("stop");
|
||||
instance.setLock(true);
|
||||
await instance.execPreset("stop");
|
||||
const startCount = instance.startCount;
|
||||
// Check the instance status every second,
|
||||
// if the instance status is stopped, restart the server immediately
|
||||
@ -28,9 +29,9 @@ export default class GeneralRestartCommand extends InstanceCommand {
|
||||
}
|
||||
if (instance.status() === Instance.STATUS_STOP) {
|
||||
instance.println("INFO", $t("TXT_CODE_restart.restarting"));
|
||||
await instance.execPreset("start");
|
||||
instance.setLock(false);
|
||||
clearInterval(task);
|
||||
await instance.execPreset("start");
|
||||
}
|
||||
} catch (error: any) {
|
||||
clearInterval(task);
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import os from "os";
|
||||
import Instance from "../../instance/instance";
|
||||
import logger from "../../../service/log";
|
||||
import fs from "fs-extra";
|
||||
import InstanceCommand from "../base/command";
|
||||
import EventEmitter from "events";
|
||||
import { IInstanceProcess } from "../../instance/interface";
|
||||
import { ChildProcess, exec, spawn } from "child_process";
|
||||
import { ChildProcess, spawn } from "child_process";
|
||||
import { commandStringToArray } from "../base/command_parser";
|
||||
import { killProcess } from "common";
|
||||
import AbsStartCommand from "../start";
|
||||
|
||||
// Error exception at startup
|
||||
class StartupError extends Error {
|
||||
@ -57,12 +56,8 @@ class ProcessAdapter extends EventEmitter implements IInstanceProcess {
|
||||
}
|
||||
}
|
||||
|
||||
export default class GeneralStartCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("StartCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance, source = "Unknown") {
|
||||
export default class GeneralStartCommand extends AbsStartCommand {
|
||||
async createProcess(instance: Instance, source = "") {
|
||||
if (
|
||||
(!instance.config.startCommand && instance.config.processType === "general") ||
|
||||
!instance.hasCwdPath() ||
|
||||
@ -70,8 +65,7 @@ export default class GeneralStartCommand extends InstanceCommand {
|
||||
!instance.config.oe
|
||||
)
|
||||
throw new StartupError($t("TXT_CODE_general_start.instanceConfigErr"));
|
||||
if (!fs.existsSync(instance.absoluteCwdPath()))
|
||||
throw new StartupError($t("TXT_CODE_general_start.cwdPathNotExist"));
|
||||
if (!fs.existsSync(instance.absoluteCwdPath())) fs.mkdirpSync(instance.absoluteCwdPath());
|
||||
|
||||
// command parsing
|
||||
const commandList = commandStringToArray(instance.config.startCommand);
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import SendCommand from "../cmd";
|
||||
import RconCommand from "../steam/rcon_command";
|
||||
|
||||
export default class GeneralStopCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
@ -15,20 +13,16 @@ export default class GeneralStopCommand extends InstanceCommand {
|
||||
return instance.failure(new Error($t("TXT_CODE_general_stop.notRunning")));
|
||||
|
||||
instance.status(Instance.STATUS_STOPPING);
|
||||
instance.ignoreEventTaskOnce();
|
||||
|
||||
const stopCommandList = stopCommand.split("\n");
|
||||
for (const stopCommand of stopCommandList) {
|
||||
if (stopCommand.toLowerCase() == "^c") {
|
||||
instance.process.kill("SIGINT");
|
||||
} else if (instance.config.enableRcon) {
|
||||
await instance.exec(new RconCommand(stopCommand));
|
||||
} else {
|
||||
await instance.exec(new SendCommand(stopCommand));
|
||||
}
|
||||
await instance.execPreset("command", stopCommand);
|
||||
}
|
||||
|
||||
instance.print("\n");
|
||||
instance.println("INFO", $t("TXT_CODE_general_stop.execCmd", { stopCommand }));
|
||||
instance.println("INFO", $t("TXT_CODE_pty_stop.execCmd", { stopCommand: `\n${stopCommand}` }));
|
||||
|
||||
const cacheStartCount = instance.startCount;
|
||||
|
||||
// If the instance is still in the stopped state after 10 minutes, restore the state
|
||||
|
@ -1,17 +0,0 @@
|
||||
import Instance from "../instance/instance";
|
||||
import InstanceCommand from "./base/command";
|
||||
|
||||
export default class KillCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("KillCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
// If the automatic restart function is enabled, the setting is ignored once
|
||||
if (instance.config.eventTask && instance.config.eventTask.autoRestart)
|
||||
instance.config.eventTask.ignore = true;
|
||||
|
||||
// send stop command
|
||||
return await instance.execPreset("kill");
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import logger from "../../../service/log";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import readline from "readline";
|
||||
import InstanceCommand from "../base/command";
|
||||
import EventEmitter from "events";
|
||||
import { IInstanceProcess } from "../../instance/interface";
|
||||
import { ChildProcess, ChildProcessWithoutNullStreams, exec, spawn } from "child_process";
|
||||
@ -15,6 +14,7 @@ import FunctionDispatcher from "../dispatcher";
|
||||
import { PTY_PATH } from "../../../const";
|
||||
import { Writable } from "stream";
|
||||
import { v4 } from "uuid";
|
||||
import AbsStartCommand from "../start";
|
||||
|
||||
interface IPtySubProcessCfg {
|
||||
pid: number;
|
||||
@ -44,22 +44,26 @@ export class GoPtyProcessAdapter extends EventEmitter implements IInstanceProces
|
||||
process.stdout?.on("data", (text) => this.emit("data", text));
|
||||
process.stderr?.on("data", (text) => this.emit("data", text));
|
||||
process.on("exit", (code) => this.emit("exit", code));
|
||||
try {
|
||||
this.initNamedPipe();
|
||||
} catch (error: any) {
|
||||
logger.error(`Init Pipe Err: ${pipeName}, ${error}`);
|
||||
}
|
||||
this.initNamedPipe();
|
||||
}
|
||||
|
||||
private initNamedPipe() {
|
||||
const fd = fs.openSync(this.pipeName, "w");
|
||||
const writePipe = fs.createWriteStream("", { fd });
|
||||
writePipe.on("close", () => {});
|
||||
writePipe.on("end", () => {});
|
||||
writePipe.on("error", (err) => {
|
||||
logger.error("Pipe error:", this.pipeName, err);
|
||||
});
|
||||
this.pipeClient = writePipe;
|
||||
private async initNamedPipe() {
|
||||
try {
|
||||
const fd = await fs.open(this.pipeName, "w");
|
||||
const writePipe = fs.createWriteStream("", { fd });
|
||||
writePipe.on("close", () => {});
|
||||
writePipe.on("end", () => {});
|
||||
writePipe.on("error", (err) => {
|
||||
logger.error("Pipe error:", this.pipeName, err);
|
||||
});
|
||||
this.pipeClient = writePipe;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
$t("TXT_CODE_9d1d244f", {
|
||||
pipeName: error
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public resize(w: number, h: number) {
|
||||
@ -111,11 +115,7 @@ export class GoPtyProcessAdapter extends EventEmitter implements IInstanceProces
|
||||
}
|
||||
}
|
||||
|
||||
export default class PtyStartCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("PtyStartCommand");
|
||||
}
|
||||
|
||||
export default class PtyStartCommand extends AbsStartCommand {
|
||||
readPtySubProcessConfig(subProcess: ChildProcessWithoutNullStreams): Promise<IPtySubProcessCfg> {
|
||||
return new Promise((r, j) => {
|
||||
const errConfig = {
|
||||
@ -141,7 +141,7 @@ export default class PtyStartCommand extends InstanceCommand {
|
||||
});
|
||||
}
|
||||
|
||||
async exec(instance: Instance, source = "Unknown") {
|
||||
async createProcess(instance: Instance) {
|
||||
if (
|
||||
!instance.config.startCommand ||
|
||||
!instance.hasCwdPath() ||
|
||||
@ -149,13 +149,12 @@ export default class PtyStartCommand extends InstanceCommand {
|
||||
!instance.config.oe
|
||||
)
|
||||
throw new StartupError($t("TXT_CODE_pty_start.cmdErr"));
|
||||
if (!fs.existsSync(instance.absoluteCwdPath()))
|
||||
throw new StartupError($t("TXT_CODE_pty_start.cwdNotExist"));
|
||||
if (!fs.existsSync(instance.absoluteCwdPath())) fs.mkdirpSync(instance.absoluteCwdPath());
|
||||
if (!path.isAbsolute(path.normalize(instance.absoluteCwdPath())))
|
||||
throw new StartupError($t("TXT_CODE_pty_start.mustAbsolutePath"));
|
||||
|
||||
// PTY mode correctness check
|
||||
logger.info($t("TXT_CODE_pty_start.startPty", { source: source }));
|
||||
logger.info($t("TXT_CODE_pty_start.startPty", { source: "" }));
|
||||
let checkPtyEnv = true;
|
||||
|
||||
if (!fs.existsSync(PTY_PATH)) {
|
||||
@ -167,12 +166,11 @@ export default class PtyStartCommand extends InstanceCommand {
|
||||
// Close the PTY type, reconfigure the instance function group, and restart the instance
|
||||
instance.config.terminalOption.pty = false;
|
||||
await instance.forceExec(new FunctionDispatcher());
|
||||
await instance.execPreset("start", source); // execute the preset command directly
|
||||
await instance.execPreset("start"); // execute the preset command directly
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the startup state & increase the number of startups
|
||||
instance.setLock(true);
|
||||
instance.status(Instance.STATUS_STARTING);
|
||||
instance.startCount++;
|
||||
|
||||
@ -210,7 +208,7 @@ export default class PtyStartCommand extends InstanceCommand {
|
||||
];
|
||||
|
||||
logger.info("----------------");
|
||||
logger.info($t("TXT_CODE_pty_start.sourceRequest", { source: source }));
|
||||
logger.info($t("TXT_CODE_pty_start.sourceRequest", { source: "" }));
|
||||
logger.info($t("TXT_CODE_pty_start.instanceUuid", { instanceUuid: instance.instanceUuid }));
|
||||
logger.info($t("TXT_CODE_pty_start.startCmd", { cmd: commandList.join(" ") }));
|
||||
logger.info($t("TXT_CODE_pty_start.ptyPath", { path: PTY_PATH }));
|
||||
@ -246,7 +244,6 @@ export default class PtyStartCommand extends InstanceCommand {
|
||||
// create process adapter
|
||||
const ptySubProcessCfg = await this.readPtySubProcessConfig(subProcess);
|
||||
const processAdapter = new GoPtyProcessAdapter(subProcess, ptySubProcessCfg.pid, pipeName);
|
||||
logger.info(`pty.exe subprocess PID: ${JSON.stringify(ptySubProcessCfg)}`);
|
||||
|
||||
// After reading the configuration, Need to check the process status
|
||||
// The "processAdapter.pid" here represents the process created by the PTY process
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import SendCommand from "../cmd";
|
||||
|
||||
export default class PtyStopCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("PtyStopCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
let stopCommand = instance.config.stopCommand;
|
||||
|
||||
if (instance.status() === Instance.STATUS_STOP || !instance.process)
|
||||
return instance.failure(new Error($t("TXT_CODE_pty_stop.notRunning")));
|
||||
instance.status(Instance.STATUS_STOPPING);
|
||||
|
||||
instance.println("INFO", $t("TXT_CODE_pty_stop.execCmd", { stopCommand: stopCommand }));
|
||||
|
||||
const stopCommandList = stopCommand.split("\n");
|
||||
for (const stopCommandColumn of stopCommandList) {
|
||||
if (stopCommandColumn.toLocaleLowerCase() == "^c") {
|
||||
await instance.exec(new SendCommand("\x03"));
|
||||
} else {
|
||||
await instance.exec(new SendCommand(stopCommandColumn));
|
||||
}
|
||||
}
|
||||
|
||||
// If the instance is still in the stopped state after 10 minutes, restore the state
|
||||
const cacheStartCount = instance.startCount;
|
||||
setTimeout(() => {
|
||||
if (
|
||||
instance.status() === Instance.STATUS_STOPPING &&
|
||||
instance.startCount === cacheStartCount
|
||||
) {
|
||||
instance.println("ERROR", $t("TXT_CODE_pty_stop.stopErr"));
|
||||
instance.status(Instance.STATUS_RUNNING);
|
||||
}
|
||||
}, 1000 * 60 * 10);
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { $t } from "../../i18n";
|
||||
import Instance from "../instance/instance";
|
||||
import InstanceCommand from "./base/command";
|
||||
|
||||
export default class RestartCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("RestartCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
// If the automatic restart function is enabled, the setting is ignored once
|
||||
if (instance.config.eventTask && instance.config.eventTask.autoRestart)
|
||||
instance.config.eventTask.ignore = true;
|
||||
|
||||
if (instance.status() !== Instance.STATUS_RUNNING) {
|
||||
throw new Error($t("TXT_CODE_d58ffa0f"));
|
||||
}
|
||||
|
||||
return await instance.execPreset("restart");
|
||||
}
|
||||
}
|
@ -2,55 +2,46 @@ import { $t } from "../../i18n";
|
||||
import Instance from "../instance/instance";
|
||||
import InstanceCommand from "./base/command";
|
||||
|
||||
class StartupError extends Error {
|
||||
export class StartupError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export default class StartCommand extends InstanceCommand {
|
||||
public source: string;
|
||||
|
||||
constructor(source = "Unknown") {
|
||||
super("StartCommand");
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
export default abstract class AbsStartCommand extends InstanceCommand {
|
||||
private async sleep() {
|
||||
return new Promise((ok) => {
|
||||
setTimeout(ok, 1000 * 3);
|
||||
setTimeout(ok, 1000 * 2);
|
||||
});
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
if (instance.status() !== Instance.STATUS_STOP)
|
||||
return instance.failure(new StartupError($t("TXT_CODE_start.instanceNotDown")));
|
||||
|
||||
try {
|
||||
instance.setLock(true);
|
||||
instance.status(Instance.STATUS_STARTING);
|
||||
instance.startCount++;
|
||||
|
||||
// expiration time check
|
||||
instance.startTimestamp = Date.now();
|
||||
|
||||
if (instance.config.endTime) {
|
||||
const endTime = instance.config.endTime;
|
||||
if (endTime) {
|
||||
const currentTime = Date.now();
|
||||
if (endTime <= currentTime) {
|
||||
if (endTime <= instance.startTimestamp) {
|
||||
throw new Error($t("TXT_CODE_start.instanceMaturity"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentTimestamp = Date.now();
|
||||
instance.startTimestamp = currentTimestamp;
|
||||
|
||||
instance.print("\n");
|
||||
instance.print("\n\n");
|
||||
instance.println("INFO", $t("TXT_CODE_start.startInstance"));
|
||||
|
||||
// prevent the dead-loop from starting
|
||||
await this.sleep();
|
||||
|
||||
return await instance.execPreset("start", this.source);
|
||||
return await this.createProcess(instance);
|
||||
} catch (error: any) {
|
||||
try {
|
||||
await instance.execPreset("kill");
|
||||
@ -62,4 +53,6 @@ export default class StartCommand extends InstanceCommand {
|
||||
instance.setLock(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract createProcess(instance: Instance): Promise<void>;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { t } from "i18next";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import Rcon from "rcon-srcds";
|
||||
import { isExitCommand } from "../general/general_command";
|
||||
|
||||
async function sendRconCommand(instance: Instance, command: string) {
|
||||
const targetIp = instance.config.rconIp || "localhost";
|
||||
@ -45,6 +46,7 @@ export default class RconCommand extends InstanceCommand {
|
||||
}
|
||||
|
||||
async exec(instance: Instance, text?: string): Promise<any> {
|
||||
if (isExitCommand(instance, text)) return;
|
||||
try {
|
||||
if (text || this.cmd) {
|
||||
await sendRconCommand(instance, String(text ?? this.cmd));
|
||||
|
@ -1,18 +0,0 @@
|
||||
import Instance from "../instance/instance";
|
||||
import InstanceCommand from "./base/command";
|
||||
import SendCommand from "./cmd";
|
||||
|
||||
export default class StopCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("StopCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
// If the automatic restart function is enabled, the setting is ignored once
|
||||
if (instance.config.eventTask && instance.config.eventTask.autoRestart)
|
||||
instance.config.eventTask.ignore = true;
|
||||
|
||||
// send stop command
|
||||
return await instance.execPreset("stop");
|
||||
}
|
||||
}
|
@ -1,12 +1,8 @@
|
||||
import { v4 } from "uuid";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { spawn, ChildProcess } from "child_process";
|
||||
import os from "os";
|
||||
import { killProcess } from "common";
|
||||
import { ILifeCycleTask } from "../../instance/life_cycle";
|
||||
import Instance from "../../instance/instance";
|
||||
import KillCommand from "../kill";
|
||||
import logger from "../../../service/log";
|
||||
import { $t } from "../../../i18n";
|
||||
import { ProcessWrapper } from "common";
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { ILifeCycleTask } from "../../instance/life_cycle";
|
||||
import Instance from "../../instance/instance";
|
||||
import KillCommand from "../kill";
|
||||
|
||||
// When the instance is running, continue to check the expiration time
|
||||
export default class TimeCheck implements ILifeCycleTask {
|
||||
@ -17,7 +16,7 @@ export default class TimeCheck implements ILifeCycleTask {
|
||||
const currentTime = Date.now();
|
||||
if (endTime <= currentTime) {
|
||||
// Expired, execute the end process command
|
||||
await instance.exec(new KillCommand());
|
||||
await instance.execPreset("kill");
|
||||
clearInterval(this.task);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
import Instance from "../instance/instance";
|
||||
import InstanceCommand from "./base/command";
|
||||
import SendCommand from "./cmd";
|
||||
|
||||
export default class UpdateCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("UpdateCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
// Execute the update preset, the preset and function scheduler are set before starting
|
||||
return await instance.execPreset("update");
|
||||
}
|
||||
}
|
@ -66,7 +66,8 @@ export default class InstanceConfig implements IGlobalInstanceConfig {
|
||||
io: 0,
|
||||
network: 0,
|
||||
workingDir: "/workspace/",
|
||||
env: []
|
||||
env: [],
|
||||
changeWorkdir: true
|
||||
};
|
||||
|
||||
public pingConfig = {
|
||||
|
@ -209,6 +209,7 @@ export default class Instance extends EventEmitter {
|
||||
configureEntityParams(this.config.docker, cfg.docker, "cpuUsage", Number);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "env");
|
||||
configureEntityParams(this.config.docker, cfg.docker, "workingDir", String);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "changeWorkdir", Boolean);
|
||||
}
|
||||
if (cfg.pingConfig) {
|
||||
configureEntityParams(this.config.pingConfig, cfg.pingConfig, "ip", String);
|
||||
@ -228,23 +229,12 @@ export default class Instance extends EventEmitter {
|
||||
}
|
||||
|
||||
setLock(bool: boolean) {
|
||||
if (this.lock === true && bool === true) {
|
||||
throw new Error($t("TXT_CODE_ca030197"));
|
||||
}
|
||||
this.lock = bool;
|
||||
}
|
||||
|
||||
// Execute the corresponding command for this instance
|
||||
async execCommand(command: InstanceCommand) {
|
||||
if (this.lock)
|
||||
throw new Error($t("TXT_CODE_instanceConf.instanceLock", { info: command.info }));
|
||||
if (this.status() == Instance.STATUS_BUSY)
|
||||
throw new Error($t("TXT_CODE_instanceConf.instanceBusy"));
|
||||
return await command.exec(this);
|
||||
}
|
||||
|
||||
// Execute the corresponding command for this instance Alias
|
||||
async exec(command: InstanceCommand) {
|
||||
return await this.execCommand(command);
|
||||
}
|
||||
|
||||
// force the command to execute
|
||||
async forceExec(command: InstanceCommand) {
|
||||
return await command.exec(this);
|
||||
@ -288,6 +278,7 @@ export default class Instance extends EventEmitter {
|
||||
this.releaseResources();
|
||||
if (this.instanceStatus != Instance.STATUS_STOP) {
|
||||
this.instanceStatus = Instance.STATUS_STOP;
|
||||
this.startTimestamp = 0;
|
||||
this.emit("exit", code);
|
||||
StorageSubsystem.store("InstanceConfig", this.instanceUuid, this.config);
|
||||
}
|
||||
@ -297,30 +288,33 @@ export default class Instance extends EventEmitter {
|
||||
this.lifeCycleTaskManager.execLifeCycleTask(0);
|
||||
|
||||
// If automatic restart is enabled, the startup operation is performed immediately
|
||||
if (this.config.eventTask.autoRestart) {
|
||||
if (!this.config.eventTask.ignore) {
|
||||
this.forceExec(new StartCommand("Event Task: Auto Restart"))
|
||||
.then(() => {
|
||||
this.println($t("TXT_CODE_instanceConf.info"), $t("TXT_CODE_instanceConf.autoRestart"));
|
||||
})
|
||||
.catch((err) => {
|
||||
this.println(
|
||||
$t("TXT_CODE_instanceConf.error"),
|
||||
$t("TXT_CODE_instanceConf.autoRestartErr", { err: err })
|
||||
);
|
||||
});
|
||||
}
|
||||
this.config.eventTask.ignore = false;
|
||||
if (!this.config.eventTask.ignore && this.config.eventTask.autoRestart) {
|
||||
this.execPreset("start")
|
||||
.then(() => {
|
||||
this.println($t("TXT_CODE_instanceConf.info"), $t("TXT_CODE_instanceConf.autoRestart"));
|
||||
})
|
||||
.catch((err) => {
|
||||
this.println(
|
||||
$t("TXT_CODE_instanceConf.error"),
|
||||
$t("TXT_CODE_instanceConf.autoRestartErr", { err: err })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
this.config.eventTask.ignore = false;
|
||||
|
||||
// Turn off the warning immediately after startup, usually the startup command is written incorrectly
|
||||
const currentTimestamp = new Date().getTime();
|
||||
const startThreshold = 6 * 1000;
|
||||
const startThreshold = 2 * 1000;
|
||||
if (currentTimestamp - this.startTimestamp < startThreshold) {
|
||||
this.println("ERROR", $t("TXT_CODE_instanceConf.instantExit"));
|
||||
this.println("ERROR", $t("TXT_CODE_aae2918f"));
|
||||
}
|
||||
}
|
||||
|
||||
ignoreEventTaskOnce() {
|
||||
if (this.config.eventTask) this.config.eventTask.ignore = true;
|
||||
}
|
||||
|
||||
// custom output method, formatting
|
||||
println(level: string, text: string) {
|
||||
const str = `[${level}] ${text}\n`;
|
||||
@ -361,9 +355,6 @@ export default class Instance extends EventEmitter {
|
||||
}
|
||||
|
||||
absoluteCwdPath() {
|
||||
const envInstancesBasePath = toText(process.env.MCSM_INSTANCES_BASE_PATH);
|
||||
if (envInstancesBasePath)
|
||||
return path.normalize(path.join(envInstancesBasePath, this.instanceUuid));
|
||||
if (!this.config || !this.config.cwd) throw new Error("Instance config error, cwd is Null!");
|
||||
if (path.isAbsolute(this.config.cwd)) return path.normalize(this.config.cwd);
|
||||
return path.normalize(path.join(process.cwd(), this.config.cwd));
|
||||
@ -413,6 +404,13 @@ export default class Instance extends EventEmitter {
|
||||
this.info.latency = 0;
|
||||
}
|
||||
|
||||
public parseTextParams(text: string) {
|
||||
text = text.replace(/\{mcsm_workspace\}/gim, this.absoluteCwdPath());
|
||||
text = text.replace(/\{mcsm_instance_id\}/gim, this.instanceUuid);
|
||||
text = text.replace(/\{mcsm_cwd\}/gim, this.absoluteCwdPath());
|
||||
return text;
|
||||
}
|
||||
|
||||
private pushOutput(data: string) {
|
||||
if (data.length > LINE_MAX_SIZE * 100) {
|
||||
this.outputStack.push(IGNORE_TEXT);
|
||||
|
@ -61,7 +61,7 @@ export class ProcessConfig {
|
||||
}
|
||||
}
|
||||
if (this.iProcessConfig.type === "json") {
|
||||
text = JSON.stringify(object);
|
||||
text = JSON.stringify(object, null, 4);
|
||||
}
|
||||
if (this.iProcessConfig.type === "txt") {
|
||||
text = object.toString();
|
||||
|
@ -7,19 +7,14 @@ import Instance from "../entity/instance/instance";
|
||||
import logger from "../service/log";
|
||||
import path from "path";
|
||||
|
||||
import StartCommand from "../entity/commands/start";
|
||||
import StopCommand from "../entity/commands/stop";
|
||||
import SendCommand from "../entity/commands/cmd";
|
||||
import KillCommand from "../entity/commands/kill";
|
||||
import { IInstanceDetail, IJson } from "../service/interfaces";
|
||||
import ProcessInfoCommand from "../entity/commands/process_info";
|
||||
import FileManager from "../service/system_file";
|
||||
import { ProcessConfig } from "../entity/instance/process_config";
|
||||
import RestartCommand from "../entity/commands/restart";
|
||||
import { TaskCenter } from "../service/async_task_service";
|
||||
import { createQuickInstallTask } from "../service/async_task_service/quick_install";
|
||||
import { QuickInstallTask } from "../service/async_task_service/quick_install";
|
||||
import { toNumber, toText } from "common";
|
||||
import { toNumber } from "common";
|
||||
import { arrayUnique } from "common";
|
||||
|
||||
// Some instances operate router authentication middleware
|
||||
@ -66,9 +61,11 @@ routerApp.on("instance/select", (ctx, data) => {
|
||||
return false;
|
||||
if (condition.status && v.instanceStatus !== Number(condition.status)) return false;
|
||||
|
||||
const curInstanceTagText = v.config.tag.join(",");
|
||||
if (searchTags.length > 0 && searchTags.some((v) => !curInstanceTagText.includes(v)))
|
||||
return false;
|
||||
if (searchTags.length > 0) {
|
||||
const myTags = v.config.tag || [];
|
||||
const res = myTags.filter((v) => searchTags.includes(v));
|
||||
if (res.length === 0 || res.length !== searchTags.length) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
result = result.sort((a, b) => (a.config.nickname > b.config.nickname ? 1 : -1));
|
||||
@ -86,6 +83,9 @@ routerApp.on("instance/select", (ctx, data) => {
|
||||
});
|
||||
|
||||
overview.sort((a, b) => {
|
||||
if (a.status !== b.status) {
|
||||
return b.status - a.status;
|
||||
}
|
||||
return a.config.nickname >= b.config.nickname ? 1 : -1;
|
||||
});
|
||||
|
||||
@ -221,7 +221,7 @@ routerApp.on("instance/open", async (ctx, data) => {
|
||||
for (const instanceUuid of data.instanceUuids) {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
try {
|
||||
await instance!.exec(new StartCommand(ctx.socket.id));
|
||||
await instance!.execPreset("start");
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/open", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
if (!disableResponse) {
|
||||
@ -242,7 +242,7 @@ routerApp.on("instance/stop", async (ctx, data) => {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
try {
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
await instance.exec(new StopCommand());
|
||||
await instance.execPreset("stop");
|
||||
//Note: Removing this reply will cause the front-end response to be slow, because the front-end will wait for the panel-side message to be forwarded
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/stop", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
@ -259,7 +259,7 @@ routerApp.on("instance/restart", async (ctx, data) => {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
try {
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
await instance.exec(new RestartCommand());
|
||||
await instance.execPreset("restart");
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/restart", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
if (!disableResponse)
|
||||
@ -275,7 +275,7 @@ routerApp.on("instance/kill", async (ctx, data) => {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
if (!instance) continue;
|
||||
try {
|
||||
await instance.forceExec(new KillCommand());
|
||||
await instance.execPreset("kill");
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/kill", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
if (!disableResponse)
|
||||
@ -292,7 +292,7 @@ routerApp.on("instance/command", async (ctx, data) => {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
try {
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
await instance.exec(new SendCommand(command));
|
||||
await instance.execPreset("command", command);
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/command", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
if (!disableResponse)
|
||||
|
@ -55,8 +55,8 @@ router.get("/download/:key/:fileName", async (ctx) => {
|
||||
|
||||
// File upload route
|
||||
router.post("/upload/:key", async (ctx) => {
|
||||
const key = ctx.params.key;
|
||||
const unzip = ctx.query.unzip;
|
||||
const key = String(ctx.params.key);
|
||||
const unzip = Boolean(ctx.query.unzip);
|
||||
const zipCode = String(ctx.query.code);
|
||||
let tmpFiles: formidable.File | formidable.File[] | undefined;
|
||||
try {
|
||||
@ -120,8 +120,8 @@ router.post("/upload/:key", async (ctx) => {
|
||||
});
|
||||
|
||||
if (unzip) {
|
||||
const fileManager = new FileManager(instance.absoluteCwdPath());
|
||||
fileManager.unzip(fileSaveAbsolutePath, "", zipCode);
|
||||
const instanceFiles = new FileManager(instance.absoluteCwdPath());
|
||||
instanceFiles.unzip(fileSaveAbsolutePath, ".", zipCode);
|
||||
}
|
||||
ctx.body = "OK";
|
||||
return;
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
streamLoginSuccessful
|
||||
} from "../service/mission_passport";
|
||||
import InstanceSubsystem from "../service/system_instance";
|
||||
import SendCommand from "../entity/commands/cmd";
|
||||
import { IGNORE } from "../const";
|
||||
|
||||
// Authorization authentication middleware
|
||||
@ -79,7 +78,7 @@ routerApp.on("stream/input", async (ctx, data) => {
|
||||
const command = data.command;
|
||||
const instanceUuid = ctx.session?.stream?.instanceUuid;
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
await instance?.exec(new SendCommand(command));
|
||||
await instance?.execPreset("command", command);
|
||||
} catch (error: any) {
|
||||
// Ignore potential high frequency exceptions here
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ import { EventEmitter } from "stream";
|
||||
import { IInstanceProcess } from "../entity/instance/interface";
|
||||
import { AsyncTask } from "./async_task_service";
|
||||
import iconv from "iconv-lite";
|
||||
import { toText } from "common";
|
||||
import fs from "fs-extra";
|
||||
|
||||
// Error exception at startup
|
||||
export class StartupDockerProcessError extends Error {
|
||||
@ -83,19 +85,19 @@ export class SetupDockerContainer extends AsyncTask {
|
||||
}
|
||||
|
||||
// memory limit
|
||||
let maxMemory = undefined;
|
||||
let maxMemory: number | undefined = undefined;
|
||||
if (instance.config.docker.memory) maxMemory = instance.config.docker.memory * 1024 * 1024;
|
||||
|
||||
// CPU usage calculation
|
||||
let cpuQuota = undefined;
|
||||
let cpuPeriod = undefined;
|
||||
let cpuQuota: number | undefined = undefined;
|
||||
let cpuPeriod: number | undefined = undefined;
|
||||
if (instance.config.docker.cpuUsage) {
|
||||
cpuQuota = instance.config.docker.cpuUsage * 10 * 1000;
|
||||
cpuPeriod = 1000 * 1000;
|
||||
}
|
||||
|
||||
// Check the number of CPU cores
|
||||
let cpusetCpus = undefined;
|
||||
let cpusetCpus: string | undefined = undefined;
|
||||
if (instance.config.docker.cpusetCpus) {
|
||||
const arr = instance.config.docker.cpusetCpus.split(",");
|
||||
arr.forEach((v) => {
|
||||
@ -115,16 +117,36 @@ export class SetupDockerContainer extends AsyncTask {
|
||||
// Whether to use TTY mode
|
||||
const isTty = instance.config.terminalOption.pty;
|
||||
|
||||
const workingDir = instance.config.docker.workingDir ?? "";
|
||||
const cwd = instance.absoluteCwdPath();
|
||||
const workingDir = instance.config.docker.workingDir || undefined;
|
||||
|
||||
let cwd = instance.absoluteCwdPath();
|
||||
const hostRealPath = toText(process.env.MCSM_DOCKER_WORKSPACE_PATH);
|
||||
if (hostRealPath) {
|
||||
cwd = path.normalize(path.join(hostRealPath, instance.instanceUuid));
|
||||
}
|
||||
|
||||
if (workingDir) {
|
||||
instance.println("CONTAINER", $t("TXT_CODE_e76e49e9") + cwd + " --> " + workingDir + "\n");
|
||||
} else {
|
||||
instance.println("CONTAINER", $t("TXT_CODE_ffa884f9"));
|
||||
}
|
||||
|
||||
// output startup log
|
||||
const mounts: Docker.MountConfig =
|
||||
extraBinds.map((v) => {
|
||||
const hostPath = instance.parseTextParams(v.hostPath);
|
||||
if (!fs.existsSync(hostPath)) fs.mkdirsSync(hostPath);
|
||||
return {
|
||||
Type: "bind",
|
||||
Source: hostPath,
|
||||
Target: instance.parseTextParams(v.containerPath)
|
||||
};
|
||||
}) || [];
|
||||
if (workingDir && cwd) {
|
||||
mounts.push({
|
||||
Type: "bind",
|
||||
Source: cwd,
|
||||
Target: instance.parseTextParams(workingDir)
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("----------------");
|
||||
logger.info(`[SetupDockerContainer]`);
|
||||
logger.info(`UUID: [${instance.instanceUuid}] [${instance.config.nickname}]`);
|
||||
@ -133,33 +155,12 @@ export class SetupDockerContainer extends AsyncTask {
|
||||
logger.info(`CWD: ${cwd}, WORKING_DIR: ${workingDir}`);
|
||||
logger.info(`NET_MODE: ${instance.config.docker.networkMode}`);
|
||||
logger.info(`OPEN_PORT: ${JSON.stringify(publicPortArray)}`);
|
||||
logger.info(
|
||||
`BINDS: ${JSON.stringify([
|
||||
workingDir ? `${cwd} --> ${workingDir}` : "<Working directory not mounted>",
|
||||
...extraBinds
|
||||
])}`
|
||||
);
|
||||
logger.info(`Volume Mounts: ${JSON.stringify(mounts)}`);
|
||||
logger.info(`NET_ALIASES: ${JSON.stringify(instance.config.docker.networkAliases)}`);
|
||||
logger.info(`MEM_LIMIT: ${maxMemory || "--"} MB`);
|
||||
logger.info(`TYPE: Docker Container`);
|
||||
logger.info("----------------");
|
||||
|
||||
const mounts: Docker.MountConfig =
|
||||
extraBinds.map((v) => {
|
||||
return {
|
||||
Type: "bind",
|
||||
Source: v.hostPath,
|
||||
Target: v.containerPath
|
||||
};
|
||||
}) || [];
|
||||
if (workingDir && cwd) {
|
||||
mounts.push({
|
||||
Type: "bind",
|
||||
Source: cwd,
|
||||
Target: workingDir
|
||||
});
|
||||
}
|
||||
|
||||
// Start Docker container creation and running
|
||||
const docker = new DefaultDocker();
|
||||
this.container = await docker.createContainer({
|
||||
@ -170,12 +171,13 @@ export class SetupDockerContainer extends AsyncTask {
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: isTty,
|
||||
WorkingDir: workingDir,
|
||||
Cmd: commandList ? commandList : undefined,
|
||||
WorkingDir: instance.config.docker.changeWorkdir ? workingDir : undefined,
|
||||
Cmd: commandList.length > 0 ? commandList : undefined,
|
||||
OpenStdin: true,
|
||||
StdinOnce: false,
|
||||
ExposedPorts: exposedPorts,
|
||||
Env: instance.config.docker?.env || [],
|
||||
|
||||
HostConfig: {
|
||||
Memory: maxMemory,
|
||||
AutoRemove: true,
|
||||
@ -198,7 +200,7 @@ export class SetupDockerContainer extends AsyncTask {
|
||||
await this.container.start();
|
||||
|
||||
// Listen to events
|
||||
this.container.wait(async (v) => {
|
||||
this.container.wait(() => {
|
||||
this.stop();
|
||||
});
|
||||
}
|
||||
|
@ -19,8 +19,7 @@ export class InstanceUpdateAction extends AsyncTask {
|
||||
}
|
||||
|
||||
public async onStart() {
|
||||
let updateCommand = this.instance.config.updateCommand;
|
||||
updateCommand = updateCommand.replace(/\{mcsm_workspace\}/gm, this.instance.absoluteCwdPath());
|
||||
const updateCommand = this.instance.parseTextParams(this.instance.config.updateCommand);
|
||||
logger.info(
|
||||
$t("TXT_CODE_general_update.readyUpdate", { instanceUuid: this.instance.instanceUuid })
|
||||
);
|
||||
@ -29,6 +28,7 @@ export class InstanceUpdateAction extends AsyncTask {
|
||||
);
|
||||
logger.info(updateCommand);
|
||||
|
||||
this.instance.print("\n");
|
||||
this.instance.println(
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
$t("TXT_CODE_general_update.readyUpdate", { instanceUuid: this.instance.instanceUuid })
|
||||
|
@ -6,6 +6,7 @@ interface IMission {
|
||||
start: number;
|
||||
end: number;
|
||||
count?: number;
|
||||
isDeleted?: boolean;
|
||||
}
|
||||
|
||||
// Task passport manager
|
||||
@ -18,9 +19,9 @@ class MissionPassport {
|
||||
setInterval(() => {
|
||||
const t = new Date().getTime();
|
||||
this.missions.forEach((m, k) => {
|
||||
if (t > m.end) this.missions.delete(k);
|
||||
if (t > m.end || m.isDeleted) this.missions.delete(k);
|
||||
});
|
||||
}, 1000);
|
||||
}, 1000 * 60);
|
||||
}
|
||||
|
||||
// register task passport
|
||||
@ -39,7 +40,8 @@ class MissionPassport {
|
||||
}
|
||||
|
||||
public deleteMission(password: string) {
|
||||
this.missions.delete(password);
|
||||
const m = this.missions.get(password);
|
||||
if (m) m.isDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,10 @@ export default class FileManager {
|
||||
|
||||
toAbsolutePath(fileName: string = "") {
|
||||
const topAbsolutePath = this.topPath;
|
||||
let finalPath: string = "";
|
||||
|
||||
if (path.normalize(fileName).indexOf(topAbsolutePath) === 0) return fileName;
|
||||
|
||||
let finalPath = "";
|
||||
if (os.platform() === "win32") {
|
||||
const reg = new RegExp("^[A-Za-z]{1}:[\\\\/]{1}");
|
||||
if (reg.test(this.cwd)) {
|
||||
@ -48,6 +51,7 @@ export default class FileManager {
|
||||
finalPath = path.normalize(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalPath) {
|
||||
finalPath = path.normalize(path.join(this.topPath, this.cwd, fileName));
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import path from "path";
|
||||
import os from "os";
|
||||
import Instance from "../entity/instance/instance";
|
||||
import EventEmitter from "events";
|
||||
import KillCommand from "../entity/commands/kill";
|
||||
import logger from "./log";
|
||||
import { v4 } from "uuid";
|
||||
import { Socket } from "socket.io";
|
||||
@ -13,7 +12,6 @@ import InstanceConfig from "../entity/instance/Instance_config";
|
||||
import { QueryMapWrapper, InstanceStreamListener } from "common";
|
||||
import FunctionDispatcher from "../entity/commands/dispatcher";
|
||||
import InstanceControl from "./system_instance_control";
|
||||
import StartCommand from "../entity/commands/start";
|
||||
import { globalConfiguration } from "../entity/config";
|
||||
|
||||
// init instance default install path
|
||||
@ -44,25 +42,27 @@ class InstanceSubsystem extends EventEmitter {
|
||||
private autoStart() {
|
||||
this.instances.forEach((instance) => {
|
||||
if (instance.config.eventTask.autoStart) {
|
||||
instance
|
||||
.exec(new StartCommand())
|
||||
.then(() => {
|
||||
logger.info(
|
||||
$t("TXT_CODE_system_instance.autoStart", {
|
||||
name: instance.config.nickname,
|
||||
uuid: instance.instanceUuid
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((reason) => {
|
||||
logger.error(
|
||||
$t("TXT_CODE_system_instance.autoStartErr", {
|
||||
name: instance.config.nickname,
|
||||
uuid: instance.instanceUuid,
|
||||
reason: reason
|
||||
})
|
||||
);
|
||||
});
|
||||
setTimeout(() => {
|
||||
instance
|
||||
.execPreset("start")
|
||||
.then(() => {
|
||||
logger.info(
|
||||
$t("TXT_CODE_system_instance.autoStart", {
|
||||
name: instance.config.nickname,
|
||||
uuid: instance.instanceUuid
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((reason) => {
|
||||
logger.error(
|
||||
$t("TXT_CODE_system_instance.autoStartErr", {
|
||||
name: instance.config.nickname,
|
||||
uuid: instance.instanceUuid,
|
||||
reason: reason
|
||||
})
|
||||
);
|
||||
});
|
||||
}, 1000 * 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -108,7 +108,6 @@ class InstanceSubsystem extends EventEmitter {
|
||||
this.GLOBAL_INSTANCE_UUID
|
||||
);
|
||||
|
||||
// handle autostart
|
||||
this.autoStart();
|
||||
}
|
||||
|
||||
@ -228,7 +227,7 @@ class InstanceSubsystem extends EventEmitter {
|
||||
`Instance ${instance.config.nickname} (${instance.instanceUuid}) is running or busy, and is being forced to end.`
|
||||
);
|
||||
promises.push(
|
||||
instance.execCommand(new KillCommand()).then(() => {
|
||||
instance.execPreset("kill").then(() => {
|
||||
if (!this.isGlobalInstance(instance))
|
||||
StorageSubsystem.store("InstanceConfig", instance.instanceUuid, instance.config);
|
||||
logger.info(
|
||||
|
@ -3,11 +3,6 @@ import schedule from "node-schedule";
|
||||
import InstanceSubsystem from "./system_instance";
|
||||
import StorageSubsystem from "../common/system_storage";
|
||||
import logger from "./log";
|
||||
import StartCommand from "../entity/commands/start";
|
||||
import StopCommand from "../entity/commands/stop";
|
||||
import SendCommand from "../entity/commands/cmd";
|
||||
import RestartCommand from "../entity/commands/restart";
|
||||
import KillCommand from "../entity/commands/kill";
|
||||
import FileManager from "./system_file";
|
||||
|
||||
// Scheduled task configuration item interface
|
||||
@ -170,26 +165,26 @@ class InstanceControlSubsystem {
|
||||
// logger.info(`Execute scheduled task: ${task.name} ${task.action} ${task.time} ${task.count} `);
|
||||
if (task.action === "start") {
|
||||
if (instanceStatus === 0) {
|
||||
return await instance.exec(new StartCommand("ScheduleJob"));
|
||||
return await instance.execPreset("start");
|
||||
}
|
||||
}
|
||||
if (task.action === "stop") {
|
||||
if (instanceStatus === 3) {
|
||||
return await instance.exec(new StopCommand());
|
||||
return await instance.execPreset("stop");
|
||||
}
|
||||
}
|
||||
if (task.action === "restart") {
|
||||
if (instanceStatus === 3) {
|
||||
return await instance.exec(new RestartCommand());
|
||||
return await instance.execPreset("restart");
|
||||
}
|
||||
}
|
||||
if (task.action === "command") {
|
||||
if (instanceStatus === 3) {
|
||||
return await instance.exec(new SendCommand(payload));
|
||||
return await instance.execPreset("command", payload);
|
||||
}
|
||||
}
|
||||
if (task.action === "kill") {
|
||||
return await instance.exec(new KillCommand());
|
||||
return await instance.execPreset("kill");
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
|
@ -1,118 +0,0 @@
|
||||
import { $t } from "../i18n";
|
||||
import readline from "readline";
|
||||
|
||||
import * as protocol from "./protocol";
|
||||
import InstanceSubsystem from "./system_instance";
|
||||
import { globalConfiguration } from "../entity/config";
|
||||
import logger from "./log";
|
||||
import StartCommand from "../entity/commands/start";
|
||||
import StopCommand from "../entity/commands/stop";
|
||||
import KillCommand from "../entity/commands/kill";
|
||||
import SendCommand from "../entity/commands/cmd";
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
console.log($t("TXT_CODE_ui.help"));
|
||||
|
||||
function stdin() {
|
||||
rl.question("> ", async (answer) => {
|
||||
try {
|
||||
const cmds = answer.split(" ");
|
||||
logger.info(`[Terminal] ${answer}`);
|
||||
const result = await command(cmds[0], cmds[1], cmds[2], cmds[3]);
|
||||
if (result) console.log(result);
|
||||
else console.log(`Command ${answer} does not exist, type help to get help.`);
|
||||
} catch (err) {
|
||||
logger.error("[Terminal]", err);
|
||||
} finally {
|
||||
// next
|
||||
stdin();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stdin();
|
||||
|
||||
/**
|
||||
* Pass in relevant UI commands and output command results
|
||||
* @param {String} cmd
|
||||
* @return {String}
|
||||
*/
|
||||
async function command(cmd: string, p1: string, p2: string, p3: string) {
|
||||
if (cmd === "instance") {
|
||||
if (p1 === "start") {
|
||||
InstanceSubsystem?.getInstance(p2)?.exec(new StartCommand("Terminal"));
|
||||
return "Done.";
|
||||
}
|
||||
if (p1 === "stop") {
|
||||
InstanceSubsystem?.getInstance(p2)?.exec(new StopCommand());
|
||||
return "Done.";
|
||||
}
|
||||
if (p1 === "kill") {
|
||||
InstanceSubsystem?.getInstance(p2)?.exec(new KillCommand());
|
||||
return "Done.";
|
||||
}
|
||||
if (p1 === "send") {
|
||||
InstanceSubsystem?.getInstance(p2)?.exec(new SendCommand(p3));
|
||||
return "Done.";
|
||||
}
|
||||
return "Parameter error";
|
||||
}
|
||||
|
||||
if (cmd === "instances") {
|
||||
const objs = InstanceSubsystem.instances;
|
||||
let result = "instance name | instance UUID | status code\n";
|
||||
objs.forEach((v) => {
|
||||
result += `${v.config.nickname} ${v.instanceUuid} ${v.status()}\n`;
|
||||
});
|
||||
result += "\nStatus Explanation:\n Busy=-1;Stop=0;Stopping=1;Starting=2;Running=3;\n";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (cmd === "sockets") {
|
||||
const sockets = protocol.socketObjects();
|
||||
let result = "IP address | identifier\n";
|
||||
sockets.forEach((v) => {
|
||||
result += `${v.handshake.address} ${v.id}\n`;
|
||||
});
|
||||
result += `Total ${sockets.size} online.\n`;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (cmd == "key") {
|
||||
return globalConfiguration.config.key;
|
||||
}
|
||||
|
||||
if (cmd == "exit") {
|
||||
try {
|
||||
logger.info("Preparing to shut down the daemon...");
|
||||
await InstanceSubsystem.exit();
|
||||
// logger.info("Data saved, thanks for using, goodbye!");
|
||||
logger.info("The data is saved, thanks for using, goodbye!");
|
||||
logger.info("closed.");
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Failed to end the program. Please check the file permissions and try again. If you still can't close it, please use Ctrl+C to close.",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd == "help") {
|
||||
console.log("----------- Help document -----------");
|
||||
console.log(" instances view all instances");
|
||||
console.log(" Sockets view all linkers");
|
||||
console.log(" key view key");
|
||||
console.log(" exit to close this program (recommended method)");
|
||||
console.log(" instance start <UUID> to start the specified instance");
|
||||
console.log(" instance stop <UUID> to start the specified instance");
|
||||
console.log(" instance kill <UUID> to start the specified instance");
|
||||
console.log(" instance send <UUID> <CMD> to send a command to the instance");
|
||||
console.log("----------- Help document -----------");
|
||||
return "\n";
|
||||
}
|
||||
}
|
36
dockerfile/daemon.dockerfile
Normal file
36
dockerfile/daemon.dockerfile
Normal file
@ -0,0 +1,36 @@
|
||||
ARG EMBEDDED_JAVA_VERSION=21
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} node:lts-alpine AS builder
|
||||
|
||||
WORKDIR /src
|
||||
COPY . /src
|
||||
|
||||
RUN apk add --no-cache wget &&\
|
||||
chmod a+x ./install-dependents.sh &&\
|
||||
chmod a+x ./build.sh &&\
|
||||
./install-dependents.sh &&\
|
||||
./build.sh &&\
|
||||
wget --input-file=lib-urls.txt --directory-prefix=production-code/daemon/lib/ &&\
|
||||
chmod a+x production-code/daemon/lib/*
|
||||
|
||||
FROM eclipse-temurin:${EMBEDDED_JAVA_VERSION}-jdk
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y curl &&\
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash &&\
|
||||
apt-get update && apt-get install -y nodejs && apt-get clean
|
||||
|
||||
WORKDIR /opt/mcsmanager/daemon
|
||||
|
||||
COPY --from=builder /src/production-code/daemon/ /opt/mcsmanager/daemon/
|
||||
|
||||
RUN npm install --production
|
||||
|
||||
EXPOSE 24444
|
||||
|
||||
ENV MCSM_INSTANCES_BASE_PATH=/opt/mcsmanager/daemon/data/InstanceData
|
||||
|
||||
VOLUME ["/opt/mcsmanager/daemon/data", "/opt/mcsmanager/daemon/logs"]
|
||||
|
||||
CMD [ "node", "app.js", "--max-old-space-size=8192" ]
|
24
dockerfile/web.dockerfile
Normal file
24
dockerfile/web.dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
FROM --platform=${BUILDPLATFORM} node:lts-alpine AS builder
|
||||
|
||||
WORKDIR /src
|
||||
COPY . /src
|
||||
|
||||
RUN chmod a+x ./install-dependents.sh &&\
|
||||
chmod a+x ./build.sh &&\
|
||||
./install-dependents.sh &&\
|
||||
./build.sh
|
||||
|
||||
FROM node:lts-alpine
|
||||
|
||||
WORKDIR /opt/mcsmanager/web
|
||||
|
||||
COPY --from=builder /src/production-code/web/ /opt/mcsmanager/web/
|
||||
|
||||
RUN npm install --production
|
||||
|
||||
EXPOSE 23333
|
||||
|
||||
VOLUME ["/opt/mcsmanager/web/data", "/opt/mcsmanager/web/logs"]
|
||||
|
||||
CMD [ "app.js", "--max-old-space-size=8192" ]
|
24
example.docker-compose.yml
Normal file
24
example.docker-compose.yml
Normal file
@ -0,0 +1,24 @@
|
||||
# docker-compose.yml
|
||||
services:
|
||||
web:
|
||||
image: githubyumao/mcsmanager-web:latest
|
||||
ports:
|
||||
- "23333:23333"
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- <CHANGE_ME_TO_INSTALL_PATH>/web/data:/opt/mcsmanager/web/data
|
||||
- <CHANGE_ME_TO_INSTALL_PATH>/web/logs:/opt/mcsmanager/web/logs
|
||||
|
||||
daemon:
|
||||
image: githubyumao/mcsmanager-daemon:latest
|
||||
restart: unless-stopped
|
||||
network_mode: host # if you want run instance in daemon container
|
||||
# ports:
|
||||
# - "24444:24444"
|
||||
environment:
|
||||
- MCSM_DOCKER_WORKSPACE_PATH=<CHANGE_ME_TO_INSTALL_PATH>/daemon/data/InstanceData
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- <CHANGE_ME_TO_INSTALL_PATH>/daemon/data:/opt/mcsmanager/daemon/data
|
||||
- <CHANGE_ME_TO_INSTALL_PATH>/daemon/logs:/opt/mcsmanager/daemon/logs
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
4
frontend/components.d.ts
vendored
4
frontend/components.d.ts
vendored
@ -26,8 +26,10 @@ declare module 'vue' {
|
||||
AEmpty: typeof import('ant-design-vue/es')['Empty']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AImage: typeof import('ant-design-vue/es')['Image']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
|
||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
AList: typeof import('ant-design-vue/es')['List']
|
||||
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
||||
@ -92,6 +94,7 @@ declare module 'vue' {
|
||||
NoPreviewCard: typeof import('./src/components/NoPreviewCard.vue')['default']
|
||||
Params: typeof import('./src/components/NewCardList/params.vue')['default']
|
||||
PlaceHolderCard: typeof import('./src/components/PlaceHolderCard.vue')['default']
|
||||
PurchaseQueryDialog: typeof import('./src/components/fc/PurchaseQueryDialog.vue')['default']
|
||||
ResponsiveLayoutGroup: typeof import('./src/components/ResponsiveLayoutGroup.vue')['default']
|
||||
RightMenu: typeof import('./src/components/fc/RightMenu.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
@ -103,6 +106,7 @@ declare module 'vue' {
|
||||
TaskLoadingDialog: typeof import('./src/components/fc/TaskLoadingDialog.vue')['default']
|
||||
TerminalCore: typeof import('./src/components/TerminalCore.vue')['default']
|
||||
UploadFileDialog: typeof import('./src/components/fc/UploadFileDialog.vue')['default']
|
||||
UseRedeemDialog: typeof import('./src/components/fc/UseRedeemDialog.vue')['default']
|
||||
WarningDialog: typeof import('./src/components/fc/WarningDialog.vue')['default']
|
||||
}
|
||||
}
|
||||
|
116
frontend/package-lock.json
generated
116
frontend/package-lock.json
generated
@ -21,7 +21,7 @@
|
||||
"@uiw/codemirror-theme-dracula": "^4.21.24",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"ant-design-vue": "^4.0.7",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.8.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"crc": "^4.3.2",
|
||||
"dayjs": "^1.11.9",
|
||||
@ -65,11 +65,11 @@
|
||||
"less-loader": "^11.1.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.64.1",
|
||||
"typescript": "~5.1.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.5.3",
|
||||
"vite": "^4.5.13",
|
||||
"vitest": "^0.33.0",
|
||||
"vue-tsc": "^1.8.6"
|
||||
}
|
||||
@ -534,11 +534,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.22.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz",
|
||||
"integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -3084,9 +3085,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
||||
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@ -6508,9 +6510,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -7359,9 +7361,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.0",
|
||||
@ -7511,13 +7514,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-visualizer": {
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz",
|
||||
"integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==",
|
||||
"version": "5.14.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz",
|
||||
"integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"open": "^8.4.0",
|
||||
"picomatch": "^2.3.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"source-map": "^0.7.4",
|
||||
"yargs": "^17.5.1"
|
||||
},
|
||||
@ -7525,12 +7529,16 @@
|
||||
"rollup-plugin-visualizer": "dist/bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rolldown": "1.x",
|
||||
"rollup": "2.x || 3.x || 4.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rolldown": {
|
||||
"optional": true
|
||||
},
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
@ -7577,6 +7585,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-visualizer/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-visualizer/node_modules/source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
@ -8935,10 +8956,11 @@
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
|
||||
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
|
||||
"version": "4.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.13.tgz",
|
||||
"integrity": "sha512-Hgp8IF/yZDzKsN1hQWOuQZbrKiaFsbQud+07jJ8h9m9PaHWkpvZ5u55Xw5yYjWRXwRQ4jwFlJvY7T7FUJG9MCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.27",
|
||||
@ -10053,11 +10075,11 @@
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.22.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz",
|
||||
"integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
@ -11957,9 +11979,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
||||
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@ -14416,9 +14438,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="
|
||||
},
|
||||
"nanopop": {
|
||||
"version": "2.3.0",
|
||||
@ -15015,9 +15037,9 @@
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"regexp.prototype.flags": {
|
||||
"version": "1.5.0",
|
||||
@ -15122,13 +15144,13 @@
|
||||
}
|
||||
},
|
||||
"rollup-plugin-visualizer": {
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz",
|
||||
"integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==",
|
||||
"version": "5.14.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz",
|
||||
"integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"open": "^8.4.0",
|
||||
"picomatch": "^2.3.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"source-map": "^0.7.4",
|
||||
"yargs": "^17.5.1"
|
||||
},
|
||||
@ -15156,6 +15178,12 @@
|
||||
"is-wsl": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
@ -16142,9 +16170,9 @@
|
||||
}
|
||||
},
|
||||
"vite": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
|
||||
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
|
||||
"version": "4.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.13.tgz",
|
||||
"integrity": "sha512-Hgp8IF/yZDzKsN1hQWOuQZbrKiaFsbQud+07jJ8h9m9PaHWkpvZ5u55Xw5yYjWRXwRQ4jwFlJvY7T7FUJG9MCA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild": "^0.18.10",
|
||||
|
@ -8,7 +8,8 @@
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit --skipLibCheck -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
"format": "prettier --write src/",
|
||||
"analyze": "vite build --mode analyze"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
@ -24,7 +25,7 @@
|
||||
"@uiw/codemirror-theme-dracula": "^4.21.24",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"ant-design-vue": "^4.0.7",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.8.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"crc": "^4.3.2",
|
||||
"dayjs": "^1.11.9",
|
||||
@ -68,11 +69,11 @@
|
||||
"less-loader": "^11.1.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.64.1",
|
||||
"typescript": "~5.1.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.5.3",
|
||||
"vite": "^4.5.13",
|
||||
"vitest": "^0.33.0",
|
||||
"vue-tsc": "^1.8.6"
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ html {
|
||||
|
||||
.global-action-float-enter-active,
|
||||
.global-action-float-leave-active {
|
||||
transition: all 0.6s ease;
|
||||
transition: all 0.4s ease;
|
||||
opacity: 1 !important;
|
||||
transform: scale(1);
|
||||
position: absolute;
|
||||
|
@ -45,6 +45,22 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -219,9 +235,9 @@
|
||||
}
|
||||
|
||||
.ellipsis-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@mixin generate-styles($depth) {
|
||||
|
@ -30,8 +30,14 @@
|
||||
|
||||
// Antdv for dark mode
|
||||
.ant-btn-default {
|
||||
background-color: var(--background-color-white);
|
||||
background-color: var(--color-gray-4);
|
||||
box-shadow: 0 2px 0 rgba(55, 55, 55, 0.15);
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
border: 1px solid #424242;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--color-gray-5);
|
||||
border-radius: 8px;
|
||||
|
@ -53,16 +53,21 @@ const menus = computed(() => {
|
||||
return router
|
||||
.getRoutes()
|
||||
.filter((v) => {
|
||||
const metaInfo = v.meta as RouterMetaInfo;
|
||||
if (metaInfo.condition && !metaInfo.condition()) {
|
||||
return false;
|
||||
}
|
||||
if (containerState.isDesignMode) {
|
||||
return v.meta.onlyDisplayEditMode || v.meta.mainMenu;
|
||||
return metaInfo.onlyDisplayEditMode || metaInfo.mainMenu;
|
||||
}
|
||||
if (isAdmin.value) {
|
||||
return v.meta.mainMenu === true && v.meta.onlyDisplayEditMode !== true;
|
||||
return metaInfo.mainMenu === true && metaInfo.onlyDisplayEditMode !== true;
|
||||
}
|
||||
|
||||
return (
|
||||
v.meta.mainMenu === true &&
|
||||
metaInfo.mainMenu === true &&
|
||||
isLogged.value &&
|
||||
Number(appState.userInfo?.permission) >= Number(v.meta.permission)
|
||||
Number(appState.userInfo?.permission) >= Number(metaInfo.permission)
|
||||
);
|
||||
})
|
||||
.map((r) => {
|
||||
|
@ -1,39 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from "@/lang/i18n";
|
||||
import { CopyOutlined } from "@ant-design/icons-vue";
|
||||
import { Modal, message } from "ant-design-vue";
|
||||
import { reportErrorMsg } from "@/tools/validator";
|
||||
import type { ButtonType } from "ant-design-vue/es/button";
|
||||
import type { SizeType } from "ant-design-vue/es/config-provider";
|
||||
import { h } from "vue";
|
||||
import { toCopy } from "@/tools/copy";
|
||||
|
||||
const props = defineProps<{
|
||||
size?: string;
|
||||
type?: string;
|
||||
value: string;
|
||||
}>();
|
||||
|
||||
const copy = async () => {
|
||||
if (!navigator.clipboard) {
|
||||
Modal.warning({
|
||||
title: t("TXT_CODE_ca07c84c"),
|
||||
content: [h("span", t("TXT_CODE_2452016e")), h("br"), h("span", props.value)]
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.value);
|
||||
message.success(t("TXT_CODE_b858d78a"));
|
||||
} catch (error: any) {
|
||||
reportErrorMsg(t("TXT_CODE_81b9b599") + error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-tooltip>
|
||||
<template #title>{{ t("TXT_CODE_13ae6a93") }}</template>
|
||||
<a-button :type="<ButtonType>type" :size="<SizeType>size" @click="copy">
|
||||
<a-button :type="<ButtonType>type" :size="<SizeType>size" @click="toCopy(props.value)">
|
||||
<template #icon>
|
||||
<CopyOutlined />
|
||||
</template>
|
||||
|
@ -8,11 +8,11 @@ const beforeEnter = (el: any) => {
|
||||
};
|
||||
|
||||
const enter = (el: any, done: () => void) => {
|
||||
let delay = el.dataset.index * Number(props.delay ?? 200);
|
||||
let delay = el.dataset.index * Number(props.delay ?? 100);
|
||||
setTimeout(() => {
|
||||
el.style.transition = "opacity 0.4s";
|
||||
el.style.transition = "opacity 0.2s";
|
||||
el.style.opacity = 1;
|
||||
el.style.animation = "global-fade-up-animation 0.4s 1";
|
||||
el.style.animation = "global-fade-up-animation 0.2s 1";
|
||||
done();
|
||||
}, delay);
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import type { FormInstance } from "ant-design-vue";
|
||||
import CopyButton from "@/components/CopyButton.vue";
|
||||
import { bind2FA } from "../services/apis/user";
|
||||
import { PERMISSION_MAP } from "@/config/const";
|
||||
import { toCopy } from "@/tools/copy";
|
||||
const { state, updateUserInfo } = useAppStateStore();
|
||||
const { state: tools } = useAppToolsStore();
|
||||
|
||||
@ -176,7 +177,6 @@ const disable2FACode = async () => {
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="APIKEY">
|
||||
<a-typography-paragraph>
|
||||
{{ t("TXT_CODE_b2dbf778") }}
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { onMounted, ref } from "vue";
|
||||
import { t } from "@/lang/i18n";
|
||||
import { CodeOutlined, DeleteOutlined, LoadingOutlined } from "@ant-design/icons-vue";
|
||||
import { useTerminal } from "../hooks/useTerminal";
|
||||
import { encodeConsoleColor, useTerminal } from "../hooks/useTerminal";
|
||||
import { getInstanceOutputLog } from "@/services/apis/instance";
|
||||
import { message } from "ant-design-vue";
|
||||
import connectErrorImage from "@/assets/daemon_connection_error.png";
|
||||
@ -29,7 +29,7 @@ const {
|
||||
clickHistoryItem
|
||||
} = useCommandHistory();
|
||||
|
||||
const { execute, initTerminalWindow, sendCommand, events, isConnect, socketAddress } =
|
||||
const { execute, initTerminalWindow, sendCommand, state, events, isConnect, socketAddress } =
|
||||
useTerminal();
|
||||
|
||||
const instanceId = props.instanceId;
|
||||
@ -60,12 +60,6 @@ const initTerminal = async () => {
|
||||
const dom = document.getElementById(terminalDomId);
|
||||
if (dom) {
|
||||
const term = initTerminalWindow(dom);
|
||||
try {
|
||||
const { value } = await getInstanceOutputLog().execute({
|
||||
params: { uuid: instanceId || "", daemonId: daemonId || "" }
|
||||
});
|
||||
if (value) term.write(value);
|
||||
} catch (error: any) {}
|
||||
return term;
|
||||
}
|
||||
throw new Error(t("TXT_CODE_42bcfe0c"));
|
||||
@ -83,6 +77,22 @@ events.on("error", (error: Error) => {
|
||||
socketError.value = error;
|
||||
});
|
||||
|
||||
events.once("detail", async () => {
|
||||
try {
|
||||
const { value } = await getInstanceOutputLog().execute({
|
||||
params: { uuid: instanceId || "", daemonId: daemonId || "" }
|
||||
});
|
||||
|
||||
if (value) {
|
||||
if (state.value?.config?.terminalOption?.haveColor) {
|
||||
term?.write(encodeConsoleColor(value));
|
||||
} else {
|
||||
term?.write(value);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {}
|
||||
});
|
||||
|
||||
const clearTerminal = () => {
|
||||
term?.clear();
|
||||
};
|
||||
|
@ -14,7 +14,9 @@ interface Props extends MountComponent {
|
||||
keyTitle?: string;
|
||||
valueTitle?: string;
|
||||
data: any[];
|
||||
subTitle?: string;
|
||||
columns?: AntColumnsType[];
|
||||
textarea?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@ -91,7 +93,7 @@ const operation = (type: "add" | "del", index = 0) => {
|
||||
<a-modal
|
||||
v-model:open="open"
|
||||
centered
|
||||
width="600px"
|
||||
width="1300px"
|
||||
:mask-closable="false"
|
||||
:title="props.title"
|
||||
:ok-text="t('TXT_CODE_d507abff')"
|
||||
@ -100,6 +102,10 @@ const operation = (type: "add" | "del", index = 0) => {
|
||||
@cancel="cancel"
|
||||
>
|
||||
<div class="dialog-overflow-container">
|
||||
<div v-if="props.subTitle" style="font-size: 13px" class="text-gray-400 mb-4">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="props.subTitle"></span>
|
||||
</div>
|
||||
<div class="flex justify-end mb-20">
|
||||
<a-button :icon="h(PlusCircleOutlined)" @click="operation('add')">
|
||||
{{ t("TXT_CODE_dfc17a0c") }}
|
||||
@ -130,9 +136,17 @@ const operation = (type: "add" | "del", index = 0) => {
|
||||
}
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
<a-textarea
|
||||
v-if="props.textarea"
|
||||
v-model:value="record[String(column.dataIndex)]"
|
||||
:placeholder="t('TXT_CODE_4ea93630')"
|
||||
:placeholder="(column as any).placeholder"
|
||||
:rows="3"
|
||||
:auto-size="false"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="record[String(column.dataIndex)]"
|
||||
:placeholder="(column as any).placeholder"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
89
frontend/src/components/fc/PurchaseQueryDialog.vue
Normal file
89
frontend/src/components/fc/PurchaseQueryDialog.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import type { MountComponent } from "@/types";
|
||||
import { t } from "@/lang/i18n";
|
||||
import { usePromiseDialog } from "@/hooks/useDialog";
|
||||
import { useRedeem, type PurchaseQueryResponse } from "@/services/apis/redeem";
|
||||
import { type FormInstance } from "ant-design-vue";
|
||||
import { reportErrorMsg } from "@/tools/validator";
|
||||
|
||||
interface Props extends MountComponent {
|
||||
instanceId?: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const { isVisible, cancel } = usePromiseDialog<PurchaseQueryResponse>(props);
|
||||
const { queryPurchase } = useRedeem();
|
||||
|
||||
const formData = ref({
|
||||
code: ""
|
||||
});
|
||||
|
||||
const queryResult = ref<PurchaseQueryResponse>();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value!.validate();
|
||||
try {
|
||||
queryResult.value = await queryPurchase(formData.value.code);
|
||||
} catch (error) {
|
||||
queryResult.value = undefined;
|
||||
reportErrorMsg(error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="isVisible"
|
||||
centered
|
||||
width="500px"
|
||||
:mask-closable="false"
|
||||
:title="t('TXT_CODE_2093cc1a')"
|
||||
:footer="null"
|
||||
@cancel="cancel"
|
||||
>
|
||||
<div class="dialog-overflow-container">
|
||||
<a-form ref="formRef" :model="formData" layout="vertical">
|
||||
<a-form-item
|
||||
name="code"
|
||||
:label="t('TXT_CODE_a3de630')"
|
||||
:rules="[{ required: true, message: t('TXT_CODE_ffda3755') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formData.code"
|
||||
name="mcsm-redeem-code"
|
||||
:placeholder="t('TXT_CODE_ffda3755')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</a-form-item>
|
||||
<div class="text-center mt-20 flex justify-center">
|
||||
<a-button type="primary" @click="handleSubmit">{{ t("TXT_CODE_ee8ae330") }}</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
|
||||
<div v-if="queryResult" class="mt-20">
|
||||
<a-descriptions bordered size="small" :column="1">
|
||||
<a-descriptions-item :label="t('TXT_CODE_f3209427')">
|
||||
<a-typography-text>
|
||||
<a :href="queryResult.panelAddr" target="_blank">{{ queryResult.panelAddr }}</a>
|
||||
</a-typography-text>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item :label="t('TXT_CODE_eb9fcdad')">
|
||||
<a-typography-text copyable :content="queryResult?.username"></a-typography-text>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item :label="t('TXT_CODE_ec7553c6')">
|
||||
<a-typography-text
|
||||
v-if="queryResult?.password"
|
||||
copyable
|
||||
:content="queryResult?.password"
|
||||
></a-typography-text>
|
||||
<a-typography-text v-else type="secondary">
|
||||
{{ t("TXT_CODE_cb61062d") }}
|
||||
</a-typography-text>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
@ -244,7 +244,7 @@ const handleChangeNode = async (item: NodeStatus) => {
|
||||
>
|
||||
<template #bodyCell="{ column, record }: AntTableCell">
|
||||
<template v-if="column.key === 'safe'">
|
||||
<span v-if="record?.config?.docker?.image" style="color: var(--color-green-6)">
|
||||
<span v-if="record?.config?.processType === 'docker'" style="color: var(--color-green-6)">
|
||||
{{ t("TXT_CODE_a3f13157") }}
|
||||
</span>
|
||||
<span v-else class="color-danger">{{ t("TXT_CODE_201bc643") }}</span>
|
||||
|
147
frontend/src/components/fc/UseRedeemDialog.vue
Normal file
147
frontend/src/components/fc/UseRedeemDialog.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import type { MountComponent } from "@/types";
|
||||
import { t } from "@/lang/i18n";
|
||||
import { usePromiseDialog } from "@/hooks/useDialog";
|
||||
import { useRedeem, type BuyInstanceResponse } from "@/services/apis/redeem";
|
||||
import { Modal, type FormInstance } from "ant-design-vue";
|
||||
import { reportErrorMsg } from "@/tools/validator";
|
||||
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||
|
||||
interface Props extends MountComponent {
|
||||
instanceId?: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const isRenewalMode = computed(() => !!props.instanceId);
|
||||
const { state: appState } = useAppStateStore();
|
||||
|
||||
const { isVisible, cancel, submit } = usePromiseDialog<BuyInstanceResponse>(props);
|
||||
const { isLoading, buyInstance, renewInstance } = useRedeem();
|
||||
|
||||
const formData = ref({
|
||||
code: "",
|
||||
username: appState.userInfo?.userName || ""
|
||||
});
|
||||
|
||||
const operateResult = ref<BuyInstanceResponse>();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value!.validate();
|
||||
try {
|
||||
if (!isRenewalMode.value) {
|
||||
const res = await buyInstance(formData.value.username, formData.value.code);
|
||||
operateResult.value = res;
|
||||
} else {
|
||||
const res = await renewInstance(
|
||||
formData.value.username,
|
||||
formData.value.code,
|
||||
props.instanceId!
|
||||
);
|
||||
operateResult.value = res;
|
||||
Modal.success({
|
||||
title: t("TXT_CODE_ae51f93b"),
|
||||
content:
|
||||
t("TXT_CODE_8074a178") + new Date(operateResult.value?.expire ?? 0).toLocaleString()
|
||||
});
|
||||
submit(operateResult.value);
|
||||
}
|
||||
} catch (error) {
|
||||
reportErrorMsg(error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="isVisible"
|
||||
centered
|
||||
width="500px"
|
||||
:mask-closable="false"
|
||||
:title="t('TXT_CODE_75bf9192')"
|
||||
:footer="null"
|
||||
@cancel="cancel"
|
||||
>
|
||||
<div class="dialog-overflow-container">
|
||||
<template v-if="!operateResult">
|
||||
<a-form ref="formRef" :model="formData" layout="vertical">
|
||||
<a-form-item
|
||||
v-if="!isRenewalMode"
|
||||
name="username"
|
||||
:label="t('TXT_CODE_c38813a8')"
|
||||
:rules="[{ required: true, message: t('TXT_CODE_2695488c') }]"
|
||||
>
|
||||
<a-typography-paragraph type="secondary">
|
||||
{{ t("TXT_CODE_b90e9abd") }}
|
||||
</a-typography-paragraph>
|
||||
<a-input
|
||||
v-model:value="formData.username"
|
||||
name="mcsm-redeem-username"
|
||||
:placeholder="t('TXT_CODE_8028e95b')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
name="code"
|
||||
:label="t('TXT_CODE_fb87ccd')"
|
||||
:rules="[{ required: true, message: t('TXT_CODE_ffda3755') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formData.code"
|
||||
name="mcsm-redeem-code"
|
||||
:placeholder="t('TXT_CODE_a95c0f85')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</a-form-item>
|
||||
<div class="text-center flex justify-center">
|
||||
<div>
|
||||
<div class="flex justify-center">
|
||||
<a-button
|
||||
class="w-28"
|
||||
type="primary"
|
||||
:loading="isLoading"
|
||||
style="width: 100px"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ t("TXT_CODE_d507abff") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a-typography-paragraph>
|
||||
<div class="text-red-600">
|
||||
<InfoCircleOutlined />
|
||||
<span v-if="!isRenewalMode">{{ t("TXT_CODE_3ee20639") }}</span>
|
||||
<span v-else>{{ t("TXT_CODE_ae51f93b") }}</span>
|
||||
</div>
|
||||
</a-typography-paragraph>
|
||||
<a-descriptions bordered size="small" :column="1">
|
||||
<a-descriptions-item :label="t('TXT_CODE_eb9fcdad')">
|
||||
<a-typography-text copyable :content="operateResult?.username"></a-typography-text>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="operateResult?.password" :label="t('TXT_CODE_551b0348')">
|
||||
<a-typography-text copyable :content="operateResult?.password"></a-typography-text>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-else :label="t('TXT_CODE_551b0348')">
|
||||
<a-typography-text type="secondary">{{ t("TXT_CODE_e1b0aab2") }}</a-typography-text>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="operateResult?.expire" :label="t('TXT_CODE_fa920c0')">
|
||||
{{ new Date(operateResult.expire).toLocaleString() }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<div class="text-center mt-20 flex justify-center">
|
||||
<a-button type="primary" @click="submit(operateResult)">
|
||||
{{ t("TXT_CODE_a676f2da") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -9,6 +9,8 @@ import type { AntColumnsType } from "@/types/ant";
|
||||
import UploadFileDialogVue from "./UploadFileDialog.vue";
|
||||
import TaskLoadingDialog from "./TaskLoadingDialog.vue";
|
||||
import TagsDialog from "./TagsDialog.vue";
|
||||
import DeleteInstanceDialog from "@/widgets/instance/dialogs/DeleteInstanceDialog.vue";
|
||||
import ImageViewerDialog from "@/widgets/instance/dialogs/ImageViewer.vue";
|
||||
|
||||
interface DockerConfigItem {
|
||||
host: string;
|
||||
@ -65,19 +67,23 @@ export async function usePortEditDialog(data: PortConfigItem[] = []) {
|
||||
{
|
||||
align: "center",
|
||||
dataIndex: "host",
|
||||
title: t("TXT_CODE_534db0b2")
|
||||
title: t("TXT_CODE_534db0b2"),
|
||||
placeholder: "eg: 8080"
|
||||
},
|
||||
{
|
||||
align: "center",
|
||||
dataIndex: "container",
|
||||
title: t("TXT_CODE_b729d2e")
|
||||
title: t("TXT_CODE_b729d2e"),
|
||||
placeholder: "eg: 25565"
|
||||
},
|
||||
{
|
||||
align: "center",
|
||||
dataIndex: "protocol",
|
||||
title: t("TXT_CODE_ad1c674c")
|
||||
title: t("TXT_CODE_ad1c674c"),
|
||||
placeholder: "tcp/udp"
|
||||
}
|
||||
] as AntColumnsType[]
|
||||
] as AntColumnsType[],
|
||||
textarea: false
|
||||
}).mount<PortConfigItem[]>(KvOptionsDialogVue)) || []
|
||||
);
|
||||
}
|
||||
@ -86,6 +92,7 @@ export async function useVolumeEditDialog(data: DockerConfigItem[] = []) {
|
||||
return (
|
||||
(await useMountComponent({
|
||||
data,
|
||||
subTitle: t("TXT_CODE_6c232c9c"),
|
||||
title: t("TXT_CODE_820ebc92"),
|
||||
columns: [
|
||||
{
|
||||
@ -98,7 +105,8 @@ export async function useVolumeEditDialog(data: DockerConfigItem[] = []) {
|
||||
dataIndex: "container",
|
||||
title: t("TXT_CODE_30258325")
|
||||
}
|
||||
] as AntColumnsType[]
|
||||
] as AntColumnsType[],
|
||||
textarea: true
|
||||
}).mount<DockerConfigItem[]>(KvOptionsDialogVue)) || []
|
||||
);
|
||||
}
|
||||
@ -119,7 +127,8 @@ export async function useDockerEnvEditDialog(data: DockerEnvItem[] = []) {
|
||||
dataIndex: "value",
|
||||
title: t("TXT_CODE_115e8a25")
|
||||
}
|
||||
] as AntColumnsType[]
|
||||
] as AntColumnsType[],
|
||||
textarea: true
|
||||
}).mount<DockerEnvItem[]>(KvOptionsDialogVue)) || []
|
||||
);
|
||||
}
|
||||
@ -148,3 +157,18 @@ export async function openInstanceTagsEditor(
|
||||
.load<InstanceType<typeof TagsDialog>>(TagsDialog)
|
||||
.openDialog();
|
||||
}
|
||||
|
||||
export async function useDeleteInstanceDialog(instanceId: string, daemonId: string) {
|
||||
return await useMountComponent({ instanceId, daemonId }).mount<boolean>(DeleteInstanceDialog);
|
||||
}
|
||||
|
||||
export async function useImageViewerDialog(
|
||||
instanceId: string,
|
||||
daemonId: string,
|
||||
fileName: string,
|
||||
frontDir: string
|
||||
) {
|
||||
return await useMountComponent({ instanceId, daemonId, fileName, frontDir }).mount(
|
||||
ImageViewerDialog
|
||||
);
|
||||
}
|
||||
|
@ -41,7 +41,8 @@ import DefaultCard from "@/widgets/DefaultCard.vue";
|
||||
import Carousel from "@/widgets/others/Carousel.vue";
|
||||
import PluginCard from "@/widgets/others/PluginCard.vue";
|
||||
import MusicCard from "@/widgets/others/MusicCard.vue";
|
||||
|
||||
import ShelvesCard from "@/widgets/ShelvesCard.vue";
|
||||
import ShopInfoCard from "@/widgets/ShopInfoCard.vue";
|
||||
import { NEW_CARD_TYPE } from "../types/index";
|
||||
import { ROLE } from "./router";
|
||||
|
||||
@ -85,7 +86,9 @@ export const LAYOUT_CARD_TYPES: { [key: string]: any } = {
|
||||
DefaultCard,
|
||||
Carousel,
|
||||
PluginCard,
|
||||
MusicCard
|
||||
MusicCard,
|
||||
ShelvesCard,
|
||||
ShopInfoCard
|
||||
};
|
||||
|
||||
export interface NewCardItem extends LayoutCard {
|
||||
@ -496,6 +499,28 @@ export function getLayoutCardPool() {
|
||||
description: t("TXT_CODE_cb84b22"),
|
||||
height: LayoutCardHeight.SMALL,
|
||||
category: NEW_CARD_TYPE.COMMON
|
||||
},
|
||||
{
|
||||
id: getRandomId(),
|
||||
permission: ROLE.GUEST,
|
||||
meta: {},
|
||||
type: "ShelvesCard",
|
||||
title: t("TXT_CODE_b99cae18"),
|
||||
width: 8,
|
||||
description: t("TXT_CODE_163e2d0a"),
|
||||
height: LayoutCardHeight.MEDIUM,
|
||||
category: NEW_CARD_TYPE.COMMON
|
||||
},
|
||||
{
|
||||
id: getRandomId(),
|
||||
permission: ROLE.GUEST,
|
||||
meta: {},
|
||||
type: "ShopInfoCard",
|
||||
title: t("TXT_CODE_48261ab7"),
|
||||
width: 8,
|
||||
description: t("TXT_CODE_1648c9ea"),
|
||||
height: LayoutCardHeight.SMALL,
|
||||
category: NEW_CARD_TYPE.COMMON
|
||||
}
|
||||
];
|
||||
return LAYOUT_CARD_POOL;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@ export interface RouterMetaInfo {
|
||||
permission?: number;
|
||||
redirect?: string;
|
||||
onlyDisplayEditMode?: boolean;
|
||||
condition?: () => boolean;
|
||||
breadcrumbs?: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
@ -28,8 +29,11 @@ export interface RouterConfig {
|
||||
}
|
||||
|
||||
export enum ROLE {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
ADMIN = 10,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
USER = 1,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
GUEST = 0
|
||||
}
|
||||
|
||||
@ -232,6 +236,19 @@ const originRouterConfig: RouterConfig[] = [
|
||||
mainMenu: true,
|
||||
onlyDisplayEditMode: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/shop",
|
||||
name: t("TXT_CODE_5a408a5e"),
|
||||
component: LayoutContainer,
|
||||
meta: {
|
||||
permission: ROLE.GUEST,
|
||||
mainMenu: true,
|
||||
condition: () => {
|
||||
const { state: appConfig } = useAppStateStore();
|
||||
return appConfig.settings.businessMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@ -277,7 +294,10 @@ router.beforeEach((to, from, next) => {
|
||||
toPagePermission
|
||||
);
|
||||
|
||||
if (toRoutePath.includes("_open_page") || ["/login", "/install", "/404"].includes(toRoutePath)) {
|
||||
if (
|
||||
toRoutePath.includes("_open_page") ||
|
||||
["/shop", "/login", "/install", "/404"].includes(toRoutePath)
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
|
35
frontend/src/hooks/useDialog.ts
Normal file
35
frontend/src/hooks/useDialog.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
export function useDialog<T = any>(props: any) {
|
||||
const isVisible = ref(false);
|
||||
|
||||
const openDialog = async () => {
|
||||
isVisible.value = true;
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
isVisible.value = false;
|
||||
if (props.destroyComponent) props.destroyComponent();
|
||||
};
|
||||
|
||||
const submit = async (data?: T) => {
|
||||
if (props.emitResult) props.emitResult(data);
|
||||
await cancel();
|
||||
};
|
||||
|
||||
return {
|
||||
openDialog,
|
||||
isVisible,
|
||||
cancel,
|
||||
submit
|
||||
};
|
||||
}
|
||||
|
||||
export function usePromiseDialog<T = any>(props: any) {
|
||||
const { isVisible, ...rest } = useDialog<T>(props);
|
||||
isVisible.value = true;
|
||||
return {
|
||||
isVisible,
|
||||
...rest
|
||||
};
|
||||
}
|
@ -29,6 +29,7 @@ import type {
|
||||
} from "@/types/fileManager";
|
||||
import { reportErrorMsg } from "@/tools/validator";
|
||||
import { openLoadingDialog } from "@/components/fc";
|
||||
import { useImageViewerDialog } from "@/components/fc";
|
||||
|
||||
export const useFileManager = (instanceId?: string, daemonId?: string) => {
|
||||
const dataSource = ref<DataType[]>();
|
||||
@ -464,28 +465,34 @@ export const useFileManager = (instanceId?: string, daemonId?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const downloadFile = async (fileName: string) => {
|
||||
const getFileLink = async (fileName: string, frontDir?: string) => {
|
||||
frontDir = frontDir || breadcrumbs[breadcrumbs.length - 1].path;
|
||||
const { state: downloadCfg, execute: getDownloadCfg } = downloadAddress();
|
||||
|
||||
try {
|
||||
await getDownloadCfg({
|
||||
params: {
|
||||
file_name: breadcrumbs[breadcrumbs.length - 1].path + fileName,
|
||||
daemonId: daemonId!,
|
||||
uuid: instanceId!
|
||||
file_name: frontDir + fileName,
|
||||
daemonId: daemonId || "",
|
||||
uuid: instanceId || ""
|
||||
}
|
||||
});
|
||||
if (!downloadCfg.value) throw new Error(t("TXT_CODE_6d772765"));
|
||||
window.open(
|
||||
`${parseForwardAddress(downloadCfg.value.addr, "http")}/download/${
|
||||
downloadCfg.value.password
|
||||
}/${fileName}`
|
||||
);
|
||||
if (!downloadCfg.value) return null;
|
||||
return `${parseForwardAddress(downloadCfg.value.addr, "http")}/download/${
|
||||
downloadCfg.value.password
|
||||
}/${fileName}`;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
return reportErrorMsg(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadFile = async (fileName: string) => {
|
||||
const link = await getFileLink(fileName);
|
||||
if (!link) throw new Error(t("TXT_CODE_6d772765"));
|
||||
window.open(link);
|
||||
};
|
||||
|
||||
const handleChangeDir = async (dir: string) => {
|
||||
if (breadcrumbs.findIndex((e) => e.path === dir) === -1)
|
||||
return reportErrorMsg(t("TXT_CODE_96281410"));
|
||||
@ -500,12 +507,17 @@ export const useFileManager = (instanceId?: string, daemonId?: string) => {
|
||||
const handleTableChange = (e: { current: number; pageSize: number }) => {
|
||||
selectedRowKeys.value = [];
|
||||
selectionData.value = [];
|
||||
operationForm.value.name = "";
|
||||
// operationForm.value.name = "";
|
||||
operationForm.value.current = e.current;
|
||||
operationForm.value.pageSize = e.pageSize;
|
||||
getFileList();
|
||||
};
|
||||
|
||||
const handleSearchChange = () => {
|
||||
operationForm.value.current = 1;
|
||||
getFileList();
|
||||
};
|
||||
|
||||
const getFileStatus = async () => {
|
||||
const { state, execute } = getFileStatusApi();
|
||||
try {
|
||||
@ -596,6 +608,16 @@ export const useFileManager = (instanceId?: string, daemonId?: string) => {
|
||||
spinning.value = false;
|
||||
};
|
||||
|
||||
const isImage = (extName: string) => {
|
||||
if (!extName) return;
|
||||
return ["JPG", "JPEG", "PNG", "GIF", "BMP", "WEBP", "ICO"].includes(extName.toUpperCase());
|
||||
};
|
||||
|
||||
const showImage = (file: DataType) => {
|
||||
const frontDir = breadcrumbs[breadcrumbs.length - 1].path;
|
||||
useImageViewerDialog(instanceId || "", daemonId || "", file.name, frontDir);
|
||||
};
|
||||
|
||||
return {
|
||||
fileStatus,
|
||||
dialog,
|
||||
@ -625,12 +647,16 @@ export const useFileManager = (instanceId?: string, daemonId?: string) => {
|
||||
selectedFile,
|
||||
rowClickTable,
|
||||
downloadFile,
|
||||
getFileLink,
|
||||
handleChangeDir,
|
||||
handleTableChange,
|
||||
handleSearchChange,
|
||||
getFileStatus,
|
||||
changePermission,
|
||||
toDisk,
|
||||
pushSelected,
|
||||
oneSelected
|
||||
oneSelected,
|
||||
isImage,
|
||||
showImage
|
||||
};
|
||||
};
|
||||
|
@ -12,13 +12,16 @@ export const TYPE_MINECRAFT_JAVA = "minecraft/java";
|
||||
export const TYPE_MINECRAFT_BUKKIT = "minecraft/java/bukkit";
|
||||
export const TYPE_MINECRAFT_SPIGOT = "minecraft/java/spigot";
|
||||
export const TYPE_MINECRAFT_PAPER = "minecraft/java/paper";
|
||||
export const TYPE_MINECRAFT_PUFFERFISH = "minecraft/java/pufferfish";
|
||||
export const TYPE_MINECRAFT_FORGE = "minecraft/java/forge";
|
||||
export const TYPE_MINECRAFT_NEOFORGE = "minecraft/java/neoforge";
|
||||
export const TYPE_MINECRAFT_FABRIC = "minecraft/java/fabric";
|
||||
export const TYPE_MINECRAFT_BUNGEECORD = "minecraft/java/bungeecord";
|
||||
export const TYPE_MINECRAFT_VELOCITY = "minecraft/java/velocity";
|
||||
export const TYPE_MINECRAFT_GEYSER = "minecraft/java/geyser";
|
||||
export const TYPE_MINECRAFT_SPONGE = "minecraft/java/sponge";
|
||||
export const TYPE_MINECRAFT_MOHIST = "minecraft/java/mohist";
|
||||
export const TYPE_MINECRAFT_PURPUR = "minecraft/java/purpur";
|
||||
export const TYPE_MINECRAFT_BEDROCK = "minecraft/bedrock";
|
||||
export const TYPE_MINECRAFT_BDS = "minecraft/bedrock/bds";
|
||||
export const TYPE_MINECRAFT_NUKKIT = "minecraft/bedrock/nukkit";
|
||||
@ -30,13 +33,18 @@ export const INSTANCE_TYPE_TRANSLATION: MapData<string> = {
|
||||
[TYPE_STEAM_SERVER_UNIVERSAL]: t("TXT_CODE_3d7fbe30"),
|
||||
[TYPE_MINECRAFT_JAVA]: t("TXT_CODE_97f779b3"),
|
||||
[TYPE_MINECRAFT_BEDROCK]: t("TXT_CODE_7f1aef9f"),
|
||||
[TYPE_MINECRAFT_NUKKIT]: t("TXT_CODE_8f3e5807"),
|
||||
[TYPE_MINECRAFT_SPIGOT]: t("TXT_CODE_6c08319b"),
|
||||
[TYPE_MINECRAFT_PAPER]: t("TXT_CODE_ec0cda88"),
|
||||
[TYPE_MINECRAFT_PUFFERFISH]: t("TXT_CODE_c6d3bd8"),
|
||||
[TYPE_MINECRAFT_BUNGEECORD]: t("TXT_CODE_ba86f4a"),
|
||||
[TYPE_MINECRAFT_VELOCITY]: t("TXT_CODE_a3abb092"),
|
||||
[TYPE_MINECRAFT_PURPUR]: t("TXT_CODE_e543f6c0"),
|
||||
[TYPE_MINECRAFT_BDS]: t("TXT_CODE_67b5f678"),
|
||||
[TYPE_MINECRAFT_SPONGE]: t("TXT_CODE_e4dbff32"),
|
||||
[TYPE_MINECRAFT_FORGE]: t("TXT_CODE_5112fcb2"),
|
||||
[TYPE_MINECRAFT_NEOFORGE]: t("TXT_CODE_98b4ac74"),
|
||||
[TYPE_MINECRAFT_MOHIST]: t("TXT_CODE_82e624d1"),
|
||||
[TYPE_MINECRAFT_FABRIC]: t("TXT_CODE_7af6d85a"),
|
||||
[TYPE_MINECRAFT_BUKKIT]: t("TXT_CODE_992bf9bc"),
|
||||
[TYPE_MINECRAFT_GEYSER]: t("TXT_CODE_4f57868"),
|
||||
@ -179,8 +187,11 @@ export const INSTANCE_CONFIGS: InstanceConfigs[] = [
|
||||
TYPE_MINECRAFT_JAVA,
|
||||
TYPE_MINECRAFT_BUKKIT,
|
||||
TYPE_MINECRAFT_FORGE,
|
||||
TYPE_MINECRAFT_NEOFORGE,
|
||||
TYPE_MINECRAFT_FABRIC,
|
||||
TYPE_MINECRAFT_SPONGE
|
||||
TYPE_MINECRAFT_SPONGE,
|
||||
TYPE_MINECRAFT_PURPUR,
|
||||
TYPE_MINECRAFT_PUFFERFISH
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -195,7 +206,11 @@ export const INSTANCE_CONFIGS: InstanceConfigs[] = [
|
||||
TYPE_MINECRAFT_JAVA,
|
||||
TYPE_MINECRAFT_BUKKIT,
|
||||
TYPE_MINECRAFT_FABRIC,
|
||||
TYPE_MINECRAFT_SPONGE
|
||||
TYPE_MINECRAFT_FORGE,
|
||||
TYPE_MINECRAFT_NEOFORGE,
|
||||
TYPE_MINECRAFT_SPONGE,
|
||||
TYPE_MINECRAFT_PURPUR,
|
||||
TYPE_MINECRAFT_PUFFERFISH
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -208,7 +223,9 @@ export const INSTANCE_CONFIGS: InstanceConfigs[] = [
|
||||
TYPE_MINECRAFT_SPIGOT,
|
||||
TYPE_MINECRAFT_PAPER,
|
||||
TYPE_MINECRAFT_JAVA,
|
||||
TYPE_MINECRAFT_BUKKIT
|
||||
TYPE_MINECRAFT_BUKKIT,
|
||||
TYPE_MINECRAFT_PURPUR,
|
||||
TYPE_MINECRAFT_PUFFERFISH
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -221,7 +238,9 @@ export const INSTANCE_CONFIGS: InstanceConfigs[] = [
|
||||
TYPE_MINECRAFT_SPIGOT,
|
||||
TYPE_MINECRAFT_PAPER,
|
||||
TYPE_MINECRAFT_JAVA,
|
||||
TYPE_MINECRAFT_BUKKIT
|
||||
TYPE_MINECRAFT_BUKKIT,
|
||||
TYPE_MINECRAFT_PURPUR,
|
||||
TYPE_MINECRAFT_PUFFERFISH
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -270,7 +289,12 @@ export const INSTANCE_CONFIGS: InstanceConfigs[] = [
|
||||
info: t("TXT_CODE_2931127f"),
|
||||
path: "config/paper-global.yml",
|
||||
redirect: "paper/paper-global.yml",
|
||||
category: [TYPE_MINECRAFT_JAVA, TYPE_MINECRAFT_PAPER]
|
||||
category: [
|
||||
TYPE_MINECRAFT_JAVA,
|
||||
TYPE_MINECRAFT_PAPER,
|
||||
TYPE_MINECRAFT_PUFFERFISH,
|
||||
TYPE_MINECRAFT_PURPUR
|
||||
]
|
||||
},
|
||||
{
|
||||
fileName: "[Paper] paper-world-defaults.yml",
|
||||
@ -278,7 +302,28 @@ export const INSTANCE_CONFIGS: InstanceConfigs[] = [
|
||||
info: t("TXT_CODE_4880ef77"),
|
||||
path: "config/paper-world-defaults.yml",
|
||||
redirect: "paper/paper-world-defaults.yml",
|
||||
category: [TYPE_MINECRAFT_JAVA, TYPE_MINECRAFT_PAPER]
|
||||
category: [
|
||||
TYPE_MINECRAFT_JAVA,
|
||||
TYPE_MINECRAFT_PAPER,
|
||||
TYPE_MINECRAFT_PUFFERFISH,
|
||||
TYPE_MINECRAFT_PURPUR
|
||||
]
|
||||
},
|
||||
{
|
||||
fileName: "[Purpur] pupur.yml",
|
||||
type: "yml",
|
||||
info: t("TXT_CODE_98e50717"),
|
||||
path: "purpur.yml",
|
||||
redirect: "purpur/purpur.yml",
|
||||
category: [TYPE_MINECRAFT_JAVA, TYPE_MINECRAFT_PAPER, TYPE_MINECRAFT_PURPUR]
|
||||
},
|
||||
{
|
||||
fileName: "[Pufferfish] pufferfish.yml",
|
||||
type: "yml",
|
||||
info: t("TXT_CODE_9213f8e3"),
|
||||
path: "pufferfish.yml",
|
||||
redirect: "pufferfish/pufferfish.yml",
|
||||
category: [TYPE_MINECRAFT_JAVA, TYPE_MINECRAFT_PUFFERFISH]
|
||||
},
|
||||
{
|
||||
fileName: "[Geyser] config.yml",
|
||||
@ -306,10 +351,34 @@ export const INSTANCE_CONFIGS: InstanceConfigs[] = [
|
||||
},
|
||||
{
|
||||
fileName: "[Tshock] config.json",
|
||||
type: "yml",
|
||||
type: "json",
|
||||
info: t("TXT_CODE_1cd8f9d2"),
|
||||
path: "tshock/config.json",
|
||||
redirect: "tshock/config.json",
|
||||
category: [TYPE_TERRARIA]
|
||||
},
|
||||
{
|
||||
fileName: "[Forge] fml.toml",
|
||||
type: "toml",
|
||||
info: t("TXT_CODE_7e6a82d8"),
|
||||
path: "config/fml.toml",
|
||||
redirect: "forge/fml.toml",
|
||||
category: [TYPE_MINECRAFT_FORGE]
|
||||
},
|
||||
{
|
||||
fileName: "[NeoForge] neoforge-server.toml",
|
||||
type: "toml",
|
||||
info: t("TXT_CODE_5b6f3691"),
|
||||
path: "config/neoforge-server.toml",
|
||||
redirect: "neoforge/neoforge-server.toml",
|
||||
category: [TYPE_MINECRAFT_NEOFORGE]
|
||||
},
|
||||
{
|
||||
fileName: "[NeoForge] neoforge-common.toml",
|
||||
type: "toml",
|
||||
info: t("TXT_CODE_1efc7c5f"),
|
||||
path: "config/neoforge-common.toml",
|
||||
redirect: "neoforge/neoforge-common.toml",
|
||||
category: [TYPE_MINECRAFT_NEOFORGE]
|
||||
}
|
||||
];
|
||||
|
@ -8,7 +8,7 @@ export function useMountComponent(data: Record<string, any> = {}) {
|
||||
const mount = <T>(component: Component) => {
|
||||
if (isOpen) return;
|
||||
isOpen = true;
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
return new Promise<T>((resolve) => {
|
||||
const div = document.createElement("div");
|
||||
document.body.appendChild(div);
|
||||
const app = createApp(component, {
|
||||
|
173
frontend/src/hooks/useSchedule.ts
Normal file
173
frontend/src/hooks/useSchedule.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import { t } from "@/lang/i18n";
|
||||
import { scheduleCreate, scheduleDelete, scheduleList } from "@/services/apis/instance";
|
||||
import { reportErrorMsg } from "@/tools/validator";
|
||||
import type { ScheduleTaskForm } from "@/types";
|
||||
import { message, notification } from "ant-design-vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export function useSchedule(instanceId: string, daemonId: string) {
|
||||
const createTaskTypeInterval = async (newTask: ScheduleTaskForm) => {
|
||||
const arr = newTask.cycle;
|
||||
let ps = Number(arr[0]);
|
||||
let pm = Number(arr[1]);
|
||||
let ph = Number(arr[2]);
|
||||
const rs = ps + pm * 60 + ph * 60 * 60;
|
||||
newTask.time = rs.toString();
|
||||
await createTask(newTask);
|
||||
};
|
||||
|
||||
const createTaskTypeCycle = async (newTask: ScheduleTaskForm) => {
|
||||
const weekend = newTask.weekend;
|
||||
if (!newTask.objTime) throw new Error(t("TXT_CODE_349edc57"));
|
||||
if (weekend.length === 0) throw new Error(t("TXT_CODE_2fe0cc84"));
|
||||
const time = newTask.objTime;
|
||||
const h = time.hour();
|
||||
const m = time.minute();
|
||||
const s = time.second();
|
||||
newTask.time = `${s} ${m} ${h} * * ${weekend.join(",")}`;
|
||||
await createTask(newTask);
|
||||
};
|
||||
|
||||
const createTaskTypeSpecify = async (newTask: ScheduleTaskForm) => {
|
||||
if (!newTask.objTime) throw new Error(t("TXT_CODE_349edc57"));
|
||||
const time = newTask.objTime;
|
||||
const mm = time.month() + 1;
|
||||
const dd = time.date();
|
||||
const h = time.hour();
|
||||
const m = time.minute();
|
||||
const s = time.second();
|
||||
newTask.time = `${s} ${m} ${h} ${dd} ${mm} *`;
|
||||
await createTask(newTask);
|
||||
};
|
||||
|
||||
const calculateIntervalFromTime = (time: string): string[] => {
|
||||
const totalSeconds = Number(time);
|
||||
const ph = Math.floor(totalSeconds / 3600);
|
||||
const pm = Math.floor((totalSeconds % 3600) / 60);
|
||||
const ps = totalSeconds % 60;
|
||||
return [ps.toString(), pm.toString(), ph.toString()];
|
||||
};
|
||||
|
||||
const calculateTimeFromCycle = (time: string) => {
|
||||
const regex = /(\d+) (\d+) (\d+) \* \* (.+)/;
|
||||
const match = time.match(regex);
|
||||
let objTime = dayjs(),
|
||||
weekend: number[] = [];
|
||||
if (match) {
|
||||
const s = match[1];
|
||||
const m = match[2];
|
||||
const h = match[3];
|
||||
const w = match[4].split(",").map(Number);
|
||||
|
||||
const now = new Date();
|
||||
now.setSeconds(Number(s));
|
||||
now.setMinutes(Number(m));
|
||||
now.setHours(Number(h));
|
||||
objTime = dayjs(now);
|
||||
weekend = w;
|
||||
} else {
|
||||
notification.error({ message: t("TXT_CODE_afabf3ca") });
|
||||
}
|
||||
|
||||
return {
|
||||
objTime,
|
||||
weekend
|
||||
};
|
||||
};
|
||||
|
||||
const parseTaskTime = (time: string) => {
|
||||
const regex = /(\d+) (\d+) (\d+) (\d+) (\d+) \*/; // 匹配 time 格式
|
||||
const match = time.match(regex);
|
||||
let objTime = dayjs();
|
||||
if (match) {
|
||||
const s = Number(match[1]);
|
||||
const m = Number(match[2]);
|
||||
const h = Number(match[3]);
|
||||
const dd = Number(match[4]);
|
||||
const mm = Number(match[5]) - 1;
|
||||
|
||||
const now = new Date();
|
||||
now.setSeconds(s);
|
||||
now.setMinutes(m);
|
||||
now.setHours(h);
|
||||
now.setDate(dd);
|
||||
now.setMonth(mm);
|
||||
objTime = dayjs(now);
|
||||
} else {
|
||||
notification.error({ message: t("TXT_CODE_afabf3ca") });
|
||||
}
|
||||
|
||||
return objTime;
|
||||
};
|
||||
|
||||
const { state: createState, execute: create } = scheduleCreate();
|
||||
const createTask = async (newTask: ScheduleTaskForm) => {
|
||||
try {
|
||||
if (!newTask.count) newTask.count = -1;
|
||||
await create({
|
||||
params: {
|
||||
daemonId: daemonId,
|
||||
uuid: instanceId
|
||||
},
|
||||
data: newTask
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
reportErrorMsg(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const { state: schedules, execute: list, isLoading: scheduleListLoading } = scheduleList();
|
||||
const getScheduleList = async () => {
|
||||
try {
|
||||
await list({
|
||||
params: {
|
||||
daemonId: daemonId ?? "",
|
||||
uuid: instanceId ?? ""
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
reportErrorMsg(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSchedule = async (name: string, showMsg: boolean = true) => {
|
||||
const { execute, state } = scheduleDelete();
|
||||
try {
|
||||
await execute({
|
||||
params: {
|
||||
daemonId: daemonId ?? "",
|
||||
uuid: instanceId ?? "",
|
||||
task_name: name
|
||||
}
|
||||
});
|
||||
if (state.value) {
|
||||
showMsg && message.success(t("TXT_CODE_28190dbc"));
|
||||
getScheduleList();
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
reportErrorMsg(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createTaskTypeInterval,
|
||||
createTaskTypeCycle,
|
||||
createTaskTypeSpecify,
|
||||
|
||||
calculateIntervalFromTime,
|
||||
calculateTimeFromCycle,
|
||||
parseTaskTime,
|
||||
|
||||
createTask,
|
||||
createState,
|
||||
|
||||
getScheduleList,
|
||||
scheduleListLoading,
|
||||
schedules,
|
||||
|
||||
deleteSchedule
|
||||
};
|
||||
}
|
@ -3,8 +3,6 @@
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { updateSettings } from "@/services/apis";
|
||||
|
||||
import enUS from "@languages/en_US.json";
|
||||
|
||||
// DO NOT I18N
|
||||
// If you want to add the language of your own country, you need to add the code here.
|
||||
export const SUPPORTED_LANGS = [
|
||||
@ -73,9 +71,7 @@ export async function initInstallPageFlow(language: string) {
|
||||
async function initI18n(lang: string) {
|
||||
lang = toStandardLang(lang);
|
||||
|
||||
const messages: Record<string, any> = {
|
||||
en_us: enUS
|
||||
};
|
||||
const messages: Record<string, any> = {};
|
||||
const langFiles = import.meta.glob("../../../languages/*.json");
|
||||
for (const path in langFiles) {
|
||||
if (
|
||||
@ -137,10 +133,10 @@ const isPT = () => {
|
||||
return getCurrentLang() === "pt_br";
|
||||
};
|
||||
|
||||
const $t = (...args: string[]): string => {
|
||||
const $t = (...args: any[]): string => {
|
||||
return (i18n.global.t as Function)(...args);
|
||||
};
|
||||
const t = (...args: string[]): string => {
|
||||
const t = (...args: any[]): string => {
|
||||
return (i18n.global.t as Function)(...args);
|
||||
};
|
||||
|
||||
|
182
frontend/src/services/apis/redeem.ts
Normal file
182
frontend/src/services/apis/redeem.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { useDefineApi } from "@/stores/useDefineApi";
|
||||
import { computed, onMounted, ref, type Ref } from "vue";
|
||||
import { queryUsername } from "./user";
|
||||
import { Modal } from "ant-design-vue";
|
||||
import { t } from "@/lang/i18n";
|
||||
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||
|
||||
export interface ShopItem {
|
||||
productId: number;
|
||||
title: string;
|
||||
price: number;
|
||||
ispId?: number;
|
||||
daemonId: number;
|
||||
payload: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface ShopInfo {
|
||||
uid?: number;
|
||||
nickname: string;
|
||||
username: string;
|
||||
lastTime: number;
|
||||
introduction: string;
|
||||
afterSalesGroup: string;
|
||||
}
|
||||
|
||||
export interface ShopInfoResponse {
|
||||
ispInfo: ShopInfo;
|
||||
products: ShopItem[];
|
||||
}
|
||||
|
||||
export interface BuyInstanceResponse {
|
||||
instanceId: string;
|
||||
expire: number;
|
||||
username: string;
|
||||
password: string;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export interface PurchaseQueryResponse {
|
||||
id: number;
|
||||
uid: number;
|
||||
nodeId: number;
|
||||
productId: number;
|
||||
panelAddr: string;
|
||||
username: string;
|
||||
password: string;
|
||||
activeTime?: number;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export const CURRENT_PANEL_ADDR = window.location.host.includes("localhost")
|
||||
? "http://localhost:23333/"
|
||||
: `${window.location.protocol}//${window.location.host}/`;
|
||||
|
||||
export const requestRedeemPlatform = useDefineApi<
|
||||
{
|
||||
data: {
|
||||
targetUrl: string;
|
||||
method: string;
|
||||
data?: object;
|
||||
params?: object;
|
||||
};
|
||||
},
|
||||
any
|
||||
>({
|
||||
url: "/api/exchange/request_redeem_platform",
|
||||
method: "POST",
|
||||
timeout: 1000 * 30
|
||||
});
|
||||
|
||||
export function useShopInfo() {
|
||||
const config = requestRedeemPlatform();
|
||||
const isError = ref<Error>();
|
||||
const { state: appState } = useAppStateStore();
|
||||
|
||||
const loadProducts = async (businessId?: string) => {
|
||||
try {
|
||||
isError.value = undefined;
|
||||
await config.execute({
|
||||
data: {
|
||||
targetUrl: "/api/instances/query_products",
|
||||
method: "GET",
|
||||
params: {
|
||||
addr: CURRENT_PANEL_ADDR,
|
||||
businessId: businessId || appState.settings.businessId
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
isError.value = error as Error;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProducts();
|
||||
});
|
||||
|
||||
return {
|
||||
...config,
|
||||
isError,
|
||||
loadProducts,
|
||||
isLoading: config.isLoading,
|
||||
state: config.state as Ref<ShopInfoResponse>,
|
||||
shopInfo: computed<ShopInfo>(() => config.state.value?.ispInfo),
|
||||
products: computed<ShopItem[]>(() => config.state.value?.products)
|
||||
};
|
||||
}
|
||||
|
||||
export function useRedeem() {
|
||||
const { state: appState } = useAppStateStore();
|
||||
const { execute, isLoading } = requestRedeemPlatform();
|
||||
|
||||
const preCheckUsername = (username: string) => {
|
||||
isLoading.value = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
queryUsername()
|
||||
.execute({ params: { username } })
|
||||
.then((res) => {
|
||||
Modal.confirm({
|
||||
title: t("TXT_CODE_893567ac"),
|
||||
content: res.value?.uuid
|
||||
? `${username} ${t("TXT_CODE_c684d8b2")}`
|
||||
: `${username} ${t("TXT_CODE_4c72565d")}`,
|
||||
onOk: () => {
|
||||
resolve(true);
|
||||
},
|
||||
onCancel: () => {
|
||||
reject(t("TXT_CODE_f94e428a"));
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
reject(t("TXT_CODE_1d8f3c33"));
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const buyInstance = async (username: string, code: string) => {
|
||||
await preCheckUsername(username);
|
||||
const res = await execute({
|
||||
data: {
|
||||
targetUrl: "/api/instances/use_redeem",
|
||||
method: "POST",
|
||||
data: { code, username, businessId: appState.settings.businessId }
|
||||
}
|
||||
});
|
||||
return res.value as BuyInstanceResponse;
|
||||
};
|
||||
|
||||
const renewalInstance = async (username: string, code: string, instanceId: string) => {
|
||||
const res = await execute({
|
||||
data: {
|
||||
targetUrl: "/api/instances/use_redeem",
|
||||
method: "POST",
|
||||
data: { code, instanceId, username, businessId: appState.settings.businessId }
|
||||
}
|
||||
});
|
||||
return res.value as BuyInstanceResponse;
|
||||
};
|
||||
|
||||
const queryPurchase = async (code: string) => {
|
||||
const res = await execute({
|
||||
data: {
|
||||
targetUrl: "/api/instances/purchase_history",
|
||||
method: "GET",
|
||||
params: { code, businessId: appState.settings.businessId }
|
||||
}
|
||||
});
|
||||
return res.value as PurchaseQueryResponse;
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
buyInstance,
|
||||
queryPurchase,
|
||||
renewInstance: renewalInstance
|
||||
};
|
||||
}
|
@ -40,3 +40,18 @@ export const confirm2FA = useDefineApi<
|
||||
url: "/api/auth/confirm2fa",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
export const queryUsername = useDefineApi<
|
||||
{
|
||||
params: {
|
||||
username: string;
|
||||
};
|
||||
},
|
||||
{
|
||||
uuid?: string;
|
||||
userName?: string;
|
||||
}
|
||||
>({
|
||||
url: "/api/auth/query_username",
|
||||
method: "GET"
|
||||
});
|
||||
|
@ -20,7 +20,10 @@ export const useAppStateStore = createGlobalState(() => {
|
||||
language: "en_us",
|
||||
settings: {
|
||||
canFileManager: false,
|
||||
allowUsePreset: false
|
||||
allowUsePreset: false,
|
||||
businessMode: false,
|
||||
businessId: "",
|
||||
allowChangeCmd: false
|
||||
}
|
||||
});
|
||||
|
||||
@ -52,15 +55,14 @@ export const useAppStateStore = createGlobalState(() => {
|
||||
|
||||
const updatePanelStatus = async () => {
|
||||
const { state } = useAppStateStore();
|
||||
const status = await panelStatus().execute();
|
||||
state.isInstall = status.value?.isInstall ?? true;
|
||||
state.versionChange = status.value?.versionChange ? true : false;
|
||||
state.settings = status.value?.settings ?? {
|
||||
canFileManager: false,
|
||||
allowUsePreset: false
|
||||
};
|
||||
const panelStatusRes = await panelStatus().execute();
|
||||
state.isInstall = panelStatusRes.value?.isInstall ?? true;
|
||||
state.versionChange = panelStatusRes.value?.versionChange ? true : false;
|
||||
if (panelStatusRes.value?.settings) {
|
||||
state.settings = panelStatusRes.value?.settings;
|
||||
}
|
||||
if (state.isInstall) {
|
||||
state.language = toStandardLang(status.value?.language);
|
||||
state.language = toStandardLang(panelStatusRes.value?.language);
|
||||
} else {
|
||||
state.language = searchSupportLanguage(window.navigator.language);
|
||||
await initInstallPageFlow(state.language);
|
||||
|
@ -198,3 +198,5 @@ export const isInt = (x: any) => {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const padZero = (num: string) => (num === "0" ? num : num.padStart(2, "0"));
|
||||
|
29
frontend/src/tools/copy.ts
Normal file
29
frontend/src/tools/copy.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { t } from "@/lang/i18n";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { h } from "vue";
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard();
|
||||
|
||||
export const toCopy = async (sth: string | number) => {
|
||||
try {
|
||||
if (!isSupported.value) {
|
||||
const input = document.createElement("input");
|
||||
input.setAttribute("value", String(sth));
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(input);
|
||||
return message.success(t("TXT_CODE_b858d78a"));
|
||||
} else {
|
||||
await copy(String(sth));
|
||||
if (copied.value) return message.success(t("TXT_CODE_b858d78a"));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
Modal.warning({
|
||||
title: t("TXT_CODE_ca07c84c"),
|
||||
content: [h("span", t("TXT_CODE_2452016e")), h("br"), h("span", sth)]
|
||||
});
|
||||
}
|
||||
};
|
@ -35,3 +35,8 @@ export function reportErrorMsg(error: any = {}) {
|
||||
}
|
||||
|
||||
export const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\x00-\x7F]{9,36}$/;
|
||||
|
||||
export function isLocalNetworkIP(ip: string): boolean {
|
||||
const localNetworks = [/^10\./, /^172\.(1[6-9]|2[0-9]|3[0-1])\./, /^192\.168\./];
|
||||
return localNetworks.some((pattern) => pattern.test(ip));
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ export const ScheduleType = {
|
||||
};
|
||||
|
||||
export enum ScheduleCreateType {
|
||||
INTERVAL = "1",
|
||||
CYCLE = "2",
|
||||
SPECIFY = "3"
|
||||
INTERVAL = 1,
|
||||
CYCLE,
|
||||
SPECIFY
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import type { Dayjs } from "dayjs";
|
||||
import type {
|
||||
IGlobalInstanceConfig,
|
||||
ILayoutCard as GlobalLayoutCard,
|
||||
@ -9,7 +10,7 @@ import type {
|
||||
IQuickStartTemplate,
|
||||
IQuickStartPackages
|
||||
} from "../../../common/global";
|
||||
import type { INSTANCE_STATUS_CODE } from "./const";
|
||||
import type { INSTANCE_STATUS_CODE, ScheduleCreateType } from "./const";
|
||||
|
||||
export type JsonData = IJsonData;
|
||||
export type MapData<T> = IMapData<T>;
|
||||
@ -73,6 +74,9 @@ export interface Settings {
|
||||
presetPackAddr: string;
|
||||
redisUrl: string;
|
||||
allowUsePreset: boolean;
|
||||
businessMode: boolean;
|
||||
businessId: string;
|
||||
allowChangeCmd: boolean;
|
||||
}
|
||||
|
||||
export interface ImageInfo {
|
||||
@ -221,10 +225,17 @@ export interface Schedule {
|
||||
|
||||
export interface NewScheduleTask {
|
||||
name: string;
|
||||
count: string;
|
||||
count: number;
|
||||
time: string;
|
||||
action: string;
|
||||
type: string;
|
||||
type: ScheduleCreateType;
|
||||
}
|
||||
|
||||
export interface ScheduleTaskForm extends NewScheduleTask {
|
||||
payload: string;
|
||||
weekend: number[];
|
||||
cycle: string[];
|
||||
objTime: Dayjs;
|
||||
}
|
||||
|
||||
export interface PanelStatus {
|
||||
@ -234,5 +245,8 @@ export interface PanelStatus {
|
||||
settings: {
|
||||
canFileManager: boolean;
|
||||
allowUsePreset: boolean;
|
||||
businessMode: boolean;
|
||||
businessId: string;
|
||||
allowChangeCmd: boolean;
|
||||
};
|
||||
}
|
||||
|
@ -115,7 +115,6 @@ const showCardOperator = (card: ILayoutCard) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
// Gloabl
|
||||
@import "../assets/variables.scss";
|
||||
|
||||
// Hide the EmptyCard component when mobile device.
|
||||
@ -152,12 +151,10 @@ const showCardOperator = (card: ILayoutCard) => {
|
||||
@keyframes scaleAnimation {
|
||||
0% {
|
||||
opacity: 0.02;
|
||||
// transform: scale(0.98);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
// transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -500,7 +500,7 @@ onMounted(async () => {
|
||||
:target-instance-info="item"
|
||||
:target-daemon-id="currentRemoteNode?.uuid"
|
||||
@click="handleSelectInstance(item)"
|
||||
@refrsh-list="initInstancesData()"
|
||||
@refresh-list="initInstancesData()"
|
||||
/>
|
||||
</a-col>
|
||||
</fade-up-animation>
|
||||
|
@ -34,7 +34,7 @@ const formData = reactive({
|
||||
});
|
||||
|
||||
const { execute: login } = loginUser();
|
||||
const { updateUserInfo, isAdmin } = useAppStateStore();
|
||||
const { updateUserInfo, isAdmin, state: appConfig } = useAppStateStore();
|
||||
|
||||
const loginStep = ref(0);
|
||||
const is2Fa = ref(false);
|
||||
@ -89,6 +89,10 @@ const loginSuccess = () => {
|
||||
router.push({ path: "/customer" });
|
||||
}
|
||||
};
|
||||
|
||||
const openBuyInstanceDialog = async () => {
|
||||
router.push({ path: "/shop" });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -157,7 +161,7 @@ const loginSuccess = () => {
|
||||
</form>
|
||||
|
||||
<div class="mt-24 flex-between align-center">
|
||||
<div class="mcsmanager-link">
|
||||
<div v-if="!appConfig.settings.businessMode" class="mcsmanager-link">
|
||||
<div
|
||||
v-if="pageInfoResult?.loginInfo"
|
||||
class="global-markdown-html"
|
||||
@ -168,9 +172,21 @@ const loginSuccess = () => {
|
||||
MCSManager
|
||||
</a>
|
||||
</div>
|
||||
<a-button size="large" type="primary" style="min-width: 95px" @click="handleLogin">
|
||||
{{ t("TXT_CODE_d507abff") }}
|
||||
</a-button>
|
||||
<div v-else></div>
|
||||
<div class="justify-end" style="gap: 10px">
|
||||
<a-button
|
||||
v-if="appConfig.settings.businessMode"
|
||||
size="large"
|
||||
class="green"
|
||||
style="min-width: 95px"
|
||||
@click="openBuyInstanceDialog"
|
||||
>
|
||||
{{ t("TXT_CODE_5a408a5e") }}
|
||||
</a-button>
|
||||
<a-button size="large" type="primary" style="min-width: 95px" @click="handleLogin">
|
||||
{{ t("TXT_CODE_d2c1a316") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
BookOutlined,
|
||||
BugOutlined,
|
||||
GithubOutlined,
|
||||
KeyOutlined,
|
||||
LockOutlined,
|
||||
MessageOutlined,
|
||||
MoneyCollectOutlined,
|
||||
@ -40,7 +41,7 @@ interface MySettings extends Settings {
|
||||
bgUrl?: string;
|
||||
}
|
||||
|
||||
const ApacheLicense = `Copyright 2024 MCSManager Dev
|
||||
const ApacheLicense = `Copyright ${new Date().getFullYear()} MCSManager Dev
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -56,7 +57,7 @@ limitations under the License.`;
|
||||
|
||||
const formData = ref<MySettings>();
|
||||
|
||||
const submit = async () => {
|
||||
const submit = async (needReload: boolean = true) => {
|
||||
if (formData.value) {
|
||||
try {
|
||||
await submitExecute({
|
||||
@ -65,7 +66,7 @@ const submit = async () => {
|
||||
}
|
||||
});
|
||||
message.success(t("TXT_CODE_a7907771"));
|
||||
setTimeout(() => window.location.reload(), 600);
|
||||
if (needReload) setTimeout(() => window.location.reload(), 600);
|
||||
} catch (error: any) {
|
||||
reportErrorMsg(error);
|
||||
}
|
||||
@ -88,6 +89,12 @@ const menus = arrayFilter([
|
||||
key: "security",
|
||||
icon: LockOutlined
|
||||
},
|
||||
{
|
||||
title: t("TXT_CODE_8bb8e2a1"),
|
||||
key: "business",
|
||||
icon: KeyOutlined,
|
||||
condition: () => isCN()
|
||||
},
|
||||
{
|
||||
title: t("TXT_CODE_3b4b656d"),
|
||||
key: "about",
|
||||
@ -186,6 +193,10 @@ const startDesignUI = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const gotoBusinessCenter = () => {
|
||||
window.open("https://redeem.mcsmanager.com/", "_blank");
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await execute();
|
||||
const cfg = await getSettingsConfig();
|
||||
@ -376,6 +387,29 @@ onMounted(async () => {
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-form-item>
|
||||
<a-typography-title :level="5">
|
||||
{{ t("TXT_CODE_a583cae4") }}
|
||||
</a-typography-title>
|
||||
<a-typography-paragraph>
|
||||
<a-typography-text type="secondary">
|
||||
{{ t("TXT_CODE_bfbdf579") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<a-select
|
||||
v-model:value.prop="(formData as any).allowChangeCmd"
|
||||
style="max-width: 320px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in allYesNo"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-typography-title :level="5">
|
||||
{{ t("TXT_CODE_adab942e") }}
|
||||
@ -387,7 +421,10 @@ onMounted(async () => {
|
||||
{{ t("TXT_CODE_e5b7522d") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<a-select v-model:value.prop="formData.canFileManager" style="max-width: 320px">
|
||||
<a-select
|
||||
v-model:value.prop="(formData as any).canFileManager"
|
||||
style="max-width: 320px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in allYesNo"
|
||||
:key="item.value"
|
||||
@ -407,7 +444,10 @@ onMounted(async () => {
|
||||
{{ t("TXT_CODE_f5f9664") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<a-select v-model:value.prop="formData.allowUsePreset" style="max-width: 320px">
|
||||
<a-select
|
||||
v-model:value.prop="(formData as any).allowUsePreset"
|
||||
style="max-width: 320px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in allYesNo"
|
||||
:key="item.value"
|
||||
@ -428,7 +468,10 @@ onMounted(async () => {
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-select v-model:value.prop="formData.crossDomain" style="max-width: 320px">
|
||||
<a-select
|
||||
v-model:value.prop="(formData as any).crossDomain"
|
||||
style="max-width: 320px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in allYesNo"
|
||||
:key="item.value"
|
||||
@ -450,7 +493,7 @@ onMounted(async () => {
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-select
|
||||
v-model:value.prop="formData.reverseProxyMode"
|
||||
v-model:value.prop="(formData as any).reverseProxyMode"
|
||||
style="max-width: 320px"
|
||||
>
|
||||
<a-select-option
|
||||
@ -473,7 +516,10 @@ onMounted(async () => {
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-select v-model:value.prop="formData.loginCheckIp" style="max-width: 320px">
|
||||
<a-select
|
||||
v-model:value.prop="(formData as any).loginCheckIp"
|
||||
style="max-width: 320px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in allYesNo"
|
||||
:key="item.value"
|
||||
@ -484,7 +530,7 @@ onMounted(async () => {
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<div class="button">
|
||||
<a-button type="primary" :loading="submitIsLoading" @click="submit()">
|
||||
<a-button type="primary" :loading="submitIsLoading" @click="submit(false)">
|
||||
{{ t("TXT_CODE_abfe9512") }}
|
||||
</a-button>
|
||||
</div>
|
||||
@ -493,6 +539,67 @@ onMounted(async () => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #business>
|
||||
<div
|
||||
:style="{
|
||||
maxHeight: card.height,
|
||||
overflowY: 'auto'
|
||||
}"
|
||||
>
|
||||
<a-typography-title :level="4" class="mb-24">
|
||||
{{ t("TXT_CODE_8bb8e2a1") }}
|
||||
</a-typography-title>
|
||||
<div class="mb-24">
|
||||
<a-typography-paragraph>
|
||||
<a-typography-title :level="5">
|
||||
{{ t("TXT_CODE_180884da") }}
|
||||
</a-typography-title>
|
||||
<a-typography-text type="secondary">
|
||||
{{ t("TXT_CODE_3f227bcf") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<div>
|
||||
<a-switch v-model:checked="formData.businessMode" @change="submit(false)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-24">
|
||||
<a-typography-paragraph>
|
||||
<a-typography-title :level="5">
|
||||
{{ t("TXT_CODE_d31196db") }}
|
||||
</a-typography-title>
|
||||
<a-typography-text type="secondary">
|
||||
{{ t("TXT_CODE_59c39e03") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<div>
|
||||
<a-button :disabled="!formData.businessMode" @click="gotoBusinessCenter()">
|
||||
{{ t("TXT_CODE_2dbd3cd3") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="formData.businessMode" class="mb-24">
|
||||
<a-typography-paragraph>
|
||||
<a-typography-title :level="5">{{ t("TXT_CODE_72cfab69") }}</a-typography-title>
|
||||
<a-typography-text type="secondary">
|
||||
{{ t("TXT_CODE_678164d7") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<div>
|
||||
<a-input
|
||||
v-model:value="formData.businessId"
|
||||
style="max-width: 200px"
|
||||
placeholder="eg: 123"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a-button type="primary" :loading="submitIsLoading" @click="submit(false)">
|
||||
{{ t("TXT_CODE_abfe9512") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #about>
|
||||
<div :style="{ maxHeight: card.height, overflowY: 'auto' }">
|
||||
<a-typography-title :level="4" class="mb-24">
|
||||
|
164
frontend/src/widgets/ShelvesCard.vue
Normal file
164
frontend/src/widgets/ShelvesCard.vue
Normal file
@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import PurchaseQueryDialog from "@/components/fc/PurchaseQueryDialog.vue";
|
||||
import UseRedeemDialog from "@/components/fc/UseRedeemDialog.vue";
|
||||
import InnerCard from "@/components/InnerCard.vue";
|
||||
import { router } from "@/config/router";
|
||||
import { useMountComponent } from "@/hooks/useMountComponent";
|
||||
import { t } from "@/lang/i18n";
|
||||
import { useShopInfo } from "@/services/apis/redeem";
|
||||
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||
import type { LayoutCard } from "@/types";
|
||||
defineProps<{
|
||||
card: LayoutCard;
|
||||
}>();
|
||||
|
||||
const { products: shopItems, isLoading, isError } = useShopInfo();
|
||||
const { isLogged } = useAppStateStore();
|
||||
|
||||
const reloadPage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const openDialog = async () => {
|
||||
useMountComponent().mount(UseRedeemDialog);
|
||||
};
|
||||
|
||||
const openPurchaseQueryDialog = async () => {
|
||||
useMountComponent().mount(PurchaseQueryDialog);
|
||||
};
|
||||
|
||||
const openLoginPage = () => {
|
||||
router.push("/login");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardPanel class="card-wrapper h-100 w-100" :style="{ maxHeight: card.height }">
|
||||
<template #title>
|
||||
{{ card.title }}
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="isError" class="error-container">
|
||||
<a-result
|
||||
status="error"
|
||||
:title="t('TXT_CODE_84cf0951')"
|
||||
:icon="null"
|
||||
:sub-title="isError.message"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="reloadPage">
|
||||
{{ t("TXT_CODE_d080f2d7") }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
<div v-if="isLoading">
|
||||
<Loading />
|
||||
</div>
|
||||
|
||||
<div v-if="shopItems" class="shop-item-container">
|
||||
<div class="flex justify-end mb-20" style="gap: 10px">
|
||||
<a-button type="primary" @click="openDialog">
|
||||
{{ t("TXT_CODE_75bf9192") }}
|
||||
</a-button>
|
||||
<a-button v-if="!isLogged" @click="openLoginPage">
|
||||
{{ t("TXT_CODE_22510c5c") }}
|
||||
</a-button>
|
||||
<a-button @click="openPurchaseQueryDialog">
|
||||
{{ t("TXT_CODE_17b3748b") }}
|
||||
</a-button>
|
||||
</div>
|
||||
<InnerCard
|
||||
v-for="item in shopItems"
|
||||
:key="item.productId"
|
||||
class="shelves-card-container"
|
||||
:full-height="false"
|
||||
>
|
||||
<template #title>{{ item.title }}</template>
|
||||
<template #operator>
|
||||
<a-typography-text type="secondary" style="font-weight: 400">
|
||||
Product ID {{ item.productId }}
|
||||
</a-typography-text>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="">
|
||||
<div class="shelves-card-item">
|
||||
<div class="shelves-card-item-title">
|
||||
<pre>{{ item.remark }}</pre>
|
||||
</div>
|
||||
<div class="shelves-card-item-price">
|
||||
<div>
|
||||
<div class="shelves-card-item-price-label">{{ t("TXT_CODE_4bf8a52f") }}</div>
|
||||
<div>
|
||||
<span class="price-text"> {{ item.price }} </span>
|
||||
<span>/{{ t("TXT_CODE_6cb9bb04") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</InnerCard>
|
||||
</div>
|
||||
</template>
|
||||
</CardPanel>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.shop-item-container {
|
||||
overflow-y: auto;
|
||||
text-align: left;
|
||||
color: var(--color-gray-13);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
font-family:
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
sans-serif,
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
Segoe UI Symbol,
|
||||
"Noto Color Emoji";
|
||||
background: unset !important;
|
||||
border: unset !important;
|
||||
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.shelves-card-container {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.shelves-card-item-title {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.shelves-card-item-price {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
|
||||
.shelves-card-item-price-label {
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.price-text {
|
||||
color: var(--color-red-6);
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
82
frontend/src/widgets/ShopInfoCard.vue
Normal file
82
frontend/src/widgets/ShopInfoCard.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from "@/lang/i18n";
|
||||
import type { LayoutCard } from "@/types";
|
||||
import { useShopInfo } from "@/services/apis/redeem";
|
||||
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||
import Loading from "@/components/Loading.vue";
|
||||
|
||||
defineProps<{
|
||||
card: LayoutCard;
|
||||
}>();
|
||||
|
||||
const { shopInfo, isLoading, isError } = useShopInfo();
|
||||
const { isAdmin } = useAppStateStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardPanel class="card-wrapper h-100 w-100" :style="{ maxHeight: card.height }">
|
||||
<template #title>
|
||||
{{ shopInfo?.nickname }}
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="isLoading">
|
||||
<Loading />
|
||||
</div>
|
||||
<div v-if="shopInfo" class="shop-item-container">
|
||||
<div class="container flex flex-between mb-20" style="gap: 20px">
|
||||
<div>
|
||||
<a-typography-paragraph>
|
||||
<a-typography-text v-if="shopInfo.nickname" type="secondary">
|
||||
{{ shopInfo.introduction }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
<a-typography-text v-if="shopInfo.afterSalesGroup">
|
||||
<span>{{ t("TXT_CODE_ef27fda1") }}</span>
|
||||
<a-tag>{{ shopInfo.afterSalesGroup }}</a-tag>
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph v-if="isAdmin">
|
||||
<a-typography-text>
|
||||
{{ t("TXT_CODE_ec0b25f5") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isError" class="error-container">
|
||||
<div>
|
||||
<p>
|
||||
{{ t("TXT_CODE_e5bf0df1") }}
|
||||
</p>
|
||||
<p>
|
||||
{{ t("TXT_CODE_4ef3f800") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CardPanel>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.shop-item-container {
|
||||
overflow-y: auto;
|
||||
text-align: left;
|
||||
color: var(--color-gray-13);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -29,16 +29,6 @@ const columns = [
|
||||
return INSTANCE_STATUS[e.text] || e.text;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("TXT_CODE_662ad338"),
|
||||
dataIndex: "ie",
|
||||
customRender: (e: { text: string; record: { oe: string; ie: string } }) => {
|
||||
if (!e.record.oe && !e.record.ie) {
|
||||
return "--";
|
||||
}
|
||||
return `${t("TXT_CODE_bb888626")}:${e.record.oe} ${t("TXT_CODE_4b6e951")}:${e.record.ie}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("TXT_CODE_5ab2062d"),
|
||||
dataIndex: "lastDatetime",
|
||||
@ -100,7 +90,7 @@ onMounted(() => {
|
||||
:disabled="record.status === INSTANCE_STATUS_CODE.BUSY"
|
||||
@click="operate(record.daemonId, record.instanceUuid)"
|
||||
>
|
||||
{{ t("TXT_CODE_5974bf24") }}
|
||||
{{ t("TXT_CODE_aa43b248") }}
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -99,10 +99,16 @@ onMounted(async () => {
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph v-if="instanceGameServerInfo">
|
||||
{{ t("TXT_CODE_855c4a1c") }}{{ instanceGameServerInfo.players }}
|
||||
<span>{{ t("TXT_CODE_855c4a1c") }}</span>
|
||||
<span>{{ instanceGameServerInfo.players }}</span>
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph v-if="instanceGameServerInfo">
|
||||
{{ t("TXT_CODE_e260a220") }}{{ instanceGameServerInfo.version }}
|
||||
<span>
|
||||
{{ t("TXT_CODE_e260a220") }}
|
||||
</span>
|
||||
<span>
|
||||
{{ instanceGameServerInfo.version }}
|
||||
</span>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
@ -126,13 +132,15 @@ onMounted(async () => {
|
||||
<div
|
||||
v-for="(item, index) in dockerPortsArray(instanceInfo?.config.docker.ports ?? [])"
|
||||
:key="index"
|
||||
style="margin-bottom: 2px"
|
||||
class="mb-4"
|
||||
>
|
||||
<span>{{ t("TXT_CODE_8dfc41ef") }}: {{ item.host }}</span>
|
||||
<span style="margin-left: 6px">{{ t("TXT_CODE_8f8103b7") }}: {{ item.container }}</span>
|
||||
<span style="margin-left: 8px">
|
||||
<span>
|
||||
<a-tag color="green">{{ item.protocol.toUpperCase() }}</a-tag>
|
||||
</span>
|
||||
<a-tag>
|
||||
<span>{{ t("TXT_CODE_8dfc41ef") }}: {{ item.host }}</span>
|
||||
<span class="ml-4"> {{ t("TXT_CODE_8f8103b7") }}: {{ item.container }} </span>
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</a-typography-paragraph>
|
||||
@ -150,9 +158,13 @@ onMounted(async () => {
|
||||
</span>
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
<a-typography-text> {{ t("TXT_CODE_30051f9b") }} </a-typography-text>
|
||||
<a-typography-text :title="instanceInfo?.instanceUuid">
|
||||
{{ t("TXT_CODE_30051f9b") }}
|
||||
</a-typography-text>
|
||||
<a-typography-text :copyable="{ text: instanceInfo?.instanceUuid }"> </a-typography-text>
|
||||
<a-typography-text class="ml-20"> {{ t("TXT_CODE_5f2d2e30") }} </a-typography-text>
|
||||
<a-typography-text class="ml-20" :title="daemonId">
|
||||
{{ t("TXT_CODE_5f2d2e30") }}
|
||||
</a-typography-text>
|
||||
<a-typography-text :copyable="{ text: daemonId }"> </a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
|
@ -70,13 +70,16 @@ const {
|
||||
beforeUpload,
|
||||
downloadFile,
|
||||
handleChangeDir,
|
||||
handleSearchChange,
|
||||
selectedFile,
|
||||
rowClickTable,
|
||||
handleTableChange,
|
||||
getFileStatus,
|
||||
changePermission,
|
||||
toDisk,
|
||||
oneSelected
|
||||
oneSelected,
|
||||
isImage,
|
||||
showImage
|
||||
} = useFileManager(instanceId, daemonId);
|
||||
|
||||
const { openRightClickMenu } = useRightClickMenu();
|
||||
@ -189,6 +192,13 @@ const editFile = (fileName: string) => {
|
||||
FileEditorDialog.value?.openDialog(path, fileName);
|
||||
};
|
||||
|
||||
const handleClickFile = async (file: DataType) => {
|
||||
if (file.type === 0) return rowClickTable(file.name, file.type);
|
||||
const fileExtName = getFileExtName(file.name);
|
||||
if (isImage(fileExtName)) return showImage(file);
|
||||
return editFile(file.name);
|
||||
};
|
||||
|
||||
const menuList = (record: DataType) =>
|
||||
arrayFilter<ItemType & { style?: CSSProperties }>([
|
||||
{
|
||||
@ -308,7 +318,11 @@ onUnmounted(() => {
|
||||
</template>
|
||||
<template #right>
|
||||
<a-typography-text v-if="selectedRowKeys.length">
|
||||
{{ t("TXT_CODE_7b2c5414") + ` ${String(selectedRowKeys.length)} ` + t("TXT_CODE_5cd3b4bd") }}
|
||||
{{
|
||||
`${t("TXT_CODE_7b2c5414")} ${String(selectedRowKeys.length)} ${t(
|
||||
"TXT_CODE_5cd3b4bd"
|
||||
)}`
|
||||
}}
|
||||
</a-typography-text>
|
||||
|
||||
<a-upload
|
||||
@ -383,7 +397,7 @@ onUnmounted(() => {
|
||||
v-model:value.trim.lazy="operationForm.name"
|
||||
:placeholder="t('TXT_CODE_7cad42a5')"
|
||||
allow-clear
|
||||
@change="getFileList()"
|
||||
@change="handleSearchChange()"
|
||||
>
|
||||
<template #suffix>
|
||||
<search-outlined />
|
||||
@ -466,12 +480,13 @@ onUnmounted(() => {
|
||||
:custom-row="
|
||||
(record: DataType) => {
|
||||
return {
|
||||
onContextmenu: (e: MouseEvent) => handleRightClickRow(e, record)
|
||||
onContextmenu: (e: MouseEvent) => handleRightClickRow(e, record as DataType)
|
||||
};
|
||||
}
|
||||
"
|
||||
@change="
|
||||
(e) => handleTableChange({ current: e.current || 0, pageSize: e.pageSize || 0 })
|
||||
(e: any) =>
|
||||
handleTableChange({ current: e.current || 0, pageSize: e.pageSize || 0 })
|
||||
"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@ -479,11 +494,7 @@ onUnmounted(() => {
|
||||
<a-button
|
||||
type="link"
|
||||
class="file-name"
|
||||
@click="
|
||||
record.type !== 1
|
||||
? rowClickTable(record.name, record.type)
|
||||
: editFile(record.name)
|
||||
"
|
||||
@click="handleClickFile(record as DataType)"
|
||||
>
|
||||
<span class="mr-4">
|
||||
<component
|
||||
|
@ -19,12 +19,17 @@ import { LayoutCardHeight } from "../../config/originLayoutConfig";
|
||||
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||
import { useAppRouters } from "@/hooks/useAppRouters";
|
||||
import { useLayoutCardTools } from "../../hooks/useCardTools";
|
||||
import { TYPE_MINECRAFT_JAVA, useInstanceInfo } from "@/hooks/useInstance";
|
||||
import {
|
||||
TYPE_MINECRAFT_JAVA,
|
||||
TYPE_STEAM_SERVER_UNIVERSAL,
|
||||
useInstanceInfo
|
||||
} from "@/hooks/useInstance";
|
||||
import TermConfig from "./dialogs/TermConfig.vue";
|
||||
import EventConfig from "./dialogs/EventConfig.vue";
|
||||
import PingConfig from "./dialogs/PingConfig.vue";
|
||||
import RconSettings from "./dialogs/RconSettings.vue";
|
||||
import InstanceDetail from "./dialogs/InstanceDetail.vue";
|
||||
import InstanceFundamentalDetail from "./dialogs/InstanceFundamentalDetail.vue";
|
||||
import type { RouteLocationPathRaw } from "vue-router";
|
||||
import { TYPE_UNIVERSAL, TYPE_WEB_SHELL } from "../../hooks/useInstance";
|
||||
import McPingSettings from "./dialogs/McPingSettings.vue";
|
||||
@ -36,6 +41,7 @@ const mcSettingsDialog = ref<InstanceType<typeof McPingSettings>>();
|
||||
const eventConfigDialog = ref<InstanceType<typeof EventConfig>>();
|
||||
const pingConfigDialog = ref<InstanceType<typeof PingConfig>>();
|
||||
const instanceDetailsDialog = ref<InstanceType<typeof InstanceDetail>>();
|
||||
const instanceFundamentalDetailDialog = ref<InstanceType<typeof InstanceFundamentalDetail>>();
|
||||
|
||||
const { toPage: toOtherPager } = useAppRouters();
|
||||
|
||||
@ -118,7 +124,9 @@ const btns = computed(() => {
|
||||
icon: BuildOutlined,
|
||||
click: () => {
|
||||
rconSettingsDialog.value?.openDialog();
|
||||
}
|
||||
},
|
||||
condition: () =>
|
||||
instanceInfo.value?.config.type.includes(TYPE_STEAM_SERVER_UNIVERSAL) ?? false
|
||||
},
|
||||
{
|
||||
title: t("TXT_CODE_d23631cb"),
|
||||
@ -148,7 +156,6 @@ const btns = computed(() => {
|
||||
eventConfigDialog.value?.openDialog();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
title: t("TXT_CODE_4f34fc28"),
|
||||
icon: AppstoreAddOutlined,
|
||||
@ -156,6 +163,17 @@ const btns = computed(() => {
|
||||
click: () => {
|
||||
instanceDetailsDialog.value?.openDialog();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("TXT_CODE_4f34fc28"),
|
||||
icon: AppstoreAddOutlined,
|
||||
condition: () =>
|
||||
!isAdmin.value &&
|
||||
instanceInfo.value?.config.processType === "docker" &&
|
||||
state.settings.allowChangeCmd,
|
||||
click: () => {
|
||||
instanceFundamentalDetailDialog.value?.openDialog();
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
@ -221,6 +239,14 @@ const btns = computed(() => {
|
||||
@update="refreshInstanceInfo"
|
||||
/>
|
||||
|
||||
<InstanceFundamentalDetail
|
||||
ref="instanceFundamentalDetailDialog"
|
||||
:instance-info="instanceInfo"
|
||||
:instance-id="instanceId"
|
||||
:daemon-id="daemonId"
|
||||
@update="refreshInstanceInfo"
|
||||
/>
|
||||
|
||||
<RconSettings
|
||||
ref="rconSettingsDialog"
|
||||
:instance-info="instanceInfo"
|
||||
|
@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { t } from "@/lang/i18n";
|
||||
import { t, $t } from "@/lang/i18n";
|
||||
import { message } from "ant-design-vue";
|
||||
import { reportErrorMsg } from "@/tools/validator";
|
||||
import { DeleteOutlined, FieldTimeOutlined } from "@ant-design/icons-vue";
|
||||
import { DeleteOutlined, EditOutlined, FieldTimeOutlined } from "@ant-design/icons-vue";
|
||||
import CardPanel from "@/components/CardPanel.vue";
|
||||
import BetweenMenus from "@/components/BetweenMenus.vue";
|
||||
import { useLayoutCardTools } from "@/hooks/useCardTools";
|
||||
import { useAppRouters } from "@/hooks/useAppRouters";
|
||||
import { scheduleList, scheduleDelete } from "@/services/apis/instance";
|
||||
import type { LayoutCard, Schedule } from "@/types/index";
|
||||
import { ScheduleAction, ScheduleType, ScheduleCreateType } from "@/types/const";
|
||||
import NewSchedule from "@/widgets/instance/dialogs/NewSchedule.vue";
|
||||
import type { AntColumnsType } from "../../types/ant";
|
||||
import { useScreen } from "@/hooks/useScreen";
|
||||
import { useSchedule } from "@/hooks/useSchedule";
|
||||
import { padZero } from "@/tools/common";
|
||||
|
||||
const props = defineProps<{
|
||||
card: LayoutCard;
|
||||
@ -25,83 +25,34 @@ const instanceId = getMetaOrRouteValue("instanceId");
|
||||
const daemonId = getMetaOrRouteValue("daemonId");
|
||||
const { toPage } = useAppRouters();
|
||||
const newScheduleDialog = ref<InstanceType<typeof NewSchedule>>();
|
||||
const { getScheduleList, schedules, scheduleListLoading, deleteSchedule } = useSchedule(
|
||||
String(instanceId),
|
||||
String(daemonId)
|
||||
);
|
||||
|
||||
const { state, execute, isLoading } = scheduleList();
|
||||
const getScheduleList = async () => {
|
||||
try {
|
||||
await execute({
|
||||
params: {
|
||||
daemonId: daemonId ?? "",
|
||||
uuid: instanceId ?? ""
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
reportErrorMsg(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSchedule = async (name: string) => {
|
||||
const { execute, state } = scheduleDelete();
|
||||
try {
|
||||
await execute({
|
||||
params: {
|
||||
daemonId: daemonId ?? "",
|
||||
uuid: instanceId ?? "",
|
||||
task_name: name
|
||||
}
|
||||
});
|
||||
if (state.value) {
|
||||
message.success(t("TXT_CODE_28190dbc"));
|
||||
await getScheduleList();
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
reportErrorMsg(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const rendTime = (text: string, schedule: Schedule) => {
|
||||
switch (schedule.type.toString()) {
|
||||
case ScheduleCreateType.INTERVAL: {
|
||||
const time = Number(text);
|
||||
let s = time;
|
||||
let m = 0;
|
||||
let h = 0;
|
||||
while (s >= 60) {
|
||||
s -= 60;
|
||||
m += 1;
|
||||
}
|
||||
while (m >= 60) {
|
||||
m -= 60;
|
||||
h += 1;
|
||||
}
|
||||
return `${t("TXT_CODE_ec6d29f4")} ${h} ${t("TXT_CODE_e3db239d")} ${m} ${t(
|
||||
const timeRender = (text: string, schedule: Schedule) => {
|
||||
const formatFunctions = {
|
||||
[ScheduleCreateType.INTERVAL]: (t: string) => {
|
||||
const time = Number(t);
|
||||
const h = padZero(Math.floor(time / 3600).toString());
|
||||
const m = padZero(Math.floor((time % 3600) / 60).toString());
|
||||
const s = padZero((time % 60).toString());
|
||||
return `${$t("TXT_CODE_ec6d29f4")} ${h} ${$t("TXT_CODE_e3db239d")} ${m} ${$t(
|
||||
"TXT_CODE_3b1bb444"
|
||||
)} ${s} ${t("TXT_CODE_acabc771")}`;
|
||||
)} ${s} ${$t("TXT_CODE_acabc771")}`;
|
||||
},
|
||||
[ScheduleCreateType.CYCLE]: (time: string) => {
|
||||
const [s, m, h, , , w] = time.split(" ");
|
||||
return `${t("TXT_CODE_76750199")} ${w} / ${padZero(h)}:${padZero(m)}:${padZero(s)}`;
|
||||
},
|
||||
[ScheduleCreateType.SPECIFY]: (time: string) => {
|
||||
const [s, m, h, dd, mm] = time.split(" ");
|
||||
return `${mm}/${dd} ${padZero(h)}:${padZero(m)}:${padZero(s)}`;
|
||||
}
|
||||
case ScheduleCreateType.CYCLE: {
|
||||
const time = text;
|
||||
const timeArr = time.split(" ");
|
||||
const h = timeArr[2];
|
||||
const m = timeArr[1];
|
||||
const s = timeArr[0];
|
||||
const w = timeArr[5];
|
||||
return `${t("TXT_CODE_76750199")} ${w} / ${h}:${m}:${s}`;
|
||||
}
|
||||
case ScheduleCreateType.SPECIFY: {
|
||||
const time = text;
|
||||
const timeArr = time.split(" ");
|
||||
const h = timeArr[2];
|
||||
const m = timeArr[1];
|
||||
const s = timeArr[0];
|
||||
const dd = timeArr[3];
|
||||
const mm = timeArr[4];
|
||||
return `${mm} ${t("TXT_CODE_6cb9bb04")} ${dd} ${t("TXT_CODE_ca923eba")} ${h}:${m}:${s}`;
|
||||
}
|
||||
default:
|
||||
return "Unknown Time";
|
||||
}
|
||||
};
|
||||
|
||||
const formatFunction = formatFunctions[schedule.type as ScheduleCreateType];
|
||||
return formatFunction(text) ?? "Unknown Time";
|
||||
};
|
||||
|
||||
const columns: AntColumnsType[] = [
|
||||
@ -148,7 +99,7 @@ const columns: AntColumnsType[] = [
|
||||
dataIndex: "time",
|
||||
key: "time",
|
||||
minWidth: 240,
|
||||
customRender: (e: { text: string; record: Schedule }) => rendTime(e.text, e.record)
|
||||
customRender: (e: { text: string; record: Schedule }) => timeRender(e.text, e.record)
|
||||
},
|
||||
{
|
||||
align: "center",
|
||||
@ -205,9 +156,9 @@ onMounted(async () => {
|
||||
<a-col :span="24">
|
||||
<CardPanel style="height: 100%">
|
||||
<template #body>
|
||||
<a-spin :spinning="isLoading">
|
||||
<a-spin :spinning="scheduleListLoading">
|
||||
<a-table
|
||||
:data-source="state"
|
||||
:data-source="schedules"
|
||||
:columns="columns"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
:pagination="{
|
||||
@ -216,11 +167,19 @@ onMounted(async () => {
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'actions'">
|
||||
<a-button
|
||||
class="mr-8"
|
||||
size="large"
|
||||
@click="newScheduleDialog?.openDialog(record as Schedule)"
|
||||
>
|
||||
{{ t("TXT_CODE_ad207008") }}
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
:title="t('TXT_CODE_6ff0668f')"
|
||||
@confirm="deleteSchedule(record.name)"
|
||||
>
|
||||
<a-button size="large">
|
||||
<a-button danger size="large">
|
||||
{{ t("TXT_CODE_ecbd7449") }}
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
|
@ -14,7 +14,8 @@ import {
|
||||
CloudDownloadOutlined,
|
||||
CodeOutlined,
|
||||
UserOutlined,
|
||||
TagsOutlined
|
||||
TagsOutlined,
|
||||
DeleteOutlined
|
||||
} from "@ant-design/icons-vue";
|
||||
import {
|
||||
openInstance,
|
||||
@ -31,7 +32,8 @@ import { parseTimestamp } from "@/tools/time";
|
||||
import { arrayFilter } from "@/tools/array";
|
||||
import { useLayoutContainerStore } from "@/stores/useLayoutContainerStore";
|
||||
import { reportErrorMsg } from "@/tools/validator";
|
||||
import { openInstanceTagsEditor } from "@/components/fc/index";
|
||||
import { openInstanceTagsEditor, useDeleteInstanceDialog } from "@/components/fc/index";
|
||||
import _ from "lodash";
|
||||
|
||||
const props = defineProps<{
|
||||
card: LayoutCard;
|
||||
@ -39,7 +41,7 @@ const props = defineProps<{
|
||||
targetDaemonId?: string;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(["refrshList"]);
|
||||
const emits = defineEmits(["refreshList"]);
|
||||
|
||||
const { containerState } = useLayoutContainerStore();
|
||||
const { getMetaOrRouteValue } = useLayoutCardTools(props.card);
|
||||
@ -69,7 +71,7 @@ const { isLoading: updateLoading, execute: executeUpdate } = updateInstance();
|
||||
|
||||
const refreshList = () => {
|
||||
setTimeout(() => {
|
||||
emits("refrshList");
|
||||
emits("refreshList");
|
||||
}, 500);
|
||||
};
|
||||
|
||||
@ -194,7 +196,8 @@ const instanceOperations = computed(() =>
|
||||
},
|
||||
loading: killLoading.value,
|
||||
disabled: containerState.isDesignMode,
|
||||
danger: true
|
||||
danger: true,
|
||||
condition: () => !isStopped.value
|
||||
},
|
||||
{
|
||||
area: true
|
||||
@ -206,8 +209,8 @@ const instanceOperations = computed(() =>
|
||||
event.stopPropagation();
|
||||
if (instanceId && daemonId) {
|
||||
const tags = instanceInfo.value?.config.tag || [];
|
||||
await openInstanceTagsEditor(instanceId, daemonId, tags);
|
||||
refreshList();
|
||||
const newTags = await openInstanceTagsEditor(instanceId, daemonId, tags);
|
||||
if (!_.isEqual(newTags, tags)) refreshList();
|
||||
}
|
||||
},
|
||||
disabled: containerState.isDesignMode
|
||||
@ -226,6 +229,22 @@ const instanceOperations = computed(() =>
|
||||
});
|
||||
},
|
||||
disabled: containerState.isDesignMode
|
||||
},
|
||||
{
|
||||
title: t("TXT_CODE_a0e19f38"),
|
||||
icon: DeleteOutlined,
|
||||
click: async (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
const deleteInstanceResult = await useDeleteInstanceDialog(
|
||||
instanceId || "",
|
||||
daemonId || ""
|
||||
);
|
||||
if (!deleteInstanceResult) return;
|
||||
message.success(t("TXT_CODE_f486dbb4"));
|
||||
refreshList();
|
||||
},
|
||||
danger: true,
|
||||
disabled: containerState.isDesignMode
|
||||
}
|
||||
])
|
||||
);
|
||||
|
@ -12,7 +12,8 @@ import {
|
||||
RedoOutlined,
|
||||
LaptopOutlined,
|
||||
InteractionOutlined,
|
||||
LoadingOutlined
|
||||
LoadingOutlined,
|
||||
MoneyCollectOutlined
|
||||
} from "@ant-design/icons-vue";
|
||||
import { CheckCircleOutlined, InfoCircleOutlined } from "@ant-design/icons-vue";
|
||||
import { arrayFilter } from "../../tools/array";
|
||||
@ -35,6 +36,8 @@ import TerminalCore from "@/components/TerminalCore.vue";
|
||||
import Reinstall from "./dialogs/Reinstall.vue";
|
||||
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||
import { INSTANCE_TYPE_TRANSLATION } from "@/hooks/useInstance";
|
||||
import { useMountComponent } from "@/hooks/useMountComponent";
|
||||
import UseRedeemDialog from "@/components/fc/UseRedeemDialog.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
card: LayoutCard;
|
||||
@ -179,6 +182,18 @@ const instanceOperations = computed(() =>
|
||||
isStopped.value &&
|
||||
(state.settings.allowUsePreset || isAdmin.value) &&
|
||||
!isGlobalTerminal.value
|
||||
},
|
||||
{
|
||||
title: t("TXT_CODE_f77093c8"),
|
||||
icon: MoneyCollectOutlined,
|
||||
noConfirm: true,
|
||||
click: () => {
|
||||
useMountComponent({
|
||||
instanceId: instanceId
|
||||
}).mount(UseRedeemDialog);
|
||||
},
|
||||
props: {},
|
||||
condition: () => state.settings.businessMode
|
||||
}
|
||||
])
|
||||
);
|
||||
|
@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { t } from "@/lang/i18n";
|
||||
import { Modal } from "ant-design-vue";
|
||||
|
||||
import { batchDelete } from "@/services/apis/instance";
|
||||
import { reportErrorMsg } from "@/tools/validator";
|
||||
|
||||
const props = defineProps<{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
emitResult: (ok: boolean) => void;
|
||||
destroyComponent: () => void;
|
||||
instanceId: string;
|
||||
daemonId: string;
|
||||
}>();
|
||||
|
||||
const deleteFiles = ref(false);
|
||||
const isOpen = ref(true);
|
||||
const submitBtnLoading = ref(false);
|
||||
|
||||
const submit = async (deleteFile: boolean | null = deleteFiles.value) => {
|
||||
const { execute } = batchDelete();
|
||||
submitBtnLoading.value = true;
|
||||
try {
|
||||
await execute({
|
||||
params: {
|
||||
daemonId: props.daemonId || ""
|
||||
},
|
||||
data: {
|
||||
uuids: [props.instanceId || ""],
|
||||
deleteFile: deleteFile || false
|
||||
}
|
||||
});
|
||||
props.emitResult(true);
|
||||
} catch (error) {
|
||||
reportErrorMsg(error);
|
||||
props.emitResult(false);
|
||||
}
|
||||
isOpen.value = false;
|
||||
props.destroyComponent();
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
isOpen.value = false;
|
||||
props.emitResult(false);
|
||||
props.destroyComponent();
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (!deleteFiles.value) return await submit();
|
||||
Modal.confirm({
|
||||
title: t("TXT_CODE_584d786d"),
|
||||
content: t("TXT_CODE_90508729"),
|
||||
okText: t("TXT_CODE_10088738"),
|
||||
onOk: async () => {
|
||||
await submit();
|
||||
Modal.destroyAll();
|
||||
},
|
||||
okType: "danger"
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :visible="isOpen" :title="t('TXT_CODE_a0e19f38')" @cancel="onCancel">
|
||||
<a-typography-paragraph>
|
||||
<a-typography-text>
|
||||
{{ t("TXT_CODE_1981470a") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-checkbox v-model:checked="deleteFiles">
|
||||
{{ t("TXT_CODE_7542201a") }}
|
||||
</a-checkbox>
|
||||
|
||||
<template #footer>
|
||||
<a-button key="back" @click="onCancel">
|
||||
{{ t("TXT_CODE_a0451c97") }}
|
||||
</a-button>
|
||||
<a-button
|
||||
key="submit"
|
||||
:danger="deleteFiles"
|
||||
:loading="submitBtnLoading"
|
||||
type="primary"
|
||||
@click="onDelete"
|
||||
>
|
||||
{{ deleteFiles ? t("TXT_CODE_584d786d") : t("TXT_CODE_a0e19f38") }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
@ -134,7 +134,7 @@ defineExpose({
|
||||
:cancel-text="t('TXT_CODE_3b1cc020')"
|
||||
:ok-text="t('TXT_CODE_abfe9512')"
|
||||
:mask-closable="false"
|
||||
:width="fullScreen ? '100%' : '1300px'"
|
||||
:width="fullScreen ? '100%' : '1600px'"
|
||||
:confirm-loading="isLoading"
|
||||
@ok="submit"
|
||||
@cancel="cancel"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user