feat(HEL-76): 手机端丝滑度整改②——换页过渡与启动登录反馈
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -27,6 +27,23 @@ body {
|
||||
border-color var(--motion-pop) var(--ease-press);
|
||||
}
|
||||
|
||||
#m-boot-splash {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--canvas);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
#m-boot-splash .m-brand-mark {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.m-motion-fade-in {
|
||||
animation: m-fade-in var(--motion-fade) var(--ease-press) both;
|
||||
}
|
||||
@@ -45,6 +62,28 @@ body {
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.m-motion-push-in {
|
||||
animation: m-push-in var(--motion-enter) var(--ease-enter) both;
|
||||
}
|
||||
|
||||
@keyframes m-push-in {
|
||||
from { opacity: 0; transform: translateX(16px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.m-motion-pop-in {
|
||||
animation: m-pop-in var(--motion-pop) var(--ease-enter) both;
|
||||
}
|
||||
|
||||
@keyframes m-pop-in {
|
||||
from { opacity: 0; transform: translateX(-16px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.m-motion-boot-in {
|
||||
animation: m-fade-in var(--motion-press-release) var(--ease-press) both;
|
||||
}
|
||||
|
||||
.m-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -301,6 +340,10 @@ body {
|
||||
.m-btn-primary {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: var(--action);
|
||||
@@ -321,6 +364,22 @@ body {
|
||||
.m-btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.m-btn-spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: 0 0 auto;
|
||||
border: 2px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: m-spin 800ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes m-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.m-tabbar {
|
||||
@@ -476,7 +535,9 @@ body {
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.m-motion-rise-in {
|
||||
.m-motion-rise-in,
|
||||
.m-motion-push-in,
|
||||
.m-motion-pop-in {
|
||||
animation-name: m-fade-in;
|
||||
animation-duration: 100ms;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
document.getElementById("m-app").setAttribute("data-theme", theme);
|
||||
})();
|
||||
</script>
|
||||
<div id="m-boot-splash" aria-hidden="true">
|
||||
<span class="m-brand-mark">复</span>
|
||||
</div>
|
||||
<header id="m-header" class="m-header">
|
||||
<button id="m-back" class="m-header-btn" type="button" aria-label="返回" hidden>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
|
||||
@@ -67,9 +67,15 @@
|
||||
} else if (/^#\/?auth/.test(hash)) {
|
||||
global.location.replace("#/hub/market");
|
||||
}
|
||||
removeBootSplash();
|
||||
global.MobileRouter.init();
|
||||
}
|
||||
|
||||
function removeBootSplash() {
|
||||
const splash = document.getElementById("m-boot-splash");
|
||||
if (splash) splash.remove();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", boot, { once: true });
|
||||
} else {
|
||||
|
||||
+40
-14
@@ -56,6 +56,17 @@
|
||||
let internalNav = 0;
|
||||
let authMode = "login";
|
||||
|
||||
const VIEW_MOTION_CLASSES = ["m-motion-fade-in", "m-motion-push-in", "m-motion-pop-in", "m-motion-boot-in"];
|
||||
|
||||
function applyViewMotion(motion) {
|
||||
const view = document.getElementById("m-view");
|
||||
VIEW_MOTION_CLASSES.forEach(function (cls) {
|
||||
view.classList.remove(cls);
|
||||
});
|
||||
void view.offsetWidth;
|
||||
view.classList.add(motion || "m-motion-fade-in");
|
||||
}
|
||||
|
||||
function currentHash() {
|
||||
let hash = window.location.hash || "";
|
||||
if (!hash || hash === "#" || hash === "#/") return DEFAULT_HASH;
|
||||
@@ -92,14 +103,14 @@
|
||||
function navigate(hash) {
|
||||
stack.push(hash);
|
||||
setHash(hash);
|
||||
render();
|
||||
render("m-motion-push-in");
|
||||
}
|
||||
|
||||
function replace(hash) {
|
||||
function replace(hash, motion) {
|
||||
if (stack.length) stack[stack.length - 1] = hash;
|
||||
else stack.push(hash);
|
||||
setHash(hash);
|
||||
render();
|
||||
render(motion);
|
||||
}
|
||||
|
||||
function resetStack(hash) {
|
||||
@@ -118,11 +129,11 @@
|
||||
if (stack.length > 1) {
|
||||
stack.pop();
|
||||
setHash(stack[stack.length - 1]);
|
||||
render();
|
||||
render("m-motion-pop-in");
|
||||
return;
|
||||
}
|
||||
const parent = parentRoute(currentRoute());
|
||||
if (parent) replace(parent);
|
||||
if (parent) replace(parent, "m-motion-pop-in");
|
||||
}
|
||||
|
||||
function switchEntry(key) {
|
||||
@@ -240,7 +251,7 @@
|
||||
'<label class="m-form-field"><span>密码</span><input id="m-auth-password" type="password" minlength="8" maxlength="128" autocomplete="current-password" required></label>',
|
||||
'<label class="m-form-field" id="m-auth-confirm-field" hidden><span>确认密码</span><input id="m-auth-confirm" type="password" minlength="8" maxlength="128" autocomplete="new-password"></label>',
|
||||
'<p class="m-auth-error" id="m-auth-error" hidden></p>',
|
||||
'<button class="m-btn-primary" id="m-auth-submit" type="submit">登录</button>',
|
||||
'<button class="m-btn-primary" id="m-auth-submit" type="submit"><span class="m-btn-spinner" aria-hidden="true" hidden></span><span class="m-btn-label">登录</span></button>',
|
||||
"</form>",
|
||||
"</div>"
|
||||
].join("");
|
||||
@@ -256,7 +267,8 @@
|
||||
document.getElementById("m-auth-confirm-field").hidden = authMode !== "register";
|
||||
document.getElementById("m-auth-confirm").required = authMode === "register";
|
||||
document.getElementById("m-auth-password").autocomplete = authMode === "register" ? "new-password" : "current-password";
|
||||
document.getElementById("m-auth-submit").textContent = authMode === "register" ? "注册并进入" : "登录";
|
||||
const submitLabel = document.querySelector("#m-auth-submit .m-btn-label");
|
||||
if (submitLabel) submitLabel.textContent = authMode === "register" ? "注册并进入" : "登录";
|
||||
document.getElementById("m-auth-error").hidden = true;
|
||||
}
|
||||
|
||||
@@ -267,18 +279,29 @@
|
||||
document.getElementById("m-auth-form").addEventListener("submit", submitAuth);
|
||||
}
|
||||
|
||||
function showAuthError(element, message) {
|
||||
element.textContent = message;
|
||||
element.classList.remove("m-motion-fade-in");
|
||||
void element.offsetWidth;
|
||||
element.classList.add("m-motion-fade-in");
|
||||
element.hidden = false;
|
||||
}
|
||||
|
||||
async function submitAuth(event) {
|
||||
event.preventDefault();
|
||||
const username = document.getElementById("m-auth-username").value.trim();
|
||||
const password = document.getElementById("m-auth-password").value;
|
||||
const errorElement = document.getElementById("m-auth-error");
|
||||
if (authMode === "register" && password !== document.getElementById("m-auth-confirm").value) {
|
||||
errorElement.textContent = "两次输入的密码不一致。";
|
||||
errorElement.hidden = false;
|
||||
showAuthError(errorElement, "两次输入的密码不一致。");
|
||||
return;
|
||||
}
|
||||
const submit = document.getElementById("m-auth-submit");
|
||||
const spinner = submit.querySelector(".m-btn-spinner");
|
||||
const label = submit.querySelector(".m-btn-label");
|
||||
submit.disabled = true;
|
||||
spinner.hidden = false;
|
||||
label.textContent = authMode === "register" ? "注册中…" : "登录中…";
|
||||
try {
|
||||
if (authMode === "register") {
|
||||
await global.MobileSession.register(username, password);
|
||||
@@ -287,10 +310,11 @@
|
||||
}
|
||||
replace(DEFAULT_HASH);
|
||||
} catch (error) {
|
||||
errorElement.textContent = error.message || "账号操作失败";
|
||||
errorElement.hidden = false;
|
||||
showAuthError(errorElement, error.message || "账号操作失败");
|
||||
} finally {
|
||||
submit.disabled = false;
|
||||
spinner.hidden = true;
|
||||
label.textContent = authMode === "register" ? "注册并进入" : "登录";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +336,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
function render(motion) {
|
||||
const route = currentRoute();
|
||||
if (route.name === "home") {
|
||||
replace(DEFAULT_HASH);
|
||||
@@ -331,6 +355,7 @@
|
||||
else if (route.name === "feature") renderFeature(route.params.join("/"));
|
||||
else if (route.name === "assistant") renderAssistant();
|
||||
else if (route.name === "auth") renderAuth();
|
||||
applyViewMotion(motion);
|
||||
}
|
||||
|
||||
function bindGlobalEvents() {
|
||||
@@ -359,10 +384,11 @@
|
||||
const hash = currentHash();
|
||||
if (stack.length > 1 && hash === stack[stack.length - 2]) {
|
||||
stack.pop();
|
||||
render("m-motion-pop-in");
|
||||
} else if (hash !== stack[stack.length - 1]) {
|
||||
stack.push(hash);
|
||||
render("m-motion-push-in");
|
||||
}
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -377,7 +403,7 @@
|
||||
} else {
|
||||
stack.push(currentHash());
|
||||
}
|
||||
render();
|
||||
render("m-motion-boot-in");
|
||||
}
|
||||
|
||||
global.MobileRouter = {
|
||||
|
||||
Reference in New Issue
Block a user