125 lines
4.4 KiB
Vue
125 lines
4.4 KiB
Vue
<template>
|
|
<view class="postcard">
|
|
<view class="postcard-header">
|
|
<text class="postcard-title">{{ notification.title }}</text>
|
|
<view class="postcard-meta">
|
|
<text v-if="notification.type" class="meta-type">[{{ typeLabel }}]</text>
|
|
<text class="meta-time">{{ formatTime(notification.created_at) }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="postcard-content">
|
|
<text>{{ notification.content }}</text>
|
|
</view>
|
|
<view v-if="businessMeta.length" class="postcard-extra">
|
|
<view v-for="m in businessMeta" :key="m.k" class="meta-row">
|
|
<text class="meta-key">{{ m.k }}:</text>
|
|
<text class="meta-val">{{ m.v }}</text>
|
|
</view>
|
|
</view>
|
|
<view v-if="!notification.is_read" class="postcard-unread-tip">
|
|
<text>● 未读</text>
|
|
</view>
|
|
<view class="postcard-toolbar">
|
|
<view v-if="!notification.is_read" class="toolbar-btn toolbar-btn-read" @click="$emit('mark-read', notification)">
|
|
<text>✓ 标已读</text>
|
|
</view>
|
|
<view class="toolbar-btn toolbar-btn-delete" @click="$emit('delete', notification)">
|
|
<text>🗑 删除</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
const props = defineProps({ notification: { type: Object, required: true } })
|
|
defineEmits(['mark-read', 'delete'])
|
|
|
|
const typeMap = {
|
|
feedback_replied: '反馈回复',
|
|
report_resolved: '举报结果',
|
|
target_reported: '被举报',
|
|
activity: '活动通知',
|
|
}
|
|
const typeLabel = computed(() => typeMap[props.notification.type] || props.notification.type || '通知')
|
|
|
|
const businessMeta = computed(() => {
|
|
// data 可能是 string(JSON) 或 object(proto structpb.AsMap() 的产物)。
|
|
// P0-1 fix:网关 convertNotification 走 AsMap() 直接发对象,旧写法 JSON.parse(obj) 会 SyntaxError → {}。
|
|
let data = {}
|
|
const raw = props.notification.data
|
|
if (raw) {
|
|
if (typeof raw === 'string') {
|
|
try { data = JSON.parse(raw) } catch (e) { data = {} }
|
|
} else if (typeof raw === 'object') {
|
|
data = raw
|
|
}
|
|
}
|
|
const list = []
|
|
if (data.feedback_id) list.push({ k: '反馈 ID', v: data.feedback_id })
|
|
if (data.report_id) list.push({ k: '举报 ID', v: data.report_id })
|
|
// P1-1 fix:admin backend moderation_admin.py 用 action(规范名),不是 resolved_action
|
|
if (data.action) list.push({ k: '处理动作', v: actionLabel(data.action) })
|
|
if (data.category_code) list.push({ k: '分类', v: data.category_code })
|
|
if (data.original_title) list.push({ k: '原标题', v: data.original_title })
|
|
if (data.activity_id) list.push({ k: '活动 ID', v: data.activity_id })
|
|
return list
|
|
})
|
|
|
|
function actionLabel(a) {
|
|
return { takedown: '已下架', ban: '已封禁', warn: '已警告', dismiss: '已驳回', restore: '已解除' }[a] || a
|
|
}
|
|
function formatTime(ms) {
|
|
if (!ms) return ''
|
|
const d = new Date(ms)
|
|
const pad = n => (n < 10 ? '0' + n : '' + n)
|
|
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.postcard {
|
|
background: #fff; border-radius: 16rpx; padding: 32rpx;
|
|
.postcard-header { border-bottom: 1rpx solid #f5f5f5; padding-bottom: 16rpx; margin-bottom: 16rpx; }
|
|
.postcard-title { font-size: 32rpx; font-weight: 600; display: block; margin-bottom: 12rpx; }
|
|
.postcard-meta {
|
|
display: flex; gap: 16rpx; font-size: 22rpx; color: #999;
|
|
.meta-type { color: #5C40FF; }
|
|
}
|
|
.postcard-content {
|
|
font-size: 28rpx; line-height: 1.7; color: #333; margin-bottom: 24rpx;
|
|
white-space: pre-wrap;
|
|
}
|
|
.postcard-extra {
|
|
.meta-row {
|
|
display: flex; font-size: 24rpx; color: #666;
|
|
padding: 8rpx 0;
|
|
.meta-key { color: #999; margin-right: 16rpx; min-width: 140rpx; }
|
|
}
|
|
}
|
|
.postcard-unread-tip {
|
|
margin-top: 24rpx; padding-top: 16rpx; border-top: 1rpx solid #f5f5f5;
|
|
color: #f5222d; font-size: 24rpx;
|
|
}
|
|
.postcard-toolbar {
|
|
margin-top: 24rpx;
|
|
padding-top: 16rpx;
|
|
border-top: 1rpx solid #f5f5f5;
|
|
display: flex;
|
|
gap: 16rpx;
|
|
background: #fafafa;
|
|
border-radius: 12rpx;
|
|
padding: 16rpx;
|
|
.toolbar-btn {
|
|
flex: 1;
|
|
text-align: center;
|
|
padding: 12rpx 0;
|
|
border-radius: 8rpx;
|
|
font-size: 26rpx;
|
|
&.toolbar-btn-read { background: #e8f4ff; color: #5C40FF; }
|
|
&.toolbar-btn-delete { background: #fff1f0; color: #f5222d; }
|
|
}
|
|
}
|
|
}
|
|
</style>
|