This commit is contained in:
@@ -15,12 +15,12 @@ export const JULY_PERMISSIONS = {
|
||||
},
|
||||
speedTier: {
|
||||
view: 'iot_card:speed_tier',
|
||||
update: 'iot_card:speed_tier:update'
|
||||
update: 'iot_card:speed_tier_update'
|
||||
},
|
||||
deviceAllocation: {
|
||||
page: 'device_task:allocation',
|
||||
create: 'device_task:allocation:create',
|
||||
detail: 'device_task:allocation:detail'
|
||||
create: 'device_task:allocation_create',
|
||||
detail: 'device_task:allocation_detail'
|
||||
},
|
||||
seriesGrants: {
|
||||
updateExpiryBase: 'series_grants:update_expiry_base'
|
||||
|
||||
1
src/template/批量订购套餐模板.csv
Normal file
1
src/template/批量订购套餐模板.csv
Normal file
@@ -0,0 +1 @@
|
||||
89861590172420377385
|
||||
|
@@ -244,6 +244,7 @@ export interface DeviceImportTask {
|
||||
operation_type: DeviceImportTaskOperationType
|
||||
operation_name: string
|
||||
target_id: number | null
|
||||
target_name?: string | null
|
||||
realname_policy: 'none' | 'before_order' | 'after_order'
|
||||
warning_count: number
|
||||
created_at: string // 创建时间
|
||||
|
||||
@@ -718,7 +718,7 @@
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => handleViewDetail(row)
|
||||
onClick: () => handleNameClick(row)
|
||||
},
|
||||
() => row.exchange_no || '--'
|
||||
)
|
||||
@@ -1410,6 +1410,14 @@
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleNameClick = (row: ExchangeResponse) => {
|
||||
if (hasAuth('exchange:detail')) {
|
||||
handleViewDetail(row)
|
||||
} else {
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: any) => {
|
||||
router.push(`${RoutesAlias.ExchangeDetail}/${row.id}`)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:close-on-press-escape="!createDrawerBusy"
|
||||
@closed="handleCreateDrawerClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="create-form">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="90px" class="create-form">
|
||||
<ElFormItem label="套餐系列" prop="series_id">
|
||||
<ElSelect
|
||||
v-model="form.series_id"
|
||||
@@ -243,7 +243,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
ElAlert,
|
||||
@@ -299,6 +299,9 @@
|
||||
const taskListSize = ref(20)
|
||||
const taskListTotal = ref(0)
|
||||
const taskListStatus = ref<BulkPurchaseTaskStatus | undefined>()
|
||||
const taskStatusPollInterval = 2000
|
||||
let taskStatusPollTimer: number | undefined
|
||||
let taskStatusPollSession = 0
|
||||
const createDrawerBusy = computed(
|
||||
() => submitting.value || voucherUploading.value || orderFileUploading.value
|
||||
)
|
||||
@@ -465,9 +468,10 @@
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success(res.data.message || '批量订购任务已创建')
|
||||
createDrawerVisible.value = false
|
||||
await router.push(`${RoutesAlias.BulkPurchaseDetail}/${taskId}`)
|
||||
await loadTaskList()
|
||||
startTaskStatusPolling(taskId, res.data?.status ?? BulkPurchaseTaskStatus.PENDING)
|
||||
ElMessage.success(res.data.message || '批量订购任务已创建')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '创建批量订购任务失败')
|
||||
} finally {
|
||||
@@ -508,6 +512,49 @@
|
||||
void loadTaskList()
|
||||
}
|
||||
|
||||
const stopTaskStatusPolling = () => {
|
||||
taskStatusPollSession += 1
|
||||
if (taskStatusPollTimer !== undefined) {
|
||||
window.clearTimeout(taskStatusPollTimer)
|
||||
taskStatusPollTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const startTaskStatusPolling = (
|
||||
taskId: number,
|
||||
initialStatus = BulkPurchaseTaskStatus.PENDING
|
||||
) => {
|
||||
stopTaskStatusPolling()
|
||||
const session = taskStatusPollSession
|
||||
|
||||
const poll = async () => {
|
||||
if (session !== taskStatusPollSession) return
|
||||
|
||||
try {
|
||||
const response = await BulkPurchaseService.getTasks({
|
||||
page: 1,
|
||||
page_size: 100
|
||||
})
|
||||
if (response.code === 0) {
|
||||
const task = response.data.items.find((item) => (item.task_id ?? item.id) === taskId)
|
||||
if (task && task.status !== initialStatus) {
|
||||
await loadTaskList()
|
||||
stopTaskStatusPolling()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('轮询批量订购任务状态失败:', error)
|
||||
}
|
||||
|
||||
if (session === taskStatusPollSession) {
|
||||
taskStatusPollTimer = window.setTimeout(() => void poll(), taskStatusPollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
void poll()
|
||||
}
|
||||
|
||||
const showTask = (task: BulkPurchaseTask) => {
|
||||
const taskId = task.task_id ?? task.id
|
||||
if (!taskId) return
|
||||
@@ -540,6 +587,10 @@
|
||||
onMounted(() => {
|
||||
void loadTaskList()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopTaskStatusPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
single-column-csv
|
||||
:max-csv-rows="1000"
|
||||
accept=".csv"
|
||||
tip="仅支持 UTF-8 单列 CSV,最多 1000 行,最大 10MB"
|
||||
tip="格式:一行一个唯一标识;仅支持 UTF-8 单列 CSV,最多 1000 行,最大 10MB"
|
||||
@uploading-change="fileUploading = $event"
|
||||
/>
|
||||
</ElFormItem>
|
||||
@@ -106,9 +106,7 @@
|
||||
<div class="task-list-toolbar">
|
||||
<ElButton
|
||||
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.create)"
|
||||
tag="a"
|
||||
href="/templates/device-batch-allocation-template.csv"
|
||||
download="设备批量任务模板.csv"
|
||||
@click="downloadTemplate"
|
||||
>
|
||||
下载模板
|
||||
</ElButton>
|
||||
@@ -148,7 +146,7 @@
|
||||
</template>
|
||||
|
||||
<ElTable v-loading="taskListLoading" :data="taskList" border>
|
||||
<ElTableColumn label="任务号" min-width="210" show-overflow-tooltip>
|
||||
<ElTableColumn label="任务号" min-width="225" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<ElButton
|
||||
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.detail)"
|
||||
@@ -178,8 +176,8 @@
|
||||
{{ scope.row.operation_name || getOperationLabel(scope.row.operation_type) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="目标 ID" width="110">
|
||||
<template #default="scope">{{ scope.row.target_id ?? '-' }}</template>
|
||||
<ElTableColumn label="目标名称" min-width="160" show-overflow-tooltip>
|
||||
<template #default="scope">{{ scope.row.target_name || '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="110">
|
||||
<template #default="scope">
|
||||
@@ -218,7 +216,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
ElAlert,
|
||||
@@ -267,6 +265,9 @@
|
||||
const taskListTotal = ref(0)
|
||||
const taskListStatus = ref<DeviceImportTaskStatus | undefined>()
|
||||
const taskListOperationType = ref<Exclude<DeviceImportTaskOperationType, 'import'> | undefined>()
|
||||
const taskStatusPollInterval = 2000
|
||||
let taskStatusPollTimer: number | undefined
|
||||
let taskStatusPollSession = 0
|
||||
const shopOptions = ref<ShopResponse[]>([])
|
||||
const seriesOptions = ref<PackageSeriesResponse[]>([])
|
||||
const targetLoading = ref(false)
|
||||
@@ -387,6 +388,21 @@
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
const downloadTemplate = () => {
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = new URL('@/template/批量订购套餐模板.csv', import.meta.url).href
|
||||
link.download = '批量订购套餐模板.csv'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
ElMessage.success('模板下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载模板失败:', error)
|
||||
ElMessage.error('模板下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskListFilterChange = () => {
|
||||
taskListPage.value = 1
|
||||
void loadTaskList()
|
||||
@@ -420,6 +436,49 @@
|
||||
void loadTaskList()
|
||||
}
|
||||
|
||||
const stopTaskStatusPolling = () => {
|
||||
taskStatusPollSession += 1
|
||||
if (taskStatusPollTimer !== undefined) {
|
||||
window.clearTimeout(taskStatusPollTimer)
|
||||
taskStatusPollTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const startTaskStatusPolling = (
|
||||
taskId: number,
|
||||
initialStatus = DeviceImportTaskStatus.PENDING
|
||||
) => {
|
||||
stopTaskStatusPolling()
|
||||
const session = taskStatusPollSession
|
||||
|
||||
const poll = async () => {
|
||||
if (session !== taskStatusPollSession) return
|
||||
|
||||
try {
|
||||
const response = await DeviceService.getImportTasks({
|
||||
page: 1,
|
||||
page_size: 100
|
||||
})
|
||||
if (response.code === 0) {
|
||||
const task = response.data.items.find((item) => item.id === taskId)
|
||||
if (task && task.status !== initialStatus) {
|
||||
await loadTaskList()
|
||||
stopTaskStatusPolling()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('轮询设备批量任务状态失败:', error)
|
||||
}
|
||||
|
||||
if (session === taskStatusPollSession) {
|
||||
taskStatusPollTimer = window.setTimeout(() => void poll(), taskStatusPollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
void poll()
|
||||
}
|
||||
|
||||
const showTask = (row: DeviceImportTask) => {
|
||||
router.push({
|
||||
path: RoutesAlias.TaskDetail,
|
||||
@@ -453,6 +512,7 @@
|
||||
}
|
||||
createDrawerVisible.value = false
|
||||
await loadTaskList()
|
||||
startTaskStatusPolling(res.data.task_id)
|
||||
ElMessage.success(`任务已创建:${res.data.task_no}`)
|
||||
} catch (error) {
|
||||
console.error('创建设备批量任务失败:', error)
|
||||
@@ -465,6 +525,10 @@
|
||||
onMounted(() => {
|
||||
void loadTaskList()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopTaskStatusPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="180"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
@@ -47,29 +45,32 @@
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDialog
|
||||
<ElDrawer
|
||||
v-model="createDialogVisible"
|
||||
title="新建订单套餐作废任务"
|
||||
width="560px"
|
||||
direction="rtl"
|
||||
size="520px"
|
||||
:close-on-click-modal="!createDrawerBusy"
|
||||
:close-on-press-escape="!createDrawerBusy"
|
||||
@closed="handleCreateDialogClosed"
|
||||
>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
|
||||
<ElFormItem label="CSV文件" prop="file">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".csv,text/csv"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text">将 CSV 文件拖到此处,或<em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">仅支持 .csv 文件,必填列:order_no</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
<VoucherUpload
|
||||
ref="csvUploadRef"
|
||||
v-model="csvFileKeys"
|
||||
voucher-name="CSV文件"
|
||||
:max-count="1"
|
||||
purpose="attachment"
|
||||
:max-size-mb="10"
|
||||
single-column-csv
|
||||
:max-csv-rows="1000"
|
||||
accept=".csv"
|
||||
tip="仅支持 UTF-8 单列 CSV,每行一个订单号,最多 1000 行,最大 10MB"
|
||||
@uploading-change="csvUploading = $event"
|
||||
@files-change="handleCsvFilesChange"
|
||||
@change="createFormRef?.validateField('file')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="凭证附件">
|
||||
<VoucherUpload
|
||||
@@ -91,17 +92,19 @@
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="createDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="creating || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
@click="handleCreateTask"
|
||||
>
|
||||
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
|
||||
</ElButton>
|
||||
<div class="drawer-footer">
|
||||
<ElButton @click="createDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="createDrawerBusy"
|
||||
:disabled="createDrawerBusy"
|
||||
@click="handleCreateTask"
|
||||
>
|
||||
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElDrawer>
|
||||
|
||||
<PaymentVoucherDialog :file-keys="voucherPreviewKeys" @close="voucherPreviewKeys = []" />
|
||||
</div>
|
||||
@@ -109,22 +112,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElDrawer,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElTag,
|
||||
ElUpload
|
||||
ElTag
|
||||
} from 'element-plus'
|
||||
import { Upload, UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
|
||||
import { OrderPackageInvalidateTaskService, StorageService } from '@/api/modules'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { OrderPackageInvalidateTaskService } from '@/api/modules'
|
||||
import type {
|
||||
CreateOrderPackageInvalidateTaskRequest,
|
||||
OrderPackageInvalidateTask,
|
||||
@@ -147,15 +149,20 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const creating = ref(false)
|
||||
const csvUploading = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const csvUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const createDialogVisible = ref(false)
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const csvFileKeys = ref<string[]>([])
|
||||
const csvFileName = ref('')
|
||||
const taskList = ref<OrderPackageInvalidateTask[]>([])
|
||||
const voucherPreviewKeys = ref<string[]>([])
|
||||
const createDrawerBusy = computed(
|
||||
() => creating.value || csvUploading.value || voucherUploading.value
|
||||
)
|
||||
|
||||
const searchForm = reactive<OrderPackageInvalidateTaskQueryParams>({
|
||||
page: 1,
|
||||
@@ -384,58 +391,31 @@
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleFileChange = (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
if (!/\.csv$/i.test(file.name)) {
|
||||
ElMessage.error('仅支持 CSV 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
selectedFile.value = null
|
||||
createForm.file = ''
|
||||
return
|
||||
}
|
||||
selectedFile.value = file
|
||||
createForm.file = file.name
|
||||
createFormRef.value?.validateField('file')
|
||||
}
|
||||
|
||||
const handleFileRemove = () => {
|
||||
selectedFile.value = null
|
||||
createForm.file = ''
|
||||
createFormRef.value?.validateField('file')
|
||||
}
|
||||
|
||||
const uploadCsvFile = async (file: File) => {
|
||||
const contentType = file.type || 'text/csv'
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: contentType,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
}
|
||||
|
||||
await StorageService.uploadFile(uploadUrlRes.data.upload_url, file, contentType)
|
||||
return uploadUrlRes.data.file_key
|
||||
const handleCsvFilesChange = (files: Array<{ file_name?: string }>) => {
|
||||
csvFileName.value = files[0]?.file_name || ''
|
||||
createForm.file = csvFileName.value
|
||||
void createFormRef.value?.validateField('file')
|
||||
}
|
||||
|
||||
const handleCreateTask = async () => {
|
||||
if (!createFormRef.value) return
|
||||
if (csvUploading.value) {
|
||||
ElMessage.warning('CSV 文件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
if (voucherUploading.value) {
|
||||
ElMessage.warning('凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
const valid = await createFormRef.value.validate().catch(() => false)
|
||||
if (!valid || !selectedFile.value) return
|
||||
if (!valid || !csvFileKeys.value[0] || !csvFileName.value) return
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const fileKey = await uploadCsvFile(selectedFile.value)
|
||||
const data: CreateOrderPackageInvalidateTaskRequest = {
|
||||
file_key: fileKey,
|
||||
file_name: selectedFile.value.name,
|
||||
file_key: csvFileKeys.value[0],
|
||||
file_name: csvFileName.value,
|
||||
voucher_keys: toVoucherKeyList(createForm.voucher_keys),
|
||||
remark: createForm.remark || undefined
|
||||
}
|
||||
@@ -453,24 +433,26 @@
|
||||
|
||||
const handleCreateDialogClosed = () => {
|
||||
createFormRef.value?.resetFields()
|
||||
uploadRef.value?.clearFiles()
|
||||
csvUploadRef.value?.clearFiles(false)
|
||||
voucherUploadRef.value?.clearFiles(false)
|
||||
selectedFile.value = null
|
||||
csvFileKeys.value = []
|
||||
csvFileName.value = ''
|
||||
createForm.file = ''
|
||||
createForm.voucher_keys = []
|
||||
createForm.remark = ''
|
||||
csvUploading.value = false
|
||||
voucherUploading.value = false
|
||||
}
|
||||
|
||||
const getActions = (row: OrderPackageInvalidateTask) => {
|
||||
const actions: any[] = []
|
||||
if (hasAuth('order_package_invalidate_task:detail')) {
|
||||
actions.push({ label: '详情', handler: () => handleNameClick(row), type: 'primary' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -166,9 +166,9 @@
|
||||
value || getOperationLabel(deviceTask.value?.operation_type)
|
||||
},
|
||||
{
|
||||
label: '目标 ID',
|
||||
prop: 'target_id',
|
||||
formatter: (value: number | null) => String(value ?? '-')
|
||||
label: '目标名称',
|
||||
prop: 'target_name',
|
||||
formatter: (value: string | null | undefined) => value || '-'
|
||||
}
|
||||
]
|
||||
: []
|
||||
|
||||
@@ -998,7 +998,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('shop:credit-limit:manage')) {
|
||||
if (hasAuth('shop:credit_limit_manage')) {
|
||||
actions.push({
|
||||
label: '调整额度',
|
||||
handler: () => showCreditDialog(row),
|
||||
|
||||
Reference in New Issue
Block a user