This commit is contained in:
@@ -3,24 +3,30 @@ import { useAssetStore } from '@/store/asset.js';
|
||||
import { useUserStore } from '@/store/user.js';
|
||||
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
|
||||
|
||||
export const normalizeAssetInfo = (data = {}) => ({
|
||||
...data,
|
||||
allowed_payment_methods: Array.isArray(data.allowed_payment_methods)
|
||||
? data.allowed_payment_methods
|
||||
: [],
|
||||
real_name_status: data.real_name_status === undefined || data.real_name_status === null
|
||||
? data.real_name_status
|
||||
: Number(data.real_name_status),
|
||||
realname_required: data.realname_required === undefined || data.realname_required === null
|
||||
? data.realname_required
|
||||
: data.realname_required === true || data.realname_required === 1,
|
||||
days_until_final_expiry: data.days_until_final_expiry === null || data.days_until_final_expiry === undefined
|
||||
? data.days_until_final_expiry
|
||||
: Number(data.days_until_final_expiry),
|
||||
is_expiring: data.is_expiring === undefined || data.is_expiring === null
|
||||
? data.is_expiring
|
||||
: data.is_expiring === true || data.is_expiring === 1
|
||||
});
|
||||
let assetInfoRequestId = 0;
|
||||
|
||||
export const normalizeAssetInfo = (data = {}) => {
|
||||
// 保护期已下线:不向展示和业务判断透传废弃字段,兼容 none 或字段缺失。
|
||||
const { device_protect_status, ...info } = data;
|
||||
return {
|
||||
...info,
|
||||
allowed_payment_methods: Array.isArray(data.allowed_payment_methods)
|
||||
? data.allowed_payment_methods
|
||||
: [],
|
||||
real_name_status: data.real_name_status === undefined || data.real_name_status === null
|
||||
? data.real_name_status
|
||||
: Number(data.real_name_status),
|
||||
realname_required: data.realname_required === undefined || data.realname_required === null
|
||||
? data.realname_required
|
||||
: data.realname_required === true || data.realname_required === 1,
|
||||
days_until_final_expiry: data.days_until_final_expiry === null || data.days_until_final_expiry === undefined
|
||||
? data.days_until_final_expiry
|
||||
: Number(data.days_until_final_expiry),
|
||||
is_expiring: data.is_expiring === undefined || data.is_expiring === null
|
||||
? data.is_expiring
|
||||
: data.is_expiring === true || data.is_expiring === 1
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePackageHistoryNode = (node = {}) => ({
|
||||
...node,
|
||||
@@ -69,6 +75,7 @@ export const assetApi = {
|
||||
return Promise.reject({ msg: '登录和微信配置尚未完成,请稍后重试' });
|
||||
}
|
||||
const phoneRevision = userStore.state.phoneRevision;
|
||||
const requestId = identifier === context.identifier ? ++assetInfoRequestId : null;
|
||||
return request({
|
||||
url: '/api/c/v1/asset/info',
|
||||
method: 'GET',
|
||||
@@ -76,7 +83,10 @@ export const assetApi = {
|
||||
}).then((response) => {
|
||||
const data = normalizeAssetInfo(response);
|
||||
if (identifier === context.identifier && isCurrentSession(context)) {
|
||||
useAssetStore().receiveExpiryReminder(context, data.expiry_reminder);
|
||||
const assetStore = useAssetStore();
|
||||
// 替换整个快照,不合并旧轮询开关;迟到的旧请求不得覆盖新状态。
|
||||
if (requestId === assetInfoRequestId) assetStore.setAssetInfo(data);
|
||||
assetStore.receiveExpiryReminder(context, data.expiry_reminder);
|
||||
if (phoneRevision === userStore.state.phoneRevision && typeof data.bound_phone === 'string') {
|
||||
userStore.setCustomerPhone(data.bound_phone);
|
||||
}
|
||||
|
||||
@@ -40,32 +40,23 @@
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
watch
|
||||
computed
|
||||
} from 'vue';
|
||||
import {
|
||||
assetApi
|
||||
} from '@/api/index.js';
|
||||
import {
|
||||
useUserStore
|
||||
} from '@/store/index.js';
|
||||
|
||||
const props = defineProps({
|
||||
identifier: {
|
||||
type: String,
|
||||
default: ''
|
||||
assetInfo: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['packageLoaded']);
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 流量数据
|
||||
const usedMB = ref(0);
|
||||
const totalMB = ref(0);
|
||||
const remainMB = ref(0);
|
||||
// 和首页共用最新资产快照,不保留只在首次进入时加载的套餐缓存。
|
||||
const metrics = computed(() => props.assetInfo?.current_package_usage_id
|
||||
? getTrafficMetrics(props.assetInfo)
|
||||
: { used: 0, total: 0, remain: 0 });
|
||||
const usedMB = computed(() => metrics.value.used);
|
||||
const totalMB = computed(() => metrics.value.total);
|
||||
const remainMB = computed(() => metrics.value.remain);
|
||||
|
||||
const toMB = (value) => Number(value) || 0;
|
||||
|
||||
@@ -125,40 +116,6 @@
|
||||
};
|
||||
};
|
||||
|
||||
// 加载套餐流量数据
|
||||
const loadPackageData = async () => {
|
||||
if (userStore.state.loginPending || !userStore.state.token) return;
|
||||
const identifier = props.identifier || userStore.state.identifier;
|
||||
if (!identifier) return;
|
||||
|
||||
try {
|
||||
const data = await assetApi.getInfo(identifier);
|
||||
|
||||
if (data.current_package_usage_id) {
|
||||
const metrics = getTrafficMetrics(data);
|
||||
usedMB.value = metrics.used;
|
||||
totalMB.value = metrics.total;
|
||||
remainMB.value = metrics.remain;
|
||||
emit('packageLoaded', data);
|
||||
} else {
|
||||
usedMB.value = 0;
|
||||
totalMB.value = 0;
|
||||
remainMB.value = 0;
|
||||
emit('packageLoaded', null);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载套餐流量数据失败', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听 identifier 变化
|
||||
watch(() => [props.identifier || userStore.state.identifier, userStore.state.token, userStore.state.loginPending], ([newVal, token, loginPending]) => {
|
||||
if (newVal && token && !loginPending) {
|
||||
loadPackageData();
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<!-- <VoiceCard v-if="!userInfo.isDevice" :voiceStats="voiceStats" :dateRangeText="dateRangeText"
|
||||
@openDatePicker="openDateRangePicker" /> -->
|
||||
|
||||
<TrafficCard :identifier="userStore.state.identifier" />
|
||||
<TrafficCard :assetInfo="assetStore.state.assetInfo" />
|
||||
|
||||
<!-- <WhitelistCard v-if="!userInfo.isDevice" :whitelistData="whitelistData" @refresh="refreshWhitelist"
|
||||
@add="showAddWhitelistDialog" @showSms="showSmsCodeDialog" /> -->
|
||||
@@ -205,6 +205,8 @@
|
||||
let recoverShow = ref(false);
|
||||
let logoutShow = ref(false);
|
||||
let loading = ref(false);
|
||||
let syncing = ref(false);
|
||||
let assetRequestId = 0;
|
||||
|
||||
let userInfo = reactive({
|
||||
isDevice: true,
|
||||
@@ -284,7 +286,6 @@
|
||||
let showSmsCode = ref(false);
|
||||
let smsCodePhone = ref('');
|
||||
let smsCode = ref('');
|
||||
let indexEntryChecking = ref(false);
|
||||
let notificationUnreadCount = ref(0);
|
||||
let notificationPopupShow = ref(false);
|
||||
let notificationItems = ref([]);
|
||||
@@ -309,11 +310,12 @@
|
||||
if (!identifier) return;
|
||||
if (userStore.state.identifier !== identifier) userStore.setIdentifier(identifier);
|
||||
const context = getSessionContext();
|
||||
const requestId = ++assetRequestId;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await assetApi.getInfo(identifier);
|
||||
if (!isCurrentSession(context)) return null;
|
||||
if (!isCurrentSession(context) || requestId !== assetRequestId) return null;
|
||||
|
||||
// 基本信息
|
||||
userInfo.isDevice = data.asset_type === 'device';
|
||||
@@ -338,7 +340,7 @@
|
||||
effectiveRealnamePolicy.value = data.effective_realname_policy || 'none';
|
||||
userStore.setRealNameStatus(data.real_name_status);
|
||||
|
||||
// 流量信息已由 TrafficCard 组件独立获取,这里不再处理
|
||||
// 流量卡片消费同一次 asset/info 快照,支付和有效期变化后一起更新。
|
||||
|
||||
// 当前卡信息
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
@@ -397,7 +399,7 @@
|
||||
console.error('加载资产信息失败', e);
|
||||
return null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (requestId === assetRequestId) loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -415,14 +417,7 @@
|
||||
}
|
||||
if (userStore.state.identifier !== identifier) userStore.setIdentifier(identifier);
|
||||
|
||||
if (indexEntryChecking.value) return;
|
||||
indexEntryChecking.value = true;
|
||||
|
||||
try {
|
||||
await loadAssetInfo();
|
||||
} finally {
|
||||
indexEntryChecking.value = false;
|
||||
}
|
||||
await loadAssetInfo();
|
||||
};
|
||||
|
||||
const onRenewalVisibilityChange = (visible) => {
|
||||
@@ -738,6 +733,7 @@
|
||||
};
|
||||
|
||||
const onSync = async () => {
|
||||
if (syncing.value) return;
|
||||
const identifier = userStore.state.identifier;
|
||||
if (!identifier) {
|
||||
uni.showToast({
|
||||
@@ -747,11 +743,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getSessionContext();
|
||||
uni.showModal({
|
||||
title: '确认同步',
|
||||
content: '确定要同步运营商数据吗?',
|
||||
content: '确定要同步运营商数据吗?受理不代表同步或停复机已执行完成,结果以稍后读取的实际状态为准。',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
if (!res.confirm || syncing.value || !isCurrentSession(context)) return;
|
||||
syncing.value = true;
|
||||
uni.showLoading({
|
||||
title: '同步中...',
|
||||
mask: true
|
||||
@@ -759,17 +757,19 @@
|
||||
|
||||
assetApi.refresh(identifier).then((data) => {
|
||||
uni.hideLoading();
|
||||
if (!isCurrentSession(context)) return;
|
||||
|
||||
if (data.accepted) {
|
||||
if (data?.accepted === true || data?.accepted === 1) {
|
||||
const refreshTypeText = data.refresh_type === 'device' ? '设备' : '卡';
|
||||
uni.showToast({
|
||||
title: `${refreshTypeText}数据同步成功`,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
title: `${refreshTypeText}同步请求已受理,请稍后查看状态`,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
|
||||
loadAssetInfo();
|
||||
setTimeout(() => {
|
||||
loadAssetInfo();
|
||||
if (homeActive.value && isCurrentSession(context)) loadAssetInfo();
|
||||
}, 2000);
|
||||
} else {
|
||||
uni.showToast({
|
||||
@@ -779,6 +779,7 @@
|
||||
}
|
||||
}).catch((e) => {
|
||||
uni.hideLoading();
|
||||
if (!isCurrentSession(context)) return;
|
||||
console.error('同步数据失败', e);
|
||||
|
||||
if (e.msg && e.msg.includes('冷却')) {
|
||||
@@ -793,7 +794,7 @@
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
}).finally(() => { syncing.value = false; });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1018,9 +1019,8 @@
|
||||
handleIndexEntry();
|
||||
return;
|
||||
}
|
||||
if (consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.HOME)) {
|
||||
loadAssetInfo();
|
||||
}
|
||||
// 每次返回首页都重新读取实际状态,支付返回不再重复发起两次请求。
|
||||
consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.HOME);
|
||||
loadNotificationUnreadCount();
|
||||
runPopupCandidate();
|
||||
await handleIndexEntry();
|
||||
@@ -1030,6 +1030,7 @@
|
||||
});
|
||||
onHide(() => {
|
||||
homeEntryId++;
|
||||
assetRequestId++;
|
||||
homeActive.value = false;
|
||||
shownExpiryReminder.value = null;
|
||||
notificationPopupShow.value = false;
|
||||
|
||||
@@ -67,14 +67,14 @@
|
||||
</view>
|
||||
|
||||
<RenewalPaymentPopup ref="renewalPopupRef" :identifier="userStore.state.identifier"
|
||||
paymentRefreshTarget="order-list" @completed="resetOrderListAndLoad" />
|
||||
paymentRefreshTarget="order-list" @completed="refreshOrderState" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { onReachBottom, onShow } from '@dcloudio/uni-app';
|
||||
import { orderApi } from '@/api/index.js';
|
||||
import { assetApi, orderApi } from '@/api/index.js';
|
||||
import RenewalPaymentPopup from '@/components/RenewalPaymentPopup.vue';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
import {
|
||||
@@ -121,17 +121,25 @@
|
||||
loadOrderList();
|
||||
};
|
||||
|
||||
const refreshOrderState = () => {
|
||||
resetOrderListAndLoad();
|
||||
// 订单支付、退款及套餐生效后,读取后端实际轮询状态,不推测已恢复。
|
||||
return assetApi.getInfo(userStore.state.identifier).catch(error => {
|
||||
console.error('刷新订单关联资产状态失败', error);
|
||||
});
|
||||
};
|
||||
|
||||
const scheduleOrderStatusRefresh = () => {
|
||||
uni.showToast({
|
||||
title: '正在同步支付状态',
|
||||
icon: 'none'
|
||||
});
|
||||
resetOrderListAndLoad();
|
||||
refreshOrderState();
|
||||
setTimeout(() => {
|
||||
resetOrderListAndLoad();
|
||||
}, 1500);
|
||||
setTimeout(() => {
|
||||
resetOrderListAndLoad();
|
||||
refreshOrderState();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
@@ -232,16 +240,13 @@
|
||||
|
||||
onShow(() => {
|
||||
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.ORDER_LIST)) {
|
||||
refreshOrderState();
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleOrderStatusRefresh();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadOrderList();
|
||||
});
|
||||
|
||||
onReachBottom(() => {
|
||||
loadMore();
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { assetApi, isAssetRealNameCompleted, orderApi, walletApi } from '@/api/index.js';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
@@ -121,6 +121,7 @@
|
||||
import { getPaymentErrorMessage, isUnknownPaymentResult } from '@/utils/payment-attempt.js';
|
||||
import { formatMoney } from '@/utils/display.js';
|
||||
import { getDefaultPaymentMethod, getPaymentMethodOptions, normalizePaymentMethods } from '@/utils/payment-methods.js';
|
||||
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const popupCandidateShow = ref(false);
|
||||
@@ -147,7 +148,7 @@
|
||||
{ label: '正式套餐', value: 'formal' },
|
||||
{ label: '加油包', value: 'addon' }
|
||||
];
|
||||
let assetTypePromise = null;
|
||||
let assetRequestId = 0;
|
||||
let packageRequestId = 0;
|
||||
const paymentMethodOptions = computed(() => getPaymentMethodOptions(allowedPaymentMethods.value));
|
||||
|
||||
@@ -177,22 +178,19 @@
|
||||
|
||||
const loadAssetType = async () => {
|
||||
if (!userStore.state.identifier) return;
|
||||
if (assetTypePromise) return assetTypePromise;
|
||||
|
||||
assetTypePromise = assetApi.getInfo(userStore.state.identifier)
|
||||
.then((data) => {
|
||||
assetInfo.value = data;
|
||||
allowedPaymentMethods.value = normalizePaymentMethods(data.allowed_payment_methods);
|
||||
normalizePaymentMethod();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('加载资产类型失败', error);
|
||||
})
|
||||
.finally(() => {
|
||||
assetTypePromise = null;
|
||||
});
|
||||
|
||||
return assetTypePromise;
|
||||
const context = getSessionContext();
|
||||
const requestId = ++assetRequestId;
|
||||
try {
|
||||
const data = await assetApi.getInfo(userStore.state.identifier);
|
||||
if (!isCurrentSession(context) || requestId !== assetRequestId) return null;
|
||||
assetInfo.value = data;
|
||||
allowedPaymentMethods.value = normalizePaymentMethods(data.allowed_payment_methods);
|
||||
normalizePaymentMethod();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('加载资产类型失败', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPackages = async (packageType = activePackageType.value) => {
|
||||
@@ -261,12 +259,21 @@
|
||||
};
|
||||
|
||||
const syncPackagePageState = async () => {
|
||||
await loadAssetType();
|
||||
if (!await loadAssetType()) return;
|
||||
await Promise.all([loadPackages(), loadWalletBalance()]);
|
||||
};
|
||||
|
||||
const refreshAfterPayment = async () => {
|
||||
const context = getSessionContext();
|
||||
await syncPackagePageState();
|
||||
// 充值自动购包可能稍后生效,再确认一次,仍以实际响应为准。
|
||||
setTimeout(() => {
|
||||
if (isCurrentSession(context)) syncPackagePageState();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const buyPackage = async (item) => {
|
||||
await loadAssetType();
|
||||
if (!await loadAssetType()) return;
|
||||
if (assetInfo.value.effective_realname_policy === 'before_order' &&
|
||||
assetInfo.value.realname_required && !isAssetRealNameCompleted(assetInfo.value)) {
|
||||
uni.showModal({
|
||||
@@ -309,9 +316,7 @@
|
||||
} else {
|
||||
showPaymentToast(true, isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功');
|
||||
}
|
||||
setTimeout(() => {
|
||||
loadWalletBalance();
|
||||
}, 1500);
|
||||
await refreshAfterPayment();
|
||||
} catch (error) {
|
||||
handlePaymentError(error);
|
||||
}
|
||||
@@ -464,9 +469,7 @@
|
||||
} catch (error) {
|
||||
showPaymentToast(false, '支付结果确认失败,请稍后查看订单');
|
||||
}
|
||||
setTimeout(() => {
|
||||
loadWalletBalance();
|
||||
}, 1500);
|
||||
await refreshAfterPayment();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -505,11 +508,12 @@
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
// 后台也可能修改套餐有效期,返回页面时始终重读,不只处理支付宝返回。
|
||||
syncPackagePageState();
|
||||
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.PACKAGE_ORDER)) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncPackagePageState();
|
||||
uni.showModal({
|
||||
title: '支付状态确认',
|
||||
content: '已返回页面,支付结果以订单状态为准。是否前往我的订单查看最新状态?',
|
||||
@@ -539,9 +543,6 @@
|
||||
popupCandidateShow.value = true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
syncPackagePageState();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -29,6 +29,13 @@ function runtime(initial = {}) {
|
||||
const timers = [];
|
||||
const sessionStorage = new Map();
|
||||
const api = {};
|
||||
const props = vue.reactive({});
|
||||
const payment = {
|
||||
consumePendingPaymentRefresh: () => false,
|
||||
PAYMENT_REFRESH_TARGETS: { HOME: 'home', PACKAGE_ORDER: 'package-order', ORDER_LIST: 'order-list', MY_WALLET: 'my-wallet' },
|
||||
handlePaymentError() {}, isValidAlipayPaymentLink: () => false, isValidWechatPayConfig: () => false,
|
||||
markPendingPaymentRefresh() {}, openAlipayPayment: async () => {}, showPaymentToast() {}, wechatH5Pay: async () => {}
|
||||
};
|
||||
let inWechat = false;
|
||||
let sdkInit = async () => {};
|
||||
let options = {};
|
||||
@@ -40,6 +47,8 @@ function runtime(initial = {}) {
|
||||
request: request => { requests.push(request); },
|
||||
showToast: value => calls.push({ kind: 'toast', ...value }),
|
||||
showModal: value => calls.push({ kind: 'modal', ...value }),
|
||||
showLoading: value => calls.push({ kind: 'loading', ...value }),
|
||||
hideLoading: () => calls.push({ kind: 'hideLoading' }),
|
||||
reLaunch: value => calls.push({ kind: 'reLaunch', ...value }),
|
||||
redirectTo: value => calls.push({ kind: 'redirectTo', ...value }),
|
||||
navigateTo: value => calls.push({ kind: 'navigateTo', ...value }),
|
||||
@@ -53,7 +62,7 @@ function runtime(initial = {}) {
|
||||
getCurrentPages: () => [{ options }],
|
||||
sessionStorage: { getItem: key => sessionStorage.get(key) || '', removeItem: key => sessionStorage.delete(key), setItem: (key, value) => sessionStorage.set(key, value) },
|
||||
window: { location: { search: '', pathname: '/', hash: '', origin: 'https://example.invalid' }, history: { replaceState() {} } },
|
||||
defineProps: () => ({}), defineEmits: () => () => {}
|
||||
defineProps: () => props, defineEmits: () => () => {}, defineExpose() {}
|
||||
});
|
||||
const cache = new Map();
|
||||
const synthetic = (key, values) => {
|
||||
@@ -81,13 +90,14 @@ function runtime(initial = {}) {
|
||||
onMounted: cb => mounted.push(cb), onUnmounted: cb => unmounted.push(cb)
|
||||
});
|
||||
if (specifier === '@dcloudio/uni-app') return synthetic('uni-app', {
|
||||
onShow: cb => shown.push(cb), onHide: cb => hidden.push(cb), onUnload: cb => unmounted.push(cb)
|
||||
onShow: cb => shown.push(cb), onHide: cb => hidden.push(cb), onUnload: cb => unmounted.push(cb),
|
||||
onLoad() {}, onReachBottom() {}
|
||||
});
|
||||
if (specifier === '@/api/index.js') return synthetic('api-index', {
|
||||
...api, deviceApi: {}, hasActiveOrPendingPackage: async () => true
|
||||
});
|
||||
if (specifier === '@/utils/payment.js') return synthetic('payment', {
|
||||
consumePendingPaymentRefresh: () => false, PAYMENT_REFRESH_TARGETS: { HOME: 'home' }
|
||||
...payment, consumePendingPaymentRefresh: (...args) => payment.consumePendingPaymentRefresh(...args)
|
||||
});
|
||||
if (specifier === '@/utils/wxsdk.js') return synthetic('wxsdk', {
|
||||
initWxConfig: () => sdkInit(), wxScan: async () => {}, isInWechat: () => inWechat
|
||||
@@ -106,7 +116,7 @@ function runtime(initial = {}) {
|
||||
const reply = (request, data = {}, code = 0, statusCode = 200, msg = 'success') => {
|
||||
request.success({ statusCode, data: { code, msg, data } });
|
||||
};
|
||||
return { storage, sessionStorage, context, calls, requests, mounted, shown, hidden, unmounted, timers, api, load, reply,
|
||||
return { storage, sessionStorage, context, calls, requests, mounted, shown, hidden, unmounted, timers, api, props, payment, load, reply,
|
||||
setOptions: value => { options = value; }, tick: () => vue.nextTick(),
|
||||
setWechatSdk: init => { inWechat = true; sdkInit = init; } };
|
||||
}
|
||||
@@ -727,3 +737,197 @@ test('401 clears phone/reminder state, but late 401 cannot log out a new session
|
||||
assert.equal(user.state.assetPhoneBound, null);
|
||||
assert.equal(r.storage.has('customerAssetPhoneState'), false);
|
||||
});
|
||||
|
||||
test('asset info ignores deprecated protection values and preserves actual polling states', async () => {
|
||||
const r = runtime();
|
||||
const { normalizeAssetInfo } = await r.load('api/modules/asset.js');
|
||||
for (const protection of [undefined, 'none', 'protecting']) {
|
||||
const input = { enable_polling: false, network_status: 0, cards: [{ enable_polling: false }] };
|
||||
if (protection !== undefined) input.device_protect_status = protection;
|
||||
const result = normalizeAssetInfo(input);
|
||||
assert.equal(Object.hasOwn(result, 'device_protect_status'), false);
|
||||
assert.equal(result.enable_polling, false);
|
||||
assert.equal(result.network_status, 0);
|
||||
assert.equal(result.cards[0].enable_polling, false);
|
||||
}
|
||||
assert.equal(normalizeAssetInfo({}).enable_polling, undefined);
|
||||
for (const path of ['pages/index/index.vue', 'components/UserInfoCard.vue', 'components/DeviceStatusCard.vue', 'components/TrafficCard.vue']) {
|
||||
assert.doesNotMatch(readFileSync(resolve(root, path), 'utf8'), /device_protect_status|保护期/);
|
||||
}
|
||||
});
|
||||
|
||||
test('fresh asset snapshots replace polling cache and late responses cannot roll it back', async () => {
|
||||
const r = runtime();
|
||||
const { assetApi } = await r.load('api/modules/asset.js');
|
||||
const asset = (await r.load('store/asset.js')).useAssetStore();
|
||||
asset.setAssetInfo({ enable_polling: false, current_package_id: 1 });
|
||||
const old = assetApi.getInfo('A');
|
||||
const fresh = assetApi.getInfo('A');
|
||||
r.reply(r.requests[1], { enable_polling: true, current_package_id: 2, cards: [{ enable_polling: true }] });
|
||||
await fresh;
|
||||
r.reply(r.requests[0], { enable_polling: false, current_package_id: 1 });
|
||||
await old;
|
||||
assert.equal(asset.state.assetInfo.enable_polling, true);
|
||||
assert.equal(asset.state.assetInfo.current_package_id, 2);
|
||||
assert.equal(asset.state.assetInfo.cards[0].enable_polling, true);
|
||||
const missing = assetApi.getInfo('A');
|
||||
r.reply(r.requests[2], { current_package_id: 3 }); await missing;
|
||||
assert.equal(asset.state.assetInfo.enable_polling, undefined);
|
||||
assert.equal(asset.state.assetInfo.cards, undefined);
|
||||
const late = assetApi.getInfo('A');
|
||||
(await r.load('store/user.js')).useUserStore().setIdentifier('B');
|
||||
r.reply(r.requests[3], { enable_polling: true }); await late;
|
||||
assert.equal(asset.state.assetInfo, null);
|
||||
const b = assetApi.getInfo('B');
|
||||
r.reply(r.requests[4], { enable_polling: false }); await b;
|
||||
assert.equal(asset.state.assetInfo.enable_polling, false);
|
||||
});
|
||||
|
||||
test('traffic uses the same fresh asset snapshot and clears expired or missing package data', async () => {
|
||||
const r = runtime();
|
||||
const card = await r.load('components/TrafficCard.vue', ['usedMB', 'totalMB', 'remainMB']);
|
||||
assert.equal(card.totalMB.value, 0);
|
||||
r.props.assetInfo = { current_package_usage_id: 1, real_used_mb: 100, real_total_mb: 1000 };
|
||||
assert.equal(card.usedMB.value, 100);
|
||||
assert.equal(card.remainMB.value, 900);
|
||||
r.props.assetInfo = { current_package_usage_id: 2, real_used_mb: 200, virtual_used_mb: 150, enable_virtual_data: true, real_total_mb: 2000 };
|
||||
assert.equal(card.usedMB.value, 150);
|
||||
assert.equal(card.totalMB.value, 2000);
|
||||
assert.equal(card.remainMB.value, 1850);
|
||||
r.props.assetInfo = { real_used_mb: 200, real_total_mb: 2000 };
|
||||
assert.equal(card.usedMB.value, 0);
|
||||
assert.equal(card.totalMB.value, 0);
|
||||
r.props.assetInfo = null;
|
||||
assert.equal(card.remainMB.value, 0);
|
||||
assert.equal(r.requests.length, 0);
|
||||
});
|
||||
|
||||
test('returning home reloads actual polling, network and expiry with one asset request per entry', async () => {
|
||||
const r = runtime();
|
||||
r.api.assetApi = (await r.load('api/modules/asset.js')).assetApi;
|
||||
r.api.notificationApi = { getUnreadCount: async () => ({ count: 0 }) };
|
||||
r.payment.consumePendingPaymentRefresh = () => true;
|
||||
const page = await r.load('pages/index/index.vue', ['deviceInfo']);
|
||||
const first = r.shown[0]();
|
||||
assert.equal(r.requests.length, 1);
|
||||
r.reply(r.requests[0], { enable_polling: false, network_status: 0, estimated_final_expires_at: '2026-10-26T00:00:00Z' });
|
||||
await first;
|
||||
const asset = (await r.load('store/asset.js')).useAssetStore();
|
||||
assert.equal(asset.state.assetInfo.enable_polling, false);
|
||||
r.hidden[0]();
|
||||
const next = r.shown[0]();
|
||||
assert.equal(r.requests.length, 2);
|
||||
r.reply(r.requests[1], { enable_polling: true, network_status: 1, estimated_final_expires_at: '2026-11-26T00:00:00Z' });
|
||||
await next;
|
||||
assert.equal(asset.state.assetInfo.enable_polling, true);
|
||||
assert.equal(page.deviceInfo.network_status, 1);
|
||||
assert.equal(page.deviceInfo.expireDate, '2026-11-26');
|
||||
});
|
||||
|
||||
test('home rejects an old asset request after a new refresh or hide-and-return', async () => {
|
||||
const r = runtime();
|
||||
r.api.notificationApi = {};
|
||||
const old = deferred();
|
||||
const fresh = deferred();
|
||||
let reads = 0;
|
||||
r.api.assetApi = { getInfo: () => (++reads === 1 ? old.promise : fresh.promise) };
|
||||
const page = await r.load('pages/index/index.vue', ['loadAssetInfo', 'deviceInfo']);
|
||||
const a = page.loadAssetInfo();
|
||||
r.hidden[0]();
|
||||
const b = page.loadAssetInfo();
|
||||
fresh.resolve({ network_status: 0, current_package: '新套餐' }); await b;
|
||||
old.resolve({ network_status: 1, current_package: '旧套餐' }); await a;
|
||||
assert.equal(page.deviceInfo.network_status, 0);
|
||||
assert.equal(page.deviceInfo.packageName, '新套餐');
|
||||
});
|
||||
|
||||
test('sync acceptance is not completion and never optimistically resumes the network', async () => {
|
||||
for (const accepted of [true, false, undefined, 'false']) {
|
||||
const r = runtime();
|
||||
r.api.notificationApi = {};
|
||||
let reads = 0;
|
||||
r.api.assetApi = {
|
||||
refresh: async () => ({ accepted, refresh_type: 'device' }),
|
||||
getInfo: async () => { reads++; return { network_status: 0 }; }
|
||||
};
|
||||
const page = await r.load('pages/index/index.vue', ['onSync', 'deviceInfo', 'homeActive']);
|
||||
page.homeActive.value = true;
|
||||
await page.onSync();
|
||||
const modal = r.calls.find(call => call.kind === 'modal');
|
||||
assert.match(modal.content, /受理不代表.*停复机已执行完成/);
|
||||
modal.success({ confirm: true });
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
const toast = r.calls.find(call => call.kind === 'toast');
|
||||
assert.equal(toast.icon, 'none');
|
||||
assert.doesNotMatch(toast.title, /同步成功|复机成功|停机成功/);
|
||||
assert.equal(reads, accepted === true ? 1 : 0);
|
||||
if (accepted === true) {
|
||||
assert.match(toast.title, /已受理/);
|
||||
assert.equal(page.deviceInfo.network_status, 0);
|
||||
await r.timers[0]();
|
||||
assert.equal(reads, 2);
|
||||
(await r.load('store/user.js')).useUserStore().setIdentifier('B');
|
||||
await r.timers[0]();
|
||||
assert.equal(reads, 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('package page reloads asset state on every entry, after wallet payment and after wechat payment', async () => {
|
||||
for (const method of ['wallet', 'wechat']) {
|
||||
const r = runtime();
|
||||
let polling = false;
|
||||
let reads = 0;
|
||||
r.api.assetApi = {
|
||||
getInfo: async () => { reads++; return { enable_polling: polling, allowed_payment_methods: [method] }; },
|
||||
getPackages: async () => ({ packages: [] })
|
||||
};
|
||||
r.api.isAssetRealNameCompleted = () => true;
|
||||
r.api.walletApi = { getDetail: async () => ({ balance: 0 }) };
|
||||
r.api.orderApi = {
|
||||
create: async () => ({ order: { order_id: 12, payment_status: 2 } }),
|
||||
pay: async () => ({}), getDetail: async () => ({ payment_status: 2 })
|
||||
};
|
||||
const page = await r.load('pages/package-order/package-order.vue', ['syncPackagePageState', 'confirmPay', 'currentPackage', 'paymentMethod', 'assetInfo', 'handleWechatPay']);
|
||||
await page.syncPackagePageState();
|
||||
assert.equal(page.assetInfo.value.enable_polling, false);
|
||||
polling = true;
|
||||
if (method === 'wallet') {
|
||||
page.currentPackage.value = { package_id: 1 };
|
||||
page.paymentMethod.value = 'wallet';
|
||||
await page.confirmPay();
|
||||
} else {
|
||||
await page.handleWechatPay({}, false, 12);
|
||||
}
|
||||
assert.equal(page.assetInfo.value.enable_polling, true);
|
||||
assert.equal(reads, 2);
|
||||
polling = false;
|
||||
await r.timers[0]();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(page.assetInfo.value.enable_polling, false);
|
||||
polling = true;
|
||||
r.shown[0]();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(page.assetInfo.value.enable_polling, true);
|
||||
assert.equal(reads, 4);
|
||||
}
|
||||
});
|
||||
|
||||
test('order completion and payment return reread actual asset state, not a presumed polling value', async () => {
|
||||
const r = runtime();
|
||||
let reads = 0;
|
||||
r.api.assetApi = { getInfo: async () => { reads++; return { enable_polling: false }; } };
|
||||
r.api.orderApi = { getList: async () => ({ items: [] }) };
|
||||
const page = await r.load('pages/order-list/order-list.vue', ['refreshOrderState']);
|
||||
await page.refreshOrderState();
|
||||
assert.equal(reads, 1);
|
||||
r.shown[0]();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(reads, 2);
|
||||
r.payment.consumePendingPaymentRefresh = () => true;
|
||||
r.shown[0]();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(reads, 3);
|
||||
await r.timers[1]();
|
||||
assert.equal(reads, 4);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user