Files
oil-formula-calculator/frontend/src/components/LoginModal.vue
Hera Zhao fdc7b20929
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 16s
Test / e2e-test (push) Successful in 52s
feat: 取消显示名称,统一用户名,大小写不敏感去重,一次性改名
- 注册: 去掉display_name字段,用户名大小写不敏感去重
- 登录: 大小写不敏感匹配
- 用户菜单: 显示用户名+改名按钮(只能改一次)
- /api/me/username: 一次性改名API
- 启动时: sync display_name=username, 发通知告知用户
- 前端: 所有display_name显示改为username

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:20:23 +00:00

260 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="login-overlay" @mousedown.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="confirmPassword"
type="password"
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 class="login-divider"></div>
<button v-if="!showFeedback" class="login-feedback-btn" @click="showFeedback = true">🐛 反馈问题无需登录</button>
<div v-if="showFeedback" class="feedback-section">
<textarea v-model="feedbackText" class="login-input" rows="3" placeholder="描述你遇到的问题..." style="resize:vertical;"></textarea>
<button class="login-submit" :disabled="!feedbackText.trim() || feedbackLoading" @click="submitFeedback">
{{ feedbackLoading ? '提交中...' : '提交反馈' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useAuthStore } from '../stores/auth'
import { useUiStore } from '../stores/ui'
import { api } from '../composables/useApi'
const emit = defineEmits(['close'])
const auth = useAuthStore()
const ui = useUiStore()
const mode = ref('login')
const username = ref('')
const password = ref('')
const confirmPassword = ref('')
const errorMsg = ref('')
const loading = ref(false)
const showFeedback = ref(false)
const feedbackText = ref('')
const feedbackLoading = ref(false)
async function submit() {
errorMsg.value = ''
if (!username.value.trim()) {
errorMsg.value = '请输入用户名'
return
}
if (!password.value) {
errorMsg.value = '请输入密码'
return
}
if (mode.value === 'register' && password.value !== confirmPassword.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)
ui.showToast('注册成功')
}
emit('close')
if (ui.pendingAction) {
ui.runPendingAction()
} else {
window.location.reload()
}
} catch (e) {
errorMsg.value = e?.message || (mode.value === 'login' ? '登录失败,请检查用户名和密码' : '注册失败,请重试')
} finally {
loading.value = false
}
}
async function submitFeedback() {
if (!feedbackText.value.trim()) return
feedbackLoading.value = true
try {
const res = await api('/api/bug-report', {
method: 'POST',
body: JSON.stringify({ content: feedbackText.value.trim(), priority: 0 }),
})
if (res.ok) {
feedbackText.value = ''
showFeedback.value = false
ui.showToast('反馈已提交,感谢!')
}
} catch {
ui.showToast('提交失败')
} finally {
feedbackLoading.value = false
}
}
</script>
<style scoped>
.login-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 6000;
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;
}
.login-divider {
height: 1px;
background: #eee;
margin: 4px 0;
}
.login-feedback-btn {
background: none;
border: none;
color: #999;
font-size: 13px;
cursor: pointer;
font-family: inherit;
text-align: center;
padding: 4px 0;
}
.login-feedback-btn:hover {
color: #666;
}
.feedback-section {
display: flex;
flex-direction: column;
gap: 10px;
}
</style>