1391 lines
37 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>