fix: 通知
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m20s

This commit is contained in:
luo
2026-07-28 18:40:54 +08:00
parent ee266c51cc
commit 831f03c3a7
9 changed files with 213 additions and 106 deletions

View File

@@ -15,12 +15,12 @@ export const JULY_PERMISSIONS = {
}, },
speedTier: { speedTier: {
view: 'iot_card:speed_tier', view: 'iot_card:speed_tier',
update: 'iot_card:speed_tier:update' update: 'iot_card:speed_tier_update'
}, },
deviceAllocation: { deviceAllocation: {
page: 'device_task:allocation', page: 'device_task:allocation',
create: 'device_task:allocation:create', create: 'device_task:allocation_create',
detail: 'device_task:allocation:detail' detail: 'device_task:allocation_detail'
}, },
seriesGrants: { seriesGrants: {
updateExpiryBase: 'series_grants:update_expiry_base' updateExpiryBase: 'series_grants:update_expiry_base'

View File

@@ -0,0 +1 @@
89861590172420377385
1 89861590172420377385

View File

@@ -244,6 +244,7 @@ export interface DeviceImportTask {
operation_type: DeviceImportTaskOperationType operation_type: DeviceImportTaskOperationType
operation_name: string operation_name: string
target_id: number | null target_id: number | null
target_name?: string | null
realname_policy: 'none' | 'before_order' | 'after_order' realname_policy: 'none' | 'before_order' | 'after_order'
warning_count: number warning_count: number
created_at: string // 创建时间 created_at: string // 创建时间

View File

@@ -718,7 +718,7 @@
{ {
type: 'primary', type: 'primary',
link: true, link: true,
onClick: () => handleViewDetail(row) onClick: () => handleNameClick(row)
}, },
() => row.exchange_no || '--' () => row.exchange_no || '--'
) )
@@ -1410,6 +1410,14 @@
} }
// 查看详情 // 查看详情
const handleNameClick = (row: ExchangeResponse) => {
if (hasAuth('exchange:detail')) {
handleViewDetail(row)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
const handleViewDetail = (row: any) => { const handleViewDetail = (row: any) => {
router.push(`${RoutesAlias.ExchangeDetail}/${row.id}`) router.push(`${RoutesAlias.ExchangeDetail}/${row.id}`)
} }

View File

@@ -10,7 +10,7 @@
:close-on-press-escape="!createDrawerBusy" :close-on-press-escape="!createDrawerBusy"
@closed="handleCreateDrawerClosed" @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"> <ElFormItem label="套餐系列" prop="series_id">
<ElSelect <ElSelect
v-model="form.series_id" v-model="form.series_id"
@@ -243,7 +243,7 @@
</template> </template>
<script setup lang="ts"> <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 { useRouter } from 'vue-router'
import { import {
ElAlert, ElAlert,
@@ -299,6 +299,9 @@
const taskListSize = ref(20) const taskListSize = ref(20)
const taskListTotal = ref(0) const taskListTotal = ref(0)
const taskListStatus = ref<BulkPurchaseTaskStatus | undefined>() const taskListStatus = ref<BulkPurchaseTaskStatus | undefined>()
const taskStatusPollInterval = 2000
let taskStatusPollTimer: number | undefined
let taskStatusPollSession = 0
const createDrawerBusy = computed( const createDrawerBusy = computed(
() => submitting.value || voucherUploading.value || orderFileUploading.value () => submitting.value || voucherUploading.value || orderFileUploading.value
) )
@@ -465,9 +468,10 @@
return return
} }
ElMessage.success(res.data.message || '批量订购任务已创建')
createDrawerVisible.value = false 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) { } catch (error: any) {
ElMessage.error(error?.message || '创建批量订购任务失败') ElMessage.error(error?.message || '创建批量订购任务失败')
} finally { } finally {
@@ -508,6 +512,49 @@
void loadTaskList() 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 showTask = (task: BulkPurchaseTask) => {
const taskId = task.task_id ?? task.id const taskId = task.task_id ?? task.id
if (!taskId) return if (!taskId) return
@@ -540,6 +587,10 @@
onMounted(() => { onMounted(() => {
void loadTaskList() void loadTaskList()
}) })
onBeforeUnmount(() => {
stopTaskStatusPolling()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -68,7 +68,7 @@
single-column-csv single-column-csv
:max-csv-rows="1000" :max-csv-rows="1000"
accept=".csv" accept=".csv"
tip="仅支持 UTF-8 单列 CSV最多 1000 行,最大 10MB" tip="格式:一行一个唯一标识;仅支持 UTF-8 单列 CSV最多 1000 行,最大 10MB"
@uploading-change="fileUploading = $event" @uploading-change="fileUploading = $event"
/> />
</ElFormItem> </ElFormItem>
@@ -106,9 +106,7 @@
<div class="task-list-toolbar"> <div class="task-list-toolbar">
<ElButton <ElButton
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.create)" v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.create)"
tag="a" @click="downloadTemplate"
href="/templates/device-batch-allocation-template.csv"
download="设备批量任务模板.csv"
> >
下载模板 下载模板
</ElButton> </ElButton>
@@ -148,7 +146,7 @@
</template> </template>
<ElTable v-loading="taskListLoading" :data="taskList" border> <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"> <template #default="scope">
<ElButton <ElButton
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.detail)" v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.detail)"
@@ -178,8 +176,8 @@
{{ scope.row.operation_name || getOperationLabel(scope.row.operation_type) }} {{ scope.row.operation_name || getOperationLabel(scope.row.operation_type) }}
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="目标 ID" width="110"> <ElTableColumn label="目标名称" min-width="160" show-overflow-tooltip>
<template #default="scope">{{ scope.row.target_id ?? '-' }}</template> <template #default="scope">{{ scope.row.target_name || '-' }}</template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="状态" width="110"> <ElTableColumn label="状态" width="110">
<template #default="scope"> <template #default="scope">
@@ -218,7 +216,7 @@
</template> </template>
<script setup lang="ts"> <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 { useRouter } from 'vue-router'
import { import {
ElAlert, ElAlert,
@@ -267,6 +265,9 @@
const taskListTotal = ref(0) const taskListTotal = ref(0)
const taskListStatus = ref<DeviceImportTaskStatus | undefined>() const taskListStatus = ref<DeviceImportTaskStatus | undefined>()
const taskListOperationType = ref<Exclude<DeviceImportTaskOperationType, 'import'> | undefined>() const taskListOperationType = ref<Exclude<DeviceImportTaskOperationType, 'import'> | undefined>()
const taskStatusPollInterval = 2000
let taskStatusPollTimer: number | undefined
let taskStatusPollSession = 0
const shopOptions = ref<ShopResponse[]>([]) const shopOptions = ref<ShopResponse[]>([])
const seriesOptions = ref<PackageSeriesResponse[]>([]) const seriesOptions = ref<PackageSeriesResponse[]>([])
const targetLoading = ref(false) const targetLoading = ref(false)
@@ -387,6 +388,21 @@
uploadRef.value?.clearFiles() 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 = () => { const handleTaskListFilterChange = () => {
taskListPage.value = 1 taskListPage.value = 1
void loadTaskList() void loadTaskList()
@@ -420,6 +436,49 @@
void loadTaskList() 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) => { const showTask = (row: DeviceImportTask) => {
router.push({ router.push({
path: RoutesAlias.TaskDetail, path: RoutesAlias.TaskDetail,
@@ -453,6 +512,7 @@
} }
createDrawerVisible.value = false createDrawerVisible.value = false
await loadTaskList() await loadTaskList()
startTaskStatusPolling(res.data.task_id)
ElMessage.success(`任务已创建:${res.data.task_no}`) ElMessage.success(`任务已创建:${res.data.task_no}`)
} catch (error) { } catch (error) {
console.error('创建设备批量任务失败:', error) console.error('创建设备批量任务失败:', error)
@@ -465,6 +525,10 @@
onMounted(() => { onMounted(() => {
void loadTaskList() void loadTaskList()
}) })
onBeforeUnmount(() => {
stopTaskStatusPolling()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -36,8 +36,6 @@
:pageSize="pagination.page_size" :pageSize="pagination.page_size"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:actions="getActions"
:actionsWidth="180"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >
@@ -47,29 +45,32 @@
</ArtTable> </ArtTable>
</ElCard> </ElCard>
<ElDialog <ElDrawer
v-model="createDialogVisible" v-model="createDialogVisible"
title="新建订单套餐作废任务" title="新建订单套餐作废任务"
width="560px" direction="rtl"
size="520px"
:close-on-click-modal="!createDrawerBusy"
:close-on-press-escape="!createDrawerBusy"
@closed="handleCreateDialogClosed" @closed="handleCreateDialogClosed"
> >
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px"> <ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
<ElFormItem label="CSV文件" prop="file"> <ElFormItem label="CSV文件" prop="file">
<ElUpload <VoucherUpload
ref="uploadRef" ref="csvUploadRef"
drag v-model="csvFileKeys"
:auto-upload="false" voucher-name="CSV文件"
:limit="1" :max-count="1"
accept=".csv,text/csv" purpose="attachment"
:on-change="handleFileChange" :max-size-mb="10"
:on-remove="handleFileRemove" single-column-csv
> :max-csv-rows="1000"
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon> accept=".csv"
<div class="el-upload__text"> CSV 文件拖到此处<em>点击选择</em></div> tip="仅支持 UTF-8 单列 CSV每行一个订单号最多 1000 行,最大 10MB"
<template #tip> @uploading-change="csvUploading = $event"
<div class="el-upload__tip">仅支持 .csv 文件必填列order_no</div> @files-change="handleCsvFilesChange"
</template> @change="createFormRef?.validateField('file')"
</ElUpload> />
</ElFormItem> </ElFormItem>
<ElFormItem label="凭证附件"> <ElFormItem label="凭证附件">
<VoucherUpload <VoucherUpload
@@ -91,17 +92,19 @@
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
<template #footer> <template #footer>
<ElButton @click="createDialogVisible = false">取消</ElButton> <div class="drawer-footer">
<ElButton <ElButton @click="createDialogVisible = false">取消</ElButton>
type="primary" <ElButton
:loading="creating || voucherUploading" type="primary"
:disabled="voucherUploading" :loading="createDrawerBusy"
@click="handleCreateTask" :disabled="createDrawerBusy"
> @click="handleCreateTask"
{{ voucherUploading ? '凭证上传中...' : '确认创建' }} >
</ElButton> {{ voucherUploading ? '凭证上传中...' : '确认创建' }}
</ElButton>
</div>
</template> </template>
</ElDialog> </ElDrawer>
<PaymentVoucherDialog :file-keys="voucherPreviewKeys" @close="voucherPreviewKeys = []" /> <PaymentVoucherDialog :file-keys="voucherPreviewKeys" @close="voucherPreviewKeys = []" />
</div> </div>
@@ -109,22 +112,21 @@
</template> </template>
<script setup lang="ts"> <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 { useRouter } from 'vue-router'
import { import {
ElButton, ElButton,
ElCard, ElCard,
ElDrawer,
ElForm, ElForm,
ElFormItem, ElFormItem,
ElIcon,
ElInput, ElInput,
ElMessage, ElMessage,
ElTag, ElTag
ElUpload
} from 'element-plus' } from 'element-plus'
import { Upload, UploadFilled } from '@element-plus/icons-vue' import { Upload } from '@element-plus/icons-vue'
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { OrderPackageInvalidateTaskService, StorageService } from '@/api/modules' import { OrderPackageInvalidateTaskService } from '@/api/modules'
import type { import type {
CreateOrderPackageInvalidateTaskRequest, CreateOrderPackageInvalidateTaskRequest,
OrderPackageInvalidateTask, OrderPackageInvalidateTask,
@@ -147,15 +149,20 @@
const loading = ref(false) const loading = ref(false)
const creating = ref(false) const creating = ref(false)
const csvUploading = ref(false)
const voucherUploading = ref(false) const voucherUploading = ref(false)
const tableRef = ref() const tableRef = ref()
const uploadRef = ref<UploadInstance>() const csvUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>() const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const createFormRef = ref<FormInstance>() const createFormRef = ref<FormInstance>()
const createDialogVisible = ref(false) const createDialogVisible = ref(false)
const selectedFile = ref<File | null>(null) const csvFileKeys = ref<string[]>([])
const csvFileName = ref('')
const taskList = ref<OrderPackageInvalidateTask[]>([]) const taskList = ref<OrderPackageInvalidateTask[]>([])
const voucherPreviewKeys = ref<string[]>([]) const voucherPreviewKeys = ref<string[]>([])
const createDrawerBusy = computed(
() => creating.value || csvUploading.value || voucherUploading.value
)
const searchForm = reactive<OrderPackageInvalidateTaskQueryParams>({ const searchForm = reactive<OrderPackageInvalidateTaskQueryParams>({
page: 1, page: 1,
@@ -384,58 +391,31 @@
getTableData() getTableData()
} }
const handleFileChange = (uploadFile: UploadFile) => { const handleCsvFilesChange = (files: Array<{ file_name?: string }>) => {
const file = uploadFile.raw csvFileName.value = files[0]?.file_name || ''
if (!file) return createForm.file = csvFileName.value
if (!/\.csv$/i.test(file.name)) { void createFormRef.value?.validateField('file')
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 handleCreateTask = async () => { const handleCreateTask = async () => {
if (!createFormRef.value) return if (!createFormRef.value) return
if (csvUploading.value) {
ElMessage.warning('CSV 文件上传中,请稍候')
return
}
if (voucherUploading.value) { if (voucherUploading.value) {
ElMessage.warning('凭证上传中,请稍候') ElMessage.warning('凭证上传中,请稍候')
return return
} }
const valid = await createFormRef.value.validate().catch(() => false) const valid = await createFormRef.value.validate().catch(() => false)
if (!valid || !selectedFile.value) return if (!valid || !csvFileKeys.value[0] || !csvFileName.value) return
creating.value = true creating.value = true
try { try {
const fileKey = await uploadCsvFile(selectedFile.value)
const data: CreateOrderPackageInvalidateTaskRequest = { const data: CreateOrderPackageInvalidateTaskRequest = {
file_key: fileKey, file_key: csvFileKeys.value[0],
file_name: selectedFile.value.name, file_name: csvFileName.value,
voucher_keys: toVoucherKeyList(createForm.voucher_keys), voucher_keys: toVoucherKeyList(createForm.voucher_keys),
remark: createForm.remark || undefined remark: createForm.remark || undefined
} }
@@ -453,24 +433,26 @@
const handleCreateDialogClosed = () => { const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields() createFormRef.value?.resetFields()
uploadRef.value?.clearFiles() csvUploadRef.value?.clearFiles(false)
voucherUploadRef.value?.clearFiles(false) voucherUploadRef.value?.clearFiles(false)
selectedFile.value = null csvFileKeys.value = []
csvFileName.value = ''
createForm.file = '' createForm.file = ''
createForm.voucher_keys = [] createForm.voucher_keys = []
createForm.remark = '' createForm.remark = ''
csvUploading.value = false
voucherUploading.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(() => { onMounted(() => {
getTableData() getTableData()
}) })
</script> </script>
<style scoped>
.drawer-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@@ -166,9 +166,9 @@
value || getOperationLabel(deviceTask.value?.operation_type) value || getOperationLabel(deviceTask.value?.operation_type)
}, },
{ {
label: '目标 ID', label: '目标名称',
prop: 'target_id', prop: 'target_name',
formatter: (value: number | null) => String(value ?? '-') formatter: (value: string | null | undefined) => value || '-'
} }
] ]
: [] : []

View File

@@ -998,7 +998,7 @@
}) })
} }
if (hasAuth('shop:credit-limit:manage')) { if (hasAuth('shop:credit_limit_manage')) {
actions.push({ actions.push({
label: '调整额度', label: '调整额度',
handler: () => showCreditDialog(row), handler: () => showCreditDialog(row),