Files
wecom_it_smart_desk/docs/01-产品文档/08-集成生态/原型-REQ-集成-002-邀请流程-v1.0.html
T
Simon 44e77dcb0e chore(docs): docs/ 目录全面重新编号 + 重组
**重构前**(旧编号 02-11):
- docs/02-产品需求/      → 00 产品规划/PRD
- docs/03-技术架构/      → 01-05 子目录散落
- docs/04-原型设计/      → 01-02 产品设计(HTML 原型)
- docs/05-原型设计/      → screens/
- docs/06-测试素材/      → 02-E2E / 03-功能 / 04-版本测试
- docs/07-项目管理/      → 任务说明书/日报/计划
- docs/08-安全审计/      → 审计报告
- docs/09-堡垒运维/      → toolbox / deploy
- docs/10-项目管理/      → 任务说明书(重复)
- docs/11-历史归档/      → deploy-nas-archived

**重构后**(新编号 00-07,语义化):
- docs/00-产品开发流程与文档管理规范.md
- docs/00-版本迭代总览.md
- docs/01-产品文档/      (PRD/原型/认证/会话/AI 服务/坐席/集成)
- docs/02-技术文档/      (技术方案/架构图/重构记录/前端改造/实现配置)
- docs/03-测试文档/      (E2E/功能用例/版本报告/缺陷单)
- docs/04-运维文档/      (部署运维/运维指南)
- docs/05-运营文档/      (品牌推广/用户手册)
- docs/06-安全审计/      (审计报告)
- docs/07-项目管理/      (任务说明书/日报/计划/看板)

**净收益**:
- 目录编号与产品文档管理规范对齐(按文档阶段 01-07 编号)
- 消除 02-产品需求 与 10-项目管理 的编号重叠
- 子目录按文档类型分组(如 01-产品文档/00-产品规划、01-产品文档/01-认证与登录)
- 把运维/安全/项目管理从 0X 散落改为 04/06/07

合计 494 文件 + 78495 行 / - 14076 行
2026-08-03 18:46:55 +08:00

