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
+303
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>
+155
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
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
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
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>
+450
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
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
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
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
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>
+408
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>
+368
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>
+531
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
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>
+485
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>