Feat: instances ui

This commit is contained in:
unitwk 2023-08-26 19:47:55 +08:00
parent 0be4ea10be
commit 5bd92a04eb
11 changed files with 192 additions and 55 deletions

View File

@ -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']

View File

@ -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;

View File

@ -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;

View 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>

View File

@ -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);

View File

@ -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"
});

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export function computeNodeName(ip: string, remark?: string) {
return remark ? `${remark} - ${ip}` : ip;
}

View File

@ -59,3 +59,11 @@ export interface InstanceDetail {
};
config: GlobalInstanceConfig;
}
export interface NodeStatus {
available: boolean;
ip: string;
port: number;
remarks: string;
uuid: string;
}

View File

@ -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>

View File

@ -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>