/* ==========================================================================
fmode 移动端 UI Demo · 路由 / 渲染 / 交互
形态:全屏移动端 App(无手机壳);参照 WorkBuddy 的版式语言,配色走项目令牌
交互范围:浏览态全部可点;写操作(发送/新建/删除/上传)只弹提示 —— 静态数据演示
========================================================================== */
(function () {
'use strict';
const D = window.DEMO;
const $ = (sel, root) => (root || document).querySelector(sel);
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
const icon = (name, cls) => ``;
const initial = name => (name || '?').trim().charAt(0).toUpperCase();
/* ---------------- 全局状态 ---------------- */
const store = {
get(key, fallback) {
try { const v = localStorage.getItem(key); return v == null ? fallback : v; } catch (e) { return fallback; }
},
// 每次读取都以存储为准:登录态可能在别的标签页/脚本里被改过(演示版无跨标签同步)
getFresh(key, fallback) { return this.get(key, fallback); },
set(key, value) { try { localStorage.setItem(key, value); } catch (e) { /* 隐私模式忽略 */ } },
};
const state = {
palette: store.get('fmode_palette', 'platinum'),
dark: store.get('fmode_theme', 'light') === 'dark',
fsBody: Number(store.get('fmode_fs', 15)),
filters: { messages: 'all', contacts: 'all', tasks: 'all', deliverables: 'project' },
expanded: { people: {}, tree: { workspace: true, daily: true, archived: false } },
search: { messages: '' },
authed: store.get('fmode_demo_auth', '') === '1',
login: { stage: 'phone', phone: '', code: '', agreed: false, token: '' },
};
const VALID_PALETTES = ['platinum', 'ice', 'clay', 'slate'];
if (VALID_PALETTES.indexOf(state.palette) < 0) state.palette = 'platinum';
/* ---------------- 主题 ---------------- */
const FS_STEPS = [11, 12, 13, 14, 15, 16, 20, 26]; // 字样板(滑块档位)
function applyTheme() {
const html = document.documentElement;
html.dataset.palette = state.palette;
html.classList.toggle('dark', state.dark);
html.style.setProperty('--fs-body', state.fsBody + 'px');
const canvas = getComputedStyle(html).getPropertyValue('--canvas').trim();
const meta = $('meta[name="theme-color"]');
if (meta && canvas) meta.setAttribute('content', canvas);
}
function setPalette(p) {
state.palette = p; store.set('fmode_palette', p); applyTheme(); render();
}
function setDark(on) {
state.dark = !!on; store.set('fmode_theme', on ? 'dark' : 'light'); applyTheme(); render();
}
function setFs(v) {
const n = Number(v);
state.fsBody = FS_STEPS.reduce((a, b) => (Math.abs(b - n) < Math.abs(a - n) ? b : a), FS_STEPS[0]);
store.set('fmode_fs', state.fsBody); applyTheme(); render();
}
/* ---------------- 登录(演示版:只校验格式,不发起请求) ---------------- */
function loginNext(step) {
// 登录分两步:手机号 → 验证码;格式不合法就停在当前步
state.login.stage = step;
render();
}
function doLogin() {
const { agreed } = state.login;
if (!agreed) return toast('请先阅读并同意服务协议和隐私政策');
store.set('fmode_demo_auth', '1');
state.authed = true;
toast('演示版:已进入体验账号');
go('/messages');
}
function logout() {
store.set('fmode_demo_auth', '');
state.authed = false;
closeAllLayers();
go('/login');
}
/* ---------------- 路由 ---------------- */
// 路由模式:#/messages、#/chat/s-1、#/agent/a-nova、#/tasks/t-1 ……
function parseHash() {
const raw = (location.hash || '#/messages').replace(/^#\/?/, '');
const [path, query] = raw.split('?');
const parts = path.split('/').filter(Boolean);
return { page: parts[0] || 'messages', id: parts[1] || null, query: query || '' };
}
function go(path) {
if (location.hash === '#' + path) { render(); return; }
location.hash = path;
}
const TAB_PAGES = ['messages', 'contacts', 'workbench', 'mine'];
/* ---------------- 通用片段 ---------------- */
const agentAvatar = (a, cls) => `${esc(initial(a.name))}`;
const personAvatar = (p, cls) => `${esc(initial(p.name))}`;
const statusBadge = a => `${D.STATUS_TEXT[a.status] || a.status}`;
const taskBadge = t => `${D.TASK_STATUS_TEXT[t.status] || t.status}`;
function navbar({ title, sub, back, right, onTab }) {
return ``;
}
function empty(text) { return `
${esc(text)}
`; }
/* ---------------- 页面渲染 ---------------- */
const PAGES = {
/* 03 新建对话引导(落地态) */
home() {
const recent = D.sessions.filter(s => s.group !== 'archived').slice(0, 3);
return `
${navbar({ title: '', right: `
`, onTab: true })}
fmode
人机共生中枢
和数字生命一起干活 · 每条会话都可追踪
最近会话
${recent.map(s => {
const a = D.agentById(s.agentId);
return ``;
}).join('')}
${icon('paperclip', 'icon-sm')}
`;
},
/* 05 会话列表 */
messages() {
const q = state.search.messages.trim().toLowerCase();
const f = state.filters.messages;
const list = D.sessions.filter(s => {
if (f === 'unread' && !s.unread) return false;
if (f === 'workspace' && s.group !== 'workspace') return false;
if (q && !(s.title.toLowerCase().includes(q) || D.agentById(s.agentId).name.toLowerCase().includes(q))) return false;
return true;
});
return `
${navbar({ title: '会话', onTab: true, right: `
` })}
${icon('search', 'icon-sm')}
${[['all', '全部'], ['unread', '未读'], ['workspace', '工作区']].map(([k, t]) => ``).join('')}
${list.length ? `
${list.map(s => {
const a = D.agentById(s.agentId);
return ``;
}).join('')}
` : empty('没有匹配的会话')}
`;
},
/* 02 登录 */
login() {
const L = state.login;
const phoneOk = /^1\d{10}$/.test(L.phone);
return `
fmode
欢迎使用
人机共生中枢 · 登录后继续
${L.stage === 'phone' ? `
使用 Token 直接登录
` : L.stage === 'code' ? `
返回修改手机号
` : `
返回手机号登录
`}
演示版:不会真的发送短信或请求接口
`;
},
/* 06 会话详情 */
chat(id) {
const s = D.sessionById(id) || D.sessions[0];
const a = D.agentById(s.agentId);
const msgs = D.messages[s.id] || [];
return `
${navbar({
title: s.title, sub: `${a.name} · ${D.STATUS_TEXT[a.status]}`,
back: true,
right: `
`,
})}
${msgs.length ? msgs.map(m => m.sender === 'me' ? msgMe(m) : msgAgent(m)).join('') : empty('这个会话还没有消息')}
${icon('mic', 'icon-sm')}
`;
},
/* 09 通讯录 */
contacts() {
const f = state.filters.contacts;
const agents = f === 'people' ? [] : D.agents;
const people = f === 'agents' ? [] : D.people;
return `
${navbar({ title: '通讯录', onTab: true })}
${[['all', '全部'], ['agents', '数字生命'], ['people', '同事']].map(([k, t]) => ``).join('')}
${agents.length ? `
数字生命 · ${agents.length}
${agents.map(a => ``).join('')}
` : ''}
${people.length ? `
同事 · ${people.length}
${people.map(p => {
const open = state.expanded.people[p.id];
return `
${open ? p.agents.map(id => { const a = D.agentById(id); return ``; }).join('') : ''}`;
}).join('')}
` : ''}
`;
},
/* 10 数字生命详情 */
agent(id) {
const a = D.agentById(id) || D.agents[0];
const hist = D.deliverables.filter(d => d.agentId === a.id);
return `
${navbar({ title: '数字生命', back: true, right: `
` })}
${agentAvatar(a, 'lg')}
${esc(a.name)}
${esc(a.specialty)}
${statusBadge(a)}${a.sessionStatus !== 'ok' ? `状态未上报` : ''}
运行指标心跳 ${esc(a.heartbeatAt)}
本会话 Token${a.usage} 万
余额¥${a.balance.remainCny}
基本信息
- 当前任务
- ${esc(a.currentTask)}
- 会话数
- ${a.usageSessions}
- 通知通道
- ${a.channels.length ? a.channels.map(c => `${esc(c)}`).join(' ') : '未配置'}
- Matrix
- ${a.matrixReady ? '已连接' : '未连接'}
权限设置
允许群内任何人指挥关闭后仅创建者可下达指令
对外共享交付成果开启后同项目成员可下载
最近交付
${hist.length ? `
${hist.map(d => ``).join('')}
` : empty('暂无交付')}
`;
},
/* 11 项目(列表 / 详情同页) */
projects(id) {
if (!id) {
return `
${navbar({ title: '项目', onTab: false, back: true })}
${D.projects.map(p => `
`).join('')}
`;
}
const p = D.projectById(id) || D.projects[0];
const sess = p.sessionIds.map(D.sessionById);
const dels = p.deliverableIds.map(D.deliverableById);
const tasks = D.tasks.filter(t => t.projectId === p.id);
return `
${navbar({ title: p.name, back: true, right: '
' })}
${esc(p.name)}${p.hasBlocked ? '有阻塞' : `${esc(p.status)}`}
${esc(p.description)}
- 编号
- ${esc(p.code)}
- 负责人
- ${esc(p.owner)}
- 截止
- ${esc(p.due)}
- 数字生命
- ${p.agentIds.map(i => esc(D.agentById(i).name)).join('、')}
关联任务 · ${tasks.length}
${tasks.map(t => taskCard(t)).join('') || empty('暂无任务')}
关联会话
${sess.map(s => ``).join('')}
交付成果 · ${dels.length}
${dels.length ? `
${dels.map(d => ``).join('')}
` : empty('暂无交付')}
`;
},
/* 12 任务看板 / 任务详情 */
tasks(id) {
if (!id) {
const f = state.filters.tasks;
const order = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'done'];
const counts = order.map(k => D.tasks.filter(t => t.status === k).length);
const list = f === 'all' ? D.tasks : D.tasks.filter(t => t.status === f);
return `
${navbar({ title: '任务', back: true })}
${order.map((k, i) => ``).join('')}
${list.map(t => taskCard(t)).join('') || empty('该状态下暂无任务')}
`;
}
const t = D.taskById(id) || D.tasks[0];
const a = D.agentById(t.agentId);
return `
${navbar({ title: '任务详情', back: true })}
${esc(t.title)}${taskBadge(t)}
${esc(a.name)}·${esc(D.projectById(t.projectId).name)}·更新于 ${esc(t.updatedAt)}
${t.status === 'blocked' ? `已阻塞 ${t.blockedHours} 小时` : ''}
${t.status === 'blocked' ? `
${icon('alert', 'icon-sm')}阻塞原因:${esc(t.blockKind)}
` : ''}
${esc(t.body)}
运行流水
${t.runs.length ? `
${t.runs.map(r => `
${esc(r.text)}
`).join('')}
` : empty('暂无运行记录')}
附件 · ${t.attachments.length}
${t.attachments.length ? `
${t.attachments.map(f => ``).join('')}
` : empty('暂无附件')}
评论 · ${t.comments.length}
${t.comments.length ? `
${t.comments.map(c => `
${personAvatar({ name: c.who }, 'sm')}${esc(c.who)} ${esc(c.at)}${esc(c.text)}
`).join('')}
` : empty('暂无评论')}
`;
},
/* 13 成果 */
deliverables() {
const f = state.filters.deliverables;
const groups = f === 'project'
? D.projects.map(p => ({ title: p.name, items: D.deliverables.filter(d => d.projectId === p.id) })).filter(g => g.items.length)
: [{ title: '按时间线', items: D.deliverables.slice().sort((x, y) => x.deliveredAt < y.deliveredAt ? 1 : -1) }];
return `
${navbar({ title: '成果', back: true })}
${[['project', '按项目'], ['timeline', '按时间线']].map(([k, t]) => ``).join('')}
${groups.map(g => `
${esc(g.title)} · ${g.items.length}
${g.items.map(d => ``).join('')}
`).join('')}
`;
},
/* 14 事件 */
events() {
const days = [];
D.events.forEach(e => { const g = days.find(d => d.day === e.day); (g ? g.items : (days.push({ day: e.day, items: [] }), days[days.length - 1].items)).push(e); });
const tone = { delivery: 'success', execution: 'info', alert: 'danger' };
const label = { delivery: '交付', execution: '执行', alert: '告警' };
return `
${navbar({ title: '事件', back: true })}
${days.map(d => `
${esc(d.day)}${d.items.length} 条
${d.items.map(e => ``).join('')}
`).join('')}
`;
},
/* 15 工作台 */
workbench() {
return `
${navbar({ title: '工作台', onTab: true, right: `
` })}
${D.agents.map(a => `
${agentAvatar(a)}
${esc(a.name)}
${esc(a.specialty)}
${statusBadge(a)}
${esc(a.currentTask)}
`).join('')}
`;
},
/* 16 我的 */
mine() {
const palettes = [
['platinum', '铂金曜黑'], ['ice', '冰白靛蓝'], ['clay', '暖瓷陶橘'], ['slate', '岩灰鸢紫'],
];
return `
${navbar({ title: '我的', onTab: true })}
${esc(initial(D.me.name))}
${esc(D.me.name)}
${esc(D.me.mobile)} · ${esc(D.me.companyName)}
账号
外观
整体配色
${palettes.map(([k, t]) => ``).join('')}
深色模式跟随手动切换
`;
},
};
function msgMe(m) {
return `${esc(m.text)}
`;
}
function msgAgent(m) {
const procs = m.processes || [];
return `
${procs.every(p => p.state === 'done') ? '已完成' : '执行中'}
共 ${procs.length} 段过程
${procs.length ? '收起' : ''}
${procs.map(p => `
${esc(p.label)}${p.state === 'done' ? `已完成 ${icon('chevron-right', 'icon-sm')}` : '执行中'}
`).join('')}
${(m.markdown || []).map(b => block(b)).join('')}
${(m.files || []).map(f => `
`).join('')}
`;
}
function block(b) {
if (b.type === 'p') return `${esc(b.text)}
`;
if (b.type === 'table') return `${b.rows.map(r => `${r.map((c, i) => i === 0 ? `| ${esc(c)} | ` : `${esc(c)} | `).join('')}
`).join('')}
`;
return '';
}
function taskCard(t) {
const a = D.agentById(t.agentId);
return ``;
}
/* ---------------- 渲染入口 ---------------- */
function render() {
applyTheme();
state.authed = store.get('fmode_demo_auth', '') === '1';
let r = parseHash();
// 权限守卫:未登录一律渲染登录页;已登录时访问登录页则直接渲染会话
// 说明:守卫只决定「渲染什么」,不修改 hash —— 改 hash 会触发新一轮 hashchange,
// 在 file:// 下(history 顺序不同步)与本次渲染互相覆盖,导致停在错误页面
if (!state.authed && r.page !== 'login') r = { page: 'login', id: null };
else if (state.authed && r.page === 'login') r = { page: 'messages', id: null };
const fn = PAGES[r.page] || PAGES.home;
$('#views').innerHTML = `${fn(r.id)}
`;
renderTabbar(r.page);
if (window.scrollY) window.scrollTo(0, 0);
}
function renderTabbar(current) {
const tabs = [
['messages', 'message', '会话'], ['contacts', 'users', '通讯录'],
['workbench', 'grid', '工作台'], ['mine', 'user', '我的'],
];
$('#tabbar').style.display = TAB_PAGES.includes(current) ? 'flex' : 'none';
const unread = D.sessions.reduce((n, s) => n + (s.group === 'archived' ? 0 : s.unread), 0);
$('#tabbar').innerHTML = tabs.map(([k, ic, label]) => ``).join('');
}
/* ---------------- 浮层:抽屉 / 底部弹层 / 预览 / toast ---------------- */
const layerRoot = $('#layers');
function layerShell(inner, cls, onClose) {
const wrap = document.createElement('div');
wrap.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:var(--z-modal)';
wrap.innerHTML = `${inner}`;
const mask = wrap.querySelector('.sheet-mask');
mask.onclick = () => { closeLayer(wrap); onClose && onClose(); };
wrap.querySelectorAll('.drawer,.sheet,.sheet-body-wrap').forEach(n => (n.style.pointerEvents = 'auto'));
layerRoot.appendChild(wrap);
requestAnimationFrame(() => { mask.classList.add('show'); wrap.querySelector('.drawer,.sheet')?.classList.add('show'); });
return wrap;
}
function closeLayer(wrap) {
const mask = wrap.querySelector('.sheet-mask');
const panel = wrap.querySelector('.drawer,.sheet');
mask.classList.remove('show');
if (panel) panel.classList.remove('show');
setTimeout(() => wrap.remove(), 260);
}
function toast(msg) {
const t = document.createElement('div');
t.className = 'toast';
t.textContent = msg;
$('#app').appendChild(t);
setTimeout(() => t.remove(), 1800);
}
/* 04 抽屉:新建对话 + 一级入口 + 会话树(工作区 / 日常对话 / 已归档) */
function treeHtml(sessions) {
return sessions.map(s => ``).join('') || `暂无会话
`;
}
function openDrawer() {
const groups = [
['工作区', 'workspace'],
['日常对话', 'daily'],
['已归档', 'archived'],
];
const tree = groups.map(([label, key]) => {
const items = D.sessions.filter(s => s.group === key);
const open = !!state.expanded.tree[key];
return `
${treeHtml(items)}
`;
}).join('');
return layerShell(`
`);
}
/* 分组折叠:只切换 aria-expanded + hidden,不重建抽屉(避免整块重绘导致的跳动) */
function toggleTree(btn) {
const key = btn.dataset.key;
const open = !state.expanded.tree[key];
state.expanded.tree[key] = open;
btn.setAttribute('aria-expanded', String(open));
const panel = document.getElementById('tree-' + key);
if (panel) panel.hidden = !open;
}
/* 新建对话:底部弹层选择数字生命(对标桌面「选择容器新建对话」) */
function openNewChat() {
closeAllLayers();
const usable = D.agents.filter(a => a.status === 'running' || a.status === 'idle');
const draft = D.sessions.filter(s => s.group === 'daily').slice(0, 3);
return layerShell(`
新建对话
选择数字生命 · ${usable.length} 个可用
${D.agents.map(a => {
const on = a.status === 'running' || a.status === 'idle';
return ``;
}).join('')}
或继续已有会话
${draft.map(s => ``).join('')}
离线 / 异常的数字生命暂不可新建对话
`);
}
/* 07 会话内成果抽屉:任务 / 文件 / 成果 / 状态 */
function openChatSheet(sessionId) {
const s = D.sessionById(sessionId) || D.sessions[0];
const a = D.agentById(s.agentId);
const tasks = D.tasks.filter(t => t.agentId === a.id);
const dels = D.deliverables.filter(d => d.agentId === a.id);
const tab = (k, t) => ``;
const wrap = layerShell(`
${esc(s.title)}
${tab('task', '任务')}${tab('file', '文件')}${tab('deliver', '成果')}${tab('state', '状态')}
`);
const body = wrap.querySelector('[data-sheet-body]');
const panels = {
task: () => tasks.length ? `${tasks.map(t => ``).join('')}
` : empty('暂无任务'),
file: () => (D.messages[s.id] || []).some(m => m.files) ? `${(D.messages[s.id] || []).flatMap(m => m.files || []).map(f => ``).join('')}
` : empty('暂无文件'),
deliver: () => dels.length ? `${dels.map(d => ``).join('')}
` : empty('暂无成果'),
state: () => `- 状态
- ${D.STATUS_TEXT[a.status]}
- 当前任务
- ${esc(a.currentTask)}
- 心跳
- ${esc(a.heartbeatAt)}
- Matrix
- ${a.matrixReady ? '已连接' : '未连接'}
- Token
- ${a.usage} 万
`,
};
const setTab = k => {
body.innerHTML = panels[k]();
wrap.querySelectorAll('[data-tab]').forEach(b => b.classList.toggle('on', b.dataset.tab === k));
};
wrap.querySelectorAll('[data-tab]').forEach(b => (b.onclick = () => setTab(b.dataset.tab)));
setTab('task');
return wrap;
}
/* 通道 bottom-sheet */
function openChannels(agentId) {
const a = D.agentById(agentId) || D.agents[0];
return layerShell(`
${esc(a.name)} 的通知通道
${['钉钉', '飞书', '企业微信', '邮件'].map(c => `
${c}
`).join('')}
`);
}
/* 08 文件预览层 */
function openPreview(deliverableId) {
const d = D.deliverableById(deliverableId) || D.deliverables[0];
const a = d.agentId ? D.agentById(d.agentId) : null;
// 兼容消息里的临时文件对象(只有文件名/元信息)
const filename = d.filename || d.name;
const type = (d.fileType || filename.split('.').pop() || '').toLowerCase();
const sub = [d.title || d.name, d.version, d.size, a ? a.name : ''].filter(Boolean).map(esc).join(' · ');
const wrap = document.createElement('div');
wrap.innerHTML = `
${esc(filename)}${sub || '示例文件'}
${type === 'xlsx' ? `
| 事业部 | Q2 营收 | 毛利率 |
| 智能制造 | ¥ 1,860 万 | 41.2% |
| 云与数据 | ¥ 2,410 万 | 52.6% |
| 消费终端 | ¥ 550 万 | 28.4% |
`
: type === 'md' ? `
${esc(d.title || d.name)}
页面结构:导航 → 主视觉 → 最近会话 → 输入坞。令牌全部来自《设计令牌.md》。
`
: `
`}
`;
layerRoot.appendChild(wrap.firstElementChild);
}
/* ---------------- 事件委托 ---------------- */
document.addEventListener('click', ev => {
const el = ev.target.closest('[data-act]');
if (!el) return;
const act = el.dataset.act;
switch (act) {
case 'tab': go('/' + el.dataset.to); break;
case 'go': go(el.dataset.to); closeAllLayers(); break;
case 'back':
if (history.length > 1) history.back();
else go('/' + (TAB_PAGES.includes(parseHash().page) ? 'messages' : DEFAULT_TAB[parseHash().page] || 'messages'));
break;
case 'drawer': openDrawer(); break;
case 'tree-toggle': toggleTree(el); break;
case 'new-chat': openNewChat(); break;
case 'expand-person': {
const id = el.dataset.id;
state.expanded.people[id] = !state.expanded.people[id];
render();
break;
}
// 与 core/agent-route.service 一致:可用(running/idle)且有活跃会话 → 进会话;否则进详情
case 'agent-route': {
const a = D.agentById(el.dataset.id);
const s = D.sessions.find(x => x.agentId === a.id && x.group !== 'archived');
const usable = a.status === 'running' || a.status === 'idle';
closeAllLayers();
go(usable && s ? `/chat/${s.id}` : `/agent/${a.id}`);
break;
}
case 'filter': state.filters[el.dataset.scope] = el.dataset.key; render(); break;
case 'sheet-chat': {
const r = parseHash();
closeAllLayers();
openChatSheet(r.page === 'chat' ? r.id : null);
break;
}
case 'sheet-channels': closeAllLayers(); openChannels(el.dataset.id); break;
case 'preview': openPreview(el.dataset.id); break;
case 'preview-dummy': openPreview({ filename: el.dataset.name, title: el.dataset.name, version: el.dataset.meta, size: '', agentId: 'a-orbit' }); break;
case 'palette': setPalette(el.dataset.key); break;
case 'toggle-dark': setDark(!state.dark); closeAllLayers(); break;
case 'switch-static': toastStatic('演示版:设置项不保存'); break;
case 'toast-static': toastStatic(); break;
case 'login-next': loginNext(el.dataset.step); break;
case 'login-submit': doLogin(); break;
case 'login-agree': state.login.agreed = !state.login.agreed; render(); break;
case 'send-static': { const dock = el.closest('.dock'); const ta = dock && dock.querySelector('textarea'); if (ta && ta.value.trim()) toast('演示版:数据为静态示例,不会真的发送'); else ta && ta.focus(); break; }
case 'logout': confirmLogout(); break;
case 'logout-confirm': logout(); break;
case 'close': {
const preview = el.closest('.preview');
if (preview) { preview.remove(); break; }
const wrap = el.closest('#layers > div');
if (wrap) { closeLayer(wrap); break; }
// 没有关闭按钮的浮层(退出确认):点取消直接移除
if (el.closest('.sheet-body-wrap')) closeAllLayers();
break;
}
}
});
// input 事件:搜索与输入坞
document.addEventListener('input', ev => {
const el = ev.target;
if (el.dataset.act === 'search-messages') {
state.search.messages = el.value;
const pos = el.selectionStart;
render();
const next = $('[data-act="search-messages"]');
if (next) { next.focus(); next.setSelectionRange(pos, pos); }
}
if (el.dataset.act === 'login-phone') {
state.login.phone = el.value.replace(/\D/g, '').slice(0, 11);
const pos = state.login.phone.length;
render();
const next = $('[data-act="login-phone"]');
if (next) { next.focus(); next.setSelectionRange(pos, pos); }
return;
}
if (el.dataset.act === 'login-code') {
state.login.code = el.value.replace(/\D/g, '').slice(0, 6);
const pos = state.login.code.length;
render();
const next = $('[data-act="login-code"]');
if (next) { next.focus(); next.setSelectionRange(pos, pos); }
return;
}
if (el.dataset.act === 'login-token') {
state.login.token = el.value;
const btn = $('[data-act="login-submit"]');
if (btn) btn.disabled = !el.value.trim();
return;
}
if (el.dataset.act === 'dock-input') {
const send = el.closest('.dock').querySelector('.send');
send.classList.toggle('ready', !!el.value.trim());
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
});
function confirmLogout() {
const wrap = layerShell(`
退出登录
退出后需要重新登录才能继续使用。演示版不会真的退出。
`);
wrap.querySelectorAll('.sheet').forEach(s => s.classList.add('show'));
}
function toastStatic(msg) { toast(msg || '演示版:数据为静态示例'); }
function closeAllLayers() {
layerRoot.querySelectorAll(':scope > div').forEach(w => closeLayer(w));
layerRoot.querySelectorAll(':scope > .preview').forEach(p => p.remove());
}
const DEFAULT_TAB = { chat: 'messages', agent: 'contacts', tasks: 'workbench', projects: 'messages', events: 'messages', deliverables: 'workbench' };
/* ---------------- 启动 ---------------- */
window.addEventListener('hashchange', render);
// 入口:首次进入落到新建对话引导页
if (!location.hash) location.replace(state.authed ? '#/messages' : '#/login');
applyTheme();
render();
})();