Compare commits

...

3 Commits

10 changed files with 147 additions and 47 deletions

View File

@@ -1,9 +1,73 @@
import * as fs from 'fs';
import * as path from 'path';
const env = process.env.DEBUG; const env = process.env.DEBUG;
const isDebug = env === "true"; const isDebug = env === "true";
// 日志级别枚举 // 日志级别枚举
type LogLevel = "debug" | "info" | "warn" | "error"; 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 日志级别 * @param level 日志级别
@@ -11,24 +75,42 @@ type LogLevel = "debug" | "info" | "warn" | "error";
* @param data 附加数据(可选) * @param data 附加数据(可选)
*/ */
function log(level: LogLevel, msg: string | Error, data?: any) { 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}]`; const prefix = `[${level.toUpperCase()}] [${timestamp}]`;
// 确保只在调试模式下输出debug日志 // 确保只在调试模式下输出debug日志
if (level === "debug" && !isDebug) return; if (level === "debug" && !isDebug) return;
let logMessage = '';
if (msg instanceof Error) { if (msg instanceof Error) {
console.error(`${prefix} ${msg.message}`); const errorMsg = `${prefix} ${msg.message}`;
console.error(msg.stack); 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); if (data) console.error(`${prefix} Data:`, data);
logMessage = errorMsg + '\n' + stackMsg;
if (data) logMessage += '\n' + dataMsg;
} else { } else {
const logFunc = level === "error" ? console.error : const logFunc = level === "error" ? console.error :
level === "warn" ? console.warn : level === "warn" ? console.warn :
console.log; 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); if (data) logFunc(`${prefix} Data:`, data);
logMessage = outputMsg;
if (data) logMessage += '\n' + dataMsg;
} }
writeToFile(logMessage);
} }
export const logger = { export const logger = {

View File

@@ -193,7 +193,7 @@ export async function fastdownload(data: [string, string]|string[][]) {
export async function Wfastdownload(data: string[][], ws: MessageWS) { export async function Wfastdownload(data: string[][], ws: MessageWS) {
logger.info(`Starting web download of ${data.length} files`); logger.info(`Starting web download of ${data.length} files`);
let index = 0;
return await pMap( return await pMap(
data, data,
async (item: string[], idx: number) => { async (item: string[], idx: number) => {
@@ -214,7 +214,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) => { { retries: 3, onFailedAttempt: (error) => {
logger.warn(`Download attempt failed for ${url}, retrying (${error.attemptNumber}/3)`); logger.warn(`Download attempt failed for ${url}, retrying (${error.attemptNumber}/3)`);

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { h, ref } from 'vue'; import { h, provide, ref } from 'vue';
import { MenuProps, message } from 'ant-design-vue'; import { MenuProps, message } from 'ant-design-vue';
import { SettingOutlined, UserOutlined, WindowsOutlined } from '@ant-design/icons-vue'; import { SettingOutlined, UserOutlined, WindowsOutlined } from '@ant-design/icons-vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -24,22 +24,35 @@ document.oncontextmenu = (event: any) => {
}; };
const router = useRouter(); 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() shell.Command.create("core").spawn()
.then(() => { .then((e) => {
// 检查后端服务是否成功启动 // 检查后端服务是否成功启动
fetch("http://localhost:37019/", { method: "GET" }) fetch("http://localhost:37019/", { method: "GET" })
.catch(() => router.push('/error')) .catch(() => router.push('/error'))
.then(() => message.success("DeEarthX.Core 启动成功")); .then(() => message.success("DeEarthX.Core 启动成功"));
console.log("DeEarthX V3 Core"); console.log("DeEarthX V3 Core");
// 保存进程对象,用于后续终止
killCoreProcess = e.kill;
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
message.error("DeEarthX.Core 启动失败请检查37019端口是否被占用"); 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']); const selectedKeys = ref<(string | number)[]>(['main']);
@@ -88,7 +101,7 @@ const theme = ref({
<a-config-provider :theme="theme"> <a-config-provider :theme="theme">
<div class="tw:h-screen tw:w-screen tw:flex tw:flex-col"> <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" <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> <template #extra>
<a-button @click="openAuthorBilibili">作者B站</a-button> <a-button @click="openAuthorBilibili">作者B站</a-button>
</template> </template>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -6,7 +6,7 @@ const sponsors = [
{ {
id: "elfidc", id: "elfidc",
name: "亿讯云", name: "亿讯云",
imageUrl: "../src/assets/elfidc.svg", imageUrl: "./elfidc.svg",
type: "金牌赞助", type: "金牌赞助",
url: "https://www.elfidc.com" url: "https://www.elfidc.com"
} }
@@ -17,18 +17,18 @@ const thanksList = [
{ {
id: "user", id: "user",
name: "天跑", name: "天跑",
avatar: "../src/assets/tianpao.jpg", avatar: "./tianpao.jpg",
contribution: "作者" contribution: "作者"
}, },
{ {
id: "mirror", id: "mirror",
name: "bangbang93", name: "bangbang93",
avatar: "../src/assets/bb93.jpg", avatar: "./bb93.jpg",
contribution: "BMCLAPI镜像" contribution: "BMCLAPI镜像"
},{ },{
id: "mirror", id: "mirror",
name: "z0z0r4", name: "z0z0r4",
avatar: "../src/assets/z0z0r4.jpg", avatar: "./z0z0r4.jpg",
contribution: "MCIM镜像" contribution: "MCIM镜像"
} }
]; ];

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { inject, ref, watch } from 'vue';
import { InboxOutlined } from '@ant-design/icons-vue'; import { InboxOutlined } from '@ant-design/icons-vue';
import { message, notification, StepsProps } from 'ant-design-vue'; import { message, notification, StepsProps } from 'ant-design-vue';
import type { UploadFile, UploadChangeParam } from 'ant-design-vue'; import type { UploadFile, UploadChangeParam } from 'ant-design-vue';
@@ -55,6 +55,10 @@ function resetState() {
currentStep.value = 0; currentStep.value = 0;
unzipProgress.value = { status: 'active', percent: 0, display: true }; unzipProgress.value = { status: 'active', percent: 0, display: true };
downloadProgress.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();
}
} }
// 模式选择相关 // 模式选择相关
@@ -217,9 +221,9 @@ function handleStartProcess() {
<h1 class="tw:text-4xl tw:text-center tw:animate-pulse">DeEarthX</h1> <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> <h1 class="tw:text-sm tw:text-gray-500 tw:text-center">让开服变成随时随地的事情</h1>
</div> </div>
<a-upload-dragger :disabled="uploadDisabled" class="tw:w-full tw:max-w-md tw:h-48" name="file" action="/" :multiple="false" <a-upload-dragger :disabled="uploadDisabled" class="tw:w-full tw:max-w-md tw:h-48" name="file"
:before-upload="beforeUpload" @change="handleFileChange" @drop="handleFileDrop" v-model:fileList="uploadedFiles" action="/" :multiple="false" :before-upload="beforeUpload" @change="handleFileChange"
accept=".zip,.mrpack"> @drop="handleFileDrop" v-model:fileList="uploadedFiles" accept=".zip,.mrpack">
<p class="ant-upload-drag-icon"> <p class="ant-upload-drag-icon">
<inbox-outlined></inbox-outlined> <inbox-outlined></inbox-outlined>
</p> </p>
@@ -227,23 +231,24 @@ function handleStartProcess() {
<p class="ant-upload-hint"> <p class="ant-upload-hint">
请使用.zipCurseForgeMCBBS.mrpackModrinth文件 请使用.zipCurseForgeMCBBS.mrpackModrinth文件
</p> </p>
<p class="ant-upload-hint">
PCL导出的zip整合包请拖拽里面的modpack.mrpack至DeX
</p>
</a-upload-dragger> </a-upload-dragger>
<a-select <a-select ref="select" :options="modeOptions" :value="selectedMode"
ref="select" style="width: 120px;margin-top: 32px" @select="handleModeSelect"></a-select>
:options="modeOptions" <a-button :disabled="startButtonDisabled" type="primary" @click="handleStartProcess"
:value="selectedMode" style="margin-top: 6px">
style="width: 120px;margin-top: 32px"
@select="handleModeSelect"
></a-select>
<a-button :disabled="startButtonDisabled" type="primary" @click="handleStartProcess" style="margin-top: 6px">
开始 开始
</a-button> </a-button>
</div> </div>
</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" /> <a-steps :current="currentStep" :items="stepItems" />
</div> </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"> <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"> <a-card title="制作进度" :bordered="true" class="tw:h-full">
<div v-if="unzipProgress.display"> <div v-if="unzipProgress.display">
<h1 class="tw:text-sm">解压进度</h1> <h1 class="tw:text-sm">解压进度</h1>