Files
intc-single-ultra-app/src/pages_mine/pages/system/role/addEdit.vue
2026-02-08 00:39:31 +08:00

554 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" style="paddingBottom:1rpx;">
<u-navbar
leftIconSize="40rpx"
leftIconColor="#333333"
:title="title"
>
</u-navbar>
<view class="section">
<view class="section-title">{{ title }}</view>
<view class="form-view">
<u--form labelPosition="left" :model="form" :rules="rules" ref="uForm" label-width="auto"
:labelStyle="{ color: '#333333', fontSize: '30rpx' }">
<u-form-item label="角色名称" prop="roleName" required>
<u--input v-model="form.roleName" placeholder="请输入角色名称" maxlength="30"
inputAlign="left" :customStyle="getInputStyle('roleName')"></u--input>
</u-form-item>
<u-form-item label="权限字符" prop="roleKey" required>
<u--input v-model="form.roleKey" placeholder="请输入权限字符" maxlength="100"
inputAlign="left" :customStyle="getInputStyle('roleKey')"></u--input>
</u-form-item>
<u-form-item label="显示顺序" required>
<u-number-box v-model="form.roleSort" :min="0" :max="999" inputAlign="right"></u-number-box>
</u-form-item>
<u-form-item label="状态" prop="statusName" @click="showStatusPicker = true" class="with-arrow">
<u--input v-model="statusName" disabled disabledColor="#ffffff" placeholder="请选择状态"
inputAlign="left" :customStyle="inputBaseStyle"></u--input>
</u-form-item>
<u-form-item label="备注" prop="remark" labelPosition="top">
<u--textarea v-model="form.remark" placeholder="请输入备注" autoHeight inputAlign="right" count
maxlength="500" style="border: 2rpx solid #dcdfe6 !important;"></u--textarea>
</u-form-item>
<u-form-item label="菜单权限" @click="openMenuPicker" class="with-arrow">
<u--input :value="menuPermissionText" disabled disabledColor="#ffffff" placeholder="请选择菜单权限"
inputAlign="left" :customStyle="inputBaseStyle"></u--input>
</u-form-item>
<!-- 已选菜单标签展示 -->
<view v-if="selectedMenuNames.length > 0" class="selected-menus">
<view class="selected-title">已选择的菜单权限</view>
<view class="menu-tags">
<view v-for="name in selectedMenuNames" :key="name" class="menu-tag">
{{ name }}
</view>
</view>
</view>
</u--form>
<view class="form-btn">
<u-button type="primary" text="提交" @click="submit"></u-button>
</view>
</view>
</view>
<!-- 状态选择器 -->
<u-picker itemHeight="88" :show="showStatusPicker" :columns="statusList" keyName="dictLabel"
@cancel="showStatusPicker = false" @confirm="handleStatusConfirm"></u-picker>
<!-- 菜单权限选择弹窗 -->
<u-popup :show="showMenuPicker" mode="bottom" :round="20" @close="showMenuPicker = false">
<view class="menu-popup">
<view class="popup-header">
<text class="header-title">选择菜单权限</text>
<text class="header-count">已选择 {{ selectedMenuCount }} </text>
</view>
<scroll-view class="popup-content" scroll-y>
<view class="menu-tree">
<u-checkbox-group v-model="tempSelectedMenuIds">
<view v-for="menu in menuOptions" :key="menu.id" class="menu-group">
<!-- 一级菜单 -->
<view class="level-1-wrapper">
<u-checkbox
:name="menu.id"
:label="menu.label"
shape="square"
activeColor="#667eea"
labelSize="32rpx"
></u-checkbox>
</view>
<!-- 二级菜单 -->
<view v-if="menu.children && menu.children.length > 0" class="level-2-list">
<view v-for="child in menu.children" :key="child.id" class="level-2-item">
<u-checkbox
:name="child.id"
:label="child.label"
shape="square"
activeColor="#667eea"
labelSize="28rpx"
></u-checkbox>
<!-- 三级菜单 -->
<view v-if="child.children && child.children.length > 0" class="level-3-list">
<view v-for="btn in child.children" :key="btn.id" class="level-3-item">
<u-checkbox
:name="btn.id"
:label="btn.label"
shape="square"
activeColor="#667eea"
labelSize="26rpx"
></u-checkbox>
</view>
</view>
</view>
</view>
</view>
</u-checkbox-group>
</view>
</scroll-view>
<view class="popup-footer">
<view class="footer-btn cancel-btn" @click="cancelMenuSelect">取消</view>
<view class="footer-btn confirm-btn" @click="confirmMenuSelect">确定</view>
</view>
</view>
</u-popup>
</view>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, computed } from 'vue'
import { getRole, addRole, updateRole } from '@/api/system/role'
import { treeselect, roleMenuTreeselect } from '@/api/system/menu'
import { getDicts } from "@/api/system/dict/data"
import { onLoad } from "@dcloudio/uni-app"
const { proxy } = getCurrentInstance()
const title = ref('添加角色')
const statusList = ref([])
const showStatusPicker = ref(false)
const statusName = ref('正常')
const menuOptions = ref([])
const selectedMenuIds = ref([])
const showMenuPicker = ref(false)
const tempSelectedMenuIds = ref([])
const form = reactive({
roleId: undefined,
roleName: undefined,
roleKey: undefined,
roleSort: 0,
status: '0',
menuIds: [],
remark: undefined
})
// 表单验证错误字段
const errorFields = ref([])
// 输入框基础样式
const inputBaseStyle = {
background: '#ffffff',
border: '2rpx solid #dcdfe6',
borderRadius: '8rpx',
padding: '0 24rpx',
height: '68rpx',
width: '100%',
boxSizing: 'border-box'
}
// 输入框错误样式
const inputErrorStyle = {
background: '#fef0f0',
border: '2rpx solid #f56c6c',
borderRadius: '8rpx',
padding: '0 24rpx',
height: '68rpx',
width: '100%',
boxSizing: 'border-box'
}
// 根据字段名获取输入框样式
const getInputStyle = (field) => {
return errorFields.value.includes(field) ? inputErrorStyle : inputBaseStyle
}
const selectedMenuCount = computed(() => {
return tempSelectedMenuIds.value.length
})
const menuPermissionText = computed(() => {
const count = selectedMenuIds.value.length
return count > 0 ? `已选择 ${count} 个菜单` : ''
})
const selectedMenuNames = computed(() => {
const names = []
const getNames = (menus) => {
menus.forEach(menu => {
if (selectedMenuIds.value.includes(menu.id)) {
names.push(menu.label)
}
if (menu.children && menu.children.length > 0) {
getNames(menu.children)
}
})
}
getNames(menuOptions.value)
return names
})
const rules = {
roleName: [
{ required: true, message: '角色名称不能为空', trigger: ['blur', 'change'] }
],
roleKey: [
{ required: true, message: '权限字符不能为空', trigger: ['blur', 'change'] }
],
roleSort: [
{ required: true, message: '显示顺序不能为空', trigger: ['blur', 'change'] }
]
}
onLoad((option) => {
getDicts('sys_normal_disable').then(res => {
statusList.value = [res.data]
})
// 获取菜单树
getMenuTree()
if (option.id) {
title.value = '修改角色'
getDetail(option.id)
} else {
// 新增时默认显示状态
setTimeout(() => {
updateStatusName()
}, 100)
}
})
// 获取菜单树
function getMenuTree() {
treeselect().then(res => {
menuOptions.value = res.data || []
})
}
// 获取详情
function getDetail(id) {
getRole(id).then(res => {
Object.assign(form, res.data)
updateStatusName()
})
// 获取角色的菜单权限
roleMenuTreeselect(id).then(res => {
menuOptions.value = res.menus || []
// 设置已选中的菜单
if (res.checkedKeys && res.checkedKeys.length > 0) {
selectedMenuIds.value = res.checkedKeys
tempSelectedMenuIds.value = [...res.checkedKeys]
}
})
}
// 更新状态名称显示
function updateStatusName() {
if (statusList.value.length > 0 && statusList.value[0].length > 0) {
const item = statusList.value[0].find(v => v.dictValue === form.status)
statusName.value = item ? item.dictLabel : ''
}
}
// 状态选择确认
function handleStatusConfirm(e) {
form.status = e.value[0].dictValue
statusName.value = e.value[0].dictLabel
showStatusPicker.value = false
}
// 打开菜单选择器
function openMenuPicker() {
// 将已选择的菜单ID复制到临时变量
tempSelectedMenuIds.value = [...selectedMenuIds.value]
showMenuPicker.value = true
}
// 取消菜单选择
function cancelMenuSelect() {
showMenuPicker.value = false
}
// 确认菜单选择
function confirmMenuSelect() {
selectedMenuIds.value = [...tempSelectedMenuIds.value]
showMenuPicker.value = false
}
// 提交
function submit() {
errorFields.value = [] // 清空错误字段
// 设置菜单权限
form.menuIds = selectedMenuIds.value
proxy.$refs['uForm'].validate().then(() => {
if (form.roleId) {
updateRole(form).then(() => {
proxy.$modal.msgSuccess('修改成功')
setTimeout(() => {
uni.navigateTo({ url: `/pages_mine/pages/system/role/list` })
}, 1500)
})
} else {
addRole(form).then(() => {
proxy.$modal.msgSuccess('新增成功')
setTimeout(() => {
uni.navigateTo({ url: `/pages_mine/pages/system/role/list` })
}, 1500)
})
}
}).catch((errors) => {
// 记录验证失败的字段
if (errors && Array.isArray(errors)) {
errorFields.value = errors.map(err => err.field || err.prop)
}
proxy.$modal.msgError('请填写完整信息')
})
}
</script>
<style lang="scss" scoped>
.container {
background-color: #f5f5f5;
}
.section {
margin: 24rpx;
padding: 0;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
.section-title {
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
line-height: 1.2;
font-size: 28rpx;
font-weight: 600;
display: flex;
align-items: center;
&::before {
content: '';
width: 6rpx;
height: 28rpx;
background: #ffffff;
border-radius: 3rpx;
margin-right: 12rpx;
}
}
.form-view {
padding: 24rpx;
.form-btn {
padding-top: 16rpx;
}
.selected-menus {
margin-top: 16rpx;
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
.selected-title {
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
}
.menu-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.menu-tag {
padding: 8rpx 16rpx;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border: 1rpx solid #667eea;
border-radius: 24rpx;
font-size: 24rpx;
color: #667eea;
}
}
}
}
}
.menu-popup {
background: #fff;
max-height: 80vh;
display: flex;
flex-direction: column;
.popup-header {
padding: 32rpx 24rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
.header-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
display: block;
}
.header-count {
font-size: 24rpx;
color: #667eea;
display: block;
}
}
.popup-content {
flex: 1;
max-height: 60vh;
.menu-tree {
padding: 24rpx;
.menu-group {
margin-bottom: 32rpx;
width: 100%;
// 一级菜单
.level-1-wrapper {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border-radius: 12rpx;
padding: 16rpx 20rpx;
border-left: 4rpx solid #667eea;
margin-bottom: 16rpx;
width: 100%;
}
// 二级菜单列表
.level-2-list {
padding-left: 0;
width: 100%;
.level-2-item {
margin-bottom: 8rpx;
padding-left: 40rpx;
position: relative;
width: 100%;
&::before {
content: '';
position: absolute;
left: 20rpx;
top: 18rpx;
width: 6rpx;
height: 6rpx;
background: #667eea;
border-radius: 50%;
}
// 三级菜单列表
.level-3-list {
margin-top: 8rpx;
padding-left: 0;
width: 100%;
.level-3-item {
margin-bottom: 8rpx;
padding: 8rpx 12rpx 8rpx 60rpx;
position: relative;
background: #fafbfc;
border-radius: 8rpx;
width: 100%;
&::before {
content: '';
position: absolute;
left: 40rpx;
top: 50%;
transform: translateY(-50%);
width: 4rpx;
height: 4rpx;
background: #c0c4cc;
border-radius: 50%;
}
}
}
}
}
}
}
}
.popup-footer {
padding: 24rpx;
border-top: 1rpx solid #f0f0f0;
display: flex;
gap: 24rpx;
.footer-btn {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 500;
transition: all 0.3s;
}
.cancel-btn {
background: #f5f7fa;
color: #909399;
&:active {
background: #e8eaed;
}
}
.confirm-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
&:active {
opacity: 0.8;
}
}
}
}
</style>
<style lang="scss">
.form-btn .u-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border: none !important;
border-radius: 24rpx !important;
height: 80rpx !important;
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4) !important;
}
.form-btn .u-button__text {
font-size: 30rpx !important;
font-weight: 500 !important;
letter-spacing: 2rpx !important;
}
</style>