Merge pull request #1031 from MCSManager/abandon

Feat: optimize creation methods
This commit is contained in:
unitwk 2023-10-09 09:26:48 +08:00 committed by GitHub
commit 314aec6661
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 389 additions and 204 deletions

View File

@ -27,6 +27,7 @@ declare module 'vue' {
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
AppHeader: typeof import('./src/components/AppHeader.vue')['default']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARow: typeof import('ant-design-vue/es')['Row']
@ -46,6 +47,7 @@ declare module 'vue' {
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUpload: typeof import('ant-design-vue/es')['Upload']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
BetweenMenus: typeof import('./src/components/BetweenMenus.vue')['default']
CardError: typeof import('./src/components/CardError.vue')['default']
CardOperator: typeof import('./src/components/CardOperator.vue')['default']

View File

@ -11,6 +11,9 @@ import {
IdcardTwoTone,
NodeIndexOutlined,
ShoppingCartOutlined,
CloudUploadOutlined,
FileZipOutlined,
FileExcelOutlined,
TransactionOutlined,
SmileTwoTone,
CodeOutlined,
@ -29,6 +32,7 @@ export enum QUICKSTART_ACTION_TYPE {
export enum QUICKSTART_METHOD {
FAST = "FAST",
FILE = "FILE",
IMPORT = "IMPORT",
SELECT = "SELECT",
EXIST = "EXIST"
@ -111,20 +115,25 @@ export function useQuickStartFlow() {
formData.appType === QUICKSTART_ACTION_TYPE.Minecraft ||
formData.appType === QUICKSTART_ACTION_TYPE.Bedrock
},
{
title: t("上传单个服务端软件"),
key: QUICKSTART_METHOD.FILE,
icon: CloudUploadOutlined
},
{
title: t("上传服务端文件压缩包"),
key: QUICKSTART_METHOD.IMPORT,
icon: ShoppingCartOutlined
icon: FileZipOutlined
},
{
title: t("选择服务器现有目录"),
key: QUICKSTART_METHOD.SELECT,
icon: TransactionOutlined
icon: FolderOpenOutlined
},
{
title: t("无需额外文件"),
key: QUICKSTART_METHOD.EXIST,
icon: TransactionOutlined
icon: FileExcelOutlined
}
]);
};

View File

@ -0,0 +1,187 @@
import { useDefineApi } from "@/stores/useDefineApi";
// 获取文件列表
export const getFileList = useDefineApi<
{
params: {
remote_uuid: string;
uuid: string;
target: string;
page: number;
page_size: number;
file_name: string;
};
},
{
items: {
name: string;
size: number;
time: string;
type: number;
mode: number;
}[];
page: number;
pageSize: number;
total: number;
absolutePath: string;
}
>({
url: "/api/files/list",
method: "GET"
});
// 获取文件状态
export const getFileStatus = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
},
{
instanceFileTask: number;
globalFileTask: number;
platform: string;
isGlobalInstance: boolean;
dist: string[];
}
>({
url: "/api/files/status",
method: "GET"
});
// 新建文件夹
export const addFolder = useDefineApi<
{
data: {
target: string;
};
params: {
uuid: string;
remote_uuid: string;
};
},
boolean
>({
url: "/api/files/mkdir",
method: "POST"
});
// 新建文件
export const touchFile = useDefineApi<
{
data: {
target: string;
};
params: {
uuid: string;
remote_uuid: string;
};
},
boolean
>({
url: "/api/files/touch",
method: "POST"
});
// 删除文件
export const deleteFile = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
data: {
targets: string[];
};
},
boolean
>({
url: "/api/files",
method: "DELETE"
});
// 复制文件
export const copyFile = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
data: {
targets: string[][];
};
},
boolean
>({
url: "/api/files/copy",
method: "POST"
});
// 移动文件
export const moveFile = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
data: {
targets: string[][];
};
},
boolean
>({
url: "/api/files/move",
method: "PUT"
});
// 解压缩
export const compressFile = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
data: {
type: number;
targets: string[] | string;
source: string;
code: string;
};
},
boolean
>({
url: "/api/files/compress",
method: "POST"
});
// 获取文件上传地址
export const uploadAddress = useDefineApi<
{
params: {
upload_dir: string;
remote_uuid: string;
uuid: string;
};
},
{
password: string;
addr: string;
}
>({
url: "/api/files/upload",
method: "POST"
});
// 上传文件
export const uploadFile = useDefineApi<
{
data: FormData;
url: string;
onUploadProgress: Function;
},
{}
>({
method: "POST",
headers: { "Content-Type": "multipart/form-data" }
});

