This commit is contained in:
@@ -5,12 +5,14 @@
|
||||
<view class="navbar-back" @tap="goBack">
|
||||
<image class="back-icon" src="../../static/left.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="navbar-title">绑定手机号</view>
|
||||
<view class="navbar-title">{{ bindingTitle }}</view>
|
||||
<view class="navbar-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<view class="container" :style="{ paddingTop: fromLogin ? '0' : '0' }">
|
||||
<view class="card flex-col-g20">
|
||||
<view class="binding-description">{{ bindingDescription }}</view>
|
||||
<view v-if="assetConflict" class="binding-conflict">该资产已绑定手机号,如需更换请联系管理员</view>
|
||||
<view class="flex-row-g20">
|
||||
<label for="">手机号:</label>
|
||||
<up-input placeholder="请输入绑定的手机号" border="surround" v-model="bind.phone" />
|
||||
@@ -20,13 +22,13 @@
|
||||
<label for="">验证码:</label>
|
||||
<up-input placeholder="验证码" v-model="bind.code">
|
||||
<template #suffix>
|
||||
<up-button @tap="getCode" :text="codeText" type="success" :disabled="cooldown > 0"></up-button>
|
||||
<up-button @tap="getCode" :text="codeText" type="success" :disabled="cooldown > 0 || assetConflict || initializing || bindingMode === 'none'"></up-button>
|
||||
</template>
|
||||
</up-input>
|
||||
</view>
|
||||
|
||||
<view class="btn">
|
||||
<up-button type="primary" @tap="bindPhone">绑定手机号</up-button>
|
||||
<up-button type="primary" @tap="bindPhone" :loading="submitting" :disabled="initializing || submitting || assetConflict || bindingMode === 'none'">{{ bindingTitle }}</up-button>
|
||||
</view>
|
||||
|
||||
<!-- 如果从登录页面跳转过来,显示退出按钮 -->
|
||||
@@ -39,14 +41,31 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { authApi, assetApi } from '@/api/index.js';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
import { setPhoneBindStatus } from '@/utils/phone-bind.js';
|
||||
import { getPhoneBindingMode } from '@/utils/phone-bind.js';
|
||||
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
|
||||
|
||||
const POST_BIND_RELOGIN_NOTICE_KEY = 'postBindReloginNotice';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const initializing = ref(true);
|
||||
const submitting = ref(false);
|
||||
const assetConflict = ref(false);
|
||||
const bindingMode = computed(() => getPhoneBindingMode(userStore.state.customerPhone, userStore.state.assetPhoneBound));
|
||||
const bindingTitle = computed(() => {
|
||||
if (bindingMode.value === 'customer') return '绑定客户手机号';
|
||||
if (bindingMode.value === 'asset') return '给资产绑定手机号';
|
||||
return '绑定手机号';
|
||||
});
|
||||
const bindingDescription = computed(() => ({
|
||||
customer: '请绑定您自己的手机号,当前资产的手机号关联不会改变。',
|
||||
asset: '请给当前资产绑定手机号,可使用与您的客户手机号不同的号码。',
|
||||
'customer-asset': '验证一次手机号,将同时绑定您的客户账号和当前资产。',
|
||||
none: '您的客户账号和当前资产均已绑定手机号,无需重复绑定。',
|
||||
unknown: '请验证手机号。客户与资产的绑定将由系统按当前状态处理,已有的资产手机号不会被替换。'
|
||||
}[bindingMode.value]));
|
||||
|
||||
let bind = reactive({
|
||||
phone: '',
|
||||
@@ -65,21 +84,33 @@
|
||||
const options = currentPage.options;
|
||||
fromLogin.value = options.fromLogin === 'true';
|
||||
|
||||
// 契约变化:账号已有主手机号时,主号 + 验证码有效 → 幂等建联。
|
||||
// 预填账号已有主手机号,避免存量用户「提示要验证却提交任何号码都被拒」的死锁。
|
||||
if (userStore.state.customerPhone) bind.phone = userStore.state.customerPhone;
|
||||
// Mandatory binding ends with a new login. Do not consume the day's
|
||||
// one-time expiry reminder here merely to prefill a phone input.
|
||||
if (fromLogin.value) {
|
||||
initializing.value = false;
|
||||
return;
|
||||
}
|
||||
const token = uni.getStorageSync('token');
|
||||
const identifier = userStore.state.identifier || uni.getStorageSync('identifier') || '';
|
||||
if (!token || !identifier) return;
|
||||
if (!token || !identifier) {
|
||||
initializing.value = false;
|
||||
return;
|
||||
}
|
||||
const context = getSessionContext();
|
||||
|
||||
try {
|
||||
const data = await assetApi.getInfo(identifier);
|
||||
const mainPhone = (data && data.bound_phone) || '';
|
||||
if (!isCurrentSession(context)) return;
|
||||
const mainPhone = data?.bound_phone || '';
|
||||
if (mainPhone) {
|
||||
bind.phone = mainPhone;
|
||||
}
|
||||
} catch (e) {
|
||||
// 预填失败不阻塞绑定流程,用户仍可手动输入手机号
|
||||
console.error('预填主手机号失败', e);
|
||||
console.error('预填客户手机号失败');
|
||||
} finally {
|
||||
initializing.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -101,6 +132,7 @@
|
||||
};
|
||||
|
||||
const getCode = async () => {
|
||||
if (cooldown.value > 0 || initializing.value || assetConflict.value || bindingMode.value === 'none') return;
|
||||
if (!bind.phone) {
|
||||
uni.showToast({ title: '请输入手机号', icon: 'none' });
|
||||
return;
|
||||
@@ -111,8 +143,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getSessionContext();
|
||||
try {
|
||||
const data = await authApi.sendCode(bind.phone, 'bind_phone');
|
||||
if (!isCurrentSession(context)) return;
|
||||
cooldown.value = data.cooldown_seconds || 60;
|
||||
codeText.value = `${cooldown.value}s`;
|
||||
|
||||
@@ -133,15 +167,24 @@
|
||||
};
|
||||
|
||||
const bindPhone = async () => {
|
||||
if (initializing.value || submitting.value || assetConflict.value || bindingMode.value === 'none') return;
|
||||
if (!bind.phone || !bind.code) {
|
||||
uni.showToast({ title: '手机号和验证码都不能为空', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(bind.phone)) {
|
||||
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getSessionContext();
|
||||
const mode = bindingMode.value;
|
||||
const submittedPhone = bind.phone;
|
||||
submitting.value = true;
|
||||
try {
|
||||
await authApi.bindPhone(bind.phone, bind.code);
|
||||
const identifier = userStore.state.identifier || uni.getStorageSync('identifier') || '';
|
||||
setPhoneBindStatus(identifier, bind.phone);
|
||||
await authApi.bindPhone(submittedPhone, bind.code);
|
||||
if (!isCurrentSession(context)) return;
|
||||
userStore.completePhoneBinding(submittedPhone, mode);
|
||||
|
||||
if (fromLogin.value) {
|
||||
prepareManualRelogin();
|
||||
@@ -152,10 +195,18 @@
|
||||
uni.showToast({ title: '绑定成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
// 从其他页面过来的,返回上一页
|
||||
uni.navigateBack();
|
||||
if (isCurrentSession(context)) uni.navigateBack();
|
||||
}, 1500);
|
||||
} catch (e) {
|
||||
console.error('绑定手机号失败', e);
|
||||
if (!isCurrentSession(context)) return;
|
||||
if (Number(e?.code) === 1192) {
|
||||
assetConflict.value = true;
|
||||
uni.showModal({ title: '无法更换资产手机号', content: '该资产已绑定手机号,如需更换请联系管理员', showCancel: false });
|
||||
} else {
|
||||
uni.showToast({ title: e?.msg || e?.message || '绑定失败,请稍后重试', icon: 'none' });
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,12 +216,14 @@
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
userStore.clearUser();
|
||||
uni.clearStorageSync();
|
||||
uni.reLaunch({ url: '/pages/login/login' });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
onUnmounted(() => clearInterval(timer));
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateTo({
|
||||
@@ -237,4 +290,6 @@
|
||||
.container {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
.binding-description { color: var(--text-secondary); font-size: 26rpx; line-height: 1.6; }
|
||||
.binding-conflict { color: var(--danger); font-size: 26rpx; line-height: 1.6; }
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<view class="caption mb-md">仅更换您的客户手机号,资产已绑定的手机号不会改变。</view>
|
||||
<view class="form-item">
|
||||
<view class="label">原手机号</view>
|
||||
<up-input placeholder="请输入原手机号" border="surround" v-model="form.oldPhone" disabled/>
|
||||
@@ -30,19 +31,20 @@
|
||||
</view>
|
||||
|
||||
<view class="btn-wrapper">
|
||||
<up-button type="primary" @click="submit">确认更换</up-button>
|
||||
<up-button type="primary" :loading="submitting" :disabled="submitting" @click="submit">确认更换</up-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { authApi } from '@/api/index.js';
|
||||
import { reactive, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { authApi, assetApi } from '@/api/index.js';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
import { setPhoneBindStatus } from '@/utils/phone-bind.js';
|
||||
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const submitting = ref(false);
|
||||
|
||||
const form = reactive({
|
||||
oldPhone: '',
|
||||
@@ -51,14 +53,18 @@
|
||||
newCode: ''
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 获取 URL 参数中的原手机号
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const options = currentPage.options;
|
||||
|
||||
if (options.oldPhone) {
|
||||
form.oldPhone = options.oldPhone;
|
||||
onMounted(async () => {
|
||||
form.oldPhone = userStore.state.customerPhone || '';
|
||||
const context = getSessionContext();
|
||||
const revision = userStore.state.phoneRevision;
|
||||
try {
|
||||
const data = await assetApi.getInfo(context.identifier);
|
||||
if (isCurrentSession(context)) {
|
||||
form.oldPhone = revision === userStore.state.phoneRevision
|
||||
? data.bound_phone || '' : userStore.state.customerPhone || '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载客户手机号失败');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,6 +132,7 @@
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (submitting.value) return;
|
||||
if (!form.oldPhone || !form.oldCode) {
|
||||
uni.showToast({ title: '请填写原手机号信息', icon: 'none' });
|
||||
return;
|
||||
@@ -134,19 +141,34 @@
|
||||
uni.showToast({ title: '请填写新手机号信息', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(form.newPhone)) {
|
||||
uni.showToast({ title: '请输入正确的新手机号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getSessionContext();
|
||||
submitting.value = true;
|
||||
try {
|
||||
await authApi.changePhone(form.oldPhone, form.oldCode, form.newPhone, form.newCode);
|
||||
setPhoneBindStatus(userStore.state.identifier, form.newPhone);
|
||||
form.oldPhone = form.newPhone;
|
||||
const data = await authApi.changePhone(form.oldPhone, form.oldCode, form.newPhone, form.newCode);
|
||||
if (!isCurrentSession(context)) return;
|
||||
if (typeof data?.phone === 'string') {
|
||||
userStore.completeCustomerPhoneChange(data.phone);
|
||||
form.oldPhone = data.phone;
|
||||
}
|
||||
uni.showToast({ title: '更换成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
if (isCurrentSession(context)) uni.navigateBack();
|
||||
}, 1500);
|
||||
} catch (e) {
|
||||
console.error('更换手机号失败', e);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
onUnmounted(() => {
|
||||
clearInterval(oldTimer);
|
||||
clearInterval(newTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
<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')" />
|
||||
@@ -10,7 +12,7 @@
|
||||
<!-- <VoiceCard v-if="!userInfo.isDevice" :voiceStats="voiceStats" :dateRangeText="dateRangeText"
|
||||
@openDatePicker="openDateRangePicker" /> -->
|
||||
|
||||
<TrafficCard :identifier="currentCardNo" />
|
||||
<TrafficCard :identifier="userStore.state.identifier" />
|
||||
|
||||
<!-- <WhitelistCard v-if="!userInfo.isDevice" :whitelistData="whitelistData" @refresh="refreshWhitelist"
|
||||
@add="showAddWhitelistDialog" @showSms="showSmsCodeDialog" /> -->
|
||||
@@ -18,7 +20,8 @@
|
||||
<WifiCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" @modify="modifyWifi"
|
||||
@copy-config="copyWifiConfig" />
|
||||
|
||||
<FunctionCard :realNameStatus="realNameStatus" :alreadyBindPhone="alreadyBindPhone"
|
||||
<FunctionCard :realNameStatus="realNameStatus" :assetPhoneBound="userStore.state.assetPhoneBound"
|
||||
:customerPhone="userStore.state.customerPhone"
|
||||
:isDevice="userInfo.isDevice" :walletBalance="deviceInfo.walletBalance" :unreadCount="notificationUnreadCount"
|
||||
@enter="enterDetail" @sync="onSync">
|
||||
<!-- 修改配置弹窗 -->
|
||||
@@ -124,9 +127,10 @@
|
||||
ref,
|
||||
reactive,
|
||||
onMounted,
|
||||
computed
|
||||
computed,
|
||||
watch
|
||||
} from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { onShow, onHide } from '@dcloudio/uni-app';
|
||||
import UserInfoCard from '@/components/UserInfoCard.vue';
|
||||
import DeviceStatusCard from '@/components/DeviceStatusCard.vue';
|
||||
import VoiceCard from '@/components/VoiceCard.vue';
|
||||
@@ -136,6 +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 RenewalPaymentPopup from '@/components/RenewalPaymentPopup.vue';
|
||||
import PopupCandidate from '@/components/PopupCandidate.vue';
|
||||
import {
|
||||
@@ -145,12 +150,33 @@
|
||||
notificationApi
|
||||
} from '@/api/index.js';
|
||||
import {
|
||||
useUserStore
|
||||
useUserStore,
|
||||
useAssetStore
|
||||
} from '@/store/index.js';
|
||||
import { consumePendingPaymentRefresh, PAYMENT_REFRESH_TARGETS } from '@/utils/payment.js';
|
||||
import { getPhoneBindStatus, setPhoneBindStatus } from '@/utils/phone-bind.js';
|
||||
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
|
||||
import { getShanghaiDay } from '@/utils/expiry-reminder.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const assetStore = useAssetStore();
|
||||
const homeActive = ref(false);
|
||||
const shownExpiryReminder = ref(null);
|
||||
const visibleExpiryReminder = computed(() => {
|
||||
const entry = shownExpiryReminder.value;
|
||||
return homeActive.value && entry && isCurrentSession(entry) && entry.day === getShanghaiDay()
|
||||
? entry.reminder : null;
|
||||
});
|
||||
const showPendingExpiryReminder = () => {
|
||||
const entry = assetStore.state.expiryReminder;
|
||||
if (!homeActive.value || !entry || entry.shown || !isCurrentSession(entry) || entry.day !== getShanghaiDay()) return;
|
||||
shownExpiryReminder.value = { ...entry };
|
||||
assetStore.markExpiryReminderShown();
|
||||
};
|
||||
const closeExpiryReminder = () => {
|
||||
shownExpiryReminder.value = null;
|
||||
assetStore.dismissExpiryReminder();
|
||||
};
|
||||
watch(() => assetStore.state.expiryReminder, showPendingExpiryReminder);
|
||||
|
||||
let showModifyWifi = ref(false);
|
||||
let restartShow = ref(false);
|
||||
@@ -200,8 +226,8 @@
|
||||
});
|
||||
|
||||
let isRealName = ref(false);
|
||||
let alreadyBindPhone = ref(false);
|
||||
let boundPhone = ref('');
|
||||
const boundPhone = computed(() => userStore.state.customerPhone || '');
|
||||
watch(boundPhone, (phone) => { deviceInfo.bound_phone = phone; });
|
||||
let realNameStatus = ref('');
|
||||
let effectiveRealnamePolicy = ref('none');
|
||||
let realnameEntryChecking = ref(false);
|
||||
@@ -259,11 +285,12 @@
|
||||
const loadAssetInfo = async () => {
|
||||
const identifier = userStore.state.identifier;
|
||||
if (!identifier) return;
|
||||
const localBindStatus = getPhoneBindStatus(identifier);
|
||||
const context = getSessionContext();
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await assetApi.getInfo(identifier);
|
||||
if (!isCurrentSession(context)) return null;
|
||||
|
||||
// 基本信息
|
||||
userInfo.isDevice = data.asset_type === 'device';
|
||||
@@ -272,9 +299,7 @@
|
||||
deviceInfo.status = data.status;
|
||||
deviceInfo.imei = data.imei || '-';
|
||||
deviceInfo.asset_type = data.asset_type || 'device';
|
||||
const apiBoundPhone = data.bound_phone || '';
|
||||
const effectiveBoundPhone = localBindStatus?.phone || apiBoundPhone || '';
|
||||
deviceInfo.bound_phone = effectiveBoundPhone;
|
||||
deviceInfo.bound_phone = userStore.state.customerPhone || '';
|
||||
deviceInfo.packageName = data.current_package || '-';
|
||||
deviceInfo.currentPackageId = Number(data.current_package_id || 0);
|
||||
deviceInfo.renewalPrice = data.renewal_price ?? null;
|
||||
@@ -289,11 +314,6 @@
|
||||
realNameStatus.value = isRealName.value ? '已实名' : '未实名';
|
||||
effectiveRealnamePolicy.value = data.effective_realname_policy || 'none';
|
||||
userStore.setRealNameStatus(data.real_name_status);
|
||||
boundPhone.value = effectiveBoundPhone;
|
||||
alreadyBindPhone.value = !!(apiBoundPhone || localBindStatus?.bound);
|
||||
if (apiBoundPhone && (!localBindStatus || apiBoundPhone === localBindStatus.phone)) {
|
||||
setPhoneBindStatus(identifier, apiBoundPhone);
|
||||
}
|
||||
|
||||
// 流量信息已由 TrafficCard 组件独立获取,这里不再处理
|
||||
|
||||
@@ -539,6 +559,7 @@
|
||||
recoverShow.value = false;
|
||||
};
|
||||
const YesLogout = () => {
|
||||
userStore.clearUser();
|
||||
uni.clearStorageSync();
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
@@ -820,7 +841,7 @@
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages/change-phone/change-phone?oldPhone=${boundPhone.value}`
|
||||
url: '/pages/change-phone/change-phone'
|
||||
});
|
||||
break;
|
||||
case 'device-exchange':
|
||||
@@ -849,8 +870,10 @@
|
||||
};
|
||||
|
||||
const loadNotificationUnreadCount = async () => {
|
||||
const context = getSessionContext();
|
||||
try {
|
||||
const data = await notificationApi.getUnreadCount();
|
||||
if (!isCurrentSession(context)) return;
|
||||
notificationUnreadCount.value = Number(data?.count || 0);
|
||||
} catch (error) {
|
||||
console.error('加载通知未读数失败', error);
|
||||
@@ -860,8 +883,10 @@
|
||||
const markNotificationRead = async (item) => {
|
||||
if (!item || item.is_read || notificationReadPending.has(item.id)) return;
|
||||
notificationReadPending.add(item.id);
|
||||
const context = getSessionContext();
|
||||
try {
|
||||
await notificationApi.markRead(item.id);
|
||||
if (!isCurrentSession(context)) return;
|
||||
item.is_read = true;
|
||||
item.read_at = new Date().toISOString();
|
||||
notificationUnreadCount.value = Math.max(notificationUnreadCount.value - 1, 0);
|
||||
@@ -873,10 +898,12 @@
|
||||
};
|
||||
|
||||
const loadUnreadNotifications = async () => {
|
||||
const context = getSessionContext();
|
||||
try {
|
||||
const data = await notificationApi.getList(1, 50, false);
|
||||
if (!homeActive.value || !isCurrentSession(context)) return;
|
||||
const items = (data?.items || [])
|
||||
.filter(item => !item.is_read);
|
||||
.filter(item => !item.is_read && item.category !== 'expiry');
|
||||
if (!items.length) return;
|
||||
|
||||
notificationItems.value = items;
|
||||
@@ -923,6 +950,13 @@
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
homeActive.value = true;
|
||||
shownExpiryReminder.value = null;
|
||||
showPendingExpiryReminder();
|
||||
if (!getSessionContext().token) {
|
||||
handleIndexEntry();
|
||||
return;
|
||||
}
|
||||
if (consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.HOME)) {
|
||||
loadAssetInfo();
|
||||
}
|
||||
@@ -930,6 +964,17 @@
|
||||
loadNotificationUnreadCount();
|
||||
runPopupCandidate();
|
||||
});
|
||||
onHide(() => {
|
||||
homeActive.value = false;
|
||||
shownExpiryReminder.value = null;
|
||||
notificationPopupShow.value = false;
|
||||
});
|
||||
watch(() => [userStore.state.token, userStore.state.identifier], () => {
|
||||
shownExpiryReminder.value = null;
|
||||
notificationItems.value = [];
|
||||
notificationUnreadCount.value = 0;
|
||||
notificationPopupShow.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
try {
|
||||
const loginData = await authApi.wechatLogin(assetToken, code);
|
||||
userStore.setToken(loginData.token);
|
||||
userStore.setLoginPhoneState(loginData);
|
||||
uni.setStorageSync('identifier', userStore.state.identifier);
|
||||
|
||||
// #ifdef H5
|
||||
@@ -350,7 +351,7 @@
|
||||
}
|
||||
// #endif
|
||||
|
||||
if (loginData.need_bind_phone) {
|
||||
if (userStore.state.needBindPhone === true) {
|
||||
uni.redirectTo({ url: '/pages/bind/bind?fromLogin=true' });
|
||||
} else {
|
||||
uni.redirectTo({ url: '/pages/index/index' });
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<view class="container">
|
||||
<view class="toolbar flex-row-sb">
|
||||
<view class="summary">共 {{ total }} 条通知</view>
|
||||
<button class="btn-apple btn-secondary mark-all" :disabled="unreadCount === 0" @tap="markAllRead">
|
||||
<button class="btn-apple btn-secondary mark-all" :disabled="unreadCount === 0 || markingAll" @tap="markAllRead">
|
||||
全部已读
|
||||
</button>
|
||||
</view>
|
||||
@@ -40,9 +40,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { notificationApi } from '@/api/index.js';
|
||||
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
|
||||
|
||||
const pageSize = 20;
|
||||
const notifications = reactive([]);
|
||||
@@ -52,6 +53,10 @@
|
||||
const loading = ref(false);
|
||||
const notificationsLoaded = ref(false);
|
||||
const noMore = ref(false);
|
||||
const markingAll = ref(false);
|
||||
let listRequestId = 0;
|
||||
let countRequestId = 0;
|
||||
const readPending = new Set();
|
||||
|
||||
const formatDate = (value) => {
|
||||
if (!value) return '-';
|
||||
@@ -73,8 +78,11 @@
|
||||
}[severity] || '提示');
|
||||
|
||||
const loadUnreadCount = async () => {
|
||||
const context = getSessionContext();
|
||||
const requestId = ++countRequestId;
|
||||
try {
|
||||
const data = await notificationApi.getUnreadCount();
|
||||
if (!isCurrentSession(context) || requestId !== countRequestId) return;
|
||||
unreadCount.value = Number(data?.count || 0);
|
||||
} catch (error) {
|
||||
console.error('加载通知未读数失败', error);
|
||||
@@ -82,10 +90,14 @@
|
||||
};
|
||||
|
||||
const loadNotifications = async (append = false) => {
|
||||
if (loading.value || (append && noMore.value)) return;
|
||||
if (append && (loading.value || noMore.value)) return;
|
||||
const context = getSessionContext();
|
||||
const requestId = ++listRequestId;
|
||||
const requestedPage = append ? page.value : 1;
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await notificationApi.getList(page.value, pageSize);
|
||||
const data = await notificationApi.getList(requestedPage, pageSize);
|
||||
if (!isCurrentSession(context) || requestId !== listRequestId) return;
|
||||
const items = (data?.items || []);
|
||||
if (append) {
|
||||
notifications.push(...items);
|
||||
@@ -94,39 +106,48 @@
|
||||
}
|
||||
total.value = Number(data?.total || 0);
|
||||
noMore.value = notifications.length >= total.value || items.length < pageSize;
|
||||
if (!noMore.value) page.value += 1;
|
||||
page.value = requestedPage + 1;
|
||||
} catch (error) {
|
||||
console.error('加载通知列表失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
notificationsLoaded.value = true;
|
||||
if (isCurrentSession(context) && requestId === listRequestId) {
|
||||
loading.value = false;
|
||||
notificationsLoaded.value = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const markItemRead = async (item) => {
|
||||
if (item.is_read) return;
|
||||
if (item.is_read || readPending.has(item.id) || markingAll.value) return;
|
||||
const context = getSessionContext();
|
||||
readPending.add(item.id);
|
||||
try {
|
||||
await notificationApi.markRead(item.id);
|
||||
if (!isCurrentSession(context)) return;
|
||||
item.is_read = true;
|
||||
item.read_at = new Date().toISOString();
|
||||
unreadCount.value = Math.max(unreadCount.value - 1, 0);
|
||||
await loadUnreadCount();
|
||||
} catch (error) {
|
||||
console.error('标记通知已读失败', error);
|
||||
} finally {
|
||||
readPending.delete(item.id);
|
||||
}
|
||||
};
|
||||
|
||||
const markAllRead = async () => {
|
||||
if (!unreadCount.value) return;
|
||||
if (!unreadCount.value || markingAll.value) return;
|
||||
const context = getSessionContext();
|
||||
markingAll.value = true;
|
||||
try {
|
||||
await notificationApi.markAllRead();
|
||||
notifications.forEach(item => {
|
||||
item.is_read = true;
|
||||
item.read_at = item.read_at || new Date().toISOString();
|
||||
});
|
||||
unreadCount.value = 0;
|
||||
if (!isCurrentSession(context)) return;
|
||||
await Promise.all([loadNotifications(), loadUnreadCount()]);
|
||||
if (!isCurrentSession(context)) return;
|
||||
uni.showToast({ title: '已全部标记为已读', icon: 'success' });
|
||||
} catch (error) {
|
||||
console.error('全部标记通知已读失败', error);
|
||||
} finally {
|
||||
markingAll.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,13 +155,14 @@
|
||||
if (!noMore.value) loadNotifications(true);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadNotifications();
|
||||
loadUnreadCount();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
loadUnreadCount();
|
||||
onShow(async () => {
|
||||
notifications.splice(0);
|
||||
total.value = 0;
|
||||
unreadCount.value = 0;
|
||||
page.value = 1;
|
||||
noMore.value = false;
|
||||
notificationsLoaded.value = false;
|
||||
await Promise.all([loadNotifications(), loadUnreadCount()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user