mirror of
https://github.com/MCSManager/MCSManager.git
synced 2025-01-12 14:54:34 +08:00
Feat: instances ui
This commit is contained in:
parent
0be4ea10be
commit
5bd92a04eb
10
frontend/components.d.ts
vendored
10
frontend/components.d.ts
vendored
@ -10,7 +10,10 @@ declare module 'vue' {
|
||||
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
|
||||
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
||||
ActionButton: typeof import('./src/components/ActionButton.vue')['default']
|
||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
||||
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
||||
@ -19,14 +22,20 @@ declare module 'vue' {
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||
AppHeader: typeof import('./src/components/AppHeader.vue')['default']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||
ATable: typeof import('ant-design-vue/es')['Table']
|
||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||
@ -45,6 +54,7 @@ declare module 'vue' {
|
||||
LayoutCard: typeof import('./src/components/LayoutCard.vue')['default']
|
||||
LeftMenuBtn: typeof import('./src/components/LeftMenuBtn.vue')['default']
|
||||
LeftMenusPanel: typeof import('./src/components/LeftMenusPanel.vue')['default']
|
||||
Loading: typeof import('./src/components/Loading.vue')['default']
|
||||
MyselfInfoDialog: typeof import('./src/components/MyselfInfoDialog.vue')['default']
|
||||
NewCardList: typeof import('./src/components/NewCardList/index.vue')['default']
|
||||
Params: typeof import('./src/components/NewCardList/params.vue')['default']
|
||||
|
@ -40,7 +40,7 @@ html {
|
||||
}
|
||||
|
||||
.global-card-container-shadow {
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 2px 1px var(--card-shadow-color);
|
||||
transition: box-shadow 0.4s ease-in-out;
|
||||
|
||||
|
@ -42,12 +42,13 @@ const props = defineProps({
|
||||
@import "@/assets/global.scss";
|
||||
.padding {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--card-border-color);
|
||||
}
|
||||
.card-panel {
|
||||
background-color: var(--background-color-white);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.4s;
|
||||
|
||||
.card-panel-title {
|
||||
font-weight: 600;
|
||||
|
26
frontend/src/components/Loading.vue
Normal file
26
frontend/src/components/Loading.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { LoadingOutlined } from "@ant-design/icons-vue";
|
||||
import { h, computed } from "vue";
|
||||
|
||||
const props = defineProps<{ fontSize?: number }>();
|
||||
|
||||
const fontSize = computed(() => {
|
||||
return props.fontSize ?? 48;
|
||||
});
|
||||
|
||||
const indicator = h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: `${fontSize.value}px`,
|
||||
fontWeight: "bold"
|
||||
},
|
||||
spin: true
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex align-center justify-center h-100 w-100">
|
||||
<a-spin :indicator="indicator" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -21,12 +21,14 @@ class ApiService {
|
||||
|
||||
public async subscribe<T>(config: RequestConfig): Promise<T | undefined> {
|
||||
const reqId = btoa(
|
||||
[
|
||||
String(config.method),
|
||||
String(config.url),
|
||||
JSON.stringify(config.data ?? {}),
|
||||
JSON.stringify(config.params ?? {})
|
||||
].join("")
|
||||
encodeURIComponent(
|
||||
[
|
||||
String(config.method),
|
||||
String(config.url),
|
||||
JSON.stringify(config.data ?? {}),
|
||||
JSON.stringify(config.params ?? {})
|
||||
].join("")
|
||||
)
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -52,7 +54,8 @@ class ApiService {
|
||||
const result = await axios(config);
|
||||
const endTime = Date.now();
|
||||
const reqSpeed = endTime - startTime;
|
||||
if (reqSpeed < 100) await this.wait(100 - reqSpeed);
|
||||
const INV = 200;
|
||||
if (reqSpeed < INV) await this.wait(INV - reqSpeed);
|
||||
let realData = result.data;
|
||||
if (realData.data) realData = realData.data;
|
||||
this.event.emit(reqId, realData);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useDefineApi } from "@/stores/useDefineApi";
|
||||
import type { InstanceDetail } from "@/types";
|
||||
import type { InstanceDetail, NodeStatus } from "@/types";
|
||||
import type { BaseUserInfo } from "@/types/user";
|
||||
|
||||
// 此处 API 接口可以用中文写注释,后期再统一翻译成英语。
|
||||
@ -29,16 +29,7 @@ export const userInfoApi = useDefineApi<any, BaseUserInfo>({
|
||||
});
|
||||
|
||||
// 获取远程服务列表
|
||||
export const remoteNodeList = useDefineApi<
|
||||
any,
|
||||
{
|
||||
available: boolean;
|
||||
ip: string;
|
||||
port: number;
|
||||
remarks: string;
|
||||
uuid: string;
|
||||
}[]
|
||||
>({
|
||||
export const remoteNodeList = useDefineApi<any, NodeStatus[]>({
|
||||
url: "/api/service/remote_services_list"
|
||||
});
|
||||
|
||||
|
@ -1,3 +1,16 @@
|
||||
import { LoadingOutlined } from "@ant-design/icons-vue";
|
||||
import { h } from "vue";
|
||||
|
||||
export async function sleep(t: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, t));
|
||||
}
|
||||
|
||||
export async function loadingIconFc(fontSize = 24) {
|
||||
const indicator = h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: `${fontSize}px`
|
||||
},
|
||||
spin: true
|
||||
});
|
||||
return indicator;
|
||||
}
|
||||
|
3
frontend/src/tools/nodes.ts
Normal file
3
frontend/src/tools/nodes.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function computeNodeName(ip: string, remark?: string) {
|
||||
return remark ? `${remark} - ${ip}` : ip;
|
||||
}
|
@ -59,3 +59,11 @@ export interface InstanceDetail {
|
||||
};
|
||||
config: GlobalInstanceConfig;
|
||||
}
|
||||
|
||||
export interface NodeStatus {
|
||||
available: boolean;
|
||||
ip: string;
|
||||
port: number;
|
||||
remarks: string;
|
||||
uuid: string;
|
||||
}
|
||||
|
@ -3,33 +3,62 @@ import CardPanel from "@/components/CardPanel.vue";
|
||||
import type { LayoutCard } from "@/types/index";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { t } from "@/lang/i18n";
|
||||
import { SearchOutlined, UserOutlined } from "@ant-design/icons-vue";
|
||||
import {
|
||||
SearchOutlined,
|
||||
DownOutlined,
|
||||
FormOutlined,
|
||||
DatabaseOutlined
|
||||
} 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 { computeNodeName } from "../tools/nodes";
|
||||
import Loading from "@/components/Loading.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
card: LayoutCard;
|
||||
}>();
|
||||
|
||||
const operationForm = ref({
|
||||
name: ""
|
||||
instanceName: "",
|
||||
currentPage: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
const { execute: getNodes, state: nodes } = remoteNodeList();
|
||||
const { execute: getInstances, state: instances } = remoteInstances();
|
||||
const currentRemoteNode = ref<NodeStatus>();
|
||||
|
||||
onMounted(async () => {
|
||||
const { execute: getNodes, state: nodes } = remoteNodeList();
|
||||
const { execute: getInstances, state: instances, isLoading } = remoteInstances();
|
||||
|
||||
const initNodes = async () => {
|
||||
await getNodes();
|
||||
if (!nodes.value?.length) {
|
||||
return message.error(t("面板未能链接到任何一个远程节点,请先前往节点界面添加远程节点"));
|
||||
}
|
||||
if (nodes.value?.length === 1) {
|
||||
currentRemoteNode.value = nodes.value[0];
|
||||
}
|
||||
};
|
||||
|
||||
const initInstancesData = async () => {
|
||||
if (!currentRemoteNode.value) {
|
||||
await initNodes();
|
||||
}
|
||||
await getInstances({
|
||||
params: {
|
||||
remote_uuid: nodes.value?.[0].uuid ?? "",
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
instance_name: ""
|
||||
remote_uuid: currentRemoteNode.value?.uuid ?? "",
|
||||
page: operationForm.value.currentPage,
|
||||
page_size: operationForm.value.pageSize,
|
||||
instance_name: operationForm.value.instanceName.trim()
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await initInstancesData();
|
||||
});
|
||||
|
||||
const toAppDetailPage = (daemonId: string, instanceId: string) => {
|
||||
@ -41,6 +70,8 @@ const toAppDetailPage = (daemonId: string, instanceId: string) => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeNode = () => {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -54,11 +85,34 @@ const toAppDetailPage = (daemonId: string, instanceId: string) => {
|
||||
</a-typography-title>
|
||||
</template>
|
||||
<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-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="toNodesPage">
|
||||
<FormOutlined />
|
||||
{{ t("管理远程节点") }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button class="mr-12">
|
||||
{{ computeNodeName(currentRemoteNode?.ip || "", currentRemoteNode?.remarks) }}
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
<a-button type="primary">{{ t("新建应用") }}</a-button>
|
||||
</template>
|
||||
<template #center>
|
||||
<div class="search-input">
|
||||
<a-input v-model:value="operationForm.name" :placeholder="t('根据应用名字搜索')">
|
||||
<a-input
|
||||
v-model:value="operationForm.instanceName"
|
||||
:placeholder="t('根据应用名字搜索')"
|
||||
@press-enter="initInstancesData"
|
||||
>
|
||||
<template #prefix>
|
||||
<search-outlined />
|
||||
</template>
|
||||
@ -67,30 +121,51 @@ const toAppDetailPage = (daemonId: string, instanceId: string) => {
|
||||
</template>
|
||||
</BetweenMenus>
|
||||
</a-col>
|
||||
<a-col v-for="item in instances?.data" :key="item" :span="24" :md="6">
|
||||
<CardPanel style="height: 100%" @click="toAppDetailPage('11111', '2222')">
|
||||
<template #title>{{ item.config.nickname }}</template>
|
||||
<template #body>
|
||||
<a-typography-paragraph>
|
||||
<div>
|
||||
{{ t("状态:") }}
|
||||
{{ item.status }}
|
||||
</div>
|
||||
<div>
|
||||
{{ t("类型:") }}
|
||||
{{ item.config.type }}
|
||||
</div>
|
||||
<div>
|
||||
{{ t("启动时间:") }}
|
||||
{{ item.config.lastDatetime }}
|
||||
</div>
|
||||
<div>
|
||||
{{ t("到期时间:") }}
|
||||
{{ item.config.endTime }}
|
||||
</div>
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
</CardPanel>
|
||||
<a-col :span="24">
|
||||
<div v-if="instances" class="flex justify-end">
|
||||
<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="!isLoading">
|
||||
<a-col v-for="item in instances?.data" :key="item" :span="24" :md="6">
|
||||
<CardPanel
|
||||
class="instance-card"
|
||||
style="height: 100%"
|
||||
@click="toAppDetailPage(currentRemoteNode?.uuid || '', item.instanceUuid)"
|
||||
>
|
||||
<template #title>{{ item.config.nickname }}</template>
|
||||
<template #body>
|
||||
<a-typography-paragraph>
|
||||
<div>
|
||||
{{ t("状态:") }}
|
||||
{{ item.status }}
|
||||
</div>
|
||||
<div>
|
||||
{{ t("类型:") }}
|
||||
{{ item.config.type }}
|
||||
</div>
|
||||
<div>
|
||||
{{ t("启动时间:") }}
|
||||
{{ item.config.lastDatetime }}
|
||||
</div>
|
||||
<div>
|
||||
{{ t("到期时间:") }}
|
||||
{{ item.config.endTime }}
|
||||
</div>
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
</CardPanel>
|
||||
</a-col>
|
||||
</template>
|
||||
|
||||
<a-col v-if="isLoading" :span="24">
|
||||
<Loading class="mt-24"></Loading>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
@ -114,4 +189,11 @@ const toAppDetailPage = (daemonId: string, instanceId: string) => {
|
||||
.search-input:hover {
|
||||
width: 100%;
|
||||
}
|
||||
.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>
|
||||
|
@ -36,7 +36,7 @@ onMounted(async () => {
|
||||
</a-typography-title>
|
||||
</template>
|
||||
<template #right>
|
||||
<a-button class="mr-6" type="primary">{{ t("新增节点") }}</a-button>
|
||||
<a-button class="mr-12" type="primary">{{ t("新增节点") }}</a-button>
|
||||
<a-button>{{ t("使用手册") }}</a-button>
|
||||
</template>
|
||||
<template #center>
|
||||
|
Loading…
Reference in New Issue
Block a user