Files
wecom_it_smart_desk/docs/01-产品文档/04-坐席工作台/原型-REQ-坐席-003-群聊双模式-v1.0.html
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +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>