Files
opencatd-open/frontend/src/views/auth/Login.vue
T
Sakurasan ac0a6808ec refactor: frontend TS migration + Tailwind4/daisyUI5 unify + BUILDPLATFORM docker build
- migrate frontend to TypeScript (vue-tsc strict in build), upgrade all deps to latest (Vite 8, Tailwind 4, daisyUI 5, Pinia 4, vue-router 5)
- restructure frontend dirs (api/components/common/layouts/styles/types/views)
- drop Element Plus, add daisyUI TagInput; main CSS 490KB->163KB, entry JS 618KB->1.3KB
- rewrite Dockerfile(.cn): frontend/backend stages pinned to $BUILDPLATFORM, CGO_ENABLED=0 cross-compile, no QEMU in multi-arch builds; add .dockerignore
- local dev: Vite /api proxy + make dev targets; go:embed all:dist with .gitkeep so backend runs without prior frontend build
- fix latent bugs: Keys.vue users ref, Settings.vue undefined userStore, Login.vue Ref-as-error display, res.error misuse
- add REFACTOR_PLAN.md (phased refactor log)
2026-08-29 23:27:31 +08:00

190 lines
8.5 KiB
Vue

<template>
<div class="min-h-screen flex items-center justify-center p-4">
<div class="card w-full max-w-md bg-base-100 shadow-xl">
<div class="card-body p-4 sm:p-6">
<img src="@/assets/openteam.webp" alt="Company Logo" class="h-32 w-auto mx-auto mb-0 pb-0 select-none hover:cursor-pointer"
@click="$router.push('/')" />
<h2 class="card-title text-md sm:text-xl mb-6 justify-center flex">
Log in to Dashboard
</h2>
<form @submit.prevent="handleLogin">
<div class="form-control mb-4">
<label class="label" for="username">
<span class="label-text">Account</span>
</label>
<input type="text" id="username" placeholder="username/email" class="input input-bordered w-full input-sm"
v-model="user.username" required />
</div>
<div class="form-control mb-4">
<label class="label" for="password">
<span class="label-text">Password</span>
<a href="#" class="label-text-alt link link-hover link-primary text-sm">
Forgot password?
</a>
</label>
<input type="password" id="password" placeholder="••••••••" class="input input-bordered w-full input-sm"
v-model="user.password" minlength="4" required />
</div>
<div class="form-control mb-6">
<label class="label cursor-pointer justify-start gap-2">
<input type="checkbox" v-model="user.rember" class="checkbox checkbox-primary checkbox-sm" />
<span class="label-text text-sm">Remember me</span>
</label>
</div>
<div class="form-control mb-4">
<button type="submit" class="btn btn-outline w-full btn-sm">
Log In
</button>
</div>
<div v-if="error" role="alert" class="alert shadow-lg bg-rose-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<span>{{ error }}</span>
</div>
<button class="btn btn-sm" @click="error = null">X</button>
</div>
</form>
<div class="divider my-2 text-sm">OR</div>
<div class="form-control mb-6">
<button @click="handlePasskeyLogin" class="btn btn-outline w-full btn-sm" :disabled="!supportWebAuth">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"
viewBox="0 0 24 24">
<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>
</div>
<div class="hidden flex flex-col sm:flex-row gap-3 w-full">
<button @click="handleGithubLogin" class="btn btn-outline flex-1 btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-github mr-1"
viewBox="0 0 16 16">
<path
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8" />
</svg>
</button>
<button @click="handleGoogleLogin" class="btn btn-outline flex-1 btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-google mr-1"
viewBox="0 0 16 16">
<path
d="M15.545 6.558a9.4 9.4 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.7 7.7 0 0 1 5.352 2.082l-2.284 2.284A4.35 4.35 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.8 4.8 0 0 0 0 3.063h.003c.635 1.896 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.7 3.7 0 0 0 1.599-2.431H8v-3.08z" />
</svg>
</button>
<button @click="handleTelegramLogin" class="btn btn-outline flex-1 btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-telegram mr-1" viewBox="0 0 16 16">
<path
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.287 5.906q-1.168.486-4.666 2.01-.567.225-.595.442c-.03.243.275.339.69.47l.175.055c.408.133.958.288 1.243.294q.39.01.868-.32 3.269-2.206 3.374-2.23c.05-.012.12-.026.166.016s.042.12.037.141c-.03.129-1.227 1.241-1.846 1.817-.193.18-.33.307-.358.336a8 8 0 0 1-.188.186c-.38.366-.664.64.015 1.088.327.216.589.393.85.571.284.194.568.387.936.629q.14.092.27.187c.331.236.63.448.997.414.214-.02.435-.22.547-.82.265-1.417.786-4.486.906-5.751a1.4 1.4 0 0 0-.013-.315.34.34 0 0 0-.114-.217.53.53 0 0 0-.31-.093c-.3.005-.763.166-2.984 1.09" />
</svg>
</button>
</div>
<div class="text-center mt-6 text-sm">
Don't have an account?
<router-link to="/signup" class="link link-primary link-hover">
Sign up
</router-link>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth';
import { useWebAuthStore } from '@/stores/webauth';
import { useToast } from '@/composables/toast';
// import request from '@/api/client';
const router = useRouter()
const authStore = useAuthStore();
const webauthStore = useWebAuthStore();
const { setToast } = useToast();
const error = ref<string | null>(null)
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;
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('登录成功', 'success');
router.push('/dashboard');
}
} catch (err: any) {
console.error('Login error:', err);
error.value = errMsg(err)
}
}
const handlePasskeyLogin = async () => {
error.value = null;
try {
const res = await webauthStore.loginPasskey();
if (!!res?.code && res.code === 200) {
setToast('登录成功', 'success');
router.push('/dashboard');
}
} catch (err: any) {
console.error('Passkey login error:', err);
error.value = errMsg(err)
}
};
const handleGithubLogin = () => {
alert('GitHub login is not yet implemented.');
};
const handleGoogleLogin = () => {
alert('Google login is not yet implemented.');
};
const handleTelegramLogin = () => {
alert('Telegram login is not yet implemented.');
};
</script>