- website-content/:官网 10 篇文档内容 + README - website-design/:官网设计稿、动画实验、预览图与资源 - 根目录官网截图两张 - .gitignore 忽略 website-design/exp/static/dist 构建产物(7.1MB)
322 lines
11 KiB
JavaScript
322 lines
11 KiB
JavaScript
/* 运行模式动画 —— 可复用模块(主页 feature 行 / anim-demo 页共用)
|
||
* AnimModes.init(container):在容器内建 DOM(模式 tab + 消息区 + 输入栏 + 假鼠标),
|
||
* 点击 tab 切换并从头播放,再次点击重播。
|
||
*/
|
||
window.AnimModes = (function () {
|
||
'use strict';
|
||
|
||
function init(container) {
|
||
container.innerHTML =
|
||
'<div class="anim-window" id="animWindow">' +
|
||
'<div class="anim-topbar">' +
|
||
'<button class="anim-mode-tab" data-mode="plan">计划</button>' +
|
||
'<button class="anim-mode-tab" data-mode="ask">询问</button>' +
|
||
'<button class="anim-mode-tab" data-mode="execute">执行</button>' +
|
||
'</div>' +
|
||
'<div class="anim-chat"></div>' +
|
||
'<div class="anim-composer"><span class="anim-composer-text"></span><span class="anim-composer-caret"></span></div>' +
|
||
'<div class="anim-cursor" aria-hidden="true">' +
|
||
'<svg viewBox="0 0 24 24" width="18" height="18"><path d="M5 3 L19 12 L12.5 12.8 L15.5 20 L13 21 L10 13.5 L5 17 Z" fill="#ffffff" stroke="#1a1f2e" stroke-width="1.4" stroke-linejoin="round"/></svg>' +
|
||
'</div>' +
|
||
'</div>';
|
||
|
||
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;
|
||
|
||
/* rAF 累计「可见时间」的等待:后台标签页 rAF 暂停,流程随之冻结,恢复时从冻结点继续——
|
||
setTimeout 在后台只是降频不会停,会攒动画导致恢复瞬间全部放完 */
|
||
function rafWait(ms) {
|
||
return new Promise(function (resolve) {
|
||
var acc = 0, last = performance.now();
|
||
function tick(now) {
|
||
acc += Math.min(now - last, 100); /* 恢复后首帧 dt 巨大,截断防跳变 */
|
||
last = now;
|
||
if (acc >= ms) { resolve(); return; }
|
||
requestAnimationFrame(tick);
|
||
}
|
||
requestAnimationFrame(tick);
|
||
});
|
||
}
|
||
|
||
/** 可中断 sleep:醒来后发现已切换模式则抛错终止序列 */
|
||
async function step(ms, myGen) {
|
||
await rafWait(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); });
|
||
});
|
||
|
||
// 首次进入默认播放「计划」——进入视口才开始:页面打开时用户可能还没滚到功能区,避免错过开场
|
||
var started = false;
|
||
var io = new IntersectionObserver(function (entries) {
|
||
if (!started && entries[0].isIntersecting) {
|
||
started = true;
|
||
io.disconnect();
|
||
play('plan');
|
||
}
|
||
}, { threshold: 0.3 });
|
||
io.observe(container);
|
||
}
|
||
|
||
return { init: init };
|
||
})();
|