513 lines
13 KiB
Vue
513 lines
13 KiB
Vue
|
|
<!--
|
|||
|
|
=============================================================================
|
|||
|
|
// 企微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>
|