增加导出功能
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
import { download } from '@/utils/request'
|
||||
import request from '@/utils/request'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
const DEFAULT_IGNORE_KEYS = ['pageNum', 'pageSize', 'orderByColumn', 'isAsc', 'reasonable']
|
||||
|
||||
/**
|
||||
* Sync list export: POST filters and download xlsx (most existing pages).
|
||||
* @param {string} url e.g. 'system/user/export'
|
||||
* @param {object} [query]
|
||||
* @param {string} [filePrefix='export']
|
||||
*/
|
||||
export function exportExcel(url, query, filePrefix = 'export') {
|
||||
const name = String(filePrefix || 'export') + '_' + Date.now() + '.xlsx'
|
||||
return download(url, { ...(query || {}) }, name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync export from basic-table current query.
|
||||
*/
|
||||
export function exportTable(url, tableRef, normalize, filePrefix = 'export') {
|
||||
const raw = (tableRef && tableRef.getQueryParams && tableRef.getQueryParams()) || {}
|
||||
const query = typeof normalize === 'function' ? normalize(raw) : raw
|
||||
return exportExcel(url, query, filePrefix)
|
||||
}
|
||||
|
||||
function isFilled(value) {
|
||||
if (value == null) return false
|
||||
if (typeof value === 'string') return value.trim() !== ''
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return true
|
||||
if (Array.isArray(value)) return value.some(v => isFilled(v))
|
||||
if (value instanceof Date) return !Number.isNaN(value.getTime())
|
||||
if (typeof value === 'object') {
|
||||
return Object.keys(value).some(k => isFilled(value[k]))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic filter gate for list export.
|
||||
* Default: any non-empty field except page/sort keys counts.
|
||||
*
|
||||
* @param {object} query
|
||||
* @param {object} [opts]
|
||||
* @param {string[]} [opts.ignoreKeys] keys that never count as filter
|
||||
* @param {string[]} [opts.requireAny] if set, only these keys (and nested path) count
|
||||
* @param {(q: object) => boolean} [opts.custom] custom predicate; wins when provided
|
||||
*/
|
||||
export function hasListFilter(query, opts = {}) {
|
||||
if (typeof opts.custom === 'function') return !!opts.custom(query)
|
||||
if (!query || typeof query !== 'object') return false
|
||||
const ignore = new Set([...(opts.ignoreKeys || []), ...DEFAULT_IGNORE_KEYS])
|
||||
const requireAny = Array.isArray(opts.requireAny) && opts.requireAny.length
|
||||
? opts.requireAny
|
||||
: null
|
||||
const keys = requireAny || Object.keys(query)
|
||||
for (const key of keys) {
|
||||
if (ignore.has(key)) continue
|
||||
if (isFilled(query[key])) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** @deprecated use hasListFilter */
|
||||
export function hasExportFilter(q, opts = {}) {
|
||||
const ignoreKeys = [...(opts.ignoreKeys || [])]
|
||||
if (opts.ignoreOrderStatuses !== false) ignoreKeys.push('orderStatuses')
|
||||
return hasListFilter(q, { ...opts, ignoreKeys })
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(url) {
|
||||
const s = String(url || '').trim()
|
||||
if (!s) return ''
|
||||
return s.startsWith('/') ? s.replace(/\/+$/, '') : ('/' + s.replace(/\/+$/, ''))
|
||||
}
|
||||
|
||||
function resolveAsyncUrls(options) {
|
||||
const base = normalizeBaseUrl(options.baseUrl || options.submitUrl || '')
|
||||
const submitUrl = normalizeBaseUrl(options.submitUrl || base)
|
||||
const taskUrlPrefix = options.taskUrlPrefix
|
||||
|| (base ? base + '/task/' : '')
|
||||
const downloadUrlPrefix = options.downloadUrlPrefix
|
||||
|| (base ? base + '/download/' : '')
|
||||
return { submitUrl, taskUrlPrefix, downloadUrlPrefix }
|
||||
}
|
||||
|
||||
function pickTaskId(payload) {
|
||||
if (payload == null) return null
|
||||
if (typeof payload === 'string' || typeof payload === 'number') return String(payload)
|
||||
return payload.taskId || payload.exportId || payload.id || null
|
||||
}
|
||||
|
||||
function taskDone(task, options) {
|
||||
const status = String(task && task.status != null ? task.status : (task && task.exportStatus) || '').toUpperCase()
|
||||
const successValues = options.successStatus || ['SUCCESS']
|
||||
const failValues = options.failStatus || ['FAIL', 'FAILED']
|
||||
if (successValues.map(v => String(v).toUpperCase()).includes(status)) return 'SUCCESS'
|
||||
if (failValues.map(v => String(v).toUpperCase()).includes(status)) return 'FAIL'
|
||||
return 'RUNNING'
|
||||
}
|
||||
|
||||
/**
|
||||
* Async export common flow: submit -> poll -> download.
|
||||
* Backend convention (recommended):
|
||||
* POST {baseUrl} -> { taskId }
|
||||
* GET {baseUrl}/task/{id} -> task progress
|
||||
* GET {baseUrl}/download/{id}-> file blob
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {string} [options.baseUrl] preferred; derives task/download urls
|
||||
* @param {string} [options.submitUrl]
|
||||
* @param {string} [options.taskUrlPrefix]
|
||||
* @param {string} [options.downloadUrlPrefix]
|
||||
* @param {object} [options.query]
|
||||
* @param {boolean} [options.requireFilter=false]
|
||||
* @param {object} [options.filterOpts] passed to hasListFilter
|
||||
* @param {string} [options.filterMessage]
|
||||
* @param {string} [options.filePrefix='export']
|
||||
* @param {string} [options.loadingText]
|
||||
* @param {(task: object) => void} [options.onProgress]
|
||||
* @param {number} [options.pollIntervalMs=800]
|
||||
* @param {number} [options.maxWaitMs]
|
||||
* @param {string[]} [options.successStatus]
|
||||
* @param {string[]} [options.failStatus]
|
||||
* @param {boolean} [options.showSuccessMessage=true]
|
||||
*/
|
||||
export async function exportAsync(options = {}) {
|
||||
const { submitUrl, taskUrlPrefix, downloadUrlPrefix } = resolveAsyncUrls(options)
|
||||
if (!submitUrl || !taskUrlPrefix || !downloadUrlPrefix) {
|
||||
throw new Error('exportAsync requires baseUrl or submit/task/download urls')
|
||||
}
|
||||
const query = { ...(options.query || {}) }
|
||||
delete query.pageNum
|
||||
delete query.pageSize
|
||||
|
||||
if (options.requireFilter) {
|
||||
const extraIgnore = (options.filterOpts && options.filterOpts.ignoreKeys) || []
|
||||
const filterOpts = {
|
||||
...(options.filterOpts || {}),
|
||||
ignoreKeys: ['orderStatuses', ...extraIgnore]
|
||||
}
|
||||
if (!hasListFilter(query, filterOpts)) {
|
||||
ElMessage.warning(options.filterMessage || '\u8bf7\u5148\u8bbe\u7f6e\u5217\u8868\u67e5\u8be2\u6761\u4ef6\u540e\u518d\u5bfc\u51fa')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const filePrefix = options.filePrefix || 'export'
|
||||
const loadingText = options.loadingText || '\u6b63\u5728\u5bfc\u51fa\uff0c\u8bf7\u7a0d\u5019'
|
||||
const pollIntervalMs = options.pollIntervalMs || 800
|
||||
const maxWaitMs = options.maxWaitMs || 30 * 60 * 1000
|
||||
const showSuccessMessage = options.showSuccessMessage !== false
|
||||
|
||||
const loading = ElLoading.service({ text: loadingText, background: 'rgba(0, 0, 0, 0.7)' })
|
||||
try {
|
||||
const submitRes = await request({
|
||||
url: submitUrl,
|
||||
method: 'post',
|
||||
data: query,
|
||||
timeout: 120000,
|
||||
headers: { repeatSubmit: false }
|
||||
})
|
||||
const taskId = pickTaskId(submitRes && submitRes.data)
|
||||
if (!taskId) throw new Error('missing export taskId')
|
||||
|
||||
const started = Date.now()
|
||||
let task = null
|
||||
while (Date.now() - started < maxWaitMs) {
|
||||
const res = await request({ url: taskUrlPrefix + String(taskId), method: 'get' })
|
||||
task = res.data || {}
|
||||
if (typeof options.onProgress === 'function') options.onProgress(task)
|
||||
const pct = Number(task.percent)
|
||||
if (!Number.isNaN(pct) && pct >= 0) {
|
||||
loading.setText(loadingText + ' ' + Math.min(100, pct) + '%')
|
||||
}
|
||||
const state = taskDone(task, options)
|
||||
if (state === 'SUCCESS') break
|
||||
if (state === 'FAIL') {
|
||||
throw new Error(task.error || task.remark || task.message || 'export failed')
|
||||
}
|
||||
await sleep(pollIntervalMs)
|
||||
}
|
||||
if (!task || taskDone(task, options) !== 'SUCCESS') {
|
||||
throw new Error('export timeout')
|
||||
}
|
||||
|
||||
const file = await request({
|
||||
url: downloadUrlPrefix + String(taskId),
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 120000
|
||||
})
|
||||
if (file && file.type && String(file.type).includes('json')) {
|
||||
const error = JSON.parse(await file.text())
|
||||
throw new Error(error.msg || 'download export file failed')
|
||||
}
|
||||
const name = task.fileName || (filePrefix + '_' + Date.now() + '.xlsx')
|
||||
saveAs(new Blob([file]), name)
|
||||
if (showSuccessMessage) ElMessage.success('\u5bfc\u51fa\u6210\u529f')
|
||||
return { task, taskId }
|
||||
} catch (e) {
|
||||
ElMessage.error((e && e.message) || '\u5bfc\u51fa\u5931\u8d25')
|
||||
return null
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async export from basic-table.
|
||||
* @param {object} options same as exportAsync + tableRef/normalize
|
||||
*/
|
||||
export async function exportAsyncFromTable(options = {}) {
|
||||
const raw = (options.tableRef && options.tableRef.getQueryParams && options.tableRef.getQueryParams()) || {}
|
||||
const query = typeof options.normalize === 'function' ? options.normalize({ ...raw }) : { ...raw }
|
||||
delete query.pageNum
|
||||
delete query.pageSize
|
||||
return exportAsync({
|
||||
...options,
|
||||
query,
|
||||
requireFilter: options.requireFilter !== false
|
||||
})
|
||||
}
|
||||
|
||||
/** @deprecated use exportAsyncFromTable */
|
||||
export async function exportTableAsyncFromRef(options = {}) {
|
||||
return exportAsyncFromTable(options)
|
||||
}
|
||||
|
||||
/** @deprecated use exportAsync */
|
||||
export async function exportTableAsync(options = {}) {
|
||||
return exportAsync(options)
|
||||
}
|
||||
Reference in New Issue
Block a user