fix: some
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m59s

This commit is contained in:
luo
2026-10-13 14:53:19 +08:00
parent 77b0e25011
commit da95a914e3
31 changed files with 1702 additions and 116 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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' });

View File

@@ -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>