449c6d4875
## 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分块上传
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>
|