feat(gateway): 添加活动服务网关支持及管理面板
- 新增 docker-compose 配置,包含活动服务、YARP 网关、PostgreSQL 与 Redis - 添加活动服务与网关集成文档,详细介绍配置步骤、API 和故障排查 - 删除旧的 bash 注册脚本,新增跨平台 PowerShell 和通用 bash 注册脚本 - 实现网关相关接口的 TypeScript 客户端调用封装,支持服务注册、路由管理 - 新增网关管理前端界面,包含服务统计、服务注册、路由刷新等功能 - 调整请求客户端默认开启 token 刷新以支持更稳定的认证体验 - 制定微服务命名与版本规范,标准化 API 路径和集群命名规则
This commit is contained in:
parent
d2d70b462e
commit
38db4e2e73
@ -86,7 +86,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
client,
|
||||
doReAuthenticate,
|
||||
doRefreshToken,
|
||||
enableRefreshToken: preferences.app.enableRefreshToken,
|
||||
enableRefreshToken: true,
|
||||
formatToken,
|
||||
}),
|
||||
);
|
||||
|
||||
111
playground/src/api/gateway/index.ts
Normal file
111
playground/src/api/gateway/index.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { defHttp } from '/@/api/core';
|
||||
import type { GatewayStatistics, GatewayService, GatewayRoute, GatewayInstance } from './model';
|
||||
|
||||
enum Api {
|
||||
STATISTICS = '/statistics',
|
||||
SERVICES = '/services',
|
||||
ROUTES = '/routes',
|
||||
INSTANCES = '/instances',
|
||||
RELOAD = '/reload',
|
||||
}
|
||||
|
||||
export const getStatistics = () => {
|
||||
return defHttp.get<GatewayStatistics>({
|
||||
url: `${Api.STATISTICS}`,
|
||||
});
|
||||
};
|
||||
|
||||
export const getServices = (params?: { globalOnly?: boolean; tenantCode?: string }) => {
|
||||
return defHttp.get<GatewayService[]>({
|
||||
url: `${Api.SERVICES}`,
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const getService = (serviceName: string, params?: { tenantCode?: string }) => {
|
||||
return defHttp.get<GatewayService>({
|
||||
url: `${Api.SERVICES}/${serviceName}`,
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const registerService = (data: {
|
||||
servicePrefix: string;
|
||||
serviceName: string;
|
||||
version: string;
|
||||
serviceAddress: string;
|
||||
destinationId?: string;
|
||||
weight?: number;
|
||||
isGlobal?: boolean;
|
||||
tenantCode?: string;
|
||||
}) => {
|
||||
return defHttp.post<GatewayService>({
|
||||
url: `${Api.SERVICES}`,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const unregisterService = (serviceName: string, params?: { tenantCode?: string }) => {
|
||||
return defHttp.delete({
|
||||
url: `${Api.SERVICES}/${serviceName}`,
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const getRoutes = (params?: { globalOnly?: boolean }) => {
|
||||
return defHttp.get<GatewayRoute[]>({
|
||||
url: `${Api.ROUTES}`,
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const createRoute = (data: {
|
||||
serviceName: string;
|
||||
clusterId: string;
|
||||
pathPattern: string;
|
||||
priority?: number;
|
||||
isGlobal?: boolean;
|
||||
tenantCode?: string;
|
||||
}) => {
|
||||
return defHttp.post<GatewayRoute>({
|
||||
url: `${Api.ROUTES}`,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const getInstances = (clusterId: string) => {
|
||||
return defHttp.get<GatewayInstance[]>({
|
||||
url: `${Api.INSTANCES}`.replace('{clusterId}', clusterId),
|
||||
});
|
||||
};
|
||||
|
||||
export const addInstance = (data: {
|
||||
clusterId: string;
|
||||
destinationId: string;
|
||||
address: string;
|
||||
weight?: number;
|
||||
}) => {
|
||||
return defHttp.post<GatewayInstance>({
|
||||
url: `${Api.INSTANCES}`,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeInstance = (instanceId: number) => {
|
||||
return defHttp.delete({
|
||||
url: `${Api.INSTANCES}/${instanceId}`,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateInstanceWeight = (instanceId: number, weight: number) => {
|
||||
return defHttp.put({
|
||||
url: `${Api.INSTANCES}/${instanceId}/weight`,
|
||||
params: { weight },
|
||||
});
|
||||
};
|
||||
|
||||
export const reloadGateway = () => {
|
||||
return defHttp.post({
|
||||
url: `${Api.RELOAD}`,
|
||||
});
|
||||
};
|
||||
66
playground/src/api/gateway/model.ts
Normal file
66
playground/src/api/gateway/model.ts
Normal file
@ -0,0 +1,66 @@
|
||||
export interface GatewayStatistics {
|
||||
totalServices: number;
|
||||
globalRoutes: number;
|
||||
tenantRoutes: number;
|
||||
totalInstances: number;
|
||||
healthyInstances: number;
|
||||
recentServices: GatewayService[];
|
||||
}
|
||||
|
||||
export interface GatewayService {
|
||||
id: number;
|
||||
servicePrefix: string;
|
||||
serviceName: string;
|
||||
version: string;
|
||||
clusterId: string;
|
||||
pathPattern: string;
|
||||
serviceAddress: string;
|
||||
destinationId: string;
|
||||
weight: number;
|
||||
instanceCount: number;
|
||||
isGlobal: boolean;
|
||||
tenantCode?: string;
|
||||
status: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface GatewayRoute {
|
||||
id: number;
|
||||
serviceName: string;
|
||||
clusterId: string;
|
||||
pathPattern: string;
|
||||
priority: number;
|
||||
isGlobal: boolean;
|
||||
tenantCode?: string;
|
||||
status: number;
|
||||
instanceCount: number;
|
||||
}
|
||||
|
||||
export interface GatewayInstance {
|
||||
id: number;
|
||||
clusterId: string;
|
||||
destinationId: string;
|
||||
address: string;
|
||||
weight: number;
|
||||
health: number;
|
||||
status: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface RegisterServiceForm {
|
||||
servicePrefix: string;
|
||||
serviceName: string;
|
||||
version: string;
|
||||
serviceAddress: string;
|
||||
destinationId?: string;
|
||||
weight: number;
|
||||
isGlobal: boolean;
|
||||
tenantCode?: string;
|
||||
}
|
||||
|
||||
export interface AddInstanceForm {
|
||||
clusterId: string;
|
||||
destinationId: string;
|
||||
address: string;
|
||||
weight: number;
|
||||
}
|
||||
@ -39,6 +39,15 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
component: () => import('#/views/system/dept/list.vue'),
|
||||
},
|
||||
{
|
||||
path: '/system/gateway',
|
||||
name: 'SystemGateway',
|
||||
meta: {
|
||||
icon: 'ant-design:api-outlined',
|
||||
title: '网关管理',
|
||||
},
|
||||
component: () => import('#/views/system/gateway/GatewayManagement.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
518
playground/src/views/system/gateway/GatewayManagement.vue
Normal file
518
playground/src/views/system/gateway/GatewayManagement.vue
Normal file
@ -0,0 +1,518 @@
|
||||
<template>
|
||||
<div class="gateway-management">
|
||||
<PageHeader title="网关管理" description="管理微服务网关配置和路由">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="handleRegisterService">
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
注册服务
|
||||
</Button>
|
||||
<Button @click="handleReloadGateway">
|
||||
<Icon icon="ant-design:reload-outlined" />
|
||||
刷新配置
|
||||
</Button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<a-row :gutter="16" class="mb-4">
|
||||
<a-col :span="4">
|
||||
<a-card>
|
||||
<Statistic title="服务总数" :value="statistics?.totalServices || 0">
|
||||
<template #prefix>
|
||||
<Icon icon="ant-design:cloud-server-outlined" />
|
||||
</template>
|
||||
</Statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-card>
|
||||
<Statistic title="全局路由" :value="statistics?.globalRoutes || 0">
|
||||
<template #prefix>
|
||||
<Icon icon="ant-design:global-outlined" />
|
||||
</template>
|
||||
</Statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-card>
|
||||
<Statistic title="租户路由" :value="statistics?.tenantRoutes || 0">
|
||||
<template #prefix>
|
||||
<Icon icon="ant-design:team-outlined" />
|
||||
</template>
|
||||
</Statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-card>
|
||||
<Statistic title="实例总数" :value="statistics?.totalInstances || 0">
|
||||
<template #prefix>
|
||||
<Icon icon="ant-design:server-outlined" />
|
||||
</template>
|
||||
</Statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-card>
|
||||
<Statistic title="健康实例" :value="statistics?.healthyInstances || 0">
|
||||
<template #prefix>
|
||||
<Icon icon="ant-design:check-circle-outlined" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<span class="success-text">/ {{ statistics?.totalInstances || 0 }}</span>
|
||||
</template>
|
||||
</Statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- Tabs -->
|
||||
<a-tabs v-model:activeKey="activeTab">
|
||||
<a-tab-pane key="services" tab="服务列表">
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="serviceColumns"
|
||||
:dataSource="services"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
rowKey="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-badge :status="record.status === 1 ? 'success' : 'default'" :text="record.status === 1 ? '活跃' : '停用'" />
|
||||
</template>
|
||||
<template v-if="column.key === 'isGlobal'">
|
||||
<a-tag :color="record.isGlobal ? 'blue' : 'green'">
|
||||
{{ record.isGlobal ? '全局' : '租户专用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="handleViewInstances(record)">
|
||||
查看实例
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要注销此服务吗?" @confirm="handleUnregisterService(record)">
|
||||
<a-button size="small" danger type="link">
|
||||
注销
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="routes" tab="路由配置">
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="routeColumns"
|
||||
:dataSource="routes"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
rowKey="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'isGlobal'">
|
||||
<a-tag :color="record.isGlobal ? 'blue' : 'green'">
|
||||
{{ record.isGlobal ? '全局' : '租户专用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-badge :status="record.status === 1 ? 'success' : 'default'" :text="record.status === 1 ? '活跃' : '停用'" />
|
||||
</template>
|
||||
<template v-if="column.key === 'actions'">
|
||||
<a-button size="small" danger type="link" @click="handleDeleteRoute(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- Register Service Modal -->
|
||||
<a-modal
|
||||
v-model:open="registerModalVisible"
|
||||
title="注册新服务"
|
||||
:confirmLoading="registerLoading"
|
||||
@ok="submitRegisterService"
|
||||
>
|
||||
<a-form :model="registerForm" :labelCol="{ span: 6 }" :wrapperCol="{ span: 18 }">
|
||||
<a-form-item label="服务前缀" required>
|
||||
<a-input v-model:value="registerForm.servicePrefix" placeholder="例如: activity, member" />
|
||||
</a-form-item>
|
||||
<a-form-item label="显示名称" required>
|
||||
<a-input v-model:value="registerForm.serviceName" placeholder="例如: 活动服务" />
|
||||
</a-form-item>
|
||||
<a-form-item label="API版本">
|
||||
<a-select v-model:value="registerForm.version">
|
||||
<a-select-option value="v1">v1</a-select-option>
|
||||
<a-select-option value="v2">v2</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="服务地址" required>
|
||||
<a-input v-model:value="registerForm.serviceAddress" placeholder="http://localhost:5001" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权重">
|
||||
<a-input-number v-model:value="registerForm.weight" :min="1" :max="100" />
|
||||
</a-form-item>
|
||||
<a-form-item label="路由类型">
|
||||
<a-radio-group v-model:value="registerForm.isGlobal">
|
||||
<a-radio :value="true">全局路由</a-radio>
|
||||
<a-radio :value="false">租户专用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="!registerForm.isGlobal" label="租户代码">
|
||||
<a-input v-model:value="registerForm.tenantCode" placeholder="租户代码" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- Instances Drawer -->
|
||||
<a-drawer
|
||||
v-model:open="instancesDrawerVisible"
|
||||
title="服务实例列表"
|
||||
:width="600"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleAddInstance">
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
添加实例
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="instanceColumns"
|
||||
:dataSource="currentInstances"
|
||||
:loading="instancesLoading"
|
||||
:pagination="false"
|
||||
rowKey="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'health'">
|
||||
<a-badge :status="record.health === 1 ? 'success' : 'error'" :text="record.health === 1 ? '健康' : '异常'" />
|
||||
</template>
|
||||
<template v-if="column.key === 'weight'">
|
||||
<a-input-number v-model:value="record.weight" :min="1" :max="100" @change="(val) => handleUpdateWeight(record, val)" />
|
||||
</template>
|
||||
<template v-if="column.key === 'actions'">
|
||||
<a-popconfirm title="确定要删除此实例吗?" @confirm="handleRemoveInstance(record)">
|
||||
<a-button size="small" danger type="link">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-drawer>
|
||||
|
||||
<!-- Add Instance Modal -->
|
||||
<a-modal
|
||||
v-model:open="addInstanceModalVisible"
|
||||
title="添加服务实例"
|
||||
:confirmLoading="addInstanceLoading"
|
||||
@ok="submitAddInstance"
|
||||
>
|
||||
<a-form :model="addInstanceForm" :labelCol="{ span: 6 }" :wrapperCol="{ span: 18 }">
|
||||
<a-form-item label="集群ID" required>
|
||||
<a-input v-model:value="addInstanceForm.clusterId" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="实例ID" required>
|
||||
<a-input v-model:value="addInstanceForm.destinationId" placeholder="例如: activity-2" />
|
||||
</a-form-item>
|
||||
<a-form-item label="实例地址" required>
|
||||
<a-input v-model:value="addInstanceForm.address" placeholder="http://localhost:5002" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权重">
|
||||
<a-input-number v-model:value="addInstanceForm.weight" :min="1" :max="100" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { PageHeader } from '/@/components/Page';
|
||||
import {
|
||||
getStatistics,
|
||||
getServices,
|
||||
getRoutes,
|
||||
getInstances,
|
||||
registerService,
|
||||
unregisterService,
|
||||
addInstance,
|
||||
removeInstance,
|
||||
updateInstanceWeight,
|
||||
reloadGateway,
|
||||
} from '/@/api/gateway';
|
||||
import type { GatewayStatistics, GatewayService, GatewayRoute, GatewayInstance } from '/@/api/gateway/model';
|
||||
|
||||
const { notification, message } = useMessage();
|
||||
|
||||
const activeTab = ref('services');
|
||||
const loading = ref(false);
|
||||
const statistics = ref<GatewayStatistics>();
|
||||
const services = ref<GatewayService[]>([]);
|
||||
const routes = ref<GatewayRoute[]>([]);
|
||||
const currentInstances = ref<GatewayInstance[]>([]);
|
||||
const instancesLoading = ref(false);
|
||||
|
||||
// Modal states
|
||||
const registerModalVisible = ref(false);
|
||||
const registerLoading = ref(false);
|
||||
const registerForm = reactive({
|
||||
servicePrefix: '',
|
||||
serviceName: '',
|
||||
version: 'v1',
|
||||
serviceAddress: '',
|
||||
destinationId: '',
|
||||
weight: 1,
|
||||
isGlobal: true,
|
||||
tenantCode: '',
|
||||
});
|
||||
|
||||
const instancesDrawerVisible = ref(false);
|
||||
const addInstanceModalVisible = ref(false);
|
||||
const addInstanceLoading = ref(false);
|
||||
const addInstanceForm = reactive({
|
||||
clusterId: '',
|
||||
destinationId: '',
|
||||
address: '',
|
||||
weight: 1,
|
||||
});
|
||||
|
||||
const currentService = ref<GatewayService>();
|
||||
|
||||
const serviceColumns = [
|
||||
{ title: '服务前缀', dataIndex: 'servicePrefix', key: 'servicePrefix' },
|
||||
{ title: '显示名称', dataIndex: 'serviceName', key: 'serviceName' },
|
||||
{ title: '版本', dataIndex: 'version', key: 'version' },
|
||||
{ title: '路径模式', dataIndex: 'pathPattern', key: 'pathPattern', ellipsis: true },
|
||||
{ title: '实例数量', dataIndex: 'instanceCount', key: 'instanceCount' },
|
||||
{ title: '类型', dataIndex: 'isGlobal', key: 'isGlobal' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'actions', fixed: 'right', width: 150 },
|
||||
];
|
||||
|
||||
const routeColumns = [
|
||||
{ title: '服务名称', dataIndex: 'serviceName', key: 'serviceName' },
|
||||
{ title: '集群ID', dataIndex: 'clusterId', key: 'clusterId' },
|
||||
{ title: '路径模式', dataIndex: 'pathPattern', key: 'pathPattern', ellipsis: true },
|
||||
{ title: '优先级', dataIndex: 'priority', key: 'priority' },
|
||||
{ title: '类型', dataIndex: 'isGlobal', key: 'isGlobal' },
|
||||
{ title: '实例数', dataIndex: 'instanceCount', key: 'instanceCount' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'actions', fixed: 'right', width: 80 },
|
||||
];
|
||||
|
||||
const instanceColumns = [
|
||||
{ title: '实例ID', dataIndex: 'destinationId', key: 'destinationId' },
|
||||
{ title: '地址', dataIndex: 'address', key: 'address', ellipsis: true },
|
||||
{ title: '权重', dataIndex: 'weight', key: 'weight', width: 100 },
|
||||
{ title: '健康', dataIndex: 'health', key: 'health' },
|
||||
{ title: '操作', key: 'actions', width: 80 },
|
||||
];
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
statistics.value = await getStatistics();
|
||||
services.value = await getServices();
|
||||
routes.value = await getRoutes();
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '加载失败',
|
||||
description: error.message || '无法加载网关数据',
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegisterService = () => {
|
||||
registerForm.servicePrefix = '';
|
||||
registerForm.serviceName = '';
|
||||
registerForm.version = 'v1';
|
||||
registerForm.serviceAddress = '';
|
||||
registerForm.destinationId = '';
|
||||
registerForm.weight = 1;
|
||||
registerForm.isGlobal = true;
|
||||
registerForm.tenantCode = '';
|
||||
registerModalVisible.value = true;
|
||||
};
|
||||
|
||||
const submitRegisterService = async () => {
|
||||
if (!registerForm.servicePrefix || !registerForm.serviceName || !registerForm.serviceAddress) {
|
||||
message.warning('请填写必填字段');
|
||||
return;
|
||||
}
|
||||
|
||||
registerLoading.value = true;
|
||||
try {
|
||||
await registerService({
|
||||
servicePrefix: registerForm.servicePrefix,
|
||||
serviceName: registerForm.serviceName,
|
||||
version: registerForm.version,
|
||||
serviceAddress: registerForm.serviceAddress,
|
||||
destinationId: registerForm.destinationId || undefined,
|
||||
weight: registerForm.weight,
|
||||
isGlobal: registerForm.isGlobal,
|
||||
tenantCode: registerForm.tenantCode || undefined,
|
||||
});
|
||||
notification.success({
|
||||
message: '注册成功',
|
||||
description: `服务 ${registerForm.serviceName} 已成功注册`,
|
||||
});
|
||||
registerModalVisible.value = false;
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '注册失败',
|
||||
description: error.message,
|
||||
});
|
||||
} finally {
|
||||
registerLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnregisterService = async (record: GatewayService) => {
|
||||
try {
|
||||
await unregisterService(record.servicePrefix, record.tenantCode);
|
||||
notification.success({
|
||||
message: '注销成功',
|
||||
description: `服务 ${record.serviceName} 已注销`,
|
||||
});
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '注销失败',
|
||||
description: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewInstances = async (record: GatewayService) => {
|
||||
currentService.value = record;
|
||||
instancesDrawerVisible.value = true;
|
||||
instancesLoading.value = true;
|
||||
try {
|
||||
currentInstances.value = await getInstances(record.clusterId);
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '加载失败',
|
||||
description: error.message,
|
||||
});
|
||||
} finally {
|
||||
instancesLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddInstance = () => {
|
||||
addInstanceForm.clusterId = currentService.value?.clusterId || '';
|
||||
addInstanceForm.destinationId = `${currentService.value?.servicePrefix}-2`;
|
||||
addInstanceForm.address = '';
|
||||
addInstanceForm.weight = 1;
|
||||
addInstanceModalVisible.value = true;
|
||||
};
|
||||
|
||||
const submitAddInstance = async () => {
|
||||
if (!addInstanceForm.destinationId || !addInstanceForm.address) {
|
||||
message.warning('请填写必填字段');
|
||||
return;
|
||||
}
|
||||
|
||||
addInstanceLoading.value = true;
|
||||
try {
|
||||
await addInstance({
|
||||
clusterId: addInstanceForm.clusterId,
|
||||
destinationId: addInstanceForm.destinationId,
|
||||
address: addInstanceForm.address,
|
||||
weight: addInstanceForm.weight,
|
||||
});
|
||||
notification.success({
|
||||
message: '添加成功',
|
||||
description: '实例已成功添加',
|
||||
});
|
||||
addInstanceModalVisible.value = false;
|
||||
handleViewInstances(currentService.value!);
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '添加失败',
|
||||
description: error.message,
|
||||
});
|
||||
} finally {
|
||||
addInstanceLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveInstance = async (record: GatewayInstance) => {
|
||||
try {
|
||||
await removeInstance(record.id);
|
||||
notification.success({
|
||||
message: '删除成功',
|
||||
description: '实例已删除',
|
||||
});
|
||||
handleViewInstances(currentService.value!);
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '删除失败',
|
||||
description: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateWeight = async (record: GatewayInstance, weight: number) => {
|
||||
try {
|
||||
await updateInstanceWeight(record.id, weight);
|
||||
notification.success({
|
||||
message: '权重已更新',
|
||||
});
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '更新失败',
|
||||
description: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRoute = async (record: GatewayRoute) => {
|
||||
message.info('删除路由功能开发中');
|
||||
};
|
||||
|
||||
const handleReloadGateway = async () => {
|
||||
try {
|
||||
await reloadGateway();
|
||||
notification.success({
|
||||
message: '配置已刷新',
|
||||
description: '网关配置已重新加载',
|
||||
});
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '刷新失败',
|
||||
description: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.gateway-management {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
color: #52c41a;
|
||||
}
|
||||
</style>
|
||||
@ -702,9 +702,6 @@ importers:
|
||||
dayjs:
|
||||
specifier: 'catalog:'
|
||||
version: 1.11.19
|
||||
oidc-client-ts:
|
||||
specifier: ^3.4.1
|
||||
version: 3.4.1
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user