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

This commit is contained in:
sexygoat
2026-05-11 15:51:50 +08:00
parent 766497dc31
commit 1990a3ccba
40 changed files with 832 additions and 127 deletions

View File

@@ -28,7 +28,9 @@ export class AccountService extends BaseService {
* POST /api/admin/accounts
* @param data 账号数据
*/
static createAccount(data: CreatePlatformAccountParams): Promise<BaseResponse<{ ID?: number; id?: number }>> {
static createAccount(
data: CreatePlatformAccountParams
): Promise<BaseResponse<{ ID?: number; id?: number }>> {
return this.create('/api/admin/accounts', data)
}

View File

@@ -23,7 +23,9 @@ import type {
ImportIotCardRequest,
ImportIotCardResponse,
IotCardImportTask,
IotCardImportTaskDetail
IotCardImportTaskDetail,
BatchSetCardSeriesBindingRequest,
BatchSetCardSeriesBindingResponse
} from '@/types/api'
export class CardService extends BaseService {
@@ -360,11 +362,13 @@ export class CardService extends BaseService {
* 批量设置卡的套餐系列绑定
* @param data 请求参数
*/
static batchSetCardSeriesBinding(data: {
iccids: string[]
series_id: number
}): Promise<BaseResponse<any>> {
return this.patch('/api/admin/iot-cards/series-binding', data)
static batchSetCardSeriesBinding(
data: BatchSetCardSeriesBindingRequest
): Promise<BaseResponse<BatchSetCardSeriesBindingResponse>> {
return this.patch<BaseResponse<BatchSetCardSeriesBindingResponse>>(
'/api/admin/iot-cards/series-binding',
data
)
}
// ========== IoT卡网关操作相关 ==========

View File

@@ -15,6 +15,8 @@ import type {
AllocateDevicesResponse,
RecallDevicesRequest,
RecallDevicesResponse,
BatchSetDeviceSeriesBindingRequest,
BatchSetDeviceSeriesBindingResponse,
ImportDeviceRequest,
ImportDeviceResponse,
DeviceImportTaskQueryParams,
@@ -158,11 +160,13 @@ export class DeviceService extends BaseService {
* 批量设置设备的套餐系列绑定
* @param data 请求参数
*/
static batchSetDeviceSeriesBinding(data: {
device_ids: number[]
series_id: number
}): Promise<BaseResponse<any>> {
return this.patch('/api/admin/devices/series-binding', data)
static batchSetDeviceSeriesBinding(
data: BatchSetDeviceSeriesBindingRequest
): Promise<BaseResponse<BatchSetDeviceSeriesBindingResponse>> {
return this.patch<BaseResponse<BatchSetDeviceSeriesBindingResponse>>(
'/api/admin/devices/series-binding',
data
)
}
// ========== 设备操作相关 ==========

View File

@@ -32,6 +32,7 @@ export class ShopService extends BaseService {
static getShopsCascade(params?: {
shop_name?: string
parent_id?: number
exclude_self?: boolean
}): Promise<
BaseResponse<
Array<{

View File

@@ -1,10 +1,5 @@
<template>
<ElDialog
v-model="dialogVisible"
title="创建退款申请"
width="30%"
@closed="handleDialogClosed"
>
<ElDialog v-model="dialogVisible" title="创建退款申请" width="30%" @closed="handleDialogClosed">
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<ElFormItem label="订单号" prop="order_id">
<ElSelect
@@ -79,7 +74,7 @@
const props = defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
'success': []
success: []
}>()
const dialogVisible = computed({

View File

@@ -42,11 +42,24 @@
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`"
:value="card.iccid"
>
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
"
>
<span>{{ card.iccid }}</span>
<div>
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
<ElTag v-if="card.carrier_name" size="small" type="success" style="margin-left: 5px">{{ card.carrier_name }}</ElTag>
<ElTag
v-if="card.carrier_name"
size="small"
type="success"
style="margin-left: 5px"
>{{ card.carrier_name }}</ElTag
>
</div>
</div>
</ElOption>

View File

@@ -58,7 +58,7 @@ export function useLogin() {
formData.password = remembered.password
formData.rememberPassword = remembered.rememberPassword
} else {
if (import.meta.env.MODE === 'development') {
if (import.meta.env.DEV) {
formData.username = 'admin'
formData.password = 'Admin@123456'
formData.rememberPassword = false

View File

@@ -14,7 +14,8 @@ export const router = createRouter({
// 处理路由错误Chunk 加载失败等)
router.onError((error) => {
const pattern = /Loading chunk (\d+) failed|Failed to fetch dynamically imported module|Couldn't resolve component/
const pattern =
/Loading chunk (\d+) failed|Failed to fetch dynamically imported module|Couldn't resolve component/
const isChunkLoadFailed = error.message && pattern.test(error.message)
if (isChunkLoadFailed) {

View File

@@ -527,8 +527,14 @@ export interface IotCardDetailResponse {
// 批量设置卡的套餐系列绑定请求参数
export interface BatchSetCardSeriesBindingRequest {
iccids: string[] // ICCID列表最多500个
selection_type?: CardSelectionType // 选卡方式(兼容旧版时可不传,默认 list
series_id: number // 套餐系列ID0表示清除关联
iccids?: string[] // ICCID列表selection_type=list时必填最多1000个
iccid_start?: string // 起始ICCIDselection_type=range时必填
iccid_end?: string // 结束ICCIDselection_type=range时必填
carrier_id?: number // 运营商IDselection_type=filter时可选
status?: StandaloneCardStatus // 卡状态selection_type=filter时可选
batch_no?: string // 批次号selection_type=filter时可选
}
// 卡套餐系列绑定失败项

View File

@@ -237,10 +237,31 @@ export interface DeviceImportTaskDetail extends DeviceImportTask {
// ========== 批量设置设备的套餐系列绑定相关 ==========
// 选设备方式枚举
export enum DeviceSelectionType {
LIST = 'list', // 设备ID列表
RANGE = 'range', // 设备号范围
FILTER = 'filter' // 筛选条件
}
// 批量设置设备的套餐系列绑定请求参数
export interface BatchSetDeviceSeriesBindingRequest {
device_ids: number[] // 设备ID列表最多500个
selection_type?: DeviceSelectionType // 设备方式(兼容旧版时可不传,默认 list
series_id: number // 套餐系列ID0表示清除关联
device_ids?: number[] // 设备ID列表selection_type=list时必填最多1000个
virtual_no_start?: string // 起始设备虚拟号selection_type=range时必填
virtual_no_end?: string // 结束设备虚拟号selection_type=range时必填
virtual_no?: string // 设备虚拟号selection_type=filter时可选
device_name?: string // 设备名称selection_type=filter时可选
device_type?: string // 设备类型selection_type=filter时可选
manufacturer?: string // 制造商selection_type=filter时可选
batch_no?: string // 批次号selection_type=filter时可选
status?: DeviceStatus // 设备状态selection_type=filter时可选
shop_id?: number // 店铺IDselection_type=filter时可选
shop_ids?: number[] // 店铺ID列表selection_type=filter时可选
filter_series_id?: number // 当前绑定套餐系列IDselection_type=filter时可选
created_at_start?: string // 创建时间开始selection_type=filter时可选
created_at_end?: string // 创建时间结束selection_type=filter时可选
}
// 设备套餐系列绑定失败项

View File

@@ -7,7 +7,7 @@
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const ElButton: typeof import('element-plus/es')['ElButton']
const ElButton: (typeof import('element-plus/es'))['ElButton']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']

View File

@@ -6,7 +6,7 @@ import type { RequestOptions, ErrorMessageMode } from '@/types/api'
const axiosInstance = axios.create({
timeout: 15000, // 请求超时时间(毫秒)
baseURL: import.meta.env.MODE === 'development' ? '' : import.meta.env.VITE_API_URL, // API地址开发环境使用代理,生产环境使用完整URL
baseURL: import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL, // 开发服务器使用代理,其他模式使用完整URL
withCredentials: false, // 异步请求携带cookie
transformRequest: [
(data, headers) => {
@@ -272,9 +272,10 @@ async function request<T = any>(config: ExtendedRequestConfig): Promise<T> {
if (axios.isAxiosError(e)) {
// 默认显示错误消息,除非明确设置为 'none'
const errorMode = config.requestOptions?.errorMessageMode || 'message'
const show404Error = config.requestOptions?.show404Error !== undefined
? config.requestOptions.show404Error
: true
const show404Error =
config.requestOptions?.show404Error !== undefined
? config.requestOptions.show404Error
: true
handleErrorMessage(e, errorMode, show404Error)
}
return Promise.reject(e)

View File

@@ -33,7 +33,9 @@
<!-- 操作按钮组 -->
<div v-if="hasCardInfo" class="operation-button-group">
<ElButton @click="handleRefresh" type="primary" :icon="Refresh" :disabled="refreshDisabled"> 刷新 </ElButton>
<ElButton @click="handleRefresh" type="primary" :icon="Refresh" :disabled="refreshDisabled">
刷新
</ElButton>
</div>
</div>
</ElCard>

View File

@@ -166,7 +166,10 @@
<div class="flow-progress-bar" style="margin-top: 16px">
<ElProgress
:percentage="
getUsageProgress(getDisplayedUsedMb(currentPackage), getDisplayedTotalMb(currentPackage))
getUsageProgress(
getDisplayedUsedMb(currentPackage),
getDisplayedTotalMb(currentPackage)
)
"
:color="
getProgressColorByPercentage(

View File

@@ -145,23 +145,13 @@
<template v-else>
<div class="progress-text">
<span class="used"
>已使用:
{{
formatDataSize(
getDisplayedUsedMb(scope.row)
)
}}</span
>已使用: {{ formatDataSize(getDisplayedUsedMb(scope.row)) }}</span
>
<span class="total"
>总量: {{ formatDataSize(scope.row.real_total_mb || 0) }}</span
>
<span class="remaining"
>剩余:
{{
formatDataSize(
getDisplayedRemainingMb(scope.row)
)
}}</span
>剩余: {{ formatDataSize(getDisplayedRemainingMb(scope.row)) }}</span
>
</div>
<ElProgress

View File

@@ -390,7 +390,6 @@
color: var(--el-text-color-primary, #374151);
}
}
}
.pagination-wrapper {

View File

@@ -146,9 +146,7 @@
set: (value) => emit('update:modelValue', value)
})
const currentModeValue = computed<0 | 1>(() =>
Number(props.currentMode) === 1 ? 1 : 0
)
const currentModeValue = computed<0 | 1>(() => (Number(props.currentMode) === 1 ? 1 : 0))
const availableCards = computed(() => props.cards || [])
@@ -157,7 +155,9 @@
const syncFormState = () => {
form.switch_mode = currentModeValue.value
const selectedStillExists = availableCards.value.some((card) => card.iot_card_id === form.iot_card_id)
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
}

View File

@@ -36,7 +36,6 @@
<ElButton
type="info"
@click="handleBatchSetSeries"
:disabled="!selectedDevices.length"
v-permission="'device:batch_set_series'"
>
批量设置套餐系列
@@ -200,6 +199,7 @@
v-model="seriesBindingDialogVisible"
title="批量设置设备套餐系列绑定"
width="600px"
@close="handleCloseSeriesBindingDialog"
>
<ElForm
ref="seriesBindingFormRef"
@@ -210,7 +210,167 @@
<ElFormItem label="已选设备数">
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span>
</ElFormItem>
<ElFormItem label="套餐系列" prop="series_id">
<ElFormItem label="选设备方式" prop="selection_type">
<ElRadioGroup v-model="seriesBindingForm.selection_type">
<ElRadio label="list">设备ID列表</ElRadio>
<ElRadio label="range">设备号范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.LIST"
label="设备列表"
>
<div>已选择 {{ selectedDevices.length }} 台设备</div>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE"
label="起始设备号"
prop="virtual_no_start"
>
<ElInput
v-model="seriesBindingForm.virtual_no_start"
placeholder="请输入起始设备号"
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE"
label="结束设备号"
prop="virtual_no_end"
>
<ElInput v-model="seriesBindingForm.virtual_no_end" placeholder="请输入结束设备号" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="设备号"
>
<ElInput
v-model="seriesBindingForm.virtual_no"
placeholder="请输入设备号"
clearable
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="设备名称"
>
<ElInput
v-model="seriesBindingForm.device_name"
placeholder="请输入设备名称"
clearable
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="设备类型"
>
<ElInput
v-model="seriesBindingForm.device_type"
placeholder="请输入设备类型"
clearable
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="制造商"
>
<ElInput
v-model="seriesBindingForm.manufacturer"
placeholder="请输入制造商"
clearable
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="状态"
>
<ElSelect
v-model="seriesBindingForm.status"
placeholder="请选择状态"
clearable
style="width: 100%"
>
<ElOption label="在库" :value="1" />
<ElOption label="已分销" :value="2" />
<ElOption label="已激活" :value="3" />
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="批次号"
>
<ElSelect
v-model="seriesBindingForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in searchBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="店铺名称"
>
<ElCascader
v-model="seriesBindingForm.shop_id_path"
:options="seriesBindingShopCascadeOptions"
:props="seriesBindingShopCascadeProps"
placeholder="请选择店铺名称"
clearable
filterable
style="width: 100%"
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="当前套餐系列"
>
<ElSelect
v-model="seriesBindingForm.filter_series_id"
placeholder="请选择或搜索当前套餐系列"
clearable
filterable
remote
:remote-method="searchSeries"
style="width: 100%"
>
<ElOption
v-for="series in searchSeriesOptions"
:key="series.id"
:label="series.series_name"
:value="series.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="创建设备时间"
>
<ElDatePicker
v-model="seriesBindingForm.created_at_range"
type="daterange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="目标套餐系列" prop="series_id">
<ElSelect
v-model="seriesBindingForm.series_id"
placeholder="请选择或搜索套餐系列"
@@ -517,6 +677,7 @@
AssetService,
CarrierService
} from '@/api/modules'
import { DeviceSelectionType } from '@/types/api'
import { ElMessage, ElMessageBox, ElTag, ElIcon, ElRadioGroup, ElRadio } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
@@ -527,6 +688,7 @@
DeviceStatus,
AllocateDevicesResponse,
RecallDevicesResponse,
BatchSetDeviceSeriesBindingRequest,
BatchSetDeviceSeriesBindingResponse
} from '@/types/api'
import type { SearchFormItem } from '@/types'
@@ -593,6 +755,38 @@
label: 'label',
children: 'children'
}
const seriesBindingShopCascadeOptions = ref<any[]>([])
const seriesBindingShopCascadeProps = {
lazy: true,
checkStrictly: true,
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({
parent_id: parentId,
exclude_self: true
})
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载批量设置套餐系列店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
const allocateResult = ref<AllocateDevicesResponse | null>(null)
const recallResult = ref<RecallDevicesResponse | null>(null)
@@ -602,11 +796,53 @@
const seriesBindingFormRef = ref<FormInstance>()
const seriesLoading = ref(false)
const packageSeriesList = ref<PackageSeriesResponse[]>([])
const seriesBindingForm = reactive({
series_id: undefined as number | undefined
const createInitialSeriesBindingState = () => ({
selection_type: DeviceSelectionType.LIST,
series_id: undefined as number | undefined,
virtual_no_start: '',
virtual_no_end: '',
virtual_no: '',
device_name: '',
device_type: '',
manufacturer: '',
batch_no: '',
status: undefined as DeviceStatus | undefined,
shop_id_path: undefined as number[] | undefined,
filter_series_id: undefined as number | undefined,
created_at_range: [] as string[],
created_at_start: '',
created_at_end: ''
})
const seriesBindingForm = reactive(createInitialSeriesBindingState())
const seriesBindingRules = reactive<FormRules>({
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }]
selection_type: [{ required: true, message: '请选择选设备方式', trigger: 'change' }],
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
virtual_no_start: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === DeviceSelectionType.RANGE && !value) {
callback(new Error('请输入起始设备号'))
} else {
callback()
}
},
trigger: 'blur'
}
],
virtual_no_end: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === DeviceSelectionType.RANGE && !value) {
callback(new Error('请输入结束设备号'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
@@ -1427,6 +1663,7 @@
.then(async () => {
try {
const res = await DeviceService.batchSetDeviceSeriesBinding({
selection_type: DeviceSelectionType.LIST,
device_ids: [device.id],
series_id: 0
})
@@ -1558,14 +1795,18 @@
// 批量设置套餐系列
const handleBatchSetSeries = async () => {
if (selectedDevices.value.length === 0) {
ElMessage.warning('请先选择要设置的设备')
return
}
seriesBindingForm.series_id = undefined
seriesBindingResult.value = null
await loadPackageSeriesList()
Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
selection_type:
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER
})
await Promise.all([
loadPackageSeriesList(),
loadSearchBatchNoOptions(),
loadSeriesBindingShopCascadeOptions()
])
seriesBindingDialogVisible.value = true
seriesBindingFormRef.value?.clearValidate()
}
// 加载套餐系列列表支持名称搜索默认20条
@@ -1596,6 +1837,66 @@
await loadPackageSeriesList(query || undefined)
}
const loadSeriesBindingShopCascadeOptions = async () => {
try {
const res = await ShopService.getShopsCascade({
parent_id: undefined,
exclude_self: true
})
if (res.code === 0) {
seriesBindingShopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载批量设置套餐系列根级店铺失败:', error)
}
}
const buildSeriesBindingRequest = (): BatchSetDeviceSeriesBindingRequest | null => {
const request: BatchSetDeviceSeriesBindingRequest = {
selection_type: seriesBindingForm.selection_type,
series_id: seriesBindingForm.series_id!
}
if (seriesBindingForm.selection_type === DeviceSelectionType.LIST) {
const deviceIds = selectedDevices.value.map((device) => device.id)
if (deviceIds.length === 0) {
ElMessage.warning('列表模式下请先选择要设置的设备')
return null
}
request.device_ids = deviceIds
return request
}
if (seriesBindingForm.selection_type === DeviceSelectionType.RANGE) {
request.virtual_no_start = seriesBindingForm.virtual_no_start
request.virtual_no_end = seriesBindingForm.virtual_no_end
return request
}
if (seriesBindingForm.virtual_no) request.virtual_no = seriesBindingForm.virtual_no
if (seriesBindingForm.device_name) request.device_name = seriesBindingForm.device_name
if (seriesBindingForm.device_type) request.device_type = seriesBindingForm.device_type
if (seriesBindingForm.manufacturer) request.manufacturer = seriesBindingForm.manufacturer
if (seriesBindingForm.batch_no) request.batch_no = seriesBindingForm.batch_no
if (seriesBindingForm.status !== undefined) request.status = seriesBindingForm.status
if (seriesBindingForm.shop_id_path?.length) {
request.shop_id = seriesBindingForm.shop_id_path[seriesBindingForm.shop_id_path.length - 1]
}
if (seriesBindingForm.filter_series_id !== undefined) {
request.filter_series_id = seriesBindingForm.filter_series_id
}
if (seriesBindingForm.created_at_range.length === 2) {
request.created_at_start = seriesBindingForm.created_at_range[0]
request.created_at_end = seriesBindingForm.created_at_range[1]
}
return request
}
// 确认设置套餐系列绑定
const handleConfirmSeriesBinding = async () => {
if (!seriesBindingFormRef.value) return
@@ -1604,9 +1905,9 @@
if (valid) {
seriesBindingLoading.value = true
try {
const data = {
device_ids: selectedDevices.value.map((d) => d.id),
series_id: seriesBindingForm.series_id!
const data = buildSeriesBindingRequest()
if (!data) {
return
}
const res = await DeviceService.batchSetDeviceSeriesBinding(data)
if (res.code === 0) {
@@ -1634,6 +1935,7 @@
const handleCloseSeriesBindingDialog = () => {
seriesBindingDialogVisible.value = false
seriesBindingResult.value = null
Object.assign(seriesBindingForm, createInitialSeriesBindingState())
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}

View File

@@ -36,7 +36,6 @@
</ElButton>
<ElButton
type="primary"
:disabled="selectedCards.length === 0"
@click="showSeriesBindingDialog"
v-permission="'iot_card:batch_setting'"
>
@@ -351,11 +350,97 @@
ref="seriesBindingFormRef"
:model="seriesBindingForm"
:rules="seriesBindingRules"
label-width="120px"
label-width="100"
>
<ElFormItem label="已选择卡数">
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="seriesBindingForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput v-model="seriesBindingForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput v-model="seriesBindingForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="seriesBindingForm.carrier_id"
placeholder="请选择或搜索运营商"
clearable
filterable
remote
:remote-method="handleSearchAllocateCarrier"
:loading="allocateCarrierLoading"
style="width: 100%"
>
<ElOption
v-for="item in allocateCarrierOptions"
:key="item.id"
:label="item.carrier_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
label="卡状态"
>
<ElSelect
v-model="seriesBindingForm.status"
placeholder="请选择状态"
clearable
style="width: 100%"
>
<ElOption label="在库" :value="1" />
<ElOption label="已分销" :value="2" />
<ElOption label="已激活" :value="3" />
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="
seriesBindingForm.selection_type === CardSelectionType.FILTER &&
userInfo.user_type !== 3
"
label="批次号"
>
<ElSelect
v-model="seriesBindingForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchAllocateBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in allocateBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="套餐系列" prop="series_id">
<ElSelect
v-model="seriesBindingForm.series_id"
@@ -550,6 +635,7 @@
AllocateStandaloneCardsRequest,
RecallStandaloneCardsRequest,
AllocateStandaloneCardsResponse,
BatchSetCardSeriesBindingRequest,
BatchSetCardSeriesBindingResponse
} from '@/types/api/card'
import { CardSelectionType } from '@/types/api/card'
@@ -593,11 +679,45 @@
const seriesBindingFormRef = ref<FormInstance>()
const seriesLoading = ref(false)
const packageSeriesList = ref<PackageSeriesResponse[]>([])
const seriesBindingForm = reactive({
series_id: undefined as number | undefined
const createInitialSeriesBindingState = () => ({
selection_type: CardSelectionType.LIST,
series_id: undefined as number | undefined,
iccid_start: '',
iccid_end: '',
carrier_id: undefined as number | undefined,
status: undefined as StandaloneCardStatus | undefined,
batch_no: ''
})
const seriesBindingForm = reactive(createInitialSeriesBindingState())
const seriesBindingRules = reactive<FormRules>({
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }]
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
iccid_start: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
iccid_end: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
const seriesBindingResult = ref<BatchSetCardSeriesBindingResponse>({
success_count: 0,
@@ -1127,7 +1247,6 @@
{ label: '最后实名检查时间', prop: 'last_real_name_check_at' },
{ label: '启用轮询', prop: 'enable_polling' },
{ label: '批次号', prop: 'batch_no' },
{ label: '供应商', prop: 'supplier' },
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' }
]
@@ -1274,12 +1393,14 @@
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.shop_name || '-'
},
{
prop: 'series_name',
label: '套餐系列',
width: 150,
width: 180,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.series_name || '-'
},
{
@@ -1393,12 +1514,6 @@
width: 200,
showOverflowTooltip: true
},
{
prop: 'supplier',
label: '供应商',
width: 150,
formatter: (row: StandaloneIotCard) => row.supplier || '-'
},
{
prop: 'created_at',
label: '创建时间',
@@ -1707,19 +1822,17 @@
// 显示套餐系列绑定对话框
const showSeriesBindingDialog = async () => {
if (selectedCards.value.length === 0) {
ElMessage.warning('请先选择要设置的卡')
return
}
// 加载套餐系列列表
await loadPackageSeriesList()
Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
selection_type:
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER
})
await Promise.all([
loadPackageSeriesList(),
loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions()
])
seriesBindingDialogVisible.value = true
seriesBindingForm.series_id = undefined
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
seriesBindingFormRef.value?.clearValidate()
}
// 加载套餐系列列表支持名称搜索默认20条
@@ -1752,6 +1865,7 @@
// 关闭套餐系列绑定对话框
const handleSeriesBindingDialogClose = () => {
Object.assign(seriesBindingForm, createInitialSeriesBindingState())
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
@@ -1769,19 +1883,30 @@
await seriesBindingFormRef.value.validate(async (valid) => {
if (valid) {
const iccids = selectedCards.value.map((card) => card.iccid)
if (iccids.length === 0) {
ElMessage.warning('请先选择要设置的卡')
return
}
seriesBindingLoading.value = true
try {
const res = await CardService.batchSetCardSeriesBinding({
iccids,
const request: BatchSetCardSeriesBindingRequest = {
selection_type: seriesBindingForm.selection_type,
series_id: seriesBindingForm.series_id!
})
}
if (seriesBindingForm.selection_type === CardSelectionType.LIST) {
const iccids = selectedCards.value.map((card) => card.iccid)
if (iccids.length === 0) {
ElMessage.warning('列表模式下请先选择要设置的卡')
return
}
request.iccids = iccids
} else if (seriesBindingForm.selection_type === CardSelectionType.RANGE) {
request.iccid_start = seriesBindingForm.iccid_start
request.iccid_end = seriesBindingForm.iccid_end
} else {
if (seriesBindingForm.carrier_id) request.carrier_id = seriesBindingForm.carrier_id
if (seriesBindingForm.status) request.status = seriesBindingForm.status
if (seriesBindingForm.batch_no) request.batch_no = seriesBindingForm.batch_no
}
const res = await CardService.batchSetCardSeriesBinding(request)
if (res.code === 0) {
seriesBindingResult.value = res.data
@@ -1833,6 +1958,7 @@
.then(async () => {
try {
const res = await CardService.batchSetCardSeriesBinding({
selection_type: CardSelectionType.LIST,
iccids: selectedCards.value.map((card) => card.iccid),
series_id: 0
})
@@ -1885,6 +2011,7 @@
.then(async () => {
try {
const res = await CardService.batchSetCardSeriesBinding({
selection_type: CardSelectionType.LIST,
iccids: [iccid],
series_id: 0
})

View File

@@ -126,8 +126,16 @@
if (!data.card_category) return h('span', '-')
return h(
ElTag,
{ type: data.card_category === 'industry' ? 'warning' : 'success', size: 'small' },
() => (data.card_category === 'normal' ? '普通卡' : data.card_category === 'industry' ? '行业卡' : data.card_category)
{
type: data.card_category === 'industry' ? 'warning' : 'success',
size: 'small'
},
() =>
data.card_category === 'normal'
? '普通卡'
: data.card_category === 'industry'
? '行业卡'
: data.card_category
)
}
}

View File

@@ -456,7 +456,8 @@
if (entityName.value || !accountList.value.length) return
const firstAccount = accountList.value[0]
if (firstAccount) {
entityName.value = (firstAccount as any).shop_name || (firstAccount as any).enterprise_name || ''
entityName.value =
(firstAccount as any).shop_name || (firstAccount as any).enterprise_name || ''
}
}

View File

@@ -123,10 +123,7 @@
}
// 获取佣金流程状态文本
const getCommissionStatusText = (
status?: OrderCommissionStatus,
statusName?: string
): string => {
const getCommissionStatusText = (status?: OrderCommissionStatus, statusName?: string): string => {
if (statusName) return statusName
if (status === undefined) return '-'
@@ -139,10 +136,7 @@
}
// 获取佣金业务结果文本
const getCommissionResultText = (
result?: OrderCommissionResult,
resultName?: string
): string => {
const getCommissionResultText = (result?: OrderCommissionResult, resultName?: string): string => {
if (resultName) return resultName
if (result === undefined) return '-'

View File

@@ -463,7 +463,9 @@
if (value > retailPriceUpperLimit.value) {
callback(
new Error(`零售价不能大于建议售价 * 2 = ${formatRetailPriceYuan(retailPriceUpperLimit.value)}`)
new Error(
`零售价不能大于建议售价 * 2 = ${formatRetailPriceYuan(retailPriceUpperLimit.value)}`
)
)
return
}

View File

@@ -263,8 +263,9 @@
placeholder="请输入成本价"
/>
<div v-if="packageForm.original_cost_price" class="form-tip">
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价:
¥{{ packageForm.original_cost_price.toFixed(2) }}
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: ¥{{
packageForm.original_cost_price.toFixed(2)
}}
</div>
</ElFormItem>
</ElForm>

View File

@@ -1992,5 +1992,4 @@
font-weight: 500;
color: var(--el-text-color-primary);
}
</style>

View File

@@ -107,8 +107,9 @@
placeholder="请输入成本价"
/>
<div v-if="packageForm.original_cost_price" class="form-tip">
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价:
¥{{ packageForm.original_cost_price.toFixed(2) }}
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: ¥{{
packageForm.original_cost_price.toFixed(2)
}}
</div>
</ElFormItem>
</ElForm>
@@ -202,7 +203,8 @@
? pkgOption.cost_price / 100
: currentCostPrice,
suggested_retail_price:
pkgOption?.suggested_retail_price !== undefined && pkgOption?.suggested_retail_price !== null
pkgOption?.suggested_retail_price !== undefined &&
pkgOption?.suggested_retail_price !== null
? pkgOption.suggested_retail_price / 100
: undefined
}

View File

@@ -794,7 +794,12 @@
const isMaskedData = (value: string | undefined | null): boolean => {
if (!value) return false
// 检查是否包含脱敏标记:纯星号、已配置标记、或包含连续星号(脱敏数据如Wuha***1218)
return value === '***' || value.includes('[已配置]') || value.includes('[未配置]') || /\*{2,}/.test(value)
return (
value === '***' ||
value.includes('[已配置]') ||
value.includes('[未配置]') ||
/\*{2,}/.test(value)
)
}
// 显示编辑对话框
@@ -906,7 +911,8 @@
description: form.description || undefined
}
// 只有填写了有效的新值才更新敏感字段(不是空值且不是脱敏数据)
if (isValidFieldValue(form.miniapp_app_secret)) data.miniapp_app_secret = form.miniapp_app_secret
if (isValidFieldValue(form.miniapp_app_secret))
data.miniapp_app_secret = form.miniapp_app_secret
if (isValidFieldValue(form.oa_app_secret)) data.oa_app_secret = form.oa_app_secret
if (isValidFieldValue(form.oa_token)) data.oa_token = form.oa_token
if (isValidFieldValue(form.oa_aes_key)) data.oa_aes_key = form.oa_aes_key