Files
wecom_it_smart_desk/docs/01-产品文档/04-坐席工作台/原型-REQ-坐席-003-群聊双模式-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

654 lines
19 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>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, 'PingFang SC', 'Helvetica Neue', sans-serif;
background: #f0f2f5;
min-height: 100vh;
display: flex;
}
/* === 左侧会话列表(模拟) === */
.sidebar {
width: 280px;
background: #fff;
border-right: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
}
.sidebar-header {
height: 56px;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid #f0f0f0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.conv-item {
padding: 12px 20px;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
}
.conv-item:hover { background: #f5f5f5; }
.conv-item.active { background: #e6f7ff; border-left: 3px solid #07C160; }
.conv-avatar {
width: 40px; height: 40px; border-radius: 4px;
background: #ccc; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 16px;
}
.conv-info { flex: 1; min-width: 0; }
.conv-name { font-size: 14px; color: #333; font-weight: 500; }
.conv-preview { font-size: 12px; color: #999; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* === 右侧聊天区域 === */
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background: #f5f5f5;
min-width: 0;
}
.chat-header {
height: 56px;
background: #fff;
border-bottom: 1px solid #e8e8e8;
display: flex;
align-items: center;
padding: 0 20px;
gap: 12px;
}
.chat-title { font-size: 16px; font-weight: 600; color: #333; }
.chat-subtitle { font-size: 12px; color: #999; }
/* === 参与者缩略条 === */
.participant-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 20px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
}
.participant-bar:hover { background: #f0f0f0; }
.bar-avatars {
display: flex;
align-items: center;
}
/* 头像基础样式 */
.avatar {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: 600;
color: #fff;
flex-shrink: 0;
border: 1.5px solid transparent;
background-size: cover;
background-position: center;
}
.avatar + .avatar { margin-left: -4px; }
/* 角色边框色 */
.avatar.role-primary { border-color: #3b82f6; }
.avatar.role-collaborator { border-color: #07C160; }
.avatar.role-owner { border-color: #FF9800; }
.avatar.role-invitee { border-color: transparent; }
/* 自己高亮环 */
.avatar.is-self {
box-shadow: 0 0 0 2px #07C160;
transform: scale(1.1);
z-index: 10;
}
/* 超员 +N */
.avatar-more {
width: 20px;
height: 20px;
border-radius: 50%;
background: #e0e0e0;
color: #666;
font-size: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-left: -4px;
border: 1.5px solid #fff;
flex-shrink: 0;
}
/* 待加入红点 */
.avatar-wrap { position: relative; display: inline-flex; }
.pending-dot {
position: absolute;
top: -1px;
right: -1px;
width: 6px;
height: 6px;
border-radius: 50%;
background: #ee0a24;
border: 1px solid #fff;
}
.bar-label {
font-size: 12px;
color: #666;
margin-left: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.bar-expand-icon {
font-size: 10px;
transition: transform 0.2s;
}
.bar-expand-icon.expanded { transform: rotate(180deg); }
/* === 就地展开面板 === */
.expanded-panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease, opacity 0.2s ease;
opacity: 0;
background: #fff;
border-bottom: 1px solid #e8e8e8;
}
.expanded-panel.show {
max-height: 150px;
overflow-y: auto;
opacity: 1;
}
.expanded-panel-inner {
padding: 8px 20px;
}
/* 参与者列表项 */
.participant-item {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
}
.participant-item:hover { background: #f9f9f9; }
.participant-item .avatar-md {
width: 24px;
height: 24px;
font-size: 11px;
border-width: 1.5px;
}
.participant-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
}
.participant-name {
font-size: 13px;
color: #333;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px;
}
/* 角色标签 */
.role-tag {
font-size: 10px;
padding: 1px 5px;
border-radius: 3px;
font-weight: 500;
}
.role-tag.primary { background: #3b82f6; color: #fff; }
.role-tag.collaborator { background: #07C160; color: #fff; }
.role-tag.owner { background: #FF9800; color: #fff; }
.role-tag.invitee { background: #f0f0f0; color: #666; }
.participant-dept {
font-size: 11px;
color: #999;
}
/* 移除图标 */
.remove-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: #ee0a24;
cursor: pointer;
font-size: 14px;
border-radius: 4px;
opacity: 0;
transition: opacity 0.15s;
}
.participant-item:hover .remove-icon { opacity: 1; }
.remove-icon:hover { background: #fee; }
/* 消息列表区域 */
.message-list {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
min-height: 200px;
}
.message-item {
display: flex;
margin-bottom: 16px;
gap: 10px;
}
.msg-avatar {
width: 32px; height: 32px; border-radius: 4px;
flex-shrink: 0; display: flex; align-items: center; justify-content: center;
font-size: 13px; color: #fff;
}
.msg-content { max-width: 60%; }
.msg-name { font-size: 11px; color: #999; margin-bottom: 2px; }
.msg-bubble {
background: #fff; padding: 8px 12px; border-radius: 0 8px 8px 8px;
font-size: 13px; color: #333; display: inline-block;
}
/* 底部输入区 */
.input-area {
height: 120px;
background: #fff;
border-top: 1px solid #e8e8e8;
padding: 12px 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.input-toolbar {
display: flex;
gap: 8px;
}
.input-toolbar button {
font-size: 12px;
color: #666;
background: #f5f5f5;
border: none;
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
}
.input-textbox {
flex: 1;
height: 60px;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 8px;
font-size: 13px;
resize: none;
outline: none;
}
.input-textbox:focus { border-color: #07C160; }
/* 控制面板 */
.control-panel {
position: fixed;
top: 10px;
right: 10px;
z-index: 200;
background: rgba(0,0,0,0.8);
border-radius: 8px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.control-panel button {
font-size: 12px;
color: #fff;
background: #07C160;
border: none;
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
white-space: nowrap;
}
.control-panel button:hover { opacity: 0.85; }
.control-panel .info {
font-size: 11px;
color: #aaa;
padding: 2px 4px;
}
/* P1: expandedStateMap 按会话ID记忆 */
.panel-section-title {
font-size: 11px;
color: #999;
padding: 4px 0 2px;
font-weight: 500;
}
</style>
</head>
<body>
<div class="control-panel">
<button onclick="toggleExpand()">点击缩略条展开/收起</button>
<button onclick="toggleScene()">切换场景(3人/7人/10人)</button>
<div class="info" id="sceneInfo">场景: 3人</div>
</div>
<!-- 左侧会话列表 -->
<div class="sidebar">
<div class="sidebar-header">会话列表</div>
<div class="conv-item active">
<div class="conv-avatar" style="background:#FF9800"></div>
<div class="conv-info">
<div class="conv-name">李四 · 网络故障</div>
<div class="conv-preview">王五: 我看了一下交换机日志...</div>
</div>
</div>
<div class="conv-item">
<div class="conv-avatar" style="background:#3b82f6"></div>
<div class="conv-info">
<div class="conv-name">陈六 · 电脑蓝屏</div>
<div class="conv-preview">已重启,问题解决</div>
</div>
</div>
<div class="conv-item">
<div class="conv-avatar" style="background:#07C160"></div>
<div class="conv-info">
<div class="conv-name">周九 · 打印机问题</div>
<div class="conv-preview">好的,我来看看</div>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div class="chat-area">
<div class="chat-header">
<span class="chat-title">李四 · 网络故障</span>
<span class="chat-subtitle" id="convStatus">3人参与</span>
</div>
<!-- 参与者缩略条 -->
<div class="participant-bar" onclick="toggleExpand()" id="bar">
<div class="bar-avatars" id="barAvatars"></div>
<span class="bar-label" id="barLabel">
<span id="barText">3位参与者</span>
<span class="bar-expand-icon" id="expandIcon"></span>
</span>
</div>
<!-- 就地展开面板 -->
<div class="expanded-panel" id="panel">
<div class="expanded-panel-inner" id="panelBody"></div>
</div>
<!-- 消息列表 -->
<div class="message-list">
<div class="message-item">
<div class="msg-avatar" style="background:#3b82f6"></div>
<div class="msg-content">
<div class="msg-name">张明(主责坐席)</div>
<div class="msg-bubble">您好,网络问题我来协助处理。已邀请网络组同事加入。</div>
</div>
</div>
<div class="message-item">
<div class="msg-avatar" style="background:#FF9800"></div>
<div class="msg-content">
<div class="msg-name">李四(发起人)</div>
<div class="msg-bubble">好的谢谢,我们网段的电脑都上不了网</div>
</div>
</div>
<div class="message-item">
<div class="msg-avatar" style="background:#07C160"></div>
<div class="msg-content">
<div class="msg-name">王五(被邀请人)</div>
<div class="msg-bubble">交换机端口有异常告警,正在排查中</div>
</div>
</div>
</div>
<!-- 底部输入区 -->
<div class="input-area">
<div class="input-toolbar">
<button>快速回复</button>
<button>转接</button>
<button>邀请</button>
<button>摇人</button>
</div>
<textarea class="input-textbox" placeholder="输入回复内容..."></textarea>
</div>
</div>
<script>
// 角色配置
const ROLE_COLORS = {
primary_agent: '#3b82f6',
collaborator: '#07C160',
owner: '#FF9800',
invitee: 'transparent'
};
const ROLE_LABELS = {
primary_agent: '主责',
collaborator: '协作',
owner: '发起人',
invitee: '被邀请'
};
const ROLE_BG = {
primary_agent: '#3b82f6',
collaborator: '#07C160',
owner: '#FF9800',
invitee: '#bbb'
};
const MAX_AVATARS = 6; // 坐席端超员阈值
const CURRENT_AGENT_ID = '1'; // 当前坐席 = 张明(主责)
// 场景数据
const scenes = [
// 3人场景
[
{ id: '1', name: '张明', avatar: '', dept: 'IT支持组', role: 'primary_agent', joined: true, isSelf: true, joinedAt: '2026-07-10T10:00:00' },
{ id: '2', name: '李四', avatar: '', dept: '财务部', role: 'owner', joined: true, isSelf: false, joinedAt: '2026-07-10T10:01:00' },
{ id: '3', name: '王五', avatar: '', dept: '网络运维组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:05:00' }
],
// 7人场景
[
{ id: '1', name: '张明', avatar: '', dept: 'IT支持组', role: 'primary_agent', joined: true, isSelf: true, joinedAt: '2026-07-10T10:00:00' },
{ id: '2', name: '李四', avatar: '', dept: '财务部', role: 'owner', joined: true, isSelf: false, joinedAt: '2026-07-10T10:01:00' },
{ id: '10', name: '陈十', avatar: '', dept: 'IT支持组', role: 'collaborator', joined: true, isSelf: false, joinedAt: '2026-07-10T10:02:00' },
{ id: '3', name: '王五', avatar: '', dept: '网络运维组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:05:00' },
{ id: '4', name: '赵六', avatar: '', dept: '系统组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:06:00' },
{ id: '5', name: '钱七', avatar: '', dept: '安全组', role: 'invitee', joined: false, isSelf: false, joinedAt: null },
{ id: '6', name: '孙八', avatar: '', dept: '桌面组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:08:00' }
],
// 10人场景(超员)
[
{ id: '1', name: '张明', avatar: '', dept: 'IT支持组', role: 'primary_agent', joined: true, isSelf: true, joinedAt: '2026-07-10T10:00:00' },
{ id: '2', name: '李四', avatar: '', dept: '财务部', role: 'owner', joined: true, isSelf: false, joinedAt: '2026-07-10T10:01:00' },
{ id: '10', name: '陈十', avatar: '', dept: 'IT支持组', role: 'collaborator', joined: true, isSelf: false, joinedAt: '2026-07-10T10:02:00' },
{ id: '11', name: '郑十一', avatar: '', dept: '运维组', role: 'collaborator', joined: true, isSelf: false, joinedAt: '2026-07-10T10:03:00' },
{ id: '3', name: '王五', avatar: '', dept: '网络运维组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:05:00' },
{ id: '4', name: '赵六', avatar: '', dept: '系统组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:06:00' },
{ id: '5', name: '钱七', avatar: '', dept: '安全组', role: 'invitee', joined: false, isSelf: false, joinedAt: null },
{ id: '6', name: '孙八', avatar: '', dept: '桌面组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:08:00' },
{ id: '7', name: '周九', avatar: '', dept: '数据库组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:09:00' },
{ id: '8', name: '吴十', avatar: '', dept: '中间件组', role: 'invitee', joined: false, isSelf: false, joinedAt: null }
]
];
let currentScene = 0;
let participants = [];
let isExpanded = false;
// P1: expandedStateMap 按会话ID记忆展开状态
const expandedStateMap = {};
function getLastChar(name) {
return name ? name.charAt(name.length - 1) : '?';
}
function sortParticipants(list) {
// 排列:主责 → 发起人 → 协作坐席 → 被邀请人(按joinedAt, 已加入在前)
return [...list].sort((a, b) => {
const order = { primary_agent: 0, owner: 1, collaborator: 2, invitee: 3 };
if (order[a.role] !== order[b.role]) return order[a.role] - order[b.role];
// 已加入在前
if (a.joined !== b.joined) return a.joined ? -1 : 1;
if (a.joinedAt && b.joinedAt) return new Date(a.joinedAt) - new Date(b.joinedAt);
return 0;
});
}
function renderBar() {
const container = document.getElementById('barAvatars');
const text = document.getElementById('barText');
const status = document.getElementById('convStatus');
container.innerHTML = '';
const sorted = sortParticipants(participants);
const showCount = Math.min(sorted.length, MAX_AVATARS);
const hasPending = sorted.some(p => !p.joined);
for (let i = 0; i < showCount; i++) {
const p = sorted[i];
const wrap = document.createElement('div');
wrap.className = 'avatar-wrap';
const av = document.createElement('div');
av.className = `avatar role-${p.role}`;
if (p.isSelf) av.classList.add('is-self');
av.style.background = ROLE_BG[p.role];
av.textContent = getLastChar(p.name);
if (!p.joined) {
const dot = document.createElement('div');
dot.className = 'pending-dot';
wrap.appendChild(dot);
}
wrap.appendChild(av);
container.appendChild(wrap);
}
if (sorted.length > MAX_AVATARS) {
const more = document.createElement('div');
more.className = 'avatar-more';
more.textContent = `+${sorted.length - MAX_AVATARS}`;
container.appendChild(more);
}
text.textContent = `${participants.length}位参与者${hasPending ? ' · 有待加入' : ''}`;
status.textContent = `${participants.length}人参与`;
}
function renderPanel() {
const body = document.getElementById('panelBody');
body.innerHTML = '';
const sorted = sortParticipants(participants);
const isPrimaryAgent = participants.some(p => p.id === CURRENT_AGENT_ID && p.role === 'primary_agent');
sorted.forEach(p => {
const item = document.createElement('div');
item.className = 'participant-item';
const av = document.createElement('div');
av.className = `avatar avatar-md role-${p.role}`;
if (p.isSelf) av.classList.add('is-self');
av.style.background = ROLE_BG[p.role];
av.textContent = getLastChar(p.name);
const info = document.createElement('div');
info.className = 'participant-info';
const name = document.createElement('span');
name.className = 'participant-name';
name.textContent = p.name + (p.isSelf ? '(我)' : '');
name.title = p.name; // P1: tooltip仅截断时显示
info.appendChild(name);
const tag = document.createElement('span');
tag.className = `role-tag ${p.role}`;
tag.textContent = ROLE_LABELS[p.role];
info.appendChild(tag);
const dept = document.createElement('span');
dept.className = 'participant-dept';
dept.textContent = p.dept + (p.joined ? '' : ' · 待加入');
info.appendChild(dept);
item.appendChild(av);
item.appendChild(info);
// P1: 移除图标仅主责坐席 + invitee角色显示
if (isPrimaryAgent && p.role === 'invitee') {
const remove = document.createElement('div');
remove.className = 'remove-icon';
remove.textContent = '×';
remove.title = '移除参与者';
remove.onclick = (e) => {
e.stopPropagation();
if (confirm(`确认移除 ${p.name}`)) {
alert('已移除(模拟)');
}
};
item.appendChild(remove);
}
body.appendChild(item);
});
}
function toggleExpand() {
isExpanded = !isExpanded;
const panel = document.getElementById('panel');
const icon = document.getElementById('expandIcon');
if (isExpanded) {
panel.classList.add('show');
icon.classList.add('expanded');
renderPanel();
// P1: 记忆展开状态
expandedStateMap['conv-1'] = true;
} else {
panel.classList.remove('show');
icon.classList.remove('expanded');
expandedStateMap['conv-1'] = false;
}
}
function toggleScene() {
currentScene = (currentScene + 1) % scenes.length;
participants = JSON.parse(JSON.stringify(scenes[currentScene]));
document.getElementById('sceneInfo').textContent = `场景: ${participants.length}`;
renderBar();
if (isExpanded) renderPanel();
}
// 初始化
participants = JSON.parse(JSON.stringify(scenes[0]));
renderBar();
</script>
</body>
</html>