增加导出功能
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)
|
||||
}
|
||||
@@ -117,6 +117,15 @@
|
||||
@click="openRecalcDialog"
|
||||
>重计费</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Download"
|
||||
v-hasPermi="['price:billing:export']"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
|
||||
@@ -225,6 +234,7 @@
|
||||
|
||||
<script setup name="BillingBill">
|
||||
import { listBill, getBill, refundBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate, recalcBillPriceAsync } from '@/api/price/billing'
|
||||
import { exportAsyncFromTable } from '@/utils/excelExport'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useProducts } from '@/components/ProductSelect/useProducts'
|
||||
import BillDetail from './components/BillDetail.vue'
|
||||
@@ -390,8 +400,8 @@ const searchColumns = [
|
||||
}
|
||||
]
|
||||
|
||||
function queryList(params) {
|
||||
const { bizDateRange, ...rest } = params
|
||||
function buildBillQuery(params) {
|
||||
const { bizDateRange, ...rest } = params || {}
|
||||
const query = { ...rest }
|
||||
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
|
||||
query.params = {
|
||||
@@ -400,9 +410,24 @@ function queryList(params) {
|
||||
endBizDate: bizDateRange[1] || null
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
function queryList(params) {
|
||||
const query = buildBillQuery(params)
|
||||
return listBill(query)
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
return exportAsyncFromTable({
|
||||
baseUrl: '/price/billing/bill/export',
|
||||
tableRef: tableRef.value,
|
||||
normalize: buildBillQuery,
|
||||
filePrefix: 'billing_bill',
|
||||
requireFilter: true
|
||||
})
|
||||
}
|
||||
|
||||
function handleUpdateArrears(ids, selection) {
|
||||
const billIds = (ids && ids.length)
|
||||
? ids
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="Document" @click="logDrawerRef.open()">操作记录</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Download"
|
||||
v-hasPermi="['price:orderBill:export']"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
@@ -129,6 +138,7 @@
|
||||
<script setup name="OrderBillQuery">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listOrderBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, localRebillAsync } from '@/api/price/billing'
|
||||
import { exportAsyncFromTable } from '@/utils/excelExport'
|
||||
import OrderSourceSyncLogDrawer from '../../monitor/orderSource/components/OrderSourceSyncLogDrawer.vue'
|
||||
import OrderBillProductCheckDrawer from './components/OrderBillProductCheckDrawer.vue'
|
||||
|
||||
@@ -284,6 +294,15 @@ function queryList(params) {
|
||||
return listOrderBill(applySearchRanges(params))
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
return exportAsyncFromTable({
|
||||
baseUrl: '/price/billing/orderBill/export',
|
||||
tableRef: tableRef.value,
|
||||
normalize: applySearchRanges,
|
||||
filePrefix: 'order_bill',
|
||||
requireFilter: true
|
||||
})
|
||||
}
|
||||
|
||||
const MAX_REBILL = 10000
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
<div class="wizard-actions">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button v-if="showBasic" type="info" :loading="saving" @click="saveDraft">保存</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="finishEdit">完成</el-button>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +66,6 @@ const route = useRoute()
|
||||
|
||||
const refData = usePriceSetRefData()
|
||||
|
||||
const saving = ref(false)
|
||||
const creating = ref(false)
|
||||
const progressVisible = ref(false)
|
||||
const progressPercent = ref(0)
|
||||
@@ -248,23 +246,6 @@ async function finishEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist basic draft (no detail wipe). */
|
||||
async function saveDraft() {
|
||||
const validBasic = await basicRef.value.validate()
|
||||
if (!validBasic) return
|
||||
saving.value = true
|
||||
try {
|
||||
const priceSetId = await runSaveWithProgress(buildPayload())
|
||||
form.priceSetId = priceSetId
|
||||
proxy.$modal.msgSuccess('已保存(待启用),套餐ID=' + priceSetId)
|
||||
goBack()
|
||||
} catch (e) {
|
||||
// surfaced
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function buildPayload(opts = {}) {
|
||||
const { basic, version } = form
|
||||
const detailRows = form.details.filter(d => d && d.productCode)
|
||||
|
||||
Reference in New Issue
Block a user