[split-upload 2/6] f2fd4fa backup via proxy

This commit is contained in:
Simon
2026-08-11 14:16:24 +08:00
parent 268bf914ce
commit e54529d307
74 changed files with 5450 additions and 65 deletions
@@ -0,0 +1,554 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>工具栏统一设计 v0.3 - 4 按钮 + 高级拟人</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", -apple-system, sans-serif;
background: #f9fafb;
color: #111827;
padding: 32px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 8px; color: #111827; }
h2 { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; margin: 40px 0 12px; color: #1f2937; }
h3 { font-size: 14px; font-weight: 600; margin: 16px 0 8px; color: #374151; }
.meta { color: #6b7280; font-size: 12.5px; margin-bottom: 24px; }
/* === 设计 token === */
:root {
--c-bg-page: #f9fafb;
--c-bg-surface: #ffffff;
--c-bg-hover: #f3f4f6;
--c-border-default: #e5e7eb;
--c-border-hover: #d1d5db;
--c-text-primary: #111827;
--c-text-secondary: #6b7280;
--c-text-tertiary: #9ca3af;
--c-accent: #2563eb;
--c-accent-soft: #eff6ff;
--c-success: #059669;
--c-success-soft: #ecfdf5;
--c-warning: #d97706;
--c-warning-soft: #fef3c7;
--c-danger: #dc2626;
--c-danger-soft: #fef2f2;
--c-info: #4f46e5;
--c-info-soft: #eef2ff;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.06);
}
/* === 4 按钮统一容器(模拟 InputBar 工具栏) === */
.toolbar {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--c-bg-surface);
border: 1px solid var(--c-border-default);
border-radius: 12px;
box-shadow: var(--shadow-sm);
}
.toolbar-section {
display: flex;
align-items: center;
gap: 4px;
}
.toolbar-divider {
width: 1px;
height: 20px;
background: var(--c-border-default);
margin: 0 4px;
}
/* === 统一按钮基础(35×35,8px 圆角,克制) === */
.tool-btn {
width: 35px;
height: 35px;
border: 1px solid var(--c-border-default);
background: var(--c-bg-surface);
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.18s cubic-bezier(0.4, 0, 0.2, 1);
-webkit-tap-highlight-color: transparent;
position: relative;
color: var(--c-text-secondary);
}
.tool-btn:hover {
background: var(--c-bg-hover);
border-color: var(--c-border-hover);
color: var(--c-text-primary);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.tool-btn:active { transform: translateY(0) scale(0.96); }
.tool-btn svg { width: 18px; height: 18px; }
/* === 高级拟人 SVG 角色(克制) === */
/* 表情:抽象圆形笑脸 */
.ico-emoji circle.face { fill: none; stroke: currentColor; stroke-width: 1.5; }
.ico-emoji circle.eye { fill: currentColor; }
.ico-emoji path.mouth { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; }
/* 文件:纸夹 + 折角 */
.ico-file path { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; }
.ico-file path.fold { fill: var(--c-bg-hover); stroke: currentColor; stroke-width: 1.5; stroke-linejoin: round; }
/* 语音:麦克风(柱形 + 弧线底座) */
.ico-voice rect { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linejoin: round; }
.ico-voice path { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; }
/* 客服:戴耳机拟人 */
.ico-agent circle.head { fill: var(--c-bg-hover); stroke: currentColor; stroke-width: 1.5; }
.ico-agent circle.ear { fill: currentColor; }
.ico-agent path { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.ico-agent .mouth { fill: currentColor; }
.ico-agent .eye { fill: currentColor; }
/* === 客服按钮 6 态(克制高级) === */
.agent-btn.is-disabled { opacity: 0.45; cursor: not-allowed; color: var(--c-text-tertiary); }
.agent-btn.is-disabled:hover { transform: none; box-shadow: none; background: var(--c-bg-surface); border-color: var(--c-border-default); }
.agent-btn.is-disabled .ico-agent .mouth { display: none; }
.agent-btn.is-active { border-color: var(--c-accent); color: var(--c-accent); background: var(--c-accent-soft); }
.agent-btn.is-urgent {
border-color: var(--c-danger);
color: var(--c-danger);
background: var(--c-danger-soft);
animation: agentBreathe 1.6s ease-in-out infinite;
}
@keyframes agentBreathe {
0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0); }
50% { box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.12); }
}
.agent-btn.is-waiting { border-color: var(--c-warning); color: var(--c-warning); background: var(--c-warning-soft); }
.agent-btn.is-waiting .ico-agent .mouth { fill: var(--c-warning); }
.agent-btn.is-end { border-color: var(--c-success); color: var(--c-success); background: var(--c-success-soft); }
.agent-btn.is-reopen { border-color: var(--c-info); color: var(--c-info); background: var(--c-info-soft); }
/* === 演示容器 === */
.demo-block {
background: var(--c-bg-surface);
border: 1px solid var(--c-border-default);
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
}
.demo-label { font-size: 11.5px; color: var(--c-text-secondary); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 12px; font-weight: 500; }
/* === 状态切换 tab === */
.state-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--c-border-default); }
.state-tab {
padding: 6px 12px;
border: 1px solid var(--c-border-default);
background: var(--c-bg-surface);
border-radius: 6px;
cursor: pointer;
font-size: 12.5px;
color: var(--c-text-secondary);
transition: all 0.15s;
font-family: inherit;
}
.state-tab:hover { background: var(--c-bg-hover); }
.state-tab.active { background: var(--c-text-primary); color: #fff; border-color: var(--c-text-primary); }
.state-stage {
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%);
border-radius: 8px;
padding: 32px;
}
.state-desc { color: var(--c-text-secondary); font-size: 12.5px; line-height: 1.7; background: #fafbfc; padding: 12px 14px; border-radius: 6px; border-left: 2px solid var(--c-accent); margin-top: 12px; }
.state-desc strong { color: var(--c-text-primary); }
/* === 4 按钮并排展示 === */
.four-btns {
display: flex;
gap: 8px;
align-items: center;
padding: 16px;
background: #fafbfc;
border: 1px solid var(--c-border-default);
border-radius: 10px;
margin: 12px 0;
}
/* === 对比表 === */
.cmp-table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 12.5px; }
.cmp-table th, .cmp-table td { border: 1px solid var(--c-border-default); padding: 8px 10px; text-align: left; }
.cmp-table th { background: #fafbfc; font-weight: 600; }
.yes { color: #059669; }
.no { color: #dc2626; }
.partial { color: #d97706; }
.palette { display: flex; gap: 6px; flex-wrap: wrap; }
.palette-item { width: 48px; height: 48px; border-radius: 6px; display: flex; align-items: flex-end; padding: 4px; font-size: 9.5px; color: #fff; font-weight: 500; }
.note { background: #fef3c7; border-left: 3px solid #d97706; padding: 12px 16px; border-radius: 4px; font-size: 12.5px; color: #78350f; margin: 16px 0; }
</style>
</head>
<body>
<h1>工具栏统一设计 v0.3</h1>
<p class="meta">4 按钮(😊📎🎤🎧)统一设计 + 高级拟人角色 + 克制 B 端配色 · 2026-08-04 凌晨</p>
<div class="note">
📌 本版核心改动(对比 v0.1 / v0.2):<br>
• 4 个按钮**统一规格** + **统一设计语言**(35×35, 8px 圆角, 1px 边框, 浅色背景, 高级拟人 SVG 角色)<br>
• 角色设计**克制**:几何抽象拟人(不夸张),符合 B 端 SaaS 工具审美(对标 Linear / Notion / Cursor)<br>
• 配色**高级**:中性灰 + 点缀色(蓝/橙/红/绿/紫),不刺眼、有质感<br>
• 动画**克制**:仅微动效(hover 浮起 + scale 0.96,6 态无夸张抖动/招手)
</div>
<h2>🎨 设计 token(可复用到全局)</h2>
<div class="demo-block">
<h3>配色色板</h3>
<div class="palette">
<div class="palette-item" style="background: #f9fafb; color: #111827;">#f9fafb page</div>
<div class="palette-item" style="background: #ffffff; color: #111827; border: 1px solid #e5e7eb;">#ffffff surface</div>
<div class="palette-item" style="background: #f3f4f6; color: #111827;">#f3f4f6 hover</div>
<div class="palette-item" style="background: #e5e7eb; color: #111827;">#e5e7eb border</div>
<div class="palette-item" style="background: #6b7280;">#6b7280 text-2</div>
<div class="palette-item" style="background: #1f2937;">#1f2937 text-1</div>
<div class="palette-item" style="background: #2563eb;">#2563eb accent</div>
<div class="palette-item" style="background: #059669;">#059669 success</div>
<div class="palette-item" style="background: #d97706;">#d97706 warning</div>
<div class="palette-item" style="background: #dc2626;">#dc2626 danger</div>
<div class="palette-item" style="background: #4f46e5;">#4f46e5 info</div>
</div>
<h3>圆角与阴影</h3>
<p style="font-size: 12.5px; color: var(--c-text-secondary);">
圆角: 8px(按钮)/ 12px(容器)/ 6px(tab)<br>
阴影: 0 1px 2px rgba(0,0,0,0.04)(静态) / 0 2px 4px rgba(0,0,0,0.06)(hover 微浮)
</p>
</div>
<h2>🎭 4 按钮统一设计(角色图谱)</h2>
<div class="demo-block">
<div class="demo-label">工具栏并排展示(模拟 InputBar 工具栏位置)</div>
<div class="four-btns">
<!-- 表情 -->
<button class="tool-btn" title="表情">
<svg class="ico-emoji" viewBox="0 0 18 18" fill="none">
<circle class="face" cx="9" cy="9" r="6.5"/>
<circle class="eye" cx="6.5" cy="8" r="0.8"/>
<circle class="eye" cx="11.5" cy="8" r="0.8"/>
<path class="mouth" d="M6 11.5 Q9 13.5 12 11.5"/>
</svg>
</button>
<!-- 文件 -->
<button class="tool-btn" title="文件">
<svg class="ico-file" viewBox="0 0 18 18" fill="none">
<path d="M3 2 L11 2 L15 6 L15 16 L3 16 Z"/>
<path class="fold" d="M11 2 L11 6 L15 6"/>
<line x1="6" y1="10" x2="12" y2="10"/>
<line x1="6" y1="13" x2="10" y2="13"/>
</svg>
</button>
<!-- 分隔 -->
<div class="toolbar-divider"></div>
<!-- 语音 -->
<button class="tool-btn" title="语音">
<svg class="ico-voice" viewBox="0 0 18 18" fill="none">
<rect x="6" y="2" width="6" height="9" rx="3"/>
<path d="M3 9 Q3 13 9 13 Q15 13 15 9"/>
<line x1="9" y1="13" x2="9" y2="16"/>
</svg>
</button>
<!-- 客服(默认 active 态) -->
<button class="tool-btn agent-btn is-active" title="客服在线 · 点击呼叫">
<svg class="ico-agent" viewBox="0 0 18 18" fill="none">
<circle class="head" cx="9" cy="8" r="4"/>
<circle class="ear" cx="3.5" cy="8" r="1.2"/>
<circle class="ear" cx="14.5" cy="8" r="1.2"/>
<path d="M2.5 8 Q2.5 6 4 6 L4 10 Q2.5 10 2.5 8 Z" fill="currentColor" opacity="0.85"/>
<path d="M15.5 8 Q15.5 6 14 6 L14 10 Q15.5 10 15.5 8 Z" fill="currentColor" opacity="0.85"/>
<circle class="eye" cx="7" cy="8" r="0.7"/>
<circle class="eye" cx="11" cy="8" r="0.7"/>
<path class="mouth" d="M7 10.5 Q9 12 11 10.5"/>
</svg>
</button>
</div>
<p style="font-size: 12px; color: var(--c-text-secondary); margin-top: 8px;">
视觉特点: 35×35 统一尺寸, 8px 圆角, 1px 边框, 18px SVG 几何抽象拟人, 中性灰 + 蓝色点缀(active)
</p>
</div>
<h2>👤 角色详细设计(4 按钮的"角色")</h2>
<div class="demo-block">
<h3>1. 表情(😊) — 抽象圆形笑脸</h3>
<div style="display: flex; align-items: center; gap: 16px; padding: 16px; background: #fafbfc; border-radius: 8px;">
<button class="tool-btn">
<svg class="ico-emoji" viewBox="0 0 18 18" fill="none">
<circle class="face" cx="9" cy="9" r="6.5"/>
<circle class="eye" cx="6.5" cy="8" r="0.8"/>
<circle class="eye" cx="11.5" cy="8" r="0.8"/>
<path class="mouth" d="M6 11.5 Q9 13.5 12 11.5"/>
</svg>
</button>
<div style="font-size: 12.5px; color: var(--c-text-secondary); line-height: 1.6;">
<strong style="color: var(--c-text-primary);">设计</strong>: 圆形轮廓 + 两个点状眼睛 + 弧形嘴<br>
<strong style="color: var(--c-text-primary);">意图</strong>: 极简笑脸,中性、克制、专业(不用 emoji 圆形笑脸)
</div>
</div>
<h3>2. 文件(📎) — 纸夹 + 折角</h3>
<div style="display: flex; align-items: center; gap: 16px; padding: 16px; background: #fafbfc; border-radius: 8px;">
<button class="tool-btn">
<svg class="ico-file" viewBox="0 0 18 18" fill="none">
<path d="M3 2 L11 2 L15 6 L15 16 L3 16 Z"/>
<path class="fold" d="M11 2 L11 6 L15 6"/>
<line x1="6" y1="10" x2="12" y2="10"/>
<line x1="6" y1="13" x2="10" y2="13"/>
</svg>
</button>
<div style="font-size: 12.5px; color: var(--c-text-secondary); line-height: 1.6;">
<strong style="color: var(--c-text-primary);">设计</strong>: 文档轮廓 + 右上折角(阴影) + 2 行文字线<br>
<strong style="color: var(--c-text-primary);">意图</strong>: 拟人化的"文档",比回形针(📎)更明确表达"文件/附件"语义
</div>
</div>
<h3>3. 语音(🎤) — 麦克风(柱形 + 弧线底座)</h3>
<div style="display: flex; align-items: center; gap: 16px; padding: 16px; background: #fafbfc; border-radius: 8px;">
<button class="tool-btn">
<svg class="ico-voice" viewBox="0 0 18 18" fill="none">
<rect x="6" y="2" width="6" height="9" rx="3"/>
<path d="M3 9 Q3 13 9 13 Q15 13 15 9"/>
<line x1="9" y1="13" x2="9" y2="16"/>
</svg>
</button>
<div style="font-size: 12.5px; color: var(--c-text-secondary); line-height: 1.6;">
<strong style="color: var(--c-text-primary);">设计</strong>: 圆角麦克风柱 + 弧形底座 + 支柱<br>
<strong style="color: var(--c-text-primary);">意图</strong>: 经典麦克风几何,清晰表达"语音输入"
</div>
</div>
<h3>4. 客服(🎧) — 戴耳机的拟人客服</h3>
<div style="display: flex; align-items: center; gap: 16px; padding: 16px; background: #fafbfc; border-radius: 8px;">
<button class="tool-btn agent-btn is-active">
<svg class="ico-agent" viewBox="0 0 18 18" fill="none">
<circle class="head" cx="9" cy="8" r="4"/>
<circle class="ear" cx="3.5" cy="8" r="1.2"/>
<circle class="ear" cx="14.5" cy="8" r="1.2"/>
<path d="M2.5 8 Q2.5 6 4 6 L4 10 Q2.5 10 2.5 8 Z" fill="currentColor" opacity="0.85"/>
<path d="M15.5 8 Q15.5 6 14 6 L14 10 Q15.5 10 15.5 8 Z" fill="currentColor" opacity="0.85"/>
<circle class="eye" cx="7" cy="8" r="0.7"/>
<circle class="eye" cx="11" cy="8" r="0.7"/>
<path class="mouth" d="M7 10.5 Q9 12 11 10.5"/>
</svg>
</button>
<div style="font-size: 12.5px; color: var(--c-text-secondary); line-height: 1.6;">
<strong style="color: var(--c-text-primary);">设计</strong>: 圆形头部(背景填充) + 两侧椭圆耳罩 + 简单眼睛弧形嘴<br>
<strong style="color: var(--c-text-primary);">意图</strong>: 戴耳机的拟人客服,克制专业(不夸张卡通)<br>
<strong style="color: var(--c-text-primary);">与之前对比</strong>: v0.2 卡通是"全身小人",v0.3 改为"头像 + 耳罩",更克制专业
</div>
</div>
</div>
<h2>🎬 客服按钮 6 态(克制设计)</h2>
<div class="demo-block">
<div class="state-tabs">
<button class="state-tab active" data-state="active">active</button>
<button class="state-tab" data-state="disabled">disabled</button>
<button class="state-tab" data-state="urgent">urgent</button>
<button class="state-tab" data-state="waiting">waiting</button>
<button class="state-tab" data-state="end">end</button>
<button class="state-tab" data-state="reopen">reopen</button>
</div>
<div class="state-stage" id="stage"></div>
<div class="state-desc" id="desc"></div>
</div>
<h2>📊 4 按钮 hover 状态对比</h2>
<div class="demo-block">
<div class="four-btns">
<button class="tool-btn" style="background: #f3f4f6; border-color: #d1d5db; color: #111827; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.06);">
<svg class="ico-emoji" viewBox="0 0 18 18" fill="none">
<circle class="face" cx="9" cy="9" r="6.5"/>
<circle class="eye" cx="6.5" cy="8" r="0.8"/>
<circle class="eye" cx="11.5" cy="8" r="0.8"/>
<path class="mouth" d="M6 11.5 Q9 13.5 12 11.5"/>
</svg>
</button>
<button class="tool-btn" style="background: #f3f4f6; border-color: #d1d5db; color: #111827; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.06);">
<svg class="ico-file" viewBox="0 0 18 18" fill="none">
<path d="M3 2 L11 2 L15 6 L15 16 L3 16 Z"/>
<path class="fold" d="M11 2 L11 6 L15 6"/>
<line x1="6" y1="10" x2="12" y2="10"/>
<line x1="6" y1="13" x2="10" y2="13"/>
</svg>
</button>
<div class="toolbar-divider"></div>
<button class="tool-btn" style="background: #f3f4f6; border-color: #d1d5db; color: #111827; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.06);">
<svg class="ico-voice" viewBox="0 0 18 18" fill="none">
<rect x="6" y="2" width="6" height="9" rx="3"/>
<path d="M3 9 Q3 13 9 13 Q15 13 15 9"/>
<line x1="9" y1="13" x2="9" y2="16"/>
</svg>
</button>
<button class="tool-btn" style="background: #f3f4f6; border-color: #d1d5db; color: #111827; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.06);">
<svg class="ico-agent" viewBox="0 0 18 18" fill="none">
<circle class="head" cx="9" cy="8" r="4"/>
<circle class="ear" cx="3.5" cy="8" r="1.2"/>
<circle class="ear" cx="14.5" cy="8" r="1.2"/>
<path d="M2.5 8 Q2.5 6 4 6 L4 10 Q2.5 10 2.5 8 Z" fill="currentColor" opacity="0.85"/>
<path d="M15.5 8 Q15.5 6 14 6 L14 10 Q15.5 10 15.5 8 Z" fill="currentColor" opacity="0.85"/>
<circle class="eye" cx="7" cy="8" r="0.7"/>
<circle class="eye" cx="11" cy="8" r="0.7"/>
<path class="mouth" d="M7 10.5 Q9 12 11 10.5"/>
</svg>
</button>
</div>
<p style="font-size: 12px; color: var(--c-text-secondary); margin-top: 8px;">
hover 状态: 背景 #f3f4f6 + 边框 #d1d5db + 文字色 #111827 + translateY(-1px) 微浮 + shadow-md
</p>
</div>
<h2>📊 与之前版本对比(为什么改方向)</h2>
<table class="cmp-table">
<thead>
<tr>
<th>维度</th>
<th>v0.1 方块按钮</th>
<th>v0.2 灵动岛卡通</th>
<th>v0.3 统一克制拟人 ⭐</th>
</tr>
</thead>
<tbody>
<tr>
<td>4 按钮统一</td>
<td class="no">× (只设计 1 个按钮)</td>
<td class="no">× (只设计 1 个按钮)</td>
<td class="yes">✓ 4 按钮统一规格 + 统一设计语言</td>
</tr>
<tr>
<td>角色设计</td>
<td class="no">emoji(无设计)</td>
<td class="partial">夸张卡通小黄人(低龄)</td>
<td class="yes">克制几何拟人头像(Linear/Notion 风)</td>
</tr>
<tr>
<td>配色</td>
<td class="partial">朴素(单调)</td>
<td class="partial">卡通色(刺眼)</td>
<td class="yes">中性灰 + 点缀色(高级 SaaS)</td>
</tr>
<tr>
<td>动画克制度</td>
<td class="yes">无动画</td>
<td class="no">抖动/招手/摇头(夸张)</td>
<td class="yes">hover 微浮 + 状态边框变色(克制)</td>
</tr>
<tr>
<td>尺寸</td>
<td class="partial">36×36 / 8px</td>
<td class="no">36×36 / 18px(不一致)</td>
<td class="yes">35×35 / 8px / 18px SVG(与现有对齐)</td>
</tr>
<tr>
<td>与现有工具按钮风格</td>
<td class="yes">★★★★★</td>
<td class="no"></td>
<td class="yes">★★★★★(统一规格 + 统一语言)</td>
</tr>
<tr>
<td>B 端专业感</td>
<td class="partial">★★★</td>
<td class="no"></td>
<td class="yes">★★★★★(对标 Linear/Notion)</td>
</tr>
</tbody>
</table>
<h2>❓ 待你拍板</h2>
<ol style="margin-left: 20px; line-height: 1.8;">
<li><strong>整体方向 OK 吗</strong>?克制拟人 + 4 按钮统一 + Linear/Notion 风</li>
<li><strong>客服角色</strong>:当前是"戴耳机圆形头像",可以吗?如需更精致,可以请设计师重画 SVG</li>
<li><strong>客服按钮 6 态</strong>的"克制版"设计(仅边框/底色变化,无夸张动画)符合你的预期吗?</li>
<li><strong>大小</strong>用 35×35 还是回归 36×36(与现有一致)?</li>
</ol>
<p style="margin-top: 32px; padding-top: 16px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 11.5px;">
v0.3 工具栏统一设计 · 2026-08-04 · Duckula 主理人 · 等待用户拍板
</p>
<script>
const states = {
active: {
cls: 'is-active',
desc: '<strong>active</strong>: 蓝色边框 + 浅蓝背景 + 蓝色图标 — 默认"客服在线,等待呼叫"'
},
disabled: {
cls: 'is-disabled',
desc: '<strong>disabled</strong>: 灰色 + opacity 0.45 + 嘴隐藏(无表情) — 表达"不可用",无动画'
},
urgent: {
cls: 'is-urgent',
desc: '<strong>urgent</strong>: 红色边框 + 浅红背景 + 红色图标 + 1.6s 呼吸(box-shadow 扩散) — 表达"紧急,需关注"'
},
waiting: {
cls: 'is-waiting',
desc: '<strong>waiting</strong>: 橙色边框 + 浅橙背景 + 橙色图标 + 嘴变色 — 表达"排队中"'
},
end: {
cls: 'is-end',
desc: '<strong>end</strong>: 绿色边框 + 浅绿背景 + 绿色图标 — 表达"已接入坐席,通话中"'
},
reopen: {
cls: 'is-reopen',
desc: '<strong>reopen</strong>: 紫色边框 + 浅紫背景 + 紫色图标 — 表达"会话已关闭,可重新打开"'
}
};
const svgHTML = `<svg class="ico-agent" viewBox="0 0 18 18" fill="none">
<circle class="head" cx="9" cy="8" r="4"/>
<circle class="ear" cx="3.5" cy="8" r="1.2"/>
<circle class="ear" cx="14.5" cy="8" r="1.2"/>
<path d="M2.5 8 Q2.5 6 4 6 L4 10 Q2.5 10 2.5 8 Z" fill="currentColor" opacity="0.85"/>
<path d="M15.5 8 Q15.5 6 14 6 L14 10 Q15.5 10 15.5 8 Z" fill="currentColor" opacity="0.85"/>
<circle class="eye" cx="7" cy="8" r="0.7"/>
<circle class="eye" cx="11" cy="8" r="0.7"/>
<path class="mouth" d="M7 10.5 Q9 12 11 10.5"/>
</svg>`;
const stage = document.getElementById('stage');
const descEl = document.getElementById('desc');
const tabs = document.querySelectorAll('.state-tab');
function render(state) {
const s = states[state];
stage.innerHTML = `<button class="tool-btn agent-btn ${s.cls}">${svgHTML}</button>`;
descEl.innerHTML = s.desc;
}
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
render(tab.dataset.state);
});
});
render('active');
</script>
</body>
</html>
@@ -0,0 +1,498 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>工具栏统一设计 v0.9 - 3 按钮居中 + 高级感</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #e0e7ff 0%, #f3e8ff 50%, #fce7f3 100%);
color: #1a1a1a;
padding: 32px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 8px; color: #1a1a1a; }
h2 { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; margin: 40px 0 12px; color: #1a1a1a; }
h3 { font-size: 14px; font-weight: 600; margin: 16px 0 8px; color: #333; }
.meta { color: #6b7280; font-size: 12.5px; margin-bottom: 24px; }
/* === InputBar 容器模拟 === */
.inputbar-mock {
width: 100%;
max-width: 800px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 20px;
padding: 16px;
box-shadow:
0 1px 0 rgba(255, 255, 255, 1) inset,
0 -1px 0 rgba(99, 102, 241, 0.08) inset,
0 4px 8px rgba(99, 102, 241, 0.05),
0 16px 32px rgba(99, 102, 241, 0.1);
}
.inputbar-mock-field {
width: 100%;
min-height: 80px;
background: rgba(255, 255, 255, 0.4);
border: 1px solid rgba(99, 102, 241, 0.12);
border-radius: 12px;
padding: 12px 16px;
font-size: 13px;
color: #9ca3af;
line-height: 1.6;
margin-top: 12px;
}
/* === 3 按钮居中工具栏(v0.5 玻璃拟态) === */
.glass-toolbar {
display: flex;
align-items: center;
justify-content: center; /* 居中 */
gap: 8px;
padding: 8px 16px;
margin: 0 auto 4px;
width: fit-content;
background: rgba(255, 255, 255, 0.55);
backdrop-filter: blur(28px) saturate(180%);
-webkit-backdrop-filter: blur(28px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 18px;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.9) inset,
0 -1px 0 rgba(99, 102, 241, 0.08) inset,
0 2px 4px rgba(99, 102, 241, 0.05),
0 8px 16px rgba(99, 102, 241, 0.08),
0 24px 48px rgba(99, 102, 241, 0.05);
position: relative;
}
.glass-toolbar::before {
content: "";
position: absolute;
inset: 0;
border-radius: 18px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0.1) 40%, transparent 60%);
pointer-events: none;
}
.glass-toolbar::after {
content: "";
position: absolute;
inset: 0;
border-radius: 18px;
background: radial-gradient(circle at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(236, 72, 153, 0.12) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
/* === 3 按钮(38px 圆 · 高级感渐变) === */
.glass-btn {
width: 38px;
height: 38px;
border: 1px solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.55);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
-webkit-tap-highlight-color: transparent;
position: relative;
z-index: 2;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.9) inset,
0 -1px 0 rgba(0, 0, 0, 0.04) inset,
0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 4px rgba(0, 0, 0, 0.04);
overflow: visible;
}
.glass-btn:hover {
background: rgba(255, 255, 255, 0.75);
transform: translateY(-1px) scale(1.06);
box-shadow:
0 1px 0 rgba(255, 255, 255, 1) inset,
0 -1px 0 rgba(0, 0, 0, 0.04) inset,
0 2px 4px rgba(0, 0, 0, 0.06),
0 8px 16px rgba(99, 102, 241, 0.15);
}
.glass-btn:active { transform: translateY(0) scale(0.95); }
.glass-btn svg { width: 22px; height: 22px; display: block; }
/* === 3 按钮高亮态(active) === */
.glass-btn.is-active-emoji { background: linear-gradient(135deg, rgba(251, 191, 36, 0.9) 0%, rgba(245, 158, 11, 0.9) 100%); border-color: rgba(255, 255, 255, 0.4); }
.glass-btn.is-active-emoji::before { content: ""; position: absolute; inset: 1px; border-radius: 50%; background: linear-gradient(180deg, rgba(255, 255, 255, 0.35) 0%, transparent 50%); pointer-events: none; }
.glass-btn.is-active-file { background: linear-gradient(135deg, rgba(59, 130, 246, 0.9) 0%, rgba(37, 99, 235, 0.9) 100%); border-color: rgba(255, 255, 255, 0.4); }
.glass-btn.is-active-file::before { content: ""; position: absolute; inset: 1px; border-radius: 50%; background: linear-gradient(180deg, rgba(255, 255, 255, 0.35) 0%, transparent 50%); pointer-events: none; }
.glass-btn.is-active-voice { background: linear-gradient(135deg, rgba(139, 92, 246, 0.9) 0%, rgba(124, 58, 237, 0.9) 100%); border-color: rgba(255, 255, 255, 0.4); }
.glass-btn.is-active-voice::before { content: ""; position: absolute; inset: 1px; border-radius: 50%; background: linear-gradient(180deg, rgba(255, 255, 255, 0.35) 0%, transparent 50%); pointer-events: none; }
/* 录音中态(语音) */
.glass-btn.is-recording {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.95) 0%, rgba(220, 38, 38, 0.95) 100%);
border-color: rgba(255, 255, 255, 0.4);
animation: recordingPulse 1.5s ease-in-out infinite;
}
@keyframes recordingPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0), 0 1px 0 rgba(255, 255, 255, 0.9) inset; }
50% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0.15), 0 1px 0 rgba(255, 255, 255, 0.9) inset; }
}
/* === 演示容器 === */
.demo-block {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 18px;
padding: 32px;
margin-bottom: 20px;
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.9) inset, 0 8px 16px rgba(99, 102, 241, 0.06);
}
.demo-label { font-size: 11.5px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 14px; font-weight: 500; }
/* === 状态切换 tab === */
.state-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid rgba(99, 102, 241, 0.12); }
.state-tab {
padding: 6px 12px;
border: 1px solid rgba(99, 102, 241, 0.2);
background: rgba(255, 255, 255, 0.6);
border-radius: 8px;
cursor: pointer;
font-size: 12.5px;
color: #4b5563;
transition: all 0.15s;
font-family: inherit;
}
.state-tab:hover { background: rgba(255, 255, 255, 0.8); }
.state-tab.active { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: #fff; border-color: transparent; }
.stage-bg {
background: linear-gradient(135deg, #fafbfc 0%, #f0f1f5 100%);
border-radius: 10px;
padding: 32px;
min-height: 90px;
display: flex;
align-items: center;
justify-content: center;
}
.state-desc { color: #4b5563; font-size: 12.5px; line-height: 1.7; background: rgba(255, 255, 255, 0.5); padding: 12px 14px; border-radius: 10px; border-left: 2px solid #6366f1; margin-top: 12px; backdrop-filter: blur(10px); }
.state-desc strong { color: #1a1a1a; }
.note { background: rgba(254, 243, 199, 0.6); border-left: 3px solid #d97706; padding: 12px 16px; border-radius: 4px; font-size: 12.5px; color: #78350f; margin: 16px 0; backdrop-filter: blur(10px); }
/* === 设计对比表 === */
.cmp-table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 12.5px; }
.cmp-table th, .cmp-table td { border: 1px solid rgba(99, 102, 241, 0.15); padding: 8px 10px; text-align: left; }
.cmp-table th { background: rgba(255, 255, 255, 0.6); font-weight: 600; }
.yes { color: #059669; }
.no { color: #dc2626; }
.partial { color: #d97706; }
</style>
</head>
<body>
<svg width="0" height="0" style="position: absolute;">
<defs>
<!-- 表情 · 立体渐变(高级感) -->
<linearGradient id="g-emoji" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#fde68a"/><stop offset="60%" stop-color="#fbbf24"/><stop offset="100%" stop-color="#d97706"/>
</linearGradient>
<radialGradient id="g-emoji-blush" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="rgba(244, 63, 94, 0.4)"/>
<stop offset="100%" stop-color="rgba(244, 63, 94, 0)"/>
</radialGradient>
<linearGradient id="g-highlight" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="white" stop-opacity="0.7"/>
<stop offset="100%" stop-color="white" stop-opacity="0"/>
</linearGradient>
<!-- 文件 · 立体渐变 -->
<linearGradient id="g-doc-body" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#ffffff"/><stop offset="100%" stop-color="#dbeafe"/>
</linearGradient>
<linearGradient id="g-doc-fold" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#c7d2fe"/><stop offset="100%" stop-color="#6366f1"/>
</linearGradient>
<linearGradient id="g-doc-text" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#a855f7"/>
</linearGradient>
<!-- 语音 · 立体渐变 -->
<linearGradient id="g-mic-body" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#c4b5fd"/><stop offset="50%" stop-color="#8b5cf6"/><stop offset="100%" stop-color="#6d28d9"/>
</linearGradient>
<linearGradient id="g-mic-shine" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="white" stop-opacity="0.65"/><stop offset="100%" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>
<h1>工具栏统一设计 v0.9 — 3 按钮居中 + 高级感</h1>
<p class="meta">表情/文件/语音输入 · v0.5 玻璃拟态 · 居中布局 · 高级感 SVG(渐变+高光+阴影)· 2026-08-04 凌晨</p>
<div class="note">
📌 本版核心改动(对比 v0.8):<br>
<strong>3 按钮</strong>(不是 4 按钮) — 客服按钮不在 InputBar 工具栏,只在<strong>整合区</strong>(v0.8 双入口设计)<br>
<strong>整体居中布局</strong> — 沿用 v0.8 位置(`justify-content: center`)<br>
• v0.5 玻璃拟态✅ 沿用<br>
<strong>高级感 SVG</strong> — 每个按钮都用渐变填充+顶部高光+阴影(v0.6 升级版)
</div>
<h2>🎨 3 按钮 InputBar 工具栏(居中布局)</h2>
<div class="demo-block">
<div class="demo-label">InputBar 工具栏 · 整体居中 · 3 按钮(表情/文件/语音)</div>
<div class="inputbar-mock">
<!-- 居中的整体工具栏 -->
<div class="glass-toolbar">
<!-- 1. 表情(高级感) -->
<button class="glass-btn" title="表情">
<svg viewBox="0 0 24 24" fill="none">
<!-- 脸(立体渐变) -->
<circle cx="12" cy="12" r="9" fill="url(#g-emoji)"/>
<!-- 顶部高光 -->
<ellipse cx="12" cy="6" rx="6" ry="2.5" fill="url(#g-highlight)"/>
<!-- 脸颊红晕 -->
<ellipse cx="6.5" cy="14" rx="1.5" ry="1" fill="url(#g-emoji-blush)"/>
<ellipse cx="17.5" cy="14" rx="1.5" ry="1" fill="url(#g-emoji-blush)"/>
<!-- 立体眼睛(双高光) -->
<ellipse cx="8.5" cy="11" rx="1.4" ry="1.6" fill="#1e1b4b"/>
<ellipse cx="15.5" cy="11" rx="1.4" ry="1.6" fill="#1e1b4b"/>
<circle cx="8.9" cy="10.4" r="0.5" fill="white"/>
<circle cx="15.9" cy="10.4" r="0.5" fill="white"/>
<circle cx="8.3" cy="11.5" r="0.3" fill="white" opacity="0.6"/>
<circle cx="15.7" cy="11.5" r="0.3" fill="white" opacity="0.6"/>
<!-- 嘴(立体感) -->
<path d="M8 15 Q12 18 16 15" fill="none" stroke="#1e1b4b" stroke-width="1.6" stroke-linecap="round"/>
<path d="M8 15 Q12 17 16 15" fill="rgba(244, 63, 94, 0.2)"/>
</svg>
</button>
<!-- 2. 文件(高级感) -->
<button class="glass-btn" title="文件">
<svg viewBox="0 0 24 24" fill="none">
<!-- 文档主体(立体渐变) -->
<path d="M4 2 L14 2 L20 8 L20 20 L4 20 Z" fill="url(#g-doc-body)" stroke="#818cf8" stroke-width="0.8" stroke-linejoin="round"/>
<!-- 折角(渐变) -->
<path d="M14 2 L14 8 L20 8" fill="url(#g-doc-fold)" stroke="#818cf8" stroke-width="0.8" stroke-linejoin="round"/>
<!-- 顶部高光 -->
<ellipse cx="10" cy="4" rx="5" ry="0.8" fill="url(#g-highlight)"/>
<!-- 文字行(渐变) -->
<rect x="7" y="11.5" width="10" height="0.8" rx="0.4" fill="url(#g-doc-text)"/>
<rect x="7" y="14" width="8" height="0.8" rx="0.4" fill="url(#g-doc-text)" opacity="0.7"/>
<rect x="7" y="16.5" width="6" height="0.8" rx="0.4" fill="url(#g-doc-text)" opacity="0.5"/>
</svg>
</button>
<!-- 3. 语音输入(高级感) -->
<button class="glass-btn" title="语音输入">
<svg viewBox="0 0 24 24" fill="none">
<!-- 麦克风主体(深紫渐变) -->
<rect x="8" y="2" width="8" height="13" rx="4" fill="url(#g-mic-body)"/>
<!-- 顶部高光(立体感) -->
<rect x="9" y="3" width="2" height="11" rx="1" fill="url(#g-mic-shine)"/>
<!-- 麦克风网格(细节) -->
<line x1="10.5" y1="6" x2="10.5" y2="11" stroke="white" stroke-width="0.3" opacity="0.4"/>
<line x1="12" y1="5" x2="12" y2="12" stroke="white" stroke-width="0.3" opacity="0.4"/>
<line x1="13.5" y1="6" x2="13.5" y2="11" stroke="white" stroke-width="0.3" opacity="0.4"/>
<!-- 底座弧线(渐变) -->
<path d="M3 12 Q3 18 12 18 Q21 18 21 12" fill="none" stroke="url(#g-mic-body)" stroke-width="2" stroke-linecap="round"/>
<!-- 支柱 -->
<line x1="12" y1="18" x2="12" y2="22" stroke="url(#g-mic-body)" stroke-width="2" stroke-linecap="round"/>
<line x1="8" y1="22" x2="16" y2="22" stroke="url(#g-mic-body)" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
<!-- 输入框(在工具栏下方) -->
<div class="inputbar-mock-field">
请输入消息...(输入框在工具栏下方)
</div>
</div>
</div>
<h2>🎨 3 按钮不同状态对比(高亮 + 录音中)</h2>
<div class="demo-block">
<div class="state-tabs">
<button class="state-tab active" data-state="default">默认</button>
<button class="state-tab" data-state="emoji-active">表情激活</button>
<button class="state-tab" data-state="file-active">文件激活</button>
<button class="state-tab" data-state="voice-active">语音激活</button>
<button class="state-tab" data-state="voice-recording">语音录音中</button>
</div>
<div class="stage-bg">
<div class="glass-toolbar" id="state-stage"></div>
</div>
<div class="state-desc" id="state-desc"></div>
</div>
<h2>📊 与之前版本对比</h2>
<table class="cmp-table">
<thead>
<tr>
<th>维度</th>
<th>v0.1 方块</th>
<th>v0.2 灵动岛</th>
<th>v0.3 克制</th>
<th>v0.4 圆形渐变</th>
<th>v0.5 玻璃</th>
<th>v0.8 4按钮居中</th>
<th><strong>v0.9 3按钮居中</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>按钮数</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td class="yes">4</td>
<td class="yes"><strong>3</strong></td>
</tr>
<tr>
<td>布局</td>
<td>左上</td>
<td>左上</td>
<td>左上</td>
<td>左上</td>
<td>左上</td>
<td>居中</td>
<td class="yes"><strong>居中</strong></td>
</tr>
<tr>
<td>背景</td>
<td></td>
<td></td>
<td></td>
<td>白底</td>
<td>玻璃</td>
<td>玻璃</td>
<td class="yes"><strong>玻璃</strong></td>
</tr>
<tr>
<td>图标</td>
<td>emoji</td>
<td>卡通</td>
<td>简单</td>
<td>抽象</td>
<td>立体</td>
<td>立体+图片</td>
<td class="yes"><strong>高级感渐变+高光</strong></td>
</tr>
<tr>
<td>客服按钮</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>✓ (图片)</td>
<td class="no"><strong>✗(只在整合区)</strong></td>
</tr>
</tbody>
</table>
<h2>❓ 待你拍板</h2>
<ol style="margin-left: 20px; line-height: 1.8;">
<li><strong>3 按钮(表情/文件/语音)</strong>符合期望吗?客服按钮<strong>只在整合区</strong>(v0.8 双入口设计)</li>
<li><strong>居中布局</strong>OK 吗?</li>
<li><strong>高级感图标</strong>(渐变填充+高光+阴影+红晕)OK 吗?如需更精致(更复杂 SVG 或真 3D),需设计师</li>
<li>激活态(表情打开/文件选/语音录)的<strong>渐变色</strong>(黄/蓝/紫)OK 吗?</li>
</ol>
<p style="margin-top: 32px; padding-top: 16px; border-top: 1px solid rgba(99, 102, 241, 0.15); color: #6b7280; font-size: 11.5px;">
v0.9 3 按钮居中 · 2026-08-04 · Duckula 主理人 · 等待用户拍板
</p>
<script>
const stateMap = {
'default': {
emoji: '', file: '', voice: '',
desc: '<strong>默认状态</strong>: 3 按钮默认(浅玻璃) — 表情/文件/语音都未激活,等待用户操作'
},
'emoji-active': {
emoji: 'is-active-emoji', file: '', voice: '',
desc: '<strong>表情激活</strong>: 表情按钮变<strong>黄橙渐变</strong>(顶部高光) — 表情面板已打开'
},
'file-active': {
emoji: '', file: 'is-active-file', voice: '',
desc: '<strong>文件激活</strong>: 文件按钮变<strong>蓝紫渐变</strong> — 文件选择器已打开'
},
'voice-active': {
emoji: '', file: '', voice: 'is-active-voice',
desc: '<strong>语音激活</strong>: 语音按钮变<strong>紫粉渐变</strong> — 准备录音(可点击开始)'
},
'voice-recording': {
emoji: '', file: '', voice: 'is-recording',
desc: '<strong>语音录音中</strong>: 语音按钮变<strong>红色渐变 + 1.5s 发光呼吸</strong> — 正在录音(点击停止)'
}
};
const svgEmoji = `<svg viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="9" fill="url(#g-emoji)"/>
<ellipse cx="12" cy="6" rx="6" ry="2.5" fill="url(#g-highlight)"/>
<ellipse cx="6.5" cy="14" rx="1.5" ry="1" fill="url(#g-emoji-blush)"/>
<ellipse cx="17.5" cy="14" rx="1.5" ry="1" fill="url(#g-emoji-blush)"/>
<ellipse cx="8.5" cy="11" rx="1.4" ry="1.6" fill="#1e1b4b"/>
<ellipse cx="15.5" cy="11" rx="1.4" ry="1.6" fill="#1e1b4b"/>
<circle cx="8.9" cy="10.4" r="0.5" fill="white"/>
<circle cx="15.9" cy="10.4" r="0.5" fill="white"/>
<circle cx="8.3" cy="11.5" r="0.3" fill="white" opacity="0.6"/>
<circle cx="15.7" cy="11.5" r="0.3" fill="white" opacity="0.6"/>
<path d="M8 15 Q12 18 16 15" fill="none" stroke="#1e1b4b" stroke-width="1.6" stroke-linecap="round"/>
<path d="M8 15 Q12 17 16 15" fill="rgba(244, 63, 94, 0.2)"/>
</svg>`;
const svgFile = `<svg viewBox="0 0 24 24" fill="none">
<path d="M4 2 L14 2 L20 8 L20 20 L4 20 Z" fill="url(#g-doc-body)" stroke="#818cf8" stroke-width="0.8" stroke-linejoin="round"/>
<path d="M14 2 L14 8 L20 8" fill="url(#g-doc-fold)" stroke="#818cf8" stroke-width="0.8" stroke-linejoin="round"/>
<ellipse cx="10" cy="4" rx="5" ry="0.8" fill="url(#g-highlight)"/>
<rect x="7" y="11.5" width="10" height="0.8" rx="0.4" fill="url(#g-doc-text)"/>
<rect x="7" y="14" width="8" height="0.8" rx="0.4" fill="url(#g-doc-text)" opacity="0.7"/>
<rect x="7" y="16.5" width="6" height="0.8" rx="0.4" fill="url(#g-doc-text)" opacity="0.5"/>
</svg>`;
const svgVoice = `<svg viewBox="0 0 24 24" fill="none">
<rect x="8" y="2" width="8" height="13" rx="4" fill="url(#g-mic-body)"/>
<rect x="9" y="3" width="2" height="11" rx="1" fill="url(#g-mic-shine)"/>
<line x1="10.5" y1="6" x2="10.5" y2="11" stroke="white" stroke-width="0.3" opacity="0.4"/>
<line x1="12" y1="5" x2="12" y2="12" stroke="white" stroke-width="0.3" opacity="0.4"/>
<line x1="13.5" y1="6" x2="13.5" y2="11" stroke="white" stroke-width="0.3" opacity="0.4"/>
<path d="M3 12 Q3 18 12 18 Q21 18 21 12" fill="none" stroke="url(#g-mic-body)" stroke-width="2" stroke-linecap="round"/>
<line x1="12" y1="18" x2="12" y2="22" stroke="url(#g-mic-body)" stroke-width="2" stroke-linecap="round"/>
<line x1="8" y1="22" x2="16" y2="22" stroke="url(#g-mic-body)" stroke-width="2" stroke-linecap="round"/>
</svg>`;
const stage = document.getElementById('state-stage');
const desc = document.getElementById('state-desc');
const tabs = document.querySelectorAll('.state-tab');
function render(state) {
const s = stateMap[state];
stage.innerHTML = `
<button class="glass-btn ${s.emoji}" title="表情">${svgEmoji}</button>
<button class="glass-btn ${s.file}" title="文件">${svgFile}</button>
<button class="glass-btn ${s.voice}" title="语音">${svgVoice}</button>
`;
desc.innerHTML = s.desc;
}
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
render(tab.dataset.state);
});
});
render('default');
</script>
</body>
</html>
@@ -0,0 +1,495 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>工具栏统一设计 v1.5 - 宝石嵌入式工具栏</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #e0e7ff 0%, #f3e8ff 50%, #fce7f3 100%);
color: #1a1a1a;
padding: 24px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
h1 { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 8px; color: #1a1a1a; }
h2 { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; margin: 32px 0 12px; color: #1a1a1a; }
h3 { font-size: 14px; font-weight: 600; margin: 12px 0 8px; color: #333; }
.meta { color: #6b7280; font-size: 12.5px; margin-bottom: 20px; }
/* === ChatPanel 整体 === */
.chatpanel-mock {
width: 100%;
max-width: 720px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
overflow: hidden;
position: relative;
}
.chatpanel-mock-header {
padding: 12px 16px;
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
background: rgba(255, 255, 255, 0.5);
font-size: 13px;
color: #6b7280;
display: flex;
align-items: center;
gap: 8px;
}
.chatpanel-mock-header .dot { width: 8px; height: 8px; border-radius: 50%; background: #10b981; }
/* === InputBar(消息发送框) === */
.inputbar-mock {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgba(99, 102, 241, 0.15);
padding: 12px 16px 16px;
position: relative;
}
/* === 普通工具栏(Before:扁平胶囊,5 按钮居中) === */
.glass-toolbar {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 8px 16px;
margin: 0 auto 12px;
width: fit-content;
background: rgba(255, 255, 255, 0.55);
backdrop-filter: blur(28px) saturate(180%);
-webkit-backdrop-filter: blur(28px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 20px;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.9) inset,
0 2px 4px rgba(99, 102, 241, 0.05),
0 8px 16px rgba(99, 102, 241, 0.08);
position: relative;
}
.glass-btn {
width: 40px;
height: 40px;
border: 1px solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.55);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
-webkit-tap-highlight-color: transparent;
position: relative;
z-index: 2;
box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 2px 4px rgba(0,0,0,0.04);
}
.glass-btn:hover { background: rgba(255,255,255,0.75); transform: translateY(-1px) scale(1.06); }
.glass-btn:active { transform: translateY(0) scale(0.95); }
.glass-btn svg { width: 22px; height: 22px; display: block; }
/* === 坐席按钮(普通,Before 用) === */
.agent-btn {
width: 44px;
height: 44px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
position: relative;
z-index: 2;
padding: 0;
background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%) padding-box;
}
.agent-btn::before {
content: ""; position: absolute; inset: -2px; border-radius: 50%;
background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%); z-index: -1;
}
.agent-btn img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; object-position: center 15%; display: block; }
.agent-badge {
position: absolute; bottom: 0; right: 0; width: 12px; height: 12px;
border-radius: 50%; border: 2px solid white; z-index: 3; box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.agent-badge.is-online { background: #10b981; }
/* === 宝石嵌入式工具栏(After / v1.5 === */
/* 容器:高度留出隆起 + 宝石探出的空间 */
.gem-toolbar {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: fit-content;
height: 84px;
margin: 0 auto 12px;
}
/* 镜片/花瓣形轨道背景(SVG,两端窄、中间隆起) */
.gem-toolbar-bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
overflow: visible;
filter: drop-shadow(0 6px 16px rgba(99, 102, 241, 0.28));
}
/* 工具栏内按钮行(5 个,坐席为第 3 个居中) */
.gem-row { position: relative; z-index: 2; display: flex; align-items: center; justify-content: center; gap: 12px; }
/* 坐席按钮 = 宝石,第 3 位,向上探出轨道形成镶嵌感 */
.gem-toolbar .agent-btn.gem {
width: 46px;
height: 46px;
transform: translateY(-18px);
z-index: 3;
box-shadow:
0 0 0 3px rgba(255, 255, 255, 0.6),
0 0 0 5px rgba(168, 85, 247, 0.35),
0 6px 14px rgba(99, 102, 241, 0.4),
0 2px 6px rgba(99, 102, 241, 0.25);
}
/* 宝石高光(左上斜向反光,模拟切面) */
.gem-toolbar .agent-btn.gem::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0.05) 38%, transparent 55%);
pointer-events: none;
}
.gem-toolbar .glass-btn { z-index: 2; }
/* === InputBar 输入框 === */
.inputbar-mock-field {
width: 100%;
min-height: 60px;
background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(99, 102, 241, 0.12);
border-radius: 12px;
padding: 10px 14px;
font-size: 13px;
color: #9ca3af;
line-height: 1.6;
}
/* === 演示容器 === */
.demo-block {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 18px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.9) inset, 0 8px 16px rgba(99, 102, 241, 0.06);
}
.compare-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.compare-panel { background: rgba(255, 255, 255, 0.5); border: 1px solid rgba(99, 102, 241, 0.15); border-radius: 12px; padding: 16px; }
.compare-panel.before { border-left: 4px solid #f59e0b; }
.compare-panel.after { border-left: 4px solid #10b981; }
.compare-title { font-size: 13px; font-weight: 600; margin-bottom: 12px; }
.compare-panel.before .compare-title { color: #b45309; }
.compare-panel.after .compare-title { color: #047857; }
.note { background: rgba(254, 243, 199, 0.6); border-left: 3px solid #d97706; padding: 12px 16px; border-radius: 4px; font-size: 12.5px; color: #78350f; margin: 16px 0; backdrop-filter: blur(10px); }
.note-success { background: rgba(209, 250, 229, 0.6); border-left: 3px solid #10b981; padding: 12px 16px; border-radius: 4px; font-size: 12.5px; color: #064e3b; margin: 16px 0; backdrop-filter: blur(10px); }
.note-info { background: rgba(219, 234, 254, 0.6); border-left: 3px solid #3b82f6; padding: 12px 16px; border-radius: 4px; font-size: 12.5px; color: #1e3a8a; margin: 16px 0; backdrop-filter: blur(10px); }
table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 12.5px; }
thead tr { background: rgba(255, 255, 255, 0.6); }
th { border: 1px solid rgba(99, 102, 241, 0.15); padding: 8px; text-align: left; font-weight: 600; }
td { border: 1px solid rgba(99, 102, 241, 0.15); padding: 8px; }
</style>
</head>
<body>
<svg width="0" height="0" style="position: absolute;">
<defs>
<linearGradient id="g-emoji" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#fde68a"/><stop offset="60%" stop-color="#fbbf24"/><stop offset="100%" stop-color="#d97706"/>
</linearGradient>
<radialGradient id="g-emoji-blush" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="rgba(244, 63, 94, 0.4)"/>
<stop offset="100%" stop-color="rgba(244, 63, 94, 0)"/>
</radialGradient>
<linearGradient id="g-highlight" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="white" stop-opacity="0.7"/>
<stop offset="100%" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="g-doc-body" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#ffffff"/><stop offset="100%" stop-color="#dbeafe"/>
</linearGradient>
<linearGradient id="g-doc-fold" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#c7d2fe"/><stop offset="100%" stop-color="#6366f1"/>
</linearGradient>
<linearGradient id="g-doc-text" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#a855f7"/>
</linearGradient>
<linearGradient id="g-mic-body" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#c4b5fd"/><stop offset="50%" stop-color="#8b5cf6"/><stop offset="100%" stop-color="#6d28d9"/>
</linearGradient>
<linearGradient id="g-group" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#a5b4fc"/><stop offset="100%" stop-color="#c4b5fd"/>
</linearGradient>
<linearGradient id="g-group-strong" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6366f1"/><stop offset="50%" stop-color="#a855f7"/><stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
<!-- 宝石轨道渐变 -->
<linearGradient id="g-rail" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#818cf8" stop-opacity="0.55"/>
<stop offset="50%" stop-color="#c4b5fd" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#818cf8" stop-opacity="0.55"/>
</linearGradient>
<linearGradient id="g-rail-gloss" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="white" stop-opacity="0.85"/>
<stop offset="45%" stop-color="white" stop-opacity="0.15"/>
<stop offset="100%" stop-color="white" stop-opacity="0"/>
</linearGradient>
<radialGradient id="g-gem-cradle" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="rgba(99,102,241,0.35)"/>
<stop offset="100%" stop-color="rgba(99,102,241,0)"/>
</radialGradient>
</defs>
</svg>
<h1>工具栏统一设计 v1.5 — 宝石嵌入式工具栏</h1>
<p class="meta">v1.4 基础(5 按钮、坐席居中)+ 工具栏重构为「两端窄、中间隆起」的镜片轨道,人工坐席头像像宝石一样镶嵌于隆起处 · 2026-08-05</p>
<div class="note-info">
💎 <strong>v1.5 改动说明(基于 v1.4</strong><br>
<strong>工具栏形态重构</strong>:由扁平胶囊改为「镜片/花瓣形轨道」——两端上下窄,向中间平滑隆起<br>
<strong>人工坐席 = 宝石</strong>:头像按钮(46px)嵌在轨道中央隆起处,上半部微微探出轨道边缘,像镶在托座里的宝石(带白色内环 + 紫色光晕 + 左上切面高光)<br>
<strong>4 个工具按钮</strong>emoji / 文件 / 语音 / 群聊)分布在隆起两侧的纤细轨道上,左右对称<br>
• 按钮数仍为 <strong>5</strong>emoji / 文件 / <strong>坐席(宝石·居中)</strong> / 语音 / 群聊),仅形态与视觉权重变化<br>
• IntegrationZone.vue 仍保持 v1.3 已删除坐席按钮的状态
</div>
<h2>🎯 v1.5 目标对比(Before / After</h2>
<div class="demo-block">
<div class="compare-grid">
<!-- BEFORE: v1.4 扁平胶囊 -->
<div class="compare-panel before">
<div class="compare-title">v1.4(上一版:扁平胶囊,居中)</div>
<div style="text-align: center; padding: 12px; background: rgba(254, 243, 199, 0.4); border-radius: 10px;">
<div class="glass-toolbar" style="margin-bottom: 8px;">
<button class="glass-btn" title="表情">
<svg viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="9" fill="url(#g-emoji)"/>
<ellipse cx="12" cy="6" rx="6" ry="2.5" fill="url(#g-highlight)"/>
<ellipse cx="6.5" cy="14" rx="1.5" ry="1" fill="url(#g-emoji-blush)"/>
<ellipse cx="17.5" cy="14" rx="1.5" ry="1" fill="url(#g-emoji-blush)"/>
<ellipse cx="8.5" cy="11" rx="1.4" ry="1.6" fill="#1e1b4b"/>
<ellipse cx="15.5" cy="11" rx="1.4" ry="1.6" fill="#1e1b4b"/>
<circle cx="8.9" cy="10.4" r="0.5" fill="white"/>
<circle cx="15.9" cy="10.4" r="0.5" fill="white"/>
<path d="M8 15 Q12 18 16 15" fill="none" stroke="#1e1b4b" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</button>
<button class="glass-btn" title="文件">
<svg viewBox="0 0 24 24" fill="none">
<path d="M4 2 L14 2 L20 8 L20 20 L4 20 Z" fill="url(#g-doc-body)" stroke="#818cf8" stroke-width="0.8"/>
<path d="M14 2 L14 8 L20 8" fill="url(#g-doc-fold)" stroke="#818cf8" stroke-width="0.8"/>
<rect x="7" y="11.5" width="10" height="0.8" rx="0.4" fill="url(#g-doc-text)"/>
</svg>
</button>
<button class="agent-btn" title="客服在线">
<img src="./agent-avatar-v0.8.jpg" alt="客服"/>
<span class="agent-badge is-online"></span>
</button>
<button class="glass-btn" title="语音输入">
<svg viewBox="0 0 24 24" fill="none">
<rect x="8" y="2" width="8" height="13" rx="4" fill="url(#g-mic-body)"/>
<path d="M3 12 Q3 18 12 18 Q21 18 21 12" fill="none" stroke="url(#g-mic-body)" stroke-width="2"/>
<line x1="12" y1="18" x2="12" y2="22" stroke="url(#g-mic-body)" stroke-width="2"/>
</svg>
</button>
<button class="glass-btn" title="群聊">
<svg viewBox="0 0 24 24" fill="none">
<circle cx="8" cy="9" r="2.2" fill="url(#g-group)"/>
<path d="M5 16 Q5 13 8 13 Q11 13 11 16 Z" fill="url(#g-group)"/>
<circle cx="16" cy="9" r="2.2" fill="url(#g-group)"/>
<path d="M13 16 Q13 13 16 13 Q19 13 19 16 Z" fill="url(#g-group)"/>
<circle cx="12" cy="10.5" r="2.8" fill="url(#g-group-strong)"/>
<path d="M8.5 18.5 Q8.5 14 12 14 Q15.5 14 15.5 18.5 Z" fill="url(#g-group-strong)"/>
</svg>
</button>
</div>
<div class="inputbar-mock-field" style="min-height: 36px; padding: 6px 12px; font-size: 12px;">请输入消息...</div>
</div>
<div style="font-size: 12px; color: #92400e; margin-top: 8px;">
⚠️ <strong>现状</strong>: 均匀扁平胶囊,坐席仅为其中一枚普通按钮,缺乏视觉焦点
</div>
</div>
<!-- AFTER: v1.5 宝石嵌入 -->
<div class="compare-panel after">
<div class="compare-title">v1.5(目标:宝石嵌入式)</div>
<div style="text-align: center; padding: 12px; background: rgba(209, 250, 229, 0.4); border-radius: 10px;">
<!-- 宝石嵌入式工具栏 -->
<div class="gem-toolbar">
<svg class="gem-toolbar-bg" viewBox="0 0 242 84" preserveAspectRatio="xMidYMid meet">
<!-- 隆起处的宝石托座光晕 -->
<ellipse cx="121" cy="42" rx="40" ry="34" fill="url(#g-gem-cradle)"/>
<!-- 镜片/花瓣形轨道主体 -->
<path d="M 18 30
C 55 30, 95 14, 121 14
C 147 14, 187 30, 224 30
L 224 54
C 187 54, 147 70, 121 70
C 95 70, 55 54, 18 54 Z"
fill="url(#g-rail)" stroke="rgba(255,255,255,0.85)" stroke-width="1.2"/>
<!-- 轨道顶部高光(玻璃质感) -->
<path d="M 18 30
C 55 30, 95 14, 121 14
C 147 14, 187 30, 224 30"
fill="none" stroke="url(#g-rail-gloss)" stroke-width="3" stroke-linecap="round"/>
</svg>
<div class="gem-row">
<button class="glass-btn" title="表情">
<svg viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="9" fill="url(#g-emoji)"/>
<ellipse cx="12" cy="6" rx="6" ry="2.5" fill="url(#g-highlight)"/>
<ellipse cx="6.5" cy="14" rx="1.5" ry="1" fill="url(#g-emoji-blush)"/>
<ellipse cx="17.5" cy="14" rx="1.5" ry="1" fill="url(#g-emoji-blush)"/>
<ellipse cx="8.5" cy="11" rx="1.4" ry="1.6" fill="#1e1b4b"/>
<ellipse cx="15.5" cy="11" rx="1.4" ry="1.6" fill="#1e1b4b"/>
<circle cx="8.9" cy="10.4" r="0.5" fill="white"/>
<circle cx="15.9" cy="10.4" r="0.5" fill="white"/>
<path d="M8 15 Q12 18 16 15" fill="none" stroke="#1e1b4b" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</button>
<button class="glass-btn" title="文件">
<svg viewBox="0 0 24 24" fill="none">
<path d="M4 2 L14 2 L20 8 L20 20 L4 20 Z" fill="url(#g-doc-body)" stroke="#818cf8" stroke-width="0.8"/>
<path d="M14 2 L14 8 L20 8" fill="url(#g-doc-fold)" stroke="#818cf8" stroke-width="0.8"/>
<rect x="7" y="11.5" width="10" height="0.8" rx="0.4" fill="url(#g-doc-text)"/>
</svg>
</button>
<!-- 人工坐席 = 宝石(居中隆起处) -->
<button class="agent-btn gem" title="客服在线">
<img src="./agent-avatar-v0.8.jpg" alt="客服"/>
<span class="agent-badge is-online"></span>
</button>
<button class="glass-btn" title="语音输入">
<svg viewBox="0 0 24 24" fill="none">
<rect x="8" y="2" width="8" height="13" rx="4" fill="url(#g-mic-body)"/>
<path d="M3 12 Q3 18 12 18 Q21 18 21 12" fill="none" stroke="url(#g-mic-body)" stroke-width="2"/>
<line x1="12" y1="18" x2="12" y2="22" stroke="url(#g-mic-body)" stroke-width="2"/>
</svg>
</button>
<button class="glass-btn" title="群聊">
<svg viewBox="0 0 24 24" fill="none">
<circle cx="8" cy="9" r="2.2" fill="url(#g-group)"/>
<path d="M5 16 Q5 13 8 13 Q11 13 11 16 Z" fill="url(#g-group)"/>
<circle cx="16" cy="9" r="2.2" fill="url(#g-group)"/>
<path d="M13 16 Q13 13 16 13 Q19 13 19 16 Z" fill="url(#g-group)"/>
<circle cx="12" cy="10.5" r="2.8" fill="url(#g-group-strong)"/>
<path d="M8.5 18.5 Q8.5 14 12 14 Q15.5 14 15.5 18.5 Z" fill="url(#g-group-strong)"/>
</svg>
</button>
</div>
</div>
<div class="inputbar-mock-field" style="min-height: 36px; padding: 6px 12px; font-size: 12px;">请输入消息...</div>
</div>
<div style="font-size: 12px; color: #065f46; margin-top: 8px;">
<strong>效果</strong>: 纤细轨道两端窄、中央隆起托住宝石坐席,形成明确视觉焦点,工具按钮对称分布两侧
</div>
</div>
</div>
</div>
<h2>📊 v1.4 vs v1.5 改动对比</h2>
<table>
<thead>
<tr>
<th>维度</th>
<th>v1.4(扁平胶囊)</th>
<th><strong>v1.5(宝石嵌入式)</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>工具栏形态</strong></td>
<td>均匀扁平胶囊(border-radius 20px</td>
<td><strong>镜片/花瓣形轨道:两端上下窄,中央平滑隆起</strong></td>
</tr>
<tr>
<td><strong>人工坐席按钮</strong></td>
<td>普通头像按钮,居中但平铺</td>
<td><strong>宝石:嵌于中央隆起,上半探出轨道,带白环+紫晕+切面高光</strong></td>
</tr>
<tr>
<td><strong>工具按钮位置</strong></td>
<td>均匀分布在胶囊内</td>
<td><strong>对称分布在隆起两侧的纤细轨道上</strong></td>
</tr>
<tr>
<td><strong>视觉焦点</strong></td>
<td>弱(5 按钮等权)</td>
<td><strong>强(坐席=宝石,天然视觉中心)</strong></td>
</tr>
<tr>
<td><strong>按钮数</strong></td>
<td>5</td>
<td><strong>5 不变</strong></td>
</tr>
</tbody>
</table>
<h2>🛠️ 需要修改的文件清单</h2>
<table>
<thead>
<tr>
<th>文件</th>
<th>改动</th>
<th>影响</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>src/frontend-h5/src/components/chat/InputBar.vue</code></td>
<td><strong></strong>:工具栏容器改为镜片轨道(SVG 背景 + flex 行);坐席按钮加 .gem 样式(translateY 镶嵌 + 白环/紫晕/高光);工具按钮改为对称分布</td>
<td>本文件工具栏局部重构</td>
</tr>
<tr>
<td><code>src/frontend-h5/src/components/chat/IntegrationZone.vue</code></td>
<td><strong>不动</strong>v1.3 已删除坐席按钮)</td>
<td></td>
</tr>
<tr>
<td><code>inputBarCallAgentState.ts</code> / <code>integrationZoneLogic.ts</code></td>
<td><strong>不动</strong>helper 保留)</td>
<td></td>
</tr>
<tr>
<td><code>InputBar.test.ts</code></td>
<td>补充轨道形态 + 坐席居中镶嵌的快照/结构测试</td>
<td>测试同步更新</td>
</tr>
</tbody>
</table>
<h2>⚠️ 注意事项与待确认点</h2>
<ol style="margin-left: 20px; line-height: 1.8; font-size: 13px;">
<li><strong>轨道弧度</strong>:当前为平滑镜片曲线;若想要更「尖」的叶子形或更「圆」的胶囊,可调 SVG path 控制点</li>
<li><strong>宝石探出量</strong>:坐席按钮 <code>translateY(-18px)</code> 控制探出高度,可调大/调小</li>
<li><strong>宝石光晕</strong>:白环(3px)+紫晕(5px)+外发光,若觉得过重可减小或去掉紫晕</li>
<li><strong>工具按钮在纤细轨道上的溢出</strong>:两端轨道高约 24px、按钮 40px,按钮会略探出轨道上下边缘——这是「图标浮于细轨」的预期效果;如想完全收进轨道,可加高两端或缩小按钮</li>
<li><strong>头像占位图</strong><code>./agent-avatar-v0.8.jpg</code> 为占位路径,实际集成时替换</li>
</ol>
<p style="margin-top: 32px; padding-top: 16px; border-top: 1px solid rgba(99, 102, 241, 0.15); color: #6b7280; font-size: 11.5px;">
v1.5 宝石嵌入式工具栏 · 2026-08-05 · Duckula 主理人 · 等待用户拍板进入开发
</p>
</body>
</html>
@@ -1,12 +1,12 @@
# PRD — 坐席端统一工作队列重构
> **REQ编号**: REQ-坐席-011
> **版本**: v0.1(方案草案 / 沟通确认阶段)
> **版本**: v0.1(方案草案 / 沟通确认阶段C-8 决策见 §6.4
> **状态**: 🟡 待评审 — 含未决项,不得据此排期开发
> **优先级**: P1(其中 Phase 0 为 P0
> **日期**: 2026-08-08
> **作者**: 宋献
> **关联**: REQ-坐席-004(任务详情视图切换)、REQ-坐席-009(会话状态Tab筛选)、任务说明书 #132
> **关联**: REQ-坐席-004(任务详情视图切换)、REQ-坐席-009(会话状态Tab筛选)、任务说明书 #132、原型 `原型-REQ-坐席-000-坐席工作台-v1.8.html`v1.8 演进)
---
@@ -66,22 +66,61 @@ D-3 不是筛选条件的调整,而是新增一条状态机路径。当前 `to
---
## 3. 阻塞项(P0
## 3. 阻塞项(原 P02026-08-09 修订为 P1
### 3.1 任务详情操作按钮全部为 Mock
### 3.1 任务详情操作按钮无服务端闭环(原「全部为 Mock」,已部分修复)
```js
// src/frontend-agent/src/components/chat/TaskDetailView.vue:117
> **📝 2026-08-09 修订**:本节原记录 `TaskDetailView.vue:117` 为无条件 `ElMessage.success` 的 Mock 实现。
> 经架构师代码核查(`技术核查-坐席会话条目操作菜单-v1.0.md` §1.1),**该无条件成功提示已被修复**,行号亦已变更。
> 原描述已过期,现更新如下,避免后续评审基于过期事实。
#### 3.1.1 当前实际代码
```ts
// src/frontend-agent/src/components/chat/TaskDetailView.vue:129-1352026-08-09 时点)
function handleAction(action: string): void {
ElMessage.success(`操作成功:${action}`) // 仅 toast,不调用任何接口
if (props.todoItem.type === 'approval') {
// 企微不支持服务端代审批,跳转由 ApprovalDetail 的 <a target="_blank"> 原生完成
console.info('[TaskDetailView] 审批动作已跳转企微审批原系统:', action)
return
}
ElMessage.info('该操作需在原系统中完成')
}
```
REQ-坐席-004 §2.4 定义的全部操作 —— 工单(接单 / 开始处理 / 结单 / 转派)、审批(通过 / 拒绝 / 转交)—— **均不生效**
**修复内容**:无条件 `ElMessage.success` 已移除,改为按 `type` 分支——
- `type === 'approval'` → 静默返回(跳转由 `ApprovalDetail``<a target="_blank">` 原生完成),不再弹成功提示;
- 其他类型(工单等) → `ElMessage.info('该操作需在原系统中完成')`,中性提示,不谎报成功。
**风险定级:P0,阻塞布局合并。**
#### 3.1.2 修订后的结论
理由:当前待办面板位于右栏底部 260px 区域,坐席误操作的暴露面有限。一旦将待办提升至左栏主队列首屏,等同于把不可用功能放置于最高可见度位置。坐席点击「审批通过」后收到绿色成功提示,而企业微信侧该单仍处于挂起状态 —— 属于会造成真实业务后果的错误反馈。
| 项 | 原结论(v0.1 初稿) | 现结论(2026-08-09 核查后) |
|---|---|---|
| 是否谎报成功 | 是(无条件绿色成功 toast) | ❌ **否,已修复**。审批静默跳转,其他类型为中性 info |
| 是否服务端闭环 | 否 | ❌ **仍否**。审批为降级跳转 + 回调回写(U-1 已定),工单受 U-1.1 / U-1.2 外部阻塞 |
| 风险定级 | 🔴 P0,阻塞布局合并 | 🟡 **降级为 P1**,不再是"错误反馈"型 P0,但**服务端闭环缺失依然阻塞 Phase 2 的工单/审批混排**(Phase 0 的降级跳转 + 回写仍须先落地) |
**剩余阻塞点**(未因本次修复而消解):
- 审批:`approval.py:902``sys_approval_change` 回调状态回写尚未补全 → 跳转后前端无法感知外部状态变更(Phase 0 待办项);
- 工单:`ITSMService.get_todo_list()` 无条件 `return []`(U-1.2),读链路即断,写接口更无从谈起(U-1.1 / T05)。
#### 3.1.3 🔴 与会话侧操作的性质差异(Phase 2 组件抽象的核心依据)
| 维度 | 会话侧 4 操作(接单/置顶/代办/转接) | 任务侧操作(审批/工单) |
|---|---|---|
| 闭环性质 | **服务端真闭环**`UserInfoBar.vue:116-164``ChatArea.vue` handler → `stores/conversation.ts:498-571``api/conversation.ts:176-244` → 后端 `conversations.py` 六端点,**全链路实证可用** | **降级跳转**(审批)/ **当前无对象**(工单) |
| 执行路径 | store → API → 后端 → `fetchConversations()` 刷新 | `<a target="_blank">` 跳外部系统 → 回调回写 → WS 推送 |
| 反馈模型 | **同步**成功/失败 | **异步最终一致** |
| 后端权限 | `assign`/`transfer`/`grab` = `update:all``resolve`/`pin`/`todo` = `update:own` | 不适用 |
> **这一差异是 Phase 2 组件抽象不可回避的约束**:左栏统一容器上的操作菜单**必须按 `kind` 分派反馈策略**,菜单项定义需携带 `execMode: 'direct' | 'redirect' | 'disabled'` 字段。
> **若三类工作项共用一套「点击 → toast 成功」的反馈模型,就会以另一种形式重演本节原先定性为 P0 的那个错误。**
> 具体设计见 `PRD-REQ-坐席-012-会话条目操作菜单-v1.0.md` §7.2。
**风险定级:🟡 P1(由 P0 降级)。**
降级理由:谎报成功的错误反馈已消除,坐席不再会因绿色提示误判外部系统已变更。
**但仍阻塞 Phase 2**:待办提升至左栏主队列首屏后,若操作仍只是"提示到原系统操作",等同于把无实际能力的入口放在最高可见度位置——Phase 0 的降级跳转 + 状态回写仍是 Phase 2 的硬前置(§6 D-2 顺序不变)。
---
@@ -123,7 +162,7 @@ REQ-坐席-004 §2.4 定义的全部操作 —— 工单(接单 / 开始处理
|---|---|---|
| 审批操作 → 降级跳转 + 回写 | 通过 / 拒绝 / 转交 → 点击「在企微审批中打开」跳转原系统,由 `approval_webhook.py` 接收 `sys_approval_change` 回调 → 补全 `approval.py:902` 状态回写 → WS 推送 | Level 0 立即可做;Level 1 需补回调回写 |
| 工单操作 → 降级跳转(暂) | 接单 / 开始处理 / 结单 / 转派 → 点击「在 ITSM 中打开」跳转原系统;ITSM 写接口到位前不承诺服务端闭环(受 T05 外部阻塞)。**⚠️ 注意:该跳转依赖工单可见,而当前 ITSM 读列表亦未实现(见 U-1.2),故本行在读链路打通前无实际可操作对象** | Level 0(暂,受 U-1.2 前置) |
| 失败态处理 | 移除无条件 `ElMessage.success`改为「跳转成功提示 + 状态回写后刷新」按三态分派;仍禁止以 toast 作为验收依据 | — |
| 失败态处理 | ~~移除无条件 `ElMessage.success`~~**已完成**2026-08-09 核查确认,见 §3.1.1)。**剩余部分**:改为「跳转成功提示 + 状态回写后刷新」按三态分派(当前仅做到中性 info,尚无回写后刷新);仍禁止以 toast 作为验收依据 | — |
**验收标准**:操作后外部系统(ITSM / 企微审批)**状态真实变更**,且前端反馈与外部状态**最终一致**(通过回调 / webhook 回写达成)。禁止以 toast 成功作为验收依据。审批 / 工单在降级跳转模式下,以「跳转成功 + 原系统状态通过回调回写并刷新」作为验收闭环,不要求前端内嵌直接操作。
@@ -158,6 +197,36 @@ REQ-坐席-004 §2.4 定义的全部操作 —— 工单(接单 / 开始处理
**观测指标**:会话首响时长(是否因混排而劣化)、待办平均处理时长、坐席 Tab 切换频次、认领冲突率。
### Phase 0.5 — TaskDetailView 操作区精简(C-8 决策,2026-08-10
**背景**:原 `TaskDetailView` 操作区固定渲染 4 按钮(接单 / 开始处理 / 结单 / 转派),垂直占 body 区 50%+ 空间,导致「处理进度 / SLA」等关键状态信息需滚动才看完。
**设计变更**:状态机驱动 + 二级收纳
| Task Type | 主操作按钮(按状态) | ⋯ 次要动作 |
|---|---|---|
| 工单 queued | 📥 接单 | 转派 / 挂起 |
| 工单 serving | ✅ 结单 | 转派 / 挂起 |
| 审批 pending | ✅ 审批通过 | 拒绝审批 / 转交审批 / 加签 |
| 设备异常 | ✅ 标记恢复 | 一键开单 / 派工 / 加入巡检计划 |
**收益**
- 操作区按钮数 4 → 1~2(-50%)
- body 区可用垂直高度 +50%↑
- 状态信息一眼可见,无需滚动
**状态机原则**:开始处理被吸收到状态推进逻辑(点「接单」后接单 + 开始处理隐式完成,不显式拆分)。
**实现细节**
- 前端:组件 `useConversationMenuItems.ts` 状态机新增 `claim / recover / approve` reducer
- 后端:**不变**(仅前端状态机调整 + ⋯ 菜单 UI)
- 视觉密度补偿:主按钮 padding 从 `8×18``8×22`
**影响范围**
- ✅ 不影响:左栏三点菜单(v1.7 §4.4 矩阵)、中栏顶栏 UserInfoBarv1.7 D-2 已删 3 留 1
- ⚠️ 涉及文件:`TaskDetailView.vue:128-135``useConversationMenuItems.ts`
- 📐 原型版本:v1.7 → v1.8(已落,决策依据详见原型变更说明段)
---
## 7. 未决项
@@ -203,3 +272,7 @@ REQ-坐席-004 §2.4 定义的全部操作 —— 工单(接单 / 开始处理
| 2026-08-08 | v0.1 | 创建方案草案;固化 D-1/D-2/D-3 三项决策;完成原始论据核查与现状事实核实 | 宋献 |
| 2026-08-08 | v0.1 | 据 U-1 技术验证结论(架构师高见远)修订 §6 Phase 0 与 §7 U-1:审批明确为降级跳转 + 回写(U-1 已验证);新增 U-1.1 ITSM 写接口外部阻塞项;同步下调 R-5 风险表述 | 宋献 |
| 2026-08-08 | v0.1 | 主理人复核补录 **U-1.2**:代码实证 `ITSMService.get_todo_list()` 无条件返回空列表,ITSM 工单在坐席端读链路即断、当前待办全为企微审批单。该项前置于 U-1.1,一并标注于 §6 Phase 0 工单行 | 齐活林(交付总监) |
| 2026-08-09 | v0.1 | **修正 §3.1(原描述已过期)**`TaskDetailView.vue` 的无条件 `ElMessage.success` 已被修复,行号由 `:117` 更新为 `:129-135`,现为按 `type` 分支(approval 静默跳企微 / 其他 `ElMessage.info('该操作需在原系统中完成')`)。风险由 🔴 P0 降级为 🟡 P1(谎报成功已消除,服务端闭环缺失仍阻塞 Phase 2)。新增 §3.1.3 明确「**会话侧 4 操作是真闭环,任务侧审批/工单是降级跳转,二者性质根本不同**」,并将其确立为 Phase 2 按 `kind` 分派反馈策略(`execMode` 字段)的核心依据,指向 PRD-REQ-坐席-012 §7.2。依据:`技术核查-坐席会话条目操作菜单-v1.0.md` §1.1 | 许清楚(产品经理) |
| 2026-08-09 | v0.1 | **Phase 0 审批线落地**:T01(前端降级跳转)+ T02(后端 `/approval/callback` + `approval_webhook` 回调回写)已实现并通过 QA 独立回归(51 例全绿,前端 type-check 改动文件干净,无源码缺陷)。代码已 commit `9292f41` 于分支 `feat/agent-approval-degrade-jump`,并已推送 Gitea(远端 SHA 与本地一致,`main` 未被改写)。PR 入口:<http://192.168.3.200:8418/simon/wecom_it_smart_desk/pulls/new/feat/agent-approval-degrade-jump>。**工单线(T03+)仍未启动**,受 U-1.2ITSM 读链路断裂)+ U-1.1(写接口缺失)外部阻塞 | 寇豆码(工程师)→ 严过关(QA)→ 齐活林(主理人编排) |
| 2026-08-10 | v0.1 | **C-8 决策**TaskDetailView 操作区由原版固定 4 按钮(接单/开始处理/结单/转派)重构为「状态驱动主操作按钮 + ⋯ 次要动作收纳」。依据:操作区占 body 区底部 50%+ 垂直空间,导致「处理进度 / SLA」等状态信息需滚动才看完。详见新增 §6.4。原型 v1.7 → v1.8,代码待 `useConversationMenuItems.ts` 状态机 reducer 改造。**不影响**:左栏三点菜单(v1.7 §4.4 矩阵)、中栏顶栏 UserInfoBar、后端 API | 宋献 |
| 2026-08-10 | v0.1 | **PR #5 已合并**commit `af87f1de` redis_client 修复(approval.py + byod.py)经 Gitea UI 合并为双亲 merge `d8e7dbe`main 现位于 `5db3079d`。本地 main 已同步。审批回调 `POST /approval/callback` 生产实测 HTTP 200 `{errcode:0}` | 齐活林 → 宋献(合并)|