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>

472
src/views/base/region.vue Normal file
View File

@@ -0,0 +1,472 @@
<template>
<el-row>
<el-col :span="9">
<div class="box">
<el-scrollbar>
<basic-container>
<avue-tree :option="treeOption" :data="treeData" @node-click="nodeClick" />
</basic-container>
</el-scrollbar>
</div>
</el-col>
<el-col :span="15">
<basic-container>
<el-button-group>
<el-button
v-if="permission.region_add"
type="primary"
icon="el-icon-plus"
@click="addChildren"
>新增下级
</el-button>
<el-button
v-if="permission.region_delete"
type="primary"
icon="el-icon-delete"
@click="handleDelete"
>删除
</el-button>
<el-button
v-if="permission.region_import"
type="primary"
icon="el-icon-upload"
@click="handleImport"
>导入
</el-button>
<el-button
v-if="permission.region_export"
type="primary"
icon="el-icon-download"
@click="handleExport"
>导出
</el-button>
<el-button
v-if="permission.region_debug"
type="primary"
icon="el-icon-video-play"
@click="handleDebug"
>调试
</el-button>
</el-button-group>
</basic-container>
<basic-container>
<avue-form ref="form" :option="regionOption" v-model="regionForm" @submit="handleSubmit">
<template #code="{}">
<el-input placeholder="请输入 区划子编号" v-model="regionForm.subCode">
<template #prepend>{{ regionForm.parentCode }}</template>
</el-input>
</template>
</avue-form>
<el-dialog title="行政区划数据导入" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOption" v-model="excelForm" :upload-after="uploadAfter">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
</el-button>
</template>
</avue-form>
</el-dialog>
<el-dialog title="行政区划数据调试" append-to-body v-model="debugBox" width="350px">
<avue-form :option="debugOption" v-model="debugForm" />
</el-dialog>
</basic-container>
</el-col>
</el-row>
</template>
<script>
import { getLazyTree, getDetail, submit, remove } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { validatenull } from '@/utils/validate';
import { downloadXls } from '@/utils/util';
import { getToken } from '@/utils/auth';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
export default {
data() {
return {
topCode: '00',
treeCode: '',
treeParentCode: '',
treeData: [],
treeOption: {
nodeKey: 'id',
lazy: true,
treeLoad: function (node, resolve) {
const parentCode = node.level === 0 ? '00' : node.data.id;
getLazyTree(parentCode).then(res => {
resolve(
res.data.data.map(item => {
return {
...item,
leaf: !item.hasChildren,
};
})
);
});
},
addBtn: false,
menu: false,
size: 'small',
props: {
labelText: '标题',
label: 'title',
value: 'value',
children: 'children',
},
},
regionForm: {},
regionOption: {
labelWidth: 100,
column: [
{
label: '父区划编号',
prop: 'parentCode',
span: 24,
disabled: true,
rules: [
{
required: true,
message: '请输入父区划编号',
trigger: 'blur',
},
],
},
{
label: '父区划名称',
prop: 'parentName',
span: 24,
disabled: true,
},
{
label: '区划编号',
prop: 'code',
formslot: true,
span: 24,
rules: [
{
required: true,
message: '请输入区划编号',
trigger: 'blur',
},
],
},
{
label: '区划子编号',
prop: 'subCode',
display: false,
},
{
label: '区划名称',
prop: 'name',
span: 24,
rules: [
{
required: true,
message: '请输入区划名称',
trigger: 'blur',
},
],
},
{
label: '区划等级',
prop: 'regionLevel',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=region',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
span: 24,
rules: [
{
required: true,
message: '请选择区划等级',
trigger: 'blur',
},
],
},
{
label: '区划排序',
prop: 'sort',
type: 'number',
span: 24,
rules: [
{
required: true,
message: '请输入区划排序',
trigger: 'blur',
},
],
},
{
label: '区划备注',
prop: 'remark',
type: 'textarea',
minRows: 6,
span: 24,
},
],
},
excelBox: false,
excelForm: {},
excelOption: {
submitBtn: false,
emptyBtn: false,
column: [
{
label: '模板上传',
prop: 'excelFile',
type: 'upload',
drag: true,
loadText: '模板上传中,请稍等',
span: 24,
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
action: '/blade-system/region/import-region',
},
{
label: '数据覆盖',
prop: 'isCovered',
type: 'switch',
align: 'center',
width: 80,
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
value: 0,
slot: true,
rules: [
{
required: true,
message: '请选择是否覆盖',
trigger: 'blur',
},
],
},
{
label: '模板下载',
prop: 'excelTemplate',
formslot: true,
span: 24,
},
],
},
debugBox: false,
debugForm: {},
debugOption: {
labelWidth: 50,
submitBtn: false,
emptyBtn: false,
column: [
{
label: '省份',
prop: 'province',
type: 'select',
props: {
label: 'name',
value: 'code',
},
cascader: ['city'],
dicUrl: '/blade-system/region/select',
span: 24,
},
{
label: '地市',
prop: 'city',
type: 'select',
props: {
label: 'name',
value: 'code',
},
cascader: ['district'],
dicFlag: false,
dicUrl: '/blade-system/region/select?code={{province}}',
span: 24,
},
{
label: '区县',
prop: 'district',
type: 'select',
props: {
label: 'name',
value: 'code',
},
dicFlag: false,
dicUrl: '/blade-system/region/select?code={{city}}',
span: 24,
},
],
},
};
},
watch: {
'regionForm.subCode'() {
this.regionForm.code = this.regionForm.parentCode + this.regionForm.subCode;
},
'excelForm.isCovered'() {
if (this.excelForm.isCovered !== '') {
const column = this.findColumn(this.excelOption.column, 'excelFile');
column.action = `/blade-system/region/import-region?isCovered=${this.excelForm.isCovered}`;
}
},
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.region_add, false),
viewBtn: this.validData(this.permission.region_view, false),
delBtn: this.validData(this.permission.region_delete, false),
editBtn: this.validData(this.permission.region_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
initTree() {
this.treeData = [];
getLazyTree(this.topCode).then(res => {
this.treeData = res.data.data.map(item => {
return {
...item,
leaf: !item.hasChildren,
};
});
});
},
nodeClick(data) {
const column = this.findColumn(this.regionOption.column, 'parentCode');
column.disabled = true;
this.treeCode = data.id;
this.treeParentCode = data.parentId;
getDetail(this.treeCode).then(res => {
this.regionForm = res.data.data;
this.regionForm.subCode = this.regionForm.code.replace(this.regionForm.parentCode, '');
});
},
addChildren() {
if (validatenull(this.regionForm.code) || validatenull(this.regionForm.name)) {
this.$message.warning('请先选择一项区划');
return;
}
this.regionForm.parentCode = this.regionForm.code;
this.regionForm.parentName = this.regionForm.name;
this.regionForm.code = '';
this.regionForm.subCode = '';
this.regionForm.name = '';
this.regionForm.regionLevel =
this.regionForm.regionLevel === 5 ? 5 : this.regionForm.regionLevel + 1;
},
handleSubmit(form, done) {
const parentCode = form.parentCode === this.topCode ? '' : form.parentCode;
form.code = parentCode + form.subCode;
submit(form).then(
() => {
this.$message({
type: 'success',
message: '操作成功!',
});
this.initTree();
this.regionForm.subCode = '';
this.$refs.form.resetForm();
done();
},
error => {
done();
window.console.log(error);
}
);
},
handleDelete() {
if (validatenull(this.regionForm.code)) {
this.$message.warning('请先选择一项区划');
return;
}
this.$confirm(`确定将 [${this.regionForm.name}] 数据删除?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.treeCode);
})
.then(() => {
this.$message({
type: 'success',
message: '操作成功!',
});
this.initTree();
this.regionForm.subCode = '';
this.$refs.form.resetForm();
});
},
uploadAfter(res, done, loading, column) {
window.console.log(column);
this.excelBox = false;
this.initTree();
done();
},
handleDebug() {
this.debugBox = true;
},
handleImport() {
this.excelBox = true;
},
handleExport() {
this.$confirm('是否导出行政区划数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
`/blade-system/region/export-region?${this.website.tokenHeader}=${getToken()}`
).then(res => {
downloadXls(res.data, `行政区划数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
NProgress.done();
});
});
},
handleTemplate() {
exportBlob(
`/blade-system/region/export-template?${this.website.tokenHeader}=${getToken()}`
).then(res => {
downloadXls(res.data, '行政区划模板.xlsx');
});
},
},
};
</script>
<style>
.box {
height: 800px;
}
.el-scrollbar {
height: 100%;
}
.box .el-scrollbar__wrap {
overflow: scroll;
}
</style>

290
src/views/data/record.vue Normal file
View File

@@ -0,0 +1,290 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
:permission="permissionList"
:before-open="beforeOpen"
v-model:page="page"
@search-change="searchChange"
@search-reset="searchReset"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #userAgent-form="{}">
<code-editor v-model="form.userAgent" :readonly="true" theme="nord" height="50px" />
</template>
<template #oldData-form="{}">
<code-editor
v-model="form.oldData"
:json="true"
:readonly="true"
theme="nord"
height="200px"
/>
</template>
<template #newData-form="{}">
<code-editor
v-model="form.newData"
:json="true"
:readonly="true"
theme="nord"
height="200px"
/>
</template>
<template #recordResult-form="{}">
<code-editor
v-model="form.recordResult"
:json="true"
:readonly="true"
theme="nord"
height="350px"
/>
</template>
<template #recordMessage-form="{}">
<code-editor v-model="form.recordMessage" :readonly="true" theme="nord" height="350px" />
</template>
<template #env="{ row }">
<el-tag>{{ row.env }}</el-tag>
</template>
<template #tableName="{ row }">
<el-tag>{{ row.tableName }}</el-tag>
</template>
<template #method="{ row }">
<el-tag>{{ row.method }}</el-tag>
</template>
<template #recordLevel="{ row }">
<el-tag>{{ row.recordLevel }}</el-tag>
</template>
<template #remoteIp="{ row }">
<el-tag>{{ row.remoteIp }}</el-tag>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getRecordList, getRecordDetail } from '@/api/data/record';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchIndex: 3,
searchIcon: true,
border: true,
index: true,
viewBtn: true,
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 120,
labelWidth: 120,
dialogType: 'drawer',
column: [
{
label: '服务ID',
prop: 'serviceId',
width: '120',
search: true,
},
{
label: '服务器名',
prop: 'serverHost',
width: '140',
search: true,
},
{
label: '服务器IP',
prop: 'serverIp',
width: '180',
search: true,
},
{
label: '环境',
prop: 'env',
width: '80',
search: true,
slot: true,
},
{
label: '审计级别',
prop: 'recordLevel',
width: '100',
search: true,
slot: true,
},
{
label: '数据表名',
prop: 'tableName',
width: '150',
search: true,
slot: true,
},
{
label: '操作方式',
prop: 'method',
width: '100',
slot: true,
},
{
label: '请求URI',
prop: 'requestUri',
search: true,
overHidden: true,
},
{
label: '操作类型',
prop: 'operation',
width: '100',
hide: true,
},
{
label: '操作IP',
prop: 'remoteIp',
width: '140',
slot: true,
},
{
label: '记录人',
prop: 'recordUserName',
width: '120',
hide: true,
},
{
label: '记录耗时',
prop: 'recordCost',
width: '100',
suffix: 'ms',
hide: true,
},
{
label: '记录时间',
prop: 'recordTime',
width: '180',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
span: 24,
},
{
label: '用户代理',
prop: 'userAgent',
span: 24,
hide: true,
},
{
label: '操作前数据',
prop: 'oldData',
type: 'textarea',
span: 24,
minRows: 3,
hide: true,
},
{
label: '操作后数据',
prop: 'newData',
type: 'textarea',
span: 24,
minRows: 3,
hide: true,
},
{
label: '审计结果',
prop: 'recordResult',
type: 'textarea',
span: 24,
minRows: 3,
hide: true,
},
{
label: '审计消息',
prop: 'recordMessage',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
viewBtn: this.validData(this.permission.record_view, false),
};
},
},
methods: {
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getRecordDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
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;
getRecordList(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;
})
.catch(() => {
this.loading = false;
});
},
},
};
</script>
<style scoped>
/* 可以根据需要添加自定义样式 */
</style>

View File

@@ -0,0 +1,309 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model:search="search"
ref="crud"
@row-del="rowDel"
v-model="form"
:permission="permissionList"
@row-update="rowUpdate"
@row-save="rowSave"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="permission.notice_delete"
@click="handleDelete"
>
</el-button>
</template>
<template #category="{ row }">
<el-tag>{{ row.categoryName }}</el-tag>
</template>
</avue-crud>
</basic-container>
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus';
import { ref, reactive, computed, onMounted } from 'vue';
import { useStore } from 'vuex';
import { getList, remove, update, add, getNotice } from '@/api/desk/notice';
// 定义响应式数据
const form = ref({});
const search = ref({});
const loading = ref(true);
const page = ref({
pageSize: 10,
currentPage: 1,
total: 0,
});
const selectionList = ref([]);
const data = ref([]);
const option = reactive({
height: 'auto',
calcHeight: 32,
dialogWidth: 950,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
excelBtn: true,
dialogClickModal: false,
grid: false,
column: [
{
label: '通知标题',
prop: 'title',
span: 24,
row: true,
search: true,
rules: [
{
required: true,
message: '请输入通知标题',
trigger: 'blur',
},
],
},
{
label: '通知类型',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=notice',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
rules: [
{
required: true,
message: '请输入通知类型',
trigger: 'blur',
},
],
},
{
label: '通知时间',
prop: 'releaseTimeRange',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
searchRange: true,
hide: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
search: true,
rules: [
{
required: true,
message: '请输入通知时间',
trigger: 'blur',
},
],
},
{
label: '通知日期',
prop: 'releaseTime',
type: 'date',
gridRow: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
rules: [
{
required: true,
message: '请输入通知日期',
trigger: 'click',
},
],
},
{
label: '通知内容',
prop: 'content',
component: 'avue-ueditor',
action: '/blade-resource/oss/endpoint/put-file',
propsHttp: {
res: 'data',
url: 'link',
},
hide: true,
minRows: 4,
span: 24,
},
],
});
// 使用 vuex store
const store = useStore();
const permission = computed(() => store.getters.permission);
// 计算权限
const permissionList = computed(() => {
const validData = (value, defaultVal = false) => (value ? true : defaultVal);
return {
addBtn: validData(permission.value.notice_add, false),
viewBtn: validData(permission.value.notice_view, false),
delBtn: validData(permission.value.notice_delete, false),
editBtn: validData(permission.value.notice_edit, false),
};
});
// 计算选中的id
const ids = computed(() => {
return selectionList.value.map(ele => ele.id).join(',');
});
// 新增
const rowSave = (row, done, loading) => {
add(row).then(
() => {
onLoad();
ElMessage.success('操作成功!');
done();
},
error => {
console.error(error);
loading();
}
);
};
// 修改
const rowUpdate = (row, index, done, loading) => {
update(row).then(
() => {
onLoad();
ElMessage.success('操作成功!');
done();
},
error => {
console.error(error);
loading();
}
);
};
// 删除
const rowDel = row => {
ElMessageBox.confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(row.id))
.then(() => {
onLoad();
ElMessage.success('操作成功!');
});
};
// 重置查询
const searchReset = () => {
search.value = {};
page.value.currentPage = 1;
onLoad();
};
// 查询
const searchChange = (params, done) => {
page.value.currentPage = 1;
onLoad();
done();
};
// 勾选事件
const selectionChange = list => {
selectionList.value = list;
};
// 重置勾选
const selectionClear = () => {
selectionList.value = [];
// 假设你有一个 ref 为 'crud' 的组件,可以调用 toggleSelection 方法
// if (crudRef.value) crudRef.value.toggleSelection();
};
// 批量删除
const handleDelete = () => {
if (selectionList.value.length === 0) {
ElMessage.success('请选择至少一条数据');
return;
}
ElMessageBox.confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(ids.value))
.then(() => {
onLoad();
ElMessage.success('操作成功!');
});
};
// 打开前事件
const beforeOpen = (done, type) => {
if (['edit', 'view'].includes(type)) {
getNotice(form.value.id).then(res => {
form.value = res.data.data;
});
}
done();
};
// 刷新事件
const refreshChange = () => {
onLoad();
};
// 加载数据
const onLoad = () => {
const { releaseTimeRange } = search.value;
let values = { ...search.value };
if (releaseTimeRange) {
values = {
...values,
releaseTime_datege: releaseTimeRange[0],
releaseTime_datelt: releaseTimeRange[1],
};
values.releaseTimeRange = null;
}
loading.value = true;
getList(page.value.currentPage, page.value.pageSize, values).then(res => {
const { total, records } = res.data.data;
page.value.total = total;
data.value = records;
loading.value = false;
selectionClear();
});
};
// 生命周期钩子
onMounted(() => {
// 可以在这里执行组件挂载后的逻辑
});
</script>
<style scoped>
/* 你的样式代码 */
</style>

317
src/views/desk/notice.vue Normal file
View File

@@ -0,0 +1,317 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
ref="crud"
@row-del="rowDel"
v-model="form"
:permission="permissionList"
@row-update="rowUpdate"
@row-save="rowSave"
:before-open="beforeOpen"
@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="permission.notice_delete"
@click="handleDelete"
>
</el-button>
</template>
<template #category="{ row }">
<el-tag>{{ row.categoryName }}</el-tag>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, remove, update, add, getNotice } from '@/api/desk/notice';
import { mapGetters } from 'vuex';
import { validatenull } from '@/utils/validate';
export default {
data() {
return {
form: {},
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 950,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
excelBtn: true,
dialogClickModal: false,
grid: false,
column: [
{
label: '通知标题',
prop: 'title',
span: 24,
row: true,
search: true,
rules: [
{
required: true,
message: '请输入通知标题',
trigger: 'blur',
},
],
},
{
label: '通知类型',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=notice',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
rules: [
{
required: true,
message: '请输入通知类型',
trigger: 'blur',
},
],
},
{
label: '通知时间',
prop: 'releaseTimeRange',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
searchRange: true,
hide: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
search: true,
rules: [
{
required: true,
message: '请输入通知时间',
trigger: 'blur',
},
],
},
{
label: '通知日期',
prop: 'releaseTime',
type: 'date',
gridRow: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
rules: [
{
required: true,
message: '请输入通知日期',
trigger: 'click',
},
],
},
{
label: '通知内容',
prop: 'content',
component: 'avue-ueditor',
action: '/blade-resource/oss/endpoint/put-file',
propsHttp: {
res: 'data',
url: 'link',
},
hide: true,
minRows: 4,
span: 24,
},
],
},
data: [],
};
},
activated() {
// 解析菜单路由携带的参数 (如 /desk/notice?test=123),激活钩子覆盖首次进入与标签切回两种场景
const { test } = this.$route.query;
if (!validatenull(test)) {
this.$message.info(`query 参数 test = ${test}`);
}
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.notice_add, false),
viewBtn: this.validData(this.permission.notice_view, false),
delBtn: this.validData(this.permission.notice_delete, false),
editBtn: this.validData(this.permission.notice_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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();
},
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 (['edit', 'view'].includes(type)) {
getNotice(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
const { releaseTimeRange } = this.query;
let values = {
...params,
...this.query,
};
if (releaseTimeRange) {
values = {
...values,
releaseTime_datege: releaseTimeRange[0],
releaseTime_datelt: releaseTimeRange[1],
};
values.releaseTimeRange = null;
}
this.loading = true;
getList(page.currentPage, page.pageSize, values).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style></style>

160
src/views/flow/deploy.vue Normal file
View File

@@ -0,0 +1,160 @@
<template>
<basic-container>
<avue-form
ref="form"
:option="option"
v-model="form"
:upload-before="uploadBefore"
:upload-after="uploadAfter"
/>
</basic-container>
</template>
<script>
import { deployUpload } from '@/api/flow/flow';
import { flowCategory } from '@/utils/flow';
export default {
data() {
return {
form: {
flowCategory: '',
tenantId: '',
flowFile: [],
file: {},
},
option: {
labelWidth: 120,
menuBtn: false,
column: [
{
label: '流程类型',
prop: 'flowCategory',
type: 'select',
dicUrl: `/blade-system/dict/dictionary?code=flow`,
props: {
label: 'dictValue',
value: 'dictKey',
},
row: true,
span: 12,
dataType: 'number',
rules: [
{
required: true,
message: '请选择流程类型',
trigger: 'blur',
},
],
},
{
label: '流程模式',
prop: 'flowMode',
type: 'radio',
dicData: [
{
label: '通用流程',
value: 1,
},
{
label: '定制流程',
value: 2,
},
],
value: 1,
row: true,
span: 12,
rules: [
{
required: true,
message: '请选择流程模式',
trigger: 'blur',
},
],
},
{
label: '所属租户',
prop: 'tenantId',
type: 'tree',
multiple: true,
dicUrl: '/blade-system/tenant/select',
props: {
label: 'tenantName',
value: 'tenantId',
},
display: false,
row: true,
span: 12,
rules: [
{
required: true,
message: '请选择所属租户',
trigger: 'blur',
},
],
},
{
label: '附件上传',
prop: 'flowFile',
type: 'upload',
loadText: '附件上传中,请稍等',
span: 24,
propsHttp: {
res: 'data',
},
tip: '请上传 bpmn20.xml 标准格式文件',
action: '/blade-flow/manager/check-upload',
},
],
},
};
},
watch: {
'form.flowMode'() {
this.$refs.form.option.column.filter(item => {
if (item.prop === 'tenantId') {
item.display = this.form.flowMode === 2;
}
});
},
},
methods: {
uploadBefore(file, done) {
this.$message.success('部署提交');
this.file = file;
done();
},
uploadAfter(res, done, loading) {
if (!this.form.flowCategory) {
this.$message.warning('清先选择流程类型');
loading();
return false;
}
if (this.form.flowMode === 2 && !this.form.tenantId) {
this.$message.warning('清先选择对应租户');
loading();
return false;
}
if (res.success) {
deployUpload(
flowCategory(this.form.flowCategory),
this.form.tenantId ? this.form.tenantId.join(',') : '',
[this.file]
).then(res => {
const data = res.data;
if (data.success) {
done();
} else {
this.$message.error(data.msg);
loading();
}
});
} else {
this.$message.warning('请上传 bpmn20.xml 标准格式文件');
loading();
return false;
}
},
},
};
</script>

201
src/views/flow/follow.vue Normal file
View File

@@ -0,0 +1,201 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
v-model:page="page"
:permission="permissionList"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #suspensionState="{ row }">
<el-tag>{{ row.suspensionState === 1 ? '激活' : '挂起' }}</el-tag>
</template>
</avue-crud>
<el-dialog title="流程删除" append-to-body v-model="followBox" width="20%">
<el-form :model="form" ref="form" label-width="80px">
<el-form-item label="删除理由">
<el-input v-model="deleteReason" placeholder="请输入删除理由" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="followBox = false"> </el-button>
<el-button type="primary" @click="handleDelete"> </el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import { followList, deleteProcessInstance } from '@/api/flow/flow';
export default {
data() {
return {
form: {},
selectionId: '',
processInstanceId: '',
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
followBox: false,
deleteReason: '',
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
editBtn: false,
addBtn: false,
viewBtn: false,
dialogWidth: 900,
menuWidth: 100,
dialogClickModal: false,
column: [
{
label: '执行id',
prop: 'executionId',
search: true,
width: 320,
},
{
label: '流程key',
prop: 'processDefinitionKey',
search: true,
},
{
label: '实例id',
prop: 'processInstanceId',
search: true,
width: 320,
},
{
label: '状态',
prop: 'suspensionState',
slot: true,
width: 80,
},
{
label: '发起人',
prop: 'startUser',
width: 100,
},
{
label: '开始时间',
prop: 'startTime',
width: 170,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
delBtn: this.validData(this.permission.flow_follow_delete, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
rowDel(row) {
this.followBox = true;
this.selectionId = row.id;
this.processInstanceId = row.processInstanceId;
},
handleDelete() {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return deleteProcessInstance({
deleteReason: this.deleteReason,
processInstanceId: this.processInstanceId,
});
})
.then(() => {
this.onLoad(this.page);
this.followBox = false;
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();
},
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;
followList(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();
});
},
},
};
</script>
<style>
.none-border {
border: 0;
background-color: transparent !important;
}
</style>

371
src/views/flow/manager.vue Normal file
View File

@@ -0,0 +1,371 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
v-model:page="page"
:permission="permissionList"
@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-radio-group v-model="mode">
<el-radio-button label="1">通用流程</el-radio-button>
<el-radio-button label="2">定制流程</el-radio-button>
</el-radio-group>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-refresh"
v-if="permission.flow_manager_state"
@click.stop="handleState(scope.row, scope.index)"
>变更状态
</el-button>
<el-button
type="primary"
text
icon="el-icon-search"
v-if="permission.flow_manager_image"
@click.stop="handleImage(scope.row, scope.index)"
>流程图
</el-button>
<el-button
type="primary"
text
icon="el-icon-delete"
v-if="permission.flow_manager_remove"
@click.stop="handleSlotDelete(scope.row, scope.index)"
>删除
</el-button>
</template>
<template #tenantId="{ row }">
<el-tag>{{ row.tenantId === '' ? '通用' : row.tenantId }}</el-tag>
</template>
<template #version="{ row }">
<el-tag>v{{ row.version }}</el-tag>
</template>
<template #suspensionState="{ row }">
<el-tag>{{ row.suspensionState === 1 ? '激活' : '挂起' }}</el-tag>
</template>
<template #category="{ row }">
<el-tag>{{ row.categoryName }}</el-tag>
</template>
</avue-crud>
<flow-design
v-if="this.website.design.designMode"
is-dialog
v-model:is-display="flowBox"
:process-definition-id="processDefinitionId"
></flow-design>
<el-dialog v-else title="流程图" append-to-body v-model="flowBox" :fullscreen="true">
<iframe
:src="flowUrl"
width="100%"
height="700"
title="流程图"
frameBorder="no"
border="0"
marginWidth="0"
marginHeight="0"
scrolling="no"
allowTransparency="yes"
>
</iframe>
<template #footer>
<span class="dialog-footer">
<el-button @click="flowBox = false"> </el-button>
</span>
</template>
</el-dialog>
<el-dialog title="流程变更" append-to-body v-model="stateBox" width="20%">
<el-form :model="form" ref="form" label-width="80px">
<el-form-item label="流程状态">
<el-select v-model="flowState" placeholder="请选择" value="">
<el-option
v-for="item in stateOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="stateBox = false"> </el-button>
<el-button type="primary" @click="handleDoState"> </el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import { managerList, changeState, deleteDeployment } from '@/api/flow/flow';
import { flowCategory } from '@/utils/flow';
export default {
data() {
return {
form: {},
mode: '1',
selectionId: '',
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
processDefinitionId: '',
flowBox: false,
flowUrl: '',
stateBox: false,
flowState: '',
stateOptions: [
{
value: 'active',
label: '激活',
},
{
value: 'suspend',
label: '挂起',
},
],
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
editBtn: false,
addBtn: false,
viewBtn: false,
delBtn: false,
dialogWidth: 900,
menuWidth: 250,
dialogClickModal: false,
column: [
{
label: '租户编号',
prop: 'tenantId',
slot: true,
width: 120,
},
{
label: '流程主键',
prop: 'id',
},
{
label: '流程标识',
prop: 'key',
search: true,
width: 150,
},
{
label: '流程名称',
prop: 'name',
width: 150,
},
{
label: '流程分类',
type: 'select',
row: true,
dicUrl: '/blade-system/dict/dictionary?code=flow',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
width: 100,
},
{
label: '流程版本',
prop: 'version',
slot: true,
width: 85,
},
{
label: '状态',
prop: 'suspensionState',
slot: true,
width: 80,
},
{
label: '部署时间',
prop: 'deploymentTime',
width: 170,
},
],
},
data: [],
};
},
watch: {
mode() {
this.onLoad(this.page);
},
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
delBtn: this.validData(this.permission.flow_manager_remove, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
deploymentIds() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.deploymentId);
});
return ids.join(',');
},
},
methods: {
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 deleteDeployment(this.deploymentIds);
})
.then(() => {
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
this.onLoad(this.page);
});
},
handleSlotDelete(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return deleteDeployment(row.deploymentId);
})
.then(() => {
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
this.onLoad(this.page);
});
},
handleState(row) {
this.stateBox = true;
this.selectionId = row.id;
},
handleDoState() {
if (!this.flowState) {
this.$message({
type: 'warn',
message: '请先选择流程状态!',
});
return;
}
changeState({ processId: this.selectionId, state: this.flowState }).then(res => {
const data = res.data;
if (data.success) {
this.$message({
type: 'success',
message: data.msg,
});
this.stateBox = false;
this.onLoad(this.page);
} else {
this.$message({
type: 'warn',
message: data.msg,
});
}
});
},
handleImage(row) {
if (this.website.design.designMode) {
this.processDefinitionId = row.id;
} else {
this.flowUrl = `/blade-flow/process/resource-view?processDefinitionId=${row.id}`;
}
this.flowBox = true;
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
const values = {
...params,
category: params.category ? flowCategory(params.category) : null,
mode: this.mode,
};
this.loading = true;
managerList(page.currentPage, page.pageSize, Object.assign(values, this.query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>

432
src/views/flow/model.vue Normal file
View File

@@ -0,0 +1,432 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
v-model:page="page"
@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="primary"
size="default"
icon="el-icon-circle-plus"
v-if="permission.flow_model_create"
plain
@click="handleCreate"
>
</el-button>
<el-button
type="danger"
size="default"
icon="el-icon-delete"
v-if="permission.flow_model_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
size="default"
icon="el-icon-setting"
v-if="permission.flow_model_update"
@click.stop="handleUpdate(scope.row, scope.index)"
>配置
</el-button>
<el-button
type="primary"
text
size="default"
icon="el-icon-upload"
v-if="permission.flow_model_deploy"
@click.stop="handleDeploy(scope.row, scope.index)"
>部署
</el-button>
<el-button
type="primary"
text
size="default"
icon="el-icon-delete"
v-if="permission.flow_model_delete"
@click.stop="handleSlotDelete(scope.row, scope.index)"
>删除
</el-button>
</template>
<template #version="{ row }">
<el-tag>v{{ row.version }}</el-tag>
</template>
</avue-crud>
<flow-design-step ref="bpmn" @loadData="onLoad"></flow-design-step>
<el-dialog
v-if="!this.website.design.designMode"
title="流程配置"
append-to-body
v-model="flowBox"
:fullscreen="true"
>
<iframe
:src="flowUrl"
width="100%"
height="700"
title="流程设计器"
frameBorder="no"
border="0"
marginWidth="0"
marginHeight="0"
scrolling="no"
allowTransparency="yes"
>
</iframe>
<template #footer>
<span class="dialog-footer">
<el-button @click="flowBox = false"> </el-button>
<el-button type="primary" @click="handleRefresh"> </el-button>
</span>
</template>
</el-dialog>
<el-dialog title="流程部署" append-to-body v-model="deployBox" width="20%">
<avue-form ref="form" :option="optionDeploy" v-model="form" @submit="handleSubmit" />
<template #footer>
<span class="dialog-footer">
<el-button @click="deployBox = false"> </el-button>
<el-button type="primary" @click="handleDoDeploy" :loading="deployLoading"
> </el-button
>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import { modelList, removeModel, deployModel, detail } from '@/api/flow/flow';
import { flowCategory } from '@/utils/flow';
export default {
data() {
return {
form: {},
optionDeploy: {
menuBtn: false,
column: [
{
label: '流程类型',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=flow',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'categoryValue',
search: true,
span: 24,
rules: [
{
required: true,
message: '请选择流程类型',
trigger: 'blur',
},
],
},
{
label: '流程模式',
prop: 'flowMode',
type: 'radio',
dicData: [
{
label: '通用流程',
value: 1,
},
{
label: '定制流程',
value: 2,
},
],
value: 1,
span: 24,
rules: [
{
required: true,
message: '请选择流程模式',
trigger: 'blur',
},
],
},
{
label: '所属租户',
prop: 'tenantId',
type: 'tree',
multiple: true,
dicUrl: '/blade-system/tenant/select',
props: {
label: 'tenantName',
value: 'tenantId',
},
display: false,
span: 24,
rules: [
{
required: true,
message: '请选择所属租户',
trigger: 'blur',
},
],
},
],
},
selectionId: '',
selectionList: [],
query: {},
loading: true,
deployLoading: false,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
deployBox: false,
flowBox: false,
flowUrl: '',
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
editBtn: false,
addBtn: false,
viewBtn: false,
delBtn: false,
dialogWidth: 900,
menuWidth: 220,
dialogClickModal: false,
size: 'default',
searchSize: 'default',
column: [
{
label: '模型主键',
prop: 'id',
},
{
label: '模型标识',
prop: 'modelKey',
search: true,
overHidden: true,
},
{
label: '模型名称',
prop: 'name',
search: true,
overHidden: true,
},
{
label: '流程版本',
prop: 'version',
slot: true,
overHidden: true,
},
{
label: '创建时间',
prop: 'created',
overHidden: true,
},
{
label: '更新时间',
prop: 'lastUpdated',
overHidden: true,
},
],
},
data: [],
};
},
watch: {
'form.flowMode'() {
this.$refs.form.option.column.filter(item => {
if (item.prop === 'tenantId') {
item.display = this.form.flowMode === 2;
}
});
},
},
computed: {
...mapGetters(['permission']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
handleSubmit(form, done) {
this.deployLoading = true;
deployModel({
modelId: this.selectionId,
category: flowCategory(form.categoryValue),
tenantIds: form.tenantId ? form.tenantId.join(',') : '',
}).then(res => {
const data = res.data;
if (data.success) {
this.$message({
type: 'success',
message: data.msg,
});
done();
this.$refs.form.resetForm();
this.deployBox = false;
this.deployLoading = false;
} else {
done();
this.deployLoading = false;
this.$message({
type: 'warn',
message: data.msg,
});
}
});
},
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 removeModel(this.ids);
})
.then(() => {
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
this.onLoad(this.page);
});
},
handleCreate() {
if (!this.website.design.designMode) {
this.flowUrl = `${this.website.design.designUrl}/index.html`;
this.flowBox = true;
} else {
this.$refs.bpmn.visible = true;
}
},
handleRefresh() {
this.flowBox = false;
this.onLoad(this.page);
},
handleUpdate(row) {
if (this.website.design.designMode) {
detail({ id: row.id }).then(res => {
const data = res.data.data;
const { modelEditorXml } = data;
this.$refs.bpmn.nutflowOption.step1.xml = modelEditorXml;
this.$refs.bpmn.nutflowOption.process = data;
this.$refs.bpmn.visible = true;
});
} else {
this.flowUrl = `${this.website.design.designUrl}/index.html#/editor/${row.id}`;
this.flowBox = true;
}
},
handleDeploy(row) {
this.deployBox = true;
this.selectionId = row.id;
},
handleDoDeploy() {
this.$refs.form.submit();
},
handleSlotDelete(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return removeModel(row.id);
})
.then(() => {
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
this.onLoad(this.page);
});
},
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;
page = page || this.page;
modelList(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();
});
},
},
};
</script>
<style lang="scss">
.nf-dialog {
.el-dialog__body {
padding: 5px;
}
.avue-dialog__footer {
text-align: center;
}
}
</style>

