fix: 套餐
This commit is contained in:
@@ -299,9 +299,6 @@
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<!-- 网络信息 -->
|
||||
<ElDescriptionsItem label="IP 地址">
|
||||
{{ deviceRealtime.ip_address || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="WAN IP">
|
||||
{{ deviceRealtime.wan_ip || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="套餐充值" width="600px" @closed="handleDialogClosed">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElDialog v-model="visible" title="套餐充值" width="30%" @closed="handleDialogClosed">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<ElFormItem label="资产信息">
|
||||
<span style="font-weight: bold; color: #409eff">
|
||||
{{ assetIdentifier }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="选择套餐" prop="package_id">
|
||||
<ElFormItem label="套餐" prop="package_id">
|
||||
<ElSelect
|
||||
v-model="form.package_id"
|
||||
placeholder="请选择套餐"
|
||||
@@ -21,9 +21,9 @@
|
||||
>
|
||||
<template v-if="!props.seriesId">
|
||||
<ElOption :value="null" disabled>
|
||||
<span style="color: var(--el-text-color-warning)"
|
||||
>该设备未关联套餐系列,无法购买套餐</span
|
||||
>
|
||||
<span style="color: var(--el-text-color-warning)">
|
||||
该设备未关联套餐系列,无法购买套餐
|
||||
</span>
|
||||
</ElOption>
|
||||
</template>
|
||||
<ElOption
|
||||
@@ -35,15 +35,38 @@
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ pkg.package_name }}</span>
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
¥{{ ((pkg.cost_price || 0) / 100).toFixed(2) }}
|
||||
<template v-if="pkg.is_gift">
|
||||
<el-tag type="warning" size="small">赠送</el-tag>
|
||||
</template>
|
||||
<template v-else> ¥{{ ((pkg.cost_price || 0) / 100).toFixed(2) }} </template>
|
||||
</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<template v-if="noSeriesId">
|
||||
<div style="color: var(--el-text-color-warning); font-size: 12px; margin-top: 4px">
|
||||
该设备未关联套餐系列,无法购买套餐
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="packagesEmptyWithSeriesId">
|
||||
<div style="color: var(--el-text-color-info); font-size: 12px; margin-top: 4px">
|
||||
该系列暂无可用套餐
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="selectedPackageIsGift">
|
||||
<div style="color: var(--el-color-warning); font-size: 12px; margin-top: 4px">
|
||||
提示: 赠送套餐只能使用线下支付方式,且必须上传支付凭证
|
||||
</div>
|
||||
</template>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="支付方式" prop="payment_method">
|
||||
<ElSelect v-model="form.payment_method" placeholder="请选择支付方式" style="width: 100%">
|
||||
<ElOption label="钱包支付" value="wallet" />
|
||||
<ElSelect
|
||||
v-model="form.payment_method"
|
||||
placeholder="请选择支付方式"
|
||||
style="width: 100%"
|
||||
:disabled="selectedPackageIsGift"
|
||||
>
|
||||
<ElOption v-if="!selectedPackageIsGift" label="钱包支付" value="wallet" />
|
||||
<ElOption v-if="showOfflinePayment" label="线下支付" value="offline" />
|
||||
</ElSelect>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
@@ -79,24 +102,32 @@
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="handleCancel">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm" :loading="loading"> 确认充值 </ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:loading="loading || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
>
|
||||
{{ voucherUploading ? '图片上传中...' : '提交' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElButton,
|
||||
ElIcon,
|
||||
ElMessage,
|
||||
ElUpload,
|
||||
ElIcon
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTag,
|
||||
ElUpload
|
||||
} from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
|
||||
@@ -124,8 +155,11 @@
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const loading = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const packageSearchLoading = ref(false)
|
||||
const packageOptions = ref<PackageResponse[]>([])
|
||||
const packagesEmptyWithSeriesId = ref(false)
|
||||
let activeVoucherUploadId = 0
|
||||
|
||||
const form = reactive({
|
||||
package_id: undefined as number | undefined,
|
||||
@@ -133,83 +167,130 @@
|
||||
payment_voucher_key: undefined as string | undefined
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
package_id: [{ required: true, message: '请选择套餐', trigger: 'change' }],
|
||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }]
|
||||
}
|
||||
const rules = computed<FormRules>(() => {
|
||||
const baseRules: FormRules = {
|
||||
package_id: [{ required: true, message: '请选择套餐', trigger: 'change' }],
|
||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }]
|
||||
}
|
||||
|
||||
if (form.payment_method === 'offline') {
|
||||
baseRules.payment_voucher_key = [{ required: true, message: '请上传支付凭证' }]
|
||||
}
|
||||
|
||||
return baseRules
|
||||
})
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
// 只有平台用户(user_type 1:超级管理员, 2:平台用户)才显示线下支付选项
|
||||
const showOfflinePayment = computed(() => {
|
||||
return userStore.info.user_type === 1 || userStore.info.user_type === 2
|
||||
})
|
||||
|
||||
// 监听对话框打开,加载套餐列表
|
||||
watch(visible, async (newVal) => {
|
||||
if (newVal) {
|
||||
if (!props.seriesId) {
|
||||
packageOptions.value = []
|
||||
return
|
||||
}
|
||||
await loadPackages()
|
||||
}
|
||||
const noSeriesId = computed(() => !props.seriesId)
|
||||
|
||||
const selectedPackageIsGift = computed(() => {
|
||||
if (!form.package_id) return false
|
||||
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === form.package_id)
|
||||
return selectedPackage?.is_gift || false
|
||||
})
|
||||
|
||||
watch(visible, async (newVal) => {
|
||||
if (!newVal) return
|
||||
|
||||
form.package_id = undefined
|
||||
packageOptions.value = []
|
||||
packagesEmptyWithSeriesId.value = false
|
||||
|
||||
if (!props.seriesId) {
|
||||
return
|
||||
}
|
||||
|
||||
await loadPackages()
|
||||
})
|
||||
|
||||
// 监听 seriesId 变化,重新加载套餐列表
|
||||
watch(
|
||||
() => props.seriesId,
|
||||
async (newSeriesId) => {
|
||||
if (visible.value && newSeriesId) {
|
||||
if (!visible.value) return
|
||||
|
||||
form.package_id = undefined
|
||||
packageOptions.value = []
|
||||
packagesEmptyWithSeriesId.value = false
|
||||
|
||||
if (newSeriesId) {
|
||||
await loadPackages()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.package_id,
|
||||
(packageId) => {
|
||||
if (!packageId) return
|
||||
|
||||
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === packageId)
|
||||
if (selectedPackage?.is_gift) {
|
||||
form.payment_method = 'offline'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let loadPackagesPromise: Promise<void> | null = null
|
||||
|
||||
const loadPackages = async () => {
|
||||
if (!props.seriesId) return
|
||||
|
||||
// 防止重复调用
|
||||
if (loadPackagesPromise) return
|
||||
if (loadPackagesPromise) return loadPackagesPromise
|
||||
|
||||
try {
|
||||
packageSearchLoading.value = true
|
||||
loadPackagesPromise = PackageManageService.getPackages({
|
||||
series_id: props.seriesId,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: 1, // 只获取启用的套餐
|
||||
shelf_status: 1 // 只获取已上架的套餐
|
||||
packageSearchLoading.value = true
|
||||
packagesEmptyWithSeriesId.value = false
|
||||
|
||||
loadPackagesPromise = PackageManageService.getPackages({
|
||||
series_id: props.seriesId,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: 1,
|
||||
shelf_status: 1
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.code === 0 && response.data) {
|
||||
const items = response.data.items || []
|
||||
packageOptions.value = items
|
||||
packagesEmptyWithSeriesId.value = items.length === 0
|
||||
return
|
||||
}
|
||||
|
||||
packageOptions.value = []
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.code === 0 && response.data) {
|
||||
packageOptions.value = response.data.items || []
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
packageSearchLoading.value = false
|
||||
loadPackagesPromise = null
|
||||
})
|
||||
await loadPackagesPromise
|
||||
} catch (error: any) {
|
||||
console.error('加载套餐列表失败:', error)
|
||||
console.log(error?.message || '加载套餐列表失败')
|
||||
}
|
||||
.catch((error: any) => {
|
||||
console.error('加载套餐列表失败:', error)
|
||||
packageOptions.value = []
|
||||
})
|
||||
.finally(() => {
|
||||
packageSearchLoading.value = false
|
||||
loadPackagesPromise = null
|
||||
})
|
||||
|
||||
await loadPackagesPromise
|
||||
}
|
||||
|
||||
const handlePackageSearch = async (query: string) => {
|
||||
if (!props.seriesId) return
|
||||
|
||||
if (!query) {
|
||||
await loadPackages()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
packageSearchLoading.value = true
|
||||
packagesEmptyWithSeriesId.value = false
|
||||
|
||||
const response = await PackageManageService.getPackages({
|
||||
package_name: query || undefined,
|
||||
package_name: query,
|
||||
series_id: props.seriesId,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
@@ -227,12 +308,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 处理支付凭证文件变化
|
||||
const invalidateVoucherUploadState = (clearFileKey = false) => {
|
||||
activeVoucherUploadId += 1
|
||||
voucherUploading.value = false
|
||||
if (clearFileKey) {
|
||||
form.payment_voucher_key = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
|
||||
// 验证文件类型
|
||||
const isImage = file.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
@@ -240,7 +327,6 @@
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB!')
|
||||
@@ -248,11 +334,13 @@
|
||||
return
|
||||
}
|
||||
|
||||
// 开始上传
|
||||
const requestId = ++activeVoucherUploadId
|
||||
voucherUploading.value = true
|
||||
form.payment_voucher_key = undefined
|
||||
|
||||
try {
|
||||
ElMessage.info('正在上传支付凭证...')
|
||||
|
||||
// 1. 获取上传 URL
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: file.type,
|
||||
@@ -260,29 +348,38 @@
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
// 2. 上传文件到对象存储
|
||||
await StorageService.uploadFile(upload_url, file, file.type)
|
||||
|
||||
// 3. 保存 file_key
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
|
||||
form.payment_voucher_key = file_key
|
||||
ElMessage.success('支付凭证上传成功')
|
||||
ElMessage.success('上传成功')
|
||||
nextTick(() => {
|
||||
formRef.value?.validateField('payment_voucher_key')
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
|
||||
console.error('上传支付凭证失败:', error)
|
||||
ElMessage.error(error?.message || '上传失败')
|
||||
ElMessage.error(error?.message || '上传失败,请重试')
|
||||
form.payment_voucher_key = undefined
|
||||
uploadRef.value?.clearFiles()
|
||||
} finally {
|
||||
if (requestId === activeVoucherUploadId) {
|
||||
voucherUploading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除支付凭证
|
||||
const handleRemoveVoucher = () => {
|
||||
form.payment_voucher_key = undefined
|
||||
invalidateVoucherUploadState(true)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -291,38 +388,39 @@
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!formRef.value) return
|
||||
if (voucherUploading.value) {
|
||||
ElMessage.warning('支付凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
if (!form.package_id) {
|
||||
ElMessage.error('请选择套餐')
|
||||
return
|
||||
}
|
||||
|
||||
if (form.payment_method === 'offline' && !form.payment_voucher_key) {
|
||||
ElMessage.error('线下支付必须上传支付凭证')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (!form.package_id) {
|
||||
ElMessage.error('请选择套餐')
|
||||
return
|
||||
}
|
||||
|
||||
// 线下支付时验证支付凭证
|
||||
if (form.payment_method === 'offline' && !form.payment_voucher_key) {
|
||||
ElMessage.error('线下支付需要上传支付凭证')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
// 创建订单(后端接收数组,前端单选转为单元素数组)
|
||||
const data: CreateOrderRequest = {
|
||||
identifier: props.assetIdentifier,
|
||||
package_ids: [form.package_id],
|
||||
payment_method: form.payment_method
|
||||
}
|
||||
|
||||
// 线下支付时,添加支付凭证
|
||||
if (form.payment_method === 'offline' && form.payment_voucher_key) {
|
||||
data.payment_voucher_key = form.payment_voucher_key
|
||||
}
|
||||
|
||||
await OrderService.createOrder(data)
|
||||
|
||||
// 根据支付方式显示不同的成功消息
|
||||
if (form.payment_method === 'wallet') {
|
||||
ElMessage.success('订单创建成功,已自动完成支付')
|
||||
} else {
|
||||
@@ -333,7 +431,6 @@
|
||||
emit('confirm')
|
||||
emit('success')
|
||||
} catch (error: any) {
|
||||
// 用户取消确认对话框
|
||||
if (error === 'cancel') {
|
||||
return
|
||||
}
|
||||
@@ -347,8 +444,9 @@
|
||||
formRef.value?.resetFields()
|
||||
form.package_id = undefined
|
||||
form.payment_method = 'wallet'
|
||||
form.payment_voucher_key = undefined
|
||||
invalidateVoucherUploadState(true)
|
||||
packageOptions.value = []
|
||||
packagesEmptyWithSeriesId.value = false
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,65 +1,143 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="设置切卡模式" width="500px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="96px">
|
||||
<ElFormItem label="设备标识">
|
||||
<span style="font-weight: bold; color: #409eff">{{ deviceIdentifier }}</span>
|
||||
<span class="device-identifier">{{ deviceIdentifier || '-' }}</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="当前模式">
|
||||
<ElTag :type="currentMode === 0 ? 'success' : 'warning'">
|
||||
{{ currentMode === 0 ? '自动切卡' : '手动切卡' }}
|
||||
<ElTag :type="currentModeValue === 0 ? 'success' : 'warning'">
|
||||
{{ currentModeValue === 0 ? '自动切卡' : '手动切卡' }}
|
||||
</ElTag>
|
||||
</ElFormItem>
|
||||
|
||||
<ElAlert
|
||||
v-if="!hasImei"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="设备未配置 IMEI,无法设置切卡模式"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<ElAlert
|
||||
v-else-if="availableCards.length === 0"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="暂无绑定卡,无法设置切卡模式"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<ElFormItem label="绑定卡" prop="iot_card_id">
|
||||
<ElSelect
|
||||
v-model="form.iot_card_id"
|
||||
style="width: 100%"
|
||||
placeholder="请选择绑定卡"
|
||||
:disabled="!canSubmit"
|
||||
>
|
||||
<ElOption
|
||||
v-for="card in availableCards"
|
||||
:key="card.iot_card_id"
|
||||
:label="getCardLabel(card)"
|
||||
:value="card.iot_card_id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="field-tip">请选择一张绑定卡</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="切卡模式" prop="switch_mode">
|
||||
<ElRadioGroup v-model="form.switch_mode">
|
||||
<ElRadio :value="0">自动切卡</ElRadio>
|
||||
<ElRadio :value="1">手动切卡</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<div style="font-size: 12px; color: #909399; line-height: 1.6">
|
||||
<div class="mode-description">
|
||||
<p>说明:</p>
|
||||
<p>• <strong>自动切卡</strong>:设备根据信号强度自动切换到最优的SIM卡</p>
|
||||
<p>• <strong>手动切卡</strong>:需要手动触发切换SIM卡操作</p>
|
||||
<p>• <strong>自动切卡</strong>:设备根据信号强度自动切换到最优的 SIM 卡</p>
|
||||
<p>• <strong>手动切卡</strong>:需要手动触发切换 SIM 卡操作</p>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="handleCancel">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm" :loading="confirmLoading">确认设置</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:loading="confirmLoading"
|
||||
:disabled="!canSubmit"
|
||||
>
|
||||
确认设置
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElDialog, ElForm, ElFormItem, ElRadioGroup, ElRadio, ElTag } from 'element-plus'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElOption,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
ElTag
|
||||
} from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface SwitchModeCardOption {
|
||||
iot_card_id: number
|
||||
iccid: string
|
||||
slot_position?: number
|
||||
carrier_name?: string
|
||||
msisdn?: string
|
||||
network_status?: number
|
||||
real_name_status?: number
|
||||
is_current?: boolean
|
||||
}
|
||||
|
||||
interface SwitchModeForm {
|
||||
iot_card_id?: number
|
||||
switch_mode: 0 | 1
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
deviceIdentifier: string
|
||||
currentMode?: number
|
||||
currentMode?: number | string
|
||||
cards?: SwitchModeCardOption[]
|
||||
hasImei?: boolean
|
||||
confirmLoading?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm', data: { switch_mode: number }): void
|
||||
(e: 'confirm', data: { iot_card_id: number; switch_mode: 0 | 1 }): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
currentMode: 0
|
||||
currentMode: 0,
|
||||
cards: () => [],
|
||||
hasImei: false,
|
||||
confirmLoading: false
|
||||
})
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const confirmLoading = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
const form = reactive<SwitchModeForm>({
|
||||
iot_card_id: undefined,
|
||||
switch_mode: 0
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
const rules: FormRules<SwitchModeForm> = {
|
||||
iot_card_id: [{ required: true, message: '请选择绑定卡', trigger: 'change' }],
|
||||
switch_mode: [{ required: true, message: '请选择切卡模式', trigger: 'change' }]
|
||||
}
|
||||
|
||||
@@ -68,27 +146,99 @@
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const currentMode = computed(() => Number(props.currentMode) ?? 0)
|
||||
const currentModeValue = computed<0 | 1>(() =>
|
||||
Number(props.currentMode) === 1 ? 1 : 0
|
||||
)
|
||||
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal) {
|
||||
form.switch_mode = Number(props.currentMode) ?? 0
|
||||
const availableCards = computed(() => props.cards || [])
|
||||
|
||||
const canSubmit = computed(() => props.hasImei && availableCards.value.length > 0)
|
||||
|
||||
const syncFormState = () => {
|
||||
form.switch_mode = currentModeValue.value
|
||||
|
||||
const selectedStillExists = availableCards.value.some((card) => card.iot_card_id === form.iot_card_id)
|
||||
if (!selectedStillExists) {
|
||||
form.iot_card_id = availableCards.value[0]?.iot_card_id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
syncFormState()
|
||||
return
|
||||
}
|
||||
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.cards, props.currentMode] as const,
|
||||
() => {
|
||||
if (visible.value) {
|
||||
syncFormState()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const getCardLabel = (card: SwitchModeCardOption) => {
|
||||
const parts = [`卡槽 ${card.slot_position ?? '-'}`, card.iccid]
|
||||
|
||||
if (card.carrier_name) {
|
||||
parts.push(card.carrier_name)
|
||||
}
|
||||
|
||||
if (card.is_current) {
|
||||
parts.push('当前使用')
|
||||
}
|
||||
|
||||
return parts.join(' / ')
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!formRef.value) return
|
||||
if (!formRef.value || !canSubmit.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
emit('confirm', { switch_mode: form.switch_mode })
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
|
||||
emit('confirm', {
|
||||
iot_card_id: form.iot_card_id!,
|
||||
switch_mode: form.switch_mode
|
||||
})
|
||||
} catch {
|
||||
// Element Plus validation rejection is expected when required fields are missing.
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.device-identifier {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.field-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.mode-description {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.mode-description p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -111,7 +111,10 @@
|
||||
<SwitchModeDialog
|
||||
v-model="switchModeDialogVisible"
|
||||
:device-identifier="cardInfo?.virtual_no || ''"
|
||||
:current-mode="cardInfo?.switch_mode as number"
|
||||
:current-mode="Number(cardInfo?.switch_mode ?? 0)"
|
||||
:cards="switchModeCards"
|
||||
:has-imei="hasSwitchModeImei"
|
||||
:confirm-loading="switchModeLoading"
|
||||
@confirm="handleConfirmSwitchMode"
|
||||
/>
|
||||
<WiFiConfigDialog
|
||||
@@ -160,10 +163,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElCard, ElEmpty } from 'element-plus'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { DeviceService } from '@/api/modules'
|
||||
|
||||
// 引入子组件
|
||||
import AssetSearchCard from './components/AssetSearchCard.vue'
|
||||
@@ -190,10 +194,21 @@
|
||||
import { useAssetOperations } from './composables/useAssetOperations'
|
||||
|
||||
// 引入类型
|
||||
import type { SwitchCardForm, WiFiForm } from './types'
|
||||
import type { BindingCard, SwitchCardForm, WiFiForm } from './types'
|
||||
|
||||
defineOptions({ name: 'SingleCard' })
|
||||
|
||||
interface SwitchModeCardOption {
|
||||
iot_card_id: number
|
||||
iccid: string
|
||||
slot_position?: number
|
||||
carrier_name?: string
|
||||
msisdn?: string
|
||||
network_status?: number
|
||||
real_name_status?: number
|
||||
is_current?: boolean
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -239,6 +254,7 @@
|
||||
// ========== 对话框状态 ==========
|
||||
const switchCardDialogVisible = ref(false)
|
||||
const switchModeDialogVisible = ref(false)
|
||||
const switchModeLoading = ref(false)
|
||||
const setWiFiDialogVisible = ref(false)
|
||||
const rechargeDialogVisible = ref(false)
|
||||
const dailyRecordsDialogVisible = ref(false)
|
||||
@@ -252,6 +268,27 @@
|
||||
const bindingCardRealnameStatusIccid = ref('')
|
||||
const bindingCardRealnameStatusValue = ref<number>(0)
|
||||
|
||||
const switchModeCards = computed<SwitchModeCardOption[]>(() =>
|
||||
(cardInfo.value?.cards || [])
|
||||
.map((card: BindingCard) => ({
|
||||
iot_card_id: Number(card.card_id ?? card.id),
|
||||
iccid: card.iccid,
|
||||
slot_position: card.slot_position,
|
||||
carrier_name: card.carrier_name,
|
||||
msisdn: card.msisdn,
|
||||
network_status:
|
||||
card.network_status === undefined ? undefined : Number(card.network_status),
|
||||
real_name_status:
|
||||
card.real_name_status === undefined ? undefined : Number(card.real_name_status),
|
||||
is_current: card.is_current
|
||||
}))
|
||||
.filter(
|
||||
(card: SwitchModeCardOption) => Number.isFinite(card.iot_card_id) && Boolean(card.iccid)
|
||||
)
|
||||
)
|
||||
|
||||
const hasSwitchModeImei = computed(() => Boolean(cardInfo.value?.imei))
|
||||
|
||||
// ========== 套餐列表分页状态 ==========
|
||||
const packagePagination = ref({
|
||||
page: 1,
|
||||
@@ -375,19 +412,25 @@
|
||||
/**
|
||||
* 确认切换模式
|
||||
*/
|
||||
const handleConfirmSwitchMode = async (form: { switch_mode: number }) => {
|
||||
const handleConfirmSwitchMode = async (form: { iot_card_id: number; switch_mode: 0 | 1 }) => {
|
||||
const identifier = cardInfo.value?.virtual_no || ''
|
||||
if (!identifier) return
|
||||
|
||||
switchModeLoading.value = true
|
||||
try {
|
||||
const { DeviceService } = await import('@/api/modules')
|
||||
const identifier = cardInfo.value?.virtual_no || ''
|
||||
const res = await DeviceService.setSwitchMode(identifier, form.switch_mode)
|
||||
const res = await DeviceService.setSwitchMode(identifier, form)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('设置切卡模式成功')
|
||||
switchModeDialogVisible.value = false
|
||||
await loadRealtimeStatus(identifier, 'device')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '设置切卡模式失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('设置切卡模式失败:', error)
|
||||
ElMessage.error('设置切卡模式失败')
|
||||
} finally {
|
||||
switchModeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,15 @@ export interface AssetInfo {
|
||||
// 设备绑定卡信息
|
||||
export interface BindingCard {
|
||||
id: number
|
||||
card_id?: number
|
||||
iccid: string
|
||||
msisdn?: string
|
||||
is_current: boolean
|
||||
status?: number
|
||||
carrier_name?: string
|
||||
slot_position?: number
|
||||
network_status?: number
|
||||
real_name_status?: number
|
||||
realname_policy?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -480,6 +480,9 @@
|
||||
v-model="switchModeDialogVisible"
|
||||
:device-identifier="currentOperatingDeviceIdentifier"
|
||||
:current-mode="currentDeviceSwitchMode"
|
||||
:cards="switchModeCards"
|
||||
:has-imei="currentOperatingDeviceHasImei"
|
||||
:confirm-loading="switchModeLoading"
|
||||
@confirm="handleConfirmSwitchMode"
|
||||
/>
|
||||
|
||||
@@ -535,6 +538,17 @@
|
||||
|
||||
defineOptions({ name: 'DeviceList' })
|
||||
|
||||
interface SwitchModeCardOption {
|
||||
iot_card_id: number
|
||||
iccid: string
|
||||
slot_position?: number
|
||||
carrier_name?: string
|
||||
msisdn?: string
|
||||
network_status?: number
|
||||
real_name_status?: number
|
||||
is_current?: boolean
|
||||
}
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -638,7 +652,9 @@
|
||||
// 切换模式相关
|
||||
const switchModeDialogVisible = ref(false)
|
||||
const switchModeLoading = ref(false)
|
||||
const switchModeCards = ref<SwitchModeCardOption[]>([])
|
||||
const currentOperatingDeviceIdentifier = ref<string>('')
|
||||
const currentOperatingDeviceHasImei = ref(false)
|
||||
const currentDeviceSwitchMode = ref<number>(0)
|
||||
|
||||
// 实名认证策略相关
|
||||
@@ -1678,7 +1694,7 @@
|
||||
await handleClearDeviceSeries(deviceNo)
|
||||
break
|
||||
case 'switch-mode':
|
||||
showSwitchModeDialog(deviceNo, device?.switch_mode)
|
||||
await showSwitchModeDialog(deviceNo)
|
||||
break
|
||||
case 'realname-policy':
|
||||
showRealnamePolicyDialog(deviceNo, device?.realname_policy)
|
||||
@@ -1866,20 +1882,62 @@
|
||||
}
|
||||
|
||||
// 显示切换模式对话框
|
||||
const showSwitchModeDialog = (deviceIdentifier: string, currentMode?: number) => {
|
||||
const normalizeSwitchModeCards = (cards: Array<Record<string, any>>): SwitchModeCardOption[] => {
|
||||
return cards
|
||||
.map((card) => ({
|
||||
iot_card_id: Number(card.iot_card_id),
|
||||
iccid: card.iccid,
|
||||
slot_position: card.slot_position,
|
||||
carrier_name: card.carrier_name,
|
||||
msisdn: card.msisdn,
|
||||
network_status:
|
||||
card.network_status === undefined ? undefined : Number(card.network_status),
|
||||
real_name_status:
|
||||
card.real_name_status === undefined ? undefined : Number(card.real_name_status),
|
||||
is_current: card.is_current
|
||||
}))
|
||||
.filter((card) => Number.isFinite(card.iot_card_id) && Boolean(card.iccid))
|
||||
}
|
||||
|
||||
// 显示切换模式对话框
|
||||
const showSwitchModeDialog = async (deviceIdentifier: string) => {
|
||||
const device = currentOperatingDevice.value
|
||||
if (!device) {
|
||||
ElMessage.error('未找到设备信息')
|
||||
return
|
||||
}
|
||||
|
||||
currentOperatingDeviceIdentifier.value = deviceIdentifier
|
||||
currentDeviceSwitchMode.value = currentMode ?? 0
|
||||
currentDeviceSwitchMode.value = Number(device.switch_mode ?? 0)
|
||||
currentOperatingDeviceHasImei.value = Boolean(device.imei)
|
||||
switchModeCards.value = []
|
||||
|
||||
if (currentOperatingDeviceHasImei.value) {
|
||||
try {
|
||||
const res = await DeviceService.getDeviceCards(device.virtual_no)
|
||||
if (res.code !== 0 || !res.data) {
|
||||
ElMessage.error(res.msg || '加载设备绑定卡失败')
|
||||
return
|
||||
}
|
||||
|
||||
switchModeCards.value = normalizeSwitchModeCards(
|
||||
(res.data.bindings || []) as Array<Record<string, any>>
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('加载切卡模式绑定卡失败:', error)
|
||||
ElMessage.error('加载设备绑定卡失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switchModeDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 确认切换模式
|
||||
const handleConfirmSwitchMode = async (data: { switch_mode: number }) => {
|
||||
const handleConfirmSwitchMode = async (data: { iot_card_id: number; switch_mode: 0 | 1 }) => {
|
||||
switchModeLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.setSwitchMode(
|
||||
currentOperatingDeviceIdentifier.value,
|
||||
data.switch_mode
|
||||
)
|
||||
const res = await DeviceService.setSwitchMode(currentOperatingDeviceIdentifier.value, data)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('设置切卡模式成功')
|
||||
switchModeDialogVisible.value = false
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
:title="'创建订单'"
|
||||
width="600px"
|
||||
width="30%"
|
||||
@closed="handleCreateDialogClosed"
|
||||
>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="80px">
|
||||
<ElFormItem :label="'订单类型'" prop="order_type">
|
||||
<ElSelect
|
||||
v-model="createForm.order_type"
|
||||
@@ -146,9 +146,7 @@
|
||||
<template v-if="pkg.is_gift">
|
||||
<el-tag type="warning" size="small">赠送</el-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
¥{{ ((pkg.cost_price || 0) / 100).toFixed(2) }}
|
||||
</template>
|
||||
<template v-else> ¥{{ ((pkg.cost_price || 0) / 100).toFixed(2) }} </template>
|
||||
</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
@@ -176,11 +174,7 @@
|
||||
style="width: 100%"
|
||||
:disabled="selectedPackageIsGift"
|
||||
>
|
||||
<ElOption
|
||||
v-if="!selectedPackageIsGift"
|
||||
label="钱包支付"
|
||||
value="wallet"
|
||||
/>
|
||||
<ElOption v-if="!selectedPackageIsGift" label="钱包支付" value="wallet" />
|
||||
<!-- 只有平台用户(user_type 1:超级管理员, 2:平台用户)才显示线下支付选项 -->
|
||||
<ElOption
|
||||
v-if="userStore.info.user_type === 1 || userStore.info.user_type === 2"
|
||||
@@ -224,9 +218,10 @@
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleCreateOrder(createFormRef)"
|
||||
:loading="createLoading"
|
||||
:loading="createLoading || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
>
|
||||
{{ '提交' }}
|
||||
{{ voucherUploading ? '图片上传中...' : '提交' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -369,11 +364,13 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const tableRef = ref()
|
||||
const createDialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentOrder = ref<Order | null>(null)
|
||||
const paymentVoucherFileKey = ref('')
|
||||
let activeVoucherUploadId = 0
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: OrderQueryParams = {
|
||||
@@ -590,6 +587,14 @@
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
const invalidateVoucherUploadState = (clearFileKey = false) => {
|
||||
activeVoucherUploadId += 1
|
||||
voucherUploading.value = false
|
||||
if (clearFileKey) {
|
||||
createForm.payment_voucher_key = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 处理支付凭证文件选择
|
||||
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
@@ -611,6 +616,10 @@
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = ++activeVoucherUploadId
|
||||
voucherUploading.value = true
|
||||
createForm.payment_voucher_key = undefined
|
||||
|
||||
// 开始上传
|
||||
try {
|
||||
ElMessage.info('正在上传支付凭证...')
|
||||
@@ -633,6 +642,8 @@
|
||||
// 2. 上传文件到对象存储
|
||||
await StorageService.uploadFile(upload_url, file, file.type)
|
||||
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
|
||||
// 3. 保存 file_key
|
||||
createForm.payment_voucher_key = file_key
|
||||
ElMessage.success('上传成功')
|
||||
@@ -641,16 +652,22 @@
|
||||
createFormRef.value?.validateField('payment_voucher_key')
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
|
||||
console.error('上传失败:', error)
|
||||
console.log(error.message || '上传失败,请重试')
|
||||
ElMessage.error(error.message || '上传失败,请重试')
|
||||
createForm.payment_voucher_key = undefined
|
||||
uploadRef.value?.clearFiles()
|
||||
} finally {
|
||||
if (requestId === activeVoucherUploadId) {
|
||||
voucherUploading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除支付凭证
|
||||
const handleRemoveVoucher = () => {
|
||||
createForm.payment_voucher_key = undefined
|
||||
invalidateVoucherUploadState(true)
|
||||
}
|
||||
|
||||
const createRules = computed<FormRules>(() => {
|
||||
@@ -1153,7 +1170,7 @@
|
||||
createForm.iot_card_id = null
|
||||
createForm.device_id = null
|
||||
createForm.payment_method = 'wallet'
|
||||
createForm.payment_voucher_key = undefined
|
||||
invalidateVoucherUploadState(true)
|
||||
|
||||
// 清空上传文件列表
|
||||
uploadRef.value?.clearFiles()
|
||||
@@ -1167,6 +1184,10 @@
|
||||
// 创建订单
|
||||
const handleCreateOrder = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
if (voucherUploading.value) {
|
||||
ElMessage.warning('支付凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
|
||||
Reference in New Issue
Block a user