Merge pull request #1081 from MCSManager/abandon

Feat: uplaod image
This commit is contained in:
unitwk 2023-11-30 16:54:19 +08:00 committed by GitHub
commit 58188a8c9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 212 additions and 87 deletions

View File

@ -115,8 +115,8 @@ const submit = async () => {
};
onMounted(async () => {
await initInstancesData();
open.value = true;
await initInstancesData();
});
const handleQueryInstance = throttle(async () => {

View File

@ -1 +1,3 @@
export const GLOBAL_INSTANCE_NAME = "__MCSM_GLOBAL_INSTANCE__";
export const GLOBAL_INSTANCE_UUID = "global0001";

View File

@ -342,6 +342,7 @@ export const useFileManager = (instanceId?: string, daemonId?: string) => {
await uploadFile({
data: uploadFormData,
timeout: Number.MAX_VALUE,
url: `${parseForwardAddress(uploadCfg.value.addr, "http")}/upload/${
uploadCfg.value.password
}`,

View File

@ -167,10 +167,8 @@ export const uploadAddress = useDefineApi<
export const uploadFile = useDefineApi<
{
data: FormData;
url: string;
onUploadProgress: Function;
},
{}
any
>({
method: "POST",
headers: { "Content-Type": "multipart/form-data" }

View File

@ -191,10 +191,8 @@ export const uploadInstanceFile = useDefineApi<
code: string;
};
data: FormData;
url: string;
onUploadProgress: Function;
},
{}
any
>({
method: "POST",
headers: { "Content-Type": "multipart/form-data" }

View File

@ -22,3 +22,14 @@ export const resetLayoutConfig = useDefineApi<any, void>({
url: "/api/overview/layout",
method: "DELETE"
});
export const uploadFile = useDefineApi<
{
data: FormData;
},
string
>({
method: "POST",
headers: { "Content-Type": "multipart/form-data" },
url: "/api/overview/upload_assets"
});

View File

@ -4,7 +4,7 @@ import { onMounted } from "vue";
import type { LayoutCard } from "@/types";
import { userInfoApi } from "@/services/apis/index";
import { useRouter } from "vue-router";
import { INSTANCE_STATUS } from "@/types/const";
import { INSTANCE_STATUS, INSTANCE_STATUS_CODE } from "@/types/const";
defineProps<{
card: LayoutCard;
@ -89,7 +89,7 @@ onMounted(() => {
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'operate'">
<a-button @click="operate(record.daemonId, record.instanceUuid)">
<a-button :disabled="record.status === INSTANCE_STATUS_CODE.UNKNOWN" @click="operate(record.daemonId, record.instanceUuid)">
{{ t("TXT_CODE_5974bf24") }}
</a-button>
</template>

View File

@ -11,7 +11,7 @@ import { useScreen } from "@/hooks/useScreen";
import { arrayFilter } from "@/tools/array";
import { useLayoutCardTools } from "@/hooks/useCardTools";
import { throttle } from "lodash";
import { filterFileName, getFileIcon } from "@/tools/fileManager";
import { filterFileName, getFileExtName, getFileIcon } from "@/tools/fileManager";
import { useFileManager } from "@/hooks/useFileManager";
import FileEditor from "./dialogs/FileEditor.vue";
import type { DataType } from "@/types/fileManager";
@ -267,10 +267,7 @@ onMounted(() => {
{{ disk }}
</a-select-option>
</a-select>
<div
class="file-breadcrumbs mb-20"
:style="{ width: isPhone ? '100%' : 'calc(100% - 135px)' }"
>
<div class="file-breadcrumbs mb-20">
<a-breadcrumb separator=">">
<a-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
<div class="file-breadcrumbs-item" @click="handleChangeDir(item.path)">
@ -361,10 +358,18 @@ onMounted(() => {
<a-menu-item key="6" @click="deleteFile(record.name)">
{{ t("TXT_CODE_ecbd7449") }}
</a-menu-item>
<a-menu-item key="7" @click="unzipFile(record.name)">
<a-menu-item
v-if="record.type === 1 && getFileExtName(record.name) === 'zip'"
key="7"
@click="unzipFile(record.name)"
>
{{ t("TXT_CODE_a64f3007") }}
</a-menu-item>
<a-menu-item key="8" @click="downloadFile(record.name)">
<a-menu-item
v-if="record.type === 1"
key="8"
@click="downloadFile(record.name)"
>
{{ t("TXT_CODE_65b21404") }}
</a-menu-item>
</a-menu>
@ -511,6 +516,7 @@ onMounted(() => {
.file-breadcrumbs {
border: 1px solid var(--color-gray-5);
border-radius: 6px;
flex: 1;
.file-breadcrumbs-item {
padding: 8px;

View File

@ -22,6 +22,8 @@ import { message } from "ant-design-vue";
import { useAppRouters } from "@/hooks/useAppRouters";
import { useLayoutCardTools } from "@/hooks/useCardTools";
import type { LayoutCard } from "@/types";
import { arrayFilter } from "@/tools/array";
import { GLOBAL_INSTANCE_UUID } from "@/config/const";
const props = defineProps<{
item?: ComputedNodeInfo;
@ -81,62 +83,67 @@ const detailList = (node: ComputedNodeInfo) => {
];
};
const nodeOperations = [
{
title: t("TXT_CODE_ae533703"),
icon: FolderOpenOutlined,
click: (item: ComputedNodeInfo) => {
const daemonId = item.uuid;
const instanceId = "global0001";
toPage({
path: "/instances/terminal/files",
query: {
daemonId,
instanceId
}
});
const nodeOperations = computed(() =>
arrayFilter([
{
title: t("TXT_CODE_ae533703"),
icon: FolderOpenOutlined,
click: (item: ComputedNodeInfo) => {
const daemonId = item.uuid;
const instanceId = GLOBAL_INSTANCE_UUID;
toPage({
path: "/instances/terminal/files",
query: {
daemonId,
instanceId
}
});
},
condition: () => item.value!.available
},
{
title: t("TXT_CODE_524e3036"),
icon: CodeOutlined,
click: (item: ComputedNodeInfo) => {
const daemonId = item.uuid;
const instanceId = GLOBAL_INSTANCE_UUID;
toPage({
path: "/instances/terminal",
query: {
daemonId,
instanceId
}
});
},
condition: () => item.value!.available
},
{
title: t("TXT_CODE_e6c30866"),
icon: BlockOutlined,
click: (item: ComputedNodeInfo) => {
const daemonId = item.uuid;
toPage({
path: "/node/image",
query: {
daemonId
}
});
},
condition: () => item.value!.available
},
{
title: t("TXT_CODE_b5c7b82d"),
icon: SettingOutlined,
click: (node: ComputedNodeInfo) => {
editDialog.value.uuid = node.uuid;
editDialog.value.data.ip = node.ip;
editDialog.value.data.port = node.port;
editDialog.value.data.remarks = node.remarks;
editDialog.value.showEdit();
}
}
},
{
title: t("TXT_CODE_524e3036"),
icon: CodeOutlined,
click: (item: ComputedNodeInfo) => {
const daemonId = item.uuid;
const instanceId = "global0001";
toPage({
path: "/instances/terminal",
query: {
daemonId,
instanceId
}
});
}
},
{
title: t("TXT_CODE_e6c30866"),
icon: BlockOutlined,
click: (item: ComputedNodeInfo) => {
const daemonId = item.uuid;
toPage({
path: "/node/image",
query: {
daemonId
}
});
}
},
{
title: t("TXT_CODE_b5c7b82d"),
icon: SettingOutlined,
click: (node: ComputedNodeInfo) => {
editDialog.value.uuid = node.uuid;
editDialog.value.data.ip = node.ip;
editDialog.value.data.port = node.port;
editDialog.value.data.remarks = node.remarks;
editDialog.value.showEdit();
}
}
];
])
);
const deleteNode = async () => {
const { execute } = deleteNodeApi();

View File

@ -1,33 +1,70 @@
<script setup lang="ts">
import { ref } from "vue";
import { $t as t } from "@/lang/i18n";
import { useAppToolsStore } from "@/stores/useAppToolsStore";
import { t } from "@/lang/i18n";
import { useLayoutContainerStore } from "@/stores/useLayoutContainerStore";
import CardPanel from "@/components/CardPanel.vue";
import type { LayoutCard } from "@/types/index";
import { Empty } from "ant-design-vue";
import { Empty, message, type UploadProps } from "ant-design-vue";
import { UploadOutlined } from "@ant-design/icons-vue";
import { useLayoutCardTools } from "@/hooks/useCardTools";
import { uploadFile } from "@/services/apis/layout";
import { useAppStateStore } from "@/stores/useAppStateStore";
const props = defineProps<{
card: LayoutCard;
}>();
const { getMetaValue, setMetaValue } = useLayoutCardTools(props.card);
const { containerState } = useLayoutContainerStore();
const { isAdmin } = useAppStateStore();
const imgSrc = ref(getMetaValue("image", ""));
const { openInputDialog } = useAppToolsStore();
const open = ref(false);
const activeKey = ref("upload");
const percentComplete = ref(0);
const uploadControl = new AbortController();
const { state, execute } = uploadFile();
const beforeUpload: UploadProps["beforeUpload"] = async (file) => {
const uploadFormData = new FormData();
uploadFormData.append("file", file);
await execute({
data: uploadFormData,
timeout: Number.MAX_VALUE,
signal: uploadControl.signal,
onUploadProgress: (progressEvent: any) => {
percentComplete.value = Math.round((progressEvent.loaded * 100) / progressEvent.total);
}
});
if (state.value) {
imgSrc.value = `${window.location.origin}/upload_files/${state.value}`;
setMetaValue("image", imgSrc.value);
percentComplete.value = 0;
message.success(t("TXT_CODE_773f36a0"));
open.value = false;
return false;
}
};
const save = async () => {
setMetaValue("image", imgSrc.value);
open.value = false;
};
const editImgSrc = async () => {
try {
imgSrc.value = (await openInputDialog(t("TXT_CODE_c8a51b2e"))) as string;
setMetaValue("image", imgSrc.value);
} catch (error) {}
open.value = true;
};
const close = () => {
if (percentComplete.value !== 0) {
percentComplete.value = 0;
uploadControl.abort();
}
open.value = false;
};
</script>
<template>
<div style="width: 100%; position: relative">
<div style="width: 100%; height: 100%; position: relative">
<div v-if="imgSrc !== '' && containerState.isDesignMode" class="mask">
<a-button type="primary" @click="editImgSrc()">
{{ t("TXT_CODE_fd13f431") }}
@ -40,13 +77,67 @@ const editImgSrc = async () => {
<template #description>
<span>{{ t("TXT_CODE_635d051") }}</span>
</template>
<a-button type="primary" @click="editImgSrc()">
<a-button
:disabled="!containerState.isDesignMode || !isAdmin"
type="primary"
@click="editImgSrc()"
>
{{ t("TXT_CODE_589e091c") }}
</a-button>
</a-empty>
</template>
</CardPanel>
</div>
<a-modal v-model:open="open" :title="null" :closable="false" :destroy-on-close="true">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="upload" :tab="t('TXT_CODE_e00c858c')">
<a-progress
v-if="percentComplete > 0"
:stroke-color="{
'0%': '#49b3ff',
'100%': '#25f5b9'
}"
:percent="percentComplete"
class="mb-20"
/>
<a-upload
:max-count="1"
:disabled="percentComplete > 0"
:show-upload-list="false"
:before-upload="beforeUpload"
>
<a-button type="primary" :loading="percentComplete > 0">
<upload-outlined v-if="percentComplete === 0" />
{{
percentComplete > 0
? t("TXT_CODE_b625dbf0") + percentComplete + "%"
: t("TXT_CODE_e00c858c")
}}
</a-button>
</a-upload>
<a-typography class="mt-20">
<a-typography-title :level="5">{{ t("温馨提示") }}</a-typography-title>
<a-typography-paragraph>
<ol>
<li>{{ t("上传后将自动保存") }}</li>
<li>{{ t("点击关闭按钮可取消上传") }}</li>
<li>{{ t("你可以通过 “重置布局” 来清空你上传的所有文件") }}</li>
</ol>
</a-typography-paragraph>
</a-typography>
</a-tab-pane>
<a-tab-pane key="url" :tab="t('网络URL')" force-render>
<a-input v-model:value.lazy.trim="imgSrc" autofocus :placeholder="t('TXT_CODE_c8a51b2e')" />
</a-tab-pane>
</a-tabs>
<template #footer>
<a-button @click="close()">{{ t("关闭") }}</a-button>
<a-button v-if="activeKey === 'url'" type="primary" @click="save">
{{ t("保存") }}
</a-button>
</template>
</a-modal>
</template>
<style scoped lang="scss">

View File

@ -152,6 +152,7 @@ const selectedFile = async () => {
unzip: isImportMode ? UNZIP.ON : UNZIP.OFF,
code: zipCode.value
},
timeout: Number.MAX_VALUE,
data: uploadFormData,
url: `${parseForwardAddress(cfg.value.addr, "http")}/upload/${cfg.value.password}`,
onUploadProgress: (progressEvent: any) => {

View File

@ -14,7 +14,10 @@ export default defineConfig({
target: "http://localhost:23333",
changeOrigin: true,
ws: true
// rewrite: (path) => path.replace(/^\/api/, "")
},
"/upload_files": {
target: "http://localhost:23333",
changeOrigin: true
},
"/socket.io": {
target: "ws://localhost:23333",

View File

@ -110,8 +110,6 @@ _ / / / / /___ ____/ /_ / / / / /_/ /_ / / / /_/ /_ /_/ // __/ /
await SystemUser.initialize();
await SystemRemoteService.initialize();
const BASE_PATH = __dirname;
const app = new Koa();
// Listen for Koa errors
@ -169,7 +167,7 @@ _ / / / / /___ ____/ /_ / / / / /_/ /_ / / / /_/ /_ /_/ // __/ /
const koaStaticOptions = {
maxAge: 10 * 24 * 60 * 60 //Cache for ten days. Changed files will not be load from cache.
};
app.use(koaStatic(path.join(BASE_PATH, "public"), koaStaticOptions));
app.use(koaStatic(path.join(process.cwd(), "public"), koaStaticOptions));
// Websocket routing (useless for now)
// import SocketService from "./app/service/socket_service";

View File

@ -15,6 +15,7 @@ import {
setFrontendLayoutConfig
} from "../../service/frontend_layout";
import { ROLE } from "../../entity/user";
import { SAVE_DIR_PATH } from "../../service/frontend_layout";
const router = new Router({ prefix: "/overview" });
@ -102,7 +103,7 @@ router.post("/upload_assets", permission({ level: ROLE.ADMIN }), async (ctx) =>
if (!tmpFiles.path || !fs.existsSync(tmpFiles.path)) throw new Error($t("TXT_CODE_1a499109"));
const tmpFile = tmpFiles;
const newFileName = v4() + path.extname(tmpFile.name);
const saveDirPath = path.join(process.cwd(), "public/upload_files/");
const saveDirPath = path.join(process.cwd(), SAVE_DIR_PATH);
if (!fs.existsSync(saveDirPath)) fs.mkdirsSync(saveDirPath);
await fs.move(tmpFile.path, path.join(saveDirPath, newFileName));
ctx.body = newFileName;

View File

@ -3,9 +3,13 @@ import { IPageLayoutConfig } from "../../../../common/global";
import { $t as t } from "../i18n";
import storage from "../common/system_storage";
import { GlobalVariable } from "common";
import path from "path";
import fs from "fs-extra";
const LAYOUT_CONFIG_NAME = "layout.json";
export const SAVE_DIR_PATH = "public/upload_files/";
function getRandomId() {
return v4();
}
@ -40,6 +44,10 @@ export function setFrontendLayoutConfig(config: IPageLayoutConfig[]) {
export function resetFrontendLayoutConfig() {
storage.deleteFile(LAYOUT_CONFIG_NAME);
const filesDir = path.join(process.cwd(), SAVE_DIR_PATH);
for (const fileName of fs.readdirSync(filesDir)) {
fs.remove(path.join(filesDir, fileName), () => {});
}
}
export enum LayoutCardHeight {