295
src/views/job/jobinfo.vue Normal file
View File

@@ -0,0 +1,295 @@
<template>
<basic-container>
<avue-crud
:option="option"
v-model:search="search"
v-model:page="page"
v-model="form"
:table-loading="loading"
:data="data"
:permission="permissionList"
:before-open="beforeOpen"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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="permission.jobinfo_delete"
@click="handleDelete"
>
</el-button>
<el-button type="info" plain icon="el-icon-sort" @click="handleSync"
>
</el-button>
</template>
<template #menu="scope">
<el-button type="primary" text icon="el-icon-video-play" @click="handleRun(scope.row)"
>
</el-button>
</template>
<template #enable="{ row }">
<el-switch
v-model="row.enable"
inline-prompt
@change="slotChange(row)"
active-text="启用"
inactive-text="暂停"
:active-value="1"
:inactive-value="0"
/>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove, change, run, sync } from '@/api/job/jobinfo';
import option from '@/option/job/jobinfo';
import { mapGetters } from 'vuex';
import 'nprogress/nprogress.css';
import func from '@/utils/func';
export default {
data() {
return {
form: {},
query: {},
search: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: option,
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.jobinfo_add, false),
viewBtn: this.validData(this.permission.jobinfo_view, false),
delBtn: this.validData(this.permission.jobinfo_delete, false),
editBtn: this.validData(this.permission.jobinfo_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
rowSave(row, done, loading) {
if (func.isArrayAndNotEmpty(row.lifecycle)) {
const lifecycleStart = row.lifecycle[0];
const lifecycleEnd = row.lifecycle[1];
if (!func.isUndefined(lifecycleStart) && !func.isUndefined(lifecycleEnd)) {
row.lifecycle = lifecycleStart + ',' + lifecycleEnd;
}
} else {
row.lifecycle = '';
}
add(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
window.console.log(error);
}
);
},
rowUpdate(row, index, done, loading) {
if (func.isArrayAndNotEmpty(row.lifecycle)) {
const lifecycleStart = row.lifecycle[0];
const lifecycleEnd = row.lifecycle[1];
if (!func.isUndefined(lifecycleStart) && !func.isUndefined(lifecycleEnd)) {
row.lifecycle = lifecycleStart + ',' + lifecycleEnd;
}
} else {
row.lifecycle = '';
}
update(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
console.log(error);
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(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();
});
},
handleSync() {
this.$confirm('确定进行数据双向同步?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return sync();
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleRun(row) {
this.$confirm('运行后将创建一个实例执行,是否继续?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return run(row);
})
.then(() => {
this.$message({
type: 'success',
message: '运行成功!',
});
});
},
slotChange(row) {
if (!row.id) {
return;
}
change(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
},
error => {
window.console.log(error);
}
);
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
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();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page) {
this.loading = true;
const { jobServerId, jobName } = this.query;
let values = {
jobServerId_like: jobServerId,
jobName_like: jobName,
};
getList(page.currentPage, page.pageSize, values).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style></style>

236
src/views/job/jobserver.vue Normal file
View File

@@ -0,0 +1,236 @@
<template>
<basic-container>
<avue-crud
:option="option"
v-model:search="search"
v-model:page="page"
v-model="form"
:table-loading="loading"
:data="data"
:permission="permissionList"
:before-open="beforeOpen"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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="permission.jobserver_delete"
@click="handleDelete"
>
</el-button>
<el-button type="info" plain icon="el-icon-sort" @click="handleSync"
>
</el-button>
</template>
<template #menu="scope">
<el-button type="primary" text icon="el-icon-video-play" @click="handleLink(scope.row)"
>服务跳转
</el-button>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove, sync } from '@/api/job/jobserver';
import option from '@/option/job/jobserver';
import { mapGetters } from 'vuex';
import 'nprogress/nprogress.css';
import func from '@/utils/func';
export default {
data() {
return {
form: {},
query: {},
search: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: option,
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.jobserver_add, false),
viewBtn: this.validData(this.permission.jobserver_view, false),
delBtn: this.validData(this.permission.jobserver_delete, false),
editBtn: this.validData(this.permission.jobserver_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
rowSave(row, done, loading) {
add(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
window.console.log(error);
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
console.log(error);
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(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();
});
},
handleLink(row) {
window.open(func.formatUrl(row.jobServerUrl));
},
handleSync() {
this.$confirm('确定进行数据双向同步?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return sync();
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
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();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page) {
this.loading = true;
const { jobServerName, jobAppName } = this.query;
let values = {
jobServerName_like: jobServerName,
jobAppName_like: jobAppName,
};
getList(page.currentPage, page.pageSize, values).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,162 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
:permission="permissionList"
:before-open="beforeOpen"
v-model:page="page"
@search-change="searchChange"
@search-reset="searchReset"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
</avue-crud>
</basic-container>
</template>
<script>
import { getApiList, getApiLogs } from '@/api/logs';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 120,
dialogType: 'drawer',
column: [
{
label: '服务id',
prop: 'serviceId',
width: '120',
search: true,
},
{
label: '服务host',
prop: 'serverHost',
search: true,
},
{
label: '服务ip',
prop: 'serverIp',
width: '160',
},
{
label: '软件环境',
prop: 'env',
width: '85',
},
{
label: '日志名',
prop: 'title',
},
{
label: '请求方法',
prop: 'method',
width: '85',
},
{
label: '请求接口',
prop: 'requestUri',
},
{
label: '日志时间',
prop: 'createTime',
width: '180',
},
{
label: '用户代理',
prop: 'userAgent',
span: 24,
hide: true,
},
{
label: '请求数据',
prop: 'params',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
viewBtn: this.validData(this.permission.log_api_view, false),
};
},
},
methods: {
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getApiLogs(this.form.id).then(res => {
this.form = res.data.data;
});
}
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;
getApiList(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;
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,166 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
:before-open="beforeOpen"
v-model="form"
:permission="permissionList"
v-model:page="page"
@search-change="searchChange"
@search-reset="searchReset"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
</avue-crud>
</basic-container>
</template>
<script>
import { getErrorList, getErrorLogs } from '@/api/logs';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 120,
dialogType: 'drawer',
column: [
{
label: '服务id',
prop: 'serviceId',
search: true,
width: '120',
},
{
label: '服务host',
prop: 'serverHost',
search: true,
width: '150',
},
{
label: '服务ip',
prop: 'serverIp',
width: '160',
},
{
label: '软件环境',
prop: 'env',
width: '85',
},
{
label: '请求方法',
prop: 'method',
width: '85',
},
{
label: '请求接口',
prop: 'requestUri',
},
{
label: '日志时间',
prop: 'createTime',
width: '180',
},
{
label: '用户代理',
prop: 'userAgent',
span: 24,
hide: true,
},
{
label: '请求数据',
prop: 'params',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
},
{
label: '日志数据',
prop: 'stackTrace',
type: 'textarea',
span: 24,
minRows: 16,
hide: true,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
viewBtn: this.validData(this.permission.log_error_view, false),
};
},
},
methods: {
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getErrorLogs(this.form.id).then(res => {
this.form = res.data.data;
});
}
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;
getErrorList(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;
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,166 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
:permission="permissionList"
v-model:page="page"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="searchReset"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
</avue-crud>
</basic-container>
</template>
<script>
import { getUsualList, getUsualLogs } from '@/api/logs';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 120,
dialogType: 'drawer',
column: [
{
label: '服务id',
prop: 'serviceId',
search: true,
},
{
label: '服务host',
prop: 'serverHost',
search: true,
},
{
label: '服务ip',
prop: 'serverIp',
},
{
label: '软件环境',
prop: 'env',
width: '100',
},
{
label: '日志级别',
prop: 'logLevel',
},
{
label: '日志id',
prop: 'logId',
},
{
label: '请求接口',
prop: 'requestUri',
},
{
label: '日志时间',
prop: 'createTime',
width: '180',
},
{
label: '用户代理',
prop: 'userAgent',
span: 24,
hide: true,
},
{
label: '日志数据',
prop: 'logData',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
},
{
label: '请求数据',
prop: 'params',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
viewBtn: this.validData(this.permission.log_usual_view, false),
};
},
},
methods: {
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getUsualLogs(this.form.id).then(res => {
this.form = res.data.data;
});
}
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;
getUsualList(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;
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,210 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
v-model:page="page"
:permission="permissionList"
@row-del="rowDel"
@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 @click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-edit"
@click.stop="handleDesign(scope.row.name)"
v-if="userInfo.authority.includes('admin')"
>设计
</el-button>
<el-button
type="primary"
text
icon="el-icon-view"
@click.stop="handlePreview(scope.row.name)"
v-if="userInfo.authority.includes('admin')"
>预览
</el-button>
</template>
<template #name="{ row }">
<el-tag style="cursor: pointer" @click="handlePreview(row.name)">{{ row.name }}</el-tag>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, remove } from '@/api/report/report';
import { getToken } from '@/utils/auth';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
viewBtn: true,
dialogClickModal: false,
column: [
{
label: '文件名',
prop: 'name',
search: true,
slot: true,
},
{
label: '创建时间',
prop: 'createTime',
},
{
label: '更新时间',
prop: 'updateTime',
},
],
},
data: [],
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: false,
viewBtn: false,
delBtn: true,
editBtn: false,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
handlePreview(name) {
window.open(
`${this.website.design.reportUrl}/preview?_u=blade-${name}&${
this.website.tokenHeader
}=bearer ${getToken()}`
);
},
handleDesign(name) {
window.open(
`${this.website.design.reportUrl}/designer?_u=blade-${name}&${
this.website.tokenHeader
}=bearer ${getToken()}`
);
},
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();
},
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 => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,343 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
ref="crud"
@row-del="rowDel"
@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="primary"
plain
v-if="permission.attach_upload"
icon="el-icon-upload"
@click="handleUpload"
>
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="permission.attach_delete"
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-download"
v-if="permission.attach_download"
@click="handleDownload(scope.row)"
>下载
</el-button>
</template>
<template #attachSize="{ row }">
<el-tag>{{ formatFileSize(row.attachSize) }}</el-tag>
</template>
</avue-crud>
<el-dialog title="附件管理" append-to-body v-model="attachBox" width="555px">
<el-upload
drag
action="/api/blade-resource/oss/endpoint/put-file-attach"
:show-file-list="false"
:headers="uploadHeaders"
:on-progress="handleUploadProgress"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">拖动文件上传 <em>或者点击上传</em></div>
</el-upload>
<el-progress
v-if="uploading"
:percentage="uploadPercent"
:indeterminate="uploadPercent === 100"
:format="progressFormat"
style="margin-top: 16px"
/>
</el-dialog>
</basic-container>
</template>
<script>
import { getList, getDetail, remove } from '@/api/resource/attach';
import { mapGetters } from 'vuex';
import { getToken } from '@/utils/auth';
import { UploadFilled } from '@element-plus/icons-vue';
import func from '@/utils/func';
export default {
components: {
UploadFilled,
},
data() {
return {
form: {},
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
attachBox: false,
uploading: false,
uploadPercent: 0,
selectionList: [],
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
grid: true,
selection: true,
dialogClickModal: false,
column: [
{
label: '附件地址',
prop: 'link',
rules: [
{
required: true,
message: '请输入附件地址',
trigger: 'blur',
},
],
},
{
label: '附件域名',
prop: 'domainUrl',
search: true,
rules: [
{
required: true,
message: '请输入附件域名',
trigger: 'blur',
},
],
},
{
label: '附件名称',
prop: 'name',
search: true,
rules: [
{
required: true,
message: '请输入附件名称',
trigger: 'blur',
},
],
},
{
label: '附件原名',
prop: 'originalName',
search: true,
rules: [
{
required: true,
message: '请输入附件原名',
trigger: 'blur',
},
],
},
{
label: '附件拓展名',
prop: 'extension',
rules: [
{
required: true,
message: '请输入附件拓展名',
trigger: 'blur',
},
],
},
{
label: '附件大小',
prop: 'attachSize',
slot: true,
rules: [
{
required: true,
message: '请输入附件大小',
trigger: 'blur',
},
],
},
],
},
data: [],
};
},
computed: {
func() {
return func;
},
...mapGetters(['permission']),
permissionList() {
return {
addBtn: false,
editBtn: false,
viewBtn: false,
delBtn: this.validData(this.permission.attach_delete, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 KB';
const mb = bytes / (1024 * 1024);
if (mb >= 1) {
return `${mb.toFixed(2)} MB`;
}
const kb = bytes / 1024;
return `${kb.toFixed(2)} KB`;
},
progressFormat(percentage) {
return percentage === 100 ? '服务器处理中' : `${percentage}%`;
},
handleUpload() {
this.attachBox = true;
this.uploading = false;
this.uploadPercent = 0;
},
handleUploadProgress(event) {
this.uploading = true;
this.uploadPercent = Math.round(event.percent) || 0;
},
handleUploadSuccess(response) {
this.uploading = false;
this.uploadPercent = 0;
if (response.success) {
this.$message.success('上传成功');
this.attachBox = false;
this.refreshChange();
} else {
this.$message.error(response.msg || '上传失败');
}
},
handleUploadError() {
this.uploading = false;
this.uploadPercent = 0;
this.$message.error('上传失败');
},
handleDownload(row) {
window.open(`${row.link}`);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(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();
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
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();
},
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();
});
},
},
};
</script>
<style></style>

469
src/views/resource/oss.vue Normal file
View File

@@ -0,0 +1,469 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
:permission="permissionList"
v-model="form"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
:before-open="beforeOpen"
@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="permission.oss_delete"
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-video-play"
v-if="userInfo.authority.includes('admin')"
@click="handleDebug(scope.row)"
>调试
</el-button>
<el-button
type="primary"
text
icon="el-icon-circle-check"
v-if="permission.oss_enable"
@click.stop="handleEnable(scope.row)"
>启用
</el-button>
</template>
<template #status="{ row }">
<el-tag>{{ row.statusName }}</el-tag>
</template>
<template #category="{ row }">
<el-tag>{{ row.categoryName }}</el-tag>
</template>
</avue-crud>
<el-dialog title="对象存储上传调试" append-to-body v-model="box" width="550px">
<avue-form ref="form" :option="debugOption" v-model="debugForm" @submit="handleSubmit" />
</el-dialog>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove, enable } from '@/api/resource/oss';
import { mapGetters } from 'vuex';
import func from '@/utils/func';
import { sensitive } from '@/utils/sensitive';
export default {
data() {
return {
form: {},
query: {},
sensitiveManager: null,
loading: true,
box: false,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
grid: true,
menuWidth: 350,
labelWidth: 100,
dialogWidth: 880,
dialogClickModal: false,
column: [
{
label: '分类',
type: 'radio',
value: 1,
span: 24,
width: 120,
searchLabelWidth: 50,
row: true,
dicUrl: '/blade-system/dict/dictionary?code=oss',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
rules: [
{
required: true,
message: '请选择分类',
trigger: 'blur',
},
],
},
{
label: '资源编号',
prop: 'ossCode',
span: 24,
width: 120,
search: true,
rules: [
{
required: true,
message: '请输入资源编号',
trigger: 'blur',
},
],
},
{
label: '资源地址',
labelTip: '对象存储通用资源地址,可以是内网也可以是外网',
prop: 'endpoint',
span: 24,
rules: [
{
required: true,
message: '请输入资源地址',
trigger: 'blur',
},
],
},
{
label: '外网地址',
labelTip: '资源地址设置为内网上传,则外部访问需要配置外网映射地址',
prop: 'transformEndpoint',
span: 24,
},
{
label: '空间名',
prop: 'bucketName',
span: 24,
width: 120,
rules: [
{
required: true,
message: '请输入空间名',
trigger: 'blur',
},
],
},
{
label: 'accessKey',
prop: 'accessKey',
span: 24,
search: true,
width: 200,
overHidden: true,
rules: [
{
required: true,
message: '请输入accessKey',
trigger: 'blur',
},
],
},
{
label: 'secretKey',
prop: 'secretKey',
span: 24,
width: 200,
hide: true,
overHidden: true,
rules: [
{
required: true,
message: '请输入secretKey',
trigger: 'blur',
},
],
},
{
label: 'appId',
prop: 'appId',
span: 24,
hide: true,
display: false,
},
{
label: 'region',
prop: 'region',
span: 24,
hide: true,
display: false,
},
{
label: '是否启用',
prop: 'status',
span: 24,
width: 85,
align: 'center',
slot: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '备注',
prop: 'remark',
span: 24,
hide: true,
},
],
},
data: [],
debugForm: {
code: '',
},
debugOption: {
submitText: '提交',
column: [
{
label: '资源编号',
prop: 'code',
disabled: true,
span: 24,
},
{
label: '上传背景',
prop: 'backgroundUrl',
type: 'upload',
listType: 'picture-img',
dataType: 'string',
action: '/blade-resource/oss/endpoint/put-file',
propsHttp: {
res: 'data',
url: 'link',
},
span: 24,
},
],
},
};
},
watch: {
'form.category'() {
const category = func.toInt(this.form.category);
this.$refs.crud.option.column.filter(item => {
if (item.prop === 'endpoint') {
item.labelTip = category === 7 ? '本地服务器上传路径使用Nginx代理后的服务地址' : '';
}
if (item.prop === 'accessKey') {
item.labelTip =
category === 7
? '本地服务器文件上传基础路径,注意必须提前创建好并分配目录读写权限'
: '';
}
if (item.prop === 'secretKey') {
item.display = category !== 7;
}
if (item.prop === 'appId') {
item.display = category === 4;
}
if (item.prop === 'region') {
item.display = category === 4 || category === 5;
}
});
},
'debugForm.code'() {
const column = this.findColumn(this.debugOption.column, 'backgroundUrl');
column.action = `/blade-resource/oss/endpoint/put-file?code=${this.debugForm.code}`;
},
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.oss_add),
viewBtn: this.validData(this.permission.oss_view),
delBtn: this.validData(this.permission.oss_delete),
editBtn: this.validData(this.permission.oss_edit),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
created() {
// 创建脱敏工具实例
this.sensitiveManager = sensitive.create({
fields: ['accessKey', 'secretKey'], // 配置需要脱敏的字段
});
},
methods: {
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) {
// 获取需要提交的数据
const submitData = this.sensitiveManager.getSubmitData(row);
update(submitData).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();
},
handleEnable(row) {
this.$confirm('是否确定启用这条配置?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return enable(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
handleDebug(row) {
this.box = true;
this.debugForm.code = row.ossCode;
this.debugForm.backgroundUrl = '';
},
handleSubmit(form, done) {
this.$message({
type: 'success',
message: `获取到图片地址:[${form.backgroundUrl}]`,
});
done();
},
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 (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
// 保存初始脱敏数据
this.sensitiveManager.saveInitialData(this.form);
});
}
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();
});
},
},
};
</script>

501
src/views/resource/sms.vue Normal file
View File

@@ -0,0 +1,501 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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="permission.sms_delete"
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-video-play"
v-if="userInfo.authority.includes('admin')"
@click="handleDebug(scope.row)"
>调试
</el-button>
<el-button
type="primary"
text
icon="el-icon-circle-check"
v-if="permission.sms_enable"
@click.stop="handleEnable(scope.row)"
>启用
</el-button>
</template>
<template #status="{ row }">
<el-tag>{{ row.statusName }}</el-tag>
</template>
<template #category="{ row }">
<el-tag>{{ row.categoryName }}</el-tag>
</template>
</avue-crud>
<el-dialog title="手机短信发送调试" append-to-body v-model="box" width="550px">
<avue-form :option="debugOption" v-model="debugForm" @submit="handleSend" />
</el-dialog>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove, enable, send } from '@/api/resource/sms';
import { mapGetters } from 'vuex';
import func from '@/utils/func';
import { sensitive } from '@/utils/sensitive';
export default {
data() {
return {
form: {},
query: {},
sensitiveManager: null,
loading: true,
box: false,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
grid: true,
menuWidth: 350,
labelWidth: 100,
dialogWidth: 880,
dialogClickModal: false,
column: [
{
label: '分类',
type: 'radio',
value: 1,
span: 24,
width: 100,
searchLabelWidth: 50,
row: true,
dicUrl: '/blade-system/dict/dictionary?code=sms',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
rules: [
{
required: true,
message: '请选择分类',
trigger: 'blur',
},
],
},
{
label: '资源编号',
prop: 'smsCode',
span: 24,
width: 200,
search: true,
rules: [
{
required: true,
message: '请输入资源编号',
trigger: 'blur',
},
],
},
{
label: '模版ID',
prop: 'templateId',
span: 24,
width: 200,
search: true,
rules: [
{
required: true,
message: '请输入模版ID',
trigger: 'blur',
},
],
},
{
label: 'SDKAppID',
prop: 'appId',
span: 24,
width: 200,
hide: true,
display: false,
rules: [
{
required: true,
message: '请输入appId',
trigger: 'blur',
},
],
},
{
label: 'accessKey',
prop: 'accessKey',
span: 24,
overHidden: true,
hide: true,
rules: [
{
required: true,
message: '请输入accessKey',
trigger: 'blur',
},
],
},
{
label: 'secretKey',
prop: 'secretKey',
span: 24,
overHidden: true,
display: true,
hide: true,
rules: [
{
required: true,
message: '请输入secretKey',
trigger: 'blur',
},
],
},
{
label: 'regionId',
prop: 'regionId',
span: 24,
hide: true,
display: false,
rules: [
{
required: true,
message: '请输入regionId',
trigger: 'blur',
},
],
},
{
label: '短信签名',
prop: 'signName',
span: 24,
width: 200,
rules: [
{
required: true,
message: '请输入短信签名',
trigger: 'blur',
},
],
},
{
label: '是否启用',
prop: 'status',
span: 24,
width: 85,
align: 'center',
slot: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '备注',
prop: 'remark',
span: 24,
hide: true,
},
],
},
data: [],
debugForm: {
code: '',
},
debugOption: {
submitText: '发送',
column: [
{
label: '资源编号',
prop: 'code',
disabled: true,
span: 24,
},
{
label: '发送手机',
prop: 'phones',
span: 24,
},
{
label: '发送参数',
prop: 'params',
span: 24,
placeholder: "例: {'code':2333,'title':'通知标题'}",
},
],
},
};
},
watch: {
'form.category'() {
const category = func.toInt(this.form.category);
this.$refs.crud.option.column.filter(item => {
if (item.prop === 'templateId') {
if (category === 1) {
item.label = '模版内容';
} else {
item.label = '模版ID';
}
}
if (item.prop === 'accessKey') {
if (category === 1) {
item.label = 'apiKey';
} else if (category === 4) {
item.label = 'secretId';
} else {
item.label = 'accessKey';
}
}
if (item.prop === 'secretKey') {
item.display = category !== 1;
if (category === 4) {
item.label = 'secretKey';
} else {
item.label = 'secretKey';
}
}
if (item.prop === 'regionId') {
if (category === 3) {
item.display = true;
this.form.regionId = 'cn-hangzhou';
} else if (category === 4) {
item.display = true;
this.form.regionId = 'ap-guangzhou';
} else {
item.display = false;
}
}
if (item.prop === 'appId') {
item.display = category === 4;
}
});
},
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.sms_add, false),
viewBtn: this.validData(this.permission.sms_view, false),
delBtn: this.validData(this.permission.sms_delete, false),
editBtn: this.validData(this.permission.sms_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
created() {
// 创建脱敏工具实例
this.sensitiveManager = sensitive.create({
fields: ['accessKey', 'secretKey'], // 配置需要脱敏的字段
});
},
methods: {
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) {
// 获取需要提交的数据
const submitData = this.sensitiveManager.getSubmitData(row);
update(submitData).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: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
handleDebug(row) {
this.box = true;
this.debugForm.code = row.smsCode;
},
handleSend(form, done) {
send(form.code, form.phones, form.params).then(
res => {
this.$message({
type: 'success',
message: '发送成功!',
});
done();
window.console.log(res);
this.box = false;
},
error => {
window.console.log(error);
done();
}
);
},
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 (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
// 保存初始脱敏数据
this.sensitiveManager.saveInitialData(this.form);
});
}
done();
},
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();
},
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();
});
},
},
};
</script>

View File

