Files
wecom_it_smart_desk/frontend-h5/src/components/assistant/RiskTabs.vue
T

513 lines
13 KiB
Vue
Raw Normal View History

<!--
=============================================================================
// 企微IT智能服务台 — H5 风险标签页
=============================================================================
// 说明:展示员工 IT 风险信息,三 Tab 切换 + 折叠手风琴
// 1. 重要信息:域账号密码到期、零信任账号冻结、税友云盘冻结、税友邮箱状态
// 2. 安全风险:火绒安装、系统补丁、高危软件、病毒状态、内部攻击(接入中)、网络代理(接入中)
// 3. 合规自查:未登记自备电脑、未审批商业软件
//
// 折叠策略:标题+状态摘要(默认折叠,点击展开详情)
// Tab 标题带数字徽章(异常数量)
// 重要信息为员工维度(非设备维度),其他两个为设备维度
//
// 数据来源:Mock 数据,后续对接后端 IT 健康 API
// =============================================================================
-->
<template>
<div class="risk-tabs">
<!-- ====== Tab 切换 ====== -->
<div class="risk-tabs__bar">
<button
v-for="tab in tabs"
:key="tab.key"
class="tab-btn"
:class="{ 'tab-btn--active': activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
<span v-if="tab.badge > 0" class="tab-btn__badge">{{ tab.badge }}</span>
</button>
</div>
<!-- ====== 重要信息 ====== -->
<div v-if="activeTab === 'important'" class="risk-tabs__content">
<div
v-for="item in importantItems"
:key="item.key"
class="risk-item"
:class="`risk-item--${item.level}`"
>
<div class="risk-item__header" @click="toggleItem(item.key)">
<span class="risk-item__icon">{{ item.icon }}</span>
<span class="risk-item__name">{{ item.label }}</span>
<span class="risk-item__status" :class="`risk-item__status--${item.level}`">
{{ item.statusText }}
</span>
<span class="risk-item__toggle">{{ expandedItems.has(item.key) ? '▴' : '▾' }}</span>
</div>
<!-- 展开详情 -->
<div v-if="expandedItems.has(item.key)" class="risk-item__detail">
<div v-for="line in item.detailLines" :key="line" class="detail-line">
{{ line }}
</div>
<button v-if="item.actionText" class="risk-item__action" @click.stop="handleAction(item)">
{{ item.actionText }}
</button>
</div>
</div>
</div>
<!-- ====== 安全风险 ====== -->
<div v-else-if="activeTab === 'security'" class="risk-tabs__content">
<div
v-for="item in securityItems"
:key="item.key"
class="risk-item"
:class="[
`risk-item--${item.level}`,
{ 'risk-item--pending': item.level === 'pending' }
]"
>
<div class="risk-item__header" @click="toggleItem(item.key)">
<span class="risk-item__icon">{{ item.icon }}</span>
<span class="risk-item__name">{{ item.label }}</span>
<span class="risk-item__status" :class="`risk-item__status--${item.level}`">
{{ item.statusText }}
</span>
<span class="risk-item__toggle">{{ expandedItems.has(item.key) ? '▴' : '▾' }}</span>
</div>
<div v-if="expandedItems.has(item.key)" class="risk-item__detail">
<div v-for="line in item.detailLines" :key="line" class="detail-line">
{{ line }}
</div>
<button v-if="item.actionText" class="risk-item__action" @click.stop="handleAction(item)">
{{ item.actionText }}
</button>
</div>
</div>
</div>
<!-- ====== 合规自查 ====== -->
<div v-else-if="activeTab === 'compliance'" class="risk-tabs__content">
<div
v-for="item in complianceItems"
:key="item.key"
class="risk-item"
:class="`risk-item--${item.level}`"
>
<div class="risk-item__header" @click="toggleItem(item.key)">
<span class="risk-item__icon">{{ item.icon }}</span>
<span class="risk-item__name">{{ item.label }}</span>
<span class="risk-item__status" :class="`risk-item__status--${item.level}`">
{{ item.statusText }}
</span>
<span class="risk-item__toggle">{{ expandedItems.has(item.key) ? '▴' : '▾' }}</span>
</div>
<div v-if="expandedItems.has(item.key)" class="risk-item__detail">
<div v-for="line in item.detailLines" :key="line" class="detail-line">
{{ line }}
</div>
<button v-if="item.actionText" class="risk-item__action" @click.stop="handleAction(item)">
{{ item.actionText }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, reactive } from 'vue'
// ── 类型定义 ──
type RiskLevel = 'normal' | 'warning' | 'danger' | 'pending'
interface RiskItem {
key: string
label: string
icon: string
level: RiskLevel // normal/warning/danger/pending
statusText: string // 状态文字(如"5天"、"正常"、"未安装"
detailLines: string[] // 展开后的详情行
actionText: string // 操作按钮文字(空则不显示)
}
// ── Mock 数据 ──
/** 重要信息(员工维度) */
const importantItems = ref<RiskItem[]>([
{
key: 'ad_password',
label: '域账号密码到期',
icon: '🔑',
level: 'warning',
statusText: '5天',
detailLines: [
'到期时间: 2026-07-17 14:30',
'修改方式: Ctrl+Alt+Del → 更改密码',
'密码规则: 至少8位, 含大小写字母+数字+特殊字符',
],
actionText: '查看修改指南',
},
{
key: 'zero_trust',
label: '零信任账号',
icon: '🛡️',
level: 'normal',
statusText: '正常',
detailLines: [
'账号状态: 正常',
'最近登录: 2026-07-12 08:45',
'登录设备: DESKTOP-SIMON01',
],
actionText: '',
},
{
key: 'cloud_disk',
label: '税友云盘',
icon: '☁️',
level: 'normal',
statusText: '正常',
detailLines: [
'状态: 正常',
'已用容量: 12.3GB / 50GB',
'最近同步: 2026-07-12 09:00',
],
actionText: '',
},
{
key: 'email_status',
label: '税友邮箱',
icon: '📧',
level: 'normal',
statusText: '正常',
detailLines: [
'邮箱: songxian@servyou.com.cn',
'密码到期: 2026-08-15',
'容量: 2.1GB / 5GB',
],
actionText: '',
},
])
/** 安全风险(设备维度) */
const securityItems = ref<RiskItem[]>([
{
key: 'huorong',
label: '火绒安装',
icon: '🛡️',
level: 'normal',
statusText: '已安装',
detailLines: [
'版本: v2.4.1',
'病毒库: 2026-07-12',
'最近全盘扫描: 2026-07-01',
],
actionText: '',
},
{
key: 'patches',
label: '系统补丁',
icon: '🔧',
level: 'warning',
statusText: '2个高危',
detailLines: [
'KB5037: Windows 安全更新 (高危)',
'KB5039: .NET Framework 更新 (高危)',
'建议: 尽快安装, 可联系IT协助',
],
actionText: '申请安装补丁',
},
{
key: 'risk_software',
label: '高危软件',
icon: '📦',
level: 'normal',
statusText: '未发现',
detailLines: [
'扫描时间: 2026-07-12 09:00',
'结果: 未发现高危软件',
],
actionText: '',
},
{
key: 'virus',
label: '病毒状态',
icon: '🦠',
level: 'normal',
statusText: '安全',
detailLines: [
'状态: 无病毒事件',
'最近扫描: 2026-07-01 全盘扫描',
'实时防护: 已开启',
],
actionText: '',
},
{
key: 'internal_attack',
label: '内部攻击',
icon: '⚔️',
level: 'pending',
statusText: '接入中',
detailLines: [
'数据源: SIEM/IDS (接入中)',
'预计完成: 2026-Q4',
'当前状态: 暂无数据',
],
actionText: '',
},
{
key: 'network_proxy',
label: '网络代理',
icon: '🌐',
level: 'pending',
statusText: '接入中',
detailLines: [
'数据源: 联软网络准入 (接入中)',
'预计完成: 2026-Q4',
'当前状态: 暂无数据',
],
actionText: '',
},
])
/** 合规自查(设备维度) */
const complianceItems = ref<RiskItem[]>([
{
key: 'byod',
label: '自备电脑',
icon: '💻',
level: 'normal',
statusText: '公司配发',
detailLines: [
'设备类型: 公司配发设备',
'资产编号: SY-PC-00821',
'登记状态: 已登记',
],
actionText: '',
},
{
key: 'unapproved_sw',
label: '未审批商业软件',
icon: '📋',
level: 'warning',
statusText: '1个未审批',
detailLines: [
'软件: Adobe Photoshop 2024 (未审批)',
'发现时间: 2026-07-10',
'建议: 提交软件安装审批申请',
],
actionText: '提交审批申请',
},
])
// ── 状态 ──
const activeTab = ref<'important' | 'security' | 'compliance'>('important')
/** 展开的条目 key 集合 */
const expandedItems = reactive(new Set<string>())
// ── 计算属性 ──
/** 各 Tab 的异常数量 */
const tabs = computed(() => {
const countAbnormal = (items: RiskItem[]) =>
items.filter(i => i.level === 'warning' || i.level === 'danger').length
return [
{ key: 'important' as const, label: '重要信息', badge: countAbnormal(importantItems.value) },
{ key: 'security' as const, label: '安全风险', badge: countAbnormal(securityItems.value) },
{ key: 'compliance' as const, label: '合规自查', badge: countAbnormal(complianceItems.value) },
]
})
// ── 方法 ──
/** 切换条目展开/折叠 */
function toggleItem(key: string) {
if (expandedItems.has(key)) {
expandedItems.delete(key)
} else {
expandedItems.add(key)
}
}
/** 处理操作按钮点击 */
function handleAction(item: RiskItem) {
// TODO: 后续对接具体操作
console.log('[Risk] action:', item.key, item.actionText)
}
</script>
<style scoped>
.risk-tabs {
background: var(--bg-primary, #fff);
border-radius: var(--border-radius-lg, 12px);
border: 1px solid var(--border-color, #e5e7eb);
overflow: hidden;
}
/* ====== Tab 切换 ====== */
.risk-tabs__bar {
display: flex;
gap: 4px;
padding: 6px;
background: var(--bg-tertiary, #f9fafb);
}
.tab-btn {
flex: 1;
padding: 5px 8px;
border: none;
border-radius: var(--border-radius-md, 8px);
font-size: 11px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
background: transparent;
color: var(--text-secondary, #6b7280);
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.tab-btn--active {
background: var(--accent, #07C160);
color: #fff;
}
.tab-btn__badge {
min-width: 16px;
height: 16px;
border-radius: 8px;
background: var(--color-danger, #ef4444);
color: #fff;
font-size: 10px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
}
.tab-btn--active .tab-btn__badge {
background: rgba(255, 255, 255, 0.3);
}
/* ====== 内容区 ====== */
.risk-tabs__content {
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
/* ====== 风险条目 ====== */
.risk-item {
border-radius: var(--border-radius-md, 8px);
border: 1px solid transparent;
overflow: hidden;
}
.risk-item--normal {
background: rgba(34, 197, 94, 0.04);
border-color: rgba(34, 197, 94, 0.12);
}
.risk-item--warning {
background: rgba(245, 158, 11, 0.04);
border-color: rgba(245, 158, 11, 0.12);
}
.risk-item--danger {
background: rgba(239, 68, 68, 0.04);
border-color: rgba(239, 68, 68, 0.12);
}
.risk-item--pending {
background: var(--bg-tertiary, #f9fafb);
border: 1px dashed var(--border-color, #e5e7eb);
opacity: 0.7;
}
/* 条目头部 */
.risk-item__header {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
cursor: pointer;
transition: background 0.2s;
}
.risk-item__header:hover {
background: rgba(0, 0, 0, 0.02);
}
.risk-item__icon {
font-size: 14px;
flex-shrink: 0;
}
.risk-item__name {
flex: 1;
font-size: 12px;
font-weight: 500;
color: var(--text-primary, #1f2937);
}
.risk-item__status {
font-size: 11px;
font-weight: 500;
padding: 1px 6px;
border-radius: 4px;
}
.risk-item__status--normal { background: rgba(34, 197, 94, 0.15); color: #16a34a; }
.risk-item__status--warning { background: rgba(245, 158, 11, 0.15); color: #d97706; }
.risk-item__status--danger { background: rgba(239, 68, 68, 0.15); color: #dc2626; }
.risk-item__status--pending { color: var(--text-tertiary, #9ca3af); font-style: italic; }
.risk-item__toggle {
font-size: 12px;
color: var(--text-tertiary, #9ca3af);
flex-shrink: 0;
}
/* 条目展开详情 */
.risk-item__detail {
padding: 0 10px 8px 30px;
border-top: 1px solid rgba(0, 0, 0, 0.04);
padding-top: 6px;
margin-top: 2px;
}
.detail-line {
font-size: 11px;
color: var(--text-secondary, #6b7280);
line-height: 1.6;
}
.risk-item__action {
margin-top: 6px;
font-size: 11px;
font-weight: 500;
font-family: inherit;
padding: 4px 12px;
border-radius: var(--border-radius-md, 8px);
border: none;
background: var(--accent, #07C160);
color: #fff;
cursor: pointer;
transition: opacity 0.2s;
}
.risk-item__action:hover { opacity: 0.85; }
</style>