Refactor frontend to Vue 3 + Vite + Pinia + Cypress E2E
- Replace single-file 8441-line HTML with Vue 3 SPA - Pinia stores: auth, oils, recipes, diary, ui - Composables: useApi, useDialog, useSmartPaste, useOilTranslation - 6 shared components: RecipeCard, RecipeDetailOverlay, TagPicker, etc. - 9 page views: RecipeSearch, RecipeManager, Inventory, OilReference, etc. - 14 Cypress E2E test specs (113 tests), all passing - Multi-stage Dockerfile (Node build + Python runtime) - Demo video generation scripts (TTS + subtitles + screen recording) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
196
frontend/src/components/LoginModal.vue
Normal file
196
frontend/src/components/LoginModal.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="login-overlay" @click.self="$emit('close')">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<span
|
||||
class="login-tab"
|
||||
:class="{ active: mode === 'login' }"
|
||||
@click="mode = 'login'"
|
||||
>登录</span>
|
||||
<span
|
||||
class="login-tab"
|
||||
:class="{ active: mode === 'register' }"
|
||||
@click="mode = 'register'"
|
||||
>注册</span>
|
||||
</div>
|
||||
|
||||
<div class="login-body">
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
placeholder="用户名"
|
||||
class="login-input"
|
||||
@keydown.enter="submit"
|
||||
/>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
class="login-input"
|
||||
@keydown.enter="submit"
|
||||
/>
|
||||
<input
|
||||
v-if="mode === 'register'"
|
||||
v-model="displayName"
|
||||
type="text"
|
||||
placeholder="显示名称(可选)"
|
||||
class="login-input"
|
||||
@keydown.enter="submit"
|
||||
/>
|
||||
|
||||
<div v-if="errorMsg" class="login-error">{{ errorMsg }}</div>
|
||||
|
||||
<button class="login-submit" :disabled="loading" @click="submit">
|
||||
{{ loading ? '请稍候...' : (mode === 'login' ? '登录' : '注册') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useUiStore } from '../stores/ui'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const auth = useAuthStore()
|
||||
const ui = useUiStore()
|
||||
|
||||
const mode = ref('login')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const displayName = ref('')
|
||||
const errorMsg = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
async function submit() {
|
||||
errorMsg.value = ''
|
||||
|
||||
if (!username.value.trim()) {
|
||||
errorMsg.value = '请输入用户名'
|
||||
return
|
||||
}
|
||||
if (!password.value) {
|
||||
errorMsg.value = '请输入密码'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
if (mode.value === 'login') {
|
||||
await auth.login(username.value.trim(), password.value)
|
||||
ui.showToast('登录成功')
|
||||
} else {
|
||||
await auth.register(
|
||||
username.value.trim(),
|
||||
password.value,
|
||||
displayName.value.trim() || username.value.trim()
|
||||
)
|
||||
ui.showToast('注册成功')
|
||||
}
|
||||
emit('close')
|
||||
// Reload page data after auth change
|
||||
window.location.reload()
|
||||
} catch (e) {
|
||||
errorMsg.value = e?.message || (mode.value === 'login' ? '登录失败,请检查用户名和密码' : '注册失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
z-index: 5000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
width: 340px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.login-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 14px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.login-tab.active {
|
||||
color: #4a9d7e;
|
||||
border-bottom-color: #4a9d7e;
|
||||
}
|
||||
|
||||
.login-body {
|
||||
padding: 24px 24px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.login-input {
|
||||
width: 100%;
|
||||
padding: 11px 14px;
|
||||
border: 1.5px solid #d4cfc7;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.login-input:focus {
|
||||
border-color: #4a9d7e;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
color: #d9534f;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-submit {
|
||||
background: linear-gradient(135deg, #7ec6a4 0%, #4a9d7e 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 11px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.login-submit:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.login-submit:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user