fix: 代理系列授权和操作日志去掉
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m58s

This commit is contained in:
luo
2026-08-10 17:27:15 +08:00
parent 8650b490d7
commit 9599a4d52b
17 changed files with 207 additions and 883 deletions

View File

@@ -1,692 +0,0 @@
<template>
<ElCard shadow="never" class="info-card operation-logs-card" v-loading="loading">
<template #header>
<div class="card-header">
<span class="header-title">操作审计日志</span>
<div class="filter-section">
<ElSelect
v-model="filterForm.result_status"
placeholder="执行结果"
clearable
class="result-status-select"
@change="handleFilterChange"
>
<ElOption label="成功" value="success" />
<ElOption label="失败" value="failed" />
<ElOption label="拒绝" value="denied" />
</ElSelect>
<ElButton type="primary" @click="handleQuery">查询</ElButton>
<ElButton @click="handleReset">重置</ElButton>
<ElTag type="info" size="small"> {{ total }} </ElTag>
</div>
</div>
</template>
<div class="logs-table-wrapper">
<ElTable :data="logList" class="logs-table" border max-height="400">
<ElTableColumn label="操作时间" width="170" align="center">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</ElTableColumn>
<ElTableColumn label="操作类型" width="160" align="center">
<template #default="{ row }">
<ElTag :type="getOperationTypeTag(row.operation_type)" size="small">
{{ row.operation_desc }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="操作人" width="130" align="center">
<template #default="{ row }">
{{ row.operator_name || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="操作人类型" width="110" align="center">
<template #default="{ row }">
{{ getOperatorTypeText(row) }}
</template>
</ElTableColumn>
<ElTableColumn label="执行结果" width="100" align="center">
<template #default="{ row }">
<ElTag :type="getResultStatusTag(row.result_status)" size="small">
{{ getResultStatusText(row.result_status) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="变更内容" min-width="320">
<template #default="{ row }">
<div v-if="getDisplayContent(row).length" class="change-content">
<span
v-for="(item, index) in getDisplayContent(row)"
:key="`${item.type}-${item.key}-${index}`"
class="change-item"
>
<span class="field-name">{{ item.fieldName }}</span>
<span class="field-value">
<template v-if="item.type === 'change'">
<span class="new-value">{{ formatFieldValue(item.key, item.afterValue) }}</span>
<ElButton
v-if="canDownloadLogFile && isDownloadableFileKey(item.key, item.afterValue)"
link
type="primary"
@click.stop="handleDownloadLogFile(item.afterValue)"
>
下载文件
</ElButton>
</template>
<span v-else :class="['message-value', item.level === 'error' ? 'is-error' : '']">
{{ item.message }}
</span>
</span>
</span>
</div>
<span v-else>-</span>
</template>
</ElTableColumn>
</ElTable>
<ElPagination
class="logs-pagination"
v-if="total > 0"
v-model:current-page="pagination.page"
v-model:page-size="pagination.page_size"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, prev, pager, next"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</ElCard>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import {
ElButton,
ElCard,
ElMessage,
ElOption,
ElPagination,
ElSelect,
ElTable,
ElTableColumn,
ElTag
} from 'element-plus'
import { AssetService, StorageService } from '@/api/modules'
import { useAuth } from '@/composables/useAuth'
import type { AssetOperationLogItem } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
interface Props {
assetIdentifier?: string
assetType?: string
downloadPermission?: string
}
type TagType = 'primary' | 'success' | 'warning' | 'info' | 'danger'
type ResultStatus = '' | 'success' | 'failed' | 'denied'
type OperationLogRow = AssetOperationLogItem & {
operator_type_code?: string
error_code?: string
error_msg?: string
request_id?: string
request_path?: string
request_method?: string
ip_address?: string
user_agent?: string
}
interface ChangeItem {
type: 'change'
key: string
fieldName: string
beforeValue?: any
afterValue?: any
}
interface MessageItem {
type: 'message'
key: string
fieldName: string
message: string
level: 'info' | 'error'
}
type DisplayItem = ChangeItem | MessageItem
const operationTypeTagMap: Record<string, TagType> = {
device_set_wifi: 'primary',
device_switch_card: 'success',
device_switch_mode: 'success',
device_stop: 'danger',
device_start: 'success',
device_reboot: 'warning',
device_reset: 'warning',
device_allocate: 'primary',
device_recall: 'warning',
device_bind_card: 'primary',
device_unbind_card: 'warning',
card_allocate: 'primary',
card_recall: 'warning',
card_stop: 'danger',
card_start: 'success',
card_manual_stop: 'danger',
card_manual_start: 'success',
card_auto_stop: 'danger',
card_auto_start: 'success',
card_polling_status: 'info',
asset_realname_policy: 'info',
asset_realname_status: 'info',
asset_polling: 'info',
asset_polling_status: 'info',
iot_card_import_task_create: 'primary'
}
const operatorTypeMap: Record<string, string> = {
system: '系统',
admin_user: '平台用户',
agent_user: '代理账号',
enterprise_user: '企业用户',
api_user: 'API用户'
}
const fieldNameMap: Record<string, string> = {
asset_type: '资产类型',
batch_no: '批次号',
card_category: '卡类型',
device_imei: '设备IMEI',
device_sn: '设备SN',
device_virtual_no: '设备虚拟号',
device_virtual_nos: '设备虚拟号',
enable_polling: '轮询开关',
file_key: '文件存储键',
first_realname_at: '首次实名时间',
iccid: 'ICCID',
iccids: 'ICCID',
network_status: '网络状态',
real_name_status: '实名状态',
realname_policy: '实名认证策略',
shop_name: '店铺',
source_service: '来源服务',
source_shop_name: '来源店铺',
status: '资产状态',
stop_reason: '停机原因',
switch_mode: '切卡模式',
target_shop_name: '目标店铺',
to_shop_name: '目标店铺'
}
const realnamePolicyMap: Record<string, string> = {
none: '无需实名',
before_order: '先实名后充值/购买',
after_order: '先充值/购买后实名'
}
const stopReasonMap: Record<string, string> = {
'': '无',
no_package: '无可用套餐',
carrier_stopped: '运营商停机',
not_realname: '未实名'
}
const assetTypeMap: Record<string, string> = {
iot_card: 'IoT卡',
device: '设备'
}
const cardCategoryMap: Record<string, string> = {
normal: '普通卡',
industry: '行业卡'
}
const assetStatusMap: Record<string, string> = {
'1': '在库',
'2': '已分销',
'3': '已激活',
'4': '已停用'
}
const newStatusMap: Record<string, string> = {
allocated: '已分配',
recalled: '已回收'
}
const props = defineProps<Props>()
const { hasAuth } = useAuth()
const loading = ref(false)
const logList = ref<OperationLogRow[]>([])
const total = ref(0)
const canDownloadLogFile = computed(() => {
return props.downloadPermission ? hasAuth(props.downloadPermission) : false
})
const pagination = reactive({
page: 1,
page_size: 20
})
const filterForm = reactive({
result_status: '' as ResultStatus
})
const loadLogs = async () => {
if (!props.assetIdentifier) return
try {
loading.value = true
const res = await AssetService.getOperationLogs(props.assetIdentifier, {
page: pagination.page,
page_size: pagination.page_size,
result_status: filterForm.result_status || undefined
})
if (res.code === 0 && res.data) {
logList.value = res.data.items || []
total.value = res.data.total || 0
}
} catch (error) {
console.error('加载操作日志失败:', error)
ElMessage.error('加载操作日志失败')
} finally {
loading.value = false
}
}
const handleQuery = () => {
pagination.page = 1
loadLogs()
}
const handleReset = () => {
filterForm.result_status = ''
pagination.page = 1
loadLogs()
}
const handleFilterChange = () => {
handleQuery()
}
const handlePageChange = () => {
loadLogs()
}
const handleSizeChange = () => {
pagination.page = 1
loadLogs()
}
const getOperationTypeTag = (type: string): TagType => {
return operationTypeTagMap[type] || 'info'
}
const getResultStatusTag = (status: string): TagType => {
const tagMap: Record<string, TagType> = {
success: 'success',
failed: 'danger',
denied: 'warning'
}
return tagMap[status] || 'info'
}
const getResultStatusText = (status: string) => {
const textMap: Record<string, string> = {
success: '成功',
failed: '失败',
denied: '拒绝'
}
return textMap[status] || status
}
const isDownloadableFileKey = (key: string, value: unknown) => {
return key === 'file_key' && typeof value === 'string' && value.trim() !== ''
}
const handleDownloadLogFile = async (fileKey: string) => {
try {
await StorageService.downloadFileByKey(fileKey)
ElMessage.success('文件下载已开始')
} catch (error) {
console.error('下载日志文件失败:', error)
ElMessage.error('下载文件失败')
}
}
const getOperatorTypeText = (row: OperationLogRow) => {
if (row.operator_type_code && operatorTypeMap[row.operator_type_code]) {
return operatorTypeMap[row.operator_type_code]
}
if (row.operator_type && operatorTypeMap[row.operator_type]) {
return operatorTypeMap[row.operator_type]
}
return row.operator_type || row.operator_type_code || '-'
}
const formatValue = (value: any): string => {
if (value === null || value === undefined) return '空'
if (typeof value === 'boolean') return value ? '是' : '否'
if (typeof value === 'number') return String(value)
if (typeof value === 'string') return value || '空'
if (Array.isArray(value)) {
if (value.length === 0) return '空'
if (value.every((item) => ['string', 'number', 'boolean'].includes(typeof item))) {
return value.map((item) => formatValue(item)).join('、')
}
return JSON.stringify(value)
}
if (typeof value === 'object') return JSON.stringify(value)
return String(value)
}
const formatFieldValue = (key: string, value: any): string => {
if (typeof value === 'string' && (key.endsWith('_at') || key.endsWith('_time'))) {
return formatDateTime(value)
}
if (key === 'enable_polling') return value ? '开启' : '关闭'
if (key === 'realname_policy') return realnamePolicyMap[value] || formatValue(value)
if (key === 'real_name_status') return value === 1 ? '已实名' : '未实名'
if (key === 'switch_mode') {
return String(value) === '0' ? '自动' : String(value) === '1' ? '手动' : formatValue(value)
}
if (key === 'new_status') return newStatusMap[value] || formatValue(value)
if (key === 'network_status') {
return Number(value) === 1 ? '正常' : Number(value) === 0 ? '停机' : formatValue(value)
}
if (key === 'stop_reason') return stopReasonMap[String(value ?? '')] ?? formatValue(value)
if (key === 'asset_type') return assetTypeMap[String(value)] || formatValue(value)
if (key === 'card_category') return cardCategoryMap[String(value)] || formatValue(value)
if (key === 'status') return assetStatusMap[String(value)] || formatValue(value)
if (key === 'source_service') return value === 'asset_polling' ? '资产轮询' : formatValue(value)
return formatValue(value)
}
const sanitizeFieldName = (fieldName: string): string => {
return fieldName
.trim()
.replace(/[^]*/g, '')
.trim()
}
const shouldHideIdField = (key: string, fieldName: string): boolean => {
const normalizedKey = key.trim()
const normalizedKeyLower = normalizedKey.toLowerCase()
const normalizedFieldName = sanitizeFieldName(fieldName)
const normalizedFieldNameUpper = normalizedFieldName.toUpperCase()
const visibleIdentifierKeys = new Set(['iccid', 'msisdn', 'imei', 'imsi', 'sn'])
const visibleIdentifierNames = ['ICCID', 'MSISDN', 'IMEI', 'IMSI', 'SN']
if (
visibleIdentifierKeys.has(normalizedKeyLower) ||
visibleIdentifierNames.some((name) => normalizedFieldNameUpper.endsWith(name))
) {
return false
}
if (
normalizedKeyLower === 'id' ||
normalizedFieldName === '绑定记录ID' ||
normalizedFieldName === 'ID'
) {
return true
}
return (
normalizedKeyLower.endsWith('_id') ||
/^[a-z][a-z0-9]*Id$/.test(normalizedKey) ||
normalizedFieldName.endsWith('ID')
)
}
const extractOperationContent = (
row: OperationLogRow,
type: 'before' | 'after'
): Record<string, any> => {
const directContent =
type === 'before' ? row.operation_content_before : row.operation_content_after
if (directContent && Object.keys(directContent).length > 0) {
return directContent
}
const fallbackContainer = type === 'before' ? row.before_data : row.after_data
const nestedContent = fallbackContainer?.operation_content
if (nestedContent && Object.keys(nestedContent).length > 0) {
return nestedContent
}
return fallbackContainer || {}
}
const isSameValue = (beforeValue: any, afterValue: any) => {
if (beforeValue === afterValue) return true
return JSON.stringify(beforeValue) === JSON.stringify(afterValue)
}
const getChangeContent = (row: OperationLogRow): ChangeItem[] => {
const before = extractOperationContent(row, 'before')
const after = extractOperationContent(row, 'after')
const desc = row.operation_fields_desc || {}
const keys = new Set([...Object.keys(before), ...Object.keys(after)])
const changes: ChangeItem[] = []
for (const key of keys) {
const fieldName = sanitizeFieldName(desc[key] || fieldNameMap[key] || key)
if (shouldHideIdField(key, fieldName)) continue
const beforeValue = before[key]
const afterValue = after[key]
if (isSameValue(beforeValue, afterValue)) continue
changes.push({
type: 'change',
key,
fieldName,
beforeValue,
afterValue
})
}
return changes
}
const getMessageItems = (row: OperationLogRow): MessageItem[] => {
const items: MessageItem[] = []
const seen = new Set<string>()
const pushMessage = (
key: string,
fieldName: string,
message?: string,
level: 'info' | 'error' = 'info'
) => {
const normalizedMessage = message?.trim()
if (!normalizedMessage) return
const duplicateKey = `${fieldName}:${normalizedMessage}`
if (seen.has(duplicateKey)) return
seen.add(duplicateKey)
items.push({
type: 'message',
key,
fieldName,
message: normalizedMessage,
level
})
}
pushMessage('error_msg', '失败原因', row.error_msg, 'error')
pushMessage('reason', '原因说明', row.reason, row.result_status === 'failed' ? 'error' : 'info')
if (row.fail_count > 0) {
pushMessage('fail_count', '批量结果', `失败 ${row.fail_count}`, 'error')
}
row.failed_items?.slice(0, 3).forEach((item, index) => {
const detail = item?.iccid
? `${item.iccid}${item.reason || '未知原因'}`
: item?.reason || ''
pushMessage(`failed_items_${index}`, '失败明细', detail, 'error')
})
if (!items.length && row.result_status === 'failed') {
pushMessage('result_status', '执行结果', '执行失败', 'error')
}
if (!items.length && row.result_status === 'denied') {
pushMessage('result_status', '执行结果', '操作被拒绝', 'info')
}
return items
}
const getDisplayContent = (row: OperationLogRow): DisplayItem[] => {
return [...getChangeContent(row), ...getMessageItems(row)]
}
watch(
() => props.assetIdentifier,
(newVal) => {
if (newVal) {
loadLogs()
}
},
{ immediate: true }
)
</script>
<style scoped lang="scss">
.operation-logs-card {
.card-header {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
.header-title {
font-size: 16px;
font-weight: 500;
}
.filter-section {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-end;
min-width: 0;
.operation-type-select {
width: 180px;
}
.result-status-select {
width: 120px;
}
}
}
.logs-table-wrapper {
overflow-x: auto;
.logs-table {
min-width: 980px;
:deep(.el-table__header-wrapper) {
th {
background-color: var(--el-fill-color-light);
}
}
.change-content {
display: flex;
flex-direction: column;
gap: 4px;
.change-item {
display: flex;
gap: 4px;
align-items: center;
font-size: 12px;
.field-name {
min-width: 72px;
color: var(--el-text-color-secondary);
}
.field-value {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
word-break: break-all;
.new-value {
color: var(--el-color-success);
}
.message-value {
color: var(--el-text-color-regular);
&.is-error {
color: var(--el-color-danger);
}
}
}
}
}
}
.logs-pagination {
justify-content: flex-end;
margin-top: 16px;
}
}
@media (width <= 1200px) {
.card-header {
flex-direction: column;
align-items: stretch;
.filter-section {
justify-content: flex-start;
width: 100%;
}
}
}
@media (width <= 768px) {
.card-header {
flex-direction: column;
align-items: stretch;
.filter-section {
flex-direction: column;
align-items: stretch;
:deep(.el-select),
.el-button,
.el-tag {
width: 100% !important;
}
.el-button {
margin-left: 0;
}
}
}
.logs-table-wrapper {
padding: 0 12px;
margin-right: -12px;
margin-left: -12px;
:deep(.el-pagination) {
justify-content: flex-start !important;
overflow-x: auto;
}
}
}
}
</style>

View File

@@ -96,15 +96,6 @@
@navigate-to-device="handleNavigateToDevice"
/>
</div>
<!-- 第五行操作审计日志 -->
<div class="row full-width">
<OperationLogsCard
:asset-identifier="cardInfo.identifier"
:asset-type="cardInfo.asset_type"
download-permission="asset_info:download_log_file"
/>
</div>
</div>
</div>
@@ -204,7 +195,6 @@
import CurrentPackageCard from './components/CurrentPackageCard.vue'
import PackageListCard from './components/PackageListCard.vue'
import WalletTransactionCard from './components/WalletTransactionCard.vue'
import OperationLogsCard from './components/OperationLogsCard.vue'
// 引入对话框组件
import {

View File

@@ -703,12 +703,6 @@
@confirm="handleConfirmBatchRealnamePolicy"
/>
<!-- 操作审计日志弹窗 -->
<OperationLogsDialog
v-model="operationLogsDialogVisible"
:identifier="operationLogsIdentifier"
download-permission="device:download_log_file"
/>
<ElDialog
v-model="enterpriseDeviceAuthorizeDialogVisible"
title="授权设备给企业"
@@ -1012,7 +1006,6 @@
} from '@/types/api/enterpriseDevice'
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
import BatchRealnamePolicyDialog from '@/components/business/BatchRealnamePolicyDialog.vue'
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
import type { AssetRealnamePolicy } from '@/types/api'
@@ -1045,8 +1038,6 @@
const batchRealnamePolicyDialogVisible = ref(false)
const batchRealnamePolicyLoading = ref(false)
const batchRealnamePolicyError = ref('')
const operationLogsDialogVisible = ref(false)
const operationLogsIdentifier = ref('')
const exportDialogVisible = ref(false)
const shopCascadeOptions = ref<any[]>([])
const shopCascadeProps = {
@@ -3167,17 +3158,6 @@
})
}
if (hasAuth('device:operation_logs')) {
moreActions.push({
label: '操作审计日志',
handler: () => {
operationLogsIdentifier.value = row.virtual_no
operationLogsDialogVisible.value = true
},
type: 'primary'
})
}
const userType = Number(userStore.getUserInfo.user_type)
const auditPermission =
userType === 3

View File

@@ -1001,13 +1001,6 @@
@success="handleUpdateRealnameStatusSuccess"
/>
<!-- 操作审计日志弹窗 -->
<OperationLogsDialog
v-model="operationLogsDialogVisible"
:identifier="operationLogsIdentifier"
download-permission="iot_card:download_log_file"
/>
<ExportTaskCreateDialog
v-model="exportDialogVisible"
scene="iot_card"
@@ -1067,7 +1060,6 @@
import type { AssetRealnamePolicy, PackageSeriesResponse } from '@/types/api'
import type { EnterpriseItem } from '@/types/api/enterprise'
import type { AllocateCardsResponse, RecallCardsResponse } from '@/types/api/enterpriseCard'
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
defineOptions({ name: 'StandaloneCardList' })
@@ -1119,9 +1111,6 @@
recalled_devices: null
})
// 操作审计日志弹窗
const operationLogsDialogVisible = ref(false)
const operationLogsIdentifier = ref('')
const exportDialogVisible = ref(false)
// 套餐系列绑定相关
@@ -2323,17 +2312,6 @@
})
}
if (hasAuth('iot_card:operation_logs')) {
actions.push({
label: '操作审计日志',
handler: () => {
operationLogsIdentifier.value = row.iccid
operationLogsDialogVisible.value = true
},
type: 'primary'
})
}
const userType = Number(userStore.getUserInfo.user_type)
const auditPermission =
userType === 3

