fix: id兼容, 通知入口
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m0s

This commit is contained in:
luo
2026-07-28 15:19:29 +08:00
parent 0e8a11430a
commit 261f5f2853
21 changed files with 271 additions and 71 deletions

View File

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

View File

@@ -12,7 +12,7 @@
<!-- 表格 --> <!-- 表格 -->
<ArtTable <ArtTable
ref="tableRef" ref="tableRef"
row-key="ID" row-key="id"
:loading="loading" :loading="loading"
:data="accountList" :data="accountList"
height="60vh" height="60vh"

View File

@@ -66,7 +66,6 @@
</div> </div>
<!-- 通知 --> <!-- 通知 -->
<div <div
v-if="hasAuth(NOTIFICATION_PERMISSIONS.page)"
class="btn-box notice-btn" class="btn-box notice-btn"
@click.stop="showNotice = !showNotice" @click.stop="showNotice = !showNotice"
> >

View File

@@ -432,7 +432,7 @@
"standaloneCardList": "IoT Card Management", "standaloneCardList": "IoT Card Management",
"iotCardTask": "IoT Card Tasks", "iotCardTask": "IoT Card Tasks",
"deviceTask": "Device Tasks", "deviceTask": "Device Tasks",
"deviceBatchAllocation": "Device Batch Allocation", "deviceBatchAllocation": "Device Batch Tasks",
"bulkPurchase": "Bulk Package Purchase", "bulkPurchase": "Bulk Package Purchase",
"bulkPurchaseDetail": "Bulk Purchase Task Detail", "bulkPurchaseDetail": "Bulk Purchase Task Detail",
"orderPackageInvalidateTask": "Order Package Void Tasks", "orderPackageInvalidateTask": "Order Package Void Tasks",

View File

@@ -370,7 +370,7 @@
"standaloneCardList": "IoT卡管理", "standaloneCardList": "IoT卡管理",
"iotCardTask": "IoT卡任务", "iotCardTask": "IoT卡任务",
"deviceTask": "设备任务", "deviceTask": "设备任务",
"deviceBatchAllocation": "设备批量分配", "deviceBatchAllocation": "设备批量任务",
"bulkPurchase": "批量订购套餐", "bulkPurchase": "批量订购套餐",
"bulkPurchaseDetail": "批量订购任务详情", "bulkPurchaseDetail": "批量订购任务详情",
"orderPackageInvalidateTask": "订单套餐批量作废", "orderPackageInvalidateTask": "订单套餐批量作废",

View File

