feat: 发货流程增加地址选择弹窗

This commit is contained in:
Zuncle 2026-04-01 00:55:23 +08:00
parent d7cd33bcca
commit 63345f4c24
2 changed files with 267 additions and 44 deletions

View File

@ -148,8 +148,10 @@ export function redeemInventory(user_id, ids) {
return authRequest({ url: `/api/app/users/${user_id}/inventory/redeem`, method: 'POST', data: { inventory_ids: ids } })
}
export function requestShipping(user_id, ids) {
return authRequest({ url: `/api/app/users/${user_id}/inventory/request-shipping`, method: 'POST', data: { inventory_ids: ids } })
export function requestShipping(user_id, ids, address_id) {
const data = { inventory_ids: ids }
if (address_id) data.address_id = address_id
return authRequest({ url: `/api/app/users/${user_id}/inventory/request-shipping`, method: 'POST', data })
}
export function createShippingFeeOrder(user_id, ids) {

View File

@ -67,7 +67,7 @@
<view v-if="!hasMore && aggregatedList.length > 0" class="no-more">没有更多了</view>
<!-- 底部操作栏 -->
<view class="bottom-bar" v-if="hasSelected">
<view class="bottom-bar" v-if="hasSelected && !showAddressPicker">
<view class="selected-info">
<text>已选 {{ totalSelectedCount }} </text>
</view>
@ -247,6 +247,41 @@
</view>
</view>
</view>
<!-- 地址选择弹窗 -->
<view class="address-picker-mask" v-if="showAddressPicker" @tap="showAddressPicker = false" @touchmove.stop></view>
<view class="address-picker-popup" :class="{ 'show': showAddressPicker }">
<view class="share-header">
<text class="share-title">选择收货地址</text>
<text class="share-close" @tap="showAddressPicker = false">×</text>
</view>
<scroll-view scroll-y class="address-scroll">
<view v-if="addressList.length === 0" class="empty-address">
<text>暂无收货地址</text>
</view>
<view
v-for="addr in addressList"
:key="addr.id"
class="address-option"
:class="{ selected: selectedAddressId === addr.id }"
@tap="selectedAddressId = addr.id"
>
<view class="addr-radio" :class="{ checked: selectedAddressId === addr.id }"></view>
<view class="addr-info">
<view class="addr-top">
<text class="addr-name">{{ addr.name || addr.realname }}</text>
<text class="addr-phone">{{ addr.phone || addr.mobile }}</text>
<view v-if="addr.is_default" class="addr-default-tag">默认</view>
</view>
<text class="addr-detail">{{ addr.province }} {{ addr.city }} {{ addr.district }} {{ addr.address || addr.detail }}</text>
</view>
</view>
</scroll-view>
<view class="address-picker-footer">
<text class="add-address-link" @tap="toAddAddress">+ 新增收货地址</text>
<button class="confirm-ship-btn" :disabled="!selectedAddressId" @tap="confirmShipWithAddress">确认发货</button>
</view>
</view>
</view>
</template>
@ -278,6 +313,12 @@ const pageSize = ref(100)
const hasMore = ref(true)
const productMetaCache = new Map()
//
const showAddressPicker = ref(false)
const addressList = ref([])
const selectedAddressId = ref(null)
const pendingShipIds = ref([])
// Synthesis tab state
const recipes = ref([])
const synthLoading = ref(false)
@ -317,7 +358,7 @@ async function fetchProductMeta(productId) {
return meta
}
onShow(() => {
onShow(async () => {
//
if (!checkPhoneBoundSync()) return
@ -345,18 +386,36 @@ onShow(() => {
return
}
const uid = uni.getStorageSync("user_id")
//
if (pendingShipIds.value.length > 0) {
try {
const addresses = await listAddresses(uid)
const list = addresses.list || addresses.data || addresses || []
addressList.value = Array.isArray(list) ? list : []
if (addressList.value.length > 0) {
if (!selectedAddressId.value) {
const defaultAddr = addressList.value.find(a => a.is_default)
selectedAddressId.value = defaultAddr ? defaultAddr.id : addressList.value[0].id
}
showAddressPicker.value = true
}
} catch (e) {}
return
}
//
page.value = 1
hasMore.value = true
aggregatedList.value = []
shippedList.value = []
const uid = uni.getStorageSync("user_id")
if (currentTab.value === 1) {
loadShipments(uid)
} else if (currentTab.value === 2) {
loadRecipes(uid)
} else {
loadInventory(uid) // onReachBottom
loadInventory(uid)
}
})
@ -779,7 +838,6 @@ async function onShip() {
const selectedItems = aggregatedList.value.filter(item => item.selected)
if (selectedItems.length === 0) return
// inventory id
let allIds = []
selectedItems.forEach(item => {
if (item.original_ids && item.original_ids.length >= item.selectedCount) {
@ -793,38 +851,45 @@ async function onShip() {
return
}
// 1.
//
try {
const addresses = await listAddresses(user_id)
const addressList = addresses.list || addresses.data || addresses || []
const list = addresses.list || addresses.data || addresses || []
addressList.value = Array.isArray(list) ? list : []
if (!addressList || addressList.length === 0) {
//
if (addressList.value.length === 0) {
uni.showModal({
title: '提示',
content: '申请发货需要设置默认地址,是否前往新建地址?',
content: '申请发货需要设置收货地址,是否前往新建地址?',
confirmText: '前往',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages-user/address/edit' })
}
if (res.confirm) uni.navigateTo({ url: '/pages-user/address/edit' })
}
})
return
}
pendingShipIds.value = allIds
const defaultAddr = addressList.value.find(a => a.is_default)
selectedAddressId.value = defaultAddr ? defaultAddr.id : addressList.value[0].id
showAddressPicker.value = true
} catch (e) {
console.error('获取地址列表失败:', e)
uni.showToast({ title: '获取地址失败', icon: 'none' })
return
}
}
async function confirmShipWithAddress() {
const user_id = uni.getStorageSync('user_id')
const allIds = pendingShipIds.value
const addressId = selectedAddressId.value
showAddressPicker.value = false
// 2.
const FREIGHT_THRESHOLD = 5
const FREIGHT_FEE = 10
if (allIds.length < FREIGHT_THRESHOLD) {
// 5 10
const confirmed = await new Promise((resolve) => {
uni.showModal({
title: '需支付运费',
@ -850,11 +915,11 @@ async function onShip() {
}
uni.hideLoading()
//
uni.showLoading({ title: '提交中...' })
try {
await requestShipping(user_id, allIds)
await requestShipping(user_id, allIds, addressId)
uni.showToast({ title: '申请成功', icon: 'success' })
pendingShipIds.value = []
aggregatedList.value = []
page.value = 1
hasMore.value = true
@ -867,30 +932,26 @@ async function onShip() {
return
}
// 5
uni.showModal({
title: '确认发货',
content: `${allIds.length} 件物品,确认申请发货?`,
confirmText: '确认发货',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '提交中...' })
try {
await requestShipping(user_id, allIds)
uni.showToast({ title: '申请成功', icon: 'success' })
//
aggregatedList.value = []
page.value = 1
hasMore.value = true
loadInventory(user_id)
} catch (e) {
uni.showToast({ title: e.message || '申请失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
}
})
// 5
uni.showLoading({ title: '提交中...' })
try {
await requestShipping(user_id, allIds, addressId)
uni.showToast({ title: '申请成功', icon: 'success' })
pendingShipIds.value = []
aggregatedList.value = []
page.value = 1
hasMore.value = true
loadInventory(user_id)
} catch (e) {
uni.showToast({ title: e.message || '申请失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
function toAddAddress() {
showAddressPicker.value = false
uni.navigateTo({ url: '/pages-user/address/edit' })
}
// 48
@ -1927,4 +1988,164 @@ function onCopyShareLink() {
0% { left: -100%; }
20%, 100% { left: 200%; }
}
/* ── 地址选择弹窗 ── */
.address-picker-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 1009;
backdrop-filter: blur(4rpx);
}
.address-picker-popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 40rpx 40rpx 0 0;
z-index: 1010;
padding: 40rpx 40rpx 0;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
max-height: 70vh;
display: flex;
flex-direction: column;
&.show {
transform: translateY(0);
}
}
.address-scroll {
flex: 1;
max-height: 50vh;
margin: 20rpx 0;
}
.address-option {
display: flex;
align-items: flex-start;
padding: 24rpx;
border-radius: $radius-md;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
background: $bg-page;
transition: all 0.2s;
&.selected {
border-color: $brand-primary;
background: rgba($brand-primary, 0.05);
}
}
.addr-radio {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 2rpx solid $text-tertiary;
margin-right: 20rpx;
margin-top: 6rpx;
flex-shrink: 0;
transition: all 0.2s;
&.checked {
border-color: $brand-primary;
background: $brand-primary;
position: relative;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16rpx;
height: 8rpx;
border-left: 3rpx solid #fff;
border-bottom: 3rpx solid #fff;
transform: translate(-50%, -65%) rotate(-45deg);
}
}
}
.addr-info {
flex: 1;
min-width: 0;
}
.addr-top {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.addr-name {
font-size: 28rpx;
font-weight: 700;
color: $text-main;
}
.addr-phone {
font-size: 26rpx;
color: $text-sub;
}
.addr-default-tag {
font-size: 18rpx;
color: #fff;
background: $brand-primary;
padding: 2rpx 10rpx;
border-radius: 6rpx;
font-weight: 600;
}
.addr-detail {
font-size: 24rpx;
color: $text-sub;
line-height: 1.5;
}
.empty-address {
text-align: center;
padding: 60rpx 0;
color: $text-tertiary;
font-size: 28rpx;
}
.address-picker-footer {
padding: 20rpx 0 calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
}
.add-address-link {
font-size: 26rpx;
color: $brand-primary;
font-weight: 500;
}
.confirm-ship-btn {
background: $gradient-brand;
color: #fff;
border: none;
border-radius: $radius-round;
height: 80rpx;
padding: 0 60rpx;
font-size: 28rpx;
font-weight: 600;
box-shadow: $shadow-warm;
&[disabled] {
opacity: 0.5;
}
&::after { border: none; }
}
</style>