daShangDao_psiWebApp/src/views/releaseRecord/releaseRecord.vue
2026-06-03 10:53:47 +08:00

415 lines
16 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>
<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>