Compare commits
4 Commits
0a47d14627
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
295f1fbece | ||
|
|
0f8ceba972 | ||
|
|
6f256048f0 | ||
|
|
475d028ed9 |
@@ -11,6 +11,7 @@ import { execPromise } from "./utils/utils.js";
|
||||
import { MessageWS } from "./utils/ws.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
import { yauzl_promise } from "./utils/ziplib.js";
|
||||
import yauzl from "yauzl";
|
||||
|
||||
export class Dex {
|
||||
wsx!: WebSocketServer;
|
||||
@@ -22,11 +23,17 @@ export class Dex {
|
||||
});
|
||||
}
|
||||
|
||||
public async Main(buffer: Buffer, dser: boolean) {
|
||||
public async Main(buffer: Buffer, dser: boolean, filename?: string) {
|
||||
try {
|
||||
const first = Date.now();
|
||||
const zps = await this._zips(buffer);
|
||||
const processedBuffer = await this._processModpack(buffer, filename);
|
||||
const zps = await this._zips(processedBuffer);
|
||||
const { contain, info } = await zps._getinfo();
|
||||
if (!contain || !info) {
|
||||
logger.error("Modpack info is empty");
|
||||
this.message.handleError(new Error("It seems that the modpack is not a valid modpack."));
|
||||
return;
|
||||
}
|
||||
const plat = what_platform(contain);
|
||||
logger.debug("Platform detected", plat);
|
||||
logger.debug("Modpack info", info);
|
||||
@@ -72,8 +79,86 @@ export class Dex {
|
||||
}
|
||||
}
|
||||
|
||||
private async _processModpack(buffer: Buffer, filename?: string): Promise<Buffer> {
|
||||
if (!filename || (!filename.endsWith('.zip') && !filename.endsWith('.mrpack'))) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
try {
|
||||
const zip = await (new Promise((resolve, reject) => {
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(zipfile);
|
||||
});
|
||||
}) as Promise<yauzl.ZipFile>);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let mrpackBuffer: Buffer | null = null;
|
||||
let hasProcessed = false;
|
||||
|
||||
zip.on('entry', (entry: yauzl.Entry) => {
|
||||
if (hasProcessed || !entry.fileName.endsWith('modpack.mrpack')) {
|
||||
zip.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.fileName === 'modpack.mrpack') {
|
||||
hasProcessed = true;
|
||||
zip.openReadStream(entry, (err, stream) => {
|
||||
if (err) {
|
||||
zip.close();
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
stream.on('end', () => {
|
||||
mrpackBuffer = Buffer.concat(chunks);
|
||||
zip.close();
|
||||
resolve(mrpackBuffer);
|
||||
});
|
||||
stream.on('error', (err) => {
|
||||
zip.close();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
zip.readEntry();
|
||||
}
|
||||
});
|
||||
|
||||
zip.on('end', () => {
|
||||
if (!hasProcessed) {
|
||||
zip.close();
|
||||
resolve(buffer);
|
||||
}
|
||||
});
|
||||
|
||||
zip.on('error', (err) => {
|
||||
zip.close();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
zip.readEntry();
|
||||
});
|
||||
} catch (e) {
|
||||
logger.warn('Failed to check for modpack.mrpack, using original buffer', e);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
private async _zips(buffer: Buffer) {
|
||||
if (buffer.length === 0) {
|
||||
throw new Error("zip buffer is empty");
|
||||
}
|
||||
const zip = await yauzl_promise(buffer);
|
||||
let index = 0;
|
||||
const _getinfo = async () => {
|
||||
const importantFiles = ["manifest.json", "modrinth.index.json"];
|
||||
for await (const entry of zip) {
|
||||
@@ -83,10 +168,13 @@ export class Dex {
|
||||
logger.debug("Found important file", { fileName: entry.fileName, info });
|
||||
return { contain: entry.fileName, info };
|
||||
}
|
||||
index++;
|
||||
}
|
||||
throw new Error("No manifest file found in modpack");
|
||||
}
|
||||
|
||||
if (index === zip.length) {
|
||||
throw new Error("No manifest file found in modpack");
|
||||
}
|
||||
const _unzip = async (instancename: string) => {
|
||||
logger.info("Starting unzip process", { instancename });
|
||||
const instancePath = `./instance/${instancename}`;
|
||||
|
||||
@@ -93,7 +93,7 @@ export class Core {
|
||||
logger.info("Starting task", { isServerMode });
|
||||
|
||||
// 非阻塞执行主要任务
|
||||
this.dex.Main(req.file.buffer, isServerMode).catch(err => {
|
||||
this.dex.Main(req.file.buffer, isServerMode, req.file.originalname).catch(err => {
|
||||
logger.error("Task execution failed", err);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Fabric } from "./fabric.js";
|
||||
import { Forge } from "./forge.js";
|
||||
import { Minecraft } from "./minecraft.js";
|
||||
import { NeoForge } from "./neoforge.js";
|
||||
import fs from "node:fs";
|
||||
|
||||
/**
|
||||
* 模组加载器接口
|
||||
@@ -54,4 +55,18 @@ export async function mlsetup(ml: string, mcv: string, mlv: string, path: string
|
||||
*/
|
||||
export async function dinstall(ml: string, mcv: string, mlv: string, path: string): Promise<void> {
|
||||
await modloader(ml, mcv, mlv, path).installer();
|
||||
|
||||
let cmd = '';
|
||||
if (ml === 'forge' || ml === 'neoforge') {
|
||||
cmd = `java -jar forge-${mcv}-${mlv}-installer.jar --installServer`;
|
||||
} else if (ml === 'fabric' || ml === 'fabric-loader') {
|
||||
await fs.promises.writeFile(`${path}/run.bat`,`@echo off\njava -jar fabric-server-launch.jar\n`)
|
||||
await fs.promises.writeFile(`${path}/run.sh`,`#!/bin/bash\njava -jar fabric-server-launch.jar\n`)
|
||||
cmd = `java -jar fabric-installer.jar server -dir . -mcversion ${mcv} -loader ${mlv} -downloadMinecraft`;
|
||||
}
|
||||
|
||||
if (cmd) {
|
||||
await fs.promises.writeFile(`${path}/install.bat`, `@echo off\n${cmd}\necho Install Successfully,Enter Some Key to Exit!\npause\n`);
|
||||
await fs.promises.writeFile(`${path}/install.sh`, `#!/bin/bash\n${cmd}\n`);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,73 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const env = process.env.DEBUG;
|
||||
const isDebug = env === "true";
|
||||
|
||||
// 日志级别枚举
|
||||
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
// 日志文件路径
|
||||
const logsDir = path.join(process.cwd(), 'logs');
|
||||
let logFilePath: string | null = null;
|
||||
|
||||
/**
|
||||
* 获取Asia/Shanghai时区的格式化时间
|
||||
*/
|
||||
function getShanghaiTime(): Date {
|
||||
const now = new Date();
|
||||
const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
|
||||
const shanghaiOffset = 8;
|
||||
return new Date(utc + (3600000 * shanghaiOffset));
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间为字符串
|
||||
*/
|
||||
function formatTimestamp(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成日志文件名,处理同一天多次打开的情况
|
||||
*/
|
||||
function generateLogFileName(): string {
|
||||
const now = getShanghaiTime();
|
||||
const dateStr = now.toISOString().split('T')[0];
|
||||
const timeStr = String(now.getHours()).padStart(2, '0') +
|
||||
String(now.getMinutes()).padStart(2, '0') +
|
||||
String(now.getSeconds()).padStart(2, '0');
|
||||
return `${dateStr}_${timeStr}.log`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化日志文件
|
||||
*/
|
||||
function initLogFile() {
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
logFilePath = path.join(logsDir, generateLogFileName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入日志到文件
|
||||
*/
|
||||
function writeToFile(logMessage: string) {
|
||||
if (!logFilePath) {
|
||||
initLogFile();
|
||||
}
|
||||
if (logFilePath) {
|
||||
fs.appendFileSync(logFilePath, logMessage + '\n', 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志记录器
|
||||
* @param level 日志级别
|
||||
@@ -11,24 +75,42 @@ type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
* @param data 附加数据(可选)
|
||||
*/
|
||||
function log(level: LogLevel, msg: string | Error, data?: any) {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const shanghaiTime = getShanghaiTime();
|
||||
const timestamp = formatTimestamp(shanghaiTime);
|
||||
const prefix = `[${level.toUpperCase()}] [${timestamp}]`;
|
||||
|
||||
// 确保只在调试模式下输出debug日志
|
||||
if (level === "debug" && !isDebug) return;
|
||||
|
||||
let logMessage = '';
|
||||
|
||||
if (msg instanceof Error) {
|
||||
console.error(`${prefix} ${msg.message}`);
|
||||
console.error(msg.stack);
|
||||
const errorMsg = `${prefix} ${msg.message}`;
|
||||
const stackMsg = msg.stack || '';
|
||||
const dataMsg = data ? `${prefix} Data: ${JSON.stringify(data, null, 2)}` : '';
|
||||
|
||||
console.error(errorMsg);
|
||||
console.error(stackMsg);
|
||||
if (data) console.error(`${prefix} Data:`, data);
|
||||
|
||||
logMessage = errorMsg + '\n' + stackMsg;
|
||||
if (data) logMessage += '\n' + dataMsg;
|
||||
} else {
|
||||
const logFunc = level === "error" ? console.error :
|
||||
level === "warn" ? console.warn :
|
||||
console.log;
|
||||
|
||||
logFunc(`${prefix} ${msg}`);
|
||||
const outputMsg = `${prefix} ${msg}`;
|
||||
const dataMsg = data ? `${prefix} Data: ${JSON.stringify(data, null, 2)}` : '';
|
||||
|
||||
logFunc(outputMsg);
|
||||
if (data) logFunc(`${prefix} Data:`, data);
|
||||
|
||||
logMessage = outputMsg;
|
||||
if (data) logMessage += '\n' + dataMsg;
|
||||
}
|
||||
|
||||
writeToFile(logMessage);
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import pRetry from "p-retry";
|
||||
import fs from "node:fs";
|
||||
import fse from "fs-extra";
|
||||
import { WebSocket } from "ws";
|
||||
import { ExecOptions, exec} from "node:child_process";
|
||||
import { ExecOptions, exec, spawn} from "node:child_process";
|
||||
|
||||
/**
|
||||
* Java版本信息接口
|
||||
@@ -135,20 +135,43 @@ export async function checkJava(): Promise<JavaCheckResult> {
|
||||
|
||||
export function execPromise(cmd:string,options?:ExecOptions){
|
||||
logger.debug(`Executing command: ${cmd}`);
|
||||
return new Promise((resolve,reject)=>{
|
||||
exec(cmd,options,(err,stdout,stderr)=>{
|
||||
if(err){
|
||||
logger.error(`Command execution failed: ${cmd}`, err);
|
||||
return new Promise<number>((resolve,reject)=>{
|
||||
const args = cmd.split(' ');
|
||||
const command = args.shift() || '';
|
||||
|
||||
const child = spawn(command, args, {
|
||||
...options,
|
||||
shell: true
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
logger.error(`Command execution failed: ${cmd}`);
|
||||
logger.debug(`Stderr: ${stderr}`);
|
||||
reject(err)
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
return;
|
||||
}
|
||||
if (stdout) logger.debug(`Command stdout: ${stdout}`);
|
||||
if (stderr) logger.debug(`Command stderr: ${stderr}`);
|
||||
}).on('exit',(code)=>{
|
||||
logger.debug(`Command completed with exit code: ${code}`);
|
||||
resolve(code)
|
||||
})
|
||||
resolve(code || 0);
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
logger.error(`Command execution error: ${cmd}`, err);
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -193,7 +216,7 @@ export async function fastdownload(data: [string, string]|string[][]) {
|
||||
|
||||
export async function Wfastdownload(data: string[][], ws: MessageWS) {
|
||||
logger.info(`Starting web download of ${data.length} files`);
|
||||
|
||||
let index = 0;
|
||||
return await pMap(
|
||||
data,
|
||||
async (item: string[], idx: number) => {
|
||||
@@ -214,7 +237,7 @@ export async function Wfastdownload(data: string[][], ws: MessageWS) {
|
||||
}
|
||||
|
||||
// 更新下载进度
|
||||
ws.download(data.length, idx + 1, filePath);
|
||||
ws.download(data.length, ++index, filePath);
|
||||
},
|
||||
{ retries: 3, onFailedAttempt: (error) => {
|
||||
logger.warn(`Download attempt failed for ${url}, retrying (${error.attemptNumber}/3)`);
|
||||
|
||||
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, ref } from 'vue';
|
||||
import { h, provide, ref } from 'vue';
|
||||
import { MenuProps, message } from 'ant-design-vue';
|
||||
import { SettingOutlined, UserOutlined, WindowsOutlined } from '@ant-design/icons-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
@@ -24,22 +24,35 @@ document.oncontextmenu = (event: any) => {
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let killCoreProcess: (() => void) | null = null;
|
||||
// 启动后端核心服务
|
||||
message.loading("DeEarthX.Core启动中,请勿操作...").then(() => {
|
||||
message.loading("DeEarthX.Core启动中,请勿操作...").then(() => runCoreProcess());
|
||||
|
||||
function runCoreProcess() {
|
||||
shell.Command.create("core").spawn()
|
||||
.then(() => {
|
||||
.then((e) => {
|
||||
// 检查后端服务是否成功启动
|
||||
fetch("http://localhost:37019/", { method: "GET" })
|
||||
.catch(() => router.push('/error'))
|
||||
.then(() => message.success("DeEarthX.Core 启动成功"));
|
||||
console.log("DeEarthX V3 Core");
|
||||
// 保存进程对象,用于后续终止
|
||||
killCoreProcess = e.kill;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
message.error("DeEarthX.Core 启动失败,请检查37019端口是否被占用!");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
provide("killCoreProcess", () => {
|
||||
if (killCoreProcess && typeof killCoreProcess === 'function') {
|
||||
killCoreProcess();
|
||||
killCoreProcess = null;
|
||||
message.info("DeEarthX.Core 重新启动!");
|
||||
runCoreProcess();
|
||||
}
|
||||
}); //全局提供kill方法
|
||||
|
||||
// 导航菜单配置
|
||||
const selectedKeys = ref<(string | number)[]>(['main']);
|
||||
@@ -88,7 +101,7 @@ const theme = ref({
|
||||
<a-config-provider :theme="theme">
|
||||
<div class="tw:h-screen tw:w-screen tw:flex tw:flex-col">
|
||||
<a-page-header class="tw:h-16" style="border: 1px solid rgb(235, 237, 240)" title="DeEarthX"
|
||||
sub-title="V3" :avatar="{ src: './public/dex.png' }">
|
||||
sub-title="V3" :avatar="{ src: './dex.png' }">
|
||||
<template #extra>
|
||||
<a-button @click="openAuthorBilibili">作者B站</a-button>
|
||||
</template>
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB |
@@ -6,7 +6,7 @@ const sponsors = [
|
||||
{
|
||||
id: "elfidc",
|
||||
name: "亿讯云",
|
||||
imageUrl: "../src/assets/elfidc.svg",
|
||||
imageUrl: "./elfidc.svg",
|
||||
type: "金牌赞助",
|
||||
url: "https://www.elfidc.com"
|
||||
}
|
||||
@@ -17,18 +17,18 @@ const thanksList = [
|
||||
{
|
||||
id: "user",
|
||||
name: "天跑",
|
||||
avatar: "../src/assets/tianpao.jpg",
|
||||
avatar: "./tianpao.jpg",
|
||||
contribution: "作者"
|
||||
},
|
||||
{
|
||||
id: "mirror",
|
||||
name: "bangbang93",
|
||||
avatar: "../src/assets/bb93.jpg",
|
||||
avatar: "./bb93.jpg",
|
||||
contribution: "BMCLAPI镜像"
|
||||
},{
|
||||
id: "mirror",
|
||||
name: "z0z0r4",
|
||||
avatar: "../src/assets/z0z0r4.jpg",
|
||||
avatar: "./z0z0r4.jpg",
|
||||
contribution: "MCIM镜像"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { inject, ref, watch } from 'vue';
|
||||
import { InboxOutlined } from '@ant-design/icons-vue';
|
||||
import { message, notification, StepsProps } from 'ant-design-vue';
|
||||
import type { UploadFile, UploadChangeParam } from 'ant-design-vue';
|
||||
@@ -55,6 +55,10 @@ function resetState() {
|
||||
currentStep.value = 0;
|
||||
unzipProgress.value = { status: 'active', percent: 0, display: true };
|
||||
downloadProgress.value = { status: 'active', percent: 0, display: true };
|
||||
const killCoreProcess = inject("killCoreProcess");
|
||||
if (killCoreProcess && typeof killCoreProcess === 'function') {
|
||||
killCoreProcess();
|
||||
}
|
||||
}
|
||||
|
||||
// 模式选择相关
|
||||
@@ -90,10 +94,10 @@ const downloadProgress = ref<ProgressStatus>({ status: 'active', percent: 0, dis
|
||||
// 运行DeEarthX核心功能
|
||||
async function runDeEarthX(file: Blob) {
|
||||
message.success('开始制作,请勿切换菜单!');
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://localhost:37019/start?mode=${selectedMode.value}`, {
|
||||
method: 'POST',
|
||||
@@ -111,11 +115,11 @@ async function runDeEarthX(file: Blob) {
|
||||
// 设置WebSocket连接
|
||||
function setupWebSocket() {
|
||||
const ws = new WebSocket('ws://localhost:37019/');
|
||||
|
||||
|
||||
ws.addEventListener('message', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as WebSocketMessage;
|
||||
|
||||
|
||||
// 处理不同状态的消息
|
||||
switch (data.status) {
|
||||
case 'error':
|
||||
@@ -139,7 +143,7 @@ function setupWebSocket() {
|
||||
notification.error({ message: '错误', description: '解析服务器消息失败' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ws.addEventListener('error', () => {
|
||||
notification.error({ message: '错误', description: 'WebSocket连接失败' });
|
||||
resetState();
|
||||
@@ -188,7 +192,7 @@ function handleFinish(result: number) {
|
||||
currentStep.value++;
|
||||
message.success(`服务端制作完成!共用时${timeSpent}秒!`);
|
||||
sendNotification({ title: 'DeEarthX V3', body: `服务端制作完成!共用时${timeSpent}秒!` });
|
||||
|
||||
|
||||
// 8秒后自动重置状态
|
||||
setTimeout(resetState, 8000);
|
||||
}
|
||||
@@ -199,10 +203,10 @@ function handleStartProcess() {
|
||||
message.warning('请先拖拽或选择文件');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const file = uploadedFiles.value[0].originFileObj;
|
||||
if (!file) return;
|
||||
|
||||
|
||||
runDeEarthX(file);
|
||||
startButtonDisabled.value = true;
|
||||
uploadDisabled.value = true;
|
||||
@@ -217,9 +221,9 @@ function handleStartProcess() {
|
||||
<h1 class="tw:text-4xl tw:text-center tw:animate-pulse">DeEarthX</h1>
|
||||
<h1 class="tw:text-sm tw:text-gray-500 tw:text-center">让开服变成随时随地的事情!</h1>
|
||||
</div>
|
||||
<a-upload-dragger :disabled="uploadDisabled" class="tw:w-full tw:max-w-md tw:h-48" name="file" action="/" :multiple="false"
|
||||
:before-upload="beforeUpload" @change="handleFileChange" @drop="handleFileDrop" v-model:fileList="uploadedFiles"
|
||||
accept=".zip,.mrpack">
|
||||
<a-upload-dragger :disabled="uploadDisabled" class="tw:w-full tw:max-w-md tw:h-48" name="file"
|
||||
action="/" :multiple="false" :before-upload="beforeUpload" @change="handleFileChange"
|
||||
@drop="handleFileDrop" v-model:fileList="uploadedFiles" accept=".zip,.mrpack">
|
||||
<p class="ant-upload-drag-icon">
|
||||
<inbox-outlined></inbox-outlined>
|
||||
</p>
|
||||
@@ -227,32 +231,33 @@ function handleStartProcess() {
|
||||
<p class="ant-upload-hint">
|
||||
请使用.zip(CurseForge、MCBBS)和.mrpack(Modrinth)文件
|
||||
</p>
|
||||
<p class="ant-upload-hint">
|
||||
PCL导出的zip整合包请拖拽里面的modpack.mrpack至DeX
|
||||
</p>
|
||||
</a-upload-dragger>
|
||||
<a-select
|
||||
ref="select"
|
||||
:options="modeOptions"
|
||||
:value="selectedMode"
|
||||
style="width: 120px;margin-top: 32px"
|
||||
@select="handleModeSelect"
|
||||
></a-select>
|
||||
<a-button :disabled="startButtonDisabled" type="primary" @click="handleStartProcess" style="margin-top: 6px">
|
||||
<a-select ref="select" :options="modeOptions" :value="selectedMode"
|
||||
style="width: 120px;margin-top: 32px" @select="handleModeSelect"></a-select>
|
||||
<a-button :disabled="startButtonDisabled" type="primary" @click="handleStartProcess"
|
||||
style="margin-top: 6px">
|
||||
开始
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showSteps" class="tw:fixed tw:bottom-2 tw:left-1/2 tw:-translate-x-1/2 tw:w-full tw:max-w-3xl tw:h-16 tw:flex tw:justify-center tw:items-center tw:text-sm">
|
||||
<div v-if="showSteps"
|
||||
class="tw:fixed tw:bottom-2 tw:left-1/2 tw:-translate-x-1/2 tw:w-full tw:max-w-3xl tw:h-16 tw:flex tw:justify-center tw:items-center tw:text-sm">
|
||||
<a-steps :current="currentStep" :items="stepItems" />
|
||||
</div>
|
||||
<div v-if="showSteps" ref="logContainer" class="tw:absolute tw:right-2 tw:bottom-20 tw:h-80 tw:w-64 tw:rounded-xl tw:overflow-y-auto">
|
||||
<a-card title="制作进度" :bordered="true" class="tw:h-full">
|
||||
<div v-if="unzipProgress.display">
|
||||
<h1 class="tw:text-sm">解压进度</h1>
|
||||
<a-progress :percent="unzipProgress.percent" :status="unzipProgress.status" size="small" />
|
||||
</div>
|
||||
<div v-if="downloadProgress.display">
|
||||
<h1 class="tw:text-sm">下载进度</h1>
|
||||
<a-progress :percent="downloadProgress.percent" :status="downloadProgress.status" size="small" />
|
||||
</div>
|
||||
<div v-if="showSteps" ref="logContainer"
|
||||
class="tw:absolute tw:right-2 tw:bottom-20 tw:h-80 tw:w-64 tw:rounded-xl tw:overflow-y-auto">
|
||||
<a-card title="制作进度" :bordered="true" class="tw:h-full">
|
||||
<div v-if="unzipProgress.display">
|
||||
<h1 class="tw:text-sm">解压进度</h1>
|
||||
<a-progress :percent="unzipProgress.percent" :status="unzipProgress.status" size="small" />
|
||||
</div>
|
||||
<div v-if="downloadProgress.display">
|
||||
<h1 class="tw:text-sm">下载进度</h1>
|
||||
<a-progress :percent="downloadProgress.percent" :status="downloadProgress.status" size="small" />
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||