277 lines
7.4 KiB
Vue
277 lines
7.4 KiB
Vue
|
|
<!-- src/components-mobile/bulletin/BulletinPostSheetMobile.vue -->
|
|||
|
|
<!-- 公告详情/编辑/创建 三合一底部面板 -->
|
|||
|
|
<!-- - post=null & editing=true → 创建 -->
|
|||
|
|
<!-- - post=非null & editing=false → 详情(含编辑/删除) -->
|
|||
|
|
<!-- - post=非null & editing=true → 编辑 -->
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { computed, ref, watch } from 'vue'
|
|||
|
|
import { useBulletinStore } from '@/stores/bulletin'
|
|||
|
|
import { useGroupStore } from '@/stores/group'
|
|||
|
|
import { pb } from '@/api/pocketbase'
|
|||
|
|
import { BulletinPriorityMap } from '@/types'
|
|||
|
|
import type { BulletinPost, BulletinPriority } from '@/types'
|
|||
|
|
import { displayName } from '@/types'
|
|||
|
|
import { showSuccessToast, showFailToast } from 'vant'
|
|||
|
|
|
|||
|
|
const props = defineProps<{
|
|||
|
|
show: boolean
|
|||
|
|
post: BulletinPost | null
|
|||
|
|
editing: boolean
|
|||
|
|
groupId: string
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
'update:show': [value: boolean]
|
|||
|
|
'edit': [post: BulletinPost]
|
|||
|
|
'delete': [post: BulletinPost]
|
|||
|
|
'saved': []
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const bulletinStore = useBulletinStore()
|
|||
|
|
const groupStore = useGroupStore()
|
|||
|
|
|
|||
|
|
const visible = computed({
|
|||
|
|
get: () => props.show,
|
|||
|
|
set: (val) => emit('update:show', val)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 表单
|
|||
|
|
const form = ref({
|
|||
|
|
title: '',
|
|||
|
|
content: '',
|
|||
|
|
priority: 'normal' as BulletinPriority,
|
|||
|
|
pinned: false,
|
|||
|
|
expiresAt: ''
|
|||
|
|
})
|
|||
|
|
const saving = ref(false)
|
|||
|
|
|
|||
|
|
// 是否创建模式
|
|||
|
|
const isCreate = computed(() => props.editing && !props.post)
|
|||
|
|
|
|||
|
|
// 权限:群主/管理员可编辑删除
|
|||
|
|
const currentUserId = computed(() => pb.authStore.model?.id || '')
|
|||
|
|
const canManage = computed(() => {
|
|||
|
|
if (!props.post) return false
|
|||
|
|
if (groupStore.isGroupOwner) return true
|
|||
|
|
if (groupStore.isGroupAdmin) return true
|
|||
|
|
if (props.post.creator === currentUserId.value) return true
|
|||
|
|
return false
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 打开时同步表单
|
|||
|
|
watch(() => props.show, (val) => {
|
|||
|
|
if (!val) return
|
|||
|
|
if (props.editing && props.post) {
|
|||
|
|
// 编辑模式:填充现有数据
|
|||
|
|
form.value = {
|
|||
|
|
title: props.post.title,
|
|||
|
|
content: props.post.content,
|
|||
|
|
priority: props.post.priority,
|
|||
|
|
pinned: props.post.pinned,
|
|||
|
|
expiresAt: props.post.expiresAt || ''
|
|||
|
|
}
|
|||
|
|
} else if (isCreate.value) {
|
|||
|
|
// 创建模式:清空
|
|||
|
|
form.value = { title: '', content: '', priority: 'normal', pinned: false, expiresAt: '' }
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
async function handleSubmit() {
|
|||
|
|
if (!form.value.title.trim()) {
|
|||
|
|
showFailToast('请输入标题')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!form.value.content.trim()) {
|
|||
|
|
showFailToast('请输入内容')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
saving.value = true
|
|||
|
|
try {
|
|||
|
|
const payload = {
|
|||
|
|
title: form.value.title.trim(),
|
|||
|
|
content: form.value.content.trim(),
|
|||
|
|
priority: form.value.priority,
|
|||
|
|
pinned: form.value.pinned,
|
|||
|
|
expiresAt: form.value.expiresAt || undefined
|
|||
|
|
}
|
|||
|
|
if (isCreate.value) {
|
|||
|
|
await bulletinStore.create({ group: props.groupId, ...payload })
|
|||
|
|
showSuccessToast('发布成功')
|
|||
|
|
} else if (props.post) {
|
|||
|
|
await bulletinStore.update(props.post.id, payload)
|
|||
|
|
showSuccessToast('修改成功')
|
|||
|
|
}
|
|||
|
|
emit('saved')
|
|||
|
|
} catch (e: any) {
|
|||
|
|
showFailToast(e.message || '操作失败')
|
|||
|
|
} finally {
|
|||
|
|
saving.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const priorityKeys = Object.keys(BulletinPriorityMap) as BulletinPriority[]
|
|||
|
|
|
|||
|
|
function priorityType(p: string): any {
|
|||
|
|
const map: Record<string, string> = { urgent: 'danger', high: 'warning', normal: 'primary', low: 'default' }
|
|||
|
|
return map[p] || 'default'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatDate(dateStr: string): string {
|
|||
|
|
return new Date(dateStr).toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<van-popup
|
|||
|
|
v-model:show="visible"
|
|||
|
|
position="bottom"
|
|||
|
|
round
|
|||
|
|
closeable
|
|||
|
|
:style="{ height: editing ? '80%' : '70%' }"
|
|||
|
|
>
|
|||
|
|
<div class="post-sheet">
|
|||
|
|
<!-- 编辑/创建模式 -->
|
|||
|
|
<template v-if="editing">
|
|||
|
|
<div class="sheet-title">{{ isCreate ? '发布公告' : '编辑公告' }}</div>
|
|||
|
|
<van-cell-group inset>
|
|||
|
|
<van-field v-model="form.title" label="标题" placeholder="公告标题" required maxlength="100" />
|
|||
|
|
<van-field
|
|||
|
|
v-model="form.content"
|
|||
|
|
type="textarea"
|
|||
|
|
label="内容"
|
|||
|
|
placeholder="公告正文(支持换行)"
|
|||
|
|
rows="5"
|
|||
|
|
autosize
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
<van-field name="priority" label="优先级">
|
|||
|
|
<template #input>
|
|||
|
|
<select v-model="form.priority" class="native-select">
|
|||
|
|
<option v-for="p in priorityKeys" :key="p" :value="p">{{ BulletinPriorityMap[p] }}</option>
|
|||
|
|
</select>
|
|||
|
|
</template>
|
|||
|
|
</van-field>
|
|||
|
|
<van-cell title="置顶" center>
|
|||
|
|
<template #right-icon>
|
|||
|
|
<van-switch v-model="form.pinned" size="22px" />
|
|||
|
|
</template>
|
|||
|
|
</van-cell>
|
|||
|
|
<van-field v-model="form.expiresAt" label="截止时间" placeholder="可选,如 2026-12-31" />
|
|||
|
|
</van-cell-group>
|
|||
|
|
<div class="sheet-actions">
|
|||
|
|
<van-button type="primary" block round :loading="saving" @click="handleSubmit">
|
|||
|
|
{{ isCreate ? '发布' : '保存' }}
|
|||
|
|
</van-button>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<!-- 详情模式 -->
|
|||
|
|
<template v-else-if="post">
|
|||
|
|
<div class="detail-header">
|
|||
|
|
<van-tag :type="priorityType(post.priority)" size="large">{{ BulletinPriorityMap[post.priority] }}</van-tag>
|
|||
|
|
<van-tag v-if="post.pinned" type="warning" size="large">置顶</van-tag>
|
|||
|
|
<span class="detail-time">{{ formatDate(post.created) }}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<h2 class="detail-title">{{ post.title }}</h2>
|
|||
|
|
|
|||
|
|
<div class="detail-meta">
|
|||
|
|
<span>发布人:{{ displayName(post.expand?.creator) }}</span>
|
|||
|
|
<span v-if="post.expiresAt" class="detail-expire">截止:{{ formatDate(post.expiresAt) }}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="detail-content">{{ post.content }}</div>
|
|||
|
|
|
|||
|
|
<div v-if="canManage" class="detail-actions">
|
|||
|
|
<van-button size="small" round icon="edit" @click="emit('edit', post)">编辑</van-button>
|
|||
|
|
<van-button size="small" round plain type="danger" icon="delete-o" @click="emit('delete', post)">删除</van-button>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</div>
|
|||
|
|
</van-popup>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.post-sheet {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
height: 100%;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
padding: 16px 0 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sheet-title {
|
|||
|
|
text-align: center;
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
padding: 8px 0 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.native-select {
|
|||
|
|
border: 1px solid var(--gg-border);
|
|||
|
|
border-radius: var(--gg-radius-sm);
|
|||
|
|
padding: 6px 10px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
background: var(--gg-bg-card);
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sheet-actions {
|
|||
|
|
padding: 20px 16px 0;
|
|||
|
|
margin-top: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 详情 */
|
|||
|
|
.detail-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 0 16px 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-time {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: var(--gg-text-muted);
|
|||
|
|
margin-left: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-title {
|
|||
|
|
font-size: 20px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: var(--gg-text);
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0 16px;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-meta {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 12px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: var(--gg-text-muted);
|
|||
|
|
padding: 8px 16px 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-expire {
|
|||
|
|
color: var(--gg-warning);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-content {
|
|||
|
|
font-size: 15px;
|
|||
|
|
color: var(--gg-text);
|
|||
|
|
line-height: 1.7;
|
|||
|
|
padding: 0 16px;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
word-break: break-word;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
justify-content: center;
|
|||
|
|
padding: 24px 16px 0;
|
|||
|
|
margin-top: auto;
|
|||
|
|
}
|
|||
|
|
</style>
|