Files
akmon/pages/mall/nfc/merchant/pos-cashier.uvue
2026-01-20 08:04:15 +08:00

858 lines
18 KiB
Plaintext

<template>
<view class="pos-cashier">
<!-- 顶部状态栏 -->
<view class="status-bar">
<view class="terminal-info">
<text class="terminal-id">终端: {{ terminalInfo.id }}</text>
<text class="merchant-name">{{ terminalInfo.merchantName }}</text>
</view>
<view class="status-indicators">
<view class="status-item" :class="{ 'online': nfcStatus }">
<text class="status-text">NFC</text>
</view>
<view class="status-item" :class="{ 'online': networkStatus }">
<text class="status-text">网络</text>
</view>
</view>
</view>
<!-- 商品清单 -->
<view class="order-list">
<view class="list-header">
<text class="header-title">商品清单</text>
<text class="item-count">{{ orderItems.length }}项</text>
</view>
<scroll-view scroll-y="true" class="items-scroll">
<view class="order-item" v-for="(item, index) in orderItems" :key="index">
<view class="item-info">
<text class="item-name">{{ item.name }}</text>
<text class="item-spec" v-if="item.spec">{{ item.spec }}</text>
</view>
<view class="item-quantity">
<button class="qty-btn" @click="decreaseQuantity(index)">-</button>
<text class="qty-text">{{ item.quantity }}</text>
<button class="qty-btn" @click="increaseQuantity(index)">+</button>
</view>
<view class="item-price">
<text class="unit-price">¥{{ item.price.toFixed(2) }}</text>
<text class="total-price">¥{{ (item.price * item.quantity).toFixed(2) }}</text>
</view>
<button class="remove-btn" @click="removeItem(index)">删除</button>
</view>
<view class="empty-state" v-if="orderItems.length === 0">
<image class="empty-icon" src="/static/icons/empty-cart.png" />
<text class="empty-text">暂无商品,请扫码或手动添加</text>
</view>
</scroll-view>
</view>
<!-- 商品操作区 -->
<view class="product-actions">
<button class="action-btn scan-btn" @click="scanProduct">
<image class="btn-icon" src="/static/icons/scan.png" />
<text class="btn-text">扫码添加</text>
</button>
<button class="action-btn search-btn" @click="searchProduct">
<image class="btn-icon" src="/static/icons/search.png" />
<text class="btn-text">搜索商品</text>
</button>
<button class="action-btn manual-btn" @click="manualAdd">
<image class="btn-icon" src="/static/icons/add.png" />
<text class="btn-text">手动添加</text>
</button>
</view>
<!-- 结算区域 -->
<view class="checkout-section">
<view class="total-info">
<view class="total-items">
<text class="total-label">共{{ totalQuantity }}件商品</text>
</view>
<view class="total-amount">
<text class="amount-label">总计:</text>
<text class="amount-value">¥{{ totalAmount.toFixed(2) }}</text>
</view>
</view>
<view class="payment-buttons">
<button class="payment-btn nfc-btn"
@click="processNFCPayment"
:disabled="orderItems.length === 0">
<image class="payment-icon" src="/static/icons/nfc.png" />
<text class="payment-text">NFC支付</text>
</button>
<button class="payment-btn qr-btn"
@click="processQRPayment"
:disabled="orderItems.length === 0">
<image class="payment-icon" src="/static/icons/qr.png" />
<text class="payment-text">扫码支付</text>
</button>
<button class="payment-btn cash-btn"
@click="processCashPayment"
:disabled="orderItems.length === 0">
<image class="payment-icon" src="/static/icons/cash.png" />
<text class="payment-text">现金支付</text>
</button>
</view>
<view class="quick-actions-row">
<button class="quick-btn" @click="clearOrder">清空订单</button>
<button class="quick-btn" @click="saveOrder">保存订单</button>
<button class="quick-btn" @click="printReceipt" :disabled="!lastTransaction">打印小票</button>
</view>
</view>
<!-- NFC支付等待界面 -->
<view class="nfc-waiting" v-if="isWaitingNFC">
<view class="waiting-content">
<view class="nfc-animation">
<image class="nfc-icon" src="/static/icons/nfc-large.png" />
<view class="ripple"></view>
<view class="ripple ripple-2"></view>
</view>
<text class="waiting-text">请顾客将校园卡或手机靠近读卡器</text>
<text class="amount-display">支付金额: ¥{{ totalAmount.toFixed(2) }}</text>
<button class="cancel-btn" @click="cancelNFCPayment">取消支付</button>
</view>
</view>
<!-- 支付成功界面 -->
<view class="payment-success" v-if="showPaymentSuccess">
<view class="success-content">
<image class="success-icon" src="/static/icons/success.png" />
<text class="success-text">支付成功</text>
<text class="success-amount">¥{{ lastTransaction.amount }}</text>
<view class="success-details">
<text class="detail-item">订单号: {{ lastTransaction.orderNo }}</text>
<text class="detail-item">支付方式: {{ lastTransaction.paymentMethod }}</text>
<text class="detail-item">交易时间: {{ formatTime(lastTransaction.time) }}</text>
</view>
<button class="continue-btn" @click="continueOrder">继续收银</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
terminalInfo: {
id: 'POS001',
merchantName: '第一饭堂'
},
nfcStatus: true,
networkStatus: true,
orderItems: [
{
name: '红烧肉',
spec: '份',
price: 12.00,
quantity: 1
},
{
name: '米饭',
spec: '份',
price: 3.50,
quantity: 2
}
],
isWaitingNFC: false,
showPaymentSuccess: false,
lastTransaction: null
}
},
computed: {
totalQuantity() {
return this.orderItems.reduce((sum, item) => sum + item.quantity, 0)
},
totalAmount() {
return this.orderItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
}
},
onLoad() {
this.initPOSSystem()
},
methods: {
initPOSSystem() {
// 初始化POS系统
this.checkNFCStatus()
this.checkNetworkStatus()
},
checkNFCStatus() {
// 检查NFC状态
// 这里可以调用硬件API检查NFC读卡器状态
setTimeout(() => {
this.nfcStatus = true
}, 1000)
},
checkNetworkStatus() {
// 检查网络状态
uni.getNetworkType({
success: (res) => {
this.networkStatus = res.networkType !== 'none'
}
})
},
scanProduct() {
// 扫码添加商品
uni.scanCode({
success: (res) => {
this.addProductByCode(res.result)
},
fail: () => {
uni.showToast({
title: '扫码失败',
icon: 'none'
})
}
})
},
searchProduct() {
// 搜索商品
uni.navigateTo({
url: '/pages/mall/nfc/merchant/product-search'
})
},
manualAdd() {
// 手动添加商品
uni.showModal({
title: '添加商品',
editable: true,
placeholderText: '输入商品名称',
success: (res) => {
if (res.confirm && res.content) {
this.addProductManually(res.content)
}
}
})
},
addProductByCode(code) {
// 根据条码添加商品
uni.request({
url: '/api/v1/merchant/product-by-code',
data: { code },
success: (res) => {
this.addItemToOrder(res.data)
},
fail: () => {
uni.showToast({
title: '商品不存在',
icon: 'none'
})
}
})
},
addProductManually(name) {
// 手动添加商品(需要输入价格)
uni.showModal({
title: '输入价格',
editable: true,
placeholderText: '0.00',
success: (res) => {
if (res.confirm && res.content) {
const price = parseFloat(res.content)
if (price > 0) {
this.addItemToOrder({
name,
price,
spec: '份'
})
}
}
}
})
},
addItemToOrder(product) {
const existingIndex = this.orderItems.findIndex(item => item.name === product.name)
if (existingIndex >= 0) {
this.orderItems[existingIndex].quantity++
} else {
this.orderItems.push({
...product,
quantity: 1
})
}
},
increaseQuantity(index) {
this.orderItems[index].quantity++
},
decreaseQuantity(index) {
if (this.orderItems[index].quantity > 1) {
this.orderItems[index].quantity--
}
},
removeItem(index) {
this.orderItems.splice(index, 1)
},
processNFCPayment() {
if (this.totalAmount <= 0) return
this.isWaitingNFC = true
this.startNFCListening()
},
startNFCListening() {
// 模拟NFC支付处理
setTimeout(() => {
const success = Math.random() > 0.1 // 90%成功率
if (success) {
this.completePayment('NFC支付')
} else {
this.isWaitingNFC = false
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
})
}
}, 3000)
},
cancelNFCPayment() {
this.isWaitingNFC = false
},
processQRPayment() {
// 二维码支付
uni.showModal({
title: '扫码支付',
content: '请顾客使用手机扫描支付二维码',
success: (res) => {
if (res.confirm) {
this.completePayment('扫码支付')
}
}
})
},
processCashPayment() {
// 现金支付
uni.showModal({
title: '现金支付',
content: `收款金额: ¥${this.totalAmount.toFixed(2)}`,
success: (res) => {
if (res.confirm) {
this.completePayment('现金支付')
}
}
})
},
completePayment(paymentMethod) {
this.isWaitingNFC = false
// 生成交易记录
this.lastTransaction = {
orderNo: this.generateOrderNo(),
amount: this.totalAmount.toFixed(2),
paymentMethod,
time: new Date(),
items: [...this.orderItems]
}
// 保存交易到服务器
this.saveTransaction()
// 显示支付成功界面
this.showPaymentSuccess = true
// 3秒后自动继续
setTimeout(() => {
this.continueOrder()
}, 3000)
},
saveTransaction() {
uni.request({
url: '/api/v1/merchant/save-transaction',
method: 'POST',
data: this.lastTransaction,
success: (res) => {
console.log('交易保存成功')
}
})
},
generateOrderNo() {
return 'ORD' + Date.now().toString()
},
continueOrder() {
this.showPaymentSuccess = false
this.clearOrder()
},
clearOrder() {
this.orderItems = []
},
saveOrder() {
// 保存订单(暂存)
const orderData = {
items: this.orderItems,
totalAmount: this.totalAmount,
timestamp: new Date()
}
uni.setStorage({
key: 'savedOrder',
data: orderData,
success: () => {
uni.showToast({
title: '订单已保存',
icon: 'success'
})
}
})
},
printReceipt() {
if (!this.lastTransaction) return
// 打印小票
uni.showToast({
title: '正在打印小票...',
icon: 'loading'
})
// 这里可以调用打印机API
setTimeout(() => {
uni.showToast({
title: '小票打印完成',
icon: 'success'
})
}, 2000)
},
formatTime(time) {
return time.toLocaleString()
}
}
}
</script>
<style>
.pos-cashier {
height: 100vh;
background-color: #f8f9fa;
display: flex;
flex-direction: column;
}
.status-bar {
background: white;
padding: 20rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #eee;
}
.terminal-info {
display: flex;
flex-direction: column;
}
.terminal-id {
font-size: 24rpx;
color: #666;
}
.merchant-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.status-indicators {
display: flex;
gap: 20rpx;
}
.status-item {
padding: 8rpx 16rpx;
border-radius: 8rpx;
background: #f0f0f0;
}
.status-item.online {
background: #d4edda;
color: #155724;
}
.status-text {
font-size: 20rpx;
}
.order-list {
flex: 1;
background: white;
margin: 20rpx;
border-radius: 16rpx;
padding: 30rpx;
display: flex;
flex-direction: column;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.item-count {
font-size: 24rpx;
color: #666;
}
.items-scroll {
flex: 1;
max-height: 500rpx;
}
.order-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.order-item:last-child {
border-bottom: none;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
}
.item-name {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.item-spec {
font-size: 24rpx;
color: #666;
}
.item-quantity {
display: flex;
align-items: center;
margin: 0 20rpx;
}
.qty-btn {
width: 60rpx;
height: 60rpx;
border: 1rpx solid #ddd;
background: white;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.qty-text {
margin: 0 20rpx;
font-size: 24rpx;
min-width: 40rpx;
text-align: center;
}
.item-price {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-right: 20rpx;
}
.unit-price {
font-size: 24rpx;
color: #666;
}
.total-price {
font-size: 28rpx;
color: #fd7e14;
font-weight: bold;
}
.remove-btn {
padding: 12rpx 20rpx;
background: #ff3b30;
color: white;
border: none;
border-radius: 8rpx;
font-size: 24rpx;
}
.empty-state {
text-align: center;
padding: 100rpx 0;
}
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
.product-actions {
display: flex;
padding: 0 20rpx;
gap: 20rpx;
}
.action-btn {
flex: 1;
background: white;
border: 1rpx solid #ddd;
border-radius: 12rpx;
padding: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.btn-icon {
width: 40rpx;
height: 40rpx;
margin-bottom: 12rpx;
}
.btn-text {
font-size: 24rpx;
color: #333;
}
.checkout-section {
background: white;
margin: 20rpx;
border-radius: 16rpx;
padding: 30rpx;
}
.total-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.total-items {
font-size: 24rpx;
color: #666;
}
.total-amount {
display: flex;
align-items: baseline;
}
.amount-label {
font-size: 28rpx;
color: #333;
margin-right: 16rpx;
}
.amount-value {
font-size: 48rpx;
color: #fd7e14;
font-weight: bold;
}
.payment-buttons {
display: flex;
gap: 20rpx;
margin-bottom: 20rpx;
}
.payment-btn {
flex: 1;
padding: 24rpx;
border-radius: 12rpx;
border: none;
display: flex;
flex-direction: column;
align-items: center;
}
.nfc-btn {
background: #007AFF;
color: white;
}
.qr-btn {
background: #28a745;
color: white;
}
.cash-btn {
background: #ffc107;
color: #333;
}
.payment-btn:disabled {
opacity: 0.5;
}
.payment-icon {
width: 32rpx;
height: 32rpx;
margin-bottom: 8rpx;
}
.payment-text {
font-size: 24rpx;
}
.quick-actions-row {
display: flex;
gap: 20rpx;
}
.quick-btn {
flex: 1;
padding: 16rpx;
background: #f8f9fa;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 24rpx;
color: #666;
}
.nfc-waiting, .payment-success {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.waiting-content, .success-content {
background: white;
border-radius: 20rpx;
padding: 60rpx;
text-align: center;
width: 600rpx;
}
.nfc-animation {
position: relative;
width: 150rpx;
height: 150rpx;
margin: 0 auto 40rpx;
}
.nfc-icon {
width: 100%;
height: 100%;
}
.ripple {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 4rpx solid #007AFF;
border-radius: 50%;
animation: ripple 2s infinite;
}
.ripple-2 {
animation-delay: 1s;
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
.waiting-text {
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.amount-display {
font-size: 48rpx;
color: #fd7e14;
font-weight: bold;
margin-bottom: 40rpx;
display: block;
}
.cancel-btn {
background: #ff3b30;
color: white;
border: none;
border-radius: 12rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
}
.success-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
}
.success-text {
font-size: 36rpx;
color: #28a745;
font-weight: bold;
margin-bottom: 20rpx;
display: block;
}
.success-amount {
font-size: 48rpx;
color: #fd7e14;
font-weight: bold;
margin-bottom: 30rpx;
display: block;
}
.success-details {
text-align: left;
margin-bottom: 40rpx;
}
.detail-item {
font-size: 24rpx;
color: #666;
margin-bottom: 12rpx;
display: block;
}
.continue-btn {
background: #007AFF;
color: white;
border: none;
border-radius: 12rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
width: 100%;
}
</style>