747 lines
46 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方案三:邀请功能原型 — 交互流程</title>
<style>
:root {
--bg: #0f172a;
--bg2: #1e293b;
--bg3: #334155;
--border: #475569;
--text: #f1f5f9;
--dim: #94a3b8;
--accent: #3b82f6;
--accent-l: #60a5fa;
--green: #22c55e;
--amber: #f59e0b;
--red: #ef4444;
--purple: #a855f7;
--cyan: #06b6d4;
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
.app{max-width:1200px;margin:0 auto;padding:20px}
h1{font-size:1.8em;font-weight:700;background:linear-gradient(135deg,var(--accent-l),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-align:center;margin-bottom:8px}
.subtitle{text-align:center;color:var(--dim);margin-bottom:32px;font-size:0.95em}
h2{font-size:1.2em;font-weight:600;color:var(--accent-l);margin:24px 0 12px;padding-left:12px;border-left:3px solid var(--accent)}
.flow-steps{display:flex;gap:8px;margin-bottom:32px;overflow-x:auto;padding:8px 0}
.step{flex-shrink:0;padding:10px 16px;border-radius:10px;background:var(--bg2);border:1px solid var(--border);cursor:pointer;transition:all .2s;font-size:0.88em;white-space:nowrap}
.step:hover{border-color:var(--accent);background:#1e3a5f}
.step.active{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
.step .step-num{display:inline-block;width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.15);text-align:center;line-height:20px;font-size:0.75em;margin-right:6px}
.step.active .step-num{background:rgba(255,255,255,0.3)}
.scene{display:none}
.scene.show{display:block}
.card{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}
.card h3{color:var(--accent-l);margin-bottom:10px;font-size:1em}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
@media(max-width:768px){.row2{grid-template-columns:1fr}}
.mockup{background:#111827;border-radius:12px;border:1px solid var(--border);overflow:hidden;position:relative}
.mockup-bar{height:32px;background:var(--bg3);display:flex;align-items:center;padding:0 12px;gap:6px;border-bottom:1px solid var(--border)}
.mockup-dot{width:10px;height:10px;border-radius:50%}
.mockup-body{padding:16px;min-height:400px;position:relative}
.tag{display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.78em;font-weight:600}
.tag-blue{background:rgba(59,130,246,0.2);color:var(--accent-l)}
.tag-green{background:rgba(34,197,94,0.2);color:var(--green)}
.tag-amber{background:rgba(245,158,11,0.2);color:var(--amber)}
table{width:100%;border-collapse:collapse;font-size:0.88em}
th,td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--border)}
th{background:var(--bg3);color:var(--accent-l);font-weight:600;font-size:0.85em}
.avatar{display:inline-flex;width:32px;height:32px;border-radius:50%;align-items:center;justify-content:center;font-size:0.8em;font-weight:600;flex-shrink:0}
.av-user{background:#3b82f6;color:#fff}
.av-agent{background:#22c55e;color:#fff}
.av-invited{background:#f59e0b;color:#fff}
.msg{display:flex;gap:8px;margin-bottom:10px;align-items:flex-start}
.msg-left{flex-direction:row}
.msg-right{flex-direction:row-reverse}
.msg-bubble{max-width:70%;padding:10px 14px;border-radius:12px;font-size:0.88em;line-height:1.5}
.bubble-user{background:#1e3a5f;color:var(--text)}
.bubble-agent{background:#1a3d2e;color:var(--text)}
.bubble-sys{background:var(--bg3);color:var(--dim);font-size:0.82em;max-width:100%;text-align:center;padding:6px 12px;border-radius:8px}
.bubble-invited{background:#3d2e1a;color:var(--text)}
.btn{padding:8px 16px;border-radius:8px;border:1px solid var(--border);background:var(--bg3);color:var(--text);cursor:pointer;font-size:0.85em;transition:all .15s;display:inline-flex;align-items:center;gap:6px}
.btn:hover{border-color:var(--accent);background:#1e3a5f}
.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-primary:hover{background:#2563eb}
.tree{padding:4px 0}
.tree-item{display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:0.85em;transition:background .15s}
.tree-item:hover{background:var(--bg3)}
.tree-item.selected{background:rgba(59,130,246,0.15);outline:1px solid var(--accent)}
.tree-expand{width:16px;text-align:center;color:var(--dim);font-size:0.8em;cursor:pointer}
.tree-ck{width:16px;height:16px;border:1px solid var(--border);border-radius:3px;display:inline-flex;align-items:center;justify-content:center;font-size:0.7em;cursor:pointer;flex-shrink:0}
.tree-ck.checked{background:var(--accent);border-color:var(--accent);color:#fff}
.desc{color:var(--dim);font-size:0.9em;margin:8px 0;line-height:1.7}
.desc strong{color:var(--text)}
.callout{background:rgba(59,130,246,0.1);border-left:3px solid var(--accent);border-radius:0 8px 8px 0;padding:12px 16px;margin:12px 0;font-size:0.9em}
.callout.warn{background:rgba(245,158,11,0.1);border-left-color:var(--amber)}
.callout.ok{background:rgba(34,197,94,0.1);border-left-color:var(--green)}
.arrow{color:var(--accent);font-size:1.2em;font-weight:700;margin:0 6px}
.seq{display:flex;flex-direction:column;gap:0;margin:16px 0}
.seq-row{display:flex;align-items:center;gap:12px;padding:8px 0}
.seq-line{width:2px;height:20px;background:var(--accent);margin-left:15px}
.seq-dot{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.8em;font-weight:700;flex-shrink:0}
.seq-dot-blue{background:var(--accent);color:#fff}
.seq-dot-green{background:var(--green);color:#fff}
.seq-dot-amber{background:var(--amber);color:#fff}
.seq-dot-purple{background:var(--purple);color:#fff}
.seq-text{flex:1;font-size:0.88em;color:var(--dim)}
.seq-text strong{color:var(--text)}
.seq-actor{font-size:0.75em;color:var(--accent-l);font-weight:600}
.wecom-notif{background:var(--bg3);border-radius:10px;padding:14px;border:1px solid var(--border);max-width:340px;margin:12px auto}
.wecom-notif-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.wecom-notif-app{font-size:0.8em;color:var(--accent-l);font-weight:600}
.wecom-notif-time{font-size:0.7em;color:var(--dim)}
.wecom-notif-title{font-weight:600;font-size:0.92em;margin-bottom:4px}
.wecom-notif-body{font-size:0.85em;color:var(--dim);line-height:1.5}
.wecom-notif-btn{display:inline-block;margin-top:8px;padding:6px 14px;border-radius:6px;background:var(--accent);color:#fff;font-size:0.82em;cursor:pointer}
.compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media(max-width:768px){.compare-grid{grid-template-columns:1fr}}
.compare-card{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:16px}
.compare-card.recommend{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
.compare-card h3{text-align:center;margin-bottom:12px;font-size:1em}
.compare-card ul{padding-left:16px;font-size:0.88em}
.compare-card li{margin-bottom:4px;color:var(--dim)}
.badge{display:inline-block;padding:2px 10px;border-radius:12px;font-size:0.75em;font-weight:600}
.badge-blue{background:rgba(59,130,246,0.2);color:var(--accent-l)}
.badge-green{background:rgba(34,197,94,0.2);color:var(--green)}
.badge-red{background:rgba(239,68,68,0.2);color:var(--red)}
</style>
</head>
<body>
<div class="app">
<h1>邀请功能 — 方案三交互原型</h1>
<div class="subtitle">WebSocket + 应用消息双通道架构 | 在现有一对一对话上扩展多人参与</div>
<div class="flow-steps" id="flowSteps">
<div class="step active" data-scene="s0">0. 架构概览</div>
<div class="step" data-scene="s1">1. 一对一会话中</div>
<div class="step" data-scene="s2">2. 点击邀请</div>
<div class="step" data-scene="s3">3. 选人界面</div>
<div class="step" data-scene="s4">4. 历史共享</div>
<div class="step" data-scene="s5">5. 确认邀请</div>
<div class="step" data-scene="s6">6. 被邀请方通知</div>
<div class="step" data-scene="s7">7. 加入会话</div>
<div class="step" data-scene="s8">8. 多人会话</div>
</div>
<!-- ===== Scene 0: 架构概览 ===== -->
<div class="scene show" id="s0">
<div class="card">
<h3>方案三核心架构</h3>
<p class="desc">在现有 <strong>WebSocket 双通道</strong> 架构上扩展,不引入群聊概念。后端维护 <strong>conversation.participants</strong> 数组,所有参与者共享同一个 WebSocket 会话通道。</p>
</div>
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.8em;margin-left:8px">架构流程图</span></div>
<div class="mockup-body" style="display:flex;align-items:center;justify-content:center;min-height:300px">
<svg viewBox="0 0 700 260" style="max-width:100%">
<defs>
<marker id="arrowBlue" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#3b82f6"/></marker>
<marker id="arrowGreen" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#22c55e"/></marker>
<marker id="arrowAmber" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f59e0b"/></marker>
</defs>
<rect x="20" y="80" width="120" height="100" rx="12" fill="#1e3a5f" stroke="#3b82f6" stroke-width="1"/>
<text x="80" y="118" text-anchor="middle" fill="#60a5fa" font-size="13" font-weight="600">H5 员工端</text>
<text x="80" y="138" text-anchor="middle" fill="#94a3b8" font-size="10">发起者</text>
<text x="80" y="155" text-anchor="middle" fill="#94a3b8" font-size="10">WebSocket</text>
<rect x="290" y="40" width="120" height="80" rx="12" fill="#1a3d2e" stroke="#22c55e" stroke-width="1"/>
<text x="350" y="72" text-anchor="middle" fill="#4ade80" font-size="13" font-weight="600">后端</text>
<text x="350" y="92" text-anchor="middle" fill="#94a3b8" font-size="10">FastAPI + Redis</text>
<text x="350" y="107" text-anchor="middle" fill="#94a3b8" font-size="10">participants[]</text>
<rect x="290" y="160" width="120" height="80" rx="12" fill="#3d2e1a" stroke="#f59e0b" stroke-width="1"/>
<text x="350" y="192" text-anchor="middle" fill="#fbbf24" font-size="13" font-weight="600">企微 API</text>
<text x="350" y="210" text-anchor="middle" fill="#94a3b8" font-size="10">应用消息推送</text>
<text x="350" y="225" text-anchor="middle" fill="#94a3b8" font-size="10">通讯录</text>
<rect x="560" y="30" width="120" height="80" rx="12" fill="#1a3d2e" stroke="#22c55e" stroke-width="1"/>
<text x="620" y="62" text-anchor="middle" fill="#4ade80" font-size="13" font-weight="600">坐席工作台</text>
<text x="620" y="82" text-anchor="middle" fill="#94a3b8" font-size="10">WebSocket</text>
<text x="620" y="97" text-anchor="middle" fill="#94a3b8" font-size="10">接单+邀请</text>
<rect x="560" y="150" width="120" height="80" rx="12" fill="#1e3a5f" stroke="#3b82f6" stroke-width="1"/>
<text x="620" y="182" text-anchor="middle" fill="#60a5fa" font-size="13" font-weight="600">H5 被邀请人</text>
<text x="620" y="202" text-anchor="middle" fill="#94a3b8" font-size="10">企微消息→H5</text>
<text x="620" y="217" text-anchor="middle" fill="#94a3b8" font-size="10">WebSocket</text>
<line x1="140" y1="110" x2="290" y2="80" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#arrowBlue)"/>
<line x1="410" y1="80" x2="560" y2="70" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrowGreen)"/>
<line x1="140" y1="140" x2="290" y2="80" stroke="#3b82f6" stroke-width="1.5" stroke-dasharray="4 3"/>
<line x1="350" y1="120" x2="350" y2="160" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arrowAmber)"/>
<line x1="410" y1="200" x2="560" y2="190" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#arrowBlue)"/>
<text x="200" y="80" fill="#94a3b8" font-size="9">WS 双向</text>
<text x="470" y="65" fill="#94a3b8" font-size="9">WS 双向</text>
<text x="370" y="148" fill="#94a3b8" font-size="9">邀请通知</text>
<text x="470" y="185" fill="#94a3b8" font-size="9">应用消息→H5→WS</text>
</svg>
</div>
</div>
<div class="callout ok"><strong>关键区别</strong>:不创建企微群聊,所有参与者通过 H5+WebSocket 接入同一个后端 conversation。企微应用消息仅用于「通知被邀请人」和「断连降级推送」。</div>
</div>
<!-- ===== Scene 1: 一对一会话中 ===== -->
<div class="scene" id="s1">
<div class="card">
<h3>场景:坐席正在与用户一对一对话</h3>
<p class="desc">用户「张三」遇到 VPN 问题,坐席「小李」正在处理。对话过程中发现需要网络安全组同事「王工」协助排查。</p>
</div>
<div class="row2">
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">坐席工作台 — 会话区</span></div>
<div class="mockup-body">
<div style="display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--border);margin-bottom:12px">
<div class="avatar av-user"></div>
<div style="flex:1">
<div style="font-weight:600;font-size:0.92em">张三</div>
<div style="font-size:0.78em;color:var(--dim)">IT研发部 · 高级开发工程师</div>
</div>
<span class="tag tag-green">会话中</span>
<button class="btn" style="font-size:0.8em" onclick="goScene('s2')">+ 邀请</button>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-user">VPN 连接不上,提示证书错误</div>
</div>
<div class="msg msg-right">
<div class="avatar av-agent" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-agent">您好,请问您使用的是 ATrust 还是零信任客户端?</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-user">ATrust,之前一直正常的,今天突然不行了</div>
</div>
<div class="msg msg-right">
<div class="avatar av-agent" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-agent">这个问题可能涉及网络策略变更,我邀请网络安全组的同事一起排查</div>
</div>
<div style="text-align:center;margin:16px 0">
<button class="btn-primary btn" onclick="goScene('s2')">点击「+ 邀请」按钮</button>
</div>
</div>
</div>
<div>
<div class="card">
<h3>交互说明</h3>
<div class="seq">
<div class="seq-row"><div class="seq-dot seq-dot-green">1</div><div class="seq-text"><strong>坐席点击「+ 邀请」</strong><br><span class="seq-actor">坐席工作台</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-blue">2</div><div class="seq-text"><strong>弹出选人弹窗</strong>(组织架构树/搜索)<br><span class="seq-actor">坐席工作台</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-amber">3</div><div class="seq-text"><strong>选择历史消息共享范围</strong><br><span class="seq-actor">选人弹窗</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-blue">4</div><div class="seq-text"><strong>确认邀请 → 后端处理</strong><br><span class="seq-actor">后端 API</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-amber">5</div><div class="seq-text"><strong>企微应用消息通知被邀请人</strong><br><span class="seq-actor">企微 API</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-green">6</div><div class="seq-text"><strong>被邀请人打开 H5 加入会话</strong><br><span class="seq-actor">H5 员工端</span></div></div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== Scene 2: 点击邀请 ===== -->
<div class="scene" id="s2">
<div class="card">
<h3>步骤 2:点击「+ 邀请」→ 弹出选人弹窗</h3>
<p class="desc">弹窗包含三个区域:<strong>组织架构树</strong>(左侧)、<strong>已选人员</strong>(右侧上方)、<strong>历史消息共享设置</strong>(右侧下方)。</p>
</div>
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">坐席工作台 — 邀请弹窗</span></div>
<div class="mockup-body" style="min-height:440px">
<div style="display:flex;gap:16px;height:100%">
<!-- 左侧:组织架构 -->
<div style="flex:1;border-right:1px solid var(--border);padding-right:12px">
<div style="font-weight:600;font-size:0.9em;margin-bottom:8px;color:var(--accent-l)">选择邀请对象</div>
<input type="text" placeholder="搜索姓名/部门..." style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:0.85em;margin-bottom:10px;outline:none">
<div class="tree">
<div class="tree-item" onclick="toggleTree(this)">
<div class="tree-expand"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>信息技术部</span>
</div>
<div style="padding-left:20px">
<div class="tree-item" onclick="toggleTree(this)">
<div class="tree-expand"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>IT支持组</span>
</div>
<div class="tree-item selected">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck checked" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>王工</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">网络安全</span>
</div>
<div class="tree-item">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>赵磊</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">系统运维</span>
</div>
</div>
<div style="padding-left:20px">
<div class="tree-item">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>软件开发组</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">8人</span>
</div>
</div>
<div class="tree-item" onclick="toggleTree(this)">
<div class="tree-expand"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>信息安全部</span>
</div>
</div>
</div>
<!-- 右侧:已选+设置 -->
<div style="flex:1;padding-left:12px">
<div style="font-weight:600;font-size:0.9em;margin-bottom:8px;color:var(--accent-l)">已选择 (1)</div>
<div style="background:var(--bg3);border-radius:8px;padding:10px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<div class="avatar av-invited" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="flex:1;font-size:0.88em">王工</div>
<span style="font-size:0.75em;color:var(--dim)">信息技术部·网络安全</span>
<span style="cursor:pointer;color:var(--red);font-size:0.85em"></span>
</div>
</div>
<div style="font-weight:600;font-size:0.9em;margin-bottom:8px;color:var(--accent-l)">历史消息共享</div>
<div style="background:var(--bg3);border-radius:8px;padding:12px;margin-bottom:16px">
<div style="font-size:0.85em;color:var(--dim);margin-bottom:8px">被邀请人加入后能看到哪些历史消息?</div>
<label style="display:flex;align-items:center;gap:8px;font-size:0.88em;margin-bottom:6px;cursor:pointer">
<input type="radio" name="share" checked style="accent-color:var(--accent)"> 共享全部历史消息
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:0.88em;margin-bottom:6px;cursor:pointer">
<input type="radio" name="share" style="accent-color:var(--accent)"> 仅共享最近10条消息
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:0.88em;cursor:pointer">
<input type="radio" name="share" style="accent-color:var(--accent)"> 不共享历史消息
</label>
</div>
<div style="font-weight:600;font-size:0.9em;margin-bottom:8px;color:var(--accent-l)">邀请说明(可选)</div>
<textarea placeholder="例如:需要你协助排查VPN证书问题..." style="width:100%;height:56px;padding:8px 12px;border-radius:8px;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:0.85em;resize:none;outline:none"></textarea>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
<button class="btn" onclick="goScene('s1')">取消</button>
<button class="btn btn-primary" onclick="goScene('s5')">确认邀请</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== Scene 3: 选人界面(同Scene2,此处展示另一种交互:选部门) ===== -->
<div class="scene" id="s3">
<div class="card">
<h3>步骤 3:选择人员 — 支持按部门批量邀请</h3>
<p class="desc">勾选部门节点 = 邀请该部门<strong>所有人</strong>。每个被邀请人都会收到独立的应用消息通知。</p>
</div>
<div class="row2">
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">选人 — 勾选整个部门</span></div>
<div class="mockup-body" style="min-height:280px">
<div class="tree">
<div class="tree-item" style="background:rgba(59,130,246,0.1)">
<div class="tree-expand"></div>
<div class="tree-ck checked"></div>
<span style="font-weight:600">信息安全部</span>
<span class="tag tag-blue" style="margin-left:8px">全部邀请</span>
</div>
<div style="padding-left:20px">
<div class="tree-item">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck checked"></div>
<span>陈安全</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">安全主管</span>
</div>
<div class="tree-item">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck checked"></div>
<span>刘防护</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">安全工程师</span>
</div>
</div>
<div class="tree-item">
<div class="tree-expand"></div>
<div class="tree-ck"></div>
<span>IT支持组</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">3人</span>
</div>
</div>
</div>
</div>
<div>
<div class="card">
<h3>选人交互规则</h3>
<table>
<tr><th>操作</th><th>效果</th></tr>
<tr><td>勾选人员</td><td>该人员加入已选列表</td></tr>
<tr><td>勾选部门</td><td>该部门下所有人员加入已选列表</td></tr>
<tr><td>搜索姓名</td><td>模糊匹配,点击结果直接加入已选</td></tr>
<tr><td>取消勾选</td><td>从已选列表移除</td></tr>
</table>
</div>
<div class="callout warn"><strong>注意</strong>:邀请人数建议上限 10 人。超过 10 人时应提示「当前邀请人数较多,建议优先邀请关键人员」。这不是硬限制,而是用户体验优化。</div>
</div>
</div>
</div>
<!-- ===== Scene 4: 历史消息共享 ===== -->
<div class="scene" id="s4">
<div class="card">
<h3>步骤 4:历史消息共享设置</h3>
<p class="desc">邀请时坐席可以选择被邀请人能看到哪些历史消息。这是<strong>隐私保护</strong>的重要设计——避免敏感信息(如员工个人账号问题)被无关人员看到。</p>
</div>
<div class="row2">
<div class="card">
<h3>三种共享模式对比</h3>
<table>
<tr><th>模式</th><th>适用场景</th><th>隐私风险</th></tr>
<tr><td><span class="badge badge-green">共享全部</span></td><td>通用IT问题(VPN/网络/打印),无敏感信息</td><td style="color:var(--green)"></td></tr>
<tr><td><span class="badge badge-blue">最近10条</span></td><td>对话较长,仅需上下文即可理解当前问题</td><td style="color:var(--amber)"></td></tr>
<tr><td><span class="badge badge-red">不共享</span></td><td>涉及员工个人账号/权限等敏感信息</td><td style="color:var(--green)"></td></tr>
</table>
</div>
<div class="card">
<h3>被邀请人视角 — 共享 vs 不共享</h3>
<div style="display:flex;gap:12px">
<div style="flex:1;background:var(--bg3);border-radius:8px;padding:10px">
<div style="font-size:0.82em;font-weight:600;color:var(--green);margin-bottom:6px">共享全部</div>
<div style="font-size:0.82em;color:var(--dim)">
[张三] VPN连不上...<br>
[小李] 请问用的是...<br>
[张三] ATrust...<br>
<span style="color:var(--accent-l)">↑ 可看到完整上下文</span>
</div>
</div>
<div style="flex:1;background:var(--bg3);border-radius:8px;padding:10px">
<div style="font-size:0.82em;font-weight:600;color:var(--red);margin-bottom:6px">不共享</div>
<div style="font-size:0.82em;color:var(--dim)">
<span style="opacity:0.4">[历史消息不可见]</span><br>
── 加入会话 ──<br>
[王工] 我来看看VPN...<br>
<span style="color:var(--amber)">↑ 仅能看到加入后的消息</span>
</div>
</div>
</div>
</div>
</div>
<div class="callout"><strong>默认值</strong>:建议默认选中「最近10条」,在可见性和隐私之间取得平衡。坐席可根据实际情况调整。</div>
</div>
<!-- ===== Scene 5: 确认邀请 ===== -->
<div class="scene" id="s5">
<div class="card">
<h3>步骤 5:确认邀请 → 后端处理流程</h3>
</div>
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">确认邀请 — 后端处理时序</span></div>
<div class="mockup-body" style="min-height:340px">
<div class="seq">
<div class="seq-row">
<div class="seq-dot seq-dot-green">1</div>
<div class="seq-text"><strong>坐席点击「确认邀请」</strong><div class="seq-actor">坐席工作台 → POST /api/conversations/:id/invite</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-blue">2</div>
<div class="seq-text"><strong>后端更新 participants 数组</strong>,将王工 userid 加入 conversation.participants<div class="seq-actor">后端 → PostgreSQL</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-blue">3</div>
<div class="seq-text"><strong>后端生成邀请卡片消息</strong>(含会话ID + H5入口链接 + 邀请说明 + 共享的历史消息摘要)<div class="seq-actor">后端 → 企微应用消息 API</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-amber">4</div>
<div class="seq-text"><strong>企微推送应用消息给王工</strong>(单聊卡片消息,非群聊消息)<div class="seq-actor">企微 → 王工企微客户端</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-green">5</div>
<div class="seq-text"><strong>坐席工作台显示「已邀请王工」系统消息</strong>,会话参与者列表更新<div class="seq-actor">WebSocket → 坐席工作台</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-blue">6</div>
<div class="seq-text"><strong>坐席工作台显示「已邀请王工」系统消息</strong>,会话参与者列表更新<div class="seq-actor">WebSocket → H5 发起者</div></div>
</div>
</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<h3>API 接口设计</h3>
<table>
<tr><th>接口</th><th>方法</th><th>说明</th></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/conversations/:id/invite</td><td>POST</td><td>邀请人员加入会话</td></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/conversations/:id/participants</td><td>GET</td><td>获取会话参与者列表</td></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/conversations/:id/participants/:uid</td><td>DELETE</td><td>移除参与者(坐席/被邀请人退出)</td></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/contacts/departments</td><td>GET</td><td>获取组织架构树(缓存自eHR/企微通讯录)</td></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/contacts/search</td><td>GET</td><td>搜索人员(姓名/工号模糊匹配)</td></tr>
</table>
</div>
</div>
<!-- ===== Scene 6: 被邀请方通知 ===== -->
<div class="scene" id="s6">
<div class="card">
<h3>步骤 6:被邀请人收到企微应用消息</h3>
<p class="desc">王工在企微中收到一条<strong>应用消息卡片</strong>,点击卡片中的按钮即可跳转到 H5 会话页面。</p>
</div>
<div class="row2">
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">企微消息卡片 — 通知样式</span></div>
<div class="mockup-body" style="display:flex;align-items:center;justify-content:center;min-height:340px">
<div class="wecom-notif">
<div class="wecom-notif-header">
<div class="avatar av-invited" style="width:24px;height:24px;font-size:0.65em">IT</div>
<div class="wecom-notif-app">IT智能服务台</div>
<div class="wecom-notif-time">刚刚</div>
</div>
<div class="wecom-notif-title">邀请你加入 IT 支持会话</div>
<div class="wecom-notif-body">
<div style="margin-bottom:6px"><strong>邀请人:</strong>小李(IT支持组)</div>
<div style="margin-bottom:6px"><strong>发起人:</strong>张三(IT研发部)</div>
<div style="margin-bottom:6px"><strong>问题摘要:</strong>VPN证书错误,ATrust客户端无法连接</div>
<div style="margin-bottom:6px"><strong>邀请说明:</strong>需要你协助排查VPN证书问题</div>
</div>
<div style="display:flex;gap:8px;margin-top:10px">
<div class="wecom-notif-btn" onclick="goScene('s7')">加入会话</div>
</div>
</div>
</div>
</div>
<div>
<div class="card">
<h3>消息卡片设计要点</h3>
<ul style="color:var(--dim);font-size:0.88em">
<li><strong style="color:var(--text)">使用交互式卡片消息</strong>textcard 类型),而非纯文本</li>
<li><strong style="color:var(--text)">包含问题摘要</strong>,让被邀请人判断是否需要立即加入</li>
<li><strong style="color:var(--text)">「加入会话」按钮</strong>直接跳转 H5 页面(企微内置浏览器)</li>
<li><strong style="color:var(--text)">如果共享了历史消息</strong>,摘要中自动包含最近 2-3 条关键消息</li>
</ul>
</div>
<div class="card">
<h3>降级方案</h3>
<p class="desc">如果被邀请人<strong>未安装企微</strong><strong>处于离线</strong>:后端记录邀请状态为「待加入」。被邀请人下次登录企微时会收到消息。同时,坐席工作台显示邀请状态:<span class="tag tag-amber">待加入</span> / <span class="tag tag-green">已加入</span></p>
</div>
</div>
</div>
</div>
<!-- ===== Scene 7: 加入会话 ===== -->
<div class="scene" id="s7">
<div class="card">
<h3>步骤 7:被邀请人点击「加入会话」→ H5 页面</h3>
<p class="desc">王工点击消息卡片按钮 → 企微内置浏览器打开 H5 页面 → 自动加入 WebSocket 会话 → 看到历史消息(如果被共享)→ 可直接发消息</p>
</div>
<div class="row2">
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">H5 被邀请人视角 — 加入后</span></div>
<div class="mockup-body">
<div style="display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--border);margin-bottom:12px">
<div style="font-weight:600;font-size:0.92em;flex:1">IT支持会话</div>
<span style="font-size:0.78em;color:var(--dim)">3人参与</span>
</div>
<div class="msg" style="justify-content:center">
<div class="msg-bubble bubble-sys">── 小李 邀请了 王工 加入会话 ──</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-user">VPN 连接不上,提示证书错误</div>
</div>
<div class="msg msg-right">
<div class="avatar av-agent" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-agent">请问您使用的是 ATrust 还是零信任客户端?</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-user">ATrust,之前一直正常的,今天突然不行了</div>
</div>
<div class="msg" style="justify-content:center">
<div class="msg-bubble bubble-sys">── 王工 已加入会话 ──</div>
</div>
<div class="msg msg-right">
<div class="avatar av-invited" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-invited">我来看看,先确认下证书服务状态</div>
</div>
</div>
</div>
<div>
<div class="card">
<h3>加入流程细节</h3>
<div class="seq">
<div class="seq-row"><div class="seq-dot seq-dot-amber">1</div><div class="seq-text"><strong>点击「加入会话」</strong><br>企微内置浏览器打开 H5 URL(含 conversation_id + token</div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-blue">2</div><div class="seq-text"><strong>H5 自动认证</strong><br>企微环境 → OAuth2 自动登录;非企微 → Mock 登录</div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-blue">3</div><div class="seq-text"><strong>建立 WebSocket 连接</strong><br>加入 conversation 的消息通道</div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-green">4</div><div class="seq-text"><strong>拉取历史消息</strong><br>根据共享设置,显示可查看的历史消息</div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-green">5</div><div class="seq-text"><strong>广播「XX 已加入」</strong><br>所有参与者(坐席+用户+其他被邀请人)都能看到</div></div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== Scene 8: 多人会话 ===== -->
<div class="scene" id="s8">
<div class="card">
<h3>步骤 8:多人会话持续进行</h3>
<p class="desc">所有人通过 <strong>WebSocket</strong> 实时通信。坐席工作台和所有 H5 参与者共享同一个消息通道。坐席拥有<strong>管理权限</strong>(可移除参与者、结束会话)。</p>
</div>
<div class="row2">
<!-- 坐席工作台视角 -->
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">坐席工作台 — 多人会话</span></div>
<div class="mockup-body" style="min-height:400px">
<!-- 参与者栏 -->
<div style="display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--border);margin-bottom:12px;flex-wrap:wrap">
<div class="avatar av-user" style="width:32px;height:32px;font-size:0.7em"></div>
<div style="font-size:0.8em">张三<span style="color:var(--dim);margin-left:4px">发起</span></div>
<div class="avatar av-agent" style="width:32px;height:32px;font-size:0.7em"></div>
<div style="font-size:0.8em">小李<span style="color:var(--dim);margin-left:4px">坐席</span></div>
<div class="avatar av-invited" style="width:32px;height:32px;font-size:0.7em"></div>
<div style="font-size:0.8em">王工<span style="color:var(--dim);margin-left:4px">邀请</span></div>
<button class="btn" style="font-size:0.75em;margin-left:auto" onclick="goScene('s2')">+ 邀请</button>
</div>
<!-- 消息 -->
<div class="msg" style="justify-content:center"><div class="msg-bubble bubble-sys">── 小李 邀请了 王工 加入会话 ──</div></div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">VPN 连接不上,提示证书错误</div>
</div>
<div class="msg msg-right">
<div class="avatar av-agent" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">小李(坐席)</div>
<div class="msg-bubble bubble-agent">请问您使用的是 ATrust 还是零信任客户端?</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">ATrust,今天突然不行了</div>
</div>
<div class="msg" style="justify-content:center"><div class="msg-bubble bubble-sys">── 王工 已加入会话 ──</div></div>
<div class="msg msg-right">
<div class="avatar av-invited" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">王工(受邀)</div>
<div class="msg-bubble bubble-invited">证书服务器刚做了更新,我来检查下白名单配置</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">好的,麻烦了</div>
</div>
</div>
</div>
<!-- H5 被邀请人视角 -->
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">H5 被邀请人视角 — 王工</span></div>
<div class="mockup-body" style="min-height:400px">
<div style="display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--border);margin-bottom:12px">
<div style="font-weight:600;font-size:0.92em;flex:1">IT支持会话</div>
<span style="font-size:0.78em;color:var(--dim)">3人</span>
<span class="tag tag-green">已加入</span>
</div>
<!-- 王工只能看到共享后的消息 -->
<div style="font-size:0.78em;color:var(--dim);text-align:center;margin-bottom:8px;padding:6px;background:var(--bg3);border-radius:6px">你可以查看最近10条历史消息</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">ATrust,今天突然不行了</div>
</div>
<div class="msg" style="justify-content:center"><div class="msg-bubble bubble-sys">── 王工 已加入会话 ──</div></div>
<div class="msg msg-right">
<div class="avatar av-invited" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-invited">证书服务器刚做了更新,我来检查下白名单配置</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">好的,麻烦了</div>
</div>
<!-- 输入区 -->
<div style="margin-top:auto;padding-top:12px;border-top:1px solid var(--border)">
<div style="display:flex;gap:8px">
<input type="text" placeholder="输入消息..." style="flex:1;padding:8px 12px;border-radius:8px;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:0.88em;outline:none">
<button class="btn btn-primary" style="font-size:0.85em">发送</button>
</div>
</div>
</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<h3>坐席管理能力</h3>
<div class="row2" style="margin-bottom:0">
<div>
<table>
<tr><th>操作</th><th>权限</th><th>说明</th></tr>
<tr><td>邀请人员</td><td>坐席</td><td>选择人员加入会话</td></tr>
<tr><td>移除参与者</td><td>坐席</td><td>将某人移出会话(不通知)</td></tr>
<tr><td>主动退出</td><td>被邀请人</td><td>被邀请人可自行退出</td></tr>
<tr><td>结束会话</td><td>坐席</td><td>关闭会话,所有人断开</td></tr>
<tr><td>转让坐席</td><td>坐席</td><td>将坐席角色转给其他参与者</td></tr>
</table>
</div>
<div>
<div class="callout warn"><strong>与企微群聊的核心区别</strong><br>1. 会话数据完全在后端控制,不依赖企微群聊 API<br>2. 被邀请人通过 H5 WebSocket 通信,无需加入企微群<br>3. 坐席拥有管理权限,可控制参与者和历史可见性<br>4. 外部联系人(供应商等)也可通过 H5 链接加入</div>
</div>
</div>
</div>
</div>
</div>
<script>
const steps = document.querySelectorAll('.step');
const scenes = document.querySelectorAll('.scene');
function goScene(id) {
steps.forEach(s => s.classList.remove('active'));
scenes.forEach(s => s.classList.remove('show'));
const idx = parseInt(id.replace('s',''));
steps[idx].classList.add('active');
document.getElementById(id).classList.add('show');
window.scrollTo({top: 0, behavior: 'smooth'});
}
steps.forEach(s => {
s.addEventListener('click', () => goScene(s.dataset.scene));
});
</script>
</body>
</html>