415 lines
16 KiB
Vue
415 lines
16 KiB
Vue
<template>
|
||
<el-card class="release-record-manager">
|
||
<template #header>
|
||
<div class="card-header">发布记录</div>
|
||
</template>
|
||
<div class="filter-bar">
|
||
<el-select v-model="searchParams.shop_type" placeholder="店铺类型" clearable style="width: 140px"
|
||
@change="handleSearch">
|
||
<el-option label="拼多多" :value="1" />
|
||
<el-option label="孔夫子" :value="2" />
|
||
<el-option label="闲鱼" :value="5" />
|
||
</el-select>
|
||
<el-select v-model="searchParams.status" placeholder="发布状态" clearable style="width: 140px">
|
||
<el-option label="发布成功" :value="1" />
|
||
<el-option label="发布失败" :value="0" />
|
||
</el-select>
|
||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
||
</div>
|
||
|
||
<!-- 未选择店铺类型时显示空状态 -->
|
||
<template v-if="!searchParams.shop_type">
|
||
<el-empty description="请先选择店铺类型" :image-size="200" />
|
||
</template>
|
||
|
||
<!-- 选择店铺类型后:tabs 切换 -->
|
||
<template v-else>
|
||
<el-tabs v-model="activeTab" type="border-card" @tab-change="handleTabChange">
|
||
<el-tab-pane v-for="shop in tableData" :key="shop.id" :label="shop.shop_alias_name"
|
||
:name="String(shop.id)">
|
||
<div style="margin-bottom: 10px;" class="tab-header">
|
||
<span>
|
||
<el-button type="primary" size="small" @click="handleSelectAll(shop.id)">全选</el-button>
|
||
<el-button type="success" size="small" @click="handleSelectInverse(shop.id)">反选</el-button>
|
||
<el-button type="warning" size="small">重试</el-button>
|
||
<el-button type="danger" size="small">发布</el-button>
|
||
</span>
|
||
|
||
<span>
|
||
<el-tag type="primary" size="small" style="margin-left: 5px;">发布商品 {{ (shop.products ||
|
||
[]).length }} 个</el-tag>
|
||
<el-tag type="success" size="small" style="margin-left: 5px;">成功 {{(shop.products ||
|
||
[]).filter(p => p.status_in_shop
|
||
=== 1).length}}</el-tag>
|
||
<el-tag type="danger" size="small" style="margin-left: 5px;">失败 {{(shop.products ||
|
||
[]).filter(p => p.status_in_shop
|
||
=== 0).length}}</el-tag>
|
||
</span>
|
||
</div>
|
||
|
||
<el-table :data="shop.products || []" v-loading="loading" border stripe style="width: 100%"
|
||
:ref="(el) => setTableRef(shop.id, el)">
|
||
<el-table-column type="selection" :reserve-selection="true" align="center" />
|
||
<el-table-column label="商品名称" min-width="120" show-overflow-tooltip align="center">
|
||
<template #default="{ row }">
|
||
{{ row.name || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="实拍图" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<el-image v-if="getFirstImage(row.live_image)" :src="getFirstImage(row.live_image)"
|
||
:preview-src-list="getImageList(row.live_image)"
|
||
style="width: 40px; height: 40px; border-radius: 4px" fit="cover"
|
||
preview-teleported />
|
||
<span v-else
|
||
style="width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; color: #c0c4cc; font-size: 12px">暂无</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="ISBN" min-width="130" show-overflow-tooltip align="center">
|
||
<template #default="{ row }">
|
||
{{ row.barcode || '--' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="quantity" label="库存" width="80" align="center" />
|
||
<el-table-column label="价格(元)" width="100" align="center">
|
||
<template #default="{ row }">{{ (row.sale_price / 100).toFixed(2) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="仓库-库位" min-width="120" align="center" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<el-tag size="small" type="danger">{{ row.warehouse_name || '未落位' }}</el-tag> -
|
||
<el-tag size="small">{{ row.location_code || '未落位' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="批次管理" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.is_batch_managed === 1 ? 'success' : 'info'" size="small">
|
||
{{ row.is_batch_managed === 1 ? '是' : '否' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="效期管理" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.is_shelf_life_managed === 1 ? 'success' : 'info'" size="small">
|
||
{{ row.is_shelf_life_managed === 1 ? '是' : '否' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发布状态" width="160" align="center">
|
||
<template #default="{ row }">
|
||
<template v-if="row.out_task_log_id == 0 && row.status_in_shop == 0">
|
||
<el-tag type="warning" effect="plain" size="small">任务已创建未发送到店铺</el-tag>
|
||
</template>
|
||
<template v-else-if="row.out_task_log_id > 0 && row.status_in_shop == 0">
|
||
<el-tag type="danger" effect="plain" size="small">发送到店铺失败</el-tag>
|
||
</template>
|
||
<template v-else-if="row.out_task_log_id > 0 && row.status_in_shop == 1">
|
||
<el-tag type="success" effect="plain" size="small">发布成功</el-tag>
|
||
</template>
|
||
<template v-else>
|
||
<el-tag effect="plain" size="small">未知状态</el-tag>
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发布消息" min-width="120" show-overflow-tooltip align="center">
|
||
<template #default="{ row }">
|
||
{{ row.msg || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link size="small"
|
||
@click="handleRetry(activeShopId, row)">重试</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<div class="pagination-wrapper">
|
||
<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.pageSize"
|
||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total"
|
||
layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange" />
|
||
</div>
|
||
</template>
|
||
</el-card>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
import { defineComponent, ref, reactive, computed, onMounted } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||
import dayjs from 'dayjs'
|
||
import { fetchReleaseRecordList, retryRelease } from '@/api/releaseRecord'
|
||
|
||
/** 店铺类型标签映射 */
|
||
const shopTypeMap: Record<number, string> = {
|
||
1: 'success',
|
||
2: 'warning',
|
||
5: 'info'
|
||
}
|
||
|
||
interface ProductItem {
|
||
id: number
|
||
name: string
|
||
barcode: string
|
||
live_image: any
|
||
price: number
|
||
sale_price: number
|
||
quantity: number
|
||
warehouse_name: string
|
||
location_code: string
|
||
is_batch_managed: number
|
||
is_shelf_life_managed: number
|
||
out_task_log_id: number
|
||
status_in_shop: number
|
||
msg: string
|
||
created_at: number
|
||
updated_at: number
|
||
}
|
||
|
||
interface ShopItem {
|
||
id: number
|
||
shop_name: string
|
||
shop_alias_name: string
|
||
shop_type: number
|
||
shop_type_name: string
|
||
created_at: number
|
||
updated_at: number
|
||
products: ProductItem[]
|
||
}
|
||
|
||
export default defineComponent({
|
||
name: 'ReleaseRecord',
|
||
setup() {
|
||
const loading = ref<boolean>(false)
|
||
const tableData = ref<ShopItem[]>([])
|
||
const activeTab = ref<string>('')
|
||
|
||
const searchParams = reactive<{
|
||
shop_type: number | null
|
||
status: number | null
|
||
}>({
|
||
shop_type: 1,
|
||
status: null
|
||
})
|
||
|
||
const pagination = reactive({
|
||
current: 1,
|
||
pageSize: 10,
|
||
total: 0
|
||
})
|
||
|
||
/** 当前激活的店铺 ID */
|
||
const activeShopId = computed<number>(() => Number(activeTab.value))
|
||
|
||
const shopTypeTag = (type: number): string => shopTypeMap[type] || 'info'
|
||
|
||
const formatTimestamp = (timestamp?: number | string | null): string => {
|
||
if (!timestamp && timestamp !== 0) return '-'
|
||
return dayjs.unix(Number(timestamp)).format('YYYY-MM-DD HH:mm:ss')
|
||
}
|
||
|
||
const getFirstImage = (liveImage: any): string => {
|
||
if (!liveImage) return ''
|
||
if (Array.isArray(liveImage) && liveImage.length > 0) return liveImage[0]
|
||
if (typeof liveImage === 'string') return liveImage
|
||
return ''
|
||
}
|
||
|
||
const getImageList = (liveImage: any): string[] => {
|
||
if (!liveImage) return []
|
||
if (Array.isArray(liveImage)) return liveImage.filter((url: string) => typeof url === 'string')
|
||
if (typeof liveImage === 'string') return [liveImage]
|
||
return []
|
||
}
|
||
|
||
const loadList = async (): Promise<void> => {
|
||
if (!searchParams.shop_type) {
|
||
tableData.value = []
|
||
pagination.total = 0
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
const res = await fetchReleaseRecordList({
|
||
shop_type: searchParams.shop_type || undefined,
|
||
status: searchParams.status ?? undefined,
|
||
page: pagination.current,
|
||
pageSize: pagination.pageSize
|
||
})
|
||
tableData.value = res.list || []
|
||
pagination.total = res.total || 0
|
||
// 自动选中第一个 tab
|
||
if (tableData.value.length > 0 && !activeTab.value) {
|
||
activeTab.value = String(tableData.value[0].id)
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error('获取发布记录失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const refreshList = (): void => {
|
||
pagination.current = 1
|
||
void loadList()
|
||
}
|
||
|
||
const handleSearch = (): void => {
|
||
pagination.current = 1
|
||
tableData.value = [] // 先清空数据,避免 el-tabs 渲染不匹配的 pane 导致 watcher 报错
|
||
activeTab.value = ''
|
||
void loadList()
|
||
}
|
||
|
||
const resetSearch = (): void => {
|
||
searchParams.shop_type = null
|
||
searchParams.status = null
|
||
tableData.value = [] // 先清空数据,避免 el-tabs 渲染不匹配的 pane 导致 watcher 报错
|
||
activeTab.value = ''
|
||
refreshList()
|
||
}
|
||
|
||
const handleRetry = (shopId: number, product: ProductItem): void => {
|
||
const shop = tableData.value.find(s => s.id === shopId)
|
||
if (!shop) return
|
||
retryRelease(shop.id, product.id, shop.shop_type).then(() => {
|
||
ElMessage.success('重试成功')
|
||
void loadList()
|
||
}).catch(() => {
|
||
ElMessage.error('重试失败')
|
||
})
|
||
}
|
||
|
||
const handleTabChange = (tabName: string): void => {
|
||
activeTab.value = tabName
|
||
}
|
||
|
||
const handleCurrentChange = (page: number): void => {
|
||
pagination.current = page
|
||
void loadList()
|
||
}
|
||
|
||
const handleSizeChange = (size: number): void => {
|
||
pagination.pageSize = size
|
||
pagination.current = 1
|
||
void loadList()
|
||
}
|
||
|
||
/** 存储每个店铺表格的 el-table 实例 */
|
||
const tableRefs = reactive<Record<number, any>>({})
|
||
|
||
const setTableRef = (shopId: number, el: any): void => {
|
||
if (el) tableRefs[shopId] = el
|
||
}
|
||
|
||
/** 全选:选中当前店铺的所有行 */
|
||
const handleSelectAll = (shopId: number): void => {
|
||
const table = tableRefs[shopId]
|
||
if (!table) return
|
||
table.toggleAllSelection()
|
||
}
|
||
|
||
/** 反选:反转当前店铺的选中状态 */
|
||
const handleSelectInverse = (shopId: number): void => {
|
||
const shop = tableData.value.find(s => s.id === shopId)
|
||
if (!shop) return
|
||
const table = tableRefs[shopId]
|
||
if (!table) return
|
||
const products = shop.products || []
|
||
// 获取当前选中的行
|
||
const selection = table.getSelectionRows()
|
||
const selectedIds = new Set(selection.map((r: any) => r.id))
|
||
// 先清空所有选中
|
||
table.clearSelection()
|
||
// 选中未被选中的行
|
||
products.forEach(product => {
|
||
if (!selectedIds.has(product.id)) {
|
||
table.toggleRowSelection(product, true)
|
||
}
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadList()
|
||
})
|
||
|
||
return {
|
||
loading, tableData, activeTab, activeShopId,
|
||
searchParams, pagination,
|
||
Search, Refresh,
|
||
shopTypeTag, formatTimestamp, getFirstImage, getImageList,
|
||
handleSearch, resetSearch, handleTabChange,
|
||
handleCurrentChange, handleSizeChange,
|
||
handleRetry, handleSelectAll, handleSelectInverse, setTableRef
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.release-record-manager {
|
||
width: 100%;
|
||
}
|
||
|
||
.card-header {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.filter-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
margin-bottom: 20px;
|
||
align-items: center;
|
||
background: white;
|
||
padding: 16px 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.pagination-wrapper {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
background: white;
|
||
padding: 14px 20px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.expand-content {
|
||
padding: 16px 20px;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.expand-header {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.expand-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
padding-left: 8px;
|
||
border-left: 3px solid #409eff;
|
||
}
|
||
|
||
.tab-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
/* 两端对齐 */
|
||
align-items: center;
|
||
/* 垂直居中 */
|
||
}
|
||
</style>
|