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
- 存为我的:修复调用错误API,改用 diaryStore.createDiary - 存为我的:同名检测(我的配方 + 公共配方库) - 我的配方:使用 RecipeCard 统一卡片格式 - 管理配方:按钮缩小、编辑时隐藏智能粘贴、精油搜索框支持拼音跳转 - 管理配方:批量操作改为按钮组(打标签/删除/导出卡片/分享到公共库) - 管理配方:我的配方加勾选框、全选按钮、编辑功能 - 搜索:模糊匹配 + 同义词扩展(37组),精确/相似分层显示 - 搜索:无匹配时通知编辑添加,搜索时隐藏无匹配的收藏/我的配方区 - 搜索:配方按首字母排序 - 共享审核:通知高级编辑+管理员,我的配方显示共享状态 - 通知:搜索未收录→已添加按钮,审核类→去审核按钮跳转 - 贡献统计:非管理员显示已贡献公共配方数 - 登录弹窗:加反馈问题按钮(无需登录) - 精油编辑:右上角加保存按钮,支持回车保存 - 后端:新增 /api/me/contribution 接口 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
273 lines
6.1 KiB
Vue
273 lines
6.1 KiB
Vue
<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>
|