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-*/
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,174 @@
|
||||
|
||||
===== DIAG login-form-missing @ 00:48:26 =====
|
||||
URL: http://localhost:5174/itdesk/
|
||||
HTML_LEN: 2595
|
||||
<!DOCTYPE html><html lang="zh-CN"><head>
|
||||
<script type="module" src="/itdesk/@vite/client"></script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<!-- 移动端视口设置(适配企微 WebView) -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<!-- CSP 安全策略 -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-eval' https://res.wx.qq.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https: http:; connect-src 'self' https://qyapi.weixin.qq.com wss://* ws://localhost ws://127.0.0.1;">
|
||||
<!-- 页面标题 -->
|
||||
<title>智能IT支持服务台</title>
|
||||
<!-- 首屏骨架屏样式 v0.5.2 强化版 -->
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #f7f8fa; }
|
||||
#app-skeleton {
|
||||
position: fixed; inset: 0;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
|
||||
color: #969799;
|
||||
z-index: 9999;
|
||||
}
|
||||
#app-skeleton .logo {
|
||||
width: 64px; height: 64px; border-radius: 16px;
|
||||
background: linear-gradient(135deg, #1989fa 0%, #1c64f2 100%);
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 8px 24px rgba(25, 137, 250, 0.3);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
#app-skeleton .title { font-size: 18px; font-weight: 600; color: #323233; margin-bottom: 8px; }
|
||||
#app-skeleton .subtitle { font-size: 14px; color: #969799; margin-bottom: 28px; }
|
||||
#app-skeleton .spinner {
|
||||
width: 28px; height: 28px;
|
||||
border: 3px solid #ebedf0;
|
||||
border-top-color: #1989fa;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.95); } }
|
||||
/* v0.5.2 强化:不再依赖 :empty 选择器(部分浏览器/Vue mount 太快会失效) */
|
||||
/* 改用 body.app-loaded 类名,由 main.ts 挂载后主动添加 */
|
||||
body.app-loaded #app-skeleton { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Vue 应用挂载点 -->
|
||||
<div id="app"></div>
|
||||
<!-- 首屏骨架屏(JS 加载期间显示,挂载后自动隐藏) v0.5.2 -->
|
||||
<div id="app-skeleton">
|
||||
<div class="logo"></div>
|
||||
<div class="title">智能IT支持服务台</div>
|
||||
<div class="subtitle">正在加载...</div>
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<!-- 入口脚本 -->
|
||||
<script type="module" src="/itdesk/src/main.ts"></script>
|
||||
|
||||
|
||||
<vite-error-overlay></vite-error-overlay></body></html>
|
||||
|
||||
===== DIAG fatal @ 00:48:26 =====
|
||||
URL: http://localhost:5174/itdesk/
|
||||
HTML_LEN: 2595
|
||||
<!DOCTYPE html><html lang="zh-CN"><head>
|
||||
<script type="module" src="/itdesk/@vite/client"></script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<!-- 移动端视口设置(适配企微 WebView) -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<!-- CSP 安全策略 -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-eval' https://res.wx.qq.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https: http:; connect-src 'self' https://qyapi.weixin.qq.com wss://* ws://localhost ws://127.0.0.1;">
|
||||
<!-- 页面标题 -->
|
||||
<title>智能IT支持服务台</title>
|
||||
<!-- 首屏骨架屏样式 v0.5.2 强化版 -->
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #f7f8fa; }
|
||||
#app-skeleton {
|
||||
position: fixed; inset: 0;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
|
||||
color: #969799;
|
||||
z-index: 9999;
|
||||
}
|
||||
#app-skeleton .logo {
|
||||
width: 64px; height: 64px; border-radius: 16px;
|
||||
background: linear-gradient(135deg, #1989fa 0%, #1c64f2 100%);
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 8px 24px rgba(25, 137, 250, 0.3);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
#app-skeleton .title { font-size: 18px; font-weight: 600; color: #323233; margin-bottom: 8px; }
|
||||
#app-skeleton .subtitle { font-size: 14px; color: #969799; margin-bottom: 28px; }
|
||||
#app-skeleton .spinner {
|
||||
width: 28px; height: 28px;
|
||||
border: 3px solid #ebedf0;
|
||||
border-top-color: #1989fa;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.95); } }
|
||||
/* v0.5.2 强化:不再依赖 :empty 选择器(部分浏览器/Vue mount 太快会失效) */
|
||||
/* 改用 body.app-loaded 类名,由 main.ts 挂载后主动添加 */
|
||||
body.app-loaded #app-skeleton { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Vue 应用挂载点 -->
|
||||
<div id="app"></div>
|
||||
<!-- 首屏骨架屏(JS 加载期间显示,挂载后自动隐藏) v0.5.2 -->
|
||||
<div id="app-skeleton">
|
||||
<div class="logo"></div>
|
||||
<div class="title">智能IT支持服务台</div>
|
||||
<div class="subtitle">正在加载...</div>
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<!-- 入口脚本 -->
|
||||
<script type="module" src="/itdesk/src/main.ts"></script>
|
||||
|
||||
|
||||
<vite-error-overlay></vite-error-overlay></body></html>
|
||||
|
||||
===== DIAG fatal @ 00:51:07 =====
|
||||
URL: http://localhost:5174/itdesk/login
|
||||
HTML_LEN: 158173
|
||||
<!DOCTYPE html><html lang="zh-CN" class="van-theme-light" data-theme="light"><head>
|
||||
<script type="module" src="/itdesk/@vite/client"></script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<!-- 移动端视口设置(适配企微 WebView) -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<!-- CSP 安全策略 -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-eval' https://res.wx.qq.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https: http:; connect-src 'self' https://qyapi.weixin.qq.com wss://* ws://localhost ws://127.0.0.1;">
|
||||
<!-- 页面标题 -->
|
||||
<title>智能IT支持服务台</title>
|
||||
<!-- 首屏骨架屏样式 v0.5.2 强化版 -->
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #f7f8fa; }
|
||||
#app-skeleton {
|
||||
position: fixed; inset: 0;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
|
||||
color: #969799;
|
||||
z-index: 9999;
|
||||
}
|
||||
#app-skeleton .logo {
|
||||
width: 64px; height: 64px; border-radius: 16px;
|
||||
background: linear-gradient(135deg, #1989fa 0%, #1c64f2 100%);
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 8px 24px rgba(25, 137, 250, 0.3);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
#app-skeleton .title { font-size: 18px; font-weight: 600; color: #323233; margin-bottom: 8px; }
|
||||
#app-skeleton .subtitle { font-size: 14px; color: #969799; margin-bottom: 28px; }
|
||||
#app-skeleton .spinner {
|
||||
width: 28px; height: 28px;
|
||||
border: 3px solid #ebedf0;
|
||||
border-top-color: #1989fa;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.95); } }
|
||||
/* v0.5.2 强化:不再依赖 :empty 选择器(部分浏览器/Vue mount 太快会失效) */
|
||||
/* 改用 body.app-loaded 类名,由 main.ts 挂载后主动添加 */
|
||||
body.app-loaded #app-skeleton { display: none !important; }
|
||||
</style>
|
||||
<style type="text/css" data-vite-dev-id="/app/node_modules/.pnpm/vant@4.10.0_vue@3.5.39_typescript@5.9.3_/node_modules/vant/es/style/base.css">:root,:host{--van-black: #000;--van-white: #fff;--van-gray-1: #f7f8fa;--van-gray-2: #f2f3f5;--van-gray-3: #ebedf0;--van-gray-4: #dcdee0;--van-gray-5: #c8c9cc;--van-gray-6: #969799;--van-gray-7: #646566;--van-gray-8: #323233;--van-red: #ee0a24;--van-blue: #1989fa;--van-orange: #ff976a;--van-orange-dark: #ed6a0c;--van-orange-light: #fffbe8;--van-green: #07c160;--van-gradient-red: linear-gradient(to right, #ff6034, #ee0a24);--van-gradient-orange: linear-gradient(to right, #ffd01e, #ff8917);--van-primary-color: var(--van-blue);--van-success-color: var(--van-green);--van-danger-color: var(--van-red);--van-warning-color: var(--van-orange);--van-text-col
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"employee_id": "E2E_BROWSER",
|
||||
"ws_open_after_login": 2,
|
||||
"send_click_s": 1.257,
|
||||
"send_ui_instant": true,
|
||||
"ai_first_render_s": 2.27,
|
||||
"final_text_len": 1469,
|
||||
"final_has_duckula": true,
|
||||
"final_has_real_content": true,
|
||||
"len_samples": [
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
880,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469,
|
||||
1469
|
||||
],
|
||||
"len_grew": true,
|
||||
"ws_open_total": 2,
|
||||
"ws_ai_chunk_frames": 343,
|
||||
"typewriter_proven": true,
|
||||
"console_errors": [
|
||||
"Loading the font 'data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAGNAAA0AAAAA6ngAAGLlAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGh4GYACCWhEICoOqHILKFAuEDgABNgIkA4QUBCAFhQ4HllAbe7dFB2rYOIAxOG/nKOrEpKWbGbVlVHRZ9v816Tis0RbhPC4JZQk1ws72WlBGJJIsL3bc5Y/x5HdtBrzwoZQX/Ls/uAhsXMZIVk73Ds/ntvd3cezvhO1/2HExro3B2ID/4d7GxjXObZwqxy0gG8pQPDBFMAW980hTNIuhpqZleVwJlHSKR6WkDM3KECuz083Qu+8BCnabd+4tsemRGtBnHBAHxuTmuLWNUbd7fuSZA88fOBlzekCqfDPnV1BArpTKjp/r0AfE0+Lc97SXNa3ugaSqW2AfIo5Ghr2YAos8H+krfQ3L8DwA+F1V4Mecr9JV2ljSM/wUzQWAITlxoJSfEmfsi321rwHH2T...LQReO8YZ6mjq2e6QIdzdGLzR8ziEWSLVQd2NoiYGkRGaMXLCt0oq7fOWoCTc11KgkMttjj0jFJ6zIc7BafifCk0pZOhSbraAopcEpG4/kOwyNmF39Mt5ppTW7jRoB8WgPkOc51rC8oKZl62iKAamlu4DmuBkvCcCtjwcMLAlVzR32t10CwTyM6AbagGllb48gU5ACDzUmBCyJqxlxs8U4XAyTcTtjWMNGdH9GuwTBmjyI3YJFEgrovcQDR5kFZ0NMkmgTDZclc183uTcoBDNAaimIE2Cwu9OXklBGRkNvItMU7zXTlX5Zz/V8YIO2iK//xMnpu22WBNXRyG9vGQuJ0mGsc26/Aag5YQelZZs5Bi2ubsxHV/DzGXRt3P3KTOvQ4HG8aKpm9OyoHnIgzG/GkIEerHqEBaYCQ8UJPWbg8/vNFDyFvqkhIe6O6l0WordG9pHAAyyTWbhCGHCtBD7vDjzljqRT4+8E2POgLpACHDw+E5jqrtyanHw0AAA==' violates the following Content Security Policy directive: \"default-src 'self'\". Note that 'font-src' was not explicitly set, so 'default-src' is used as a fallback. The action has been blocked.",
|
||||
"Loading the font 'http://at.alicdn.com/t/c/font_2553510_ciljc7axaw7.woff?t=1705587463221' violates the following Content Security Policy directive: \"default-src 'self'\". Note that 'font-src' was not explicitly set, so 'default-src' is used as a fallback. The action has been blocked.",
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>打字机效果演示 — IT智能服务台</title>
|
||||
<style>
|
||||
/* ================================================================
|
||||
整体布局 — 模拟手机端 H5 聊天界面
|
||||
================================================================ */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f0f2f5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 手机框 */
|
||||
.phone-frame {
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
height: 700px;
|
||||
background: #ededed;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶栏 */
|
||||
.chat-header {
|
||||
background: #fff;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.chat-header .avatar {
|
||||
width: 36px; height: 36px;
|
||||
background: #07C160;
|
||||
border-radius: 6px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #fff; font-weight: bold; font-size: 14px;
|
||||
}
|
||||
.chat-header .title { font-size: 15px; font-weight: 600; }
|
||||
.chat-header .subtitle { font-size: 12px; color: #999; }
|
||||
|
||||
/* 消息区 */
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
/* 滚动条美化 */
|
||||
.chat-messages::-webkit-scrollbar { width: 4px; }
|
||||
.chat-messages::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; }
|
||||
|
||||
/* 消息行 */
|
||||
.msg-row { display: flex; flex-direction: column; }
|
||||
.msg-row.employee { align-items: flex-end; }
|
||||
.msg-row.ai { align-items: flex-start; }
|
||||
.msg-row.system { align-items: center; }
|
||||
|
||||
/* 发送者名称 */
|
||||
.msg-sender {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.msg-sender .ai-avatar {
|
||||
width: 20px; height: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 消息气泡 */
|
||||
.msg-bubble {
|
||||
max-width: 80%;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 员工气泡 — 蓝底白字靠右 */
|
||||
.msg-row.employee .msg-bubble {
|
||||
background: #1989fa;
|
||||
color: #fff;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* AI 气泡 — 绿底白字靠左 */
|
||||
.msg-row.ai .msg-bubble {
|
||||
background: #07C160;
|
||||
color: #fff;
|
||||
border-top-left-radius: 2px;
|
||||
}
|
||||
|
||||
/* 系统消息 */
|
||||
.msg-row.system .msg-bubble {
|
||||
background: transparent;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
打字机光标 — 闪烁动画
|
||||
================================================================ */
|
||||
.typewriter-cursor {
|
||||
display: inline;
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-weight: 400;
|
||||
animation: cursor-blink 1s step-end infinite;
|
||||
}
|
||||
@keyframes cursor-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* 控制面板 */
|
||||
.control-panel {
|
||||
background: #fff;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-primary { background: #1989fa; color: #fff; }
|
||||
.btn-primary:hover { background: #1676d9; }
|
||||
.btn-success { background: #07C160; color: #fff; }
|
||||
.btn-success:hover { background: #06ad56; }
|
||||
.btn-outline { background: #fff; color: #666; border: 1px solid #ddd; }
|
||||
.btn-outline:hover { background: #f5f5f5; }
|
||||
.btn-warning { background: #ff976a; color: #fff; }
|
||||
.btn-warning:hover { background: #f08050; }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.control-info {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 高亮标记 */
|
||||
.highlight-box {
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 12px;
|
||||
color: #8c6d00;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="phone-frame">
|
||||
<!-- 顶栏 -->
|
||||
<div class="chat-header">
|
||||
<div class="avatar">IT</div>
|
||||
<div>
|
||||
<div class="title">IT智能服务台</div>
|
||||
<div class="subtitle">Duckula(达寇拉)在线</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
<!-- 消息由 JS 动态插入 -->
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel">
|
||||
<div class="highlight-box" id="statusBox">
|
||||
👆 上方聊天框展示了打字机效果在实际界面中的表现<br>
|
||||
点击下方按钮查看不同场景:
|
||||
</div>
|
||||
<div class="control-buttons">
|
||||
<button class="btn btn-success" id="btnDemo" onclick="runDemo()">
|
||||
▶ 演示完整对话
|
||||
</button>
|
||||
<button class="btn btn-outline" id="btnSkip" onclick="skipAll()" disabled>
|
||||
⏩ 跳过动画
|
||||
</button>
|
||||
<button class="btn btn-outline" id="btnReset" onclick="resetChat()">
|
||||
🔄 重置
|
||||
</button>
|
||||
<button class="btn btn-warning" id="btnNoEffect" onclick="showNoEffect()">
|
||||
❌ 无打字机(对比)
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-info" id="controlInfo">
|
||||
当前:等待开始...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ================================================================
|
||||
// 打字机核心逻辑(与 useTypewriter.ts 完全一致的 JS 实现)
|
||||
// ================================================================
|
||||
class Typewriter {
|
||||
constructor(speed = 25, speedUpThreshold = 300, speedUpBatch = 3) {
|
||||
this.speed = speed;
|
||||
this.speedUpThreshold = speedUpThreshold;
|
||||
this.speedUpBatch = speedUpBatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对目标元素执行打字机动画
|
||||
* @param {HTMLElement} el 显示文本的元素
|
||||
* @param {HTMLElement} cursorEl 光标元素
|
||||
* @param {string} fullText 完整文本
|
||||
* @returns {Promise} 动画完成时 resolve
|
||||
*/
|
||||
typeTo(el, cursorEl, fullText) {
|
||||
return new Promise((resolve) => {
|
||||
let index = 0;
|
||||
el.textContent = '';
|
||||
|
||||
const typeNext = () => {
|
||||
if (index >= fullText.length) {
|
||||
// 完成:隐藏光标
|
||||
if (cursorEl) cursorEl.style.display = 'none';
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 自适应速度:前 300 字逐字,超出后批量
|
||||
const batchSize = index < this.speedUpThreshold ? 1 : this.speedUpBatch;
|
||||
const end = Math.min(index + batchSize, fullText.length);
|
||||
el.textContent += fullText.slice(index, end);
|
||||
index = end;
|
||||
|
||||
this._timer = setTimeout(typeNext, this.speed);
|
||||
};
|
||||
|
||||
// 初始微延迟让气泡先渲染
|
||||
this._timer = setTimeout(typeNext, 80);
|
||||
});
|
||||
}
|
||||
|
||||
/** 立即完成 */
|
||||
finish(el, cursorEl, fullText) {
|
||||
clearTimeout(this._timer);
|
||||
el.textContent = fullText;
|
||||
if (cursorEl) cursorEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 全局状态
|
||||
// ================================================================
|
||||
const chatEl = document.getElementById('chatMessages');
|
||||
const statusBox = document.getElementById('statusBox');
|
||||
const controlInfo = document.getElementById('controlInfo');
|
||||
const btnDemo = document.getElementById('btnDemo');
|
||||
const btnSkip = document.getElementById('btnSkip');
|
||||
const btnReset = document.getElementById('btnReset');
|
||||
const btnNoEffect = document.getElementById('btnNoEffect');
|
||||
const tw = new Typewriter();
|
||||
let activeTypewriters = []; // 追踪所有活跃的打字机,用于 skipAll
|
||||
|
||||
// ================================================================
|
||||
// 消息创建
|
||||
// ================================================================
|
||||
function createMessage(type, senderName, text, withTypewriter = true) {
|
||||
const row = document.createElement('div');
|
||||
row.className = `msg-row ${type}`;
|
||||
|
||||
// 发送者名称
|
||||
if (type === 'ai' || type === 'employee') {
|
||||
const sender = document.createElement('div');
|
||||
sender.className = 'msg-sender';
|
||||
if (type === 'ai') {
|
||||
sender.innerHTML = `🦆 <span>${senderName}</span>`;
|
||||
} else {
|
||||
sender.textContent = senderName;
|
||||
}
|
||||
row.appendChild(sender);
|
||||
}
|
||||
|
||||
// 消息气泡
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'msg-bubble';
|
||||
|
||||
if (type === 'system') {
|
||||
bubble.textContent = text;
|
||||
} else {
|
||||
// 文本内容 + 光标 span
|
||||
const textSpan = document.createElement('span');
|
||||
textSpan.className = 'typewriter-text';
|
||||
bubble.appendChild(textSpan);
|
||||
|
||||
const cursorSpan = document.createElement('span');
|
||||
cursorSpan.className = 'typewriter-cursor';
|
||||
cursorSpan.textContent = '|';
|
||||
cursorSpan.style.display = 'none';
|
||||
bubble.appendChild(cursorSpan);
|
||||
|
||||
// 打字机动画
|
||||
if (withTypewriter && type === 'ai') {
|
||||
cursorSpan.style.display = 'inline';
|
||||
const promise = tw.typeTo(textSpan, cursorSpan, text);
|
||||
activeTypewriters.push({ bubble, textSpan, cursorSpan, text, promise });
|
||||
promise.then(() => {
|
||||
activeTypewriters = activeTypewriters.filter(t => t.textSpan !== textSpan);
|
||||
updateSkipButton();
|
||||
});
|
||||
} else {
|
||||
// 无打字机:直接显示全部
|
||||
textSpan.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
row.appendChild(bubble);
|
||||
chatEl.appendChild(row);
|
||||
|
||||
// 滚动到底部
|
||||
chatEl.scrollTop = chatEl.scrollHeight;
|
||||
|
||||
updateSkipButton();
|
||||
return row;
|
||||
}
|
||||
|
||||
function updateSkipButton() {
|
||||
btnSkip.disabled = activeTypewriters.length === 0;
|
||||
}
|
||||
|
||||
function skipAll() {
|
||||
activeTypewriters.forEach(t => {
|
||||
tw.finish(t.textSpan, t.cursorSpan, t.text);
|
||||
});
|
||||
activeTypewriters = [];
|
||||
updateSkipButton();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 演示场景
|
||||
// ================================================================
|
||||
async function runDemo() {
|
||||
btnDemo.disabled = true;
|
||||
btnNoEffect.disabled = true;
|
||||
resetChat();
|
||||
|
||||
// 场景 1:员工发消息(无打字机效果 — 直接显示)
|
||||
statusBox.innerHTML = '📌 <b>场景 1</b>:员工消息直接显示(无打字机效果)<br>员工消息靠右蓝底白字,一次性完整渲染';
|
||||
controlInfo.textContent = '场景 1/4:员工发消息 → 直接显示';
|
||||
createMessage('employee', '我(宋献)', '打印机连不上了,帮忙看看');
|
||||
|
||||
await sleep(800);
|
||||
|
||||
// 场景 2:AI 短回复(打字机效果明显)
|
||||
statusBox.innerHTML = '📌 <b>场景 2</b>:AI 短回复 — 打字机逐字显示<br>注意看 👈 绿色气泡中的文字一个一个字地"打"出来,<br>末尾有闪烁的 <span style="color:#07C160">|</span> 光标';
|
||||
controlInfo.textContent = '场景 2/4:AI 回复中...(逐字打字)';
|
||||
createMessage('ai', 'Duckula(达寇拉)', '您好!打印机连接问题可能有几种原因:\n\n1. 检查打印机电源是否开启\n2. 检查 USB 或网络连接\n3. 尝试重启打印服务\n\n请先确认打印机电源灯是否亮起?');
|
||||
|
||||
await sleep(5000);
|
||||
|
||||
// 场景 3:员工回复
|
||||
statusBox.innerHTML = '📌 <b>场景 3</b>:员工继续对话(正常显示)';
|
||||
controlInfo.textContent = '场景 3/4:员工回复 → 直接显示';
|
||||
createMessage('employee', '我(宋献)', '电源灯是亮的,但电脑显示离线');
|
||||
|
||||
await sleep(800);
|
||||
|
||||
// 场景 4:AI 长回复(超过 300 字会加速)
|
||||
statusBox.innerHTML = '📌 <b>场景 4</b>:AI 长回复 — 前 300 字逐字,之后加速<br>长消息会自动提速,避免等太久<br>按 <b>"⏩ 跳过动画"</b> 可以立即显示全部内容';
|
||||
controlInfo.textContent = '场景 4/4:AI 长回复中...(前300字逐字,之后批量加速)';
|
||||
createMessage('ai', 'Duckula(达寇拉)',
|
||||
'好的,电源灯亮说明打印机本身通电正常。电脑显示"离线"通常是因为以下原因:\n\n' +
|
||||
'🔌 连接问题:如果是有线打印机,检查 USB 线是否插紧,尝试换个 USB 口。无线打印机需要确认电脑和打印机连接的是同一个 WiFi。\n\n' +
|
||||
'🖨️ 驱动问题:打印机驱动程序可能损坏或过时。建议访问打印机品牌官网下载最新驱动重新安装。\n\n' +
|
||||
'⚙️ 打印服务:按 Win+R 输入 services.msc,找到 Print Spooler 服务,右键"重新启动"。\n\n' +
|
||||
'📋 打印队列:打开"设置 → 蓝牙和其他设备 → 打印机和扫描仪",选中你的打印机,点"打开打印队列",取消所有挂起的文档。\n\n' +
|
||||
'按以上顺序排查,80% 的情况都能解决。需要我进一步指导哪一步吗?'
|
||||
);
|
||||
|
||||
btnDemo.disabled = false;
|
||||
btnNoEffect.disabled = false;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 对比模式:无打字机效果
|
||||
// ================================================================
|
||||
function showNoEffect() {
|
||||
btnDemo.disabled = true;
|
||||
btnNoEffect.disabled = true;
|
||||
resetChat();
|
||||
|
||||
statusBox.innerHTML = '📌 <b>对比模式</b>:AI 消息一次性完整渲染(<b>没有</b>打字机效果)<br>文字瞬间全部出现 — 这就是现在的行为';
|
||||
controlInfo.textContent = '对比:无打字机 → 文字瞬间全部出现';
|
||||
|
||||
createMessage('employee', '我(宋献)', '打印机连不上了,帮忙看看');
|
||||
// withTypewriter = false → 直接显示全部
|
||||
createMessage('ai', 'Duckula(达寇拉)',
|
||||
'您好!打印机连接问题可能有几种原因:\n\n1. 检查打印机电源是否开启\n2. 检查 USB 或网络连接\n3. 尝试重启打印服务\n\n请先确认打印机电源灯是否亮起?',
|
||||
false // ← 关键参数:不启用打字机
|
||||
);
|
||||
|
||||
btnDemo.disabled = false;
|
||||
btnNoEffect.disabled = false;
|
||||
}
|
||||
|
||||
function resetChat() {
|
||||
// 清理活跃打字机
|
||||
activeTypewriters.forEach(t => tw.finish(t.textSpan, t.cursorSpan, t.text));
|
||||
activeTypewriters = [];
|
||||
chatEl.innerHTML = '';
|
||||
statusBox.innerHTML = '👆 上方聊天框展示了打字机效果在实际界面中的表现<br>点击下方按钮查看不同场景:';
|
||||
controlInfo.textContent = '当前:等待开始...';
|
||||
btnDemo.disabled = false;
|
||||
btnNoEffect.disabled = false;
|
||||
updateSkipButton();
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// 初始化
|
||||
updateSkipButton();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user