Files
oil-formula-calculator/frontend/src/components/LoginModal.vue
Hera Zhao 7f66dae32e
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 17s
Test / e2e-test (push) Successful in 59s
feat: 大量管理配方和搜索改进
- 存为我的:修复调用错误API,改用 diaryStore.createDiary
- 存为我的:同名检测(我的配方 + 公共配方库)
- 我的配方:使用 RecipeCard 统一卡片格式
- 管理配方:按钮缩小、编辑时隐藏智能粘贴、精油搜索框支持拼音跳转
- 管理配方:批量操作改为按钮组(打标签/删除/导出卡片/分享到公共库)
- 管理配方:我的配方加勾选框、全选按钮、编辑功能
- 搜索:模糊匹配 + 同义词扩展(37组),精确/相似分层显示
- 搜索:无匹配时通知编辑添加,搜索时隐藏无匹配的收藏/我的配方区
- 搜索:配方按首字母排序
- 共享审核:通知高级编辑+管理员,我的配方显示共享状态
- 通知:搜索未收录→已添加按钮,审核类→去审核按钮跳转
- 贡献统计:非管理员显示已贡献公共配方数
- 登录弹窗:加反馈问题按钮(无需登录)
- 精油编辑:右上角加保存按钮,支持回车保存
- 后端:新增 /api/me/contribution 接口

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

273 lines
6.1 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" @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="confirmPassword"
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 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 displayName = 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,
displayName.value.trim() || username.value.trim()
)
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>