feat(monitoring): dispatch monitoring tasks to desktop via AMQP outbox
Replace SSE /desktop/events with priority AMQP dispatch for monitoring
runs, and add phase1/phase2 infrastructure so desktop workers can lease,
resume, report, skip, and cancel monitoring tasks over the existing
dispatch WebSocket.
- Add monitoring collect outbox worker and phase2 desktop task fields
- Add /desktop/monitoring/tasks/{lease,resume,result,skip,cancel} routes
- Introduce execution-devtools and network-observer on desktop runtime
- Refactor runtime-controller, LoginView, and doubao adapter for the new flow
- Add channelName column to tracking views
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, onMounted } from "vue";
|
||||
import { defineAsyncComponent, onMounted, watch } from "vue";
|
||||
import { useDesktopSession } from "./composables/useDesktopSession";
|
||||
|
||||
const DesktopShell = defineAsyncComponent(() => import("./components/DesktopShell.vue"));
|
||||
@@ -7,8 +7,28 @@ const LoginView = defineAsyncComponent(() => import("./views/LoginView.vue"));
|
||||
|
||||
const { isAuthenticated, syncRuntimeSession } = useDesktopSession();
|
||||
|
||||
interface WindowModeBridge {
|
||||
setWindowMode?: (mode: "login" | "main") => Promise<unknown>;
|
||||
}
|
||||
|
||||
function getModeBridge(): WindowModeBridge | undefined {
|
||||
return (globalThis as unknown as {
|
||||
desktopBridge?: { app?: WindowModeBridge };
|
||||
}).desktopBridge?.app;
|
||||
}
|
||||
|
||||
function syncWindowMode(authenticated: boolean): void {
|
||||
void getModeBridge()?.setWindowMode?.(authenticated ? "main" : "login");
|
||||
}
|
||||
|
||||
// Fire immediately during setup so the main process can size + reveal the
|
||||
// window before the user sees any "wrong-sized" frame.
|
||||
syncWindowMode(isAuthenticated.value);
|
||||
watch(isAuthenticated, (next) => syncWindowMode(next));
|
||||
|
||||
onMounted(() => {
|
||||
void syncRuntimeSession();
|
||||
syncWindowMode(isAuthenticated.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ const clientLabel = computed(
|
||||
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<div class="window-drag-bar" aria-hidden="true"></div>
|
||||
<aside class="sidebar">
|
||||
<div class="brand-block">
|
||||
<div class="brand-mark">
|
||||
@@ -144,6 +145,7 @@ const clientLabel = computed(
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<div class="content-dragbar" aria-hidden="true"></div>
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
@@ -156,6 +158,23 @@ const clientLabel = computed(
|
||||
overflow: hidden;
|
||||
background: #f0f2f5;
|
||||
color: #1a1a1a;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Real drag handle for the macOS hidden title bar — must be above content
|
||||
but only spans the empty zone behind the traffic lights. */
|
||||
.window-drag-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 80px;
|
||||
right: 0;
|
||||
height: 28px;
|
||||
z-index: 1000;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.app-shell :where(button, a, input, select, textarea, [role="button"], [role="link"]) {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@@ -165,6 +184,7 @@ const clientLabel = computed(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
padding-top: 44px;
|
||||
border-right: 1px solid #e6edf5;
|
||||
background: #ffffff;
|
||||
flex-shrink: 0;
|
||||
@@ -174,6 +194,7 @@ const clientLabel = computed(
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
@@ -459,6 +480,18 @@ h1 {
|
||||
height: 100vh;
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-dragbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 24px;
|
||||
margin: -24px -32px 0;
|
||||
-webkit-app-region: drag;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
|
||||
+3
@@ -5,6 +5,7 @@ import type {
|
||||
DesktopRuntimeSessionSyncRequest,
|
||||
ListDesktopPublishTasksParams,
|
||||
} from "@geo/shared-types";
|
||||
import type { DesktopObservedRequest } from "../shared/network-debug";
|
||||
import type { DesktopRuntimeSnapshot } from "./types";
|
||||
|
||||
export {};
|
||||
@@ -29,9 +30,11 @@ declare global {
|
||||
retryPublishTask(taskId: string): Promise<CreatePublishJobResponse>;
|
||||
syncRuntimeSession(session: DesktopRuntimeSessionSyncRequest | null): Promise<null>;
|
||||
releaseRuntimeSession(revoke?: boolean): Promise<null>;
|
||||
setWindowMode(mode: "login" | "main"): Promise<null>;
|
||||
onRuntimeInvalidated(
|
||||
listener: (event: { reason: "account-health"; at: number }) => void,
|
||||
): () => void;
|
||||
onNetworkObserved(listener: (event: DesktopObservedRequest) => void): () => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createApp } from "vue";
|
||||
|
||||
import App from "./App.vue";
|
||||
import { router } from "./routes";
|
||||
import type { DesktopObservedRequest } from "../shared/network-debug";
|
||||
|
||||
import "ant-design-vue/dist/reset.css";
|
||||
import "../../../../packages/ui-shared/src/tokens/index.css";
|
||||
@@ -14,4 +15,42 @@ const themeQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
applyTheme(themeQuery.matches);
|
||||
themeQuery.addEventListener("change", (event) => applyTheme(event.matches));
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
window.desktopBridge.app.onNetworkObserved((event) => {
|
||||
logObservedRequest(event);
|
||||
});
|
||||
}
|
||||
|
||||
createApp(App).use(router).mount("#app");
|
||||
|
||||
function logObservedRequest(event: DesktopObservedRequest): void {
|
||||
const summary = `${event.method} ${event.url}`;
|
||||
const detail = {
|
||||
kind: event.kind,
|
||||
source: event.source,
|
||||
label: event.label,
|
||||
partition: event.partition,
|
||||
resourceType: event.resourceType,
|
||||
status: event.status,
|
||||
durationMs: event.durationMs,
|
||||
error: event.error,
|
||||
at: event.at,
|
||||
};
|
||||
|
||||
if (event.phase === "error") {
|
||||
console.warn("[electron-request:error]", summary, detail);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.phase === "close") {
|
||||
console.info("[electron-request:close]", summary, detail);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.phase === "response") {
|
||||
console.info("[electron-request:response]", summary, detail);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[electron-request:start]", summary, detail);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import { useDesktopSession } from "../composables/useDesktopSession";
|
||||
|
||||
const { apiBaseURL, error, pending, setApiBaseURL, login } = useDesktopSession();
|
||||
|
||||
const showServerSettings = ref(false);
|
||||
const menuOpen = ref(false);
|
||||
const showServerDialog = ref(false);
|
||||
const rememberPassword = ref(!!localStorage.getItem("geo_rankly_saved_password"));
|
||||
|
||||
const form = reactive({
|
||||
@@ -13,6 +14,35 @@ const form = reactive({
|
||||
password: localStorage.getItem("geo_rankly_saved_password") || "",
|
||||
});
|
||||
|
||||
const menuRef = ref<HTMLElement | null>(null);
|
||||
|
||||
function toggleMenu() {
|
||||
menuOpen.value = !menuOpen.value;
|
||||
}
|
||||
|
||||
function handleOutsideClick(event: MouseEvent) {
|
||||
if (!menuOpen.value) return;
|
||||
const root = menuRef.value;
|
||||
if (root && !root.contains(event.target as Node)) {
|
||||
menuOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openServerSettings() {
|
||||
menuOpen.value = false;
|
||||
form.apiBaseURL = apiBaseURL.value;
|
||||
showServerDialog.value = true;
|
||||
}
|
||||
|
||||
function closeServerSettings() {
|
||||
showServerDialog.value = false;
|
||||
}
|
||||
|
||||
function saveServerSettings() {
|
||||
setApiBaseURL(form.apiBaseURL);
|
||||
showServerDialog.value = false;
|
||||
}
|
||||
|
||||
async function submitLogin() {
|
||||
if (rememberPassword.value) {
|
||||
localStorage.setItem("geo_rankly_saved_email", form.email);
|
||||
@@ -22,12 +52,19 @@ async function submitLogin() {
|
||||
localStorage.removeItem("geo_rankly_saved_password");
|
||||
}
|
||||
|
||||
setApiBaseURL(form.apiBaseURL);
|
||||
await login({
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("mousedown", handleOutsideClick);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("mousedown", handleOutsideClick);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -36,64 +73,91 @@ async function submitLogin() {
|
||||
<div class="orb orb-1"></div>
|
||||
<div class="orb orb-2"></div>
|
||||
<div class="orb orb-3"></div>
|
||||
<div class="grid-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div class="top-bar" ref="menuRef">
|
||||
<button
|
||||
type="button"
|
||||
class="menu-trigger"
|
||||
:class="{ active: menuOpen }"
|
||||
aria-label="菜单"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<div v-if="menuOpen" class="menu-dropdown">
|
||||
<button type="button" class="menu-item" @click="openServerSettings">
|
||||
服务器设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-wrapper">
|
||||
<div class="auth-card">
|
||||
<header class="brand-header">
|
||||
<div class="brand-logo">
|
||||
<span class="brand-core"></span>
|
||||
</div>
|
||||
<h2>登录 GEO Rankly</h2>
|
||||
<p>登录租户账号,绑定专属客户端环境</p>
|
||||
</header>
|
||||
<header class="brand-header">
|
||||
<div class="brand-logo">
|
||||
<span class="brand-core"></span>
|
||||
</div>
|
||||
<h1 class="brand-name">GEO Rankly</h1>
|
||||
</header>
|
||||
|
||||
<form class="login-form" @submit.prevent="submitLogin">
|
||||
<label class="field">
|
||||
<span>邮箱 / 账号</span>
|
||||
<input
|
||||
v-model.trim="form.email"
|
||||
type="email"
|
||||
autocomplete="username"
|
||||
placeholder="you@company.com"
|
||||
required
|
||||
/>
|
||||
<form class="login-form" @submit.prevent="submitLogin">
|
||||
<div class="field">
|
||||
<input
|
||||
v-model.trim="form.email"
|
||||
type="email"
|
||||
autocomplete="username"
|
||||
placeholder="邮箱 / 账号"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="密码"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="options-row">
|
||||
<label class="check-label">
|
||||
<input type="checkbox" v-model="rememberPassword" />
|
||||
<span>记住密码</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span>密码</span>
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" class="submit-button" :disabled="pending">
|
||||
{{ pending ? "登录中..." : "登 录" }}
|
||||
</button>
|
||||
|
||||
<div class="remember-row">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="rememberPassword" />
|
||||
<span>记住账号和密码</span>
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="error" class="error-text">{{ error }}</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-button" :disabled="pending">
|
||||
{{ pending ? "验证中..." : "登 录" }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="advanced-section">
|
||||
<a
|
||||
v-if="!showServerSettings"
|
||||
class="toggle-link"
|
||||
@click="showServerSettings = true"
|
||||
>
|
||||
服务器选项
|
||||
</a>
|
||||
<div v-else class="advanced-fields">
|
||||
<label class="field subtle-field">
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showServerDialog"
|
||||
class="dialog-mask"
|
||||
@click.self="closeServerSettings"
|
||||
>
|
||||
<div class="dialog" role="dialog" aria-modal="true">
|
||||
<header class="dialog-header">
|
||||
<h3>服务器设置</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="dialog-close"
|
||||
aria-label="关闭"
|
||||
@click="closeServerSettings"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<div class="dialog-body">
|
||||
<label class="dialog-field">
|
||||
<span>网关 API 地址</span>
|
||||
<input
|
||||
v-model.trim="form.apiBaseURL"
|
||||
@@ -102,29 +166,40 @@ async function submitLogin() {
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<p class="dialog-hint">修改后对本机下次登录生效。</p>
|
||||
</div>
|
||||
<footer class="dialog-footer">
|
||||
<button type="button" class="btn-ghost" @click="closeServerSettings">
|
||||
取消
|
||||
</button>
|
||||
<button type="button" class="btn-primary" @click="saveServerSettings">
|
||||
保存
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error-text">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<footer class="login-footer">
|
||||
<p>GEO Rankly Desktop Environment</p>
|
||||
</footer>
|
||||
</div>
|
||||
</Transition>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-shell {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: #f7f9fc;
|
||||
padding: 24px;
|
||||
background: linear-gradient(180deg, #eaf2ff 0%, #e4ecff 40%, #e8efff 100%);
|
||||
padding: 0 24px 28px;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.login-shell input,
|
||||
.login-shell button,
|
||||
.login-shell a,
|
||||
.login-shell label {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.bg-layer {
|
||||
@@ -139,269 +214,81 @@ async function submitLogin() {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
opacity: 0.6;
|
||||
animation: float 20s infinite ease-in-out alternate;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.orb-1 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, rgba(22, 119, 255, 0.4), rgba(22, 119, 255, 0));
|
||||
top: -200px;
|
||||
left: -200px;
|
||||
animation-delay: 0s;
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
background: radial-gradient(circle, rgba(120, 170, 255, 0.45), rgba(120, 170, 255, 0));
|
||||
top: -180px;
|
||||
left: -160px;
|
||||
}
|
||||
|
||||
.orb-2 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(circle, rgba(20, 184, 166, 0.3), rgba(20, 184, 166, 0));
|
||||
bottom: -150px;
|
||||
right: -100px;
|
||||
animation-duration: 25s;
|
||||
animation-delay: -5s;
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
background: radial-gradient(circle, rgba(180, 210, 255, 0.5), rgba(180, 210, 255, 0));
|
||||
bottom: -140px;
|
||||
right: -120px;
|
||||
}
|
||||
|
||||
.orb-3 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0));
|
||||
top: 50%;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(200, 220, 255, 0.45), rgba(200, 220, 255, 0));
|
||||
top: 55%;
|
||||
left: 60%;
|
||||
animation-duration: 22s;
|
||||
animation-delay: -10s;
|
||||
}
|
||||
|
||||
.grid-overlay {
|
||||
.top-bar {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
opacity: 0.6;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(5%, 10%) scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-10%, -5%) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
animation: slideUpFade 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes slideUpFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
padding: 40px 32px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.brand-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
background: #f0f7ff;
|
||||
border: 1px solid #bae0ff;
|
||||
}
|
||||
|
||||
.brand-core {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #1677ff;
|
||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.2);
|
||||
}
|
||||
|
||||
.brand-header h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.brand-header p {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.field span {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.field input {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(217, 217, 217, 0.8);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #1a1a1a;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field input:focus {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
|
||||
.field input::placeholder {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.remember-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
.menu-trigger {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
margin: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
accent-color: #1677ff;
|
||||
}
|
||||
|
||||
.checkbox-label span {
|
||||
font-size: 13px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
margin-top: 8px;
|
||||
height: 40px;
|
||||
background: #1677ff;
|
||||
color: #ffffff;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.submit-button:hover:not(:disabled) {
|
||||
background: #4096ff;
|
||||
.menu-trigger span {
|
||||
width: 14px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: #3a4a66;
|
||||
}
|
||||
|
||||
.submit-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
.menu-trigger:hover,
|
||||
.menu-trigger.active {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.advanced-section {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
.menu-dropdown {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
right: 0;
|
||||
min-width: 140px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 32px rgba(31, 58, 112, 0.18);
|
||||
padding: 8px;
|
||||
animation: dropdown-in 0.15s ease;
|
||||
}
|
||||
|
||||
.toggle-link {
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.toggle-link:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.advanced-fields {
|
||||
text-align: left;
|
||||
padding-top: 16px;
|
||||
border-top: 1px dashed #e6edf5;
|
||||
animation: fade-in 0.3s ease;
|
||||
}
|
||||
|
||||
.subtle-field span {
|
||||
color: #8c8c8c;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.subtle-field input {
|
||||
background: #fafafb;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin: 16px 0 0;
|
||||
padding: 10px 12px;
|
||||
background: #fff2f0;
|
||||
border: 1px solid #ffccc7;
|
||||
border-radius: 6px;
|
||||
color: #ff4d4f;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
@keyframes dropdown-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
@@ -411,4 +298,316 @@ async function submitLogin() {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #f2f6ff;
|
||||
}
|
||||
|
||||
.auth-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
animation: slideUpFade 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes slideUpFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
|
||||
box-shadow: 0 10px 22px rgba(22, 119, 255, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.brand-core {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.25);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.brand-core::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 7px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(135deg, #1677ff, #69b1ff);
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
margin: 10px 0 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.field input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
box-shadow: 0 4px 12px rgba(31, 58, 112, 0.08);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.field input::placeholder {
|
||||
color: #9aa4b8;
|
||||
}
|
||||
|
||||
.field input:focus {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 4px 14px rgba(31, 58, 112, 0.08), 0 0 0 3px rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
|
||||
.options-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.check-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.check-label input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #1677ff;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
margin-top: 6px;
|
||||
height: 40px;
|
||||
background: #1677ff;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.4em;
|
||||
padding-left: 0.4em;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 16px rgba(22, 119, 255, 0.25);
|
||||
transition: background 0.2s ease, transform 0.1s ease;
|
||||
}
|
||||
|
||||
.submit-button:hover:not(:disabled) {
|
||||
background: #4096ff;
|
||||
}
|
||||
|
||||
.submit-button:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.submit-button:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin: 4px 0 0;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 77, 79, 0.08);
|
||||
border: 1px solid rgba(255, 77, 79, 0.3);
|
||||
border-radius: 10px;
|
||||
color: #d9363e;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(17, 24, 39, 0.35);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: min(360px, calc(100vw - 32px));
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #eef1f6;
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
color: #9aa4b8;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
background: #f2f6ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 18px 20px 8px;
|
||||
}
|
||||
|
||||
.dialog-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-field span {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.dialog-field input {
|
||||
height: 38px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #d6dde8;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-field input:focus {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
|
||||
.dialog-hint {
|
||||
margin: 10px 0 0;
|
||||
font-size: 12px;
|
||||
color: #9aa4b8;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 14px 20px 18px;
|
||||
}
|
||||
|
||||
.btn-ghost,
|
||||
.btn-primary {
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
border-color: #d6dde8;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: #f6f8fc;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #1677ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #4096ff;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user