feat(pickup): 支持线下提号 CSV 导出
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user