@@ -0,0 +1,303 @@
<template>
<!-- 账号锁定配置抽屉 -->
<el-drawer title="账号锁定配置" append-to-body v-model="authLockBox" size="60%">
<avue-crud
:option="authLockOption"
:table-loading="authLockLoading"
:data="authLockData"
ref="authLockCrud"
v-model="authLockForm"
v-model:page="authLockPage"
v-model:search="authLockSearch"
:before-open="authLockBeforeOpen"
@search-change="authLockSearchChange"
@search-reset="authLockSearchReset"
@current-change="authLockCurrentChange"
@size-change="authLockSizeChange"
@refresh-change="authLockRefreshChange"
@on-load="authLockOnLoad"
>
<template #lockTarget="{ row }">
<el-tag type="primary" size="small" effect="light">{{ row.lockTarget }}</el-tag>
</template>
<template #userName="{ row }">
{{ row.userName || '-' }}
</template>
<template #remoteIp="{ row }">
<el-tag type="primary" size="small" effect="light">{{ row.remoteIp || '暂无' }}</el-tag>
</template>
<template #lockType="{ row }">
<el-tag :type="row.lockType === 'SYSTEM' ? 'info' : 'danger'" size="small">
{{ row.lockTypeName }}
</el-tag>
</template>
<template #lockStatus="{ row }">
<el-tag
:type="row.expired ? 'info' : row.lockStatus === 'LOCKED' ? 'danger' : row.lockStatus === 'PRE_LOCKED' ? 'warning' : 'success'"
size="small"
>
{{ row.lockStatusName }}
</el-tag>
</template>
<template #lockBeginTime="{ row }">
{{ row.lockBeginTime || '立即生效' }}
</template>
<template #lockEndTime="{ row }">
{{ row.lockEndTime || '永久' }}
</template>
<template #menu="{ row }">
<el-button
text
type="primary"
icon="el-icon-view"
@click.stop="$refs.authLockCrud.rowView(row)"
>查看</el-button
>
<el-button
v-if="(row.lockStatus === 'LOCKED' || row.lockStatus === 'PRE_LOCKED') && !row.expired"
text
type="primary"
icon="el-icon-key"
@click.stop="handleAuthLockUnlock(row)"
>解锁</el-button
>
</template>
</avue-crud>
</el-drawer>
<!-- 手动锁定用户 -->
<el-dialog title="锁定用户" append-to-body v-model="lockUserBox" width="500px">
<el-form :model="lockUserForm" label-width="100px" :rules="lockUserRules" ref="lockUserFormRef">
<el-form-item label="用户账号">
<el-input :model-value="lockUserForm.account" disabled />
</el-form-item>
<el-form-item label="锁定原因" prop="lockReason">
<el-input
v-model="lockUserForm.lockReason"
type="textarea"
:rows="3"
placeholder="请输入锁定原因"
/>
</el-form-item>
<el-form-item label="锁定开始" prop="lockBeginTime">
<el-date-picker
v-model="lockUserForm.lockBeginTime"
type="datetime"
placeholder="不填则立即生效"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="锁定结束" prop="lockEndTime">
<el-date-picker
v-model="lockUserForm.lockEndTime"
type="datetime"
placeholder="不填则永久锁定"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="lockUserBox = false"> </el-button>
<el-button type="primary" :loading="lockSubmitLoading" @click="submitLockUser"> </el-button>
</span>
</template>
</el-dialog>
<!-- 解锁原因确认 -->
<el-dialog title="解锁确认" append-to-body v-model="unlockReasonBox" width="450px">
<el-form :model="unlockReasonForm" label-width="80px">
<el-form-item label="解锁原因">
<el-input
v-model="unlockReasonForm.unlockReason"
type="textarea"
:rows="3"
placeholder="请输入解锁原因(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="unlockReasonBox = false"> </el-button>
<el-button type="primary" :loading="unlockSubmitLoading" @click="submitUnlockReason"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import {
getAuthLockPage,
getAuthLockDetail,
authLockUnlock,
lockUser,
} from '@/api/system/authlock';
import { validatenull } from '@/utils/validate';
import { authLockOption } from '@/option/system/authlock';
export default {
name: 'AuthLock',
emits: ['lock-success'],
data() {
return {
authLockOption,
// 锁定日志抽屉
authLockBox: false,
authLockLoading: false,
authLockData: [],
authLockForm: {},
authLockSearch: {},
authLockPage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
// 手动锁定用户
lockUserBox: false,
lockUserForm: {},
lockUserRules: {
lockReason: [{ required: true, message: '请输入锁定原因', trigger: 'blur' }],
lockEndTime: [
{
validator: (rule, value, callback) => {
if (!value) {
callback();
} else if (this.lockUserForm.lockBeginTime && value < this.lockUserForm.lockBeginTime) {
callback(new Error('结束时间不能早于开始时间'));
} else if (!this.lockUserForm.lockBeginTime && value < new Date().toISOString().slice(0, 19).replace('T', ' ')) {
callback(new Error('结束时间不能早于当前时间'));
} else {
callback();
}
},
trigger: 'change',
},
],
},
// 解锁确认
unlockReasonBox: false,
unlockReasonForm: {},
currentUnlockId: null,
lockSubmitLoading: false,
unlockSubmitLoading: false,
};
},
methods: {
// ========== 对外暴露的入口方法 ==========
openLockLog() {
this.authLockSearch = {};
this.authLockPage.currentPage = 1;
this.authLockBox = true;
this.$nextTick(() => {
this.authLockOnLoad(this.authLockPage);
});
},
openLockUser(userId, account) {
this.lockUserForm = {
userId,
account,
lockReason: '',
lockBeginTime: '',
lockEndTime: '',
};
this.lockUserBox = true;
},
// ========== 锁定日志列表 ==========
authLockBeforeOpen(done, type) {
if (type === 'view') {
getAuthLockDetail(this.authLockForm.id).then(res => {
this.authLockForm = res.data.data;
});
}
done();
},
authLockSearchChange(params, done) {
this.authLockSearch = params;
this.authLockPage.currentPage = 1;
this.authLockOnLoad(this.authLockPage, params);
done();
},
authLockSearchReset() {
this.authLockSearch = {};
this.authLockPage.currentPage = 1;
this.authLockOnLoad(this.authLockPage);
},
authLockSizeChange(pageSize) {
this.authLockPage.pageSize = pageSize;
},
authLockCurrentChange(currentPage) {
this.authLockPage.currentPage = currentPage;
},
authLockRefreshChange() {
this.authLockOnLoad(this.authLockPage, this.authLockSearch);
},
authLockOnLoad(page, params = {}) {
this.authLockLoading = true;
const merged = Object.assign({}, params, this.authLockSearch);
const query = Object.fromEntries(Object.entries(merged).filter(([, val]) => !validatenull(val)));
getAuthLockPage(page.currentPage, page.pageSize, query)
.then(res => {
const data = res.data.data;
this.authLockPage.total = data.total;
this.authLockData = data.records;
this.authLockLoading = false;
})
.catch(() => {
this.authLockLoading = false;
});
},
// ========== 解锁操作 ==========
handleAuthLockUnlock(row) {
this.currentUnlockId = row.id;
this.unlockReasonForm = { unlockReason: '' };
this.unlockReasonBox = true;
},
submitUnlockReason() {
this.unlockSubmitLoading = true;
authLockUnlock(this.currentUnlockId, this.unlockReasonForm.unlockReason)
.then(() => {
this.unlockSubmitLoading = false;
this.unlockReasonBox = false;
this.$message({ type: 'success', message: '解锁成功!' });
this.authLockOnLoad(this.authLockPage, this.authLockSearch);
})
.catch(() => {
this.unlockSubmitLoading = false;
this.$message({ type: 'error', message: '解锁失败,请重试!' });
});
},
// ========== 手动锁定用户 ==========
submitLockUser() {
this.$refs.lockUserFormRef.validate(valid => {
if (!valid) return;
this.lockSubmitLoading = true;
lockUser(
this.lockUserForm.userId,
this.lockUserForm.lockReason,
this.lockUserForm.lockBeginTime,
this.lockUserForm.lockEndTime
)
.then(() => {
this.lockSubmitLoading = false;
this.lockUserBox = false;
this.$message({ type: 'success', message: '锁定成功!' });
this.$emit('lock-success');
})
.catch(() => {
this.lockSubmitLoading = false;
this.$message({ type: 'error', message: '锁定失败,请重试!' });
});
});
},
},
};
</script>

View File

@@ -0,0 +1,155 @@
<template>
<!-- 认证日志抽屉 -->
<el-drawer :title="authLogTitle" append-to-body v-model="authLogBox" size="60%">
<avue-crud
:option="authLogOption"
:table-loading="authLogLoading"
:data="authLogData"
ref="authLogCrud"
v-model="authLogForm"
v-model:page="authLogPage"
v-model:search="authLogSearch"
:before-open="authLogBeforeOpen"
@search-change="authLogSearchChange"
@search-reset="authLogSearchReset"
@current-change="authLogCurrentChange"
@size-change="authLogSizeChange"
@refresh-change="authLogRefreshChange"
@on-load="authLogOnLoad"
>
<template #menu="{ row }">
<el-button
text
type="primary"
icon="el-icon-view"
@click.stop="$refs.authLogCrud.rowView(row)"
>查看</el-button
>
</template>
<template #account="{ row }">
<el-tag size="small" effect="light">{{ row.account }}</el-tag>
</template>
<template #grantType="{ row }">
<el-tag size="small" effect="light">{{ getGrantTypeName(row.grantType) }}</el-tag>
</template>
<template #remoteIp="{ row }">
<el-tag size="small" effect="light">{{ row.remoteIp || '-' }}</el-tag>
</template>
<template #env="{ row }">
<el-tag
:type="row.env === 'prod' ? 'danger' : row.env === 'test' ? 'warning' : row.env === 'dev' ? 'success' : 'info'"
size="small"
>{{ row.env || '-' }}</el-tag>
</template>
</avue-crud>
</el-drawer>
</template>
<script>
import { getAuthLogPage, getAuthLogDetail } from '@/api/system/authlog';
import { validatenull } from '@/utils/validate';
import { authLogOption, GRANT_TYPE_DIC } from '@/option/system/authlog';
export default {
name: 'AuthLog',
data() {
return {
authLogOption,
authLogTitle: '认证日志',
// 抽屉状态
authLogBox: false,
authLogLoading: false,
authLogData: [],
authLogForm: {},
authLogSearch: {},
authLogPage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
// 用户ID过滤
filterUserId: null,
};
},
methods: {
// ========== 对外暴露的入口方法 ==========
openAuthLog() {
this.filterUserId = null;
this.authLogTitle = '认证日志';
this.authLogSearch = {};
this.authLogPage.currentPage = 1;
this.authLogBox = true;
this.$nextTick(() => {
this.authLogOnLoad(this.authLogPage);
});
},
openUserAuthLog(userId, userName) {
this.filterUserId = userId;
this.authLogTitle = userName ? `认证日志 - ${userName}` : '认证日志';
this.authLogSearch = {};
this.authLogPage.currentPage = 1;
this.authLogBox = true;
this.$nextTick(() => {
this.authLogOnLoad(this.authLogPage);
});
},
// ========== 授权类型转译 ==========
getGrantTypeName(grantType) {
const item = GRANT_TYPE_DIC.find(d => d.value === grantType);
return item ? item.label : grantType || '-';
},
// ========== 认证日志列表 ==========
authLogBeforeOpen(done, type) {
if (type === 'view') {
getAuthLogDetail(this.authLogForm.id).then(res => {
this.authLogForm = res.data.data;
});
}
done();
},
authLogSearchChange(params, done) {
this.authLogSearch = params;
this.authLogPage.currentPage = 1;
this.authLogOnLoad(this.authLogPage, params);
done();
},
authLogSearchReset() {
this.authLogSearch = {};
this.authLogPage.currentPage = 1;
this.authLogOnLoad(this.authLogPage);
},
authLogSizeChange(pageSize) {
this.authLogPage.pageSize = pageSize;
},
authLogCurrentChange(currentPage) {
this.authLogPage.currentPage = currentPage;
},
authLogRefreshChange() {
this.authLogOnLoad(this.authLogPage, this.authLogSearch);
},
authLogOnLoad(page, params = {}) {
this.authLogLoading = true;
const merged = Object.assign({}, params, this.authLogSearch);
if (this.filterUserId) {
merged.userId = this.filterUserId;
}
const query = Object.fromEntries(Object.entries(merged).filter(([, val]) => !validatenull(val)));
getAuthLogPage(page.currentPage, page.pageSize, query)
.then(res => {
const data = res.data.data;
this.authLogPage.total = data.total;
this.authLogData = data.records;
this.authLogLoading = false;
})
.catch(() => {
this.authLogLoading = false;
});
},
},
};
</script>

385
src/views/system/client.vue Normal file
View File

@@ -0,0 +1,385 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
@row-del="rowDel"
v-model="form"
ref="crud"
:permission="permissionList"
@row-update="rowUpdate"
@row-save="rowSave"
:before-open="beforeOpen"
@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="permission.client_delete"
@click="handleDelete"
>
</el-button>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove } from '@/api/system/client';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
grid: true,
selection: true,
dialogClickModal: false,
column: [
{
label: '应用id',
prop: 'clientId',
search: true,
rules: [
{
required: true,
message: '请输入客户端id',
trigger: 'blur',
},
],
},
{
label: '应用密钥',
prop: 'clientSecret',
search: true,
rules: [
{
required: true,
message: '请输入客户端密钥',
trigger: 'blur',
},
],
},
{
label: '授权类型',
prop: 'authorizedGrantTypes',
type: 'checkbox',
value: 'refresh_token,password,authorization_code,captcha,social,sms_code,register',
dicData: [
{
label: 'refresh_token',
value: 'refresh_token',
},
{
label: 'password',
value: 'password',
},
{
label: 'authorization_code',
value: 'authorization_code',
},
{
label: 'captcha',
value: 'captcha',
},
{
label: 'social',
value: 'social',
},
{
label: 'sms_code',
value: 'sms_code',
},
{
label: 'register',
value: 'register',
},
],
rules: [
{
required: true,
message: '请输入授权类型',
trigger: 'blur',
},
],
},
{
label: '授权范围',
prop: 'scope',
value: 'all',
rules: [
{
required: true,
message: '请输入授权范围',
trigger: 'blur',
},
],
},
{
label: '令牌秒数',
prop: 'accessTokenValidity',
type: 'number',
value: 3600,
rules: [
{
required: true,
message: '请输入令牌过期秒数',
trigger: 'blur',
},
],
},
{
label: '刷新秒数',
prop: 'refreshTokenValidity',
type: 'number',
value: 604800,
hide: true,
rules: [
{
required: true,
message: '请输入刷新令牌过期秒数',
trigger: 'blur',
},
],
},
{
label: '回调地址',
prop: 'webServerRedirectUri',
hide: true,
rules: [
{
required: true,
message: '请输入回调地址',
trigger: 'blur',
},
],
},
{
label: '自动授权',
prop: 'autoapprove',
hide: true,
type: 'radio',
dicData: [
{
label: '是',
value: 'true',
},
{
label: '否',
value: 'false',
},
],
value: 'true',
rules: [
{
required: true,
message: '请输入自动授权',
trigger: 'blur',
},
],
},
{
label: '资源集合',
prop: 'resourceIds',
hide: true,
rules: [
{
message: '请输入资源集合',
trigger: 'blur',
},
],
},
{
label: '权限',
prop: 'authorities',
hide: true,
rules: [
{
message: '请输入权限',
trigger: 'blur',
},
],
},
{
label: '附加说明',
hide: true,
prop: 'additionalInformation',
span: 24,
rules: [
{
message: '请输入附加说明',
trigger: 'blur',
},
],
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.client_add),
viewBtn: this.validData(this.permission.client_view),
delBtn: this.validData(this.permission.client_delete),
editBtn: this.validData(this.permission.client_edit),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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();
},
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 (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
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();
});
},
},
};
</script>
<style></style>

404
src/views/system/dept.vue Normal file
View File

