This commit is contained in:
@@ -9,6 +9,14 @@
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="card-header-right">
|
||||
<ElButton
|
||||
v-if="cardInfo?.asset_type === 'card' && hasAuth(JULY_PERMISSIONS.speedTier.view)"
|
||||
type="primary"
|
||||
link
|
||||
@click="emit('showSpeedLimit')"
|
||||
>
|
||||
设置限速
|
||||
</ElButton>
|
||||
<ElTooltip content="开启后系统将自动轮询更新资产状态" placement="top">
|
||||
<div v-if="canShowPolling" class="polling-switch-wrapper">
|
||||
<span class="polling-label">自动轮询</span>
|
||||
@@ -194,33 +202,6 @@
|
||||
</template>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">同步状态</ElDivider>
|
||||
<ElDescriptions :column="descriptionsColumn" border>
|
||||
<ElDescriptionsItem label="轮询状态">
|
||||
<ElTag
|
||||
v-if="cardInfo?.polling"
|
||||
:type="cardInfo.polling.enabled ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ cardInfo.polling.enabled ? '已启用' : '未启用' }}
|
||||
</ElTag>
|
||||
<span v-else>-</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后活跃时间">
|
||||
{{ formatDateTime(cardInfo?.polling?.last_activity_at) || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="同步轨迹">
|
||||
<ElButton
|
||||
v-permission="'asset_info:view_sync_trail'"
|
||||
type="primary"
|
||||
link
|
||||
@click="emit('viewSyncTrail')"
|
||||
>
|
||||
查看同步轨迹
|
||||
</ElButton>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template v-if="previousExchangeAsset || nextExchangeAsset">
|
||||
<ElDivider content-position="left">换货链路</ElDivider>
|
||||
<ElDescriptions :column="descriptionsColumn" border>
|
||||
@@ -596,6 +577,7 @@
|
||||
} from '@/types/api'
|
||||
import { useAssetFormatters } from '../composables/useAssetFormatters'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
@@ -798,7 +780,6 @@
|
||||
(e: 'navigateToCard', iccid: string): void
|
||||
(e: 'navigateToDevice', deviceNo: string): void
|
||||
(e: 'navigateToAsset', identifier: string): void
|
||||
(e: 'viewSyncTrail'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -336,7 +337,11 @@
|
||||
data.payment_voucher_key = toVoucherKeyList(form.payment_voucher_key)
|
||||
}
|
||||
|
||||
await OrderService.createOrder(data)
|
||||
const response = await OrderService.createOrder(data)
|
||||
if (response.code !== 0) {
|
||||
ElMessage.error(response.msg || '订单创建失败')
|
||||
return
|
||||
}
|
||||
|
||||
if (form.payment_method === 'wallet') {
|
||||
ElMessage.success('订单创建成功,已自动完成支付')
|
||||
@@ -352,6 +357,7 @@
|
||||
return
|
||||
}
|
||||
console.error('创建订单失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="设置限速" width="500px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="下行速率" prop="download_speed">
|
||||
<ElInputNumber
|
||||
v-model="form.download_speed"
|
||||
:min="1"
|
||||
:step="128"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="上行速率" prop="upload_speed">
|
||||
<ElInputNumber
|
||||
v-model="form.upload_speed"
|
||||
:min="1"
|
||||
:step="128"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
|
||||
<ElFormItem label="固定档位" prop="code">
|
||||
<ElSelect v-model="form.code" style="width: 100%">
|
||||
<ElOption v-for="tier in speedTiers" :key="tier.code" :label="tier.label" :value="tier.code" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -31,7 +16,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElDialog, ElForm, ElFormItem, ElInputNumber, ElButton } from 'element-plus'
|
||||
import { ElDialog, ElForm, ElFormItem, ElSelect, ElOption, ElButton } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
@@ -40,7 +25,7 @@
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm', data: { download_speed: number; upload_speed: number }): void
|
||||
(e: 'confirm', data: { code: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 }): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -49,14 +34,23 @@
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
download_speed: 1024,
|
||||
upload_speed: 512
|
||||
})
|
||||
const speedTiers = [
|
||||
{ code: -1, label: '不限速' },
|
||||
{ code: 0, label: '0kbps' },
|
||||
{ code: 1, label: '128Kbps' },
|
||||
{ code: 2, label: '512Kbps' },
|
||||
{ code: 3, label: '1Mbps' },
|
||||
{ code: 4, label: '2Mbps' },
|
||||
{ code: 5, label: '10Mbps' },
|
||||
{ code: 6, label: '20Mbps' },
|
||||
{ code: 7, label: '50Mbps' },
|
||||
{ code: 8, label: '100Mbps' }
|
||||
] as const
|
||||
|
||||
const form = reactive({ code: 3 as (typeof speedTiers)[number]['code'] })
|
||||
|
||||
const rules: FormRules = {
|
||||
download_speed: [{ required: true, message: '请输入下行速率', trigger: 'blur' }],
|
||||
upload_speed: [{ required: true, message: '请输入上行速率', trigger: 'blur' }]
|
||||
code: [{ required: true, message: '请选择限速档位', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const visible = computed({
|
||||
@@ -67,8 +61,7 @@
|
||||
// 监听对话框打开,重置表单
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal) {
|
||||
form.download_speed = 1024
|
||||
form.upload_speed = 512
|
||||
form.code = 3
|
||||
}
|
||||
})
|
||||
|
||||
@@ -82,8 +75,7 @@
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
emit('confirm', {
|
||||
download_speed: form.download_speed,
|
||||
upload_speed: form.upload_speed
|
||||
code: form.code
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
|
||||
@@ -16,7 +16,6 @@ export function useAssetOperations(cardInfo: Ref<any>, refreshAssetFn?: () => Pr
|
||||
const manualDeactivateCardLoading = ref(false)
|
||||
const rebootDeviceLoading = ref(false)
|
||||
const resetDeviceLoading = ref(false)
|
||||
const speedLimitLoading = ref(false)
|
||||
const switchCardLoading = ref(false)
|
||||
const setWiFiLoading = ref(false)
|
||||
const pollingLoading = ref(false)
|
||||
@@ -181,35 +180,6 @@ export function useAssetOperations(cardInfo: Ref<any>, refreshAssetFn?: () => Pr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set device speed limit
|
||||
*/
|
||||
const setSpeedLimit = async (form: { download_speed: number; upload_speed: number }) => {
|
||||
try {
|
||||
speedLimitLoading.value = true
|
||||
const res = await DeviceService.setSpeedLimit(cardInfo.value.imei, {
|
||||
download_speed: form.download_speed,
|
||||
upload_speed: form.upload_speed
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('限速设置成功')
|
||||
if (refreshAssetFn) {
|
||||
await refreshAssetFn()
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
ElMessage.error(res.msg || '设置失败')
|
||||
return false
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('设置限速失败:', error)
|
||||
console.log(error?.message || '设置失败')
|
||||
return false
|
||||
} finally {
|
||||
speedLimitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to different card
|
||||
*/
|
||||
@@ -320,7 +290,6 @@ export function useAssetOperations(cardInfo: Ref<any>, refreshAssetFn?: () => Pr
|
||||
manualDeactivateCardLoading,
|
||||
rebootDeviceLoading,
|
||||
resetDeviceLoading,
|
||||
speedLimitLoading,
|
||||
switchCardLoading,
|
||||
setWiFiLoading,
|
||||
pollingLoading,
|
||||
@@ -333,7 +302,6 @@ export function useAssetOperations(cardInfo: Ref<any>, refreshAssetFn?: () => Pr
|
||||
// Device operations
|
||||
rebootDevice,
|
||||
resetDevice,
|
||||
setSpeedLimit,
|
||||
switchCard,
|
||||
setWiFi,
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
@show-switch-card="showSwitchCardDialog"
|
||||
@show-switch-mode="showSwitchModeDialog"
|
||||
@show-set-wifi="showSetWiFiDialog"
|
||||
@show-realname-policy="showRealnamePolicyDialog"
|
||||
@show-realname-policy="showRealnamePolicyDialog"
|
||||
@show-speed-limit="speedLimitDialogVisible = true"
|
||||
@show-update-realname-status="showUpdateRealnameStatusDialog"
|
||||
@enable-binding-card="handleEnableBindingCard"
|
||||
@disable-binding-card="handleDisableBindingCard"
|
||||
@@ -44,7 +45,6 @@
|
||||
@navigate-to-asset="handleNavigateToAsset"
|
||||
@navigate-to-card="handleNavigateToCard"
|
||||
@navigate-to-device="handleNavigateToDevice"
|
||||
@view-sync-trail="handleViewSyncTrail"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -174,6 +174,7 @@
|
||||
:current-realname-status="bindingCardRealnameStatusValue"
|
||||
@success="handleBindingCardRealnameStatusSuccess"
|
||||
/>
|
||||
<SpeedLimitDialog v-model="speedLimitDialogVisible" @confirm="handleSpeedTierConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -182,7 +183,7 @@
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElCard, ElEmpty } from 'element-plus'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { DeviceService } from '@/api/modules'
|
||||
import { CardService, DeviceService } from '@/api/modules'
|
||||
import {
|
||||
formatRemainingTime,
|
||||
FrontendRateLimitError,
|
||||
@@ -203,8 +204,9 @@
|
||||
SwitchModeDialog,
|
||||
WiFiConfigDialog,
|
||||
PackageRechargeDialog,
|
||||
DailyRecordsDialog,
|
||||
OrderHistoryDialog
|
||||
DailyRecordsDialog,
|
||||
OrderHistoryDialog,
|
||||
SpeedLimitDialog
|
||||
} from './components/dialogs'
|
||||
import SwitchCardDialog from '@/components/device/SwitchCardDialog.vue'
|
||||
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
|
||||
@@ -291,6 +293,7 @@
|
||||
|
||||
// 绑定卡实名状态更新
|
||||
const bindingCardRealnameStatusDialogVisible = ref(false)
|
||||
const speedLimitDialogVisible = ref(false)
|
||||
const bindingCardRealnameStatusIccid = ref('')
|
||||
const bindingCardRealnameStatusValue = ref<number>(0)
|
||||
|
||||
@@ -444,18 +447,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewSyncTrail = () => {
|
||||
if (!cardInfo.value) return
|
||||
|
||||
router.push({
|
||||
path: '/audit/integrations',
|
||||
query: {
|
||||
resource_type: cardInfo.value.asset_type,
|
||||
resource_key: cardInfo.value.identifier
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT卡操作 - 启用卡
|
||||
*/
|
||||
@@ -814,6 +805,15 @@
|
||||
handleSearch({ identifier })
|
||||
}
|
||||
}
|
||||
const handleSpeedTierConfirm = async (data: { code: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 }) => {
|
||||
if (!cardInfo.value?.iccid) return
|
||||
const response = await CardService.setSpeedTier(cardInfo.value.iccid, data.code)
|
||||
if (response.code === 0) {
|
||||
ElMessage.success(`限速设置成功:${response.data.speed_tier_name}`)
|
||||
speedLimitDialogVisible.value = false
|
||||
await handleRefresh()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2299,7 +2299,8 @@
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success('批量修改实名顺序成功')
|
||||
const successCount = res.data?.success_count ?? assetIds.length
|
||||
ElMessage.success(`批量修改实名顺序成功:${successCount}/${assetIds.length}`)
|
||||
batchRealnamePolicyDialogVisible.value = false
|
||||
selectedDevices.value = []
|
||||
await getTableData()
|
||||
|
||||
@@ -83,6 +83,11 @@
|
||||
formatter: (_, data) =>
|
||||
data.flow_type_name || (flowType === 'direct' ? '直接换货' : '物流换货')
|
||||
},
|
||||
{
|
||||
label: '提交人',
|
||||
formatter: (value) => value || '--',
|
||||
prop: 'submitter_name'
|
||||
},
|
||||
{
|
||||
label: '换货原因',
|
||||
prop: 'exchange_reason',
|
||||
|
||||
@@ -380,7 +380,13 @@
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ExchangeService, CardService, DeviceService } from '@/api/modules'
|
||||
import type { ExchangeResponse } from '@/api/modules/exchange'
|
||||
import type {
|
||||
CreateExchangeRequest,
|
||||
ExchangeAssetType,
|
||||
ExchangeFlowType,
|
||||
ExchangeQueryParams,
|
||||
ExchangeResponse
|
||||
} from '@/api/modules/exchange'
|
||||
import type { StandaloneIotCard, Device } from '@/types/api'
|
||||
import { ElMessage, ElTag, ElButton, ElMessageBox, ElSwitch } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
@@ -422,7 +428,7 @@
|
||||
]
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
const searchForm = reactive<ExchangeQueryParams & { created_at_range: string[] }>({
|
||||
status: undefined,
|
||||
flow_type: undefined, // 流程类型筛选
|
||||
old_asset_keyword: '',
|
||||
@@ -433,7 +439,16 @@
|
||||
})
|
||||
|
||||
// 创建换货单表单
|
||||
const createForm = reactive({
|
||||
const createForm = reactive<{
|
||||
exchange_reason: string
|
||||
exchange_reason_other: string
|
||||
old_asset_type: ExchangeAssetType | ''
|
||||
old_identifier: string
|
||||
flow_type: ExchangeFlowType
|
||||
new_identifier: string
|
||||
migrate_data: boolean
|
||||
remark: string
|
||||
}>({
|
||||
exchange_reason: '',
|
||||
exchange_reason_other: '',
|
||||
old_asset_type: '',
|
||||
@@ -819,7 +834,7 @@
|
||||
loading.value = true
|
||||
try {
|
||||
// 过滤掉空值参数
|
||||
const params: any = {
|
||||
const params: ExchangeQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size
|
||||
}
|
||||
@@ -1328,9 +1343,9 @@
|
||||
? createForm.exchange_reason_other.trim()
|
||||
: createForm.exchange_reason
|
||||
|
||||
const requestData: any = {
|
||||
const requestData: CreateExchangeRequest = {
|
||||
exchange_reason: exchangeReason,
|
||||
old_asset_type: createForm.old_asset_type,
|
||||
old_asset_type: createForm.old_asset_type as ExchangeAssetType,
|
||||
old_identifier: createForm.old_identifier,
|
||||
flow_type: createForm.flow_type,
|
||||
remark: createForm.remark || undefined
|
||||
@@ -1351,7 +1366,7 @@
|
||||
}
|
||||
|
||||
ElMessage.error(res.msg || '换货单创建失败')
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
console.error('创建换货单失败:', error)
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
|
||||
364
src/views/asset-management/expiring-assets/index.vue
Normal file
364
src/views/asset-management/expiring-assets/index.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="expiring-assets-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="true"
|
||||
label-width="85"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<div class="page-intro"> 仅展示可预计且尚未过期的资产,0-3 天资产优先显示。 </div>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="asset_id"
|
||||
:loading="loading"
|
||||
:data="items"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="140"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, reactive, ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElTag } from 'element-plus'
|
||||
import { AssetService, PackageManageService, ShopService } from '@/api/modules'
|
||||
import type {
|
||||
AssetType,
|
||||
ExpiringAssetItem,
|
||||
ExpiringAssetQueryParams,
|
||||
PackageResponse,
|
||||
ShopResponse
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'ExpiringAssets' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tableRef = ref()
|
||||
const loading = ref(false)
|
||||
const shopLoading = ref(false)
|
||||
const packageLoading = ref(false)
|
||||
const shopOptions = ref<ShopResponse[]>([])
|
||||
const packageOptions = ref<PackageResponse[]>([])
|
||||
const items = ref<ExpiringAssetItem[]>([])
|
||||
const searchForm = reactive<ExpiringAssetQueryParams>({
|
||||
asset_type: undefined,
|
||||
keyword: '',
|
||||
shop_id: undefined,
|
||||
package_id: undefined,
|
||||
days_min: undefined,
|
||||
days_max: undefined,
|
||||
expires_from: undefined,
|
||||
expires_to: undefined
|
||||
})
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '资产类型',
|
||||
prop: 'asset_type',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: [
|
||||
{ label: '网卡', value: 'card' },
|
||||
{ label: '设备', value: 'device' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '关键词',
|
||||
prop: 'keyword',
|
||||
type: 'input',
|
||||
config: { clearable: true, placeholder: '请输入资产标识' }
|
||||
},
|
||||
{
|
||||
label: '店铺',
|
||||
prop: 'shop_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
loading: shopLoading.value,
|
||||
remoteMethod: (query: string) => searchShops(query),
|
||||
placeholder: '请选择或搜索店铺'
|
||||
},
|
||||
options: () => shopOptions.value.map((shop) => ({ label: shop.shop_name, value: shop.id }))
|
||||
},
|
||||
{
|
||||
label: '套餐',
|
||||
prop: 'package_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
loading: packageLoading.value,
|
||||
remoteMethod: (query: string) => searchPackages(query),
|
||||
placeholder: '请选择或搜索套餐'
|
||||
},
|
||||
options: () =>
|
||||
packageOptions.value.map((pkg) => ({
|
||||
label: `${pkg.package_name} (${pkg.package_code})`,
|
||||
value: pkg.id
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '最小剩余天数',
|
||||
prop: 'days_min',
|
||||
type: 'input',
|
||||
config: { clearable: true, inputmode: 'numeric', placeholder: '最小天数' }
|
||||
},
|
||||
{
|
||||
label: '最大剩余天数',
|
||||
prop: 'days_max',
|
||||
type: 'input',
|
||||
config: { clearable: true, inputmode: 'numeric', placeholder: '最大天数' }
|
||||
},
|
||||
{
|
||||
label: '预计到期起始',
|
||||
prop: 'expires_from',
|
||||
type: 'date',
|
||||
config: { valueFormat: 'YYYY-MM-DD', clearable: true }
|
||||
},
|
||||
{
|
||||
label: '预计到期结束',
|
||||
prop: 'expires_to',
|
||||
type: 'date',
|
||||
config: { valueFormat: 'YYYY-MM-DD', clearable: true }
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '资产类型', prop: 'asset_type' },
|
||||
{ label: '资产标识', prop: 'identifier' },
|
||||
{ label: '店铺', prop: 'shop_name' },
|
||||
{ label: '当前套餐', prop: 'package_name' },
|
||||
{ label: '预计最终到期', prop: 'estimated_final_expires_at' },
|
||||
{ label: '剩余天数', prop: 'days_until_final_expiry' },
|
||||
{ label: '临期等级', prop: 'expiry_level_name' }
|
||||
]
|
||||
|
||||
const getExpiryClass = (level?: string | null) => {
|
||||
if (level === 'critical' || level === '0_3') return 'expiry-critical'
|
||||
if (level === 'warning' || level === '4_7') return 'expiry-warning'
|
||||
if (level === 'notice' || level === '8_15') return 'expiry-notice'
|
||||
return ''
|
||||
}
|
||||
|
||||
const getExpiryTagType = (level?: string | null) => {
|
||||
if (level === 'critical' || level === '0_3') return 'danger'
|
||||
if (level === 'warning' || level === '4_7') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const searchShops = async (query = '') => {
|
||||
shopLoading.value = true
|
||||
try {
|
||||
const response = await ShopService.getShops({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
shop_name: query || undefined
|
||||
})
|
||||
if (response.code === 0) shopOptions.value = response.data.items || []
|
||||
} finally {
|
||||
shopLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchPackages = async (query = '') => {
|
||||
packageLoading.value = true
|
||||
try {
|
||||
const response = await PackageManageService.getPackages({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
package_name: query || undefined
|
||||
})
|
||||
if (response.code === 0) packageOptions.value = response.data.items || []
|
||||
} finally {
|
||||
packageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'asset_type',
|
||||
label: '资产类型',
|
||||
width: 100,
|
||||
formatter: (row: ExpiringAssetItem) => (row.asset_type === 'card' ? '网卡' : '设备')
|
||||
},
|
||||
{
|
||||
prop: 'identifier',
|
||||
label: '资产标识',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ExpiringAssetItem) => row.identifier || '-'
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺',
|
||||
minWidth: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ExpiringAssetItem) => row.shop_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'package_name',
|
||||
label: '当前套餐',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ExpiringAssetItem) => row.package_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'estimated_final_expires_at',
|
||||
label: '预计最终到期',
|
||||
width: 180,
|
||||
formatter: (row: ExpiringAssetItem) =>
|
||||
h(
|
||||
'span',
|
||||
{ class: getExpiryClass(row.expiry_level) },
|
||||
formatDateTime(row.estimated_final_expires_at)
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'days_until_final_expiry',
|
||||
label: '剩余天数',
|
||||
width: 100,
|
||||
formatter: (row: ExpiringAssetItem) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: getExpiryTagType(row.expiry_level), size: 'small' },
|
||||
() => `${row.days_until_final_expiry}天`
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'expiry_level_name',
|
||||
label: '临期等级',
|
||||
width: 120,
|
||||
formatter: (row: ExpiringAssetItem) => row.expiry_level_name || '-'
|
||||
}
|
||||
])
|
||||
|
||||
const loadAssets = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await AssetService.getExpiringAssets({
|
||||
...searchForm,
|
||||
page: pagination.currentPage,
|
||||
size: pagination.pageSize
|
||||
})
|
||||
if (response.code === 0 && response.data) {
|
||||
items.value = response.data.items || []
|
||||
pagination.total = response.data.total || 0
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.currentPage = 1
|
||||
void loadAssets()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
asset_type: undefined,
|
||||
keyword: '',
|
||||
shop_id: undefined,
|
||||
package_id: undefined,
|
||||
days_min: undefined,
|
||||
days_max: undefined,
|
||||
expires_from: undefined,
|
||||
expires_to: undefined
|
||||
})
|
||||
pagination.currentPage = 1
|
||||
void loadAssets()
|
||||
}
|
||||
|
||||
const handleRefresh = () => void loadAssets()
|
||||
const handleSizeChange = (value: number) => {
|
||||
pagination.pageSize = value
|
||||
void loadAssets()
|
||||
}
|
||||
const handleCurrentChange = (value: number) => {
|
||||
pagination.currentPage = value
|
||||
void loadAssets()
|
||||
}
|
||||
|
||||
const goToAsset = (row: ExpiringAssetItem) => {
|
||||
router.push({
|
||||
path: RoutesAlias.AssetInformation,
|
||||
query: row.asset_type === 'card' ? { iccid: row.identifier } : { virtual_no: row.identifier }
|
||||
})
|
||||
}
|
||||
|
||||
const getActions = (row: ExpiringAssetItem) => [
|
||||
{ label: '查看资产', type: 'primary' as const, handler: () => goToAsset(row) },
|
||||
...(row.can_renew
|
||||
? [{ label: '续费', type: 'primary' as const, handler: () => goToAsset(row) }]
|
||||
: [])
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
const assetType = route.query.asset_type
|
||||
if (assetType === 'card' || assetType === 'device')
|
||||
searchForm.asset_type = assetType as AssetType
|
||||
void Promise.all([loadAssets(), searchShops(), searchPackages()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.expiring-assets-page {
|
||||
.page-intro {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.expiry-critical,
|
||||
.expiry-warning,
|
||||
.expiry-notice {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.expiry-critical {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.expiry-warning {
|
||||
color: #8e44ad;
|
||||
}
|
||||
|
||||
.expiry-notice {
|
||||
color: #e83e8c;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2448,7 +2448,8 @@
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success('批量修改实名顺序成功')
|
||||
const successCount = res.data?.success_count ?? assetIds.length
|
||||
ElMessage.success(`批量修改实名顺序成功:${successCount}/${assetIds.length}`)
|
||||
batchRealnamePolicyDialogVisible.value = false
|
||||
selectedCards.value = []
|
||||
await getTableData()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="device-task-page" id="table-full-screen">
|
||||
<div v-if="isPlatformAccount" class="device-task-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
@@ -10,6 +10,16 @@
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<div v-if="pollingError || pollingForbidden" class="task-polling-error">
|
||||
<ElAlert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="pollingForbidden ? '当前账号无权查看该任务' : pollingError || '任务状态获取失败'"
|
||||
/>
|
||||
<ElButton size="small" @click="polling.retry">重试</ElButton>
|
||||
</div>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
@@ -19,10 +29,11 @@
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
v-if="isPlatformAccount && hasAuth(JULY_PERMISSIONS.deviceAllocation.page)"
|
||||
type="primary"
|
||||
:icon="Upload"
|
||||
@click="importDialogVisible = true"
|
||||
v-permission="'device_task:bulk_import'"
|
||||
v-permission="JULY_PERMISSIONS.deviceAllocation.page"
|
||||
>
|
||||
批量导入设备
|
||||
</ElButton>
|
||||
@@ -57,8 +68,8 @@
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载 Excel 模板文件,按照模板格式填写设备信息</p>
|
||||
<p>2. 仅支持 Excel 格式(.xlsx),单次最多导入 1000 条</p>
|
||||
<p>1. 设备分配使用单列 UTF-8 CSV,导入设备仍使用 Excel 模板</p>
|
||||
<p>2. CSV 单次最多 1000 条,文件不超过 10MB</p>
|
||||
<p>3. 列格式请设置为文本格式,避免长数字被转为科学计数法</p>
|
||||
<p>4. <strong>重要:列顺序固定,不可调整。</strong>系统按位置读取,不识别列名</p>
|
||||
<p style="color: var(--el-color-primary)">5. 必填列:虚拟号(第1列)</p>
|
||||
@@ -77,7 +88,23 @@
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="任务类型">
|
||||
<ElSelect v-model="importForm.operation_type" style="width: 100%">
|
||||
<ElOption label="导入设备" value="import" />
|
||||
<ElOption label="分配目标代理" value="assign_shop" />
|
||||
<ElOption label="设置套餐系列" value="assign_series" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol v-if="importForm.operation_type !== 'import'" :span="12">
|
||||
<ElFormItem label="目标 ID">
|
||||
<ElInput v-model="importForm.target_id" placeholder="代理店铺或套餐系列 ID" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="批次号" prop="batch_no">
|
||||
<ElInput
|
||||
@@ -108,18 +135,18 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElUpload
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:limit="1"
|
||||
accept=".xlsx"
|
||||
>
|
||||
accept=".xlsx,.csv"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将 Excel 文件拖到此处,或<em>点击选择</em></div>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 .xlsx 格式的 Excel 文件,且不超过 300MB</div>
|
||||
<div class="el-upload__tip">导入设备支持 .xlsx;批量分配支持单列 .csv</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
|
||||
@@ -140,8 +167,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { DeviceService } from '@/api/modules'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
||||
@@ -149,16 +176,29 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useAsyncTaskPolling } from '@/composables/useAsyncTaskPolling'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import type { DeviceImportTask, DeviceImportTaskStatus, RealnamePolicy } from '@/types/api/device'
|
||||
import type {
|
||||
DeviceImportTask,
|
||||
DeviceImportTaskDetail,
|
||||
DeviceImportTaskQueryParams,
|
||||
DeviceImportTaskStatus,
|
||||
RealnamePolicy
|
||||
} from '@/types/api/device'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { generatePackageCode } from '@/utils/codeGenerator'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
|
||||
defineOptions({ name: 'DeviceTask' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { hasAuth } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
const isPlatformAccount = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
@@ -168,14 +208,16 @@
|
||||
const importDialogVisible = ref(false)
|
||||
const importForm = reactive({
|
||||
batch_no: '',
|
||||
realname_policy: '' as '' | 'none' | 'before_order' | 'after_order'
|
||||
realname_policy: '' as '' | 'none' | 'before_order' | 'after_order',
|
||||
operation_type: 'import' as 'import' | 'assign_shop' | 'assign_series',
|
||||
target_id: ''
|
||||
})
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
status: undefined,
|
||||
batch_no: '',
|
||||
dateRange: undefined as any,
|
||||
dateRange: undefined as string[] | undefined,
|
||||
start_time: '',
|
||||
end_time: ''
|
||||
}
|
||||
@@ -247,6 +289,23 @@
|
||||
|
||||
const taskList = ref<DeviceImportTask[]>([])
|
||||
|
||||
const polling = useAsyncTaskPolling<DeviceImportTaskDetail>({
|
||||
storageKey: 'device-import-active-task',
|
||||
autoRestore: false,
|
||||
fetchTask: async (taskId) => {
|
||||
const res = await DeviceService.getImportTaskDetail(taskId)
|
||||
if (res.code !== 0 || !res.data) throw new Error(res.msg || '获取设备任务失败')
|
||||
return res.data
|
||||
},
|
||||
isForbidden: (error) => {
|
||||
const response = (error as { response?: { status?: number; data?: { code?: number } } })
|
||||
?.response
|
||||
return response?.status === 403 || response?.data?.code === 403
|
||||
}
|
||||
})
|
||||
const pollingError = polling.error
|
||||
const pollingForbidden = polling.forbidden
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: DeviceImportTaskStatus) => {
|
||||
switch (status) {
|
||||
@@ -265,6 +324,7 @@
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (row: DeviceImportTask) => {
|
||||
if (!isPlatformAccount.value) return
|
||||
router.push({
|
||||
path: RoutesAlias.TaskDetail,
|
||||
query: {
|
||||
@@ -276,7 +336,7 @@
|
||||
|
||||
// 处理名称点击
|
||||
const handleNameClick = (row: DeviceImportTask) => {
|
||||
if (hasAuth('device_task:view_detail')) {
|
||||
if (isPlatformAccount.value && hasAuth('device_task:view_detail')) {
|
||||
viewDetail(row)
|
||||
} else {
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
@@ -385,15 +445,31 @@
|
||||
}
|
||||
])
|
||||
|
||||
watch(polling.task, () => {
|
||||
if (isPlatformAccount.value) void getTableData()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
if (!isPlatformAccount.value) return
|
||||
void getTableData()
|
||||
const routeTaskId = Number(route.query.task_id)
|
||||
if (routeTaskId) {
|
||||
void polling.start(routeTaskId)
|
||||
} else if (polling.taskId.value) {
|
||||
void polling.retry()
|
||||
}
|
||||
})
|
||||
|
||||
// 获取设备任务列表
|
||||
const getTableData = async () => {
|
||||
if (!isPlatformAccount.value) {
|
||||
taskList.value = []
|
||||
pagination.total = 0
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
const params: DeviceImportTaskQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
status: searchForm.status,
|
||||
@@ -406,13 +482,6 @@
|
||||
params.end_time = searchForm.dateRange[1]
|
||||
}
|
||||
|
||||
// 清理空值
|
||||
Object.keys(params).forEach((key) => {
|
||||
if (params[key] === '' || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const res = await DeviceService.getImportTasks(params)
|
||||
if (res.code === 0) {
|
||||
const taskItems = (res.data as typeof res.data & { items?: DeviceImportTask[] | null })
|
||||
@@ -422,6 +491,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error(normalizeApiError(error).message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -473,8 +543,9 @@
|
||||
}
|
||||
|
||||
// 文件选择变化
|
||||
const handleFileChange = (uploadFile: any) => {
|
||||
const maxSize = 300 * 1024 * 1024
|
||||
const handleFileChange = async (uploadFile: any) => {
|
||||
const isCsvAllocation = importForm.operation_type !== 'import'
|
||||
const maxSize = (isCsvAllocation ? 10 : 300) * 1024 * 1024
|
||||
if (uploadFile.raw && uploadFile.raw.size > maxSize) {
|
||||
ElMessage.error('文件大小不能超过 300MB')
|
||||
uploadRef.value?.clearFiles()
|
||||
@@ -482,13 +553,23 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.xlsx')) {
|
||||
ElMessage.error('只能上传 .xlsx 格式的 Excel 文件')
|
||||
if (uploadFile.raw && (isCsvAllocation ? !uploadFile.raw.name.endsWith('.csv') : !uploadFile.raw.name.endsWith('.xlsx'))) {
|
||||
ElMessage.error(isCsvAllocation ? '批量分配只能上传 .csv 文件' : '设备导入只能上传 .xlsx 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (isCsvAllocation && uploadFile.raw) {
|
||||
const rows = (await uploadFile.raw.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter(Boolean)
|
||||
if (rows.length > 1001 || rows.some((row: string) => row.includes(','))) {
|
||||
ElMessage.error('设备分配 CSV 必须是单列且最多 1000 行数据')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fileList.value = uploadFile.raw ? [uploadFile.raw] : []
|
||||
}
|
||||
|
||||
@@ -510,24 +591,33 @@
|
||||
clearFiles()
|
||||
importForm.batch_no = ''
|
||||
importForm.realname_policy = ''
|
||||
importForm.operation_type = 'import'
|
||||
importForm.target_id = ''
|
||||
importDialogVisible.value = false
|
||||
}
|
||||
// 提交上传
|
||||
const submitUpload = async () => {
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择 Excel 文件')
|
||||
return
|
||||
}
|
||||
if (!isPlatformAccount.value || !hasAuth(JULY_PERMISSIONS.deviceAllocation.page)) return
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
if (importForm.operation_type !== 'import' && !Number(importForm.target_id)) {
|
||||
ElMessage.warning('请输入有效的目标 ID')
|
||||
return
|
||||
}
|
||||
|
||||
const file = fileList.value[0]
|
||||
uploading.value = true
|
||||
|
||||
try {
|
||||
ElMessage.info('正在准备上传...')
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
purpose: 'iot_import'
|
||||
const isAllocation = importForm.operation_type !== 'import'
|
||||
const contentType = isAllocation ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: contentType,
|
||||
purpose: isAllocation ? 'device_batch_allocation' : 'iot_import'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
@@ -538,18 +628,20 @@
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
ElMessage.info('正在上传文件...')
|
||||
await StorageService.uploadFile(
|
||||
upload_url,
|
||||
file,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
await StorageService.uploadFile(upload_url, file, contentType)
|
||||
|
||||
ElMessage.info('正在创建导入任务...')
|
||||
const importRes = await DeviceService.importDevices({
|
||||
file_key,
|
||||
batch_no: importForm.batch_no || undefined,
|
||||
realname_policy: (importForm.realname_policy || undefined) as RealnamePolicy | undefined
|
||||
})
|
||||
ElMessage.info(isAllocation ? '正在创建分配任务...' : '正在创建导入任务...')
|
||||
const importRes = isAllocation
|
||||
? await DeviceService.createAllocationTask({
|
||||
file_key,
|
||||
operation_type: importForm.operation_type as 'assign_shop' | 'assign_series',
|
||||
target_id: Number(importForm.target_id)
|
||||
})
|
||||
: await DeviceService.importDevices({
|
||||
file_key,
|
||||
batch_no: importForm.batch_no || undefined,
|
||||
realname_policy: (importForm.realname_policy || undefined) as RealnamePolicy | undefined
|
||||
})
|
||||
|
||||
if (importRes.code !== 0) {
|
||||
ElMessage.error(importRes.msg || '创建导入任务失败')
|
||||
@@ -557,8 +649,11 @@
|
||||
}
|
||||
|
||||
const taskNo = importRes.data.task_no
|
||||
const taskId = importRes.data.task_id
|
||||
|
||||
handleCancelImport()
|
||||
await router.replace({ path: route.path, query: { task_id: String(taskId) } })
|
||||
await polling.start(taskId)
|
||||
await getTableData()
|
||||
|
||||
ElMessage.success({
|
||||
@@ -568,7 +663,7 @@
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('设备导入失败:', error)
|
||||
console.log(error.message || '设备导入失败')
|
||||
ElMessage.error(normalizeApiError(error).message || error.message || '设备导入失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
@@ -576,6 +671,7 @@
|
||||
|
||||
// 从行数据下载失败数据
|
||||
const downloadFailDataByRow = async (row: DeviceImportTask) => {
|
||||
if (!isPlatformAccount.value) return
|
||||
try {
|
||||
const res = await DeviceService.getImportTaskDetail(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
@@ -622,6 +718,7 @@
|
||||
}
|
||||
|
||||
const downloadTaskFile = async (row: DeviceImportTask) => {
|
||||
if (!isPlatformAccount.value) return
|
||||
const fileKey = row.file_name?.trim()
|
||||
if (!fileKey) {
|
||||
ElMessage.warning('当前任务没有可下载的原始文件')
|
||||
@@ -639,6 +736,7 @@
|
||||
|
||||
// 获取操作按钮
|
||||
const getActions = (row: DeviceImportTask) => {
|
||||
if (!isPlatformAccount.value) return []
|
||||
const actions: any[] = []
|
||||
const showDownloadFileAction = false
|
||||
|
||||
@@ -664,6 +762,13 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-task-page {
|
||||
.task-polling-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-icon--upload) {
|
||||
margin-bottom: 16px;
|
||||
font-size: 67px;
|
||||
|
||||
@@ -59,11 +59,14 @@
|
||||
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'TaskDetail' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const isPlatformAccount = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
|
||||
|
||||
type TaskType = 'card' | 'device'
|
||||
type TaskDetail = IotCardImportTaskDetail | DeviceImportTaskDetail
|
||||
@@ -244,6 +247,12 @@
|
||||
taskType.value = queryTaskType
|
||||
}
|
||||
|
||||
if (taskType.value === 'device' && !isPlatformAccount.value) {
|
||||
ElMessage.error('当前账号无权查看设备任务详情')
|
||||
goBack()
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
if (taskType.value === 'device') {
|
||||
|
||||
Reference in New Issue
Block a user