feat:更换企业授权
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m41s

This commit is contained in:
luo
2026-06-22 10:36:33 +08:00
parent 866a2587b3
commit d3b5f55c64
34 changed files with 2478 additions and 3335 deletions

View File

@@ -5,7 +5,6 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
label-width="120"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -41,6 +40,22 @@
>
批量设置套餐系列
</ElButton>
<ElButton
type="primary"
:disabled="!selectedDevices.length"
@click="showEnterpriseDeviceAuthorizeDialog"
v-permission="'devices:authorize_enterprise'"
>
授权设备给企业
</ElButton>
<ElButton
type="warning"
:disabled="!selectedDevices.length"
@click="showEnterpriseDeviceRecallDialog"
v-permission="'devices:recall_enterprise_authorization'"
>
撤销设备授权
</ElButton>
<ElButton type="primary" @click="showExportDialog" v-permission="'devices:export'">
导出
</ElButton>
@@ -677,6 +692,250 @@
:identifier="operationLogsIdentifier"
download-permission="device:download_log_file"
/>
<ElDialog
v-model="enterpriseDeviceAuthorizeDialogVisible"
title="授权设备给企业"
width="620px"
@close="handleEnterpriseDeviceAuthorizeDialogClose"
>
<ElForm
ref="enterpriseDeviceAuthorizeFormRef"
:model="enterpriseDeviceAuthorizeForm"
:rules="enterpriseDeviceAuthorizeRules"
label-width="120px"
>
<ElFormItem label="授权企业" prop="enterprise_id">
<ElSelect
v-model="enterpriseDeviceAuthorizeForm.enterprise_id"
placeholder="请选择或搜索企业"
clearable
filterable
remote
reserve-keyword
:remote-method="handleSearchEnterprise"
:loading="enterpriseLoading"
style="width: 100%"
>
<ElOption
v-for="item in enterpriseOptions"
:key="item.id"
:label="item.enterprise_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选设备方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseDeviceAuthorizeForm.selection_type">
<ElRadio label="list">设备号列表</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="enterpriseDeviceAuthorizeForm.selection_type === DeviceSelectionType.LIST"
label="设备号列表"
>
<div>已选择 {{ selectedDevices.length }} 台设备</div>
</ElFormItem>
<template
v-if="enterpriseDeviceAuthorizeForm.selection_type === DeviceSelectionType.FILTER"
>
<ElFormItem label="设备号">
<ElInput
v-model="enterpriseDeviceAuthorizeForm.virtual_no"
placeholder="请输入设备号"
clearable
/>
</ElFormItem>
<ElFormItem label="批次号">
<ElSelect
v-model="enterpriseDeviceAuthorizeForm.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 label="店铺名称">
<ElCascader
v-model="enterpriseDeviceAuthorizeForm.shop_id_path"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择店铺名称"
clearable
filterable
style="width: 100%"
/>
</ElFormItem>
</template>
<ElFormItem label="备注">
<ElInput
v-model="enterpriseDeviceAuthorizeForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="enterpriseDeviceAuthorizeDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleEnterpriseDeviceAuthorize"
:loading="enterpriseDeviceAuthorizeLoading"
>
确认授权
</ElButton>
</div>
</template>
</ElDialog>
<ElDialog
v-model="enterpriseDeviceRecallDialogVisible"
title="撤销设备授权"
width="620px"
@close="handleEnterpriseDeviceRecallDialogClose"
>
<ElForm
ref="enterpriseDeviceRecallFormRef"
:model="enterpriseDeviceRecallForm"
:rules="enterpriseDeviceRecallRules"
label-width="120px"
>
<ElFormItem label="授权企业" prop="enterprise_id">
<ElSelect
v-model="enterpriseDeviceRecallForm.enterprise_id"
placeholder="请选择或搜索企业"
clearable
filterable
remote
reserve-keyword
:remote-method="handleSearchEnterprise"
:loading="enterpriseLoading"
style="width: 100%"
>
<ElOption
v-for="item in enterpriseOptions"
:key="item.id"
:label="item.enterprise_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选设备方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseDeviceRecallForm.selection_type">
<ElRadio label="list">设备号列表</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="enterpriseDeviceRecallForm.selection_type === DeviceSelectionType.LIST"
label="设备号列表"
>
<div>已选择 {{ selectedDevices.length }} 台设备</div>
</ElFormItem>
<template
v-if="enterpriseDeviceRecallForm.selection_type === DeviceSelectionType.FILTER"
>
<ElFormItem label="设备号">
<ElInput
v-model="enterpriseDeviceRecallForm.virtual_no"
placeholder="请输入设备号"
clearable
/>
</ElFormItem>
<ElFormItem label="批次号">
<ElSelect
v-model="enterpriseDeviceRecallForm.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>
</template>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="enterpriseDeviceRecallDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleEnterpriseDeviceRecall"
:loading="enterpriseDeviceRecallLoading"
>
确认撤销
</ElButton>
</div>
</template>
</ElDialog>
<ElDialog
v-model="enterpriseDeviceResultDialogVisible"
:title="enterpriseDeviceResultTitle"
width="700px"
>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ enterpriseDeviceOperationResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<ElTag type="danger">{{ enterpriseDeviceOperationResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<div
v-if="
enterpriseDeviceOperationResult.failed_items &&
enterpriseDeviceOperationResult.failed_items.length > 0
"
style="margin-top: 20px"
>
<ElDivider content-position="left">失败项详情</ElDivider>
<ElTable :data="enterpriseDeviceOperationResult.failed_items" border max-height="300">
<ElTableColumn prop="virtual_no" label="设备号" width="180" />
<ElTableColumn prop="reason" label="失败原因" />
</ElTable>
</div>
<div
v-if="
enterpriseDeviceOperationResult.authorized_devices &&
enterpriseDeviceOperationResult.authorized_devices.length > 0
"
style="margin-top: 20px"
>
<ElDivider content-position="left">已授权设备</ElDivider>
<ElTable
:data="enterpriseDeviceOperationResult.authorized_devices"
border
max-height="220"
>
<ElTableColumn prop="virtual_no" label="设备号" width="180" />
<ElTableColumn prop="card_count" label="绑定卡数" />
</ElTable>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="enterpriseDeviceResultDialogVisible = false"
>确定</ElButton
>
</div>
</template>
</ElDialog>
<ExportTaskCreateDialog
v-model="exportDialogVisible"
scene="device"
@@ -699,7 +958,7 @@
CardService,
PackageSeriesService,
AssetService,
CarrierService
EnterpriseService
} from '@/api/modules'
import { DeviceSelectionType } from '@/types/api'
import { ElMessage, ElMessageBox, ElTag, ElIcon, ElRadioGroup, ElRadio } from 'element-plus'
@@ -720,6 +979,11 @@
import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format'
import type { PackageSeriesResponse } from '@/types/api'
import type { EnterpriseItem } from '@/types/api/enterprise'
import type {
EnterpriseAllocateDevicesResponse,
EnterpriseRecallDevicesResponse
} from '@/types/api/enterpriseDevice'
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
@@ -781,6 +1045,20 @@
label: 'label',
children: 'children'
}
const loadTopLevelShopCascadeOptions = async () => {
try {
const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) {
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载顶级店铺失败:', error)
}
}
const seriesBindingShopCascadeOptions = ref<any[]>([])
const seriesBindingShopCascadeProps = {
lazy: true,
@@ -815,6 +1093,27 @@
}
const allocateResult = ref<AllocateDevicesResponse | null>(null)
const recallResult = ref<RecallDevicesResponse | null>(null)
const enterpriseDeviceAuthorizeDialogVisible = ref(false)
const enterpriseDeviceRecallDialogVisible = ref(false)
const enterpriseDeviceResultDialogVisible = ref(false)
const enterpriseDeviceAuthorizeLoading = ref(false)
const enterpriseDeviceRecallLoading = ref(false)
const enterpriseDeviceResultTitle = ref('')
const enterpriseDeviceAuthorizeFormRef = ref<FormInstance>()
const enterpriseDeviceRecallFormRef = ref<FormInstance>()
const enterpriseOptions = ref<EnterpriseItem[]>([])
const enterpriseLoading = ref(false)
const enterpriseDeviceOperationResult = ref<
Partial<EnterpriseAllocateDevicesResponse & EnterpriseRecallDevicesResponse> & {
success_count: number
fail_count: number
}
>({
success_count: 0,
fail_count: 0,
failed_items: null,
authorized_devices: null
})
// 套餐系列绑定相关
const seriesBindingDialogVisible = ref(false)
@@ -840,6 +1139,22 @@
created_at_end: ''
})
const seriesBindingForm = reactive(createInitialSeriesBindingState())
const createInitialEnterpriseDeviceAuthorizeState = () => ({
enterprise_id: undefined as number | undefined,
selection_type: DeviceSelectionType.LIST,
virtual_no: '',
batch_no: '',
shop_id_path: undefined as number[] | undefined,
remark: ''
})
const createInitialEnterpriseDeviceRecallState = () => ({
enterprise_id: undefined as number | undefined,
selection_type: DeviceSelectionType.LIST,
virtual_no: '',
batch_no: ''
})
const enterpriseDeviceAuthorizeForm = reactive(createInitialEnterpriseDeviceAuthorizeState())
const enterpriseDeviceRecallForm = reactive(createInitialEnterpriseDeviceRecallState())
const parseNumericStringToBigInt = (value: string): bigint | null => {
const normalizedValue = value.trim()
if (!normalizedValue || !/^\d+$/.test(normalizedValue)) {
@@ -931,6 +1246,13 @@
}
]
})
const createEnterpriseDeviceRules = () =>
reactive<FormRules>({
enterprise_id: [{ required: true, message: '请选择企业', trigger: 'change' }],
selection_type: [{ required: true, message: '请选择选设备方式', trigger: 'change' }]
})
const enterpriseDeviceAuthorizeRules = createEnterpriseDeviceRules()
const enterpriseDeviceRecallRules = createEnterpriseDeviceRules()
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
// 搜索表单店铺/系列选项
@@ -999,13 +1321,15 @@
shop_id: undefined as number | undefined,
series_id: undefined as number | undefined,
has_active_package: undefined as boolean | undefined,
authorized_enterprise_id: undefined as number | undefined,
is_authorized_to_enterprise: undefined as boolean | undefined,
dateRange: [] as string[],
created_at_start: '',
created_at_end: ''
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
const searchForm = reactive<Record<string, any>>({ ...initialSearchState })
// 搜索批次号(用于搜索表单)
const loadSearchBatchNoOptions = async (batchNo?: string) => {
@@ -1174,7 +1498,7 @@
}))
},
{
label: '起始至结束',
label: '起止时间',
prop: 'dateRange',
type: 'date',
config: {
@@ -1189,7 +1513,7 @@
// 分页
searchFormItems.splice(searchFormItems.length - 1, 0, {
label: '是否有生效套餐',
label: '效套餐',
prop: 'has_active_package',
type: 'select',
config: {
@@ -1201,6 +1525,37 @@
{ label: '否', value: false }
]
})
searchFormItems.splice(searchFormItems.length - 1, 0, {
label: '授权企业',
prop: 'authorized_enterprise_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => handleSearchEnterprise(query),
loading: enterpriseLoading.value,
placeholder: '请选择或搜索企业'
},
options: () =>
enterpriseOptions.value.map((enterprise) => ({
label: enterprise.enterprise_name,
value: enterprise.id
}))
})
searchFormItems.splice(searchFormItems.length - 1, 0, {
label: '授权状态',
prop: 'is_authorized_to_enterprise',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '已授权', value: true },
{ label: '未授权', value: false }
]
})
const pagination = reactive({
page: 1,
@@ -1704,6 +2059,26 @@
}
}
const handleSearchEnterprise = async (query?: string) => {
try {
enterpriseLoading.value = true
const params = {
page: 1,
page_size: 20,
enterprise_name: query || undefined,
status: 1
}
const res = await EnterpriseService.getEnterprises(params)
if (res.code === 0) {
enterpriseOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索企业失败:', error)
} finally {
enterpriseLoading.value = false
}
}
let isFirstActivation = true
onMounted(() => {
getTableData()
@@ -1735,6 +2110,8 @@
shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined,
has_active_package: searchForm.has_active_package ?? undefined,
authorized_enterprise_id: searchForm.authorized_enterprise_id || undefined,
is_authorized_to_enterprise: searchForm.is_authorized_to_enterprise ?? undefined,
created_at_start: searchForm.created_at_start || undefined,
created_at_end: searchForm.created_at_end || undefined
}
@@ -1763,6 +2140,8 @@
shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined,
has_active_package: searchForm.has_active_package ?? undefined,
authorized_enterprise_id: searchForm.authorized_enterprise_id || undefined,
is_authorized_to_enterprise: searchForm.is_authorized_to_enterprise ?? undefined,
created_at_start: searchForm.created_at_start || undefined,
created_at_end: searchForm.created_at_end || undefined
}
@@ -1993,6 +2372,160 @@
recallForm.remark = ''
}
const getInferredDeviceEnterpriseId = () => {
const filterEnterpriseId = searchForm.authorized_enterprise_id
if (filterEnterpriseId) return filterEnterpriseId
const selectedEnterpriseIds = Array.from(
new Set(
selectedDevices.value
.map((device) => device.authorized_enterprise_id)
.filter((id): id is number => typeof id === 'number')
)
)
return selectedEnterpriseIds.length === 1 ? selectedEnterpriseIds[0] : undefined
}
const resetEnterpriseDeviceAuthorizeForm = () => {
Object.assign(enterpriseDeviceAuthorizeForm, createInitialEnterpriseDeviceAuthorizeState(), {
selection_type:
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER,
virtual_no: searchForm.virtual_no || '',
batch_no: searchForm.batch_no || '',
shop_id_path: undefined
})
}
const resetEnterpriseDeviceRecallForm = () => {
Object.assign(enterpriseDeviceRecallForm, createInitialEnterpriseDeviceRecallState(), {
enterprise_id: getInferredDeviceEnterpriseId(),
selection_type:
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER,
virtual_no: searchForm.virtual_no || '',
batch_no: searchForm.batch_no || ''
})
}
const showEnterpriseDeviceAuthorizeDialog = async () => {
resetEnterpriseDeviceAuthorizeForm()
await Promise.all([
handleSearchEnterprise(),
loadSearchBatchNoOptions(),
loadTopLevelShopCascadeOptions()
])
enterpriseDeviceAuthorizeDialogVisible.value = true
enterpriseDeviceAuthorizeFormRef.value?.clearValidate()
}
const showEnterpriseDeviceRecallDialog = async () => {
resetEnterpriseDeviceRecallForm()
await Promise.all([handleSearchEnterprise(), loadSearchBatchNoOptions()])
enterpriseDeviceRecallDialogVisible.value = true
enterpriseDeviceRecallFormRef.value?.clearValidate()
}
const handleEnterpriseDeviceAuthorizeDialogClose = () => {
Object.assign(enterpriseDeviceAuthorizeForm, createInitialEnterpriseDeviceAuthorizeState())
enterpriseDeviceAuthorizeFormRef.value?.resetFields()
}
const handleEnterpriseDeviceRecallDialogClose = () => {
Object.assign(enterpriseDeviceRecallForm, createInitialEnterpriseDeviceRecallState())
enterpriseDeviceRecallFormRef.value?.resetFields()
}
const buildEnterpriseDeviceParams = (form: any) => {
const params: Record<string, unknown> = {
selection_type: form.selection_type,
remark: form.remark || undefined
}
if (form.selection_type === DeviceSelectionType.LIST) {
const deviceNos = selectedDevices.value.map((device) => device.virtual_no).filter(Boolean)
if (!deviceNos.length) {
ElMessage.warning('列表模式下请先选择设备')
return null
}
params.device_nos = deviceNos
} else {
if (form.batch_no) params.batch_no = form.batch_no
if (Array.isArray(form.shop_id_path) && form.shop_id_path.length > 0) {
params.shop_id = form.shop_id_path[form.shop_id_path.length - 1]
}
if (form.virtual_no) params.virtual_no = form.virtual_no
}
Object.keys(params).forEach((key) => {
if (params[key] === undefined || params[key] === '') delete params[key]
})
return params
}
const applyEnterpriseDeviceOperationSuccess = async (
title: string,
result: EnterpriseAllocateDevicesResponse | EnterpriseRecallDevicesResponse
) => {
enterpriseDeviceOperationResult.value = result
enterpriseDeviceResultTitle.value = title
enterpriseDeviceResultDialogVisible.value = true
selectedDevices.value = []
await getTableData()
}
const handleEnterpriseDeviceAuthorize = async () => {
if (!enterpriseDeviceAuthorizeFormRef.value) return
await enterpriseDeviceAuthorizeFormRef.value.validate(async (valid) => {
if (!valid) return
const params = buildEnterpriseDeviceParams(enterpriseDeviceAuthorizeForm)
if (!params) return
enterpriseDeviceAuthorizeLoading.value = true
try {
const res = await EnterpriseService.allocateDevices(
enterpriseDeviceAuthorizeForm.enterprise_id!,
params as any
)
if (res.code === 0) {
enterpriseDeviceAuthorizeDialogVisible.value = false
await applyEnterpriseDeviceOperationSuccess('授权设备给企业结果', res.data)
}
} catch (error) {
console.error('授权设备给企业失败:', error)
} finally {
enterpriseDeviceAuthorizeLoading.value = false
}
})
}
const handleEnterpriseDeviceRecall = async () => {
if (!enterpriseDeviceRecallFormRef.value) return
await enterpriseDeviceRecallFormRef.value.validate(async (valid) => {
if (!valid) return
const params = buildEnterpriseDeviceParams(enterpriseDeviceRecallForm)
if (!params) return
enterpriseDeviceRecallLoading.value = true
try {
const res = await EnterpriseService.recallDevices(
enterpriseDeviceRecallForm.enterprise_id!,
params as any
)
if (res.code === 0) {
enterpriseDeviceRecallDialogVisible.value = false
await applyEnterpriseDeviceOperationSuccess('撤销设备授权结果', res.data)
}
} catch (error) {
console.error('撤销设备授权失败:', error)
} finally {
enterpriseDeviceRecallLoading.value = false
}
})
}
// 批量设置套餐系列
const handleBatchSetSeries = async () => {
seriesBindingResult.value = null