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,
|
client,
|
||||||
doReAuthenticate,
|
doReAuthenticate,
|
||||||
doRefreshToken,
|
doRefreshToken,
|
||||||
enableRefreshToken: preferences.app.enableRefreshToken,
|
enableRefreshToken: true,
|
||||||
formatToken,
|
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'),
|
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:
|
dayjs:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.11.19
|
version: 1.11.19
|
||||||
oidc-client-ts:
|
|
||||||
specifier: ^3.4.1
|
|
||||||
version: 3.4.1
|
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user