feat(rbac-web): 前端页面级权限与「权限与角色」管理页
- 新增 rbac 的 api/types/mock,对接后端 catalog/roles/users;mock 模式与后端 PageCatalog 完全对齐 - EffectivePermissions 增加 allowedPages,auth store 新增 hasPage 判定 - 路由守卫按 route.name = 页面 Key 做页面级放行,无权时跳转到 scope 内首个可访问页面;AppShell 菜单按 hasPage 过滤 - 路由守卫 switchScope 失败自动重试一次;新增懒加载 chunk 失败整页自愈 onError,修复切页白屏 - PermissionGuard hidden 态改为友好空态提示;AuthRoleView 重写为完整的用户/角色/权限编辑界面
This commit is contained in:
@@ -1,57 +1,410 @@
|
||||
<template>
|
||||
<ConfigPageBase
|
||||
section="auth"
|
||||
title="权限与角色管理"
|
||||
description="角色、权限码、控件级 WidgetGrant(auth.user.write / auth.role.write)"
|
||||
:defaults="DEFAULT_AUTH">
|
||||
<template #default="{ payload }">
|
||||
<el-tabs model-value="roles">
|
||||
<el-tab-pane name="roles" label="角色">
|
||||
<el-table :data="payload.roles" size="small" border>
|
||||
<el-table-column label="ID" prop="id" width="120" />
|
||||
<el-table-column label="名称" prop="name" width="120" />
|
||||
<el-table-column label="Scope" prop="scope" width="120" />
|
||||
<el-table-column label="权限码">
|
||||
<template #default="s">
|
||||
<el-tag v-for="p in s.row.permissions" :key="p" size="small" effect="plain" style="margin: 2px">{{ p }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="WidgetGrant" width="280">
|
||||
<template #default="s">
|
||||
<el-tag v-for="g in s.row.widgetGrants" :key="g.widgetId" size="small" :type="grantTag(g.visibility)" effect="plain" style="margin: 2px">{{ g.widgetId }}={{ g.visibility }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="users" label="用户">
|
||||
<el-table :data="payload.users" size="small" border>
|
||||
<el-table-column label="ID" prop="id" width="120" />
|
||||
<el-table-column label="用户名" prop="username" width="140" />
|
||||
<el-table-column label="角色">
|
||||
<template #default="s">
|
||||
<el-tag v-for="r in s.row.roles" :key="r" size="small">{{ r }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="80">
|
||||
<template #default="s"><el-switch v-model="s.row.enabled" /></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-card shadow="never" class="rbac-card">
|
||||
<template #header>
|
||||
<div class="rbac-header">
|
||||
<div class="rbac-title-wrap">
|
||||
<span class="rbac-title">权限与角色管理</span>
|
||||
<span class="rbac-desc">为角色分配可访问页面、操作与控件,并为用户指派角色(标准 RBAC)。</span>
|
||||
</div>
|
||||
<el-button :icon="Refresh" :loading="loading" @click="reloadAll">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</ConfigPageBase>
|
||||
|
||||
<el-tabs v-model="activeTab" class="rbac-tabs">
|
||||
<!-- ───────────────────────── 用户 ───────────────────────── -->
|
||||
<el-tab-pane label="用户管理" name="users">
|
||||
<div class="tab-toolbar">
|
||||
<span class="toolbar-hint">共 {{ users.length }} 个用户</span>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateUser">新增用户</el-button>
|
||||
</div>
|
||||
<el-table :data="users" v-loading="loading" size="default" border stripe>
|
||||
<el-table-column label="用户名" prop="username" width="140" />
|
||||
<el-table-column label="显示名" prop="displayName" width="150" />
|
||||
<el-table-column label="角色" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="rid in row.roleIds" :key="rid" size="small" effect="plain" class="mg-tag">{{ roleName(rid) }}</el-tag>
|
||||
<span v-if="!row.roleIds.length" class="muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可用域" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="s in row.scopes" :key="s" size="small" type="info" effect="plain" class="mg-tag">{{ scopeLabel(s) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch :model-value="row.enabled" :loading="row._busy" @change="(v: string | number | boolean) => toggleEnabled(row, Boolean(v))" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEditUser(row)">编辑</el-button>
|
||||
<el-button link type="warning" :icon="Key" @click="openResetPassword(row)">改密</el-button>
|
||||
<el-button link type="danger" :icon="Delete" :disabled="row.id === selfId" @click="removeUser(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ───────────────────────── 角色 ───────────────────────── -->
|
||||
<el-tab-pane label="角色与页面权限" name="roles">
|
||||
<div class="tab-toolbar">
|
||||
<span class="toolbar-hint">共 {{ roles.length }} 个角色</span>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateRole">新增角色</el-button>
|
||||
</div>
|
||||
<el-table :data="roles" v-loading="loading" size="default" border stripe>
|
||||
<el-table-column label="角色名称" width="160">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.name }}</span>
|
||||
<el-tag v-if="row.system" size="small" type="success" effect="plain" class="mg-tag">内置</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="描述" prop="description" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="归属域" width="120">
|
||||
<template #default="{ row }"><el-tag size="small" :type="scopeTag(row.scope)" effect="plain">{{ scopeLabel(row.scope) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可访问页面" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.pages.includes('*')" size="small" type="danger" effect="plain">全部页面</el-tag>
|
||||
<span v-else>{{ row.pages.length }} 个</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEditRole(row)">编辑</el-button>
|
||||
<el-button link type="danger" :icon="Delete" :disabled="row.system" @click="removeRole(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<!-- ───────────────────────── 用户 Dialog ───────────────────────── -->
|
||||
<el-dialog v-model="userDialog" :title="userForm.id ? '编辑用户' : '新增用户'" width="480px" destroy-on-close>
|
||||
<el-form label-width="84px" label-position="right">
|
||||
<el-form-item label="用户名" required>
|
||||
<el-input v-model="userForm.username" :disabled="!!userForm.id" placeholder="登录用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="显示名">
|
||||
<el-input v-model="userForm.displayName" placeholder="界面展示名称(留空则同用户名)" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!userForm.id" label="初始密码" required>
|
||||
<el-input v-model="userForm.password" type="password" show-password placeholder="设置初始登录密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="userForm.roleIds" multiple filterable placeholder="选择一个或多个角色" style="width: 100%">
|
||||
<el-option v-for="r in roles" :key="r.id" :label="`${r.name}(${scopeLabel(r.scope)})`" :value="r.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="userForm.enabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="userDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitUser">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ───────────────────────── 改密 Dialog ───────────────────────── -->
|
||||
<el-dialog v-model="pwdDialog" title="重置密码" width="420px" destroy-on-close>
|
||||
<el-form label-width="84px">
|
||||
<el-form-item label="用户">
|
||||
<span>{{ pwdTarget?.displayName }}({{ pwdTarget?.username }})</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" required>
|
||||
<el-input v-model="newPassword" type="password" show-password placeholder="输入新密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="pwdDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitPassword">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ───────────────────────── 角色 Dialog ───────────────────────── -->
|
||||
<el-dialog v-model="roleDialog" :title="roleForm.id ? '编辑角色' : '新增角色'" width="760px" top="6vh" destroy-on-close>
|
||||
<el-form label-width="92px" label-position="right">
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="角色名称" required>
|
||||
<el-input v-model="roleForm.name" placeholder="如:场景设计员" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="归属域" required>
|
||||
<el-select v-model="roleForm.scope" style="width: 100%" @change="onScopeChange">
|
||||
<el-option v-for="s in catalog?.scopes ?? []" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="roleForm.description" placeholder="角色用途说明" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">可访问页面</el-divider>
|
||||
<el-form-item label="授权方式">
|
||||
<el-switch v-model="wildcardPages" active-text="该域全部页面(含未来新增)" inactive-text="按页面精确勾选" inline-prompt style="--el-switch-on-color: var(--mg-primary, #6366f1)" />
|
||||
</el-form-item>
|
||||
<div v-if="!wildcardPages" class="page-picker">
|
||||
<div v-for="g in groupedPages" :key="g.group" class="page-group">
|
||||
<div class="page-group-head">
|
||||
<span class="page-group-title">{{ g.group }}</span>
|
||||
<span class="page-group-actions">
|
||||
<el-button link type="primary" size="small" @click="selectGroup(g.pages, true)">全选</el-button>
|
||||
<el-button link type="info" size="small" @click="selectGroup(g.pages, false)">清空</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<el-checkbox-group v-model="selectedPages" class="page-checks">
|
||||
<el-checkbox v-for="p in g.pages" :key="p.key" :value="p.key" :label="p.key">{{ p.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<el-empty v-if="!groupedPages.length" description="该域暂无可分配页面" :image-size="60" />
|
||||
</div>
|
||||
<div v-else class="wildcard-hint">已授予「{{ scopeLabel(roleForm.scope) }}」下的全部页面(包括未来新增的页面)。</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="roleDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitRole">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ConfigPageBase from '@/components/ConfigPageBase.vue'
|
||||
import { DEFAULT_AUTH } from '@/mock/data/configs'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Plus, Edit, Delete, Key, Refresh } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
fetchCatalog, fetchRoles, fetchUsers,
|
||||
saveRole, deleteRole, createUser, updateUser, setUserPassword, deleteUser
|
||||
} from '@/api/rbac'
|
||||
import type { RbacRole, RbacUserView, RbacCatalog, PageDef, RbacScope } from '@/types/rbac'
|
||||
import type { WidgetGrant } from '@/types/auth'
|
||||
|
||||
function grantTag(v: string) {
|
||||
switch (v) {
|
||||
case 'hidden': return 'danger'
|
||||
case 'readonly': return 'warning'
|
||||
case 'interactive': return 'success'
|
||||
default: return 'info'
|
||||
const auth = useAuthStore()
|
||||
const selfId = computed(() => auth.user?.id ?? '')
|
||||
|
||||
const activeTab = ref<'users' | 'roles'>('users')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
const catalog = ref<RbacCatalog | null>(null)
|
||||
const roles = ref<RbacRole[]>([])
|
||||
const users = ref<Array<RbacUserView & { _busy?: boolean }>>([])
|
||||
|
||||
function roleName(id: string): string {
|
||||
return roles.value.find((r) => r.id === id)?.name ?? id
|
||||
}
|
||||
function scopeLabel(scope: string): string {
|
||||
return scope === '*' ? '通用' : scope === 'Platform' ? '管理端' : scope === 'RCSMonitor' ? '运营端' : scope
|
||||
}
|
||||
function scopeTag(scope: string): 'success' | 'warning' | 'danger' | 'info' {
|
||||
return scope === '*' ? 'danger' : scope === 'Platform' ? 'warning' : 'success'
|
||||
}
|
||||
|
||||
async function reloadAll() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [cat, rs, us] = await Promise.all([fetchCatalog(), fetchRoles(), fetchUsers()])
|
||||
catalog.value = cat
|
||||
roles.value = rs
|
||||
users.value = us
|
||||
} catch (e) {
|
||||
ElMessage.error(`加载失败:${msg(e)}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────── 用户 ─────────────────────────
|
||||
const userDialog = ref(false)
|
||||
const userForm = reactive<{ id: string | null; username: string; displayName: string; password: string; roleIds: string[]; enabled: boolean }>({
|
||||
id: null, username: '', displayName: '', password: '', roleIds: [], enabled: true
|
||||
})
|
||||
|
||||
function openCreateUser() {
|
||||
Object.assign(userForm, { id: null, username: '', displayName: '', password: '', roleIds: [], enabled: true })
|
||||
userDialog.value = true
|
||||
}
|
||||
function openEditUser(row: RbacUserView) {
|
||||
Object.assign(userForm, { id: row.id, username: row.username, displayName: row.displayName, password: '', roleIds: [...row.roleIds], enabled: row.enabled })
|
||||
userDialog.value = true
|
||||
}
|
||||
async function submitUser() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (userForm.id) {
|
||||
await updateUser(userForm.id, { displayName: userForm.displayName, roleIds: userForm.roleIds, enabled: userForm.enabled })
|
||||
ElMessage.success('用户已更新')
|
||||
} else {
|
||||
await createUser({ username: userForm.username, displayName: userForm.displayName, password: userForm.password, roleIds: userForm.roleIds, enabled: userForm.enabled })
|
||||
ElMessage.success('用户已创建')
|
||||
}
|
||||
userDialog.value = false
|
||||
await reloadAll()
|
||||
} catch (e) {
|
||||
ElMessage.error(msg(e))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
async function toggleEnabled(row: RbacUserView & { _busy?: boolean }, val: boolean) {
|
||||
row._busy = true
|
||||
try {
|
||||
await updateUser(row.id, { enabled: val })
|
||||
row.enabled = val
|
||||
ElMessage.success(val ? '已启用' : '已停用')
|
||||
} catch (e) {
|
||||
ElMessage.error(msg(e))
|
||||
} finally {
|
||||
row._busy = false
|
||||
}
|
||||
}
|
||||
async function removeUser(row: RbacUserView) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除用户「${row.username}」?此操作不可恢复。`, '删除确认', { type: 'warning' })
|
||||
} catch { return }
|
||||
try {
|
||||
await deleteUser(row.id)
|
||||
ElMessage.success('用户已删除')
|
||||
await reloadAll()
|
||||
} catch (e) {
|
||||
ElMessage.error(msg(e))
|
||||
}
|
||||
}
|
||||
|
||||
// 改密
|
||||
const pwdDialog = ref(false)
|
||||
const pwdTarget = ref<RbacUserView | null>(null)
|
||||
const newPassword = ref('')
|
||||
function openResetPassword(row: RbacUserView) {
|
||||
pwdTarget.value = row
|
||||
newPassword.value = ''
|
||||
pwdDialog.value = true
|
||||
}
|
||||
async function submitPassword() {
|
||||
if (!pwdTarget.value) return
|
||||
if (!newPassword.value) { ElMessage.warning('请输入新密码'); return }
|
||||
saving.value = true
|
||||
try {
|
||||
await setUserPassword(pwdTarget.value.id, newPassword.value)
|
||||
ElMessage.success('密码已重置')
|
||||
pwdDialog.value = false
|
||||
} catch (e) {
|
||||
ElMessage.error(msg(e))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────── 角色 ─────────────────────────
|
||||
const roleDialog = ref(false)
|
||||
const roleForm = reactive<{ id: string | null; name: string; description: string; scope: RbacScope }>({
|
||||
id: null, name: '', description: '', scope: 'Platform'
|
||||
})
|
||||
const selectedPages = ref<string[]>([])
|
||||
const wildcardPages = ref(false)
|
||||
// 操作权限 / 控件可见性不在本界面暴露编辑:编辑时记录原值、保存时原样回传,
|
||||
// 避免把内置角色(如运营)已有的运维操作码 / 控件可见性配置清空。
|
||||
const preservedOps = ref<string[]>([])
|
||||
const preservedWidgets = ref<WidgetGrant[]>([])
|
||||
|
||||
const availablePages = computed<PageDef[]>(() =>
|
||||
(catalog.value?.pages ?? []).filter((p) => roleForm.scope === '*' || p.scope === roleForm.scope))
|
||||
|
||||
const groupedPages = computed(() => {
|
||||
const map = new Map<string, PageDef[]>()
|
||||
for (const p of availablePages.value) {
|
||||
if (!map.has(p.group)) map.set(p.group, [])
|
||||
map.get(p.group)!.push(p)
|
||||
}
|
||||
return [...map.entries()].map(([group, pages]) => ({ group, pages }))
|
||||
})
|
||||
|
||||
function openCreateRole() {
|
||||
Object.assign(roleForm, { id: null, name: '', description: '', scope: 'Platform' })
|
||||
selectedPages.value = []
|
||||
wildcardPages.value = false
|
||||
preservedOps.value = []
|
||||
preservedWidgets.value = []
|
||||
roleDialog.value = true
|
||||
}
|
||||
function openEditRole(row: RbacRole) {
|
||||
Object.assign(roleForm, { id: row.id, name: row.name, description: row.description, scope: row.scope })
|
||||
wildcardPages.value = row.pages.includes('*')
|
||||
selectedPages.value = wildcardPages.value ? [] : [...row.pages]
|
||||
preservedOps.value = [...row.ops]
|
||||
preservedWidgets.value = row.widgetGrants.map((g) => ({ ...g }))
|
||||
roleDialog.value = true
|
||||
}
|
||||
function onScopeChange() {
|
||||
// 切换归属域后,剔除不属于新域的已选页面
|
||||
const valid = new Set(availablePages.value.map((p) => p.key))
|
||||
selectedPages.value = selectedPages.value.filter((k) => valid.has(k))
|
||||
}
|
||||
function selectGroup(pages: PageDef[], on: boolean) {
|
||||
const keys = pages.map((p) => p.key)
|
||||
if (on) selectedPages.value = [...new Set([...selectedPages.value, ...keys])]
|
||||
else selectedPages.value = selectedPages.value.filter((k) => !keys.includes(k))
|
||||
}
|
||||
async function submitRole() {
|
||||
if (!roleForm.name.trim()) { ElMessage.warning('请填写角色名称'); return }
|
||||
saving.value = true
|
||||
try {
|
||||
await saveRole({
|
||||
id: roleForm.id ?? undefined,
|
||||
name: roleForm.name,
|
||||
description: roleForm.description,
|
||||
scope: roleForm.scope,
|
||||
pages: wildcardPages.value ? ['*'] : selectedPages.value,
|
||||
ops: preservedOps.value,
|
||||
widgetGrants: preservedWidgets.value
|
||||
})
|
||||
ElMessage.success(roleForm.id ? '角色已更新' : '角色已创建')
|
||||
roleDialog.value = false
|
||||
await reloadAll()
|
||||
} catch (e) {
|
||||
ElMessage.error(msg(e))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
async function removeRole(row: RbacRole) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除角色「${row.name}」?`, '删除确认', { type: 'warning' })
|
||||
} catch { return }
|
||||
try {
|
||||
await deleteRole(row.id)
|
||||
ElMessage.success('角色已删除')
|
||||
await reloadAll()
|
||||
} catch (e) {
|
||||
ElMessage.error(msg(e))
|
||||
}
|
||||
}
|
||||
|
||||
function msg(e: unknown): string {
|
||||
return e instanceof Error ? e.message : String(e)
|
||||
}
|
||||
|
||||
onMounted(reloadAll)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rbac-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
||||
.rbac-title-wrap { display: flex; flex-direction: column; gap: 4px; }
|
||||
.rbac-title { font-weight: 600; font-size: 16px; color: var(--mg-text-light); }
|
||||
.rbac-desc { font-size: 12.5px; color: var(--mg-text-muted); }
|
||||
.tab-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||||
.toolbar-hint { font-size: 13px; color: var(--mg-text-muted); }
|
||||
.mg-tag { margin: 2px 4px 2px 0; }
|
||||
.muted { color: var(--mg-text-faint); }
|
||||
|
||||
.page-picker { display: flex; flex-direction: column; gap: 14px; max-height: 320px; overflow-y: auto; padding: 4px 2px; }
|
||||
.page-group-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
|
||||
.page-group-title { font-weight: 600; font-size: 13px; color: var(--mg-accent, #c4b5fd); }
|
||||
.page-checks { display: flex; flex-wrap: wrap; gap: 4px 18px; }
|
||||
.wildcard-hint { font-size: 13px; color: var(--mg-status-warning, #f59e0b); padding: 4px 0 8px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user