/* 运行模式动画 —— 可复用模块(主页 feature 行 / anim-demo 页共用) * AnimModes.init(container):在容器内建 DOM(模式 tab + 消息区 + 输入栏 + 假鼠标), * 点击 tab 切换并从头播放,再次点击重播。 */ window.AnimModes = (function () { 'use strict'; function init(container) { container.innerHTML = '
' + '
' + '' + '' + '' + '
' + '
' + '
' + '' + '
'; var chat = container.querySelector('.anim-chat'); var composer = container.querySelector('.anim-composer'); var composerText = composer.querySelector('.anim-composer-text'); var cursor = container.querySelector('.anim-cursor'); var tabs = Array.prototype.slice.call(container.querySelectorAll('.anim-mode-tab')); var animWindow = container.querySelector('.anim-window'); /* ── 代次令牌:切换模式时作废旧序列的所有后续步骤 ── */ var gen = 0; function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } /** 可中断 sleep:醒来后发现已切换模式则抛错终止序列 */ async function step(ms, myGen) { await sleep(ms); if (myGen !== gen) throw new Error('cancelled'); } function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; } /* ── 基础构件 ── */ /** 入场元素统一入口:挂载并平滑滚到底部(旧内容自然顶出) */ function push(node) { chat.appendChild(node); requestAnimationFrame(function () { chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' }); }); return node; } function addUserMsg(text) { return push(el('div', 'msg-user anim-item', text)); } function addThinkRow() { var row = el('div', 'think-row anim-item'); var dots = el('span', 'think-dots'); dots.appendChild(el('i')); dots.appendChild(el('i')); dots.appendChild(el('i')); row.appendChild(dots); return push(row); } function removeEl(e) { if (e && e.parentNode) e.parentNode.removeChild(e); } /** 往目标元素里逐字打出(带光标),返回 Promise */ async function typeInto(span, text, myGen, cps) { var caret = el('span', 'type-caret'); span.parentNode.insertBefore(caret, span.nextSibling); for (var i = 0; i < text.length; i++) { span.textContent += text[i]; chat.scrollTop = chat.scrollHeight; // 打字时保持底部可见 await step(cps || 34, myGen); } removeEl(caret); } /** AI 裸文字消息:逐字流式 */ async function typeAiMsg(text, myGen) { var b = el('div', 'msg-ai anim-item'); var span = el('span'); b.appendChild(span); push(b); await typeInto(span, text, myGen, 34); return b; } /** 工具摘要行:状态图标 + intent + 关键参数(不显示工具名) */ async function addToolLine(intent, param, runMs, myGen) { var b = el('div', 'tool-line is-running anim-item'); b.appendChild(el('span', 'tool-status')); b.appendChild(el('span', 'tool-intent', intent)); if (param) b.appendChild(el('span', 'tool-param', param)); push(b); await step(runMs, myGen); b.classList.remove('is-running'); b.classList.add('is-done'); return b; } /* ── 假鼠标:出现 → 移到目标中心 → 点击 → 消失 ── */ function cursorMoveTo(x, y, immediate) { if (immediate) cursor.style.transition = 'none'; cursor.style.transform = 'translate(' + x + 'px,' + y + 'px)'; if (immediate) { cursor.getBoundingClientRect(); // 强制 reflow,恢复过渡 cursor.style.transition = ''; } } async function cursorClick(targetEl, myGen) { var win = animWindow; var wr = win.getBoundingClientRect(); var tr = targetEl.getBoundingClientRect(); cursorMoveTo(wr.width - 60, wr.height - 40, true); cursor.classList.add('is-visible'); await step(260, myGen); cursorMoveTo(tr.left - wr.left + tr.width / 2 - 4, tr.top - wr.top + tr.height / 2 - 2, false); await step(760, myGen); cursor.classList.add('is-clicking'); targetEl.classList.add('is-pressed'); await step(140, myGen); cursor.classList.remove('is-clicking'); targetEl.classList.remove('is-pressed'); await step(120, myGen); cursor.classList.remove('is-visible'); } /* ── 模式一:计划 ── */ async function playPlan(myGen) { addUserMsg('帮我给这个项目加一个设置页面'); await step(700, myGen); var think = addThinkRow(); await step(1300, myGen); removeEl(think); await addToolLine('规划设计页面', '.astrion/plan/settings-page.md', 900, myGen); await step(350, myGen); // 计划:标题与条目全部逐字流式 var body = el('div', 'plan-body anim-item'); push(body); var title = el('div', 'plan-title'); var titleSpan = el('span'); title.appendChild(titleSpan); body.appendChild(title); await typeInto(titleSpan, '设置页面开发计划', myGen, 55); var items = ['梳理现有个性化设置字段与保存接口', '编写页面布局与表单组件', '接入保存并构建验证']; for (var i = 0; i < items.length; i++) { var row = el('div', 'plan-item'); row.appendChild(el('b', null, (i + 1) + '.')); var itemSpan = el('span'); row.appendChild(itemSpan); body.appendChild(row); await typeInto(itemSpan, items[i], myGen, 26); await step(120, myGen); } var actions = el('div', 'plan-actions anim-item'); var approve = el('button', 'plan-btn is-primary', '批准计划'); actions.appendChild(approve); body.appendChild(actions); chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' }); await step(700, myGen); // 鼠标点击批准 await cursorClick(approve, myGen); approve.textContent = '已批准 ✓'; approve.classList.remove('is-primary'); approve.classList.add('is-approved'); await step(700, myGen); // 按钮淡出收起,释放垂直空间 actions.classList.add('is-gone'); await step(380, myGen); removeEl(actions); // 批准后开始实施 await typeAiMsg('好的,开始实施。', myGen); await step(250, myGen); await addToolLine('编写设置页面组件', 'SettingsPanel.tsx', 1000, myGen); await step(280, myGen); await addToolLine('构建验证', 'npm run build', 1000, myGen); await step(320, myGen); await typeAiMsg('设置页面已完成,构建通过。', myGen); } /* ── 模式二:询问 ── */ async function playAsk(myGen) { addUserMsg('帮我优化一下这个页面的性能'); await step(700, myGen); var think = addThinkRow(); await step(1100, myGen); removeEl(think); await typeAiMsg('好的,我先和您聊聊。这个页面是首次加载慢,还是滚动、交互的时候慢?', myGen); await step(500, myGen); // 输入栏出现,用户逐字输入回答 composerText.textContent = ''; composer.classList.add('is-visible'); await step(450, myGen); var reply = '主要是首次加载慢'; for (var i = 0; i < reply.length; i++) { composerText.textContent += reply[i]; await step(70, myGen); } await step(600, myGen); // 发送:输入栏淡出,内容变成右侧用户气泡 composer.classList.add('is-sent'); await step(240, myGen); composer.classList.remove('is-visible', 'is-sent'); composerText.textContent = ''; addUserMsg(reply); await step(700, myGen); // 模型开始调查 var think2 = addThinkRow(); await step(1000, myGen); removeEl(think2); await typeAiMsg('好的,我来调查一下。', myGen); await step(250, myGen); await addToolLine('分析构建产物体积', 'npm run build', 1200, myGen); await step(300, myGen); await typeAiMsg('找到了:图表库被全量引入,首屏多出 2.1MB。', myGen); } /* ── 模式三:执行 ── */ async function playExecute(myGen) { addUserMsg('把页面上的按钮都改成蓝色'); await step(700, myGen); var think = addThinkRow(); await step(900, myGen); removeEl(think); await typeAiMsg('好的,我来改。', myGen); await step(250, myGen); await addToolLine('修改按钮颜色', 'css/style.css', 1000, myGen); await step(300, myGen); await addToolLine('构建验证', 'npm run build', 1100, myGen); await step(350, myGen); await typeAiMsg('已完成,所有按钮已切换为蓝色。', myGen); } /* ── 调度 ── */ var PLAYERS = { plan: playPlan, ask: playAsk, execute: playExecute }; function setActiveTab(mode) { tabs.forEach(function (t) { t.classList.toggle('is-active', t.dataset.mode === mode); }); } function play(mode) { gen += 1; var myGen = gen; chat.innerHTML = ''; chat.scrollTop = 0; composerText.textContent = ''; composer.classList.remove('is-visible', 'is-sent'); cursor.classList.remove('is-visible', 'is-clicking'); setActiveTab(mode); PLAYERS[mode](myGen).catch(function (e) { if (e && e.message !== 'cancelled') throw e; }); } tabs.forEach(function (t) { t.addEventListener('click', function () { play(t.dataset.mode); }); }); // 首次进入默认播放「计划」 play('plan'); } return { init: init }; })();