This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<view class="card expiry-banner" :class="`expiry-${reminder.level}`" role="status">
|
||||
<view class="banner-heading">
|
||||
<view class="banner-title">{{ reminder.level_name || '套餐临期提醒' }}</view>
|
||||
<button class="banner-close" aria-label="关闭临期提醒" @tap="$emit('close')">关闭</button>
|
||||
</view>
|
||||
<view class="banner-body">当前资产套餐剩余 {{ reminder.remaining_days }} 天,请及时续费。</view>
|
||||
<view class="banner-expiry">预计到期:{{ formatShanghaiExpiry(reminder.expires_at) }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatShanghaiExpiry } from '@/utils/expiry-reminder.js';
|
||||
defineProps({ reminder: { type: Object, required: true } });
|
||||
defineEmits(['close']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.expiry-banner {
|
||||
border-left: 8rpx solid var(--reminder-color);
|
||||
.banner-heading { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||
.banner-title { color: var(--reminder-color); font-size: 30rpx; font-weight: 600; }
|
||||
.banner-close { margin: 0; padding: 8rpx 16rpx; font-size: 24rpx; line-height: 1.5; color: var(--text-secondary); background: var(--gray-100); }
|
||||
.banner-close::after { border: none; }
|
||||
.banner-body { margin-top: 12rpx; font-size: 28rpx; line-height: 1.6; color: var(--text-primary); }
|
||||
.banner-expiry { margin-top: 8rpx; font-size: 24rpx; color: var(--text-secondary); }
|
||||
}
|
||||
.expiry-red { --reminder-color: var(--danger); }
|
||||
.expiry-purple { --reminder-color: var(--purple); }
|
||||
.expiry-pink { --reminder-color: var(--pink); }
|
||||
</style>
|
||||
142
components/ExpiryReminderPopup.vue
Normal file
142
components/ExpiryReminderPopup.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<view class="expiry-overlay" @tap.stop>
|
||||
<view class="expiry-dialog" :class="`expiry-${reminder.level}`" role="dialog"
|
||||
aria-modal="true" aria-label="套餐临期提醒" @keydown.esc="$emit('close')">
|
||||
<view class="reminder-header">
|
||||
<view class="reminder-level">{{ reminder.level_name || '套餐临期提醒' }}</view>
|
||||
<button class="reminder-close" aria-label="关闭临期提醒" @tap="$emit('close')">×</button>
|
||||
<view class="clock-halo" aria-hidden="true"><view class="clock-face"></view></view>
|
||||
<view class="reminder-title">套餐即将到期</view>
|
||||
<view class="remaining-label">当前资产套餐剩余</view>
|
||||
<view class="remaining-days"><text class="day-number">{{ reminder.remaining_days }}</text><text class="day-unit">天</text></view>
|
||||
</view>
|
||||
<view class="reminder-content">
|
||||
<view class="expiry-details">
|
||||
<text class="expiry-label">预计到期时间</text>
|
||||
<text class="expiry-value">{{ formatShanghaiExpiry(reminder.expires_at) }}</text>
|
||||
</view>
|
||||
<view class="reminder-description">为避免套餐到期影响使用,请及时续费。</view>
|
||||
<button class="reminder-confirm" @tap="$emit('close')">我知道了</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatShanghaiExpiry } from '@/utils/expiry-reminder.js';
|
||||
defineProps({ reminder: { type: Object, required: true } });
|
||||
defineEmits(['close']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.expiry-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
box-sizing: border-box;
|
||||
background: rgba(20, 26, 36, 0.48);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.expiry-dialog {
|
||||
width: 100%;
|
||||
max-width: 640rpx;
|
||||
max-height: calc(100vh - 80rpx);
|
||||
overflow-y: auto;
|
||||
border-radius: 32rpx;
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
.reminder-header {
|
||||
position: relative;
|
||||
padding: 40rpx 36rpx 28rpx;
|
||||
text-align: center;
|
||||
background: linear-gradient(180deg, var(--reminder-tint), var(--bg-primary));
|
||||
}
|
||||
.reminder-level {
|
||||
display: inline-block;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: var(--reminder-color);
|
||||
background: var(--reminder-tint);
|
||||
}
|
||||
.reminder-close {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
font-size: 40rpx;
|
||||
line-height: 52rpx;
|
||||
color: var(--text-tertiary);
|
||||
background: transparent;
|
||||
}
|
||||
.reminder-close::after, .reminder-confirm::after { border: none; }
|
||||
.clock-halo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 108rpx;
|
||||
height: 108rpx;
|
||||
margin: 28rpx auto 22rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--reminder-tint);
|
||||
}
|
||||
.clock-face {
|
||||
position: relative;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
border: 4rpx solid var(--reminder-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.clock-face::before, .clock-face::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 23rpx;
|
||||
top: 10rpx;
|
||||
width: 4rpx;
|
||||
height: 17rpx;
|
||||
border-radius: 4rpx;
|
||||
background: var(--reminder-color);
|
||||
}
|
||||
.clock-face::after { top: 23rpx; height: 14rpx; transform: rotate(-55deg); transform-origin: top center; }
|
||||
.reminder-title { color: var(--text-primary); font-size: 38rpx; font-weight: 700; }
|
||||
.remaining-label { margin-top: 20rpx; color: var(--text-secondary); font-size: 26rpx; }
|
||||
.remaining-days { margin-top: 8rpx; color: var(--reminder-color); line-height: 1.2; }
|
||||
.day-number { font-size: 88rpx; font-weight: 700; letter-spacing: -3rpx; font-variant-numeric: tabular-nums; }
|
||||
.day-unit { margin-left: 12rpx; font-size: 28rpx; }
|
||||
.reminder-content { padding: 0 36rpx 36rpx; }
|
||||
.expiry-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
padding: 22rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
.expiry-label { font-size: 22rpx; color: var(--text-tertiary); }
|
||||
.expiry-value { font-size: 26rpx; color: var(--text-primary); font-weight: 500; }
|
||||
.reminder-description { margin: 22rpx 0 28rpx; color: var(--text-secondary); font-size: 24rpx; line-height: 1.6; text-align: center; }
|
||||
.reminder-confirm {
|
||||
margin: 0;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
background: var(--reminder-color);
|
||||
}
|
||||
.expiry-red { --reminder-color: #d94040; --reminder-tint: rgba(217, 64, 64, 0.08); }
|
||||
.expiry-purple { --reminder-color: #8850c4; --reminder-tint: rgba(136, 80, 196, 0.08); }
|
||||
.expiry-pink { --reminder-color: #ce4878; --reminder-tint: rgba(206, 72, 120, 0.08); }
|
||||
</style>
|
||||
@@ -3,8 +3,6 @@
|
||||
<UserInfoCard :currentCardNo="currentCardNo" :deviceInfo="deviceInfo" :onlineStatus="onlineStatus"
|
||||
:isDevice="userInfo.isDevice" :networkStatus="deviceInfo.network_status"
|
||||
:isExpiring="deviceInfo.isExpiring" :expiryDays="deviceInfo.expiryDays" @renew="startHomepageRenewal" />
|
||||
<ExpiryReminderBanner v-if="visibleExpiryReminder" :reminder="visibleExpiryReminder"
|
||||
@close="closeExpiryReminder" />
|
||||
|
||||
<DeviceStatusCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" :isRealName="isRealName"
|
||||
:isDevice="userInfo.isDevice" @authentication="enterDetail('authentication')" />
|
||||
@@ -99,6 +97,8 @@
|
||||
</FunctionCard>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
<ExpiryReminderPopup v-if="visibleExpiryReminder" :reminder="visibleExpiryReminder"
|
||||
@close="closeExpiryReminder" />
|
||||
|
||||
<NotificationPopup :show="notificationPopupShow" :items="notificationItems"
|
||||
:current="notificationPopupCurrent" @change="onNotificationChange" @close="closeNotificationPopup"
|
||||
@@ -140,7 +140,7 @@
|
||||
import FunctionCard from '@/components/FunctionCard.vue';
|
||||
import FloatingButton from '@/components/FloatingButton.vue';
|
||||
import NotificationPopup from '@/components/NotificationPopup.vue';
|
||||
import ExpiryReminderBanner from '@/components/ExpiryReminderBanner.vue';
|
||||
import ExpiryReminderPopup from '@/components/ExpiryReminderPopup.vue';
|
||||
import RenewalPaymentPopup from '@/components/RenewalPaymentPopup.vue';
|
||||
import PopupCandidate from '@/components/PopupCandidate.vue';
|
||||
import {
|
||||
@@ -161,6 +161,10 @@
|
||||
const assetStore = useAssetStore();
|
||||
const homeActive = ref(false);
|
||||
const shownExpiryReminder = ref(null);
|
||||
const awaitingHomeReminders = ref(false);
|
||||
let notificationFallbackPending = false;
|
||||
let notificationQueued = false;
|
||||
let homeEntryId = 0;
|
||||
const visibleExpiryReminder = computed(() => {
|
||||
const entry = shownExpiryReminder.value;
|
||||
return homeActive.value && entry && isCurrentSession(entry) && entry.day === getShanghaiDay()
|
||||
@@ -168,15 +172,31 @@
|
||||
});
|
||||
const showPendingExpiryReminder = () => {
|
||||
const entry = assetStore.state.expiryReminder;
|
||||
if (!homeActive.value || !entry || entry.shown || !isCurrentSession(entry) || entry.day !== getShanghaiDay()) return;
|
||||
if (!homeActive.value || awaitingHomeReminders.value || popupCandidateShow.value || notificationPopupShow.value ||
|
||||
visibleExpiryReminder.value || !entry || entry.shown || !isCurrentSession(entry) || entry.day !== getShanghaiDay()) return;
|
||||
shownExpiryReminder.value = { ...entry };
|
||||
assetStore.markExpiryReminderShown();
|
||||
};
|
||||
const closeExpiryReminder = () => {
|
||||
shownExpiryReminder.value = null;
|
||||
assetStore.dismissExpiryReminder();
|
||||
advanceHomePopupQueue();
|
||||
};
|
||||
watch(() => assetStore.state.expiryReminder, showPendingExpiryReminder);
|
||||
const advanceHomePopupQueue = () => {
|
||||
if (!homeActive.value || awaitingHomeReminders.value || popupCandidateShow.value ||
|
||||
visibleExpiryReminder.value || notificationPopupShow.value) return;
|
||||
showPendingExpiryReminder();
|
||||
if (visibleExpiryReminder.value) return;
|
||||
if (notificationQueued) {
|
||||
notificationQueued = false;
|
||||
notificationPopupShow.value = true;
|
||||
markNotificationRead(notificationItems.value[0]);
|
||||
} else if (notificationFallbackPending) {
|
||||
notificationFallbackPending = false;
|
||||
loadUnreadNotifications();
|
||||
}
|
||||
};
|
||||
watch(() => assetStore.state.expiryReminder, advanceHomePopupQueue);
|
||||
|
||||
let showModifyWifi = ref(false);
|
||||
let restartShow = ref(false);
|
||||
@@ -899,18 +919,18 @@
|
||||
|
||||
const loadUnreadNotifications = async () => {
|
||||
const context = getSessionContext();
|
||||
const entryId = homeEntryId;
|
||||
try {
|
||||
const data = await notificationApi.getList(1, 50, false);
|
||||
if (!homeActive.value || !isCurrentSession(context)) return;
|
||||
if (!homeActive.value || entryId !== homeEntryId || !isCurrentSession(context)) return;
|
||||
const items = (data?.items || [])
|
||||
.filter(item => !item.is_read && item.category !== 'expiry');
|
||||
if (!items.length) return;
|
||||
|
||||
notificationItems.value = items;
|
||||
notificationPopupCurrent.value = 0;
|
||||
notificationPopupShow.value = true;
|
||||
// 弹窗打开即视为用户已看到第一条通知。
|
||||
markNotificationRead(items[0]);
|
||||
notificationQueued = true;
|
||||
advanceHomePopupQueue();
|
||||
} catch (error) {
|
||||
console.error('加载首页未读通知失败', error);
|
||||
}
|
||||
@@ -931,6 +951,7 @@
|
||||
|
||||
const closeNotificationPopup = () => {
|
||||
notificationPopupShow.value = false;
|
||||
advanceHomePopupQueue();
|
||||
};
|
||||
|
||||
const runPopupCandidate = () => {
|
||||
@@ -938,10 +959,12 @@
|
||||
};
|
||||
|
||||
const onPopupCandidateClose = (reason) => {
|
||||
if (!homeActive.value) return;
|
||||
popupCandidateShow.value = false;
|
||||
if (reason === 'empty' || reason === 'invisible') {
|
||||
loadUnreadNotifications();
|
||||
notificationFallbackPending = true;
|
||||
}
|
||||
advanceHomePopupQueue();
|
||||
loadNotificationUnreadCount();
|
||||
};
|
||||
|
||||
@@ -949,31 +972,44 @@
|
||||
initCurrentMonth();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
onShow(async () => {
|
||||
const entryId = ++homeEntryId;
|
||||
homeActive.value = true;
|
||||
shownExpiryReminder.value = null;
|
||||
showPendingExpiryReminder();
|
||||
awaitingHomeReminders.value = true;
|
||||
notificationFallbackPending = false;
|
||||
notificationQueued = false;
|
||||
if (!getSessionContext().token) {
|
||||
awaitingHomeReminders.value = false;
|
||||
handleIndexEntry();
|
||||
return;
|
||||
}
|
||||
if (consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.HOME)) {
|
||||
loadAssetInfo();
|
||||
}
|
||||
handleIndexEntry();
|
||||
loadNotificationUnreadCount();
|
||||
runPopupCandidate();
|
||||
await handleIndexEntry();
|
||||
if (!homeActive.value || entryId !== homeEntryId) return;
|
||||
awaitingHomeReminders.value = false;
|
||||
advanceHomePopupQueue();
|
||||
});
|
||||
onHide(() => {
|
||||
homeEntryId++;
|
||||
homeActive.value = false;
|
||||
shownExpiryReminder.value = null;
|
||||
notificationPopupShow.value = false;
|
||||
popupCandidateShow.value = false;
|
||||
notificationQueued = false;
|
||||
notificationFallbackPending = false;
|
||||
});
|
||||
watch(() => [userStore.state.token, userStore.state.identifier], () => {
|
||||
shownExpiryReminder.value = null;
|
||||
notificationItems.value = [];
|
||||
notificationUnreadCount.value = 0;
|
||||
notificationPopupShow.value = false;
|
||||
notificationQueued = false;
|
||||
notificationFallbackPending = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
</view>
|
||||
|
||||
<view v-else class="notification-list">
|
||||
<view v-for="item in notifications" :key="item.id" class="card notification-card"
|
||||
:class="{ unread: !item.is_read }" @tap="markItemRead(item)">
|
||||
<view v-for="item in notifications" :key="item.id" class="card notification-card" @tap="markItemRead(item)">
|
||||
<view class="notification-header flex-row-sb">
|
||||
<view class="notification-title">{{ item.title || '业务通知' }}</view>
|
||||
<view v-if="!item.is_read" class="unread-dot"></view>
|
||||
<button class="read-status" :class="item.is_read ? 'status-read' : 'status-unread'"
|
||||
@tap.stop="markItemRead(item)">{{ item.is_read ? '已读' : '未读' }}</button>
|
||||
</view>
|
||||
<view class="notification-body">{{ item.body || '-' }}</view>
|
||||
<view class="notification-footer flex-row-sb">
|
||||
@@ -171,11 +171,13 @@
|
||||
.toolbar { margin-bottom: var(--space-md); }
|
||||
.summary { color: var(--text-tertiary); font-size: 24rpx; }
|
||||
.mark-all { margin: 0; padding: 0 24rpx; height: 64rpx; line-height: 64rpx; font-size: 24rpx; }
|
||||
.notification-card { margin-bottom: var(--space-md); border-left: 6rpx solid transparent; }
|
||||
.notification-card.unread { border-left-color: var(--primary); }
|
||||
.notification-header { margin-bottom: var(--space-sm); }
|
||||
.notification-card { margin-bottom: var(--space-md); }
|
||||
.notification-header { margin-bottom: var(--space-sm); gap: 20rpx; align-items: flex-start; }
|
||||
.notification-title { color: var(--text-primary); font-size: 30rpx; font-weight: 600; flex: 1; }
|
||||
.unread-dot { width: 14rpx; height: 14rpx; margin-left: 16rpx; border-radius: 50%; background: var(--primary); }
|
||||
.read-status { flex-shrink: 0; margin: 0; padding: 6rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 500; line-height: 1.5; }
|
||||
.read-status::after { border: none; }
|
||||
.status-unread { color: var(--primary-dark); background: var(--success-light); }
|
||||
.status-read { color: var(--text-tertiary); background: var(--gray-100); }
|
||||
.notification-body { color: var(--text-secondary); font-size: 26rpx; line-height: 1.6; white-space: pre-wrap; }
|
||||
.notification-footer { margin-top: var(--space-md); }
|
||||
.notification-meta, .notification-time { color: var(--text-tertiary); font-size: 22rpx; }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Local-only visual QA: no request reaches the real backend.
|
||||
import { preview } from 'vite';
|
||||
|
||||
let notificationRead = false;
|
||||
|
||||
const server = await preview({
|
||||
configFile: false,
|
||||
build: { outDir: 'dist/build/h5' },
|
||||
@@ -19,6 +21,14 @@ const server = await preview({
|
||||
expiry_reminder: { remaining_days: 0, expires_at: '2026-10-13T16:00:00Z', level: 'red', level_name: '即将到期' }
|
||||
};
|
||||
if (req.url.startsWith('/__mock/notifications')) data = { count: 0, items: [], total: 0 };
|
||||
if (process.env.H5_MOCK_NOTIFICATIONS === '1') {
|
||||
if (req.url.startsWith('/__mock/notifications/unread-count')) data = { count: notificationRead ? 0 : 1 };
|
||||
else if (/^\/__mock\/notifications\/42\/read/.test(req.url)) { notificationRead = true; data = { success: true }; }
|
||||
else if (req.url.startsWith('/__mock/notifications/read-all')) { notificationRead = true; data = { updated_count: 1 }; }
|
||||
else if (req.url.startsWith('/__mock/notifications')) data = {
|
||||
items: [{ id: 42, title: '套餐临期提醒', body: '您的套餐即将到期,请及时续费。', category: 'expiry', severity: 'warning', is_read: notificationRead, created_at: '2026-10-13T08:00:00+08:00' }], total: 1
|
||||
};
|
||||
}
|
||||
const conflict = req.url.startsWith('/__mock/auth/bind-phone');
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({ code: conflict ? 1192 : 0, msg: conflict ? '该资产已绑定手机号,如需更换请联系管理员' : 'success', data }));
|
||||
|
||||
@@ -351,13 +351,43 @@ test('home shows shared reminder once and excludes only expiry from old notifica
|
||||
};
|
||||
const asset = (await r.load('store/asset.js')).useAssetStore();
|
||||
asset.receiveExpiryReminder({ token: 'session-A', identifier: 'A' }, { remaining_days: 0, level: 'red' });
|
||||
const page = await r.load('pages/index/index.vue', ['visibleExpiryReminder', 'loadUnreadNotifications', 'notificationItems', 'closeExpiryReminder']);
|
||||
r.shown[0](); await r.tick();
|
||||
const page = await r.load('pages/index/index.vue', ['visibleExpiryReminder', 'loadUnreadNotifications', 'notificationItems', 'notificationPopupShow', 'closeExpiryReminder', 'onPopupCandidateClose']);
|
||||
await r.shown[0]();
|
||||
assert.equal(page.visibleExpiryReminder.value, null);
|
||||
assert.equal(asset.state.expiryReminder.shown, false);
|
||||
page.onPopupCandidateClose('empty'); await r.tick();
|
||||
assert.equal(page.visibleExpiryReminder.value.remaining_days, 0);
|
||||
await page.loadUnreadNotifications();
|
||||
assert.deepEqual(plain(page.notificationItems.value).map(item => item.id), [2]);
|
||||
assert.equal(page.notificationPopupShow.value, false);
|
||||
page.closeExpiryReminder();
|
||||
r.hidden[0](); r.shown[0](); await r.tick();
|
||||
assert.equal(page.notificationPopupShow.value, true);
|
||||
r.hidden[0](); await r.shown[0](); page.onPopupCandidateClose('empty'); await r.tick();
|
||||
assert.equal(page.visibleExpiryReminder.value, null);
|
||||
});
|
||||
|
||||
test('expiry popup waits for an open notification and closing it does not mark expiry read', async () => {
|
||||
const r = runtime();
|
||||
let markReads = 0;
|
||||
r.api.assetApi = { getInfo: async () => ({ identifier: 'A' }) };
|
||||
r.api.notificationApi = {
|
||||
getList: async () => ({ items: [{ id: 2, category: 'approval', is_read: false }] }),
|
||||
getUnreadCount: async () => ({ count: 1 }), markRead: async () => { markReads++; }
|
||||
};
|
||||
const page = await r.load('pages/index/index.vue', ['visibleExpiryReminder', 'notificationPopupShow', 'onPopupCandidateClose', 'closeNotificationPopup', 'closeExpiryReminder']);
|
||||
await r.shown[0](); page.onPopupCandidateClose('empty');
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(page.notificationPopupShow.value, true);
|
||||
const asset = (await r.load('store/asset.js')).useAssetStore();
|
||||
asset.receiveExpiryReminder({ token: 'session-A', identifier: 'A' }, { remaining_days: 7, level: 'purple' });
|
||||
await r.tick();
|
||||
assert.equal(page.visibleExpiryReminder.value, null);
|
||||
assert.equal(asset.state.expiryReminder.shown, false);
|
||||
page.closeNotificationPopup();
|
||||
assert.equal(page.visibleExpiryReminder.value.remaining_days, 7);
|
||||
const previousReads = markReads;
|
||||
page.closeExpiryReminder();
|
||||
assert.equal(markReads, previousReads);
|
||||
assert.equal(page.visibleExpiryReminder.value, null);
|
||||
});
|
||||
|
||||
@@ -378,6 +408,26 @@ test('notification center refreshes server scope after read-all; failures keep u
|
||||
}
|
||||
});
|
||||
|
||||
test('clicking a notification card updates its read badge only after successful mark-read', async () => {
|
||||
for (const fail of [false, true]) {
|
||||
const r = runtime();
|
||||
let marked = false;
|
||||
r.api.notificationApi = {
|
||||
getUnreadCount: async () => ({ count: marked ? 0 : 1 }),
|
||||
markRead: async id => { assert.equal(id, 42); if (fail) throw new Error('offline'); marked = true; }
|
||||
};
|
||||
const page = await r.load('pages/notifications/notifications.vue', ['notifications', 'markItemRead', 'unreadCount']);
|
||||
page.notifications.push({ id: 42, is_read: false, title: '套餐提醒' });
|
||||
page.unreadCount.value = 1;
|
||||
await page.markItemRead(page.notifications[0]);
|
||||
assert.equal(page.notifications[0].is_read, !fail);
|
||||
assert.equal(page.unreadCount.value, fail ? 1 : 0);
|
||||
}
|
||||
const source = readFileSync(resolve(root, 'pages/notifications/notifications.vue'), 'utf8');
|
||||
assert.match(source, /item\.is_read \? '已读' : '未读'/);
|
||||
assert.doesNotMatch(source, /border-left|unread-dot/);
|
||||
});
|
||||
|
||||
test('notification center rejects an old asset response after switching context', async () => {
|
||||
const r = runtime();
|
||||
const oldList = deferred();
|
||||
|
||||
Reference in New Issue
Block a user