fix: 取消关联,居左,店铺列表
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 2m53s

This commit is contained in:
sexygoat
2026-04-17 16:37:41 +08:00
parent 5adeade9af
commit 90b13eb5ec
3 changed files with 167 additions and 108 deletions

View File

@@ -77,14 +77,13 @@
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span>
</ElFormItem>
<ElFormItem label="店铺" prop="target_shop_id">
<ElTreeSelect
<ElCascader
v-model="allocateForm.target_shop_id"
:data="shopTreeData"
:props="{ label: 'shop_name', value: 'id', children: 'children' }"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择目标店铺"
clearable
filterable
check-strictly
style="width: 100%"
/>
</ElFormItem>
@@ -222,7 +221,6 @@
:loading="seriesLoading"
clearable
>
<ElOption label="清除关联" :value="0" />
<ElOption
v-for="series in packageSeriesList"
:key="series.id"
@@ -572,15 +570,7 @@
PackageSeriesService,
AssetService
} from '@/api/modules'
import {
ElMessage,
ElMessageBox,
ElTag,
ElIcon,
ElTreeSelect,
ElRadioGroup,
ElRadio
} from 'element-plus'
import { ElMessage, ElMessageBox, ElTag, ElIcon, ElRadioGroup, ElRadio } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import type {
@@ -610,7 +600,35 @@
const allocateDialogVisible = ref(false)
const recallDialogVisible = ref(false)
const selectedDevices = ref<Device[]>([])
const shopTreeData = ref<any[]>([])
const shopCascadeOptions = ref<any[]>([])
const shopCascadeProps = {
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 })
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)
@@ -1146,7 +1164,6 @@
onMounted(() => {
getTableData()
loadShopTree()
})
// 当页面被 keep-alive 激活时自动刷新数据
@@ -1154,49 +1171,6 @@
getTableData()
})
// 加载店铺树形数据
const loadShopTree = async () => {
try {
const res = await ShopService.getShops({
page: 1,
page_size: 9999, // 获取所有数据用于构建树形结构
status: 1 // 只获取启用的店铺
})
if (res.code === 0) {
shopTreeData.value = buildShopTree(res.data.items || [])
}
} catch (error) {
console.error('获取店铺列表失败:', error)
}
}
// 构建店铺树形结构
const buildShopTree = (shops: any[]) => {
const map = new Map<number, any>()
const tree: any[] = []
// 先将所有项放入 map
shops.forEach((shop) => {
map.set(shop.id, { ...shop, children: [] })
})
// 构建树形结构
shops.forEach((shop) => {
const node = map.get(shop.id)!
if (shop.parent_id && map.has(shop.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(shop.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
// 获取设备列表
const getTableData = async () => {
loading.value = true
@@ -1278,7 +1252,43 @@
}
})
.catch(() => {
// 用户取消删除
// 用户取消
})
}
// 清除设备套餐系列绑定
const handleClearDeviceSeries = async (deviceNo: string) => {
const device = deviceList.value.find((d) => d.virtual_no === deviceNo)
if (!device) {
ElMessage.error('未找到该设备')
return
}
ElMessageBox.confirm(`确定要清除设备 ${deviceNo} 的套餐系列关联吗?`, '确认清除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const res = await DeviceService.batchSetDeviceSeriesBinding({
device_ids: [device.id],
series_id: 0
})
if (res.code === 0) {
if (res.data.fail_count === 0) {
ElMessage.success('清除关联成功')
} else {
ElMessage.warning(`部分失败:${res.data.failed_items?.[0]?.reason || '未知原因'}`)
}
await getTableData()
}
} catch (error) {
console.error('清除关联失败:', error)
}
})
.catch(() => {
// 用户取消
})
}
@@ -1534,6 +1544,9 @@
case 'delete':
await handleDeleteDeviceByNo(deviceNo)
break
case 'clear-series':
await handleClearDeviceSeries(deviceNo)
break
}
}
@@ -1794,6 +1807,14 @@
})
}
if (hasAuth('device:clear_series')) {
moreActions.push({
label: '清除关联',
handler: () => handleDeviceOperation('clear-series', row.virtual_no, row),
type: 'primary'
})
}
// if (hasAuth('device:manual_deactivate')) {
// moreActions.push({
// label: '手动停用',

View File

@@ -94,24 +94,15 @@
label-width="120px"
>
<ElFormItem label="店铺" prop="to_shop_id">
<ElSelect
<ElCascader
v-model="allocateForm.to_shop_id"
placeholder="请选择或搜索目标店铺"
filterable
remote
reserve-keyword
:remote-method="searchTargetShops"
:loading="targetShopLoading"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择目标店铺"
clearable
filterable
style="width: 100%"
>
<ElOption
v-for="shop in targetShopList"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
/>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="allocateForm.selection_type">
@@ -342,7 +333,6 @@
:loading="seriesLoading"
clearable
>
<ElOption label="清除关联" :value="0" />
<ElOption
v-for="series in packageSeriesList"
:key="series.id"
@@ -650,8 +640,35 @@
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
// 店铺相关
const targetShopLoading = ref(false)
const targetShopList = ref<any[]>([])
const shopCascadeOptions = ref<any[]>([])
const shopCascadeProps = {
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 })
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 initialSearchState: {
@@ -1213,33 +1230,6 @@
selectedCards.value = selection
}
// 加载目标店铺列表
const loadTargetShops = async (shopName?: string) => {
targetShopLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
targetShopList.value = res.data.items || []
}
} catch (error) {
console.error('获取目标店铺列表失败:', error)
} finally {
targetShopLoading.value = false
}
}
// 搜索目标店铺
const searchTargetShops = async (query: string) => {
await loadTargetShops(query || undefined)
}
// 显示批量分配对话框
const showAllocateDialog = () => {
if (selectedCards.value.length === 0) {
@@ -1258,8 +1248,6 @@
batch_no: '',
remark: ''
})
// 加载默认店铺列表
loadTargetShops()
if (allocateFormRef.value) {
allocateFormRef.value.resetFields()
}
@@ -1536,6 +1524,13 @@
})
}
if (hasAuth('iot_card:clear_series')) {
items.push({
key: 'clearSeries',
label: '清除关联'
})
}
return items
})
@@ -1565,9 +1560,52 @@
case 'changePackage':
changePackage()
break
case 'clearSeries':
handleClearCardSeries()
break
}
}
// 清除卡套餐系列绑定
const handleClearCardSeries = async () => {
if (selectedCards.value.length === 0) {
ElMessage.warning('请先选择要清除关联的卡')
return
}
ElMessageBox.confirm(
`确定要清除所选 ${selectedCards.value.length} 张卡的套餐系列关联吗?`,
'确认清除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
try {
const res = await CardService.batchSetCardSeriesBinding({
iccids: selectedCards.value.map((card) => card.iccid),
series_id: 0
})
if (res.code === 0) {
if (res.data.fail_count === 0) {
ElMessage.success('清除关联成功')
} else {
ElMessage.warning(`部分失败:${res.data.failed_items?.[0]?.reason || '未知原因'}`)
}
selectedCards.value = []
await getTableData()
}
} catch (error) {
console.error('清除关联失败:', error)
}
})
.catch(() => {
// 用户取消
})
}
// 网卡分销 - 正在开发中
const cardDistribution = () => {
ElMessage.info('功能正在开发中')

View File

@@ -51,7 +51,7 @@
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="165px">
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElRow :gutter="20">
<ElCol :span="24">
<ElFormItem label="套餐编码" prop="package_code">