feat: phase 2 - polls, memories, notifications, stats v0.1.0
- Group polls with option/rollcall modes, edit by creator, auto-settle - Multimedia memories with upload, preview, inline video playback - In-app notifications for poll/team/group events - Points system and group stats dashboard - Group detail tabs with icons (activity/polls/memories/stats) - Fix: nginx file upload size, static cache blocking API, timezone, auto-cancel Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { pb } from './pocketbase'
|
||||
import type { Memory } from '@/types'
|
||||
|
||||
export const GROUP_STORAGE_LIMIT = 10 * 1024 * 1024 * 1024
|
||||
|
||||
export function detectFileType(file: File): 'image' | 'video' | 'audio' | 'document' | 'other' {
|
||||
const mime = file.type || ''
|
||||
if (mime.startsWith('image/')) return 'image'
|
||||
if (mime.startsWith('video/')) return 'video'
|
||||
if (mime.startsWith('audio/')) return 'audio'
|
||||
if (
|
||||
mime.startsWith('application/pdf') ||
|
||||
mime.startsWith('application/msword') ||
|
||||
mime.startsWith('application/vnd.') ||
|
||||
mime.startsWith('text/')
|
||||
) return 'document'
|
||||
return 'other'
|
||||
}
|
||||
|
||||
export async function uploadMemory(
|
||||
groupId: string,
|
||||
file: File,
|
||||
meta?: { title?: string; description?: string; tags?: string[] }
|
||||
) {
|
||||
const used = await getGroupStorageUsed(groupId)
|
||||
if (used + file.size > GROUP_STORAGE_LIMIT) {
|
||||
throw new Error('群组存储空间不足,无法上传')
|
||||
}
|
||||
|
||||
const user = pb.authStore.model
|
||||
const formData = new FormData()
|
||||
formData.append('group', groupId)
|
||||
formData.append('uploader', user?.id || '')
|
||||
formData.append('file', file)
|
||||
formData.append('fileType', detectFileType(file))
|
||||
formData.append('size', file.size.toString())
|
||||
formData.append('title', meta?.title || file.name)
|
||||
if (meta?.description) formData.append('description', meta.description)
|
||||
|
||||
return pb.collection('memories').create(formData)
|
||||
}
|
||||
|
||||
export async function listMemories(
|
||||
groupId: string,
|
||||
options?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
fileType?: string
|
||||
}
|
||||
): Promise<{ items: Memory[]; total: number }> {
|
||||
const { page = 1, limit = 30, fileType } = options || {}
|
||||
let filter = `group="${groupId}"`
|
||||
if (fileType) filter += ` && fileType="${fileType}"`
|
||||
|
||||
const result = await pb.collection('memories').getList(page, limit, {
|
||||
filter,
|
||||
sort: '-created',
|
||||
expand: 'uploader'
|
||||
})
|
||||
return { items: result.items as unknown as Memory[], total: result.totalItems }
|
||||
}
|
||||
|
||||
export async function deleteMemory(memoryId: string) {
|
||||
return pb.collection('memories').delete(memoryId)
|
||||
}
|
||||
|
||||
export async function getGroupStorageUsed(groupId: string): Promise<number> {
|
||||
let total = 0
|
||||
let page = 1
|
||||
const batchSize = 500
|
||||
let hasMore = true
|
||||
|
||||
while (hasMore) {
|
||||
const result = await pb.collection('memories').getList(page, batchSize, {
|
||||
filter: `group="${groupId}"`,
|
||||
fields: 'size'
|
||||
})
|
||||
total += result.items.reduce((sum: number, r: any) => sum + (r.size || 0), 0)
|
||||
hasMore = result.items.length === batchSize && page * batchSize < result.totalItems
|
||||
page++
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
export function getGroupStorageLimit(): number {
|
||||
return GROUP_STORAGE_LIMIT
|
||||
}
|
||||
|
||||
export function subscribeMemories(
|
||||
groupId: string,
|
||||
callback: (data: any) => void
|
||||
) {
|
||||
return pb.collection('memories').subscribe('*', (data) => {
|
||||
if (data.record?.group === groupId) {
|
||||
callback(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user