新增h5qrcode依赖

This commit is contained in:
2026-02-05 17:38:19 +08:00
parent 5a13803743
commit f476cee76d
16 changed files with 2036 additions and 559 deletions
+115 -30
View File
@@ -989,21 +989,39 @@
const processScanResult = async (scanResult) => {
try {
console.log('===== 处理扫码结果 =====');
console.log('扫码结果对象:', scanResult);
console.log('scanType:', scanResult.scanType);
console.log('result:', scanResult.result);
console.log('path:', scanResult.path);
let deviceNo;
if (scanResult.scanType == 'MANUAL') {
deviceNo = scanResult.result;
} else if (scanResult.scanType == '"QR_CODE"') {
deviceNo = getQueryString(scanResult.result, 'deviceNo')
console.log('手动输入模式,设备号:', deviceNo);
} else if (scanResult.scanType == 'QR_CODE') {
// 修复:移除多余的引号
deviceNo = getQueryString(scanResult.result, 'deviceNo');
console.log('二维码扫描模式,提取设备号:', deviceNo);
} else {
deviceNo = getQueryString(scanResult.path || scanResult.result, 'deviceNo')
deviceNo = getQueryString(scanResult.path || scanResult.result, 'deviceNo');
console.log('其他模式,提取设备号:', deviceNo);
}
console.log('最终设备号:', deviceNo);
if (!deviceNo) {
console.warn('未能提取到设备号');
uni.showToast({
title: t('home.invalidQRCode'),
icon: 'none'
})
return
});
// 关闭扫码页面
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
}
return;
}
// 检查是否有使用中的订单
@@ -1011,10 +1029,24 @@
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
const inUseOrder = inUseRes.data
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
});
}, 100);
}
});
} else {
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
});
}
return;
}
// 检查是否有待支付订单
@@ -1022,9 +1054,23 @@
if (orderRes && orderRes.code === 200 && orderRes.data) {
const unpaidOrder = orderRes.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
});
}, 100);
}
});
} else {
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
});
}
} else {
try {
const deviceInfoRes = await getDeviceInfo(deviceNo)
@@ -1032,40 +1078,79 @@
if (deviceInfoRes.code == 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
const closeScanPageAndNavigate = (url) => {
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.navigateTo({ url });
}, 100);
}
});
} else {
uni.navigateTo({ url });
}
};
if (deviceInfo.feeConfig) {
try {
const feeConfig = JSON.parse(deviceInfo.feeConfig)
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
})
closeScanPageAndNavigate(`/pages/device/detail?deviceNo=${deviceNo}&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`);
} catch (e) {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
closeScanPageAndNavigate(`/pages/device/detail?deviceNo=${deviceNo}`);
}
} else {
closeScanPageAndNavigate(`/pages/device/detail?deviceNo=${deviceNo}`);
}
} else {
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
});
}, 100);
}
});
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
});
}
} else {
// uni.showToast({
// title: t('device.getDeviceInfoFailed'),
// icon: 'none'
// })
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('获取设备信息异常:', error)
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
});
}, 100);
}
});
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
});
}
}
}
} catch (error) {
console.error('处理扫码结果失败:', error)
// 关闭扫码页面
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack();
}
}
}
+135
View File
@@ -0,0 +1,135 @@
<template>
<view class="terms-page">
<view class="content">
<view class="title">{{ $t('legal.termsAndConditions') }}</view>
<view class="section">
<view class="section-title">{{ $t('legal.applicableLaw') }}</view>
<view class="section-content">
<text>{{ $t('legal.applicableLawContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.paymentMethods') }}</view>
<view class="section-content">
<text>{{ $t('legal.paymentMethodsContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.refundPolicy') }}</view>
<view class="section-content">
<text>{{ $t('legal.refundPolicyContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.serviceTerms') }}</view>
<view class="section-content">
<text>{{ $t('legal.serviceTermsContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.liabilityLimitation') }}</view>
<view class="section-content">
<text>{{ $t('legal.liabilityLimitationContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.disputeResolution') }}</view>
<view class="section-content">
<text>{{ $t('legal.disputeResolutionContent') }}</text>
</view>
</view>
<view class="footer">
<text class="update-time">{{ $t('legal.lastUpdate') }}: 2025-02-05</text>
<text class="notice">{{ $t('legal.footerNotice') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
onMounted(() => {
uni.setNavigationBarTitle({
title: t('legal.termsAndConditions')
})
})
</script>
<style lang="scss" scoped>
.terms-page {
min-height: 100vh;
background-color: #f6f6f6;
padding-bottom: env(safe-area-inset-bottom);
}
.content {
background-color: #ffffff;
padding: 40rpx 30rpx;
margin: 20rpx;
border-radius: 16rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #333333;
text-align: center;
margin-bottom: 40rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 20rpx;
padding-left: 20rpx;
border-left: 6rpx solid #07c160;
}
.section-content {
font-size: 28rpx;
color: #666666;
line-height: 1.8;
text-align: justify;
padding: 20rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
.footer {
margin-top: 60rpx;
padding-top: 30rpx;
border-top: 1rpx solid #e5e5e5;
text-align: center;
}
.update-time {
display: block;
font-size: 24rpx;
color: #999999;
margin-bottom: 20rpx;
}
.notice {
display: block;
font-size: 26rpx;
color: #666666;
line-height: 1.6;
}
</style>
+201 -231
View File
@@ -1,19 +1,23 @@
<template>
<view class="payment-container">
<!-- 订单状态 -->
<view class="status-card">
<view class="status-icon-wrapper">
<view class="status-icon" :class="orderStatus.class">
<text class="icon-text">💳</text>
</view>
<!-- 地点信息卡片 -->
<view class="location-card">
<view class="location-header">
<!-- <view class="location-icon">📍</view> -->
<image src="@/static/device_location.png" mode="aspectFit" class="location-icon"></image>
<text class="location-name">{{ locationName }}</text>
<view class="status-badge">{{ $t('device.available') }}</view>
</view>
<view class="device-info">
<text class="device-label">{{ $t('order.deviceNo') }}</text>
<text class="device-value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="status-text">{{ orderStatus.text }}</view>
<view class="status-desc">{{ orderStatus.desc }}</view>
</view>
<!-- 订单信息 -->
<!-- 订单信息和费用信息 -->
<view class="order-card">
<view class="card-header">
<view class="card-title-bar"></view>
<view class="card-title">{{ $t('payment.orderInfo') }}</view>
</view>
<view class="info-item">
@@ -28,52 +32,35 @@
<text class="label">{{ $t('payment.createTime') }}</text>
<text class="value">{{ orderInfo.createTime || '-' }}</text>
</view>
</view>
<!-- 费用信息 -->
<view class="price-card">
<view class="card-header">
<!-- 费用信息部分 -->
<view class="card-header" style="margin-top: 32rpx;">
<view class="card-title-bar"></view>
<view class="card-title">{{ $t('payment.feeInfo') }}</view>
</view>
<view class="price-item">
<text class="label">{{ $t('payment.deposit') }}</text>
<text class="value">{{ orderInfo.deposit || '99.00' }}</text>
<text class="value">¥ {{ orderInfo.deposit || '90' }}</text>
</view>
<!-- <view class="price-divider"></view> -->
<!-- <view class="price-item">
<text class="label">{{ $t('payment.package') }}</text>
<text class="value">{{ packageText }}</text>
</view> -->
<view class="price-item total">
<text class="label">{{ $t('payment.total') }}</text>
<text class="value">{{ totalAmount }}</text>
</view>
</view>
<!-- 支付方式选择 -->
<view class="payment-methods" v-if="paymentMethods.length > 0">
<view class="card-header">
<view class="card-title">支付方式</view>
</view>
<view class="method-item" v-for="method in paymentMethods" :key="method?.paymentMethodType"
:class="{ active: selectedPaymentMethod === method?.paymentMethodType }"
@click="selectPaymentMethod(method?.paymentMethodType)">
<view class="method-info">
<view class="method-icon">
<image :src="method?.logo?.logoUrl" mode="aspectFit" style="width: 48rpx; height: 48rpx;">
</image>
</view>
<text class="method-name">{{ method?.logo?.logoName }}</text>
</view>
<view class="method-radio" :class="{ checked: selectedPaymentMethod === method?.paymentMethodType }">
<view class="total-value">
<text class="currency">¥</text>
<text class="amount">{{ totalAmount }}</text>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="total-amount">
<text class="label-text">{{ $t('payment.total') }}</text>
<text class="amount">{{ totalAmount }}</text>
</view>
<view class="pay-btn" @click="handlePayment">
<text>{{ $t('payment.payNow') }}</text>
<text class="currency-small">¥</text>
<text class="amount-large">{{ totalAmount }}</text>
<text class="pay-text">{{ $t('payment.payNow') }}</text>
</view>
</view>
</view>
@@ -116,10 +103,23 @@
const passedTotalAmount = ref(null)
const passedDepositAmount = ref(null)
// 倒计时相关
const countdown = ref(15 * 60) // 15分钟 = 900秒
let countdownTimer = null
// 支付方式相关
const paymentMethods = ref([])
const selectedPaymentMethod = ref('ALIPAY') // 默认选择支付宝
// 地点名称(可以从设备信息中获取,这里先用默认值)
const locationName = ref('澎创办公室')
// 套餐文本(可以从设备信息中获取,这里先用默认值)
const packageText = computed(() => {
// 这里可以根据实际的设备信息动态生成
return '2元/小时'
})
const orderStatus = reactive({
get text() {
return t('payment.waitingForPayment')
@@ -191,11 +191,17 @@
deviceNo: orderData.deviceNo,
createTime: formattedTime,
deposit: passedDepositAmount.value || orderData.depositAmount || '99.00',
orderStatus:orderData.orderStatus
}
deviceNo.value = orderData.deviceNo;
await loadDeviceInfo();
await loadPaymentMethods();
// #ifdef H5
if(orderInfo.value.orderStatus=='waiting_for_payment'){
startPaymentStatusPolling();
}
// #endif
} else {
throw new Error(t('order.getOrderFailed'))
}
@@ -322,12 +328,12 @@
uni.hideLoading();
// #ifdef H5
uni.setStorageSync('pendingPaymentNo', orderId.value);
// #endif
// 跳转到支付页面
uni.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(paymentUrl)}&title=支付`
});
// uni.navigateTo({
// url: `/pages/webview/index?url=${encodeURIComponent(paymentUrl)}&title=支付`
// });
window.open(paymentUrl);
// #endif
// 开始轮询支付状态
startPaymentStatusPolling();
@@ -409,12 +415,56 @@
}, 5000); // 每5秒查询一次
}
// 更新导航栏倒计时
const updateNavBarCountdown = () => {
const minutes = Math.floor(countdown.value / 60).toString().padStart(2, '0')
const seconds = (countdown.value % 60).toString().padStart(2, '0')
uni.setNavigationBarTitle({
title: `待支付 ${minutes}:${seconds}`
})
}
// 开始倒计时
const startCountdown = () => {
// 清除之前的定时器
if (countdownTimer) {
clearInterval(countdownTimer)
}
// 立即更新一次
updateNavBarCountdown()
// 每秒更新
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(countdownTimer)
uni.showToast({
title: t('order.paymentFailedRetry'),
icon: 'none'
})
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
})
}, 1500)
return
}
updateNavBarCountdown()
}, 1000)
}
// 页面卸载时清除定时器
onMounted(() => {
return () => {
if (pollingTimer) {
clearInterval(pollingTimer);
}
if (countdownTimer) {
clearInterval(countdownTimer);
}
};
});
@@ -430,9 +480,8 @@
}
onLoad((options) => {
uni.setNavigationBarTitle({
title: t('payment.orderPayment')
})
// 启动倒计时
startCountdown()
// 优先从 options 中获取 orderId
if (options && options.orderId) {
@@ -470,7 +519,7 @@
}
}
// #endif
loadOrderInfo()
// 调用 loadOrderInfo,内部会再次检查 orderId 是否存在
})
@@ -479,78 +528,81 @@
<style lang="scss" scoped>
.payment-container {
min-height: 100vh;
background: #f7f8fa;
padding: 30rpx;
padding-bottom: 180rpx;
background: #F5F5F5;
padding: 24rpx;
padding-bottom: 200rpx;
box-sizing: border-box;
.status-card {
.location-card {
background: #fff;
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.status-icon-wrapper {
margin-bottom: 20rpx;
.location-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.status-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.location-icon {
font-size: 40rpx;
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
}
.icon-text {
font-size: 60rpx;
}
.location-name {
font-size: 36rpx;
font-weight: 600;
color: #333;
flex: 1;
}
&.waiting {
background: #FFF9C4;
}
&.success {
background: #E8F5E9;
}
&.failed {
background: #FFEBEE;
}
.status-badge {
background: #D4F4DD;
color: #52C41A;
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 8rpx;
}
}
.status-text {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.status-desc {
.device-info {
font-size: 28rpx;
color: #999;
color: #666;
.device-label {
color: #999;
}
.device-value {
color: #333;
}
}
}
.order-card,
.price-card,
.payment-methods {
.order-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.card-header {
margin-bottom: 24rpx;
display: flex;
align-items: center;
margin-bottom: 32rpx;
.card-title-bar {
width: 8rpx;
height: 32rpx;
background: #52C41A;
border-radius: 4rpx;
margin-right: 12rpx;
}
.card-title {
font-size: 32rpx;
font-weight: bold;
font-weight: 600;
color: #333;
}
}
@@ -560,16 +612,11 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
padding: 24rpx 0;
.label {
font-size: 28rpx;
color: #999;
color: #666;
}
.value {
@@ -579,108 +626,34 @@
}
}
.price-divider {
height: 1rpx;
background: #f0f0f0;
margin: 10rpx 0;
}
.price-item.total {
margin-top: 10rpx;
padding-top: 20rpx;
border-top: none;
padding-top: 32rpx;
justify-content: flex-end !important;
// border-top: 1rpx solid #F0F0F0;
margin-top: 16rpx;
.label {
font-size: 28rpx;
color: #999;
color: #666;
margin-right: 10rpx;
}
.value {
font-size: 48rpx;
font-weight: bold;
color: #ff6b6b;
}
}
}
.payment-methods {
.method-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 20rpx;
margin-bottom: 16rpx;
background: #f7f8fa;
border-radius: 12rpx;
border: 2rpx solid transparent;
&:last-child {
margin-bottom: 0;
}
&.active {
background: #E8F5E9;
border-color: #07c160;
}
.method-info {
.total-value {
display: flex;
align-items: center;
align-items: baseline;
.method-icon {
width: 48rpx;
height: 48rpx;
margin-right: 16rpx;
border-radius: 8rpx;
&.alipay {
background: #1677FF;
}
&.wechat {
background: #07C160;
}
&.default {
background: #999;
}
.currency {
font-size: 22rpx;
color: #52C41A;
margin-right: 4rpx;
}
.method-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
.amount {
font-size: 32rpx;
font-weight: 700;
color: #52C41A;
}
}
.method-radio {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #ddd;
border-radius: 50%;
position: relative;
&.checked {
border-color: #07c160;
background: #07c160;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16rpx;
height: 16rpx;
background: #fff;
border-radius: 50%;
}
}
}
&:active {
opacity: 0.8;
}
}
}
@@ -690,47 +663,44 @@
right: 0;
bottom: 0;
background: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
z-index: 10;
gap: 20rpx;
.total-amount {
display: flex;
align-items: baseline;
.label-text {
font-size: 28rpx;
color: #666;
}
.amount {
font-size: 36rpx;
font-weight: bold;
color: #ff6b6b;
margin-left: 8rpx;
}
}
padding: 24rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 100;
.pay-btn {
height: 88rpx;
width: 100%;
padding: 20rpx 0;
// height: 96rpx;
background: linear-gradient(135deg, #52C41A 0%, #73D13D 100%);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
background: #07c160;
color: #fff;
font-size: 30rpx;
font-weight: 600;
padding: 0 60rpx;
border-radius: 44rpx;
border: none;
box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.3);
.currency-small {
font-size: 32rpx;
font-weight: 600;
margin-right: 4rpx;
// text-align: ;
}
.amount-large {
font-size: 52rpx;
font-weight: 700;
margin-right: 16rpx;
}
.pay-text {
font-size: 32rpx;
font-weight: 600;
}
&:active {
opacity: 0.8;
opacity: 0.9;
transform: scale(0.98);
}
}
}
+145 -96
View File
@@ -1,30 +1,36 @@
<template>
<view class="success-container">
<!-- 支付成功状态 -->
<view class="status-card">
<view class="status-icon success"></view>
<view class="status-text">{{ $t('success.paymentSuccess') }}</view>
<view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</view>
</view>
<!-- 支付成功状态和订单信息 -->
<view class="status-order-card">
<!-- 支付成功状态 -->
<view class="status-section">
<view class="status-icon success"></view>
<view class="status-text">{{ $t('success.paymentSuccess') }}</view>
<view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</view>
</view>
<!-- 订单信息 -->
<view class="order-card">
<view class="card-title">{{ $t('success.orderInfo') }}</view>
<view class="info-item">
<text class="label">{{ $t('order.orderNo') }}</text>
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('order.deviceNo') }}</text>
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('success.paymentAmount') }}</text>
<text class="value">{{ orderInfo.amount || '0.00' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('success.paymentTime') }}</text>
<text class="value">{{ orderInfo.payTime || '-' }}</text>
<!-- 分割线 -->
<view class="section-divider"></view>
<!-- 订单信息 -->
<view class="order-section">
<view class="card-title">{{ $t('success.orderInfo') }}</view>
<view class="info-item">
<text class="label">{{ $t('order.orderNo') }}</text>
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('order.deviceNo') }}</text>
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('success.paymentAmount') }}</text>
<text class="value">{{ orderInfo.amount || '0.00' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('success.paymentTime') }}</text>
<text class="value">{{ orderInfo.payTime || '-' }}</text>
</view>
</view>
</view>
@@ -38,8 +44,8 @@
<!-- 操作按钮 -->
<view class="button-group">
<button class="primary-btn" @click="goToHome">{{ $t('success.backToHome') }}</button>
<button class="secondary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</button>
<view class="secondary-btn" @click="goToHome">{{ $t('success.backToHome') }}</view>
<view class="primary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</view>
</view>
</view>
</template>
@@ -196,81 +202,109 @@
<style lang="scss" scoped>
.success-container {
padding: 20px;
padding-bottom: 180rpx;
background-color: #f5f5f5;
min-height: 100vh;
box-sizing: border-box;
}
.status-card {
.status-order-card {
background-color: #fff;
border-radius: 12px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
overflow: hidden;
.status-icon {
width: 60px;
height: 60px;
margin: 0 auto 16px;
background-color: #07c160;
border-radius: 50%;
position: relative;
.status-section {
padding: 30px;
text-align: center;
&::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 20px;
border: 3px solid #fff;
border-top: none;
border-right: none;
transform-origin: center;
transform: translate(-50%, -70%) rotate(-45deg);
.status-icon {
width: 60px;
height: 60px;
margin: 0 auto 16px;
background-color: #07c160;
border-radius: 50%;
position: relative;
&::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 20px;
border: 3px solid #fff;
border-top: none;
border-right: none;
transform-origin: center;
transform: translate(-50%, -70%) rotate(-45deg);
}
}
}
.status-text {
font-size: 24px;
font-weight: bold;
color: #07c160;
margin-bottom: 8px;
}
.status-text {
font-size: 24px;
font-weight: bold;
color: #07c160;
margin-bottom: 8px;
}
.status-desc {
font-size: 14px;
color: #666;
}
}
.order-card {
background-color: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
.card-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.label {
.status-desc {
font-size: 14px;
color: #666;
font-size: 14px;
}
}
.section-divider {
height: 1px;
background-color: #f0f0f0;
margin: 0 20px;
}
.order-section {
padding: 20px;
.card-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
padding-left: 12px;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #07c160;
border-radius: 2px;
}
}
.value {
color: #333;
font-size: 14px;
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.label {
color: #666;
font-size: 14px;
}
.value {
color: #333;
font-size: 14px;
font-weight: 500;
}
}
}
}
@@ -316,18 +350,30 @@
}
.button-group {
margin-top: 30px;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
z-index: 10;
display: flex;
// flex-direction: column;
gap: 16px;
justify-content: flex-end;
align-items: center;
gap: 20rpx;
.primary-btn {
background-color: #07c160;
color: #fff;
border: none;
border-radius: 24px;
padding: 12px;
font-size: 16px;
border-radius: 32rpx;
padding: 0 32rpx;
font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active {
opacity: 0.8;
@@ -337,10 +383,13 @@
.secondary-btn {
background-color: #fff;
color: #07c160;
border: 1px solid #07c160;
border-radius: 24px;
padding: 12px;
font-size: 16px;
border: 2rpx solid #07c160;
border-radius: 32rpx;
padding: 0 32rpx;
font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active {
background-color: #f5f5f5;
+478 -182
View File
@@ -1,14 +1,14 @@
<template>
<view class="scan-page">
<!-- 扫码区域 -->
<!-- 扫码区域容器 -->
<view class="scan-window">
<video id="scanVideo" ref="videoRef" class="scan-video" autoplay playsinline muted></video>
<canvas id="scanCanvas" ref="canvasRef" class="hidden-canvas" style="display: none;"></canvas>
<!-- html5-qrcode 扫描器容器 -->
<view id="qr-reader" class="qr-reader"></view>
<!-- 扫描装饰 -->
<view class="scan-mask">
<view class="scan-frame">
<view class="scan-line"></view>
<view class="scan-line" v-if="scanning"></view>
<view class="corner top-left"></view>
<view class="corner top-right"></view>
<view class="corner bottom-left"></view>
@@ -21,18 +21,22 @@
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="action-item" @click="chooseImage">
<uv-icon name="photo" size="28" color="#fff"></uv-icon>
<view class="action-item" @click.stop="chooseImage">
<!-- <view class="action-icon">📷</view> -->
<uv-icon name="photo" size="24" color="#fff"></uv-icon>
<text>相册</text>
</view>
<view class="action-item" @click="toggleInput">
<uv-icon name="edit-pen" size="28" color="#fff"></uv-icon>
<view class="action-item" @click.stop="toggleInput">
<!-- <view class="action-icon"></view> -->
<uv-icon name="edit-pen" size="24" color="#fff"></uv-icon>
<text>手动输入</text>
</view>
<view class="action-item" @click="goBack">
<uv-icon name="arrow-left" size="28" color="#fff"></uv-icon>
<view class="action-item" @click.stop="goBack">
<!-- <view class="action-icon"></view> -->
<uv-icon name="arrow-left" size="24" color="#fff"></uv-icon>
<text>返回</text>
</view>
</view>
@@ -46,7 +50,6 @@
placeholder="请输入设备上的编号"
class="device-input"
type="text"
focus
/>
<view class="dialog-btns">
<button class="cancel-btn" @click="closeInput">取消</button>
@@ -59,234 +62,464 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { getQueryString } from '@/util/index.js';
import { getQueryString } from '../../util/index.js';
import { Html5Qrcode } from 'html5-qrcode';
const videoRef = ref(null);
const canvasRef = ref(null);
const inputPopup = ref(null);
const manualDeviceNo = ref('');
const tipText = ref('正在启动扫描...');
const tipText = ref('正在初始化...');
const scanning = ref(false);
const hasFlash = ref(false);
const flashOn = ref(false);
let stream = null;
let animationId = null;
// 动态加载解码库
const loadJsQR = () => {
return new Promise((resolve, reject) => {
if (window.jsQR) {
resolve(window.jsQR);
return;
}
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js';
script.onload = () => resolve(window.jsQR);
script.onerror = (e) => {
console.error('jsQR 加载失败:', e);
reject(new Error('解码组件加载失败,请检查网络'));
};
document.head.appendChild(script);
});
};
let html5QrCode = null;
let isProcessing = false; // 防止重复处理
// 初始化扫码
const initScan = async () => {
try {
// 1. 检查环境
tipText.value = '正在初始化...';
console.log('=== 开始初始化扫码 ===');
// 检查浏览器支持
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('您的浏览器不支持摄像头访问,请使用微信扫描或手动输入');
throw new Error('您的浏览器不支持摄像头访问');
}
// 等待 DOM 渲染
await new Promise(resolve => setTimeout(resolve, 500));
// 检查容器元素
const readerElement = document.getElementById('qr-reader');
if (!readerElement) {
throw new Error('扫码容器元素未找到');
}
console.log('✓ 扫码容器元素已找到');
// 创建 Html5Qrcode 实例
html5QrCode = new Html5Qrcode('qr-reader', {
verbose: false // 关闭详细日志
});
console.log('✓ Html5Qrcode 实例创建成功');
// 启动扫描
await startScanning();
} catch (err) {
console.error('❌ 初始化失败:', err);
handleInitError(err);
}
};
// 2. 加载解码库
const jsQR = await loadJsQR();
// 3. 启动摄像头 - 尝试逐步降低约束
let constraints = {
video: {
facingMode: 'environment',
width: { ideal: 1280 },
height: { ideal: 720 }
}
// 开始扫描
const startScanning = async () => {
try {
if (!html5QrCode) {
throw new Error('Html5Qrcode 实例不存在');
}
tipText.value = '正在启动摄像头...';
console.log('=== 开始启动扫描 ===');
console.log('html5QrCode 实例:', html5QrCode);
console.log('html5QrCode.start 方法:', typeof html5QrCode.start);
// 配置扫描参数
const config = {
fps: 10, // 每秒10帧
qrbox: 250, // 扫描框大小(正方形)
disableFlip: false
};
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (e) {
console.warn('尝试理想约束失败,降级请求:', e);
// 降级:仅请求视频,不限制分辨率和模式
stream = await navigator.mediaDevices.getUserMedia({ video: true });
}
console.log('扫描配置:', config);
console.log('准备调用 html5QrCode.start()...');
if (videoRef.value) {
const video = videoRef.value;
// 关键:在赋值 srcObject 之前先重置
video.pause();
video.srcObject = stream;
// 使用更稳健的事件监听
const startPlay = () => {
video.play().then(() => {
console.log('视频开始播放');
scanning.value = true;
tipText.value = '将二维码放入框内即可自动扫描';
tick(jsQR);
}).catch(e => {
console.error('视频播放 Promise 失败:', e);
// 如果是由于用户交互限制,提示用户点击
tipText.value = '请点击屏幕启动扫描';
});
// 启动扫描 - 使用 { facingMode: "environment" } 优先后置摄像头
const startResult = await html5QrCode.start(
{ facingMode: "environment" },
config,
onScanSuccess,
onScanFailure
);
console.log('start() 调用结果:', startResult);
scanning.value = true;
tipText.value = '将二维码放入框内扫描';
console.log('✅ 扫描已成功启动');
// 延迟隐藏默认UI
setTimeout(() => {
hideDefaultUI();
}, 200);
} catch (err) {
console.error('❌ 启动扫描失败:', err);
console.error('错误详情:', {
name: err.name,
message: err.message,
stack: err.stack
});
// 尝试使用前置摄像头
console.log('尝试使用前置摄像头...');
try {
const config = {
fps: 10,
qrbox: 250,
disableFlip: false
};
if (video.readyState >= 2) {
startPlay();
} else {
video.onloadedmetadata = startPlay;
await html5QrCode.start(
{ facingMode: "user" },
config,
onScanSuccess,
onScanFailure
);
scanning.value = true;
tipText.value = '将二维码放入框内扫描';
console.log('✅ 使用前置摄像头启动成功');
setTimeout(() => {
hideDefaultUI();
}, 200);
} catch (err2) {
console.error('❌ 前置摄像头也失败:', err2);
// 最后尝试:不指定摄像头
console.log('最后尝试:使用默认摄像头...');
try {
const config = {
fps: 10,
qrbox: 250,
disableFlip: false
};
// 获取摄像头列表
const cameras = await Html5Qrcode.getCameras();
console.log('可用摄像头:', cameras);
if (cameras && cameras.length > 0) {
const cameraId = cameras[0].id;
console.log('使用摄像头ID:', cameraId);
await html5QrCode.start(
cameraId,
config,
onScanSuccess,
onScanFailure
);
scanning.value = true;
tipText.value = '将二维码放入框内扫描';
console.log('✅ 使用默认摄像头启动成功');
setTimeout(() => {
hideDefaultUI();
}, 200);
} else {
throw new Error('未找到可用的摄像头');
}
} catch (err3) {
console.error('❌ 所有方式都失败:', err3);
throw err3;
}
}
}
};
// 隐藏默认UI
const hideDefaultUI = () => {
try {
const shaded = document.getElementById('qr-shaded-region');
if (shaded) {
shaded.style.border = 'none';
console.log('✓ 已隐藏默认边框');
}
} catch (err) {
console.error('摄像头初始化失败:', err);
let errMsg = '摄像头开启失败';
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
errMsg = '请授予摄像头访问权限后重试';
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
errMsg = '未找到可用的摄像头';
} else if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
errMsg = '非加密连接(HTTPS)无法开启摄像头';
}
tipText.value = errMsg;
uni.showModal({
title: '提示',
content: errMsg,
showCancel: false,
success: () => {
// 如果失败且无法恢复,引导手动输入
toggleInput();
}
});
console.warn('隐藏默认UI失败:', err);
}
};
const tick = (jsQR) => {
if (!scanning.value) return;
const video = videoRef.value;
const canvas = canvasRef.value;
// 扫描成功回调
const onScanSuccess = (decodedText, decodedResult) => {
// 防止重复处理
if (isProcessing) {
console.log('正在处理中,忽略重复扫码');
return;
}
if (video && video.readyState === video.HAVE_ENOUGH_DATA && canvas) {
const ctx = canvas.getContext('2d');
canvas.height = video.videoHeight;
canvas.width = video.videoWidth;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
if (!scanning.value) {
console.log('扫描已停止,忽略结果');
return;
}
isProcessing = true;
console.log('========== 扫码成功 ==========');
console.log('解码文本:', decodedText);
console.log('解码结果:', decodedResult);
console.log('==============================');
// 验证结果
if (!decodedText || decodedText.trim() === '') {
console.error('扫码结果为空');
isProcessing = false;
return;
}
const finalResult = decodedText.trim();
// 停止扫描
stopScan().then(() => {
// 震动反馈
if (navigator.vibrate) {
navigator.vibrate(200);
}
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert',
// 通过全局事件通知首页
uni.$emit('h5ScanSuccess', {
result: finalResult,
scanType: 'QR_CODE',
path: finalResult
});
if (code && code.data) {
console.log('扫码结果:', code.data);
handleSuccess(code.data);
return; // 停止循环
}
}
animationId = requestAnimationFrame(() => tick(jsQR));
};
const handleSuccess = (result) => {
stopScan();
// 通过全局事件通知首页
uni.$emit('h5ScanSuccess', {
result: result,
scanType: 'QR_CODE'
// 不要立即返回,等待首页处理完成
// 首页会在处理完成后自动关闭扫码页
console.log('扫码结果已发送,等待首页处理...');
});
};
// 扫描失败回调
const onScanFailure = (error) => {
// 正常情况,不需要处理
};
// 停止扫描
const stopScan = async () => {
if (!html5QrCode) {
return;
}
uni.navigateBack();
};
const stopScan = () => {
console.log('=== 停止扫描 ===');
scanning.value = false;
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
flashOn.value = false;
try {
const isScanning = html5QrCode.isScanning;
if (isScanning) {
await html5QrCode.stop();
console.log('✓ Html5Qrcode 已停止');
}
} catch (err) {
console.warn('停止扫描失败:', err);
}
};
const chooseImage = () => {
// 处理初始化错误
const handleInitError = (err) => {
console.error('处理初始化错误:', err);
let errMsg = '初始化失败';
let errDetail = '';
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
errMsg = '摄像头权限被拒绝';
errDetail = '请在浏览器设置中允许访问摄像头';
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
errMsg = '未找到可用的摄像头';
errDetail = '请确保设备有摄像头';
} else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
errMsg = '摄像头被占用';
errDetail = '请关闭其他使用摄像头的应用';
} else if (err.name === 'NotSupportedError') {
errMsg = '浏览器不支持';
errDetail = '请使用现代浏览器访问';
} else if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
errMsg = '需要 HTTPS 环境';
errDetail = '摄像头功能需要在安全环境下使用';
} else {
errMsg = err.message || '摄像头启动失败';
errDetail = '请尝试刷新页面或使用其他方式';
}
tipText.value = errMsg;
// 显示错误提示,提供备选方案
uni.showModal({
title: errMsg,
content: errDetail + '\n\n您可以:\n1. 从相册选择二维码图片\n2. 手动输入设备号',
showCancel: true,
cancelText: '返回',
confirmText: '手动输入',
success: (res) => {
if (res.confirm) {
toggleInput();
} else if (res.cancel) {
goBack();
}
}
});
};
// 从相册选择图片识别
const chooseImage = async () => {
// #ifdef H5
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
uni.showLoading({ title: '正在识别...' });
try {
// 先停止摄像头扫描
const wasScanning = scanning.value;
if (wasScanning) {
await stopScan();
console.log('已停止摄像头扫描,准备识别图片');
// 等待停止完成
await new Promise(resolve => setTimeout(resolve, 300));
}
if (!html5QrCode) {
html5QrCode = new Html5Qrcode('qr-reader', { verbose: false });
}
// 使用 html5-qrcode 扫描文件
const result = await html5QrCode.scanFile(file, true);
console.log(result);
uni.hideLoading();
if (result) {
console.log('图片识别成功:', result);
// 震动反馈
if (navigator.vibrate) {
navigator.vibrate(200);
}
// 通过全局事件通知首页
uni.$emit('h5ScanSuccess', {
result: result,
scanType: 'QR_CODE',
path: result
});
// 不要立即返回,等待首页处理完成
console.log('图片识别结果已发送,等待首页处理...');
} else {
uni.showToast({ title: '未识别到二维码', icon: 'none' });
// 识别失败,重新启动摄像头扫描
if (wasScanning) {
setTimeout(async () => {
await startScanning();
}, 500);
}
}
} catch (err) {
console.error('图片识别失败:', err);
uni.hideLoading();
uni.showToast({ title: '识别失败', icon: 'none' });
// 识别失败,重新启动摄像头扫描
setTimeout(async () => {
try {
await startScanning();
} catch (e) {
console.error('重新启动扫描失败:', e);
}
}, 500);
}
};
input.click();
// #endif
// #ifndef H5
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: async (res) => {
uni.showLoading({ title: '正在识别...' });
try {
const jsQR = await loadJsQR();
const img = new Image();
img.src = res.tempFilePaths[0];
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
uni.hideLoading();
if (code && code.data) {
handleSuccess(code.data);
} else {
uni.showToast({ title: '未识别到二维码', icon: 'none' });
}
};
} catch (e) {
uni.hideLoading();
uni.showToast({ title: '识别出错', icon: 'none' });
}
success: (res) => {
uni.showToast({ title: '该功能仅在H5环境可用', icon: 'none' });
}
});
// #endif
};
// 打开手动输入弹窗
const toggleInput = () => {
inputPopup.value.open();
if (inputPopup.value) {
inputPopup.value.open();
}
};
// 关闭手动输入弹窗
const closeInput = () => {
inputPopup.value.close();
if (inputPopup.value) {
inputPopup.value.close();
}
};
// 确认手动输入
const confirmManualInput = () => {
if (!manualDeviceNo.value) return;
const deviceNo = manualDeviceNo.value.trim();
let deviceNo = manualDeviceNo.value.trim();
if (deviceNo.includes('deviceNo=')) {
deviceNo = getQueryString(deviceNo, 'deviceNo') || deviceNo;
if (!deviceNo) {
uni.showToast({ title: '请输入设备号', icon: 'none' });
return;
}
closeInput();
stopScan();
// 处理可能包含 URL 的情况
let finalDeviceNo = deviceNo;
if (deviceNo.includes('deviceNo=')) {
finalDeviceNo = getQueryString(deviceNo, 'deviceNo') || deviceNo;
}
// 通过全局事件通知首页
uni.$emit('h5ScanSuccess', {
result: deviceNo,
result: finalDeviceNo,
scanType: 'MANUAL'
});
uni.navigateBack();
// 不要立即返回,等待首页处理完成
console.log('手动输入结果已发送,等待首页处理...');
};
// 返回
const goBack = () => {
stopScan();
uni.navigateBack();
};
onMounted(() => {
initScan();
console.log('扫码页面已挂载');
// 延迟初始化,确保 DOM 已渲染
setTimeout(() => {
console.log('开始初始化扫码');
initScan();
}, 500);
});
onUnmounted(() => {
stopScan();
console.log('扫码页面卸载,清理资源');
isProcessing = false;
if (html5QrCode) {
stopScan().then(() => {
html5QrCode.clear().catch(err => {
console.warn('清理 Html5Qrcode 实例失败:', err);
});
html5QrCode = null;
});
}
});
</script>
@@ -304,10 +537,31 @@ onUnmounted(() => {
height: 100%;
position: relative;
.scan-video {
.qr-reader {
width: 100%;
height: 100%;
object-fit: cover;
// 覆盖 html5-qrcode 默认样式
:deep(#qr-shaded-region) {
border: none !important; // 隐藏默认边框
background: transparent !important; // 透明背景
}
:deep(video) {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
display: block !important;
}
:deep(canvas) {
display: none !important;
}
// 隐藏 html5-qrcode 的所有内置 UI 元素
:deep(#qr-shaded-region > div) {
display: none !important;
}
}
}
@@ -317,17 +571,19 @@ onUnmounted(() => {
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 10;
background: rgba(0, 0, 0, 0.5); // 添加半透明遮罩
.scan-frame {
width: 500rpx;
height: 500rpx;
position: relative;
box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.4);
background: transparent; // 扫描区域透明
box-shadow: 0 0 0 2000px rgba(0, 0, 0, 0.5); // 使用 box-shadow 创建外部遮罩
.scan-line {
width: 100%;
@@ -336,6 +592,7 @@ onUnmounted(() => {
position: absolute;
top: 0;
animation: scanMove 3s linear infinite;
box-shadow: 0 0 10rpx #3EAB64;
}
.corner {
@@ -343,6 +600,7 @@ onUnmounted(() => {
width: 40rpx;
height: 40rpx;
border: 6rpx solid #3EAB64;
box-shadow: 0 0 10rpx rgba(62, 171, 100, 0.5);
}
.top-left { top: -2rpx; left: -2rpx; border-right: none; border-bottom: none; }
@@ -353,8 +611,10 @@ onUnmounted(() => {
}
@keyframes scanMove {
0% { top: 0; }
100% { top: 100%; }
0% { top: 0; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { top: 100%; opacity: 0; }
}
.scan-tip {
@@ -366,6 +626,10 @@ onUnmounted(() => {
color: #fff;
font-size: 28rpx;
padding: 0 40rpx;
line-height: 1.6;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
z-index: 11;
font-weight: 500;
}
.bottom-actions {
@@ -376,16 +640,36 @@ onUnmounted(() => {
display: flex;
justify-content: space-around;
padding: 0 60rpx;
z-index: 20;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
gap: 8rpx;
margin-bottom: 20rpx;
padding: 12rpx 16rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 16rpx;
backdrop-filter: blur(10rpx);
transition: all 0.3s ease;
min-width: 100rpx;
border: 1rpx solid rgba(255, 255, 255, 0.1);
.action-icon {
font-size: 40rpx;
line-height: 1;
}
text {
color: #fff;
font-size: 24rpx;
font-size: 22rpx;
white-space: nowrap;
}
&:active {
transform: scale(0.95);
background: rgba(62, 171, 100, 0.3);
}
}
}
@@ -401,6 +685,7 @@ onUnmounted(() => {
font-weight: 600;
text-align: center;
margin-bottom: 40rpx;
color: #333;
}
.device-input {
@@ -411,6 +696,12 @@ onUnmounted(() => {
font-size: 28rpx;
border: 1rpx solid #eee;
margin-bottom: 40rpx;
box-sizing: border-box;
&:focus {
border-color: #3EAB64;
background: #fff;
}
}
.dialog-btns {
@@ -426,6 +717,11 @@ onUnmounted(() => {
font-size: 28rpx;
border-radius: 40rpx;
border: none;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
}
.cancel-btn {
+23 -7
View File
@@ -18,6 +18,10 @@
<text class="label">{{ $t('user.privacyPolicy') }}</text>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="item" @click="navigateTo('/pages/legal/terms')">
<text class="label">{{ $t('legal.termsAndConditions') }}</text>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
</view>
<view class="group">
<view class="item" @click="handleLogout">
@@ -51,7 +55,13 @@ const currentLanguage = ref(uni.getStorageSync('language') || 'zh-CN')
// 当前语言文本显示
const currentLanguageText = computed(() => {
return currentLanguage.value === 'zh-CN' ? t('settings.chinese') : t('settings.english')
if (currentLanguage.value === 'zh-CN') {
return t('settings.chinese')
} else if (currentLanguage.value === 'id-ID') {
return t('settings.indonesian')
} else {
return t('settings.english')
}
})
const navigateTo = (url) => {
@@ -60,21 +70,27 @@ const navigateTo = (url) => {
// 显示语言选择器
const showLanguageSelector = () => {
const languages = [
{ code: 'zh-CN', label: t('settings.chinese') },
{ code: 'en-US', label: t('settings.english') },
{ code: 'id-ID', label: t('settings.indonesian') }
]
uni.showActionSheet({
itemList: [t('settings.chinese'), t('settings.english')],
itemList: languages.map(lang => lang.label),
success: (res) => {
const lang = res.tapIndex === 0 ? 'zh-CN' : 'en-US'
if (lang !== currentLanguage.value) {
const selectedLang = languages[res.tapIndex].code
if (selectedLang !== currentLanguage.value) {
// 1. 保存到缓存
uni.setStorageSync('language', lang)
uni.setStorageSync('language', selectedLang)
// 2. 立即更新 i18n 实例(重要!)
if (globalI18n) {
globalI18n.locale = lang
globalI18n.locale = selectedLang
}
// 3. 更新当前语言状态
currentLanguage.value = lang
currentLanguage.value = selectedLang
// 4. 提示用户
uni.showToast({