Files
medical-mall/pages/mall/admin/setting/auth/permission.uvue

264 lines
10 KiB
Plaintext
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="admin-page-container">
<view class="page-card">
<!-- 顶部操作栏 -->
<view class="action-wrap">
<button class="btn btn-primary" @click="onAdd(null)">添加顶级权限</button>
<button class="btn btn-ghost ml-10" @click="loadData">刷新</button>
</view>
<!-- 树形表格区域 -->
<view class="table-wrap border-shadow">
<view class="table-header">
<view class="th" style="flex: 4; text-align: left; padding-left: 20px;">菜单/按钮名称</view>
<view class="th" style="flex: 2;">编码</view>
<view class="th" style="flex: 2;">类型</view>
<view class="th" style="flex: 1;">排序</view>
<view class="th" style="flex: 1;">显示</view>
<view class="th" style="flex: 3;">操作</view>
</view>
<view class="table-body">
<view v-if="loading" class="loading-box">
<text>加载中...</text>
</view>
<view v-else-if="permissionList.length === 0" class="no-data">
<text class="no-data-text">暂无数据</text>
</view>
<!-- 递归渲染或平铺渲染 (这里采用平铺+缩进模拟树形) -->
<view v-else v-for="item in permissionList" :key="item.id" class="tr">
<view class="td" style="flex: 4; text-align: left; padding-left: 20px;">
<text class="menu-name">{{ item.name }}</text>
</view>
<view class="td" style="flex: 2;"><text class="td-txt-small">{{ item.code }}</text></view>
<view class="td" style="flex: 2;">
<text :class="['type-tag', item.type === 'menu' ? 'menu' : 'button']">
{{ item.type === 'menu' ? '菜单' : '按钮' }}
</text>
</view>
<view class="td" style="flex: 1;"><text class="td-txt">{{ item.sort_order }}</text></view>
<view class="td" style="flex: 1;">
<switch :checked="item.is_visible" color="#1890ff" scale="0.6" @change="onToggleVisible(item)" />
</view>
<view class="td" style="flex: 3;">
<view class="op-links">
<text class="action-btn" @click="onAdd(item.id)">新增子项</text>
<text class="op-split">|</text>
<text class="action-btn" @click="onEdit(item)">编辑</text>
<text class="op-split">|</text>
<text class="action-btn danger" @click="onDelete(item)">删除</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 添加/编辑 弹窗 -->
<view v-if="showModal" class="modal-overlay" @click="closeModal">
<view class="modal-card" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ isEdit ? '编辑权限' : '添加权限' }}</text>
<text class="close-btn" @click="closeModal">×</text>
</view>
<view class="modal-body">
<scroll-view scroll-y="true" style="max-height: 500px;">
<view class="form-item">
<text class="f-label">父级ID</text>
<input class="f-input disabled" :value="form.parent_id || '顶级'" disabled />
</view>
<view class="form-item">
<text class="f-label">名称:</text>
<input class="f-input" v-model="form.name" placeholder="请输入菜单或按钮名称" />
</view>
<view class="form-item">
<text class="f-label">编码:</text>
<input class="f-input" v-model="form.code" placeholder="如: user_view" />
</view>
<view class="form-item">
<text class="f-label">类型:</text>
<radio-group class="radio-group" @change="(e : any) => form.type = e.detail.value">
<label class="radio-label"><radio value="menu" :checked="form.type === 'menu'" color="#1890ff" /> 菜单</label>
<label class="radio-label"><radio value="button" :checked="form.type === 'button'" color="#1890ff" /> 按钮</label>
</radio-group>
</view>
<view class="form-item" v-if="form.type === 'menu'">
<text class="f-label">路由路径:</text>
<input class="f-input" v-model="form.path" placeholder="请输入前端路由地址" />
</view>
<view class="form-item">
<text class="f-label">排序:</text>
<input class="f-input" type="number" v-model="form.sort_order" />
</view>
</scroll-view>
</view>
<view class="modal-footer">
<button class="btn" @click="closeModal">取消</button>
<button class="btn btn-primary" @click="handleSave">提交</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { fetchPermissionList, savePermission, deletePermission, type AdminPermission } from '@/services/admin/authService.uts'
const permissionList = ref<AdminPermission[]>([])
const loading = ref(false)
const showModal = ref(false)
const isEdit = ref(false)
const form = reactive({
id: '' as string | null,
parent_id: '' as string | null,
name: '',
code: '',
type: 'menu',
path: '',
icon: '',
sort_order: 0,
is_visible: true
})
onMounted(() => {
loadData()
})
async function loadData() {
loading.value = true
try {
const res = await fetchPermissionList()
permissionList.value = res
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function onAdd(parentId : string | null) {
isEdit.value = false
form.id = null
form.parent_id = parentId
form.name = ''
form.code = ''
form.type = 'menu'
form.path = ''
form.sort_order = 0
form.is_visible = true
showModal.value = true
}
function onEdit(item : AdminPermission) {
isEdit.value = true
form.id = item.id
form.parent_id = item.parent_id
form.name = item.name
form.code = item.code
form.type = item.type
form.path = item.path || ''
form.sort_order = item.sort_order
form.is_visible = item.is_visible
showModal.value = true
}
async function onToggleVisible(item : AdminPermission) {
const nextVal = !item.is_visible
const ok = await savePermission({ ...item, is_visible: nextVal })
if (ok != null) {
item.is_visible = nextVal
uni.showToast({ title: '显示状态已更新' })
}
}
async function handleSave() {
if (!form.name || !form.code) {
uni.showToast({ title: '请填写必填项', icon: 'none' })
return
}
loading.value = true
try {
const resId = await savePermission(form)
if (resId != null) {
uni.showToast({ title: '保存成功' })
showModal.value = false
loadData()
}
} finally {
loading.value = false
}
}
async function onDelete(item : AdminPermission) {
uni.showModal({
title: '确认删除',
content: `确定要删除权限项 "${item.name}" 吗?此操作不可撤销。`,
success: async (res) => {
if (res.confirm) {
const ok = await deletePermission(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
}
}
}
})
}
function closeModal() {
showModal.value = false
}
</script>
<style scoped lang="scss">
.admin-page-container { padding: 24px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 24px; }
.action-wrap { margin-bottom: 24px; display: flex; flex-direction: row; }
.btn { height: 32px; padding: 0 16px; font-size: 14px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.btn-primary { background-color: #1890ff; color: #fff; }
.btn-ghost { background-color: #fff; color: #666; border: 1px solid #dcdfe6; }
.ml-10 { margin-left: 10px; }
.table-wrap { border: 1px solid #f0f0f0; border-radius: 4px; }
.table-header { display: flex; flex-direction: row; background-color: #f8f8f9; }
.th { padding: 12px 10px; font-size: 14px; font-weight: bold; color: #515a6e; border-bottom: 1px solid #f0f0f0; text-align: center; }
.tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 50px; align-items: center; }
.td { padding: 8px 10px; font-size: 13px; color: #606266; text-align: center; display: flex; align-items: center; justify-content: center; }
.menu-name { font-weight: 500; color: #333; }
.td-txt-small { font-size: 12px; color: #999; }
.type-tag { padding: 2px 8px; border-radius: 4px; font-size: 11px; }
.type-tag.menu { background-color: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
.type-tag.button { background-color: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.op-links { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.action-btn { color: #1890ff; font-size: 13px; cursor: pointer; }
.danger { color: #ff4d4f; }
.op-split { color: #eee; }
.loading-box, .no-data { padding: 60px 0; text-align: center; width: 100%; }
.no-data-text { font-size: 14px; color: #ccc; }
/* Modal */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
.modal-card { width: 500px; background-color: #fff; border-radius: 8px; overflow: hidden; }
.modal-header { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
.modal-title { font-size: 16px; font-weight: bold; color: #333; }
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
.modal-body { padding: 24px; }
.form-item { margin-bottom: 20px; display: flex; flex-direction: row; align-items: center; }
.align-start { align-items: flex-start; }
.f-label { width: 90px; font-size: 14px; color: #666; text-align: right; margin-right: 15px; }
.f-input { flex: 1; border: 1px solid #dcdfe6; height: 36px; padding: 0 12px; border-radius: 4px; font-size: 14px; }
.f-input.disabled { background-color: #f5f5f5; color: #999; }
.radio-group { display: flex; flex-direction: row; gap: 20px; }
.radio-label { display: flex; flex-direction: row; align-items: center; font-size: 14px; }
.modal-footer { padding: 16px 24px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; gap: 12px; }
</style>