feat: 支持工作流插件复用与试运行
- 新增工作流插件类型、发布快照同步、实时可用性与下线影响检查 - 收口绑定候选、分类权限、间接环路校验与运行态优雅降级 - 补齐管理端工作流插件配置、详情与试运行界面及定向测试
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { Plus, Remove } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
@@ -22,15 +24,28 @@ import { api } from '#/api/request';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const embeddingLlmList = ref<any>([]);
|
||||
const rerankerLlmList = ref<any>([]);
|
||||
const categoryList = ref<any[]>([]);
|
||||
interface headersType {
|
||||
interface HeaderItem {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
const authTypeList = ref<headersType[]>([
|
||||
|
||||
interface WorkflowCandidate {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const btnLoading = ref(false);
|
||||
const categoryList = ref<any[]>([]);
|
||||
const workflowCandidates = ref<WorkflowCandidate[]>([]);
|
||||
const tempAddHeaders = ref<HeaderItem[]>([]);
|
||||
const entity = ref<any>(createDefaultEntity());
|
||||
|
||||
const authTypeList = [
|
||||
{
|
||||
label: 'None',
|
||||
value: 'none',
|
||||
@@ -39,79 +54,211 @@ const authTypeList = ref<headersType[]>([
|
||||
label: 'Service token / ApiKey',
|
||||
value: 'apiKey',
|
||||
},
|
||||
]);
|
||||
onMounted(() => {
|
||||
api.get('/api/v1/plugin/modelList?supportEmbed=true').then((res) => {
|
||||
embeddingLlmList.value = res.data;
|
||||
});
|
||||
api
|
||||
.get('/api/v1/plugin/modelList?supportRerankerLlmList=true')
|
||||
.then((res) => {
|
||||
rerankerLlmList.value = res.data;
|
||||
});
|
||||
api.get('/api/v1/pluginCategory/list').then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
categoryList.value = res.data;
|
||||
}
|
||||
});
|
||||
});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const tempAddHeaders = ref<headersType[]>([]);
|
||||
const entity = ref<any>({
|
||||
alias: '',
|
||||
categoryIds: [],
|
||||
deptId: '',
|
||||
icon: '',
|
||||
title: '',
|
||||
authType: 'none',
|
||||
description: '',
|
||||
englishName: '',
|
||||
headers: '',
|
||||
position: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
];
|
||||
|
||||
const pluginTypeOptions = [
|
||||
{
|
||||
label: $t('plugin.typeHttp'),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: $t('plugin.typeWorkflow'),
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const isWorkflowType = computed(
|
||||
() => Number(entity.value.type || 1) === 2,
|
||||
);
|
||||
|
||||
const rules = computed<FormRules>(() => ({
|
||||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
description: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
baseUrl: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!isWorkflowType.value && !value) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
workflowId: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (isWorkflowType.value && !value) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
authType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
tokenKey: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
tokenValue: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!isWorkflowType.value && !value) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
position: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (
|
||||
!isWorkflowType.value &&
|
||||
entity.value.authType === 'apiKey' &&
|
||||
!value
|
||||
) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
tokenKey: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (
|
||||
!isWorkflowType.value &&
|
||||
entity.value.authType === 'apiKey' &&
|
||||
!value
|
||||
) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
tokenValue: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (
|
||||
!isWorkflowType.value &&
|
||||
entity.value.authType === 'apiKey' &&
|
||||
!value
|
||||
) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => entity.value.type,
|
||||
(type) => {
|
||||
if (Number(type || 1) === 2) {
|
||||
entity.value.baseUrl = '';
|
||||
entity.value.authType = 'none';
|
||||
entity.value.position = '';
|
||||
entity.value.tokenKey = '';
|
||||
entity.value.tokenValue = '';
|
||||
tempAddHeaders.value = [];
|
||||
return;
|
||||
}
|
||||
entity.value.workflowId = '';
|
||||
entity.value.workflowTitle = '';
|
||||
entity.value.available = true;
|
||||
entity.value.reasonMessage = '';
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadCategories(), loadWorkflowCandidates()]);
|
||||
});
|
||||
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
tempAddHeaders.value = [];
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
if (row.headers) {
|
||||
tempAddHeaders.value = JSON.parse(row.headers);
|
||||
}
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
|
||||
function createDefaultEntity() {
|
||||
return {
|
||||
alias: '',
|
||||
authType: 'none',
|
||||
available: true,
|
||||
baseUrl: '',
|
||||
categoryIds: [],
|
||||
deptId: '',
|
||||
description: '',
|
||||
englishName: '',
|
||||
headers: '',
|
||||
icon: '',
|
||||
name: '',
|
||||
pluginType: 1,
|
||||
position: '',
|
||||
reasonMessage: '',
|
||||
title: '',
|
||||
tokenKey: '',
|
||||
tokenValue: '',
|
||||
type: 1,
|
||||
workflowId: '',
|
||||
workflowTitle: '',
|
||||
};
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
const res = await api.get('/api/v1/pluginCategory/list');
|
||||
if (res.errorCode === 0) {
|
||||
categoryList.value = res.data;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkflowCandidates(keyword: string = '') {
|
||||
const res = await api.get('/api/v1/plugin/workflowCandidates', {
|
||||
params: {
|
||||
keyword,
|
||||
},
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
workflowCandidates.value = Array.isArray(res.data) ? res.data : [];
|
||||
ensureCurrentWorkflowOption();
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCurrentWorkflowOption() {
|
||||
if (!entity.value.workflowId || !entity.value.workflowTitle) {
|
||||
return;
|
||||
}
|
||||
const exists = workflowCandidates.value.some(
|
||||
(item) => String(item.id) === String(entity.value.workflowId),
|
||||
);
|
||||
if (!exists) {
|
||||
workflowCandidates.value.unshift({
|
||||
id: entity.value.workflowId,
|
||||
title: entity.value.workflowTitle,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(row: any) {
|
||||
tempAddHeaders.value = row.headers ? JSON.parse(row.headers) : [];
|
||||
isAdd.value = !row.id;
|
||||
entity.value = {
|
||||
...createDefaultEntity(),
|
||||
...row,
|
||||
authType: row.authType || 'none',
|
||||
categoryIds: row.categoryIds?.map((item: any) => item.id) || [],
|
||||
type: Number(row.type || row.pluginType || 1),
|
||||
};
|
||||
ensureCurrentWorkflowOption();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -131,83 +278,86 @@ async function syncPluginCategories(pluginId: string, categoryIds: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePayload() {
|
||||
const plainEntity = { ...entity.value };
|
||||
const categoryIds = [...(plainEntity.categoryIds || [])];
|
||||
delete plainEntity.categoryIds;
|
||||
if (isWorkflowType.value) {
|
||||
plainEntity.baseUrl = '';
|
||||
plainEntity.headers = [];
|
||||
plainEntity.authType = 'none';
|
||||
plainEntity.position = '';
|
||||
plainEntity.tokenKey = '';
|
||||
plainEntity.tokenValue = '';
|
||||
}
|
||||
return {
|
||||
payload: {
|
||||
...plainEntity,
|
||||
headers: isWorkflowType.value ? [] : [...tempAddHeaders.value],
|
||||
},
|
||||
categoryIds,
|
||||
};
|
||||
}
|
||||
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
const plainEntity = { ...entity.value };
|
||||
const plainHeaders = [...tempAddHeaders.value];
|
||||
const categoryIds = [...(plainEntity.categoryIds || [])];
|
||||
delete plainEntity.categoryIds;
|
||||
if (isAdd.value) {
|
||||
api
|
||||
.post('/api/v1/plugin/plugin/save', {
|
||||
...plainEntity,
|
||||
headers: plainHeaders,
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.errorCode === 0) {
|
||||
const pluginId =
|
||||
res.data?.id || plainEntity.id || entity.value.id;
|
||||
if (!pluginId) {
|
||||
throw new Error('插件保存成功,但未返回插件ID');
|
||||
}
|
||||
await syncPluginCategories(pluginId, categoryIds);
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
emit('reload');
|
||||
} else {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
||||
})
|
||||
.finally(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
} else {
|
||||
api
|
||||
.post('/api/v1/plugin/plugin/update', {
|
||||
...plainEntity,
|
||||
headers: plainHeaders,
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.errorCode === 0) {
|
||||
await syncPluginCategories(entity.value.id, categoryIds);
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
emit('reload');
|
||||
} else {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
||||
})
|
||||
.finally(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
saveForm.value?.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
btnLoading.value = true;
|
||||
const { payload, categoryIds } = normalizePayload();
|
||||
const requestUrl = isAdd.value
|
||||
? '/api/v1/plugin/plugin/save'
|
||||
: '/api/v1/plugin/plugin/update';
|
||||
try {
|
||||
const res = await api.post(requestUrl, payload);
|
||||
if (res.errorCode !== 0) {
|
||||
ElMessage.error(res.message);
|
||||
return;
|
||||
}
|
||||
const pluginId = res.data?.id || payload.id || entity.value.id;
|
||||
if (!pluginId) {
|
||||
throw new Error('插件保存成功,但未返回插件ID');
|
||||
}
|
||||
await syncPluginCategories(pluginId, categoryIds);
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success(
|
||||
isAdd.value ? $t('message.saveOkMessage') : $t('message.updateOkMessage'),
|
||||
);
|
||||
emit('reload');
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
tempAddHeaders.value = [];
|
||||
entity.value = {};
|
||||
entity.value = createDefaultEntity();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
function addHeader() {
|
||||
tempAddHeaders.value.push({
|
||||
label: '',
|
||||
value: '',
|
||||
});
|
||||
}
|
||||
|
||||
function removeHeader(index: number) {
|
||||
tempAddHeaders.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function handleWorkflowChange(value: string) {
|
||||
const target = workflowCandidates.value.find(
|
||||
(item) => String(item.id) === String(value),
|
||||
);
|
||||
entity.value.workflowTitle = target?.title || '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -237,15 +387,22 @@ function removeHeader(index: number) {
|
||||
>
|
||||
<UploadAvatar v-model="entity.icon" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="type" :label="$t('plugin.type')">
|
||||
<ElSelect v-model="entity.type" :disabled="!isAdd">
|
||||
<ElOption
|
||||
v-for="item in pluginTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="name" :label="$t('plugin.name')">
|
||||
<ElInput
|
||||
v-model.trim="entity.name"
|
||||
:placeholder="$t('plugin.placeholder.name')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="baseUrl" :label="$t('plugin.baseUrl')">
|
||||
<ElInput v-model.trim="entity.baseUrl" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="description" :label="$t('plugin.description')">
|
||||
<ElInput
|
||||
v-model.trim="entity.description"
|
||||
@@ -270,61 +427,108 @@ function removeHeader(index: number) {
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="Headers" label="Headers">
|
||||
<div
|
||||
class="headers-container-reduce flex flex-row gap-4"
|
||||
v-for="(item, index) in tempAddHeaders"
|
||||
:key="index"
|
||||
>
|
||||
<div class="head-con-content flex flex-row gap-4">
|
||||
<ElInput v-model.trim="item.label" placeholder="header name" />
|
||||
<ElInput v-model.trim="item.value" placeholder="header value" />
|
||||
<ElIcon size="20" @click="removeHeader" style="cursor: pointer">
|
||||
<Remove />
|
||||
</ElIcon>
|
||||
<template v-if="isWorkflowType">
|
||||
<ElAlert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
:title="$t('plugin.workflowPluginHint')"
|
||||
/>
|
||||
<ElFormItem prop="workflowId" :label="$t('plugin.workflowId')">
|
||||
<ElSelect
|
||||
v-model="entity.workflowId"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="loadWorkflowCandidates"
|
||||
:placeholder="$t('plugin.placeholder.workflow')"
|
||||
@change="handleWorkflowChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in workflowCandidates"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="form-helper-text">
|
||||
{{ $t('plugin.onlyPublishedWorkflow') }}
|
||||
</div>
|
||||
</div>
|
||||
<ElButton @click="addHeader" class="addHeadersBtn">
|
||||
<ElIcon size="18" style="margin-right: 4px">
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
{{ $t('button.add') }}headers
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="authType" :label="$t('plugin.authType')">
|
||||
<ElSelect v-model="entity.authType">
|
||||
<ElOption
|
||||
v-for="item in authTypeList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="position"
|
||||
:label="$t('plugin.position')"
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
>
|
||||
<ElRadioGroup v-model="entity.position">
|
||||
<ElRadio value="headers">headers</ElRadio>
|
||||
<ElRadio value="query">query</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="tokenKey"
|
||||
:label="$t('plugin.tokenKey')"
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenKey" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="tokenValue"
|
||||
:label="$t('plugin.tokenValue')"
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenValue" />
|
||||
</ElFormItem>
|
||||
</ElFormItem>
|
||||
<ElAlert
|
||||
v-if="!entity.available && entity.reasonMessage"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="$t('plugin.workflowPluginUnavailable')"
|
||||
:description="entity.reasonMessage"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElFormItem prop="baseUrl" :label="$t('plugin.baseUrl')">
|
||||
<ElInput v-model.trim="entity.baseUrl" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="Headers" label="Headers">
|
||||
<div
|
||||
v-for="(item, index) in tempAddHeaders"
|
||||
:key="index"
|
||||
class="headers-container-reduce flex flex-row gap-4"
|
||||
>
|
||||
<div class="head-con-content flex flex-row gap-4">
|
||||
<ElInput v-model.trim="item.label" placeholder="header name" />
|
||||
<ElInput v-model.trim="item.value" placeholder="header value" />
|
||||
<ElIcon
|
||||
size="20"
|
||||
style="cursor: pointer"
|
||||
@click="removeHeader(index)"
|
||||
>
|
||||
<Remove />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
<ElButton class="addHeadersBtn" @click="addHeader">
|
||||
<ElIcon size="18" style="margin-right: 4px">
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
{{ $t('button.add') }}headers
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="authType" :label="$t('plugin.authType')">
|
||||
<ElSelect v-model="entity.authType">
|
||||
<ElOption
|
||||
v-for="item in authTypeList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
prop="position"
|
||||
:label="$t('plugin.position')"
|
||||
>
|
||||
<ElRadioGroup v-model="entity.position">
|
||||
<ElRadio value="headers">headers</ElRadio>
|
||||
<ElRadio value="query">query</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
prop="tokenKey"
|
||||
:label="$t('plugin.tokenKey')"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenKey" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
prop="tokenValue"
|
||||
:label="$t('plugin.tokenValue')"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenValue" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
</template>
|
||||
@@ -346,4 +550,11 @@ function removeHeader(index: number) {
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-helper-text {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user