feat(pickup): 支持线下提号 CSV 导出

This commit is contained in:
yml
2026-11-12 14:53:36 +08:00
parent a5601eb76c
commit e8a281fce6
8 changed files with 628 additions and 2 deletions
@@ -1,3 +1,4 @@
import axios from 'axios'
import { apiClient } from '@/shared/api/client'
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
@@ -166,6 +167,8 @@ export interface AdminPickupListQuery {
end_time?: string
}
export type AdminPickupExportQuery = Omit<AdminPickupListQuery, 'page' | 'page_size'>
export interface PickupShop {
id: number
name: string
@@ -254,11 +257,57 @@ export async function fetchAdminPickups(query: AdminPickupListQuery = {}) {
}
}
export async function exportAdminPickups(query: AdminPickupExportQuery = {}) {
try {
const response = await apiClient.get<Blob>('/admin/pickups/export', {
params: cleanParams(query),
responseType: 'blob',
timeout: 60000,
skipErrorHandler: true,
})
return {
blob: response.data,
filename: readExportFilename(response.headers['content-disposition']),
rowCount: Number(response.headers['x-export-row-count'] || 0),
}
} catch (error) {
if (axios.isAxiosError(error) && error.response?.data instanceof Blob) {
const text = await error.response.data.text()
let message = ''
try {
const payload = JSON.parse(text) as { message?: unknown }
if (typeof payload.message === 'string') {
message = payload.message
}
} catch {
// 非 JSON 错误响应保留原始 Axios 错误。
}
if (message) {
error.message = message
}
}
throw error
}
}
export async function fetchAdminPickup(id: string | number) {
const { data } = await apiClient.get<ApiResponse<AdminPickup>>(`/admin/pickups/${id}`)
return data.data
}
function readExportFilename(contentDisposition: unknown) {
if (typeof contentDisposition !== 'string') return 'pickup-export.csv'
const encoded = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1]
if (encoded) {
try {
return decodeURIComponent(encoded)
} catch {
return encoded
}
}
return contentDisposition.match(/filename="?([^";]+)"?/i)?.[1] || 'pickup-export.csv'
}
export async function fetchAdminPickupShopOptions(platform = '') {
const { data } = await apiClient.get<ApiResponse<string[]>>('/admin/pickups/shop-options', {
params: cleanParams({ platform }),
@@ -1,13 +1,14 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { EditPen, Plus, Refresh, Search, Setting, Shop } from '@element-plus/icons-vue'
import { Download, EditPen, Plus, Refresh, Search, Setting, Shop } from '@element-plus/icons-vue'
import {
cancelAdminPickup,
confirmAdminPickupOfflineSettlement,
completeAdminPickup,
createAdminPickupFinancialAdjustment,
createAdminPickup,
exportAdminPickups,
fetchAdminPickups,
fetchAdminPickupShopOptions,
fetchAvailableListings,
@@ -30,6 +31,7 @@ const adminSession = useAdminSessionStore()
const currentAdminName = computed(() => adminSession.nickname || adminSession.username || '')
const loading = ref(false)
const exporting = ref(false)
const items = ref<AdminPickup[]>([])
const total = ref(0)
const filterTimeRange = ref<[string, string] | null>(null)
@@ -44,6 +46,16 @@ const filters = reactive({
start_time: '',
end_time: '',
})
const hasActiveFilters = computed(
() =>
Boolean(filters.keyword.trim()) ||
Boolean(filters.status) ||
Boolean(filters.shop_name) ||
Boolean(filters.account_source) ||
Boolean(filters.offline_settlement_status) ||
Boolean(filters.start_time) ||
Boolean(filters.end_time)
)
const createDialogVisible = ref(false)
const completeDialogVisible = ref(false)
@@ -153,6 +165,48 @@ function handleSizeChange(s: number) {
loadList()
}
async function handleExportCommand(command: string) {
const exportAll = command === 'all'
if (!exportAll && hasActiveFilters.value && total.value === 0) {
ElMessage.warning('当前筛选条件下没有可导出的数据')
return
}
exporting.value = true
try {
const result = await exportAdminPickups(
exportAll
? {}
: {
keyword: filters.keyword.trim(),
status: filters.status,
shop_name: filters.shop_name,
account_source: filters.account_source,
offline_settlement_status: filters.offline_settlement_status,
start_time: filters.start_time,
end_time: filters.end_time,
}
)
downloadBlob(result.blob, result.filename)
ElMessage.success(result.rowCount > 0 ? `已导出 ${result.rowCount} 条提号记录` : '导出成功')
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '导出失败')
} finally {
exporting.value = false
}
}
function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
function openCreateDialog() {
createForm.listing_id = null
createForm.operator = currentAdminName.value || ''
@@ -531,6 +585,19 @@ function listingOptionLabel(item: AvailableListing) {
<p>管理员线下提号,站内上传结算到钱包,平台代管通过线下结算确认打款。</p>
</div>
<div class="toolbar-actions">
<el-dropdown
trigger="click"
:disabled="exporting || loading"
@command="handleExportCommand"
>
<el-button :icon="Download" :loading="exporting">导出 CSV</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="filtered">导出筛选结果</el-dropdown-item>
<el-dropdown-item command="all">导出全部</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button :icon="Shop" @click="shopMgrVisible = true">店铺管理</el-button>
<el-button type="primary" :icon="Plus" @click="openCreateDialog">创建提号</el-button>
</div>