diff --git a/web-admin/src/components/business/list-export-dropdown/index.vue b/web-admin/src/components/business/list-export-dropdown/index.vue
new file mode 100644
index 000000000..1f97e700f
--- /dev/null
+++ b/web-admin/src/components/business/list-export-dropdown/index.vue
@@ -0,0 +1,144 @@
+
+
+
+
+ 导出筛选结果
+
+
+
+
+
+
+
+
+ 导出已选
+
+
+
+
+
+
+
+
+ 导出
+
+
+
+
+
+
diff --git a/web-admin/src/utils/list-export-download.ts b/web-admin/src/utils/list-export-download.ts
new file mode 100644
index 000000000..dc1f0245a
--- /dev/null
+++ b/web-admin/src/utils/list-export-download.ts
@@ -0,0 +1,41 @@
+/** 从 Content-Disposition 解析文件名;失败则用 fallback */
+export function filenameFromContentDisposition(
+ header: string | undefined,
+ fallback: string
+): string {
+ if (!header) return fallback
+ const utf8 = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(header)
+ if (utf8?.[1]) {
+ try {
+ return decodeURIComponent(utf8[1].trim().replace(/["']/g, ''))
+ } catch {
+ /* ignore */
+ }
+ }
+ const plain = /filename\s*=\s*("?)([^";]+)\1/i.exec(header)
+ if (plain?.[2]) return plain[2].trim()
+ return fallback
+}
+
+export function triggerBlobDownload(blob: Blob, filename: string): void {
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+ link.click()
+ URL.revokeObjectURL(url)
+}
+
+/** Axios 成功时若拦截器已把 data 设为 Blob,直接用;否则兼容 { data: Blob, headers } */
+export function pickBlobFromExportResponse(res: any): { blob: Blob; filenameHeader?: string } {
+ if (res instanceof Blob) {
+ return { blob: res }
+ }
+ if (res?.data instanceof Blob) {
+ return {
+ blob: res.data,
+ filenameHeader: res.headers?.['content-disposition'] || res.headers?.['Content-Disposition']
+ }
+ }
+ throw new Error('导出响应不是文件')
+}