mirror of
https://github.com/MCSManager/MCSManager.git
synced 2024-11-27 06:59:54 +08:00
Merge pull request #1042 from MCSManager/abandon
Feat: select instances
This commit is contained in:
commit
17b635f9c7
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@ -71,5 +71,6 @@ declare module 'vue' {
|
||||
PlaceHolderCard: typeof import('./src/components/PlaceHolderCard.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SelectInstances: typeof import('./src/components/SelectInstances.vue')['default']
|
||||
}
|
||||
}
|
||||
|
252
frontend/src/components/SelectInstances.vue
Normal file
252
frontend/src/components/SelectInstances.vue
Normal file
@ -0,0 +1,252 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import type { UserInstance, MountComponent } from "@/types";
|
||||
import { t } from "@/lang/i18n";
|
||||
import {
|
||||
SearchOutlined,
|
||||
DownOutlined,
|
||||
FormOutlined,
|
||||
DatabaseOutlined,
|
||||
FrownOutlined
|
||||
} from "@ant-design/icons-vue";
|
||||
import BetweenMenus from "@/components/BetweenMenus.vue";
|
||||
import { remoteInstances } from "@/services/apis";
|
||||
import { remoteNodeList } from "../services/apis";
|
||||
import type { NodeStatus } from "../types/index";
|
||||
import { message } from "ant-design-vue";
|
||||
import { computeNodeName } from "../tools/nodes";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
const props = defineProps<MountComponent>();
|
||||
|
||||
const open = ref(false);
|
||||
|
||||
const cancel = async () => {
|
||||
open.value = false;
|
||||
if (props.destroyComponent) props.destroyComponent(1000);
|
||||
};
|
||||
|
||||
const operationForm = ref({
|
||||
instanceName: "",
|
||||
currentPage: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
const currentRemoteNode = ref<NodeStatus>();
|
||||
|
||||
const { execute: getNodes, state: nodes } = remoteNodeList();
|
||||
const { execute: getInstances, state: instances, isLoading } = remoteInstances();
|
||||
|
||||
const instancesList = computed(() => {
|
||||
const newInstances: UserInstance[] = [];
|
||||
for (const instance of instances.value?.data || []) {
|
||||
newInstances.push({
|
||||
instanceUuid: instance.instanceUuid,
|
||||
serviceUuid: currentRemoteNode.value?.uuid ?? "",
|
||||
nickname: instance.config.nickname,
|
||||
status: instance.status,
|
||||
hostIp: `${currentRemoteNode.value?.ip}:${currentRemoteNode.value?.port}`
|
||||
});
|
||||
}
|
||||
return newInstances;
|
||||
});
|
||||
|
||||
const initNodes = async () => {
|
||||
await getNodes();
|
||||
if (!nodes.value?.length) {
|
||||
return message.error(t("TXT_CODE_e3d96a26"));
|
||||
}
|
||||
if (localStorage.getItem("pageSelectedRemote")) {
|
||||
currentRemoteNode.value = JSON.parse(localStorage.pageSelectedRemote);
|
||||
} else {
|
||||
currentRemoteNode.value = nodes.value[0];
|
||||
}
|
||||
};
|
||||
|
||||
const initInstancesData = async () => {
|
||||
if (!currentRemoteNode.value) {
|
||||
await initNodes();
|
||||
}
|
||||
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("访问远程节点异常"));
|
||||
}
|
||||
};
|
||||
|
||||
const selectedItems = ref<UserInstance[]>([]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
align: "center",
|
||||
title: t("实例名称"),
|
||||
dataIndex: "nickname",
|
||||
key: "instanceUuid"
|
||||
},
|
||||
{
|
||||
align: "center",
|
||||
title: t("操作"),
|
||||
key: "operation"
|
||||
}
|
||||
];
|
||||
|
||||
const selectItem = (item: UserInstance) => {
|
||||
selectedItems.value.push(item);
|
||||
};
|
||||
|
||||
const findItem = (item: UserInstance) => {
|
||||
return selectedItems.value.some((i) => JSON.stringify(i) === JSON.stringify(item));
|
||||
};
|
||||
|
||||
const removeItem = (item: UserInstance) => {
|
||||
selectedItems.value.splice(selectedItems.value.indexOf(item), 1);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (props.emitResult) props.emitResult(selectedItems.value);
|
||||
await cancel();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await initInstancesData();
|
||||
open.value = true;
|
||||
});
|
||||
|
||||
const handleQueryInstance = throttle(async () => {
|
||||
await initInstancesData();
|
||||
}, 600);
|
||||
|
||||
const handleChangeNode = async (item: NodeStatus) => {
|
||||
try {
|
||||
currentRemoteNode.value = item;
|
||||
await initInstancesData();
|
||||
localStorage.setItem("pageSelectedRemote", JSON.stringify(item));
|
||||
} catch (err: any) {
|
||||
console.error(err.message);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="open"
|
||||
centered
|
||||
:mask-closable="false"
|
||||
:title="t('请选择实例')"
|
||||
:ok-text="t('保存')"
|
||||
:cancel-text="t('取消')"
|
||||
@ok="submit"
|
||||
@cancel="cancel"
|
||||
>
|
||||
<a-typography-paragraph>
|
||||
<a-typography-text type="secondary">
|
||||
{{ t("利用远程主机地址与模糊查询来为此用户增加应用实例") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<a-row :gutter="[24, 24]" style="height: 100%">
|
||||
<a-col :span="24">
|
||||
<BetweenMenus>
|
||||
<template #left>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<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">
|
||||
<FormOutlined />
|
||||
{{ t("TXT_CODE_28e53fed") }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button class="mr-12">
|
||||
{{
|
||||
computeNodeName(
|
||||
currentRemoteNode?.ip || "",
|
||||
currentRemoteNode?.available || true,
|
||||
currentRemoteNode?.remarks
|
||||
)
|
||||
}}
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template #right>
|
||||
<div class="search-input">
|
||||
<a-input
|
||||
v-model:value="operationForm.instanceName"
|
||||
:placeholder="t('TXT_CODE_ce132192')"
|
||||
@press-enter="handleQueryInstance"
|
||||
@change="handleQueryInstance"
|
||||
>
|
||||
<template #prefix>
|
||||
<search-outlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
</template>
|
||||
</BetweenMenus>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<div v-if="instances" class="flex-between align-center">
|
||||
<a-typography-text>
|
||||
{{ t("已选择") }} {{ selectedItems.length }} {{ t("项") }}
|
||||
</a-typography-text>
|
||||
<a-pagination
|
||||
v-model:current="operationForm.currentPage"
|
||||
v-model:pageSize="operationForm.pageSize"
|
||||
:total="instances.maxPage * operationForm.pageSize"
|
||||
show-size-changer
|
||||
@change="initInstancesData"
|
||||
/>
|
||||
</div>
|
||||
</a-col>
|
||||
<template v-if="instancesList">
|
||||
<a-col :span="24">
|
||||
<a-table
|
||||
:loading="isLoading"
|
||||
:data-source="instancesList"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'operation'">
|
||||
<a-button v-if="findItem(record)" danger size="" @click="removeItem(record)">
|
||||
{{ t("移除") }}
|
||||
</a-button>
|
||||
<a-button v-else size="" @click="selectItem(record)">
|
||||
{{ t("选择") }}
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-col>
|
||||
</template>
|
||||
</a-row>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.instance-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
.instance-card:hover {
|
||||
border: 1px solid var(--color-gray-8);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
</style>
|
32
frontend/src/hooks/useMountComponent.ts
Normal file
32
frontend/src/hooks/useMountComponent.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { createApp, type Component } from "vue";
|
||||
import { sleep } from "@/tools/commom";
|
||||
|
||||
export function useMountComponent() {
|
||||
let isOpen = false;
|
||||
const mount = <T>(component: Component) => {
|
||||
if (isOpen) return;
|
||||
isOpen = true;
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const div = document.createElement("div");
|
||||
document.body.appendChild(div);
|
||||
|
||||
const app = createApp(component, {
|
||||
async destroyComponent(delay = 0) {
|
||||
await sleep(delay);
|
||||
app.unmount();
|
||||
div.remove();
|
||||
isOpen = false;
|
||||
},
|
||||
emitResult(data: T) {
|
||||
isOpen = false;
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
app.mount(div);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
mount
|
||||
};
|
||||
}
|
8
frontend/src/hooks/useSelectInstances.ts
Normal file
8
frontend/src/hooks/useSelectInstances.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { useMountComponent } from "./useMountComponent";
|
||||
import SelectInstances from "@/components/SelectInstances.vue";
|
||||
import type { UserInstance } from "@/types";
|
||||
|
||||
export function useSelectInstances() {
|
||||
const { mount } = useMountComponent();
|
||||
return mount<UserInstance>(SelectInstances);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { t } from "@/lang/i18n";
|
||||
export const CARD_FIXED_HEIGHT = 200;
|
||||
|
||||
export const TERMINAL_CODE = [
|
||||
@ -11,3 +12,11 @@ export const TERMINAL_CODE = [
|
||||
"KS_C_5601",
|
||||
"UTF-16"
|
||||
];
|
||||
|
||||
export const INSTANCE_STATUS = {
|
||||
"-1": t("状态未知"),
|
||||
"0": t("已停止"),
|
||||
"1": t("正在停止"),
|
||||
"2": t("正在启动"),
|
||||
"3": t("正在运行")
|
||||
};
|
||||
|
@ -99,17 +99,11 @@ export interface UserInfo {
|
||||
}
|
||||
|
||||
export interface UserInstance {
|
||||
endTime: string;
|
||||
hostIp: string;
|
||||
ie: string;
|
||||
instanceUuid: string;
|
||||
lastDatetime: string;
|
||||
nickname: string;
|
||||
oe: string;
|
||||
remarks: string;
|
||||
serviceUuid: string;
|
||||
status: number;
|
||||
stopCommand: string;
|
||||
}
|
||||
|
||||
export interface ImageInfo {
|
||||
@ -185,3 +179,8 @@ export interface QuickStartTemplate {
|
||||
author: string;
|
||||
targetLink: string;
|
||||
}
|
||||
|
||||
export interface MountComponent {
|
||||
destroyComponent: Function;
|
||||
emitResult: Function;
|
||||
}
|
||||
|
@ -4,6 +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";
|
||||
|
||||
defineProps<{
|
||||
card: LayoutCard;
|
||||
@ -13,14 +14,6 @@ const router = useRouter();
|
||||
|
||||
const { execute, state } = userInfoApi();
|
||||
|
||||
const status = {
|
||||
"-1": t("状态未知"),
|
||||
"0": t("已停止"),
|
||||
"1": t("正在停止"),
|
||||
"2": t("正在启动"),
|
||||
"3": t("正在运行")
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("实例名称"),
|
||||
@ -32,7 +25,7 @@ const columns = [
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
customRender: (e: { text: "-1" | "1" | "2" | "3" }) => {
|
||||
return status[e.text] || e.text;
|
||||
return INSTANCE_STATUS[e.text] || e.text;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -346,7 +346,7 @@ onMounted(async () => {
|
||||
</a-menu-item> -->
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button>
|
||||
<a-button size="">
|
||||
{{ t("TXT_CODE_fe731dfc") }}
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
|
@ -10,6 +10,9 @@ import { arrayFilter } from "@/tools/array";
|
||||
import { userInfoApiAdvanced } from "@/services/apis";
|
||||
import { useLayoutCardTools } from "@/hooks/useCardTools";
|
||||
import { updateUserInstance } from "@/services/apis";
|
||||
import { useSelectInstances } from "@/hooks/useSelectInstances";
|
||||
import { message } from "ant-design-vue";
|
||||
import { INSTANCE_STATUS } from "@/types/const";
|
||||
|
||||
const props = defineProps<{
|
||||
card: LayoutCard;
|
||||
@ -30,19 +33,37 @@ const handleDelete = async (deletedInstance: UserInstance) => {
|
||||
deletedInstance.instanceUuid == instance.instanceUuid
|
||||
) {
|
||||
dataSource.value.splice(valueKey, 1);
|
||||
await updateUserInstance().execute({
|
||||
data: {
|
||||
config: {
|
||||
instances: dataSource.value
|
||||
},
|
||||
uuid: <string>userUuid
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const assignApp = async () => {
|
||||
try {
|
||||
const selectedInstances = await useSelectInstances();
|
||||
if (selectedInstances) dataSource.value = dataSource.value.concat(selectedInstances);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const saveData = async () => {
|
||||
try {
|
||||
await updateUserInstance().execute({
|
||||
data: {
|
||||
config: {
|
||||
instances: dataSource.value
|
||||
},
|
||||
uuid: <string>userUuid
|
||||
}
|
||||
});
|
||||
return message.success(t("更新成功"));
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
return message.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
async function refreshChart() {
|
||||
if (userUuid == null) {
|
||||
return;
|
||||
@ -97,6 +118,9 @@ const columns = computed(() => {
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
minWidth: "200px",
|
||||
customRender: (e: { text: "-1" | "1" | "2" | "3" }) => {
|
||||
return INSTANCE_STATUS[e.text] || e.text;
|
||||
},
|
||||
condition: () => !screen.isPhone.value
|
||||
},
|
||||
{
|
||||
@ -112,37 +136,43 @@ const columns = computed(() => {
|
||||
|
||||
<template>
|
||||
<div style="height: 100%" class="container">
|
||||
<a-row :gutter="[24, 24]" style="height: 100%">
|
||||
<div v-if="userUuid" class="h-100 w-100">
|
||||
<a-col :span="24">
|
||||
<BetweenMenus>
|
||||
<template #left>
|
||||
<a-typography-title class="mb-0" :level="4">
|
||||
{{ t("TXT_CODE_e1c9a6ac") }}
|
||||
</a-typography-title>
|
||||
</template>
|
||||
<template #right>
|
||||
<a-button type="primary">{{ t("TXT_CODE_a60466a1") }}</a-button>
|
||||
</template>
|
||||
</BetweenMenus>
|
||||
</a-col>
|
||||
<a-row v-if="userUuid" :gutter="[24, 24]" style="height: 100%">
|
||||
<a-col :span="24">
|
||||
<BetweenMenus>
|
||||
<template #left>
|
||||
<a-typography-title class="mb-0" :level="4">
|
||||
{{ t("TXT_CODE_e1c9a6ac") }}
|
||||
</a-typography-title>
|
||||
</template>
|
||||
<template #right>
|
||||
<a-button v-show="!screen.isPhone.value" class="mr-8" @click="refreshChart()">
|
||||
{{ t("刷新") }}
|
||||
</a-button>
|
||||
<a-button class="mr-8" type="primary" ghost @click="saveData()">
|
||||
{{ t("保存数据") }}
|
||||
</a-button>
|
||||
<a-button type="primary" @click="assignApp">
|
||||
{{ t("TXT_CODE_a60466a1") }}
|
||||
</a-button>
|
||||
</template>
|
||||
</BetweenMenus>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24">
|
||||
<CardPanel class="h-100">
|
||||
<template #body>
|
||||
<a-table :data-source="dataSource" :columns="columns">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'operation'">
|
||||
<a-button danger @click="handleDelete(record)">
|
||||
{{ t("TXT_CODE_ecbd7449") }}
|
||||
</a-button>
|
||||
</template>
|
||||
<a-col :span="24">
|
||||
<CardPanel class="h-100">
|
||||
<template #body>
|
||||
<a-table :data-source="dataSource" :columns="columns">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'operation'">
|
||||
<a-button danger size="" @click="handleDelete(record)">
|
||||
{{ t("TXT_CODE_ecbd7449") }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
</CardPanel>
|
||||
</a-col>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
</CardPanel>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
Loading…
Reference in New Issue
Block a user