This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

View File

@@ -0,0 +1,359 @@
<template>
<el-drawer title="路由配置" size="40%" v-model="visible">
<el-tabs v-model="activeName">
<el-tab-pane label="基础模式" name="first">
<template #label>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6 13.5V3.75m0 9.75a1.5 1.5 0 0 1 0 3m0-3a1.5 1.5 0 0 0 0 3m0 3.75V16.5m12-3V3.75m0 9.75a1.5 1.5 0 0 1 0 3m0-3a1.5 1.5 0 0 0 0 3m0 3.75V16.5m-6-9V3.75m0 3.75a1.5 1.5 0 0 1 0 3m0-3a1.5 1.5 0 0 0 0 3m0 9.75V10.5"/>
</svg>
基础模式
</template>
<el-form :model="formData" :rules="dataRules" label-width="100px" ref="dataFormRef" v-loading="loading">
<el-form-item label="路由ID" prop="routeId">
<el-input clearable v-model="formData.routeId"></el-input>
</el-form-item>
<el-form-item label="路由名称" prop="routeName">
<el-input clearable v-model="formData.routeName"></el-input>
</el-form-item>
<el-form-item label="路由前缀" prop="path">
<template #label>
路由前缀
<tip content="访问微服务的公共前缀 "/>
</template>
<el-input clearable v-model="formData.path">
<template #prepend>/</template>
</el-input>
</el-form-item>
<el-form-item label="目标服务" prop="serviceName">
<template #label>
目标服务
<tip content="注册到注册中心对应的服务名称 "/>
</template>
<el-input clearable v-model="formData.serviceName"></el-input>
</el-form-item>
<el-collapse v-model="collapseActive">
<el-collapse-item name="1" title="安全属性">
<template #title>
<el-icon class="header-icon">
<info-filled/>
</el-icon>
高级属性
</template>
<el-form-item label="允许跨域" prop="cors">
<el-switch v-model="formData.cors" :active-value="true" :inactive-value="false"></el-switch>
</el-form-item>
<el-form-item label="超时时间" prop="timeout">
<el-input type="number" clearable v-model="formData.timeout">
<template #suffix>毫秒</template>
</el-input>
</el-form-item>
<el-form-item label="IP限制" prop="replenishRate">
<template #label>
IP限制
<tip content="每个IP每秒最大请求多少次 "/>
</template>
<el-input type="number" clearable v-model="formData.replenishRate">
<template #suffix>/</template>
</el-input>
</el-form-item>
<el-form-item label="总数限制" prop="timeout">
<template #label>
总数限制
<tip content="当前服务器每秒最大的请求数量"/>
</template>
<el-input type="number" :min="1000" clearable v-model="formData.burstCapacity">
<template #suffix>/</template>
</el-input>
</el-form-item>
<el-form-item label="优先级" prop="sortOrder">
<el-input type="number" clearable v-model="formData.sortOrder"></el-input>
</el-form-item>
</el-collapse-item>
</el-collapse>
</el-form>
</el-tab-pane>
<el-tab-pane label="编码模式" name="second">
<template #label>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"/>
</svg>
编码模式
</template>
<json-editor
ref="jsonEditorRef"
v-model="jsonData"
codec
@change="handleJsonChange"
/>
</el-tab-pane>
</el-tabs>
<div class="flex items-center justify-center mt-4">
<el-button type="primary" @click="submit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</div>
</el-drawer>
</template>
<script lang="ts" name="routeForm" setup>
// @ts-ignore
import JsonEditor from '@axolo/json-editor-vue'
import {addObj, fetchList, validateExist} from '/@/api/admin/route';
import {useI18n} from 'vue-i18n';
import {useMessage} from '/@/hooks/message';
import {rule} from "/@/utils/validate";
import Tip from "/@/components/Tip/index.vue";
const emit = defineEmits(['refresh']);
const {t} = useI18n();
const activeName = ref('first')
const visible = ref(false);
const loading = ref(false);
const jsonData = ref({});
const dataFormRef = ref();
const collapseActive = ref('1');
const formData = ref({
routeId: '',
routeName: '',
path: '',
sortOrder: 0,
serviceName: '',
timeout: 30000,
burstCapacity: 100000,
replenishRate: 10000,
cors: false,
});
// 初始化数据
const demoData = reactive({
routeId: new Date().getTime(),
routeName: '路由名称',
predicates: [{args: {_genkey_0: '/路由前缀/**'}, name: 'Path'}],
filters: [],
uri: 'lb://服务名称',
sortOrder: 0,
metadata: {},
});
// 定义校验规则
const dataRules = ref({
routeId: [
{required: true, message: '路由标识不能为空', trigger: 'blur'},
{
validator: (rule: any, value: any, callback: any) => {
validateExist(rule, value, callback, selectRouteId.value);
},
trigger: 'blur',
},
{min: 3, max: 64, message: '长度在 3 到 64 个字符', trigger: 'blur'},
],
routeName: [
{required: true, message: '路由名称不能为空', trigger: 'blur'},
{min: 3, max: 64, message: '长度在 3 到 64 个字符', trigger: 'blur'},
],
path: [
{required: true, message: '路由前缀不能为空', trigger: 'blur'},
{validator: rule.validatorLower, trigger: 'blur'},
{min: 3, max: 64, message: '长度在 3 到 64 个字符', trigger: 'blur'},
],
serviceName: [
{required: true, message: '目标服务不能为空', trigger: 'blur'},
{min: 3, max: 64, message: '长度在 3 到 64 个字符', trigger: 'blur'},
],
sortOrder: [
{required: true, message: '排序值不能为空', trigger: 'blur'}
],
});
// 监听 formData 的变化并同步更新 jsonData
watch(formData, (val) => {
jsonData.value = {
routeId: val.routeId,
routeName: val.routeName,
sortOrder: val.sortOrder,
predicates: [{args: {_genkey_0: `/${val.path}/**`}, name: 'Path'}],
uri: `lb://${val.serviceName}`,
metadata: {
"response-timeout": val.timeout,
...(val.cors ? {
cors: {
"allowedOrigins": "*",
"allowedMethods": "*",
"allowedHeaders": "*",
"allowedCredentials": true
}
} : {})
},
filters: [
{
"name": "RequestRateLimiter", "args": {
"redis-rate-limiter.replenishRate": val.replenishRate
, "redis-rate-limiter.burstCapacity": val.burstCapacity
, "key-resolver": "#{@remoteAddrKeyResolver}"
}
}
]
};
}, {deep: true});
/**
* 处理 json 数据变化
* @param val
*/
const handleJsonChange = (val: any) => {
formData.value.routeId = val.routeId;
formData.value.routeName = val.routeName;
formData.value.sortOrder = val.sortOrder;
// 提取 属性为 name:path 的 path 字段
val.predicates.forEach((item: any) => {
if (item.name === 'Path') {
// 提取 path 字段 /路由前缀/** 只需要路由前缀 不带/
formData.value.path = item.args._genkey_0.replace('/**', '').replace('/', '');
}
});
formData.value.serviceName = val.uri.replace('lb://', '');
// 提出 filters 中的数据 给 formData replenishRate burstCapacity 赋值
val.filters.forEach((item: any) => {
if (item.name === 'RequestRateLimiter') {
formData.value.replenishRate = item.args['redis-rate-limiter.replenishRate'];
formData.value.burstCapacity = item.args['redis-rate-limiter.burstCapacity'];
}
});
// 提取metadata中的timeout字段
formData.value.timeout = val.metadata['response-timeout'];
// 检查 metadata 中是否有 cors 配置,并设置 formData.cors
formData.value.cors = !!val.metadata['cors'];
jsonData.value = val;
}
/**
* 校验 json 数据
* @param data
*/
const validateJsonData = (data: any): boolean => {
if (!data.predicates || !Array.isArray(data.predicates) || data.predicates.length === 0) {
useMessage().error('路由前缀配置不合法');
return false;
}
// 检查是否存在 name 为 'Path' 的 predicate
const pathPredicate = data.predicates.find((p: any) => p.name === 'Path');
if (!pathPredicate) {
useMessage().error('路由配置中必须包含 Path 规则');
return false;
}
if (!data.uri || typeof data.uri !== 'string' || !data.uri.startsWith('lb://')) {
useMessage().error('目标服务不合法');
return false;
}
// redis-rate-limiter.burstCapacity 不能小于 1000
if (data.filters && Array.isArray(data.filters)) {
const rateLimiter = data.filters.find((f: any) => f.name === 'RequestRateLimiter');
if (rateLimiter) {
if (rateLimiter.args['redis-rate-limiter.burstCapacity'] < 1000) {
useMessage().error('总数限制不能小于1000');
return false;
}
}
}
return true;
};
const submit = async () => {
// 设置 loading 的值为 true
loading.value = true;
try {
const valid = await dataFormRef.value.validate().catch(() => {
});
if (!valid) return false;
// 添加对 jsonData 的校验
if (!validateJsonData(jsonData.value)) {
loading.value = false;
return false;
}
// 调用 addObj 方法向服务器发送请求,传入 jsonData 的值
await addObj(jsonData.value);
// 调用 useMessage().success 方法,显示成功的提示信息
useMessage().success(t('common.optSuccessText'));
// 设置 visible 的值为 false
visible.value = false;
} catch (err: any) {
// 调用 useMessage().error 方法,显示错误的提示信息
useMessage().error(err.msg);
} finally {
// 设置 loading 的值为 false
loading.value = false;
// 触发 refresh 事件
emit('refresh');
}
};
/**
* 获取数据
* @param {string} id - 路径ID
* @returns {Promise<Array<Object>>} - 返回一个包含对象的数组
*/
const getData = async (id: string) => {
// 获取数据
const response = await fetchList({routeId: id});
const result = response.data[0];
// 解析 predicates 字段
if (result.predicates) {
result.predicates = JSON.parse(result.predicates);
}
// 解析 filters 字段
if (result.filters) {
result.filters = JSON.parse(result.filters);
}
// 解析 metadata 字段
if (result.metadata) {
result.metadata = JSON.parse(result.metadata);
}
return result;
};
const selectRouteId = ref()
const openDialog = async (id?: string) => {
selectRouteId.value = id;
visible.value = true;
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
if (id) {
await getData(id).then((data) => {
jsonData.value = data;
// parse data to formData
handleJsonChange(data);
});
} else {
jsonData.value = demoData;
}
return;
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,114 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row>
<div class="mt-4" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="routeFormRef.openDialog()">
{{ $t('common.addBtn') }}
</el-button>
</div>
</el-row>
<el-scrollbar class="mt-2">
<div class="flex flex-col">
<div class="grid grid-cols-1 gap-4 mt-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div
v-for="item in jsonData"
:key="item.routeId"
class="relative flex items-start p-4 shadow-lg rounded-xl bg-gray-100 dark:bg-[#1d1e1f] hover:bg-white dark:hover:bg-[#303030] hover:scale-105 hover:shadow-lg transition-all duration-200"
@click="routeFormRef.openDialog(item.routeId)"
>
<div class="flex items-center justify-center w-12 h-12 border border-blue-100 rounded-full bg-blue-50">
<svg
t="1698042326978"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5403"
width="32"
height="32"
>
<path d="M77 403.4v228.5c1.5 93.7 195.7 183.5 435 183.5s433.4-89.8 435-183.5V403.4H77z" fill="#1B9BDB" p-id="5404"></path>
<path
d="M947 402.7c0 99.4-194.8 194-435 194s-435-94.6-435-194 194.8-180 435-180 435 80.5 435 180z"
fill="#3ED6FF"
p-id="5405"
></path>
<path
d="M474.1 311.4H503l0.1 63.2h29.5l-0.7-63.2h28.9l-43.7-75.1zM533 417.2h-29.9l0.1 73.9h-30.6l46.2 75.2 45.5-75.2h-30.6zM654.5 380.9l-1.4-30-72.1 45 76.4 45.1-1.4-30h126.2l-2.6-30.1zM381.1 380.9h-125l-2.3 30.1H380l-1.1 30 75.9-45.1-72.5-45z"
fill="#FFFFFF"
p-id="5406"
></path>
</svg>
</div>
<div class="ml-4">
<h2 class="font-semibold">{{ item.routeName }}</h2>
<p class="mt-2 text-sm text-gray-500">{{ item.routeId }}</p>
</div>
<div class="absolute top-0 right-0 flex items-center justify-center w-12 h-12" @click.stop="deleteRoute(item.routeId)">
<el-icon>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
</el-scrollbar>
<route-form ref="routeFormRef" @refresh="getData" />
</div>
</div>
</template>
<script lang="ts" name="routeConfig" setup>
import { deleteObj, fetchList } from '/@/api/admin/route';
import type { QueryLanguageId } from 'vue3-ts-jsoneditor';
import { useMessage } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
const RouteForm = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
const jsonData = ref<any[]>([]);
const routeFormRef = ref();
const deleteRoute = (id: string) => {
deleteObj(id)
.then(() => {
useMessage().success(t('common.optSuccessText'));
})
.finally(() => {
getData();
});
};
const getData = async () => {
const { data } = await fetchList();
for (let i = 0; i < data.length; i++) {
const route = data[i];
if (route.predicates) {
const predicates = route.predicates;
route.predicates = JSON.parse(predicates);
}
if (route.filters) {
const filters = route.filters;
route.filters = JSON.parse(filters);
}
}
jsonData.value = data;
};
onMounted(() => {
getData();
});
</script>
<style lang="scss" scoped>
.copy_btn {
position: absolute;
top: 60px;
right: 20px;
z-index: 9;
color: rgb(255, 255, 255);
}
</style>