@@ -0,0 +1,404 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
:permission="permissionList"
:before-open="beforeOpen"
:before-close="beforeClose"
@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"
@tree-load="treeLoad"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dept_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-circle-plus"
@click.stop="handleAdd(scope.row, scope.index)"
v-if="userInfo.authority.includes('admin')"
>新增子项
</el-button>
</template>
<template #deptCategory="{ row }">
<el-tag>{{ row.deptCategoryName }}</el-tag>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getLazyList, remove, update, add, getDept, getDeptTree } from '@/api/system/dept';
import { getLeaderList } from '@/api/system/user';
import { mapGetters } from 'vuex';
import website from '@/config/website';
import func from '@/utils/func';
export default {
data() {
return {
form: {},
selectionList: [],
query: {},
loading: true,
parentId: 0,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
lazy: true,
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
menuWidth: 320,
dialogWidth: 800,
dialogClickModal: false,
column: [
{
label: '机构名称',
prop: 'deptName',
search: true,
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: '机构全称',
prop: 'fullName',
search: true,
rules: [
{
required: true,
message: '请输入机构全称',
trigger: 'blur',
},
],
},
{
label: '上级机构',
prop: 'parentId',
dicData: [],
type: 'tree',
hide: true,
addDisabled: false,
props: {
label: 'title',
},
rules: [
{
required: false,
message: '请选择上级机构',
trigger: 'click',
},
],
},
{
label: '直属主管',
prop: 'leaderId',
type: 'tree',
multiple: true,
filterable: true,
dicData: [],
props: {
label: 'realName',
value: 'id',
},
clearable: true,
hide: true,
},
{
label: '机构类型',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=org_category',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
width: 120,
prop: 'deptCategory',
slot: true,
rules: [
{
required: true,
message: '请输入机构类型',
trigger: 'blur',
},
],
},
{
label: '排序',
prop: 'sort',
type: 'number',
align: 'right',
width: 80,
rules: [
{
required: true,
message: '请输入排序',
trigger: 'blur',
},
],
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.dept_add, false),
viewBtn: this.validData(this.permission.dept_view, false),
delBtn: this.validData(this.permission.dept_delete, false),
editBtn: this.validData(this.permission.dept_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
initData(tenantId) {
getDeptTree(tenantId).then(res => {
const column = this.findColumn(this.option.column, 'parentId');
column.dicData = res.data.data;
});
getLeaderList(tenantId).then(res => {
const column = this.findColumn(this.option.column, 'leaderId');
column.dicData = res.data.data;
});
},
handleAdd(row) {
this.parentId = row.id;
const column = this.findColumn(this.option.column, 'parentId');
column.value = row.id;
column.addDisabled = true;
this.$refs.crud.rowAdd();
},
rowSave(row, done, loading) {
row.leaderId = func.join(row.leaderId);
add(row).then(
res => {
// 获取新增数据的相关字段
const data = res.data.data;
row.id = data.id;
row.deptCategoryName = data.deptCategoryName;
row.tenantId = data.tenantId;
this.$message({
type: 'success',
message: '操作成功!',
});
// 数据回调进行刷新
done(row);
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
row.leaderId = func.join(row.leaderId);
update(row).then(
() => {
this.$message({
type: 'success',
message: '操作成功!',
});
// 数据回调进行刷新
done(row);
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row, index, done) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.$message({
type: 'success',
message: '操作成功!',
});
// 数据回调进行刷新
done(row);
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
// 刷新表格数据并重载
this.data = [];
this.parentId = 0;
this.$refs.crud.refreshTable();
this.$refs.crud.toggleSelection();
// 表格数据重载
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
searchReset() {
this.query = {};
this.parentId = 0;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.parentId = '';
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.tenantId);
}
if (['edit', 'view'].includes(type)) {
getDept(this.form.id).then(res => {
this.form = Object.assign(res.data.data, {
hasChildren: this.form.hasChildren,
});
if (this.form.parentId === '0') {
this.form.parentId = '';
}
if (this.form.hasOwnProperty('leaderId')) {
this.form.leaderId = func.split(this.form.leaderId);
}
});
}
done();
},
beforeClose(done) {
this.parentId = '';
const column = this.findColumn(this.option.column, 'parentId');
column.value = '';
column.addDisabled = false;
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;
getLazyList(this.parentId, Object.assign(params, this.query)).then(res => {
this.data = res.data.data;
this.loading = false;
this.selectionClear();
});
},
treeLoad(tree, treeNode, resolve) {
const parentId = tree.id;
getLazyList(parentId).then(res => {
resolve(res.data.data);
});
},
},
};
</script>
<style></style>

450
src/views/system/dict.vue Normal file
View File

@@ -0,0 +1,450 @@
<template>
<basic-container>
<avue-crud
:option="optionParent"
:table-loading="loading"
:data="dataParent"
:page="pageParent"
ref="crud"
v-model="formParent"
:permission="permissionList"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@row-click="handleRowClick"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoadParent"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-setting"
@click.stop="handleRowClick(scope.row)"
v-if="userInfo.authority.includes('admin')"
>字典配置
</el-button>
</template>
<template #code="{ row }">
<el-tag @click="handleRowClick(row)" style="cursor: pointer">{{ row.code }}</el-tag>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
<el-dialog :title="`[${dictValue}]字典配置`" append-to-body v-model="box" width="1000px">
<avue-crud
:option="optionChild"
:table-loading="loadingChild"
:data="dataChild"
ref="crudChild"
v-model="formChild"
:permission="permissionList"
:before-open="beforeOpenChild"
:before-close="beforeCloseChild"
@row-del="rowDelChild"
@row-update="rowUpdateChild"
@row-save="rowSaveChild"
@search-change="searchChangeChild"
@search-reset="searchResetChild"
@selection-change="selectionChangeChild"
@current-change="currentChangeChild"
@size-change="sizeChangeChild"
@refresh-change="refreshChangeChild"
@on-load="onLoadChild"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-circle-plus"
@click.stop="handleAdd(scope.row, scope.index)"
v-if="userInfo.authority.includes('admin')"
>新增子项
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</el-dialog>
</basic-container>
</template>
<script>
import {
getParentList,
getChildList,
remove,
update,
add,
getDict,
getDictTree,
} from '@/api/system/dict';
import { optionParent, optionChild } from '@/option/system/dict';
import { mapGetters } from 'vuex';
export default {
data() {
return {
dictValue: '暂无',
parentId: -1,
formParent: {},
formChild: {},
selectionList: [],
query: {},
box: false,
loading: true,
loadingChild: true,
pageParent: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
pageChild: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
dataParent: [],
dataChild: [],
optionParent: optionParent,
optionChild: optionChild,
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.dict_add, false),
delBtn: this.validData(this.permission.dict_delete, false),
editBtn: this.validData(this.permission.dict_edit, false),
viewBtn: false,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
mounted() {
this.initData();
},
methods: {
initData() {
getDictTree().then(res => {
const column = this.findColumn(this.optionChild.column, 'parentId');
column.dicData = res.data.data;
});
},
handleAdd(row) {
this.formChild.dictValue = '';
this.formChild.dictKey = '';
this.formChild.sort = 0;
this.formChild.isSealed = 0;
this.formChild.remark = '';
this.formChild.parentId = row.id;
this.$refs.crudChild.rowAdd();
},
rowSave(row, done, loading) {
const form = {
...row,
dictKey: -1,
};
add(form).then(
() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
this.onLoadChild(this.pageChild);
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleRowClick(row) {
this.query = {};
this.parentId = row.id;
this.dictValue = row.dictValue;
const code = this.findColumn(this.optionChild.column, 'code');
code.value = row.code;
const parentId = this.findColumn(this.optionChild.column, 'parentId');
parentId.value = row.id;
this.formChild.code = row.code;
this.formChild.parentId = row.id;
this.box = true;
this.onLoadChild(this.pageChild);
},
searchReset() {
this.query = {};
this.onLoadParent(this.pageParent);
},
searchChange(params, done) {
this.query = params;
this.pageParent.currentPage = 1;
this.onLoadParent(this.pageParent, 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.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDict(this.formParent.id).then(res => {
this.formParent = res.data.data;
});
}
done();
},
currentChange(currentPage) {
this.pageParent.currentPage = currentPage;
},
sizeChange(pageSize) {
this.pageParent.pageSize = pageSize;
},
refreshChange() {
this.onLoadParent(this.pageParent, this.query);
},
rowSaveChild(row, done, loading) {
add(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateChild(row, index, done, loading) {
update(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelChild(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
searchResetChild() {
this.query = {};
this.onLoadChild(this.pageChild);
},
searchChangeChild(params, done) {
this.query = params;
this.pageChild.currentPage = 1;
this.onLoadChild(this.pageChild, params);
done();
},
selectionChangeChild(list) {
this.selectionList = list;
},
selectionClearChild() {
this.selectionList = [];
this.$refs.crudChild.toggleSelection();
},
handleDeleteChild() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudChild.toggleSelection();
});
},
beforeOpenChild(done, type) {
if (['add', 'edit'].includes(type)) {
this.initData();
}
if (['edit', 'view'].includes(type)) {
getDict(this.formChild.id).then(res => {
this.formChild = res.data.data;
});
}
done();
},
beforeCloseChild(done) {
this.$refs.crudChild.modelValue.parentId = this.parentId;
this.$refs.crudChild.option.column.filter(item => {
if (item.prop === 'parentId') {
item.value = this.parentId;
}
});
done();
},
currentChangeChild(currentPage) {
this.pageChild.currentPage = currentPage;
},
sizeChangeChild(pageSize) {
this.pageChild.pageSize = pageSize;
},
refreshChangeChild() {
this.onLoadChild(this.pageChild, this.query);
},
onLoadParent(page, params = {}) {
this.loading = true;
getParentList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(
res => {
const data = res.data.data;
this.pageParent.total = data.total;
this.dataParent = data.records;
this.loading = false;
this.selectionClear();
}
);
},
onLoadChild(page, params = {}) {
this.loadingChild = true;
getChildList(
page.currentPage,
page.pageSize,
this.parentId,
Object.assign(params, this.query)
).then(res => {
this.dataChild = res.data.data;
this.loadingChild = false;
this.selectionClear();
});
},
},
};
</script>

View File

@@ -0,0 +1,450 @@
<template>
<basic-container>
<avue-crud
:option="optionParent"
:table-loading="loading"
:data="dataParent"
:page="pageParent"
ref="crud"
v-model="formParent"
:permission="permissionList"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@row-click="handleRowClick"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoadParent"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dictbiz_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-setting"
@click.stop="handleRowClick(scope.row)"
v-if="userInfo.authority.includes('admin')"
>字典配置
</el-button>
</template>
<template #code="{ row }">
<el-tag @click="handleRowClick(row)" style="cursor: pointer">{{ row.code }}</el-tag>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
<el-dialog :title="`[${dictValue}]字典配置`" append-to-body v-model="box" width="1000px">
<avue-crud
:option="optionChild"
:table-loading="loadingChild"
:data="dataChild"
ref="crudChild"
v-model="formChild"
:permission="permissionList"
:before-open="beforeOpenChild"
:before-close="beforeCloseChild"
@row-del="rowDelChild"
@row-update="rowUpdateChild"
@row-save="rowSaveChild"
@search-change="searchChangeChild"
@search-reset="searchResetChild"
@selection-change="selectionChangeChild"
@current-change="currentChangeChild"
@size-change="sizeChangeChild"
@refresh-change="refreshChangeChild"
@on-load="onLoadChild"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-circle-plus"
@click.stop="handleAdd(scope.row, scope.index)"
v-if="userInfo.authority.includes('admin')"
>新增子项
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</el-dialog>
</basic-container>
</template>
<script>
import {
getParentList,
getChildList,
remove,
update,
add,
getDict,
getDictTree,
} from '@/api/system/dictbiz';
import { optionParent, optionChild } from '@/option/system/dictbiz';
import { mapGetters } from 'vuex';
export default {
data() {
return {
dictValue: '暂无',
parentId: -1,
formParent: {},
formChild: {},
selectionList: [],
query: {},
box: false,
loading: true,
loadingChild: true,
pageParent: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
pageChild: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
dataParent: [],
dataChild: [],
optionParent: optionParent,
optionChild: optionChild,
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.dictbiz_add, false),
delBtn: this.validData(this.permission.dictbiz_delete, false),
editBtn: this.validData(this.permission.dictbiz_edit, false),
viewBtn: false,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
mounted() {
this.initData();
},
methods: {
initData() {
getDictTree().then(res => {
const column = this.findColumn(this.optionChild.column, 'parentId');
column.dicData = res.data.data;
});
},
handleAdd(row) {
this.formChild.dictValue = '';
this.formChild.dictKey = '';
this.formChild.sort = 0;
this.formChild.isSealed = 0;
this.formChild.remark = '';
this.formChild.parentId = row.id;
this.$refs.crudChild.rowAdd();
},
rowSave(row, done, loading) {
const form = {
...row,
dictKey: -1,
};
add(form).then(
() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
this.onLoadChild(this.pageChild);
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleRowClick(row) {
this.query = {};
this.parentId = row.id;
this.dictValue = row.dictValue;
const code = this.findColumn(this.optionChild.column, 'code');
code.value = row.code;
const parentId = this.findColumn(this.optionChild.column, 'parentId');
parentId.value = row.id;
this.formChild.code = row.code;
this.formChild.parentId = row.id;
this.box = true;
this.onLoadChild(this.pageChild);
},
searchReset() {
this.query = {};
this.onLoadParent(this.pageParent);
},
searchChange(params, done) {
this.query = params;
this.pageParent.currentPage = 1;
this.onLoadParent(this.pageParent, 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.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDict(this.formParent.id).then(res => {
this.formParent = res.data.data;
});
}
done();
},
currentChange(currentPage) {
this.pageParent.currentPage = currentPage;
},
sizeChange(pageSize) {
this.pageParent.pageSize = pageSize;
},
refreshChange() {
this.onLoadParent(this.pageParent, this.query);
},
rowSaveChild(row, done, loading) {
add(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateChild(row, index, done, loading) {
update(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelChild(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
searchResetChild() {
this.query = {};
this.onLoadChild(this.pageChild);
},
searchChangeChild(params, done) {
this.query = params;
this.pageChild.currentPage = 1;
this.onLoadChild(this.pageChild, params);
done();
},
selectionChangeChild(list) {
this.selectionList = list;
},
selectionClearChild() {
this.selectionList = [];
this.$refs.crudChild.toggleSelection();
},
handleDeleteChild() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudChild.toggleSelection();
});
},
beforeOpenChild(done, type) {
if (['add', 'edit'].includes(type)) {
this.initData();
}
if (['edit', 'view'].includes(type)) {
getDict(this.formChild.id).then(res => {
this.formChild = res.data.data;
});
}
done();
},
beforeCloseChild(done) {
this.$refs.crudChild.modelValue.parentId = this.parentId;
this.$refs.crudChild.option.column.filter(item => {
if (item.prop === 'parentId') {
item.value = this.parentId;
}
});
done();
},
currentChangeChild(currentPage) {
this.pageChild.currentPage = currentPage;
},
sizeChangeChild(pageSize) {
this.pageChild.pageSize = pageSize;
},
refreshChangeChild() {
this.onLoadChild(this.pageChild, this.query);
},
onLoadParent(page, params = {}) {
this.loading = true;
getParentList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(
res => {
const data = res.data.data;
this.pageParent.total = data.total;
this.dataParent = data.records;
this.loading = false;
this.selectionClear();
}
);
},
onLoadChild(page, params = {}) {
this.loadingChild = true;
getChildList(
page.currentPage,
page.pageSize,
this.parentId,
Object.assign(params, this.query)
).then(res => {
this.dataChild = res.data.data;
this.loadingChild = false;
this.selectionClear();
});
},
},
};
</script>

438
src/views/system/menu.vue Normal file
View File

@@ -0,0 +1,438 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
:permission="permissionList"
:before-open="beforeOpen"
:before-close="beforeClose"
@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"
@tree-load="treeLoad"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.menu_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-circle-plus"
@click.stop="handleAdd(scope.row, scope.index)"
v-if="userInfo.authority.includes('admin') && scope.row.category === 1"
>新增子项
</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>
</basic-container>
</template>
<script>
import { getLazyList, remove, update, add, getMenu } from '@/api/system/menu';
import { mapGetters } from 'vuex';
import iconList from '@/config/iconList';
import func from '@/utils/func';
import { getMenuTree } from '@/api/system/menu';
export default {
data() {
return {
form: {},
query: {},
loading: true,
selectionList: [],
parentId: 0,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
lazy: true,
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
dialogWidth: '60%',
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
menuWidth: 320,
dialogClickModal: false,
column: [
{
label: '菜单名称',
prop: 'name',
width: 300,
search: true,
rules: [
{
required: true,
message: '请输入菜单名称',
trigger: 'blur',
},
],
},
{
label: '路由地址',
prop: 'path',
rules: [
{
required: true,
message: '请输入路由地址',
trigger: 'blur',
},
],
},
{
label: '上级菜单',
prop: 'parentId',
type: 'tree',
dicData: [],
hide: true,
addDisabled: false,
props: {
label: 'title',
},
rules: [
{
required: false,
message: '请选择上级菜单',
trigger: 'click',
},
],
},
{
label: '菜单图标',
prop: 'source',
type: 'icon',
slot: true,
iconList: iconList,
rules: [
{
required: true,
message: '请输入菜单图标',
trigger: 'click',
},
],
},
{
label: '菜单编号',
prop: 'code',
search: true,
rules: [
{
required: true,
message: '请输入菜单编号',
trigger: 'blur',
},
],
},
{
label: '菜单类型',
prop: 'category',
type: 'radio',
dicData: [
{
label: '菜单',
value: 1,
},
{
label: '按钮',
value: 2,
},
],
hide: true,
rules: [
{
required: true,
message: '请选择菜单类型',
trigger: 'blur',
},
],
},
{
label: '菜单别名',
prop: 'alias',
search: true,
rules: [
{
required: true,
message: '请输入菜单别名',
trigger: 'blur',
},
],
},
{
label: '是否缓存',
prop: 'isOpen',
type: 'radio',
hide: true,
dicData: [
{
label: '否',
value: 1,
},
{
label: '是',
value: 2,
},
],
value: 1,
rules: [
{
required: true,
message: '请选择是否缓存',
trigger: 'blur',
},
],
},
{
label: '菜单排序',
prop: 'sort',
type: 'number',
rules: [
{
required: true,
message: '请输入菜单排序',
trigger: 'blur',
},
],
},
{
label: '菜单备注',
prop: 'remark',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
},
],
},
data: [],
};
},
watch: {
'form.category'() {
const category = func.toInt(this.form.category);
this.$refs.crud.option.column.filter(item => {
if (item.prop === 'path') {
item.rules[0].required = category === 1;
}
if (item.prop === 'isOpen') {
item.disabled = category === 2;
}
});
},
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.menu_add, false),
viewBtn: this.validData(this.permission.menu_view, false),
delBtn: this.validData(this.permission.menu_delete, false),
editBtn: this.validData(this.permission.menu_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
initData() {
getMenuTree().then(res => {
const column = this.findColumn(this.option.column, 'parentId');
column.dicData = res.data.data;
});
},
handleAdd(row) {
this.parentId = row.id;
const column = this.findColumn(this.option.column, 'parentId');
column.value = row.id;
column.addDisabled = true;
this.$refs.crud.rowAdd();
},
rowSave(row, done, loading) {
add(row).then(
res => {
// 获取新增数据的相关字段
const data = res.data.data;
row.id = data.id;
this.$message({
type: 'success',
message: '操作成功!',
});
// 数据回调进行刷新
done(row);
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.$message({
type: 'success',
message: '操作成功!',
});
// 数据回调进行刷新
done(row);
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row, index, done) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.$message({
type: 'success',
message: '操作成功!',
});
// 数据回调进行刷新
done(row);
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
// 刷新表格数据并重载
this.data = [];
this.parentId = 0;
this.$refs.crud.refreshTable();
this.$refs.crud.toggleSelection();
// 表格数据重载
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
searchReset() {
this.query = {};
this.parentId = 0;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.parentId = '';
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();
}
if (['edit', 'view'].includes(type)) {
getMenu(this.form.id).then(res => {
this.form = Object.assign(res.data.data, {
hasChildren: this.form.hasChildren,
});
if (this.form.parentId === '0') {
this.form.parentId = '';
}
});
}
done();
},
beforeClose(done) {
this.parentId = '';
const column = this.findColumn(this.option.column, 'parentId');
column.value = '';
column.addDisabled = false;
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;
getLazyList(this.parentId, Object.assign(params, this.query)).then(res => {
this.data = res.data.data;
this.loading = false;
this.selectionClear();
});
},
treeLoad(tree, treeNode, resolve) {
const parentId = tree.id;
getLazyList(parentId).then(res => {
resolve(res.data.data);
});
},
},
};
</script>
<style></style>

404
src/views/system/param.vue Normal file
View File

@@ -0,0 +1,404 @@
<template>
<basic-container>
<div class="avue-crud">
<el-row :hidden="!search" style="padding: 6px 18px">
<!-- 查询模块 -->
<el-form :inline="true" :model="query">
<el-form-item label="参数名:">
<el-input v-model="query.paramName" placeholder="请输入参数名"></el-input>
</el-form-item>
<el-form-item label="参数键:">
<el-input v-model="query.paramKey" placeholder="请输入参数键"></el-input>
</el-form-item>
<!-- 查询按钮 -->
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="searchChange"> </el-button>
<el-button icon="el-icon-delete" @click="searchReset()"> </el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="avue-crud__header">
<!-- 头部左侧按钮模块 -->
<div class="avue-crud__left">
<el-button
v-if="this.permissionList.addBtn"
type="primary"
icon="el-icon-plus"
@click="handleAdd"
>
</el-button>
<el-button
v-if="this.permissionList.delBtn"
type="danger"
icon="el-icon-delete"
@click="handleDelete"
plain
>
</el-button>
</div>
<!-- 头部右侧按钮模块 -->
<div class="avue-crud__right">
<el-button icon="el-icon-refresh" @click="searchChange" circle></el-button>
<el-button icon="el-icon-search" @click="searchHide" circle></el-button>
</div>
</div>
</el-row>
<el-row>
<!-- 列表模块 -->
<el-table
ref="table"
v-loading="loading"
@selection-change="selectionChange"
:data="data"
style="width: 100%"
:border="option.border"
>
<el-table-column
type="selection"
v-if="option.selection"
width="55"
align="center"
></el-table-column>
<el-table-column type="expand" v-if="option.expand" align="center"></el-table-column>
<el-table-column v-if="option.index" label="#" type="index" width="50" align="center">
</el-table-column>
<template v-for="(item, index) in option.column">
<!-- table字段 -->
<el-table-column
v-if="item.hide !== true"
:prop="item.prop"
:label="item.label"
:width="item.width"
:key="index"
>
<template v-if="item.tip" #header>
<span>{{ item.label }}</span>
<el-tooltip :content="item.tip" placement="top">
<el-icon style="margin-left: 4px; vertical-align: middle; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
</el-tooltip>
</template>
</el-table-column>
</template>
<!-- 操作栏模块 -->
<el-table-column prop="menu" label="操作" :width="220" align="center">
<template #="{ row }">
<el-button
v-if="this.permissionList.viewBtn"
type="primary"
text
icon="el-icon-view"
@click="handleView(row)"
>查看
</el-button>
<el-button
v-if="this.permissionList.editBtn"
type="primary"
text
icon="el-icon-edit"
@click="handleEdit(row)"
>编辑
</el-button>
<el-button
v-if="this.permissionList.delBtn"
type="primary"
text
icon="el-icon-delete"
@click="rowDel(row)"
>删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-row>
<el-row>
<div class="avue-crud__pagination" style="width: 100%">
<!-- 分页模块 -->
<el-pagination
align="right"
background
@size-change="sizeChange"
@current-change="currentChange"
:current-page="page.currentPage"
:page-sizes="[10, 20, 30, 40, 50, 100]"
:page-size="page.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="page.total"
>
</el-pagination>
</div>
</el-row>
<!-- 表单模块 -->
<el-dialog
:title="title"
v-model="box"
width="50%"
:before-close="beforeClose"
append-to-body
>
<el-form :disabled="view" ref="form" :model="form" label-width="80px">
<!-- 表单字段 -->
<el-form-item label="参数名:" prop="paramName">
<el-input v-model="form.paramName" placeholder="请输入参数名" />
</el-form-item>
<el-form-item prop="paramKey">
<template #label>
<el-tooltip content="参数键为系统内部唯一标识,请勿随意修改" placement="top">
<el-icon style="margin-right: 4px; vertical-align: middle; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
</el-tooltip>
<span>参数键:</span>
</template>
<el-input v-model="form.paramKey" placeholder="请输入参数键" />
</el-form-item>
<el-form-item prop="paramValue">
<template #label>
<el-tooltip content="包含敏感关键词的配置不展示具体数值" placement="top">
<el-icon style="margin-right: 4px; vertical-align: middle; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
</el-tooltip>
<span>参数值:</span>
</template>
<el-input v-model="form.paramValue" placeholder="请输入参数值" />
</el-form-item>
<el-form-item label="备注:" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<!-- 表单按钮 -->
<template #footer>
<span v-if="!view" class="dialog-footer">
<el-button type="primary" icon="el-icon-circle-check" @click="handleSubmit"
> </el-button
>
<el-button icon="el-icon-circle-close" @click="box = false"> </el-button>
</span>
</template>
</el-dialog>
</div>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove } from '@/api/system/param';
import { mapGetters } from 'vuex';
import { sensitive } from '@/utils/sensitive';
export default {
data() {
return {
// 弹框标题
title: '',
// 是否展示弹框
box: false,
// 是否显示查询
search: true,
// 加载中
loading: true,
// 是否为查看模式
view: false,
// 脱敏工具实例
sensitiveManager: null,
// 查询信息
query: {},
// 分页信息
page: {
currentPage: 1,
pageSize: 10,
total: 40,
},
// 表单数据
form: {},
// 选择行
selectionList: [],
// 表单配置
option: {
expand: false,
index: true,
border: true,
selection: true,
column: [
{
label: '参数名',
prop: 'paramName',
},
{
label: '参数键',
prop: 'paramKey',
tip: '参数键为系统内部唯一标识,请勿随意修改',
},
{
label: '参数值',
prop: 'paramValue',
tip: '包含敏感关键词的配置不展示具体数值',
},
{
label: '备注',
prop: 'remark',
},
],
},
// 表单列表
data: [],
};
},
created() {
// 创建脱敏工具实例
this.sensitiveManager = sensitive.create({
fields: ['paramValue'], // 配置需要脱敏的字段
});
},
mounted() {
this.init();
this.onLoad(this.page);
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.param_add, false),
viewBtn: this.validData(this.permission.param_view, false),
delBtn: this.validData(this.permission.param_delete, false),
editBtn: this.validData(this.permission.param_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
init() {
this.height = this.setPx(document.body.clientHeight - 340);
},
searchHide() {
this.search = !this.search;
},
searchChange() {
this.onLoad(this.page);
},
searchReset() {
this.query = {};
this.page.currentPage = 1;
this.onLoad(this.page);
},
handleSubmit() {
if (!this.form.id) {
add(this.form).then(() => {
this.box = false;
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
} else {
// 获取需要提交的数据
const submitData = this.sensitiveManager.getSubmitData(this.form);
update(submitData).then(() => {
this.box = false;
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
}
},
handleAdd() {
this.title = '新增';
this.form = {};
this.box = true;
},
handleEdit(row) {
this.title = '编辑';
this.box = true;
getDetail(row.id).then(res => {
this.form = res.data.data;
// 保存初始脱敏数据
this.sensitiveManager.saveInitialData(this.form);
});
},
handleView(row) {
this.title = '查看';
this.view = true;
this.box = true;
getDetail(row.id).then(res => {
this.form = res.data.data;
// 保存初始脱敏数据
this.sensitiveManager.saveInitialData(this.form);
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.selectionClear();
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
beforeClose(done) {
done();
this.form = {};
this.view = false;
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.table.clearSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
this.onLoad(this.page);
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
this.onLoad(this.page);
},
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();
});
},
},
};
</script>

298
src/views/system/post.vue Normal file
View File

@@ -0,0 +1,298 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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="permission.post_delete"
@click="handleDelete"
>
</el-button>
</template>
<template #category="{ row }">
<el-tag>{{ row.categoryName }}</el-tag>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove } from '@/api/system/post';
import { mapGetters } from 'vuex';
import website from '@/config/website';
export default {
data() {
return {
form: {},
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
column: [
{
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,
rules: [
{
required: true,
message: '请输入所属租户',
trigger: 'click',
},
],
},
{
label: '岗位类型',
prop: 'category',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=post_category',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
search: true,
rules: [
{
required: true,
message: '请选择岗位类型',
trigger: 'blur',
},
],
},
{
label: '岗位编号',
prop: 'postCode',
search: true,
rules: [
{
required: true,
message: '请输入岗位编号',
trigger: 'blur',
},
],
},
{
label: '岗位名称',
prop: 'postName',
search: true,
rules: [
{
required: true,
message: '请输入岗位名称',
trigger: 'blur',
},
],
},
{
label: '岗位排序',
prop: 'sort',
type: 'number',
rules: [
{
required: true,
message: '请输入岗位排序',
trigger: 'blur',
},
],
},
{
label: '岗位描述',
prop: 'remark',
type: 'textarea',
span: 24,
minRows: 6,
hide: true,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.post_add, false),
viewBtn: this.validData(this.permission.post_view, false),
delBtn: this.validData(this.permission.post_delete, false),
editBtn: this.validData(this.permission.post_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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: '操作成功!',
});
});
},
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 (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
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();
},
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();
});
},
},
};
</script>
<style></style>

811
src/views/system/tenant.vue Normal file
View File

@@ -0,0 +1,811 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
v-model:page="page"
: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
v-if="permission.tenant_add && !recycleMode"
type="primary"
icon="el-icon-plus"
@click="$refs.crud.rowAdd()"
>
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.tenant_delete && !recycleMode"
plain
@click="handleDelete"
>
</el-button>
<el-button
type="primary"
plain
icon="el-icon-refresh"
v-if="userInfo.authority.includes('administrator') && !recycleMode"
@click="handleRecycle"
>回收站
</el-button>
<el-button
type="success"
plain
icon="el-icon-check"
v-if="userInfo.authority.includes('administrator') && recycleMode"
@click="handleRecyclePass"
>
</el-button>
<el-button
type="danger"
plain
icon="el-icon-close"
v-if="userInfo.authority.includes('administrator') && recycleMode"
@click="handleRecycleRemove"
>
</el-button>
<el-button
type="primary"
plain
icon="el-icon-refresh-left"
v-if="userInfo.authority.includes('administrator') && recycleMode"
@click="handleRecycleBack"
>
</el-button>
<el-tooltip
class="item"
effect="dark"
content="给租户配置账号额度、过期时间等授权信息"
placement="top"
>
<el-button
plain
type="info"
v-if="userInfo.authority.includes('administrator') && !recycleMode"
icon="el-icon-setting"
@click="handleSetting"
>批量授权配置
</el-button>
</el-tooltip>
</template>
<template #menu-right>
<el-tooltip
class="item"
effect="dark"
content="将自定义的数据源定制为租户绑定的独立数据源"
placement="top"
>
<el-button
plain
v-if="userInfo.authority.includes('administrator') && !recycleMode"
icon="el-icon-message-box"
@click="handleDatasourceSetting"
>数据源管理
</el-button>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
content="将自定义的菜单集合定制为租户绑定的菜单产品包"
placement="top"
>
<el-button
plain
v-if="userInfo.authority.includes('administrator') && !recycleMode"
icon="el-icon-set-up"
@click="handlePackageSetting"
>产品包管理
</el-button>
</el-tooltip>
</template>
<template #menu="scope">
<el-button
v-if="userInfo.authority.includes('administrator') && !recycleMode"
type="primary"
text
icon="el-icon-coin"
@click.stop="handleRowDatasource(scope.row)"
>数据源
</el-button>
<el-button
v-if="userInfo.authority.includes('administrator') && !recycleMode"
type="primary"
text
icon="el-icon-notebook"
@click.stop="handleRowPackage(scope.row)"
>产品包
</el-button>
</template>
<template #accountNumber="{ row }">
<el-tag>{{ row.accountNumber > 0 ? row.accountNumber : '不限制' }}</el-tag>
</template>
<template #expireTime="{ row }">
<el-tag>{{ row.expireTime ? row.expireTime : '不限制' }}</el-tag>
</template>
</avue-crud>
<el-dialog title="租户授权配置" append-to-body v-model="box" width="450px">
<avue-form :option="settingOption" v-model="settingForm" @submit="handleSubmit" />
</el-dialog>
<el-dialog title="租户数据源配置" append-to-body v-model="datasourceBox" width="450px">
<avue-form
ref="formDatasource"
:option="datasourceOption"
v-model="datasourceForm"
@submit="handleDatasourceSubmit"
/>
</el-dialog>
<el-drawer
title="租户数据源管理"
append-to-body
v-model="datasourceSettingBox"
direction="rtl"
size="50%"
>
<tenant-datasource></tenant-datasource>
</el-drawer>
<el-dialog title="租户产品包配置" append-to-body v-model="packageBox" width="450px">
<avue-form
ref="formPackage"
:option="packageOption"
v-model="packageForm"
@submit="handlePackageSubmit"
>
<template #menuId>
<el-tree
ref="packageMenuTree"
:data="packageMenuTreeData"
:props="packageMenuTreeProps"
node-key="id"
default-expand-all
/>
</template>
</avue-form>
</el-dialog>
<el-drawer
title="租户产品包管理"
append-to-body
v-model="packageSettingBox"
direction="rtl"
size="50%"
>
<tenant-package></tenant-package>
</el-drawer>
</basic-container>
</template>
<script>
import {
getList,
getDetail,
update,
add,
setting,
datasource,
packageInfo,
packageSetting,
recycle,
pass,
remove,
} from '@/api/system/tenant';
import { getDetail as packageDetail } from '@/api/system/tenantpackage';
import { mapGetters } from 'vuex';
import { getMenuTree } from '@/api/system/menu';
import { validatenull } from '@/utils/validate';
export default {
data() {
return {
form: {},
selectionList: [],
query: {},
loading: true,
box: false,
datasourceBox: false,
datasourceSettingBox: false,
packageBox: false,
packageSettingBox: false,
recycleMode: false,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
addBtn: false,
viewBtn: true,
menuWidth: 380,
dialogWidth: 900,
dialogClickModal: false,
column: [
{
label: '租户ID',
prop: 'tenantId',
width: 100,
search: true,
addDisplay: false,
editDisplay: false,
span: 24,
rules: [
{
required: true,
message: '请输入租户ID',
trigger: 'blur',
},
],
},
{
label: '租户名称',
prop: 'tenantName',
search: true,
width: 180,
span: 24,
rules: [
{
required: true,
message: '请输入参数名称',
trigger: 'blur',
},
],
},
{
label: '联系人',
prop: 'linkman',
width: 100,
search: true,
rules: [
{
required: true,
message: '请输入联系人',
trigger: 'blur',
},
],
},
{
label: '联系电话',
prop: 'contactNumber',
width: 150,
},
{
label: '联系地址',
prop: 'address',
span: 24,
minRows: 2,
type: 'textarea',
hide: true,
},
{
label: '账号额度',
prop: 'accountNumber',
width: 90,
slot: true,
addDisplay: false,
editDisplay: false,
},
{
label: '过期时间',
prop: 'expireTime',
width: 180,
slot: true,
addDisplay: false,
editDisplay: false,
},
{
label: '绑定域名',
prop: 'domainUrl',
span: 24,
},
{
label: '系统背景',
prop: 'backgroundUrl',
type: 'upload',
listType: 'picture-img',
dataType: 'string',
action: '/blade-resource/oss/endpoint/put-file',
propsHttp: {
res: 'data',
url: 'link',
},
hide: true,
span: 24,
},
],
},
data: [],
settingForm: {},
settingOption: {
column: [
{
label: '账号额度',
labelTip: '代表租户可创建的最大额度,若不限制则默认为-1',
prop: 'accountNumber',
type: 'number',
span: 24,
},
{
label: '过期时间',
labelTip: '代表租户可使用的最后日期,若不限制则默认为空',
prop: 'expireTime',
type: 'date',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
span: 24,
},
],
},
datasourceForm: {},
datasourceOption: {
column: [
{
label: '数据源',
prop: 'datasourceId',
search: true,
span: 24,
type: 'select',
dicUrl: '/blade-system/tenant-datasource/select',
props: {
label: 'name',
value: 'id',
},
rules: [
{
required: true,
message: '请选择数据源',
trigger: 'blur',
},
],
},
],
},
packageForm: {},
packageMenuTreeData: [],
packageMenuTreeProps: {
label: 'title',
value: 'key',
children: 'children',
},
packageOption: {
column: [
{
label: '产品包',
prop: 'packageId',
search: true,
span: 24,
type: 'select',
dicUrl: '/blade-system/tenant-package/select',
props: {
label: 'packageName',
value: 'id',
},
},
{
label: '菜单预览',
prop: 'menuId',
span: 24,
formslot: true,
},
],
},
};
},
watch: {
'packageForm.packageId'() {
if (!validatenull(this.packageForm.packageId)) {
packageDetail(this.packageForm.packageId).then(res => {
const menuId = res.data.data.menuId;
this.packageForm.menuId = menuId;
// 解析选中的菜单ID列表
const selectedIds = this.parseMenuIds(menuId);
// 先加载完整菜单树再根据选中ID筛选
getMenuTree().then(treeRes => {
const fullTreeData = treeRes.data.data;
const selectedIdSet = new Set(selectedIds);
// 筛选树:只保留选中节点及其所有祖先节点
this.packageMenuTreeData = this.filterTreeBySelectedIds(fullTreeData, selectedIdSet);
});
});
}
},
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.tenant_add, false),
viewBtn: this.validData(this.permission.tenant_view, false),
delBtn: this.validData(this.permission.tenant_delete, false),
editBtn: this.validData(this.permission.tenant_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
tenantId() {
return this.selectionList[0].tenantId;
},
},
methods: {
initData() {
getMenuTree().then(res => {
this.packageMenuTreeData = res.data.data;
});
},
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 recycle(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
beforeOpen(done, type) {
if (['view'].includes(type)) {
getDetail(this.form.id).then(res => {
const data = res.data.data;
if (!(data.accountNumber > 0)) {
data.accountNumber = '不限制';
}
if (!data.expireTime) {
data.expireTime = '不限制';
}
this.form = data;
});
}
done();
},
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 recycle(this.ids);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
handleRecycle() {
this.recycleMode = true;
this.$refs.crud.option.editBtn = false;
this.$refs.crud.option.delBtn = false;
this.onLoad(this.page, this.query);
},
handleRecyclePass() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将所选租户进行恢复?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return pass(this.ids);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
handleRecycleRemove() {
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();
});
},
handleRecycleBack() {
this.recycleMode = false;
this.$refs.crud.option.editBtn = true;
this.$refs.crud.option.delBtn = true;
this.onLoad(this.page, this.query);
},
handleSetting() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
if (this.selectionList.length === 1) {
getDetail(this.selectionList[0].id).then(res => {
const data = res.data.data;
this.settingForm.accountNumber = data.accountNumber;
this.settingForm.expireTime = data.expireTime;
});
} else {
this.settingForm.accountNumber = -1;
this.settingForm.expireTime = '';
}
this.box = true;
},
handleRowDatasource(row) {
getDetail(row.id).then(res => {
const data = res.data.data;
this.datasourceForm.tenantId = row.tenantId;
this.datasourceForm.datasourceId = data.datasourceId;
});
this.datasourceBox = true;
this.$nextTick(() => {
this.$refs.formDatasource.updateDic('datasourceId');
});
},
handleDatasource() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
if (this.selectionList.length !== 1) {
this.$message.warning('只能选择一条数据');
return;
}
getDetail(this.selectionList[0].id).then(res => {
const data = res.data.data;
this.datasourceForm.tenantId = this.selectionList[0].tenantId;
this.datasourceForm.datasourceId = data.datasourceId;
});
this.datasourceBox = true;
},
handleRowPackage(row) {
// 重置数据
this.packageMenuTreeData = [];
packageInfo(row.id).then(res => {
const data = res.data.data;
this.packageForm.tenantId = row.tenantId;
this.packageForm.packageId = data.id;
this.packageForm.menuId = data.menuId;
// 加载菜单树数据
this.initData();
});
this.packageBox = true;
//更新字典远程数据
setTimeout(() => {
const form = this.$refs.formPackage;
form.updateDic('packageId');
}, 10);
},
handleDatasourceSetting() {
this.datasourceSettingBox = true;
},
handlePackageSetting() {
this.packageSettingBox = true;
},
handleSubmit(form, done) {
setting(this.ids, form).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '配置成功!',
});
done();
this.box = false;
},
error => {
window.console.log(error);
done();
}
);
},
handleDatasourceSubmit(form, done) {
datasource(form.tenantId, form.datasourceId).then(
() => {
this.$message({
type: 'success',
message: '配置成功!',
});
done();
this.datasourceBox = false;
},
error => {
window.console.log(error);
done();
}
);
},
handlePackageSubmit(form, done) {
packageSetting(form.tenantId, form.packageId).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '配置成功!',
});
done();
this.packageBox = false;
},
error => {
window.console.log(error);
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, {
status_equal: this.recycleMode ? -1 : 1,
})
).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
/**
* 解析菜单ID为字符串数组
* @param {string|Array} menuId - 菜单ID可能是逗号分隔的字符串或数组
* @returns {string[]} - 菜单ID字符串数组
*/
parseMenuIds(menuId) {
if (!menuId) return [];
if (Array.isArray(menuId)) {
return menuId.map(id => String(id).trim());
}
return menuId
.split(',')
.map(id => id.trim())
.filter(Boolean);
},
/**
* 递归筛选树节点,只保留选中的节点及其所有祖先节点
* @param {Array} tree - 原始树数据
* @param {Set<string>} selectedIdSet - 选中的节点ID集合
* @returns {Array} - 筛选后的树数据
*/
filterTreeBySelectedIds(tree, selectedIdSet) {
if (!tree || !tree.length) return [];
return tree.reduce((result, node) => {
// 递归处理子节点
const filteredChildren = this.filterTreeBySelectedIds(node.children, selectedIdSet);
// 判断当前节点是否需要保留:自身被选中 或 有子节点被保留(作为祖先节点)
const nodeId = String(node.id);
const isSelected = selectedIdSet.has(nodeId);
const hasRetainedChildren = filteredChildren.length > 0;
if (isSelected || hasRetainedChildren) {
result.push({
...node,
children: hasRetainedChildren ? filteredChildren : undefined,
});
}
return result;
}, []);
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,408 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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 @click="handleDelete"
>
</el-button>
</template>
<template #shardingConfig-form="{}">
<code-editor v-model="form.shardingConfig" height="300px" />
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove } from '@/api/system/tenantdatasource';
import { mapGetters } from 'vuex';
import func from '@/utils/func';
export default {
data() {
return {
form: {},
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 900,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
grid: true,
selection: true,
dialogClickModal: false,
column: [
{
label: '名称',
prop: 'name',
width: 120,
span: 24,
search: true,
gridRow: true,
rules: [
{
required: true,
message: '请输入数据源名称',
trigger: 'blur',
},
],
},
{
label: '数据类型',
type: 'radio',
value: 1,
span: 24,
width: 120,
searchLabelWidth: 100,
row: true,
gridRow: true,
dicUrl: '/blade-system/dict/dictionary?code=datasource_category',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
prop: 'category',
search: true,
rules: [
{
required: true,
message: '请选择分类',
trigger: 'blur',
},
],
},
{
label: 'YAML',
prop: 'shardingConfig',
span: 24,
minRows: 5,
hide: true,
display: false,
gridRow: true,
slot: true,
type: 'textarea',
rules: [
{
required: true,
message: '请输入YAML配置',
trigger: 'blur',
},
],
},
{
label: '驱动类',
prop: 'driverClass',
type: 'select',
span: 24,
gridRow: true,
dicData: [
{
label: 'com.mysql.cj.jdbc.Driver',
value: 'com.mysql.cj.jdbc.Driver',
},
{
label: 'org.postgresql.Driver',
value: 'org.postgresql.Driver',
},
{
label: 'oracle.jdbc.OracleDriver',
value: 'oracle.jdbc.OracleDriver',
},
{
label: 'com.microsoft.sqlserver.jdbc.SQLServerDriver',
value: 'com.microsoft.sqlserver.jdbc.SQLServerDriver',
},
{
label: 'dm.jdbc.driver.DmDriver',
value: 'dm.jdbc.driver.DmDriver',
},
{
label: 'com.yashandb.jdbc.Driver',
value: 'com.yashandb.jdbc.Driver',
},
{
label: 'com.kingbase8.Driver',
value: 'com.kingbase8.Driver',
},
],
width: 200,
display: true,
rules: [
{
required: true,
message: '请输入驱动类',
trigger: 'blur',
},
],
},
{
label: '连接地址',
prop: 'url',
span: 24,
display: true,
gridRow: true,
hide: true,
rules: [
{
required: true,
message: '请输入连接地址',
trigger: 'blur',
},
],
},
{
label: '用户名',
prop: 'username',
width: 120,
display: true,
gridRow: true,
rules: [
{
required: true,
message: '请输入用户名',
trigger: 'blur',
},
],
},
{
label: '密码',
prop: 'password',
hide: true,
display: true,
rules: [
{
required: true,
message: '请输入密码',
trigger: 'blur',
},
],
},
{
label: '备注',
prop: 'remark',
span: 24,
minRows: 3,
hide: true,
type: 'textarea',
},
],
},
data: [],
driverUrlTemplates: {
'com.mysql.cj.jdbc.Driver':
'jdbc:mysql://localhost:3306/bladex?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true',
'org.postgresql.Driver': 'jdbc:postgresql://127.0.0.1:5432/bladex',
'oracle.jdbc.OracleDriver': 'jdbc:oracle:thin:@//127.0.0.1:1521/ORCLPDB',
'com.microsoft.sqlserver.jdbc.SQLServerDriver':
'jdbc:sqlserver://127.0.0.1:1433;DatabaseName=bladex',
'dm.jdbc.driver.DmDriver':
'jdbc:dm://127.0.0.1:5236/bladex?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8',
'com.yashandb.jdbc.Driver': 'jdbc:yasdb://127.0.0.1:1688/bladex',
'com.kingbase8.Driver': 'jdbc:kingbase8://localhost:4321/bladex',
},
};
},
watch: {
'form.category'() {
const category = func.toInt(this.form.category);
this.$refs.crud.option.column.filter(item => {
if (item.prop === 'driverClass') {
item.display = category === 1;
}
if (item.prop === 'url') {
item.display = category === 1;
}
if (item.prop === 'username') {
item.display = category === 1;
}
if (item.prop === 'password') {
item.display = category === 1;
}
if (item.prop === 'shardingConfig') {
item.display = category === 2;
}
});
},
'form.driverClass'(newDriverClass) {
// 当驱动类发生变化时自动设置对应的默认URL
if (newDriverClass && this.driverUrlTemplates[newDriverClass]) {
// 只在URL为空或者是其他驱动的默认URL时才自动填充
const isDefaultUrl =
!this.form.url || Object.values(this.driverUrlTemplates).includes(this.form.url);
if (isDefaultUrl) {
this.form.url = this.driverUrlTemplates[newDriverClass];
}
}
},
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: true,
viewBtn: true,
delBtn: true,
editBtn: true,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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: '操作成功!',
});
});
},
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 (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
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();
},
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();
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,368 @@
<template>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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 @click="handleDelete"> </el-button>
</template>
<template #menuId-form>
<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"
/>
<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">全选</el-button>
<el-button size="small" plain @click="handleInvertSelect">反选</el-button>
</el-button-group>
</el-row>
<el-tree
ref="menuTree"
:data="menuTreeData"
:props="menuTreeProps"
show-checkbox
node-key="id"
:default-checked-keys="checkedMenuKeys"
:default-expand-all="false"
:check-strictly="!menuLinked"
@check="handleMenuCheck"
/>
</template>
</avue-crud>
</template>
<script>
import { getList, getDetail, add, update, remove } from '@/api/system/tenantpackage';
import { mapGetters } from 'vuex';
import { getMenuTree } from '@/api/system/menu';
export default {
name: 'tenantPackage',
data() {
return {
form: {},
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
menuTreeData: [],
checkedMenuKeys: [],
menuLinked: false,
menuTreeProps: {
label: 'title',
value: 'key',
children: 'children',
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
dialogWidth: 600,
column: [
{
label: '产品包名',
prop: 'packageName',
search: true,
span: 24,
rules: [
{
required: true,
message: '请输入产品包名称',
trigger: 'blur',
},
],
},
{
label: '菜单列表',
prop: 'menuId',
span: 24,
hide: true,
formslot: true,
rules: [
{
required: true,
message: '请选择菜单',
trigger: 'change',
},
],
},
{
label: '备注',
prop: 'remark',
span: 24,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: true,
viewBtn: false,
delBtn: true,
editBtn: true,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
initData() {
getMenuTree().then(res => {
this.menuTreeData = res.data.data;
this.$nextTick(() => this.applyCheckedKeys());
});
},
// el-tree 会把 setCheckedKeys 的入参记录为默认勾选集并在树数据重载时回放,
// 复用的树实例需在打开表单时整体覆盖勾选状态,避免残留上一次配置的勾选
applyCheckedKeys() {
const tree = this.$refs.menuTree;
if (!tree) return;
tree.setCheckedKeys(this.checkedMenuKeys);
},
handleMenuCheck(node, data) {
// 只保存全选的节点,不保存半选的节点
// 恢复时半选状态会根据子节点自动计算
this.form.menuId = data.checkedKeys;
},
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() {
const tree = this.$refs.menuTree;
if (!tree) return;
const allKeys = this.getAllNodeKeys(this.menuTreeData);
tree.setCheckedKeys(allKeys);
this.form.menuId = tree.getCheckedKeys();
},
handleInvertSelect() {
const tree = this.$refs.menuTree;
if (!tree) return;
const checkedKeys = new Set(tree.getCheckedKeys());
if (this.menuLinked) {
const leafKeys = this.getLeafKeys(this.menuTreeData);
const invertedKeys = leafKeys.filter(key => !checkedKeys.has(key));
tree.setCheckedKeys(invertedKeys);
} else {
const allKeys = this.getAllNodeKeys(this.menuTreeData);
const invertedKeys = allKeys.filter(key => !checkedKeys.has(key));
tree.setCheckedKeys(invertedKeys);
}
this.form.menuId = tree.getCheckedKeys();
},
handleLinkedChange() {
const tree = this.$refs.menuTree;
if (!tree) return;
const checkedKeys = tree.getCheckedKeys();
const halfCheckedKeys = tree.getHalfCheckedKeys();
this.$nextTick(() => {
tree.setCheckedKeys([...checkedKeys, ...halfCheckedKeys]);
this.form.menuId = tree.getCheckedKeys();
});
},
rowSave(row, done, loading) {
// 将 menuId 数组转为逗号分隔的字符串
if (Array.isArray(row.menuId)) {
row.menuId = row.menuId.join(',');
}
add(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
window.console.log(error);
}
);
},
rowUpdate(row, index, done, loading) {
// 将 menuId 数组转为逗号分隔的字符串
if (Array.isArray(row.menuId)) {
row.menuId = row.menuId.join(',');
}
update(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
window.console.log(error);
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(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();
});
},
beforeOpen(done, type) {
// 重置选中状态和树数据
this.checkedMenuKeys = [];
this.menuTreeData = [];
this.menuLinked = false;
if (['add', 'edit'].includes(type)) {
this.initData();
}
if (['edit'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
// 设置选中的菜单
if (this.form.menuId) {
this.checkedMenuKeys = Array.isArray(this.form.menuId)
? this.form.menuId
: this.form.menuId.split(',').map(id => id.trim());
}
});
}
done();
},
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();
},
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();
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,531 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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="permission.topmenu_delete"
@click="handleDelete"
>
</el-button>
<el-button
icon="el-icon-setting"
@click="handleMenuSetting"
v-if="permission.topmenu_setting"
plain
>菜单配置
</el-button>
</template>
<template #menu="scope">
<el-button
v-if="permission.topmenu_setting"
type="primary"
text
icon="el-icon-setting"
@click.stop="handleRowMenuSetting(scope.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"></i>
</div>
</template>
<template #sort="{ row }">
<el-input-number
v-model="row.sort"
@change="sortChange(row)"
:min="1"
:max="100"
></el-input-number>
</template>
<template #isMain="{ row }">
<el-switch
v-model="row.isMain"
inline-prompt
:before-change="() => isMainChange(row)"
active-text=""
inactive-text=""
:active-value="1"
:inactive-value="0"
/>
</template>
</avue-crud>
<el-dialog
title="下级菜单配置"
append-to-body
v-model="box"
width="345px"
@closed="handleDialogClose"
>
<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"
/>
<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">全选</el-button>
<el-button size="small" plain @click="handleInvertSelect">反选</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>
<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 {
getList,
getDetail,
add,
update,
remove,
grant,
grantTree,
getTopTree,
enable,
} from '@/api/system/topmenu';
import { mapGetters } from 'vuex';
import iconList from '@/config/iconList';
export default {
data() {
return {
form: {},
box: false,
query: {},
loading: true,
currentMenuIds: [],
props: {
label: 'title',
value: 'key',
},
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
menuGrantList: [],
menuTreeObj: [],
menuLinked: false,
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
menuWidth: 300,
dialogWidth: 900,
dialogClickModal: false,
column: [
{
label: '菜单名',
prop: 'name',
search: true,
rules: [
{
required: true,
message: '请输入菜单名',
trigger: 'blur',
},
],
},
{
label: '菜单图标',
prop: 'source',
type: 'icon',
slot: true,
iconList: iconList,
rules: [
{
required: true,
message: '请输入菜单图标',
trigger: 'click',
},
],
},
{
label: '菜单编号',
prop: 'code',
search: true,
rules: [
{
required: true,
message: '请输入菜单编号',
trigger: 'blur',
},
],
},
{
label: '菜单排序',
prop: 'sort',
type: 'number',
slot: true,
rules: [
{
required: true,
message: '请输入菜单排序',
trigger: 'blur',
},
],
},
{
label: '是否主页',
prop: 'isMain',
slot: true,
align: 'center',
width: 100,
value: 0,
display: false,
dicData: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
},
{
label: '菜单路由',
prop: 'path',
span: 24,
hide: true,
rules: [
{
required: false,
message: '请输入菜单路由',
trigger: 'blur',
},
],
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
return {
addBtn: this.validData(this.permission.topmenu_add, false),
viewBtn: this.validData(this.permission.topmenu_view, false),
delBtn: this.validData(this.permission.topmenu_delete, false),
editBtn: this.validData(this.permission.topmenu_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;
},
},
methods: {
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() {
const tree = this.$refs.treeMenu;
if (!tree) return;
const allKeys = this.getAllNodeKeys(this.menuGrantList);
tree.setCheckedKeys(allKeys);
},
handleInvertSelect() {
const tree = this.$refs.treeMenu;
if (!tree) return;
const checkedKeys = new Set(tree.getCheckedKeys());
if (this.menuLinked) {
const leafKeys = this.getLeafKeys(this.menuGrantList);
const invertedKeys = leafKeys.filter(key => !checkedKeys.has(key));
tree.setCheckedKeys(invertedKeys);
} else {
const allKeys = this.getAllNodeKeys(this.menuGrantList);
const invertedKeys = allKeys.filter(key => !checkedKeys.has(key));
tree.setCheckedKeys(invertedKeys);
}
},
handleLinkedChange() {
const tree = this.$refs.treeMenu;
if (!tree) return;
const checkedKeys = tree.getCheckedKeys();
const halfCheckedKeys = tree.getHalfCheckedKeys();
this.$nextTick(() => {
tree.setCheckedKeys([...checkedKeys, ...halfCheckedKeys]);
});
},
// el-tree 会把 setCheckedKeys 的入参记录为默认勾选集并在树数据重载时回放,
// 复用的树实例需在打开配置时整体覆盖勾选状态,避免残留上一次配置的勾选
applyCheckedKeys() {
const tree = this.$refs.treeMenu;
if (!tree) return;
tree.setCheckedKeys(this.menuTreeObj);
},
isMainChange(row) {
const newValue = row.isMain === 1 ? 0 : 1;
const request = newValue === 1 ? enable(row.id) : update({ ...row, isMain: newValue });
return request.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
return true;
}).catch(error => {
window.console.log(error);
return false;
});
},
submit() {
const menuList = this.$refs.treeMenu.getCheckedKeys();
grant(this.currentMenuIds, menuList).then(() => {
this.box = false;
this.$message({
type: 'success',
message: '操作成功!',
});
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: '操作成功!',
});
});
},
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();
});
},
handleMenuSetting() {
if (this.selectionList.length !== 1) {
this.$message.warning('只能选择一条数据');
return;
}
this.currentMenuIds = this.idsArray;
this.menuTreeObj = [];
grantTree().then(res => {
this.menuGrantList = res.data.data.menu;
getTopTree(this.ids).then(res => {
this.menuTreeObj = res.data.data.menu;
this.box = true;
this.$nextTick(() => this.applyCheckedKeys());
});
});
},
handleRowMenuSetting(row) {
this.currentMenuIds = [row.id];
this.menuTreeObj = [];
grantTree().then(res => {
this.menuGrantList = res.data.data.menu;
getTopTree(row.id).then(res => {
this.menuTreeObj = res.data.data.menu;
this.box = true;
this.$nextTick(() => this.applyCheckedKeys());
});
});
},
handleDialogClose() {
this.currentMenuIds = [];
this.menuLinked = false;
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
sortChange(row) {
update(row).then(
() => {
this.onLoad(this.page);
},
error => {
window.console.log(error);
}
);
},
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();
},
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();
});
},
},
};
</script>
<style>
.none-border {
border: 0;
background-color: transparent !important;
}
</style>

