Files
gamegroup2/frontend/src/components/bulletin/BulletinPinned.vue
T
congsh f99c44f716 feat(bulletin): add group bulletin board (信息公示板)
- Add bulletin_posts and bulletin_reads PocketBase collections with migrations
- Add BulletinPost/BulletinRead types and priority levels (low/normal/high/urgent)
- Add bulletin API (CRUD, read tracking, realtime subscription)
- Add bulletin Pinia store with pinned/normal sections and expiration filtering
- Add BulletinPinned header component for pinned announcements
- Add BulletinBoard tab component with sectioned list layout
- Add BulletinPostCard with priority tags, unread dots, and hover actions
- Add CreateBulletinDialog with title, content, priority, pinned, expiresAt
- Integrate bulletin tab into GroupView with Bell icon and real-time updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 12:46:35 +08:00

119 lines
2.6 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import { useBulletinStore } from '@/stores/bulletin'
import { BulletinPriorityMap } from '@/types'
import type { BulletinPost } from '@/types'
const emit = defineEmits<{
viewAll: []
}>()
const bulletinStore = useBulletinStore()
const hasPinned = computed(() => bulletinStore.pinnedPosts.length > 0)
function handleClick(post: BulletinPost) {
bulletinStore.markAsRead(post.id)
emit('viewAll')
}
</script>
<template>
<div v-if="hasPinned" class="bulletin-pinned">
<div
v-for="post in bulletinStore.pinnedPosts"
:key="post.id"
class="pinned-item"
:class="{ 'pinned-item--urgent': post.priority === 'urgent' }"
@click="handleClick(post)"
>
<span class="pinned-item__dot" />
<span class="pinned-item__priority">{{ BulletinPriorityMap[post.priority] }}</span>
<span class="pinned-item__title">{{ post.title }}</span>
<span v-if="!bulletinStore.isRead(post.id)" class="pinned-item__unread" />
</div>
</div>
</template>
<style scoped>
.bulletin-pinned {
display: flex;
flex-direction: column;
gap: 8px;
}
.pinned-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--gg-bg-card);
border: 1px solid var(--gg-border);
border-left: 3px solid var(--gg-primary);
border-radius: var(--gg-radius-md);
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
}
.pinned-item:hover {
border-color: var(--gg-primary-light);
box-shadow: 0 0 16px rgba(5, 150, 105, 0.08);
}
.pinned-item--urgent {
border-left-color: var(--gg-danger);
background: rgba(239, 68, 68, 0.04);
}
.pinned-item--urgent:hover {
box-shadow: 0 0 16px rgba(239, 68, 68, 0.08);
}
.pinned-item__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--gg-primary);
flex-shrink: 0;
}
.pinned-item--urgent .pinned-item__dot {
background: var(--gg-danger);
}
.pinned-item__priority {
font-size: 11px;
font-weight: 600;
padding: 1px 6px;
border-radius: 4px;
background: var(--gg-bg-elevated);
color: var(--gg-text-muted);
flex-shrink: 0;
}
.pinned-item--urgent .pinned-item__priority {
background: rgba(239, 68, 68, 0.12);
color: var(--gg-danger);
}
.pinned-item__title {
font-size: 13px;
font-weight: 500;
color: var(--gg-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.pinned-item__unread {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--gg-danger);
box-shadow: 0 0 6px var(--gg-danger);
flex-shrink: 0;
}
</style>