order-confirm.vue 9.55 KB
<template>
  <view class="page-shell order-confirm-page">
    <view class="page-blur"></view>
    <view class="page-blur-secondary"></view>
    <view class="page-blur-tertiary"></view>

    <view class="page-content subpage-content">
      <view class="subpage-nav">
        <view class="subpage-back" @tap="goBack">‹</view>
        <view class="subpage-title-wrap">
          <text class="subpage-title">确认订单</text>
          <text class="subpage-subtitle">支付后自动生成取件码,到自提门店出示核销即可自提</text>
        </view>
      </view>

      <view class="subpage-card">
        <view class="section-head">
          <text class="section-title">自提门店</text>
          <text class="section-link">到店自提</text>
        </view>
        <view v-if="stores.length" class="store-row">
          <view
            v-for="store in stores"
            :key="store.id || store.name"
            class="store-chip"
            :class="{ active: selectedStoreId && selectedStoreId === store.id }"
            @tap="selectStore(store)"
          >
            <text class="store-chip-name">{{ store.name }}</text>
            <text class="store-chip-addr">{{ store.address }}</text>
          </view>
        </view>
        <text v-if="storesLoading" class="store-tip">门店加载中…</text>
        <text v-else-if="!hasValidStore" class="store-tip">
          暂无可自提门店,请稍后重试或联系客服。
        </text>
      </view>

      <view class="product-card glass-card">
        <image v-if="product.coverImg" class="product-image" :src="product.coverImg" mode="aspectFill" />
        <view class="product-copy">
          <text class="product-name">{{ product.productName }}</text>
          <text class="product-spec">{{ selectedSku ? selectedSku.skuName : '默认规格' }} · {{ quantity }} 件</text>
          <text v-if="product.description" class="product-desc">{{ product.description }}</text>
          <text class="product-price">¥{{ unitPrice }}</text>
        </view>
      </view>

      <view class="subpage-card">
        <view class="form-item no-border">
          <text class="form-label">订单备注</text>
          <input v-model="remark" class="form-input" placeholder="可填写备注(选填)" placeholder-class="form-placeholder" />
        </view>
      </view>

      <view class="subpage-card">
        <view class="info-list">
          <view class="info-row">
            <text class="info-label">商品单价</text>
            <text class="info-value">¥{{ unitPrice }}</text>
          </view>
          <view class="info-row">
            <text class="info-label">购买数量</text>
            <text class="info-value">{{ quantity }}</text>
          </view>
          <view class="info-row">
            <text class="info-label">提货方式</text>
            <text class="info-value">到店自提</text>
          </view>
          <view class="info-row">
            <text class="info-label">实付金额</text>
            <text class="info-value accent">¥{{ totalAmount }}</text>
          </view>
        </view>
      </view>

      <view class="cta-row">
        <view class="secondary-button" @tap="goBack">返回商品</view>
        <view class="primary-button" :class="{ disabled: submitting }" @tap="submitOrder">提交并支付</view>
      </view>
    </view>
  </view>
</template>

<script>
import { getMallDetail, getPickupStores, createOrder, prepay, mockPay } from '@/api/mall'
import { STORES, DEFAULT_STORE_ID } from '@/utils/config'

