JewelryMall/miniprogram/pages/order/submit.vue
2026-03-05 00:43:04 +08:00

586 lines
15 KiB
Vue

<template>
<view class="order-submit">
<!-- 自定义导航栏 -->
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar__content" :style="{ height: navBarHeight + 'px' }">
<view class="navbar__back" @click="goBack">
<image src="/static/ic_back.png" class="navbar__back-icon" mode="aspectFit" />
</view>
<text class="navbar__title">订单提交</text>
<view class="navbar__placeholder"></view>
</view>
</view>
<view :style="{ height: (statusBarHeight + navBarHeight) + 'px' }"></view>
<!-- 公司地址 -->
<view class="address-section">
<view class="address-section__header">
<image src="/static/ic_address.png" class="address-section__icon" mode="aspectFit" />
<text class="address-section__title">公司地址</text>
</view>
<view class="address-section__content">
<text class="address-section__name">{{ companyInfo.name }}</text>
<text class="address-section__detail">{{ companyInfo.address }}</text>
<text class="address-section__phone">{{ companyInfo.phone }}</text>
<text class="address-section__contact">{{ companyInfo.contact }}</text>
</view>
</view>
<!-- 您的信息 -->
<view class="info-section">
<view class="info-section__header">
<image src="/static/ic_tip.png" class="info-section__icon" mode="aspectFit" />
<text class="info-section__title">您的信息</text>
<text class="info-section__subtitle">请留下您的联系方式</text>
</view>
<!-- 地址选择 -->
<view v-if="addressList.length > 0" class="address-picker">
<view class="address-picker__label">选择已有地址:</view>
<view class="address-picker__list">
<view
v-for="addr in addressList" :key="addr.id"
class="address-picker__item"
:class="{ 'address-picker__item--active': selectedAddrId === addr.id }"
@click="selectAddress(addr)"
>
<view class="address-picker__item-top">
<text class="address-picker__name">{{ addr.name }}</text>
<text class="address-picker__phone">{{ addr.phone }}</text>
<text v-if="addr.isDefault" class="address-picker__default">默认</text>
</view>
<text class="address-picker__detail">{{ addr.province }}{{ addr.city }}{{ addr.district }}{{ addr.detail }}</text>
</view>
</view>
<view class="address-picker__manual" @click="clearSelected">
<text>{{ selectedAddrId ? '手动填写新地址' : '当前为手动填写' }}</text>
</view>
</view>
<view class="form-item">
<text class="form-item__label required">姓名</text>
<input class="form-item__input" v-model="receiverName" placeholder="请输入真实姓名" />
</view>
<view class="form-item">
<text class="form-item__label required">电话</text>
<input class="form-item__input" v-model="receiverPhone" type="number" placeholder="请输入电话" />
</view>
<view class="form-item">
<text class="form-item__label required">地址</text>
<input class="form-item__input" v-model="receiverAddress" placeholder="请输入收货地址" />
</view>
<view class="form-item">
<text class="form-item__label">备注</text>
<input class="form-item__input" v-model="remark" placeholder="请输入备注信息" />
</view>
</view>
<!-- 发货时间 -->
<view class="delivery-section">
<view class="delivery-section__header">
<image src="/static/ic_time.png" class="delivery-section__icon" mode="aspectFit" />
<text class="delivery-section__title">发货时间</text>
</view>
<text class="delivery-section__content">{{ deliveryTimeText }}</text>
</view>
<!-- 商品列表 -->
<view class="product-section">
<view class="product-item" v-for="item in orderItems" :key="item.id">
<image class="product-item__img" :src="fullUrl(item.product.thumb || item.product.bannerImages?.[0] || '')" mode="aspectFill" />
<view class="product-item__info">
<text class="product-item__name">{{ item.product.name }}</text>
<view class="product-item__specs">
<text class="product-item__spec">款号:{{ item.specData.modelName || 'B2022' }}</text>
<text class="product-item__spec">商品型号:{{ item.specData.fineness || '2606' }}</text>
<text class="product-item__spec">成色:{{ item.specData.fineness || '30' }}</text>
<text class="product-item__spec">主石:{{ item.specData.mainStone || '13.00#' }}</text>
<text class="product-item__spec" v-if="item.specData.subStone">副石:{{ item.specData.subStone }}</text>
<text class="product-item__spec">手寸:{{ item.specData.ringSize || '13.00#' }}</text>
<text class="product-item__spec">金料总重:{{ item.specData.goldTotalWeight || '236' }}</text>
</view>
<text class="product-item__price">¥{{ item.specData.totalPrice }}元</text>
</view>
</view>
</view>
<!-- 底部固定区域:协议勾选 + 提交栏 -->
<view class="bottom-fixed">
<view class="agreement" @click="agreed = !agreed">
<image :src="agreed ? '/static/ic_check_s.png' : '/static/ic_check.png'" class="agreement__checkbox" mode="aspectFit" />
<text class="agreement__text">因珠宝产品属于贵重物品,一旦出货,产品无质量问题不支持退换!</text>
</view>
<view class="submit-bar">
<view class="submit-bar__left">
<text class="submit-bar__label">合计:</text>
<text class="submit-bar__price">¥{{ totalPrice }}</text>
</view>
<view
class="submit-bar__btn"
:class="{ 'submit-bar__btn--disabled': !canSubmit }"
@click="handleSubmit"
>
开始下单
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useCartStore } from '../../store/cart'
import { createOrder } from '../../api/order'
import { getAddressList } from '../../api/user'
import { BASE_URL, get } from '../../utils/request'
import type { CartItem, Address } from '../../types'
const cartStore = useCartStore()
const statusBarHeight = ref(0)
const navBarHeight = ref(44)
const receiverName = ref('')
const receiverPhone = ref('')
const receiverAddress = ref('')
const remark = ref('')
const agreed = ref(false)
const submitting = ref(false)
const companyInfo = ref({ name: '', address: '', phone: '', contact: '' })
const deliveryTimeText = ref('')
const addressList = ref<Address[]>([])
const selectedAddrId = ref<number | null>(null)
function selectAddress(addr: Address) {
selectedAddrId.value = addr.id
receiverName.value = addr.name
receiverPhone.value = addr.phone
receiverAddress.value = `${addr.province}${addr.city}${addr.district}${addr.detail}`
}
function clearSelected() {
selectedAddrId.value = null
receiverName.value = ''
receiverPhone.value = ''
receiverAddress.value = ''
}
function fullUrl(path: string): string {
if (!path) return ''
if (path.startsWith('http')) return path
return BASE_URL + path
}
function goBack() {
uni.navigateBack()
}
/** 从购物车获取已勾选的商品作为订单项 */
const orderItems = computed<CartItem[]>(() => cartStore.checkedItems)
const totalPrice = computed(() =>
orderItems.value.reduce((sum, item) => sum + item.specData.totalPrice * item.quantity, 0),
)
const canSubmit = computed(() => agreed.value && !submitting.value)
async function handleSubmit() {
if (!canSubmit.value) return
if (!receiverName.value.trim()) {
uni.showToast({ title: '请输入收货人姓名', icon: 'none' })
return
}
if (!receiverPhone.value.trim()) {
uni.showToast({ title: '请输入联系电话', icon: 'none' })
return
}
if (!receiverAddress.value.trim()) {
uni.showToast({ title: '请输入收货地址', icon: 'none' })
return
}
submitting.value = true
try {
const order: any = await createOrder({
items: orderItems.value.map((item) => ({
productId: item.productId,
specDataId: item.specDataId,
quantity: item.quantity,
unitPrice: item.specData.totalPrice,
})),
receiverName: receiverName.value.trim(),
receiverPhone: receiverPhone.value.trim(),
receiverAddress: receiverAddress.value.trim(),
})
// 先保存要删除的购物车项ID
const itemIdsToRemove = orderItems.value.map((item) => item.id)
uni.redirectTo({ url: `/pages/order/detail?id=${order.orderId}` })
// 跳转后再清除购物车
itemIdsToRemove.forEach((id) => cartStore.removeFromCart(id))
} catch {
uni.showToast({ title: '提交订单失败,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
onMounted(() => {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 0
const menuBtn = uni.getMenuButtonBoundingClientRect()
navBarHeight.value = menuBtn.height + (menuBtn.top - (sysInfo.statusBarHeight || 0)) * 2
// 获取公司地址配置
get('/api/config/company_address').then((data: any) => {
if (data && typeof data === 'object') companyInfo.value = { ...companyInfo.value, ...data }
}).catch(() => {})
// 获取发货时间配置
get('/api/config/delivery_time').then((data: any) => {
if (data) deliveryTimeText.value = data
}).catch(() => {})
// 获取用户地址列表
getAddressList().then((list: Address[]) => {
addressList.value = list || []
// 自动选择默认地址
const defaultAddr = list.find(a => a.isDefault)
if (defaultAddr) selectAddress(defaultAddr)
}).catch(() => {})
})
</script>
<style scoped>
.order-submit {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 220rpx;
}
/* 导航栏 */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: linear-gradient(135deg, #FFCFDE 0%, #FFA6C4 100%);
}
.navbar__content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
}
.navbar__back {
padding: 10rpx;
}
.navbar__back-icon {
width: 40rpx;
height: 40rpx;
}
.navbar__title {
font-size: 34rpx;
font-weight: 600;
color: #000;
}
.navbar__placeholder {
width: 60rpx;
}
/* 公司地址 */
.address-section {
background: #fff;
margin: 16rpx 24rpx;
padding: 24rpx;
border-radius: 16rpx;
}
.address-section__header {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.address-section__icon {
width: 32rpx;
height: 32rpx;
margin-right: 8rpx;
}
.address-section__title {
font-size: 28rpx;
color: #e91e63;
font-weight: 600;
}
.address-section__content {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.address-section__name {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
.address-section__detail,
.address-section__phone,
.address-section__contact {
font-size: 24rpx;
color: #666;
line-height: 36rpx;
}
/* 您的信息 */
.info-section {
background: #fff;
margin: 0 24rpx 16rpx;
padding: 24rpx;
border-radius: 16rpx;
}
.info-section__header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.info-section__icon {
width: 32rpx;
height: 32rpx;
margin-right: 8rpx;
}
.info-section__title {
font-size: 28rpx;
color: #e91e63;
font-weight: 600;
margin-right: 12rpx;
}
.info-section__subtitle {
font-size: 24rpx;
color: #999;
}
.form-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.form-item:last-child {
border-bottom: none;
}
/* 地址选择器 */
.address-picker {
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.address-picker__label {
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
}
.address-picker__list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.address-picker__item {
padding: 16rpx 20rpx;
border: 2rpx solid #eee;
border-radius: 12rpx;
background: #fafafa;
}
.address-picker__item--active {
border-color: #FF6D9B;
background: #fff5f8;
}
.address-picker__item-top {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 6rpx;
}
.address-picker__name {
font-size: 26rpx;
color: #333;
font-weight: 600;
}
.address-picker__phone {
font-size: 24rpx;
color: #666;
}
.address-picker__default {
font-size: 20rpx;
color: #FF6D9B;
border: 1rpx solid #FF6D9B;
border-radius: 4rpx;
padding: 2rpx 8rpx;
}
.address-picker__detail {
font-size: 24rpx;
color: #999;
line-height: 36rpx;
}
.address-picker__manual {
margin-top: 12rpx;
text-align: center;
font-size: 24rpx;
color: #FF6D9B;
}
.form-item__label {
font-size: 26rpx;
color: #333;
width: 100rpx;
flex-shrink: 0;
}
.form-item__label.required::before {
content: '*';
color: #e91e63;
margin-right: 4rpx;
}
.form-item__input {
flex: 1;
font-size: 26rpx;
color: #333;
}
/* 发货时间 */
.delivery-section {
background: #fff;
margin: 0 24rpx 16rpx;
padding: 24rpx;
border-radius: 16rpx;
}
.delivery-section__header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.delivery-section__icon {
width: 32rpx;
height: 32rpx;
margin-right: 8rpx;
}
.delivery-section__title {
font-size: 28rpx;
color: #e91e63;
font-weight: 600;
}
.delivery-section__content {
font-size: 24rpx;
color: #e91e63;
line-height: 36rpx;
}
/* 商品列表 */
.product-section {
background: #fff;
margin: 0 24rpx 16rpx;
padding: 24rpx;
border-radius: 16rpx;
}
.product-item {
display: flex;
gap: 20rpx;
}
.product-item__img {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
flex-shrink: 0;
background: #f5f5f5;
}
.product-item__info {
flex: 1;
display: flex;
flex-direction: column;
}
.product-item__name {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 8rpx;
}
.product-item__specs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8rpx;
margin-bottom: 12rpx;
}
.product-item__spec {
font-size: 22rpx;
color: #666;
}
.product-item__price {
font-size: 30rpx;
color: #e91e63;
font-weight: 700;
}
/* 底部固定区域 */
.bottom-fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
padding-bottom: env(safe-area-inset-bottom);
z-index: 50;
}
/* 协议 */
.agreement {
background: #fffbe6;
padding: 16rpx 24rpx;
display: flex;
align-items: center;
border-bottom: 1rpx solid #f0f0f0;
}
.agreement__checkbox {
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.agreement__text {
font-size: 22rpx;
color: #333;
line-height: 34rpx;
}
/* 底部提交栏 */
.submit-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 24rpx;
}
.submit-bar__left {
display: flex;
align-items: baseline;
}
.submit-bar__label {
font-size: 26rpx;
color: #333;
}
.submit-bar__price {
font-size: 36rpx;
color: #e91e63;
font-weight: 700;
}
.submit-bar__btn {
background: linear-gradient(135deg, #FF6D9B, #FF4081);
color: #fff;
padding: 20rpx 60rpx;
border-radius: 44rpx;
font-size: 28rpx;
font-weight: 600;
}
.submit-bar__btn--disabled {
background: #ccc;
pointer-events: none;
}
</style>