1391 lines
37 KiB
Vue
1391 lines
37 KiB
Vue
<template>
|
||
<view class="wrap">
|
||
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||
<view class="bg-decoration"></view>
|
||
<!-- 顶部 Tab -->
|
||
<view class="tabs glass-card">
|
||
<view class="tab-item" :class="{ active: currentTab === 0 }" @tap="switchTab(0)">
|
||
<text class="tab-text">待处理</text>
|
||
<text class="tab-count" v-if="aggregatedList.length > 0">({{ aggregatedList.length }})</text>
|
||
</view>
|
||
<view class="tab-item" :class="{ active: currentTab === 1 }" @tap="switchTab(1)">
|
||
<text class="tab-text">已申请发货</text>
|
||
<text class="tab-count" v-if="shippedList.length > 0">({{ shippedList.length }})</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Tab 0: 待处理商品 -->
|
||
<block v-if="currentTab === 0">
|
||
<!-- 全选栏 -->
|
||
<view class="action-bar" v-if="aggregatedList.length > 0">
|
||
<view class="select-all" @tap="toggleSelectAll">
|
||
<view class="checkbox" :class="{ checked: isAllSelected }"></view>
|
||
<text>全选</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="loading && aggregatedList.length === 0" class="status-text">加载中...</view>
|
||
<view v-else-if="!aggregatedList || aggregatedList.length === 0" class="status-text">背包空空如也</view>
|
||
|
||
<view v-else class="inventory-grid">
|
||
<view v-for="(item, index) in aggregatedList" :key="index" class="inventory-item">
|
||
<view class="checkbox-area" @tap.stop="toggleSelect(item)">
|
||
<view class="checkbox" :class="{ checked: item.selected }"></view>
|
||
</view>
|
||
<image :src="item.image" mode="aspectFill" class="item-image" @error="onImageError(index, 'aggregated')" />
|
||
<view class="item-info">
|
||
<text class="item-name">{{ item.name || '未命名道具' }}</text>
|
||
<text class="item-price" v-if="item.price">单价: ¥{{ item.price }}</text>
|
||
<view class="item-actions">
|
||
<text class="invite-btn" v-if="!item.selected" @tap.stop="onInvite(item)">邀请填写</text>
|
||
<text class="item-count" v-if="!item.selected">x{{ item.count || 1 }}</text>
|
||
<view class="stepper" v-else @tap.stop>
|
||
<text class="step-btn minus" @tap.stop="changeCount(item, -1)">-</text>
|
||
<text class="step-num">{{ item.selectedCount }}</text>
|
||
<text class="step-btn plus" @tap.stop="changeCount(item, 1)">+</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="loading && aggregatedList.length > 0" class="loading-more">加载更多...</view>
|
||
<view v-if="!hasMore && aggregatedList.length > 0" class="no-more">没有更多了</view>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<view class="bottom-bar" v-if="hasSelected">
|
||
<view class="selected-info">
|
||
<text>已选 {{ totalSelectedCount }} 件</text>
|
||
</view>
|
||
<view class="btn-group">
|
||
<button class="action-btn btn-ship" @tap="onShip">发货</button>
|
||
<button class="action-btn btn-redeem" @tap="onRedeem">兑换</button>
|
||
</view>
|
||
</view>
|
||
<view class="bottom-spacer" v-if="hasSelected"></view>
|
||
</block>
|
||
|
||
<!-- Tab 1: 已申请发货 -->
|
||
<block v-if="currentTab === 1">
|
||
<view v-if="loading && shippedList.length === 0" class="status-text">加载中...</view>
|
||
<view v-else-if="!shippedList || shippedList.length === 0" class="status-text">暂无发货记录</view>
|
||
|
||
<view v-else class="shipment-list">
|
||
<view v-for="(item, index) in shippedList" :key="index" class="shipment-card">
|
||
<!-- 头部:批次信息和状态 -->
|
||
<view class="shipment-header">
|
||
<view class="shipment-batch">
|
||
<text class="batch-label">发货单</text>
|
||
<text class="batch-no" v-if="item.batch_no">{{ item.batch_no }}</text>
|
||
<view class="count-badge">{{ item.count }}件商品</view>
|
||
</view>
|
||
<view class="shipment-actions">
|
||
<view class="shipment-status" :class="getStatusClass(item.status)">
|
||
{{ getStatusText(item.status) }}
|
||
</view>
|
||
<text class="shipment-cancel" v-if="Number(item.status) === 1 && item.batch_no" @tap="onCancelShipping(item)">撤销发货</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 商品缩略图列表 -->
|
||
<view class="product-thumbnails">
|
||
<view class="thumb-scroll">
|
||
<image
|
||
v-for="(img, imgIdx) in item.product_images.slice(0, 4)"
|
||
:key="imgIdx"
|
||
:src="img"
|
||
mode="aspectFill"
|
||
class="thumb-img"
|
||
@error="onThumbError(index, imgIdx)"
|
||
/>
|
||
<view class="thumb-more" v-if="item.product_images.length > 4">
|
||
+{{ item.product_images.length - 4 }}
|
||
</view>
|
||
</view>
|
||
<!-- 商品名称列表 -->
|
||
<view class="product-names">
|
||
<text class="product-name-item" v-for="(name, nIdx) in item.product_names.slice(0, 3)" :key="nIdx">
|
||
{{ name }}
|
||
</text>
|
||
<text class="product-name-more" v-if="item.product_names.length > 3">
|
||
等{{ item.product_names.length }}件商品
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 物流信息 -->
|
||
<view class="shipment-express" v-if="item.express_code || item.express_no">
|
||
<view class="express-icon">
|
||
<text class="iconfont">📦</text>
|
||
</view>
|
||
<view class="express-info">
|
||
<text class="express-company">{{ item.express_code || '待发货' }}</text>
|
||
<text class="express-no" v-if="item.express_no">{{ item.express_no }}</text>
|
||
</view>
|
||
<text class="express-copy" v-if="item.express_no" @tap="copyExpressNo(item.express_no)">复制</text>
|
||
</view>
|
||
|
||
<!-- 时间信息 -->
|
||
<view class="shipment-time">
|
||
<text class="time-item" v-if="item.created_at">申请时间:{{ formatDate(item.created_at) }}</text>
|
||
<text class="time-item" v-if="item.shipped_at">发货时间:{{ formatDate(item.shipped_at) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="loading && shippedList.length > 0" class="loading-more">加载更多...</view>
|
||
<view v-if="!hasMore && shippedList.length > 0" class="no-more">没有更多了</view>
|
||
</block>
|
||
<!-- 分享弹窗 -->
|
||
<view class="share-mask" v-if="showSharePopup" @tap="showSharePopup = false" @touchmove.stop></view>
|
||
<view class="share-popup glass-card" :class="{ 'show': showSharePopup }">
|
||
<view class="share-header">
|
||
<text class="share-title">邀请好友填写地址</text>
|
||
<text class="share-close" @tap="showSharePopup = false">×</text>
|
||
</view>
|
||
<view class="share-body">
|
||
<view class="share-item-preview">
|
||
<image class="preview-img" :src="sharingItem.image" mode="aspectFit"></image>
|
||
<view class="preview-info">
|
||
<text class="preview-name">{{ sharingItem.name }}</text>
|
||
<text class="preview-desc">邀请好友填写地址后,该奖品将发货至好友手中,并认领归属于分享账号。</text>
|
||
</view>
|
||
</view>
|
||
<view class="share-actions">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<button class="action-btn share-card-btn" open-type="share">发送给微信好友</button>
|
||
<!-- #endif -->
|
||
<button class="action-btn copy-link-btn" @tap="onCopyShareLink">复制分享链接</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue'
|
||
import { onShow, onReachBottom, onShareAppMessage, onPullDownRefresh } from '@dcloudio/uni-app'
|
||
import { getInventory, getProductDetail, redeemInventory, requestShipping, cancelShipping, listAddresses, getShipments, createAddressShare } from '@/api/appUser'
|
||
import { vibrateShort } from '@/utils/vibrate.js'
|
||
import { checkPhoneBound } from '@/utils/checkPhone.js'
|
||
|
||
const currentTab = ref(0)
|
||
const aggregatedList = ref([])
|
||
const shippedList = ref([])
|
||
const showSharePopup = ref(false)
|
||
const sharingItem = ref({})
|
||
const currentShareToken = ref('')
|
||
const currentShortLink = ref('')
|
||
const loading = ref(false)
|
||
const page = ref(1)
|
||
const pageSize = ref(100)
|
||
const hasMore = ref(true)
|
||
const productMetaCache = new Map()
|
||
|
||
const totalCount = computed(() => {
|
||
return aggregatedList.value.reduce((sum, item) => sum + (item.count || 1), 0)
|
||
})
|
||
|
||
const hasSelected = computed(() => {
|
||
return aggregatedList.value.some(item => item.selected)
|
||
})
|
||
|
||
const totalSelectedCount = computed(() => {
|
||
return aggregatedList.value.reduce((sum, item) => sum + (item.selected ? item.selectedCount : 0), 0)
|
||
})
|
||
|
||
const isAllSelected = computed(() => {
|
||
return aggregatedList.value.length > 0 && aggregatedList.value.every(item => item.selected)
|
||
})
|
||
|
||
async function fetchProductMeta(productId) {
|
||
const key = String(productId || '').trim()
|
||
if (!key) return null
|
||
if (productMetaCache.has(key)) return productMetaCache.get(key)
|
||
const res = await getProductDetail(productId)
|
||
const p = res && (res.data ?? res.result ?? res)
|
||
const meta = {
|
||
price: null
|
||
}
|
||
const rawPrice = (p && (p.price_sale ?? p.price)) ?? (res && res.price)
|
||
if (rawPrice !== undefined && rawPrice !== null) {
|
||
const n = Number(rawPrice)
|
||
if (!Number.isNaN(n)) meta.price = n / 100
|
||
}
|
||
productMetaCache.set(key, meta)
|
||
return meta
|
||
}
|
||
|
||
onShow(() => {
|
||
// 检查手机号绑定状态(快速检查本地缓存)
|
||
if (!checkPhoneBoundSync()) return
|
||
|
||
// Check for external tab switch request
|
||
try {
|
||
const targetTab = uni.getStorageSync('cabinet_target_tab')
|
||
if (targetTab !== '' && targetTab !== null && targetTab !== undefined) {
|
||
currentTab.value = Number(targetTab)
|
||
uni.removeStorageSync('cabinet_target_tab')
|
||
}
|
||
} catch (e) {}
|
||
|
||
const token = uni.getStorageSync('token')
|
||
if (!token) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '请先登录',
|
||
confirmText: '去登录',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.navigateTo({ url: '/pages/login/index' })
|
||
}
|
||
}
|
||
})
|
||
return
|
||
}
|
||
|
||
// 重置并加载第一页
|
||
page.value = 1
|
||
hasMore.value = true
|
||
aggregatedList.value = []
|
||
shippedList.value = []
|
||
const uid = uni.getStorageSync("user_id")
|
||
if (currentTab.value === 1) {
|
||
loadShipments(uid)
|
||
} else {
|
||
loadInventory(uid) // 改为只加载第一页,后续由 onReachBottom 触发
|
||
}
|
||
})
|
||
|
||
onPullDownRefresh(() => {
|
||
const uid = uni.getStorageSync("user_id")
|
||
page.value = 1
|
||
hasMore.value = true
|
||
// Reset lists
|
||
if (currentTab.value === 1) {
|
||
shippedList.value = []
|
||
loadShipments(uid).finally(() => uni.stopPullDownRefresh())
|
||
} else {
|
||
aggregatedList.value = []
|
||
loadInventory(uid).finally(() => uni.stopPullDownRefresh())
|
||
}
|
||
})
|
||
|
||
onReachBottom(() => {
|
||
if (hasMore.value && !loading.value) {
|
||
const uid = uni.getStorageSync("user_id")
|
||
if (currentTab.value === 1) {
|
||
loadShipments(uid)
|
||
} else {
|
||
loadInventory(uid)
|
||
}
|
||
}
|
||
})
|
||
|
||
function switchTab(index) {
|
||
if (loading.value) return // 防止切换过快导致并发加载冲突
|
||
currentTab.value = index
|
||
// 切换时重新加载数据
|
||
page.value = 1
|
||
hasMore.value = true
|
||
aggregatedList.value = []
|
||
shippedList.value = []
|
||
const uid = uni.getStorageSync("user_id")
|
||
if (currentTab.value === 1) {
|
||
loadShipments(uid)
|
||
} else {
|
||
loadInventory(uid) // 改为按需加载
|
||
}
|
||
}
|
||
|
||
function cleanUrl(u) {
|
||
if (!u) return '/static/logo.png'
|
||
let s = String(u).trim()
|
||
|
||
// 尝试解析 JSON 数组字符串
|
||
if (s.startsWith('[') && s.endsWith(']')) {
|
||
try {
|
||
const arr = JSON.parse(s)
|
||
if (Array.isArray(arr) && arr.length > 0) {
|
||
s = arr[0]
|
||
}
|
||
} catch (e) {
|
||
console.warn('JSON parse failed for image:', s)
|
||
}
|
||
}
|
||
|
||
// 清理反引号和空格
|
||
s = s.replace(/[`'"]/g, '').trim()
|
||
|
||
// 提取 http 链接
|
||
const m = s.match(/https?:\/\/[^\s]+/)
|
||
if (m && m[0]) return m[0]
|
||
|
||
return s || '/static/logo.png'
|
||
}
|
||
|
||
function onImageError(index, type = 'aggregated') {
|
||
if (type === 'aggregated' && aggregatedList.value[index]) {
|
||
aggregatedList.value[index].image = '/static/logo.png'
|
||
} else if (type === 'shipped' && shippedList.value[index]) {
|
||
shippedList.value[index].image = '/static/logo.png'
|
||
}
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
if (!dateStr) return ''
|
||
const d = new Date(dateStr)
|
||
if (isNaN(d.getTime())) return ''
|
||
const y = d.getFullYear()
|
||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||
const da = String(d.getDate()).padStart(2, '0')
|
||
const h = String(d.getHours()).padStart(2, '0')
|
||
const min = String(d.getMinutes()).padStart(2, '0')
|
||
return `${y}-${m}-${da} ${h}:${min}`
|
||
}
|
||
|
||
// 发货状态辅助函数
|
||
function getStatusClass(status) {
|
||
const statusMap = {
|
||
1: 'status-pending', // 待发货
|
||
2: 'status-shipped', // 已发货
|
||
3: 'status-delivered', // 已签收
|
||
4: 'status-abnormal', // 异常
|
||
5: 'status-cancelled' // 已取消
|
||
}
|
||
return statusMap[status] || 'status-pending'
|
||
}
|
||
|
||
function getStatusText(status) {
|
||
const statusMap = {
|
||
1: '待发货',
|
||
2: '运输中',
|
||
3: '已签收',
|
||
4: '异常',
|
||
5: '已取消'
|
||
}
|
||
return statusMap[status] || '待发货'
|
||
}
|
||
|
||
function copyExpressNo(expressNo) {
|
||
uni.setClipboardData({
|
||
data: expressNo,
|
||
success: () => {
|
||
uni.showToast({ title: '已复制单号', icon: 'success' })
|
||
}
|
||
})
|
||
}
|
||
|
||
function onThumbError(shipmentIndex, imgIndex) {
|
||
if (shippedList.value[shipmentIndex] && shippedList.value[shipmentIndex].product_images) {
|
||
shippedList.value[shipmentIndex].product_images[imgIndex] = '/static/logo.png'
|
||
}
|
||
}
|
||
|
||
async function loadShipments(uid) {
|
||
if (loading.value) return
|
||
loading.value = true
|
||
try {
|
||
const res = await getShipments(uid, page.value, pageSize.value)
|
||
let list = []
|
||
let total = 0
|
||
if (res && Array.isArray(res.list)) { list = res.list; total = res.total || 0 }
|
||
else if (res && Array.isArray(res.data)) { list = res.data; total = res.total || 0 }
|
||
else if (Array.isArray(res)) { list = res; total = res.length }
|
||
|
||
// 处理每个发货单 - 直接使用返回的 products 数组
|
||
const mapped = list.map(s => {
|
||
const products = s.products || []
|
||
let productImages = []
|
||
let productNames = []
|
||
|
||
// 从 products 数组中提取图片和名称
|
||
products.forEach(product => {
|
||
if (product) {
|
||
// image 字段是 JSON 数组字符串,需要解析
|
||
const rawImg = product.image || product.images || product.main_image
|
||
const img = cleanUrl(rawImg)
|
||
productImages.push(img)
|
||
productNames.push(product.name || product.title || '商品')
|
||
}
|
||
})
|
||
|
||
// 如果没有获取到任何图片,使用默认图片
|
||
if (productImages.length === 0) {
|
||
productImages = ['/static/logo.png']
|
||
productNames = ['未知商品']
|
||
}
|
||
|
||
return {
|
||
batch_no: s.batch_no || '',
|
||
count: s.count ?? (Array.isArray(s.inventory_ids) ? s.inventory_ids.length : 0),
|
||
product_ids: s.product_ids || [],
|
||
product_images: productImages,
|
||
product_names: productNames,
|
||
express_code: s.express_code || '',
|
||
express_no: s.express_no || '',
|
||
created_at: s.created_at || '',
|
||
shipped_at: s.shipped_at || '',
|
||
received_at: s.received_at || '',
|
||
status: s.status || 1
|
||
}
|
||
})
|
||
|
||
const next = page.value === 1 ? mapped : [...shippedList.value, ...mapped]
|
||
shippedList.value = next
|
||
if (page.value * pageSize.value >= total && total > 0) { hasMore.value = false } else { page.value += 1 }
|
||
if (list.length === 0) { hasMore.value = false }
|
||
} catch (e) {
|
||
console.error('Load shipments error:', e)
|
||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadInventory(uid) {
|
||
if (loading.value) return
|
||
loading.value = true
|
||
try {
|
||
const params = {}
|
||
if (currentTab.value === 0) {
|
||
params.status = 1
|
||
}
|
||
const res = await getInventory(uid, page.value, pageSize.value, params)
|
||
console.log('Inventory loaded:', res)
|
||
|
||
let list = []
|
||
let total = 0
|
||
|
||
if (res && Array.isArray(res.list)) {
|
||
list = res.list
|
||
total = res.total || 0
|
||
} else if (res && Array.isArray(res.data)) { // 兼容 data: [] 格式
|
||
list = res.data
|
||
total = res.total || 0
|
||
} else if (Array.isArray(res)) { // 兼容直接返回数组
|
||
list = res
|
||
total = res.length
|
||
}
|
||
|
||
// 后端已经按 status 分页并聚合了,这里直接映射返回的 items
|
||
const nextList = page.value === 1 ? [] : (currentTab.value === 1 ? [...shippedList.value] : [...aggregatedList.value])
|
||
|
||
list.forEach(item => {
|
||
let imageUrl = cleanUrl(item.product_images || item.image)
|
||
const mappedItem = {
|
||
id: item.product_id,
|
||
original_ids: item.inventory_ids || [],
|
||
name: (item.product_name || '未知商品').trim(),
|
||
image: imageUrl,
|
||
price: item.product_price ? item.product_price / 100 : null,
|
||
count: item.count || 0,
|
||
selected: false,
|
||
selectedCount: item.count || 0,
|
||
has_shipment: item.has_shipment,
|
||
updated_at: item.updated_at
|
||
}
|
||
nextList.push(mappedItem)
|
||
})
|
||
|
||
console.log('Final list (tab=' + currentTab.value + '):', JSON.parse(JSON.stringify(nextList)))
|
||
|
||
if (currentTab.value === 1) {
|
||
shippedList.value = nextList
|
||
} else {
|
||
aggregatedList.value = nextList
|
||
}
|
||
|
||
// 判断是否还有更多
|
||
// 注意:这里的 total 是后端匹配过滤后的总记录数
|
||
if ((page.value * pageSize.value >= total && total > 0) || list.length === 0) {
|
||
hasMore.value = false
|
||
} else {
|
||
page.value += 1
|
||
}
|
||
|
||
// 如果返回空列表(且 total 为 0),也认为没有更多了
|
||
if (list.length === 0) {
|
||
hasMore.value = false
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Failed to load inventory:', error)
|
||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function toggleSelect(item) {
|
||
vibrateShort()
|
||
item.selected = !item.selected
|
||
if (item.selected) {
|
||
// 选中时默认数量为最大值
|
||
item.selectedCount = item.count
|
||
if (!item.price && item.id) {
|
||
fetchProductMeta(item.id).then(meta => {
|
||
if (!meta) return
|
||
if (!item.price && meta.price !== null) item.price = meta.price
|
||
}).catch(() => {})
|
||
}
|
||
}
|
||
}
|
||
|
||
function toggleSelectAll() {
|
||
vibrateShort()
|
||
const newState = !isAllSelected.value
|
||
aggregatedList.value.forEach(item => {
|
||
item.selected = newState
|
||
if (newState) {
|
||
item.selectedCount = item.count
|
||
if (!item.price && item.id) {
|
||
fetchProductMeta(item.id).then(meta => {
|
||
if (!meta) return
|
||
if (!item.price && meta.price !== null) item.price = meta.price
|
||
}).catch(() => {})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function changeCount(item, delta) {
|
||
if (!item.selected) return
|
||
const newCount = item.selectedCount + delta
|
||
if (newCount >= 1 && newCount <= item.count) {
|
||
item.selectedCount = newCount
|
||
}
|
||
}
|
||
|
||
async function onRedeem() {
|
||
vibrateShort()
|
||
const user_id = uni.getStorageSync('user_id')
|
||
if (!user_id) return
|
||
|
||
const selectedItems = aggregatedList.value.filter(item => item.selected)
|
||
if (selectedItems.length === 0) return
|
||
|
||
// 收集所有需要兑换的 inventory id
|
||
let allIds = []
|
||
selectedItems.forEach(item => {
|
||
// 确保有足够的 original_ids
|
||
if (item.original_ids && item.original_ids.length >= item.selectedCount) {
|
||
// 取出前 selectedCount 个 id
|
||
const idsToRedeem = item.original_ids.slice(0, item.selectedCount)
|
||
allIds.push(...idsToRedeem)
|
||
}
|
||
})
|
||
|
||
if (allIds.length === 0) {
|
||
uni.showToast({ title: '选择无效', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
uni.showModal({
|
||
title: '确认兑换',
|
||
content: `确定要兑换选中的 ${allIds.length} 件物品吗?此操作不可撤销。`,
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
uni.showLoading({ title: '处理中...' })
|
||
try {
|
||
await redeemInventory(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()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
async function onShip() {
|
||
vibrateShort()
|
||
const user_id = uni.getStorageSync('user_id')
|
||
if (!user_id) return
|
||
|
||
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) {
|
||
const idsToShip = item.original_ids.slice(0, item.selectedCount)
|
||
allIds.push(...idsToShip)
|
||
}
|
||
})
|
||
|
||
if (allIds.length === 0) {
|
||
uni.showToast({ title: '选择无效', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 1. 先检查是否有默认地址
|
||
try {
|
||
const addresses = await listAddresses(user_id)
|
||
const addressList = addresses.list || addresses.data || addresses || []
|
||
|
||
if (!addressList || addressList.length === 0) {
|
||
// 没有默认地址,提示用户跳转到新建地址页面
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '申请发货需要设置默认地址,是否前往新建地址?',
|
||
confirmText: '前往',
|
||
cancelText: '取消',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.navigateTo({ url: '/pages-user/address/edit' })
|
||
}
|
||
}
|
||
})
|
||
return
|
||
}
|
||
} catch (e) {
|
||
console.error('获取地址列表失败:', e)
|
||
uni.showToast({ title: '获取地址失败', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 2. 有默认地址,确认发货
|
||
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()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function onCancelShipping(shipment) {
|
||
const user_id = uni.getStorageSync('user_id')
|
||
const batchNo = shipment && shipment.batch_no
|
||
if (!user_id || !batchNo) return
|
||
uni.showModal({
|
||
title: '撤销发货',
|
||
content: `确认不再发货,并撤销发货单 ${batchNo} 吗?`,
|
||
confirmText: '确认撤销',
|
||
success: async (res) => {
|
||
if (!res.confirm) return
|
||
uni.showLoading({ title: '处理中...' })
|
||
try {
|
||
await cancelShipping(user_id, batchNo)
|
||
uni.showToast({ title: '已撤销发货', icon: 'success' })
|
||
page.value = 1
|
||
hasMore.value = true
|
||
shippedList.value = []
|
||
await loadShipments(user_id)
|
||
} catch (e) {
|
||
uni.showToast({ title: e?.message || '取消失败', icon: 'none' })
|
||
} finally {
|
||
}
|
||
}
|
||
})
|
||
}
|
||
// 微信分享逻辑
|
||
onShareAppMessage((res) => {
|
||
showSharePopup.value = false
|
||
return {
|
||
title: `送你一个好礼,快来填写地址领走吧!`,
|
||
path: `/pages-user/address/submit?token=${currentShareToken.value}`,
|
||
imageUrl: sharingItem.value.image || '/static/logo.png'
|
||
}
|
||
})
|
||
|
||
async function onInvite(item) {
|
||
vibrateShort()
|
||
const user_id = uni.getStorageSync('user_id')
|
||
if (!user_id) {
|
||
uni.navigateTo({ url: '/pages/login/index' })
|
||
return
|
||
}
|
||
|
||
// 获取第一个可用的 inventory id
|
||
const invId = item.original_ids && item.original_ids[0]
|
||
if (!invId) {
|
||
uni.showToast({ title: '无效的资产', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
uni.showLoading({ title: '准备分享...' })
|
||
try {
|
||
const res = await createAddressShare(user_id, invId)
|
||
// 兼容不同的数据返回格式
|
||
currentShareToken.value = res.data?.share_token || res.share_token
|
||
currentShortLink.value = res.data?.short_link || res.short_link || ''
|
||
|
||
// 准备分享预览数据
|
||
sharingItem.value = {
|
||
id: invId,
|
||
name: item.name,
|
||
image: item.image,
|
||
count: item.count
|
||
}
|
||
|
||
// 显示分享弹窗
|
||
showSharePopup.value = true
|
||
} catch (e) {
|
||
uni.showToast({ title: e.message || '生成失败', icon: 'none' })
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
|
||
function onCopyShareLink() {
|
||
let url = currentShortLink.value
|
||
if (!url) {
|
||
url = `${window?.location?.origin || ''}/pages-user/address/submit?token=${currentShareToken.value}`
|
||
}
|
||
|
||
uni.setClipboardData({
|
||
data: url,
|
||
success: () => {
|
||
uni.showToast({ title: '已复制链接', icon: 'success' })
|
||
showSharePopup.value = false
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* ============================================
|
||
柯大鸭潮玩 - 盒柜页面
|
||
采用现代卡片式布局,统一设计语言
|
||
============================================ */
|
||
|
||
.wrap {
|
||
padding: 0;
|
||
min-height: 100vh;
|
||
background: $bg-page;
|
||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 顶部 Tab */
|
||
.tabs {
|
||
@extend .glass-card;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
padding: 0;
|
||
margin: 0;
|
||
border-radius: 0;
|
||
border-top: none;
|
||
border-left: none;
|
||
border-right: none;
|
||
}
|
||
|
||
.tab-item {
|
||
position: relative;
|
||
flex: 1;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 28rpx;
|
||
color: $text-sub;
|
||
transition: all 0.3s;
|
||
padding: 0;
|
||
border-radius: 0;
|
||
|
||
&.active {
|
||
color: $brand-primary;
|
||
font-weight: 700;
|
||
font-size: 30rpx;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 40rpx;
|
||
height: 4rpx;
|
||
background: $brand-primary;
|
||
border-radius: 4rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.tab-count {
|
||
margin-left: 8rpx;
|
||
font-size: 20rpx;
|
||
background: rgba($brand-primary, 0.1);
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 20rpx;
|
||
color: $brand-primary;
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 状态提示 */
|
||
.status-text {
|
||
padding-top: 200rpx;
|
||
text-align: center;
|
||
color: $text-tertiary;
|
||
font-size: 28rpx;
|
||
margin-top: 0;
|
||
}
|
||
.loading-more, .no-more {
|
||
text-align: center;
|
||
padding: 30rpx;
|
||
color: $text-tertiary;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
/* Tab 0: 待处理列表 */
|
||
.action-bar {
|
||
margin-top: 108rpx; /* 88rpx tabs + 20rpx spacing */
|
||
padding: 0 $spacing-lg;
|
||
margin-bottom: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.select-all {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 28rpx;
|
||
color: $text-main;
|
||
}
|
||
|
||
.inventory-grid {
|
||
padding: 0 $spacing-lg;
|
||
margin-top: 108rpx; /* default margin if no action bar */
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
}
|
||
/* Adjust margin if action bar exists */
|
||
.action-bar + .inventory-grid {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.inventory-item {
|
||
background: $bg-card;
|
||
border-radius: $radius-lg;
|
||
padding: 24rpx;
|
||
margin-bottom: 0; /* handled by gap */
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
box-shadow: $shadow-sm;
|
||
border: 1rpx solid rgba(0,0,0,0.02);
|
||
transition: all 0.2s ease;
|
||
animation: fadeInUp 0.4s ease-out backwards;
|
||
|
||
@for $i from 1 through 10 {
|
||
&:nth-child(#{$i}) {
|
||
animation-delay: #{$i * 0.05}s;
|
||
}
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
|
||
.checkbox-area {
|
||
padding: 20rpx 20rpx 20rpx 0;
|
||
}
|
||
.checkbox {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border: 2rpx solid $text-tertiary;
|
||
border-radius: 50%;
|
||
transition: all 0.2s;
|
||
|
||
&.checked {
|
||
background: $brand-primary;
|
||
border-color: $brand-primary;
|
||
position: relative;
|
||
box-shadow: 0 0 10rpx rgba($brand-primary, 0.3);
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 18rpx;
|
||
height: 10rpx;
|
||
border-left: 3rpx solid #fff;
|
||
border-bottom: 3rpx solid #fff;
|
||
transform: translate(-50%, -65%) rotate(-45deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
.item-image {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: $radius-md;
|
||
background: $bg-page;
|
||
flex-shrink: 0;
|
||
margin-right: 0; /* reset old margin */
|
||
}
|
||
.item-info {
|
||
flex: 1;
|
||
margin-left: 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
height: 140rpx;
|
||
text-align: left; /* reset old text-align */
|
||
}
|
||
.item-name {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: $text-main;
|
||
@include text-ellipsis(2);
|
||
margin-bottom: 0; /* reset old margin */
|
||
}
|
||
.item-price {
|
||
font-size: 24rpx;
|
||
color: $brand-primary;
|
||
margin-top: 8rpx;
|
||
font-weight: 600;
|
||
}
|
||
.item-actions {
|
||
margin-top: auto;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.invite-btn {
|
||
font-size: 22rpx;
|
||
color: $brand-primary;
|
||
background: rgba($brand-primary, 0.1);
|
||
padding: 6rpx 16rpx;
|
||
border-radius: 20rpx;
|
||
font-weight: 500;
|
||
|
||
&:active {
|
||
opacity: 0.7;
|
||
background: rgba($brand-primary, 0.2);
|
||
}
|
||
}
|
||
.item-count {
|
||
font-size: 28rpx;
|
||
color: $text-main;
|
||
font-weight: 600;
|
||
}
|
||
.stepper {
|
||
display: flex;
|
||
align-items: center;
|
||
background: $bg-page;
|
||
border-radius: $radius-sm;
|
||
padding: 4rpx;
|
||
height: auto; /* reset old height */
|
||
border: none; /* reset old border */
|
||
}
|
||
.step-btn {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 32rpx;
|
||
color: $text-main;
|
||
background: transparent;
|
||
|
||
&.minus { color: $text-sub; border: none; }
|
||
&.plus { color: $brand-primary; border: none; }
|
||
|
||
&:active { opacity: 0.6; background: transparent !important; transform: scale(0.9); }
|
||
}
|
||
.step-num {
|
||
min-width: 60rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: $text-main;
|
||
}
|
||
|
||
/* 底部操作栏 */
|
||
.bottom-bar {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: rgba($bg-card, 0.95);
|
||
backdrop-filter: blur(20rpx);
|
||
padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
|
||
z-index: 99;
|
||
height: auto; /* reset old height */
|
||
animation: slideUp 0.3s ease-out;
|
||
}
|
||
.selected-info {
|
||
font-size: 28rpx;
|
||
color: $text-main;
|
||
font-weight: 600;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6rpx;
|
||
}
|
||
.btn-group {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
.action-btn {
|
||
height: 72rpx;
|
||
padding: 0 40rpx;
|
||
border-radius: $radius-round;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s;
|
||
margin: 0;
|
||
line-height: 1; /* reset line-height */
|
||
|
||
&:active { transform: scale(0.96); }
|
||
}
|
||
.btn-ship {
|
||
background: rgba($brand-primary, 0.1);
|
||
color: $brand-primary;
|
||
box-shadow: none; /* reset shadow */
|
||
}
|
||
.btn-redeem {
|
||
background: $gradient-brand;
|
||
color: #fff;
|
||
box-shadow: $shadow-warm;
|
||
animation: none; /* reset animation */
|
||
}
|
||
|
||
/* Tab 1: 已申请发货 */
|
||
.shipment-list {
|
||
padding: 0 $spacing-lg;
|
||
margin-top: 108rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
}
|
||
.shipment-card {
|
||
background: $bg-card;
|
||
border-radius: $radius-lg;
|
||
padding: 30rpx;
|
||
margin-bottom: 0; /* handled by gap */
|
||
box-shadow: $shadow-sm;
|
||
border: 1rpx solid rgba(0,0,0,0.02);
|
||
animation: fadeInUp 0.4s ease-out backwards;
|
||
}
|
||
.shipment-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
padding-bottom: 20rpx;
|
||
border-bottom: 1rpx solid rgba(0,0,0,0.05);
|
||
}
|
||
.shipment-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 10rpx;
|
||
}
|
||
.shipment-batch {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
.batch-label {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
}
|
||
.batch-no {
|
||
font-size: 24rpx;
|
||
color: $text-sub;
|
||
font-family: monospace;
|
||
}
|
||
.count-badge {
|
||
font-size: 20rpx;
|
||
color: $brand-primary;
|
||
background: rgba($brand-primary, 0.1);
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
.shipment-status {
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
padding: 4rpx 16rpx;
|
||
border-radius: 20rpx;
|
||
|
||
&.status-pending { background: #FFF7E6; color: #FA8C16; }
|
||
&.status-shipped { background: #E6F7FF; color: #1890FF; }
|
||
&.status-delivered { background: #F6FFED; color: #52C41A; }
|
||
&.status-cancelled { background: #F5F5F5; color: #999; }
|
||
}
|
||
.shipment-cancel {
|
||
font-size: 22rpx;
|
||
color: $text-sub;
|
||
padding: 6rpx 14rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(0,0,0,0.04);
|
||
}
|
||
|
||
.product-thumbnails {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
.thumb-scroll {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
margin-bottom: 16rpx;
|
||
overflow-x: auto;
|
||
}
|
||
.thumb-img {
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
border-radius: $radius-sm;
|
||
background: $bg-page;
|
||
flex-shrink: 0;
|
||
border: 1rpx solid rgba(0,0,0,0.05);
|
||
}
|
||
.thumb-more {
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
border-radius: $radius-sm;
|
||
background: $bg-page;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: $text-sub;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
.product-names {
|
||
font-size: 24rpx;
|
||
color: $text-sub;
|
||
line-height: 1.4;
|
||
display: block; /* reset flex */
|
||
}
|
||
.product-name-item {
|
||
margin-right: 12rpx;
|
||
background: transparent; /* reset bg */
|
||
padding: 0;
|
||
|
||
&:not(:last-child)::after {
|
||
content: '、';
|
||
}
|
||
}
|
||
|
||
.shipment-express {
|
||
background: $bg-page;
|
||
border-radius: $radius-md;
|
||
padding: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.express-icon {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
background: #fff;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 32rpx;
|
||
margin-right: 16rpx;
|
||
}
|
||
.express-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.express-company {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: $text-main;
|
||
}
|
||
.express-no {
|
||
font-size: 24rpx;
|
||
color: $text-sub;
|
||
margin-top: 4rpx;
|
||
}
|
||
.express-copy {
|
||
font-size: 22rpx;
|
||
color: $brand-primary;
|
||
padding: 6rpx 16rpx;
|
||
border: 1rpx solid $brand-primary;
|
||
border-radius: 20rpx;
|
||
background: transparent; /* reset bg */
|
||
box-shadow: none; /* reset shadow */
|
||
|
||
&:active { background: rgba($brand-primary, 0.05); }
|
||
}
|
||
|
||
.shipment-time {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
border-top: 1rpx dashed rgba(0,0,0,0.05);
|
||
padding-top: 20rpx;
|
||
}
|
||
.time-item {
|
||
font-size: 22rpx;
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
from { opacity: 0; transform: translateY(20rpx); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.bottom-spacer {
|
||
height: 120rpx;
|
||
}
|
||
|
||
.share-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
z-index: 998;
|
||
backdrop-filter: blur(4rpx);
|
||
}
|
||
|
||
.share-popup {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: #fff;
|
||
border-radius: 40rpx 40rpx 0 0;
|
||
z-index: 999;
|
||
padding: 40rpx;
|
||
transform: translateY(100%);
|
||
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||
|
||
&.show {
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.share-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 40rpx;
|
||
|
||
.share-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
}
|
||
|
||
.share-close {
|
||
font-size: 40rpx;
|
||
color: $text-sub;
|
||
padding: 10rpx;
|
||
}
|
||
}
|
||
|
||
.share-item-preview {
|
||
display: flex;
|
||
background: #f8f9fa;
|
||
padding: 30rpx;
|
||
border-radius: 24rpx;
|
||
margin-bottom: 50rpx;
|
||
|
||
.preview-img {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 16rpx;
|
||
margin-right: 20rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
.preview-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
|
||
.preview-name {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: $text-main;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.preview-desc {
|
||
font-size: 20rpx;
|
||
color: $text-sub;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
}
|
||
|
||
.share-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
|
||
.action-btn {
|
||
height: 90rpx;
|
||
border-radius: 45rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
|
||
&::after { border: none; }
|
||
|
||
&.share-card-btn {
|
||
background: #07c160;
|
||
color: #fff;
|
||
}
|
||
|
||
&.copy-link-btn {
|
||
background: #f0f0f0;
|
||
color: $text-main;
|
||
}
|
||
|
||
&:active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
}
|
||
</style>
|