This commit is contained in:
kk
2026-07-08 18:00:33 +08:00
commit e66b4a3680
312 changed files with 54718 additions and 0 deletions

View File

@@ -0,0 +1,831 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
ref="crud"
v-model="form"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="userInfo.authority.includes('admin')"
@click="handleDelete"
>
</el-button>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
<template #apiKey="{ row }">
<el-tag>{{ row.apiKey }}</el-tag>
</template>
<template #expireTime="{ row }">
<el-tag>{{ row.expireTimeInfo }}</el-tag>
</template>
<template #menu="{ row }">
<el-button
type="primary"
text
icon="el-icon-circle-check"
v-if="row.status === -1 && userInfo.authority.includes('admin')"
@click.stop="handleEnable(row)"
>启用
</el-button>
<el-button
type="primary"
text
icon="el-icon-circle-close"
v-if="row.status === 1 && userInfo.authority.includes('admin')"
@click.stop="handleDisable(row)"
>禁用
</el-button>
<el-button
type="primary"
text
icon="el-icon-notebook"
v-if="userInfo.authority.includes('admin')"
@click.stop="handleRowLog(row)"
>日志
</el-button>
</template>
<!-- 访问权限动态表单 -->
<template #apiPath-form="{ type }">
<el-row
v-for="(item, index) in apiPathList"
:key="index"
:gutter="8"
style="margin-bottom: 8px"
>
<el-col :span="22">
<el-input
v-model="apiPathList[index]"
placeholder="请输入URL路径"
:disabled="type === 'view'"
/>
</el-col>
<el-col :span="2" v-if="type !== 'view'">
<el-button type="danger" text icon="el-icon-delete" @click="removeApiPath(index)" />
</el-col>
</el-row>
<el-row v-if="type !== 'view'" :gutter="8">
<el-col :span="8">
<el-button type="primary" text icon="el-icon-circle-plus" @click="addApiPath"
>添加路径</el-button
>
</el-col>
</el-row>
<el-row style="margin-top: 8px">
<el-text type="info" size="small">
<el-icon><WarningFilled /></el-icon>
提示: 支持 Ant 风格路径匹配路径需以服务前缀开头 /** 匹配所有/blade-system/user/*
匹配单层/blade-system/user/** 匹配多层
</el-text>
</el-row>
</template>
<!-- 扩展参数动态表单 -->
<template #extParams-form="{ type }">
<el-row
v-for="(item, index) in extParamsList"
:key="index"
:gutter="8"
style="margin-bottom: 8px"
>
<el-col :span="8">
<el-input v-model="item.key" placeholder="参数名" :disabled="type === 'view'" />
</el-col>
<el-col :span="14">
<el-input v-model="item.value" placeholder="参数值" :disabled="type === 'view'" />
</el-col>
<el-col :span="2" v-if="type !== 'view'">
<el-button type="danger" text icon="el-icon-delete" @click="removeExtParam(index)" />
</el-col>
</el-row>
<el-row v-if="type !== 'view'" :gutter="8">
<el-col :span="8">
<el-button type="primary" text icon="el-icon-circle-plus" @click="addExtParam"
>添加参数</el-button
>
</el-col>
</el-row>
<el-row style="margin-top: 8px">
<el-text type="info" size="small">
<el-icon><WarningFilled /></el-icon>
提示: 可配置 deptIdpostIdroleId 等参数自定义 API Key 属性
</el-text>
</el-row>
</template>
</avue-crud>
<!-- 单个密钥调用日志 Drawer -->
<el-drawer
:title="`[${logKeyName}] 调用日志`"
v-model="logDrawerVisible"
append-to-body
size="1200px"
>
<basic-container>
<avue-crud
:option="logOption"
:table-loading="logLoading"
:data="logData"
v-model:page="logPage"
ref="logCrud"
v-model="logForm"
:before-open="beforeOpenLog"
@search-change="logSearchChange"
@search-reset="logSearchReset"
@current-change="logCurrentChange"
@size-change="logSizeChange"
@refresh-change="logRefreshChange"
@on-load="onLoadLog"
>
<template #requestUri="{ row }">
<el-tag>{{ row.requestUri }}</el-tag>
</template>
<template #method="{ row }">
<el-tag>{{ row.method }}</el-tag>
</template>
</avue-crud>
</basic-container>
</el-drawer>
<!-- API Key 创建成功弹框 -->
<el-dialog
v-model="apiKeyDialogVisible"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
width="620px"
align-center
title=""
>
<el-row justify="center">
<el-icon :size="48" color="#10b981"><CircleCheckFilled /></el-icon>
</el-row>
<el-row justify="center" style="margin-top: 16px">
<el-text size="large" tag="b">API Key 创建成功</el-text>
</el-row>
<el-row style="margin-top: 24px">
<el-divider content-position="left">
<el-text type="primary" tag="b">密钥信息</el-text>
</el-divider>
</el-row>
<el-row style="margin-top: 12px">
<el-card
shadow="never"
style="width: 100%; background: #f5f7fa"
:body-style="{ textAlign: 'center', padding: '12px' }"
>
<el-text tag="code" style="word-break: break-all">{{ createdApiKey }}</el-text>
</el-card>
</el-row>
<el-row justify="center" style="margin-top: 12px">
<el-space :size="6">
<el-icon color="#3b82f6" :size="16"><Clock /></el-icon>
<el-text size="small" v-if="createdExpireTime">过期时间{{ createdExpireTime }}</el-text>
<el-text size="small" v-else>过期时间永久有效</el-text>
</el-space>
</el-row>
<el-row justify="center" style="margin-top: 12px">
<el-space :size="6">
<el-icon color="#f59e0b" :size="16"><WarningFilled /></el-icon>
<el-text size="small" type="warning">请尽快复制保存关闭后将无法再次查看</el-text>
</el-space>
</el-row>
<el-row style="margin-top: 24px">
<el-divider content-position="left">
<el-text type="primary" tag="b">使用说明</el-text>
</el-divider>
</el-row>
<el-row style="margin-top: 12px">
<el-text type="info" size="small"> 在调用 API 需要在请求头中携带以下信息 </el-text>
</el-row>
<el-row style="margin-top: 12px">
<el-card
shadow="never"
style="width: 100%; background: #f5f7fa"
:body-style="{ padding: '12px' }"
>
<el-space direction="vertical" :size="8" style="width: 100%; align-items: flex-start">
<el-text size="small" style="word-break: break-all">
<el-text type="primary" tag="b">Blade-Auth:</el-text> {{ createdApiKey }}
</el-text>
<el-text size="small">
<el-text type="primary" tag="b">Blade-Requested-With:</el-text> BladeHttpRequest
</el-text>
</el-space>
</el-card>
</el-row>
<el-row style="margin-top: 16px">
<el-text type="info" size="small">cURL 调用示例</el-text>
</el-row>
<el-row style="margin-top: 8px">
<el-card
shadow="never"
style="width: 100%; background: #1e1e1e"
:body-style="{ padding: '12px' }"
>
<el-text
type="success"
size="small"
tag="code"
style="
color: #a6e3a1;
word-break: break-all;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
line-height: 1.6;
"
>{{ curlCommand }}</el-text
>
</el-card>
</el-row>
<template #footer>
<el-row justify="center" :gutter="12">
<el-col :span="12" style="text-align: right; padding-right: 6px">
<el-button type="primary" size="large" @click="copyAndCloseDialog" style="width: 100%">
<el-space>
<el-icon><DocumentCopy /></el-icon>
<span>复制 API Key</span>
</el-space>
</el-button>
</el-col>
<el-col :span="12" style="text-align: left; padding-left: 6px">
<el-button size="large" @click="copyCurlCommand" style="width: 100%">
<el-space>
<el-icon><DocumentCopy /></el-icon>
<span>复制 cURL 命令</span>
</el-space>
</el-button>
</el-col>
</el-row>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { getList, getDetail, remove, add, update, enable, disable } from '@/api/system/apikey';
import { getList as getLogList, getDetail as getLogDetail } from '@/api/system/apikeylog';
import { getUserPage, getUser } from '@/api/system/user';
import { mapGetters } from 'vuex';
import {
CircleCheckFilled,
Clock,
WarningFilled,
DocumentCopy,
Delete,
Plus,
Notebook,
} from '@element-plus/icons-vue';
export default {
components: {
CircleCheckFilled,
Clock,
WarningFilled,
DocumentCopy,
Delete,
Plus,
Notebook,
},
data() {
return {
form: {},
query: {},
loading: true,
apiPathList: [],
extParamsList: [],
apiKeyDialogVisible: false,
createdApiKey: '',
createdExpireTime: '',
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 700,
menuWidth: 350,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
column: [
{
label: 'API Key',
prop: 'apiKey',
slot: true,
addDisplay: false,
editDisplay: false,
span: 24,
width: 280,
},
{
label: '密钥名称',
prop: 'name',
search: true,
editDisabled: true,
span: 24,
rules: [
{
required: true,
message: '请输入密钥名称',
trigger: 'blur',
},
],
},
{
label: '绑定用户',
prop: 'userId',
type: 'table',
editDisabled: true,
props: {
label: 'name',
value: 'id',
},
children: {
border: true,
column: [
{
label: '账号',
prop: 'account',
search: true,
},
{
label: '姓名',
prop: 'name',
search: true,
},
{
label: '角色',
prop: 'roleName',
},
{
label: '部门',
prop: 'deptName',
},
{
label: '岗位',
prop: 'postName',
},
],
},
formatter(row) {
if (!row.name) return '';
return row.name + '' + row.account + '';
},
onLoad: ({ page, value, data }, callback) => {
if (value) {
getUser(value).then(res => {
callback(res.data.data);
});
return;
}
// 首次打开弹窗时设置默认每页5条
if (page && (!page.pageSizes || !page.pageSizes.includes(5))) {
page.pageSize = 5;
page.pageSizes = [5, 10, 20];
}
let currentPage = page ? page.currentPage : 1;
let pageSize = page ? page.pageSize : 5;
let param = {};
if (data) {
param = data;
}
getUserPage(currentPage, pageSize, param.account, param.name).then(res => {
callback({
total: res.data.data.total,
data: res.data.data.records,
});
});
},
span: 24,
rules: [
{
required: true,
message: '请选择用户',
trigger: 'change',
},
],
},
{
label: '过期时间',
prop: 'expireTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
span: 24,
width: 180,
slot: true,
},
{
label: '状态',
prop: 'status',
type: 'select',
slot: true,
search: true,
dicData: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
value: 1,
span: 24,
display: false,
width: 80,
},
{
label: '访问权限',
labelTip: '配置允许访问的 API 路径,留空则表示不限制访问路径',
prop: 'apiPath',
formslot: true,
hide: true,
span: 24,
},
{
label: '扩展参数',
labelTip: '配置扩展参数,可用于自定义 API Key 属性',
prop: 'extParams',
formslot: true,
hide: true,
span: 24,
},
],
},
data: [],
// 单个密钥日志
logDrawerVisible: false,
logKeyName: '',
logApiKeyId: null,
logForm: {},
logQuery: {},
logLoading: false,
logData: [],
logPage: { pageSize: 10, currentPage: 1, total: 0 },
logOption: {
height: 'auto',
calcHeight: 70,
tip: false,
header: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 100,
dialogType: 'drawer',
dialogWidth: 800,
column: [
{ label: '服务ID', prop: 'serviceId', width: 120 },
{ label: '服务host', prop: 'serverHost', hide: true },
{ label: '服务IP', prop: 'serverIp', hide: true, width: 160 },
{ label: '软件环境', prop: 'env', hide: true, width: 85 },
{ label: '请求接口', prop: 'requestUri', search: true, slot: true },
{ label: '请求方法', prop: 'method', slot: true, width: 85 },
{ label: '调用IP', prop: 'remoteIp', search: true, width: 140 },
{ label: '耗时(ms)', prop: 'time', width: 85 },
{ label: '日志时间', prop: 'createTime', span: 24, width: 180 },
{ label: '用户代理', prop: 'userAgent', span: 24, hide: true },
{ label: '请求数据', prop: 'params', type: 'textarea', span: 24, minRows: 2, hide: true },
],
},
};
},
computed: {
...mapGetters(['userInfo']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
curlCommand() {
if (!this.createdApiKey) return '';
return `curl -X GET 'https://api.bladex.cn/blade-system/user/info' \\
-H 'Blade-Auth: ${this.createdApiKey}' \\
-H 'Blade-Requested-With: BladeHttpRequest'`;
},
},
methods: {
copyAndCloseDialog() {
navigator.clipboard
.writeText(this.createdApiKey)
.then(() => {
this.$message({
type: 'success',
message: 'API Key 已复制到剪贴板',
});
this.apiKeyDialogVisible = false;
this.createdApiKey = '';
this.createdExpireTime = '';
})
.catch(() => {
this.$message({
type: 'error',
message: '复制失败,请手动复制',
});
});
},
copyCurlCommand() {
navigator.clipboard
.writeText(this.curlCommand)
.then(() => {
this.$message({
type: 'success',
message: 'cURL 命令已复制到剪贴板',
});
})
.catch(() => {
this.$message({
type: 'error',
message: '复制失败,请手动复制',
});
});
},
showApiKeyDialog(apiKey, expireTime) {
this.createdApiKey = apiKey;
this.createdExpireTime = expireTime || '';
this.apiKeyDialogVisible = true;
},
addApiPath() {
this.apiPathList.push('');
},
removeApiPath(index) {
this.apiPathList.splice(index, 1);
},
parseApiPath(apiPath) {
if (!apiPath) return [];
return apiPath
.split(',')
.map(path => path.trim())
.filter(path => path.length > 0);
},
buildApiPath() {
const paths = this.apiPathList.filter(path => path && path.trim());
return paths.length > 0 ? paths.join(',') : '';
},
addExtParam() {
this.extParamsList.push({ key: '', value: '' });
},
removeExtParam(index) {
this.extParamsList.splice(index, 1);
},
parseExtParams(extParams) {
if (!extParams) return [];
try {
const obj = typeof extParams === 'string' ? JSON.parse(extParams) : extParams;
return Object.entries(obj).map(([key, value]) => ({ key, value: String(value) }));
} catch (e) {
return [];
}
},
buildExtParams() {
const result = {};
this.extParamsList.forEach(item => {
if (item.key && item.key.trim()) {
result[item.key.trim()] = item.value;
}
});
return Object.keys(result).length > 0 ? JSON.stringify(result) : '';
},
rowSave(row, done, loading) {
const data = {
...row,
apiPath: this.buildApiPath(),
extParams: this.buildExtParams(),
};
add(data).then(
res => {
this.onLoad(this.page);
done();
// 显示 API Key 弹框
this.showApiKeyDialog(res.data.data, row.expireTime);
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
const data = {
...row,
apiPath: this.buildApiPath(),
extParams: this.buildExtParams(),
};
update(data).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleEnable(row) {
this.$confirm('确定启用该 API Key?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return enable(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '启用成功!',
});
});
},
handleDisable(row) {
this.$confirm('确定禁用该 API Key?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return disable(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '禁用成功!',
});
});
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
if (['add'].includes(type)) {
this.apiPathList = [];
this.extParamsList = [];
}
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
this.apiPathList = this.parseApiPath(this.form.apiPath);
this.extParamsList = this.parseExtParams(this.form.extParams);
});
}
done();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
// ==================== 单个密钥日志 ====================
handleRowLog(row) {
this.logKeyName = row.name || 'API Key';
this.logApiKeyId = row.id;
this.logDrawerVisible = true;
this.logPage.currentPage = 1;
this.logQuery = {};
this.onLoadLog(this.logPage);
},
beforeOpenLog(done, type) {
if (['view'].includes(type)) {
getLogDetail(this.logForm.id).then(res => {
this.logForm = res.data.data;
});
}
done();
},
logSearchChange(params, done) {
this.logQuery = params;
this.logPage.currentPage = 1;
this.onLoadLog(this.logPage, params);
done();
},
logSearchReset() {
this.logQuery = {};
this.onLoadLog(this.logPage);
},
logCurrentChange(currentPage) {
this.logPage.currentPage = currentPage;
},
logSizeChange(pageSize) {
this.logPage.pageSize = pageSize;
},
logRefreshChange() {
this.onLoadLog(this.logPage, this.logQuery);
},
onLoadLog(page, params = {}) {
this.logLoading = true;
const query = Object.assign({ apiKeyId: this.logApiKeyId }, params, this.logQuery);
getLogList(page.currentPage, page.pageSize, query).then(res => {
const data = res.data.data;
this.logPage.total = data.total;
this.logData = data.records;
this.logLoading = false;
});
},
},
};
</script>

View File

@@ -0,0 +1,733 @@
<template>
<basic-container>
<!-- ========== 列表模式 ========== -->
<avue-crud
v-if="viewMode === 'list'"
ref="crudList"
v-model="formList"
:option="optionList"
:table-loading="listLoading"
:data="dataList"
:page="pageList"
:permission="permissionListScope"
:before-open="beforeOpenList"
@row-del="rowDelList"
@row-update="rowUpdateList"
@row-save="rowSaveList"
@search-change="searchChangeList"
@search-reset="searchResetList"
@selection-change="selectionChangeList"
@current-change="currentChangeList"
@size-change="sizeChangeList"
@refresh-change="refreshChangeList"
@on-load="onLoadList"
>
<template #menu-left>
<el-button type="danger" icon="el-icon-delete" plain @click="handleDeleteList"
>
</el-button>
</template>
<template #menu-right>
<el-button :type="viewMode === 'list' ? 'primary' : ''" @click="viewMode = 'list'"
>列表模式
</el-button>
<el-button :type="viewMode === 'tree' ? 'primary' : ''" @click="viewMode = 'tree'"
>树形模式
</el-button>
</template>
<template #scopeType="{ row }">
<el-tag>{{ row.scopeTypeName }}</el-tag>
</template>
</avue-crud>
<!-- ========== 树形模式 ========== -->
<avue-crud
v-if="viewMode === 'tree'"
ref="crudTree"
:option="optionTree"
:table-loading="treeLoading"
:data="dataTree"
@search-change="searchChangeTree"
@search-reset="searchResetTree"
@refresh-change="refreshChangeTree"
@on-load="onLoadTree"
@tree-load="treeLoad"
>
<template #menu-right>
<el-button :type="viewMode === 'list' ? 'primary' : ''" @click="viewMode = 'list'"
>列表模式
</el-button>
<el-button :type="viewMode === 'tree' ? 'primary' : ''" @click="viewMode = 'tree'"
>树形模式
</el-button>
</template>
<template #menu="{ row }">
<el-button
type="primary"
text
icon="el-icon-setting"
v-if="permission.api_scope_setting"
plain
style="border: 0; background-color: transparent !important"
@click.stop="handleDataScope(row)"
>权限配置
</el-button>
</template>
<template #name="{ row }">
<i :class="row.source" style="margin-right: 5px" />
<span>{{ row.name }}</span>
</template>
<template #source="{ row }">
<div style="text-align: center">
<i :class="row.source" />
</div>
</template>
</avue-crud>
<!-- ========== 接口权限配置抽屉树形模式 ========== -->
<el-drawer
:title="`[${scopeMenuName}] 接口权限配置`"
v-model="drawerVisible"
:direction="direction"
append-to-body
:before-close="handleDrawerClose"
size="1200px"
>
<basic-container>
<avue-crud
:option="optionScope"
:data="dataScope"
:page="pageScope"
v-model="formScope"
:table-loading="scopeLoading"
ref="crudScope"
@row-del="rowDelScope"
@row-update="rowUpdateScope"
@row-save="rowSaveScope"
:before-open="beforeOpenScope"
@search-change="searchChangeScope"
@search-reset="searchResetScope"
@selection-change="selectionChangeScope"
@current-change="currentChangeScope"
@size-change="sizeChangeScope"
@on-load="onLoadScope"
>
<template #menu-left>
<el-button type="danger" icon="el-icon-delete" plain @click="handleDeleteScope"
>
</el-button>
</template>
<template #scopeType="{ row }">
<el-tag>{{ row.scopeTypeName }}</el-tag>
</template>
</avue-crud>
</basic-container>
</el-drawer>
</basic-container>
</template>
<script>
import { getLazyMenuList } from '@/api/system/menu';
import {
addApiScope,
removeApiScope,
updateApiScope,
getListApiScope,
getMenuApiScope,
} from '@/api/system/scope';
import { mapGetters } from 'vuex';
export default {
data() {
return {
viewMode: 'list',
// ========== 列表模式 ==========
formList: {},
queryList: {},
selectionListData: [],
listLoading: true,
dataList: [],
pageList: {
pageSize: 10,
currentPage: 1,
total: 0,
},
optionList: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
labelWidth: 120,
searchLabelWidth: 120,
menuWidth: 250,
dialogWidth: 900,
dialogClickModal: false,
column: [
{
label: '所属菜单',
labelTip: '仅用于权限规则的分组归类,不参与实际鉴权与数据过滤逻辑',
prop: 'menuId',
type: 'tree',
dicUrl: '/blade-system/menu/tree',
props: {
label: 'title',
},
search: true,
span: 24,
width: 200,
rules: [
{
required: true,
message: '请选择所属菜单',
trigger: 'click',
},
],
},
{
label: '权限名称',
prop: 'scopeName',
search: true,
rules: [
{
required: true,
message: '请输入权限名称',
trigger: 'blur',
},
],
},
{
label: '权限编号',
prop: 'resourceCode',
search: true,
width: 180,
rules: [
{
required: true,
message: '请输入权限编号',
trigger: 'blur',
},
],
},
{
label: '权限路径',
prop: 'scopePath',
width: 180,
rules: [
{
required: true,
message: '请输入权限路径',
trigger: 'blur',
},
],
},
{
label: '接口类型',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=api_scope_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
width: 100,
prop: 'scopeType',
rules: [
{
required: true,
message: '请选择接口类型',
trigger: 'blur',
},
],
},
{
label: '备注',
prop: 'remark',
span: 24,
hide: true,
},
],
},
// ========== 树形模式 ==========
dataTree: [],
treeLoading: true,
parentId: 0,
queryTree: {},
optionTree: {
lazy: true,
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
tree: true,
border: true,
index: true,
selection: true,
viewBtn: false,
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 200,
dialogClickModal: false,
column: [
{
label: '菜单名称',
prop: 'name',
slot: true,
width: 300,
search: true,
},
{
label: '菜单编号',
prop: 'code',
search: true,
},
{
label: '路由地址',
prop: 'path',
},
{
label: '菜单图标',
prop: 'source',
slot: true,
width: 85,
},
{
label: '菜单别名',
prop: 'alias',
},
{
label: '菜单排序',
prop: 'sort',
width: 85,
},
],
},
// ========== 接口权限抽屉(树形模式) ==========
drawerVisible: false,
direction: 'rtl',
scopeMenuId: 0,
scopeMenuName: '菜单',
scopeLoading: false,
formScope: {},
selectionListScope: [],
pageScope: {
pageSize: 10,
currentPage: 1,
total: 0,
},
optionScope: {
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
labelWidth: 120,
menuWidth: 250,
dialogWidth: 900,
dialogClickModal: false,
column: [
{
label: '所属菜单',
labelTip: '仅用于权限规则的分组归类,不参与实际鉴权与数据过滤逻辑',
prop: 'menuId',
type: 'tree',
dicUrl: '/blade-system/menu/tree',
props: {
label: 'title',
},
span: 24,
disabled: true,
hide: true,
rules: [
{
required: true,
message: '请选择所属菜单',
trigger: 'click',
},
],
},
{
label: '权限名称',
prop: 'scopeName',
search: true,
rules: [
{
required: true,
message: '请输入权限名称',
trigger: 'blur',
},
],
},
{
label: '权限编号',
prop: 'resourceCode',
search: true,
width: 180,
rules: [
{
required: true,
message: '请输入权限编号',
trigger: 'blur',
},
],
},
{
label: '权限路径',
prop: 'scopePath',
width: 180,
rules: [
{
required: true,
message: '请输入权限路径',
trigger: 'blur',
},
],
},
{
label: '接口类型',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=api_scope_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
width: 100,
prop: 'scopeType',
rules: [
{
required: true,
message: '请选择接口类型',
trigger: 'blur',
},
],
},
{
label: '备注',
prop: 'remark',
span: 24,
hide: true,
},
],
},
dataScope: [],
};
},
watch: {
viewMode() {
this.drawerVisible = false;
},
},
computed: {
...mapGetters(['permission']),
permissionListScope() {
return {
addBtn: this.validData(this.permission.api_scope_setting, false),
viewBtn: true,
delBtn: this.validData(this.permission.api_scope_setting, false),
editBtn: this.validData(this.permission.api_scope_setting, false),
};
},
listIds() {
let ids = [];
this.selectionListData.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
scopeIds() {
let ids = [];
this.selectionListScope.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
// ==================== 列表模式 ====================
rowSaveList(row, done, loading) {
addApiScope(row).then(
() => {
this.onLoadList(this.pageList);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateList(row, index, done, loading) {
updateApiScope(row).then(
() => {
this.onLoadList(this.pageList);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelList(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return removeApiScope(row.id);
})
.then(() => {
this.onLoadList(this.pageList);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleDeleteList() {
if (this.selectionListData.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return removeApiScope(this.listIds);
})
.then(() => {
this.onLoadList(this.pageList);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudList.toggleSelection();
});
},
beforeOpenList(done, type) {
if (['edit', 'view'].includes(type)) {
getMenuApiScope(this.formList.id).then(res => {
this.formList = res.data.data;
});
}
done();
},
searchResetList() {
this.queryList = {};
this.pageList.currentPage = 1;
this.onLoadList(this.pageList);
},
searchChangeList(params, done) {
this.queryList = params;
this.pageList.currentPage = 1;
this.onLoadList(this.pageList, params);
done();
},
selectionChangeList(list) {
this.selectionListData = list;
},
currentChangeList(currentPage) {
this.pageList.currentPage = currentPage;
},
sizeChangeList(pageSize) {
this.pageList.pageSize = pageSize;
},
refreshChangeList() {
this.onLoadList(this.pageList, this.queryList);
},
onLoadList(page, params = {}) {
this.listLoading = true;
getListApiScope(page.currentPage, page.pageSize, Object.assign(params, this.queryList)).then(
res => {
const data = res.data.data;
this.pageList.total = data.total;
this.dataList = data.records;
this.selectionListData = [];
this.listLoading = false;
}
);
},
// ==================== 树形模式 ====================
onLoadTree(page, params = {}) {
this.treeLoading = true;
getLazyMenuList(this.parentId, Object.assign(params, this.queryTree)).then(res => {
this.dataTree = res.data.data;
this.treeLoading = false;
});
},
treeLoad(tree, treeNode, resolve) {
const parentId = tree.id;
getLazyMenuList(parentId).then(res => {
resolve(res.data.data);
});
},
searchResetTree() {
this.queryTree = {};
this.parentId = 0;
this.onLoadTree();
},
searchChangeTree(params, done) {
this.queryTree = params;
this.parentId = '';
this.onLoadTree({}, params);
done();
},
refreshChangeTree() {
this.onLoadTree({}, this.queryTree);
},
handleDataScope(row) {
this.drawerVisible = true;
this.scopeMenuId = row.id;
this.scopeMenuName = row.name;
this.onLoadScope(this.pageScope);
},
handleDrawerClose(hide) {
hide();
},
// ==================== 抽屉内权限 CRUD ====================
rowSaveScope(row, done, loading) {
row = {
...row,
menuId: this.scopeMenuId,
};
addApiScope(row).then(
() => {
this.onLoadScope(this.pageScope);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateScope(row, index, done, loading) {
row = {
...row,
menuId: this.scopeMenuId,
};
updateApiScope(row).then(
() => {
this.onLoadScope(this.pageScope);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelScope(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return removeApiScope(row.id);
})
.then(() => {
this.onLoadScope(this.pageScope);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleDeleteScope() {
if (this.selectionListScope.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return removeApiScope(this.scopeIds);
})
.then(() => {
this.onLoadScope(this.pageScope);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudScope.toggleSelection();
});
},
beforeOpenScope(done, type) {
if (['add'].includes(type)) {
this.formScope.menuId = this.scopeMenuId;
}
if (['edit', 'view'].includes(type)) {
getMenuApiScope(this.formScope.id).then(res => {
this.formScope = res.data.data;
});
}
done();
},
searchResetScope() {
this.onLoadScope(this.pageScope);
},
searchChangeScope(params, done) {
this.onLoadScope(this.pageScope, params);
done();
},
selectionChangeScope(list) {
this.selectionListScope = list;
},
currentChangeScope(currentPage) {
this.pageScope.currentPage = currentPage;
},
sizeChangeScope(pageSize) {
this.pageScope.pageSize = pageSize;
},
onLoadScope(page, params = {}) {
this.scopeLoading = true;
const values = {
...params,
menuId: this.scopeMenuId,
};
getListApiScope(page.currentPage, page.pageSize, values).then(res => {
const data = res.data.data;
this.pageScope.total = data.total;
this.dataScope = data.records;
this.selectionListScope = [];
this.scopeLoading = false;
});
},
},
};
</script>

View File

@@ -0,0 +1,868 @@
<template>
<basic-container>
<!-- ========== 列表模式 ========== -->
<avue-crud
v-if="viewMode === 'list'"
ref="crudList"
v-model="formList"
:option="optionList"
:table-loading="listLoading"
:data="dataList"
:page="pageList"
:permission="permissionListScope"
:before-open="beforeOpenList"
@row-del="rowDelList"
@row-update="rowUpdateList"
@row-save="rowSaveList"
@search-change="searchChangeList"
@search-reset="searchResetList"
@selection-change="selectionChangeList"
@current-change="currentChangeList"
@size-change="sizeChangeList"
@refresh-change="refreshChangeList"
@on-load="onLoadList"
>
<template #menu-left>
<el-button type="danger" icon="el-icon-delete" plain @click="handleDeleteList"
>
</el-button>
</template>
<template #menu-right>
<el-button :type="viewMode === 'list' ? 'primary' : ''" @click="viewMode = 'list'"
>列表模式
</el-button>
<el-button :type="viewMode === 'tree' ? 'primary' : ''" @click="viewMode = 'tree'"
>树形模式
</el-button>
</template>
<template #scopeType="{ row }">
<el-tag>{{ row.scopeTypeName }}</el-tag>
</template>
</avue-crud>
<!-- ========== 树形模式 ========== -->
<avue-crud
v-if="viewMode === 'tree'"
ref="crudTree"
:option="optionTree"
:table-loading="treeLoading"
:data="dataTree"
@search-change="searchChangeTree"
@search-reset="searchResetTree"
@refresh-change="refreshChangeTree"
@on-load="onLoadTree"
@tree-load="treeLoad"
>
<template #menu-right>
<el-button :type="viewMode === 'list' ? 'primary' : ''" @click="viewMode = 'list'"
>列表模式
</el-button>
<el-button :type="viewMode === 'tree' ? 'primary' : ''" @click="viewMode = 'tree'"
>树形模式
</el-button>
</template>
<template #menu="{ row }">
<el-button
type="primary"
text
icon="el-icon-setting"
v-if="permission.data_scope_setting"
plain
style="border: 0; background-color: transparent !important"
@click.stop="handleDataScope(row)"
>权限配置
</el-button>
</template>
<template #name="{ row }">
<i :class="row.source" style="margin-right: 5px" />
<span>{{ row.name }}</span>
</template>
<template #source="{ row }">
<div style="text-align: center">
<i :class="row.source" />
</div>
</template>
</avue-crud>
<!-- ========== 数据权限配置抽屉树形模式 ========== -->
<el-drawer
:title="`[${scopeMenuName}] 数据权限配置`"
v-model="drawerVisible"
:direction="direction"
append-to-body
:before-close="handleDrawerClose"
size="1200px"
>
<basic-container>
<avue-crud
:option="optionScope"
:data="dataScope"
:page="pageScope"
v-model="formScope"
:table-loading="scopeLoading"
ref="crudScope"
@row-del="rowDelScope"
@row-update="rowUpdateScope"
@row-save="rowSaveScope"
:before-open="beforeOpenScope"
@search-change="searchChangeScope"
@search-reset="searchResetScope"
@selection-change="selectionChangeScope"
@current-change="currentChangeScope"
@size-change="sizeChangeScope"
@on-load="onLoadScope"
>
<template #menu-left>
<el-button type="danger" icon="el-icon-delete" plain @click="handleDeleteScope"
>
</el-button>
</template>
<template #scopeType="{ row }">
<el-tag>{{ row.scopeTypeName }}</el-tag>
</template>
</avue-crud>
</basic-container>
</el-drawer>
</basic-container>
</template>
<script>
import { getLazyMenuList } from '@/api/system/menu';
import {
addDataScope,
removeDataScope,
updateDataScope,
getListDataScope,
getMenuDataScope,
} from '@/api/system/scope';
import { mapGetters } from 'vuex';
import func from '@/utils/func';
export default {
data() {
return {
viewMode: 'list',
// ========== 列表模式 ==========
formList: {},
queryList: {},
selectionListData: [],
listLoading: true,
dataList: [],
pageList: {
pageSize: 10,
currentPage: 1,
total: 0,
},
optionList: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
labelWidth: 120,
searchLabelWidth: 120,
menuWidth: 250,
dialogWidth: 900,
dialogClickModal: false,
column: [
{
label: '所属菜单',
labelTip: '仅用于权限规则的分组归类,不参与实际鉴权与数据过滤逻辑',
prop: 'menuId',
type: 'tree',
dicUrl: '/blade-system/menu/tree',
props: {
label: 'title',
},
search: true,
span: 24,
width: 200,
rules: [
{
required: true,
message: '请选择所属菜单',
trigger: 'click',
},
],
},
{
label: '权限名称',
prop: 'scopeName',
search: true,
rules: [
{
required: true,
message: '请输入数据权限名称',
trigger: 'blur',
},
],
},
{
label: '权限编号',
prop: 'resourceCode',
search: true,
width: 120,
rules: [
{
required: true,
message: '请输入数据权限编号',
trigger: 'blur',
},
],
},
{
label: '权限字段',
prop: 'scopeColumn',
width: 130,
rules: [
{
required: true,
message: '请输入权限字段',
trigger: 'blur',
},
],
},
{
label: '规则类型',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=data_scope_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
width: 140,
prop: 'scopeType',
rules: [
{
required: true,
message: '请选择规则类型',
trigger: 'blur',
},
],
},
{
label: '可见字段',
prop: 'scopeField',
span: 24,
hide: true,
value: '*',
rules: [
{
required: true,
message: '请输入数据权限可见的字段',
trigger: 'blur',
},
],
},
{
label: '权限类名',
prop: 'scopeClass',
span: 24,
hide: true,
rules: [
{
required: true,
message: '请输入MybatisMapper对应方法的完整类名路径',
trigger: 'blur',
},
],
},
{
label: '规则值',
prop: 'scopeValue',
span: 24,
minRows: 5,
type: 'textarea',
display: false,
hide: true,
},
{
label: '备注',
prop: 'remark',
span: 24,
hide: true,
},
],
},
// ========== 树形模式 ==========
dataTree: [],
treeLoading: true,
parentId: 0,
queryTree: {},
optionTree: {
lazy: true,
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
tree: true,
border: true,
index: true,
selection: true,
viewBtn: false,
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 200,
dialogClickModal: false,
column: [
{
label: '菜单名称',
prop: 'name',
slot: true,
width: 300,
search: true,
},
{
label: '菜单编号',
prop: 'code',
search: true,
},
{
label: '路由地址',
prop: 'path',
},
{
label: '菜单图标',
prop: 'source',
slot: true,
width: 85,
},
{
label: '菜单别名',
prop: 'alias',
},
{
label: '菜单排序',
prop: 'sort',
width: 85,
},
],
},
// ========== 数据权限抽屉(树形模式) ==========
drawerVisible: false,
direction: 'rtl',
scopeMenuId: 0,
scopeMenuCode: '',
scopeMenuName: '菜单',
scopeLoading: false,
watchMode: true,
formScope: {},
selectionListScope: [],
pageScope: {
pageSize: 10,
currentPage: 1,
total: 0,
},
optionScope: {
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
labelWidth: 120,
menuWidth: 250,
dialogWidth: 900,
dialogClickModal: false,
column: [
{
label: '所属菜单',
labelTip: '仅用于权限规则的分组归类,不参与实际鉴权与数据过滤逻辑',
prop: 'menuId',
type: 'tree',
dicUrl: '/blade-system/menu/tree',
props: {
label: 'title',
},
span: 24,
disabled: true,
hide: true,
rules: [
{
required: true,
message: '请选择所属菜单',
trigger: 'click',
},
],
},
{
label: '权限名称',
prop: 'scopeName',
search: true,
value: '',
rules: [
{
required: true,
message: '请输入数据权限名称',
trigger: 'blur',
},
],
},
{
label: '权限编号',
prop: 'resourceCode',
search: true,
width: 100,
rules: [
{
required: true,
message: '请输入数据权限编号',
trigger: 'blur',
},
],
},
{
label: '权限字段',
prop: 'scopeColumn',
width: 130,
rules: [
{
required: true,
message: '请输入数据权限编号',
trigger: 'blur',
},
],
},
{
label: '规则类型',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=data_scope_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
width: 140,
prop: 'scopeType',
rules: [
{
required: true,
message: '请输入通知类型',
trigger: 'blur',
},
],
},
{
label: '可见字段',
prop: 'scopeField',
span: 24,
hide: true,
value: '*',
rules: [
{
required: true,
message: '请输入数据权限可见的字段',
trigger: 'blur',
},
],
},
{
label: '权限类名',
prop: 'scopeClass',
span: 24,
hide: true,
rules: [
{
required: true,
message: '请输入MybatisMapper对应方法的完整类名路径',
trigger: 'blur',
},
],
},
{
label: '规则值',
prop: 'scopeValue',
span: 24,
minRows: 5,
type: 'textarea',
display: true,
hide: true,
},
{
label: '备注',
prop: 'remark',
span: 24,
hide: true,
},
],
},
dataScope: [],
};
},
watch: {
'formList.scopeType'() {
this.initListScope();
},
'formScope.scopeType'() {
this.initScope();
},
viewMode() {
this.drawerVisible = false;
},
},
computed: {
...mapGetters(['permission']),
permissionListScope() {
return {
addBtn: this.validData(this.permission.data_scope_setting, false),
viewBtn: true,
delBtn: this.validData(this.permission.data_scope_setting, false),
editBtn: this.validData(this.permission.data_scope_setting, false),
};
},
listIds() {
let ids = [];
this.selectionListData.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
scopeIds() {
let ids = [];
this.selectionListScope.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
// ==================== 列表模式 ====================
initListScope() {
const scopeType = func.toInt(this.formList.scopeType);
const column = this.$refs.crudList?.option?.column;
if (column) {
column.forEach(item => {
if (item.prop === 'scopeValue') {
item.display = scopeType === 5;
}
});
}
},
rowSaveList(row, done, loading) {
addDataScope(row).then(
() => {
this.onLoadList(this.pageList);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateList(row, index, done, loading) {
updateDataScope(row).then(
() => {
this.onLoadList(this.pageList);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelList(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return removeDataScope(row.id);
})
.then(() => {
this.onLoadList(this.pageList);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleDeleteList() {
if (this.selectionListData.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return removeDataScope(this.listIds);
})
.then(() => {
this.onLoadList(this.pageList);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudList.toggleSelection();
});
},
beforeOpenList(done, type) {
if (['edit', 'view'].includes(type)) {
getMenuDataScope(this.formList.id).then(res => {
this.formList = res.data.data;
});
}
done();
},
searchResetList() {
this.queryList = {};
this.pageList.currentPage = 1;
this.onLoadList(this.pageList);
},
searchChangeList(params, done) {
this.queryList = params;
this.pageList.currentPage = 1;
this.onLoadList(this.pageList, params);
done();
},
selectionChangeList(list) {
this.selectionListData = list;
},
currentChangeList(currentPage) {
this.pageList.currentPage = currentPage;
},
sizeChangeList(pageSize) {
this.pageList.pageSize = pageSize;
},
refreshChangeList() {
this.onLoadList(this.pageList, this.queryList);
},
onLoadList(page, params = {}) {
this.listLoading = true;
getListDataScope(page.currentPage, page.pageSize, Object.assign(params, this.queryList)).then(
res => {
const data = res.data.data;
this.pageList.total = data.total;
this.dataList = data.records;
this.selectionListData = [];
this.listLoading = false;
}
);
},
// ==================== 树形模式 ====================
onLoadTree(page, params = {}) {
this.treeLoading = true;
getLazyMenuList(this.parentId, Object.assign(params, this.queryTree)).then(res => {
this.dataTree = res.data.data;
this.treeLoading = false;
});
},
treeLoad(tree, treeNode, resolve) {
const parentId = tree.id;
getLazyMenuList(parentId).then(res => {
resolve(res.data.data);
});
},
searchResetTree() {
this.queryTree = {};
this.parentId = 0;
this.onLoadTree();
},
searchChangeTree(params, done) {
this.queryTree = params;
this.parentId = '';
this.onLoadTree({}, params);
done();
},
refreshChangeTree() {
this.onLoadTree({}, this.queryTree);
},
handleDataScope(row) {
this.drawerVisible = true;
this.scopeMenuId = row.id;
this.scopeMenuCode = row.code;
this.scopeMenuName = row.name;
this.onLoadScope(this.pageScope);
},
handleDrawerClose(hide) {
hide();
},
// ==================== 抽屉内权限 CRUD ====================
initScope() {
const scopeType = func.toInt(this.formScope.scopeType);
const watchMode = this.watchMode;
let column = '-',
name = '暂无';
if (scopeType === 1) {
column = '-';
name = '全部可见';
} else if (scopeType === 2) {
column = 'create_user';
name = '本人可见';
} else if (scopeType === 3) {
column = 'create_dept';
name = '所在机构可见';
} else if (scopeType === 4) {
column = 'create_dept';
name = '所在机构可见及子级可见';
} else if (scopeType === 5) {
column = '';
name = '自定义';
}
this.$refs.crudScope.option.column.filter(item => {
if (watchMode) {
if (item.prop === 'scopeName') {
this.formScope.scopeName = `${this.scopeMenuName} [${name}]`;
}
if (item.prop === 'resourceCode') {
this.formScope.resourceCode = this.scopeMenuCode;
}
if (item.prop === 'scopeColumn') {
this.formScope.scopeColumn = column;
}
}
if (item.prop === 'scopeValue') {
item.display = scopeType === 5;
}
});
},
rowSaveScope(row, done, loading) {
row = {
...row,
menuId: this.scopeMenuId,
};
addDataScope(row).then(
() => {
this.onLoadScope(this.pageScope);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateScope(row, index, done, loading) {
row = {
...row,
menuId: this.scopeMenuId,
};
updateDataScope(row).then(
() => {
this.onLoadScope(this.pageScope);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelScope(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return removeDataScope(row.id);
})
.then(() => {
this.onLoadScope(this.pageScope);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleDeleteScope() {
if (this.selectionListScope.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return removeDataScope(this.scopeIds);
})
.then(() => {
this.onLoadScope(this.pageScope);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudScope.toggleSelection();
});
},
beforeOpenScope(done, type) {
if (['add'].includes(type)) {
this.formScope.menuId = this.scopeMenuId;
this.watchMode = true;
this.initScope();
}
if (['edit', 'view'].includes(type)) {
this.watchMode = false;
getMenuDataScope(this.formScope.id).then(res => {
this.formScope = res.data.data;
});
}
done();
},
searchResetScope() {
this.onLoadScope(this.pageScope);
},
searchChangeScope(params, done) {
this.onLoadScope(this.pageScope, params);
done();
},
selectionChangeScope(list) {
this.selectionListScope = list;
},
currentChangeScope(currentPage) {
this.pageScope.currentPage = currentPage;
},
sizeChangeScope(pageSize) {
this.pageScope.pageSize = pageSize;
},
onLoadScope(page, params = {}) {
this.scopeLoading = true;
const values = {
...params,
menuId: this.scopeMenuId,
};
getListDataScope(page.currentPage, page.pageSize, values).then(res => {
const data = res.data.data;
this.pageScope.total = data.total;
this.dataScope = data.records;
this.selectionListScope = [];
this.scopeLoading = false;
});
},
},
};
</script>

View File

@@ -0,0 +1,639 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
ref="crud"
v-model="form"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="userInfo.authority.includes('administrator')"
@click="handleDelete"
>
</el-button>
</template>
<template #limitPattern="{ row }">
<el-tag
v-for="(pattern, index) in parseLimitPattern(row.limitPattern)"
:key="index"
style="margin: 2px 4px 2px 0"
>{{ pattern }}</el-tag
>
</template>
<template #requestMethod="{ row }">
<el-tag>{{ requestMethodLabel(row.requestMethod) }}</el-tag>
</template>
<template #limitDimension="{ row }">
<el-tag>{{ limitDimensionLabel(row.limitDimension) }}</el-tag>
</template>
<template #rate="{ row }">
{{ row.limitCount }} / {{ row.limitPeriod }} {{ timeUnitLabel(row.timeUnit) }}
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
<template #menu="{ row }">
<el-button
type="primary"
text
icon="el-icon-circle-check"
v-if="row.status !== 1 && userInfo.authority.includes('administrator')"
@click.stop="handleEnable(row)"
>启用
</el-button>
<el-button
type="primary"
text
icon="el-icon-circle-close"
v-if="row.status === 1 && userInfo.authority.includes('administrator')"
@click.stop="handleDisable(row)"
>禁用
</el-button>
<el-button
type="primary"
text
icon="el-icon-notebook"
v-if="userInfo.authority.includes('administrator')"
@click.stop="handleRowLog(row)"
>日志
</el-button>
</template>
<!-- 匹配路径动态表单 -->
<template #limitPattern-form="{ type }">
<el-row
v-for="(item, index) in limitPatternList"
:key="index"
:gutter="8"
style="margin-bottom: 8px"
>
<el-col :span="22">
<el-input
v-model="limitPatternList[index]"
placeholder="请输入URL匹配路径如 /blade-system/user/**"
:disabled="type === 'view'"
/>
</el-col>
<el-col :span="2" v-if="type !== 'view'">
<el-button
type="danger"
text
icon="el-icon-delete"
@click="removeLimitPattern(index)"
/>
</el-col>
</el-row>
<el-row v-if="type !== 'view'" :gutter="8">
<el-col :span="8">
<el-button type="primary" text icon="el-icon-circle-plus" @click="addLimitPattern"
>添加路径</el-button
>
</el-col>
</el-row>
<el-row style="margin-top: 8px">
<el-text type="info" size="small">
<el-icon><WarningFilled /></el-icon>
提示: 支持 Ant 风格路径匹配多路径命中任一即生效共享同一配额
</el-text>
</el-row>
</template>
</avue-crud>
<!-- 单条规则触发日志 Drawer -->
<el-drawer :title="`[${logLimitName}] 触发日志`" v-model="logDrawerVisible" append-to-body size="1200px">
<basic-container>
<avue-crud
:option="logOption"
:table-loading="logLoading"
:data="logData"
v-model:page="logPage"
ref="logCrud"
v-model="logForm"
:before-open="beforeOpenLog"
@search-change="logSearchChange"
@search-reset="logSearchReset"
@current-change="logCurrentChange"
@size-change="logSizeChange"
@refresh-change="logRefreshChange"
@on-load="onLoadLog"
>
<template #requestUri="{ row }">
<el-tag>{{ row.requestUri }}</el-tag>
</template>
<template #method="{ row }">
<el-tag>{{ row.method }}</el-tag>
</template>
<template #limitDimension="{ row }">
<el-tag>{{ limitDimensionLabel(row.limitDimension) }}</el-tag>
</template>
<template #rate="{ row }">
<span v-if="row.limitCount"
>{{ row.limitCount }} / {{ row.limitPeriod }} {{ timeUnitLabel(row.timeUnit) }}</span
>
<span v-else>-</span>
</template>
<template #rate-form>
<span v-if="logForm.limitCount"
>{{ logForm.limitCount }} / {{ logForm.limitPeriod }} {{ timeUnitLabel(logForm.timeUnit) }}</span
>
<span v-else>-</span>
</template>
</avue-crud>
</basic-container>
</el-drawer>
</basic-container>
</template>
<script>
import { getList, getDetail, remove, add, update, enable, disable } from '@/api/system/ratelimit';
import { getList as getLogList, getDetail as getLogDetail } from '@/api/system/ratelimitlog';
import { mapGetters } from 'vuex';
import { WarningFilled } from '@element-plus/icons-vue';
export default {
components: {
WarningFilled,
},
data() {
return {
form: {},
query: {},
loading: true,
limitPatternList: [],
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
timeUnitDic: [
{ label: '秒', value: 'SECONDS' },
{ label: '分钟', value: 'MINUTES' },
{ label: '小时', value: 'HOURS' },
],
requestMethodDic: [
{ label: '全部', value: 'ALL' },
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
],
limitDimensionDic: [
{ label: '全局', value: 'GLOBAL' },
{ label: '租户', value: 'TENANT' },
{ label: 'IP', value: 'IP' },
{ label: '用户', value: 'USER' },
],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 700,
tip: false,
searchShow: true,
searchMenuSpan: 6,
labelWidth: 110,
searchLabelWidth: 90,
border: true,
index: true,
viewBtn: true,
selection: true,
menuWidth: 350,
dialogClickModal: false,
column: [
{
label: '规则名称',
prop: 'limitName',
search: true,
span: 24,
rules: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
},
{
label: '匹配路径',
prop: 'limitPattern',
slot: true,
formslot: true,
span: 24,
labelTip:
'支持 Ant 风格路径匹配,可配置多个,如 /blade-system/user/** 匹配多层、/api/* 匹配单层;多个路径共享该规则的同一份配额',
rules: [{ required: true, message: '请输入匹配路径', trigger: 'change' }],
},
{
label: '请求方法',
prop: 'requestMethod',
type: 'select',
slot: true,
search: true,
value: 'ALL',
width: 100,
labelTip: '指定规则生效的 HTTP 请求方法,选择「全部」时对所有方法生效,否则仅对所选方法限流',
dicData: [
{ label: '全部', value: 'ALL' },
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
],
rules: [{ required: true, message: '请选择请求方法', trigger: 'change' }],
},
{
label: '限流维度',
prop: 'limitDimension',
type: 'select',
slot: true,
search: true,
value: 'IP',
width: 100,
labelTip: '全局所有请求共享配额租户按租户隔离IP按来源IP隔离用户按登录用户隔离',
dicData: [
{ label: '全局', value: 'GLOBAL' },
{ label: '租户', value: 'TENANT' },
{ label: 'IP', value: 'IP' },
{ label: '用户', value: 'USER' },
],
rules: [{ required: true, message: '请选择限流维度', trigger: 'change' }],
},
{
label: '请求阈值',
prop: 'limitCount',
type: 'number',
minRows: 1,
value: 100,
hide: true,
labelTip: '单个时间窗口内允许通过的最大请求数,超出后触发限流并返回 429',
rules: [{ required: true, message: '请输入请求阈值', trigger: 'blur' }],
},
{
label: '时间窗口',
prop: 'limitPeriod',
type: 'number',
minRows: 1,
value: 1,
hide: true,
labelTip: '限流统计周期的数值,与时间单位组合使用,如时间窗口 1 + 时间单位分钟表示每分钟一个周期',
rules: [{ required: true, message: '请输入时间窗口', trigger: 'blur' }],
},
{
label: '时间单位',
prop: 'timeUnit',
type: 'select',
value: 'MINUTES',
hide: true,
labelTip: '限流统计周期的计量单位,与请求阈值、时间窗口共同构成限流速率,如 100 次 / 分钟',
dicData: [
{ label: '秒', value: 'SECONDS' },
{ label: '分钟', value: 'MINUTES' },
{ label: '小时', value: 'HOURS' },
],
rules: [{ required: true, message: '请选择时间单位', trigger: 'change' }],
},
{
label: '限流速率',
prop: 'rate',
slot: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
width: 150,
},
{
label: '优先级',
prop: 'priority',
type: 'number',
value: 1,
hide: true,
labelTip: '数值越小越优先匹配,同一路径命中多条规则时取优先级最高的一条',
},
{
label: '状态',
prop: 'status',
type: 'select',
slot: true,
value: 1,
display: false,
width: 90,
dicData: [
{ label: '启用', value: 1 },
{ label: '禁用', value: -1 },
],
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 2,
span: 24,
hide: true,
},
],
},
data: [],
// 单条规则触发日志
logDrawerVisible: false,
logLimitName: '',
logLimitId: null,
logForm: {},
logQuery: {},
logLoading: false,
logData: [],
logPage: { pageSize: 10, currentPage: 1, total: 0 },
logOption: {
height: 'auto',
calcHeight: 70,
tip: false,
header: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 100,
dialogType: 'drawer',
dialogWidth: 800,
column: [
{ label: '请求接口', prop: 'requestUri', search: true, slot: true },
{ label: '请求方法', prop: 'method', slot: true, width: 90 },
{
label: '限流维度',
prop: 'limitDimension',
type: 'select',
slot: true,
width: 100,
dicData: [
{ label: '全局', value: 'GLOBAL' },
{ label: '租户', value: 'TENANT' },
{ label: 'IP', value: 'IP' },
{ label: '用户', value: 'USER' },
],
},
{ label: '限流速率', prop: 'rate', slot: true, formslot: true, width: 150 },
{ label: '租户ID', prop: 'tenantId', hide: true },
{ label: '账号', prop: 'createBy', hide: true },
{ label: '触发IP', prop: 'remoteIp', search: true, width: 140 },
{ label: '服务ID', prop: 'serviceId', hide: true },
{ label: '服务IP', prop: 'serverIp', hide: true, width: 160 },
{ label: '环境', prop: 'env', hide: true, width: 85 },
{ label: '用户代理', prop: 'userAgent', span: 24, hide: true },
{ label: '触发时间', prop: 'createTime', span: 24, width: 170 },
{ label: '请求数据', prop: 'params', type: 'textarea', span: 24, minRows: 2, hide: true },
],
},
};
},
computed: {
...mapGetters(['userInfo']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
watch: {
// 同步动态多行输入到表单字段,使匹配路径的必填校验生效
limitPatternList: {
handler() {
this.form.limitPattern = this.buildLimitPattern();
},
deep: true,
},
},
methods: {
timeUnitLabel(value) {
const item = this.timeUnitDic.find(unit => unit.value === value);
return item ? item.label : value;
},
requestMethodLabel(value) {
const item = this.requestMethodDic.find(method => method.value === value);
return item ? item.label : value;
},
limitDimensionLabel(value) {
const item = this.limitDimensionDic.find(dimension => dimension.value === value);
return item ? item.label : value;
},
addLimitPattern() {
this.limitPatternList.push('');
},
removeLimitPattern(index) {
this.limitPatternList.splice(index, 1);
},
parseLimitPattern(limitPattern) {
if (!limitPattern) return [];
return limitPattern
.split(',')
.map(pattern => pattern.trim())
.filter(pattern => pattern.length > 0);
},
buildLimitPattern() {
const patterns = this.limitPatternList
.map(pattern => pattern && pattern.trim())
.filter(pattern => pattern && pattern.length > 0);
return patterns.length > 0 ? patterns.join(',') : '';
},
rowSave(row, done, loading) {
add(row).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
handleEnable(row) {
this.$confirm('确定启用该限流规则?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return enable(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '启用成功!' });
});
},
handleDisable(row) {
this.$confirm('确定禁用该限流规则?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return disable(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '禁用成功!' });
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
this.$refs.crud.toggleSelection();
});
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
beforeOpen(done, type) {
if (['add'].includes(type)) {
this.limitPatternList = [''];
}
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
const patternList = this.parseLimitPattern(this.form.limitPattern);
this.limitPatternList = patternList.length > 0 ? patternList : [''];
});
}
done();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
// ==================== 单条规则触发日志 ====================
handleRowLog(row) {
this.logLimitName = row.limitName || '限流规则';
this.logLimitId = row.id;
this.logDrawerVisible = true;
this.logPage.currentPage = 1;
this.logQuery = {};
this.onLoadLog(this.logPage);
},
beforeOpenLog(done, type) {
if (['view'].includes(type)) {
getLogDetail(this.logForm.id).then(res => {
this.logForm = res.data.data;
});
}
done();
},
logSearchChange(params, done) {
this.logQuery = params;
this.logPage.currentPage = 1;
this.onLoadLog(this.logPage, params);
done();
},
logSearchReset() {
this.logQuery = {};
this.onLoadLog(this.logPage);
},
logCurrentChange(currentPage) {
this.logPage.currentPage = currentPage;
},
logSizeChange(pageSize) {
this.logPage.pageSize = pageSize;
},
logRefreshChange() {
this.onLoadLog(this.logPage, this.logQuery);
},
onLoadLog(page, params = {}) {
this.logLoading = true;
const query = Object.assign({ limitId: this.logLimitId }, params, this.logQuery);
getLogList(page.currentPage, page.pageSize, query).then(res => {
const data = res.data.data;
this.logPage.total = data.total;
this.logData = data.records;
this.logLoading = false;
});
},
},
};
</script>

View File

@@ -0,0 +1,655 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
:permission="permissionList"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.role_delete"
plain
@click="handleDelete"
>
</el-button>
<el-button
icon="el-icon-setting"
@click="handleRole"
v-if="userInfo.authority.includes('admin')"
plain
>权限设置
</el-button>
</template>
<template #menu="{ row }">
<el-button
type="primary"
text
icon="el-icon-setting"
v-if="userInfo.authority.includes('admin')"
plain
style="border: 0; background-color: transparent !important"
@click.stop="handleRowRole(row)"
>权限设置
</el-button>
</template>
</avue-crud>
<el-dialog
title="角色权限配置"
append-to-body
v-model="box"
width="345px"
@close="handleDialogClose"
>
<el-tabs type="border-card">
<el-tab-pane label="菜单权限">
<el-row
justify="space-between"
align="middle"
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
>
<span style="display: inline-flex; align-items: center">
<el-switch
v-model="menuLinked"
active-text="节点联动"
size="small"
@change="handleLinkedChange('treeMenu')"
/>
<el-tooltip content="开启后勾选父节点会自动勾选所有子节点,关闭则可独立勾选任意节点" placement="top">
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
</el-tooltip>
</span>
<el-button-group>
<el-button size="small" plain @click="handleSelectAll('treeMenu', menuGrantList)"
>全选</el-button
>
<el-button
size="small"
plain
@click="handleInvertSelect('treeMenu', menuGrantList, menuLinked)"
>反选</el-button
>
</el-button-group>
</el-row>
<el-tree
:data="menuGrantList"
show-checkbox
:check-strictly="!menuLinked"
node-key="id"
ref="treeMenu"
:default-checked-keys="menuTreeObj"
:props="props"
>
</el-tree>
</el-tab-pane>
<el-tab-pane label="数据权限">
<el-row
justify="space-between"
align="middle"
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
>
<span style="display: inline-flex; align-items: center">
<el-switch
v-model="dataScopeLinked"
active-text="节点联动"
size="small"
@change="handleLinkedChange('treeDataScope')"
/>
<el-tooltip content="开启后勾选父节点会自动勾选所有子节点,关闭则可独立勾选任意节点" placement="top">
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
</el-tooltip>
</span>
<el-button-group>
<el-button
size="small"
plain
@click="handleSelectAll('treeDataScope', dataScopeGrantList)"
>全选</el-button
>
<el-button
size="small"
plain
@click="handleInvertSelect('treeDataScope', dataScopeGrantList, dataScopeLinked)"
>反选</el-button
>
</el-button-group>
</el-row>
<el-tree
:data="dataScopeGrantList"
show-checkbox
:check-strictly="!dataScopeLinked"
node-key="id"
ref="treeDataScope"
:default-checked-keys="dataScopeTreeObj"
:props="props"
>
</el-tree>
</el-tab-pane>
<el-tab-pane label="接口权限">
<el-row
justify="space-between"
align="middle"
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
>
<span style="display: inline-flex; align-items: center">
<el-switch
v-model="apiScopeLinked"
active-text="节点联动"
size="small"
@change="handleLinkedChange('treeApiScope')"
/>
<el-tooltip content="开启后勾选父节点会自动勾选所有子节点,关闭则可独立勾选任意节点" placement="top">
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
</el-tooltip>
</span>
<el-button-group>
<el-button
size="small"
plain
@click="handleSelectAll('treeApiScope', apiScopeGrantList)"
>全选</el-button
>
<el-button
size="small"
plain
@click="handleInvertSelect('treeApiScope', apiScopeGrantList, apiScopeLinked)"
>反选</el-button
>
</el-button-group>
</el-row>
<el-tree
:data="apiScopeGrantList"
show-checkbox
:check-strictly="!apiScopeLinked"
node-key="id"
ref="treeApiScope"
:default-checked-keys="apiScopeTreeObj"
:props="props"
>
</el-tree>
</el-tab-pane>
</el-tabs>
<template #footer>
<span class="dialog-footer">
<el-button @click="box = false"> </el-button>
<el-button type="primary" @click="submit"> </el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import {
add,
getList,
getRole,
getRoleAlias,
getRoleTreeById,
grant,
grantTree,
remove,
update,
} from '@/api/system/role';
import { mapGetters } from 'vuex';
import website from '@/config/website';
import { validatenull } from '@/utils/validate';
export default {
data() {
return {
form: {},
box: false,
props: {
label: 'title',
value: 'key',
},
menuGrantList: [],
dataScopeGrantList: [],
apiScopeGrantList: [],
apiGrantList: [],
menuTreeObj: [],
dataScopeTreeObj: [],
apiScopeTreeObj: [],
menuLinked: false,
dataScopeLinked: false,
apiScopeLinked: false,
currentRoleIds: [],
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
labelWidth: 120,
searchLabelWidth: 120,
menuWidth: 350,
dialogWidth: 600,
dialogClickModal: false,
column: [
{
label: '上级角色',
prop: 'parentId',
dicData: [],
type: 'tree',
hide: true,
span: 24,
props: {
label: 'title',
},
rules: [
{
required: false,
message: '请选择上级角色',
trigger: 'click',
},
],
},
{
label: '角色名称',
prop: 'roleName',
search: true,
span: 24,
rules: [
{
required: true,
message: '请输入角色名称',
trigger: 'blur',
},
],
},
{
label: '所属租户',
prop: 'tenantId',
type: 'tree',
dicUrl: '/blade-system/tenant/select',
addDisplay: false,
editDisplay: false,
viewDisplay: website.tenantMode,
span: 24,
props: {
label: 'tenantName',
value: 'tenantId',
},
hide: !website.tenantMode,
search: website.tenantMode,
rules: [
{
required: true,
message: '请输入所属租户',
trigger: 'click',
},
],
},
{
label: '角色别名',
labelTip: '用于权限校验与标注的属性,可手动录入也可点击现有别名快速录入',
prop: 'roleAlias',
search: true,
span: 24,
rules: [
{
required: true,
message: '请输入角色别名',
trigger: 'blur',
},
],
},
{
label: '现有别名',
labelTip: '当前系统已存在的角色别名,点击可快速录入',
prop: 'currentAlias',
type: 'radio',
dicData: [],
props: {
label: 'roleName',
value: 'roleAlias',
},
hide: true,
span: 24,
},
{
label: '角色排序',
prop: 'sort',
type: 'number',
span: 24,
rules: [
{
required: true,
message: '请输入角色排序',
trigger: 'blur',
},
],
},
],
},
data: [],
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.role_add, false),
viewBtn: this.validData(this.permission.role_view, false),
delBtn: this.validData(this.permission.role_delete, false),
editBtn: this.validData(this.permission.role_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
idsArray() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids;
},
},
watch: {
'form.currentAlias'() {
const alias = this.form.currentAlias;
if (!validatenull(alias)) {
this.form.roleAlias = alias;
}
},
},
methods: {
initData(roleId) {
getRoleTreeById(roleId).then(res => {
const column = this.findColumn(this.option.column, 'parentId');
column.dicData = res.data.data;
});
},
getAllNodeKeys(nodes) {
let keys = [];
nodes.forEach(node => {
keys.push(node.id);
if (node.children && node.children.length > 0) {
keys = keys.concat(this.getAllNodeKeys(node.children));
}
});
return keys;
},
getLeafKeys(nodes) {
let keys = [];
nodes.forEach(node => {
if (!node.children || node.children.length === 0) {
keys.push(node.id);
} else {
keys = keys.concat(this.getLeafKeys(node.children));
}
});
return keys;
},
handleSelectAll(treeRef, dataList) {
const tree = this.$refs[treeRef];
if (!tree) return;
const allKeys = this.getAllNodeKeys(dataList);
tree.setCheckedKeys(allKeys);
},
handleInvertSelect(treeRef, dataList, isLinked) {
const tree = this.$refs[treeRef];
if (!tree) return;
const checkedKeys = new Set(tree.getCheckedKeys());
if (isLinked) {
const leafKeys = this.getLeafKeys(dataList);
const invertedKeys = leafKeys.filter(key => !checkedKeys.has(key));
tree.setCheckedKeys(invertedKeys);
} else {
const allKeys = this.getAllNodeKeys(dataList);
const invertedKeys = allKeys.filter(key => !checkedKeys.has(key));
tree.setCheckedKeys(invertedKeys);
}
},
handleLinkedChange(treeRef) {
const tree = this.$refs[treeRef];
if (!tree) return;
const checkedKeys = tree.getCheckedKeys();
const halfCheckedKeys = tree.getHalfCheckedKeys();
this.$nextTick(() => {
tree.setCheckedKeys([...checkedKeys, ...halfCheckedKeys]);
});
},
// el-tree 会把 setCheckedKeys 的入参记录为默认勾选集并在树数据重载时回放,
// 复用的树实例需在打开配置时整体覆盖勾选状态,避免残留上一次配置的勾选
applyCheckedKeys(treeRef, keys) {
const tree = this.$refs[treeRef];
if (!tree) return;
tree.setCheckedKeys(keys);
},
submit() {
const menuList = this.$refs.treeMenu.getCheckedKeys();
const dataScopeList = this.$refs.treeDataScope.getCheckedKeys();
const apiScopeList = this.$refs.treeApiScope.getCheckedKeys();
grant(this.currentRoleIds, menuList, dataScopeList, apiScopeList).then(() => {
this.box = false;
this.$message({
type: 'success',
message: '操作成功!',
});
this.currentRoleIds = [];
this.selectionList = [];
this.onLoad(this.page);
});
},
rowSave(row, done, loading) {
add(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
beforeOpen(done, type) {
if (['add', 'edit'].includes(type)) {
this.initData(this.form.id);
}
if (['edit', 'view'].includes(type)) {
if (this.form.parentId === '0') {
this.form.parentId = '';
}
}
getRoleAlias().then(res => {
const column = this.findColumn(this.option.column, 'currentAlias');
column.dicData = res.data.data;
});
done();
},
handleRole() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择一条数据');
return;
}
if (this.selectionList.length > 1) {
this.$message.warning('只能选择一条数据');
return;
}
this.menuTreeObj = [];
this.dataScopeTreeObj = [];
this.apiScopeTreeObj = [];
// 保存当前要配置权限的角色ID
this.currentRoleIds = this.idsArray;
grantTree().then(res => {
this.menuGrantList = res.data.data.menu;
this.dataScopeGrantList = res.data.data.dataScope;
this.apiScopeGrantList = res.data.data.apiScope;
getRole(this.ids).then(res => {
this.menuTreeObj = res.data.data.menu;
this.dataScopeTreeObj = res.data.data.dataScope;
this.apiScopeTreeObj = res.data.data.apiScope;
this.box = true;
this.$nextTick(() => {
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj);
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
});
});
});
},
handleRowRole(row) {
this.menuTreeObj = [];
this.dataScopeTreeObj = [];
this.apiScopeTreeObj = [];
// 保存当前要配置权限的角色ID单个行操作
this.currentRoleIds = [row.id];
grantTree().then(res => {
this.menuGrantList = res.data.data.menu;
this.dataScopeGrantList = res.data.data.dataScope;
this.apiScopeGrantList = res.data.data.apiScope;
getRole(row.id).then(res => {
this.menuTreeObj = res.data.data.menu;
this.dataScopeTreeObj = res.data.data.dataScope;
this.apiScopeTreeObj = res.data.data.apiScope;
this.box = true;
this.$nextTick(() => {
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj);
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
});
});
});
},
handleDialogClose() {
// 对话框关闭时清空选择状态和权限树数据
this.menuTreeObj = [];
this.dataScopeTreeObj = [];
this.apiScopeTreeObj = [];
this.menuLinked = false;
this.dataScopeLinked = false;
this.apiScopeLinked = false;
this.selectionClear();
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
this.data = res.data.data;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>