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-row">
<view class="filter-item">
<text class="filter-label">账号</text>
<picker class="filter-picker" @change="handleAccountChange" :value="accountIndex" :range="accountList">
<text class="filter-label">平台</text>
<picker class="filter-picker" @change="handlePlatformChange" :value="platformIndex" :range="platformList">
<view class="picker-value">
<text class="picker-text">{{ accountList[accountIndex] }}</text>
<text class="picker-text">{{ platformList[platformIndex] }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<view class="filter-item">
<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">
<text class="picker-text">{{ shopList[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-text">{{ shopNames[shopIndex] }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
@ -40,210 +29,300 @@
<!-- 统计信息 -->
<view class="stats-section">
<view class="stats-card">
<text class="stats-icon">📊</text>
<text class="stats-label">总记录数</text>
<text class="stats-value">{{ recordList.length }} </text>
<view class="stats-item">
<text class="stats-num success">{{ stats.successCount }}</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 class="record-list">
<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="image-overlay">
<text class="zoom-icon">🔍</text>
<scroll-view class="record-scroll" scroll-y :refresher-enabled="true" :refresher-triggered="isRefreshing" @refresherrefresh="onRefresh" @scrolltolower="loadMore">
<view class="record-list">
<view class="record-item" v-for="(item, index) in recordList" :key="index">
<view class="record-main">
<!-- 图片 -->
<view class="record-image" @click="previewImage(item.live_image)">
<image class="book-image" :src="getFirstImage(item.live_image)" mode="aspectFill"></image>
</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 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.isbn }}</text>
</view>
<view class="info-row">
<text class="info-label">品相</text>
<text class="condition-badge" :class="getConditionClass(item.condition)">{{ item.condition }}</text>
</view>
<view class="info-row">
<text class="info-label">上书时间</text>
<text class="info-value">{{ item.uploadTime }}</text>
<!-- 价格和库存 -->
<view class="record-detail">
<view class="detail-row">
<view class="detail-item">
<text class="detail-label">定价</text>
<text class="detail-value">¥{{ formatPrice(item.price) }}</text>
</view>
<view class="detail-item">
<text class="detail-label">售价</text>
<text class="detail-value price">¥{{ formatPrice(item.sale_price) }}</text>
</view>
<view class="detail-item">
<text class="detail-label">库存</text>
<text class="detail-value">{{ item.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 价格和库存 -->
<view class="record-detail">
<view class="detail-row">
<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 class="publish-status">
<view class="status-row">
<text class="status-tag" :class="getStatusClass(item.status_in_shop)">{{ item.msg || getStatusText(item.status_in_shop) }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="recordList.length === 0">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无上书记录</text>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="loadingMore">
<view class="loading-spinner"></view>
<text class="load-more-text">加载中...</text>
</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>
</template>
<script>
import { getShopList, getShopDetail } from '@/utils/api.js'
export default {
data() {
return {
//
accountIndex: 0,
accountList: ['全部账号', '账号A', '账号B', '账号C'],
shopIndex: 0,
shopList: ['全部店铺', '店铺1', '店铺2', '店铺3'],
selectedDate: this.getTodayDate(),
//
platformIndex: 0,
platformList: ['全部平台', '拼多多', '孔夫子', '闲鱼'],
platformTypes: [0, 1, 2, 5],
//
recordList: [
{
image: 'https://picsum.photos/200/280?random=1',
name: '红楼梦',
isbn: '9787020002207',
condition: '全新',
uploadTime: '2024-01-15 14:30',
stock: 5,
price: '45.00',
pddPublished: true,
kfzPublished: true,
xianyuPublished: false
},
{
image: 'https://picsum.photos/200/280?random=2',
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
}
]
//
shopIndex: 0,
shopList: [],
shopNames: ['全部店铺'],
//
page: 1,
pageSize: 10,
hasMore: true,
isLoading: false,
loadingMore: false,
isRefreshing: false,
//
recordList: [],
stats: { successCount: 0, notSentCount: 0, failedCount: 0, total: 0 }
}
},
onLoad() {
uni.setNavigationBarTitle({
title: '上书记录'
})
uni.setNavigationBarTitle({ title: '上书记录' })
this.loadShopList()
this.fetchRecords()
},
methods: {
//
getTodayDate() {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
//
async loadShopList() {
try {
const platformType = this.platformTypes[this.platformIndex]
const params = { pageNum: 1, pageSize: 100 }
if (platformType > 0) params.shop_type = String(platformType)
const res = await getShopList(params)
this.shopList = res.list || []
this.updateShopNames()
} catch (e) {
console.error('加载店铺列表失败:', e)
}
},
//
handleAccountChange(e) {
this.accountIndex = e.detail.value
this.filterRecords()
//
updateShopNames() {
const names = ['全部店铺']
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) {
this.shopIndex = e.detail.value
this.filterRecords()
this.resetAndFetch()
},
//
handleDateChange(e) {
this.selectedDate = e.detail.value
this.filterRecords()
//
resetAndFetch() {
this.page = 1
this.hasMore = true
this.recordList = []
this.stats = { successCount: 0, notSentCount: 0, failedCount: 0, total: 0 }
this.fetchRecords()
},
//
filterRecords() {
// TODO:
uni.showToast({
title: '筛选条件已更新',
icon: 'none'
})
// ID
getSelectedShopId() {
if (this.shopIndex <= 0) return ''
const shop = this.shopList[this.shopIndex - 1]
return shop ? shop.id : ''
},
//
getConditionClass(condition) {
const classMap = {
'全新': 'condition-new',
'九成新': 'condition-good',
'八成新': 'condition-fair',
'七成新': 'condition-poor'
//
async fetchRecords() {
if (this.isLoading || this.loadingMore) return
this.isLoading = true
try {
const shopId = this.getSelectedShopId()
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({
urls: [imageUrl],
current: imageUrl,
longPressActions: {
itemList: ['发送给朋友', '保存图片', '收藏']
}
urls: urls,
current: urls[0]
})
}
}
@ -286,10 +365,6 @@ export default {
align-items: center;
}
.filter-item.full-width {
flex: 1;
}
.filter-label {
font-size: 26rpx;
color: #4e5969;
@ -328,30 +403,47 @@ export default {
.stats-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
padding: 20rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-around;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.stats-icon {
font-size: 40rpx;
margin-right: 16rpx;
}
.stats-label {
font-size: 28rpx;
color: #4e5969;
.stats-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
flex: 1;
}
.stats-value {
.stats-num {
font-size: 36rpx;
color: #1d2129;
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 {
display: flex;
flex-direction: column;
@ -377,7 +469,6 @@ export default {
height: 180rpx;
border-radius: 12rpx;
overflow: hidden;
position: relative;
flex-shrink: 0;
}
@ -386,34 +477,12 @@ export default {
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 {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
gap: 8rpx;
}
.info-row {
@ -422,14 +491,17 @@ export default {
}
.book-name {
font-size: 32rpx;
font-size: 30rpx;
color: #1d2129;
font-weight: 600;
lines: 1;
overflow: hidden;
}
.info-label {
font-size: 24rpx;
color: #86909c;
flex-shrink: 0;
}
.info-value {
@ -437,38 +509,6 @@ export default {
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 {
background: #f5f6fa;
@ -504,7 +544,7 @@ export default {
color: #f56c6c;
}
/* 发布状态 */
/* 状态 */
.publish-status {
border-top: 1rpx solid #ebeef5;
padding-top: 16rpx;
@ -512,40 +552,63 @@ export default {
.status-row {
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;
}
.status-item.published .status-icon {
background: #67c23a;
}
.status-text {
.status-tag {
font-size: 22rpx;
color: #86909c;
padding: 6rpx 20rpx;
border-radius: 8rpx;
}
.status-item.published .status-text {
.status-success {
background: #e1f3d8;
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 {
display: flex;

View File

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

View File

@ -360,6 +360,76 @@ function buildFormBodyWithImages(params, imageUrls, imageKey) {
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 {
calculateSign,
buildFormBodyWithImages,
@ -367,5 +437,7 @@ export default {
getLocationList,
psiLogin,
searchBookByIsbn,
generateSignedUrl
generateSignedUrl,
getShopList,
getShopDetail
}