|
|
@@ -24,7 +24,34 @@
|
|
|
</view>
|
|
|
<!-- 中间:气泡 + 时间 -->
|
|
|
<view class="center" :class="{ isMe: msg.isMe }">
|
|
|
- <view class="bubble">
|
|
|
+ <!-- 用户通知:与系统通知一致的卡片(绿标题条 + 白底正文 + 蓝边框按钮) -->
|
|
|
+ <view
|
|
|
+ v-if="msg.contentType === 'USER_NOTIFICATION'"
|
|
|
+ class="bubble notify-card"
|
|
|
+ >
|
|
|
+ <view class="notify-header">
|
|
|
+ <text class="notify-header-text">{{ msg.title || '通知' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="notify-body">
|
|
|
+ <text v-if="msg.content" class="notify-content">{{ msg.content }}</text>
|
|
|
+ </view>
|
|
|
+ <!-- 接收方:可打开 callback;发送方:再次提醒(不展示 action 文案) -->
|
|
|
+ <view v-if="!msg.isMe && (msg.actionText || msg.actionUrl)" class="notify-footer">
|
|
|
+ <view class="notify-btn" @click="onOpenNotificationUrl">
|
|
|
+ <text class="notify-btn-text">{{ msg.actionText || '打开应用' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view v-if="msg.isMe && !isTempMessage" class="notify-footer">
|
|
|
+ <view
|
|
|
+ class="notify-btn"
|
|
|
+ :class="{ 'is-disabled': remindLoading }"
|
|
|
+ @click="onRemindClick"
|
|
|
+ >
|
|
|
+ <text class="notify-btn-text">{{ remindLoading ? '发送中...' : '再次提醒' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view v-else class="bubble">
|
|
|
<text v-if="msg.contentType === 'TEXT'" class="bubble-text">{{ msg.content }}</text>
|
|
|
<image
|
|
|
v-else-if="msg.contentType === 'IMAGE'"
|
|
|
@@ -80,10 +107,20 @@ const props = defineProps({
|
|
|
showDateLabel: {
|
|
|
type: Boolean,
|
|
|
default: false
|
|
|
+ },
|
|
|
+ /** 当前消息是否正在「再次提醒」发送中(用于按钮 loading / 防连点) */
|
|
|
+ remindLoading: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-const emit = defineEmits(['preview-image', 'open-notification-url', 'retry'])
|
|
|
+const emit = defineEmits(['preview-image', 'open-notification-url', 'retry', 'remind-user-notification'])
|
|
|
+
|
|
|
+const isTempMessage = computed(() => {
|
|
|
+ const m = props.msg
|
|
|
+ return !!(m.tempId || (m.id != null && String(m.id).startsWith('temp-')))
|
|
|
+})
|
|
|
|
|
|
const dateLabel = computed(() => {
|
|
|
const t = props.msg.createdAt
|
|
|
@@ -111,6 +148,11 @@ function onOpenNotificationUrl() {
|
|
|
emit('open-notification-url', props.msg)
|
|
|
}
|
|
|
|
|
|
+function onRemindClick() {
|
|
|
+ if (props.remindLoading) return
|
|
|
+ emit('remind-user-notification', props.msg)
|
|
|
+}
|
|
|
+
|
|
|
function onRetry() {
|
|
|
if (props.msg.status === 'failed') emit('retry', props.msg)
|
|
|
}
|
|
|
@@ -187,9 +229,21 @@ function onRetry() {
|
|
|
background: #e5e7eb;
|
|
|
box-shadow: 0 4rpx 10rpx rgba(15, 23, 42, 0.06);
|
|
|
}
|
|
|
+.bubble.notify-card {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 560rpx;
|
|
|
+ min-width: 0;
|
|
|
+ padding: 0;
|
|
|
+ border-radius: 18rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
.message-row.isMe .bubble {
|
|
|
background: #259653;
|
|
|
}
|
|
|
+.message-row.isMe .bubble.notify-card {
|
|
|
+ background: #ffffff;
|
|
|
+}
|
|
|
.bubble-text {
|
|
|
font-size: 28rpx;
|
|
|
color: #111827;
|
|
|
@@ -219,6 +273,47 @@ function onRetry() {
|
|
|
font-size: 26rpx;
|
|
|
color: #259653;
|
|
|
}
|
|
|
+.notify-header {
|
|
|
+ background-color: #bbf7d0;
|
|
|
+ padding: 16rpx 20rpx;
|
|
|
+}
|
|
|
+.notify-header-text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #166534;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.notify-body {
|
|
|
+ padding: 20rpx 20rpx 8rpx;
|
|
|
+}
|
|
|
+.notify-content {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #111827;
|
|
|
+ line-height: 1.5;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+.notify-footer {
|
|
|
+ padding: 12rpx 20rpx 20rpx;
|
|
|
+}
|
|
|
+.notify-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 72rpx;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ border-width: 2rpx;
|
|
|
+ border-style: solid;
|
|
|
+ border-color: #1677ff;
|
|
|
+ background-color: #ffffff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.notify-btn-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #1677ff;
|
|
|
+}
|
|
|
+.notify-btn.is-disabled {
|
|
|
+ opacity: 0.65;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
.side-right {
|
|
|
flex-shrink: 0;
|
|
|
width: 72rpx;
|