增加每日推送的导出和排序

This commit is contained in:
2026-07-21 15:49:29 +08:00
parent 5aad851108
commit 2fbc07329e
2 changed files with 468 additions and 0 deletions

View File

@@ -0,0 +1,263 @@
<template>
<el-dialog
v-model="visible"
class="ba-operate-dialog"
:close-on-click-modal="false"
width="560px"
@open="onOpen"
>
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ t('mall.dailyPush.export_title') }}
</div>
</template>
<el-alert type="info" :closable="false" class="export-tip">
{{ t('mall.dailyPush.export_tip') }}
</el-alert>
<div class="export-section">
<div class="section-title">{{ t('mall.dailyPush.export_fields') }}</div>
<div class="field-actions">
<el-button link type="primary" @click="selectAllFields">{{ t('mall.dailyPush.export_select_all') }}</el-button>
<el-button link type="primary" @click="clearFields">{{ t('mall.dailyPush.export_clear_all') }}</el-button>
</div>
<el-checkbox-group v-model="form.fields" class="field-group">
<el-checkbox v-for="item in fieldOptions" :key="item.value" :label="item.value" :value="item.value">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</div>
<div class="export-section">
<div class="section-title">{{ t('mall.dailyPush.export_limit') }}</div>
<div class="limit-row">
<el-input-number v-model="form.exportLimit" :min="1" :max="maxLimit" :step="1000" controls-position="right" />
<div class="limit-presets">
<el-button
v-for="preset in limitPresets"
:key="preset"
size="small"
:type="form.exportLimit === preset ? 'primary' : 'default'"
@click="form.exportLimit = preset"
>
{{ preset }}
</el-button>
<el-button size="small" :type="form.exportLimit === matchedCount ? 'primary' : 'default'" @click="useMatchedCount">
{{ t('mall.dailyPush.export_all_matched') }}
</el-button>
</div>
</div>
<div class="count-info" v-loading="countLoading">
{{ t('mall.dailyPush.export_matched_count', { count: matchedCount }) }}
<span v-if="actualExportCount < form.exportLimit">
{{ t('mall.dailyPush.export_actual_count', { count: actualExportCount }) }}
</span>
</div>
<el-alert v-if="form.exportLimit > 10000" type="warning" :closable="false" class="large-export-warning">
{{ t('mall.dailyPush.export_large_warning', { max: maxLimit }) }}
</el-alert>
</div>
<template #footer>
<el-button @click="visible = false">{{ t('Cancel') }}</el-button>
<el-button type="primary" :loading="exporting" :disabled="form.fields.length === 0" @click="submitExport">
{{ t('mall.dailyPush.export_confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, inject, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElNotification } from 'element-plus'
import createAxios from '/@/utils/axios'
import type baTableClass from '/@/utils/baTable'
import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
const { t } = useI18n()
const baTable = inject('baTable') as baTableClass
const visible = defineModel<boolean>({ default: false })
const exporting = ref(false)
const countLoading = ref(false)
const matchedCount = ref(0)
const maxLimit = ref(100000)
const limitPresets = [1000, 5000, 10000, 50000]
const fieldOptions = computed(() => [
{ value: 'id', label: t('mall.dailyPush.id') },
{ value: 'user_id', label: t('mall.dailyPush.user_id') },
{ value: 'date', label: t('mall.dailyPush.date') },
{ value: 'username', label: t('mall.dailyPush.username') },
{ value: 'yesterday_win_loss_net', label: t('mall.dailyPush.yesterday_win_loss_net') },
{ value: 'yesterday_total_deposit', label: t('mall.dailyPush.yesterday_total_deposit') },
{ value: 'lifetime_total_deposit', label: t('mall.dailyPush.lifetime_total_deposit') },
{ value: 'lifetime_total_withdraw', label: t('mall.dailyPush.lifetime_total_withdraw') },
{ value: 'create_time', label: t('mall.dailyPush.create_time') },
])
const form = reactive({
fields: fieldOptions.value.map((item) => item.value),
exportLimit: 10000,
})
const actualExportCount = computed(() => Math.min(form.exportLimit, matchedCount.value, maxLimit.value))
const selectAllFields = () => {
form.fields = fieldOptions.value.map((item) => item.value)
}
const clearFields = () => {
form.fields = []
}
const useMatchedCount = () => {
form.exportLimit = Math.min(matchedCount.value || 1, maxLimit.value)
}
const buildExportParams = () => {
const filter = baTable.table.filter || {}
return {
...filter,
fields: form.fields,
export_limit: actualExportCount.value,
}
}
const loadMatchedCount = async () => {
countLoading.value = true
try {
const res = await createAxios<{ count: number; max_limit: number }>({
url: '/admin/mall.DailyPush/exportCount',
method: 'get',
params: baTable.table.filter || {},
})
matchedCount.value = res.data.count
maxLimit.value = res.data.max_limit
} catch {
matchedCount.value = baTable.table.total || 0
} finally {
countLoading.value = false
}
}
const parseBlobError = async (blob: Blob): Promise<string> => {
try {
const text = await blob.text()
const json = JSON.parse(text)
if (json && typeof json.msg === 'string' && json.msg !== '') {
return json.msg
}
} catch {
// ignore
}
return t('mall.dailyPush.export_failed')
}
const downloadBlob = (blob: Blob, filename: string) => {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
}
const submitExport = async () => {
if (form.fields.length === 0) {
return
}
exporting.value = true
try {
const response = await createAxios<Blob>(
{
url: '/admin/mall.DailyPush/export',
method: 'get',
params: buildExportParams(),
responseType: 'blob',
timeout: 0,
},
{
reductDataFormat: false,
showErrorMessage: false,
cancelDuplicateRequest: false,
}
)
const blob = response.data
const contentType = String(response.headers['content-type'] || '')
if (contentType.includes('application/json')) {
throw new Error(await parseBlobError(blob))
}
const disposition = String(response.headers['content-disposition'] || '')
const filenameMatch = disposition.match(/filename="?([^";]+)"?/i)
const filename = filenameMatch?.[1] || `daily_push_${Date.now()}.csv`
downloadBlob(blob, filename)
ElNotification({
type: 'success',
message: t('mall.dailyPush.export_success', { count: actualExportCount.value }),
zIndex: SYSTEM_ZINDEX,
})
visible.value = false
} catch (error) {
const message = error instanceof Error ? error.message : t('mall.dailyPush.export_failed')
ElNotification({
type: 'error',
message,
zIndex: SYSTEM_ZINDEX,
})
} finally {
exporting.value = false
}
}
const onOpen = () => {
loadMatchedCount()
}
</script>
<style scoped lang="scss">
.export-tip {
margin-bottom: 16px;
}
.export-section {
margin-bottom: 18px;
}
.section-title {
font-weight: 600;
margin-bottom: 8px;
}
.field-actions {
margin-bottom: 8px;
}
.field-group {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 12px;
}
.limit-row {
display: flex;
flex-direction: column;
gap: 10px;
}
.limit-presets {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.count-info {
margin-top: 10px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
.large-export-warning {
margin-top: 12px;
}
</style>