feat: 7月迭代
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m21s

This commit is contained in:
luo
2026-07-28 14:00:22 +08:00
parent f0a2b84e53
commit 2706c52a47
63 changed files with 4113 additions and 3945 deletions

View File

@@ -0,0 +1,506 @@
<template>
<ArtTableFullScreen>
<div class="bulk-purchase-page" id="table-full-screen">
<ElDrawer
v-model="createDrawerVisible"
title="批量订购套餐"
direction="rtl"
size="520px"
:close-on-click-modal="!createDrawerBusy"
:close-on-press-escape="!createDrawerBusy"
@closed="handleCreateDrawerClosed"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="create-form">
<ElFormItem label="套餐" prop="package_id">
<ElSelect
v-model="form.package_id"
filterable
remote
clearable
:remote-method="handlePackageSearch"
:loading="packageLoading"
placeholder="请输入套餐名称搜索"
style="width: 100%"
@visible-change="handlePackageVisibleChange"
>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
:label="pkg.package_name"
:value="pkg.id"
>
<span class="package-option">{{ pkg.package_name }}</span>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="支付方式" prop="payment_method">
<ElRadioGroup v-model="form.payment_method" @change="handlePaymentMethodChange">
<ElRadio value="wallet">代理钱包</ElRadio>
<ElRadio value="offline">线下支付</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="订单文件" prop="orderFile">
<VoucherUpload
ref="orderUploadRef"
v-model="orderFileKeys"
voucher-name="订单文件"
:max-count="1"
purpose="batch_purchase"
:max-size-mb="10"
single-column-csv
:max-csv-rows="1000"
accept=".csv"
tip="仅支持 UTF-8 单列 CSV最多 1000 行,最大 10MB"
@uploading-change="orderFileUploading = $event"
@change="formRef?.validateField('orderFile')"
/>
</ElFormItem>
<ElFormItem
v-if="form.payment_method === 'offline'"
label="整批支付凭证"
prop="voucherFile"
>
<VoucherUpload
ref="voucherUploadRef"
v-model="voucherFileKeys"
voucher-name="整批支付凭证"
@uploading-change="voucherUploading = $event"
@change="formRef?.validateField('voucherFile')"
/>
</ElFormItem>
<ElAlert
v-if="createDrawerBusy"
type="info"
:closable="false"
show-icon
:title="
voucherUploading
? '支付凭证上传中,请稍候'
: orderFileUploading
? '订单文件上传中,请稍候'
: '批量订购文件上传及任务创建中,请勿重复提交'
"
/>
</ElForm>
<template #footer>
<div class="drawer-footer">
<ElButton :disabled="createDrawerBusy" @click="createDrawerVisible = false">
取消
</ElButton>
<ElButton
type="primary"
:loading="createDrawerBusy"
:disabled="!hasAuth(BULK_PURCHASE_PERMISSIONS.create)"
@click="submitTask"
>
创建批量订购任务
</ElButton>
</div>
</template>
</ElDrawer>
<ElCard
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.detail)"
shadow="never"
class="art-table-card task-list-card"
>
<template #header>
<div class="task-list-header">
<span>批量订购任务</span>
<div class="task-list-toolbar">
<ElButton
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.template)"
tag="a"
href="/templates/bulk-purchase-template.csv"
download="批量订购套餐模板.csv"
>
下载模板
</ElButton>
<ElButton
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.create)"
type="primary"
@click="openCreateDrawer"
>
批量订购套餐
</ElButton>
<ElSelect
v-model="taskListStatus"
clearable
placeholder="筛选任务状态"
style="width: 150px"
@change="handleTaskListStatusChange"
>
<ElOption label="待处理" :value="BulkPurchaseTaskStatus.PENDING" />
<ElOption label="处理中" :value="BulkPurchaseTaskStatus.PROCESSING" />
<ElOption label="已完成" :value="BulkPurchaseTaskStatus.COMPLETED" />
<ElOption label="已失败" :value="BulkPurchaseTaskStatus.FAILED" />
<ElOption label="已取消" :value="BulkPurchaseTaskStatus.CANCELED" />
</ElSelect>
<ElButton :loading="taskListLoading" @click="loadTaskList">刷新</ElButton>
</div>
</div>
</template>
<ElTable v-loading="taskListLoading" :data="taskList" border>
<ElTableColumn label="任务号" min-width="200" show-overflow-tooltip>
<template #default="scope">
<ElButton type="primary" link @click="showTask(scope.row)">
{{ scope.row.task_no || '-' }}
</ElButton>
</template>
</ElTableColumn>
<ElTableColumn prop="file_name" label="订单文件" min-width="180" show-overflow-tooltip />
<ElTableColumn
prop="package_name"
label="套餐名称"
min-width="180"
show-overflow-tooltip
/>
<ElTableColumn
prop="package_code"
label="套餐编码"
min-width="180"
show-overflow-tooltip
/>
<ElTableColumn label="支付方式" width="110">
<template #default="scope">{{
getPaymentMethodName(scope.row.payment_method)
}}</template>
</ElTableColumn>
<ElTableColumn label="状态" width="110">
<template #default="scope">
<ElTag :type="getStatusType(scope.row.status)">
{{ scope.row.status_name || getStatusName(scope.row.status) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="total_count" label="总数" width="90" />
<ElTableColumn prop="success_count" label="成功数" width="90" />
<ElTableColumn prop="fail_count" label="失败数" width="90" />
<ElTableColumn label="创建时间" width="180">
<template #default="scope">{{ formatDateTime(scope.row.created_at) }}</template>
</ElTableColumn>
</ElTable>
<div class="pagination-wrapper">
<ElPagination
v-model:current-page="taskListPage"
v-model:page-size="taskListSize"
layout="total, sizes, prev, pager, next"
:total="taskListTotal"
@current-change="loadTaskList"
@size-change="handleTaskListSizeChange"
/>
</div>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import {
ElAlert,
ElButton,
ElCard,
ElDrawer,
ElForm,
ElMessage,
ElOption,
ElPagination,
ElRadio,
ElRadioGroup,
ElSelect,
ElTable,
ElTableColumn,
ElTag
} from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
import { BulkPurchaseService, PackageManageService } from '@/api/modules'
import type { BulkPurchasePaymentMethod, BulkPurchaseTask, PackageResponse } from '@/types/api'
import { BulkPurchaseTaskStatus } from '@/types/api'
import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format'
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'BulkPurchase' })
const router = useRouter()
const { hasAuth } = useAuth()
const formRef = ref<FormInstance>()
const orderUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const createDrawerVisible = ref(false)
const submitting = ref(false)
const orderFileKeys = ref<string[]>([])
const orderFileUploading = ref(false)
const voucherFileKeys = ref<string[]>([])
const voucherUploading = ref(false)
const packageOptions = ref<PackageResponse[]>([])
const packageLoading = ref(false)
const taskList = ref<BulkPurchaseTask[]>([])
const taskListLoading = ref(false)
const taskListPage = ref(1)
const taskListSize = ref(20)
const taskListTotal = ref(0)
const taskListStatus = ref<BulkPurchaseTaskStatus | undefined>()
const createDrawerBusy = computed(
() => submitting.value || voucherUploading.value || orderFileUploading.value
)
const form = reactive<{
package_id?: number
payment_method: BulkPurchasePaymentMethod
}>({
package_id: undefined,
payment_method: 'wallet'
})
const rules = computed<FormRules>(() => ({
package_id: [{ required: true, message: '请选择套餐', trigger: 'change' }],
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
orderFile: [
{
validator: (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
if (orderFileKeys.value.length === 0) callback(new Error('请上传订单文件'))
else callback()
},
trigger: 'change'
}
],
voucherFile: [
{
validator: (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
if (form.payment_method === 'offline' && voucherFileKeys.value.length === 0) {
callback(new Error('线下支付必须上传整批支付凭证'))
} else callback()
},
trigger: 'change'
}
]
}))
const loadPackages = async (query = '') => {
packageLoading.value = true
try {
const res = await PackageManageService.getPackages({
page: 1,
page_size: 20,
package_name: query.trim() || undefined,
status: 1,
shelf_status: 1
})
if (res.code !== 0) {
ElMessage.error(res.msg || '获取套餐列表失败')
return
}
packageOptions.value = (res.data?.items || []).filter(
(item) => item.status === 1 && item.shelf_status === 1
)
} catch (error: any) {
ElMessage.error(error?.message || '获取套餐列表失败')
} finally {
packageLoading.value = false
}
}
const handlePackageSearch = (query: string) => {
void loadPackages(query)
}
const handlePackageVisibleChange = (visible: boolean) => {
if (visible && packageOptions.value.length === 0) void loadPackages()
}
const openCreateDrawer = () => {
createDrawerVisible.value = true
if (packageOptions.value.length === 0) void loadPackages()
}
const handleCreateDrawerClosed = () => {
formRef.value?.resetFields()
form.package_id = undefined
form.payment_method = 'wallet'
orderFileKeys.value = []
voucherFileKeys.value = []
orderUploadRef.value?.clearFiles(false)
voucherUploadRef.value?.clearFiles(false)
orderFileUploading.value = false
voucherUploading.value = false
}
const handlePaymentMethodChange = (value: string | number | boolean | undefined) => {
if (value === 'wallet') {
voucherFileKeys.value = []
voucherUploadRef.value?.clearFiles()
formRef.value?.clearValidate('voucherFile')
}
}
const submitTask = async () => {
if (
!hasAuth(BULK_PURCHASE_PERMISSIONS.create) ||
submitting.value ||
voucherUploading.value ||
orderFileUploading.value
)
return
const valid = await formRef.value?.validate().catch(() => false)
if (!valid || !form.package_id || orderFileKeys.value.length === 0) return
submitting.value = true
try {
const data = {
file_key: orderFileKeys.value[0],
package_id: form.package_id,
payment_method: form.payment_method,
...(form.payment_method === 'offline' ? { voucher_keys: voucherFileKeys.value } : {})
}
const res = await BulkPurchaseService.createTask(data)
const taskId = res.data?.task_id ?? res.data?.id
if (res.code !== 0 || !taskId) {
ElMessage.error(res.msg || '创建批量订购任务失败')
return
}
ElMessage.success(res.data.message || '批量订购任务已创建')
createDrawerVisible.value = false
await router.push(`${RoutesAlias.BulkPurchaseDetail}/${taskId}`)
} catch (error: any) {
ElMessage.error(error?.message || '创建批量订购任务失败')
} finally {
submitting.value = false
}
}
const loadTaskList = async () => {
if (!hasAuth(BULK_PURCHASE_PERMISSIONS.detail)) return
taskListLoading.value = true
try {
const res = await BulkPurchaseService.getTasks({
page: taskListPage.value,
page_size: taskListSize.value,
status: taskListStatus.value
})
if (res.code !== 0) {
ElMessage.error(res.msg || '获取批量订购任务列表失败')
return
}
taskList.value = res.data?.items || []
taskListTotal.value = res.data?.total || 0
} catch (error: any) {
ElMessage.error(error?.message || '获取批量订购任务列表失败')
} finally {
taskListLoading.value = false
}
}
const handleTaskListStatusChange = () => {
taskListPage.value = 1
void loadTaskList()
}
const handleTaskListSizeChange = () => {
taskListPage.value = 1
void loadTaskList()
}
const showTask = (task: BulkPurchaseTask) => {
const taskId = task.task_id ?? task.id
if (!taskId) return
void router.push(`${RoutesAlias.BulkPurchaseDetail}/${taskId}`)
}
const getStatusName = (status: BulkPurchaseTaskStatus) => {
const names: Record<BulkPurchaseTaskStatus, string> = {
[BulkPurchaseTaskStatus.PENDING]: '待处理',
[BulkPurchaseTaskStatus.PROCESSING]: '处理中',
[BulkPurchaseTaskStatus.COMPLETED]: '已完成',
[BulkPurchaseTaskStatus.FAILED]: '已失败',
[BulkPurchaseTaskStatus.CANCELED]: '已取消'
}
return names[status] || '-'
}
const getStatusType = (status: BulkPurchaseTaskStatus) => {
if (status === BulkPurchaseTaskStatus.COMPLETED) return 'success'
if (status === BulkPurchaseTaskStatus.FAILED) return 'danger'
if (status === BulkPurchaseTaskStatus.PROCESSING) return 'warning'
return 'info'
}
const getPaymentMethodName = (method?: BulkPurchasePaymentMethod) => {
return method === 'offline' ? '线下支付' : method === 'wallet' ? '代理钱包' : '-'
}
onMounted(() => {
void loadTaskList()
})
</script>
<style scoped lang="scss">
.bulk-purchase-page {
.task-list-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.task-list-toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.package-option {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.create-form {
padding-bottom: 8px;
}
.drawer-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
@media (max-width: 768px) {
.task-list-header {
align-items: flex-start;
flex-direction: column;
}
.task-list-toolbar {
width: 100%;
.el-select {
flex: 1;
}
}
}
}
</style>