refactor: major project restructuring and cleanup
Changes: - Remove deprecated Fengling.Activity and YarpGateway.Admin projects - Add points processing services with distributed lock support - Update Vben frontend with gateway management pages - Add gateway config controller and database listener - Update routing to use header-mixed-nav layout - Add comprehensive test suites for Member services - Add YarpGateway integration tests - Update package versions in Directory.Packages.props Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
8e0448d5c2
commit
75b2d130c2
@ -1,5 +1,6 @@
|
||||
# 端口号
|
||||
VITE_PORT=5777
|
||||
VITE_APP_TITLE=Fengling Console
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@vben/access": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/constants": "workspace:*",
|
||||
|
||||
@ -3,254 +3,177 @@ import type { Api } from '#/api/typings';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace GatewayApi {
|
||||
const apiPrefix = '/api/gateway';
|
||||
// 注意: baseURL 已包含 /api,所以这里不再重复
|
||||
const apiPrefix = '/console/gateway';
|
||||
|
||||
// ========== 租户管理 ==========
|
||||
export async function getTenantList(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
// ========== 统计数据 ==========
|
||||
export async function getStatistics() {
|
||||
return requestClient.get<GatewayApi.GatewayStatistics>(`${apiPrefix}/statistics`);
|
||||
}
|
||||
|
||||
export async function getOverviewStats() {
|
||||
return requestClient.get<GatewayApi.GatewayStatistics>(`${apiPrefix}/statistics`);
|
||||
}
|
||||
|
||||
// ========== 服务管理 ==========
|
||||
export async function getServices(params?: {
|
||||
globalOnly?: boolean;
|
||||
tenantCode?: string;
|
||||
}) {
|
||||
return requestClient.get<Api.PaginatedResponse<GatewayApi.Tenant>>(`${apiPrefix}/tenants`, {
|
||||
return requestClient.get<GatewayApi.GatewayService[]>(`${apiPrefix}/services`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTenantById(id: number) {
|
||||
return requestClient.get<GatewayApi.Tenant>(`${apiPrefix}/tenants/${id}`);
|
||||
export async function getService(serviceName: string, tenantCode?: string) {
|
||||
return requestClient.get<GatewayApi.GatewayService>(
|
||||
`${apiPrefix}/services/${serviceName}`,
|
||||
{ params: { tenantCode } }
|
||||
);
|
||||
}
|
||||
|
||||
export async function createTenant(data: GatewayApi.CreateTenantRequest) {
|
||||
return requestClient.post<GatewayApi.Tenant, GatewayApi.CreateTenantRequest>(
|
||||
`${apiPrefix}/tenants`,
|
||||
export async function createService(data: GatewayApi.CreateServiceRequest) {
|
||||
return requestClient.post<GatewayApi.GatewayService, GatewayApi.CreateServiceRequest>(
|
||||
`${apiPrefix}/services`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateTenant(id: number, data: GatewayApi.UpdateTenantRequest) {
|
||||
return requestClient.put<void, GatewayApi.UpdateTenantRequest>(
|
||||
`${apiPrefix}/tenants/${id}`,
|
||||
data
|
||||
export async function deleteService(serviceName: string, tenantCode?: string) {
|
||||
return requestClient.delete<void>(
|
||||
`${apiPrefix}/services/${serviceName}`,
|
||||
{ params: { tenantCode } }
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteTenant(id: number) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/tenants/${id}`);
|
||||
}
|
||||
|
||||
// ========== 路由管理 ==========
|
||||
export async function getRouteList(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
tenantCode?: string;
|
||||
isGlobal?: boolean;
|
||||
export async function getRoutes(params?: {
|
||||
globalOnly?: boolean;
|
||||
}) {
|
||||
return requestClient.get<Api.PaginatedResponse<GatewayApi.Route>>(`${apiPrefix}/routes`, {
|
||||
return requestClient.get<GatewayApi.GatewayRoute[]>(`${apiPrefix}/routes`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getGlobalRoutes() {
|
||||
return requestClient.get<GatewayApi.Route[]>(`${apiPrefix}/routes/global`);
|
||||
}
|
||||
|
||||
export async function getTenantRoutes(tenantCode: string) {
|
||||
return requestClient.get<GatewayApi.Route[]>(`${apiPrefix}/routes/tenant/${tenantCode}`);
|
||||
}
|
||||
|
||||
export async function getRouteById(id: number) {
|
||||
return requestClient.get<GatewayApi.Route>(`${apiPrefix}/routes/${id}`);
|
||||
// 兼容旧的函数名
|
||||
export async function getRouteList(params?: {
|
||||
globalOnly?: boolean;
|
||||
}) {
|
||||
return requestClient.get<Api.PaginatedResponse<GatewayApi.GatewayRoute>>(`${apiPrefix}/routes`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createRoute(data: GatewayApi.CreateRouteRequest) {
|
||||
return requestClient.post<GatewayApi.Route, GatewayApi.CreateRouteRequest>(
|
||||
return requestClient.post<GatewayApi.GatewayRoute, GatewayApi.CreateRouteRequest>(
|
||||
`${apiPrefix}/routes`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function batchCreateRoutes(data: GatewayApi.BatchCreateRoutesRequest) {
|
||||
return requestClient.post<{ successCount: number }, GatewayApi.BatchCreateRoutesRequest>(
|
||||
`${apiPrefix}/routes/batch`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateRoute(id: number, data: GatewayApi.CreateRouteRequest) {
|
||||
return requestClient.put<void, GatewayApi.CreateRouteRequest>(
|
||||
`${apiPrefix}/routes/${id}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteRoute(id: number) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/routes/${id}`);
|
||||
}
|
||||
|
||||
// ========== 集群管理 ==========
|
||||
export async function getClusterList() {
|
||||
return requestClient.get<GatewayApi.Cluster[]>(`${apiPrefix}/clusters`);
|
||||
}
|
||||
|
||||
export async function getClusterById(clusterId: string) {
|
||||
return requestClient.get<GatewayApi.Cluster>(`${apiPrefix}/clusters/${clusterId}`);
|
||||
}
|
||||
|
||||
export async function createCluster(data: GatewayApi.CreateClusterRequest) {
|
||||
return requestClient.post<GatewayApi.Cluster, GatewayApi.CreateClusterRequest>(
|
||||
`${apiPrefix}/clusters`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateCluster(
|
||||
clusterId: string,
|
||||
data: GatewayApi.CreateClusterRequest
|
||||
) {
|
||||
return requestClient.put<void, GatewayApi.CreateClusterRequest>(
|
||||
`${apiPrefix}/clusters/${clusterId}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteCluster(clusterId: string) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/clusters/${clusterId}`);
|
||||
}
|
||||
|
||||
// ========== 实例管理 ==========
|
||||
export async function getInstanceList(clusterId: string) {
|
||||
return requestClient.get<GatewayApi.Instance[]>(
|
||||
export async function getInstances(clusterId: string) {
|
||||
return requestClient.get<GatewayApi.GatewayInstance[]>(
|
||||
`${apiPrefix}/clusters/${clusterId}/instances`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getInstanceById(id: number) {
|
||||
return requestClient.get<GatewayApi.Instance>(`${apiPrefix}/instances/${id}`);
|
||||
// 兼容旧的函数名
|
||||
export async function getInstanceList(clusterId: string) {
|
||||
return requestClient.get<GatewayApi.GatewayInstance[]>(
|
||||
`${apiPrefix}/clusters/${clusterId}/instances`
|
||||
);
|
||||
}
|
||||
|
||||
export async function createInstance(clusterId: string, data: GatewayApi.CreateInstanceRequest) {
|
||||
return requestClient.post<GatewayApi.Instance, GatewayApi.CreateInstanceRequest>(
|
||||
`${apiPrefix}/clusters/${clusterId}/instances`,
|
||||
export async function createInstance(data: GatewayApi.CreateInstanceRequest) {
|
||||
return requestClient.post<GatewayApi.GatewayInstance, GatewayApi.CreateInstanceRequest>(
|
||||
`${apiPrefix}/instances`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateInstance(id: number, data: GatewayApi.CreateInstanceRequest) {
|
||||
return requestClient.put<void, GatewayApi.CreateInstanceRequest>(
|
||||
`${apiPrefix}/instances/${id}`,
|
||||
data
|
||||
export async function deleteInstance(instanceId: number) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/instances/${instanceId}`);
|
||||
}
|
||||
|
||||
export async function updateInstanceWeight(instanceId: number, weight: number) {
|
||||
return requestClient.put<void, { weight: number }>(
|
||||
`${apiPrefix}/instances/${instanceId}/weight`,
|
||||
{ weight }
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteInstance(id: number) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/instances/${id}`);
|
||||
}
|
||||
|
||||
// ========== 配置热更新 ==========
|
||||
// ========== 配置重载 ==========
|
||||
export async function reloadConfig() {
|
||||
return requestClient.post<{ message: string }>(`${apiPrefix}/config/reload`);
|
||||
}
|
||||
|
||||
export async function getConfigStatus() {
|
||||
return requestClient.get<GatewayApi.ConfigStatus>(`${apiPrefix}/config/status`);
|
||||
}
|
||||
|
||||
export async function getVersionInfo() {
|
||||
return requestClient.get<GatewayApi.VersionInfo>(`${apiPrefix}/config/versions`);
|
||||
}
|
||||
|
||||
// ========== 统计 ==========
|
||||
export async function getOverviewStats() {
|
||||
return requestClient.get<GatewayApi.OverviewStats>(`${apiPrefix}/stats/overview`);
|
||||
return requestClient.post<{ message: string }>(`${apiPrefix}/reload`);
|
||||
}
|
||||
|
||||
// ========== 类型定义 ==========
|
||||
export interface Tenant {
|
||||
// 与后端 GatewayStatisticsDto 对应
|
||||
export interface GatewayStatistics {
|
||||
totalServices: number;
|
||||
globalRoutes: number;
|
||||
tenantRoutes: number;
|
||||
totalInstances: number;
|
||||
healthyInstances: number;
|
||||
recentServices: GatewayService[];
|
||||
}
|
||||
|
||||
// 与后端 GatewayServiceDto 对应
|
||||
export interface GatewayService {
|
||||
id: number;
|
||||
tenantCode: string;
|
||||
tenantName: string;
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
maxUsers?: 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;
|
||||
description?: string;
|
||||
routeCount: number;
|
||||
version: number;
|
||||
createdTime: string;
|
||||
updatedTime?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateTenantRequest {
|
||||
tenantCode: string;
|
||||
tenantName: string;
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
maxUsers?: number;
|
||||
description?: string;
|
||||
// 与后端 CreateGatewayServiceDto 对应
|
||||
export interface CreateServiceRequest {
|
||||
servicePrefix: string;
|
||||
serviceName: string;
|
||||
version?: string;
|
||||
serviceAddress: string;
|
||||
destinationId: string;
|
||||
weight?: number;
|
||||
isGlobal?: boolean;
|
||||
tenantCode?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTenantRequest {
|
||||
tenantName?: string;
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
maxUsers?: number;
|
||||
description?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
// 与后端 GatewayRouteDto 对应
|
||||
export interface GatewayRoute {
|
||||
id: number;
|
||||
tenantCode: string;
|
||||
serviceName: string;
|
||||
clusterId: string;
|
||||
pathPattern: string;
|
||||
priority: number;
|
||||
isGlobal: boolean;
|
||||
tenantCode?: string;
|
||||
status: number;
|
||||
metadata?: Record<string, string>;
|
||||
version: number;
|
||||
createdTime: string;
|
||||
updatedTime?: string;
|
||||
instanceCount: number;
|
||||
}
|
||||
|
||||
// 与后端 CreateGatewayRouteDto 对应
|
||||
export interface CreateRouteRequest {
|
||||
tenantCode?: string;
|
||||
serviceName: string;
|
||||
clusterId: string;
|
||||
pathPattern: string;
|
||||
priority?: number;
|
||||
isGlobal?: boolean;
|
||||
metadata?: Record<string, string>;
|
||||
tenantCode?: string;
|
||||
}
|
||||
|
||||
export interface BatchCreateRoutesRequest {
|
||||
routes: CreateRouteRequest[];
|
||||
}
|
||||
|
||||
export interface Cluster {
|
||||
clusterId: string;
|
||||
clusterName: string;
|
||||
description?: string;
|
||||
loadBalancingPolicy: string;
|
||||
instanceCount: number;
|
||||
healthyInstanceCount: number;
|
||||
version: number;
|
||||
createdTime: string;
|
||||
updatedTime?: string;
|
||||
instances: Instance[];
|
||||
}
|
||||
|
||||
export interface CreateClusterRequest {
|
||||
clusterId: string;
|
||||
clusterName: string;
|
||||
description?: string;
|
||||
loadBalancingPolicy?: string;
|
||||
}
|
||||
|
||||
export interface Instance {
|
||||
// 与后端 GatewayInstanceDto 对应
|
||||
export interface GatewayInstance {
|
||||
id: number;
|
||||
clusterId: string;
|
||||
destinationId: string;
|
||||
@ -258,42 +181,44 @@ export namespace GatewayApi {
|
||||
weight: number;
|
||||
health: number;
|
||||
status: number;
|
||||
version: number;
|
||||
createdTime: string;
|
||||
updatedTime?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// 与后端 CreateGatewayInstanceDto 对应
|
||||
export interface CreateInstanceRequest {
|
||||
clusterId: string;
|
||||
destinationId: string;
|
||||
address: string;
|
||||
weight?: number;
|
||||
isHealthy?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigStatus {
|
||||
routeCount: number;
|
||||
clusterCount: number;
|
||||
instanceCount: number;
|
||||
healthyInstanceCount: number;
|
||||
lastReloadTime: string;
|
||||
isListening: boolean;
|
||||
listenerStatus: string;
|
||||
// ========== 保留旧接口兼容性 (已废弃) ==========
|
||||
export interface OverviewStats extends GatewayStatistics {}
|
||||
export interface Route extends GatewayRoute {}
|
||||
export interface Instance extends GatewayInstance {}
|
||||
export interface Tenant {
|
||||
id: number;
|
||||
tenantCode: string;
|
||||
tenantName: string;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
routeVersion: number;
|
||||
clusterVersion: number;
|
||||
routeVersionUpdatedAt: string;
|
||||
clusterVersionUpdatedAt: string;
|
||||
export interface CreateTenantRequest {
|
||||
tenantCode: string;
|
||||
tenantName: string;
|
||||
}
|
||||
|
||||
export interface OverviewStats {
|
||||
totalTenants: number;
|
||||
activeTenants: number;
|
||||
totalRoutes: number;
|
||||
totalClusters: number;
|
||||
totalInstances: number;
|
||||
healthyInstances: number;
|
||||
lastUpdated: string;
|
||||
export interface UpdateTenantRequest {
|
||||
tenantName?: string;
|
||||
}
|
||||
export interface Cluster {
|
||||
clusterId: string;
|
||||
clusterName: string;
|
||||
}
|
||||
export interface CreateClusterRequest {
|
||||
clusterId: string;
|
||||
clusterName: string;
|
||||
}
|
||||
export interface ConfigStatus extends GatewayStatistics {}
|
||||
export interface VersionInfo {}
|
||||
export interface BatchCreateRoutesRequest {
|
||||
routes: CreateRouteRequest[];
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
},
|
||||
name: 'Root',
|
||||
path: '/',
|
||||
redirect: preferences.app.defaultHomePath,
|
||||
redirect: '/dashboard/index',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: $t('page.dashboard.title'),
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
name: 'Analytics',
|
||||
path: '/analytics',
|
||||
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||
meta: {
|
||||
affixTab: true,
|
||||
icon: 'lucide:area-chart',
|
||||
title: $t('page.dashboard.analytics'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Workspace',
|
||||
path: '/workspace',
|
||||
component: () => import('#/views/dashboard/workspace/index.vue'),
|
||||
meta: {
|
||||
icon: 'carbon:workspace',
|
||||
title: $t('page.dashboard.workspace'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@ -1,36 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: $t('demos.title'),
|
||||
},
|
||||
name: 'Demos',
|
||||
path: '/demos',
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
title: $t('demos.elementPlus'),
|
||||
},
|
||||
name: 'NaiveDemos',
|
||||
path: '/demos/element',
|
||||
component: () => import('#/views/demos/element/index.vue'),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: $t('demos.form'),
|
||||
},
|
||||
name: 'BasicForm',
|
||||
path: '/demos/form',
|
||||
component: () => import('#/views/demos/form/basic.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@ -1,77 +1,140 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
// ========== 系统管理 ==========
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:building-2',
|
||||
icon: 'lucide:settings',
|
||||
order: 1,
|
||||
title: 'Fengling Console',
|
||||
title: '系统管理',
|
||||
},
|
||||
name: 'Fengling',
|
||||
path: '/fengling',
|
||||
name: 'System',
|
||||
path: '/system',
|
||||
children: [
|
||||
{
|
||||
name: 'FenglingDashboard',
|
||||
path: '/fengling/dashboard',
|
||||
component: () => import('#/views/fengling/dashboard/index.vue'),
|
||||
meta: {
|
||||
affixTab: true,
|
||||
icon: 'lucide:layout-dashboard',
|
||||
title: 'Dashboard',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TenantManagement',
|
||||
path: '/fengling/tenants',
|
||||
path: '/system/tenants',
|
||||
component: () => import('#/views/fengling/tenants/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:building',
|
||||
title: 'Tenant Management',
|
||||
title: '租户管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'UserManagement',
|
||||
path: '/fengling/users',
|
||||
path: '/system/users',
|
||||
component: () => import('#/views/fengling/users/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:users',
|
||||
title: 'User Management',
|
||||
title: '用户管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'RoleManagement',
|
||||
path: '/fengling/roles',
|
||||
path: '/system/roles',
|
||||
component: () => import('#/views/fengling/roles/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:shield',
|
||||
title: 'Role Management',
|
||||
title: '角色管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'OAuthClientManagement',
|
||||
path: '/fengling/oauth',
|
||||
component: () => import('#/views/fengling/oauth/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:key',
|
||||
title: 'OAuth Clients',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
path: '/fengling/logs',
|
||||
name: 'SystemLogs',
|
||||
path: '/system/logs',
|
||||
component: () => import('#/views/fengling/logs/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:scroll-text',
|
||||
title: 'Logs',
|
||||
title: '日志管理',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// ========== 认证管理 ==========
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:shield-check',
|
||||
order: 2,
|
||||
title: '认证管理',
|
||||
},
|
||||
name: 'Auth',
|
||||
path: '/auth',
|
||||
children: [
|
||||
{
|
||||
name: 'OAuthClientManagement',
|
||||
path: '/auth/oauth',
|
||||
component: () => import('#/views/fengling/oauth/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:key',
|
||||
title: 'OAuth 客户端',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PointsRules',
|
||||
path: '/fengling/points-rules',
|
||||
path: '/auth/points-rules',
|
||||
component: () => import('#/views/fengling/points-rules/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:coins',
|
||||
title: 'Points Rules',
|
||||
title: '积分规则',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// ========== 网关管理 ==========
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:network',
|
||||
order: 3,
|
||||
title: '网关管理',
|
||||
},
|
||||
name: 'Gateway',
|
||||
path: '/gateway',
|
||||
children: [
|
||||
{
|
||||
name: 'GatewayDashboard',
|
||||
path: '/gateway/index',
|
||||
component: () => import('#/views/fengling/gateway/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
title: '概览',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'GatewayRoutes',
|
||||
path: '/gateway/routes',
|
||||
component: () => import('#/views/fengling/gateway/routes.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:route',
|
||||
title: '路由管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'GatewayServices',
|
||||
path: '/gateway/services',
|
||||
component: () => import('#/views/fengling/gateway/clusters.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:server',
|
||||
title: '服务管理',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// ========== Dashboard (独立页面) ==========
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: 0,
|
||||
title: '仪表盘',
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
name: 'FenglingDashboard',
|
||||
path: '/dashboard/index',
|
||||
component: () => import('#/views/fengling/dashboard/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
title: '仪表盘',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,94 +1,18 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import {
|
||||
VBEN_ANT_PREVIEW_URL,
|
||||
VBEN_DOC_URL,
|
||||
VBEN_GITHUB_URL,
|
||||
VBEN_LOGO_URL,
|
||||
VBEN_NAIVE_PREVIEW_URL,
|
||||
VBEN_TD_PREVIEW_URL,
|
||||
} from '@vben/constants';
|
||||
import { SvgAntdvLogoIcon, SvgTDesignIcon } from '@vben/icons';
|
||||
|
||||
import { IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: VBEN_LOGO_URL,
|
||||
order: 9998,
|
||||
title: $t('demos.vben.title'),
|
||||
},
|
||||
name: 'VbenProject',
|
||||
path: '/vben-admin',
|
||||
children: [
|
||||
{
|
||||
name: 'VbenDocument',
|
||||
path: '/vben-admin/document',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'lucide:book-open-text',
|
||||
link: VBEN_DOC_URL,
|
||||
title: $t('demos.vben.document'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenGithub',
|
||||
path: '/vben-admin/github',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'mdi:github',
|
||||
link: VBEN_GITHUB_URL,
|
||||
title: 'Github',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenNaive',
|
||||
path: '/vben-admin/naive',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: 'logos:naiveui',
|
||||
link: VBEN_NAIVE_PREVIEW_URL,
|
||||
title: $t('demos.vben.naive-ui'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenAntd',
|
||||
path: '/vben-admin/antd',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: SvgAntdvLogoIcon,
|
||||
link: VBEN_ANT_PREVIEW_URL,
|
||||
title: $t('demos.vben.antdv'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenTDesign',
|
||||
path: '/vben-admin/tdesign',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: SvgTDesignIcon,
|
||||
link: VBEN_TD_PREVIEW_URL,
|
||||
title: $t('demos.vben.tdesign'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// ========== 关于 ==========
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
path: '/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
title: '关于',
|
||||
order: 9999,
|
||||
},
|
||||
},
|
||||
// ========== 个人中心 (隐藏菜单) ==========
|
||||
{
|
||||
name: 'Profile',
|
||||
path: '/profile',
|
||||
@ -96,7 +20,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
icon: 'lucide:user',
|
||||
hideInMenu: true,
|
||||
title: $t('page.auth.profile'),
|
||||
title: '个人中心',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||
111,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#019680',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#019680',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
// xAxis: {
|
||||
// axisTick: {
|
||||
// show: false,
|
||||
// },
|
||||
// boundaryGap: false,
|
||||
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
// type: 'category',
|
||||
// },
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@ -1,82 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['访问', '趋势'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{
|
||||
name: '网页',
|
||||
},
|
||||
{
|
||||
name: '移动端',
|
||||
},
|
||||
{
|
||||
name: 'Ipad',
|
||||
},
|
||||
{
|
||||
name: '客户端',
|
||||
},
|
||||
{
|
||||
name: '第三方',
|
||||
},
|
||||
{
|
||||
name: '其它',
|
||||
},
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#b6a2de',
|
||||
},
|
||||
name: '访问',
|
||||
value: [90, 50, 86, 40, 50, 20],
|
||||
},
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
name: '趋势',
|
||||
value: [70, 75, 70, 76, 20, 85],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@ -1,46 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 400;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '外包', value: 500 },
|
||||
{ name: '定制', value: 310 },
|
||||
{ name: '技术支持', value: 274 },
|
||||
{ name: '远程', value: 400 },
|
||||
].toSorted((a, b) => {
|
||||
return a.value - b.value;
|
||||
}),
|
||||
name: '商业占比',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@ -1,65 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '搜索引擎', value: 1048 },
|
||||
{ name: '直接访问', value: 735 },
|
||||
{ name: '邮件营销', value: 580 },
|
||||
{ name: '联盟广告', value: 484 },
|
||||
],
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '访问来源',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@ -1,55 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
// color: '#4f69fd',
|
||||
data: [
|
||||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
||||
3200, 4800,
|
||||
],
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
// color: '#4f69fd',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 8000,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@ -1,90 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '用户量',
|
||||
totalTitle: '总用户量',
|
||||
totalValue: 120_000,
|
||||
value: 2000,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: 500_000,
|
||||
value: 20_000,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '下载量',
|
||||
totalTitle: '总下载量',
|
||||
totalValue: 120_000,
|
||||
value: 8000,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '使用量',
|
||||
totalTitle: '总使用量',
|
||||
totalValue: 50_000,
|
||||
value: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '流量趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月访问量',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||
<AnalyticsVisitsData />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSales />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,266 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
content: '不要等待机会,而要创造机会。',
|
||||
date: '2021-04-01',
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
content: '现在的你决定将来的你。',
|
||||
date: '2021-04-01',
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
content: '没有什么才能比努力更重要。',
|
||||
date: '2021-04-01',
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
content: '热情和欲望可以突破一切难关。',
|
||||
date: '2021-04-01',
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
content: '健康的身体是实现目标的基石。',
|
||||
date: '2021-04-01',
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
content: '路是走出来的,而不是空想出来的。',
|
||||
date: '2021-04-01',
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
{
|
||||
completed: false,
|
||||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '审查前端代码提交',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: `检查并优化系统性能,降低CPU使用率。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '系统性能优化',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '安全检查',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '更新项目依赖',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '修复UI显示问题',
|
||||
},
|
||||
]);
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||
date: '刚刚',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关注了 <a>威廉</a> `,
|
||||
date: '1个小时前',
|
||||
title: '艾文',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1天前',
|
||||
title: '克里斯',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
date: '2天前',
|
||||
title: 'Vben',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
date: '3天前',
|
||||
title: '皮特',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
date: '1周前',
|
||||
title: '杰克',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1周前',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `推送了代码到 <a>Github</a>`,
|
||||
date: '2021-04-01 20:00',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||
date: '2021-03-01 20:00',
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
>
|
||||
<template #title>
|
||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||
</template>
|
||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,117 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElMessage,
|
||||
ElNotification,
|
||||
ElSegmented,
|
||||
ElSpace,
|
||||
ElTable,
|
||||
} from 'element-plus';
|
||||
|
||||
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
||||
|
||||
function info() {
|
||||
ElMessage.info('How many roads must a man walk down');
|
||||
}
|
||||
|
||||
function error() {
|
||||
ElMessage.error({
|
||||
duration: 2500,
|
||||
message: 'Once upon a time you dressed so fine',
|
||||
});
|
||||
}
|
||||
|
||||
function warning() {
|
||||
ElMessage.warning('How many roads must a man walk down');
|
||||
}
|
||||
function success() {
|
||||
ElMessage.success(
|
||||
'Cause you walked hand in hand With another man in my place',
|
||||
);
|
||||
}
|
||||
|
||||
function notify(type: NotificationType) {
|
||||
ElNotification({
|
||||
duration: 2500,
|
||||
message: '说点啥呢',
|
||||
type,
|
||||
});
|
||||
}
|
||||
const tableData = [
|
||||
{ prop1: '1', prop2: 'A' },
|
||||
{ prop1: '2', prop2: 'B' },
|
||||
{ prop1: '3', prop2: 'C' },
|
||||
{ prop1: '4', prop2: 'D' },
|
||||
{ prop1: '5', prop2: 'E' },
|
||||
{ prop1: '6', prop2: 'F' },
|
||||
];
|
||||
|
||||
const segmentedValue = ref('Mon');
|
||||
|
||||
const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="支持多语言,主题功能集成切换等"
|
||||
title="Element Plus组件使用演示"
|
||||
>
|
||||
<div class="flex flex-wrap gap-5">
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> 按钮 </template>
|
||||
<ElSpace>
|
||||
<ElButton text>Text</ElButton>
|
||||
<ElButton>Default</ElButton>
|
||||
<ElButton type="primary"> Primary </ElButton>
|
||||
<ElButton type="info"> Info </ElButton>
|
||||
<ElButton type="success"> Success </ElButton>
|
||||
<ElButton type="warning"> Warning </ElButton>
|
||||
<ElButton type="danger"> Error </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Message </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="info"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="error"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="warning"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="success"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Notification </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="notify('info')"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="notify('success')"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> Segmented </template>
|
||||
<ElSegmented
|
||||
v-model="segmentedValue"
|
||||
:options="segmentedOptions"
|
||||
size="large"
|
||||
/>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> V-Loading </template>
|
||||
<div class="flex size-72 items-center justify-center" v-loading="true">
|
||||
一些演示的内容
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<ElTable :data="tableData" stripe>
|
||||
<ElTable.TableColumn label="测试列1" prop="prop1" />
|
||||
<ElTable.TableColumn label="测试列2" prop="prop2" />
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,191 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
// wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
handleSubmit: (values) => {
|
||||
ElMessage.success(`表单数据:${JSON.stringify(values)}`);
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'IconPicker',
|
||||
fieldName: 'icon',
|
||||
label: 'IconPicker',
|
||||
},
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'ApiSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { name: string; path: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.path,
|
||||
}));
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
// 界面显示的label
|
||||
label: 'ApiTreeSelect',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'string',
|
||||
label: 'String',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'number',
|
||||
label: 'Number',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radio',
|
||||
label: 'Radio',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: 'A' },
|
||||
{ value: 'B', label: 'B' },
|
||||
{ value: 'C', label: 'C' },
|
||||
{ value: 'D', label: 'D' },
|
||||
{ value: 'E', label: 'E' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radioButton',
|
||||
label: 'RadioButton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
|
||||
value: v,
|
||||
label: `选项${v}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox',
|
||||
label: 'Checkbox',
|
||||
componentProps: {
|
||||
options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox1',
|
||||
label: 'Checkbox1',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => {
|
||||
return ['A', 'B', 'C', 'D'].map((v) =>
|
||||
h(ElCheckbox, { label: v, value: v }),
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbotton',
|
||||
label: 'CheckBotton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'select',
|
||||
label: 'Select',
|
||||
componentProps: {
|
||||
filterable: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer();
|
||||
function setFormValues() {
|
||||
formApi.setValues({
|
||||
string: 'string',
|
||||
number: 123,
|
||||
radio: 'B',
|
||||
radioButton: 'C',
|
||||
checkbox: ['A', 'C'],
|
||||
checkbotton: ['B', 'C'],
|
||||
checkbox1: ['A', 'B'],
|
||||
date: new Date(),
|
||||
select: 'B',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
|
||||
title="表单演示"
|
||||
>
|
||||
<Drawer class="w-[600px]" title="基础表单示例">
|
||||
<Form />
|
||||
</Drawer>
|
||||
<ElCard>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="flex-auto">基础表单演示</span>
|
||||
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<ElButton type="primary" @click="drawerApi.open"> 打开抽屉 </ElButton>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
@ -13,189 +13,297 @@ import {
|
||||
ElTag,
|
||||
ElPopconfirm,
|
||||
ElMessage,
|
||||
ElCard,
|
||||
ElSwitch,
|
||||
ElRadioGroup,
|
||||
ElRadioButton,
|
||||
} from 'element-plus';
|
||||
import { Plus, Edit, Delete, Refresh, View } from '@element-plus/icons-vue';
|
||||
import { Plus, Refresh, View } from '@element-plus/icons-vue';
|
||||
|
||||
import { GatewayApi, type GatewayApi as GApi } from '#/api/fengling/gateway';
|
||||
import { GatewayApi } from '#/api/fengling/gateway';
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref<GApi.Cluster[]>([]);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('Create Cluster');
|
||||
const formData = ref<Partial<GApi.CreateClusterRequest>>({});
|
||||
const selectedCluster = ref<GApi.Cluster | null>(null);
|
||||
const servicesLoading = ref(false);
|
||||
const services = ref<GatewayApi.GatewayService[]>([]);
|
||||
const instances = ref<GatewayApi.GatewayInstance[]>([]);
|
||||
const serviceDialogVisible = ref(false);
|
||||
const instanceDialogVisible = ref(false);
|
||||
const selectedService = ref<GatewayApi.GatewayService | null>(null);
|
||||
|
||||
const loadClusters = async () => {
|
||||
const serviceForm = ref<Partial<GatewayApi.CreateServiceRequest>>({});
|
||||
const instanceForm = ref<Partial<GatewayApi.CreateInstanceRequest>>({});
|
||||
const globalOnly = ref(false);
|
||||
|
||||
const loadServices = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await GatewayApi.getClusterList();
|
||||
tableData.value = response || [];
|
||||
const response = await GatewayApi.getServices({ globalOnly: globalOnly.value });
|
||||
services.value = response || [];
|
||||
} catch (error: any) {
|
||||
console.error('[Gateway] Error loading clusters:', error);
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to load clusters');
|
||||
console.error('[Gateway] Error loading services:', error);
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to load services');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogTitle.value = 'Create Cluster';
|
||||
formData.value = {
|
||||
loadBalancingPolicy: 'DistributedWeightedRoundRobin',
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: GApi.Cluster) => {
|
||||
dialogTitle.value = 'Edit Cluster';
|
||||
formData.value = { ...row };
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (clusterId: string) => {
|
||||
const loadInstances = async (clusterId: string) => {
|
||||
instances.value = [];
|
||||
servicesLoading.value = true;
|
||||
try {
|
||||
await GatewayApi.deleteCluster(clusterId);
|
||||
ElMessage.success('Cluster deleted successfully');
|
||||
await loadClusters();
|
||||
const response = await GatewayApi.getInstances(clusterId);
|
||||
instances.value = response || [];
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to delete cluster');
|
||||
console.error('[Gateway] Error loading instances:', error);
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to load instances');
|
||||
} finally {
|
||||
servicesLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewInstances = async (row: GApi.Cluster) => {
|
||||
selectedCluster.value = row;
|
||||
const handleCreateService = () => {
|
||||
serviceForm.value = {
|
||||
isGlobal: true,
|
||||
weight: 1,
|
||||
version: 'v1',
|
||||
};
|
||||
serviceDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleViewInstances = async (row: GatewayApi.GatewayService) => {
|
||||
selectedService.value = row;
|
||||
await loadInstances(row.clusterId);
|
||||
instanceDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleDeleteService = async (serviceName: string, tenantCode?: string) => {
|
||||
try {
|
||||
if (formData.value.clusterId) {
|
||||
await GatewayApi.updateCluster(formData.value.clusterId, formData.value as GApi.CreateClusterRequest);
|
||||
ElMessage.success('Cluster updated successfully');
|
||||
} else {
|
||||
await GatewayApi.createCluster(formData.value as GApi.CreateClusterRequest);
|
||||
ElMessage.success('Cluster created successfully');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
await loadClusters();
|
||||
await GatewayApi.deleteService(serviceName, tenantCode);
|
||||
ElMessage.success('Service deleted successfully');
|
||||
await loadServices();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to delete service');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitService = async () => {
|
||||
try {
|
||||
await GatewayApi.createService(serviceForm.value as GatewayApi.CreateServiceRequest);
|
||||
ElMessage.success('Service created successfully');
|
||||
serviceDialogVisible.value = false;
|
||||
await loadServices();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Operation failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteInstance = async (id: number) => {
|
||||
const handleCreateInstance = () => {
|
||||
if (!selectedService.value) return;
|
||||
instanceForm.value = {
|
||||
clusterId: selectedService.value.clusterId,
|
||||
weight: 1,
|
||||
};
|
||||
// 创建一个临时的 dialog 来添加实例
|
||||
instanceDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSubmitInstance = async () => {
|
||||
try {
|
||||
await GatewayApi.deleteInstance(id);
|
||||
await GatewayApi.createInstance(instanceForm.value as GatewayApi.CreateInstanceRequest);
|
||||
ElMessage.success('Instance created successfully');
|
||||
if (selectedService.value) {
|
||||
await loadInstances(selectedService.value.clusterId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Operation failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteInstance = async (instanceId: number) => {
|
||||
try {
|
||||
await GatewayApi.deleteInstance(instanceId);
|
||||
ElMessage.success('Instance deleted successfully');
|
||||
if (selectedCluster.value) {
|
||||
const updated = await GatewayApi.getClusterById(selectedCluster.value.clusterId);
|
||||
selectedCluster.value = updated;
|
||||
if (selectedService.value) {
|
||||
await loadInstances(selectedService.value.clusterId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to delete instance');
|
||||
}
|
||||
};
|
||||
|
||||
const handleGlobalFilterChange = (value: boolean | string) => {
|
||||
globalOnly.value = value === true;
|
||||
loadServices();
|
||||
};
|
||||
|
||||
const getStatusTag = (status: number) => {
|
||||
return status === 1 ? 'success' : 'danger';
|
||||
};
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
return status === 1 ? '启用' : '禁用';
|
||||
};
|
||||
|
||||
const getHealthTag = (health: number) => {
|
||||
return health === 1 ? 'success' : 'danger';
|
||||
};
|
||||
|
||||
const getHealthText = (health: number) => {
|
||||
return health === 1 ? 'Healthy' : 'Unhealthy';
|
||||
return health === 1 ? '健康' : '不健康';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadClusters();
|
||||
loadServices();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gateway-clusters">
|
||||
<div class="toolbar">
|
||||
<ElButton type="primary" :icon="Plus" @click="handleCreate">Create Cluster</ElButton>
|
||||
<ElButton :icon="Refresh" @click="loadClusters">Refresh</ElButton>
|
||||
<ElButton type="primary" :icon="Plus" @click="handleCreateService">创建服务</ElButton>
|
||||
<ElButton :icon="Refresh" @click="loadServices">刷新</ElButton>
|
||||
<ElRadioGroup v-model="globalOnly" @change="handleGlobalFilterChange" style="margin-left: 20px">
|
||||
<ElRadioButton :value="false">全部服务</ElRadioButton>
|
||||
<ElRadioButton :value="true">全局服务</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
|
||||
<ElTable :data="tableData" v-loading="loading" style="width: 100%">
|
||||
<ElTableColumn prop="clusterId" label="Cluster ID" width="200" />
|
||||
<ElTableColumn prop="clusterName" label="Name" width="200" />
|
||||
<ElTableColumn prop="loadBalancingPolicy" label="Load Balancing" width="220" />
|
||||
<ElTableColumn prop="instanceCount" label="Instances" width="120">
|
||||
<!-- 服务列表 -->
|
||||
<ElTable :data="services" v-loading="loading" style="width: 100%">
|
||||
<ElTableColumn prop="id" label="ID" width="80" />
|
||||
<ElTableColumn prop="serviceName" label="服务名称" width="150" />
|
||||
<ElTableColumn prop="servicePrefix" label="服务前缀" width="120" />
|
||||
<ElTableColumn prop="clusterId" label="集群ID" width="150" />
|
||||
<ElTableColumn prop="pathPattern" label="路径匹配" min-width="150" />
|
||||
<ElTableColumn prop="serviceAddress" label="服务地址" min-width="150" />
|
||||
<ElTableColumn prop="instanceCount" label="实例数" width="80" />
|
||||
<ElTableColumn prop="isGlobal" label="全局" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.instanceCount }} ({{ row.healthyInstanceCount }} healthy)
|
||||
<ElTag :type="row.isGlobal ? 'warning' : 'info'">
|
||||
{{ row.isGlobal ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="description" label="Description" min-width="200" />
|
||||
<ElTableColumn label="Actions" width="200" fixed="right">
|
||||
<ElTableColumn prop="tenantCode" label="租户" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="primary" link :icon="View" @click="handleViewInstances(row)">Instances</ElButton>
|
||||
<ElButton type="primary" link :icon="Edit" @click="handleEdit(row)">Edit</ElButton>
|
||||
<ElPopconfirm title="Are you sure to delete this cluster?" @confirm="handleDelete(row.clusterId)">
|
||||
{{ row.tenantCode || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getStatusTag(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="primary" link :icon="View" @click="handleViewInstances(row)">实例</ElButton>
|
||||
<ElPopconfirm title="确定删除此服务?" @confirm="handleDeleteService(row.serviceName, row.tenantCode)">
|
||||
<template #reference>
|
||||
<ElButton type="danger" link :icon="Delete">Delete</ElButton>
|
||||
<ElButton type="danger" link>删除</ElButton>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<ElDialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<ElForm :model="formData" label-width="120px">
|
||||
<ElFormItem label="Cluster ID" required>
|
||||
<ElInput v-model="formData.clusterId" :disabled="!!formData.clusterId" />
|
||||
<!-- 创建服务对话框 -->
|
||||
<ElDialog v-model="serviceDialogVisible" title="创建服务" width="600px">
|
||||
<ElForm :model="serviceForm" label-width="120px">
|
||||
<ElFormItem label="服务前缀" required>
|
||||
<ElInput v-model="serviceForm.servicePrefix" placeholder="例如: user" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Cluster Name" required>
|
||||
<ElInput v-model="formData.clusterName" />
|
||||
<ElFormItem label="服务名称" required>
|
||||
<ElInput v-model="serviceForm.serviceName" placeholder="例如: user-service" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Load Balancing">
|
||||
<ElInput v-model="formData.loadBalancingPolicy" />
|
||||
<ElFormItem label="版本">
|
||||
<ElInput v-model="serviceForm.version" placeholder="v1" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Description">
|
||||
<ElInput v-model="formData.description" type="textarea" />
|
||||
<ElFormItem label="服务地址" required>
|
||||
<ElInput v-model="serviceForm.serviceAddress" placeholder="http://localhost:5000" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="目标ID" required>
|
||||
<ElInput v-model="serviceForm.destinationId" placeholder="例如: user-dest" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="权重">
|
||||
<ElInputNumber v-model="serviceForm.weight" :min="1" :max="100" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="是否全局">
|
||||
<ElSwitch v-model="serviceForm.isGlobal" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="租户代码" v-if="!serviceForm.isGlobal">
|
||||
<ElInput v-model="serviceForm.tenantCode" placeholder="租户代码" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">Cancel</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">Submit</ElButton>
|
||||
<ElButton @click="serviceDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmitService">提交</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog v-model="instanceDialogVisible" title="Service Instances" width="800px">
|
||||
<ElCard v-if="selectedCluster" class="cluster-info">
|
||||
<h3>{{ selectedCluster.clusterName }}</h3>
|
||||
<p>Cluster ID: {{ selectedCluster.clusterId }}</p>
|
||||
<!-- 实例管理对话框 -->
|
||||
<ElDialog v-model="instanceDialogVisible" title="服务实例" width="800px">
|
||||
<ElCard v-if="selectedService" class="cluster-info">
|
||||
<h3>{{ selectedService.serviceName }}</h3>
|
||||
<p>集群ID: {{ selectedService.clusterId }} | 实例数: {{ selectedService.instanceCount }}</p>
|
||||
</ElCard>
|
||||
|
||||
<ElTable :data="selectedCluster?.instances || []" style="width: 100%">
|
||||
<ElTableColumn prop="destinationId" label="Destination ID" width="150" />
|
||||
<ElTableColumn prop="address" label="Address" min-width="200" />
|
||||
<ElTableColumn prop="weight" label="Weight" width="100" />
|
||||
<ElTableColumn prop="health" label="Health" width="100">
|
||||
<div style="margin-bottom: 10px">
|
||||
<ElButton type="primary" :icon="Plus" @click="handleCreateInstance">添加实例</ElButton>
|
||||
</div>
|
||||
|
||||
<ElTable :data="instances" v-loading="servicesLoading" style="width: 100%">
|
||||
<ElTableColumn prop="id" label="ID" width="80" />
|
||||
<ElTableColumn prop="clusterId" label="集群ID" width="150" />
|
||||
<ElTableColumn prop="destinationId" label="目标ID" width="150" />
|
||||
<ElTableColumn prop="address" label="地址" min-width="200" />
|
||||
<ElTableColumn prop="weight" label="权重" width="80" />
|
||||
<ElTableColumn prop="health" label="健康状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getHealthTag(row.health)">
|
||||
{{ getHealthText(row.health) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" label="Status" width="100">
|
||||
<ElTableColumn prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? 'Active' : 'Inactive' }}
|
||||
<ElTag :type="getStatusTag(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="Actions" width="100">
|
||||
<ElTableColumn label="操作" width="80">
|
||||
<template #default="{ row }">
|
||||
<ElPopconfirm title="Delete this instance?" @confirm="handleDeleteInstance(row.id)">
|
||||
<ElPopconfirm title="确定删除此实例?" @confirm="handleDeleteInstance(row.id)">
|
||||
<template #reference>
|
||||
<ElButton type="danger" link :icon="Delete">Delete</ElButton>
|
||||
<ElButton type="danger" link>删除</ElButton>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<!-- 添加实例表单 -->
|
||||
<ElDialog title="添加实例" width="500px" :open="false" append-to-body>
|
||||
<ElForm :model="instanceForm" label-width="100px">
|
||||
<ElFormItem label="集群ID" required>
|
||||
<ElInput v-model="instanceForm.clusterId" :disabled="true" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="目标ID" required>
|
||||
<ElInput v-model="instanceForm.destinationId" placeholder="例如: user-dest-1" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="地址" required>
|
||||
<ElInput v-model="instanceForm.address" placeholder="http://localhost:5001" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="权重">
|
||||
<ElInputNumber v-model="instanceForm.weight" :min="1" :max="100" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="instanceDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmitInstance">提交</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
@ -207,6 +315,8 @@ onMounted(() => {
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cluster-info {
|
||||
|
||||
@ -1,24 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { ElRow, ElCol, ElCard, ElStatistic, ElButton, ElTag, ElMessage } from 'element-plus';
|
||||
import { ElRow, ElCol, ElCard, ElStatistic, ElButton, ElTag, ElMessage, ElTable, ElTableColumn } from 'element-plus';
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
|
||||
import { GatewayApi } from '#/api/fengling/gateway';
|
||||
|
||||
const loading = ref(false);
|
||||
const stats = ref<GatewayApi.OverviewStats | null>(null);
|
||||
const configStatus = ref<GatewayApi.ConfigStatus | null>(null);
|
||||
const stats = ref<GatewayApi.GatewayStatistics | null>(null);
|
||||
|
||||
const loadStats = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [statsRes, statusRes] = await Promise.all([
|
||||
GatewayApi.getOverviewStats(),
|
||||
GatewayApi.getConfigStatus(),
|
||||
]);
|
||||
const statsRes = await GatewayApi.getStatistics();
|
||||
stats.value = statsRes;
|
||||
configStatus.value = statusRes;
|
||||
} catch (error: any) {
|
||||
console.error('[Gateway] Error loading stats:', error);
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to load gateway stats');
|
||||
@ -49,14 +44,14 @@ onMounted(() => {
|
||||
<ElCard class="header-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>Gateway Overview</span>
|
||||
<span>网关概览</span>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="Refresh"
|
||||
:loading="loading"
|
||||
@click="handleReload"
|
||||
>
|
||||
Reload Config
|
||||
重新加载配置
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -65,52 +60,32 @@ onMounted(() => {
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20" class="stats-row">
|
||||
<ElCol :span="6">
|
||||
<ElCol :span="8">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Total Tenants" :value="stats?.totalTenants ?? 0" />
|
||||
<ElStatistic title="服务总数" :value="stats?.totalServices ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCol :span="8">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Active Tenants" :value="stats?.activeTenants ?? 0" />
|
||||
<ElStatistic title="全局路由" :value="stats?.globalRoutes ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCol :span="8">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Total Routes" :value="stats?.totalRoutes ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Total Clusters" :value="stats?.totalClusters ?? 0" />
|
||||
<ElStatistic title="租户路由" :value="stats?.tenantRoutes ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20" class="stats-row">
|
||||
<ElCol :span="6">
|
||||
<ElCol :span="12">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Total Instances" :value="stats?.totalInstances ?? 0" />
|
||||
<ElStatistic title="实例总数" :value="stats?.totalInstances ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCol :span="12">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Healthy Instances" :value="stats?.healthyInstances ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Route Version" :value="configStatus?.routeCount ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<template #title>
|
||||
<span>Listener Status</span>
|
||||
</template>
|
||||
<ElTag :type="configStatus?.isListening ? 'success' : 'danger'" size="large">
|
||||
{{ configStatus?.isListening ? 'Active' : 'Inactive' }}
|
||||
</ElTag>
|
||||
<ElStatistic title="健康实例" :value="stats?.healthyInstances ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
@ -119,9 +94,29 @@ onMounted(() => {
|
||||
<ElCol :span="24">
|
||||
<ElCard>
|
||||
<template #header>
|
||||
<span>Last Updated</span>
|
||||
<span>最近注册的服务</span>
|
||||
</template>
|
||||
<p>{{ stats?.lastUpdated ?? 'N/A' }}</p>
|
||||
<ElTable :data="stats?.recentServices ?? []" style="width: 100%" max-height="400">
|
||||
<ElTableColumn prop="serviceName" label="服务名称" width="180" />
|
||||
<ElTableColumn prop="servicePrefix" label="服务前缀" width="120" />
|
||||
<ElTableColumn prop="clusterId" label="集群ID" width="120" />
|
||||
<ElTableColumn prop="pathPattern" label="路径匹配" />
|
||||
<ElTableColumn prop="instanceCount" label="实例数" width="80" />
|
||||
<ElTableColumn prop="isGlobal" label="全局" width="80">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.isGlobal ? 'success' : 'info'" size="small">
|
||||
{{ row.isGlobal ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
|
||||
import {
|
||||
ElTable,
|
||||
@ -13,42 +13,26 @@ import {
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElTag,
|
||||
ElPopconfirm,
|
||||
ElPagination,
|
||||
ElMessage,
|
||||
ElSwitch,
|
||||
ElRadioGroup,
|
||||
ElRadioButton,
|
||||
} from 'element-plus';
|
||||
import { Plus, Edit, Delete, Refresh } from '@element-plus/icons-vue';
|
||||
import { Plus, Refresh } from '@element-plus/icons-vue';
|
||||
|
||||
import { GatewayApi, type GatewayApi as GApi } from '#/api/fengling/gateway';
|
||||
import { GatewayApi } from '#/api/fengling/gateway';
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref<GApi.Route[]>([]);
|
||||
const tableData = ref<GatewayApi.GatewayRoute[]>([]);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('Create Route');
|
||||
const formData = ref<Partial<GApi.CreateRouteRequest>>({});
|
||||
const editingId = ref<number | null>(null);
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const isGlobalOptions = [
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false },
|
||||
];
|
||||
const formData = ref<Partial<GatewayApi.CreateRouteRequest>>({});
|
||||
const globalOnly = ref(false);
|
||||
|
||||
const loadRoutes = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await GatewayApi.getRouteList({
|
||||
page: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
});
|
||||
tableData.value = response.items || [];
|
||||
pagination.value.total = response.totalCount || 0;
|
||||
const response = await GatewayApi.getRoutes({ globalOnly: globalOnly.value });
|
||||
tableData.value = response || [];
|
||||
} catch (error: any) {
|
||||
console.error('[Gateway] Error loading routes:', error);
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to load routes');
|
||||
@ -58,49 +42,17 @@ const loadRoutes = async () => {
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogTitle.value = 'Create Route';
|
||||
editingId.value = null;
|
||||
formData.value = {
|
||||
isGlobal: false,
|
||||
isGlobal: true,
|
||||
priority: 10,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: GApi.Route) => {
|
||||
dialogTitle.value = 'Edit Route';
|
||||
editingId.value = row.id;
|
||||
formData.value = {
|
||||
tenantCode: row.tenantCode,
|
||||
serviceName: row.serviceName,
|
||||
clusterId: row.clusterId,
|
||||
pathPattern: row.pathPattern,
|
||||
priority: row.priority,
|
||||
isGlobal: row.isGlobal,
|
||||
metadata: row.metadata,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await GatewayApi.deleteRoute(id);
|
||||
ElMessage.success('Route deleted successfully');
|
||||
await loadRoutes();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to delete route');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await GatewayApi.updateRoute(editingId.value, formData.value as GApi.CreateRouteRequest);
|
||||
ElMessage.success('Route updated successfully');
|
||||
} else {
|
||||
await GatewayApi.createRoute(formData.value as GApi.CreateRouteRequest);
|
||||
ElMessage.success('Route created successfully');
|
||||
}
|
||||
await GatewayApi.createRoute(formData.value as GatewayApi.CreateRouteRequest);
|
||||
ElMessage.success('Route created successfully');
|
||||
dialogVisible.value = false;
|
||||
await loadRoutes();
|
||||
} catch (error: any) {
|
||||
@ -108,8 +60,8 @@ const handleSubmit = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
pagination.value.page = page;
|
||||
const handleGlobalFilterChange = (value: boolean | string) => {
|
||||
globalOnly.value = value === true;
|
||||
loadRoutes();
|
||||
};
|
||||
|
||||
@ -118,7 +70,7 @@ const getStatusTag = (status: number) => {
|
||||
};
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
return status === 1 ? 'Active' : 'Inactive';
|
||||
return status === 1 ? '启用' : '禁用';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@ -129,80 +81,66 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="gateway-routes">
|
||||
<div class="toolbar">
|
||||
<ElButton type="primary" :icon="Plus" @click="handleCreate">Create Route</ElButton>
|
||||
<ElButton :icon="Refresh" @click="loadRoutes">Refresh</ElButton>
|
||||
<ElButton type="primary" :icon="Plus" @click="handleCreate">创建路由</ElButton>
|
||||
<ElButton :icon="Refresh" @click="loadRoutes">刷新</ElButton>
|
||||
<ElRadioGroup v-model="globalOnly" @change="handleGlobalFilterChange" style="margin-left: 20px">
|
||||
<ElRadioButton :value="false">全部路由</ElRadioButton>
|
||||
<ElRadioButton :value="true">全局路由</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
|
||||
<ElTable :data="tableData" v-loading="loading" style="width: 100%">
|
||||
<ElTableColumn prop="id" label="ID" width="80" />
|
||||
<ElTableColumn prop="tenantCode" label="Tenant" width="120">
|
||||
<ElTableColumn prop="tenantCode" label="租户" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.isGlobal ? '-' : row.tenantCode }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="serviceName" label="Service" width="150" />
|
||||
<ElTableColumn prop="clusterId" label="Cluster" width="150" />
|
||||
<ElTableColumn prop="pathPattern" label="Path Pattern" min-width="200" />
|
||||
<ElTableColumn prop="priority" label="Priority" width="100" />
|
||||
<ElTableColumn prop="isGlobal" label="Global" width="100">
|
||||
<ElTableColumn prop="serviceName" label="服务名称" width="150" />
|
||||
<ElTableColumn prop="clusterId" label="集群ID" width="150" />
|
||||
<ElTableColumn prop="pathPattern" label="路径匹配" min-width="200" />
|
||||
<ElTableColumn prop="priority" label="优先级" width="100" />
|
||||
<ElTableColumn prop="isGlobal" label="全局" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.isGlobal ? 'warning' : 'info'">
|
||||
{{ row.isGlobal ? 'Yes' : 'No' }}
|
||||
{{ row.isGlobal ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" label="Status" width="100">
|
||||
<ElTableColumn prop="instanceCount" label="实例数" width="100" />
|
||||
<ElTableColumn prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getStatusTag(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="Actions" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="primary" link :icon="Edit" @click="handleEdit(row)">Edit</ElButton>
|
||||
<ElPopconfirm title="Are you sure to delete this route?" @confirm="handleDelete(row.id)">
|
||||
<template #reference>
|
||||
<ElButton type="danger" link :icon="Delete">Delete</ElButton>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<ElPagination
|
||||
class="pagination"
|
||||
v-model:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<ElDialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<ElDialog v-model="dialogVisible" title="创建路由" width="600px">
|
||||
<ElForm :model="formData" label-width="120px">
|
||||
<ElFormItem label="Tenant Code">
|
||||
<ElInput v-model="formData.tenantCode" placeholder="Leave empty for global routes" />
|
||||
<ElFormItem label="服务名称" required>
|
||||
<ElInput v-model="formData.serviceName" placeholder="例如: user-service" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Service Name">
|
||||
<ElInput v-model="formData.serviceName" required />
|
||||
<ElFormItem label="集群ID" required>
|
||||
<ElInput v-model="formData.clusterId" placeholder="例如: default-cluster" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Cluster ID">
|
||||
<ElInput v-model="formData.clusterId" required />
|
||||
<ElFormItem label="路径匹配" required>
|
||||
<ElInput v-model="formData.pathPattern" placeholder="/api/users/**" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Path Pattern">
|
||||
<ElInput v-model="formData.pathPattern" placeholder="/api/{**path}" required />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Priority">
|
||||
<ElFormItem label="优先级">
|
||||
<ElInputNumber v-model="formData.priority" :min="0" :max="100" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Is Global">
|
||||
<ElFormItem label="是否全局">
|
||||
<ElSwitch v-model="formData.isGlobal" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="租户代码" v-if="!formData.isGlobal">
|
||||
<ElInput v-model="formData.tenantCode" placeholder="租户代码" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">Cancel</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">Submit</ElButton>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
@ -215,10 +153,7 @@ onMounted(() => {
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -13,11 +13,10 @@ export default defineConfig(async () => {
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
// Console API 代理
|
||||
'/api': {
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
// mock代理目标地址
|
||||
target: 'http://localhost:5320/api',
|
||||
target: 'http://localhost:5231',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
|
||||
@ -17,17 +17,17 @@ const defaultPreferences: Preferences = {
|
||||
contentPaddingTop: 0,
|
||||
defaultAvatar:
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||
defaultHomePath: '/analytics',
|
||||
defaultHomePath: '/dashboard/index',
|
||||
dynamicTitle: true,
|
||||
enableCheckUpdates: true,
|
||||
enablePreferences: true,
|
||||
enableRefreshToken: false,
|
||||
enableStickyPreferencesNavigationBar: true,
|
||||
isMobile: false,
|
||||
layout: 'sidebar-nav',
|
||||
layout: 'header-mixed-nav',
|
||||
locale: 'zh-CN',
|
||||
loginExpiredMode: 'page',
|
||||
name: 'Vben Admin',
|
||||
name: 'Fengling Console',
|
||||
preferencesButtonPosition: 'auto',
|
||||
watermark: false,
|
||||
watermarkContent: '',
|
||||
|
||||
@ -77,14 +77,15 @@ class PreferenceManager {
|
||||
this.cache = new StorageManager({ prefix: namespace });
|
||||
|
||||
// 合并初始偏好设置
|
||||
this.initialPreferences = merge({}, overrides, defaultPreferences);
|
||||
this.initialPreferences = merge({}, defaultPreferences, overrides);
|
||||
|
||||
// 加载缓存的偏好设置并与初始配置合并
|
||||
// 注意:默认值优先,缓存仅用于未设置的项
|
||||
const cachedPreferences = this.loadFromCache() || {};
|
||||
const mergedPreference = merge(
|
||||
{},
|
||||
cachedPreferences,
|
||||
this.initialPreferences,
|
||||
cachedPreferences
|
||||
);
|
||||
|
||||
// 更新偏好设置
|
||||
|
||||
@ -714,6 +714,9 @@ importers:
|
||||
|
||||
apps/web-ele:
|
||||
dependencies:
|
||||
'@element-plus/icons-vue':
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2(vue@3.5.27(typescript@5.9.3))
|
||||
'@vben/access':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/effects/access
|
||||
|
||||
Loading…
Reference in New Issue
Block a user