advance 1-8 post-mvp execution batches

This commit is contained in:
lix-2026
2026-05-21 23:53:39 +08:00
parent 3ebcbff728
commit fdb20300e9
67 changed files with 4378 additions and 275 deletions
+65 -34
View File
@@ -17,7 +17,7 @@ pub fn AuthPage() -> impl IntoView {
</a>
<section class="mnote-auth-panel" aria-labelledby="mnote-auth-title">
<div class="mnote-auth-heading">
<h1 id="mnote-auth-title">"邮箱登录"</h1>
<h1 id="mnote-auth-title">"账号登录"</h1>
<p>"登录后进入你的工作区"</p>
</div>
<form class="mnote-auth-form" method="post" action="/api/auth" data-auth-mode="convex-password">
@@ -25,24 +25,14 @@ pub fn AuthPage() -> impl IntoView {
<input type="hidden" name="provider" value="password" />
<input type="hidden" name="flow" value="signIn" data-auth-flow />
<label class="mnote-auth-field">
<span>"邮箱"</span>
<span data-auth-account-label>"邮箱或用户名"</span>
<input
id="email"
name="email"
type="email"
autocomplete="email"
placeholder="请输入邮箱"
required
/>
</label>
<label class="mnote-auth-field mnote-auth-username" hidden>
<span>"用户名"</span>
<input
id="username"
name="username"
id="account"
name="account"
type="text"
autocomplete="username"
placeholder="用于显示的用户名"
placeholder="请输入邮箱或用户名"
required
/>
</label>
<label class="mnote-auth-field">
@@ -85,8 +75,8 @@ const AUTH_SCRIPT: &str = r#"
if (!root) return;
var form = root.querySelector('form[data-auth-mode="convex-password"]');
var flowInput = root.querySelector('[data-auth-flow]');
var usernameField = root.querySelector('.mnote-auth-username');
var usernameInput = root.querySelector('#username');
var accountInput = root.querySelector('#account');
var accountLabel = root.querySelector('[data-auth-account-label]');
var title = root.querySelector('#mnote-auth-title');
var subtitle = root.querySelector('.mnote-auth-heading p');
var submit = root.querySelector('[data-auth-submit]');
@@ -110,19 +100,54 @@ const AUTH_SCRIPT: &str = r#"
var isSignUp = nextFlow === 'signUp';
flowInput.value = nextFlow;
switcher.dataset.flow = nextFlow;
if (title) title.textContent = isSignUp ? '创建账号' : '邮箱登录';
if (title) title.textContent = isSignUp ? '创建账号' : '账号登录';
if (subtitle) subtitle.textContent = isSignUp ? '创建账号后进入你的工作区' : '登录后进入你的工作区';
submit.textContent = isSignUp ? '注册并登录' : '登录';
switcher.textContent = isSignUp ? '已有账号?登录' : '没有账号?注册';
if (usernameField) usernameField.hidden = !isSignUp;
if (usernameInput) usernameInput.required = isSignUp;
if (accountLabel) accountLabel.textContent = isSignUp ? '邮箱' : '邮箱或用户名';
if (accountInput) {
accountInput.placeholder = isSignUp ? '请输入邮箱' : '请输入邮箱或用户名';
accountInput.autocomplete = isSignUp ? 'email' : 'username';
}
quickLogin.hidden = isSignUp;
var passwordInput = root.querySelector('#password');
if (passwordInput) passwordInput.autocomplete = isSignUp ? 'new-password' : 'current-password';
setMessage('', '');
}
function buildPayload(flow, email, password, username) {
function accountMap() {
try {
return JSON.parse(window.localStorage.getItem('mnote.auth.accountEmailByName') || '{}') || {};
} catch (_) {
return {};
}
}
function rememberAccount(email, name) {
var normalizedEmail = String(email || '').trim();
var normalizedName = String(name || '').trim();
if (!normalizedEmail || !normalizedName || normalizedName.indexOf('@') !== -1) return;
try {
var map = accountMap();
map[normalizedName.toLowerCase()] = normalizedEmail;
window.localStorage.setItem('mnote.auth.accountEmailByName', JSON.stringify(map));
} catch (_) {}
}
function resolveAccountEmail(account) {
var normalized = String(account || '').trim();
if (normalized.indexOf('@') !== -1) return normalized;
var mapped = accountMap()[normalized.toLowerCase()];
return String(mapped || normalized).trim();
}
function defaultNameFromEmail(email) {
return String(email || '').split('@')[0].trim();
}
function buildPayload(flow, account, password) {
var email = flow === 'signIn' ? resolveAccountEmail(account) : String(account || '').trim();
var name = defaultNameFromEmail(email);
var payload = {
action: 'auth:signIn',
args: {
@@ -134,23 +159,25 @@ const AUTH_SCRIPT: &str = r#"
}
}
};
if (flow === 'signUp' && username) {
payload.args.params.name = username;
if (name) {
payload.args.params.name = name;
}
return payload;
}
async function requestAuth(flow, email, password, username) {
async function requestAuth(flow, account, password) {
var response = await fetch('/api/auth', {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'include',
body: JSON.stringify(buildPayload(flow, email, password, username))
body: JSON.stringify(buildPayload(flow, account, password))
});
var data = await response.json().catch(function () { return {}; });
if (!response.ok || data.error) {
throw new Error(data.error || '登录失败,请重试');
}
var params = buildPayload(flow, account, password).args.params;
rememberAccount(params.email, params.name);
return data;
}
@@ -160,18 +187,21 @@ const AUTH_SCRIPT: &str = r#"
form.addEventListener('submit', async function (event) {
event.preventDefault();
var email = String((root.querySelector('#email') || {}).value || '').trim();
var account = String((accountInput || {}).value || '').trim();
var password = String((root.querySelector('#password') || {}).value || '');
var username = String((usernameInput || {}).value || '').trim();
var flow = flowInput.value === 'signUp' ? 'signUp' : 'signIn';
if (!email || !password || (flow === 'signUp' && !username)) {
if (!account || !password) {
setMessage('请完整填写信息', 'error');
return;
}
if (flow === 'signUp' && account.indexOf('@') === -1) {
setMessage('注册时请填写邮箱;登录时可输入邮箱或已记住的用户名', 'error');
return;
}
setBusy(true);
setMessage(flow === 'signUp' ? '正在创建账号...' : '正在登录...', 'info');
try {
await requestAuth(flow, email, password, username);
await requestAuth(flow, account, password);
setMessage('登录成功,正在进入工作区...', 'success');
window.location.assign('/');
} catch (error) {
@@ -189,15 +219,15 @@ const AUTH_SCRIPT: &str = r#"
return;
}
var emailInput = root.querySelector('#email');
var passwordInput = root.querySelector('#password');
if (emailInput) emailInput.value = email;
if (accountInput) accountInput.value = email;
if (passwordInput) passwordInput.value = password;
setBusy(true);
setMessage('正在登录测试账号...', 'info');
try {
await requestAuth('signIn', email, password, username);
await requestAuth('signIn', email, password);
rememberAccount(email, username);
setMessage('登录成功,正在进入工作区...', 'success');
window.location.assign('/');
return;
@@ -206,7 +236,8 @@ const AUTH_SCRIPT: &str = r#"
}
try {
await requestAuth('signUp', email, password, username);
await requestAuth('signUp', email, password);
rememberAccount(email, username);
setMessage('测试账号已创建,正在进入工作区...', 'success');
window.location.assign('/');
} catch (error) {
+1 -24
View File
@@ -61,30 +61,7 @@ pub fn HomePage(
</main>
}.into_any()
} else {
view! {
<section class="mnote-workspace-empty-state" data-testid="mnote-workspace-empty-state">
<h1>"暂无页面"</h1>
<p>"当前还没有可显示的本地工作区。"</p>
<button
type="button"
class="mnote-empty-create-page"
data-testid="mnote-create-default-local-workspace"
data-mnote-action="create-local-workspace"
>"创建我的空间"</button>
<button
type="button"
class="mnote-empty-create-page"
data-testid="mnote-open-local-folder-empty"
data-mnote-action="open-local-folder"
>"打开本地文件夹"</button>
<button
type="button"
class="mnote-empty-create-page"
data-testid="mnote-empty-create-page"
data-mnote-action="create-local-workspace"
>"新建页面"</button>
</section>
}.into_any()
view! {}.into_any()
}}
</PageLayout>
}
+144 -4
View File
@@ -1214,6 +1214,110 @@ const SIDEBAR_TREE_JS: &str = r##"
trigger.closest('[data-testid="wolai-workspace-identity"]')?.appendChild(menu);
}
function closeAccountMenu() {
var existing = document.querySelector('[data-testid="mnote-account-menu"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
document.querySelectorAll('[data-testid="mnote-account-menu-trigger"]').forEach(function(trigger) {
if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', 'false');
});
}
function accountMenuFallbackSession() {
var body = document.body instanceof HTMLElement ? document.body : null;
var actorId = body ? String(body.getAttribute('data-mnote-actor-id') || '').trim() : '';
return {
ok: true,
name: actorId || '',
email: '',
userId: actorId || 'anonymous',
actorType: actorId && actorId !== 'anonymous' ? 'user' : 'anonymous',
authMode: 'browser'
};
}
function renderAccountInfo(menu, session) {
var nameNode = menu.querySelector('[data-account-info="name"]');
var emailNode = menu.querySelector('[data-account-info="email"]');
var idNode = menu.querySelector('[data-account-info="user-id"]');
var typeNode = menu.querySelector('[data-account-info="actor-type"]');
if (nameNode) nameNode.textContent = session.name || session.userId || '';
if (emailNode) emailNode.textContent = session.email || '';
if (idNode) idNode.textContent = session.userId || 'anonymous';
if (typeNode) typeNode.textContent = session.actorType || session.authMode || 'unknown';
}
async function loadAccountInfo(menu) {
try {
var response = await fetch('/api/auth/session', {
method: 'GET',
headers: { 'accept': 'application/json' },
credentials: 'include'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload) throw new Error('session_failed_' + response.status);
renderAccountInfo(menu, payload);
} catch (_) {
renderAccountInfo(menu, accountMenuFallbackSession());
}
}
async function signOutAccount(trigger) {
setCommandPending(trigger, true);
try {
var response = await fetch('/api/auth', {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ action: 'auth:signOut', args: {} })
});
var payload = await response.json().catch(function() { return {}; });
payload = payload || {};
if (!response.ok || payload.error) {
throw new Error(payload.error || '退');
}
window.location.assign('/auth');
} catch (error) {
var menu = document.querySelector('[data-testid="mnote-account-menu"]');
var errorNode = menu ? menu.querySelector('[data-account-error]') : null;
if (errorNode instanceof HTMLElement) {
errorNode.textContent = error && error.message ? error.message : '退';
errorNode.hidden = false;
}
setCommandPending(trigger, false);
}
}
function openAccountMenu(trigger) {
closeAccountMenu();
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('aria-expanded', 'true');
var menu = document.createElement('div');
menu.className = 'mnote-workspace-source-menu mnote-account-menu';
menu.setAttribute('data-testid', 'mnote-account-menu');
menu.setAttribute('role', 'menu');
menu.innerHTML =
'<div class="mnote-account-menu__profile" role="group" aria-label="个人信息">' +
'<div class="mnote-account-menu__title"></div>' +
'<div class="mnote-account-menu__name" data-account-info="name">...</div>' +
'<div class="mnote-account-menu__row"><span></span><strong data-account-info="email">...</strong></div>' +
'<div class="mnote-account-menu__row"><span> ID</span><strong data-account-info="user-id">...</strong></div>' +
'<div class="mnote-account-menu__row"><span></span><strong data-account-info="actor-type">...</strong></div>' +
'</div>' +
'<div class="mnote-account-menu__separator" aria-hidden="true"></div>' +
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__logout" data-testid="mnote-account-sign-out" role="menuitem">退</button>' +
'<div class="mnote-account-menu__error" data-account-error hidden></div>';
var signOutButton = menu.querySelector('[data-testid="mnote-account-sign-out"]');
if (signOutButton) {
signOutButton.addEventListener('click', function(event) {
event.preventDefault();
void signOutAccount(signOutButton);
});
}
var host = trigger.closest('[data-testid="wolai-sidebar-quick-actions"]') || trigger.parentElement;
if (host) host.appendChild(menu);
void loadAccountInfo(menu);
}
autoOpenRecentLocalRootOnHome();
function setCommandPending(trigger, pending) {
@@ -1825,7 +1929,7 @@ const SIDEBAR_TREE_JS: &str = r##"
});
var activeId = currentDocumentId();
var activeRowId = currentFileTreeActiveRowId();
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '<li class="tree-empty" data-rust-rendered-row="page-empty"></li>') + '</ul>';
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '') + '</ul>';
return true;
}
@@ -1934,7 +2038,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var rows = projectionItems(projection);
var activeId = currentDocumentId();
var activeRowId = currentFileTreeActiveRowId();
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId, activeRowId) : '<li class="tree-empty" data-rust-rendered-row="filetree-empty"></li>') + '</ul>';
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId, activeRowId) : '') + '</ul>';
return true;
}
@@ -2006,6 +2110,9 @@ const SIDEBAR_TREE_JS: &str = r##"
};
var poll = async function() {
if (document.hidden) return;
// If tree live SSE transport is active for local_folder, skip polling (fallback)
var treeTransport = document.documentElement.getAttribute('data-mnote-tree-live-transport') || '';
if (treeTransport === 'local-folder-events') return;
var url = new URL('/api/tree/local-folder-watch', window.location.origin);
url.searchParams.set('rootUri', rootUri);
var response = await fetch(url.toString(), { headers: { accept: 'application/json' } });
@@ -8864,6 +8971,17 @@ const SIDEBAR_TREE_JS: &str = r##"
var sourceMenu = closestAction(e.target, '[data-testid="mnote-workspace-source-menu"]');
if (!sourceMenu) closeWorkspaceSourceMenu();
var accountMenuTrigger = closestAction(e.target, '[data-mnote-action="open-account-menu"]');
if (accountMenuTrigger) {
e.preventDefault();
var existingAccountMenu = document.querySelector('[data-testid="mnote-account-menu"]');
if (existingAccountMenu) closeAccountMenu();
else openAccountMenu(accountMenuTrigger);
return;
}
var accountMenu = closestAction(e.target, '[data-testid="mnote-account-menu"]');
if (!accountMenu) closeAccountMenu();
var trashTrigger = closestAction(e.target, '[data-mnote-action="open-trash-modal"]');
if (trashTrigger) {
e.preventDefault();
@@ -9706,6 +9824,18 @@ const TREE_LIVE_CONTROLLER_JS: &str = r##"
var params = new URLSearchParams(window.location.search);
var sourceKind = (params.get('sourceKind') || '').trim();
if (sourceKind === 'local_folder') {
applyTransport('local-folder-events');
applyStatus('connecting');
var localRootUri = (params.get('rootUri') || '').trim()
|| (document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-root-uri') || '').trim() : '');
if (localRootUri && 'EventSource' in window) {
var url = new URL('/api/local-folder/events', window.location.origin);
url.searchParams.set('rootUri', localRootUri);
url.searchParams.set('treeLive', 'true');
startWithSse(bootstrap, '', url);
return;
}
// No rootUri or EventSource unavailable — mark static and let polling fallback handle it
applyTransport('local-folder-static');
applyStatus('static');
return;
@@ -9841,7 +9971,7 @@ pub fn PageLayout(
<span class="wolai-sidebar-chevron" aria-hidden="true">""</span>
</button>
</div>
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作" data-testid="wolai-sidebar-quick-actions">
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
<a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
<a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
@@ -9863,7 +9993,17 @@ pub fn PageLayout(
} else {
view! {}.into_any()
}}
<a href="/more" title="更多" aria-label="更多"><span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span></a>
<button
type="button"
title="更多"
aria-label="更多"
data-testid="mnote-account-menu-trigger"
data-mnote-action="open-account-menu"
aria-haspopup="menu"
aria-expanded="false"
>
<span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span>
</button>
</nav>
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>