View File

@@ -126,8 +126,6 @@
multiple
:multiple-limit="100"
filterable
remote
:remote-method="searchPackages"
:loading="packageLoading"
clearable
>
@@ -139,8 +137,9 @@
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
:label="pkg.package_name"
:label="`${pkg.package_name}${pkg.authorized ? '(已授权)' : ''}`"
:value="pkg.id"
:disabled="pkg.authorized"
/>
</ElSelect>
<div class="form-tip">首次创建必须选择 1100 个不重复套餐</div>
@@ -626,7 +625,14 @@
const currentStep = ref(0) // 当前步骤0 为第一步1 为第二步
const seriesOptions = ref<PackageSeriesResponse[]>([])
const packageOptions = ref<PackageResponse[]>([])
type GrantPackageOptionView = {
id: number
package_name: string
cost_price: number
suggested_retail_price?: number | null
authorized: boolean
}
const packageOptions = ref<GrantPackageOptionView[]>([])
const selectedPackageIds = ref<number[]>([])
const searchSeriesOptions = ref<PackageSeriesResponse[]>([])
const searchShopOptions = ref<ShopResponse[]>([])
@@ -1082,7 +1088,7 @@
isFirstActivation = false
})
// 加载套餐选项(用于新增对话框)
// 加载套餐选项(用于编辑对话框)
const loadPackageOptions = async (packageName?: string) => {
packageLoading.value = true
try {
@@ -1104,8 +1110,17 @@
page += 1
} while (allPackages.length < total)
// 过滤掉赠送套餐
packageOptions.value = allPackages.filter((pkg) => !pkg.is_gift)
packageOptions.value = allPackages
.filter((pkg) => !pkg.is_gift)
.map(
(pkg): GrantPackageOptionView => ({
id: pkg.id,
package_name: pkg.package_name,
cost_price: pkg.cost_price ?? 0,
suggested_retail_price: pkg.suggested_retail_price,
authorized: false
})
)
} catch (error) {
console.error('加载套餐选项失败:', error)
} finally {
@@ -1113,12 +1128,21 @@
}
}
// 搜索套餐
const searchPackages = (query: string) => {
if (query) {
loadPackageOptions(query)
} else {
loadPackageOptions()
const loadGrantPackageOptions = async () => {
const shopId = form.shop_id
const seriesId = form.series_id
if (dialogType.value !== 'add' || !shopId || !seriesId) return
packageLoading.value = true
try {
const res = await ShopSeriesGrantService.getPackageOptions(shopId, seriesId)
if (res.code === 0 && form.shop_id === shopId && form.series_id === seriesId) {
packageOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载授权套餐候选项失败:', error)
} finally {
packageLoading.value = false
}
}
@@ -1263,8 +1287,8 @@
}
}
// 重新加载该系列下的套餐
loadPackageOptions()
// 仅在已选择目标店铺后加载该组合的授权套餐候选项
loadGrantPackageOptions()
} else {
// 未选择系列,重置佣金类型
form.series_name = ''
@@ -1278,6 +1302,17 @@
}
)
watch(
() => form.shop_id,
(newShopId, oldShopId) => {
if (dialogType.value !== 'add' || newShopId === oldShopId) return
form.packages = []
selectedPackageIds.value = []
packageOptions.value = []
loadGrantPackageOptions()
}
)
// 监听启用强制充值开关,自动设置默认强充金额
watch(
() => form.enable_force_recharge,
@@ -1778,16 +1813,6 @@
}
if (packageItems.length > 0) {
// 验证是否包含赠送套餐
for (const pkg of form.packages) {
const pkgInfo = packageOptions.value.find((p) => p.id === pkg.package_id)
if (pkgInfo?.is_gift) {
ElMessage.error(`套餐 ${getPackageName(pkg.package_id)} 是赠送套餐,不能参与授权`)
submitLoading.value = false
return
}
}
// 验证成本价是否在允许范围内
for (const pkg of form.packages) {
const costPrice = pkg.cost_price ?? 0