1190 lines
29 KiB
Plaintext
1190 lines
29 KiB
Plaintext
<template>
|
||
<view class="roles-page">
|
||
<admin-layout>
|
||
<view class="container">
|
||
<view class="page-header">
|
||
<text class="page-title">角色权限管理</text>
|
||
<view class="header-actions">
|
||
<button class="primary-btn" @click="showAddModal = true">添加角色</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 横向tab -->
|
||
<view class="swiper-tabs">
|
||
<scroll-view direction="horizontal" :show-scrollbar="false">
|
||
<view class="flex-row">
|
||
<text v-for="(tab, idx) in tabList" :key="tab.key" class="swiper-tabs-item"
|
||
:class="swiperIndex==idx ? 'swiper-tabs-item-active' : ''" @click="onTabClick(idx)">
|
||
{{tab.name}}
|
||
</text>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="swiper-tabs-indicator" :style="indicatorStyle ?? {}"></view>
|
||
</view>
|
||
|
||
<!-- swiper内容-->
|
||
<swiper class="swiper-view" :current="swiperIndex" @change="onSwiperChange">
|
||
<swiper-item class="swiper-item">
|
||
<!-- 角色管理tab,supadb表格大屏,小屏卡-->
|
||
<supadb ref="rolesdb" :collection="'ak_roles'" :field="'*'" :orderby="'level.desc'"
|
||
:page-size="10" :page-current="rolesPageCurrent" getcount="exact" loadtime="manual"
|
||
v-slot:default="{ data, current, loading, total, error }" @load="onRolesLoad">
|
||
<view v-if="loading" class="loading-container">加载中...</view>
|
||
<view v-else-if="error">{{ error }}</view>
|
||
<view v-else>
|
||
<!-- 大屏表格 -->
|
||
<view class="table-view">
|
||
<view class="table-header">
|
||
<text>角色名称</text>
|
||
<text>描述</text>
|
||
<text>级别</text>
|
||
<text>系统角色</text>
|
||
<text>操作</text>
|
||
</view>
|
||
<view class="table-content">
|
||
<view v-for="(role, idx) in (data as Array<UTSJSONObject>)"
|
||
:key="role.get('id')"
|
||
:class="['table-row', idx % 2 === 0 ? 'row-even' : 'row-odd']">
|
||
<text>{{ role.get('name') }}</text>
|
||
<text>{{ (role.get('description') ?? '') !== '' ? role.get('description') : '-' }}</text>
|
||
<text>{{ role.get('level') }}</text>
|
||
<text>{{ role.get('is_system') === true ? '是' : '否' }}</text>
|
||
<view class="action-buttons">
|
||
<button @click="viewRoleDetail(roleToRoleObj(role))">查看权限</button>
|
||
<button v-if="role.get('is_system') !== true"
|
||
@click="editRole(roleToRoleObj(role))">编辑</button>
|
||
<button v-if="role.get('is_system') !== true"
|
||
@click="confirmDeleteRole(roleToRoleObj(role))">删除</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 小屏卡片 -->
|
||
<view class="card-view">
|
||
<view v-for="(role, idx) in (data as Array<UTSJSONObject>)" :key="role.get('id')"
|
||
class="user-card">
|
||
<view class="card-row"><text class="card-label">角色名称:</text><text
|
||
class="card-value">{{ role.get('name') }}</text></view>
|
||
<view class="card-row"><text class="card-label">描述:</text><text
|
||
class="card-value">{{ (role.get('description') ?? '') !== '' ? role.get('description') : '-' }}</text>
|
||
</view>
|
||
<view class="card-row"><text class="card-label">级别:</text><text
|
||
class="card-value">{{ role.get('level') }}</text></view>
|
||
<view class="card-row"><text class="card-label">系统角色:</text><text
|
||
class="card-value">{{ role.get('is_system') === true ? '是' : '否' }}</text>
|
||
</view>
|
||
<view class="card-actions">
|
||
<button @click="viewRoleDetail(roleToRoleObj(role))">查看权限</button>
|
||
<button v-if="role.get('is_system') !== true"
|
||
@click="editRole(roleToRoleObj(role))">编辑</button>
|
||
<button v-if="role.get('is_system') !== true"
|
||
@click="confirmDeleteRole(roleToRoleObj(role))">删除</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="pagination">
|
||
<button @click="rolesPrevPage()">上一页</button>
|
||
<text>{{ current }}/{{ total }}</text>
|
||
<button @click="rolesNextPage()">下一页</button>
|
||
</view>
|
||
</view>
|
||
</supadb>
|
||
</swiper-item>
|
||
<swiper-item class="swiper-item">
|
||
<!-- 权限管理tab,supadb表格大屏,小屏卡片-->
|
||
<supadb ref="permissionsdb" :collection="'ak_permissions'" :field="'*'" :orderby="'id.desc'"
|
||
:page-size="10" :page-current="permissionsPageCurrent" getcount="exact" loadtime="manual"
|
||
v-slot:default="{ data, current, loading, total, error }" @load="onPermissionsLoad">
|
||
<view v-if="loading" class="loading-container">加载中...</view>
|
||
<view v-else-if="error">{{ error }}</view>
|
||
<view v-else>
|
||
<!-- 大屏表格 -->
|
||
<view class="table-view">
|
||
<view class="table-header">
|
||
<text>权限代码</text>
|
||
<text>名称</text>
|
||
<text>资源类型</text>
|
||
<text>操作</text>
|
||
<text>描述</text>
|
||
</view>
|
||
<scroll-view class="table-content" direction="vertical">
|
||
<view v-for="(perm, idx) in (data as Array<UTSJSONObject>)"
|
||
:key="perm.get('id')"
|
||
:class="['table-row', idx % 2 === 0 ? 'row-even' : 'row-odd']">
|
||
<text>{{ perm.get('code') }}</text>
|
||
<text>{{ perm.get('name') }}</text>
|
||
<text>{{ perm.get('resource_type') }}</text>
|
||
<text>{{ perm.get('action') }}</text>
|
||
<text>{{ (perm.get('description') ?? '') !== '' ? perm.get('description') : '-' }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
<!-- 小屏卡片 -->
|
||
<scroll-view class="card-view" direction="vertical">
|
||
<view v-for="(perm, idx) in (data as Array<UTSJSONObject>)" :key="perm.get('id')"
|
||
class="user-card">
|
||
<view class="card-row"><text class="card-label">权限代码:</text><text
|
||
class="card-value">{{ perm.get('code') }}</text></view>
|
||
<view class="card-row"><text class="card-label">名称:</text><text
|
||
class="card-value">{{ perm.get('name') }}</text></view>
|
||
<view class="card-row"><text class="card-label">资源类型:</text><text
|
||
class="card-value">{{ perm.get('resource_type') }}</text></view>
|
||
<view class="card-row"><text class="card-label">操作:</text><text
|
||
class="card-value">{{ perm.get('action') }}</text></view>
|
||
<view class="card-row"><text class="card-label">描述:</text><text
|
||
class="card-value">{{ (perm.get('description') ?? '') !== '' ? perm.get('description') : '-' }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="pagination">
|
||
<button @click="permissionsPrevPage()">上一页</button>
|
||
<text>{{ current }}/{{ total }}</text>
|
||
<button @click="permissionsNextPage()">下一页</button>
|
||
</view>
|
||
</view>
|
||
</supadb>
|
||
</swiper-item>
|
||
</swiper>
|
||
|
||
</view>
|
||
|
||
<!-- 添加/编辑角色模态框 -->
|
||
<view class="modal" v-if="showAddModal || showEditModal">
|
||
<view class="modal-content">
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ showEditModal ? '编辑角色' : '添加角色' }}</text>
|
||
<text class="close-btn" @click="closeModal">×</text>
|
||
</view>
|
||
|
||
<view class="modal-body">
|
||
<view class="form-group">
|
||
<text class="form-label">角色名称:</text>
|
||
<input type="text" :value="formData.name" @input="onInput('name', $event.detail.value)" placeholder="请输入角色名称" />
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">描述:</text>
|
||
<textarea :value="formData.description" @input="onInput('description', $event.detail.value)" placeholder="请输入角色描述"></textarea>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">级别:</text>
|
||
<input type="number" :value="formData.level" @input="onInput('level', $event.detail.value as number)" placeholder="请输入角色级别(数字越大权限越高)" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal-footer">
|
||
<button class="cancel-btn" @click="closeModal">取消</button>
|
||
<button class="submit-btn" @click="submitRole">确定</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 角色详情权限分配 -->
|
||
<view class="modal large" v-if="showRoleDetailModal">
|
||
<view class="modal-content">
|
||
<view class="modal-header">
|
||
<text class="modal-title">角色权限管理: {{ selectedRole?.name }}</text>
|
||
<text class="close-btn" @click="showRoleDetailModal = false">×</text>
|
||
</view>
|
||
<view class="modal-body">
|
||
<view v-if="rolePermissionsLoading" class="loading">
|
||
<text>加载中...</text>
|
||
</view>
|
||
<view v-else>
|
||
<text class="section-title">已分配权限</text>
|
||
<view class="permissions-section">
|
||
<view v-if="rolePermissions.length === 0" class="empty-data">
|
||
<text>暂无权限</text>
|
||
</view>
|
||
<view v-else class="permission-tags">
|
||
<view v-for="(perm, index) in rolePermissions" :key="perm.id" class="perm-tag">
|
||
<text>{{ perm.name }}</text>
|
||
<text v-if="selectedRole?.is_system !== true" class="remove-icon" @click="removePermission(perm)">×</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="selectedRole?.is_system !== true">
|
||
<text class="section-title">添加权限</text>
|
||
<view class="permission-filter">
|
||
<view class="filter-group">
|
||
<text>资源类型:</text>
|
||
<view class="picker-field" @click="showResourceTypePicker = true">
|
||
<text class="picker-text">{{ (addPermResourceType ?? '') !== '' ? addPermResourceType : '全部' }}</text>
|
||
<text class="picker-arrow">></text>
|
||
</view>
|
||
<view v-if="showResourceTypePicker" class="picker-modal">
|
||
<picker-view
|
||
class="picker-view"
|
||
:value="[tempResourceTypeIndex]"
|
||
:indicator-style="'height: 50px;'"
|
||
@change="onResourceTypePickerViewChange"
|
||
>
|
||
<picker-view-column style="width:750rpx;">
|
||
<view v-for="(type, idx) in resourceTypes" :key="type" class="picker-item">{{ type }}</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
<view class="picker-actions">
|
||
<button @click="showResourceTypePicker = false">取消</button>
|
||
<button @click="confirmResourceTypePicker" class="picker-actions-button">确定</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="permission-select-list">
|
||
<view v-for="(perm, index) in (availablePermissions as Permission[])" :key="perm.id" class="permission-select-item" @click="assignPermission(perm)">
|
||
<view class="perm-info">
|
||
<text class="perm-name">{{ perm.name }}</text>
|
||
<text class="perm-code">{{ perm.code }}</text>
|
||
</view>
|
||
<text class="add-icon">+</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="modal-footer">
|
||
<button class="submit-btn" @click="showRoleDetailModal = false">关闭</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 删除确认框-->
|
||
<view class="modal" v-if="showDeleteModal">
|
||
<view class="modal-content">
|
||
<view class="modal-header">
|
||
<text class="modal-title">确认删除</text>
|
||
<text class="close-btn" @click="showDeleteModal = false">×</text>
|
||
</view>
|
||
|
||
<view class="modal-body">
|
||
<text>确定要删除角色“{{ deleteRole?.name }}”吗?此操作不可恢复,所有拥有该角色的用户将失去相应权限。</text>
|
||
</view>
|
||
|
||
<view class="modal-footer">
|
||
<button class="cancel-btn" @click="showDeleteModal = false">取消</button>
|
||
<button class="delete-btn" @click="deleteRoleConfirmed">删除</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</admin-layout>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="uts">
|
||
import {
|
||
ref,
|
||
reactive,
|
||
onMounted,
|
||
computed,
|
||
watch
|
||
} from 'vue'
|
||
import {
|
||
unref
|
||
} from 'vue'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
// 定义角色对象类型
|
||
type Role = {
|
||
id: number
|
||
name: string
|
||
description: string
|
||
level: number
|
||
is_system: boolean
|
||
}
|
||
|
||
// 定义权限对象类型
|
||
type Permission = {
|
||
id: number
|
||
code: string
|
||
name: string
|
||
resource_type: string
|
||
action: string
|
||
description: string
|
||
}
|
||
|
||
// 定义角色-权限关联类型
|
||
type RolePermission = {
|
||
role_id: number
|
||
permission_id: number
|
||
}
|
||
|
||
export default {
|
||
setup() {
|
||
|
||
// 页面状态
|
||
const rolesdb = ref(null)
|
||
const permissionsdb = ref(null)
|
||
const rolesPageCurrent = ref(1)
|
||
const permissionsPageCurrent = ref(1)
|
||
const rolesTotal = ref(0)
|
||
const permissionsTotal = ref(0)
|
||
|
||
// 模态框状态
|
||
const showAddModal = ref(false)
|
||
const showEditModal = ref(false)
|
||
const showDeleteModal = ref(false)
|
||
const showRoleDetailModal = ref(false)
|
||
|
||
// 表单数据
|
||
const formData = reactive < Role > ({
|
||
id: -1,
|
||
name: '',
|
||
description: '',
|
||
level: 0,
|
||
is_system: false
|
||
})
|
||
|
||
// 当前操作的角色
|
||
const selectedRole = ref < Role | null > (null)
|
||
const deleteRole = ref < Role | null > (null)
|
||
|
||
// 权限管理
|
||
const rolePermissions = ref < Permission[] > ([])
|
||
const allPermissions = ref < Permission[] > ([])
|
||
const availablePermissions = ref < Permission[] > ([])
|
||
const rolePermissionsLoading = ref(false)
|
||
const addPermResourceType = ref < string | null > (null)
|
||
const resourceTypes = ref < string[] > ([])
|
||
|
||
// picker相关
|
||
const showResourceTypePicker = ref(false)
|
||
const tempResourceTypeIndex = ref(0)
|
||
|
||
|
||
// Tab切换相关
|
||
const swiperIndex = ref(0)
|
||
const tabList = [{
|
||
key: 'roles',
|
||
name: '角色管理'
|
||
}, {
|
||
key: 'permissions',
|
||
name: '权限列表'
|
||
}]
|
||
const indicatorStyle = computed(() => {
|
||
const tabWidth = 100 / tabList.length
|
||
const left = swiperIndex.value * tabWidth
|
||
return {
|
||
width: `${tabWidth}%`,
|
||
left: `${left}%`
|
||
}
|
||
})
|
||
|
||
const onTabClick = (index: number) => {
|
||
swiperIndex.value = index
|
||
}
|
||
|
||
const onSwiperChange = (e: any) => {
|
||
swiperIndex.value = e.detail.current
|
||
}
|
||
|
||
// 数据加载
|
||
onMounted(() => {
|
||
loadRoles()
|
||
loadPermissions()
|
||
loadAllPermissionsForAssignment()
|
||
})
|
||
|
||
const loadRoles = () => {
|
||
const rolesdbRef = unref(rolesdb)
|
||
if (rolesdbRef != null) {
|
||
(rolesdbRef as any).loadData()
|
||
}
|
||
}
|
||
|
||
const loadPermissions = () => {
|
||
const permissionsdbRef = unref(permissionsdb)
|
||
if (permissionsdbRef != null) {
|
||
(permissionsdbRef as any).loadData()
|
||
}
|
||
}
|
||
|
||
const onRolesLoad = (data: UTSJSONObject[], {
|
||
total
|
||
}: {
|
||
total: number
|
||
}) => {
|
||
rolesTotal.value = Math.ceil(total / 10)
|
||
}
|
||
|
||
const onPermissionsLoad = (data: UTSJSONObject[], {
|
||
total
|
||
}: {
|
||
total: number
|
||
}) => {
|
||
permissionsTotal.value = Math.ceil(total / 10)
|
||
}
|
||
|
||
// 分页
|
||
const rolesPrevPage = () => {
|
||
if (rolesPageCurrent.value > 1) {
|
||
rolesPageCurrent.value--
|
||
}
|
||
}
|
||
const rolesNextPage = () => {
|
||
if (rolesPageCurrent.value < rolesTotal.value) {
|
||
rolesPageCurrent.value++
|
||
}
|
||
}
|
||
const permissionsPrevPage = () => {
|
||
if (permissionsPageCurrent.value > 1) {
|
||
permissionsPageCurrent.value--
|
||
}
|
||
}
|
||
const permissionsNextPage = () => {
|
||
if (permissionsPageCurrent.value < permissionsTotal.value) {
|
||
permissionsPageCurrent.value++
|
||
}
|
||
}
|
||
|
||
// 表单操作
|
||
const onInput = (key: string, value: string | number) => {
|
||
if (key === 'level') {
|
||
(formData as any)[key] = parseInt(value as string, 10) || 0
|
||
} else {
|
||
(formData as any)[key] = value
|
||
}
|
||
}
|
||
|
||
const closeModal = () => {
|
||
showAddModal.value = false
|
||
showEditModal.value = false
|
||
resetFormData()
|
||
}
|
||
|
||
const resetFormData = () => {
|
||
formData.id = -1
|
||
formData.name = ''
|
||
formData.description = ''
|
||
formData.level = 0
|
||
formData.is_system = false
|
||
}
|
||
|
||
// 角色CRUD
|
||
const submitRole = async () => {
|
||
if (formData.name.trim() === '') {
|
||
uni.showToast({
|
||
title: '角色名称不能为空',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
let result: any
|
||
if (showEditModal.value) {
|
||
// 编辑
|
||
const {
|
||
data,
|
||
error
|
||
} = await supabase
|
||
.from('ak_roles')
|
||
.update({
|
||
name: formData.name,
|
||
description: formData.description,
|
||
level: formData.level
|
||
})
|
||
.eq('id', formData.id)
|
||
result = {
|
||
data,
|
||
error
|
||
}
|
||
} else {
|
||
// 添加
|
||
const {
|
||
data,
|
||
error
|
||
} = await supabase
|
||
.from('ak_roles')
|
||
.insert([{
|
||
name: formData.name,
|
||
description: formData.description,
|
||
level: formData.level,
|
||
is_system: false
|
||
}])
|
||
result = {
|
||
data,
|
||
error
|
||
}
|
||
}
|
||
|
||
if (result.error) {
|
||
uni.showToast({
|
||
title: `操作失败: ${result.error.message}`,
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: '操作成功',
|
||
icon: 'success'
|
||
})
|
||
closeModal()
|
||
loadRoles()
|
||
}
|
||
}
|
||
|
||
const editRole = (role: Role) => {
|
||
formData.id = role.id
|
||
formData.name = role.name
|
||
formData.description = role.description
|
||
formData.level = role.level
|
||
showEditModal.value = true
|
||
}
|
||
|
||
const confirmDeleteRole = (role: Role) => {
|
||
deleteRole.value = role
|
||
showDeleteModal.value = true
|
||
}
|
||
|
||
const deleteRoleConfirmed = async () => {
|
||
if (!deleteRole.value) return
|
||
|
||
const {
|
||
error
|
||
} = await supabase
|
||
.from('ak_roles')
|
||
.delete()
|
||
.eq('id', deleteRole.value.id)
|
||
|
||
if (error) {
|
||
uni.showToast({
|
||
title: `删除失败: ${error.message}`,
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: '删除成功',
|
||
icon: 'success'
|
||
})
|
||
showDeleteModal.value = false
|
||
deleteRole.value = null
|
||
loadRoles()
|
||
}
|
||
}
|
||
|
||
// 权限详情与分配
|
||
const viewRoleDetail = async (role: Role) => {
|
||
selectedRole.value = role
|
||
showRoleDetailModal.value = true
|
||
rolePermissionsLoading.value = true
|
||
|
||
// 加载当前角色的权限
|
||
const { data, error } = await supabase
|
||
.from('ak_role_permissions')
|
||
.select('permission_id')
|
||
.eq('role_id', role.id)
|
||
|
||
if (error) {
|
||
console.error('获取角色权限失败:', error)
|
||
rolePermissions.value = []
|
||
} else {
|
||
const permissionIds = data.map(item => item.permission_id)
|
||
const { data: permsData, error: permsError } = await supabase
|
||
.from('ak_permissions')
|
||
.select('*')
|
||
.in('id', permissionIds)
|
||
|
||
if (permsError) {
|
||
rolePermissions.value = []
|
||
} else {
|
||
rolePermissions.value = permsData.map(p => ({
|
||
id: p.id,
|
||
code: p.code,
|
||
name: p.name,
|
||
resource_type: p.resource_type,
|
||
action: p.action,
|
||
description: p.description
|
||
}))
|
||
}
|
||
}
|
||
rolePermissionsLoading.value = false
|
||
filterAvailablePermissions()
|
||
}
|
||
|
||
const loadAllPermissionsForAssignment = async () => {
|
||
const { data, error } = await supabase
|
||
.from('ak_permissions')
|
||
.select('*')
|
||
.order('resource_type', { ascending: true })
|
||
.order('name', { ascending: true })
|
||
|
||
if (error) {
|
||
console.error('加载所有权限失败:', error)
|
||
} else {
|
||
allPermissions.value = data.map(p => ({
|
||
id: p.id,
|
||
code: p.code,
|
||
name: p.name,
|
||
resource_type: p.resource_type,
|
||
action: p.action,
|
||
description: p.description
|
||
}))
|
||
|
||
// 提取所有唯一的 resource_type
|
||
const types = new Set<string>()
|
||
types.add('全部') // 添加一个“全部”选项
|
||
allPermissions.value.forEach(p => types.add(p.resource_type))
|
||
resourceTypes.value = Array.from(types)
|
||
}
|
||
}
|
||
|
||
const filterAvailablePermissions = () => {
|
||
const assignedIds = new Set(rolePermissions.value.map(p => p.id))
|
||
let filtered = allPermissions.value.filter(p => !assignedIds.has(p.id))
|
||
|
||
if (addPermResourceType.value != null && addPermResourceType.value !== '全部') {
|
||
filtered = filtered.filter(p => p.resource_type === addPermResourceType.value)
|
||
}
|
||
|
||
availablePermissions.value = filtered
|
||
}
|
||
|
||
watch([() => rolePermissions.value, () => addPermResourceType.value], filterAvailablePermissions, { deep: true })
|
||
|
||
const assignPermission = async (permission: Permission) => {
|
||
if (!selectedRole.value) return
|
||
|
||
const { error } = await supabase
|
||
.from('ak_role_permissions')
|
||
.insert([{
|
||
role_id: selectedRole.value.id,
|
||
permission_id: permission.id
|
||
}])
|
||
|
||
if (error) {
|
||
uni.showToast({ title: `添加失败: ${error.message}`, icon: 'none' })
|
||
} else {
|
||
uni.showToast({ title: '添加成功', icon: 'success' })
|
||
// 重新加载权限
|
||
viewRoleDetail(selectedRole.value!)
|
||
}
|
||
}
|
||
|
||
const removePermission = async (permission: Permission) => {
|
||
if (!selectedRole.value) return
|
||
|
||
const { error } = await supabase
|
||
.from('ak_role_permissions')
|
||
.delete()
|
||
.eq('role_id', selectedRole.value.id)
|
||
.eq('permission_id', permission.id)
|
||
|
||
if (error) {
|
||
uni.showToast({ title: `移除失败: ${error.message}`, icon: 'none' })
|
||
} else {
|
||
uni.showToast({ title: '移除成功', icon: 'success' })
|
||
// 重新加载权限
|
||
viewRoleDetail(selectedRole.value!)
|
||
}
|
||
}
|
||
|
||
// Picker相关方法
|
||
const onResourceTypePickerViewChange = (e: any) => {
|
||
tempResourceTypeIndex.value = e.detail.value[0]
|
||
}
|
||
|
||
const confirmResourceTypePicker = () => {
|
||
addPermResourceType.value = resourceTypes.value[tempResourceTypeIndex.value]
|
||
showResourceTypePicker.value = false
|
||
}
|
||
|
||
// 工具函数
|
||
const roleToRoleObj = (role: UTSJSONObject): Role => {
|
||
return {
|
||
id: role.get('id') as number,
|
||
name: role.get('name') as string,
|
||
description: role.get('description') as string,
|
||
level: role.get('level') as number,
|
||
is_system: role.get('is_system') as boolean,
|
||
}
|
||
}
|
||
|
||
return {
|
||
// data
|
||
rolesdb,
|
||
permissionsdb,
|
||
rolesPageCurrent,
|
||
permissionsPageCurrent,
|
||
rolesTotal,
|
||
permissionsTotal,
|
||
showAddModal,
|
||
showEditModal,
|
||
showDeleteModal,
|
||
showRoleDetailModal,
|
||
formData,
|
||
selectedRole,
|
||
deleteRole,
|
||
rolePermissions,
|
||
availablePermissions,
|
||
rolePermissionsLoading,
|
||
addPermResourceType,
|
||
resourceTypes,
|
||
showResourceTypePicker,
|
||
tempResourceTypeIndex,
|
||
// computed
|
||
swiperIndex,
|
||
tabList,
|
||
indicatorStyle,
|
||
// methods
|
||
onTabClick,
|
||
onSwiperChange,
|
||
loadRoles,
|
||
loadPermissions,
|
||
onRolesLoad,
|
||
onPermissionsLoad,
|
||
rolesPrevPage,
|
||
rolesNextPage,
|
||
permissionsPrevPage,
|
||
permissionsNextPage,
|
||
onInput,
|
||
closeModal,
|
||
submitRole,
|
||
editRole,
|
||
confirmDeleteRole,
|
||
deleteRoleConfirmed,
|
||
viewRoleDetail,
|
||
assignPermission,
|
||
removePermission,
|
||
onResourceTypePickerViewChange,
|
||
confirmResourceTypePicker,
|
||
roleToRoleObj
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* 通用样式 */
|
||
.roles-page {
|
||
background-color: #f5f5f5;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.container {
|
||
padding: 20px;
|
||
background-color: #ffffff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
margin: 16px;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.header-actions .primary-btn {
|
||
background-color: #007aff;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 15px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.primary-btn:hover {
|
||
background-color: #0056b3;
|
||
}
|
||
|
||
/* Tab样式 */
|
||
.swiper-tabs {
|
||
position: relative;
|
||
background-color: #FFF;
|
||
border-bottom: 1px solid #ddd;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.swiper-tabs-item {
|
||
padding: 10px 20px;
|
||
font-size: 16px;
|
||
color: #555;
|
||
cursor: pointer;
|
||
text-align: center;
|
||
}
|
||
|
||
.swiper-tabs-item-active {
|
||
color: #007aff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.swiper-tabs-indicator {
|
||
position: absolute;
|
||
bottom: 0;
|
||
height: 3px;
|
||
background-color: #007aff;
|
||
transition: left 0.2s ease-in-out;
|
||
}
|
||
|
||
.swiper-view {
|
||
height: 600px; /* 根据内容调整 */
|
||
}
|
||
|
||
.loading-container {
|
||
text-align: center;
|
||
padding: 50px;
|
||
color: #888;
|
||
}
|
||
|
||
/* 分页 */
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.pagination button {
|
||
padding: 5px 10px;
|
||
margin: 0 10px;
|
||
background-color: #007aff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.pagination text {
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
/* 模态框样式 */
|
||
.modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal.large .modal-content {
|
||
width: 80%;
|
||
max-width: 900px;
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: white;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-bottom: 1px solid #eee;
|
||
padding-bottom: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.close-btn {
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #888;
|
||
}
|
||
|
||
.modal-body .form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.modal-body .form-label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.modal-body input,
|
||
.modal-body textarea {
|
||
width: 100%;
|
||
padding: 8px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.modal-body textarea {
|
||
min-height: 80px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.modal-footer button {
|
||
padding: 10px 15px;
|
||
border-radius: 5px;
|
||
border: none;
|
||
cursor: pointer;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.cancel-btn {
|
||
background-color: #f0f0f0;
|
||
}
|
||
|
||
.submit-btn {
|
||
background-color: #007aff;
|
||
color: white;
|
||
}
|
||
|
||
.delete-btn {
|
||
background-color: #ff3b30;
|
||
color: white;
|
||
}
|
||
|
||
/* 权限详情 */
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-top: 20px;
|
||
margin-bottom: 10px;
|
||
display: block;
|
||
}
|
||
|
||
.permissions-section .empty-data {
|
||
color: #888;
|
||
font-style: italic;
|
||
}
|
||
|
||
.permission-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.perm-tag {
|
||
background-color: #eef5ff;
|
||
color: #007aff;
|
||
padding: 5px 10px;
|
||
border-radius: 15px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.perm-tag .remove-icon {
|
||
margin-left: 8px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
color: #ff3b30;
|
||
}
|
||
|
||
.permission-filter {
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.picker-field {
|
||
border: 1px solid #ccc;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
min-width: 150px;
|
||
}
|
||
|
||
.picker-text {
|
||
color: #333;
|
||
}
|
||
|
||
.picker-arrow {
|
||
color: #888;
|
||
}
|
||
|
||
.picker-modal {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
background-color: white;
|
||
z-index: 1100;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.picker-view {
|
||
width: 100%;
|
||
height: 200px;
|
||
}
|
||
|
||
.picker-item {
|
||
line-height: 50px;
|
||
text-align: center;
|
||
}
|
||
|
||
.picker-actions {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 10px;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.picker-actions-button {
|
||
color: #007aff;
|
||
}
|
||
|
||
.permission-select-list {
|
||
max-height: 250px;
|
||
overflow-y: auto;
|
||
border: 1px solid #eee;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.permission-select-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.permission-select-item:hover {
|
||
background-color: #f9f9f9;
|
||
}
|
||
|
||
.perm-info .perm-name {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.perm-info .perm-code {
|
||
font-size: 12px;
|
||
color: #888;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.add-icon {
|
||
font-size: 20px;
|
||
color: #007aff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 响应式布局 */
|
||
.table-view {
|
||
display: none;
|
||
}
|
||
|
||
.card-view {
|
||
display: block;
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
.table-view {
|
||
display: block;
|
||
}
|
||
|
||
.card-view {
|
||
display: none;
|
||
}
|
||
|
||
.table-header,
|
||
.table-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 2fr 0.5fr 0.5fr 1.5fr;
|
||
gap: 10px;
|
||
padding: 10px;
|
||
align-items: center;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.table-header {
|
||
font-weight: bold;
|
||
background-color: #f9f9f9;
|
||
}
|
||
|
||
.table-content {
|
||
max-height: 450px; /* 设定一个最大高度 */
|
||
overflow-y: auto; /* 超出部分显示滚动条 */
|
||
}
|
||
|
||
.table-row.row-odd {
|
||
background-color: #f9f9f9;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
.action-buttons button {
|
||
padding: 5px 8px;
|
||
font-size: 12px;
|
||
border: 1px solid #007aff;
|
||
background-color: transparent;
|
||
color: #007aff;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
.user-card {
|
||
background: #fff;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.card-row {
|
||
display: flex;
|
||
padding: 5px 0;
|
||
}
|
||
|
||
.card-label {
|
||
width: 80px;
|
||
font-weight: bold;
|
||
color: #555;
|
||
}
|
||
|
||
.card-value {
|
||
flex: 1;
|
||
color: #333;
|
||
}
|
||
|
||
.card-actions {
|
||
margin-top: 10px;
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.card-actions button {
|
||
padding: 5px 10px;
|
||
font-size: 13px;
|
||
border: 1px solid #007aff;
|
||
background-color: transparent;
|
||
color: #007aff;
|
||
border-radius: 4px;
|
||
}
|
||
</style>
|