264 lines
10 KiB
Plaintext
264 lines
10 KiB
Plaintext
<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>
|