Files
wecom_it_smart_desk/frontend-h5/src/components/assistant/RiskTabs.vue
T
Simon 449c6d4875 feat: 2026-07-12~13 全量更新 - AI对话链路改造+H5 v4/v5+坐席端v5+上下文感知诊断+知识库迭代3
## H5 员工端 v4 (2026-07-13 00:48 已部署)
- 人工按钮三态文案统一为"人工坐席"
- 按钮位置移至发送键和语音按钮上方(垂直堆叠)
- 点按钮直接调 store.shakeAgent(),删除 CallAgentModal 弹窗动画
- 截图快捷键提示改为"截图->粘贴:Alt+Shift+A-Ctrl+V ---> Ctrl+V"
- 移动端隐藏截图提示(CSS 媒体查询)
- AI转人工提示改为"已为您呼叫人工坐席,请稍等!"
- 坐席接入提示改为"坐席正在查看您的信息,请等待处理回复!"
- 删除"摇铃呼叫坐席"入口和文案
- 删除孤儿组件 MessageList.vue + shake 动画 CSS

## H5 员工端 v5 (2026-07-13 02:08 已部署)
- RightPanel v2.1:删除"软件安装"和"资源权限"标签页
- 移除标签栏,智能推荐(DynamicRecommend)直接展示
- 删除 SoftwareDownloads/ApprovalLinks 引用和相关 CSS

## AI 对话链路全栈改造 Phase 1-6 (已部署)
- Phase 1: Dify JSON输出 + 后端blocking解析 + 双WS推送 + 错误降级
- Phase 2: 关键词收窄(~25强意图词) + 两级分类Prompt + 删除前端checkApprovalIntent
- Phase 3: WS扩展(ai_thinking+dynamic_recommend) + ai_structured气泡 + RightPanel v2 + 选项回传
- Phase 4: VisionService接入 + 图片消息融合(5秒窗口) + 降级策略
- Phase 5: 坐席端ai_thinking指示器 + ai_structured/byod_card渲染 + handleNewMessage修复
- Phase 6: diagnosis_stage(6值) + response_time_ms计时 + 慢响应告警(>10s)

## 坐席端 v5 (2026-07-13 01:38 已部署)
- ai_structured/byod_card 只读渲染
- AI思考指示器 UI
- handleNewMessage 透传 msg_type/extra_data 修复
- 布局优化v2.0: QuickReplyBar L1+L2悬浮 + ReplyBox左右分区 + 右栏260/560px切换
- 键盘快捷键v2.3: 纯数字路由 + ESC分层撤销 + Shift+Space用event.code

## 上下文感知智能诊断闭环 (2026-07-12 已部署)
- 三层诊断(API→Script→AI) + 三段排队(VIP→info_locked→not locked)
- 答题插队 + 五场景关闭
- 迁移052(6表+6列) + queue_service + quiz_service + closing_service
- H5前端: QueueWaiting + RightPanel双Tab + InputBar三态 + ResolveConfirmCard
- 坐席前端: pending_close结单流程 + 信息锁定(Dify步骤完成+有效回答率≥70%)

## 知识库迭代3 (2026-07-12 已部署)
- 分诊交互(H5+坐席+Dify独立应用)
- 拓扑预览(ECharts只读)
- 代答排除(4种匹配器: keyword/regex/intent/category)
- 迁移051 + 44文件43测试通过

## 后端变更
- 6个Python文件改造(h5_ai_task.py/h5.py/ai_service.py/closing_service.py等)
- funny_phrase_service.py: shake/connected/keyword 默认文案更新
- session_service.py: 企微消息文案同步
- 新增: queue.py/quiz.py/triage.py/exclusion_rules.py 等API端点
- 新增: diagnostic.py/quiz.py/triage_session.py 等模型
- 新增: closing_service/queue_service/quiz_service/triage_service 等服务

## 文档更新
- CHANGELOG.md: 新增 [未发布] 区全部变更记录
- 项目管理主文档 v2.5: 新增v0.7.3版本 + 已完成看板 + 最近搞定
- 版本记录: 新增v0.7.3条目
- AI对话链路实施计划: Phase 1-6 全部标记已实施
- 新增架构图/时序图/类图(mermaid)

## 部署路径修正
- 服务器项目根路径: /opt/wecom-it-desk/
- 所有前端dist均为ro bind mount,只能在宿主机源路径操作
- 服务器nginx /h5/ 是静态文件服务(非proxy_pass)
- elFinder上传二进制不可靠(MD5不匹配),改用base64分块上传
2026-07-13 02:17:03 +08:00

513 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
=============================================================================
// 企微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>