View File

@ -215,159 +215,3 @@ export const connectNode = useDefineApi<
url: "/api/service/link_remote_service",
method: "GET"
});
// 文件管理
// 获取文件列表
export const getFileList = useDefineApi<
{
params: {
remote_uuid: string;
uuid: string;
target: string;
page: number;
page_size: number;
file_name: string;
};
},
{
items: {
name: string;
size: number;
time: string;
type: number;
mode: number;
}[];
page: number;
pageSize: number;
total: number;
absolutePath: string;
}
>({
url: "/api/files/list",
method: "GET"
});
// 获取文件状态
export const getFileStatus = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
},
{
instanceFileTask: number;
globalFileTask: number;
platform: string;
isGlobalInstance: boolean;
dist: string[];
}
>({
url: "/api/files/status",
method: "GET"
});
// 新建文件夹
export const addFolder = useDefineApi<
{
data: {
target: string;
};
params: {
uuid: string;
remote_uuid: string;
};
},
boolean
>({
url: "/api/files/mkdir",
method: "POST"
});
// 新建文件
export const touchFile = useDefineApi<
{
data: {
target: string;
};
params: {
uuid: string;
remote_uuid: string;
};
},
boolean
>({
url: "/api/files/touch",
method: "POST"
});
// 删除文件
export const deleteFile = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
data: {
targets: string[];
};
},
boolean
>({
url: "/api/files",
method: "DELETE"
});
// 复制文件
export const copyFile = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
data: {
targets: string[][];
};
},
boolean
>({
url: "/api/files/copy",
method: "POST"
});
// 移动文件
export const moveFile = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
data: {
targets: string[][];
};
},
boolean
>({
url: "/api/files/move",
method: "PUT"
});
// 解压缩
export const compressFile = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
data: {
type: number;
targets: string[] | string;
source: string;
code: string;
};
},
boolean
>({
url: "/api/files/compress",
method: "POST"
});

View File

@ -144,6 +144,7 @@ export const uploadInstanceFile = useDefineApi<
unzip: number;
code: string;
};
data: FormData;
url: string;
onUploadProgress: Function;
},

View File

