账单导出

This commit is contained in:
liu
2026-08-14 20:41:25 +08:00
parent 03434feb35
commit 4138164bd8
4 changed files with 464 additions and 39 deletions
+110
View File
@@ -0,0 +1,110 @@
import Taro from '@tarojs/taro'
import { getToken } from '@/utils/request'
/**
* 导出文件下载(双端)
* 后端导出接口成功返回 xlsx 文件流,业务失败时仍以文件流形式返回 JSON
* blob.type 为 application/json),两端都需探测并解析 msg 提示
*/
/** 从 Content-Disposition 解析文件名(优先 RFC 5987 filename*=utf-8''... */
export function parseExportFilename(disposition?: string | null): string {
if (!disposition) return ''
const star = disposition.match(/filename\*=utf-8''([^;]+)/i)
if (star?.[1]) {
try {
return decodeURIComponent(star[1])
} catch {
// 编码异常时退化为普通 filename
}
}
const plain = disposition.match(/filename="?([^";]+)"?/i)
return plain?.[1] ?? ''
}
/** 解析 JSON 文本中的业务错误信息 */
function parseJsonError(text: string): Error {
try {
const json = JSON.parse(text)
return new Error(json.msg || '导出失败,请稍后重试')
} catch {
return new Error('导出失败,请稍后重试')
}
}
/** H5fetch 带鉴权拉取 blob → <a download> 触发保存 */
async function downloadFileH5(url: string, fallbackName: string): Promise<void> {
const token = getToken()
const res = await fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
if (!res.ok) {
throw new Error(`下载失败(${res.status}`)
}
const blob = await res.blob()
// 业务失败:返回的是 JSON blob
if (blob.type.includes('application/json')) {
throw parseJsonError(await blob.text())
}
const filename =
parseExportFilename(res.headers.get('Content-Disposition')) || fallbackName
const objectUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = objectUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(objectUrl)
}
/** 读取小程序本地文件内容(utf8) */
function readTempFile(filePath: string, length?: number): Promise<string> {
return new Promise((resolve, reject) => {
Taro.getFileSystemManager().readFile({
filePath,
encoding: 'utf8',
...(length !== undefined ? { position: 0, length } : {}),
success: r => resolve(r.data as string),
fail: reject,
})
})
}
/** 小程序:downloadFile 下载后 openDocument 打开;业务失败时下载"成功"但内容是 JSON */
async function downloadFileWeapp(url: string): Promise<void> {
const token = getToken()
const { tempFilePath } = await Taro.downloadFile({
url,
header: token ? { Authorization: `Bearer ${token}` } : {},
})
// 探测前 200 字节:以 '{' 开头即为 JSON 错误响应
const head = await readTempFile(tempFilePath, 200)
if (head.trimStart().startsWith('{')) {
throw parseJsonError(await readTempFile(tempFilePath))
}
await Taro.openDocument({
filePath: tempFilePath,
fileType: 'xlsx',
showMenu: true,
fail: () => {
Taro.showToast({ title: '文件已下载,打开失败', icon: 'none' })
},
} as Parameters<typeof Taro.openDocument>[0])
}
/**
* 带鉴权下载导出文件
* - H5:保存到浏览器下载目录,返回保存的文件名
* - 小程序:直接调起系统文档预览(showMenu 支持转发/保存)
*/
export async function downloadExportFile(url: string, fallbackName: string): Promise<void> {
if (process.env.TARO_ENV === 'h5') {
return downloadFileH5(url, fallbackName)
}
return downloadFileWeapp(url)
}