权限监控与管理界面全面美化

重新设计权限系统所有 UI 组件的视觉风格,统一配色、圆角、阴影和交互动效:
- 实时概览:统计卡片加图标和渐变色条,健康评分改为环形进度,告警改为卡片式
- 趋势分析:图表卡片加彩色图标标识,ECharts 配色升级为渐变面积填充
- 访问日志:指标卡片带图标,日志审计改为卡片式入口,IP排行前三高亮
- IP属地:工具栏重设计,排行列表前三渐变高亮,指标卡片统一新风格
- 系统级权限:从 el-table 改为自定义卡片列表,模块块独立圆角卡片
- 项目权限配置:空状态引导优化,成员表格加头像,工具栏加背景容器
- 角色概览卡片:加进度条可视化,hover 微动效
- 接口权限矩阵:工具栏分离布局,表格圆角包裹
- 角色管理抽屉:侧边栏选中态渐变,操作行 hover 高亮

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Cheng Zhou
2026-05-20 14:36:35 +08:00
parent e95eeed90e
commit 6cefa620e4
68 changed files with 6927 additions and 984 deletions
@@ -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>