@@ -441,7 +441,7 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true keepAlive: true
} }
}, },
// 设备批量分配任务 // 设备批量任务
{ {
path: 'device-batch-allocation', path: 'device-batch-allocation',
name: 'DeviceBatchAllocation', name: 'DeviceBatchAllocation',

View File

@@ -59,7 +59,7 @@ export enum RoutesAlias {
// 任务管理 // 任务管理
IotCardTask = '/asset-management/task-management/iot-card-task', // IoT卡任务 IotCardTask = '/asset-management/task-management/iot-card-task', // IoT卡任务
DeviceTask = '/asset-management/task-management/device-task', // 设备任务 DeviceTask = '/asset-management/task-management/device-task', // 设备任务
DeviceBatchAllocation = '/asset-management/task-management/device-batch-allocation', // 设备批量分配 DeviceBatchAllocation = '/asset-management/task-management/device-batch-allocation', // 设备批量任务
BulkPurchase = '/asset-management/task-management/bulk-purchase', // 批量订购套餐 BulkPurchase = '/asset-management/task-management/bulk-purchase', // 批量订购套餐
BulkPurchaseDetail = '/asset-management/task-management/bulk-purchase/detail', // 批量订购套餐详情 BulkPurchaseDetail = '/asset-management/task-management/bulk-purchase/detail', // 批量订购套餐详情
OrderPackageInvalidateTask = '/asset-management/task-management/order-package-invalidate-task', // 订单套餐批量作废任务 OrderPackageInvalidateTask = '/asset-management/task-management/order-package-invalidate-task', // 订单套餐批量作废任务

View File

@@ -12,7 +12,8 @@ export enum AccountStatus {
// 账号实体(统一账号模型,匹配后端 ModelAccountResponse // 账号实体(统一账号模型,匹配后端 ModelAccountResponse
export interface PlatformAccount { export interface PlatformAccount {
ID: number id?: number
ID?: number
CreatedAt?: string CreatedAt?: string
UpdatedAt?: string UpdatedAt?: string
DeletedAt?: string | null DeletedAt?: string | null

View File

@@ -18,7 +18,8 @@ export enum PermissionStatus {
// 权限实体(匹配后端 ModelPermission // 权限实体(匹配后端 ModelPermission
export interface Permission { export interface Permission {
ID: number id?: number
ID?: number
CreatedAt?: string CreatedAt?: string
UpdatedAt?: string UpdatedAt?: string
DeletedAt?: string | null DeletedAt?: string | null

View File

@@ -19,7 +19,7 @@ export enum RoleStatus {
// 角色实体(匹配后端 ModelRole // 角色实体(匹配后端 ModelRole
export interface PlatformRole { export interface PlatformRole {
id?: number id?: number
ID: number ID?: number
CreatedAt?: string CreatedAt?: string
UpdatedAt?: string UpdatedAt?: string
DeletedAt?: string | null DeletedAt?: string | null

58
src/utils/business/id.ts Normal file
View File

@@ -0,0 +1,58 @@
/**
* 兼容后端实体 ID 字段的大小写差异。
* 后端历史接口可能返回 `ID`,新接口返回 `id`,前端统一优先使用小写 id。
*/
export type CompatibleId = number | string
export interface CompatibleIdFields {
id?: CompatibleId | null
ID?: CompatibleId | null
}
/** 从实体中读取兼容的 ID 字段。0 也是有效 ID不能使用 ||。 */
export const getCompatibleId = (value: CompatibleIdFields | null | undefined) => {
const id = value?.id ?? value?.ID
return id === null || id === undefined || id === '' ? undefined : id
}
/** 读取并转换为数字 ID供只接受 number 的接口参数使用。 */
export const getCompatibleNumericId = (
value: CompatibleIdFields | null | undefined
): number | undefined => {
const id = getCompatibleId(value)
if (id === undefined) return undefined
const numericId = Number(id)
return Number.isFinite(numericId) ? numericId : undefined
}
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
if (value === null || typeof value !== 'object') return false
const prototype = Object.getPrototypeOf(value)
return prototype === Object.prototype || prototype === null
}
/** 递归补齐响应中的 id/ID 别名,不影响 Blob、FormData、Date 等特殊数据。 */
export const syncCompatibleIdFields = <T>(value: T): T => {
if (Array.isArray(value)) {
value.forEach((item) => syncCompatibleIdFields(item))
return value
}
if (!isPlainObject(value)) return value
const record = value as Record<string, unknown>
const hasId = Object.prototype.hasOwnProperty.call(record, 'id')
const hasUpperId = Object.prototype.hasOwnProperty.call(record, 'ID')
if (hasId || hasUpperId) {
const id = record.id ?? record.ID
if (id !== null && id !== undefined && id !== '') {
if (!hasId) record.id = id
if (!hasUpperId) record.ID = id
}
}
Object.values(record).forEach((item) => syncCompatibleIdFields(item))
return value
}

View File

@@ -9,3 +9,4 @@ export * from './voucher'
export * from './apiRateLimit' export * from './apiRateLimit'
export * from './approvalSummary' export * from './approvalSummary'
export * from './expiryEstimate' export * from './expiryEstimate'
export * from './id'

View File

@@ -4,6 +4,7 @@ import { useUserStore } from '@/store/modules/user'
import { ApiStatus } from './status' import { ApiStatus } from './status'
import type { RequestOptions, ErrorMessageMode } from '@/types/api' import type { RequestOptions, ErrorMessageMode } from '@/types/api'
import { normalizeApiError } from '@/utils/business/apiError' import { normalizeApiError } from '@/utils/business/apiError'
import { syncCompatibleIdFields } from '@/utils/business/id'
const axiosInstance = axios.create({ const axiosInstance = axios.create({
timeout: 15000, // 请求超时时间(毫秒) timeout: 15000, // 请求超时时间(毫秒)
@@ -90,6 +91,9 @@ const processQueue = (error: any, token: string | null = null) => {
// 响应拦截器 // 响应拦截器
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
(response: AxiosResponse) => { (response: AxiosResponse) => {
// 兼容历史接口返回的 ID 与新接口返回的 id。
syncCompatibleIdFields(response.data)
// 401 未授权 - 尝试刷新 token // 401 未授权 - 尝试刷新 token
if (response.data.code === ApiStatus.unauthorized) { if (response.data.code === ApiStatus.unauthorized) {
const userStore = useUserStore() const userStore = useUserStore()

View File

@@ -82,9 +82,9 @@
> >
<ElOption <ElOption
v-for="role in availableRolesForCreate" v-for="role in availableRolesForCreate"
:key="role.ID" :key="getRoleId(role)"
:label="role.role_name" :label="role.role_name"
:value="role.ID" :value="getRoleId(role)"
> >
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
@@ -133,8 +133,15 @@
<ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" /> <ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" />
<template v-else-if="isPlatformUser"> <template v-else-if="isPlatformUser">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list"> <ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item"> <div
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)"> v-for="role in filteredAvailableRoles"
:key="getRoleId(role)"
class="role-item"
>
<ElCheckbox
:label="getRoleId(role)"
:disabled="selectedRoles.includes(getRoleId(role))"
>
<span class="role-info"> <span class="role-info">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small"> <ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
@@ -147,8 +154,15 @@
</template> </template>
<template v-else> <template v-else>
<ElRadioGroup v-model="roleToAdd" class="role-list"> <ElRadioGroup v-model="roleToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item"> <div
<ElRadio :label="role.ID" :disabled="selectedRoles.includes(role.ID)"> v-for="role in filteredAvailableRoles"
:key="getRoleId(role)"
class="role-item"
>
<ElRadio
:label="getRoleId(role)"
:disabled="selectedRoles.includes(getRoleId(role))"
>
<span class="role-info"> <span class="role-info">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small"> <ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
@@ -190,7 +204,7 @@
<div class="role-list"> <div class="role-list">
<div <div
v-for="role in filteredAssignedRoles" v-for="role in filteredAssignedRoles"
:key="role.ID" :key="getRoleId(role)"
class="role-item assigned-role-item" class="role-item assigned-role-item"
> >
<span class="role-info"> <span class="role-info">
@@ -199,7 +213,12 @@
{{ role.role_type === 1 ? '平台角色' : '客户角色' }} {{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag> </ElTag>
</span> </span>
<ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)"> <ElButton
type="danger"
size="small"
link
@click="removeSingleRole(getRoleId(role))"
>
移除 移除
</ElButton> </ElButton>
</div> </div>
@@ -310,6 +329,7 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import type { PlatformRole, WecomApplication, WecomMember } from '@/types/api' import type { PlatformRole, WecomApplication, WecomMember } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { getCompatibleNumericId } from '@/utils/business/id'
import { import {
CommonStatus, CommonStatus,
getStatusText, getStatusText,
@@ -356,6 +376,9 @@
// 是否为平台用户(平台用户可多选,其他单选) // 是否为平台用户(平台用户可多选,其他单选)
const isPlatformUser = computed(() => currentAccountType.value === 2) const isPlatformUser = computed(() => currentAccountType.value === 2)
const getAccountId = (account: any) => getCompatibleNumericId(account) ?? 0
const getRoleId = (role: PlatformRole | any) => getCompatibleNumericId(role) ?? 0
// 定义表单搜索初始值 // 定义表单搜索初始值
const initialSearchState = { const initialSearchState = {
name: '', name: '',
@@ -631,14 +654,6 @@
const getActions = (row: any) => { const getActions = (row: any) => {
const actions: any[] = [] const actions: any[] = []
if (row.user_type === 2 && hasAuth(JULY_PERMISSIONS.wecom.binding)) {
actions.push({
label: '绑定账号',
handler: () => showWecomBindingDialog(row),
type: 'primary'
})
}
if (hasAuth('account:patch_role')) { if (hasAuth('account:patch_role')) {
actions.push({ actions.push({
label: '分配角色', label: '分配角色',
@@ -647,6 +662,14 @@
}) })
} }
if (row.user_type === 2 && hasAuth(JULY_PERMISSIONS.wecom.binding)) {
actions.push({
label: '绑定账号',
handler: () => showWecomBindingDialog(row),
type: 'primary'
})
}
if (hasAuth('account:edit')) { if (hasAuth('account:edit')) {
actions.push({ actions.push({
label: '编辑', label: '编辑',
@@ -716,7 +739,7 @@
return return
} }
const accountId = Number(currentWecomAccount.value.id ?? currentWecomAccount.value.ID) const accountId = getAccountId(currentWecomAccount.value)
if (!accountId) { if (!accountId) {
ElMessage.error('当前账号缺少账号标识,无法绑定企微') ElMessage.error('当前账号缺少账号标识,无法绑定企微')
return return
@@ -823,7 +846,9 @@
// 计算属性:过滤后的已分配角色 // 计算属性:过滤后的已分配角色
const filteredAssignedRoles = computed(() => { const filteredAssignedRoles = computed(() => {
const assignedRolesList = allRoles.value.filter((role) => selectedRoles.value.includes(role.ID)) const assignedRolesList = allRoles.value.filter((role) =>
selectedRoles.value.includes(getRoleId(role))
)
if (!rightRoleFilter.value) return assignedRolesList if (!rightRoleFilter.value) return assignedRolesList
const keyword = rightRoleFilter.value.toLowerCase() const keyword = rightRoleFilter.value.toLowerCase()
return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword)) return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword))
@@ -835,7 +860,12 @@
ElMessage.warning('超级管理员无法分配角色') ElMessage.warning('超级管理员无法分配角色')
return return
} }
currentAccountId.value = row.id const accountId = getAccountId(row)
if (!accountId) {
ElMessage.error('当前账号缺少账号标识,无法分配角色')
return
}
currentAccountId.value = accountId
currentAccountName.value = row.username currentAccountName.value = row.username
currentAccountType.value = row.user_type currentAccountType.value = row.user_type
selectedRoles.value = [] selectedRoles.value = []
@@ -852,12 +882,12 @@
await loadAllRoles('', roleType) await loadAllRoles('', roleType)
// 先加载当前账号的角色,再打开对话框 // 先加载当前账号的角色,再打开对话框
const res = await AccountService.getAccountRoles(row.id) const res = await AccountService.getAccountRoles(accountId)
if (res.code === 0) { if (res.code === 0) {
// 提取角色ID数组 // 提取角色ID数组
const roles = res.data || [] const roles = res.data || []
// 兼容 ID 和 id 两种字段名 // 兼容 ID 和 id 两种字段名
selectedRoles.value = roles.map((role: any) => role.ID || role.id) selectedRoles.value = roles.map((role: any) => getRoleId(role)).filter(Boolean)
// 数据加载完成后再打开对话框 // 数据加载完成后再打开对话框
roleDialogVisible.value = true roleDialogVisible.value = true
} }
@@ -1013,7 +1043,7 @@
// 创建成功后分配角色(如果选择了) // 创建成功后分配角色(如果选择了)
if (createRes.code === 0) { if (createRes.code === 0) {
const accountId = createRes.data?.ID || createRes.data?.id const accountId = getCompatibleNumericId(createRes.data)
if (formData.role_id && accountId) { if (formData.role_id && accountId) {
try { try {
await AccountService.assignRolesToAccount(accountId, [formData.role_id]) await AccountService.assignRolesToAccount(accountId, [formData.role_id])

View File

@@ -22,7 +22,7 @@
<template v-if="!props.seriesId"> <template v-if="!props.seriesId">
<ElOption value="__NO_PACKAGE__" disabled> <ElOption value="__NO_PACKAGE__" disabled>
<span style="color: var(--el-text-color-warning)"> <span style="color: var(--el-text-color-warning)">
该设备未关联套餐系列无法购买套餐 {{ noSeriesMessage }}
</span> </span>
</ElOption> </ElOption>
</template> </template>
@@ -45,7 +45,7 @@
</ElSelect> </ElSelect>
<template v-if="noSeriesId"> <template v-if="noSeriesId">
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-warning)"> <div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-warning)">
该设备未关联套餐系列无法购买套餐 {{ noSeriesMessage }}
</div> </div>
</template> </template>
<template v-else-if="packagesEmptyWithSeriesId"> <template v-else-if="packagesEmptyWithSeriesId">
@@ -182,6 +182,9 @@
}) })
const noSeriesId = computed(() => !props.seriesId) const noSeriesId = computed(() => !props.seriesId)
const noSeriesMessage = computed(
() => `${props.assetType === 'card' ? '卡' : '设备'}未关联套餐系列,无法购买套餐`
)
const selectedPackageIsGift = computed(() => { const selectedPackageIsGift = computed(() => {
if (!form.package_id) return false if (!form.package_id) return false

View File

@@ -11,6 +11,28 @@
@closed="handleCreateDrawerClosed" @closed="handleCreateDrawerClosed"
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="create-form"> <ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="create-form">
<ElFormItem label="套餐系列" prop="series_id">
<ElSelect
v-model="form.series_id"
filterable
remote
clearable
:remote-method="handleSeriesSearch"
:loading="seriesLoading"
placeholder="请输入系列名称搜索"
style="width: 100%"
@change="handleSeriesChange"
@visible-change="handleSeriesVisibleChange"
>
<ElOption
v-for="series in seriesOptions"
:key="series.id"
:label="series.series_name"
:value="series.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="套餐" prop="package_id"> <ElFormItem label="套餐" prop="package_id">
<ElSelect <ElSelect
v-model="form.package_id" v-model="form.package_id"
@@ -19,7 +41,8 @@
clearable clearable
:remote-method="handlePackageSearch" :remote-method="handlePackageSearch"
:loading="packageLoading" :loading="packageLoading"
placeholder="请输入套餐名称搜索" :disabled="!form.series_id"
placeholder="请先选择套餐系列"
style="width: 100%" style="width: 100%"
@visible-change="handlePackageVisibleChange" @visible-change="handlePackageVisibleChange"
> >
@@ -223,8 +246,13 @@
} from 'element-plus' } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import VoucherUpload from '@/components/business/VoucherUpload.vue' import VoucherUpload from '@/components/business/VoucherUpload.vue'
import { BulkPurchaseService, PackageManageService } from '@/api/modules' import { BulkPurchaseService, PackageManageService, PackageSeriesService } from '@/api/modules'
import type { BulkPurchasePaymentMethod, BulkPurchaseTask, PackageResponse } from '@/types/api' import type {
BulkPurchasePaymentMethod,
BulkPurchaseTask,
PackageResponse,
PackageSeriesResponse
} from '@/types/api'
import { BulkPurchaseTaskStatus } from '@/types/api' import { BulkPurchaseTaskStatus } from '@/types/api'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
@@ -244,6 +272,8 @@
const orderFileUploading = ref(false) const orderFileUploading = ref(false)
const voucherFileKeys = ref<string[]>([]) const voucherFileKeys = ref<string[]>([])
const voucherUploading = ref(false) const voucherUploading = ref(false)
const seriesOptions = ref<PackageSeriesResponse[]>([])
const seriesLoading = ref(false)
const packageOptions = ref<PackageResponse[]>([]) const packageOptions = ref<PackageResponse[]>([])
const packageLoading = ref(false) const packageLoading = ref(false)
const taskList = ref<BulkPurchaseTask[]>([]) const taskList = ref<BulkPurchaseTask[]>([])
@@ -257,14 +287,17 @@
) )
const form = reactive<{ const form = reactive<{
series_id?: number
package_id?: number package_id?: number
payment_method: BulkPurchasePaymentMethod payment_method: BulkPurchasePaymentMethod
}>({ }>({
series_id: undefined,
package_id: undefined, package_id: undefined,
payment_method: 'wallet' payment_method: 'wallet'
}) })
const rules = computed<FormRules>(() => ({ const rules = computed<FormRules>(() => ({
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
package_id: [{ required: true, message: '请选择套餐', trigger: 'change' }], package_id: [{ required: true, message: '请选择套餐', trigger: 'change' }],
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }], payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
orderFile: [ orderFile: [
@@ -289,12 +322,18 @@
})) }))
const loadPackages = async (query = '') => { const loadPackages = async (query = '') => {
if (!form.series_id) {
packageOptions.value = []
return
}
packageLoading.value = true packageLoading.value = true
try { try {
const res = await PackageManageService.getPackages({ const res = await PackageManageService.getPackages({
page: 1, page: 1,
page_size: 20, page_size: 20,
package_name: query.trim() || undefined, package_name: query.trim() || undefined,
series_id: form.series_id,
status: 1, status: 1,
shelf_status: 1 shelf_status: 1
}) })
@@ -314,20 +353,21 @@
} }
const handlePackageSearch = (query: string) => { const handlePackageSearch = (query: string) => {
void loadPackages(query) if (form.series_id) void loadPackages(query)
} }
const handlePackageVisibleChange = (visible: boolean) => { const handlePackageVisibleChange = (visible: boolean) => {
if (visible && packageOptions.value.length === 0) void loadPackages() if (visible && form.series_id && packageOptions.value.length === 0) void loadPackages()
} }
const openCreateDrawer = () => { const openCreateDrawer = () => {
createDrawerVisible.value = true createDrawerVisible.value = true
if (packageOptions.value.length === 0) void loadPackages() if (seriesOptions.value.length === 0) void loadSeries()
} }
const handleCreateDrawerClosed = () => { const handleCreateDrawerClosed = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
form.series_id = undefined
form.package_id = undefined form.package_id = undefined
form.payment_method = 'wallet' form.payment_method = 'wallet'
orderFileKeys.value = [] orderFileKeys.value = []
@@ -338,6 +378,41 @@
voucherUploading.value = false voucherUploading.value = false
} }
const loadSeries = async (query = '') => {
seriesLoading.value = true
try {
const res = await PackageSeriesService.getPackageSeries({
page: 1,
page_size: 20,
series_name: query.trim() || undefined,
status: 1
})
if (res.code !== 0) {
ElMessage.error(res.msg || '获取套餐系列失败')
return
}
seriesOptions.value = res.data?.items || []
} catch (error: any) {
ElMessage.error(error?.message || '获取套餐系列失败')
} finally {
seriesLoading.value = false
}
}
const handleSeriesSearch = (query: string) => {
void loadSeries(query)
}
const handleSeriesVisibleChange = (visible: boolean) => {
if (visible && seriesOptions.value.length === 0) void loadSeries()
}
const handleSeriesChange = () => {
form.package_id = undefined
packageOptions.value = []
if (form.series_id) void loadPackages()
}
const handlePaymentMethodChange = (value: string | number | boolean | undefined) => { const handlePaymentMethodChange = (value: string | number | boolean | undefined) => {
if (value === 'wallet') { if (value === 'wallet') {
voucherFileKeys.value = [] voucherFileKeys.value = []

View File

@@ -3,7 +3,7 @@
<div class="device-batch-allocation-page" id="table-full-screen"> <div class="device-batch-allocation-page" id="table-full-screen">
<ElDrawer <ElDrawer
v-model="createDrawerVisible" v-model="createDrawerVisible"
title="设备批量分配" title="设备批量任务"
direction="rtl" direction="rtl"
size="520px" size="520px"
:close-on-click-modal="!createDrawerBusy" :close-on-click-modal="!createDrawerBusy"
@@ -102,13 +102,13 @@
<ElCard shadow="never" class="art-table-card task-list-card"> <ElCard shadow="never" class="art-table-card task-list-card">
<template #header> <template #header>
<div class="task-list-header"> <div class="task-list-header">
<span>设备批量分配任务</span> <span>设备批量任务</span>
<div class="task-list-toolbar"> <div class="task-list-toolbar">
<ElButton <ElButton
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.create)" v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.create)"
tag="a" tag="a"
href="/templates/device-batch-allocation-template.csv" href="/templates/device-batch-allocation-template.csv"
download="设备批量分配模板.csv" download="设备批量任务模板.csv"
> >
下载模板 下载模板
</ElButton> </ElButton>
@@ -117,7 +117,7 @@
type="primary" type="primary"
@click="openCreateDrawer" @click="openCreateDrawer"
> >
设备批量分配 设备批量任务
</ElButton> </ElButton>
<ElSelect <ElSelect
v-model="taskListOperationType" v-model="taskListOperationType"
@@ -344,7 +344,7 @@
if (res.code === 0) seriesOptions.value = res.data.items || [] if (res.code === 0) seriesOptions.value = res.data.items || []
} }
} catch (error) { } catch (error) {
console.error('获取批量分配目标失败:', error) console.error('获取设备批量任务目标失败:', error)
ElMessage.error(normalizeApiError(error).message) ElMessage.error(normalizeApiError(error).message)
} finally { } finally {
targetLoading.value = false targetLoading.value = false
@@ -399,13 +399,13 @@
operation_type: taskListOperationType.value operation_type: taskListOperationType.value
}) })
if (res.code !== 0) { if (res.code !== 0) {
ElMessage.error(res.msg || '获取设备批量分配任务失败') ElMessage.error(res.msg || '获取设备批量任务失败')
return return
} }
taskList.value = (res.data.items || []).filter((item) => item.operation_type !== 'import') taskList.value = (res.data.items || []).filter((item) => item.operation_type !== 'import')
taskListTotal.value = res.data.total || 0 taskListTotal.value = res.data.total || 0
} catch (error) { } catch (error) {
console.error('获取设备批量分配任务失败:', error) console.error('获取设备批量任务失败:', error)
ElMessage.error(normalizeApiError(error).message) ElMessage.error(normalizeApiError(error).message)
} finally { } finally {
taskListLoading.value = false taskListLoading.value = false
@@ -445,14 +445,14 @@
try { try {
const res = await DeviceService.createAllocationTask(request) const res = await DeviceService.createAllocationTask(request)
if (res.code !== 0) { if (res.code !== 0) {
ElMessage.error(res.msg || '创建设备批量分配任务失败') ElMessage.error(res.msg || '创建设备批量任务失败')
return return
} }
createDrawerVisible.value = false createDrawerVisible.value = false
await loadTaskList() await loadTaskList()
ElMessage.success(`任务已创建:${res.data.task_no}`) ElMessage.success(`任务已创建:${res.data.task_no}`)
} catch (error) { } catch (error) {
console.error('创建设备批量分配任务失败:', error) console.error('创建设备批量任务失败:', error)
ElMessage.error(normalizeApiError(error).message) ElMessage.error(normalizeApiError(error).message)
} finally { } finally {
submitting.value = false submitting.value = false

View File

@@ -34,7 +34,7 @@
<!-- 表格 --> <!-- 表格 -->
<ArtTable <ArtTable
ref="tableRef" ref="tableRef"
row-key="ID" row-key="id"
:loading="loading" :loading="loading"
:data="accountList" :data="accountList"
:currentPage="pagination.page" :currentPage="pagination.page"
@@ -120,8 +120,11 @@
<ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" /> <ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" />
<template v-else-if="isPlatformUser"> <template v-else-if="isPlatformUser">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list"> <ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item"> <div v-for="role in filteredAvailableRoles" :key="getRoleId(role)" class="role-item">
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)"> <ElCheckbox
:label="getRoleId(role)"
:disabled="selectedRoles.includes(getRoleId(role))"
>
<span class="role-info"> <span class="role-info">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small"> <ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
@@ -134,8 +137,11 @@
</template> </template>
<template v-else> <template v-else>
<ElRadioGroup v-model="roleToAdd" class="role-list"> <ElRadioGroup v-model="roleToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item"> <div v-for="role in filteredAvailableRoles" :key="getRoleId(role)" class="role-item">
<ElRadio :label="role.ID" :disabled="selectedRoles.includes(role.ID)"> <ElRadio
:label="getRoleId(role)"
:disabled="selectedRoles.includes(getRoleId(role))"
>
<span class="role-info"> <span class="role-info">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small"> <ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
@@ -177,7 +183,7 @@
<div class="role-list"> <div class="role-list">
<div <div
v-for="role in filteredAssignedRoles" v-for="role in filteredAssignedRoles"
:key="role.ID" :key="getRoleId(role)"
class="role-item assigned-role-item" class="role-item assigned-role-item"
> >
<span class="role-info"> <span class="role-info">
@@ -186,7 +192,12 @@
{{ role.role_type === 1 ? '平台角色' : '客户角色' }} {{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag> </ElTag>
</span> </span>
<ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)"> <ElButton
type="danger"
size="small"
link
@click="removeSingleRole(getRoleId(role))"
>
移除 移除
</ElButton> </ElButton>
</div> </div>
@@ -231,6 +242,7 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import type { PlatformAccount, PlatformRole } from '@/types/api' import type { PlatformAccount, PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { getCompatibleNumericId } from '@/utils/business/id'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'CommonAccountList' }) defineOptions({ name: 'CommonAccountList' })
@@ -287,6 +299,9 @@
// 是否为平台用户(平台用户可多选,其他单选) // 是否为平台用户(平台用户可多选,其他单选)
const isPlatformUser = computed(() => currentAccountType.value === 2) const isPlatformUser = computed(() => currentAccountType.value === 2)
const getAccountId = (account: PlatformAccount | any) => getCompatibleNumericId(account) ?? 0
const getRoleId = (role: PlatformRole | any) => getCompatibleNumericId(role) ?? 0
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
username: '', username: '',
@@ -646,7 +661,9 @@
// 计算属性:过滤后的已分配角色 // 计算属性:过滤后的已分配角色
const filteredAssignedRoles = computed(() => { const filteredAssignedRoles = computed(() => {
const assignedRolesList = allRoles.value.filter((role) => selectedRoles.value.includes(role.ID)) const assignedRolesList = allRoles.value.filter((role) =>
selectedRoles.value.includes(getRoleId(role))
)
if (!rightRoleFilter.value) return assignedRolesList if (!rightRoleFilter.value) return assignedRolesList
const keyword = rightRoleFilter.value.toLowerCase() const keyword = rightRoleFilter.value.toLowerCase()
return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword)) return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword))
@@ -654,7 +671,12 @@
// 显示分配角色对话框 // 显示分配角色对话框
const showRoleDialog = async (row: PlatformAccount) => { const showRoleDialog = async (row: PlatformAccount) => {
currentAccountId.value = (row as any).id const accountId = getAccountId(row)
if (!accountId) {
ElMessage.error('当前账号缺少账号标识,无法分配角色')
return
}
currentAccountId.value = accountId
currentAccountName.value = row.username currentAccountName.value = row.username
currentAccountType.value = row.user_type currentAccountType.value = row.user_type
selectedRoles.value = [] selectedRoles.value = []
@@ -671,12 +693,12 @@
await loadAllRoles('', roleType) await loadAllRoles('', roleType)
// 先加载当前账号的角色,再打开对话框 // 先加载当前账号的角色,再打开对话框
const res = await AccountService.getAccountRoles((row as any).id) const res = await AccountService.getAccountRoles(accountId)
if (res.code === 0) { if (res.code === 0) {
// 提取角色ID数组 // 提取角色ID数组
const roles = res.data || [] const roles = res.data || []
// 兼容 ID 和 id 两种字段名 // 兼容 ID 和 id 两种字段名
selectedRoles.value = roles.map((role: any) => role.ID || role.id) selectedRoles.value = roles.map((role: any) => getRoleId(role)).filter((id) => id > 0)
// 数据加载完成后再打开对话框 // 数据加载完成后再打开对话框
roleDialogVisible.value = true roleDialogVisible.value = true
} }

View File

@@ -163,7 +163,7 @@
</ElSelect> </ElSelect>
<template v-if="noSeriesId"> <template v-if="noSeriesId">
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-warning)"> <div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-warning)">
该设备未关联套餐系列无法购买套餐 {{ noSeriesMessage }}
</div> </div>
</template> </template>
<template v-else-if="packagesEmptyWithSeriesId"> <template v-else-if="packagesEmptyWithSeriesId">
@@ -616,6 +616,10 @@
return false return false
}) })
const noSeriesMessage = computed(
() => `${createForm.order_type === 'single_card' ? '卡' : '设备'}未关联套餐系列,无法购买套餐`
)
// 检查是否选择了赠送套餐 // 检查是否选择了赠送套餐
const selectedPackageIsGift = computed(() => { const selectedPackageIsGift = computed(() => {
if (!createForm.package_id) return false if (!createForm.package_id) return false
@@ -892,7 +896,8 @@
prop: 'is_expired', prop: 'is_expired',
label: '是否过期', label: '是否过期',
width: 100, width: 100,
formatter: (row: Order) => (row.is_expired === undefined ? '-' : row.is_expired ? '是' : '否') formatter: (row: Order) =>
row.is_expired === undefined ? '-' : row.is_expired ? '是' : '否'
}, },
{ {
prop: 'expires_at', prop: 'expires_at',

View File

@@ -337,6 +337,7 @@
} from '@/types/api' } from '@/types/api'
import { RoleType, RoleStatus } from '@/types/api' import { RoleType, RoleStatus } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { getCompatibleNumericId } from '@/utils/business/id'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants' import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
import { regionData } from '@/utils/constants/regionData' import { regionData } from '@/utils/constants/regionData'
import { RoutesAlias } from '@/router/routesAlias' import { RoutesAlias } from '@/router/routesAlias'
@@ -1144,7 +1145,7 @@
// 将 PlatformRole 转换为与 ShopRoleResponse 兼容的格式 // 将 PlatformRole 转换为与 ShopRoleResponse 兼容的格式
availableRoles.value = (res.data.items || []).map((role) => ({ availableRoles.value = (res.data.items || []).map((role) => ({
...role, ...role,
role_id: role.id ?? role.ID, role_id: getCompatibleNumericId(role) ?? 0,
role_name: role.role_name, role_name: role.role_name,
role_desc: role.role_desc, role_desc: role.role_desc,
role_type: role.role_type, role_type: role.role_type,

View File

@@ -255,6 +255,7 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format' import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
import { getCompatibleNumericId } from '@/utils/business/id'
import { import {
CommonStatus, CommonStatus,
getStatusText, getStatusText,
@@ -415,7 +416,7 @@
const roleList = ref<PlatformRole[]>([]) const roleList = ref<PlatformRole[]>([])
const getRoleId = (role: PlatformRole | any) => role.id ?? role.ID const getRoleId = (role: PlatformRole | any) => getCompatibleNumericId(role) ?? 0
const handleNameClick = (row: PlatformRole) => { const handleNameClick = (row: PlatformRole) => {
if (hasAuth('role:detail')) { if (hasAuth('role:detail')) {
@@ -1037,12 +1038,9 @@
if (Array.isArray(permissions) && permissions.length > 0) { if (Array.isArray(permissions) && permissions.length > 0) {
// 如果返回的是权限对象数组提取ID或id字段 // 如果返回的是权限对象数组提取ID或id字段
if (typeof permissions[0] === 'object') { if (typeof permissions[0] === 'object') {
// 优先使用ID字段如果没有则使用id字段 selectedPermissions.value = permissions
if ('ID' in permissions[0]) { .map((perm: any) => getCompatibleNumericId(perm))
selectedPermissions.value = permissions.map((perm: any) => perm.ID) .filter((id): id is number => id !== undefined)
} else if ('id' in permissions[0]) {
selectedPermissions.value = permissions.map((perm: any) => perm.id)
}
} else { } else {
// 如果返回的是ID数组 // 如果返回的是ID数组
selectedPermissions.value = permissions selectedPermissions.value = permissions
@@ -1189,7 +1187,9 @@
status: form.status status: form.status
} }
const res = await RoleService.createRole(data) const res = await RoleService.createRole(data)
const createdRoleId = res.data?.ID ?? res.data?.id ?? res.data?.role_id const createdRoleId =
getCompatibleNumericId(res.data) ??
(res.data?.role_id !== undefined ? Number(res.data.role_id) : undefined)
if (form.role_type === RoleType.CUSTOMER && createdRoleId) { if (form.role_type === RoleType.CUSTOMER && createdRoleId) {
await RoleService.updateRoleDefaultCredit(createdRoleId, getDefaultCreditPayload()) await RoleService.updateRoleDefaultCredit(createdRoleId, getDefaultCreditPayload())
} }