863
src/views/system/user.vue Normal file
View File

@@ -0,0 +1,863 @@
<template>
<el-row>
<el-col :span="5">
<div class="box">
<el-scrollbar>
<basic-container>
<avue-tree :option="treeOption" :data="treeData" @node-click="nodeClick" />
</basic-container>
</el-scrollbar>
</div>
</el-col>
<el-col :span="19">
<basic-container>
<avue-crud
:option="userOption"
v-model:search="search"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
:permission="permissionList"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
:before-open="beforeOpen"
v-model:page="page"
@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
v-if="permission.user_add && !auditMode"
type="primary"
icon="el-icon-plus"
@click="$refs.crud.rowAdd()"
>
</el-button>
<el-button
type="danger"
plain
icon="el-icon-delete"
v-if="permission.user_delete && !auditMode"
@click="handleDelete"
>
</el-button>
<el-button
type="primary"
plain
icon="el-icon-operation"
v-if="userInfo.authority.includes('admin') && !auditMode"
@click="handleAudit"
>
</el-button>
<el-button
type="success"
plain
icon="el-icon-check"
v-if="userInfo.authority.includes('admin') && auditMode"
@click="handleAuditPass"
>
</el-button>
<el-button
type="danger"
plain
icon="el-icon-close"
v-if="userInfo.authority.includes('admin') && auditMode"
@click="handleAuditRefuse"
>
</el-button>
<el-button
type="primary"
plain
icon="el-icon-refresh-left"
v-if="userInfo.authority.includes('admin') && auditMode"
@click="handleAuditBack"
>
</el-button>
<el-button
type="info"
plain
v-if="permission.user_role && !auditMode"
icon="el-icon-user"
@click="handleGrant"
>角色配置
</el-button>
<el-dropdown
v-if="userInfo.authority.includes('admin') && !auditMode"
@command="handleDataManageCommand"
style="margin-left: 10px"
>
<el-button type="primary" plain icon="el-icon-files">
数据管理<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="import">
<el-icon><upload /></el-icon>
导入数据
</el-dropdown-item>
<el-dropdown-item command="export">
<el-icon><download /></el-icon>
导出数据
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #menu-right>
<el-button
v-if="userInfo.authority.includes('admin') && !auditMode"
icon="el-icon-document"
@click="$refs.authLog.openAuthLog()"
>认证日志
</el-button>
<el-button
v-if="userInfo.authority.includes('admin') && !auditMode"
icon="el-icon-lock"
@click="$refs.authLock.openLockLog()"
>锁定管理
</el-button>
</template>
<template #menu="{ row, index, size }">
<el-button
text
type="primary"
icon="el-icon-view"
v-if="this.permission.user_view"
@click.stop="$refs.crud.rowView(row, index)"
>
</el-button>
<el-button
text
type="primary"
icon="el-icon-edit"
v-if="this.permission.user_edit"
@click.stop="$refs.crud.rowEdit(row, index)"
>
</el-button>
<el-dropdown @command="command => handleDropdownCommand(command, row, index)">
<el-button text type="primary" icon="el-icon-setting"> </el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="setLeader" v-if="userInfo.authority.includes('admin')">
<el-icon><circle-close v-if="row.isLeader === 1" /><coordinate v-else /></el-icon>
{{ row.isLeader === 1 ? '取消主管' : '设置主管' }}
</el-dropdown-item>
<el-dropdown-item command="grantRole" v-if="permission.user_role">
<el-icon><user /></el-icon>
角色配置
</el-dropdown-item>
<el-dropdown-item
command="platformConfig"
v-if="userInfo.authority.includes('admin')"
>
<el-icon><setting /></el-icon>
平台配置
</el-dropdown-item>
<el-dropdown-item command="authLog" v-if="userInfo.authority.includes('admin')">
<el-icon><document /></el-icon>
认证日志
</el-dropdown-item>
<el-dropdown-item command="lockUser" v-if="userInfo.authority.includes('admin')">
<el-icon><lock /></el-icon>
锁定账号
</el-dropdown-item>
<el-dropdown-item command="resetPassword" v-if="permission.user_reset">
<el-icon><refresh /></el-icon>
密码重置
</el-dropdown-item>
<el-dropdown-item command="delete" v-if="this.permission.user_delete">
<el-icon><delete /></el-icon>
删除用户
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #tenantName="{ row }">
<el-tag>{{ row.tenantName }}</el-tag>
</template>
<template #roleName="{ row }">
<el-tag>{{ row.roleName }}</el-tag>
</template>
<template #deptName="{ row }">
<el-tag>{{ row.deptName }}</el-tag>
</template>
<template #userTypeName="{ row }">
<el-tag>{{ row.userTypeName }}</el-tag>
</template>
</avue-crud>
<!-- 用户角色配置 -->
<el-dialog title="用户角色配置" append-to-body v-model="roleBox" width="345px">
<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="roleLinked"
active-text="节点联动"
size="small"
@change="handleLinkedChange('treeRole')"
/>
<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('treeRole', roleGrantList)"
>全选</el-button
>
<el-button
size="small"
plain
@click="handleInvertSelect('treeRole', roleGrantList, roleLinked)"
>反选</el-button
>
</el-button-group>
</el-row>
<el-tree
:data="roleGrantList"
show-checkbox
:check-strictly="!roleLinked"
default-expand-all
node-key="id"
ref="treeRole"
:default-checked-keys="roleTreeObj"
:props="props"
>
</el-tree>
<template #footer>
<span class="dialog-footer">
<el-button @click="roleBox = false"> </el-button>
<el-button type="primary" @click="submitRole"> </el-button>
</span>
</template>
</el-dialog>
<!-- 用户数据导入 -->
<el-dialog title="用户数据导入" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOption" v-model="excelForm" :upload-after="uploadAfter">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
</el-button>
</template>
</avue-form>
</el-dialog>
<!-- 用户平台配置行级别 -->
<el-dialog title="用户平台配置" append-to-body v-model="platformBox" width="600px">
<avue-form ref="platformFormRef" :option="platformFormOption" v-model="platformForm" />
<template #footer>
<span class="dialog-footer">
<el-button @click="platformBox = false"> </el-button>
<el-button type="primary" @click="submitPlatformConfig"> </el-button>
</span>
</template>
</el-dialog>
<!-- 认证日志组件 -->
<auth-log ref="authLog" />
<!-- 认证锁定配置组件 -->
<auth-lock ref="authLock" @lock-success="onLoad(page)" />
</basic-container>
</el-col>
</el-row>
</template>
<script>
import {
getList,
getUser,
getUserPlatform,
remove,
update,
updatePlatform,
add,
grant,
resetPassword,
auditPass,
auditRefuse,
setLeader,
getLeaderList,
} from '@/api/system/user';
import { exportBlob } from '@/api/common';
import { getDeptTree } from '@/api/system/dept';
import { getRoleTree } from '@/api/system/role';
import { getPostList } from '@/api/system/post';
import { mapGetters } from 'vuex';
import website from '@/config/website';
import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import func from '@/utils/func';
import { sensitive } from '@/utils/sensitive';
import { excelOption, platformFormOption, treeOption, userOption } from '@/option/system/user';
import AuthLog from './authlog.vue';
import AuthLock from './authlock.vue';
export default {
components: { AuthLog, AuthLock },
data() {
return {
form: {},
search: {},
sensitiveManager: null,
roleBox: false,
roleLinked: false,
grantUserId: '',
excelBox: false,
platformBox: false,
initFlag: true,
auditMode: false,
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
props: {
label: 'title',
value: 'key',
},
roleGrantList: [],
roleTreeObj: [],
treeDeptId: '',
treeData: [],
treeOption: treeOption,
userOption: userOption(this),
platformFormOption: platformFormOption,
data: [],
platformForm: {},
excelForm: {},
excelOption: excelOption,
};
},
watch: {
'form.tenantId'() {
if (this.form.tenantId !== '' && this.initFlag) {
this.initData(this.form.tenantId);
}
},
'excelForm.isCovered'() {
if (this.excelForm.isCovered !== '') {
const column = this.findColumn(this.excelOption.column, 'excelFile');
column.action = `/blade-system/user/import-user?isCovered=${this.excelForm.isCovered}`;
}
},
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.user_add, false),
viewBtn: false,
delBtn: false,
editBtn: false,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
mounted() {
// 非租户模式默认加载管理组数据
if (!website.tenantMode) {
this.initData(website.tenantId);
} else {
this.initData();
}
},
created() {
// 创建脱敏工具实例
this.sensitiveManager = sensitive.create({
fields: ['phone', 'email'], // 配置需要脱敏的字段
});
},
methods: {
nodeClick(data) {
this.treeDeptId = data.id;
this.page.currentPage = 1;
this.onLoad(this.page);
},
handleDropdownCommand(command, row, index) {
if (command === 'setLeader') {
this.handleSetLeader(row);
} else if (command === 'grantRole') {
this.handleGrantForRow(row);
} else if (command === 'authLog') {
this.$refs.authLog.openUserAuthLog(row.id, row.name);
} else if (command === 'lockUser') {
this.$refs.authLock.openLockUser(row.id, row.account);
} else if (command === 'platformConfig') {
this.handlePlatformForRow(row);
} else if (command === 'resetPassword') {
this.handleResetForRow(row);
} else if (command === 'delete') {
this.$refs.crud.rowDel(row, index);
}
},
handleResetForRow(row) {
this.$confirm(`确定将账号【${row.account}】的密码重置为初始密码?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return resetPassword(row.id);
})
.then(() => {
this.$message({
type: 'success',
message: '密码重置成功!',
});
});
},
handleDataManageCommand(command) {
if (command === 'import') {
this.handleImport();
} else if (command === 'export') {
this.handleExport();
}
},
handleSetLeader(row) {
const tip = row.isLeader === 1 ? '确定取消用户的主管职务?' : '确定设置用户为主管职务?';
const message = row.isLeader === 1 ? '取消主管成功!' : '设置主管成功!';
this.$confirm(tip, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return setLeader(row.id);
})
.then(() => {
this.$message({
type: 'success',
message: message,
});
this.onLoad(this.page);
});
},
initData(tenantId) {
getRoleTree(tenantId).then(res => {
const column = this.findColumn(this.userOption.group, 'roleId');
column.dicData = res.data.data;
});
getDeptTree(tenantId).then(res => {
const column = this.findColumn(this.userOption.group, 'deptId');
column.dicData = res.data.data;
});
getPostList(tenantId).then(res => {
const column = this.findColumn(this.userOption.group, 'postId');
column.dicData = res.data.data;
});
getLeaderList(tenantId).then(res => {
const column = this.findColumn(this.userOption.group, 'leaderId');
column.dicData = res.data.data;
});
},
submitRole() {
const roleList = this.$refs.treeRole.getCheckedKeys().join(',');
grant(this.grantUserId, roleList).then(() => {
this.roleBox = false;
this.$message({
type: 'success',
message: '操作成功!',
});
this.onLoad(this.page);
});
},
rowSave(row, done, loading) {
row.deptId = func.join(row.deptId);
row.roleId = func.join(row.roleId);
row.postId = func.join(row.postId);
row.leaderId = func.join(row.leaderId);
add(row).then(
() => {
this.initFlag = false;
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
row.deptId = func.join(row.deptId);
row.roleId = func.join(row.roleId);
row.postId = func.join(row.postId);
row.leaderId = func.join(row.leaderId);
// 获取需要提交的数据
const submitData = this.sensitiveManager.getSubmitData(row);
update(submitData).then(
() => {
this.initFlag = false;
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.treeDeptId = '';
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();
});
},
handleAudit() {
this.auditMode = true;
this.onLoad(this.page, this.query);
},
handleAuditPass() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据通过审核?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return auditPass(this.ids);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
handleAuditRefuse() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据拒绝审核?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return auditRefuse(this.ids);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
handleAuditBack() {
this.auditMode = false;
this.onLoad(this.page, this.query);
},
handleReset() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择账号密码重置为初始密码?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return resetPassword(this.ids);
})
.then(() => {
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
handleGrant() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.roleTreeObj = [];
this.roleLinked = false;
this.grantUserId = this.ids;
if (this.selectionList.length === 1) {
this.roleTreeObj = this.selectionList[0].roleId.split(',');
}
getRoleTree().then(res => {
this.roleGrantList = res.data.data;
this.roleBox = true;
this.$nextTick(() => this.applyCheckedKeys('treeRole', this.roleTreeObj));
});
},
handleGrantForRow(row) {
this.roleTreeObj = row.roleId ? row.roleId.split(',') : [];
this.roleLinked = false;
this.grantUserId = row.id;
getRoleTree().then(res => {
this.roleGrantList = res.data.data;
this.roleBox = true;
this.$nextTick(() => this.applyCheckedKeys('treeRole', this.roleTreeObj));
});
},
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);
},
// ========== 平台配置(行级别) ==========
handlePlatformForRow(row) {
getUserPlatform(row.id).then(res => {
this.platformForm = { ...res.data.data, account: row.account };
this.platformBox = true;
});
},
submitPlatformConfig() {
updatePlatform(
this.platformForm.id,
this.platformForm.userType,
this.platformForm.userExt
).then(() => {
this.platformBox = false;
this.$message({ type: 'success', message: '操作成功!' });
});
},
handleImport() {
this.excelBox = true;
},
uploadAfter(res, done, loading, column) {
window.console.log(column);
this.excelBox = false;
this.refreshChange();
done();
},
handleExport() {
const account = func.toStr(this.search.account);
const realName = func.toStr(this.search.realName);
this.$confirm('是否导出用户数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
`/blade-system/user/export-user?${
this.website.tokenHeader
}=${getToken()}&account=${account}&realName=${realName}`
).then(res => {
downloadXls(res.data, `用户数据表${this.$dayjs().format('YYYY-MM-DD')}.xlsx`);
NProgress.done();
});
});
},
handleTemplate() {
exportBlob(
`/blade-system/user/export-template?${this.website.tokenHeader}=${getToken()}`
).then(res => {
downloadXls(res.data, '用户数据模板.xlsx');
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getUser(this.form.id).then(res => {
this.form = res.data.data;
if (this.form.hasOwnProperty('deptId')) {
this.form.deptId = func.split(this.form.deptId);
}
if (this.form.hasOwnProperty('roleId')) {
this.form.roleId = func.split(this.form.roleId);
}
if (this.form.hasOwnProperty('postId')) {
this.form.postId = func.split(this.form.postId);
}
if (this.form.hasOwnProperty('leaderId')) {
this.form.leaderId = func.split(this.form.leaderId);
}
if (this.form.sex === -1) {
this.form.sex = '';
}
// 保存初始脱敏数据
this.sensitiveManager.saveInitialData(this.form);
});
}
this.initFlag = true;
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, { status: this.auditMode ? 0 : 1 }),
this.treeDeptId
).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style>
.box {
height: 800px;
}
.el-scrollbar {
height: 100%;
}
.box .el-scrollbar__wrap {
overflow: scroll;
}
</style>

View File

@@ -0,0 +1,485 @@
<template>
<div>
<basic-container>
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<!-- 个人信息 Tab -->
<el-tab-pane label="个人信息" name="info">
<el-form ref="infoForm" :model="infoForm" label-width="100px">
<el-form-item label="头像">
<div class="avatar-upload-container">
<div class="avatar-wrapper" v-if="infoForm.avatar">
<img :src="infoForm.avatar" alt="用户头像" class="avatar" />
<div class="avatar-overlay">
<el-icon class="avatar-action" @click="previewAvatar"><View /></el-icon>
<el-icon class="avatar-action" @click="deleteAvatar"><Delete /></el-icon>
</div>
</div>
<el-upload
v-else
class="avatar-uploader"
action="/api/blade-resource/oss/endpoint/put-file"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
accept=".jpg,.jpeg,.png,.gif"
>
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="el-upload__tip">支持 JPG/PNG/GIF 格式文件大小不超过 1MB</div>
</div>
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="infoForm.realName" placeholder="请输入姓名">
<template #prefix>
<el-icon><Bell /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="用户名">
<el-input v-model="infoForm.name" placeholder="请输入用户名">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="infoForm.phone" placeholder="请输入手机号">
<template #prefix>
<el-icon><Iphone /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="infoForm.email" placeholder="请输入邮箱">
<template #prefix>
<el-icon><Message /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="button-group">
<el-button type="primary" @click="submitInfo" :loading="loading">
<el-icon><Check /></el-icon>
<span>提交</span>
</el-button>
<el-button @click="resetForm">
<el-icon><RefreshRight /></el-icon>
<span>清空</span>
</el-button>
</div>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 修改密码 Tab -->
<el-tab-pane label="修改密码" name="password">
<el-form ref="passwordForm" :model="passwordForm" label-width="100px">
<el-form-item label="原密码">
<el-input
v-model="passwordForm.oldPassword"
type="password"
placeholder="请输入原密码"
show-password
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="新密码">
<el-input
v-model="passwordForm.newPassword"
type="password"
placeholder="请输入新密码"
show-password
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="确认密码">
<el-input
v-model="passwordForm.newPassword1"
type="password"
placeholder="请确认新密码"
show-password
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="button-group">
<el-button type="primary" @click="submitPassword" :loading="loading">
<el-icon><Check /></el-icon>
<span>提交</span>
</el-button>
<el-button @click="resetForm">
<el-icon><RefreshRight /></el-icon>
<span>清空</span>
</el-button>
</div>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</basic-container>
<!-- 头像预览组件 -->
<el-image-viewer
v-if="showAvatarPreview"
:url-list="avatarPreviewList"
:initial-index="0"
:hide-on-click-modal="true"
:teleported="true"
@close="showAvatarPreview = false"
/>
</div>
</template>
<script>
import { getUserInfo, updateInfo, updatePassword } from '@/api/system/user';
import md5 from 'js-md5';
import { sensitive } from '@/utils/sensitive';
import {
Plus,
Bell,
User,
Iphone,
Message,
Lock,
Check,
RefreshRight,
View,
Delete,
} from '@element-plus/icons-vue';
import { ElImageViewer } from 'element-plus';
import { getToken } from '@/utils/auth';
export default {
components: {
ElImageViewer,
Plus,
Bell,
User,
Iphone,
Message,
Lock,
Check,
RefreshRight,
View,
Delete,
},
data() {
return {
activeTab: 'info',
loading: false,
infoForm: {
id: '',
avatar: '',
name: '',
realName: '',
phone: '',
email: '',
},
passwordForm: {
oldPassword: '',
newPassword: '',
newPassword1: '',
},
sensitiveManager: null,
showAvatarPreview: false,
avatarPreviewList: [],
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
};
},
created() {
// 初始化脱敏工具
this.sensitiveManager = sensitive.create({
fields: ['phone', 'email'],
});
// 初始化时根据默认tab加载数据
if (this.activeTab === 'info') {
this.loadUserInfo();
}
},
methods: {
// Tab切换处理
handleTabChange(name) {
if (name === 'info') {
this.loadUserInfo();
}
},
// 加载用户信息
loadUserInfo() {
getUserInfo().then(res => {
const user = res.data.data;
this.infoForm = {
id: user.id,
avatar: user.avatar,
name: user.name,
realName: user.realName,
phone: user.phone,
email: user.email,
};
// 保存初始脱敏数据
this.sensitiveManager.saveInitialData(this.infoForm);
});
},
// 提交个人信息
submitInfo() {
this.loading = true;
const submitData = this.sensitiveManager.getSubmitData(this.infoForm);
updateInfo(submitData).then(
res => {
if (res.data.success) {
this.$message({
type: 'success',
message: '修改信息成功!',
});
} else {
this.$message({
type: 'error',
message: res.data.msg,
});
}
this.loading = false;
},
error => {
window.console.log(error);
this.loading = false;
}
);
},
// 提交密码修改
submitPassword() {
this.loading = true;
updatePassword(
md5(this.passwordForm.oldPassword),
md5(this.passwordForm.newPassword),
md5(this.passwordForm.newPassword1)
).then(
res => {
if (res.data.success) {
this.$message({
type: 'success',
message: '修改密码成功!',
});
// 清空密码表单
this.passwordForm = {
oldPassword: '',
newPassword: '',
newPassword1: '',
};
} else {
this.$message({
type: 'error',
message: res.data.msg,
});
}
this.loading = false;
},
error => {
window.console.log(error);
this.loading = false;
}
);
},
// 头像上传成功处理
handleAvatarSuccess(response) {
if (response && response.data) {
this.infoForm.avatar = response.data.link || response.data;
}
},
// 头像上传前验证
beforeAvatarUpload(file) {
const isValidType = ['image/jpeg', 'image/png', 'image/gif'].includes(file.type);
const isLt1M = file.size / 1024 < 1024;
if (!isValidType) {
this.$message.error('上传头像只支持 JPG/PNG/GIF 格式!');
}
if (!isLt1M) {
this.$message.error('上传头像大小不能超过 1MB!');
}
return isValidType && isLt1M;
},
// 重置表单
resetForm() {
if (this.activeTab === 'info') {
// 清空个人信息表单但保留id
const id = this.infoForm.id;
this.infoForm = {
id: id,
avatar: '',
name: '',
realName: '',
phone: '',
email: '',
};
} else if (this.activeTab === 'password') {
// 清空密码表单
this.passwordForm = {
oldPassword: '',
newPassword: '',
newPassword1: '',
};
}
},
// 预览头像
previewAvatar() {
if (this.infoForm.avatar) {
this.avatarPreviewList = [this.infoForm.avatar];
this.showAvatarPreview = true;
} else {
this.$message.warning('暂无头像可预览');
}
},
// 删除头像
deleteAvatar() {
this.$confirm('确定要删除头像吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.infoForm.avatar = '';
this.$message.success('头像已删除');
})
.catch(() => {});
},
},
};
</script>
<style scoped>
.avatar-upload-container {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.avatar-wrapper {
position: relative;
width: 200px;
height: 200px;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
.avatar-wrapper:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.avatar-wrapper .avatar {
width: 200px;
height: 200px;
display: block;
border-radius: 12px;
object-fit: cover;
}
.avatar-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.6) 100%);
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
opacity: 0;
transition: opacity 0.3s ease;
backdrop-filter: blur(2px);
}
.avatar-wrapper:hover .avatar-overlay {
opacity: 1;
}
.avatar-action {
font-size: 20px;
color: #fff;
cursor: pointer;
padding: 8px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-action:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.avatar-uploader :deep(.el-upload) {
border: 2px dashed #d9d9d9;
border-radius: 12px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
width: 200px;
height: 200px;
background-color: #fafafa;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-uploader :deep(.el-upload:hover) {
border-color: var(--el-color-primary);
background-color: #f5f5f5;
}
.avatar-uploader-icon {
font-size: 40px;
color: #8c939d;
transition: color 0.3s ease;
}
.avatar-uploader :deep(.el-upload:hover) .avatar-uploader-icon {
color: var(--el-color-primary);
}
.el-upload__tip {
font-size: 12px;
color: #969799;
margin-top: 10px;
line-height: 1.5;
padding-left: 4px;
}
.button-group {
display: flex;
justify-content: center;
gap: 16px;
width: 100%;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

9
src/views/test.vue Normal file
View File

@@ -0,0 +1,9 @@
<template>
<basic-container>测试页</basic-container>
</template>
<script>
export default {};
</script>
<style></style>

588
src/views/tool/code.vue Normal file
View File

@@ -0,0 +1,588 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
:permission="permissionList"
v-model:page="page"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
:before-open="beforeOpen"
@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.code_delete"
plain
@click="handleDelete"
>
</el-button>
<el-tooltip
class="item"
effect="dark"
content="将选中的多重配置集合批量生成代码"
placement="top"
>
<el-button type="primary" plain icon="el-icon-refresh" @click="handleBuild"
>代码批量生成
</el-button>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
content="不通过多重配置直接生成最简化的CRUD代码"
placement="top"
>
<el-button type="info" plain icon="el-icon-cpu" @click="handleCodeGen"
>代码快速生成
</el-button>
</el-tooltip>
</template>
<template #menu-right>
<el-tooltip
class="item"
effect="dark"
content="通过可视化表单设计器快速生成单表模块"
placement="top"
>
<el-button
plain
v-if="userInfo.authority.includes('administrator')"
:loading="!componentLoaded"
icon="el-icon-connection"
@click="handleFormSetting"
>可视化表单设计器
</el-button>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
content="配置代码生成的默认参数后自动绑定"
placement="top"
>
<el-button
plain
v-if="userInfo.authority.includes('administrator')"
icon="el-icon-message-box"
@click="handleCodeSetting"
>默认配置管理
</el-button>
</el-tooltip>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-document-copy"
v-if="permission.code_edit"
class="none-border"
@click.stop="handleCopy(scope.row)"
>复制
</el-button>
<el-button
type="primary"
text
icon="el-icon-refresh"
v-if="userInfo.authority.includes('administrator')"
class="none-border"
@click.stop="handleBuildSingle(scope.row)"
>生成
</el-button>
</template>
</avue-crud>
<el-drawer title="代码快速生成" append-to-body v-model="codeGenBox" direction="rtl" size="50%">
<avue-form :option="genOption" v-model="genForm" @submit="handleCodeGenSubmit" />
</el-drawer>
<el-drawer
title="可视化表单管理"
append-to-body
v-model="formSettingBox"
direction="rtl"
size="1000px"
>
<form-setting></form-setting>
</el-drawer>
<el-drawer
title="默认配置管理"
append-to-body
v-model="codeSettingBox"
direction="rtl"
size="1000px"
>
<code-setting></code-setting>
</el-drawer>
</basic-container>
</template>
<script>
import { getList, getCode, build, remove, add, update, copy, buildFast } from '@/api/tool/code';
import { getEnableDetail, getTableForm } from '@/api/tool/codesetting';
import {
getDetail as modelDetail,
getTableInfoByName,
getTableList,
prototypeDetail,
} from '@/api/tool/model';
import { codeOption, genOption } from '@/option/tool/code';
import { validatejson, validatenull } from '@/utils/validate';
import { mapGetters } from 'vuex';
import { getMenuTree } from '@/api/system/menu';
import { loadFormDesignModule, loadFormModule } from '@/utils/module';
export default {
data() {
return {
form: {},
genForm: {},
selectionList: [],
componentLoaded: false,
loading: true,
loadingOption: {
lock: true,
text: '物理表读取中',
background: 'rgba(0, 0, 0, 0.7)',
},
addMode: false,
codeGenBox: false,
codeSettingBox: false,
codeSetting: {},
formSettingBox: false,
query: {},
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: codeOption,
genOption: genOption,
data: [],
};
},
created() {
// 懒加载表单设计器模块
Promise.all([loadFormModule(this.$app), loadFormDesignModule(this.$app)]).then(() => {
this.componentLoaded = true;
});
},
watch: {
'form.modelId'() {
if (!validatenull(this.form.modelId) && this.addMode) {
// 获取数据模型信息
modelDetail(this.form.modelId).then(res => {
const result = res.data;
if (result.success) {
const { modelName, modelTable, modelCode } = result.data;
const lowerModelCode = modelCode.toLowerCase();
//if (validatenull(this.form.tablePrefix)) {
this.form.tablePrefix = modelTable.split('_')[0] + '_';
//}
//if (validatenull(this.form.tableName)) {
this.form.tableName = modelTable;
//}
//if (validatenull(this.form.codeName)) {
this.form.codeName = modelName;
//}
if (validatenull(this.form.serviceName)) {
this.form.serviceName = `blade-${lowerModelCode}`;
}
//if (validatenull(this.form.pkName)) {
this.form.pkName = 'id';
//}
if (validatenull(this.form.packageName)) {
this.form.packageName = `org.springblade.${lowerModelCode}`;
}
//if (validatenull(this.form.subFkId) && !validatenull(this.form.tablePrefix)) {
this.form.subFkId = modelTable.replace(this.form.tablePrefix, '') + '_id';
//}
// 获取数据原型信息
prototypeDetail(this.form.modelId).then(res => {
const result = res.data;
if (result.success) {
const columnTreeId = this.findColumn(this.option.group, 'treeId');
const columnTreePid = this.findColumn(this.option.group, 'treePid');
const columnTreeName = this.findColumn(this.option.group, 'treeName');
columnTreeId.dicData = result.data;
columnTreePid.dicData = result.data;
columnTreeName.dicData = result.data;
}
});
}
});
}
},
'form.templateType'() {
// 模版类型
const type = this.form.templateType;
// 主子表字段显隐
const columnSubModelId = this.findColumn(this.option.group, 'subModelId');
const columnSubFkId = this.findColumn(this.option.group, 'subFkId');
columnSubModelId.display = type === 'sub';
columnSubFkId.display = type === 'sub';
// 树表字段显隐
const columnTreeId = this.findColumn(this.option.group, 'treeId');
const columnTreePid = this.findColumn(this.option.group, 'treePid');
const columnTreeName = this.findColumn(this.option.group, 'treeName');
columnTreeId.display = type === 'tree';
columnTreePid.display = type === 'tree';
columnTreeName.display = type === 'tree';
},
'genForm.datasourceId'() {
if (!validatenull(this.genForm.datasourceId)) {
const fullLoading = this.$loading(this.loadingOption);
getTableList(this.genForm.datasourceId)
.then(res => {
const column = this.findColumn(this.genOption.column, 'modelTable');
column.dicData = res.data.data;
fullLoading.close();
})
.catch(() => {
fullLoading.close();
});
}
},
'genForm.modelTable'() {
if (!validatenull(this.genForm.modelTable)) {
const fullLoading = this.$loading(this.loadingOption);
getTableInfoByName(this.genForm.modelTable, this.genForm.datasourceId)
.then(res => {
const result = res.data;
if (result.success) {
// 赋默认值
const { comment, entityName } = result.data;
this.genForm.modelClass = entityName;
this.genForm.modelCode = entityName.replace(/^\S/, s => s.toLowerCase());
const lowerModelCode = this.genForm.modelCode.toLowerCase();
this.genForm.tablePrefix = this.genForm.modelTable.split('_')[0] + '_';
this.genForm.tableName = this.genForm.modelTable;
this.genForm.codeName = comment;
this.genForm.pkName = 'id';
if (validatenull(this.genForm.serviceName)) {
this.genForm.serviceName = `blade-${lowerModelCode}`;
}
if (validatenull(this.genForm.packageName)) {
this.genForm.packageName = `org.springblade.${lowerModelCode}`;
}
// 字段显隐
const columnModelForm = this.findColumn(this.genOption.column, 'modelForm');
const columnModelClass = this.findColumn(this.genOption.column, 'modelClass');
const columnModelCode = this.findColumn(this.genOption.column, 'modelCode');
const columnTablePrefix = this.findColumn(this.genOption.column, 'tablePrefix');
const columnTableName = this.findColumn(this.genOption.column, 'tableName');
const columnCodeName = this.findColumn(this.genOption.column, 'codeName');
const columnPkName = this.findColumn(this.genOption.column, 'pkName');
this.genForm.modelForm = '';
getTableForm(this.genForm.modelTable).then(res => {
columnModelForm.dicData = res.data.data;
});
columnModelForm.display = true;
columnModelClass.display = true;
columnModelCode.display = true;
columnTablePrefix.display = true;
columnTableName.display = true;
columnCodeName.display = true;
columnPkName.display = true;
fullLoading.close();
}
})
.catch(() => {
fullLoading.close();
});
}
},
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.code_add, false),
viewBtn: this.validData(this.permission.code_view, false),
delBtn: this.validData(this.permission.code_delete, false),
editBtn: this.validData(this.permission.code_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
initData() {
return getMenuTree().then(res => {
const column = this.findColumn(this.option.group, 'menuId');
column.dicData = res.data.data;
});
},
initGenData() {
getMenuTree().then(res => {
const column = this.findColumn(this.genOption.column, 'menuId');
column.dicData = res.data.data;
});
getEnableDetail().then(res => {
if (validatejson(res.data.data.settings)) {
this.codeSetting = JSON.parse(res.data.data.settings);
this.genForm = {
menuId: this.codeSetting.menuId,
serviceName: this.codeSetting.serviceName,
packageName: this.codeSetting.packageName,
baseMode: this.codeSetting.baseMode,
wrapMode: this.codeSetting.wrapMode,
feignMode: this.codeSetting.feignMode,
codeStyle: this.codeSetting.codeStyle,
apiPath: this.codeSetting.apiPath,
webPath: this.codeSetting.webPath,
};
this.$message({
type: 'success',
message: '默认配置加载成功',
});
}
});
},
initSetting() {
return getEnableDetail().then(res => {
if (validatejson(res.data.data.settings)) {
this.codeSetting = JSON.parse(res.data.data.settings);
this.form = {
menuId: this.codeSetting.menuId,
serviceName: this.codeSetting.serviceName,
packageName: this.codeSetting.packageName,
baseMode: this.codeSetting.baseMode,
wrapMode: this.codeSetting.wrapMode,
feignMode: this.codeSetting.feignMode,
codeStyle: this.codeSetting.codeStyle,
apiPath: this.codeSetting.apiPath,
webPath: this.codeSetting.webPath,
};
this.$message({
type: 'success',
message: '默认配置加载成功',
});
}
});
},
initCode(id) {
return getCode(id).then(res => {
this.form = res.data.data;
});
},
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: '操作成功!',
});
});
},
handleCodeGen() {
this.initGenData();
this.codeGenBox = true;
},
handleFormSetting() {
this.formSettingBox = true;
},
handleCodeSetting() {
this.codeSettingBox = true;
},
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();
});
},
handleBuild() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('是否生成选中模块的代码?', {
title: '代码生成确认',
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
return build(this.ids).then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
});
},
handleBuildSingle(row) {
this.$confirm(`是否生成选中模块 [${row.codeName}] 的代码?`, {
title: '代码生成确认',
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
return build(row.id).then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
});
},
handleCodeGenSubmit(form, done) {
buildFast(form).then(
() => {
this.$message({
type: 'success',
message: '代码生成成功!',
});
done();
this.codeGenBox = false;
},
error => {
window.console.log(error);
done();
}
);
},
handleCopy(row) {
copy(row.id).then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '复制成功!',
});
});
},
async beforeOpen(done, type) {
const tasks = [];
if (['add'].includes(type)) {
this.addMode = true;
tasks.push(this.initSetting());
}
if (['add', 'edit'].includes(type)) {
tasks.push(this.initData());
}
if (['edit', 'view'].includes(type)) {
this.addMode = false;
tasks.push(this.initCode(this.form.id));
}
await Promise.all(tasks);
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();
});
},
},
};
</script>

View File

@@ -0,0 +1,281 @@
<template>
<basic-container>
<avue-crud
:option="option"
v-model:search="search"
v-model:page="page"
v-model="form"
:table-loading="loading"
:data="data"
:permission="permissionList"
:before-open="beforeOpen"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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 @click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-circle-check"
@click.stop="handleEnable(scope.row)"
>
</el-button>
</template>
<template #status="{ row }">
<el-tag>{{ row.status === 1 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove, enable } from '@/api/tool/codesetting';
import { getMenuTree } from '@/api/system/menu';
import option from '@/option/tool/codesetting';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
query: {},
search: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: option,
data: [],
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: true,
viewBtn: true,
delBtn: true,
editBtn: true,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
initData() {
getMenuTree().then(res => {
const column = this.findColumn(this.option.column, 'menuId');
column.dicData = res.data.data;
});
},
rowSettings(row) {
return JSON.stringify({
serviceName: row.serviceName,
packageName: row.packageName,
menuId: row.menuId,
baseMode: row.baseMode,
wrapMode: row.wrapMode,
feignMode: row.feignMode,
codeStyle: row.codeStyle,
apiPath: row.apiPath,
webPath: row.webPath,
});
},
rowSettingsForm(data) {
const row = JSON.parse(data);
return {
serviceName: row.serviceName,
packageName: row.packageName,
menuId: row.menuId,
baseMode: row.baseMode,
wrapMode: row.wrapMode,
feignMode: row.feignMode,
codeStyle: row.codeStyle,
apiPath: row.apiPath,
webPath: row.webPath,
};
},
rowSave(row, done, loading) {
const data = {
name: row.name,
code: row.code,
settings: this.rowSettings(row),
};
add(data).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
window.console.log(error);
}
);
},
rowUpdate(row, index, done, loading) {
const data = {
id: row.id,
name: row.name,
code: row.code,
settings: this.rowSettings(row),
};
update(data).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
console.log(error);
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(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();
});
},
handleEnable(row) {
this.$confirm('是否确定启用这条配置?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return enable(row.id);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
if (['add', 'edit'].includes(type)) {
this.initData();
}
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = {
...this.form,
...this.rowSettingsForm(res.data.data.settings),
};
});
}
done();
},
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();
},
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;
let values = {
...params,
...this.query,
category: 1,
};
getList(page.currentPage, page.pageSize, values).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,409 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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="permission.datasource_delete"
@click="handleDelete"
>
</el-button>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove } from '@/api/tool/datasource';
import { mapGetters } from 'vuex';
import func from '@/utils/func';
export default {
data() {
return {
form: {},
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 900,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
grid: true,
selection: true,
dialogClickModal: false,
column: [
{
label: '名称',
prop: 'name',
width: 120,
span: 24,
search: true,
gridRow: true,
rules: [
{
required: true,
message: '请输入数据源名称',
trigger: 'blur',
},
],
},
{
label: '数据类型',
type: 'radio',
value: 1,
span: 24,
width: 120,
searchLabelWidth: 100,
row: true,
gridRow: true,
display: false,
dicData: [
{
label: 'jdbc',
value: 1,
},
],
dataType: 'number',
prop: 'category',
rules: [
{
required: true,
message: '请选择分类',
trigger: 'blur',
},
],
},
{
label: 'YAML',
prop: 'shardingConfig',
span: 24,
minRows: 5,
hide: true,
display: false,
gridRow: true,
type: 'textarea',
rules: [
{
required: true,
message: '请输入YAML配置',
trigger: 'blur',
},
],
},
{
label: '驱动类',
prop: 'driverClass',
type: 'select',
span: 24,
gridRow: true,
dicData: [
{
label: 'com.mysql.cj.jdbc.Driver',
value: 'com.mysql.cj.jdbc.Driver',
},
{
label: 'org.postgresql.Driver',
value: 'org.postgresql.Driver',
},
{
label: 'oracle.jdbc.OracleDriver',
value: 'oracle.jdbc.OracleDriver',
},
{
label: 'com.microsoft.sqlserver.jdbc.SQLServerDriver',
value: 'com.microsoft.sqlserver.jdbc.SQLServerDriver',
},
{
label: 'dm.jdbc.driver.DmDriver',
value: 'dm.jdbc.driver.DmDriver',
},
{
label: 'com.yashandb.jdbc.Driver',
value: 'com.yashandb.jdbc.Driver',
},
{
label: 'com.kingbase8.Driver',
value: 'com.kingbase8.Driver',
},
],
width: 200,
display: true,
rules: [
{
required: true,
message: '请输入驱动类',
trigger: 'blur',
},
],
},
{
label: '连接地址',
prop: 'url',
span: 24,
display: true,
gridRow: true,
rules: [
{
required: true,
message: '请输入连接地址',
trigger: 'blur',
},
],
},
{
label: '用户名',
prop: 'username',
width: 120,
display: true,
gridRow: true,
rules: [
{
required: true,
message: '请输入用户名',
trigger: 'blur',
},
],
},
{
label: '密码',
prop: 'password',
hide: true,
display: true,
rules: [
{
required: true,
message: '请输入密码',
trigger: 'blur',
},
],
},
{
label: '备注',
prop: 'remark',
span: 24,
minRows: 3,
hide: true,
type: 'textarea',
},
],
},
data: [],
driverUrlTemplates: {
'com.mysql.cj.jdbc.Driver':
'jdbc:mysql://localhost:3306/bladex?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true',
'org.postgresql.Driver': 'jdbc:postgresql://127.0.0.1:5432/bladex',
'oracle.jdbc.OracleDriver': 'jdbc:oracle:thin:@//127.0.0.1:1521/ORCLPDB',
'com.microsoft.sqlserver.jdbc.SQLServerDriver':
'jdbc:sqlserver://127.0.0.1:1433;DatabaseName=bladex',
'dm.jdbc.driver.DmDriver':
'jdbc:dm://127.0.0.1:5236/bladex?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8',
'com.yashandb.jdbc.Driver': 'jdbc:yasdb://127.0.0.1:1688/bladex',
'com.kingbase8.Driver': 'jdbc:kingbase8://localhost:4321/bladex',
},
};
},
watch: {
'form.category'() {
const category = func.toInt(this.form.category);
this.$refs.crud.option.column.filter(item => {
if (item.prop === 'driverClass') {
item.display = category === 1;
}
if (item.prop === 'url') {
item.display = category === 1;
}
if (item.prop === 'username') {
item.display = category === 1;
}
if (item.prop === 'password') {
item.display = category === 1;
}
if (item.prop === 'shardingConfig') {
item.display = category === 2;
}
});
},
'form.driverClass'(newDriverClass) {
// 当驱动类发生变化时自动设置对应的默认URL
if (newDriverClass && this.driverUrlTemplates[newDriverClass]) {
// 只在URL为空或者是其他驱动的默认URL时才自动填充
const isDefaultUrl =
!this.form.url || Object.values(this.driverUrlTemplates).includes(this.form.url);
if (isDefaultUrl) {
this.form.url = this.driverUrlTemplates[newDriverClass];
}
}
},
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.datasource_add, false),
viewBtn: this.validData(this.permission.datasource_view, false),
delBtn: this.validData(this.permission.datasource_delete, false),
editBtn: this.validData(this.permission.datasource_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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: '操作成功!',
});
});
},
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 (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
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();
},
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();
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,419 @@
<template>
<basic-container>
<avue-crud
:option="option"
v-model:search="search"
v-model:page="page"
v-model="form"
:table-loading="loading"
:data="data"
:permission="permissionList"
:before-open="beforeOpen"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@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 @click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button type="primary" text icon="el-icon-setting" @click.stop="handleDesign(scope.row)"
>
</el-button>
</template>
</avue-crud>
<el-dialog
title="可视化表单设计"
v-model="designBox"
:fullscreen="true"
:before-close="handleDesignClose"
append-to-body
>
<nf-form-design
ref="formDesign"
style="height: 90vh"
:options="options"
:toolbar="['clear', 'preview', 'import', 'generate']"
:includeFields="[
'group', // 分组
'dynamic', // 子表单
'title', // 标题
'table', // 表格
'input', // 输入框
'password', // 密码
'textarea', // 文本域
'number', // 数字
'ueditor', // 富文本
'map', // 地图选择器
'radio', // 单选
'checkbox', // 多选
'select', // 下拉选择
'tree', // 树形选择
'cascader', // 级联选择
'table-select', // 表格选择
'upload', // 上传
'year', // 年
'month', // 月
'week', // 周
'date', // 日期
'time', // 时间
'datetime', // 日期时间
'daterange', // 日期范围
'datetimerange', // 日期时间范围
'timerange', // 时间范围
'sign', // 签名
'switch', // 开关
'rate', // 评价
'color', // 颜色
'icon', // 图标
'slider', // 滑块
]"
:is-crud="isCrud"
>
<template #toolbar>
<el-button
style="padding: 0"
text
type="primary"
size="default"
icon="el-icon-download"
@click="handleSubmit"
>
保存
</el-button>
</template>
</nf-form-design>
</el-dialog>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove, getTablePrototype } from '@/api/tool/codesetting';
import option from '@/option/tool/formsetting';
import { mapGetters } from 'vuex';
import { validatenull } from '@/utils/validate';
import { getTableInfoByName, getTableList } from '@/api/tool/model';
import func from '@/utils/func';
export default {
data() {
return {
form: {},
query: {},
search: {},
loading: true,
loadingOption: {
lock: true,
text: '物理表读取中',
background: 'rgba(0, 0, 0, 0.7)',
},
designId: '',
designBox: false,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: option,
data: [],
modelTable: '',
datasourceId: '',
isCrud: true,
options: {},
// 默认不需要显示的字段名
hideFields: [
'id',
'tenant_id',
'create_user',
'create_dept',
'create_time',
'update_user',
'update_time',
'status',
'is_deleted',
],
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: true,
viewBtn: true,
delBtn: true,
editBtn: true,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
watch: {
'form.datasourceId'() {
if (!validatenull(this.form.datasourceId)) {
this.datasourceId = this.form.datasourceId;
const fullLoading = this.$loading(this.loadingOption);
getTableList(this.form.datasourceId)
.then(res => {
const column = this.findColumn(this.option.column, 'modelTable');
column.dicData = res.data.data;
fullLoading.close();
})
.catch(() => {
fullLoading.close();
});
}
},
'form.modelTable'() {
if (!validatenull(this.form.modelTable)) {
this.modelTable = this.form.modelTable;
const fullLoading = this.$loading(this.loadingOption);
getTableInfoByName(this.form.modelTable, this.form.datasourceId)
.then(res => {
const result = res.data;
if (result.success) {
// 赋默认值
const { comment } = result.data;
this.form.name = comment;
this.form.code = this.form.modelTable;
fullLoading.close();
}
})
.catch(() => {
fullLoading.close();
});
}
},
},
methods: {
rowSave(row, done, loading) {
const data = {
name: row.name,
code: row.code,
category: 2,
settings: row.settings,
};
add(data).then(
res => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
let design = {
id: res.data.data.id,
name: data.name,
code: data.code,
};
this.handleDesign(design);
done();
},
error => {
loading();
window.console.log(error);
}
);
},
rowUpdate(row, index, done, loading) {
const data = {
id: row.id,
name: row.name,
code: row.code,
settings: row.settings,
};
update(data).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
console.log(error);
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(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();
});
},
handleDesign(design) {
this.designBox = true;
this.designId = design.id;
getDetail(design.id).then(res => {
const settings = res.data.data.settings;
if (!validatenull(settings)) {
this.options = JSON.parse(settings);
} else if (!validatenull(this.modelTable) && !validatenull(this.datasourceId)) {
getTablePrototype(this.modelTable, this.datasourceId).then(res => {
const result = res.data;
if (result.success) {
const column = result.data
.filter(field => {
return !validatenull(field.name) && !this.hideFields.includes(field.name);
})
.map(field => {
return {
type: 'input',
label: field.comment,
display: true,
prop: func.camelCaseString(field.name),
};
});
this.options = {
column: column,
};
this.loading = false;
}
});
}
});
},
handleDesignClose() {
this.modelTable = '';
this.datasourceId = '';
this.options = {};
this.designBox = false;
},
// 可视化表单提交
handleSubmit() {
// json, string, app
this.$refs.formDesign.getData('json').then(data => {
// 表单/表格option
console.log(data);
const row = {
id: this.designId,
settings: JSON.stringify(data),
};
update(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.designBox = false;
},
error => {
console.log(error);
}
);
this.$refs.formDesign.getChangeList().then(changeList => {
// 字段prop, type, label修改记录
console.log(changeList);
});
});
},
beforeOpen(done, type) {
if (['add', 'edit'].includes(type)) {
}
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = {
...this.form,
};
});
}
done();
},
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();
},
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;
let values = {
...params,
...this.query,
category: 2,
};
getList(page.currentPage, page.pageSize, values).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style></style>

393
src/views/tool/model.vue Normal file
View File

@@ -0,0 +1,393 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
v-loading.fullscreen.lock="fullscreenLoading"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button type="danger" icon="el-icon-delete" plain @click="handleDelete"
>
</el-button>
</template>
<template #menu="{ row }">
<el-button
type="primary"
text
icon="el-icon-setting"
plain
class="none-border"
@click.stop="handleModel(row)"
>模型配置
</el-button>
</template>
<template #modelTable="{ row }">
<el-tag>{{ row.modelTable }}</el-tag>
</template>
</avue-crud>
<el-dialog title="数据库模型配置" v-model="modelBox" :fullscreen="true" append-to-body>
<avue-crud
ref="crudModel"
:option="optionModel"
:table-loading="loading"
:data="fields"
></avue-crud>
<template #footer>
<span class="dialog-footer">
<el-button type="danger" @click="modelBox = false"> </el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import {
getList,
getDetail,
add,
update,
remove,
getTableList,
getTableInfoByName,
getModelPrototype,
submitModelPrototype,
} from '@/api/tool/model';
import { entityDic, option, optionModel } from '@/const/tool/model';
import { validatenull } from '@/utils/validate';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
query: {},
loading: true,
loadingOption: {
lock: true,
text: '物理表读取中',
background: 'rgba(0, 0, 0, 0.7)',
},
fullscreenLoading: false,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
modelBox: false,
modelId: 0,
datasourceId: 1,
tableInfo: {},
active: 0,
stepStart: 0,
stepEnd: 4,
data: [],
option: option,
optionModel: optionModel,
formStep: {},
fields: [],
selectionModelList: [],
// 默认不需要显示的字段名
hideFields: [
'id',
'tenant_id',
'create_user',
'create_dept',
'create_time',
'update_user',
'update_time',
'status',
'is_deleted',
],
};
},
watch: {
'form.datasourceId'() {
if (!validatenull(this.form.datasourceId)) {
const fullLoading = this.$loading(this.loadingOption);
getTableList(this.form.datasourceId)
.then(res => {
const column = this.findColumn(this.option.column, 'modelTable');
column.dicData = res.data.data;
fullLoading.close();
})
.catch(() => {
fullLoading.close();
});
}
},
'form.modelTable'() {
if (!validatenull(this.form.modelTable)) {
const fullLoading = this.$loading(this.loadingOption);
getTableInfoByName(this.form.modelTable, this.form.datasourceId)
.then(res => {
const result = res.data;
if (result.success) {
const { comment, entityName } = result.data;
//if (validatenull(this.form.modelClass)) {
this.form.modelClass = entityName;
//}
//if (validatenull(this.form.modelName)) {
this.form.modelName = comment;
//}
//if (validatenull(this.form.modelCode)) {
this.form.modelCode = entityName.replace(/^\S/, s => s.toLowerCase());
//}
fullLoading.close();
}
})
.catch(() => {
fullLoading.close();
});
}
},
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: true,
delBtn: true,
editBtn: true,
viewBtn: false,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
rowSave(row, done, loading) {
add(row).then(
res => {
done();
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$confirm('是否进行模型配置?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
let model = {
id: res.data.data.id,
datasourceId: res.data.data.datasourceId,
};
this.handleModel(model);
});
},
error => {
loading();
window.console.log(error);
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
done();
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
},
error => {
loading();
window.console.log(error);
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(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();
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionModelChange(list) {
this.selectionModelList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
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();
});
},
handleModel(row) {
this.fields = [];
this.modelBox = true;
this.loading = true;
this.modelId = row.id;
this.datasourceId = row.datasourceId;
getModelPrototype(this.modelId, this.datasourceId).then(res => {
const result = res.data;
if (result.success) {
this.fields = result.data;
this.fields.forEach(item => {
item.$cellEdit = true;
item.modelId = this.modelId;
// 根据字段物理类型自动适配实体类型
if (!validatenull(item.name)) {
item.jdbcName = item.name;
item.jdbcType = item.propertyType;
item.jdbcComment = item.comment;
if (item.propertyType === 'LocalDateTime') {
item.propertyType = 'Date';
item.propertyEntity = 'java.util.Date';
} else {
entityDic.forEach(d => {
if (d.label === item.propertyType) {
item.propertyType = d.label;
item.propertyEntity = d.value;
}
});
}
}
// 首次加载配置默认值
if (validatenull(item.id)) {
item.isList = 1;
item.isForm = 1;
item.isRow = 0;
item.isRequired = 0;
item.isQuery = 0;
item.componentType = 'input';
// 默认不需要显示的字段名配置
if (this.hideFields.includes(item.jdbcName)) {
item.isList = 0;
item.isForm = 0;
item.isRequired = 0;
}
}
});
this.loading = false;
}
});
},
handleSubmit() {
console.log(this.fields);
this.$confirm('确定提交模型配置?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.fields.forEach(item => {
entityDic.forEach(d => {
if (d.value === item.propertyEntity) {
item.propertyType = d.label;
}
});
});
submitModelPrototype(this.fields).then(res => {
const result = res.data;
if (result.success) {
this.$message.success(result.msg);
this.modelBox = false;
} else {
this.$message.error(result.msg);
}
});
});
},
},
};
</script>
<style scoped>
.none-border {
border: 0;
background-color: transparent !important;
}
.step-div {
margin-top: 30px;
}
</style>

45
src/views/util/affix.vue Normal file
View File

@@ -0,0 +1,45 @@
<template>
<div class="affix">
<avue-affix id="avue-view">
<span class="affix-affix">固定在最顶部</span>
</avue-affix>
<div class="affix-line"></div>
<avue-affix id="avue-view" :offset-top="50">
<span class="affix-affix">固定在距离顶部 50px 的位置</span>
</avue-affix>
<div class="affix-line"></div>
<avue-affix id="avue-view" :offset-top="100">
<span class="affix-affix">固定在距离顶部 100px 的位置</span>
</avue-affix>
<div class="affix-line"></div>
<avue-affix id="avue-view" :offset-top="150">
<span class="affix-affix">固定在距离顶部 150px 的位置</span>
</avue-affix>
<div style="height: 2000px">
<div style="padding: 15px 20px; font-size: 18px">往下拉就会出现图钉</div>
</div>
</div>
</template>
<script>
export default {};
</script>
<style lang="scss">
.affix {
position: relative;
background-color: #fff;
&-affix {
display: inline-block;
color: #fff;
padding: 10px 30px;
text-align: center;
background: rgba(0, 153, 229, 0.9);
}
&-line {
height: 100px;
}
}
</style>

26
src/views/util/cache.vue Normal file
View File

@@ -0,0 +1,26 @@
<template>
<basic-container>
<h3>这个页面会被 keep-alive</h3>
<el-tag
>在下面的输入框输入任意字符后切换到其它页面再回到此页时输入框文字保留证明被缓存
</el-tag>
<br />
<br />
<el-tag>同时滚动下拉返回时还会保持滚动条所在的位置</el-tag>
<br />
<br />
<el-input v-model="value" placeholder="input here" />
<div style="height: 1000px"></div>
</basic-container>
</template>
<script>
export default {
data() {
return {
value: '',
};
},
};
</script>

View File

@@ -0,0 +1,52 @@
<template>
<basic-container>
<h3>点击新增或编辑跳转到新的页面</h3>
<avue-crud :option="option" :data="data">
<template #menu-left>
<el-button type="primary" @click="handleForm()" icon="el-icon-plus">add</el-button>
</template>
<template #menu="{ row }">
<el-button type="primary" text @click="handleForm(row.id)" icon="el-icon-edit"
>edit
</el-button>
</template>
</avue-crud>
</basic-container>
</template>
<script>
export default {
data() {
return {
option: {
addBtn: false,
editBtn: false,
column: [
{
label: '姓名',
prop: 'name',
},
],
},
data: [
{
id: 1,
name: 'small',
},
],
};
},
methods: {
handleForm(id) {
this.$router.push({
path: '/form-detail/index',
query: {
id: id,
},
});
},
},
};
</script>
<style></style>

198
src/views/util/data.vue Normal file
View File

@@ -0,0 +1,198 @@
<template>
<basic-container>
<h3>数据展示</h3>
<avue-data-pay :option="option"></avue-data-pay>
</basic-container>
</template>
<script>
export default {
data() {
return {
option: {
span: 8,
data: [
{
title: '后台模版',
src: '/img/bg/vip1.png',
money: '299',
dismoney: '199',
tip: '/永久',
color: '#808695',
subtext: '购买',
click: () => {
this.box = true;
},
list: [
{
title: '点击体验',
href: 'https://cli1.avue.top',
check: true,
},
{
title: '面向全屏幕尺寸的响应式适配能力',
check: true,
},
{
title: '支持IE9+等系列浏览器',
check: true,
},
{
title: '全新的前端错误日志监控机制',
check: true,
},
{
title: '基于最新的avuex底层开发',
check: true,
},
{
title: '前端路由动态服务端加载',
check: true,
},
{
title: '灵活的多款主题自由配置',
check: true,
},
{
title: '模块的可拆卸化,达到开箱即用',
check: true,
},
{
title: '免费的私人git私服',
},
{
title: '专属会员群',
},
{
title: '前端最新干货分享',
},
{
title: '赠送 Avue-cli脚手架文档价值¥59.99',
href: 'https://www.kancloud.cn/smallwei/avue',
},
{
title: '赠送 Avue 修仙系列视频教程',
href: 'https://www.bilibili.com/video/av24644922',
check: true,
},
],
},
{
title: 'Avuex源码',
src: '/img/bg/vip2.png',
color: '#ffa820',
money: '999',
dismoney: '399',
tip: '/永久',
subtext: '购买',
click: () => {
this.box = true;
},
list: [
{
title: '一键集成表格的导出excel,打印,等功能',
check: true,
},
{
title: '底层代码可重用轻松对接多个UI框架',
check: true,
},
{
title: '底层更加完善的开发错误调试机制',
check: true,
},
{
title: '一套代码多个终端自适应',
check: true,
},
{
title: '一键集成表格的导出excel打印等常用功能',
check: true,
},
{
title: '表格的批量操作,表单的级联操作更加便捷',
check: true,
},
{
title: '新增大量常用组件(搜索,选项卡)',
check: true,
},
{
title: '新增大量全新可配置的骚属性',
check: true,
},
{
title: '丰富的数据展示模版组件包',
check: true,
},
{
title: '专属的开发者文档,助你快速掌握',
check: true,
},
{
title: '赠送 Avue-cli脚手架文档价值¥59.99',
href: 'https://www.kancloud.cn/smallwei/avue',
check: true,
},
{
title: '赠送 Avue 修仙系列视频教程',
href: 'https://www.bilibili.com/video/av24644922',
check: true,
},
],
},
{
title: '全家桶',
src: '/img/bg/vip3.png',
color: '#ef4868',
money: '999.99',
dismoney: '399.99',
tip: '/永久',
subtext: '购买',
click: () => {
this.box = true;
},
list: [
{
title: '授权商业化开发,永久更新授权使用',
check: true,
},
{
title: '后期更新和新产品将全部免费',
check: true,
},
{
title: '拥有avuex系列的全部特权和全部源码',
check: true,
},
{
title: '免费的私人git私服',
check: true,
},
{
title: '专属会员群',
check: true,
},
{
title: '前端最新干货分享',
check: true,
},
{
title: '赠送 Avue-cli脚手架文档价值¥59.99',
href: 'https://www.kancloud.cn/smallwei/avue',
check: true,
},
{
title: '赠送 Avue 修仙系列视频教程',
href: 'https://www.bilibili.com/video/av24644922',
check: true,
},
],
},
],
},
};
},
};
</script>
<style></style>

View File

@@ -0,0 +1,352 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
:permission="permissionList"
:before-open="beforeOpen"
:before-close="beforeClose"
@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.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-circle-plus"
@click.stop="handleAdd(scope.row, scope.index)"
>新增子项
</el-button>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, remove, update, add, getDict, getDictTree } from '@/api/system/dict';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
option: {
tip: false,
searchShow: true,
searchMenuSpan: 6,
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
menuWidth: 300,
dialogWidth: 880,
column: [
{
label: '字典编号',
prop: 'code',
search: true,
span: 24,
rules: [
{
required: true,
message: '请输入字典编号',
trigger: 'blur',
},
],
},
{
label: '字典名称',
prop: 'dictValue',
search: true,
align: 'center',
rules: [
{
required: true,
message: '请输入字典名称',
trigger: 'blur',
},
],
},
{
label: '上级字典',
prop: 'parentId',
type: 'tree',
dicData: [],
hide: true,
props: {
label: 'title',
},
rules: [
{
required: false,
message: '请选择上级字典',
trigger: 'click',
},
],
},
{
label: '字典键值',
prop: 'dictKey',
type: 'number',
rules: [
{
required: true,
message: '请输入字典键值',
trigger: 'blur',
},
],
},
{
label: '字典排序',
prop: 'sort',
type: 'number',
rules: [
{
required: true,
message: '请输入字典排序',
trigger: 'blur',
},
],
},
{
label: '封存',
prop: 'isSealed',
type: 'select',
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
slot: true,
rules: [
{
required: true,
message: '请选择封存',
trigger: 'blur',
},
],
},
{
label: '字典备注',
prop: 'remark',
search: true,
hide: true,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.dict_add, false),
viewBtn: this.validData(this.permission.dict_view, false),
delBtn: this.validData(this.permission.dict_delete, false),
editBtn: this.validData(this.permission.dict_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
mounted() {
getDictTree().then(res => {
const column = this.findColumn(this.optionChild.column, 'parentId');
column.dicData = res.data.data;
});
},
methods: {
handleAdd(row) {
this.$refs.crud.value.code = row.code;
this.$refs.crud.value.parentId = row.id;
this.$refs.crud.option.column.filter(item => {
if (item.prop === 'code') {
item.value = row.code;
item.addDisabled = true;
}
if (item.prop === 'parentId') {
item.value = row.id;
item.addDisabled = true;
}
});
this.$refs.crud.rowAdd();
},
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();
},
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 (['edit', 'view'].includes(type)) {
getDict(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
beforeClose(done) {
this.$refs.crud.tableForm = {};
this.$refs.crud.value.code = '';
this.$refs.crud.value.parentId = '';
this.$refs.crud.value.addDisabled = false;
this.$refs.crud.option.column.filter(item => {
if (item.prop === 'code') {
item.value = '';
item.addDisabled = false;
}
if (item.prop === 'parentId') {
item.value = '';
item.addDisabled = false;
}
});
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 => {
this.data = res.data.data;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,469 @@
<template>
<div>
<basic-container>
<el-row>
<el-col :span="12">
<avue-crud
:option="option"
:table-loading="loading"
:data="dataParent"
:page="pageParent"
ref="crud"
v-model="formParent"
:permission="permissionList"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@row-click="handleRowClick"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoadParent"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</el-col>
<el-col :span="12">
<avue-crud
:option="option"
:table-loading="loading"
:data="dataChild"
:page="pageChild"
ref="crudChild"
v-model="formChild"
:permission="permissionList"
:before-open="beforeOpenChild"
@row-del="rowDelChild"
@row-update="rowUpdateChild"
@row-save="rowSaveChild"
@search-change="searchChangeChild"
@search-reset="searchResetChild"
@selection-change="selectionChangeChild"
@current-change="currentChangeChild"
@size-change="sizeChangeChild"
@refresh-change="refreshChangeChild"
@on-load="onLoadChild"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</el-col>
</el-row>
</basic-container>
</div>
</template>
<script>
import { getParentList, getChildList, remove, update, add, getDict } from '@/api/system/dict';
import { mapGetters } from 'vuex';
export default {
data() {
return {
parentId: 0,
formParent: {},
formChild: {},
selectionList: [],
query: {},
loading: true,
pageParent: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
pageChild: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
dataParent: [],
dataChild: [],
option: {
tip: false,
searchShow: true,
searchMenuSpan: 6,
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
menuWidth: 300,
dialogWidth: 880,
column: [
{
label: '字典编号',
prop: 'code',
search: true,
span: 24,
rules: [
{
required: true,
message: '请输入字典编号',
trigger: 'blur',
},
],
},
{
label: '字典键值',
prop: 'dictKey',
type: 'number',
rules: [
{
required: true,
message: '请输入字典键值',
trigger: 'blur',
},
],
},
{
label: '字典名称',
prop: 'dictValue',
search: true,
align: 'center',
rules: [
{
required: true,
message: '请输入字典名称',
trigger: 'blur',
},
],
},
{
label: '字典排序',
prop: 'sort',
type: 'number',
rules: [
{
required: true,
message: '请输入字典排序',
trigger: 'blur',
},
],
},
{
label: '封存',
prop: 'isSealed',
type: 'select',
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
slot: true,
rules: [
{
required: true,
message: '请选择封存',
trigger: 'blur',
},
],
},
{
label: '字典备注',
prop: 'remark',
search: true,
hide: true,
},
],
},
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.dict_add, false),
viewBtn: this.validData(this.permission.dict_view, false),
delBtn: this.validData(this.permission.dict_delete, false),
editBtn: this.validData(this.permission.dict_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
rowSave(row, done, loading) {
add(row).then(
() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoadParent(this.pageParent);
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.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleRowClick(row) {
this.parentId = row.id;
this.onLoadChild(this.pageChild);
},
searchReset() {
this.query = {};
this.onLoadParent(this.pageParent);
},
searchChange(params, done) {
this.query = params;
this.pageParent.currentPage = 1;
this.onLoadParent(this.pageParent, 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.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDict(this.formParent.id).then(res => {
this.formParent = res.data.data;
});
}
done();
},
currentChange(currentPage) {
this.pageParent.currentPage = currentPage;
},
sizeChange(pageSize) {
this.pageParent.pageSize = pageSize;
},
refreshChange() {
this.onLoadParent(this.page, this.query);
},
rowSaveChild(row, done, loading) {
add(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateChild(row, index, done, loading) {
update(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelChild(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
searchResetChild() {
this.query = {};
this.onLoadChild(this.pageChild);
},
searchChangeChild(params, done) {
this.query = params;
this.pageChild.currentPage = 1;
this.onLoadChild(this.pageChild, params);
done();
},
selectionChangeChild(list) {
this.selectionList = list;
},
selectionClearChild() {
this.selectionList = [];
this.$refs.crudChild.toggleSelection();
},
handleDeleteChild() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudChild.toggleSelection();
});
},
beforeOpenChild(done, type) {
if (['edit', 'view'].includes(type)) {
getDict(this.formChild.id).then(res => {
this.formChild = res.data.data;
});
}
done();
},
currentChangeChild(currentPage) {
this.pageChild.currentPage = currentPage;
},
sizeChangeChild(pageSize) {
this.pageChild.pageSize = pageSize;
},
refreshChangeChild() {
this.onLoadChild(this.pageChild, this.query);
},
onLoadParent(page, params = {}) {
this.loading = true;
getParentList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(
res => {
const data = res.data.data;
this.pageParent.total = data.total;
this.dataParent = data.records;
this.loading = false;
this.selectionClear();
}
);
},
onLoadChild(page, params = {}) {
this.loading = true;
getChildList(
page.currentPage,
page.pageSize,
this.parentId,
Object.assign(params, this.query)
).then(res => {
const data = res.data.data;
this.pageChild.total = data.total;
this.dataChild = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,474 @@
<template>
<div>
<basic-container>
<el-row>
<el-col :span="24">
<avue-crud
:option="option"
:table-loading="loading"
:data="dataParent"
:page="pageParent"
ref="crud"
v-model="formParent"
:permission="permissionList"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@row-click="handleRowClick"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoadParent"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</el-col>
</el-row>
</basic-container>
<basic-container>
<el-row>
<el-col :span="24">
<avue-crud
:option="option"
:table-loading="loading"
:data="dataChild"
:page="pageChild"
ref="crudChild"
v-model="formChild"
:permission="permissionList"
:before-open="beforeOpenChild"
@row-del="rowDelChild"
@row-update="rowUpdateChild"
@row-save="rowSaveChild"
@search-change="searchChangeChild"
@search-reset="searchResetChild"
@selection-change="selectionChangeChild"
@current-change="currentChangeChild"
@size-change="sizeChangeChild"
@refresh-change="refreshChangeChild"
@on-load="onLoadChild"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</el-col>
</el-row>
</basic-container>
</div>
</template>
<script>
import { getParentList, getChildList, remove, update, add, getDict } from '@/api/system/dict';
import { mapGetters } from 'vuex';
export default {
data() {
return {
parentId: 0,
formParent: {},
formChild: {},
selectionList: [],
query: {},
loading: true,
pageParent: {
pageSize: 5,
pageSizes: [5, 10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
pageChild: {
pageSize: 5,
pageSizes: [5, 10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
dataParent: [],
dataChild: [],
option: {
tip: false,
searchShow: true,
searchMenuSpan: 6,
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
height: 300,
menuWidth: 300,
dialogWidth: 880,
column: [
{
label: '字典编号',
prop: 'code',
search: true,
span: 24,
rules: [
{
required: true,
message: '请输入字典编号',
trigger: 'blur',
},
],
},
{
label: '字典键值',
prop: 'dictKey',
type: 'number',
rules: [
{
required: true,
message: '请输入字典键值',
trigger: 'blur',
},
],
},
{
label: '字典名称',
prop: 'dictValue',
search: true,
align: 'center',
rules: [
{
required: true,
message: '请输入字典名称',
trigger: 'blur',
},
],
},
{
label: '字典排序',
prop: 'sort',
type: 'number',
rules: [
{
required: true,
message: '请输入字典排序',
trigger: 'blur',
},
],
},
{
label: '封存',
prop: 'isSealed',
type: 'select',
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
slot: true,
rules: [
{
required: true,
message: '请选择封存',
trigger: 'blur',
},
],
},
{
label: '字典备注',
prop: 'remark',
search: true,
hide: true,
},
],
},
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.dict_add, false),
viewBtn: this.validData(this.permission.dict_view, false),
delBtn: this.validData(this.permission.dict_delete, false),
editBtn: this.validData(this.permission.dict_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
rowSave(row, done, loading) {
add(row).then(
() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoadParent(this.pageParent);
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.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleRowClick(row) {
this.parentId = row.id;
this.onLoadChild(this.pageChild);
},
searchReset() {
this.query = {};
this.onLoadParent(this.pageParent);
},
searchChange(params, done) {
this.query = params;
this.pageParent.currentPage = 1;
this.onLoadParent(this.pageParent, 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.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDict(this.formParent.id).then(res => {
this.formParent = res.data.data;
});
}
done();
},
currentChange(currentPage) {
this.pageParent.currentPage = currentPage;
},
sizeChange(pageSize) {
this.pageParent.pageSize = pageSize;
},
refreshChange() {
this.onLoadParent(this.page, this.query);
},
rowSaveChild(row, done, loading) {
add(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateChild(row, index, done, loading) {
update(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelChild(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
searchResetChild() {
this.query = {};
this.onLoadChild(this.pageChild);
},
searchChangeChild(params, done) {
this.query = params;
this.pageChild.currentPage = 1;
this.onLoadChild(this.pageChild, params);
done();
},
selectionChangeChild(list) {
this.selectionList = list;
},
selectionClearChild() {
this.selectionList = [];
this.$refs.crudChild.toggleSelection();
},
handleDeleteChild() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudChild.toggleSelection();
});
},
beforeOpenChild(done, type) {
if (['edit', 'view'].includes(type)) {
getDict(this.formChild.id).then(res => {
this.formChild = res.data.data;
});
}
done();
},
currentChangeChild(currentPage) {
this.pageChild.currentPage = currentPage;
},
sizeChangeChild(pageSize) {
this.pageChild.pageSize = pageSize;
},
refreshChangeChild() {
this.onLoadChild(this.pageChild, this.query);
},
onLoadParent(page, params = {}) {
this.loading = true;
getParentList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(
res => {
const data = res.data.data;
this.pageParent.total = data.total;
this.dataParent = data.records;
this.loading = false;
this.selectionClear();
}
);
},
onLoadChild(page, params = {}) {
this.loading = true;
getChildList(
page.currentPage,
page.pageSize,
this.parentId,
Object.assign(params, this.query)
).then(res => {
const data = res.data.data;
this.pageChild.total = data.total;
this.dataChild = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,465 @@
<template>
<el-row>
<el-col :span="11">
<basic-container>
<el-card class="box-card">
<template #header>
<div class="clearfix">
<span>字典列表</span>
</div>
</template>
<div class="clearfix">
<avue-crud
:option="optionParent"
:table-loading="loading"
:data="dataParent"
:page="pageParent"
ref="crud"
v-model="formParent"
:permission="permissionList"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@row-click="handleRowClick"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoadParent"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</div>
</el-card>
</basic-container>
</el-col>
<el-col :span="13">
<basic-container>
<el-card class="box-card">
<template #header>
<div class="clearfix">
<span>[{{ dictValue }}] 字典详情</span>
</div>
</template>
<div class="clearfix">
<avue-crud
:option="optionChild"
:table-loading="loadingChild"
:data="dataChild"
ref="crudChild"
v-model="formChild"
:permission="permissionList"
:before-open="beforeOpenChild"
:before-close="beforeCloseChild"
@row-del="rowDelChild"
@row-update="rowUpdateChild"
@row-save="rowSaveChild"
@search-change="searchChangeChild"
@search-reset="searchResetChild"
@selection-change="selectionChangeChild"
@current-change="currentChangeChild"
@size-change="sizeChangeChild"
@refresh-change="refreshChangeChild"
@on-load="onLoadChild"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-circle-plus"
@click.stop="handleAdd(scope.row, scope.index)"
v-if="userInfo.authority.includes('admin')"
>新增子项
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</div>
</el-card>
</basic-container>
</el-col>
</el-row>
</template>
<script>
import {
getParentList,
getChildList,
remove,
update,
add,
getDict,
getDictTree,
} from '@/api/system/dict';
import { optionParent, optionChild } from '@/option/system/dict';
import { mapGetters } from 'vuex';
export default {
data() {
return {
dictValue: '暂无',
parentId: -1,
formParent: {},
formChild: {},
selectionList: [],
query: {},
loading: true,
loadingChild: true,
pageParent: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
pageChild: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
dataParent: [],
dataChild: [],
optionParent: optionParent,
optionChild: optionChild,
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.dict_add, false),
delBtn: this.validData(this.permission.dict_delete, false),
editBtn: this.validData(this.permission.dict_edit, false),
viewBtn: false,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
mounted() {
this.initData();
},
methods: {
initData() {
getDictTree().then(res => {
const column = this.findColumn(this.optionChild.column, 'parentId');
column.dicData = res.data.data;
});
},
handleAdd(row) {
this.$refs.crudChild.value.dictValue = '';
this.$refs.crudChild.value.dictKey = '';
this.$refs.crudChild.value.sort = 0;
this.$refs.crudChild.value.isSealed = 0;
this.$refs.crudChild.value.remark = '';
this.$refs.crudChild.modelValue.parentId = row.id;
this.$refs.crudChild.option.column.filter(item => {
if (item.prop === 'parentId') {
item.value = row.id;
}
});
this.$refs.crudChild.rowAdd();
},
rowSave(row, done, loading) {
const form = {
...row,
dictKey: -1,
};
add(form).then(
() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
this.onLoadChild(this.pageChild);
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleRowClick(row) {
this.query = {};
this.parentId = row.id;
this.dictValue = row.dictValue;
this.$refs.crudChild.value.code = row.code;
this.$refs.crudChild.modelValue.parentId = row.id;
this.$refs.crudChild.option.column.filter(item => {
if (item.prop === 'code') {
item.value = row.code;
}
if (item.prop === 'parentId') {
item.value = row.id;
}
});
this.onLoadChild(this.pageChild);
},
searchReset() {
this.query = {};
this.onLoadParent(this.pageParent);
},
searchChange(params, done) {
this.query = params;
this.pageParent.currentPage = 1;
this.onLoadParent(this.pageParent, 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.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDict(this.formParent.id).then(res => {
this.formParent = res.data.data;
});
}
done();
},
currentChange(currentPage) {
this.pageParent.currentPage = currentPage;
},
sizeChange(pageSize) {
this.pageParent.pageSize = pageSize;
},
refreshChange() {
this.onLoadParent(this.pageParent, this.query);
},
rowSaveChild(row, done, loading) {
add(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateChild(row, index, done, loading) {
update(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelChild(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
searchResetChild() {
this.query = {};
this.onLoadChild(this.pageChild);
},
searchChangeChild(params, done) {
this.query = params;
this.pageChild.currentPage = 1;
this.onLoadChild(this.pageChild, params);
done();
},
selectionChangeChild(list) {
this.selectionList = list;
},
selectionClearChild() {
this.selectionList = [];
this.$refs.crudChild.toggleSelection();
},
handleDeleteChild() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudChild.toggleSelection();
});
},
beforeOpenChild(done, type) {
if (['add', 'edit'].includes(type)) {
this.initData();
}
if (['edit', 'view'].includes(type)) {
getDict(this.formChild.id).then(res => {
this.formChild = res.data.data;
});
}
done();
},
beforeCloseChild(done) {
this.$refs.crudChild.modelValue.parentId = this.parentId;
this.$refs.crudChild.option.column.filter(item => {
if (item.prop === 'parentId') {
item.value = this.parentId;
}
});
done();
},
currentChangeChild(currentPage) {
this.pageChild.currentPage = currentPage;
},
sizeChangeChild(pageSize) {
this.pageChild.pageSize = pageSize;
},
refreshChangeChild() {
this.onLoadChild(this.pageChild, this.query);
},
onLoadParent(page, params = {}) {
this.loading = true;
getParentList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(
res => {
const data = res.data.data;
this.pageParent.total = data.total;
this.dataParent = data.records;
this.loading = false;
this.selectionClear();
}
);
},
onLoadChild(page, params = {}) {
this.loadingChild = true;
getChildList(
page.currentPage,
page.pageSize,
this.parentId,
Object.assign(params, this.query)
).then(res => {
this.dataChild = res.data.data;
this.loadingChild = false;
this.selectionClear();
});
},
},
};
</script>

View File

@@ -0,0 +1,459 @@
<template>
<el-row>
<el-col :span="11">
<basic-container>
<el-card class="box-card">
<template #header>
<div class="clearfix">
<span>业务字典列表</span>
</div>
</template>
<div class="clearfix">
<avue-crud
:option="optionParent"
:table-loading="loading"
:data="dataParent"
:page="pageParent"
ref="crud"
v-model="formParent"
:permission="permissionList"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@row-click="handleRowClick"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoadParent"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</div>
</el-card>
</basic-container>
</el-col>
<el-col :span="13">
<basic-container>
<el-card class="box-card">
<template #header>
<div class="clearfix">
<span>[{{ dictValue }}] 业务字典详情</span>
</div>
</template>
<div class="clearfix">
<avue-crud
:option="optionChild"
:table-loading="loadingChild"
:data="dataChild"
ref="crudChild"
v-model="formChild"
:permission="permissionList"
:before-open="beforeOpenChild"
:before-close="beforeCloseChild"
@row-del="rowDelChild"
@row-update="rowUpdateChild"
@row-save="rowSaveChild"
@search-change="searchChangeChild"
@search-reset="searchResetChild"
@selection-change="selectionChangeChild"
@current-change="currentChangeChild"
@size-change="sizeChangeChild"
@refresh-change="refreshChangeChild"
@on-load="onLoadChild"
>
<template #menu-left>
<el-button
type="danger"
icon="el-icon-delete"
v-if="permission.dict_delete"
plain
@click="handleDelete"
>
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-circle-plus"
@click.stop="handleAdd(scope.row, scope.index)"
v-if="userInfo.authority.includes('admin')"
>新增子项
</el-button>
</template>
<template #isSealed="{ row }">
<el-tag>{{ row.isSealed === 0 ? '否' : '是' }}</el-tag>
</template>
</avue-crud>
</div>
</el-card>
</basic-container>
</el-col>
</el-row>
</template>
<script>
import {
getParentList,
getChildList,
remove,
update,
add,
getDict,
getDictTree,
} from '@/api/system/dictbiz';
import { optionParent, optionChild } from '@/option/system/dictbiz';
import { mapGetters } from 'vuex';
export default {
data() {
return {
dictValue: '暂无',
parentId: -1,
formParent: {},
formChild: {},
selectionList: [],
query: {},
loading: true,
loadingChild: true,
pageParent: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
pageChild: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
dataParent: [],
dataChild: [],
optionParent: optionParent,
optionChild: optionChild,
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.dictbiz_add, false),
delBtn: this.validData(this.permission.dictbiz_delete, false),
editBtn: this.validData(this.permission.dictbiz_edit, false),
viewBtn: false,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
mounted() {
this.initData();
},
methods: {
initData() {
getDictTree().then(res => {
const column = this.findColumn(this.optionChild.column, 'parentId');
column.dicData = res.data.data;
});
},
handleAdd(row) {
this.$refs.crudChild.modelValue.parentId = row.id;
this.$refs.crudChild.option.column.filter(item => {
if (item.prop === 'parentId') {
item.value = row.id;
}
});
this.$refs.crudChild.rowAdd();
},
rowSave(row, done, loading) {
const form = {
...row,
dictKey: -1,
};
add(form).then(
() => {
this.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoadParent(this.pageParent);
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.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleRowClick(row) {
this.query = {};
this.parentId = row.id;
this.dictValue = row.dictValue;
this.$refs.crudChild.value.code = row.code;
this.$refs.crudChild.modelValue.parentId = row.id;
this.$refs.crudChild.option.column.filter(item => {
if (item.prop === 'code') {
item.value = row.code;
}
if (item.prop === 'parentId') {
item.value = row.id;
}
});
this.onLoadChild(this.pageChild);
},
searchReset() {
this.query = {};
this.onLoadParent(this.pageParent);
},
searchChange(params, done) {
this.query = params;
this.pageParent.currentPage = 1;
this.onLoadParent(this.pageParent, 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.onLoadParent(this.pageParent);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDict(this.formParent.id).then(res => {
this.formParent = res.data.data;
});
}
done();
},
currentChange(currentPage) {
this.pageParent.currentPage = currentPage;
},
sizeChange(pageSize) {
this.pageParent.pageSize = pageSize;
},
refreshChange() {
this.onLoadParent(this.pageParent, this.query);
},
rowSaveChild(row, done, loading) {
add(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdateChild(row, index, done, loading) {
update(row).then(
() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDelChild(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(row.id);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
searchResetChild() {
this.query = {};
this.onLoadChild(this.pageChild);
},
searchChangeChild(params, done) {
this.query = params;
this.pageChild.currentPage = 1;
this.onLoadChild(this.pageChild, params);
done();
},
selectionChangeChild(list) {
this.selectionList = list;
},
selectionClearChild() {
this.selectionList = [];
this.$refs.crudChild.toggleSelection();
},
handleDeleteChild() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
})
.then(() => {
this.onLoadChild(this.pageChild);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crudChild.toggleSelection();
});
},
beforeOpenChild(done, type) {
if (['add', 'edit'].includes(type)) {
this.initData();
}
if (['edit', 'view'].includes(type)) {
getDict(this.formChild.id).then(res => {
this.formChild = res.data.data;
});
}
done();
},
beforeCloseChild(done) {
this.$refs.crudChild.modelValue.parentId = this.parentId;
this.$refs.crudChild.option.column.filter(item => {
if (item.prop === 'parentId') {
item.value = this.parentId;
}
});
done();
},
currentChangeChild(currentPage) {
this.pageChild.currentPage = currentPage;
},
sizeChangeChild(pageSize) {
this.pageChild.pageSize = pageSize;
},
refreshChangeChild() {
this.onLoadChild(this.pageChild, this.query);
},
onLoadParent(page, params = {}) {
this.loading = true;
getParentList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(
res => {
const data = res.data.data;
this.pageParent.total = data.total;
this.dataParent = data.records;
this.loading = false;
this.selectionClear();
}
);
},
onLoadChild(page, params = {}) {
this.loadingChild = true;
getChildList(
page.currentPage,
page.pageSize,
this.parentId,
Object.assign(params, this.query)
).then(res => {
this.dataChild = res.data.data;
this.loadingChild = false;
this.selectionClear();
});
},
},
};
</script>

View File

@@ -0,0 +1,37 @@
<template>
<basic-container>
<h3>{{ $route.query.id ? '编辑' : '新增' }}</h3>
<avue-form :option="option" v-model="form">
<template #menu-form>
<el-button icon="el-icon-back" @click="handleBack()"> </el-button>
</template>
</avue-form>
</basic-container>
</template>
<script>
export default {
data() {
return {
form: {},
option: {
labelWidth: 110,
column: [
{
label: '姓名',
prop: 'name',
},
],
},
};
},
methods: {
handleBack() {
this.$router.$avueRouter.closeTag();
this.$router.back();
},
},
};
</script>
<style></style>

53
src/views/util/form.vue Normal file
View File

@@ -0,0 +1,53 @@
<template>
<basic-container>
<h3>表单例子</h3>
<avue-form :option="option" v-model="form"></avue-form>
</basic-container>
</template>
<script>
export default {
data() {
return {
form: {},
option: {
labelWidth: 110,
column: [
{
label: '用户名',
prop: 'username',
row: true,
},
{
label: '密码',
prop: 'password',
type: 'password',
row: true,
},
{
label: '再次输入密码',
prop: 'password',
type: 'newpassword',
row: true,
},
{
label: '申请日期',
prop: 'date',
type: 'date',
row: true,
},
{
label: '个性签名',
prop: 'textarea',
type: 'textarea',
minRows: 8,
row: true,
},
],
},
};
},
};
</script>
<style></style>

16
src/views/util/logs.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<basic-container>
<h3>请打开浏览器控制台然后点击下面的按钮</h3>
<el-button type="danger" @click="handleNewError">触发一个错误</el-button>
</basic-container>
</template>
<script>
export default {
methods: {
handleNewError() {
window.console.log(a); // eslint-disable-line
},
},
};
</script>

View File

@@ -0,0 +1,81 @@
<template>
<div>
<basic-container>
<h3>表格权限控制</h3>
<avue-crud ref="crud" :permission="permission" :option="option" :data="data">
<template #expand="scope">
{{ scope }}
</template>
</avue-crud>
</basic-container>
<basic-container>
权限开关
<el-switch
:active-value="false"
:inactive-value="true"
v-model="text"
active-color="#13ce66"
inactive-color="#ff4949"
>
</el-switch>
<p>
具体参考<a href="https://avuex.avue.top/#/doc/crud-permission"
>https://avuex.avue.top/#/doc/crud-permission</a
>
</p>
</basic-container>
</div>
</template>
<script>
export default {
data() {
return {
text: false,
permission: {},
option: {
expand: true,
column: [
{
label: '姓名',
prop: 'name',
},
{
label: '年龄',
prop: 'sex',
},
],
},
data: [
{
id: 1,
name: '张三',
sex: 12,
},
{
id: 2,
name: '李四',
sex: 20,
},
],
};
},
watch: {
text() {
if (this.text === true) {
this.permission = {
delBtn: false,
addBtn: false,
};
} else {
this.permission = {
delBtn: true,
addBtn: true,
};
}
},
},
};
</script>
<style></style>

95
src/views/util/store.vue Normal file
View File

@@ -0,0 +1,95 @@
<template>
<basic-container>
<h3>存储</h3>
<el-tag class="title">基本读写删(持久化存储)</el-tag>
<div class="box">
<el-button type="primary" @click="setItem({ name: 'username', value: 'avuex' })"
>set('username', 'avuex')
</el-button>
<el-button type="success" @click="getItem({ name: 'username' })">get('username')</el-button>
<el-button type="danger" @click="delItem({ name: 'username' })"
>remove('username')
</el-button>
</div>
<el-tag class="title">设置session(session存储)</el-tag>
<div class="box">
<el-button
type="primary"
@click="setItem({ name: 'username', value: 'avuex', type: 'session' })"
>set('username', 'avuex')
</el-button>
<el-button type="success" @click="getItem({ name: 'username', type: 'session' })"
>get('username')
</el-button>
<el-button type="danger" @click="delItem({ name: 'username', type: 'session' })"
>remove('username')
</el-button>
</div>
<el-tag class="title">获取所有可以获得的数据</el-tag>
<div class="box">
<el-button type="success" @click="getAll()">getAll(持久化存储)</el-button>
<el-button type="success" @click="getAll({ type: 'session' })"
>getAll(session存储)
</el-button>
<el-button type="danger" @click="clearAll()">delAll(持久化存储)</el-button>
<el-button type="danger" @click="clearAll({ type: 'session' })"
>delAll(session存储)
</el-button>
</div>
</basic-container>
</template>
<script>
import { setStore, getStore, removeStore, clearStore, getAllStore } from '@/utils/store';
export default {
name: 'store',
methods: {
setItem(params = {}) {
const { name, value, type } = params;
setStore({
name: name,
content: value,
type: type,
});
this.$message(`设置数据 ${name} = ${value}`);
},
getItem(params = {}) {
const { name, type } = params;
const content = getStore({
name: name,
type: type,
});
this.$message(`获取数据 ${name} = ${content}`);
},
delItem(params = {}) {
const { name, type } = params;
removeStore({ name, type });
this.$message(`删除数据 ${name}`);
},
clearAll(params = {}) {
clearStore(params);
this.$message(`清除全部数据完成`);
},
getAll(params = {}) {
const list = getAllStore(params);
window.console.log(list);
this.$message(`结果已经打印到控制台`);
},
},
};
</script>
<style lang="scss">
.title {
margin-bottom: 10px;
}
.box {
margin-bottom: 20px;
}
</style>

126
src/views/util/table.vue Normal file
View File

@@ -0,0 +1,126 @@
<template>
<basic-container>
<h3>表格例子</h3>
<avue-crud :option="option" v-model:page="page" :data="data" />
</basic-container>
</template>
<script>
export default {
data() {
return {
page: {
total: 122,
},
data: [
{
username: 'smallwei',
name: 'avue',
password: '123456',
newpassword: '123456',
date: '2019-01-01',
textarea: '这是一条很长很长很长很长很长很长很长很长的个性签名',
},
{
username: 'smallwei',
name: 'avue',
password: '123456',
newpassword: '123456',
date: '2019-01-01',
textarea: '这是一条很长很长很长很长很长很长很长很长的个性签名',
},
{
username: 'smallwei',
name: 'avue',
password: '123456',
newpassword: '123456',
date: '2019-01-01',
textarea: '这是一条很长很长很长很长很长很长很长很长的个性签名',
},
{
username: 'smallwei',
name: 'avue',
password: '123456',
newpassword: '123456',
date: '2019-01-01',
textarea: '这是一条很长很长很长很长很长很长很长很长的个性签名',
},
{
username: 'smallwei',
name: 'avue',
password: '123456',
newpassword: '123456',
date: '2019-01-01',
textarea: '这是一条很长很长很长很长很长很长很长很长的个性签名',
},
{
username: 'smallwei',
name: 'avue',
password: '123456',
newpassword: '123456',
date: '2019-01-01',
textarea: '这是一条很长很长很长很长很长很长很长很长的个性签名',
},
{
username: 'smallwei',
name: 'avue',
password: '123456',
newpassword: '123456',
date: '2019-01-01',
textarea: '这是一条很长很长很长很长很长很长很长很长的个性签名',
},
],
option: {
column: [
{
label: '用户名',
prop: 'username',
span: 14,
row: true,
},
{
label: '姓名',
prop: 'name',
span: 14,
row: true,
},
{
label: '密码',
prop: 'password',
type: 'password',
span: 14,
row: true,
},
{
label: '确认密码',
prop: 'newpassword',
type: 'password',
hide: true,
span: 14,
row: true,
},
{
label: '申请日期',
prop: 'date',
type: 'date',
span: 14,
row: true,
},
{
label: '个性签名',
prop: 'textarea',
type: 'textarea',
minRows: 8,
span: 24,
overHidden: true,
row: true,
},
],
},
};
},
};
</script>
<style></style>

18
src/views/util/tags.vue Normal file
View File

@@ -0,0 +1,18 @@
<template>
<basic-container>
<h3>标签</h3>
<el-button type="primary" @click="$router.push('/test/index')">打开一个页面</el-button>
<el-button type="primary" @click="$router.$avueRouter.closeTag('/test/index')"
>关闭打开的页面
</el-button>
<el-button type="primary" @click="$router.$avueRouter.closeTag()">关闭本标签</el-button>
</basic-container>
</template>
<script>
export default {
methods: {},
};
</script>
<style></style>

19
src/views/util/test.vue Normal file
View File

@@ -0,0 +1,19 @@
<template>
<div>
<basic-container>测试页</basic-container>
</div>
</template>
<script>
export default {
name: 'wel',
data() {
return {};
},
computed: {},
created() {},
methods: {},
};
</script>
<style scoped="scoped" lang="scss"></style>

26
src/views/util/top.vue Normal file
View File

@@ -0,0 +1,26 @@
<template>
<div>
<div style="height: 2000px; background-color: #fff">
<div style="padding: 15px 20px; font-size: 18px">往下拉就会出现返回菜单</div>
</div>
<avue-back-top id="avue-view"></avue-back-top>
<avue-back-top id="avue-view" :height="100" :bottom="200">
<div class="top">返回顶端</div>
</avue-back-top>
</div>
</template>
<script>
export default {};
</script>
<style scoped>
.top {
padding: 10px;
font-size: 14px;
background: rgba(0, 153, 229, 0.7);
color: #fff;
text-align: center;
border-radius: 2px;
}
</style>

196
src/views/wel/dashboard.vue Normal file
View File

@@ -0,0 +1,196 @@
<template>
<basic-container>
<div class="wel">
<basic-block
:width="width"
:height="height"
icon="el-icon-platform-eleme"
text="开始菜单1"
time="1"
background="/img/bg/bg3.jpg"
color="#d56259"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-eleme"
text="开始菜单2"
time="2"
background="/img/bg/bg2.jpg"
color="#419ce7"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-delete-solid"
text="开始菜单3"
time="3"
color="#56b69b"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-delete"
text="开始菜单4"
time="4"
color="#d44858"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-s-tools"
text="开始菜单5"
time="5"
color="#3a1f7e"
></basic-block>
<basic-block
:width="410"
:height="height"
icon="el-icon-setting"
text="开始菜单6"
time="6"
background="/img/bg/bg1.jpg"
dept="这是一段很长的很长很长很长的描述这是一段很长的很长很长很长的描述"
color="#422829"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-user-solid"
text="开始菜单7"
time="7"
color="#613cbd"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-star-off"
text="开始菜单8"
time="8"
color="#da542e"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-goods"
text="开始菜单9"
time="9"
color="#2e8aef"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-circle-check"
text="开始菜单10"
time="10"
color="#3d17b8"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-s-platform"
text="开始菜单11"
time="11"
color="#e31462"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-s-fold"
text="开始菜单12"
time="12"
color="#d9532d"
></basic-block>
<basic-block
:width="410"
:height="height"
icon="el-icon-s-open"
text="开始菜单13"
time="13"
dept="这是一段很长的很长很长很长的描述这是一段很长的很长很长很长的描述"
color="#b72147"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-s-flag"
text="开始菜单14"
time="14"
color="#01a100"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-s-data"
text="开始菜单15"
time="15"
color="#0c56bf"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-s-grid"
text="开始菜单16"
time="16"
color="#0098a9"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-s-release"
text="开始菜单17"
time="17"
background="/img/bg/bg2.jpg"
color="#209bdf"
></basic-block>
<basic-block
:width="width"
:height="height"
icon="el-icon-s-home"
text="开始菜单18"
time="18"
background="/img/bg/bg3.jpg"
color="#603bbc"
></basic-block>
<basic-block
:width="515"
:height="height"
icon="el-icon-s-promotion"
text="开始菜单19"
time="19"
dept="这是一段很长的很长很长很长的描述这是一段很长的很长很长很长的描述"
color="#009bad"
></basic-block>
<basic-block
:width="515"
:height="height"
icon="el-icon-s-custom"
text="开始菜单20"
time="20"
background="/img/bg/bg4.jpg"
dept="这是一段很长的很长很长很长的描述这是一段很长的很长很长很长的描述"
color="#d74e2a"
></basic-block>
</div>
</basic-container>
</template>
<script>
export default {
data() {
return {
width: 200,
height: 120,
};
},
};
</script>
<style lang="scss">
.wel {
display: flex;
flex-wrap: wrap;
width: 1100px;
margin: 0 auto;
}
</style>

1524
src/views/wel/index.vue Normal file

File diff suppressed because it is too large Load Diff

242
src/views/work/claim.vue Normal file
View File

@@ -0,0 +1,242 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-download"
v-if="permission.work_claim_sign"
@click.stop="handleClaim(scope.row)"
>签收
</el-button>
<el-button
type="primary"
text
icon="el-icon-view"
v-if="permission.work_claim_detail"
@click.stop="handleDetail(scope.row)"
>详情
</el-button>
<el-button
type="primary"
text
icon="el-icon-search"
v-if="permission.work_claim_follow"
@click.stop="handleImage(scope.row, scope.index)"
>流程图
</el-button>
</template>
<template #processDefinitionVersion="{ row }">
<el-tag>v{{ row.processDefinitionVersion }}</el-tag>
</template>
</avue-crud>
<flow-design
v-if="this.website.design.designMode"
is-dialog
v-model:is-display="flowBox"
:process-instance-id="processInstanceId"
></flow-design>
<el-dialog v-else title="流程图" append-to-body v-model="flowBox" :fullscreen="true">
<iframe
:src="flowUrl"
width="100%"
height="700"
title="流程图"
frameBorder="no"
border="0"
marginWidth="0"
marginHeight="0"
scrolling="no"
allowTransparency="yes"
>
</iframe>
<template #footer>
<span class="dialog-footer">
<el-button @click="flowBox = false"> </el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import { claimList, claimTask } from '@/api/work/work';
import { flowCategory, flowRoute } from '@/utils/flow';
export default {
data() {
return {
form: {},
selectionId: '',
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
processInstanceId: '',
flowBox: false,
flowUrl: '',
workBox: false,
option: {
height: 'auto',
calcHeight: 32,
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
editBtn: false,
addBtn: false,
viewBtn: false,
delBtn: false,
dialogWidth: 900,
menuWidth: 250,
dialogClickModal: false,
column: [
{
label: '流程分类',
type: 'select',
row: true,
dicUrl: '/blade-system/dict/dictionary?code=flow',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
hide: true,
width: 100,
},
{
label: '流程名称',
prop: 'processDefinitionName',
search: true,
},
{
label: '当前步骤',
prop: 'taskName',
},
{
label: '流程版本',
prop: 'processDefinitionVersion',
slot: true,
width: 85,
},
{
label: '申请时间',
prop: 'createTime',
width: 180,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission', 'flowRoutes']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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();
},
handleClaim(row) {
this.$confirm('确定签收此任务?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return claimTask(row.taskId);
})
.then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleDetail(row) {
this.$router.push({
path: `/work/process/${flowRoute(this.flowRoutes, row.category)}/detail/${
row.processInstanceId
}/${row.businessId}`,
});
},
handleImage(row) {
if (this.website.design.designMode) {
this.processInstanceId = row.processInstanceId;
} else {
this.flowUrl = `/api/blade-flow/process/diagram-view?processInstanceId=${row.processInstanceId}`;
}
this.flowBox = true;
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
const query = {
...this.query,
category: params.category ? flowCategory(params.category) : null,
};
this.loading = true;
claimList(page.currentPage, page.pageSize, Object.assign(params, query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>

217
src/views/work/done.vue Normal file
View File

@@ -0,0 +1,217 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
v-model:page="page"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-view"
v-if="permission.work_done_detail"
@click.stop="handleDetail(scope.row)"
>详情
</el-button>
<el-button
type="primary"
text
icon="el-icon-search"
v-if="permission.work_done_follow"
@click.stop="handleImage(scope.row, scope.index)"
>流程图
</el-button>
</template>
<template #processDefinitionVersion="{ row }">
<el-tag>v{{ row.processDefinitionVersion }}</el-tag>
</template>
</avue-crud>
<flow-design
v-if="this.website.design.designMode"
is-dialog
v-model:is-display="flowBox"
:process-instance-id="processInstanceId"
></flow-design>
<el-dialog v-else title="流程图" append-to-body v-model="flowBox" :fullscreen="true">
<iframe
:src="flowUrl"
width="100%"
height="700"
title="流程图"
frameBorder="no"
border="0"
marginWidth="0"
marginHeight="0"
scrolling="no"
allowTransparency="yes"
>
</iframe>
<template #footer>
<span class="dialog-footer">
<el-button @click="flowBox = false"> </el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import { doneList } from '@/api/work/work';
import { flowCategory, flowRoute } from '@/utils/flow';
export default {
data() {
return {
form: {},
selectionId: '',
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
processInstanceId: '',
flowBox: false,
flowUrl: '',
workBox: false,
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
editBtn: false,
addBtn: false,
viewBtn: false,
delBtn: false,
dialogWidth: 900,
menuWidth: 200,
dialogClickModal: false,
column: [
{
label: '流程分类',
type: 'select',
row: true,
dicUrl: '/blade-system/dict/dictionary?code=flow',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
hide: true,
width: 85,
},
{
label: '流程名称',
prop: 'processDefinitionName',
search: true,
},
{
label: '当前步骤',
prop: 'taskName',
},
{
label: '流程版本',
prop: 'processDefinitionVersion',
slot: true,
width: 85,
},
{
label: '申请时间',
prop: 'createTime',
width: 180,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission', 'flowRoutes']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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();
},
handleDetail(row) {
this.$router.push({
path: `/work/process/${flowRoute(this.flowRoutes, row.category)}/detail/${
row.processInstanceId
}/${row.businessId}`,
});
},
handleImage(row) {
if (this.website.design.designMode) {
this.processInstanceId = row.processInstanceId;
} else {
this.flowUrl = `/api/blade-flow/process/diagram-view?processInstanceId=${row.processInstanceId}`;
}
this.flowBox = true;
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
const query = {
...this.query,
category: params.category ? flowCategory(params.category) : null,
};
this.loading = true;
doneList(page.currentPage, page.pageSize, Object.assign(params, query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>

View File

@@ -0,0 +1,144 @@
<template>
<basic-container>
<el-form ref="form" :model="form" label-width="80px">
<el-row type="flex" class="row-bg" justify="end">
<el-form-item>
<el-button @click="handleCancel">关闭</el-button>
</el-form-item>
</el-row>
<el-card shadow="hover">
<template #header>
<div>
<span>审批信息</span>
</div>
</template>
<el-form-item label="申请人">
<el-input :disabled="true" v-model="form.flow.assigneeName" />
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="开始时间">
<el-input :disabled="true" v-model="form.startTime" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间">
<el-input :disabled="true" v-model="form.endTime" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="请假理由">
<el-input :disabled="true" type="textarea" v-model="form.reason" />
</el-form-item>
</el-card>
<el-card shadow="hover">
<template #header>
<div>
<span>流程信息</span>
</div>
</template>
<el-row type="flex" class="row-bg">
<el-timeline>
<el-timeline-item
:key="flow.id"
:timestamp="flow.createTime"
v-for="flow in flowList"
placement="top"
>
<el-card shadow="hover">
<p>
{{ flow.assigneeName }} [{{ flow.createTime }}] 开始处理 [{{
flow.historyActivityName
}}] 环节
</p>
<p v-if="flow.historyActivityDurationTime !== ''">
任务历时 [{{ flow.historyActivityDurationTime }}]
</p>
<p v-if="flow.comment !== ''">批复意见: [{{ flow.comment }}]</p>
<p v-if="flow.endTime !== ''">结束时间: [{{ flow.endTime }}]</p>
</el-card>
</el-timeline-item>
</el-timeline>
</el-row>
</el-card>
<el-card shadow="hover">
<template #header>
<div>
<span>流程跟踪</span>
</div>
</template>
<el-row class="row-bg">
<flow-design
style="width: 100%"
:is-display="true"
:process-instance-id="processInstanceId"
></flow-design>
</el-row>
</el-card>
</el-form>
</basic-container>
</template>
<script>
import { historyFlowList, leaveDetail } from '@/api/work/process';
import func from '@/utils/func';
export default {
data() {
return {
businessId: '',
processInstanceId: '',
src: '',
flowList: [],
form: {
flow: {
assigneeName: '',
},
startTime: '',
endTime: '',
reason: '',
},
};
},
created() {
this.init();
},
watch: {
$route(to, from) {
if (to.path !== from.path) {
this.init();
}
},
},
methods: {
init() {
if (!this.website.design.designMode) {
this.src = `/blade-flow/process/diagram-view?processInstanceId=${
this.$route.params.processInstanceId
}&t=${new Date().getTime()}`;
}
this.processInstanceId = this.$route.params.processInstanceId;
this.businessId = this.$route.params.businessId;
if (func.isEmpty(this.processInstanceId) || func.isEmpty(this.businessId)) {
return;
}
historyFlowList(this.processInstanceId).then(res => {
const data = res.data;
if (data.success) {
this.flowList = data.data;
}
});
leaveDetail(this.businessId).then(res => {
const data = res.data;
if (data.success) {
this.form = data.data;
}
});
},
handleCancel() {
this.$router.$avueRouter.closeTag();
this.$router.push({ path: `/work/start` });
},
},
};
</script>

View File

@@ -0,0 +1,114 @@
<template>
<div>
<basic-container>
<avue-form :option="option" v-model="form" @submit="handleSubmit" />
</basic-container>
<basic-container v-if="this.website.design.designMode">
<flow-design :is-display="true" :process-definition-id="processDefinitionId"></flow-design>
</basic-container>
</div>
</template>
<script>
import { leaveProcess } from '@/api/work/process';
export default {
data() {
return {
processDefinitionId: '',
form: {},
option: {
group: [
{
icon: 'el-icon-view',
label: '请假基础信息',
prop: 'group1',
column: [
{
label: '审批人员',
prop: 'taskUser',
type: 'select',
dicUrl: `/blade-system/user/user-list`,
props: {
label: 'account',
value: 'id',
},
span: 24,
rules: [
{
required: true,
message: '请选择审批人员',
trigger: 'blur',
},
],
},
{
label: '开始时间',
prop: 'startTime',
type: 'datetime',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
rules: [
{
required: true,
message: '请选择开始时间',
trigger: 'blur',
},
],
},
{
label: '结束时间',
prop: 'endTime',
type: 'datetime',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
rules: [
{
required: true,
message: '请选择结束时间',
trigger: 'blur',
},
],
},
{
label: '请假理由',
prop: 'reason',
type: 'textarea',
span: 24,
rules: [
{
required: true,
message: '请输入请假理由',
trigger: 'blur',
},
],
},
],
},
],
},
};
},
created() {
if (this.website.design.designMode) {
this.processDefinitionId = this.$route.params.processDefinitionId;
}
},
methods: {
handleSubmit() {
const params = {
processDefinitionId: this.$route.params.processDefinitionId,
...this.form,
};
leaveProcess(params).then(resp => {
const data = resp.data;
if (data.success) {
this.$message.success(data.msg);
this.$router.$avueRouter.closeTag();
this.$router.push({ path: `/work/start` });
} else {
this.$message.error(data.msg || '提交失败');
}
});
},
},
};
</script>

View File

@@ -0,0 +1,203 @@
<template>
<basic-container>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row type="flex" class="row-bg" justify="end">
<el-form-item>
<el-button type="primary" @click="handleAgree">同意</el-button>
<el-button type="danger" @click="handleDisagree">驳回</el-button>
<el-button @click="handleCancel">关闭</el-button>
</el-form-item>
</el-row>
<el-card shadow="hover">
<template #header>
<div>
<span>审批信息</span>
</div>
</template>
<el-form-item label="申请人">
<el-input :disabled="true" v-model="form.flow.assigneeName" />
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="开始时间">
<el-input :disabled="true" v-model="form.startTime" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间">
<el-input :disabled="true" v-model="form.endTime" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="请假理由">
<el-input :disabled="true" type="textarea" v-model="form.reason" />
</el-form-item>
<el-form-item label="批复意见">
<el-input type="textarea" v-model="form.comment" />
</el-form-item>
</el-card>
<el-card shadow="hover">
<template #header>
<div>
<span>流程信息</span>
</div>
</template>
<el-row type="flex" class="row-bg">
<el-timeline>
<el-timeline-item
:key="flow.id"
:timestamp="flow.createTime"
v-for="flow in flowList"
placement="top"
>
<el-card shadow="hover">
<p>
{{ flow.assigneeName }} [{{ flow.createTime }}] 开始处理 [{{
flow.historyActivityName
}}] 环节
</p>
<p v-if="flow.historyActivityDurationTime !== ''">
任务历时 [{{ flow.historyActivityDurationTime }}]
</p>
<p v-if="flow.comment !== ''">批复意见: [{{ flow.comment }}]</p>
<p v-if="flow.endTime !== ''">结束时间: [{{ flow.endTime }}]</p>
</el-card>
</el-timeline-item>
</el-timeline>
</el-row>
</el-card>
<el-card shadow="hover">
<template #header>
<div>
<span>流程跟踪</span>
</div>
</template>
<el-row v-if="this.website.design.designMode">
<flow-design
style="width: 100%"
:is-display="true"
:process-instance-id="processInstanceId"
></flow-design>
</el-row>
<el-row v-else type="flex" class="row-bg">
<img :src="src" alt="design" />
</el-row>
</el-card>
</el-form>
</basic-container>
</template>
<script>
import { historyFlowList, leaveDetail } from '@/api/work/process';
import { completeTask } from '@/api/work/work';
import func from '@/utils/func';
export default {
data() {
return {
taskId: '',
businessId: '',
processInstanceId: '',
src: '',
flowList: [],
form: {
flow: {
assigneeName: '',
},
startTime: '',
endTime: '',
reason: '',
comment: '',
},
};
},
created() {
this.init();
},
watch: {
$route(to, from) {
if (to.path !== from.path) {
this.init();
}
},
},
methods: {
init() {
if (!this.website.design.designMode) {
this.src = `/blade-flow/process/diagram-view?processInstanceId=${
this.$route.params.processInstanceId
}&t=${new Date().getTime()}`;
}
this.taskId = this.$route.params.taskId;
this.processInstanceId = this.$route.params.processInstanceId;
this.businessId = this.$route.params.businessId;
if (
func.isEmpty(this.taskId) ||
func.isEmpty(this.processInstanceId) ||
func.isEmpty(this.businessId)
) {
return;
}
historyFlowList(this.processInstanceId).then(res => {
const data = res.data;
if (data.success) {
this.flowList = data.data;
}
});
leaveDetail(this.businessId).then(res => {
const data = res.data;
if (data.success) {
this.form = data.data;
}
});
},
handleAgree() {
if (!this.form.comment) {
this.$message.warning('请先填写批复意见');
return;
}
const params = {
taskId: this.taskId,
processInstanceId: this.processInstanceId,
flag: 'ok',
comment: this.form.comment,
};
completeTask(params).then(res => {
const data = res.data;
if (data.success) {
this.$message.success(data.msg);
this.$router.$avueRouter.closeTag();
this.$router.push({ path: `/work/start` });
} else {
this.$message.error(data.msg || '提交失败');
}
});
},
handleDisagree() {
if (!this.form.comment) {
this.$message.warning('请先填写批复意见');
return;
}
const params = {
taskId: this.taskId,
processInstanceId: this.processInstanceId,
comment: this.form.comment,
};
completeTask(params).then(res => {
const data = res.data;
if (data.success) {
this.$message.success(data.msg);
this.$router.$avueRouter.closeTag();
this.$router.push({ path: `/work/start` });
} else {
this.$message.error(data.msg || '提交失败');
}
});
},
handleCancel() {
this.$router.$avueRouter.closeTag();
this.$router.push({ path: `/work/start` });
},
},
};
</script>

226
src/views/work/send.vue Normal file
View File

@@ -0,0 +1,226 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
v-model:page="page"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-view"
v-if="permission.work_send_detail"
@click.stop="handleDetail(scope.row)"
>详情
</el-button>
<el-button
type="primary"
text
icon="el-icon-search"
v-if="permission.work_send_follow"
@click.stop="handleImage(scope.row, scope.index)"
>流程图
</el-button>
</template>
<template #processDefinitionVersion="{ row }">
<el-tag>v{{ row.processDefinitionVersion }}</el-tag>
</template>
<template #processIsFinished="{ row }">
<el-tag>{{ row.processIsFinished === 'finished' ? '已完成' : '未完成' }}</el-tag>
</template>
</avue-crud>
<flow-design
v-if="this.website.design.designMode"
is-dialog
v-model:is-display="flowBox"
:process-instance-id="processInstanceId"
></flow-design>
<el-dialog v-else title="流程图" append-to-body v-model="flowBox" :fullscreen="true">
<iframe
:src="flowUrl"
width="100%"
height="700"
title="流程图"
frameBorder="no"
border="0"
marginWidth="0"
marginHeight="0"
scrolling="no"
allowTransparency="yes"
>
</iframe>
<template #footer>
<span class="dialog-footer">
<el-button @click="flowBox = false"> </el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import { sendList } from '@/api/work/work';
import { flowCategory, flowRoute } from '@/utils/flow';
export default {
data() {
return {
form: {},
selectionId: '',
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
processInstanceId: '',
flowBox: false,
flowUrl: '',
workBox: false,
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
editBtn: false,
addBtn: false,
viewBtn: false,
delBtn: false,
dialogWidth: 900,
menuWidth: 200,
dialogClickModal: false,
column: [
{
label: '流程分类',
type: 'select',
row: true,
dicUrl: '/blade-system/dict/dictionary?code=flow',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
hide: true,
width: 85,
},
{
label: '流程名称',
prop: 'processDefinitionName',
search: true,
},
{
label: '当前步骤',
prop: 'taskName',
},
{
label: '流程版本',
prop: 'processDefinitionVersion',
slot: true,
width: 85,
},
{
label: '流程进度',
prop: 'processIsFinished',
slot: true,
width: 85,
},
{
label: '申请时间',
prop: 'createTime',
width: 180,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission', 'flowRoutes']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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();
},
handleDetail(row) {
this.$router.push({
path: `/work/process/${flowRoute(this.flowRoutes, row.category)}/detail/${
row.processInstanceId
}/${row.businessId}`,
});
},
handleImage(row) {
if (this.website.design.designMode) {
this.processInstanceId = row.processInstanceId;
} else {
this.flowUrl = `/api/blade-flow/process/diagram-view?processInstanceId=${row.processInstanceId}`;
}
this.flowBox = true;
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
const query = {
...this.query,
category: params.category ? flowCategory(params.category) : null,
};
this.loading = true;
sendList(page.currentPage, page.pageSize, Object.assign(params, query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>

248
src/views/work/start.vue Normal file
View File

@@ -0,0 +1,248 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
ref="crud"
@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-radio-group v-model="mode">
<el-radio-button label="1">通用流程</el-radio-button>
<el-radio-button label="2">定制流程</el-radio-button>
</el-radio-group>
</template>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-video-play"
v-if="permission.work_start_flow"
@click.stop="handleStart(scope.row)"
>发起
</el-button>
<el-button
type="primary"
text
icon="el-icon-search"
v-if="permission.work_start_image"
@click.stop="handleImage(scope.row, scope.index)"
>流程图
</el-button>
</template>
<template #tenantId="{ row }">
<el-tag>{{ row.tenantId === '' ? '通用' : row.tenantId }}</el-tag>
</template>
<template #version="{ row }">
<el-tag>v{{ row.version }}</el-tag>
</template>
<template #suspensionState="{ row }">
<el-tag>{{ row.suspensionState === 1 ? '激活' : '挂起' }}</el-tag>
</template>
<template #category="{ row }">
<el-tag>{{ row.categoryName }}</el-tag>
</template>
</avue-crud>
<flow-design
v-if="this.website.design.designMode"
is-dialog
v-model:is-display="flowBox"
:process-definition-id="processDefinitionId"
></flow-design>
<el-dialog v-else title="流程图" append-to-body v-model="flowBox" :fullscreen="true">
<iframe
:src="flowUrl"
width="100%"
height="700"
title="流程图"
frameBorder="no"
border="0"
marginWidth="0"
marginHeight="0"
scrolling="no"
allowTransparency="yes"
>
</iframe>
<template #footer>
<span class="dialog-footer">
<el-button @click="flowBox = false"> </el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import { startList } from '@/api/work/work';
import { flowCategory, flowRoute } from '@/utils/flow';
export default {
data() {
return {
form: {},
mode: '1',
selectionId: '',
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
processDefinitionId: '',
flowBox: false,
flowUrl: '',
workBox: false,
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
editBtn: false,
addBtn: false,
viewBtn: false,
delBtn: false,
menuWidth: 200,
dialogWidth: 900,
dialogClickModal: false,
column: [
{
label: '租户编号',
prop: 'tenantId',
slot: true,
width: 120,
},
{
label: '流程分类',
type: 'select',
row: true,
dicUrl: '/blade-system/dict/dictionary?code=flow',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
width: 100,
},
{
label: '流程标识',
prop: 'key',
},
{
label: '流程名称',
prop: 'name',
search: true,
},
{
label: '流程版本',
prop: 'version',
slot: true,
width: 85,
},
{
label: '状态',
prop: 'suspensionState',
slot: true,
width: 80,
},
{
label: '部署时间',
prop: 'deploymentTime',
width: 170,
},
],
},
data: [],
};
},
watch: {
mode() {
this.onLoad(this.page);
},
},
computed: {
...mapGetters(['permission', 'flowRoutes']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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();
},
handleStart(row) {
this.$router.push({
path: `/work/process/${flowRoute(this.flowRoutes, row.category)}/form/${row.id}`,
});
},
handleImage(row) {
if (this.website.design.designMode) {
this.processDefinitionId = row.id;
} else {
this.flowUrl = `/api/blade-flow/process/resource-view?processDefinitionId=${row.id}`;
}
this.flowBox = true;
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
const query = {
...this.query,
category: params.category ? flowCategory(params.category) : null,
mode: this.mode,
};
this.loading = true;
startList(page.currentPage, page.pageSize, Object.assign(params, query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>

232
src/views/work/todo.vue Normal file
View File

@@ -0,0 +1,232 @@
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
ref="crud"
v-model="form"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu="scope">
<el-button
type="primary"
text
icon="el-icon-video-play"
v-if="permission.work_todo_handle"
@click.stop="handleWork(scope.row)"
>处理
</el-button>
<el-button
type="primary"
text
icon="el-icon-view"
v-if="permission.work_todo_detail"
@click.stop="handleDetail(scope.row)"
>详情
</el-button>
<el-button
type="primary"
text
icon="el-icon-search"
v-if="permission.work_todo_follow"
@click.stop="handleImage(scope.row, scope.index)"
>流程图
</el-button>
</template>
<template #processDefinitionVersion="{ row }">
<el-tag>v{{ row.processDefinitionVersion }}</el-tag>
</template>
</avue-crud>
<flow-design
v-if="this.website.design.designMode"
is-dialog
v-model:is-display="flowBox"
:process-instance-id="processInstanceId"
></flow-design>
<el-dialog v-else title="流程图" append-to-body v-model="flowBox" :fullscreen="true">
<iframe
:src="flowUrl"
width="100%"
height="700"
title="流程图"
frameBorder="no"
border="0"
marginWidth="0"
marginHeight="0"
scrolling="no"
allowTransparency="yes"
>
</iframe>
<template #footer>
<span class="dialog-footer">
<el-button @click="flowBox = false"> </el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import { todoList } from '@/api/work/work';
import { flowCategory, flowRoute } from '@/utils/flow';
export default {
data() {
return {
form: {},
selectionId: '',
selectionList: [],
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
processInstanceId: '',
flowBox: false,
flowUrl: '',
workBox: false,
option: {
height: 'auto',
calcHeight: 32,
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
editBtn: false,
addBtn: false,
viewBtn: false,
delBtn: false,
dialogWidth: 900,
menuWidth: 250,
dialogClickModal: false,
column: [
{
label: '流程分类',
type: 'select',
row: true,
dicUrl: '/blade-system/dict/dictionary?code=flow',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'category',
search: true,
hide: true,
width: 85,
},
{
label: '流程名称',
prop: 'processDefinitionName',
search: true,
},
{
label: '当前步骤',
prop: 'taskName',
},
{
label: '流程版本',
prop: 'processDefinitionVersion',
slot: true,
width: 85,
},
{
label: '申请时间',
prop: 'createTime',
width: 180,
},
],
},
data: [],
};
},
computed: {
...mapGetters(['permission', 'flowRoutes']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
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();
},
handleWork(row) {
this.$router.push({
path: `/work/process/${flowRoute(this.flowRoutes, row.category)}/handle/${row.taskId}/${
row.processInstanceId
}/${row.businessId}`,
});
},
handleDetail(row) {
this.$router.push({
path: `/work/process/${flowRoute(this.flowRoutes, row.category)}/detail/${
row.processInstanceId
}/${row.businessId}`,
});
},
handleImage(row) {
if (this.website.design.designMode) {
this.processInstanceId = row.processInstanceId;
} else {
this.flowUrl = `/api/blade-flow/process/diagram-view?processInstanceId=${row.processInstanceId}`;
}
this.flowBox = true;
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
const query = {
...this.query,
category: params.category ? flowCategory(params.category) : null,
};
this.loading = true;
todoList(page.currentPage, page.pageSize, Object.assign(params, query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>