@ -1,3 +1,6 @@
export function computeNodeName(ip: string, remark?: string) {
return remark ? `${remark} - ${ip}` : ip;
import { t } from "@/lang/i18n";
export function computeNodeName(ip: string, available: boolean, remark?: string) {
const online = available ? "" : t("(离线)");
return remark ? `${remark} - ${ip} ${online}` : `${ip} ${online}`;
}

View File

@ -1,21 +1,22 @@
<script setup lang="ts">
import CardPanel from "@/components/CardPanel.vue";
import type { LayoutCard } from "@/types/index";
import { ref, onMounted, computed } from "vue";
import { ref, onMounted, computed, reactive } from "vue";
import { t } from "@/lang/i18n";
import {
SearchOutlined,
DownOutlined,
FormOutlined,
DatabaseOutlined,
AppstoreOutlined
AppstoreOutlined,
FrownOutlined
} from "@ant-design/icons-vue";
import BetweenMenus from "@/components/BetweenMenus.vue";
import { router } from "@/config/router";
import { remoteInstances } from "@/services/apis";
import { remoteNodeList } from "../services/apis";
import type { NodeStatus } from "../types/index";
import { message } from "ant-design-vue";
import { message, type ItemType } from "ant-design-vue";
import { computeNodeName } from "../tools/nodes";
import Loading from "@/components/Loading.vue";
import { useInstanceInfo } from "@/hooks/useInstance";
@ -53,7 +54,9 @@ const initNodes = async () => {
if (!nodes.value?.length) {
return message.error(t("TXT_CODE_e3d96a26"));
}
if (nodes.value?.length > 0) {
if (localStorage.getItem("pageSelectedRemote")) {
currentRemoteNode.value = JSON.parse(localStorage.pageSelectedRemote);
} else {
currentRemoteNode.value = nodes.value[0];
}
};
@ -62,14 +65,18 @@ const initInstancesData = async () => {
if (!currentRemoteNode.value) {
await initNodes();
}
await getInstances({
params: {
remote_uuid: currentRemoteNode.value?.uuid ?? "",
page: operationForm.value.currentPage,
page_size: operationForm.value.pageSize,
instance_name: operationForm.value.instanceName.trim()
}
});
try {
await getInstances({
params: {
remote_uuid: currentRemoteNode.value?.uuid ?? "",
page: operationForm.value.currentPage,
page_size: operationForm.value.pageSize,
instance_name: operationForm.value.instanceName.trim()
}
});
} catch (err) {
return message.error(t("访问远程节点异常"));
}
};
onMounted(async () => {
@ -90,7 +97,15 @@ const toAppDetailPage = (daemonId: string, instanceId: string) => {
});
};
const handleChangeNode = () => {};
const handleChangeNode = async (item: NodeStatus) => {
try {
currentRemoteNode.value = item;
await initInstancesData();
localStorage.setItem("pageSelectedRemote", JSON.stringify(item));
} catch (err: any) {
console.error(err.message);
}
};
const toCreateAppPage = () => {
router.push({
@ -113,10 +128,15 @@ const toCreateAppPage = () => {
<template #right>
<a-dropdown>
<template #overlay>
<a-menu @click="handleChangeNode">
<a-menu-item v-for="item in nodes" :key="item.uuid">
<DatabaseOutlined />
{{ computeNodeName(item.ip, item.remarks) }}
<a-menu>
<a-menu-item
v-for="item in nodes"
:key="item.uuid"
@click="handleChangeNode(item)"
>
<DatabaseOutlined v-if="item.available" />
<FrownOutlined v-else />
{{ computeNodeName(item.ip, item.available, item.remarks) }}
</a-menu-item>
<a-menu-divider />
<a-menu-item key="toNodesPage">
@ -126,7 +146,13 @@ const toCreateAppPage = () => {
</a-menu>
</template>
<a-button class="mr-12">
{{ computeNodeName(currentRemoteNode?.ip || "", currentRemoteNode?.remarks) }}
{{
computeNodeName(
currentRemoteNode?.ip || "",
currentRemoteNode?.available || true,
currentRemoteNode?.remarks
)
}}
<DownOutlined />
</a-button>
</a-dropdown>

View File

@ -3,7 +3,7 @@ import CardPanel from "@/components/CardPanel.vue";
import type { LayoutCard } from "@/types/index";
import { ref, computed, reactive, onMounted, watch, h, createVNode } from "vue";
import { t } from "@/lang/i18n";
import type { TableProps } from "ant-design-vue";
import type { TableProps, UploadProps } from "ant-design-vue";
import { convertFileSize } from "@/tools/fileSize";
import dayjs from "dayjs";
import {
@ -12,7 +12,8 @@ import {
FileOutlined,
FolderOutlined,
LoadingOutlined,
ExclamationCircleOutlined
ExclamationCircleOutlined,
UploadOutlined
} from "@ant-design/icons-vue";
import BetweenMenus from "@/components/BetweenMenus.vue";
import { useScreen } from "@/hooks/useScreen";
@ -26,10 +27,13 @@ import {
touchFile as touchFileApi,
copyFile as copyFileApi,
moveFile as moveFileApi,
compressFile as compressFileApi
} from "@/services/apis";
compressFile as compressFileApi,
uploadAddress,
uploadFile as uploadFileApi
} from "@/services/apis/fileManager";
import { throttle } from "lodash";
import { message, Modal } from "ant-design-vue";
import { parseForwardAddress } from "@/tools/protocol";
const props = defineProps<{
card: LayoutCard;
@ -414,6 +418,44 @@ const unzipFile = async (name: string) => {
}
};
const beforeUpload: UploadProps["beforeUpload"] = async (file) => {
await selectedFile(file);
return false;
};
const { state: cfg, execute: getCfg } = uploadAddress();
const { execute: uploadFile } = uploadFileApi();
const percentComplete = ref(0);
const selectedFile = async (file: File) => {
try {
await getCfg({
params: {
upload_dir: breadcrumbs[breadcrumbs.length - 1].path,
remote_uuid: daemonId!,
uuid: instanceId!
}
});
if (!cfg.value) throw new Error(t("获取上传地址失败"));
const uploadFormData = new FormData();
uploadFormData.append("file", file);
await uploadFile({
data: uploadFormData,
url: `${parseForwardAddress(cfg.value.addr, "http")}/upload/${cfg.value.password}`,
onUploadProgress: (progressEvent: any) => {
percentComplete.value = Math.round((progressEvent.loaded * 100) / progressEvent.total);
}
});
await getFileList();
percentComplete.value = 0;
return message.success(t("上传成功"));
} catch (err: any) {
console.error(err);
return message.error(err.message);
}
};
const rowSelection: TableProps["rowSelection"] = {
onChange: (selectedRowKeys: any, selectedRows: DataType[]) => {
selectionData.value = selectedRows;
@ -504,6 +546,22 @@ onMounted(() => {
</a-typography-title>
</template>
<template #right>
<a-upload
v-if="!screen.isPhone.value"
:before-upload="beforeUpload"
:max-count="1"
:disabled="percentComplete > 0"
:show-upload-list="false"
>
<a-button class="mr-8" type="dashed" :loading="percentComplete > 0">
<upload-outlined v-if="percentComplete === 0" />
{{
percentComplete > 0
? t("正在上传:") + percentComplete + "%"
: t("TXT_CODE_e00c858c")
}}
</a-button>
</a-upload>
<a-button
v-if="clipboard?.value && clipboard.value.length > 0"
type="dashed"
@ -513,13 +571,26 @@ onMounted(() => {
>
{{ t("TXT_CODE_f0260e51") }}
</a-button>
<a-button v-else class="mr-8" @click="reloadList()">
<a-button v-else class="mr-8" type="default" @click="reloadList()">
{{ t("刷新列表") }}
</a-button>
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item key="1">{{ t("TXT_CODE_e00c858c") }}</a-menu-item>
<a-upload
v-if="screen.isPhone.value"
:before-upload="beforeUpload"
:max-count="1"
:disabled="percentComplete > 0"
:show-upload-list="false"
>
<a-menu-item key="1" :disabled="percentComplete > 0">
{{ percentComplete > 0 ? t("上传中...") : t("TXT_CODE_e00c858c") }}
</a-menu-item>
<template #itemRender=""></template>
</a-upload>
<a-menu-item key="2" @click="touchFile(true)">
{{ t("TXT_CODE_6215388a") }}
</a-menu-item>
@ -559,6 +630,15 @@ onMounted(() => {
<a-col :span="24">
<CardPanel style="height: 100%">
<template #body>
<a-progress
v-if="percentComplete > 0"
:stroke-color="{
'0%': '#49b3ff',
'100%': '#25f5b9'
}"
:percent="percentComplete"
class="mb-20"
/>
<div class="file-breadcrumbs mb-20">
<a-breadcrumb separator=">">
<a-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
@ -586,6 +666,7 @@ onMounted(() => {
:scroll="{
x: 'max-content'
}"
size="small"
:pagination="{
current: operationForm.current,
pageSize: operationForm.pageSize,

View File

@ -52,21 +52,24 @@ const formData = reactive<NewInstanceForm>({
endTime: ""
});
const isImportMode = props.createMethod === QUICKSTART_METHOD.IMPORT;
const isFileMode = props.createMethod === QUICKSTART_METHOD.FILE;
const needUpload = isImportMode || isFileMode;
if (props.appType === QUICKSTART_ACTION_TYPE.Minecraft) {
formData.startCommand =
props.createMethod === QUICKSTART_METHOD.IMPORT ? "" : "java -jar ${ProgramName}";
formData.startCommand = isFileMode ? "java -jar ${ProgramName}" : "";
formData.stopCommand = "stop";
formData.type = TYPE_MINECRAFT_JAVA;
}
if (props.appType === QUICKSTART_ACTION_TYPE.Bedrock) {
formData.startCommand = props.createMethod === QUICKSTART_METHOD.IMPORT ? "" : "${ProgramName}";
formData.startCommand = isFileMode ? "${ProgramName}" : "";
formData.stopCommand = "stop";
formData.type = TYPE_MINECRAFT_BEDROCK;
}
if (props.appType === QUICKSTART_ACTION_TYPE.SteamGameServer) {
formData.startCommand = "${ProgramName}";
formData.startCommand = isFileMode ? "${ProgramName}" : "";
formData.type = TYPE_STEAM_SERVER_UNIVERSAL;
}
@ -88,9 +91,16 @@ const rules: Record<string, Rule[]> = {
const uFile = ref<File>();
const beforeUpload: UploadProps["beforeUpload"] = async (file) => {
if (file.type !== "application/x-zip-compressed") return message.error(t("只能上传zip压缩文件"));
uFile.value = file;
selectUnzipCodeDialog.value?.openDialog();
if (isImportMode) {
if (file.type !== "application/x-zip-compressed")
return message.error(t("只能上传zip压缩文件"));
selectUnzipCodeDialog.value?.openDialog();
} else {
finalConfirm();
}
return false;
};
@ -105,18 +115,15 @@ const finalConfirm = async () => {
const thisModal = Modal.confirm({
title: t("最终确认"),
icon: createVNode(InfoCircleOutlined),
content:
props.createMethod === QUICKSTART_METHOD.IMPORT
? t("上传文件时将同时创建实例,此操作不可逆,是否继续?")
: t("实例将创建,是否继续?"),
content: needUpload
? t("上传文件时将同时创建实例,此操作不可逆,是否继续?")
: t("实例将创建,是否继续?"),
okText: t("确定"),
async onOk() {
thisModal.destroy();
try {
await formRef.value?.validateFields();
thisModal.destroy();
props.createMethod === QUICKSTART_METHOD.IMPORT
? await selectedFile()
: await createInstance();
needUpload ? await selectedFile() : await createInstance();
} catch {
return message.error(t("请先完善基本参数再进行上传文件操作"));
}
@ -125,13 +132,16 @@ const finalConfirm = async () => {
});
};
//
//
const { state: cfg, execute: getCfg } = uploadAddress();
const { execute: uploadFile } = uploadInstanceFile();
const percentComplete = ref(0);
const selectedFile = async () => {
try {
if (!formData.cwd) formData.cwd = ".";
if (isFileMode) {
formData.startCommand = formData.startCommand.replace("${ProgramName}", uFile.value!.name);
}
await getCfg({
params: {
upload_dir: ".",
@ -139,14 +149,14 @@ const selectedFile = async () => {
},
data: formData
});
if (!cfg.value) return;
if (!cfg.value) throw new Error(t("获取上传地址失败"));
const uploadFormData = new FormData();
uploadFormData.append("file", uFile.value as any);
await uploadFile({
params: {
unzip: UNZIP.ON,
unzip: isImportMode ? UNZIP.ON : UNZIP.OFF,
code: zipCode.value
},
data: uploadFormData,
@ -219,6 +229,7 @@ const createInstance = async () => {
v-model:value="formData.startCommand"
:rows="3"
:placeholder="t('如 java -jar server.jarcmd.exe 等等')"
style="min-height: 40px"
/>
<a-button
type="default"
@ -241,7 +252,29 @@ const createInstance = async () => {
<a-input v-model:value="formData.cwd" />
</a-form-item>
<a-form-item v-if="createMethod === QUICKSTART_METHOD.IMPORT">
<a-form-item v-if="createMethod === QUICKSTART_METHOD.FILE">
<a-typography-title :level="5" class="require-field">
{{ t("上传单个服务端软件") }}
</a-typography-title>
<a-typography-paragraph>
<a-typography-text type="secondary">
{{ t("上传文件后实例将自动创建") }}
</a-typography-text>
</a-typography-paragraph>
<a-upload
:before-upload="beforeUpload"
:max-count="1"
:change="selectedFile"
:disabled="percentComplete > 0"
>
<a-button type="primary" :loading="percentComplete > 0">
<upload-outlined v-if="percentComplete === 0" />
{{ percentComplete > 0 ? t("正在上传:") + percentComplete + "%" : t("选择文件") }}
</a-button>
</a-upload>
</a-form-item>
<a-form-item v-else-if="createMethod === QUICKSTART_METHOD.IMPORT">
<a-typography-title :level="5" class="require-field">
{{ t("上传服务端压缩包") }}
</a-typography-title>
@ -253,7 +286,6 @@ const createInstance = async () => {
</a-typography-text>
</a-typography-paragraph>
<a-upload
action="/api/instance/upload"
:before-upload="beforeUpload"
:max-count="1"
:change="selectedFile"