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: {
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'

View File

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

View File

@@ -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 // 创建时间

View File

@@ -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}`)
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 || '-'
}
]
: []

View File

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