Files
consumer-front/src/components/door-panel/door-panel.vue
T

661 lines
16 KiB
Vue
Raw Normal View History

<template>
<view v-if="step" class="door-overlay">
<!-- 开门中 -->
<view v-if="step === 'opening'" class="door-overlay-content">
<view class="door-spinner">
<view class="spinner-dot" v-for="i in 3" :key="i"></view>
</view>
<text class="door-overlay-text">开门中...</text>
<text class="door-overlay-tip">请稍候正在为您打开柜门</text>
</view>
<!-- 已开门 -->
<view v-if="step === 'opened'" class="door-state-bg">
<view class="door-state-content">
<view class="door-state-icon-wrap">
<view class="door-state-ring"></view>
<view class="door-state-icon">
<view class="icon-door-left"></view>
<view class="icon-door-right"></view>
</view>
</view>
2026-07-13 17:27:06 +08:00
<text class="door-state-title">门锁已打开</text>
<text class="door-state-desc">请先拉开柜门取走商品后随手关门</text>
<view class="door-state-divider"></view>
<view class="door-steps">
<view class="step-item">
<view class="step-dot active">1</view>
<text class="step-text">取走商品</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-dot">2</view>
<text class="step-text">关闭柜门</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-dot">3</view>
<text class="step-text">完成订单</text>
</view>
</view>
2026-07-11 16:56:15 +08:00
<view class="door-btn-area">
<view class="door-btn-close" @click="handleClose">返回首页</view>
</view>
</view>
</view>
<!-- 已关门 -->
<view v-if="step === 'closed'" class="door-state-bg">
<view class="door-state-content">
<view class="door-state-icon-wrap">
<view class="door-state-ring warning-ring"></view>
<view class="door-state-icon">
<view class="icon-lock">
<view class="lock-body"></view>
<view class="lock-hole"></view>
</view>
</view>
</view>
<text class="door-state-title">柜门已关闭</text>
<text class="door-state-desc">订单处理中请稍候...</text>
<view class="door-state-divider"></view>
<view class="door-steps">
<view class="step-item">
<view class="step-dot done"></view>
<text class="step-text">取走商品</text>
</view>
<view class="step-line done"></view>
<view class="step-item">
<view class="step-dot active">2</view>
<text class="step-text">已关柜门</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-dot">3</view>
<text class="step-text">完成订单</text>
</view>
</view>
2026-07-11 16:56:15 +08:00
<view class="door-btn-area">
<view class="door-btn-close" @click="handleClose">返回首页</view>
</view>
</view>
</view>
<!-- 订单完成 -->
<view v-if="step === 'complete'" class="door-state-bg">
<view class="door-state-content door-complete-content">
<view class="door-state-icon-wrap">
<view class="door-state-ring success-ring"></view>
<view class="door-state-icon">
<view class="icon-check"></view>
</view>
</view>
<text class="door-state-title">订单已完成</text>
<text class="door-state-desc">感谢您的购买</text>
<!-- 订单信息 -->
<view class="door-order-card" v-if="orderInfo">
2026-07-11 17:17:34 +08:00
<!-- 商品列表 -->
<view class="door-product-list" v-if="orderInfo.products && orderInfo.products.length">
<view class="door-product-item" v-for="(p, idx) in orderInfo.products" :key="idx">
<image class="door-product-img" :src="p.image" mode="aspectFill" />
<view class="door-product-info">
<text class="door-product-name">{{ p.name }}</text>
<text class="door-product-price">¥{{ getDecimal(p.unit_price) }} x {{ p.qty }}</text>
</view>
</view>
</view>
<view class="door-order-row">
<text class="door-order-label">订单编号</text>
<text class="door-order-value">{{ orderInfo.order_no }}</text>
</view>
<view class="door-order-row">
<text class="door-order-label">商品数量</text>
2026-07-11 17:17:34 +08:00
<text class="door-order-value">{{ orderInfo.total_qty || '-' }} </text>
</view>
<view class="door-order-row total">
<text class="door-order-label">订单金额</text>
2026-07-11 17:17:34 +08:00
<text class="door-order-value door-price">¥{{ getDecimal(orderInfo.total_amount) }}</text>
</view>
</view>
2026-07-11 16:56:15 +08:00
<view class="door-btn-area">
<view class="door-btn-detail" @click="goOrderDetail">查看订单详情</view>
<view class="door-btn-close" @click="handleClose">返回首页</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { get, post } from '@/utils/request.js';
2026-07-15 17:02:07 +08:00
import { detectPlatform } from '@/utils/env.js';
export default {
props: {
deviceId: { type: String, default: '' },
userId: { type: String, default: '' }
},
data() {
return {
step: '', // '' | 'opening' | 'opened' | 'closed' | 'complete'
flowId: '',
orderInfo: null,
orderLoading: false,
cancelled: false
};
},
beforeDestroy() {
this.cancelled = true;
},
methods: {
// 外部调用:启动开门流程
2026-07-11 20:43:56 +08:00
async open(userId) {
if (this.step) return;
this.cancelled = false;
this.step = 'opening';
2026-07-11 20:43:56 +08:00
const uid = userId || this.userId;
try {
const res = await post('/device-scan/open', {
device_id: this.deviceId,
door_index: 0,
2026-07-15 17:02:07 +08:00
user_id: uid,
platform: detectPlatform()
});
const ret = res.data || {};
if (ret.code !== 0) {
this.step = '';
uni.showToast({ title: ret.message || '开门失败', icon: 'none' });
return;
}
const flowId = ret.data && ret.data.flow_id;
if (!flowId) {
this.step = '';
uni.showToast({ title: '未获取到开门流水号', icon: 'none' });
return;
}
this.flowId = flowId;
this.pollStatus(flowId);
} catch (e) {
this.step = '';
uni.showToast({ title: '网络请求失败', icon: 'none' });
}
},
// 轮询状态
async pollStatus(flowId) {
const maxRetries = 30;
const interval = 2000;
let remaining = maxRetries;
2026-07-13 17:27:06 +08:00
let lastStep = '';
while (remaining > 0) {
if (this.step === 'complete' || this.cancelled) return;
try {
const res = await get('/flow/getStatusLog', {
flow_id: flowId,
status: '101,103,201'
});
const ret = res.data || {};
if (ret.code === 0 && Array.isArray(ret.data) && ret.data.length > 0) {
const latestStatus = ret.data[0].status;
2026-07-13 17:27:06 +08:00
let newStep = '';
if (latestStatus === 101) {
2026-07-13 17:27:06 +08:00
newStep = 'opened';
} else if (latestStatus === 103) {
2026-07-13 17:27:06 +08:00
newStep = 'closed';
} else if (latestStatus === 201) {
this.step = 'complete';
2026-07-11 17:22:41 +08:00
this.fetchOrder(flowId);
return;
}
2026-07-13 17:27:06 +08:00
// 状态真正变化时才重置计数器
if (newStep && newStep !== lastStep) {
this.step = newStep;
lastStep = newStep;
remaining = maxRetries;
}
}
} catch (e) {
console.error('[door-panel] 轮询失败:', e);
}
remaining--;
await new Promise(resolve => setTimeout(resolve, interval));
}
// 轮询结束
if (this.step === 'opening') {
this.step = '';
uni.showToast({ title: '开门超时,请重试', icon: 'none' });
}
},
// 查询订单信息
2026-07-11 17:17:34 +08:00
async fetchOrder(orderId) {
if (!orderId || !this.userId) {
this.orderLoading = false;
return;
}
this.orderLoading = true;
try {
2026-07-11 17:17:34 +08:00
const res = await get('/user/orders', {
page: 1,
limit: 1,
filter: JSON.stringify({ user_id: this.userId, order_id: orderId })
});
const ret = res.data || {};
2026-07-11 17:17:34 +08:00
if (ret.code === 0 && Array.isArray(ret.data) && ret.data.length > 0) {
this.orderInfo = ret.data[0];
}
} catch (e) {
console.error('[door-panel] 获取订单信息失败:', e);
} finally {
this.orderLoading = false;
}
},
2026-07-11 17:17:34 +08:00
// MongoDB Decimal128 转换
getDecimal(val) {
if (val == null) return '0.00';
if (typeof val === 'object' && val.$numberDecimal) return val.$numberDecimal;
return String(val);
},
// 跳转订单详情
goOrderDetail() {
2026-07-11 16:56:15 +08:00
if (this.orderInfo) {
2026-07-11 17:17:34 +08:00
const orderId = this.orderInfo.order_id || '';
uni.setStorageSync('__order_detail_cache', this.orderInfo);
2026-07-11 16:56:15 +08:00
uni.navigateTo({ url: `/pages/order/detail?order_id=${orderId}` });
} else if (this.flowId) {
uni.navigateTo({ url: `/pages/order/detail?flow_id=${this.flowId}` });
} else {
uni.showToast({ title: '无法获取订单信息', icon: 'none' });
}
},
// 关闭遮罩
handleClose() {
this.step = '';
this.flowId = '';
this.orderInfo = null;
this.orderLoading = false;
this.$emit('close');
}
}
};
</script>
<style scoped>
/* ====== 开门中遮罩 ====== */
.door-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 3000;
display: flex;
align-items: center;
justify-content: center;
}
.door-overlay-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx;
}
.door-spinner {
display: flex;
gap: 16rpx;
margin-bottom: 40rpx;
}
.spinner-dot {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background: #fff;
animation: spinner-bounce 1.2s ease-in-out infinite;
}
.spinner-dot:nth-child(2) { animation-delay: 0.2s; }
.spinner-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes spinner-bounce {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-20rpx); opacity: 1; }
}
.door-overlay-text {
font-size: 36rpx;
color: #fff;
font-weight: bold;
margin-bottom: 16rpx;
}
.door-overlay-tip {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.7);
}
/* ====== 通用状态页背景 ====== */
.door-state-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
2026-07-13 17:27:06 +08:00
background: #fff;
z-index: 3000;
display: flex;
align-items: center;
justify-content: center;
}
.door-state-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 60rpx;
width: 100%;
}
.door-complete-content {
max-height: 85vh;
overflow-y: auto;
}
/* ====== 图标区域 ====== */
.door-state-icon-wrap {
position: relative;
width: 180rpx;
height: 180rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 48rpx;
2026-07-13 17:27:06 +08:00
/* 动效环 scale(1.6) 会超出容器,预留空间避免被裁剪 */
margin-top: 60rpx;
}
.door-state-ring {
position: absolute;
width: 180rpx;
height: 180rpx;
border-radius: 50%;
2026-07-13 17:27:06 +08:00
border: 4rpx solid rgba(41, 121, 255, 0.3);
animation: ring-pulse 2s ease-out infinite;
}
.warning-ring {
2026-07-13 17:27:06 +08:00
border-color: rgba(250, 173, 20, 0.3);
}
.success-ring {
2026-07-13 17:27:06 +08:00
border-color: rgba(82, 196, 26, 0.3);
}
@keyframes ring-pulse {
0% { transform: scale(0.8); opacity: 1; }
100% { transform: scale(1.6); opacity: 0; }
}
.door-state-icon {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
/* 柜门图标(已开门) */
.icon-door-left {
width: 28rpx;
height: 56rpx;
2026-07-13 17:27:06 +08:00
background: #2979ff;
border-radius: 6rpx;
transform: perspective(200rpx) rotateY(25deg);
animation: door-left-open 0.8s ease-out forwards;
}
.icon-door-right {
width: 28rpx;
height: 56rpx;
2026-07-13 17:27:06 +08:00
background: #2979ff;
border-radius: 6rpx;
transform: perspective(200rpx) rotateY(-25deg);
animation: door-right-open 0.8s ease-out forwards;
}
@keyframes door-left-open {
0% { transform: perspective(200rpx) rotateY(0deg); }
100% { transform: perspective(200rpx) rotateY(35deg); }
}
@keyframes door-right-open {
0% { transform: perspective(200rpx) rotateY(0deg); }
100% { transform: perspective(200rpx) rotateY(-35deg); }
}
/* 锁图标(已关门) */
.icon-lock {
position: relative;
width: 48rpx;
height: 56rpx;
}
.lock-body {
width: 48rpx;
height: 36rpx;
2026-07-13 17:27:06 +08:00
background: #faad14;
border-radius: 6rpx;
position: absolute;
bottom: 0;
}
.lock-hole {
width: 28rpx;
height: 24rpx;
2026-07-13 17:27:06 +08:00
border: 5rpx solid #faad14;
border-radius: 14rpx 14rpx 0 0;
border-bottom: none;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
}
/* 勾图标(订单完成) */
.icon-check {
width: 40rpx;
height: 24rpx;
2026-07-13 17:27:06 +08:00
border-left: 5rpx solid #52c41a;
border-bottom: 5rpx solid #52c41a;
transform: rotate(-45deg);
margin-top: -8rpx;
}
/* ====== 标题 ====== */
.door-state-title {
font-size: 44rpx;
font-weight: bold;
2026-07-13 17:27:06 +08:00
color: #333;
margin-bottom: 16rpx;
letter-spacing: 4rpx;
}
.door-state-desc {
font-size: 28rpx;
2026-07-13 17:27:06 +08:00
color: #999;
margin-bottom: 48rpx;
}
/* ====== 分割线 ====== */
.door-state-divider {
width: 80rpx;
height: 4rpx;
2026-07-13 17:27:06 +08:00
background: linear-gradient(90deg, transparent, #2979ff, transparent);
border-radius: 2rpx;
margin-bottom: 48rpx;
}
/* ====== 步骤条 ====== */
.door-steps {
display: flex;
align-items: center;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.step-dot {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
2026-07-13 17:27:06 +08:00
background: #f5f5f5;
border: 2rpx solid #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
2026-07-13 17:27:06 +08:00
color: #999;
}
.step-dot.active {
2026-07-13 17:27:06 +08:00
background: rgba(41, 121, 255, 0.1);
border-color: #2979ff;
color: #2979ff;
}
.step-dot.done {
2026-07-13 17:27:06 +08:00
background: rgba(41, 121, 255, 0.1);
border-color: #2979ff;
color: #2979ff;
font-size: 20rpx;
}
.step-text {
font-size: 22rpx;
2026-07-13 17:27:06 +08:00
color: #999;
white-space: nowrap;
}
.step-line {
width: 60rpx;
height: 2rpx;
2026-07-13 17:27:06 +08:00
background: #e0e0e0;
margin-bottom: 36rpx;
}
.step-line.done {
2026-07-13 17:27:06 +08:00
background: #2979ff;
}
/* ====== 订单卡片 ====== */
.door-order-card {
width: 100%;
2026-07-13 17:27:06 +08:00
background: #f8f8f8;
border: 1rpx solid #f0f0f0;
border-radius: 20rpx;
padding: 24rpx 30rpx;
margin-top: 16rpx;
}
2026-07-11 17:17:34 +08:00
/* 商品列表 */
.door-product-list {
margin-bottom: 12rpx;
}
.door-product-item {
display: flex;
align-items: center;
padding: 12rpx 0;
}
.door-product-item + .door-product-item {
2026-07-13 17:27:06 +08:00
border-top: 1rpx solid #f0f0f0;
2026-07-11 17:17:34 +08:00
}
.door-product-img {
width: 80rpx;
height: 80rpx;
border-radius: 10rpx;
2026-07-13 17:27:06 +08:00
background: #f0f0f0;
2026-07-11 17:17:34 +08:00
flex-shrink: 0;
}
.door-product-info {
flex: 1;
margin-left: 16rpx;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.door-product-name {
font-size: 26rpx;
2026-07-13 17:27:06 +08:00
color: #333;
2026-07-11 17:17:34 +08:00
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.door-product-price {
font-size: 24rpx;
2026-07-13 17:27:06 +08:00
color: #999;
2026-07-11 17:17:34 +08:00
}
.door-order-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
}
.door-order-row + .door-order-row {
2026-07-13 17:27:06 +08:00
border-top: 1rpx solid #f0f0f0;
}
.door-order-row.total {
2026-07-13 17:27:06 +08:00
border-top: 1rpx solid #e0e0e0;
margin-top: 8rpx;
padding-top: 20rpx;
}
.door-order-label {
font-size: 26rpx;
2026-07-13 17:27:06 +08:00
color: #999;
}
.door-order-value {
font-size: 26rpx;
2026-07-13 17:27:06 +08:00
color: #333;
}
.door-price {
font-size: 36rpx;
font-weight: bold;
2026-07-13 17:27:06 +08:00
color: #e4393c;
}
.door-order-loading {
font-size: 26rpx;
2026-07-13 17:27:06 +08:00
color: #999;
text-align: center;
padding: 16rpx 0;
}
2026-07-11 16:56:15 +08:00
/* ====== 按钮区域 ====== */
.door-btn-area {
margin-top: 64rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
/* ====== 查看订单详情按钮 ====== */
.door-btn-detail {
font-size: 30rpx;
color: #fff;
padding: 24rpx 80rpx;
background: linear-gradient(135deg, #2979ff, #1e60e0);
border-radius: 48rpx;
letter-spacing: 2rpx;
}
/* ====== 关闭按钮 ====== */
.door-btn-close {
font-size: 30rpx;
2026-07-13 17:27:06 +08:00
color: #2979ff;
padding: 24rpx 100rpx;
2026-07-13 17:27:06 +08:00
background: #fff;
border: 2rpx solid #2979ff;
border-radius: 48rpx;
letter-spacing: 2rpx;
}
</style>