Files
intc-single-ultra-app/src/pages_mine/pages/system/menu/list.vue
2026-02-05 20:45:04 +08:00

635 lines
15 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>
<view class="container">
<u-sticky offsetTop="0rpx" customNavHeight="0rpx">
<view class="search-view">
<u--input v-model="queryParams.menuName" border="false" placeholder="请输入菜单名称" class="search-input"
@blur="searchBlur" suffixIcon="search" suffixIconStyle="color: #909399">
</u--input>
<view class="filter-btn" @click="filterPanel = !filterPanel">
<uni-icons type="list" size="18" color="#667eea"></uni-icons>
<text>筛选</text>
</view>
<view class="add-btn" @click="handleAdd()">
<uni-icons type="plusempty" size="18" color="#667eea"></uni-icons>
<text>新增</text>
</view>
<u-transition :show="filterPanel" mode="fade">
<view class="filter-panel" :style="{ height: `${windowHeight - 42}px` }">
<view class="filter-panel-content">
<view class="filter-title">状态</view>
<view class="state-list">
<view v-for="item in statusList" :key="item.dictValue" class="state-item"
:class="item.selected ? 'active' : ''" @click="selectStatus(item)">{{ item.dictLabel }}</view>
</view>
</view>
<view class="btn-box">
<view class="btn-reset" @click="resetQuery()">
<uni-icons type="reload" size="16" color="#909399"></uni-icons>
<text>重置</text>
</view>
<view class="btn-confirm" @click="searchSubmit()">
<uni-icons type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
<text>确定</text>
</view>
</view>
</view>
</u-transition>
</view>
</u-sticky>
<u-list @scrolltolower="loadmore" :spaceHeight="116" lowerThreshold="100">
<u-list-item v-for="(item, index) in listData" :key="index">
<view class="list-item">
<view class="item-header">
<view class="card-name-section">
<view class="card-icon">
<uni-icons type="list" size="20" color="#ffffff"></uni-icons>
</view>
<view class="card-info">
<text class="card-name">{{ item.menuName }}</text>
</view>
</view>
<view class="balance-section">
<view class="status-tag" :class="item.status === '0' ? 'status-normal' : 'status-disabled'">
{{ dictStr(item.status, statusList) }}
</view>
</view>
</view>
<view class="card-body">
<view class="info-row">
<view class="info-item" v-if="item.menuType">
<text class="info-label">菜单类型</text>
<text class="info-value">{{ getMenuType(item.menuType) }}</text>
</view>
<view class="info-item" v-if="item.orderNum !== undefined">
<text class="info-label">排序</text>
<text class="info-value">{{ item.orderNum }}</text>
</view>
<view class="info-item full-width" v-if="item.perms">
<text class="info-label">权限标识</text>
<text class="info-value">{{ item.perms }}</text>
</view>
<view class="info-item full-width" v-if="item.component">
<text class="info-label">组件路径</text>
<text class="info-value">{{ item.component }}</text>
</view>
</view>
</view>
<view class="operate" @click.stop>
<view class="btn-edit" @click="handleUpdate(item)">
<uni-icons type="compose" size="16" color="#667eea"></uni-icons>
<text>修改</text>
</view>
<view class="btn-add" @click="handleAddChild(item)">
<uni-icons type="plus" size="16" color="#52c41a"></uni-icons>
<text>添加</text>
</view>
<view class="btn-delete" @click="handleDelete(item)">
<uni-icons type="trash" size="16" color="#f5576c"></uni-icons>
<text>删除</text>
</view>
</view>
</view>
</u-list-item>
<view></view>
<u-loadmore :status="loadStatus" loadingIcon="semicircle" height="88" fontSize="32rpx" @loadmore="loadmore" />
</u-list>
<u-toast ref="uToast"></u-toast>
</view>
<systemBtn></systemBtn>
<refresh></refresh>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { listMenu, delMenu } from '@/api/system/menu'
import { getDicts } from "@/api/system/dict/data"
import { onLoad, onShow } from "@dcloudio/uni-app"
import modal from '@/plugins/modal'
const listData = ref([])
const isShow = ref(false)
const loadStatus = ref('loadmore')
const statusList = ref([])
const filterPanel = ref(false)
const queryParams = reactive({
menuName: undefined,
status: undefined,
menuType: 'C' // 只查询菜单类型为"菜单"的数据
})
const windowHeight = computed(() => {
uni.getSystemInfoSync().windowHeight - 50
})
onLoad(() => {
getDicts('sys_normal_disable').then(res => {
statusList.value = res.data.map(item => ({
...item,
selected: false
}))
})
getList()
})
onShow(() => {
if (isShow.value) {
listData.value = []
getList()
isShow.value = false
}
})
// 构建树形结构
function buildTree(data) {
const map = {}
const tree = []
data.forEach(item => {
map[item.menuId] = { ...item, children: [] }
})
data.forEach(item => {
const node = map[item.menuId]
if (item.parentId === 0 || !map[item.parentId]) {
tree.push(node)
} else {
map[item.parentId].children.push(node)
}
})
return tree
}
// 将树形结构扁平化
function flattenTree(tree, result = []) {
tree.forEach(item => {
result.push(item)
if (item.children && item.children.length > 0) {
flattenTree(item.children, result)
}
})
return result
}
// 获取列表
function getList() {
loadStatus.value = 'loading'
const params = {
...queryParams
}
listMenu(params).then(res => {
const treeData = buildTree(res.data)
listData.value = flattenTree(treeData)
loadStatus.value = 'nomore'
}).catch(() => {
loadStatus.value = 'nomore'
})
}
// 搜索框失焦
function searchBlur() {
listData.value = []
getList()
}
// 选择状态
function selectStatus(item) {
statusList.value.forEach(v => v.selected = false)
item.selected = !item.selected
queryParams.status = item.selected ? item.dictValue : undefined
}
// 搜索提交
function searchSubmit() {
listData.value = []
filterPanel.value = false
getList()
}
// 重置查询
function resetQuery() {
queryParams.menuName = undefined
queryParams.status = undefined
queryParams.menuType = 'C' // 保持只查询菜单类型
listData.value = []
statusList.value.forEach(v => v.selected = false)
filterPanel.value = false
getList()
}
// 上拉加载
function loadmore() {
// 菜单数据不支持分页
}
// 新增按钮
function handleAdd() {
isShow.value = true
uni.navigateTo({
url: '/pages_mine/pages/system/menu/addEdit'
})
}
// 添加子菜单
function handleAddChild(row) {
isShow.value = true
uni.navigateTo({
url: `/pages_mine/pages/system/menu/addEdit?parentId=${row.menuId}`
})
}
// 修改按钮
function handleUpdate(row) {
isShow.value = true
uni.navigateTo({
url: `/pages_mine/pages/system/menu/addEdit?id=${row.menuId}`
})
}
// 删除按钮
function handleDelete(row) {
modal.confirm('是否确认删除菜单"' + row.menuName + '"').then(() => {
return delMenu(row.menuId)
}).then(() => {
listData.value = []
getList()
modal.msgSuccess('删除成功')
}).catch(() => {})
}
// 获取菜单类型
function getMenuType(type) {
const types = {
'M': '目录',
'C': '菜单',
'F': '按钮'
}
return types[type] || type
}
// 字典翻译
function dictStr(value, list) {
if (value === undefined || value === null) return ''
const item = list.find(v => v.dictValue == value)
return item ? item.dictLabel : value
}
</script>
<style lang="scss" scoped>
page {
height: 100%;
overflow: auto;
}
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
.search-view {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
background-color: #ffffff;
position: relative;
.search-input {
background: rgba(102, 126, 234, 0.08);
color: #333333;
flex: 1;
margin-right: 16rpx;
border-radius: 24rpx;
border: 2rpx solid rgba(102, 126, 234, 0.3);
height: 66rpx !important;
display: flex;
align-items: center;
}
.filter-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 12rpx 24rpx;
background: rgba(102, 126, 234, 0.08);
border-radius: 24rpx;
border: 2rpx solid rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
flex-shrink: 0;
&:active {
transform: scale(0.95);
background: rgba(102, 126, 234, 0.12);
}
text {
color: #667eea;
font-size: 28rpx;
font-weight: 600;
}
uni-icons {
color: #667eea;
}
}
.add-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 12rpx 24rpx;
background: rgba(102, 126, 234, 0.08);
border-radius: 24rpx;
border: 2rpx solid rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
flex-shrink: 0;
margin-left: 10rpx;
&:active {
transform: scale(0.95);
background: rgba(102, 126, 234, 0.12);
}
text {
color: #667eea;
font-size: 28rpx;
font-weight: 600;
}
uni-icons {
color: #667eea;
}
}
.filter-panel {
position: absolute;
top: 100rpx;
left: 0;
right: 0;
background-color: #ffffff;
z-index: 999;
display: flex;
flex-direction: column;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
.filter-panel-content {
flex: 1;
padding: 32rpx;
overflow-y: auto;
.filter-title {
font-size: 28rpx;
font-weight: bold;
margin: 32rpx 0 24rpx 0;
color: #303133;
&:first-child {
margin-top: 0;
}
}
.state-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.state-item {
padding: 16rpx 32rpx;
background-color: #f5f5f5;
border-radius: 32rpx;
font-size: 28rpx;
color: #606266;
&.active {
background-color: #667eea;
color: #ffffff;
}
}
}
}
.btn-box {
display: flex;
padding: 24rpx 32rpx;
border-top: 1rpx solid #f0f0f0;
.btn-reset,
.btn-confirm {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
border-radius: 32rpx;
font-size: 28rpx;
text {
margin-left: 12rpx;
}
}
.btn-reset {
background-color: #f5f5f5;
color: #909399;
margin-right: 16rpx;
}
.btn-confirm {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
}
}
}
}
.list-item {
margin: 10rpx 24rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.06);
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.card-name-section {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
margin-right: 16rpx;
}
.card-icon {
width: 40rpx;
height: 40rpx;
background: rgba(255, 255, 255, 0.25);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
flex-shrink: 0;
}
.card-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4rpx;
.card-name {
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.balance-section {
display: flex;
align-items: center;
flex-shrink: 0;
white-space: nowrap;
.status-tag {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
&.status-normal {
background-color: rgba(82, 196, 26, 0.2);
color: #52c41a;
}
&.status-disabled {
background-color: rgba(255, 77, 79, 0.15);
color: #ff4d4f;
}
}
}
}
.card-body {
padding: 24rpx;
background: #fafbfc;
border: 2rpx solid #f0f2f5;
margin: 15rpx 16rpx 2rpx;
border-radius: 12rpx;
}
.info-row {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
margin-bottom: 0;
.info-item {
flex: 0 0 calc(50% - 12rpx);
display: flex;
flex-direction: column;
gap: 4rpx;
min-width: 0;
margin-bottom: -5rpx;
.info-label {
font-size: 24rpx;
color: #667eea;
font-weight: 500;
background: rgba(102, 126, 234, 0.08);
padding: 6rpx 12rpx;
border-radius: 8rpx;
align-self: flex-start;
}
.info-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
flex: 1;
line-height: 1.5;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.full-width {
flex: 0 0 100%;
.info-value {
white-space: normal;
overflow: visible;
text-overflow: unset;
}
}
}
}
.operate {
display: flex;
justify-content: flex-end;
padding: 16rpx 24rpx 24rpx;
gap: 8rpx;
flex-wrap: nowrap;
.btn-edit,
.btn-add,
.btn-delete {
display: flex;
align-items: center;
justify-content: center;
gap: 4rpx;
padding: 0 14rpx;
height: 60rpx;
border-radius: 10rpx;
font-size: 24rpx;
font-weight: 500;
transition: all 0.3s ease;
white-space: nowrap;
flex-shrink: 0;
&:active {
transform: scale(0.95);
}
}
.btn-edit {
background: rgba(102, 126, 234, 0.1);
color: #667eea;
border: 1rpx solid rgba(102, 126, 234, 0.3);
}
.btn-add {
background: rgba(82, 196, 26, 0.1);
color: #52c41a;
border: 1rpx solid rgba(82, 196, 26, 0.3);
}
.btn-delete {
background: rgba(245, 87, 108, 0.1);
color: #f5576c;
border: 1rpx solid rgba(245, 87, 108, 0.3);
}
}
}
</style>