Files
laravel_swoole/resources/admin/src/pages/auth/permissions/save.vue
2026-02-10 22:36:05 +08:00

380 lines
9.7 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>
<div class="save-form">
<a-form :model="form" :rules="rules" ref="dialogForm" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
<!-- 第一行权限名称和类型 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="权限名称" name="title">
<a-input v-model:value="form.title" placeholder="权限名称" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="类型" name="type" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-radio-group v-model:value="form.type" button-style="solid">
<a-radio-button value="menu">菜单</a-radio-button>
<a-radio-button value="api">接口</a-radio-button>
<a-radio-button value="button">按钮</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<!-- 第二行上级菜单和权限编码 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="上级权限" name="parent_id">
<a-tree-select v-model:value="form.parent_id" :tree-data="menuOptions"
:field-names="menuFieldNames" :tree-default-expand-all="false" show-icon placeholder="顶级权限"
allow-clear tree-node-filter-prop="title" :disabled="!!menuId" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="权限编码" name="name">
<a-input v-model:value="form.name" placeholder="如: system.user.list" allow-clear />
<div class="form-item-msg">格式模块.功能.操作系统唯一标识</div>
</a-form-item>
</a-col>
</a-row>
<!-- 第三行路由地址和组件路径 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="路由地址" name="path">
<a-input v-model:value="form.path" placeholder="/system/users" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="组件路径" name="component">
<a-input v-model:value="form.component" placeholder="system/users/index" allow-clear>
<template #addonBefore>pages/</template>
</a-input>
<div class="form-item-msg">顶级菜单或有子菜单的父节点不需要填写</div>
</a-form-item>
</a-col>
</a-row>
<!-- 第四行菜单图标和排序 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="菜单图标" name="icon">
<sc-icon-picker v-model:value="form.icon" placeholder="请选择图标" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<!-- 分隔线 -->
<a-divider />
<!-- 选项设置区域 -->
<div class="options-section">
<h4>选项设置</h4>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="是否隐藏" name="hidden" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
<a-checkbox v-model:checked="form.hidden">隐藏菜单</a-checkbox>
<a-checkbox v-model:checked="form.hiddenBreadcrumb">隐藏面包屑</a-checkbox>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="是否缓存" name="keepAlive" :label-col="{ span: 8 }" :wrapper-col="{ span: 14 }">
<a-switch v-model:checked="form.keepAlive" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="是否固定" name="affix" :label-col="{ span: 8 }" :wrapper-col="{ span: 14 }">
<a-switch v-model:checked="form.affix" />
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 状态 -->
<a-divider />
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="状态" name="status" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
</a-form-item>
</a-col>
</a-row>
<!-- 操作按钮 -->
<a-form-item :wrapper-col="{ span: 18, offset: 4 }" style="margin-top: 32px">
<a-space>
<a-button type="primary" @click="handleSave" :loading="loading" size="large">保存</a-button>
<a-button @click="$emit('cancel')" size="large">取消</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
</template>
<script setup>
import { ref, reactive, watch, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
import ScIconPicker from '@/components/scIconPicker/index.vue'
defineOptions({
name: 'PermissionSave'
})
const props = defineProps({
menu: { type: [Object, Array], default: () => [] },
menuId: { type: [Number, String], default: null },
parentId: { type: [Number, String], default: null }
})
const emit = defineEmits(['success', 'cancel'])
// 表单数据
const form = reactive({
id: '',
parent_id: 0,
name: '',
title: '',
path: '',
component: '',
icon: '',
sort: 0,
type: 'menu',
status: 1,
// meta 字段内容
hidden: false,
hiddenBreadcrumb: false,
keepAlive: false,
affix: false
})
// 状态开关计算属性
const statusChecked = computed({
get: () => form.status === 1,
set: (val) => {
form.status = val ? 1 : 0
}
})
// 表单引用
const dialogForm = ref()
const loading = ref(false)
// 验证规则
const rules = {
name: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入权限编码', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
}
// 菜单选项
const menuOptions = ref([])
const menuFieldNames = {
value: 'id',
label: 'title',
children: 'children'
}
// 简单化菜单树,排除自己和子节点
const treeToMap = (tree, excludeId = null) => {
const map = []
tree.forEach(item => {
if (item.id === excludeId) return // 排除自己
const obj = {
id: item.id,
parent_id: item.parent_id,
title: item.title,
children: item.children && item.children.length > 0 ? treeToMap(item.children, excludeId) : null
}
map.push(obj)
})
return map
}
// 查找权限节点
const findMenuNode = (tree, id) => {
for (const node of tree) {
if (node.id === id) {
return node
}
if (node.children && node.children.length > 0) {
const found = findMenuNode(node.children, id)
if (found) return found
}
}
return null
}
// 监听菜单树变化
watch(
() => props.menu,
(newVal) => {
if (newVal) {
// 排除当前编辑的节点,避免选择自己作为父节点
menuOptions.value = treeToMap(newVal, props.menuId)
}
},
{ deep: true, immediate: true }
)
// 监听 menuId 变化,从菜单树中查找并赋值
watch(
() => props.menuId,
(newVal) => {
if (newVal && props.menu && props.menu.length > 0) {
const menuNode = findMenuNode(props.menu, newVal)
if (menuNode) {
setData(menuNode, props.parentId)
}
} else if (!newVal) {
// 清空表单
setData({
id: '',
parent_id: props.parentId || 0,
name: '',
title: '',
route: '',
component: '',
icon: '',
sort: 0,
type: 'menu',
status: 1,
hidden: false,
hiddenBreadcrumb: false,
keepAlive: false,
affix: false
})
}
}
)
// 加载权限详情
const loadMenuDetail = async (id) => {
try {
const res = await authApi.permissions.detail.get(id)
if (res.code === 200 && res.data) {
setData(res.data, props.parentId)
}
} catch (error) {
console.error('加载权限详情失败:', error)
}
}
// 保存
const handleSave = async () => {
try {
await dialogForm.value.validate()
loading.value = true
// 构建提交数据
const submitData = {
id: form.id || undefined,
parent_id: form.parent_id || 0,
name: form.name,
title: form.title,
path: form.path,
component: form.component,
icon: form.icon,
sort: form.sort,
type: form.type,
status: form.status,
meta: {
hidden: form.hidden,
hiddenBreadcrumb: form.hiddenBreadcrumb,
keepAlive: form.keepAlive,
affix: form.affix
}
}
let res = {}
if (form.id) {
res = await authApi.permissions.edit.put(form.id, submitData)
} else {
res = await authApi.permissions.add.post(submitData)
}
loading.value = false
if (res.code === 200) {
message.success('保存成功')
emit('success')
} else {
message.error(res.message || '保存失败')
}
} catch (error) {
console.error('表单验证失败', error)
loading.value = false
}
}
// 表单注入数据
const setData = (data, pid) => {
form.id = data.id || ''
form.parent_id = data.parent_id !== undefined ? data.parent_id : (pid || 0)
form.name = data.name || ''
form.title = data.title || ''
form.path = data.path || ''
form.component = data.component || ''
form.icon = data.icon || ''
form.sort = data.sort || 0
form.type = data.type || 'menu'
form.status = data.status !== undefined ? data.status : 1
// 解析 meta 字段
const meta = data.meta && typeof data.meta === 'string' ? JSON.parse(data.meta) : (data.meta || {})
form.hidden = meta.hidden || false
form.hiddenBreadcrumb = meta.hiddenBreadcrumb || false
form.keepAlive = meta.keepAlive || false
form.affix = meta.affix || false
}
// 初始化
onMounted(() => {
if (props.menuId) {
loadMenuDetail(props.menuId)
} else if (props.parentId) {
form.parent_id = props.parentId
}
})
// 暴露方法给父组件
defineExpose({
setData
})
</script>
<style scoped lang="scss">
.save-form {
padding: 24px;
background: #fff;
.form-item-msg {
font-size: 12px;
color: #8c8c8c;
margin-top: 4px;
line-height: 1.5;
}
.options-section {
margin-top: 16px;
h4 {
font-size: 16px;
font-weight: 500;
color: #262626;
margin-bottom: 24px;
}
}
:deep(.ant-divider) {
margin: 32px 0;
}
:deep(.ant-form-item) {
margin-bottom: 24px;
}
}
</style>