feat(meeting): Task 13 完成 meeting_invite 通知卡片对接

## 后端
- MeetingService.InviteUsers 的 PushPayload.Extra 补齐设计 §10.1 要求字段:
  inviter_id / inviter_name / inviter_avatar / expired_at(Unix 秒,
  与 Redis TTL MeetingInviteTokenTTL=600s 对齐)

## 前端
- constants/notify.js:supportsInlineAction 扩展支持 meeting_invite;
  新增 NOTIFY_INLINE_ACTION_LABEL 映射(meeting_invite → 立即加入/稍后)
  + NOTIFY_INLINE_ACTION_DEFAULT 兜底
- components/notify/NotifyItem.vue:
  * 按 type 动态渲染按钮文案(actionLabel 计算属性)
  * isExpired 计算属性:meeting_invite 比对 extra.expired_at * 1000
  * 过期态合并为单个 disabled 的"邀请已过期"按钮
  * 新增 .notify-btn--expired 灰显样式
- pages/notify/index.vue:
  * handleAccept / handleReject / _navigateByNotify 增加 meeting_invite 分支
  * 新增 _navigateToMeetingInvite 辅助:跳 /pages/meeting/preview?mode=join&code=xxx
  * 过期时 toast "邀请已过期"不跳转,"稍后"仅 markRead 不发接口

## 文档
- docs/plans/2026-04-21-phase2e-2-implementation.plan.md:Task 13 标  + 详细产出
- docs/progress/CURRENT_STATUS.md:新增 Task 13 专节(关键技术点 / 验证记录 / 下一步)
- .cursor/rules/project-context.mdc:2e-2 进度刷 Task 0-13 

## 验证
后端 curl 双角色端到端(testuser1 创会 → 邀请 testuser2 → 拉通知列表):
- type=meeting_invite,extra 8 字段齐全
- expired_at = invited_at + 600,与 Redis TTL 对齐

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-22 17:26:31 +08:00
parent 310ea03ce1
commit d6fb7ce291
7 changed files with 193 additions and 41 deletions

View File

@@ -34,25 +34,35 @@
</view>
<text class="notify-content" v-if="notify.content">{{ notify.content }}</text>
<!-- 内联操作按钮仅限群聊邀请 / 入群申请 -->
<!-- 内联操作按钮群聊邀请 / 入群申请 / 会议邀请 -->
<view
v-if="showActions"
class="notify-actions"
>
<!-- 会议邀请过期时合并为单个 disabled 提示按钮 -->
<button
class="notify-btn notify-btn--primary"
:disabled="processing"
@tap.stop="handleAccept"
v-if="isExpired"
class="notify-btn notify-btn--ghost notify-btn--expired"
disabled
>
接受
</button>
<button
class="notify-btn notify-btn--ghost"
:disabled="processing"
@tap.stop="handleReject"
>
拒绝
邀请已过期
</button>
<template v-else>
<button
class="notify-btn notify-btn--primary"
:disabled="processing"
@tap.stop="handleAccept"
>
{{ actionLabel.accept }}
</button>
<button
class="notify-btn notify-btn--ghost"
:disabled="processing"
@tap.stop="handleReject"
>
{{ actionLabel.reject }}
</button>
</template>
</view>
</view>
</view>
@@ -69,6 +79,9 @@ import {
NOTIFY_DEFAULT_ICON,
NOTIFY_TYPE_LABEL,
NOTIFY_CATEGORY_COLOR,
NOTIFY_TYPE_MEETING_INVITE,
NOTIFY_INLINE_ACTION_LABEL,
NOTIFY_INLINE_ACTION_DEFAULT,
supportsInlineAction
} from '@/constants/notify'
@@ -110,6 +123,34 @@ const showActions = computed(() => {
return supportsInlineAction(props.notify.type)
})
/** 内联按钮文案(按 type 映射;兜底为"接受/拒绝" */
const actionLabel = computed(() => {
return NOTIFY_INLINE_ACTION_LABEL[props.notify.type] || NOTIFY_INLINE_ACTION_DEFAULT
})
/**
* 会议邀请是否已过期
* extra.expired_at 为 Unix 秒,与后端 Redis TTL 保持一致
* 非 meeting_invite 或无 expired_at 时一律返回 false
*/
const isExpired = computed(() => {
if (props.notify.type !== NOTIFY_TYPE_MEETING_INVITE) return false
const extra = _parseExtra(props.notify.extra)
if (!extra || !extra.expired_at) return false
return Number(extra.expired_at) * 1000 < Date.now()
})
/** 容错解析 extra后端入库后以 JSON 字符串下发,也可能已是对象 */
function _parseExtra(extra) {
if (!extra) return null
if (typeof extra === 'object') return extra
try {
return JSON.parse(extra)
} catch (e) {
return null
}
}
const handleTap = () => {
emit('item-tap', props.notify)
}
@@ -284,4 +325,9 @@ const formatTime = (str) => {
background-color: #F1F5F9;
color: #1E293B;
}
/* 会议邀请过期态:合并成一个灰显按钮,占满整行 */
.notify-btn--expired {
color: #94A3B8;
}
</style>