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);
|
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 {
|
.m-motion-fade-in {
|
||||||
animation: m-fade-in var(--motion-fade) var(--ease-press) both;
|
animation: m-fade-in var(--motion-fade) var(--ease-press) both;
|
||||||
}
|
}
|
||||||
@@ -45,6 +62,28 @@ body {
|
|||||||
to { opacity: 1; transform: none; }
|
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 {
|
.m-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -301,6 +340,10 @@ body {
|
|||||||
.m-btn-primary {
|
.m-btn-primary {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--action);
|
background: var(--action);
|
||||||
@@ -321,6 +364,22 @@ body {
|
|||||||
.m-btn-primary:disabled {
|
.m-btn-primary:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: default;
|
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 {
|
.m-tabbar {
|
||||||
@@ -476,7 +535,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@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-name: m-fade-in;
|
||||||
animation-duration: 100ms;
|
animation-duration: 100ms;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
document.getElementById("m-app").setAttribute("data-theme", theme);
|
document.getElementById("m-app").setAttribute("data-theme", theme);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<div id="m-boot-splash" aria-hidden="true">
|
||||||
|
<span class="m-brand-mark">复</span>
|
||||||
|
</div>
|
||||||
<header id="m-header" class="m-header">
|
<header id="m-header" class="m-header">
|
||||||
<button id="m-back" class="m-header-btn" type="button" aria-label="返回" hidden>
|
<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>
|
<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)) {
|
} else if (/^#\/?auth/.test(hash)) {
|
||||||
global.location.replace("#/hub/market");
|
global.location.replace("#/hub/market");
|
||||||
}
|
}
|
||||||
|
removeBootSplash();
|
||||||
global.MobileRouter.init();
|
global.MobileRouter.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeBootSplash() {
|
||||||
|
const splash = document.getElementById("m-boot-splash");
|
||||||
|
if (splash) splash.remove();
|
||||||
|
}
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", boot, { once: true });
|
document.addEventListener("DOMContentLoaded", boot, { once: true });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+40
-14
@@ -56,6 +56,17 @@
|
|||||||
let internalNav = 0;
|
let internalNav = 0;
|
||||||
let authMode = "login";
|
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() {
|
function currentHash() {
|
||||||
let hash = window.location.hash || "";
|
let hash = window.location.hash || "";
|
||||||
if (!hash || hash === "#" || hash === "#/") return DEFAULT_HASH;
|
if (!hash || hash === "#" || hash === "#/") return DEFAULT_HASH;
|
||||||
@@ -92,14 +103,14 @@
|
|||||||
function navigate(hash) {
|
function navigate(hash) {
|
||||||
stack.push(hash);
|
stack.push(hash);
|
||||||
setHash(hash);
|
setHash(hash);
|
||||||
render();
|
render("m-motion-push-in");
|
||||||
}
|
}
|
||||||
|
|
||||||
function replace(hash) {
|
function replace(hash, motion) {
|
||||||
if (stack.length) stack[stack.length - 1] = hash;
|
if (stack.length) stack[stack.length - 1] = hash;
|
||||||
else stack.push(hash);
|
else stack.push(hash);
|
||||||
setHash(hash);
|
setHash(hash);
|
||||||
render();
|
render(motion);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetStack(hash) {
|
function resetStack(hash) {
|
||||||
@@ -118,11 +129,11 @@
|
|||||||
if (stack.length > 1) {
|
if (stack.length > 1) {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
setHash(stack[stack.length - 1]);
|
setHash(stack[stack.length - 1]);
|
||||||
render();
|
render("m-motion-pop-in");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parent = parentRoute(currentRoute());
|
const parent = parentRoute(currentRoute());
|
||||||
if (parent) replace(parent);
|
if (parent) replace(parent, "m-motion-pop-in");
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchEntry(key) {
|
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"><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>',
|
'<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>',
|
'<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>",
|
"</form>",
|
||||||
"</div>"
|
"</div>"
|
||||||
].join("");
|
].join("");
|
||||||
@@ -256,7 +267,8 @@
|
|||||||
document.getElementById("m-auth-confirm-field").hidden = authMode !== "register";
|
document.getElementById("m-auth-confirm-field").hidden = authMode !== "register";
|
||||||
document.getElementById("m-auth-confirm").required = 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-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;
|
document.getElementById("m-auth-error").hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,18 +279,29 @@
|
|||||||
document.getElementById("m-auth-form").addEventListener("submit", submitAuth);
|
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) {
|
async function submitAuth(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const username = document.getElementById("m-auth-username").value.trim();
|
const username = document.getElementById("m-auth-username").value.trim();
|
||||||
const password = document.getElementById("m-auth-password").value;
|
const password = document.getElementById("m-auth-password").value;
|
||||||
const errorElement = document.getElementById("m-auth-error");
|
const errorElement = document.getElementById("m-auth-error");
|
||||||
if (authMode === "register" && password !== document.getElementById("m-auth-confirm").value) {
|
if (authMode === "register" && password !== document.getElementById("m-auth-confirm").value) {
|
||||||
errorElement.textContent = "两次输入的密码不一致。";
|
showAuthError(errorElement, "两次输入的密码不一致。");
|
||||||
errorElement.hidden = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const submit = document.getElementById("m-auth-submit");
|
const submit = document.getElementById("m-auth-submit");
|
||||||
|
const spinner = submit.querySelector(".m-btn-spinner");
|
||||||
|
const label = submit.querySelector(".m-btn-label");
|
||||||
submit.disabled = true;
|
submit.disabled = true;
|
||||||
|
spinner.hidden = false;
|
||||||
|
label.textContent = authMode === "register" ? "注册中…" : "登录中…";
|
||||||
try {
|
try {
|
||||||
if (authMode === "register") {
|
if (authMode === "register") {
|
||||||
await global.MobileSession.register(username, password);
|
await global.MobileSession.register(username, password);
|
||||||
@@ -287,10 +310,11 @@
|
|||||||
}
|
}
|
||||||
replace(DEFAULT_HASH);
|
replace(DEFAULT_HASH);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorElement.textContent = error.message || "账号操作失败";
|
showAuthError(errorElement, error.message || "账号操作失败");
|
||||||
errorElement.hidden = false;
|
|
||||||
} finally {
|
} finally {
|
||||||
submit.disabled = false;
|
submit.disabled = false;
|
||||||
|
spinner.hidden = true;
|
||||||
|
label.textContent = authMode === "register" ? "注册并进入" : "登录";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +336,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render(motion) {
|
||||||
const route = currentRoute();
|
const route = currentRoute();
|
||||||
if (route.name === "home") {
|
if (route.name === "home") {
|
||||||
replace(DEFAULT_HASH);
|
replace(DEFAULT_HASH);
|
||||||
@@ -331,6 +355,7 @@
|
|||||||
else if (route.name === "feature") renderFeature(route.params.join("/"));
|
else if (route.name === "feature") renderFeature(route.params.join("/"));
|
||||||
else if (route.name === "assistant") renderAssistant();
|
else if (route.name === "assistant") renderAssistant();
|
||||||
else if (route.name === "auth") renderAuth();
|
else if (route.name === "auth") renderAuth();
|
||||||
|
applyViewMotion(motion);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindGlobalEvents() {
|
function bindGlobalEvents() {
|
||||||
@@ -359,10 +384,11 @@
|
|||||||
const hash = currentHash();
|
const hash = currentHash();
|
||||||
if (stack.length > 1 && hash === stack[stack.length - 2]) {
|
if (stack.length > 1 && hash === stack[stack.length - 2]) {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
|
render("m-motion-pop-in");
|
||||||
} else if (hash !== stack[stack.length - 1]) {
|
} else if (hash !== stack[stack.length - 1]) {
|
||||||
stack.push(hash);
|
stack.push(hash);
|
||||||
|
render("m-motion-push-in");
|
||||||
}
|
}
|
||||||
render();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +403,7 @@
|
|||||||
} else {
|
} else {
|
||||||
stack.push(currentHash());
|
stack.push(currentHash());
|
||||||
}
|
}
|
||||||
render();
|
render("m-motion-boot-in");
|
||||||
}
|
}
|
||||||
|
|
||||||
global.MobileRouter = {
|
global.MobileRouter = {
|
||||||
|
|||||||
Reference in New Issue
Block a user