openxiangda 1.0.258 → 1.0.260

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -748,6 +748,35 @@ export function AdminList<Row>(props: AdminListProps<Row>) {
748
748
  [dataSource, notificationApi],
749
749
  )
750
750
 
751
+ const downloadExportTask = async (task: AdminListExportTask) => {
752
+ try {
753
+ if (dataSource.downloadExportTask) {
754
+ await dataSource.downloadExportTask(task, { fileName })
755
+ return
756
+ }
757
+ if (
758
+ typeof window !== "undefined" &&
759
+ task.downloadUrl &&
760
+ (/^(https?:)?\/\//i.test(task.downloadUrl) ||
761
+ /^(blob|data):/i.test(task.downloadUrl))
762
+ ) {
763
+ window.open(task.downloadUrl, "_blank", "noopener")
764
+ return
765
+ }
766
+ throw new Error("当前数据源未配置导出文件下载能力")
767
+ } catch (downloadError) {
768
+ notificationApi.error({
769
+ key: `admin-list-export-download:${task.id}`,
770
+ title: "下载失败",
771
+ description:
772
+ downloadError instanceof Error
773
+ ? downloadError.message
774
+ : String(downloadError || "请稍后重试"),
775
+ duration: 0,
776
+ })
777
+ }
778
+ }
779
+
751
780
  const executeExport = async (scope: "selected" | "all") => {
752
781
  if (!dataSource.createExportTask) return
753
782
  setExporting(true)
@@ -806,11 +835,7 @@ export function AdminList<Row>(props: AdminListProps<Row>) {
806
835
  <Button
807
836
  size="small"
808
837
  type="primary"
809
- onClick={() => {
810
- if (typeof window !== "undefined") {
811
- window.open(finished.downloadUrl, "_blank", "noopener")
812
- }
813
- }}
838
+ onClick={() => void downloadExportTask(finished)}
814
839
  >
815
840
  下载文件
816
841
  </Button>
@@ -11,6 +11,61 @@ import type {
11
11
  AdminListResult,
12
12
  } from "./types"
13
13
  import { createAdminListPreferenceStore } from "./preferences"
14
+ import type { PageBinaryResponse } from "openxiangda/runtime"
15
+
16
+ const getHeaderValue = (
17
+ headers: PageBinaryResponse["headers"],
18
+ name: string,
19
+ ) => {
20
+ const match = Object.entries(headers || {}).find(
21
+ ([key]) => key.toLowerCase() === name.toLowerCase(),
22
+ )
23
+ return match?.[1] || ""
24
+ }
25
+
26
+ const decodeFileName = (value: string) => {
27
+ try {
28
+ return decodeURIComponent(value)
29
+ } catch {
30
+ return value
31
+ }
32
+ }
33
+
34
+ const parseContentDispositionFileName = (value: string) => {
35
+ const utf8Match = value.match(/filename\*=UTF-8''([^;]+)/i)
36
+ if (utf8Match?.[1]) return decodeFileName(utf8Match[1].trim())
37
+ const plainMatch = value.match(/filename="?([^";]+)"?/i)
38
+ return plainMatch?.[1] ? decodeFileName(plainMatch[1].trim()) : ""
39
+ }
40
+
41
+ const resolveExportFileName = (
42
+ response: PageBinaryResponse,
43
+ fallbackName: string,
44
+ ) => {
45
+ const disposition =
46
+ getHeaderValue(response.headers, "content-disposition") ||
47
+ (response.fileName?.includes("filename") ? response.fileName : "")
48
+ const candidate =
49
+ parseContentDispositionFileName(disposition) ||
50
+ response.fileName ||
51
+ fallbackName
52
+ return candidate.split(/[\\/]/).pop() || fallbackName
53
+ }
54
+
55
+ const saveExportBlob = (
56
+ response: PageBinaryResponse,
57
+ fallbackName: string,
58
+ ) => {
59
+ if (typeof window === "undefined" || typeof document === "undefined") return
60
+ const objectUrl = window.URL.createObjectURL(response.blob)
61
+ const anchor = document.createElement("a")
62
+ anchor.href = objectUrl
63
+ anchor.download = resolveExportFileName(response, fallbackName)
64
+ document.body.appendChild(anchor)
65
+ anchor.click()
66
+ anchor.remove()
67
+ window.URL.revokeObjectURL(objectUrl)
68
+ }
14
69
 
15
70
  const nonEmpty = (value: unknown) =>
16
71
  value !== undefined &&
@@ -99,6 +154,7 @@ const createExportClient = (
99
154
  source: Record<string, unknown>,
100
155
  ) => {
101
156
  const appType = options.appType || options.sdk.context.app.appType || ""
157
+ const requestedFileNames = new Map<string, string>()
102
158
  const basePath = `/openxiangda-api/v1/apps/${encodeURIComponent(
103
159
  appType,
104
160
  )}/admin-list/exports`
@@ -110,6 +166,7 @@ const createExportClient = (
110
166
  typeof rowId === "number" ? rowId : String(rowId),
111
167
  ),
112
168
  }
169
+ let task: AdminListExportTask
113
170
  if (options.sdk.export?.create) {
114
171
  const response = await options.sdk.export.create({
115
172
  appType,
@@ -122,21 +179,24 @@ const createExportClient = (
122
179
  if (!response.result) {
123
180
  throw new Error("创建导出任务失败:服务端未返回任务")
124
181
  }
125
- return response.result
126
- }
127
- const response = await options.sdk.request<AdminListExportTask>({
128
- path: basePath,
129
- method: "post",
130
- body: {
131
- listKey: options.listKey,
132
- source,
133
- ...normalizedInput,
134
- },
135
- })
136
- if (!response.result) {
137
- throw new Error("创建导出任务失败:服务端未返回任务")
182
+ task = response.result
183
+ } else {
184
+ const response = await options.sdk.request<AdminListExportTask>({
185
+ path: basePath,
186
+ method: "post",
187
+ body: {
188
+ listKey: options.listKey,
189
+ source,
190
+ ...normalizedInput,
191
+ },
192
+ })
193
+ if (!response.result) {
194
+ throw new Error("创建导出任务失败:服务端未返回任务")
195
+ }
196
+ task = response.result
138
197
  }
139
- return response.result
198
+ if (input.fileName) requestedFileNames.set(task.id, input.fileName)
199
+ return task
140
200
  },
141
201
  getExportTask: async (taskId: string) => {
142
202
  if (options.sdk.export?.get) {
@@ -155,6 +215,24 @@ const createExportClient = (
155
215
  }
156
216
  return response.result
157
217
  },
218
+ downloadExportTask: async (
219
+ task: AdminListExportTask,
220
+ downloadOptions: { fileName?: string } = {},
221
+ ) => {
222
+ if (!task.downloadUrl) {
223
+ throw new Error("导出任务未返回下载地址")
224
+ }
225
+ const response = await options.sdk.transport.download({
226
+ path: task.downloadUrl,
227
+ method: "get",
228
+ })
229
+ const fallbackName =
230
+ downloadOptions.fileName ||
231
+ requestedFileNames.get(task.id) ||
232
+ `${options.listKey}.xlsx`
233
+ saveExportBlob(response, fallbackName)
234
+ requestedFileNames.delete(task.id)
235
+ },
158
236
  }
159
237
  }
160
238
 
@@ -19,6 +19,7 @@ export type {
19
19
  AdminListDataViewSourceOptions,
20
20
  AdminListDensity,
21
21
  AdminListExportInput,
22
+ AdminListExportDownloadOptions,
22
23
  AdminListExportScope,
23
24
  AdminListExportTask,
24
25
  AdminListExportTaskStatus,
@@ -187,6 +187,10 @@ export interface AdminListExportTask {
187
187
  expiresAt?: string
188
188
  }
189
189
 
190
+ export interface AdminListExportDownloadOptions {
191
+ fileName?: string
192
+ }
193
+
190
194
  export interface AdminListDataSource<Row> {
191
195
  key?: string
192
196
  query: (query: AdminListQuery) => Promise<AdminListResult<Row>>
@@ -195,6 +199,10 @@ export interface AdminListDataSource<Row> {
195
199
  input: AdminListExportInput,
196
200
  ) => Promise<AdminListExportTask>
197
201
  getExportTask?: (taskId: string) => Promise<AdminListExportTask>
202
+ downloadExportTask?: (
203
+ task: AdminListExportTask,
204
+ options?: AdminListExportDownloadOptions,
205
+ ) => Promise<void>
198
206
  }
199
207
 
200
208
  export interface AdminListDataSourceOptions {