| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595 |
- <template>
- <view class="container">
- <!-- 阶段一:选购中 (门已开) -->
- <view v-if="doorStatus === 'opened'" class="status-section animate-fade-in">
- <view class="status-icon-wrapper">
- <view class="status-icon door-open">
- <svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
- <rect x="20" y="10" width="80" height="100" rx="8" fill="#FFF8E1" stroke="#FFC107" stroke-width="3"/>
- <path d="M 35 10 L 35 110" stroke="#FFC107" stroke-width="2" stroke-dasharray="4,4"/>
- <circle cx="60" cy="60" r="15" fill="#FFC107" opacity="0.3"/>
- <path d="M 55 60 L 65 60 M 60 55 L 60 65" stroke="#FFC107" stroke-width="3" stroke-linecap="round"/>
- </svg>
- </view>
- <view class="status-icon-pulse"></view>
- </view>
- <view class="status-title">门已开,请选购商品</view>
- <view class="status-tip">请在60秒内完成选购并关门</view>
- <view class="countdown-wrapper">
- <view class="countdown" v-if="countdown > 0">
- <text class="countdown-number">{{ countdown }}</text>
- <text class="countdown-label">秒</text>
- </view>
- <view class="countdown warning" v-else>
- <text class="countdown-number">请尽快关门</text>
- </view>
- </view>
- <CustomerServiceButton
- mode="link"
- title="购物-门已开"
- :path="'/pages/shopping/shopping'"
- >
- 遇到问题?联系在线客服
- </CustomerServiceButton>
- </view>
-
- <!-- 阶段二:购物完成 (门已关) -->
- <view v-else-if="doorStatus === 'closing'" class="status-section animate-scale-in">
- <view class="status-icon-wrapper">
- <view class="status-icon success">
- <svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
- <circle cx="60" cy="60" r="50" fill="#E8F5E9" stroke="#4CAF50" stroke-width="3"/>
- <path d="M 35 60 L 50 75 L 85 40" stroke="#4CAF50" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </view>
- </view>
- <view class="status-title">购物已完成</view>
- <view class="status-tip">
- 订单稍后自动扣款,
- <CustomerServiceButton
- mode="link"
- title="购物-扣款疑问"
- :path="'/pages/shopping/shopping'"
- >
- 如有疑问请联系客服
- </CustomerServiceButton>
- </view>
- <view class="countdown-wrapper">
- <view class="countdown-return" v-if="returnCountdown > 0">
- <text class="countdown-return-text">{{ returnCountdown }}秒后返回首页</text>
- </view>
- </view>
- <button class="action-button" @click="goHome">
- <text class="button-text">回到首页</text>
- </button>
- </view>
-
- <!-- 阶段三:结算完成 -->
- <view v-else-if="doorStatus === 'closed'" class="status-section animate-slide-up">
- <view class="status-icon-wrapper">
- <view class="status-icon success">
- <svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
- <circle cx="60" cy="60" r="50" fill="#E8F5E9" stroke="#4CAF50" stroke-width="3"/>
- <path d="M 35 60 L 50 75 L 85 40" stroke="#4CAF50" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </view>
- </view>
- <view class="status-title">结算完成</view>
- <view class="status-tip">您已成功购买以下商品</view>
- <view class="purchase-info">
- <view class="product-item" v-for="product in purchasedProducts" :key="product.id">
- <view class="product-name">{{ product.name }}</view>
- <view class="product-quantity">×{{ product.quantity }}</view>
- <view class="product-price">¥{{ product.price }}</view>
- </view>
- <view class="total-info">
- <view class="total-label">总计</view>
- <view class="total-price">¥{{ totalPrice }}</view>
- </view>
- </view>
- <button class="action-button primary" @click="goToOrderDetail">
- <text class="button-text">查看订单详情</text>
- </button>
- </view>
-
- <!-- 错误状态 -->
- <view v-else-if="doorStatus === 'error'" class="status-section animate-shake">
- <view class="status-icon-wrapper">
- <view class="status-icon error">
- <svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
- <circle cx="60" cy="60" r="50" fill="#FFEBEE" stroke="#F44336" stroke-width="3"/>
- <path d="M 40 40 L 80 80 M 80 40 L 40 80" stroke="#F44336" stroke-width="5" fill="none" stroke-linecap="round"/>
- </svg>
- </view>
- </view>
- <view class="status-title">出错了</view>
- <view class="status-tip">{{ errorMessage }}</view>
- <CustomerServiceButton
- mode="link"
- title="购物-错误求助"
- :path="'/pages/shopping/shopping'"
- >
- 联系客服解决
- </CustomerServiceButton>
- <button class="action-button" @click="goHome">
- <text class="button-text">返回首页</text>
- </button>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { ref, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue';
- import { pollDeviceStatus, pollRecognizeResult, pollOrderInfo } from '../../api/status';
- import type { RecognizeResultResponse } from '../../api/status';
- import { logger } from '../../utils/logger';
- import type { OrderProduct } from '../../api/order';
- import CustomerServiceButton from '../../components/CustomerServiceButton.vue';
- const doorStatus = ref<'opened' | 'closing' | 'closed' | 'error'>('opened');
- const countdown = ref(60);
- const returnCountdown = ref(5); // 返回首页倒计时
- const purchasedProducts = ref<OrderProduct[]>([]);
- const totalPrice = ref(0);
- const errorMessage = ref('');
- const currentDeviceId = ref('');
- const currentActivityId = ref('');
- const currentOrderNo = ref('');
- let countdownTimer: number | null = null;
- let returnCountdownTimer: number | null = null; // 返回首页倒计时定时器
- let statusCheckTimer: number | null = null;
- let isComponentActive = true; // 组件活跃状态标志
- // 统一的定时器清理函数
- const cleanupTimers = () => {
- if (countdownTimer) {
- clearInterval(countdownTimer);
- countdownTimer = null;
- }
- if (returnCountdownTimer) {
- clearInterval(returnCountdownTimer);
- returnCountdownTimer = null;
- }
- if (statusCheckTimer) {
- clearTimeout(statusCheckTimer);
- statusCheckTimer = null;
- }
- };
- onMounted(() => {
- // 标记当前页面轮询为活跃状态
- uni.setStorageSync('shoppingPollingActive', 'true');
- isComponentActive = true;
-
- // 获取存储的设备信息
- currentDeviceId.value = uni.getStorageSync('currentDeviceId') || '';
- currentOrderNo.value = uni.getStorageSync('currentOrderNo') || '';
-
- if (!currentDeviceId.value) {
- doorStatus.value = 'error';
- errorMessage.value = '未找到设备信息';
- return;
- }
-
- // 开始倒计时
- startCountdown();
-
- // 开始轮询设备状态
- startStatusPolling();
- });
- onUnmounted(() => {
- // 设置组件为非活跃状态,停止所有轮询
- isComponentActive = false;
- // 清理所有定时器
- cleanupTimers();
- });
- // 页面获得焦点时重新激活轮询
- onActivated(() => {
- if (doorStatus.value === 'opened' || doorStatus.value === 'closing') {
- isComponentActive = true;
- // 重新开始轮询
- startStatusPolling();
- }
- });
- // 页面失去焦点时暂停轮询
- onDeactivated(() => {
- isComponentActive = false;
- // 清理定时器
- cleanupTimers();
- });
- const startCountdown = () => {
- countdownTimer = setInterval(() => {
- if (countdown.value > 0) {
- countdown.value--;
- }
- }, 1000);
- };
- // 开始返回首页倒计时
- const startReturnCountdown = () => {
- returnCountdown.value = 5; // 重置为5秒
- returnCountdownTimer = setInterval(() => {
- if (returnCountdown.value > 0) {
- returnCountdown.value--;
- if (returnCountdown.value === 0) {
- // 倒计时结束,自动返回首页
- goHome();
- }
- }
- }, 1000);
- };
- const startStatusPolling = async () => {
- // 检查组件是否仍然活跃
- if (!isComponentActive) {
- return;
- }
-
- try {
- // 轮询设备状态,等待关门
- const status = await pollDeviceStatus(currentDeviceId.value, 120000, 2000);
-
- // 再次检查组件是否仍然活跃
- if (!isComponentActive) {
- return;
- }
-
- logger.log('设备状态更新:', status);
-
- // 检查门是否关闭
- if (status.doorStatus === 'close') {
- // 门已关,显示购物完成
- doorStatus.value = 'closing';
- currentActivityId.value = status.activityId;
-
- logger.log('门已关,购物完成,活动ID:', status.activityId);
-
- // 停止选购倒计时
- if (countdownTimer) {
- clearInterval(countdownTimer);
- countdownTimer = null;
- }
-
- // 开始返回首页倒计时
- startReturnCountdown();
- } else {
- // 继续等待关门,设置延迟后重新轮询
- if (isComponentActive && doorStatus.value === 'opened') {
- statusCheckTimer = setTimeout(() => {
- if (isComponentActive) {
- startStatusPolling();
- }
- }, 3000);
- }
- }
- } catch (error: any) {
- console.error('状态轮询失败:', error);
-
- // 再次检查组件是否仍然活跃
- if (!isComponentActive) {
- return;
- }
-
- // 如果还在开门状态,继续轮询
- if (doorStatus.value === 'opened' || doorStatus.value === 'closing') {
- statusCheckTimer = setTimeout(() => {
- if (isComponentActive) {
- startStatusPolling();
- }
- }, 3000);
- } else {
- doorStatus.value = 'error';
- errorMessage.value = error.message || '获取状态失败';
- }
- }
- };
- const showProblem = () => {
- uni.vibrateShort({ type: 'light' });
- uni.showActionSheet({
- itemList: ['辅助远程开门', '报修'],
- success: function (res) {
- if (res.tapIndex === 0) {
- uni.showToast({
- title: '正在远程开门...',
- icon: 'loading'
- });
- } else if (res.tapIndex === 1) {
- uni.vibrateShort({ type: 'medium' });
- uni.showToast({
- title: '已提交报修申请',
- icon: 'success'
- });
- }
- }
- });
- };
- const goToOrderDetail = () => {
- uni.vibrateShort({ type: 'light' });
- // 跳转到订单详情页
- uni.navigateTo({
- url: `/pages/orderDetail/orderDetail?orderNo=${currentOrderNo.value}`
- });
- };
- const goHome = () => {
- uni.vibrateShort({ type: 'light' });
- uni.reLaunch({
- url: '/pages/index/index'
- });
- };
- </script>
- <style lang="scss">
- .container {
- min-height: 100vh;
- background: linear-gradient(180deg, $color-bg-secondary 0%, $color-bg-primary 100%);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-start;
- padding: $spacing-xl;
- padding-top: 2vh;
- box-sizing: border-box;
- }
- .status-section {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-start;
- text-align: center;
- width: 100%;
- max-width: 600rpx;
- }
- /* ========== 状态图标 ========== */
- .status-icon-wrapper {
- position: relative;
- width: 200rpx;
- height: 200rpx;
- margin-bottom: $spacing-xl;
- animation: scaleIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
- }
- .status-icon {
- width: 100%;
- height: 100%;
-
- svg {
- width: 100%;
- height: 100%;
- }
-
- &.door-open {
- animation: doorOpen 1s $ease-out;
- }
-
- &.success {
- animation: successPop 0.6s $bounce;
- }
-
- &.error {
- animation: shake 0.5s $ease-in-out;
- }
- }
- .status-icon-pulse {
- position: absolute;
- top: 50%;
- left: 50%;
- width: 100%;
- height: 100%;
- border-radius: $radius-circle;
- background: $color-primary;
- transform: translate(-50%, -50%);
- animation: pulse 2s cubic-bezier(0.42, 0, 0.58, 1) infinite;
- opacity: 0.2;
- z-index: -1;
- }
- @keyframes doorOpen {
- 0% {
- transform: scale(0) rotate(-180deg);
- opacity: 0;
- }
- 100% {
- transform: scale(1) rotate(0);
- opacity: 1;
- }
- }
- @keyframes successPop {
- 0% {
- transform: scale(0);
- opacity: 0;
- }
- 50% {
- transform: scale(1.1);
- }
- 100% {
- transform: scale(1);
- opacity: 1;
- }
- }
- /* ========== 状态文本 ========== */
- .status-title {
- font-size: 48rpx;
- font-weight: 600;
- margin-bottom: $spacing-sm;
- color: $color-text-primary;
- animation: slideUp 0.8s cubic-bezier(0.25, 0.1, 0.25, 1);
- }
- .status-tip {
- font-size: 30rpx;
- color: $color-text-secondary;
- margin-bottom: $spacing-xl;
- animation: slideUp 0.9s cubic-bezier(0.25, 0.1, 0.25, 1);
- }
- /* ========== 倒计时 ========== */
- .countdown-wrapper {
- margin-bottom: $spacing-xl;
- animation: scaleIn 1s cubic-bezier(0.68, -0.55, 0.265, 1.55);
- }
- .countdown {
- display: flex;
- align-items: baseline;
- gap: 8rpx;
-
- &-number {
- font-size: 88rpx;
- font-weight: 700;
- color: $color-text-primary;
- font-variant-numeric: tabular-nums;
- }
-
- &-label {
- font-size: 32rpx;
- color: $color-text-secondary;
- }
-
- &.warning {
- .countdown-number {
- color: $color-error;
- animation: blink 1s infinite;
- }
- }
- }
- @keyframes blink {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
- }
- .countdown-return {
- padding: $spacing-md $spacing-lg;
- background: $color-bg-tertiary;
- border-radius: $radius-lg;
-
- &-text {
- font-size: 26rpx;
- color: $color-text-secondary;
- }
- }
- /* ========== 问题链接 ========== */
- .problem-link {
- font-size: 28rpx;
- color: $color-primary-dark;
- text-decoration: underline;
- margin-top: $spacing-lg;
- animation: fadeIn 1.2s cubic-bezier(0.25, 0.1, 0.25, 1);
- }
- /* ========== 商品列表 ========== */
- .purchase-info {
- width: 100%;
- background: $color-bg-primary;
- border-radius: $radius-lg;
- padding: $spacing-lg;
- margin: $spacing-xl 0;
- box-shadow: $shadow-sm;
- animation: slideUp 1s cubic-bezier(0.25, 0.1, 0.25, 1);
- }
- .product-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: $spacing-sm 0;
- border-bottom: 1rpx solid $color-border;
-
- &:last-child {
- border-bottom: none;
- }
- }
- .product-name {
- font-size: 28rpx;
- color: $color-text-primary;
- flex: 1;
- text-align: left;
- }
- .product-quantity {
- font-size: 26rpx;
- color: $color-text-secondary;
- margin: 0 $spacing-md;
- }
- .product-price {
- font-size: 28rpx;
- color: $color-text-primary;
- font-weight: 500;
- }
- .total-info {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: $spacing-md;
- margin-top: $spacing-md;
- border-top: 2rpx solid $color-border;
- }
- .total-label {
- font-size: 32rpx;
- font-weight: 500;
- color: $color-text-primary;
- }
- .total-price {
- font-size: 36rpx;
- font-weight: 600;
- color: $color-error;
- }
- /* ========== 操作按钮 ========== */
- .action-button {
- background: $color-bg-primary;
- color: $color-text-primary;
- border: 2rpx solid $color-border;
- padding: $spacing-md $spacing-xl;
- border-radius: $radius-xl;
- font-size: 28rpx;
- font-weight: 500;
- margin-top: $spacing-lg;
- transition: all $duration-fast $ease-out;
-
- &::after {
- border: none;
- }
-
- &:active {
- transform: scale(0.98);
- background: $color-bg-secondary;
- }
-
- &.primary {
- background: linear-gradient(135deg, $color-primary-light 0%, $color-primary 100%);
- border: none;
- box-shadow: $shadow-primary;
- color: $color-text-primary;
-
- &:active {
- box-shadow: 0 4rpx 16rpx rgba(255, 193, 7, 0.3);
- }
- }
-
- .button-text {
- font-size: 28rpx;
- font-weight: 500;
- }
- }
- </style>
|