export default {
  data() {
    return {
      productId: '',
      skuId: '',
      quantity: 1,
      product: {},
      selectedSku: null,
      stores: [],
      storesLoading: false,
      selectedStoreId: DEFAULT_STORE_ID || '',
      remark: '',
      submitting: false
    }
  },
  computed: {
    hasValidStore() {
      return this.stores.some(s => s.id)
    },
    unitPrice() {
      return this.selectedSku ? this.selectedSku.price : (this.product.price || 0)
    },
    totalAmount() {
      return (Number(this.unitPrice) * Number(this.quantity)).toFixed(2)
    }
  },
  onLoad(options) {
    this.productId = options.productId
    this.skuId = options.skuId || ''
    this.quantity = Number(options.quantity) || 1
    this.loadStores()
    this.loadDetail()
  },
  methods: {
    // 拉取真实自提门店;失败时回退到 config.js 占位列表
    loadStores() {
      this.storesLoading = true
      getPickupStores({})
        .then((list) => {
          const stores = Array.isArray(list) ? list : []
          this.stores = stores.length ? stores : STORES
        })
        .catch(() => {
          this.stores = STORES
        })
        .then(() => {
          this.storesLoading = false
          if (!this.selectedStoreId) {
            const firstValid = this.stores.find(s => s.id)
            if (firstValid) this.selectedStoreId = firstValid.id
          }
        })
    },
    loadDetail() {
      getMallDetail(this.productId)
        .then((data) => {
          this.product = data || {}
          const skus = (data && data.skus) || []
          if (this.skuId) {
            this.selectedSku = skus.find(s => String(s.id) === String(this.skuId)) || null
          }
        })
        .catch(() => {})
    },
    selectStore(store) {
      if (!store.id) {
        uni.showToast({ title: '该门店未配置真实 Id', icon: 'none' })
        return
      }
      this.selectedStoreId = store.id
    },
    goBack() {
      uni.navigateBack({ fail: () => uni.redirectTo({ url: `/packageMall/product-detail/product-detail?id=${this.productId}` }) })
    },
    submitOrder() {
      if (this.submitting) return
      if (!this.selectedStoreId) {
        uni.showToast({ title: '请选择自提门店', icon: 'none' })
        return
      }
      this.submitting = true
      uni.showLoading({ title: '提交中', mask: true })

      const item = { ProductId: this.productId, Quantity: this.quantity }
      if (this.skuId) item.SkuId = this.skuId

      createOrder({
        StoreId: this.selectedStoreId,
        Remark: this.remark,
        Items: [item]
      })
        .then((order) => {
          const orderId = order && order.orderId
          if (!orderId) throw new Error('下单失败')
          return this.payOrder(orderId)
        })
        .catch(() => {
          uni.hideLoading()
          this.submitting = false
        })
    },
    // 预支付后按 mock / 真实两种分支处理
    payOrder(orderId) {
      return prepay({ OrderId: orderId }).then((pay) => {
        if (pay && pay.mock === true) {
          // 支付未配置:直接走 MockPay 完成联调
          return mockPay({ OrderId: orderId }).then(() => {
            uni.hideLoading()
            this.submitting = false
            this.goSuccess(orderId)
          })
        }
        // 真实环境:使用后端返回的 prepayParams 调起微信支付
        const params = (pay && pay.prepayParams) || {}
        uni.hideLoading()
        uni.requestPayment({
          provider: 'wxpay',
          timeStamp: params.timeStamp,
          nonceStr: params.nonceStr,
          package: params.package,
          signType: params.signType,
          paySign: params.paySign,
          success: () => {
            // TODO: 微信支付回调(PayNotify)会异步置订单为已支付;
            //       这里也可主动查询订单状态确认,再进入成功页。
            this.submitting = false
            this.goSuccess(orderId)
          },
          fail: () => {
            this.submitting = false
            uni.showToast({ title: '支付未完成', icon: 'none' })
          }
        })
      })
    },
    goSuccess(orderId) {
      uni.redirectTo({ url: `/packageMall/payment-success/payment-success?orderId=${orderId}` })
    }
  }
}
</script>

<style lang="scss" scoped>
@import '@/styles/mixins.scss';
@import '@/styles/subpage.scss';

.store-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16rpx;
}

.store-chip {
  flex: 1;
  min-width: 30%;
  padding: 20rpx 22rpx;
  border-radius: 24rpx;
  background: rgba(255, 255, 255, 0.82);
  border: 1rpx solid rgba(210, 191, 156, 0.16);
  transition: transform 240ms ease, background 240ms ease, border-color 240ms ease;
}

.store-chip.active {
  background: rgba(100, 140, 118, 0.12);
  border-color: rgba(100, 140, 118, 0.4);
}

.store-chip-name,
.store-chip-addr {
  display: block;
}

.store-chip-name {
  font-size: 26rpx;
  font-weight: 600;
  color: $text-primary;
}

.store-chip-addr {
  margin-top: 8rpx;
  font-size: 22rpx;
  color: $text-secondary;
}

.store-tip {
  display: block;
  margin-top: 16rpx;
  font-size: 22rpx;
  line-height: 1.7;
  color: $brand-rose;
}

.product-card {
  display: flex;
  padding: 28rpx;
  margin-bottom: 24rpx;
}

.product-image {
  width: 180rpx;
  height: 180rpx;
  border-radius: 28rpx;
  flex-shrink: 0;
}

.product-copy {
  flex: 1;
  margin-left: 22rpx;
}

.product-name,
.product-spec,
.product-desc,
.product-price {
  display: block;
}

.product-name {
  font-size: 30rpx;
  line-height: 1.5;
  font-weight: 700;
}

.product-spec,
.product-desc {
  margin-top: 12rpx;
  font-size: 24rpx;
  line-height: 1.7;
  color: $text-secondary;
}

.product-price {
  margin-top: 16rpx;
  font-size: 36rpx;
  font-weight: 700;
  color: $brand-primary-deep;
}

.form-item {
  padding: 8rpx 0;
}

.form-item.no-border {
  border-bottom: 0;
}

.form-label {
  display: block;
  font-size: 22rpx;
  color: $text-tertiary;
  letter-spacing: 2rpx;
}

.form-input {
  margin-top: 14rpx;
  height: 60rpx;
  font-size: 28rpx;
  color: $text-primary;
}

.form-placeholder {
  color: $text-tertiary;
}

.accent {
  color: $brand-primary-deep;
}
</style>