- unified design system: focus-visible rings, reduced-motion, color-scheme, theme persistence, tightened emerald radius tokens (checkbox circle bug) - redesigned all pages: Home/Login/Signup/404, dashboard shell, Overview hero (time-based gradient), list pages with empty states, form modals, detail pages - role-scoped navigation: console menu vs admin console with cross-switch entries, Chinese route titles and breadcrumbs - theme switcher (light/dark/auto) via composables/theme, home + dashboard - state-driven dropdowns (touch-safe logout), menu divider pointer-events fix - btn-primary unified to black-on-white / white-on-black per theme - update REFACTOR_PLAN.md (increments 2-6)
143 lines
5.7 KiB
Vue
143 lines
5.7 KiB
Vue
<template>
|
|
<div class="flex min-h-screen items-center justify-center bg-base-200 p-4">
|
|
<div class="card w-full max-w-md border border-base-300/60 bg-base-100 shadow-sm">
|
|
<div class="card-body p-6 sm:p-8">
|
|
<RouterLink to="/" class="mx-auto select-none" aria-label="Back to home">
|
|
<img src="@/assets/openteam.webp" alt="OpenTeam logo" width="176" height="128" class="h-28 w-auto" />
|
|
</RouterLink>
|
|
|
|
<h1 class="mt-2 text-center text-xl font-semibold tracking-tight">Log in to Dashboard</h1>
|
|
|
|
<div v-if="error" role="alert" class="alert alert-error mt-4 text-sm">
|
|
<CircleAlert class="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
<span class="flex-1">{{ error }}</span>
|
|
<button type="button" class="btn btn-ghost btn-xs" aria-label="Dismiss error" @click="error = null">✕</button>
|
|
</div>
|
|
|
|
<form class="mt-4 space-y-4" @submit.prevent="handleLogin">
|
|
<div>
|
|
<label class="mb-1 block text-sm font-medium" for="username">Account</label>
|
|
<input type="text" id="username" name="username" autocomplete="username" spellcheck="false"
|
|
placeholder="username or email" class="input input-bordered w-full"
|
|
v-model="user.username" required />
|
|
</div>
|
|
|
|
<div>
|
|
<label class="mb-1 block text-sm font-medium" for="password">Password</label>
|
|
<input type="password" id="password" name="password" autocomplete="current-password"
|
|
placeholder="••••••••" class="input input-bordered w-full"
|
|
v-model="user.password" minlength="4" required />
|
|
</div>
|
|
|
|
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
|
<input type="checkbox" name="remember" v-model="user.rember" class="checkbox checkbox-primary checkbox-sm" />
|
|
Remember me
|
|
</label>
|
|
|
|
<button type="submit" class="btn btn-primary w-full" :disabled="loggingIn">
|
|
<span v-if="loggingIn" class="loading loading-spinner loading-xs" aria-hidden="true"></span>
|
|
Log In
|
|
</button>
|
|
</form>
|
|
|
|
<div class="divider my-2 text-xs text-base-content/40">OR</div>
|
|
|
|
<button type="button" @click="handlePasskeyLogin" class="btn btn-outline w-full" :disabled="!supportWebAuth"
|
|
:aria-label="supportWebAuth ? 'Sign in with Passkey' : 'Passkey not supported in this browser'">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path fill="currentColor"
|
|
d="M7 13.23q-.517 0-.874-.356T5.769 12t.357-.874t.874-.357t.874.357t.357.874t-.357.874t-.874.357M7 17q-2.077 0-3.538-1.461T2 12t1.462-3.538T7 7q1.54 0 2.778.835q1.238.834 1.807 2.165h9.204l2 2l-3.193 3.154l-1.712-1.288l-1.807 1.326L14.298 14h-2.713q-.57 1.312-1.807 2.156T7 17m0-1q1.477 0 2.52-.889T10.856 13h3.76l1.43.967l1.858-1.333l1.621 1.222L21.381 12l-1-1h-9.525q-.292-1.223-1.336-2.111T7 8Q5.35 8 4.175 9.175T3 12t1.175 2.825T7 16" />
|
|
</svg>
|
|
Sign in with Passkey
|
|
</button>
|
|
|
|
<p class="mt-5 text-center text-sm text-base-content/60">
|
|
Don't have an account?
|
|
<RouterLink to="/signup" class="link link-primary link-hover">Sign up</RouterLink>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { CircleAlert } from '@lucide/vue'
|
|
import { useAuthStore } from '@/stores/auth';
|
|
import { useWebAuthStore } from '@/stores/webauth';
|
|
import { useToast } from '@/composables/toast';
|
|
|
|
const router = useRouter()
|
|
const authStore = useAuthStore();
|
|
const webauthStore = useWebAuthStore();
|
|
const { setToast } = useToast();
|
|
|
|
const error = ref<string | null>(null)
|
|
const loggingIn = ref(false)
|
|
const user = reactive({
|
|
username: localStorage.getItem('account') || '',
|
|
password: localStorage.getItem('password') || '',
|
|
rember: localStorage.getItem('rember') || false,
|
|
})
|
|
|
|
const supportWebAuth = ref(false);
|
|
onMounted(() => {
|
|
if (localStorage.getItem('token')) {
|
|
router.push('/dashboard')
|
|
}
|
|
supportWebAuth.value = !!window.PublicKeyCredential;
|
|
})
|
|
|
|
// store 的 catch 里 throw 的是 error ref,这里统一取出可展示的错误信息
|
|
const errMsg = (err: any): string => {
|
|
if (typeof err === 'string') return err
|
|
if (err?.__v_isRef) return errMsg(err.value)
|
|
return err?.response?.data?.error || err?.message || String(err)
|
|
}
|
|
|
|
|
|
const handleLogin = async () => {
|
|
error.value = null;
|
|
loggingIn.value = true;
|
|
try {
|
|
const response = await authStore.login({ username: user.username, password: user.password });
|
|
if (response.status === 200) {
|
|
if (user.rember) {
|
|
localStorage.setItem('account', user.username);
|
|
localStorage.setItem('password', user.password);
|
|
localStorage.setItem('rember', String(user.rember));
|
|
} else {
|
|
localStorage.removeItem('account');
|
|
localStorage.removeItem('password');
|
|
localStorage.removeItem('rember');
|
|
}
|
|
setToast('Logged in successfully.', 'success');
|
|
router.push('/dashboard');
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Login error:', err);
|
|
error.value = errMsg(err)
|
|
} finally {
|
|
loggingIn.value = false;
|
|
}
|
|
}
|
|
|
|
const handlePasskeyLogin = async () => {
|
|
error.value = null;
|
|
loggingIn.value = true;
|
|
try {
|
|
const res = await webauthStore.loginPasskey();
|
|
if (!!res?.code && res.code === 200) {
|
|
setToast('Logged in successfully.', 'success');
|
|
router.push('/dashboard');
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Passkey login error:', err);
|
|
error.value = errMsg(err)
|
|
} finally {
|
|
loggingIn.value = false;
|
|
}
|
|
};
|
|
</script>
|