权限监控与管理界面全面美化
重新设计权限系统所有 UI 组件的视觉风格,统一配色、圆角、阴影和交互动效: - 实时概览:统计卡片加图标和渐变色条,健康评分改为环形进度,告警改为卡片式 - 趋势分析:图表卡片加彩色图标标识,ECharts 配色升级为渐变面积填充 - 访问日志:指标卡片带图标,日志审计改为卡片式入口,IP排行前三高亮 - IP属地:工具栏重设计,排行列表前三渐变高亮,指标卡片统一新风格 - 系统级权限:从 el-table 改为自定义卡片列表,模块块独立圆角卡片 - 项目权限配置:空状态引导优化,成员表格加头像,工具栏加背景容器 - 角色概览卡片:加进度条可视化,hover 微动效 - 接口权限矩阵:工具栏分离布局,表格圆角包裹 - 角色管理抽屉:侧边栏选中态渐变,操作行 hover 高亮 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,61 +1,62 @@
|
||||
<template>
|
||||
<div class="template-selector">
|
||||
<div class="selector-header">
|
||||
<h3>角色权限概览</h3>
|
||||
<p class="selector-desc">展示当前项目各角色的权限配置状态</p>
|
||||
<div class="selector-title-group">
|
||||
<h3>角色权限概览</h3>
|
||||
<p class="selector-desc">展示当前项目各角色的权限配置状态</p>
|
||||
</div>
|
||||
<el-tag effect="plain" size="small" type="info" round>{{ templates.length }} 个角色</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="template-grid">
|
||||
<el-tooltip
|
||||
<div
|
||||
v-for="template in templates"
|
||||
:key="template.id"
|
||||
:content="template.description"
|
||||
placement="top"
|
||||
:disabled="!template.description"
|
||||
>
|
||||
<div
|
||||
class="template-card"
|
||||
:class="{ inactive: template.category && !isRoleActive(template.category) }"
|
||||
@click="template.category && emit('edit-role', template.category!)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="card-top">
|
||||
<span class="card-icon">{{ roleIcon(template.category) }}</span>
|
||||
<div class="card-title-wrap">
|
||||
<span class="card-name">{{ template.category ? (ROLE_LABELS[template.category] || template.name) : template.name }}</span>
|
||||
<el-tag v-if="!template.is_system" size="small" type="success">自定义</el-tag>
|
||||
<span v-if="template.description" class="card-desc">{{ template.description }}</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="template.category"
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
class="edit-role-btn"
|
||||
@click.stop="emit('edit-role', template.category!)"
|
||||
>编辑权限</el-button>
|
||||
<el-tag v-if="!template.is_system" size="small" effect="dark" round type="success" class="card-badge">自定义</el-tag>
|
||||
</div>
|
||||
<div class="card-stats">
|
||||
<template v-if="props.currentPermissions && template.category && props.currentPermissions[template.category]">
|
||||
<span class="stat">
|
||||
<el-icon><Check /></el-icon>
|
||||
{{ countCurrentEnabled(template.category) }} 项已启用
|
||||
</span>
|
||||
<span class="stat disabled">
|
||||
<el-icon><Close /></el-icon>
|
||||
{{ countCurrentDisabled(template.category) }} 项已禁用
|
||||
</span>
|
||||
<div class="stat-bar">
|
||||
<div class="stat-fill" :style="{ width: enabledPercent(template.category) + '%' }" />
|
||||
</div>
|
||||
<div class="stat-numbers">
|
||||
<span class="stat enabled">
|
||||
<span class="stat-dot green" />
|
||||
{{ countCurrentEnabled(template.category) }} 启用
|
||||
</span>
|
||||
<span class="stat disabled">
|
||||
<span class="stat-dot red" />
|
||||
{{ countCurrentDisabled(template.category) }} 禁用
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="stat">
|
||||
<el-icon><Check /></el-icon>
|
||||
{{ countEnabled(template) }} 项已启用
|
||||
</span>
|
||||
<span class="stat disabled">
|
||||
<el-icon><Close /></el-icon>
|
||||
{{ countDisabled(template) }} 项已禁用
|
||||
</span>
|
||||
<div class="stat-bar">
|
||||
<div class="stat-fill" :style="{ width: templateEnabledPercent(template) + '%' }" />
|
||||
</div>
|
||||
<div class="stat-numbers">
|
||||
<span class="stat enabled">
|
||||
<span class="stat-dot green" />
|
||||
{{ countEnabled(template) }} 启用
|
||||
</span>
|
||||
<span class="stat disabled">
|
||||
<span class="stat-dot red" />
|
||||
{{ countDisabled(template) }} 禁用
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -63,7 +64,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Check, Close } from "@element-plus/icons-vue";
|
||||
import {
|
||||
fetchPermissionTemplates,
|
||||
type PermissionTemplate,
|
||||
@@ -110,6 +110,12 @@ const countDisabled = (t: PermissionTemplate) => {
|
||||
return n;
|
||||
};
|
||||
|
||||
const templateEnabledPercent = (t: PermissionTemplate) => {
|
||||
const enabled = countEnabled(t);
|
||||
const total = enabled + countDisabled(t);
|
||||
return total > 0 ? Math.round((enabled / total) * 100) : 0;
|
||||
};
|
||||
|
||||
const countCurrentEnabled = (role: string) => {
|
||||
const perms = props.currentPermissions?.[role];
|
||||
if (!perms) return 0;
|
||||
@@ -122,6 +128,12 @@ const countCurrentDisabled = (role: string) => {
|
||||
return Object.values(perms).filter((v) => !v).length;
|
||||
};
|
||||
|
||||
const enabledPercent = (role: string) => {
|
||||
const enabled = countCurrentEnabled(role);
|
||||
const total = enabled + countCurrentDisabled(role);
|
||||
return total > 0 ? Math.round((enabled / total) * 100) : 0;
|
||||
};
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
const res = await fetchPermissionTemplates();
|
||||
@@ -136,117 +148,155 @@ onMounted(loadTemplates);
|
||||
|
||||
<style scoped>
|
||||
.template-selector {
|
||||
padding: 16px 0;
|
||||
padding: 8px 0 12px;
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.selector-header h3 {
|
||||
.selector-title-group h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
color: #1a2332;
|
||||
}
|
||||
|
||||
.selector-desc {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
align-items: start;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
padding: 14px;
|
||||
border: 1.5px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
border-color: #93c5fd;
|
||||
box-shadow: 0 4px 14px rgba(59, 130, 246, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.template-card.inactive {
|
||||
border-style: dashed;
|
||||
background: #fafafa;
|
||||
opacity: 0.75;
|
||||
border-color: #e2e8f0;
|
||||
background: #fafbfc;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
border-color: #c0c4cc;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
.template-card.inactive:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.template-card:not(.inactive):hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-title-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
color: #1a2332;
|
||||
}
|
||||
|
||||
.template-card.inactive .card-name {
|
||||
color: #909399;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.edit-role-btn {
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
.card-desc {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.template-card:hover .edit-role-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
.card-badge {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-stats {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: #67c23a;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.template-card.inactive .card-stats {
|
||||
color: #b0b0b0;
|
||||
.stat-bar {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: #f1f5f9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-stats .stat {
|
||||
.stat-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(90deg, #10b981, #34d399);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-numbers {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.card-stats .stat.disabled {
|
||||
color: #f56c6c;
|
||||
.stat-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.template-card.inactive .card-stats .stat.disabled {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.stat-dot.green { background: #10b981; }
|
||||
.stat-dot.red { background: #ef4444; }
|
||||
|
||||
.template-card.inactive .stat-dot.green { background: #94a3b8; }
|
||||
.template-card.inactive .stat-dot.red { background: #cbd5e1; }
|
||||
.template-card.inactive .stat-fill { background: #cbd5e1; }
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user