fix: 套餐

This commit is contained in:
sexygoat
2026-04-29 11:29:53 +08:00
parent 686bddad79
commit 1dc4701540
44 changed files with 3912 additions and 150 deletions

View File

@@ -241,11 +241,11 @@ export class DeviceService extends BaseService {
*/
static setSwitchMode(
identifier: string,
switchMode: number
data: { iot_card_id: number; switch_mode: 0 | 1 }
): Promise<BaseResponse<DeviceOperationResponse>> {
return this.post<BaseResponse<DeviceOperationResponse>>(
`/api/admin/devices/by-identifier/${identifier}/switch-mode`,
{ switch_mode: switchMode }
data
)
}
}

View File

@@ -299,9 +299,6 @@
</ElDescriptionsItem>
<!-- 网络信息 -->
<ElDescriptionsItem label="IP 地址">
{{ deviceRealtime.ip_address || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="WAN IP">
{{ deviceRealtime.wan_ip || '-' }}
</ElDescriptionsItem>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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