Files
device-voice-h5/pages/switch/switch.vue
luo 9f07f9d0ef
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m10s
fix: ui
2026-08-05 15:23:58 +08:00

479 lines
12 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<view v-if="!cardsLoaded || (loading && mchList.length === 0)" class="loading-state">
<up-loading-icon text="加载中" size="30"></up-loading-icon>
</view>
<view v-else-if="mchList.length === 0" class="empty-state">
<image class="empty-icon" src="/static/change.png" mode="aspectFit" alt="切换运营商"></image>
<view class="empty-title">运营商列表为空</view>
<view class="empty-desc">当前设备暂无可切换的运营商</view>
</view>
<view class="card carrier-card" v-for="item in mchList" :key="item.iccid">
<view class="carrier-main">
<view class="logo-stack">
<image class="carrier-logo" :src="getCarrier(item.carrier_type).logo" mode="aspectFit"></image>
</view>
<view class="carrier-details">
<view class="carrier-heading">
<view class="carrier-name">{{ getCarrier(item.carrier_type).name }}</view>
<image class="slot-badge" :src="getSlotIcon(item.slot_position)" mode="aspectFit"></image>
<view class="carrier-statuses">
<up-tag :type="item.real_name_status === 1 ? 'primary' : 'success'" size="mini">
{{ item.real_name_status === 1 ? '已实名' : '未实名' }}
</up-tag>
<up-tag v-if="item.network_status" type="info" size="mini">{{ item.network_status }}</up-tag>
</view>
</view>
<view class="carrier-iccid">
<text class="iccid-value-text">{{ item.iccid }}</text>
<image class="copy-icon" src="/static/复制.png" mode="aspectFit"
@tap.stop="copyIccid(item.iccid)" aria-label="复制ICCID"></image>
</view>
<view class="card-actions">
<button v-if="item.is_current" class="btn-apple btn-primary action-button" disabled>当前使用</button>
<button v-else class="btn-apple btn-primary action-button" :disabled="switching" @tap="switchOperator(item)">
切换此运营商
</button>
<button v-if="item.real_name_status !== 1" class="btn-apple btn-secondary action-button" @tap="toReal(item)">
去实名
</button>
</view>
</view>
</view>
</view>
<view class="modal-overlay" v-if="showIccidModal" @tap="closeModal">
<view class="modal-content" @tap.stop>
<view class="modal-close" @tap="closeModal"></view>
<view class="modal-body">
<view class="modal-title">实名认证</view>
<view class="iccid-value">{{ currentModalIccid }}</view>
<view class="modal-button" @tap="doRealName">点击复制ICCID并跳转实名</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { assetApi, deviceApi, hasActiveOrPendingPackage, realnameApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js';
import slot1Icon from '@/static/卡槽1.jpeg';
import slot2Icon from '@/static/卡槽2.jpeg';
import slot3Icon from '@/static/卡槽3.jpeg';
import cmccLogo from '@/static/中国移动.png';
import cuccLogo from '@/static/中国联通.png';
import ctccLogo from '@/static/中国电信.png';
import cbnLogo from '@/static/中国广电.png';
const userStore = useUserStore();
let mchList = reactive([]);
let loading = ref(false);
let cardsLoaded = ref(false);
let switching = ref(false);
let showIccidModal = ref(false);
let currentModalIccid = ref('');
let realnameEntryChecking = ref(false);
const carrierMap = {
CMCC: { name: '中国移动', logo: cmccLogo },
CUCC: { name: '中国联通', logo: cuccLogo },
CTCC: { name: '中国电信', logo: ctccLogo },
CBN: { name: '中国广电', logo: cbnLogo }
};
const slotIconMap = {
1: slot1Icon,
2: slot2Icon,
3: slot3Icon
};
const getCarrier = (carrierType) => {
return carrierMap[carrierType] || { name: '-', logo: '' };
};
const getSlotIcon = (slotPosition) => slotIconMap[Number(slotPosition)] || slotIconMap[1];
const sortBySlotPosition = (cards) => cards.sort((left, right) => {
const leftSlot = Number(left.slot_position);
const rightSlot = Number(right.slot_position);
const normalizedLeft = Number.isFinite(leftSlot) && leftSlot > 0 ? leftSlot : Number.MAX_SAFE_INTEGER;
const normalizedRight = Number.isFinite(rightSlot) && rightSlot > 0 ? rightSlot : Number.MAX_SAFE_INTEGER;
return normalizedLeft - normalizedRight;
});
const copyIccid = (value) => {
if (!value) return;
uni.setClipboardData({
data: String(value),
success: () => uni.showToast({ title: 'ICCID已复制', icon: 'success' })
});
};
const loadCards = async () => {
loading.value = true;
try {
const assetData = await assetApi.getInfo(userStore.state.identifier);
// 判断是否为设备
if (assetData.asset_type === 'device') {
// 是设备,调用设备卡列表接口
const data = await deviceApi.getCards(userStore.state.identifier);
if (data.cards && data.cards.length > 0) {
const cards = data.cards.map(card => ({
iccid: card.iccid,
carrier_type: card.carrier_type,
carrier_name: card.carrier_name,
is_current: card.is_active,
real_name_status: card.real_name_status,
realname_policy: card.realname_policy,
network_status: card.network_status,
slot_position: card.slot_position
}));
mchList.splice(0, mchList.length, ...sortBySlotPosition(cards));
}
} else {
// 不是设备(单卡),直接使用 asset info 数据,只有一个卡
if (assetData.identifier) {
mchList.splice(0, mchList.length, {
iccid: assetData.identifier,
carrier_type: assetData.carrier_type,
carrier_name: assetData.carrier_name,
is_current: true,
real_name_status: assetData.real_name_status,
network_status: assetData.network_status,
slot_position: 1
});
}
}
} catch (e) {
console.error('加载卡列表失败', e);
}
loading.value = false;
cardsLoaded.value = true;
};
const switchOperator = (item) => {
if (switching.value) return;
uni.showModal({
title: '确认切换',
content: `确定要切换到${getCarrier(item.carrier_type).name}吗?切换后预计 3-5 分钟生效。`,
cancelText: '取消',
confirmText: '确认切换',
success: ({ confirm }) => {
if (confirm) executeSwitch(item);
}
});
};
const executeSwitch = async (item) => {
switching.value = true;
try {
await deviceApi.switchCard(userStore.state.identifier, item.iccid);
uni.showToast({ title: '切换成功3-5分钟后生效', icon: 'none' });
loadCards();
} catch (e) {
console.error('切换运营商失败', e);
}
switching.value = false;
};
const openRealnameModal = (iccid) => {
currentModalIccid.value = iccid;
showIccidModal.value = true;
};
const toReal = async (card) => {
if (realnameEntryChecking.value) return;
if (card.realname_policy !== 'after_order') {
openRealnameModal(card.iccid);
return;
}
realnameEntryChecking.value = true;
uni.showLoading({ title: '校验中...', mask: true });
try {
const data = await deviceApi.getCards(userStore.state.identifier);
const cards = Array.isArray(data?.cards) ? data.cards : [];
const currentCard = cards.find(item => item.iccid === card.iccid) || card;
const hasRealNamedCard = cards.some(item => Number(item?.real_name_status) === 1);
if (currentCard.realname_policy !== 'after_order' || hasRealNamedCard ||
await hasActiveOrPendingPackage(userStore.state.identifier)) {
uni.hideLoading();
openRealnameModal(card.iccid);
return;
}
uni.hideLoading();
uni.showModal({
title: '提示',
content: '当前设备所有卡均未实名,且暂无生效中或待生效套餐。请先订购套餐,再进行实名认证。',
confirmText: '去订购',
cancelText: '取消',
success: ({ confirm }) => {
if (confirm) uni.navigateTo({ url: '/pages/package-order/package-order' });
}
});
} catch (error) {
uni.hideLoading();
console.error('校验切换运营商实名条件失败', error);
uni.showToast({ title: '实名条件校验失败,请稍后重试', icon: 'none' });
} finally {
realnameEntryChecking.value = false;
}
};
const closeModal = () => {
showIccidModal.value = false;
};
const doRealName = async () => {
const iccid = currentModalIccid.value;
closeModal();
try {
const data = await realnameApi.getLink(userStore.state.identifier, iccid, { showError: false });
if (data.realname_url) {
uni.setClipboardData({
data: iccid,
success: () => {
uni.showToast({ title: 'ICCID已复制', icon: 'success' });
}
});
setTimeout(() => {
window.location.href = data.realname_url;
}, 1500);
}
} catch (e) {
console.error('获取实名链接失败', e);
uni.showToast({ title: e?.msg || '获取实名链接失败', icon: 'none' });
}
};
onMounted(() => {
loadCards();
});
</script>
<style lang="scss" scoped>
.container {
.carrier-card {
padding: 28rpx;
}
.carrier-main {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24rpx;
}
.logo-stack {
width: 88rpx;
height: 88rpx;
margin-top: 12rpx;
flex-shrink: 0;
box-sizing: border-box;
}
.carrier-logo {
width: 88rpx;
height: 88rpx;
flex-shrink: 0;
}
.slot-badge {
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
}
.carrier-details {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.carrier-name {
color: var(--text-primary);
font-size: 30rpx;
font-weight: 600;
line-height: 1.3;
}
.carrier-heading {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.carrier-iccid {
display: flex;
align-items: center;
gap: 10rpx;
margin-top: 14rpx;
min-width: 0;
}
.iccid-value-text {
flex: 0 1 auto;
min-width: 0;
color: var(--text-secondary);
font-size: 24rpx;
line-height: 1.35;
word-break: break-all;
}
.copy-icon {
width: 30rpx;
height: 30rpx;
flex-shrink: 0;
cursor: pointer;
}
.carrier-statuses {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 0;
}
.card-actions {
width: 100%;
margin-top: 20rpx;
display: flex;
flex-direction: row;
gap: 12rpx;
}
.action-button {
flex: 1 1 0;
width: auto;
min-width: 0;
height: 64rpx;
padding: 0 12rpx;
font-size: 24rpx;
line-height: 64rpx;
box-sizing: border-box;
&::after { border: none; }
}
}
.btn-apple {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: var(--radius-small);
font-weight: 500;
font-size: 26rpx;
line-height: 1;
cursor: pointer;
padding: 20rpx;
background: var(--gray-100);
color: var(--text-primary);
&.btn-primary {
background: var(--primary);
color: var(--text-inverse);
}
&.btn-success {
background: var(--success);
color: var(--text-inverse);
}
}
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-content {
background: var(--bg-primary);
border-radius: var(--radius-large);
width: 620rpx;
max-width: 90%;
overflow: hidden;
position: relative;
}
.modal-close {
position: absolute;
top: 24rpx; left: 24rpx;
width: 48rpx; height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: var(--gray-600);
cursor: pointer;
}
.modal-body {
padding: 60rpx 32rpx 32rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 32rpx;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: var(--text-primary);
text-align: center;
}
.iccid-value {
font-size: 40rpx;
font-weight: bold;
color: var(--primary);
text-align: center;
word-break: break-all;
line-height: 1.4;
}
.modal-button {
padding: 24rpx 32rpx;
background: var(--success);
color: var(--text-inverse);
font-size: 28rpx;
font-weight: 500;
text-align: center;
border-radius: var(--radius-small);
cursor: pointer;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
min-height: 400rpx;
.empty-icon { width: 120rpx; height: 120rpx; margin-bottom: 30rpx; opacity: 0.6; }
.empty-title { font-size: 32rpx; font-weight: 600; color: var(--text-primary); margin-bottom: 16rpx; }
.empty-desc { font-size: 26rpx; color: var(--text-tertiary); text-align: center; }
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 500rpx;
}
</style>