feat(web-admin): add ListExportDropdown and Excel blob download helper

Support filtered-result primary action and optional export-selected menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-21 17:01:49 +08:00
1 parent eb4233f23f
commit 3643a3b14e
2 files changed
+185

No files matched your search

@@ -0,0 +1,144 @@
<template>
<div v-if="hasSelection" class="list-export-dropdown">
<el-button
class="btn-ghost list-export-dropdown__main"
:disabled="disabled"
:loading="exporting"
@click="handleExportFiltered"
>
<el-icon><Download /></el-icon>
导出筛选结果
</el-button>
<el-dropdown
trigger="click"
:disabled="disabled || exporting"
@command="handleCommand"
>
<el-button class="btn-ghost list-export-dropdown__caret" :disabled="disabled || exporting">
<el-icon><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="selected" :disabled="!selectedIds?.length">
导出已选
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-button
v-else
class="btn-ghost"
:disabled="disabled"
:loading="exporting"
@click="handleExportFiltered"
>
<el-icon><Download /></el-icon>
导出
</el-button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ArrowDown, Download } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import {
filenameFromContentDisposition,
pickBlobFromExportResponse,
triggerBlobDownload
} from '@/utils/list-export-download'
defineOptions({ name: 'ListExportDropdown' })
export type ListExportPayload = Record<string, any> & {
ids?: string[]
columns?: string[]
}
const props = withDefaults(
defineProps<{
exportFn: (payload: ListExportPayload) => Promise<any>
getFilterParams: () => Record<string, any>
selectedIds?: string[]
hasSelection?: boolean
visibleColumns?: string[]
filename?: string
disabled?: boolean
}>(),
{
selectedIds: () => [],
hasSelection: false,
visibleColumns: undefined,
filename: '导出.xlsx',
disabled: false
}
)
const exporting = ref(false)
function buildPayload(withIds: boolean): ListExportPayload {
const payload: ListExportPayload = { ...props.getFilterParams() }
if (props.visibleColumns?.length) {
payload.columns = [...props.visibleColumns]
}
if (withIds) {
payload.ids = [...(props.selectedIds || [])]
}
return payload
}
async function runExport(withIds: boolean) {
if (exporting.value || props.disabled) return
if (withIds && !(props.selectedIds?.length > 0)) return
exporting.value = true
try {
const res = await props.exportFn(buildPayload(withIds))
const { blob, filenameHeader } = pickBlobFromExportResponse(res)
const name = filenameFromContentDisposition(filenameHeader, props.filename)
triggerBlobDownload(blob, name)
ElMessage.success('导出成功')
} catch (e: any) {
const msg = e?.message || e?.msg || '导出失败'
ElMessage.error(msg)
} finally {
exporting.value = false
}
}
function handleExportFiltered() {
void runExport(false)
}
function handleCommand(cmd: string) {
if (cmd === 'selected') void runExport(true)
}
</script>
<style scoped lang="scss">
.list-export-dropdown {
display: inline-flex;
align-items: stretch;
vertical-align: middle;
:deep(.el-button) {
.el-icon + span,
span + .el-icon {
margin-left: 6px;
}
}
}
.list-export-dropdown__main {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
margin-right: -1px;
}
.list-export-dropdown__caret {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
min-width: 36px;
padding: 0 10px;
}
</style>