feat:上书记录对接shop/list和product/shop-detail接口,平台+店铺联动,分页

This commit is contained in:
ShenQiLun 2026-06-30 10:16:28 +08:00
parent 5a4968881a
commit d64c9aad01
3 changed files with 426 additions and 278 deletions

View File

@ -5,30 +5,19 @@
<view class="filter-card"> <view class="filter-card">
<view class="filter-row"> <view class="filter-row">
<view class="filter-item"> <view class="filter-item">
<text class="filter-label">账号</text> <text class="filter-label">平台</text>
<picker class="filter-picker" @change="handleAccountChange" :value="accountIndex" :range="accountList"> <picker class="filter-picker" @change="handlePlatformChange" :value="platformIndex" :range="platformList">
<view class="picker-value"> <view class="picker-value">
<text class="picker-text">{{ accountList[accountIndex] }}</text> <text class="picker-text">{{ platformList[platformIndex] }}</text>
<text class="picker-arrow"></text> <text class="picker-arrow"></text>
</view> </view>
</picker> </picker>
</view> </view>
<view class="filter-item"> <view class="filter-item">
<text class="filter-label">店铺</text> <text class="filter-label">店铺</text>
<picker class="filter-picker" @change="handleShopChange" :value="shopIndex" :range="shopList"> <picker class="filter-picker" @change="handleShopChange" :value="shopIndex" :range="shopNames">
<view class="picker-value"> <view class="picker-value">
<text class="picker-text">{{ shopList[shopIndex] }}</text> <text class="picker-text">{{ shopNames[shopIndex] }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
</view>
<view class="filter-row">
<view class="filter-item full-width">
<text class="filter-label">日期</text>
<picker class="filter-picker" mode="date" @change="handleDateChange" :value="selectedDate">
<view class="picker-value">
<text class="picker-text">{{ selectedDate }}</text>
<text class="picker-arrow"></text> <text class="picker-arrow"></text>
</view> </view>
</picker> </picker>
@ -40,210 +29,300 @@
<!-- 统计信息 --> <!-- 统计信息 -->
<view class="stats-section"> <view class="stats-section">
<view class="stats-card"> <view class="stats-card">
<text class="stats-icon">📊</text> <view class="stats-item">
<text class="stats-label">总记录数</text> <text class="stats-num success">{{ stats.successCount }}</text>
<text class="stats-value">{{ recordList.length }} </text> <text class="stats-label">已发送</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-num pending">{{ stats.notSentCount }}</text>
<text class="stats-label">未发送</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-num failed">{{ stats.failedCount }}</text>
<text class="stats-label">失败</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-num total">{{ stats.total }}</text>
<text class="stats-label">总计</text>
</view>
</view> </view>
</view> </view>
<!-- 记录列表 --> <!-- 记录列表 -->
<view class="record-list"> <scroll-view class="record-scroll" scroll-y :refresher-enabled="true" :refresher-triggered="isRefreshing" @refresherrefresh="onRefresh" @scrolltolower="loadMore">
<view class="record-item" v-for="(item, index) in recordList" :key="index"> <view class="record-list">
<view class="record-main"> <view class="record-item" v-for="(item, index) in recordList" :key="index">
<!-- 图片 --> <view class="record-main">
<view class="record-image" @click="previewImage(item.image)"> <!-- 图片 -->
<image class="book-image" :src="item.image" mode="aspectFill"></image> <view class="record-image" @click="previewImage(item.live_image)">
<view class="image-overlay"> <image class="book-image" :src="getFirstImage(item.live_image)" mode="aspectFill"></image>
<text class="zoom-icon">🔍</text> </view>
<!-- 基本信息 -->
<view class="record-info">
<view class="info-row">
<text class="book-name">{{ item.name }}</text>
</view>
<view class="info-row">
<text class="info-label">ISBN</text>
<text class="info-value">{{ item.barcode || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">波次</text>
<text class="info-value">{{ item.wave_no || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">货位</text>
<text class="info-value">{{ item.warehouse_name }}{{ item.location_code ? ' - ' + item.location_code : '' }}</text>
</view>
<view class="info-row">
<text class="info-label">时间</text>
<text class="info-value">{{ formatTime(item.created_at) }}</text>
</view>
</view> </view>
</view> </view>
<!-- 基本信息 --> <!-- 价格和库存 -->
<view class="record-info"> <view class="record-detail">
<view class="info-row"> <view class="detail-row">
<text class="book-name">{{ item.name }}</text> <view class="detail-item">
</view> <text class="detail-label">定价</text>
<view class="info-row"> <text class="detail-value">¥{{ formatPrice(item.price) }}</text>
<text class="info-label">ISBN</text> </view>
<text class="info-value">{{ item.isbn }}</text> <view class="detail-item">
</view> <text class="detail-label">售价</text>
<view class="info-row"> <text class="detail-value price">¥{{ formatPrice(item.sale_price) }}</text>
<text class="info-label">品相</text> </view>
<text class="condition-badge" :class="getConditionClass(item.condition)">{{ item.condition }}</text> <view class="detail-item">
</view> <text class="detail-label">库存</text>
<view class="info-row"> <text class="detail-value">{{ item.quantity }}</text>
<text class="info-label">上书时间</text> </view>
<text class="info-value">{{ item.uploadTime }}</text>
</view> </view>
</view> </view>
</view> <!-- 状态 -->
<!-- 价格和库存 --> <view class="publish-status">
<view class="record-detail"> <view class="status-row">
<view class="detail-row"> <text class="status-tag" :class="getStatusClass(item.status_in_shop)">{{ item.msg || getStatusText(item.status_in_shop) }}</text>
<view class="detail-item">
<text class="detail-label">库存</text>
<text class="detail-value">{{ item.stock }}</text>
</view>
<view class="detail-item">
<text class="detail-label">价格</text>
<text class="detail-value price">¥{{ item.price }}</text>
</view>
</view>
</view>
<!-- 发布状态 -->
<view class="publish-status">
<view class="status-row">
<view class="status-item" :class="{ published: item.pddPublished }">
<text class="status-icon">{{ item.pddPublished ? '✓' : '✗' }}</text>
<text class="status-text">拼多多{{ item.pddPublished ? '已发布' : '未发布' }}</text>
</view>
<view class="status-item" :class="{ published: item.kfzPublished }">
<text class="status-icon">{{ item.kfzPublished ? '✓' : '✗' }}</text>
<text class="status-text">孔夫子{{ item.kfzPublished ? '已发布' : '未发布' }}</text>
</view>
<view class="status-item" :class="{ published: item.xianyuPublished }">
<text class="status-icon">{{ item.xianyuPublished ? '✓' : '✗' }}</text>
<text class="status-text">闲鱼{{ item.xianyuPublished ? '已发布' : '未发布' }}</text>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view>
<!-- 空状态 --> <!-- 加载更多 -->
<view class="empty-state" v-if="recordList.length === 0"> <view class="load-more" v-if="loadingMore">
<text class="empty-icon">📭</text> <view class="loading-spinner"></view>
<text class="empty-text">暂无上书记录</text> <text class="load-more-text">加载中...</text>
</view> </view>
<view class="load-more" v-if="!hasMore && recordList.length > 0">
<text class="no-more-text"> 已全部加载 </text>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="recordList.length === 0 && !isLoading">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无上书记录</text>
</view>
</scroll-view>
</view> </view>
</template> </template>
<script> <script>
import { getShopList, getShopDetail } from '@/utils/api.js'
export default { export default {
data() { data() {
return { return {
// //
accountIndex: 0, platformIndex: 0,
accountList: ['全部账号', '账号A', '账号B', '账号C'], platformList: ['全部平台', '拼多多', '孔夫子', '闲鱼'],
shopIndex: 0, platformTypes: [0, 1, 2, 5],
shopList: ['全部店铺', '店铺1', '店铺2', '店铺3'],
selectedDate: this.getTodayDate(),
// //
recordList: [ shopIndex: 0,
{ shopList: [],
image: 'https://picsum.photos/200/280?random=1', shopNames: ['全部店铺'],
name: '红楼梦',
isbn: '9787020002207', //
condition: '全新', page: 1,
uploadTime: '2024-01-15 14:30', pageSize: 10,
stock: 5, hasMore: true,
price: '45.00', isLoading: false,
pddPublished: true, loadingMore: false,
kfzPublished: true, isRefreshing: false,
xianyuPublished: false
}, //
{ recordList: [],
image: 'https://picsum.photos/200/280?random=2', stats: { successCount: 0, notSentCount: 0, failedCount: 0, total: 0 }
name: '西游记',
isbn: '9787020002214',
condition: '九成新',
uploadTime: '2024-01-15 15:20',
stock: 3,
price: '38.00',
pddPublished: true,
kfzPublished: false,
xianyuPublished: true
},
{
image: 'https://picsum.photos/200/280?random=3',
name: '水浒传',
isbn: '9787020002221',
condition: '八成新',
uploadTime: '2024-01-15 16:45',
stock: 2,
price: '32.00',
pddPublished: false,
kfzPublished: true,
xianyuPublished: true
},
{
image: 'https://picsum.photos/200/280?random=4',
name: '三国演义',
isbn: '9787020002238',
condition: '全新',
uploadTime: '2024-01-15 17:10',
stock: 8,
price: '52.00',
pddPublished: true,
kfzPublished: true,
xianyuPublished: true
}
]
} }
}, },
onLoad() { onLoad() {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({ title: '上书记录' })
title: '上书记录' this.loadShopList()
}) this.fetchRecords()
}, },
methods: { methods: {
// //
getTodayDate() { async loadShopList() {
const today = new Date() try {
const year = today.getFullYear() const platformType = this.platformTypes[this.platformIndex]
const month = String(today.getMonth() + 1).padStart(2, '0') const params = { pageNum: 1, pageSize: 100 }
const day = String(today.getDate()).padStart(2, '0') if (platformType > 0) params.shop_type = String(platformType)
return `${year}-${month}-${day}` const res = await getShopList(params)
this.shopList = res.list || []
this.updateShopNames()
} catch (e) {
console.error('加载店铺列表失败:', e)
}
}, },
// //
handleAccountChange(e) { updateShopNames() {
this.accountIndex = e.detail.value const names = ['全部店铺']
this.filterRecords() this.shopList.forEach(function(s) {
names.push(s.shop_alias_name || s.shop_name || '未命名')
})
this.shopNames = names
if (this.shopIndex >= names.length) {
this.shopIndex = 0
}
}, },
// //
handlePlatformChange(e) {
this.platformIndex = e.detail.value
this.shopIndex = 0
this.loadShopList()
this.resetAndFetch()
},
//
handleShopChange(e) { handleShopChange(e) {
this.shopIndex = e.detail.value this.shopIndex = e.detail.value
this.filterRecords() this.resetAndFetch()
}, },
// //
handleDateChange(e) { resetAndFetch() {
this.selectedDate = e.detail.value this.page = 1
this.filterRecords() this.hasMore = true
this.recordList = []
this.stats = { successCount: 0, notSentCount: 0, failedCount: 0, total: 0 }
this.fetchRecords()
}, },
// // ID
filterRecords() { getSelectedShopId() {
// TODO: if (this.shopIndex <= 0) return ''
uni.showToast({ const shop = this.shopList[this.shopIndex - 1]
title: '筛选条件已更新', return shop ? shop.id : ''
icon: 'none'
})
}, },
// //
getConditionClass(condition) { async fetchRecords() {
const classMap = { if (this.isLoading || this.loadingMore) return
'全新': 'condition-new', this.isLoading = true
'九成新': 'condition-good', try {
'八成新': 'condition-fair', const shopId = this.getSelectedShopId()
'七成新': 'condition-poor' if (!shopId) {
this.isLoading = false
this.recordList = []
this.hasMore = false
return
}
const params = {
page: this.page,
page_size: this.pageSize,
shop_id: shopId
}
const res = await getShopDetail(params)
const products = res.products || []
if (this.page === 1) {
this.recordList = products
} else {
this.recordList = this.recordList.concat(products)
}
this.hasMore = products.length >= this.pageSize
this.stats = {
successCount: res.success_count || 0,
notSentCount: res.not_sent_count || 0,
failedCount: res.failed_count || 0,
total: res.total || 0
}
} catch (e) {
console.error('获取上书记录失败:', e)
} finally {
this.isLoading = false
this.loadingMore = false
this.isRefreshing = false
} }
return classMap[condition] || 'condition-default' },
//
onRefresh() {
this.isRefreshing = true
this.page = 1
this.hasMore = true
this.fetchRecords()
},
//
loadMore() {
if (this.loadingMore || !this.hasMore) return
this.loadingMore = true
this.page++
this.fetchRecords()
},
//
getFirstImage(images) {
if (!images) return ''
if (typeof images === 'string') return images
return images[0] || ''
},
//
formatPrice(price) {
if (!price && price !== 0) return '0.00'
return (parseFloat(price) / 100).toFixed(2)
},
//
formatTime(timestamp) {
if (!timestamp) return '-'
const date = new Date(parseInt(timestamp) * 1000)
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const mi = String(date.getMinutes()).padStart(2, '0')
return y + '-' + m + '-' + d + ' ' + h + ':' + mi
},
// CSS
getStatusClass(status) {
if (status === 1) return 'status-success'
if (status === 0) return 'status-pending'
return 'status-failed'
},
//
getStatusText(status) {
if (status === 1) return '已发布到店铺'
if (status === 0) return '未发送到店铺'
return '发布失败'
}, },
// //
previewImage(imageUrl) { previewImage(images) {
if (!images) return
const urls = typeof images === 'string' ? [images] : images
if (urls.length === 0) return
uni.previewImage({ uni.previewImage({
urls: [imageUrl], urls: urls,
current: imageUrl, current: urls[0]
longPressActions: {
itemList: ['发送给朋友', '保存图片', '收藏']
}
}) })
} }
} }
@ -286,10 +365,6 @@ export default {
align-items: center; align-items: center;
} }
.filter-item.full-width {
flex: 1;
}
.filter-label { .filter-label {
font-size: 26rpx; font-size: 26rpx;
color: #4e5969; color: #4e5969;
@ -328,30 +403,47 @@ export default {
.stats-card { .stats-card {
background: #ffffff; background: #ffffff;
border-radius: 16rpx; border-radius: 16rpx;
padding: 24rpx; padding: 20rpx 24rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
} }
.stats-icon { .stats-item {
font-size: 40rpx; display: flex;
margin-right: 16rpx; flex-direction: column;
} align-items: center;
gap: 6rpx;
.stats-label {
font-size: 28rpx;
color: #4e5969;
flex: 1; flex: 1;
} }
.stats-value { .stats-num {
font-size: 36rpx; font-size: 36rpx;
color: #1d2129;
font-weight: 700; font-weight: 700;
} }
/* 记录列表 */ .stats-num.success { color: #67c23a; }
.stats-num.pending { color: #e6a23c; }
.stats-num.failed { color: #f56c6c; }
.stats-num.total { color: #409eff; }
.stats-label {
font-size: 22rpx;
color: #909399;
}
.stats-divider {
width: 1px;
height: 48rpx;
background: #ebeef5;
}
/* 滚动列表 */
.record-scroll {
height: calc(100vh - 300rpx);
}
.record-list { .record-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -377,7 +469,6 @@ export default {
height: 180rpx; height: 180rpx;
border-radius: 12rpx; border-radius: 12rpx;
overflow: hidden; overflow: hidden;
position: relative;
flex-shrink: 0; flex-shrink: 0;
} }
@ -386,34 +477,12 @@ export default {
height: 100%; height: 100%;
} }
.image-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60rpx;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.5));
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.record-image:active .image-overlay {
opacity: 1;
}
.zoom-icon {
font-size: 28rpx;
}
/* 基本信息 */ /* 基本信息 */
.record-info { .record-info {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10rpx; gap: 8rpx;
} }
.info-row { .info-row {
@ -422,14 +491,17 @@ export default {
} }
.book-name { .book-name {
font-size: 32rpx; font-size: 30rpx;
color: #1d2129; color: #1d2129;
font-weight: 600; font-weight: 600;
lines: 1;
overflow: hidden;
} }
.info-label { .info-label {
font-size: 24rpx; font-size: 24rpx;
color: #86909c; color: #86909c;
flex-shrink: 0;
} }
.info-value { .info-value {
@ -437,38 +509,6 @@ export default {
color: #4e5969; color: #4e5969;
} }
.condition-badge {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 500;
}
.condition-new {
background: #e1f3d8;
color: #67c23a;
}
.condition-good {
background: #d9ecff;
color: #409eff;
}
.condition-fair {
background: #faecd8;
color: #e6a23c;
}
.condition-poor {
background: #fde2e2;
color: #f56c6c;
}
.condition-default {
background: #f4f4f5;
color: #909399;
}
/* 价格和库存 */ /* 价格和库存 */
.record-detail { .record-detail {
background: #f5f6fa; background: #f5f6fa;
@ -504,7 +544,7 @@ export default {
color: #f56c6c; color: #f56c6c;
} }
/* 发布状态 */ /* 状态 */
.publish-status { .publish-status {
border-top: 1rpx solid #ebeef5; border-top: 1rpx solid #ebeef5;
padding-top: 16rpx; padding-top: 16rpx;
@ -512,40 +552,63 @@ export default {
.status-row { .status-row {
display: flex; display: flex;
justify-content: space-between;
}
.status-item {
display: flex;
align-items: center;
gap: 6rpx;
}
.status-icon {
font-size: 20rpx;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #c0c4cc;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center; justify-content: center;
} }
.status-item.published .status-icon { .status-tag {
background: #67c23a;
}
.status-text {
font-size: 22rpx; font-size: 22rpx;
color: #86909c; padding: 6rpx 20rpx;
border-radius: 8rpx;
} }
.status-item.published .status-text { .status-success {
background: #e1f3d8;
color: #67c23a; color: #67c23a;
} }
.status-pending {
background: #faecd8;
color: #e6a23c;
}
.status-failed {
background: #fde2e2;
color: #f56c6c;
}
/* 加载更多 */
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
gap: 12rpx;
}
.load-more-text {
font-size: 24rpx;
color: #909399;
}
.no-more-text {
font-size: 24rpx;
color: #c0c4cc;
}
.loading-spinner {
width: 32rpx;
height: 32rpx;
border: 3rpx solid #e4e7ed;
border-top-color: #409eff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 空状态 */ /* 空状态 */
.empty-state { .empty-state {
display: flex; display: flex;

View File

@ -199,8 +199,9 @@
<!-- 上书记录 --> <!-- 上书记录 -->
<view class="form-section"> <view class="form-section">
<view class="section-title"> <view class="section-title" @click="goRecordPage">
<text class="title-text">上书记录</text> <text class="title-text">上书记录</text>
<text class="title-more">查看全部 </text>
</view> </view>
<view class="history-list"> <view class="history-list">
<view class="history-item" v-for="(item, index) in historyList" :key="index"> <view class="history-item" v-for="(item, index) in historyList" :key="index">
@ -505,8 +506,9 @@
<!-- ===== 上书记录 ===== --> <!-- ===== 上书记录 ===== -->
<view class="form-section" v-if="noIsbnHistoryList.length > 0"> <view class="form-section" v-if="noIsbnHistoryList.length > 0">
<view class="section-title"> <view class="section-title" @click="goRecordPage">
<text class="title-text">上书记录</text> <text class="title-text">上书记录</text>
<text class="title-more">查看全部 </text>
</view> </view>
<view class="history-list"> <view class="history-list">
<view class="history-item" v-for="(item, index) in noIsbnHistoryList" :key="index"> <view class="history-item" v-for="(item, index) in noIsbnHistoryList" :key="index">
@ -2595,6 +2597,11 @@ export default {
this.pendingCount = 0 this.pendingCount = 0
}, },
//
goRecordPage() {
uni.navigateTo({ url: '/pages/record/record' })
},
// 2 // 2
requestWithRetry(options, maxRetries) { requestWithRetry(options, maxRetries) {
if (maxRetries === undefined) maxRetries = 2 if (maxRetries === undefined) maxRetries = 2
@ -3851,6 +3858,12 @@ export default {
font-weight: 600; font-weight: 600;
} }
.title-more {
font-size: 24rpx;
color: #409eff;
margin-left: auto;
}
.photo-count { .photo-count {
font-size: 22rpx; font-size: 22rpx;
color: #909399; color: #909399;

View File

@ -360,6 +360,76 @@ function buildFormBodyWithImages(params, imageUrls, imageKey) {
export { buildFormBodyWithImages } export { buildFormBodyWithImages }
/**
* 获取店铺列表上书记录用
* @param {Object} params - { pageNum, pageSize, shop_type }
*/
export function getShopList(params = {}) {
return requestWithRetry((token) => {
return new Promise((resolve, reject) => {
const url = generateSignedUrl(`${BASE_URL}/api/shop/list`, params)
console.log('【店铺列表】请求URL:', url)
uni.request({
url: url,
method: 'GET',
header: {
'Authorization': 'Bearer ' + token
},
success: (res) => {
console.log('【店铺列表】响应:', JSON.stringify(res.data))
if (res.statusCode === 200 && res.data && res.data.code === 0) {
resolve(res.data.data)
} else {
resolve({ list: [], total: 0 })
}
},
fail: (err) => {
console.error('【店铺列表】请求失败:', err)
reject(err)
}
})
})
}, '店铺列表')
}
/**
* 获取店铺上书详情上书记录列表
* @param {Object} params - { page, page_size, shop_id }
*/
export function getShopDetail(params = {}) {
return requestWithRetry((token) => {
return new Promise((resolve, reject) => {
const url = generateSignedUrl(`${BASE_URL}/api/product/shop-detail`, params)
console.log('【上书详情】请求URL:', url)
uni.request({
url: url,
method: 'GET',
header: {
'Authorization': 'Bearer ' + token
},
success: (res) => {
console.log('【上书详情】响应:', JSON.stringify(res.data))
if (res.statusCode === 200 && res.data) {
if (res.data.code === 200 && res.data.data) {
resolve(res.data.data)
} else if (res.data.data) {
resolve(res.data.data)
} else {
resolve({ products: [], total: 0, page: 1, page_size: 10 })
}
} else {
resolve({ products: [], total: 0, page: 1, page_size: 10 })
}
},
fail: (err) => {
console.error('【上书详情】请求失败:', err)
reject(err)
}
})
})
}, '上书详情')
}
export default { export default {
calculateSign, calculateSign,
buildFormBodyWithImages, buildFormBodyWithImages,
@ -367,5 +437,7 @@ export default {
getLocationList, getLocationList,
psiLogin, psiLogin,
searchBookByIsbn, searchBookByIsbn,
generateSignedUrl generateSignedUrl,
getShopList,
getShopDetail
} }