Initial commit
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { zebraApi, type ZebraOrder } from '../../api/zebra'
|
||||
|
||||
type Shop = { id: string; shopName: string }
|
||||
|
||||
const shopList = ref<Shop[]>([])
|
||||
const selectedShops = ref<string[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const progressPercentage = ref(0)
|
||||
const showProgress = ref(false)
|
||||
const allOrderData = ref<ZebraOrder[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
|
||||
// 批量获取状态
|
||||
const fetchCurrentPage = ref(1)
|
||||
const fetchTotalPages = ref(0)
|
||||
const fetchTotalItems = ref(0)
|
||||
const isFetching = ref(false)
|
||||
|
||||
const paginatedData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return allOrderData.value.slice(start, end)
|
||||
})
|
||||
|
||||
function formatJpy(v?: number) {
|
||||
const n = Number(v || 0)
|
||||
return `¥${n.toLocaleString('ja-JP')}`
|
||||
}
|
||||
|
||||
function formatCny(v?: number) {
|
||||
const n = Number(v || 0)
|
||||
return `¥${n.toLocaleString('zh-CN')}`
|
||||
}
|
||||
|
||||
async function loadShops() {
|
||||
try {
|
||||
const resp = await zebraApi.getShops()
|
||||
const list = (resp as any)?.data?.data?.list ?? (resp as any)?.list ?? []
|
||||
shopList.value = list
|
||||
} catch (e) {
|
||||
console.error('获取店铺列表失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSizeChange(size: number) {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleCurrentChange(page: number) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
if (isFetching.value) return
|
||||
|
||||
loading.value = true
|
||||
isFetching.value = true
|
||||
showProgress.value = true
|
||||
progressPercentage.value = 0
|
||||
allOrderData.value = []
|
||||
fetchCurrentPage.value = 1
|
||||
fetchTotalItems.value = 0
|
||||
|
||||
const [startDate = '', endDate = ''] = dateRange.value || []
|
||||
await fetchPageData(startDate, endDate)
|
||||
}
|
||||
|
||||
async function fetchPageData(startDate: string, endDate: string) {
|
||||
if (!isFetching.value) return
|
||||
|
||||
try {
|
||||
const data = await zebraApi.getOrders({
|
||||
startDate,
|
||||
endDate,
|
||||
page: fetchCurrentPage.value,
|
||||
pageSize: 50,
|
||||
shopIds: selectedShops.value.join(',')
|
||||
})
|
||||
|
||||
const orders = data.orders || []
|
||||
allOrderData.value = [...allOrderData.value, ...orders]
|
||||
|
||||
fetchTotalPages.value = data.totalPages || 0
|
||||
fetchTotalItems.value = data.total || 0
|
||||
|
||||
if (fetchCurrentPage.value < fetchTotalPages.value && isFetching.value) {
|
||||
progressPercentage.value = Math.round((fetchCurrentPage.value / fetchTotalPages.value) * 100)
|
||||
fetchCurrentPage.value++
|
||||
setTimeout(() => fetchPageData(startDate, endDate), 200)
|
||||
} else {
|
||||
progressPercentage.value = 100
|
||||
finishFetching()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取订单数据失败:', e)
|
||||
finishFetching()
|
||||
}
|
||||
}
|
||||
|
||||
function finishFetching() {
|
||||
isFetching.value = false
|
||||
loading.value = false
|
||||
// 确保进度条完全填满
|
||||
progressPercentage.value = 100
|
||||
currentPage.value = 1
|
||||
// 进度条保留显示,不自动隐藏
|
||||
}
|
||||
|
||||
function stopFetch() {
|
||||
isFetching.value = false
|
||||
loading.value = false
|
||||
// 进度条保留显示,不自动隐藏
|
||||
}
|
||||
|
||||
async function exportToExcel() {
|
||||
if (!allOrderData.value.length) return
|
||||
exportLoading.value = true
|
||||
try {
|
||||
const result = await zebraApi.exportAndSaveOrders({ orders: allOrderData.value })
|
||||
alert(`Excel文件已保存到: ${result.filePath}`)
|
||||
} catch (e) {
|
||||
alert('导出Excel失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadShops()
|
||||
try {
|
||||
const latest = await zebraApi.getLatestOrders()
|
||||
allOrderData.value = latest?.orders || []
|
||||
} catch {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="zebra-root">
|
||||
<div class="main-container">
|
||||
|
||||
<!-- 筛选和操作区域 -->
|
||||
<div class="import-section">
|
||||
<div class="import-controls">
|
||||
<!-- 店铺选择 -->
|
||||
<el-select v-model="selectedShops" multiple placeholder="选择店铺" style="width: 260px;" :disabled="loading">
|
||||
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.shopName" :value="shop.id"></el-option>
|
||||
</el-select>
|
||||
|
||||
<!-- 日期选择 -->
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 200px;"
|
||||
:disabled="loading"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" :disabled="loading" @click="fetchData">
|
||||
📂 {{ loading ? '处理中...' : '获取订单数据' }}
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!loading" @click="stopFetch">停止获取</el-button>
|
||||
<el-button type="success" :disabled="exportLoading || !allOrderData.length" @click="exportToExcel">导出Excel</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条显示 -->
|
||||
<div class="progress-section" v-if="showProgress">
|
||||
<div class="progress-box">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
|
||||
</div>
|
||||
<div class="progress-text">{{ progressPercentage }}%</div>
|
||||
</div>
|
||||
<div class="current-status" v-if="fetchTotalItems > 0">
|
||||
{{ progressPercentage >= 100 ? '完成' : `获取中... (${allOrderData.length}/${fetchTotalItems})` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据显示区域 -->
|
||||
<div class="table-container">
|
||||
<!-- 数据表格(无数据时也显示表头) -->
|
||||
<div class="table-section">
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>下单时间</th>
|
||||
<th>商品图片</th>
|
||||
<th>商品名称</th>
|
||||
<th>乐天订单号</th>
|
||||
<th>下单距今</th>
|
||||
<th>订单金额/日元</th>
|
||||
<th>数量</th>
|
||||
<th>税费/日元</th>
|
||||
<th>回款抽点rmb</th>
|
||||
<th>商品番号</th>
|
||||
<th>1688订单号</th>
|
||||
<th>采购金额/rmb</th>
|
||||
<th>国际运费/rmb</th>
|
||||
<th>国内物流</th>
|
||||
<th>国内单号</th>
|
||||
<th>日本单号</th>
|
||||
<th>地址状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="paginatedData.length === 0">
|
||||
<td colspan="16" class="empty-tip">暂无数据,请选择日期范围获取订单</td>
|
||||
</tr>
|
||||
<tr v-else v-for="row in paginatedData" :key="row.shopOrderNumber + (row.productNumber || '')">
|
||||
<td>{{ row.orderedAt || '-' }}</td>
|
||||
<td>
|
||||
<div class="image-container" v-if="row.productImage">
|
||||
<img :src="row.productImage" class="thumb" alt="thumb" />
|
||||
</div>
|
||||
<span v-else>无图片</span>
|
||||
</td>
|
||||
<td class="truncate" :title="row.productTitle">{{ row.productTitle }}</td>
|
||||
<td class="truncate" :title="row.shopOrderNumber">{{ row.shopOrderNumber }}</td>
|
||||
<td>{{ row.timeSinceOrder || '-' }}</td>
|
||||
<td><span class="price-tag">{{ formatJpy(row.priceJpy) }}</span></td>
|
||||
<td>{{ row.productQuantity || 0 }}</td>
|
||||
<td><span class="fee-tag">{{ formatJpy(row.shippingFeeJpy) }}</span></td>
|
||||
<td>{{ row.serviceFee || '-' }}</td>
|
||||
<td class="truncate" :title="row.productNumber">{{ row.productNumber }}</td>
|
||||
<td class="truncate" :title="row.poNumber">{{ row.poNumber }}</td>
|
||||
<td><span class="fee-tag">{{ formatCny(row.shippingFeeCny) }}</span></td>
|
||||
<td>{{ row.internationalShippingFee || '-' }}</td>
|
||||
<td>{{ row.poLogisticsCompany || '-' }}</td>
|
||||
<td class="truncate" :title="row.poTrackingNumber">{{ row.poTrackingNumber }}</td>
|
||||
<td class="truncate" :title="row.internationalTrackingNumber">{{ row.internationalTrackingNumber }}</td>
|
||||
<td>
|
||||
<span v-if="row.trackInfo" class="tag">{{ row.trackInfo }}</span>
|
||||
<span v-else>暂无</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 表格加载遮罩 -->
|
||||
<div v-if="loading && !allOrderData.length" class="table-loading">
|
||||
<div class="spinner">⟳</div>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-fixed">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[15,30,50,100]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="allOrderData.length"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ZebraDashboard',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zebra-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
|
||||
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
|
||||
.import-section { margin-bottom: 10px; flex-shrink: 0; }
|
||||
.import-controls { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.action-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.progress-section { margin: 15px 0 10px 0; }
|
||||
.progress-box { padding: 8px 0; }
|
||||
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
|
||||
.progress-bar { flex: 1; height: 6px; background: #ebeef5; border-radius: 3px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 3px; transition: width 0.3s ease; }
|
||||
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
|
||||
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
||||
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
|
||||
.empty-section { flex: 1; display: flex; justify-content: center; align-items: center; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; }
|
||||
.empty-container { text-align: center; }
|
||||
.empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.6; }
|
||||
.empty-text { font-size: 14px; color: #909399; }
|
||||
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
|
||||
.table-wrapper { height: 100%; overflow: auto; }
|
||||
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; }
|
||||
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
|
||||
.table tbody tr:hover { background: #f9f9f9; }
|
||||
.truncate { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.image-container { display: flex; justify-content: center; align-items: center; width: 24px; height: 20px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; }
|
||||
.thumb { width: 16px; height: 16px; object-fit: contain; border-radius: 2px; }
|
||||
.price-tag { color: #e6a23c; font-weight: bold; }
|
||||
.fee-tag { color: #909399; font-weight: 500; }
|
||||
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
|
||||
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
|
||||
.tag { display: inline-block; padding: 2px 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user