init
This commit is contained in:
359
src/views/tools/route/form.vue
Normal file
359
src/views/tools/route/form.vue
Normal 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>
|
||||
114
src/views/tools/route/index.vue
Normal file
114
src/views/tools/route/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user