Files
tms-erp-web/src/views/system/topmenu.vue
T
gxwebsoft 565553b3f7 refactor(table): 统一调整全仓表格操作列宽度及按钮样式
- 将所有操作按钮数量≤3的表格操作列宽度统一调整为180px
- 调整涉及全局默认值、各模块和页面中menuWidth显式设定
- 单独修改/authority/role页面操作列宽度为240px,/system/menu及流程配置对应调为220px
- 通过CSS新规则视觉统一Avue内置操作按钮为el-link链接样式,无功能变更
- 优化通知公告操作列宽度,解决三按钮链接换行问题
- 调整首页Logo图标尺寸及边距以适配设计需求
- 保证所有相关改动经过npm run build验证通过
2026-08-07 12:02:29 +08:00

533 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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-link
v-if="permission.topmenu_setting"
type="primary"
@click.stop="handleRowMenuSetting(scope.row)"
>配置
</el-link>
</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: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
menuWidth: 180,
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>