Fix UserMenu: add notifications panel + bug report form
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 13s
Test / e2e-test (push) Failing after 23s
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 13s
Test / e2e-test (push) Failing after 23s
- Notifications show inline in dropdown (not a separate route) - Load from /api/notifications, show unread count badge - Mark all read button - Bug report form with /api/bug-report POST - Both panels toggle in the dropdown Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,23 +10,53 @@
|
||||
<button class="usermenu-btn" @click="goMyDiary">
|
||||
📖 我的
|
||||
</button>
|
||||
<button class="usermenu-btn" @click="goNotifications">
|
||||
<button class="usermenu-btn" @click="toggleNotifications">
|
||||
🔔 通知
|
||||
<span v-if="unreadCount > 0" class="unread-badge">{{ unreadCount }}</span>
|
||||
</button>
|
||||
<button class="usermenu-btn" @click="showBugReport">
|
||||
🐛 反馈问题
|
||||
</button>
|
||||
<button class="usermenu-btn usermenu-btn-logout" @click="handleLogout">
|
||||
🚪 退出登录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Inline Notification Panel -->
|
||||
<div v-if="showNotifPanel" class="notif-panel">
|
||||
<div class="notif-header">
|
||||
<span>通知 ({{ notifications.length }})</span>
|
||||
<button v-if="unreadCount > 0" class="notif-mark-all" @click="markAllRead">全部已读</button>
|
||||
</div>
|
||||
<div class="notif-list">
|
||||
<div v-for="n in notifications.slice(0, 20)" :key="n.id"
|
||||
class="notif-item" :class="{ unread: !n.is_read }">
|
||||
<div class="notif-title">{{ n.title }}</div>
|
||||
<div v-if="n.body" class="notif-body">{{ n.body }}</div>
|
||||
<div class="notif-time">{{ formatTime(n.created_at) }}</div>
|
||||
</div>
|
||||
<div v-if="notifications.length === 0" class="notif-empty">暂无通知</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bug Report Modal -->
|
||||
<div v-if="showBugForm" class="bug-form">
|
||||
<textarea v-model="bugContent" class="bug-textarea" rows="3" placeholder="描述你遇到的问题..."></textarea>
|
||||
<div class="bug-form-actions">
|
||||
<button class="btn-sm btn-outline" @click="showBugForm = false">取消</button>
|
||||
<button class="btn-sm btn-primary" @click="submitBug" :disabled="!bugContent.trim()">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useUiStore } from '../stores/ui'
|
||||
import { api } from '../composables/useApi'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
@@ -34,7 +64,12 @@ const auth = useAuthStore()
|
||||
const ui = useUiStore()
|
||||
const router = useRouter()
|
||||
|
||||
const unreadCount = ref(0)
|
||||
const notifications = ref([])
|
||||
const showNotifPanel = ref(false)
|
||||
const showBugForm = ref(false)
|
||||
const bugContent = ref('')
|
||||
|
||||
const unreadCount = computed(() => notifications.value.filter(n => !n.is_read).length)
|
||||
|
||||
const roleLabel = computed(() => {
|
||||
const map = {
|
||||
@@ -46,14 +81,55 @@ const roleLabel = computed(() => {
|
||||
return map[auth.user.role] || auth.user.role
|
||||
})
|
||||
|
||||
function formatTime(d) {
|
||||
if (!d) return ''
|
||||
return new Date(d + 'Z').toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function goMyDiary() {
|
||||
emit('close')
|
||||
router.push('/mydiary')
|
||||
}
|
||||
|
||||
function goNotifications() {
|
||||
emit('close')
|
||||
router.push('/notifications')
|
||||
function toggleNotifications() {
|
||||
showNotifPanel.value = !showNotifPanel.value
|
||||
showBugForm.value = false
|
||||
}
|
||||
|
||||
function showBugReport() {
|
||||
showBugForm.value = !showBugForm.value
|
||||
showNotifPanel.value = false
|
||||
}
|
||||
|
||||
async function submitBug() {
|
||||
if (!bugContent.value.trim()) return
|
||||
try {
|
||||
const res = await api('/api/bug-report', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content: bugContent.value.trim(), priority: 0 }),
|
||||
})
|
||||
if (res.ok) {
|
||||
bugContent.value = ''
|
||||
showBugForm.value = false
|
||||
ui.showToast('反馈已提交')
|
||||
}
|
||||
} catch {
|
||||
ui.showToast('提交失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function markAllRead() {
|
||||
try {
|
||||
await api('/api/notifications/read-all', { method: 'POST', body: '{}' })
|
||||
notifications.value.forEach(n => n.is_read = 1)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function loadNotifications() {
|
||||
try {
|
||||
const res = await api('/api/notifications')
|
||||
if (res.ok) notifications.value = await res.json()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
@@ -62,6 +138,8 @@ function handleLogout() {
|
||||
emit('close')
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
onMounted(loadNotifications)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -79,78 +157,71 @@ function handleLogout() {
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
|
||||
padding: 18px 20px 14px;
|
||||
min-width: 180px;
|
||||
min-width: 200px;
|
||||
max-width: 340px;
|
||||
z-index: 4001;
|
||||
}
|
||||
|
||||
.usermenu-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #3e3a44;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.usermenu-role {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.usermenu-name { font-size: 16px; font-weight: 600; color: #3e3a44; margin-bottom: 4px; }
|
||||
.usermenu-role { margin-bottom: 14px; }
|
||||
.role-badge {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
|
||||
color: #4a9d7e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.usermenu-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
display: inline-block; font-size: 11px; padding: 2px 10px;
|
||||
border-radius: 8px; background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
|
||||
color: #4a9d7e; font-weight: 500;
|
||||
}
|
||||
|
||||
.usermenu-actions { display: flex; flex-direction: column; gap: 4px; }
|
||||
.usermenu-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #3e3a44;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
transition: background 0.15s;
|
||||
position: relative;
|
||||
display: flex; align-items: center; gap: 6px; width: 100%;
|
||||
padding: 9px 10px; border: none; background: none; border-radius: 8px;
|
||||
font-size: 14px; color: #3e3a44; cursor: pointer; font-family: inherit;
|
||||
text-align: left; transition: background 0.15s; position: relative;
|
||||
}
|
||||
|
||||
.usermenu-btn:hover {
|
||||
background: #f5f3f0;
|
||||
}
|
||||
|
||||
.usermenu-btn:hover { background: #f5f3f0; }
|
||||
.usermenu-btn-logout {
|
||||
color: #d9534f;
|
||||
margin-top: 6px;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 12px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
color: #d9534f; margin-top: 6px; border-top: 1px solid #eee;
|
||||
padding-top: 12px; border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
background: #d9534f;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 9px;
|
||||
padding: 0 5px;
|
||||
margin-left: auto;
|
||||
background: #d9534f; color: #fff; font-size: 11px; font-weight: 600;
|
||||
min-width: 18px; height: 18px; line-height: 18px; text-align: center;
|
||||
border-radius: 9px; padding: 0 5px; margin-left: auto;
|
||||
}
|
||||
|
||||
/* Notification panel */
|
||||
.notif-panel {
|
||||
margin-top: 12px; border-top: 1px solid #eee; padding-top: 10px;
|
||||
}
|
||||
.notif-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
font-size: 13px; font-weight: 600; color: #666; margin-bottom: 8px;
|
||||
}
|
||||
.notif-mark-all {
|
||||
background: none; border: none; color: var(--sage, #7a9e7e);
|
||||
cursor: pointer; font-size: 12px; font-family: inherit;
|
||||
}
|
||||
.notif-list { max-height: 250px; overflow-y: auto; }
|
||||
.notif-item {
|
||||
padding: 8px 0; border-bottom: 1px solid #f5f5f5; font-size: 13px;
|
||||
}
|
||||
.notif-item.unread { background: #fafafa; }
|
||||
.notif-title { font-weight: 500; color: #333; }
|
||||
.notif-body { color: #888; font-size: 12px; margin-top: 2px; white-space: pre-line; }
|
||||
.notif-time { color: #bbb; font-size: 11px; margin-top: 2px; }
|
||||
.notif-empty { text-align: center; color: #ccc; padding: 16px; font-size: 13px; }
|
||||
|
||||
/* Bug report form */
|
||||
.bug-form { margin-top: 12px; border-top: 1px solid #eee; padding-top: 10px; }
|
||||
.bug-textarea {
|
||||
width: 100%; padding: 8px 10px; border: 1.5px solid #d4cfc7;
|
||||
border-radius: 8px; font-size: 13px; font-family: inherit;
|
||||
outline: none; resize: vertical; box-sizing: border-box;
|
||||
}
|
||||
.bug-textarea:focus { border-color: #7a9e7e; }
|
||||
.bug-form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; }
|
||||
.btn-sm { padding: 6px 14px; border-radius: 8px; font-size: 13px; cursor: pointer; font-family: inherit; border: none; }
|
||||
.btn-primary { background: #7a9e7e; color: white; }
|
||||
.btn-outline { background: white; color: #666; border: 1px solid #d4cfc7; }
|
||||
.btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user