f99c44f716
- 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>
119 lines
2.6 KiB
Vue
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>
|