修复bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 11m14s

This commit is contained in:
sexygoat
2026-04-08 10:19:25 +08:00
parent ff087abd44
commit b510b4539f
55 changed files with 2274 additions and 2612 deletions

View File

@@ -56,6 +56,7 @@
"axios": "1.13.5",
"crypto-js": "^4.2.0",
"echarts": "^5.4.0",
"element-china-area-data": "^6.1.0",
"element-plus": "2.11.3",
"file-saver": "^2.0.5",
"highlight.js": "^11.10.0",

15
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
echarts:
specifier: ^5.4.0
version: 5.5.1
element-china-area-data:
specifier: ^6.1.0
version: 6.1.0
element-plus:
specifier: 2.11.3
version: 2.11.3(vue@3.5.15(typescript@5.6.3))
@@ -1638,6 +1641,9 @@ packages:
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
china-division@2.7.0:
resolution: {integrity: sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -2056,6 +2062,9 @@ packages:
echarts@5.5.1:
resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==}
element-china-area-data@6.1.0:
resolution: {integrity: sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==}
element-plus@2.11.3:
resolution: {integrity: sha512-769xsjLR4B9Vf9cl5PDXnwTEdmFJvMgAkYtthdJKPhjVjU3hdAwTJ+gXKiO+PUyo2KWFwOYKZd4Ywh6PHfkbJg==}
peerDependencies:
@@ -6519,6 +6528,8 @@ snapshots:
chardet@0.7.0: {}
china-division@2.7.0: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -6987,6 +6998,10 @@ snapshots:
tslib: 2.3.0
zrender: 5.6.0
element-china-area-data@6.1.0:
dependencies:
china-division: 2.7.0
element-plus@2.11.3(vue@3.5.15(typescript@5.6.3)):
dependencies:
'@ctrl/tinycolor': 3.6.1

View File

@@ -6,21 +6,18 @@
import { BaseService } from '../BaseService'
import type {
BaseResponse,
AssetType,
AssetResolveResponse,
AssetRealtimeStatusResponse,
AssetRefreshResponse,
AssetPackageUsageRecord,
AssetPackageListResponse,
AssetPackageParams,
AssetCurrentPackageResponse,
DeviceStopResponse,
DeviceStartResponse,
CardStopResponse,
CardStartResponse,
AssetWalletTransactionListResponse,
AssetWalletTransactionParams,
AssetWalletResponse
AssetWalletResponse,
AssetOrdersParams,
AssetOrdersResponse
} from '@/types/api'
export class AssetService extends BaseService {
@@ -36,148 +33,150 @@ export class AssetService extends BaseService {
/**
* 读取资产实时状态(直接读 DB/Redis不调网关
* GET /api/admin/assets/:asset_type/:id/realtime-status
* @param assetType 资产类型 (card 或 device)
* @param id 资产ID
* GET /api/admin/assets/:identifier/realtime-status
* @param identifier 资产标识符ICCID 或 VirtualNo
*/
static getRealtimeStatus(
assetType: AssetType,
id: number
): Promise<BaseResponse<AssetRealtimeStatusResponse>> {
static getRealtimeStatus(identifier: string): Promise<BaseResponse<AssetRealtimeStatusResponse>> {
return this.getOne<AssetRealtimeStatusResponse>(
`/api/admin/assets/${assetType}/${id}/realtime-status`
`/api/admin/assets/${identifier}/realtime-status`
)
}
/**
* 主动调网关拉取最新数据后返回
* POST /api/admin/assets/:asset_type/:id/refresh
* POST /api/admin/assets/:identifier/refresh
* 注意:设备有 30 秒冷却期,冷却中调用返回 429
* @param assetType 资产类型 (card 或 device)
* @param id 资产ID
* @param identifier 资产标识符ICCID 或 VirtualNo
*/
static refreshAsset(
assetType: AssetType,
id: number
): Promise<BaseResponse<AssetRefreshResponse>> {
static refreshAsset(identifier: string): Promise<BaseResponse<AssetRefreshResponse>> {
return this.post<BaseResponse<AssetRefreshResponse>>(
`/api/admin/assets/${assetType}/${id}/refresh`,
`/api/admin/assets/${identifier}/refresh`,
{}
)
}
/**
* 查询该资产所有套餐记录,含虚流量换算字段(分页)
* GET /api/admin/assets/:asset_type/:id/packages?page=1&page_size=50
* @param assetType 资产类型 (card 或 device)
* @param id 资产ID
* GET /api/admin/assets/:identifier/packages?page=1&page_size=50
* @param identifier 资产标识符ICCID 或 VirtualNo
* @param params 查询参数(可选分页参数)
*/
static getAssetPackages(
assetType: AssetType,
id: number,
identifier: string,
params?: AssetPackageParams
): Promise<BaseResponse<AssetPackageListResponse>> {
return this.get<BaseResponse<AssetPackageListResponse>>(
`/api/admin/assets/${assetType}/${id}/packages`,
`/api/admin/assets/${identifier}/packages`,
params
)
}
/**
* 查询当前生效中的主套餐
* GET /api/admin/assets/:asset_type/:id/current-package
* GET /api/admin/assets/:identifier/current-package
* 无生效套餐时返回 404
* @param assetType 资产类型 (card 或 device)
* @param id 资产ID
* @param identifier 资产标识符ICCID 或 VirtualNo
*/
static getCurrentPackage(
assetType: AssetType,
id: number
): Promise<BaseResponse<AssetCurrentPackageResponse>> {
static getCurrentPackage(identifier: string): Promise<BaseResponse<AssetCurrentPackageResponse>> {
return this.getOne<AssetCurrentPackageResponse>(
`/api/admin/assets/${assetType}/${id}/current-package`
`/api/admin/assets/${identifier}/current-package`
)
}
// ========== 设备停复机操作 ==========
// ========== 资产停复机操作(统一接口)==========
/**
* 批量停机设备下所有已实名卡
* POST /api/admin/assets/device/:device_id/stop
* 停机资产(卡或设备)
* POST /api/admin/assets/:identifier/stop
* 停机成功后设置 1 小时停机保护期(保护期内禁止复机)
* @param deviceId 设备ID
* @param identifier 资产标识符ICCID 或 VirtualNo
*/
static stopDevice(deviceId: number): Promise<BaseResponse<DeviceStopResponse>> {
return this.post<BaseResponse<DeviceStopResponse>>(
`/api/admin/assets/device/${deviceId}/stop`,
static stopAsset(identifier: string): Promise<BaseResponse<DeviceStopResponse | void>> {
return this.post<BaseResponse<DeviceStopResponse | void>>(
`/api/admin/assets/${identifier}/stop`,
{}
)
}
/**
* 批量复机设备下所有已实名卡
* POST /api/admin/assets/device/:device_id/start
* 复机资产(卡或设备)
* POST /api/admin/assets/:identifier/start
* 复机成功后设置 1 小时复机保护期(保护期内禁止停机)
* @param deviceId 设备ID
* @param identifier 资产标识符ICCID 或 VirtualNo
*/
static startDevice(deviceId: number): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/assets/device/${deviceId}/start`, {})
static startAsset(identifier: string): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/assets/${identifier}/start`, {})
}
// ========== 单卡停复机操作 ==========
// ========== 资产停用 ==========
/**
* 手动停机单张卡(通过 ICCID
* POST /api/admin/assets/card/:iccid/stop
* 若卡绑定的设备在复机保护期内,返回 403
* @param iccid ICCID
* 手动停用资产(卡或设备
* PATCH /api/admin/assets/:identifier/deactivate
* @param identifier 资产标识符ICCID 或 VirtualNo
*/
static stopCard(iccid: string): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/assets/card/${iccid}/stop`, {})
}
/**
* 手动复机单张卡(通过 ICCID
* POST /api/admin/assets/card/:iccid/start
* 若卡绑定的设备在停机保护期内,返回 403
* @param iccid ICCID
*/
static startCard(iccid: string): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/assets/card/${iccid}/start`, {})
static deactivateAsset(identifier: string): Promise<BaseResponse> {
return this.patch<BaseResponse>(`/api/admin/assets/${identifier}/deactivate`, {})
}
// ========== 钱包查询 ==========
/**
* 查询指定卡或设备的钱包余额概况
* GET /api/admin/assets/:asset_type/:id/wallet
* GET /api/admin/assets/:identifier/wallet
* 企业账号禁止调用
* @param assetType 资产类型 (card 或 device)
* @param id 资产ID
* @param identifier 资产标识符ICCID 或 VirtualNo
*/
static getAssetWallet(
assetType: AssetType,
id: number
): Promise<BaseResponse<AssetWalletResponse>> {
return this.getOne<AssetWalletResponse>(`/api/admin/assets/${assetType}/${id}/wallet`)
static getAssetWallet(identifier: string): Promise<BaseResponse<AssetWalletResponse>> {
return this.getOne<AssetWalletResponse>(`/api/admin/assets/${identifier}/wallet`)
}
/**
* 分页查询指定资产的钱包收支流水
* GET /api/admin/assets/:asset_type/:id/wallet/transactions
* GET /api/admin/assets/:identifier/wallet/transactions
* 企业账号禁止调用
* @param assetType 资产类型 (card 或 device)
* @param id 资产ID
* @param identifier 资产标识符ICCID 或 VirtualNo
* @param params 查询参数
*/
static getWalletTransactions(
assetType: AssetType,
id: number,
identifier: string,
params?: AssetWalletTransactionParams
): Promise<BaseResponse<AssetWalletTransactionListResponse>> {
return this.get<BaseResponse<AssetWalletTransactionListResponse>>(
`/api/admin/assets/${assetType}/${id}/wallet/transactions`,
`/api/admin/assets/${identifier}/wallet/transactions`,
params
)
}
// ========== 轮询状态更新 ==========
/**
* 更新资产轮询状态
* PATCH /api/admin/assets/:identifier/polling-status
* @param identifier 资产标识符ICCID 或 VirtualNo
* @param data 轮询状态数据
*/
static updatePollingStatus(
identifier: string,
data: { enable_polling: boolean }
): Promise<BaseResponse> {
return this.patch<BaseResponse>(`/api/admin/assets/${identifier}/polling-status`, data)
}
// ========== 资产历史订单 ==========
/**
* 查询资产历史订单
* GET /api/admin/assets/:identifier/orders
* @param identifier 资产标识符ICCID 或 VirtualNo
* @param params 查询参数
*/
static getAssetOrders(
identifier: string,
params?: AssetOrdersParams
): Promise<BaseResponse<AssetOrdersResponse>> {
return this.get<BaseResponse<AssetOrdersResponse>>(
`/api/admin/assets/${identifier}/orders`,
params
)
}

View File

@@ -56,45 +56,48 @@ export class DeviceService extends BaseService {
/**
* 删除设备
* @param id 设备ID
* @param virtualNo 设备虚拟号
*/
static deleteDevice(id: number): Promise<BaseResponse> {
return this.remove(`/api/admin/devices/${id}`)
static deleteDevice(virtualNo: string): Promise<BaseResponse> {
return this.remove(`/api/admin/devices/${virtualNo}`)
}
// ========== 设备卡绑定管理 ==========
/**
* 获取设备绑定的卡列表
* @param id 设备ID
* @param virtualNo 设备虚拟号
*/
static getDeviceCards(id: number): Promise<BaseResponse<DeviceCardsResponse>> {
return this.getOne<DeviceCardsResponse>(`/api/admin/devices/${id}/cards`)
static getDeviceCards(virtualNo: string): Promise<BaseResponse<DeviceCardsResponse>> {
return this.getOne<DeviceCardsResponse>(`/api/admin/devices/${virtualNo}/cards`)
}
/**
* 绑定卡到设备
* @param id 设备ID
* @param virtualNo 设备虚拟号
* @param data 绑定参数
*/
static bindCard(
id: number,
virtualNo: string,
data: BindCardToDeviceRequest
): Promise<BaseResponse<BindCardToDeviceResponse>> {
return this.post<BaseResponse<BindCardToDeviceResponse>>(`/api/admin/devices/${id}/cards`, data)
return this.post<BaseResponse<BindCardToDeviceResponse>>(
`/api/admin/devices/${virtualNo}/cards`,
data
)
}
/**
* 解绑设备上的卡
* @param deviceId 设备ID
* @param cardId IoT卡ID
* @param virtualNo 设备虚拟号
* @param iccid 卡ICCID
*/
static unbindCard(
deviceId: number,
cardId: number
virtualNo: string,
iccid: string
): Promise<BaseResponse<UnbindCardFromDeviceResponse>> {
return this.delete<BaseResponse<UnbindCardFromDeviceResponse>>(
`/api/admin/devices/${deviceId}/cards/${cardId}`
`/api/admin/devices/${virtualNo}/cards/${iccid}`
)
}
@@ -230,12 +233,4 @@ export class DeviceService extends BaseService {
data
)
}
/**
* 手动停用设备
* @param id 设备ID
*/
static deactivateDevice(id: number): Promise<BaseResponse> {
return this.patch<BaseResponse>(`/api/admin/devices/${id}/deactivate`, {})
}
}

View File

@@ -66,10 +66,10 @@ export class ExchangeService extends BaseService {
static getExchanges(
params?: ExchangeQueryParams
): Promise<
BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>
BaseResponse<{ items: ExchangeResponse[]; page: number; page_size: number; total: number }>
> {
return this.get<
BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>
BaseResponse<{ items: ExchangeResponse[]; page: number; page_size: number; total: number }>
>('/api/admin/exchanges', params)
}

View File

@@ -279,19 +279,6 @@
background-color: #fff !important;
transform: scale(0.6) !important;
}
&::after {
width: 4px;
height: 8px;
left: 0;
right: 0;
top: 0;
bottom: 4px;
margin: auto;
border: 2px solid var(--el-checkbox-checked-icon-color);
border-left: 0;
border-top: 0;
}
}
}

View File

@@ -47,6 +47,21 @@
<!-- 动态列 -->
<slot v-if="tableData.length"></slot>
<!-- 操作列固定在右侧 -->
<el-table-column
v-if="actions && tableData.length > 0"
label="操作"
:width="actionsWidth"
fixed="right"
align="left"
>
<template #default="{ row }">
<div class="table-actions">
<component :is="renderActions(row)" />
</div>
</template>
</el-table-column>
<!-- 空数据 -->
<template #empty>
<el-empty :description="emptyText" v-show="!loading" />
@@ -78,10 +93,20 @@
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElButton, ElDropdown, ElDropdownMenu, ElDropdownItem, ElIcon } from 'element-plus'
import { ArrowDown } from '@element-plus/icons-vue'
import { useTableStore } from '@/store/modules/table'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
interface TableAction {
label: string
handler: (row: any) => void
type?: 'primary' | 'danger'
permission?: string
}
interface TableProps {
/** 表格数据源 */
data?: any[]
@@ -135,6 +160,10 @@
treeProps?: { children?: string; hasChildren?: string }
/** 是否默认展开所有行 */
defaultExpandAll?: boolean
/** 操作列配置 */
actions?: ((row: any) => TableAction[]) | null
/** 操作列宽度 */
actionsWidth?: number
}
const props = withDefaults(defineProps<TableProps>(), {
@@ -161,7 +190,9 @@
showHeaderBackground: null,
marginTop: 20,
treeProps: undefined,
defaultExpandAll: false
defaultExpandAll: false,
actions: null,
actionsWidth: 140
})
/*
@@ -324,6 +355,78 @@
}
})
}
// 渲染操作列
const renderActions = (row: any) => {
if (!props.actions) return null
const actionList = props.actions(row)
// 如果操作数量 <= 2直接显示所有按钮
if (actionList.length <= 2) {
return h(
'div',
{ style: 'display: flex; gap: 8px;' },
actionList.map((action) =>
h(
ElButton,
{
type: action.type === 'danger' ? 'danger' : 'primary',
link: true,
onClick: () => action.handler(row)
},
() => action.label
)
)
)
}
// 如果操作数量 > 2显示第一个 + 更多下拉菜单
const firstAction = actionList[0]
const moreActions = actionList.slice(1)
return h('div', { style: 'display: flex; gap: 12px; align-items: center;' }, [
h(
ElButton,
{
type: firstAction.type === 'danger' ? 'danger' : 'primary',
link: true,
onClick: () => firstAction.handler(row)
},
() => firstAction.label
),
h(
ElDropdown,
{
onCommand: (handler: (row: any) => void) => handler(row)
},
{
default: () =>
h(
'span',
{
style:
'display: flex; align-items: center; cursor: pointer; color: var(--el-color-primary);'
},
[h('span', '更多'), h(ElIcon, { style: 'margin-left: 4px;' }, () => h(ArrowDown))]
),
dropdown: () =>
h(ElDropdownMenu, {}, () =>
moreActions.map((action) =>
h(
ElDropdownItem,
{
command: () => action.handler(row),
style: action.type === 'danger' ? 'color: var(--el-color-danger);' : ''
},
() => action.label
)
)
)
}
)
])
}
</script>
<style lang="scss" scoped>
@@ -365,9 +468,15 @@
}
// 解决el-image 和 el-table冲突层级冲突问题
::v-deep(.el-table__cell) {
// 只对包含图片的单元格应用,避免影响 fixed 列
::v-deep(.el-table__cell:has(.el-image)) {
position: static !important;
}
// 确保固定列能正常工作
:deep(.el-table__fixed-right) {
right: 0 !important;
}
}
// 移动端分页

View File

@@ -12,7 +12,7 @@
controls-position="right"
style="width: 100%"
/>
<div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
</ElFormItem>
<ElFormItem label="上行速率" prop="upload_speed">
<ElInputNumber
@@ -22,7 +22,7 @@
controls-position="right"
style="width: 100%"
/>
<div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
</ElFormItem>
</ElForm>
<template #footer>

View File

@@ -1,6 +1,6 @@
<template>
<ElDialog v-model="visible" title="切换SIM卡" width="600px" @close="handleClose">
<div v-if="loading" style=" padding: 40px;text-align: center">
<div v-if="loading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
</div>

View File

@@ -185,6 +185,12 @@ export function useLogin() {
return
}
// 检查用户类型 - 禁止企业账号登录
if (response.data.user && response.data.user.user_type === 4) {
ElMessage.error('企业账号无法登录后台管理系统')
return
}
// 保存 Token
userStore.setToken(response.data.access_token, response.data.refresh_token)

View File

@@ -0,0 +1,15 @@
/**
* 启用状态配置
* 用于一次性佣金、强充等功能的启用/未启用状态
*/
// 启用状态选项(用于搜索表单和下拉选择)
export const ENABLE_STATUS_OPTIONS = [
{ label: '已启用', value: true },
{ label: '未启用', value: false }
]
// 获取启用状态文本
export function getEnableStatusText(enabled: boolean): string {
return enabled ? '已启用' : '未启用'
}

View File

@@ -25,3 +25,9 @@ export * from './carrierTypes'
// 套餐管理相关
export * from './package'
// 角色类型相关
export * from './roleTypes'
// 启用状态相关
export * from './enableStatus'

View File

@@ -132,6 +132,14 @@ export const SHELF_STATUS_OPTIONS = [
}
]
/**
* 上架状态选项(精简版,用于搜索表单和下拉选择)
*/
export const SHELF_STATUS_SELECT_OPTIONS = [
{ label: '上架', value: ShelfStatus.ON_SHELF },
{ label: '下架', value: ShelfStatus.OFF_SHELF }
]
/**
* 上架状态映射
*/

View File

@@ -0,0 +1,25 @@
/**
* 角色类型配置
*/
import { RoleType } from '@/types/api'
// 角色类型选项
export const ROLE_TYPE_OPTIONS = [
{ label: '平台角色', value: RoleType.PLATFORM },
{ label: '客户角色', value: RoleType.CUSTOMER }
]
// 角色类型映射
export const ROLE_TYPE_MAP = ROLE_TYPE_OPTIONS.reduce(
(map, item) => {
map[item.value] = item
return map
},
{} as Record<RoleType, { label: string; value: RoleType }>
)
// 获取角色类型标签
export function getRoleTypeLabel(type: RoleType): string {
return ROLE_TYPE_MAP[type]?.label || '未知'
}

View File

@@ -348,6 +348,7 @@
"title": "System Settings",
"role": "Role Manage",
"permission": "Permission Manage",
"carrierManagement": "Carrier Management",
"userCenter": "User Center",
"menu": "Menu Manage",
"nested": "Nested Menu",
@@ -414,14 +415,9 @@
"enterpriseCards": "Enterprise Card Management",
"customerCommission": "Customer Commission"
},
"account": {
"title": "Account Management",
"customerAccount": "Customer Account",
"withdrawal": "Commission Withdrawal",
"withdrawalSettings": "Withdrawal Settings",
"myAccount": "My Account",
"carrierManagement": "Carrier Management",
"orders": "Order Management",
"orderManagement": {
"title": "Order Management",
"orderList": "Order List",
"orderDetail": "Order Details"
},
"deviceManagement": {
@@ -435,13 +431,17 @@
"packageSeries": "Package Series Management",
"packageList": "Package Management",
"packageAssign": "Package Assignment",
"shop": "Shop Management",
"shopAccounts": "Shop Accounts"
},
"shopManagement": {
"title": "Shop Management",
"shopList": "Shop List"
},
"assetManagement": {
"title": "Asset Management",
"cardSearch": "IoT Card Query",
"deviceSearch": "Device Search",
"assetInformation": "Asset Information",
"singleCard": "Single Card Info",
"standaloneCardList": "IoT Card Management",
"iotCardTask": "IoT Card Tasks",
@@ -844,7 +844,8 @@
"paymentMethod": {
"wallet": "Wallet Payment",
"wechat": "WeChat Payment",
"alipay": "Alipay Payment"
"alipay": "Alipay Payment",
"offline": "Offline Payment"
},
"commissionStatus": {
"notApplicable": "Not Applicable",

View File

@@ -360,6 +360,7 @@
"title": "系统管理",
"role": "角色管理",
"permission": "权限管理",
"carrierManagement": "运营商管理",
"userCenter": "个人中心",
"menu": "菜单管理",
"nested": "嵌套菜单",
@@ -401,7 +402,7 @@
"title": "套餐管理",
"packageCreate": "新建套餐",
"packageBatch": "批量管理",
"packageList": "套餐管理",
"packageList": "套餐列表",
"packageDetail": "套餐详情",
"packageChange": "套餐变更",
"seriesGrants": "代理系列授权",
@@ -429,15 +430,19 @@
"simCard": "号卡管理",
"simCardAssign": "号卡分配",
"packageSeries": "套餐系列管理",
"packageList": "套餐管理",
"packageList": "套餐列表",
"packageAssign": "套餐分配",
"shop": "店铺管理",
"shopAccounts": "店铺账号列表"
},
"shopManagement": {
"title": "店铺管理",
"shopList": "店铺列表"
},
"assetManagement": {
"title": "资产管理",
"cardSearch": "IoT卡查询",
"deviceSearch": "设备查询",
"assetInformation": "资产信息",
"singleCard": "单卡信息",
"standaloneCardList": "IoT卡管理",
"iotCardDetail": "IoT卡详情",
@@ -459,12 +464,9 @@
"exchangeManagement": "换货管理",
"exchangeDetail": "换货单详情"
},
"account": {
"orderManagement": {
"title": "订单管理",
"customerAccount": "客户账号",
"myAccount": "我的账户",
"carrierManagement": "运营商管理",
"orders": "订单管理",
"orderList": "订单列表",
"orderDetail": "订单详情"
},
"commission": {
@@ -842,7 +844,8 @@
"paymentMethod": {
"wallet": "钱包支付",
"wechat": "微信支付",
"alipay": "支付宝支付"
"alipay": "支付宝支付",
"offline": "线下支付"
},
"commissionStatus": {
"notApplicable": "不适用",

View File

@@ -303,7 +303,7 @@ export const asyncRoutes: AppRouteRecord[] = [
name: 'CarrierManagement',
component: RoutesAlias.CarrierManagement,
meta: {
title: 'menus.account.carrierManagement',
title: 'menus.system.carrierManagement',
keepAlive: true
}
},
@@ -790,7 +790,7 @@ export const asyncRoutes: AppRouteRecord[] = [
name: 'ShopManagement',
component: RoutesAlias.Home,
meta: {
title: 'menus.product.shop',
title: 'menus.shopManagement.title',
icon: '&#xe81a;'
},
children: [
@@ -799,7 +799,7 @@ export const asyncRoutes: AppRouteRecord[] = [
name: 'Shop',
component: RoutesAlias.Shop,
meta: {
title: 'menus.product.shop',
title: 'menus.shopManagement.shopList',
keepAlive: true
}
}
@@ -968,11 +968,11 @@ export const asyncRoutes: AppRouteRecord[] = [
},
children: [
{
path: 'single-card',
name: 'SingleCard',
component: RoutesAlias.SingleCard,
path: 'asset-information',
name: 'AssetInformation',
component: RoutesAlias.AssetInformation,
meta: {
title: 'menus.cardManagement.singleCard',
title: 'menus.assetManagement.assetInformation',
keepAlive: true
}
},
@@ -1121,51 +1121,33 @@ export const asyncRoutes: AppRouteRecord[] = [
},
{
path: '/account',
name: 'Finance',
path: '/order-management',
name: 'OrderManagement',
component: RoutesAlias.Home,
meta: {
title: 'menus.account.title',
title: 'menus.orderManagement.title',
icon: '&#xe7ae;'
},
children: [
// {
// path: 'customer-account',
// name: 'CustomerAccountList',
// component: RoutesAlias.CustomerAccountList,
// meta: {
// title: 'menus.account.customerAccount',
// keepAlive: true
// }
// },
{
path: 'orders',
name: 'OrderManagement',
component: '/order-management/order-list',
path: 'order-list',
name: 'OrderList',
component: RoutesAlias.OrderList,
meta: {
title: 'menus.account.orders',
title: 'menus.orderManagement.orderList',
keepAlive: true
}
},
{
path: 'orders/detail/:id',
path: 'order-list/detail/:id',
name: 'OrderDetail',
component: '/order-management/order-list/detail',
component: RoutesAlias.OrderDetail,
meta: {
title: 'menus.account.orderDetail',
title: 'menus.orderManagement.orderDetail',
isHide: true,
keepAlive: false
}
}
// {
// path: 'my-account',
// name: 'MyAccount',
// component: RoutesAlias.MyAccount,
// meta: {
// title: 'menus.account.myAccount',
// keepAlive: true
// }
// }
]
},

View File

@@ -2,18 +2,94 @@
* 路由别名,方便快速找到页面,同时可以用作路由跳转
*/
export enum RoutesAlias {
// 登录 布局容器
Home = '/index/index', // 布局容器
Login = '/auth/login', // 登录
Register = '/auth/register', // 注册
ForgetPassword = '/auth/forget-password', // 忘记密码
// 异常页面
Exception403 = '/exception/403', // 403
Exception404 = '/exception/404', // 404
Exception500 = '/exception/500', // 500
Success = '/result/success', // 成功
Fail = '/result/fail', // 失败
// 仪表台
Dashboard = '/dashboard/console', // 工作台
Analysis = '/dashboard/analysis', // 分析页
Ecommerce = '/dashboard/ecommerce', // 电子商务
// 系统管理
Role = '/system/role', // 角色
Permission = '/system/permission', // 权限管理
CarrierManagement = '/system/carrier-management', // 运营商管理
UserCenter = '/system/user-center', // 用户中心
// 套餐管理
PackageList = '/package-management/package-list', // 套餐列表
PackageDetail = '/package-management/package-list/detail', // 套餐列表详情
PackageSeries = '/package-management/package-series', // 套餐系列
PackageSeriesDetail = '/package-management/package-series/detail', // 套餐系列详情
SeriesGrants = '/package-management/series-grants', // 代理系列授权
SeriesGrantsDetail = '/package-management/series-grants/detail', // 代理系列授权详情
// 店铺管理
Shop = '/shop-management/list', // 店铺列表
// 通用页面(店铺账号-企业账号)
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表(通用)
// 账号管理
Account = '/account-management/account', // 账号列表
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
EnterpriseCards = '/account-management/enterprise-cards', // 企业卡授权
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备授权
// 资产管理
AssetInformation = '/asset-management/asset-information', // 资产信息
StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理
DeviceList = '/asset-management/device-list', // 设备列表
ExchangeManagement = '/asset-management/exchange-management', // 换货管理
ExchangeDetail = '/asset-management/exchange-management/detail', // 换货单详情
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
AuthorizationRecordDetail = '/asset-management/authorization-records/detail', // 授权记录详情
AssetAssign = '/asset-management/asset-assign', // 分配记录
AssetAssignDetail = '/asset-management/asset-assign/detail', // 分配记录详情
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
DeviceTask = '/asset-management/device-task', // 设备任务
TaskDetail = '/asset-management/task-detail', // 任务详情IoT卡/设备任务详情)
// 订单管理
OrderList = '/order-management/order-list', // 订单列表
OrderDetail = '/order-management/order-list/detail', // 订单详情
// 财务管理
AgentRecharge = '/finance/agent-recharge', // 代理充值
AgentRechargeDetail = '/finance/agent-recharge/detail', // 代理充值详情
RefundManagement = '/finance/refund', // 退款管理
RefundDetail = '/finance/refund/detail', // 退款详情
// 佣金管理
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
CommissionWithdrawalSettings = '/finance/commission/withdrawal-settings', // 提现配置
MyCommission = '/finance/commission/my-commission', // 我的佣金
AgentCommission = '/finance/commission/agent-commission', // 代理商佣金管理
// 设置管理
WechatConfig = '/settings/wechat-config', // 微信支付配置
WechatConfigDetail = '/settings/wechat-config/detail', // 微信支付配置详情
// 轮询管理
DataCleanup = '/polling-management/data-cleanup', // 数据清理
AlertRules = '/polling-management/alert-rules', // 告警规则
AlertHistory = '/polling-management/alert-history', // 告警历史
PollingConcurrency = '/polling-management/concurrency', // 并发控制
PollingConfig = '/polling-management/config', // 轮询配置
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
PollingMonitor = '/polling-management/monitor', // 轮询监控
// 其他页面------------------------------------------------暂时不知道有没有用到不要动
Register = '/auth/register', // 注册
ForgetPassword = '/auth/forget-password', // 忘记密码
Success = '/result/success', // 成功
Fail = '/result/fail', // 失败
IconList = '/widgets/icon-list', // 图标列表
IconSelector = '/widgets/icon-selector', // 图标选择器
ImageCrop = '/widgets/image-crop', // 图片裁剪
@@ -38,15 +114,6 @@ export enum RoutesAlias {
ArticleDetail = '/article/detail', // 文章详情
Comment = '/article/comment', // 评论
ArticlePublish = '/article/publish', // 文章发布
Role = '/system/role', // 角色
Permission = '/system/permission', // 权限管理
Shop = '/product/shop', // 店铺管理
UserCenter = '/system/user-center', // 用户中心
Menu = '/system/menu', // 菜单
NestedMenu1 = '/system/nested/menu1', // 嵌套菜单1
NestedMenu21 = '/system/nested/menu2', // 嵌套菜单2-1
NestedMenu31 = '/system/nested/menu3', // 嵌套菜单3-1
NestedMenu321 = '/system/nested/menu3/menu3-2', // 嵌套菜单3-2-1
Server = '/safeguard/server', // 服务器
ChangeLog = '/change/log', // 更新日志
@@ -54,7 +121,7 @@ export enum RoutesAlias {
// 我的网卡
CardList = '/card-management/card-list', // 网卡管理
CardDetail = '/card-management/card-detail', // 网卡明细
SingleCard = '/my-simcard/single-card', // 单卡信息
CardAssign = '/card-management/card-assign', // 网卡分配
CardShutdown = '/card-management/card-shutdown', // 停机管理
MyCards = '/card-management/my-cards', // 我的网卡
@@ -64,28 +131,18 @@ export enum RoutesAlias {
PackageGift = '/card-management/package-gift', // 套餐赠送
CardChangeCard = '/card-management/card-change-card', // 换卡网卡
// 我的套餐
// other
PackageCreate = '/package-management/package-create', // 新建套餐
PackageBatch = '/package-management/package-batch', // 批量管理
PackageList = '/package-management/package-list', // 套餐管理
PackageDetail = '/package-management/package-list/detail', // 套餐详情
PackageChange = '/package-management/package-change', // 套餐变更
SeriesGrants = '/package-management/series-grants', // 代理系列授权
SeriesGrantsDetail = '/package-management/series-grants/detail', // 代理系列授权详情
PackageSeries = '/package-management/package-series', // 套餐系列
PackageSeriesDetail = '/package-management/package-series/detail', // 套餐系列详情
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
// 账号管理
Account = '/account-management/account', // 账号管理
// PlatformAccount = '/account-management/platform-account', // 平台账号管理
CustomerManagement = '/account-management/customer', // 客户管理
CustomerRole = '/account-management/customer-role', // 客户角色
AgentManagement = '/account-management/agent', // 代理商管理
ShopAccount = '/account-management/shop-account', // 代理账号管理
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表(通用)
EnterpriseCards = '/account-management/enterprise-cards', // 企业卡管理
CustomerCommission = '/account-management/customer-commission', // 客户账号佣金
// 产品管理
@@ -94,55 +151,17 @@ export enum RoutesAlias {
ShopAccounts = '/common/account-list', // 店铺账号列表(通用)
// 资产管理
StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
DeviceTask = '/asset-management/device-task', // 设备任务
TaskDetail = '/asset-management/task-detail', // 任务详情IoT卡/设备任务详情)
DeviceList = '/asset-management/device-list', // 设备列表
DeviceDetail = '/asset-management/device-detail', // 设备详情
AssetAssign = '/asset-management/asset-assign', // 资产分配(分配记录)
AssetAssignDetail = '/asset-management/asset-assign/detail', // 资产分配详情
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
AuthorizationRecordDetail = '/asset-management/authorization-records/detail', // 授权记录详情
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备列表
ExchangeManagement = '/asset-management/exchange-management', // 换货管理
ExchangeDetail = '/asset-management/exchange-management/detail', // 换货单详情
// 账户管理
CustomerAccountList = '/finance/customer-account', // 客户账号
MyAccount = '/finance/my-account', // 我的账户
CarrierManagement = '/finance/carrier-management', // 运营商管理
OrderList = '/account/orders', // 订单管理
OrderDetail = '/account/orders/detail', // 订单详情
AgentRecharge = '/finance/agent-recharge', // 代理充值
AgentRechargeDetail = '/finance/agent-recharge/detail', // 代理充值详情
// 佣金管理
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
CommissionWithdrawalSettings = '/finance/commission/withdrawal-settings', // 提现配置
MyCommission = '/finance/commission/my-commission', // 我的佣金
AgentCommission = '/finance/commission/agent-commission', // 代理商佣金管理
// 设置管理
PaymentMerchant = '/settings/payment-merchant', // 支付商户
DeveloperApi = '/settings/developer-api', // 开发者API
CommissionTemplate = '/settings/commission-template', // 分佣模板
WechatConfig = '/settings/wechat-config', // 微信支付配置
WechatConfigDetail = '/settings/wechat-config/detail', // 微信支付配置详情
// 退款管理
RefundManagement = '/finance/refund', // 退款管理
RefundDetail = '/finance/refund/detail', // 退款详情
// 轮询管理
DataCleanup = '/polling-management/data-cleanup', // 数据清理
AlertRules = '/polling-management/alert-rules', // 告警规则
AlertHistory = '/polling-management/alert-history', // 告警历史
PollingConcurrency = '/polling-management/concurrency', // 并发控制
PollingConfig = '/polling-management/config', // 轮询配置
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
PollingMonitor = '/polling-management/monitor' // 轮询监控
CommissionTemplate = '/settings/commission-template' // 分佣模板
}
// 主页路由

View File

@@ -47,6 +47,7 @@ export type UsageType = 'single_card' | 'device'
export interface AssetResolveResponse {
asset_type: AssetType // 资产类型card 或 device
asset_id: number // 数据库 ID
identifier?: string // 原样回传本次查询所用的标识符
virtual_no: string // 虚拟号
status: number // 资产状态
batch_no: string // 批次号
@@ -97,6 +98,7 @@ export interface AssetResolveResponse {
software_version?: string // 固件版本号(仅 device
switch_mode?: string // 切卡模式:"0"=自动, "1"=手动(仅 device
last_gateway_sync_at?: string | null // 最后 sync-info 同步时间(仅 device
extend?: any // 扩展信息字段
}
/**
@@ -343,3 +345,64 @@ export interface AssetWalletResponse {
created_at?: string // 创建时间RFC3339
updated_at?: string // 更新时间RFC3339
}
// ========== 资产历史订单 ==========
/**
* 资产历史订单查询参数
*/
export interface AssetOrdersParams {
page?: number // 页码,默认 1
page_size?: number // 每页条数,默认 20最大 100
include_previous?: boolean // 是否追溯换货前代,默认 false
}
/**
* 订单项
*/
export interface AssetOrderItem {
package_name: string // 套餐名称
quantity: number // 数量
unit_price: number // 单价(分)
amount: number // 金额(分)
}
/**
* 订单信息
*/
export interface AssetOrder {
order_no: string // 订单号
order_type: string // 订单类型
payment_status: number // 支付状态
payment_status_text: string // 支付状态文本
total_amount: number // 总金额(分)
payment_method: string // 支付方式
paid_at?: string // 支付时间
generation?: number // 代数
items: AssetOrderItem[] // 订单项
created_at: string // 创建时间
}
/**
* 代订单信息
*/
export interface GenerationOrders {
generation: number // 代数
identifier: string // 标识符
asset_type: string // 资产类型
exchange_no?: string // 换货单号(前代才有)
exchanged_at?: string // 换货时间(前代才有)
total: number // 订单总数
page?: number // 页码(当前代才有)
page_size?: number // 每页条数(当前代才有)
items: AssetOrder[] // 订单列表
}
/**
* 资产历史订单响应
*/
export interface AssetOrdersResponse {
current_generation: GenerationOrders // 当前代订单
previous_generations?: GenerationOrders[] // 前代订单(仅 include_previous=true 时有)
truncated: boolean // 是否截断(换货链超过 10 代)
}

View File

@@ -50,6 +50,8 @@ export interface Device {
software_version?: string // 固件版本号
switch_mode?: SwitchMode // 切卡模式:"0"=自动, "1"=手动
last_gateway_sync_at?: string | null // 最后 sync-info 同步时间
imei?: string // IMEI设备国际移动设备识别码
sn?: string // 设备序列号
}
// 设备查询参数
@@ -64,6 +66,7 @@ export interface DeviceQueryParams extends PaginationParams {
created_at_start?: string // 创建时间起始
created_at_end?: string // 创建时间结束
series_id?: number // 套餐系列ID
keyword?: string // 关键词搜索虚拟号或IMEI
}
// 设备列表响应

View File

@@ -52,6 +52,8 @@ export interface Order {
commission_config_version: number
device_id: number | null
iot_card_id: number | null
asset_identifier?: string // 资产标识符快照ICCID 或 VirtualNo
asset_type?: string // 资产类型single_card 或 device
items: OrderItem[] | null
created_at: string
updated_at: string
@@ -64,6 +66,7 @@ export interface OrderQueryParams {
payment_status?: PaymentStatus
order_type?: OrderType
order_no?: string
identifier?: string // 按资产标识符过滤
start_time?: string
end_time?: string
dateRange?: string[] | any // For date range picker in UI
@@ -80,10 +83,8 @@ export interface OrderListResponse {
// 创建订单请求
export interface CreateOrderRequest {
order_type: OrderType
package_ids: number[]
iot_card_id?: number | null
device_id?: number | null
identifier: string // 资产标识符ICCID 或 VirtualNo
package_ids: number[] // 套餐ID列表
payment_method: OrderPaymentMethod // 支付方式
}
@@ -93,8 +94,8 @@ export type CreateOrderResponse = Order
// 套餐购买预检请求
export interface PurchaseCheckRequest {
order_type: OrderType // 订单类型 (single_card:单卡购买, device:设备购买)
package_ids: number[] // 套餐ID列表
resource_id: number // 资源ID (IoT卡ID或设备ID)
package_ids: number[] // 套餐ID列表
}
// 套餐购买预检响应

View File

@@ -7,19 +7,6 @@
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const ElButton: (typeof import('element-plus/es'))['ElButton']
const ElButton2: (typeof import('element-plus/es'))['ElButton2']
const ElDropdown: (typeof import('element-plus/es'))['ElDropdown']
const ElDropdown2: (typeof import('element-plus/es'))['ElDropdown2']
const ElDropdownItem: (typeof import('element-plus/es'))['ElDropdownItem']
const ElDropdownMenu: (typeof import('element-plus/es'))['ElDropdownMenu']
const ElMessage: (typeof import('element-plus/es'))['ElMessage']
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
const ElPopover: (typeof import('element-plus/es'))['ElPopover']
const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn']
const ElTag: (typeof import('element-plus/es'))['ElTag']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']

View File

@@ -27,7 +27,7 @@ export type KeyValue<T = any> = {
// 选项类型
export interface Option {
label: string
value: string | number
value: string | number | boolean
disabled?: boolean
children?: Option[]
}

View File

@@ -61,7 +61,6 @@ declare module 'vue' {
ArtSidebarMenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/index.vue')['default']
ArtStatsCard: typeof import('./../components/core/cards/ArtStatsCard.vue')['default']
ArtTable: typeof import('./../components/core/tables/ArtTable.vue')['default']
ArtTableColumn: typeof import('./../components/core/tables/ArtTableColumn.vue')['default']
ArtTableFullScreen: typeof import('./../components/core/tables/ArtTableFullScreen.vue')['default']
ArtTableHeader: typeof import('./../components/core/tables/ArtTableHeader.vue')['default']
ArtTextScroll: typeof import('./../components/core/text-effect/ArtTextScroll.vue')['default']
@@ -86,12 +85,8 @@ declare module 'vue' {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
@@ -107,40 +102,25 @@ declare module 'vue' {
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSegmented: typeof import('element-plus/es')['ElSegmented']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
ElStep: typeof import('element-plus/es')['ElStep']
ElSteps: typeof import('element-plus/es')['ElSteps']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTransfer: typeof import('element-plus/es')['ElTransfer']
ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
ElWatermark: typeof import('element-plus/es')['ElWatermark']

View File

@@ -1,220 +1,26 @@
/**
* 省市区三级联动数据
* 简化版数据,包含主要省市区
* 使用 element-china-area-data 包提供完整的中国省市区数据
*/
import { regionData as chinaRegionData } from 'element-china-area-data'
export interface RegionItem {
value: string
label: string
children?: RegionItem[]
}
export const regionData: RegionItem[] = [
{
value: '北京市',
label: '北京市',
children: [
{
value: '北京市',
label: '北京市',
children: [
{ value: '东城区', label: '东城区' },
{ value: '西城区', label: '西城区' },
{ value: '朝阳区', label: '朝阳区' },
{ value: '丰台区', label: '丰台区' },
{ value: '石景山区', label: '石景山区' },
{ value: '海淀区', label: '海淀区' },
{ value: '门头沟区', label: '门头沟区' },
{ value: '房山区', label: '房山区' },
{ value: '通州区', label: '通州区' },
{ value: '顺义区', label: '顺义区' },
{ value: '昌平区', label: '昌平区' },
{ value: '大兴区', label: '大兴区' },
{ value: '怀柔区', label: '怀柔区' },
{ value: '平谷区', label: '平谷区' },
{ value: '密云区', label: '密云区' },
{ value: '延庆区', label: '延庆区' }
]
}
]
},
{
value: '上海市',
label: '上海市',
children: [
{
value: '上海市',
label: '上海市',
children: [
{ value: '黄浦区', label: '黄浦区' },
{ value: '徐汇区', label: '徐汇区' },
{ value: '长宁区', label: '长宁区' },
{ value: '静安区', label: '静安区' },
{ value: '普陀区', label: '普陀区' },
{ value: '虹口区', label: '虹口区' },
{ value: '杨浦区', label: '杨浦区' },
{ value: '闵行区', label: '闵行区' },
{ value: '宝山区', label: '宝山区' },
{ value: '嘉定区', label: '嘉定区' },
{ value: '浦东新区', label: '浦东新区' },
{ value: '金山区', label: '金山区' },
{ value: '松江区', label: '松江区' },
{ value: '青浦区', label: '青浦区' },
{ value: '奉贤区', label: '奉贤区' },
{ value: '崇明区', label: '崇明区' }
]
}
]
},
{
value: '广东省',
label: '广东省',
children: [
{
value: '广州市',
label: '广州市',
children: [
{ value: '荔湾区', label: '荔湾区' },
{ value: '越秀区', label: '越秀区' },
{ value: '海珠区', label: '海珠区' },
{ value: '天河区', label: '天河区' },
{ value: '白云区', label: '白云区' },
{ value: '黄埔区', label: '黄埔区' },
{ value: '番禺区', label: '番禺区' },
{ value: '花都区', label: '花都区' },
{ value: '南沙区', label: '南沙区' },
{ value: '从化区', label: '从化区' },
{ value: '增城区', label: '增城区' }
]
},
{
value: '深圳市',
label: '深圳市',
children: [
{ value: '罗湖区', label: '罗湖区' },
{ value: '福田区', label: '福田区' },
{ value: '南山区', label: '南山区' },
{ value: '宝安区', label: '宝安区' },
{ value: '龙岗区', label: '龙岗区' },
{ value: '盐田区', label: '盐田区' },
{ value: '龙华区', label: '龙华区' },
{ value: '坪山区', label: '坪山区' },
{ value: '光明区', label: '光明区' }
]
},
{
value: '东莞市',
label: '东莞市',
children: [
{ value: '莞城区', label: '莞城区' },
{ value: '南城区', label: '南城区' },
{ value: '东城区', label: '东城区' },
{ value: '万江区', label: '万江区' }
]
},
{
value: '佛山市',
label: '佛山市',
children: [
{ value: '禅城区', label: '禅城区' },
{ value: '南海区', label: '南海区' },
{ value: '顺德区', label: '顺德区' },
{ value: '三水区', label: '三水区' },
{ value: '高明区', label: '高明区' }
]
}
]
},
{
value: '浙江省',
label: '浙江省',
children: [
{
value: '杭州市',
label: '杭州市',
children: [
{ value: '上城区', label: '上城区' },
{ value: '拱墅区', label: '拱墅区' },
{ value: '西湖区', label: '西湖区' },
{ value: '滨江区', label: '滨江区' },
{ value: '萧山区', label: '萧山区' },
{ value: '余杭区', label: '余杭区' },
{ value: '临平区', label: '临平区' },
{ value: '钱塘区', label: '钱塘区' },
{ value: '富阳区', label: '富阳区' },
{ value: '临安区', label: '临安区' }
]
},
{
value: '宁波市',
label: '宁波市',
children: [
{ value: '海曙区', label: '海曙区' },
{ value: '江北区', label: '江北区' },
{ value: '北仑区', label: '北仑区' },
{ value: '镇海区', label: '镇海区' },
{ value: '鄞州区', label: '鄞州区' },
{ value: '奉化区', label: '奉化区' }
]
}
]
},
{
value: '江苏省',
label: '江苏省',
children: [
{
value: '南京市',
label: '南京市',
children: [
{ value: '玄武区', label: '玄武区' },
{ value: '秦淮区', label: '秦淮区' },
{ value: '建邺区', label: '建邺区' },
{ value: '鼓楼区', label: '鼓楼区' },
{ value: '浦口区', label: '浦口区' },
{ value: '栖霞区', label: '栖霞区' },
{ value: '雨花台区', label: '雨花台区' },
{ value: '江宁区', label: '江宁区' },
{ value: '六合区', label: '六合区' },
{ value: '溧水区', label: '溧水区' },
{ value: '高淳区', label: '高淳区' }
]
},
{
value: '苏州市',
label: '苏州市',
children: [
{ value: '姑苏区', label: '姑苏区' },
{ value: '虎丘区', label: '虎丘区' },
{ value: '吴中区', label: '吴中区' },
{ value: '相城区', label: '相城区' },
{ value: '吴江区', label: '吴江区' }
]
}
]
},
{
value: '四川省',
label: '四川省',
children: [
{
value: '成都市',
label: '成都市',
children: [
{ value: '锦江区', label: '锦江区' },
{ value: '青羊区', label: '青羊区' },
{ value: '金牛区', label: '金牛区' },
{ value: '武侯区', label: '武侯区' },
{ value: '成华区', label: '成华区' },
{ value: '龙泉驿区', label: '龙泉驿区' },
{ value: '青白江区', label: '青白江区' },
{ value: '新都区', label: '新都区' },
{ value: '温江区', label: '温江区' },
{ value: '双流区', label: '双流区' },
{ value: '郫都区', label: '郫都区' },
{ value: '新津区', label: '新津区' }
]
}
]
}
]
/**
* 将 element-china-area-data 的数据格式转换为我们需要的格式
* element-china-area-data 使用 code 作为 value,我们需要使用 label
*/
function convertRegionData(data: any[]): RegionItem[] {
return data.map((item) => ({
value: item.label,
label: item.label,
children: item.children ? convertRegionData(item.children) : undefined
}))
}
export const regionData: RegionItem[] = convertRegionData(chinaRegionData)

View File

@@ -31,30 +31,17 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="160"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '添加账号' : '编辑账号'"
@@ -197,10 +184,6 @@
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { AccountService } from '@/api/modules/account'
import { RoleService } from '@/api/modules/role'
import { ShopService, EnterpriseService } from '@/api/modules'
@@ -214,25 +197,13 @@
const { hasAuth } = useAuth()
const route = useRoute()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const dialogType = ref('add')
const dialogVisible = ref(false)
const roleDialogVisible = ref(false)
const loading = ref(false)
const roleSubmitLoading = ref(false)
const currentAccountId = ref<number>(0)
const currentAccountName = ref<string>('')
const currentAccountType = ref<number>(0)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<any | null>(null)
const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([])
const rolesToAdd = ref<number[]>([])
@@ -493,6 +464,37 @@
}
])
// 操作列配置
const getActions = (row: any) => {
const actions: any[] = []
if (hasAuth('account:patch_role')) {
actions.push({
label: '分配角色',
handler: () => showRoleDialog(row),
type: 'primary'
})
}
if (hasAuth('account:edit')) {
actions.push({
label: '编辑',
handler: () => showDialog('edit', row),
type: 'primary'
})
}
if (hasAuth('account:delete')) {
actions.push({
label: '删除',
handler: () => deleteAccount(row),
type: 'danger'
})
}
return actions
}
// 表单实例
const formRef = ref<FormInstance>()
@@ -678,7 +680,12 @@
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请输入账号名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_-]+$/,
message: '用户名只能包含字母、数字、下划线和横线',
trigger: 'blur'
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
@@ -692,7 +699,7 @@
user_type: [{ required: true, message: '请选择账号类型', trigger: 'change' }],
shop_id: [
{
validator: (rule: any, value: any, callback: any) => {
validator: (_rule: any, value: any, callback: any) => {
if (formData.user_type === 3 && !value) {
callback(new Error('代理账号必须关联店铺'))
} else {
@@ -704,7 +711,7 @@
],
enterprise_id: [
{
validator: (rule: any, value: any, callback: any) => {
validator: (_rule: any, value: any, callback: any) => {
if (formData.user_type === 4 && !value) {
callback(new Error('企业账号必须关联企业'))
} else {
@@ -832,59 +839,9 @@
}
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('account:patch_role')) {
items.push({ key: 'assignRole', label: '分配角色' })
}
if (hasAuth('account:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('account:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'assignRole':
showRoleDialog(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteAccount(currentRow.value)
break
}
}
</script>
<style lang="scss" scoped>
.account-page {
// 账号管理页面样式
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.dialog-header {
display: flex;
align-items: center;

View File

@@ -35,26 +35,21 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="160"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增企业客户' : '编辑企业客户'"
width="600px"
width="40%"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px">
<ElFormItem label="企业编号" prop="enterprise_code">
@@ -204,14 +199,6 @@
</div>
</template>
</ElDialog>
<!-- 企业客户操作右键菜单 -->
<ArtMenuRight
ref="enterpriseOperationMenuRef"
:menu-items="enterpriseOperationMenuItems"
:menu-width="140"
@select="handleEnterpriseOperationMenuSelect"
/>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -228,12 +215,7 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { regionData } from '@/utils/constants/regionData'
@@ -244,15 +226,6 @@
const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 判断是否是代理账号 (user_type === 3)
const isAgentAccount = computed(() => userStore.info.user_type === 3)
@@ -266,10 +239,6 @@
const currentEnterpriseId = ref<number>(0)
const shopTreeData = ref<ShopResponse[]>([])
// 右键菜单
const enterpriseOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingEnterprise = ref<EnterpriseItem | null>(null)
// 搜索表单初始值
const initialSearchState = {
enterprise_name: '',
@@ -414,7 +383,7 @@
{
prop: 'enterprise_code',
label: '企业编号',
minWidth: 150,
minWidth: 200,
showOverflowTooltip: true
},
{
@@ -482,47 +451,56 @@
label: '创建时间',
width: 180,
formatter: (row: EnterpriseItem) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 280,
fixed: 'right',
formatter: (row: EnterpriseItem) => {
const buttons = []
if (hasAuth('enterprise_customer:look_customer')) {
buttons.push(
h(ArtButtonTable, {
text: '账号列表',
onClick: () => viewCustomerAccounts(row)
})
)
}
if (hasAuth('enterprise_customer:card_authorization')) {
buttons.push(
h(ArtButtonTable, {
text: '卡授权',
onClick: () => manageCards(row)
})
)
}
if (hasAuth('enterprise_customer:device_authorization')) {
buttons.push(
h(ArtButtonTable, {
text: '设备授权',
onClick: () => manageDevices(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
// 操作列配置
const getActions = (row: EnterpriseItem) => {
const actions: any[] = []
if (hasAuth('enterprise_customer:look_customer')) {
actions.push({
label: '账号列表',
handler: () => viewCustomerAccounts(row),
type: 'primary'
})
}
if (hasAuth('enterprise_customer:card_authorization')) {
actions.push({
label: '卡授权',
handler: () => manageCards(row),
type: 'primary'
})
}
if (hasAuth('enterprise_customer:device_authorization')) {
actions.push({
label: '设备授权',
handler: () => manageDevices(row),
type: 'primary'
})
}
if (hasAuth('enterprise_customer:edit')) {
actions.push({
label: '编辑',
handler: () => showDialog('edit', row),
type: 'primary'
})
}
if (hasAuth('enterprise_customer:update_pwd')) {
actions.push({
label: '修改密码',
handler: () => showPasswordDialog(row),
type: 'primary'
})
}
return actions
}
onMounted(() => {
getTableData()
// 只有非代理账号才需要加载店铺列表
@@ -840,59 +818,6 @@
}
// 企业客户操作菜单项配置
const enterpriseOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('enterprise_customer:edit')) {
items.push({
key: 'edit',
label: '编辑'
})
}
if (hasAuth('enterprise_customer:update_pwd')) {
items.push({
key: 'updatePassword',
label: '修改密码'
})
}
return items
})
// 显示企业客户操作右键菜单
const showEnterpriseOperationMenu = (e: MouseEvent, row: EnterpriseItem) => {
e.preventDefault()
e.stopPropagation()
currentOperatingEnterprise.value = row
enterpriseOperationMenuRef.value?.show(e)
}
// 处理企业客户操作菜单选择
const handleEnterpriseOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingEnterprise.value) return
switch (item.key) {
case 'edit':
showDialog('edit', currentOperatingEnterprise.value)
break
case 'updatePassword':
showPasswordDialog(currentOperatingEnterprise.value)
break
}
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: EnterpriseItem, column: any, event: MouseEvent) => {
// 如果用户有编辑或修改密码权限,显示右键菜单
if (hasAuth('enterprise_customer:edit') || hasAuth('enterprise_customer:update_pwd')) {
showEnterpriseOperationMenu(event, row)
}
}
</script>
<style scoped lang="scss">
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>
<style scoped lang="scss"></style>

View File

@@ -60,10 +60,31 @@
<ElCard shadow="never" class="info-card basic-info">
<template #header>
<div class="card-header">
<span>{{ cardInfo?.asset_type === 'card' ? '卡信息' : '设备信息' }}</span>
<ElTag :type="cardInfo?.asset_type === 'card' ? 'success' : 'primary'" size="small">
{{ cardInfo?.asset_type === 'card' ? 'IoT卡' : '设备' }}
</ElTag>
<div class="card-header-left">
<span>{{ cardInfo?.asset_type === 'card' ? '卡信息' : '设备信息' }}</span>
<ElTag
:type="cardInfo?.asset_type === 'card' ? 'success' : 'primary'"
size="small"
>
{{ cardInfo?.asset_type === 'card' ? 'IoT卡' : '设备' }}
</ElTag>
</div>
<div class="card-header-right">
<ElTooltip content="开启后系统将自动轮询更新资产状态" placement="top">
<div class="polling-switch-wrapper">
<span class="polling-label">自动轮询</span>
<ElSwitch
v-model="pollingEnabled"
:loading="pollingLoading"
@change="handlePollingStatusChange"
active-text="开"
inactive-text="关"
inline-prompt
size="default"
/>
</div>
</ElTooltip>
</div>
</div>
</template>
<ElDescriptions :column="4" border>
@@ -222,17 +243,17 @@
>
<ElTableColumn prop="iccid" label="ICCID" />
<ElTableColumn prop="msisdn" label="MSISDN" />
<ElTableColumn prop="slot_position" label="卡槽位置" align="center">
<ElTableColumn prop="slot_position" label="卡槽位置" align="center" width="130">
<template #default="scope">
<div
style="display: flex; gap: 4px; align-items: center; justify-content: center"
style="display: flex; gap: 10px; align-items: center; justify-content: center"
>
<span>{{ scope.row.slot_position }}</span>
<ElTag v-if="scope.row.is_current" type="success" size="small">当前</ElTag>
<span>SIM-{{ scope.row.slot_position }}</span>
<ElTag v-if="scope.row.is_current" type="primary" size="small">当前</ElTag>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="网络状态" align="center">
<ElTableColumn label="网络状态" align="center" width="100">
<template #default="scope">
<ElTag
:type="scope.row.network_status === 1 ? 'success' : 'danger'"
@@ -242,14 +263,14 @@
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="实名状态" align="center">
<ElTableColumn label="实名状态" align="center" width="100">
<template #default="scope">
<ElTag :type="getRealNameStatusType(scope.row.real_name_status)" size="small">
{{ getRealNameStatusName(scope.row.real_name_status) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="280" align="center">
<ElTableColumn label="操作" width="200" align="center">
<template #default="scope">
<!-- 根据网络状态显示启用或停用按钮 -->
<ElButton
@@ -575,10 +596,9 @@
<!-- 操作按钮组 -->
<div class="package-actions">
<ElButton type="primary">套餐充值</ElButton>
<ElButton type="primary" @click="showRechargeDialog">套餐充值</ElButton>
<ElButton type="warning">更改过期时间</ElButton>
<ElButton type="info">往期订单</ElButton>
<ElButton type="success">增减流量</ElButton>
<ElButton type="info" @click="showOrderHistoryDialog">往期订单</ElButton>
</div>
<!-- 流量使用进度条 -->
@@ -1091,6 +1111,92 @@
</template>
</ElDialog>
<!-- 套餐充值对话框 -->
<ElDialog
v-model="rechargeDialogVisible"
title="套餐充值"
width="600px"
@closed="handleRechargeDialogClosed"
>
<ElForm
ref="rechargeFormRef"
:model="rechargeForm"
:rules="rechargeRules"
label-width="120px"
>
<ElFormItem label="资产信息">
<span style="font-weight: bold; color: #409eff">
{{ cardInfo?.asset_type === 'card' ? cardInfo?.iccid : cardInfo?.imei }} ({{
cardInfo?.virtual_no
}})
</span>
</ElFormItem>
<ElFormItem label="选择套餐" prop="package_ids">
<ElSelect
v-model="rechargeForm.package_ids"
placeholder="请选择套餐"
multiple
filterable
remote
reserve-keyword
:remote-method="handleRechargePackageSearch"
:loading="packageSearchLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
:label="`${pkg.package_name} (¥${(pkg.cost_price / 100).toFixed(2)})`"
:value="pkg.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ pkg.package_name }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
¥{{ (pkg.cost_price / 100).toFixed(2) }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="支付方式" prop="payment_method">
<ElSelect
v-model="rechargeForm.payment_method"
placeholder="请选择支付方式"
style="width: 100%"
>
<ElOption label="钱包支付" value="wallet" />
<!-- 只有平台用户(user_type 1:超级管理员, 2:平台用户)才显示线下支付选项 -->
<ElOption
v-if="userStore.info.user_type === 1 || userStore.info.user_type === 2"
label="线下支付"
value="offline"
/>
</ElSelect>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
<template v-if="rechargeForm.payment_method === 'wallet'">
提示: 使用钱包支付时,订单将直接完成
</template>
<template v-else-if="rechargeForm.payment_method === 'offline'">
提示: 线下支付订单需要手动确认支付
</template>
</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="rechargeDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleRechargeOrder(rechargeFormRef)"
:loading="rechargeLoading"
>
确认充值
</ElButton>
</div>
</template>
</ElDialog>
<!-- 流量详单对话框 -->
<ElDialog
v-model="dailyRecordsDialogVisible"
@@ -1183,6 +1289,142 @@
<ElEmpty description="暂无流量详单数据" />
</div>
</ElDialog>
<!-- 往期订单对话框 -->
<ElDialog v-model="orderHistoryDialogVisible" title="往期订单" width="60%" destroy-on-close>
<div v-if="orderHistoryLoading" style="padding: 40px 0; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
</div>
<div v-else-if="orderHistoryData">
<!-- 当前代订单 -->
<div class="generation-section">
<h3 style="margin-bottom: 16px">
当前代 ({{ orderHistoryData.current_generation.identifier }})
</h3>
<ElTable :data="orderHistoryData.current_generation.items" border>
<ElTableColumn prop="order_no" label="订单号" width="200" />
<ElTableColumn prop="order_type" label="订单类型" width="120" />
<ElTableColumn prop="payment_status_text" label="支付状态" width="100">
<template #default="{ row }">
<ElTag :type="getPaymentStatusType(row.payment_status)">
{{ row.payment_status_text }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="total_amount" label="总金额" width="120">
<template #default="{ row }"> ¥{{ (row.total_amount / 100).toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn prop="payment_method" label="支付方式" width="120" />
<ElTableColumn prop="paid_at" label="支付时间" width="180">
<template #default="{ row }">
{{ row.paid_at ? formatDateTime(row.paid_at) : '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="订单项" min-width="200">
<template #default="{ row }">
<div v-for="(item, idx) in row.items" :key="idx" style="margin-bottom: 4px">
{{ item.package_name }} × {{ item.quantity }}
</div>
</template>
</ElTableColumn>
<ElTableColumn prop="created_at" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</ElTableColumn>
</ElTable>
<!-- 分页 -->
<div style="margin-top: 16px; text-align: right">
<ElPagination
v-model:current-page="orderHistoryPagination.page"
v-model:page-size="orderHistoryPagination.page_size"
:total="orderHistoryData.current_generation.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleOrderHistorySizeChange"
@current-change="handleOrderHistoryPageChange"
/>
</div>
</div>
<!-- 前代订单 -->
<div
v-if="
orderHistoryData.previous_generations &&
orderHistoryData.previous_generations.length > 0
"
style="margin-top: 32px"
>
<div
v-for="gen in orderHistoryData.previous_generations"
:key="gen.generation"
class="generation-section"
style="margin-bottom: 24px"
>
<h3 style="margin-bottom: 16px">
{{ gen.generation }} ({{ gen.identifier }})
<span
v-if="gen.exchange_no"
style="margin-left: 12px; font-size: 14px; color: #909399"
>
换货单号: {{ gen.exchange_no }}
</span>
<span
v-if="gen.exchanged_at"
style="margin-left: 12px; font-size: 14px; color: #909399"
>
换货时间: {{ formatDateTime(gen.exchanged_at) }}
</span>
</h3>
<ElTable :data="gen.items" border>
<ElTableColumn prop="order_no" label="订单号" width="200" />
<ElTableColumn prop="order_type" label="订单类型" width="120" />
<ElTableColumn prop="payment_status_text" label="支付状态" width="100">
<template #default="{ row }">
<ElTag :type="getPaymentStatusType(row.payment_status)">
{{ row.payment_status_text }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="total_amount" label="总金额" width="120">
<template #default="{ row }"> ¥{{ (row.total_amount / 100).toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn prop="payment_method" label="支付方式" width="120" />
<ElTableColumn prop="paid_at" label="支付时间" width="180">
<template #default="{ row }">
{{ row.paid_at ? formatDateTime(row.paid_at) : '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="订单项" min-width="200">
<template #default="{ row }">
<div v-for="(item, idx) in row.items" :key="idx" style="margin-bottom: 4px">
{{ item.package_name }} × {{ item.quantity }}
</div>
</template>
</ElTableColumn>
<ElTableColumn prop="created_at" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</ElTableColumn>
</ElTable>
</div>
<ElAlert
v-if="orderHistoryData.truncated"
type="warning"
:closable="false"
style="margin-top: 16px"
>
换货链超过10代仅显示最近10代的订单记录
</ElAlert>
</div>
</div>
<div v-else style="padding: 40px 0; text-align: center">
<ElEmpty description="暂无订单数据" />
</div>
</ElDialog>
</div>
</template>
@@ -1220,18 +1462,29 @@
import { Loading, Search, Refresh } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router'
import { EnterpriseService } from '@/api/modules/enterprise'
import { CardService, AssetService, DeviceService } from '@/api/modules'
import {
CardService,
AssetService,
DeviceService,
OrderService,
PackageManageService
} from '@/api/modules'
import { formatDateTime } from '@/utils/business/format'
import type {
AssetWalletTransactionItem,
TransactionType,
AssetWalletResponse
AssetWalletResponse,
CreateOrderRequest,
PackageResponse,
PurchaseCheckRequest
} from '@/types/api'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'SingleCard' })
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const loading = ref(false)
const operationLoading = ref(false)
const refreshLoading = ref(false)
@@ -1243,6 +1496,10 @@
const disableCardLoading = ref(false)
const manualDeactivateCardLoading = ref(false)
//
const pollingEnabled = ref(false)
const pollingLoading = ref(false)
// loading
const rebootDeviceLoading = ref(false)
const resetDeviceLoading = ref(false)
@@ -1295,6 +1552,21 @@
]
})
//
const rechargeDialogVisible = ref(false)
const rechargeLoading = ref(false)
const rechargeFormRef = ref<FormInstance>()
const rechargeForm = reactive({
package_ids: [] as number[],
payment_method: 'wallet' as 'wallet' | 'offline'
})
const rechargeRules: FormRules = {
package_ids: [{ required: true, message: '请选择套餐', trigger: 'change' }],
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }]
}
const packageOptions = ref<PackageResponse[]>([])
const packageSearchLoading = ref(false)
//
const dailyRecordsDialogVisible = ref(false)
const dailyRecordsLoading = ref(false)
@@ -1304,6 +1576,15 @@
const selectedMonth = ref<string>('')
const filteredRecords = ref<any[]>([])
//
const orderHistoryDialogVisible = ref(false)
const orderHistoryLoading = ref(false)
const orderHistoryData = ref<any>(null)
const orderHistoryPagination = reactive({
page: 1,
page_size: 20
})
// ICCID
const searchIccid = ref('')
const iccidInputFocused = ref(false)
@@ -1443,15 +1724,19 @@
packageList: [] // API
}
// asset_idasset_type
if (data.asset_type && data.asset_id) {
//
pollingEnabled.value = data.enable_polling || false
//
const identifier = data.asset_type === 'card' ? data.iccid : data.virtual_no
if (identifier) {
// 5:
Promise.all([
loadCurrentPackage(data.asset_type, data.asset_id),
loadPackageList(data.asset_type, data.asset_id),
loadRealtimeStatus(data.asset_type, data.asset_id),
loadAssetWallet(data.asset_type, data.asset_id),
loadWalletTransactions(data.asset_type, data.asset_id)
loadCurrentPackage(identifier),
loadPackageList(identifier),
loadRealtimeStatus(identifier, data.asset_type),
loadAssetWallet(identifier),
loadWalletTransactions(identifier)
])
}
} else {
@@ -1474,9 +1759,9 @@
}
//
const loadPackageList = async (assetType: string, assetId: number) => {
const loadPackageList = async (identifier: string) => {
try {
const response = await AssetService.getAssetPackages(assetType, assetId, {
const response = await AssetService.getAssetPackages(identifier, {
page: 1,
page_size: 50
})
@@ -1490,12 +1775,12 @@
}
//
const loadCurrentPackage = async (assetType: string, assetId: number) => {
const loadCurrentPackage = async (identifier: string) => {
try {
//
currentPackageErrorMsg.value = ''
const response = await AssetService.getCurrentPackage(assetType, assetId)
const response = await AssetService.getCurrentPackage(identifier)
if (response.code === 0) {
if (response.data) {
const pkg = response.data
@@ -1549,9 +1834,9 @@
}
//
const loadRealtimeStatus = async (assetType: string, assetId: number) => {
const loadRealtimeStatus = async (identifier: string, assetType: string) => {
try {
const response = await AssetService.getRealtimeStatus(assetType, assetId)
const response = await AssetService.getRealtimeStatus(identifier)
if (response.code === 0 && response.data) {
const data = response.data
//
@@ -1605,9 +1890,9 @@
}
//
const loadAssetWallet = async (assetType: string, assetId: number) => {
const loadAssetWallet = async (identifier: string) => {
try {
const response = await AssetService.getAssetWallet(assetType, assetId)
const response = await AssetService.getAssetWallet(identifier)
if (response.code === 0 && response.data) {
walletInfo.value = response.data
}
@@ -1985,12 +2270,14 @@
try {
refreshLoading.value = true
await AssetService.refreshAsset(cardInfo.value.asset_type, cardInfo.value.asset_id)
const identifier =
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
await AssetService.refreshAsset(identifier)
ElMessage.success('刷新成功')
//
if (cardInfo.value.asset_type && cardInfo.value.asset_id) {
await loadRealtimeStatus(cardInfo.value.asset_type, cardInfo.value.asset_id)
if (identifier) {
await loadRealtimeStatus(identifier, cardInfo.value.asset_type)
}
} catch (error: any) {
// 429()
@@ -2004,6 +2291,45 @@
}
}
//
const handlePollingStatusChange = async (value: boolean) => {
if (!cardInfo.value) {
ElMessage.warning('无法更新轮询状态:缺少资产信息')
//
pollingEnabled.value = !value
return
}
try {
pollingLoading.value = true
const identifier =
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
const response = await AssetService.updatePollingStatus(identifier, {
enable_polling: value
})
if (response.code === 0) {
ElMessage.success(value ? '已开启自动轮询' : '已关闭自动轮询')
//
if (cardInfo.value) {
cardInfo.value.enable_polling = value
}
} else {
ElMessage.error(response.msg || '更新轮询状态失败')
//
pollingEnabled.value = !value
}
} catch (error: any) {
console.error('更新轮询状态失败:', error)
ElMessage.error(error?.message || '更新轮询状态失败')
//
pollingEnabled.value = !value
} finally {
pollingLoading.value = false
}
}
//
const handleOperation = async (operation: string) => {
if (!cardInfo.value) {
@@ -2072,7 +2398,7 @@
)
stopLoading.value = true
await AssetService.stopCard(cardInfo.value.iccid)
await AssetService.stopAsset(cardInfo.value.iccid)
ElMessage.success('停机操作成功')
//
@@ -2111,7 +2437,7 @@
)
startLoading.value = true
await AssetService.startCard(cardInfo.value.iccid)
await AssetService.startAsset(cardInfo.value.iccid)
ElMessage.success('复机操作成功')
//
@@ -2150,7 +2476,7 @@
)
stopLoading.value = true
const response = await AssetService.stopDevice(cardInfo.value.asset_id)
const response = await AssetService.stopAsset(cardInfo.value.virtual_no)
//
if (response.code === 0 && response.data) {
@@ -2201,7 +2527,7 @@
)
startLoading.value = true
await AssetService.startDevice(cardInfo.value.asset_id)
await AssetService.startAsset(cardInfo.value.virtual_no)
ElMessage.success('设备复机操作成功')
//
@@ -2220,14 +2546,10 @@
// ========== ==========
//
const loadWalletTransactions = async (assetType: string, assetId: number) => {
const loadWalletTransactions = async (identifier: string) => {
try {
walletLoading.value = true
const response = await AssetService.getWalletTransactions(
assetType,
assetId,
walletQueryParams.value
)
const response = await AssetService.getWalletTransactions(identifier, walletQueryParams.value)
if (response.code === 0 && response.data) {
walletTransactionList.value = response.data.list || []
walletTransactionTotal.value = response.data.total || 0
@@ -2263,8 +2585,10 @@
//
const handleWalletFilterChange = () => {
walletQueryParams.value.page = 1 //
if (cardInfo.value?.asset_type && cardInfo.value?.asset_id) {
loadWalletTransactions(cardInfo.value.asset_type, cardInfo.value.asset_id)
const identifier =
cardInfo.value?.asset_type === 'card' ? cardInfo.value?.iccid : cardInfo.value?.virtual_no
if (identifier) {
loadWalletTransactions(identifier)
}
}
@@ -2290,8 +2614,10 @@
end_time: null
}
walletDateRange.value = null
if (cardInfo.value?.asset_type && cardInfo.value?.asset_id) {
loadWalletTransactions(cardInfo.value.asset_type, cardInfo.value.asset_id)
const identifier =
cardInfo.value?.asset_type === 'card' ? cardInfo.value?.iccid : cardInfo.value?.virtual_no
if (identifier) {
loadWalletTransactions(identifier)
}
}
@@ -2299,16 +2625,20 @@
const handleWalletPageSizeChange = (size: number) => {
walletQueryParams.value.page_size = size
walletQueryParams.value.page = 1 //
if (cardInfo.value?.asset_type && cardInfo.value?.asset_id) {
loadWalletTransactions(cardInfo.value.asset_type, cardInfo.value.asset_id)
const identifier =
cardInfo.value?.asset_type === 'card' ? cardInfo.value?.iccid : cardInfo.value?.virtual_no
if (identifier) {
loadWalletTransactions(identifier)
}
}
//
const handleWalletPageChange = (page: number) => {
walletQueryParams.value.page = page
if (cardInfo.value?.asset_type && cardInfo.value?.asset_id) {
loadWalletTransactions(cardInfo.value.asset_type, cardInfo.value.asset_id)
const identifier =
cardInfo.value?.asset_type === 'card' ? cardInfo.value?.iccid : cardInfo.value?.virtual_no
if (identifier) {
loadWalletTransactions(identifier)
}
}
@@ -2335,7 +2665,7 @@
})
enableCardLoading.value = true
const res = await CardService.enableIotCard(cardInfo.value.iccid)
const res = await AssetService.startAsset(cardInfo.value.iccid)
if (res.code === 0) {
ElMessage.success('启用成功')
await handleRefreshAsset()
@@ -2358,7 +2688,7 @@
})
disableCardLoading.value = true
const res = await CardService.disableIotCard(cardInfo.value.iccid)
const res = await AssetService.stopAsset(cardInfo.value.iccid)
if (res.code === 0) {
ElMessage.success('停用成功')
await handleRefreshAsset()
@@ -2387,7 +2717,7 @@
)
manualDeactivateCardLoading.value = true
const res = await CardService.deactivateIotCard(cardInfo.value.asset_id)
const res = await AssetService.deactivateAsset(cardInfo.value.iccid)
if (res.code === 0) {
ElMessage.success('手动停用成功')
await handleRefreshAsset()
@@ -2411,7 +2741,7 @@
type: 'warning'
})
const res = await CardService.enableIotCard(card.iccid)
const res = await AssetService.startAsset(card.iccid)
if (res.code === 0) {
ElMessage.success('启用成功')
await handleRefreshAsset()
@@ -2431,7 +2761,7 @@
type: 'warning'
})
const res = await CardService.disableIotCard(card.iccid)
const res = await AssetService.stopAsset(card.iccid)
if (res.code === 0) {
ElMessage.success('停用成功')
await handleRefreshAsset()
@@ -2457,7 +2787,7 @@
}
)
const res = await CardService.deactivateIotCard(card.id)
const res = await AssetService.deactivateAsset(card.iccid)
if (res.code === 0) {
ElMessage.success('手动停用成功')
await handleRefreshAsset()
@@ -2644,6 +2974,221 @@
})
}
// ========== ==========
//
const showRechargeDialog = async () => {
if (!cardInfo.value) {
ElMessage.warning('请先查询资产信息')
return
}
rechargeDialogVisible.value = true
//
await loadRechargePackages()
}
//
const loadRechargePackages = async () => {
if (!cardInfo.value) return
try {
packageSearchLoading.value = true
const response = await PackageManageService.getPackages({
series_id: cardInfo.value.series_id,
page: 1,
page_size: 20,
status: 1, //
shelf_status: 1 //
})
if (response.code === 0 && response.data) {
packageOptions.value = response.data.items || []
}
} catch (error: any) {
console.error('加载套餐列表失败:', error)
ElMessage.error(error?.message || '加载套餐列表失败')
} finally {
packageSearchLoading.value = false
}
}
//
const handleRechargePackageSearch = async (query: string) => {
if (!cardInfo.value) return
try {
packageSearchLoading.value = true
const response = await PackageManageService.getPackages({
package_name: query || undefined,
series_id: cardInfo.value.series_id,
page: 1,
page_size: 20,
status: 1, //
shelf_status: 1 //
})
if (response.code === 0 && response.data) {
packageOptions.value = response.data.items || []
}
} catch (error: any) {
console.error('搜索套餐失败:', error)
} finally {
packageSearchLoading.value = false
}
}
//
const handleRechargeOrder = async (formEl: FormInstance | undefined) => {
if (!formEl || !cardInfo.value) return
await formEl.validate(async (valid) => {
if (valid) {
rechargeLoading.value = true
try {
// ID
const orderType: 'single_card' | 'device' =
cardInfo.value.asset_type === 'card' ? 'single_card' : 'device'
const resourceId = cardInfo.value.asset_id
const identifier =
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
//
const checkData: PurchaseCheckRequest = {
order_type: orderType,
resource_id: resourceId,
package_ids: rechargeForm.package_ids
}
const checkResponse = await OrderService.purchaseCheck(checkData)
//
if (checkResponse.code === 0 && checkResponse.data) {
const checkResult = checkResponse.data
// ,
if (checkResult.need_force_recharge) {
const confirmMessage = `${checkResult.message || '钱包余额不足'}\n\n套餐总价: ¥${(checkResult.total_package_amount! / 100).toFixed(2)}\n需要强充: ¥${(checkResult.force_recharge_amount! / 100).toFixed(2)}\n钱包到账: ¥${(checkResult.wallet_credit! / 100).toFixed(2)}\n实际支付: ¥${(checkResult.actual_payment! / 100).toFixed(2)}\n\n是否继续创建订单?`
await ElMessageBox.confirm(confirmMessage, '购买预检提示', {
confirmButtonText: '继续创建',
cancelButtonText: '取消',
type: 'warning'
})
}
}
// ,
const data: CreateOrderRequest = {
identifier,
package_ids: rechargeForm.package_ids,
payment_method: rechargeForm.payment_method
}
await OrderService.createOrder(data)
//
if (rechargeForm.payment_method === 'wallet') {
ElMessage.success('订单创建成功,已自动完成支付')
} else {
ElMessage.success('订单创建成功')
}
rechargeDialogVisible.value = false
formEl.resetFields()
//
if (searchIccid.value) {
await fetchCardDetailByIccid(searchIccid.value)
}
} catch (error: any) {
//
if (error === 'cancel') {
return
}
console.error('创建订单失败:', error)
} finally {
rechargeLoading.value = false
}
}
})
}
//
const handleRechargeDialogClosed = () => {
rechargeFormRef.value?.resetFields()
rechargeForm.package_ids = []
rechargeForm.payment_method = 'wallet'
packageOptions.value = []
}
// ========== ==========
//
const showOrderHistoryDialog = async () => {
if (!cardInfo.value) {
ElMessage.warning('请先查询资产信息')
return
}
orderHistoryDialogVisible.value = true
orderHistoryPagination.page = 1
orderHistoryPagination.page_size = 20
await loadOrderHistory()
}
//
const loadOrderHistory = async () => {
if (!cardInfo.value) return
try {
orderHistoryLoading.value = true
// 使ICCID VirtualNo
const identifier =
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
const response = await AssetService.getAssetOrders(identifier, {
page: orderHistoryPagination.page,
page_size: orderHistoryPagination.page_size,
include_previous: true //
})
if (response.code === 0 && response.data) {
orderHistoryData.value = response.data
}
} catch (error: any) {
console.error('加载往期订单失败:', error)
ElMessage.error(error?.message || '加载往期订单失败')
} finally {
orderHistoryLoading.value = false
}
}
//
const handleOrderHistorySizeChange = (size: number) => {
orderHistoryPagination.page_size = size
loadOrderHistory()
}
const handleOrderHistoryPageChange = (page: number) => {
orderHistoryPagination.page = page
loadOrderHistory()
}
//
const getPaymentStatusType = (status: number): 'success' | 'warning' | 'danger' | 'info' => {
const statusMap: Record<number, 'success' | 'warning' | 'danger' | 'info'> = {
1: 'warning', //
2: 'success', //
3: 'danger', //
4: 'info' //
}
return statusMap[status] || 'info'
}
//
const handleManualDeactivateDevice = async () => {
try {
@@ -2658,7 +3203,9 @@
)
manualDeactivateDeviceLoading.value = true
const res = await DeviceService.deactivateDevice(cardInfo.value.asset_id)
const identifier =
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
const res = await AssetService.deactivateAsset(identifier)
if (res.code === 0) {
ElMessage.success('手动停用成功')
await handleRefreshAsset()
@@ -2965,10 +3512,53 @@
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary, #1f2937);
.card-header-left {
display: flex;
gap: 8px;
align-items: center;
}
.card-header-right {
display: flex;
gap: 12px;
align-items: center;
.polling-switch-wrapper {
display: flex;
gap: 10px;
align-items: center;
.polling-label {
font-size: 15px;
font-weight: 500;
color: var(--el-text-color-regular);
}
:deep(.el-switch) {
height: 26px;
.el-switch__core {
height: 26px;
min-width: 50px;
.el-switch__inner {
font-size: 13px;
}
}
.el-switch__action {
width: 20px;
height: 20px;
}
}
}
}
.header-title {
color: var(--el-text-color-primary);
}
@@ -3439,4 +4029,17 @@
}
}
}
//
.generation-section {
padding: 16px;
background: var(--el-fill-color-lighter);
border-radius: 8px;
h3 {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
</style>

View File

@@ -53,13 +53,11 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="180"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn type="selection" width="55" />
@@ -67,9 +65,6 @@
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 批量分配对话框 -->
<ElDialog v-model="allocateDialogVisible" title="批量分配设备" width="600px">
<ElForm
@@ -284,7 +279,7 @@
<!-- 设备详情弹窗 -->
<ElDialog v-model="deviceDetailDialogVisible" title="设备详情" width="1000px">
<div v-if="deviceDetailLoading" style=" padding: 40px 0;text-align: center">
<div v-if="deviceDetailLoading" style="padding: 40px 0; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
</div>
<div v-else-if="currentDeviceDetail">
@@ -296,17 +291,17 @@
}}</ElDescriptionsItem>
<ElDescriptionsItem label="设备名称">{{
currentDeviceDetail.device_name || '--'
currentDeviceDetail.device_name || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="设备型号">{{
currentDeviceDetail.device_model || '--'
currentDeviceDetail.device_model || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="设备类型">{{
currentDeviceDetail.device_type || '--'
currentDeviceDetail.device_type || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="制造商">{{
currentDeviceDetail.manufacturer || '--'
currentDeviceDetail.manufacturer || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="最大插槽数">{{
currentDeviceDetail.max_sim_slots
@@ -321,11 +316,11 @@
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="批次号" :span="2">{{
currentDeviceDetail.batch_no || '--'
currentDeviceDetail.batch_no || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="3">{{
formatDateTime(currentDeviceDetail.created_at) || '--'
formatDateTime(currentDeviceDetail.created_at) || '-'
}}</ElDescriptionsItem>
</ElDescriptions>
</div>
@@ -398,7 +393,7 @@
</ElCard>
<!-- 已绑定卡片列表 -->
<div v-if="deviceCardsLoading" style=" padding: 40px 0;text-align: center">
<div v-if="deviceCardsLoading" style="padding: 40px 0; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
</div>
<div v-else>
@@ -406,15 +401,15 @@
<ElTableColumn prop="slot_position" label="插槽位置" width="120" align="center">
<template #default="{ row }">
<div
style="display: flex; gap: 4px; align-items: center; justify-content: center"
style="display: flex; gap: 10px; align-items: center; justify-content: center"
>
<span>{{ row.slot_position }}</span>
<span>SIM-{{ row.slot_position }}</span>
<ElTag v-if="row.is_current" type="success" size="small">当前</ElTag>
</div>
</template>
</ElTableColumn>
<ElTableColumn prop="iccid" label="ICCID" min-width="180" />
<ElTableColumn prop="msisdn" label="接入号" width="140" />
<ElTableColumn prop="iccid" label="ICCID" min-width="120" />
<ElTableColumn prop="msisdn" label="接入号" width="160" />
<ElTableColumn prop="network_status" label="网络状态" width="100" align="center">
<template #default="{ row }">
<ElTag :type="getNetworkStatusTagType(row.network_status)">
@@ -424,7 +419,7 @@
</ElTableColumn>
<ElTableColumn prop="bind_time" label="绑定时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.bind_time) || '--' }}
{{ formatDateTime(row.bind_time) || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="100" fixed="right" align="center">
@@ -437,21 +432,13 @@
</ElTable>
<div
v-if="deviceCards.length === 0"
style=" padding: 20px; color: #909399;text-align: center"
style="padding: 20px; color: #909399; text-align: center"
>
暂无设备绑定的卡
</div>
</div>
</ElDialog>
<!-- 设备操作右键菜单 -->
<ArtMenuRight
ref="deviceOperationMenuRef"
:menu-items="deviceOperationMenuItems"
:menu-width="140"
@select="handleDeviceOperationMenuSelect"
/>
<!-- 设置限速对话框 -->
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="50%">
<ElForm
@@ -471,7 +458,7 @@
controls-position="right"
style="width: 100%"
/>
<div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
</ElFormItem>
<ElFormItem label="上行速率" prop="upload_speed">
<ElInputNumber
@@ -481,7 +468,7 @@
controls-position="right"
style="width: 100%"
/>
<div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
</ElFormItem>
</ElForm>
<template #footer>
@@ -494,7 +481,7 @@
<!-- 切换SIM卡对话框 -->
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="600px">
<div v-if="loadingDeviceCards" style=" padding: 40px;text-align: center">
<div v-if="loadingDeviceCards" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
</div>
@@ -647,11 +634,7 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants'
import type { PackageSeriesResponse } from '@/types/api'
@@ -661,15 +644,6 @@
const { hasAuth } = useAuth()
const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false)
const allocateLoading = ref(false)
const recallLoading = ref(false)
@@ -738,11 +712,8 @@
]
})
// 设备操作右键菜单
const deviceOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingDeviceNo = ref<string>('')
const currentOperatingDevice = ref<Device | null>(null)
const currentOperatingImei = ref<string>('') // 用于存储当前操作设备的IMEI
const currentOperatingDevice = ref<Device | null>(null)
const switchCardDialogVisible = ref(false)
const switchCardLoading = ref(false)
@@ -901,7 +872,7 @@
const goToDeviceSearchDetail = (deviceNo: string) => {
if (hasAuth('device:view_detail')) {
router.push({
path: '/asset-management/single-card',
path: '/asset-management/asset-information',
query: {
virtual_no: deviceNo
}
@@ -920,14 +891,14 @@
bindCardForm.iot_card_id = undefined
bindCardForm.slot_position = 1
// 加载设备卡列表和默认IoT卡列表
await Promise.all([loadDeviceCards(device.id), loadDefaultIotCards()])
await Promise.all([loadDeviceCards(device.virtual_no), loadDefaultIotCards()])
}
// 加载设备绑定的卡列表
const loadDeviceCards = async (deviceId: number) => {
const loadDeviceCards = async (virtualNo: string) => {
deviceCardsLoading.value = true
try {
const res = await DeviceService.getDeviceCards(deviceId)
const res = await DeviceService.getDeviceCards(virtualNo)
if (res.code === 0 && res.data) {
deviceCards.value = res.data.bindings || []
console.log('设备绑定的卡列表:', deviceCards.value)
@@ -995,7 +966,7 @@
if (valid) {
bindCardLoading.value = true
try {
const res = await DeviceService.bindCard(currentDeviceDetail.value.id, {
const res = await DeviceService.bindCard(currentDeviceDetail.value.virtual_no, {
iot_card_id: bindCardForm.iot_card_id!,
slot_position: bindCardForm.slot_position
})
@@ -1006,7 +977,7 @@
bindCardForm.slot_position = 1
bindCardFormRef.value.resetFields()
// 重新加载卡列表
await loadDeviceCards(currentDeviceDetail.value.id)
await loadDeviceCards(currentDeviceDetail.value.virtual_no)
// 刷新设备详情以更新绑定卡数量
const detailRes = await AssetService.resolveAsset(currentDeviceDetail.value.virtual_no)
if (detailRes.code === 0 && detailRes.data) {
@@ -1031,11 +1002,14 @@
})
.then(async () => {
try {
const res = await DeviceService.unbindCard(currentDeviceDetail.value.id, row.iot_card_id)
const res = await DeviceService.unbindCard(
currentDeviceDetail.value.virtual_no,
row.iccid
)
if (res.code === 0) {
ElMessage.success('解绑成功')
// 重新加载卡列表
await loadDeviceCards(currentDeviceDetail.value.id)
await loadDeviceCards(currentDeviceDetail.value.virtual_no)
// 刷新设备详情以更新绑定卡数量
const detailRes = await AssetService.resolveAsset(currentDeviceDetail.value.virtual_no)
if (detailRes.code === 0 && detailRes.data) {
@@ -1367,7 +1341,7 @@
)
.then(async () => {
try {
await DeviceService.deleteDevice(row.id)
await DeviceService.deleteDevice(row.virtual_no)
ElMessage.success('删除成功')
await getTableData()
} catch (error) {
@@ -1581,13 +1555,16 @@
}
// 设备操作路由
const handleDeviceOperation = async (command: string, deviceNo: string) => {
const handleDeviceOperation = async (command: string, deviceNo: string, device?: Device) => {
// 设置当前操作的设备
currentOperatingDevice.value = device || null
switch (command) {
case 'view-cards':
handleViewCardsByDeviceNo(deviceNo)
break
case 'reboot': {
const imei = await getDeviceImei(deviceNo)
const imei = device?.imei || (await getDeviceImei(deviceNo))
if (!imei) {
ElMessage.error('无法获取设备IMEI,无法执行重启操作')
return
@@ -1596,7 +1573,7 @@
break
}
case 'reset': {
const imei = await getDeviceImei(deviceNo)
const imei = device?.imei || (await getDeviceImei(deviceNo))
if (!imei) {
ElMessage.error('无法获取设备IMEI,无法执行重置操作')
return
@@ -1605,7 +1582,7 @@
break
}
case 'speed-limit': {
const imei = await getDeviceImei(deviceNo)
const imei = device?.imei || (await getDeviceImei(deviceNo))
if (!imei) {
ElMessage.error('无法获取设备IMEI,无法执行限速操作')
return
@@ -1614,7 +1591,7 @@
break
}
case 'switch-card': {
const imei = await getDeviceImei(deviceNo)
const imei = device?.imei || (await getDeviceImei(deviceNo))
if (!imei) {
ElMessage.error('无法获取设备IMEI,无法执行切卡操作')
return
@@ -1623,7 +1600,7 @@
break
}
case 'set-wifi': {
const imei = await getDeviceImei(deviceNo)
const imei = device?.imei || (await getDeviceImei(deviceNo))
if (!imei) {
ElMessage.error('无法获取设备IMEI,无法执行WiFi设置操作')
return
@@ -1669,7 +1646,7 @@
)
.then(async () => {
try {
const res = await DeviceService.deactivateDevice(device.id)
const res = await AssetService.deactivateAsset(device.virtual_no)
if (res.code === 0) {
ElMessage.success('手动停用成功')
loadDeviceList()
@@ -1798,7 +1775,7 @@
// 加载设备绑定的卡列表
loadingDeviceCards.value = true
try {
const res = await DeviceService.getDeviceCards(device.id)
const res = await DeviceService.getDeviceCards(device.virtual_no)
if (res.code === 0 && res.data) {
deviceBindingCards.value = res.data.bindings || []
@@ -1884,89 +1861,76 @@
}
// 设备操作菜单项配置
const deviceOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
// 表格操作列配置
const getActions = (row: Device) => {
const actions: any[] = []
// 第一个按钮切换SIM卡
if (hasAuth('device:switch_sim')) {
actions.push({
label: '切换SIM卡',
handler: () => handleDeviceOperation('switch-card', row.virtual_no, row),
type: 'primary'
})
}
// 其他操作放到"更多"下拉菜单中
const moreActions: any[] = []
// 添加设备绑定的卡到菜单最前面
if (hasAuth('device:view_cards')) {
items.push({
key: 'view-cards',
label: '设备绑定的卡'
moreActions.push({
label: '设备绑定的卡',
handler: () => handleDeviceOperation('view-cards', row.virtual_no, row),
type: 'primary'
})
}
if (hasAuth('device:reboot')) {
items.push({
key: 'reboot',
label: '重启设备'
moreActions.push({
label: '重启设备',
handler: () => handleDeviceOperation('reboot', row.virtual_no, row),
type: 'primary'
})
}
if (hasAuth('device:factory_reset')) {
items.push({
key: 'reset',
label: '恢复出厂'
})
}
// if (hasAuth('device:set_speed_limit')) {
// items.push({
// key: 'speed-limit',
// label: '设置限速'
// })
// }
if (hasAuth('device:switch_sim')) {
items.push({
key: 'switch-card',
label: '切换SIM卡'
moreActions.push({
label: '恢复出厂',
handler: () => handleDeviceOperation('reset', row.virtual_no, row),
type: 'primary'
})
}
if (hasAuth('device:set_wifi')) {
items.push({
key: 'set-wifi',
label: '设置WiFi'
moreActions.push({
label: '设置WiFi',
handler: () => handleDeviceOperation('set-wifi', row.virtual_no, row),
type: 'primary'
})
}
if (hasAuth('device:manual_deactivate')) {
items.push({
key: 'manual-deactivate',
label: '手动停用'
moreActions.push({
label: '手动停用',
handler: () => handleDeviceOperation('manual-deactivate', row.virtual_no, row),
type: 'primary'
})
}
if (hasAuth('device:delete')) {
items.push({
key: 'delete',
label: '删除设备'
moreActions.push({
label: '删除设备',
handler: () => handleDeviceOperation('delete', row.virtual_no, row),
type: 'primary'
})
}
return items
})
// 如果有更多操作,添加到 actions 中
if (moreActions.length > 0) {
actions.push(...moreActions)
}
// 显示设备操作菜单
const showDeviceOperationMenu = (e: MouseEvent, deviceNo: string, device?: Device) => {
e.preventDefault()
e.stopPropagation()
currentOperatingDeviceNo.value = deviceNo
currentOperatingDevice.value = device || null
deviceOperationMenuRef.value?.show(e)
}
// 处理设备操作菜单选择
const handleDeviceOperationMenuSelect = (item: MenuItemType) => {
const deviceNo = currentOperatingDeviceNo.value
if (!deviceNo) return
handleDeviceOperation(item.key, deviceNo)
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: Device, column: any, event: MouseEvent) => {
showDeviceOperationMenu(event, row.virtual_no, row)
return actions
}
</script>

View File

@@ -37,13 +37,18 @@
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showAllocateDialog">{{
$t('enterpriseDevices.buttons.allocateDevices')
}}</ElButton>
<ElButton
type="primary"
@click="showAllocateDialog"
v-permission="'enterprise_device:allocate'"
>
{{ $t('enterpriseDevices.buttons.allocateDevices') }}
</ElButton>
<ElButton
type="warning"
:disabled="selectedDevices.length === 0"
@click="showRecallDialog"
v-permission="'enterprise_device:recall'"
>
{{ $t('enterpriseDevices.buttons.recallDevices') }}
</ElButton>

View File

@@ -75,16 +75,73 @@
v-model="createForm.old_asset_type"
placeholder="请选择旧资产类型"
style="width: 100%"
@change="handleOldAssetTypeChange"
>
<ElOption label="IoT卡" value="iot_card" />
<ElOption label="设备" value="device" />
</ElSelect>
</ElFormItem>
<ElFormItem label="旧资产标识符" prop="old_identifier">
<ElInput
<ElFormItem
v-if="createForm.old_asset_type === 'iot_card'"
label="旧资产标识符"
prop="old_identifier"
>
<ElSelect
v-model="createForm.old_identifier"
placeholder="请输入ICCID/虚拟号/IMEI/SN"
/>
filterable
remote
reserve-keyword
placeholder="请输入ICCID搜索"
:remote-method="searchOldIotCards"
:loading="oldCardSearchLoading"
style="width: 100%"
clearable
>
<ElOption
v-for="card in oldIotCardOptions"
:key="card.id"
:label="`${card.iccid} (${card.msisdn || '无接入号'})`"
:value="card.iccid"
>
<div style="display: flex; justify-content: space-between">
<span>{{ card.iccid }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ card.msisdn || '无接入号' }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="createForm.old_asset_type === 'device'"
label="旧资产标识符"
prop="old_identifier"
>
<ElSelect
v-model="createForm.old_identifier"
filterable
remote
reserve-keyword
placeholder="请输入虚拟号或IMEI搜索"
:remote-method="searchOldDevices"
:loading="oldDeviceSearchLoading"
style="width: 100%"
clearable
>
<ElOption
v-for="device in oldDeviceOptions"
:key="device.id"
:label="`${device.virtual_no} (${device.device_name || device.imei})`"
:value="device.virtual_no"
>
<div style="display: flex; justify-content: space-between">
<span>{{ device.virtual_no }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ device.device_name || device.imei }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="备注">
<ElInput
@@ -129,7 +186,7 @@
</ElFormItem>
<ElFormItem label="是否迁移数据">
<ElSwitch v-model="shipForm.migrate_data" />
<div style=" margin-top: 4px;font-size: 12px; color: #909399">
<div style="margin-top: 4px; font-size: 12px; color: #909399">
开启后将迁移钱包余额套餐记录到新资产
</div>
</ElFormItem>
@@ -152,8 +209,9 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { ExchangeService } from '@/api/modules'
import { ExchangeService, CardService, DeviceService } from '@/api/modules'
import type { ExchangeResponse } from '@/api/modules/exchange'
import type { StandaloneIotCard, Device } from '@/types/api'
import { ElMessage, ElTag, ElButton, ElMessageBox, ElSwitch } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { SearchFormItem } from '@/types'
@@ -208,9 +266,15 @@
const createRules = reactive<FormRules>({
exchange_reason: [{ required: true, message: '请输入换货原因', trigger: 'blur' }],
old_asset_type: [{ required: true, message: '请选择旧资产类型', trigger: 'change' }],
old_identifier: [{ required: true, message: '请输入旧资产标识符', trigger: 'blur' }]
old_identifier: [{ required: true, message: '请选择旧资产标识符', trigger: 'change' }]
})
// 旧资产搜索相关
const oldIotCardOptions = ref<StandaloneIotCard[]>([])
const oldCardSearchLoading = ref(false)
const oldDeviceOptions = ref<Device[]>([])
const oldDeviceSearchLoading = ref(false)
// 发货表单
const shipForm = reactive({
new_identifier: '',
@@ -311,7 +375,7 @@
prop: 'new_asset_identifier',
label: '新资产标识符',
width: 180,
formatter: (row: any) => row.new_asset_identifier || '--'
formatter: (row: any) => row.new_asset_identifier
},
{
prop: 'status',
@@ -327,8 +391,8 @@
}
])
const getStatusType = (status: number) => {
const types: Record<number, string> = {
const getStatusType = (status: number): 'success' | 'info' | 'warning' | 'danger' | 'primary' => {
const types: Record<number, 'success' | 'info' | 'warning' | 'danger' | 'primary'> = {
1: 'warning',
2: 'info',
3: 'primary',
@@ -363,7 +427,7 @@
const res = await ExchangeService.getExchanges(params)
if (res.code === 0 && res.data) {
exchangeList.value = res.data.list || []
exchangeList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
@@ -406,6 +470,123 @@
createDialogVisible.value = true
}
// 旧资产类型变更
const handleOldAssetTypeChange = async () => {
// 清空旧资产标识符
createForm.old_identifier = ''
oldIotCardOptions.value = []
oldDeviceOptions.value = []
// 根据类型加载默认数据
if (createForm.old_asset_type === 'iot_card') {
await loadDefaultIotCards()
} else if (createForm.old_asset_type === 'device') {
await loadDefaultDevices()
}
}
// 加载默认IoT卡列表
const loadDefaultIotCards = async () => {
oldCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
page: 1,
page_size: 10
})
if (res.code === 0 && res.data) {
oldIotCardOptions.value = res.data.items || []
if (oldIotCardOptions.value.length === 0) {
ElMessage.info('暂无可用的IoT卡')
}
}
} catch (error) {
console.error('加载IoT卡列表失败:', error)
oldIotCardOptions.value = []
} finally {
oldCardSearchLoading.value = false
}
}
// 加载默认设备列表
const loadDefaultDevices = async () => {
oldDeviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
page: 1,
page_size: 10
})
if (res.code === 0 && res.data) {
oldDeviceOptions.value = res.data.items || []
if (oldDeviceOptions.value.length === 0) {
ElMessage.info('暂无可用的设备')
}
}
} catch (error) {
console.error('加载设备列表失败:', error)
oldDeviceOptions.value = []
} finally {
oldDeviceSearchLoading.value = false
}
}
// 搜索旧IoT卡
const searchOldIotCards = async (query: string) => {
if (!query) {
// 如果清空搜索,加载默认列表
await loadDefaultIotCards()
return
}
oldCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
iccid: query,
page: 1,
page_size: 20
})
if (res.code === 0 && res.data) {
oldIotCardOptions.value = res.data.items || []
if (oldIotCardOptions.value.length === 0) {
ElMessage.info('未找到匹配的IoT卡')
}
}
} catch (error) {
console.error('搜索IoT卡失败:', error)
oldIotCardOptions.value = []
} finally {
oldCardSearchLoading.value = false
}
}
// 搜索旧设备
const searchOldDevices = async (query: string) => {
if (!query) {
// 如果清空搜索,加载默认列表
await loadDefaultDevices()
return
}
oldDeviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
keyword: query,
page: 1,
page_size: 20
})
if (res.code === 0 && res.data) {
oldDeviceOptions.value = res.data.items || []
if (oldDeviceOptions.value.length === 0) {
ElMessage.info('未找到匹配的设备')
}
}
} catch (error) {
console.error('搜索设备失败:', error)
oldDeviceOptions.value = []
} finally {
oldDeviceSearchLoading.value = false
}
}
// 确认创建
const handleConfirmCreate = () => {
createFormRef.value?.validate(async (valid) => {
@@ -417,7 +598,7 @@
if (res.code === 0) {
ElMessage.success('换货单创建成功')
createDialogVisible.value = false
loadExchangeList()
await loadExchangeList()
}
} catch (error) {
console.error('创建换货单失败:', error)
@@ -430,6 +611,9 @@
// 关闭创建对话框
const handleCloseCreateDialog = () => {
createFormRef.value?.resetFields()
// 清空搜索选项
oldIotCardOptions.value = []
oldDeviceOptions.value = []
}
// 查看详情
@@ -492,7 +676,7 @@
})
// 处理右键菜单
const handleRowContextMenu = (row: ExchangeResponse, column: any, event: MouseEvent) => {
const handleRowContextMenu = (row: ExchangeResponse, _column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentExchangeRow.value = row
@@ -531,7 +715,7 @@
const res = await ExchangeService.cancelExchange(row.id, { remark: value })
if (res.code === 0) {
ElMessage.success('取消成功')
loadExchangeList()
await loadExchangeList()
}
} catch (error) {
console.error('取消换货失败:', error)
@@ -562,7 +746,7 @@
if (res.code === 0) {
ElMessage.success('发货成功')
shipDialogVisible.value = false
loadExchangeList()
await loadExchangeList()
}
} catch (error) {
console.error('发货失败:', error)
@@ -589,7 +773,7 @@
const res = await ExchangeService.completeExchange(row.id)
if (res.code === 0) {
ElMessage.success('操作成功')
loadExchangeList()
await loadExchangeList()
}
} catch (error) {
console.error('确认完成失败:', error)
@@ -610,7 +794,7 @@
const res = await ExchangeService.renewExchange(row.id)
if (res.code === 0) {
ElMessage.success('操作成功')
loadExchangeList()
await loadExchangeList()
}
} catch (error) {
console.error('旧资产转新失败:', error)

View File

@@ -68,13 +68,11 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="160"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn type="selection" width="55" />
@@ -82,9 +80,6 @@
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 批量分配对话框 -->
<ElDialog
v-model="allocateDialogVisible"
@@ -126,26 +121,32 @@
</ElRadioGroup>
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'list'" label="ICCID列表">
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === 'range'"
v-if="allocateForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput v-model="allocateForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === 'range'"
v-if="allocateForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput v-model="allocateForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="运营商">
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="allocateForm.carrier_id"
placeholder="请选择运营商"
@@ -157,7 +158,10 @@
<ElOption label="中国电信" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="卡状态">
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
label="卡状态"
>
<ElSelect
v-model="allocateForm.status"
placeholder="请选择状态"
@@ -170,7 +174,10 @@
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="批次号">
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
label="批次号"
>
<ElInput v-model="allocateForm.batch_no" placeholder="请输入批次号" />
</ElFormItem>
@@ -209,26 +216,32 @@
</ElRadioGroup>
</ElFormItem>
<ElFormItem v-if="recallForm.selection_type === 'list'" label="ICCID列表">
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === 'range'"
v-if="recallForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput v-model="recallForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === 'range'"
v-if="recallForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput v-model="recallForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<ElFormItem v-if="recallForm.selection_type === 'filter'" label="运营商">
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="recallForm.carrier_id"
placeholder="请选择运营商"
@@ -240,7 +253,10 @@
<ElOption label="中国电信" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="recallForm.selection_type === 'filter'" label="批次号">
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.FILTER"
label="批次号"
>
<ElInput v-model="recallForm.batch_no" placeholder="请输入批次号" />
</ElFormItem>
@@ -385,7 +401,7 @@
<!-- 卡详情对话框 -->
<ElDialog v-model="cardDetailDialogVisible" title="卡片详情" width="900px">
<div v-if="cardDetailLoading" style=" padding: 40px;text-align: center">
<div v-if="cardDetailLoading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">加载中...</div>
</div>
@@ -465,7 +481,7 @@
<!-- 流量使用查询对话框 -->
<ElDialog v-model="flowUsageDialogVisible" title="流量使用查询" width="500px">
<div v-if="flowUsageLoading" style=" padding: 40px;text-align: center">
<div v-if="flowUsageLoading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div>
</div>
@@ -494,7 +510,7 @@
<!-- 实名状态查询对话框 -->
<ElDialog v-model="realnameStatusDialogVisible" title="实名认证状态" width="500px">
<div v-if="realnameStatusLoading" style=" padding: 40px;text-align: center">
<div v-if="realnameStatusLoading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div>
</div>
@@ -517,7 +533,7 @@
<!-- 卡实时状态查询对话框 -->
<ElDialog v-model="cardStatusDialogVisible" title="卡实时状态" width="500px">
<div v-if="cardStatusLoading" style=" padding: 40px;text-align: center">
<div v-if="cardStatusLoading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div>
</div>
@@ -548,14 +564,6 @@
:menu-width="180"
@select="handleMoreMenuSelect"
/>
<!-- 表格行操作右键菜单 -->
<ArtMenuRight
ref="cardOperationMenuRef"
:menu-items="cardOperationMenuItems"
:menu-width="160"
@select="handleCardOperationMenuSelect"
/>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -571,12 +579,9 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import type {
StandaloneIotCard,
StandaloneCardStatus,
@@ -585,23 +590,14 @@
AllocateStandaloneCardsResponse,
BatchSetCardSeriesBindingResponse
} from '@/types/api/card'
import { CardSelectionType } from '@/types/api/card'
import type { PackageSeriesResponse } from '@/types/api'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'StandaloneCardList' })
const { hasAuth } = useAuth()
const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false)
const allocateDialogVisible = ref(false)
const allocateLoading = ref(false)
@@ -660,18 +656,21 @@
// 更多操作右键菜单
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const cardOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingIccid = ref<string>('')
const currentOperatingCard = ref<StandaloneIotCard | null>(null)
// 店铺相关
const targetShopLoading = ref(false)
const fromShopLoading = ref(false)
const targetShopList = ref<any[]>([])
const fromShopList = ref<any[]>([])
// 搜索表单初始值
const initialSearchState = {
const initialSearchState: {
status: undefined | number
carrier_id: undefined | number
iccid: string
msisdn: string
virtual_no: string
is_distributed: undefined | number
[key: string]: any
} = {
status: undefined,
carrier_id: undefined,
iccid: '',
@@ -685,7 +684,7 @@
// 批量分配表单
const allocateForm = reactive<Partial<AllocateStandaloneCardsRequest>>({
selection_type: 'list',
selection_type: CardSelectionType.LIST,
to_shop_id: undefined,
iccids: [],
iccid_start: '',
@@ -703,8 +702,8 @@
iccid_start: [
{
required: true,
validator: (rule, value, callback) => {
if (allocateForm.selection_type === 'range' && !value) {
validator: (_rule, value, callback) => {
if (allocateForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
@@ -716,8 +715,8 @@
iccid_end: [
{
required: true,
validator: (rule, value, callback) => {
if (allocateForm.selection_type === 'range' && !value) {
validator: (_rule, value, callback) => {
if (allocateForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
@@ -730,7 +729,7 @@
// 批量回收表单
const recallForm = reactive<Partial<RecallStandaloneCardsRequest>>({
selection_type: 'list',
selection_type: CardSelectionType.LIST,
iccids: [],
iccid_start: '',
iccid_end: '',
@@ -745,8 +744,8 @@
iccid_start: [
{
required: true,
validator: (rule, value, callback) => {
if (recallForm.selection_type === 'range' && !value) {
validator: (_rule, value, callback) => {
if (recallForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
@@ -758,8 +757,8 @@
iccid_end: [
{
required: true,
validator: (rule, value, callback) => {
if (recallForm.selection_type === 'range' && !value) {
validator: (_rule, value, callback) => {
if (recallForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
@@ -907,7 +906,7 @@
const goToCardDetail = (iccid: string) => {
if (hasAuth('iot_card:view_detail')) {
router.push({
path: '/asset-management/single-card',
path: '/asset-management/asset-information',
query: {
iccid: iccid
}
@@ -983,13 +982,13 @@
{
prop: 'msisdn',
label: '卡接入号',
width: 130
width: 160
},
{
prop: 'virtual_no',
label: '虚拟号',
width: 130,
formatter: (row: StandaloneIotCard) => row.virtual_no || '-'
width: 160,
formatter: (row: StandaloneIotCard) => row.virtual_no
},
{
prop: 'card_category',
@@ -1081,6 +1080,63 @@
}
])
// 操作列配置
const getActions = (row: StandaloneIotCard) => {
const actions: any[] = []
// 查询流量按钮
if (hasAuth('iot_card:query_flow')) {
actions.push({
label: '查询流量',
handler: () => showFlowUsageDialog(row.iccid),
type: 'primary'
})
}
// 其他操作放到更多中
if (hasAuth('iot_card:query_realname_status')) {
actions.push({
label: '查询实名状态',
handler: () => handleCardOperation('realname-status', row.iccid),
type: 'primary'
})
}
if (hasAuth('iot_card:query_card_status')) {
actions.push({
label: '查询卡状态',
handler: () => handleCardOperation('card-status', row.iccid),
type: 'primary'
})
}
if (hasAuth('iot_card:start_card')) {
actions.push({
label: '启用此卡',
handler: () => handleCardOperation('start-card', row.iccid),
type: 'primary'
})
}
if (hasAuth('iot_card:stop_card')) {
actions.push({
label: '停用此卡',
handler: () => handleCardOperation('stop-card', row.iccid),
type: 'primary'
})
}
if (hasAuth('iot_card:manual_deactivate')) {
actions.push({
label: '手动停用',
handler: () => handleCardOperation('manual-deactivate', row.iccid),
type: 'primary'
})
}
return actions
}
onMounted(() => {
getTableData()
})
@@ -1089,7 +1145,7 @@
const getTableData = async () => {
loading.value = true
try {
const params = {
const params: Record<string, any> = {
page: pagination.page,
page_size: pagination.pageSize,
...formFilters
@@ -1174,33 +1230,6 @@
await loadTargetShops(query || undefined)
}
// 加载来源店铺列表
const loadFromShops = async (shopName?: string) => {
fromShopLoading.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) {
fromShopList.value = res.data.items || []
}
} catch (error) {
console.error('获取来源店铺列表失败:', error)
} finally {
fromShopLoading.value = false
}
}
// 搜索来源店铺
const searchFromShops = async (query: string) => {
await loadFromShops(query || undefined)
}
// 显示批量分配对话框
const showAllocateDialog = () => {
if (selectedCards.value.length === 0) {
@@ -1209,7 +1238,7 @@
}
allocateDialogVisible.value = true
Object.assign(allocateForm, {
selection_type: 'list',
selection_type: CardSelectionType.LIST,
to_shop_id: undefined,
iccids: selectedCards.value.map((card) => card.iccid),
iccid_start: '',
@@ -1234,7 +1263,7 @@
}
recallDialogVisible.value = true
Object.assign(recallForm, {
selection_type: 'list',
selection_type: CardSelectionType.LIST,
iccids: selectedCards.value.map((card) => card.iccid),
iccid_start: '',
iccid_end: '',
@@ -1274,16 +1303,16 @@
remark: allocateForm.remark
}
if (allocateForm.selection_type === 'list') {
if (allocateForm.selection_type === CardSelectionType.LIST) {
params.iccids = selectedCards.value.map((card) => card.iccid)
if (params.iccids.length === 0) {
ElMessage.warning('请先选择要分配的卡')
return
}
} else if (allocateForm.selection_type === 'range') {
} else if (allocateForm.selection_type === CardSelectionType.RANGE) {
params.iccid_start = allocateForm.iccid_start
params.iccid_end = allocateForm.iccid_end
} else if (allocateForm.selection_type === 'filter') {
} else if (allocateForm.selection_type === CardSelectionType.FILTER) {
if (allocateForm.carrier_id) params.carrier_id = allocateForm.carrier_id
if (allocateForm.status) params.status = allocateForm.status
if (allocateForm.batch_no) params.batch_no = allocateForm.batch_no
@@ -1327,16 +1356,16 @@
remark: recallForm.remark
}
if (recallForm.selection_type === 'list') {
if (recallForm.selection_type === CardSelectionType.LIST) {
params.iccids = selectedCards.value.map((card) => card.iccid)
if (params.iccids.length === 0) {
ElMessage.warning('请先选择要回收的卡')
return
}
} else if (recallForm.selection_type === 'range') {
} else if (recallForm.selection_type === CardSelectionType.RANGE) {
params.iccid_start = recallForm.iccid_start
params.iccid_end = recallForm.iccid_end
} else if (recallForm.selection_type === 'filter') {
} else if (recallForm.selection_type === CardSelectionType.FILTER) {
if (recallForm.carrier_id) params.carrier_id = recallForm.carrier_id
if (recallForm.batch_no) params.batch_no = recallForm.batch_no
}
@@ -1358,7 +1387,7 @@
// 刷新列表
await getTableData()
} else {
// code !== 0 才是真正失败(接口调用失败)
// code !== 0 才是真正失败(接口调用失败)
ElMessage.error(res.msg || '批量回收失败,请重试')
}
} catch (error) {
@@ -1501,60 +1530,6 @@
})
// 卡操作菜单项配置
const cardOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('iot_card:query_flow')) {
items.push({
key: 'query-flow',
label: '查询流量'
})
}
if (hasAuth('iot_card:query_realname_status')) {
items.push({
key: 'realname-status',
label: '查询实名状态'
})
}
if (hasAuth('iot_card:query_card_status')) {
items.push({
key: 'card-status',
label: '查询卡状态'
})
}
if (hasAuth('iot_card:start_card')) {
items.push({
key: 'start-card',
label: '启用此卡'
})
}
if (hasAuth('iot_card:stop_card')) {
items.push({
key: 'stop-card',
label: '停用此卡'
})
}
if (hasAuth('iot_card:manual_deactivate')) {
items.push({
key: 'manual-deactivate',
label: '手动停用'
})
}
return items
})
// 显示更多操作菜单 (右键)
const showMoreMenu = (e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
moreMenuRef.value?.show(e)
}
// 显示更多操作菜单 (左键点击)
const showMoreMenuOnClick = (e: MouseEvent) => {
@@ -1583,27 +1558,6 @@
}
}
// 显示卡操作菜单
const showCardOperationMenu = (e: MouseEvent, iccid: string, card?: StandaloneIotCard) => {
e.preventDefault()
e.stopPropagation()
currentOperatingIccid.value = iccid
currentOperatingCard.value = card || null
cardOperationMenuRef.value?.show(e)
}
// 处理卡操作菜单选择
const handleCardOperationMenuSelect = (item: MenuItemType) => {
const iccid = currentOperatingIccid.value
if (!iccid) return
if (item.key === 'query-flow') {
showFlowUsageDialog(iccid)
} else {
handleCardOperation(item.key, iccid)
}
}
// 网卡分销 - 正在开发中
const cardDistribution = () => {
ElMessage.info('功能正在开发中')
@@ -1645,7 +1599,7 @@
handleStopCard(iccid)
break
case 'manual-deactivate':
handleManualDeactivate()
handleManualDeactivate(iccid)
break
}
}
@@ -1668,7 +1622,7 @@
extend: res.data.extend
}
} else {
ElMessage.error(res.message || '查询失败')
ElMessage.error(res.msg || '查询失败')
flowUsageDialogVisible.value = false
}
} catch (error: any) {
@@ -1704,7 +1658,7 @@
extend: res.data.extend
}
} else {
ElMessage.error(res.message || '查询失败')
ElMessage.error(res.msg || '查询失败')
realnameStatusDialogVisible.value = false
}
} catch (error: any) {
@@ -1732,7 +1686,7 @@
extend: res.data.extend
}
} else {
ElMessage.error(res.message || '查询失败')
ElMessage.error(res.msg || '查询失败')
cardStatusDialogVisible.value = false
}
} catch (error: any) {
@@ -1752,11 +1706,11 @@
})
.then(async () => {
try {
// 使用接口:直接通过 ICCID 启用此卡
const res = await AssetService.startCard(iccid)
// 使用统一接口:通过 identifier (ICCID) 启用资产
const res = await AssetService.startAsset(iccid)
if (res.code === 0) {
ElMessage.success('启用成功')
getTableData()
await getTableData()
}
} catch (error: any) {
console.error('启用此卡失败:', error)
@@ -1776,11 +1730,11 @@
})
.then(async () => {
try {
// 使用接口:直接通过 ICCID 停用此卡
const res = await AssetService.stopCard(iccid)
// 使用统一接口:通过 identifier (ICCID) 停用资产
const res = await AssetService.stopAsset(iccid)
if (res.code === 0) {
ElMessage.success('停用成功')
getTableData()
await getTableData()
}
} catch (error: any) {
console.error('停用此卡失败:', error)
@@ -1792,15 +1746,9 @@
}
// 手动停用 IoT 卡(资产层面的停用)
const handleManualDeactivate = () => {
const card = currentOperatingCard.value
if (!card) {
ElMessage.error('未找到卡片信息')
return
}
const handleManualDeactivate = (iccid: string) => {
ElMessageBox.confirm(
`确定要手动停用卡片 ${card.iccid} 吗?此操作将在资产管理层面停用该卡。`,
`确定要手动停用卡片 ${iccid} 吗?此操作将在资产管理层面停用该卡。`,
'确认手动停用',
{
confirmButtonText: '确定',
@@ -1810,7 +1758,7 @@
)
.then(async () => {
try {
const res = await CardService.deactivateIotCard(card.id)
const res = await AssetService.deactivateAsset(iccid)
if (res.code === 0) {
ElMessage.success('手动停用成功')
await getTableData()
@@ -1823,18 +1771,9 @@
// 用户取消
})
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: StandaloneIotCard, column: any, event: MouseEvent) => {
showCardOperationMenu(event, row.iccid, row)
}
</script>
<style lang="scss" scoped>
.standalone-card-list-page {
// Card list page styles
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}

View File

@@ -72,7 +72,7 @@
align-center
destroy-on-close
>
<div v-if="failDataLoading" style=" padding: 40px;text-align: center">
<div v-if="failDataLoading" style="padding: 40px; text-align: center">
<ElSkeleton :rows="5" animated />
</div>
<div v-else-if="failedItems.length > 0">
@@ -92,7 +92,7 @@
<ElTableColumn prop="message" label="失败原因" min-width="200" show-overflow-tooltip />
</ElTable>
</div>
<div v-else style=" padding: 40px;text-align: center">
<div v-else style="padding: 40px; text-align: center">
<ElEmpty description="暂无失败数据" />
</div>
@@ -118,8 +118,10 @@
<p>1. 请先下载 Excel 模板文件按照模板格式填写IoT卡信息</p>
<p>2. 仅支持 Excel 格式.xlsx单次最多导入 1000 </p>
<p>3. 列格式请设置为文本格式避免长数字被转为科学计数法</p>
<p>4. 必填字段ICCIDMSISDN手机号可选字段virtual_no虚拟号</p>
<p>5. 必须选择运营商</p>
<p>4. <strong>必填字段ICCIDMSISDN手机号virtual_no虚拟号</strong></p>
<p>5. <strong>Excel 文件必须包含 virtual_no 否则整批导入将失败</strong></p>
<p>6. virtual_no 为空将导入失败</p>
<p>7. 必须选择运营商</p>
</div>
</template>
</ElAlert>

View File

@@ -17,13 +17,17 @@
<p>3. CSV 文件编码UTF-8推荐 GBK</p>
<p
>4.
必填字段virtual_no设备号device_name设备名称device_model设备型号</p
<strong>必填字段virtual_no设备虚拟</strong
>device_name设备名称device_model设备型号</p
>
<p
>5.
可选字段device_type设备类型manufacturer制造商max_sim_slots最大插槽数默认1</p
>
<p>6. 设备号重复将自动跳过导入后可在任务管理中查看详情</p>
<p
>6. virtual_no
为空将导入失败设备号重复将自动跳过导入后可在任务管理中查看详情</p
>
</div>
</template>
</ElAlert>

View File

@@ -14,8 +14,14 @@
<p><strong>导入说明</strong></p>
<p>1. 请先下载模板文件按照模板格式填写网卡信息</p>
<p>2. 支持 Excel 格式.xlsx, .xls单次最多导入 1000 </p>
<p>3. 必填字段ICCID运营商套餐类型流量规格</p>
<p>4. 导入后系统将自动校验数据重复 ICCID 将跳过</p>
<p
>3.
<strong
>必填字段ICCIDvirtual_no虚拟号运营商套餐类型流量规格</strong
></p
>
<p>4. <strong>Excel 文件必须包含 virtual_no 否则整批导入将失败</strong></p>
<p>5. virtual_no 为空将导入失败重复 ICCID 将跳过</p>
</div>
</template>
</ElAlert>

View File

@@ -480,7 +480,12 @@
const accountRules = reactive<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_-]+$/,
message: '用户名只能包含字母、数字、下划线和横线',
trigger: 'blur'
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },

View File

@@ -277,7 +277,7 @@
style="width: 100%"
placeholder="请输入入账金额(分)"
/>
<div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-secondary)">
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
金额单位为分例如100 = 10000
</div>
</ElFormItem>

View File

@@ -63,7 +63,7 @@
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
<ElFormItem label="关联订单ID" prop="order_id">
<ElFormItem label="订单号" prop="order_id">
<ElSelect
v-model="createForm.order_id"
filterable
@@ -73,6 +73,7 @@
:loading="orderSearchLoading"
placeholder="请输入订单号搜索"
style="width: 100%"
@focus="handleOrderSelectFocus"
>
<ElOption
v-for="order in orderSearchOptions"
@@ -677,17 +678,18 @@
// 搜索订单(用于创建弹窗)
const searchOrders = async (query: string) => {
if (!query) {
orderSearchOptions.value = []
return
}
orderSearchLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20,
order_no: query
page_size: 20
}
// 如果有搜索关键词,则按订单号搜索
if (query) {
params.order_no = query
}
const res = await OrderService.getOrders(params)
if (res.code === 0) {
orderSearchOptions.value = res.data.items || []
@@ -756,6 +758,14 @@
createDialogVisible.value = true
}
// 订单选择框获取焦点时加载默认数据
const handleOrderSelectFocus = () => {
// 如果还没有数据则加载默认的20条
if (orderSearchOptions.value.length === 0) {
searchOrders('')
}
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields()

View File

@@ -99,7 +99,7 @@
>
<div style="display: flex; justify-content: space-between">
<span>{{ card.iccid }}</span>
<span style=" font-size: 12px;color: var(--el-text-color-secondary)">
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ card.msisdn || '无接入号' }}
</span>
</div>
@@ -130,7 +130,7 @@
>
<div style="display: flex; justify-content: space-between">
<span>{{ device.virtual_no }}</span>
<span style=" font-size: 12px;color: var(--el-text-color-secondary)">
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ device.device_name }}
</span>
</div>
@@ -163,7 +163,7 @@
>
<div style="display: flex; justify-content: space-between">
<span>{{ pkg.package_name }}</span>
<span style=" font-size: 12px;color: var(--el-text-color-secondary)">
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
¥{{ (pkg.cost_price / 100).toFixed(2) }}
</span>
</div>
@@ -364,7 +364,6 @@
payment_status: undefined,
order_type: undefined,
purchase_role: undefined,
is_expired: undefined,
start_time: '',
end_time: ''
}
@@ -437,19 +436,6 @@
clearable: true
}
},
{
label: '超时状态',
prop: 'is_expired',
type: 'select',
placeholder: '请选择超时状态',
options: [
{ label: '已超时', value: true },
{ label: '未超时', value: false }
],
config: {
clearable: true
}
},
{
label: t('orderManagement.searchForm.dateRange'),
prop: 'dateRange',
@@ -480,8 +466,6 @@
{ label: '购买备注', prop: 'purchase_remark' },
{ label: '操作者', prop: 'operator_name' },
{ label: t('orderManagement.table.paymentStatus'), prop: 'payment_status' },
{ label: '超时状态', prop: 'is_expired' },
{ label: '超时时间', prop: 'expires_at' },
{ label: t('orderManagement.table.totalAmount'), prop: 'total_amount' },
{ label: '实付金额', prop: 'actual_paid_amount' },
{ label: t('orderManagement.table.paymentMethod'), prop: 'payment_method' },
@@ -656,7 +640,8 @@
const methodMap: Record<OrderPaymentMethod, string> = {
wallet: t('orderManagement.paymentMethod.wallet'),
wechat: t('orderManagement.paymentMethod.wechat'),
alipay: t('orderManagement.paymentMethod.alipay')
alipay: t('orderManagement.paymentMethod.alipay'),
offline: t('orderManagement.paymentMethod.offline')
}
return methodMap[method] || method
}
@@ -758,25 +743,6 @@
)
}
},
{
prop: 'is_expired',
label: '超时状态',
width: 100,
formatter: (row: Order) => {
if (row.is_expired === undefined || row.is_expired === null) return '-'
if (row.is_expired) {
return h(ElTag, { type: 'danger', size: 'small' }, () => '已超时')
} else {
return h(ElTag, { type: 'success', size: 'small' }, () => '未超时')
}
}
},
{
prop: 'expires_at',
label: '超时时间',
width: 180,
formatter: (row: Order) => (row.expires_at ? formatDateTime(row.expires_at) : '-')
},
{
prop: 'total_amount',
label: t('orderManagement.table.totalAmount'),
@@ -863,7 +829,6 @@
payment_status: searchForm.payment_status,
order_type: searchForm.order_type,
purchase_role: searchForm.purchase_role,
is_expired: searchForm.is_expired,
start_time: searchForm.start_time || undefined,
end_time: searchForm.end_time || undefined
}
@@ -1009,20 +974,41 @@
if (valid) {
createLoading.value = true
try {
// 获取资源ID (IoT卡ID或设备ID)
const resourceId =
createForm.order_type === 'single_card' ? createForm.iot_card_id : createForm.device_id
if (!resourceId) {
ElMessage.error('请选择IoT卡或设备')
return
// 获取 identifier (ICCID 或 VirtualNo)
let identifier = ''
if (createForm.order_type === 'single_card') {
if (!createForm.iot_card_id) {
ElMessage.error('请选择IoT卡')
return
}
const selectedCard = iotCardOptions.value.find(
(card) => card.id === createForm.iot_card_id
)
if (!selectedCard?.iccid) {
ElMessage.error('未找到选中的卡信息')
return
}
identifier = selectedCard.iccid
} else {
if (!createForm.device_id) {
ElMessage.error('请选择设备')
return
}
const selectedDevice = deviceOptions.value.find(
(dev) => dev.id === createForm.device_id
)
if (!selectedDevice?.virtual_no) {
ElMessage.error('未找到选中的设备信息')
return
}
identifier = selectedDevice.virtual_no
}
// 调用套餐购买预检接口
const checkData: PurchaseCheckRequest = {
order_type: createForm.order_type,
package_ids: createForm.package_ids,
resource_id: resourceId
resource_id: createForm.order_type === 'single_card' ? createForm.iot_card_id! : createForm.device_id!,
package_ids: createForm.package_ids
}
const checkResponse = await OrderService.purchaseCheck(checkData)
@@ -1045,11 +1031,9 @@
// 预检通过或用户确认后,创建订单
const data: CreateOrderRequest = {
order_type: createForm.order_type,
identifier,
package_ids: createForm.package_ids,
iot_card_id: createForm.order_type === 'single_card' ? createForm.iot_card_id : null,
device_id: createForm.order_type === 'device' ? createForm.device_id : null,
payment_method: createForm.payment_method // 必填字段
payment_method: createForm.payment_method
}
await OrderService.createOrder(data)

View File

@@ -34,6 +34,7 @@
import { PackageManageService } from '@/api/modules'
import type { PackageResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { getStatusLabel, getShelfStatusText } from '@/config/constants'
defineOptions({ name: 'PackageDetail' })
@@ -55,7 +56,7 @@
{
label: '套餐类型',
formatter: (_, data) => {
const typeMap = {
const typeMap: Record<string, string> = {
formal: '正式套餐',
addon: '附加套餐'
}
@@ -72,7 +73,7 @@
label: '套餐周期类型',
formatter: (_, data) => {
if (!data.calendar_type) return '-'
const typeMap = {
const typeMap: Record<string, string> = {
natural_month: '自然月',
by_day: '按天'
}
@@ -90,7 +91,7 @@
label: '流量重置周期',
formatter: (_, data) => {
if (!data.data_reset_cycle) return '-'
const cycleMap = {
const cycleMap: Record<string, string> = {
daily: '每日',
monthly: '每月',
yearly: '每年',
@@ -109,13 +110,13 @@
{
label: '状态',
formatter: (_, data) => {
return data.status === 1 ? '启用' : '禁用'
return getStatusLabel(data.status ?? 0)
}
},
{
label: '上架状态',
formatter: (_, data) => {
return data.shelf_status === 1 ? '上架' : '下架'
return getShelfStatusText(data.shelf_status ?? 0)
}
},
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },

View File

@@ -34,29 +34,15 @@
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -204,14 +190,20 @@
<!-- 真流量额度 -->
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="真流量额度(MB)" prop="real_data_mb">
<ElFormItem label="真流量额度(GB)" prop="real_data_gb">
<ElInputNumber
v-model="form.real_data_mb"
v-model="realDataGb"
:min="0"
:precision="2"
:step="0.1"
:controls="false"
style="width: 100%"
placeholder="请输入真流量额度"
@change="handleRealDataChange"
/>
<div v-if="realDataGb" style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
{{ realDataGb }}GB = {{ form.real_data_mb }}MB
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12">
@@ -228,14 +220,20 @@
<!-- 虚流量额度和到期计时基准 -->
<ElRow :gutter="20">
<ElCol :span="12" v-if="form.enable_virtual_data">
<ElFormItem label="虚流量额度(MB)" prop="virtual_data_mb">
<ElFormItem label="虚流量额度(GB)" prop="virtual_data_gb">
<ElInputNumber
v-model="form.virtual_data_mb"
v-model="virtualDataGb"
:min="0"
:precision="2"
:step="0.1"
:controls="false"
style="width: 100%"
placeholder="请输入虚流量额度"
@change="handleVirtualDataChange"
/>
<div v-if="virtualDataGb" style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
{{ virtualDataGb }}GB = {{ form.virtual_data_mb }}MB
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12">
@@ -363,16 +361,13 @@
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { PackageManageService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton, ElInputNumber } from 'element-plus'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElInputNumber } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { PackageResponse, SeriesSelectOption } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import {
@@ -382,13 +377,17 @@
apiStatusToFrontend,
PACKAGE_TYPE_OPTIONS,
getPackageTypeLabel,
getPackageTypeTag
getPackageTypeTag,
STATUS_SELECT_OPTIONS,
SHELF_STATUS_SELECT_OPTIONS,
getShelfStatusText
} from '@/config/constants'
import { generatePackageCode } from '@/utils/codeGenerator'
defineOptions({ name: 'PackageList' })
const { hasAuth } = useAuth()
const userStore = useUserStore()
const router = useRouter()
const dialogVisible = ref(false)
@@ -397,8 +396,6 @@
const seriesLoading = ref(false)
const tableRef = ref()
const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<PackageResponse | null>(null)
const seriesOptions = ref<SeriesSelectOption[]>([])
const searchSeriesOptions = ref<SeriesSelectOption[]>([])
@@ -419,14 +416,6 @@
})
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 搜索表单初始值
const initialSearchState = {
package_name: '',
@@ -490,10 +479,7 @@
clearable: true,
placeholder: '请选择上架状态'
},
options: () => [
{ label: '上架', value: 1 },
{ label: '下架', value: 2 }
]
options: SHELF_STATUS_SELECT_OPTIONS
},
{
label: '状态',
@@ -503,10 +489,7 @@
clearable: true,
placeholder: '请选择状态'
},
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 2 }
]
options: STATUS_SELECT_OPTIONS
}
])
@@ -559,7 +542,7 @@
baseRules.virtual_data_mb = [
{ required: true, message: '请输入虚流量额度', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validator: (_rule: any, value: any, callback: any) => {
if (value && form.real_data_mb && value > form.real_data_mb) {
callback(new Error('虚流量额度不能超过真流量额度'))
} else {
@@ -602,6 +585,27 @@
description: ''
})
// GB 输入值(用于界面显示)
const realDataGb = ref<number>(0)
const virtualDataGb = ref<number>(0)
// GB 转 MB 处理
const handleRealDataChange = (value: number | null) => {
if (value === null || value === undefined) {
form.real_data_mb = 0
return
}
form.real_data_mb = Math.round(value * 1024)
}
const handleVirtualDataChange = (value: number | null) => {
if (value === null || value === undefined) {
form.virtual_data_mb = 0
return
}
form.virtual_data_mb = Math.round(value * 1024)
}
const packageList = ref<PackageResponse[]>([])
const dialogType = ref('add')
@@ -619,14 +623,19 @@
minWidth: 160,
showOverflowTooltip: true,
formatter: (row: PackageResponse) => {
const hasPermission = hasAuth('package:detail')
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
style: hasPermission
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
: '',
onClick: hasPermission
? (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
: undefined
},
row.package_name
)
@@ -652,13 +661,29 @@
prop: 'real_data_mb',
label: '真流量',
width: 120,
formatter: (row: PackageResponse) => `${row.real_data_mb}MB`
formatter: (row: PackageResponse) => {
const mb = row.real_data_mb ?? 0
if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)}GB`
}
return `${mb}MB`
}
},
{
prop: 'virtual_data_mb',
label: '虚流量',
width: 100,
formatter: (row: PackageResponse) => `${row.virtual_data_mb}MB`
width: 120,
formatter: (row: PackageResponse) => {
// 如果没有启用虚流量,显示 "-"
if (!row.enable_virtual_data) {
return '-'
}
const mb = row.virtual_data_mb ?? 0
if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)}GB`
}
return `${mb}MB`
}
},
{
prop: 'virtual_ratio',
@@ -666,8 +691,10 @@
width: 120,
formatter: (row: PackageResponse) => {
// 如果启用虚流量且虚流量大于0计算比例
if (row.enable_virtual_data && row.virtual_data_mb > 0) {
const ratio = row.real_data_mb / row.virtual_data_mb
const virtualData = row.virtual_data_mb ?? 0
const realData = row.real_data_mb ?? 0
if (row.enable_virtual_data && virtualData > 0) {
const ratio = realData / virtualData
return ratio.toFixed(2)
}
// 否则返回 1.0
@@ -684,14 +711,16 @@
prop: 'cost_price',
label: '成本价',
width: 100,
formatter: (row: PackageResponse) => `¥${(row.cost_price / 100).toFixed(2)}`
formatter: (row: PackageResponse) => `¥${((row.cost_price ?? 0) / 100).toFixed(2)}`
},
{
prop: 'suggested_retail_price',
label: '建议售价',
width: 100,
formatter: (row: PackageResponse) =>
row.suggested_retail_price ? `¥${(row.suggested_retail_price / 100).toFixed(2)}` : '-'
row.suggested_retail_price
? `¥${((row.suggested_retail_price ?? 0) / 100).toFixed(2)}`
: '-'
},
{
prop: 'shelf_status',
@@ -700,8 +729,8 @@
formatter: (row: PackageResponse) => {
return h(ElSwitch, {
modelValue: row.shelf_status === 1,
activeText: '上架',
inactiveText: '下架',
activeText: getShelfStatusText(1),
inactiveText: getShelfStatusText(2),
inlinePrompt: true,
disabled: !hasAuth('package:update_away'),
'onUpdate:modelValue': (val: string | number | boolean) =>
@@ -714,7 +743,7 @@
label: '状态',
width: 100,
formatter: (row: PackageResponse) => {
const frontendStatus = apiStatusToFrontend(row.status)
const frontendStatus = apiStatusToFrontend(row.status ?? 0)
return h(ElSwitch, {
modelValue: frontendStatus,
activeValue: CommonStatus.ENABLED,
@@ -736,33 +765,29 @@
}
])
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
// 操作列配置
const getActions = (row: PackageResponse) => {
const actions: any[] = []
if (hasAuth('package:edit')) {
items.push({
key: 'edit',
label: '编辑'
})
actions.push({ label: '编辑', handler: () => showDialog('edit', row), type: 'primary' })
}
if (hasAuth('package:update_retail_price')) {
items.push({
key: 'update-retail-price',
label: '修改零售价'
// 只有代理账号(user_type=3)才显示修改零售价
if (hasAuth('package:update_retail_price') && userStore.getUserInfo.user_type === 3) {
actions.push({
label: '修改零售价',
handler: () => showRetailPriceDialog(row),
type: 'primary'
})
}
if (hasAuth('package:delete')) {
items.push({
key: 'delete',
label: '删除'
})
actions.push({ label: '删除', handler: () => deletePackage(row), type: 'danger' })
}
return items
})
return actions
}
// 监听虚流量开关变化,关闭时重置虚流量额度
watch(
@@ -950,7 +975,10 @@
form.expiry_base = row.expiry_base || 'from_activation'
form.real_data_mb = row.real_data_mb || 0
form.virtual_data_mb = row.virtual_data_mb || 0
form.cost_price = row.cost_price / 100 // 分转换为元显示
// MB 转换为 GB 显示
realDataGb.value = form.real_data_mb ? Number((form.real_data_mb / 1024).toFixed(2)) : 0
virtualDataGb.value = form.virtual_data_mb ? Number((form.virtual_data_mb / 1024).toFixed(2)) : 0
form.cost_price = (row.cost_price ?? 0) / 100 // 分转换为元显示
form.suggested_retail_price = row.suggested_retail_price
? row.suggested_retail_price / 100
: undefined
@@ -969,6 +997,9 @@
form.expiry_base = 'from_activation'
form.real_data_mb = 0
form.virtual_data_mb = 0
// 重置 GB 值
realDataGb.value = 0
virtualDataGb.value = 0
form.cost_price = 0
form.suggested_retail_price = undefined
form.description = ''
@@ -1008,6 +1039,9 @@
form.expiry_base = 'from_activation'
form.real_data_mb = 0
form.virtual_data_mb = 0
// 重置 GB 值
realDataGb.value = 0
virtualDataGb.value = 0
form.cost_price = 0
form.suggested_retail_price = undefined
form.description = ''
@@ -1147,20 +1181,12 @@
}
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PackageResponse, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 显示零售价修改对话框
const showRetailPriceDialog = (row: PackageResponse) => {
retailPriceForm.id = row.id
retailPriceForm.package_name = row.package_name
// 将分转换为元显示
retailPriceForm.retail_price = row.retail_price ? row.retail_price / 100 : 0
retailPriceForm.retail_price = row.suggested_retail_price ? row.suggested_retail_price / 100 : 0
retailPriceDialogVisible.value = true
}
@@ -1177,7 +1203,7 @@
if (res.code === 0) {
ElMessage.success('零售价修改成功')
retailPriceDialogVisible.value = false
loadPackageList()
await getTableData()
}
} catch (error: any) {
console.error('修改零售价失败:', error)
@@ -1192,34 +1218,9 @@
retailPriceDialogVisible.value = false
retailPriceFormRef.value?.resetFields()
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showDialog('edit', currentRow.value)
break
case 'update-retail-price':
showRetailPriceDialog(currentRow.value)
break
case 'delete':
deletePackage(currentRow.value)
break
}
}
</script>
<style lang="scss" scoped>
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.package-list-page {
// 可以添加特定样式
}
.dialog-footer {
text-align: right;
}

View File

@@ -34,6 +34,7 @@
import { PackageSeriesService } from '@/api/modules'
import type { PackageSeriesResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { getEnableStatusText } from '@/config/constants'
defineOptions({ name: 'PackageSeriesDetail' })
@@ -72,7 +73,7 @@
{
label: '启用状态',
formatter: (_, data) => {
return data.one_time_commission_config?.enable ? '已启用' : '未启用'
return getEnableStatusText(data.one_time_commission_config?.enable || false)
}
},
{
@@ -151,7 +152,9 @@
{
label: '启用状态',
formatter: (_, data) => {
return data.one_time_commission_config?.enable_force_recharge ? '已启用' : '未启用'
return getEnableStatusText(
data.one_time_commission_config?.enable_force_recharge || false
)
}
},
{
@@ -180,7 +183,7 @@
formatter: (_, data) => {
const config = data.one_time_commission_config
if (!config?.validity_type) return '-'
const typeMap = {
const typeMap: Record<string, string> = {
permanent: '永久',
fixed_date: '固定日期',
relative: '相对时长'

View File

@@ -35,29 +35,15 @@
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 套餐系列操作右键菜单 -->
<ArtMenuRight
ref="seriesOperationMenuRef"
:menu-items="seriesOperationMenuItems"
:menu-width="140"
@select="handleSeriesOperationMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -404,18 +390,16 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
import { formatDateTime } from '@/utils/business/format'
import {
CommonStatus,
getStatusText,
frontendStatusToApi,
apiStatusToFrontend
apiStatusToFrontend,
STATUS_SELECT_OPTIONS,
ENABLE_STATUS_OPTIONS,
getEnableStatusText
} from '@/config/constants'
import { useRouter } from 'vue-router'
@@ -430,15 +414,11 @@
const tableRef = ref()
const formRef = ref<FormInstance>()
// 右键菜单
const seriesOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingSeries = ref<PackageSeriesResponse | null>(null)
// 搜索表单初始值
const initialSearchState = {
series_name: '',
status: undefined as number | undefined,
enable_one_time_commission: undefined as boolean | undefined
status: undefined,
enable_one_time_commission: undefined
}
// 搜索表单
@@ -463,10 +443,7 @@
clearable: true,
placeholder: '请选择状态'
},
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 2 }
]
options: STATUS_SELECT_OPTIONS
},
{
label: '一次性佣金',
@@ -476,10 +453,7 @@
clearable: true,
placeholder: '请选择'
},
options: () => [
{ label: '已启用', value: true },
{ label: '未启用', value: false }
]
options: ENABLE_STATUS_OPTIONS
}
]
@@ -557,14 +531,19 @@
label: '系列名称',
minWidth: 150,
formatter: (row: PackageSeriesResponse) => {
const hasPermission = hasAuth('package_series:detail')
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
style: hasPermission
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
: '',
onClick: hasPermission
? (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
: undefined
},
row.series_name
)
@@ -582,10 +561,10 @@
return h(
ElTag,
{
type: row.one_time_commission_config.enable ? 'success' : 'info',
type: row.one_time_commission_config?.enable ? 'success' : 'info',
size: 'small'
},
() => (row.one_time_commission_config.enable ? '已启用' : '未启用')
() => getEnableStatusText(row.one_time_commission_config?.enable || false)
)
}
},
@@ -641,10 +620,10 @@
return h(
ElTag,
{
type: row.one_time_commission_config.enable_force_recharge ? 'warning' : 'info',
type: row.one_time_commission_config?.enable_force_recharge ? 'warning' : 'info',
size: 'small'
},
() => (row.one_time_commission_config.enable_force_recharge ? '已启用' : '未启用')
() => getEnableStatusText(row.one_time_commission_config?.enable_force_recharge || false)
)
}
},
@@ -738,63 +717,22 @@
}
])
// 操作列配置
const getActions = (row: PackageSeriesResponse) => {
const actions: any[] = []
if (hasAuth('package_series:edit')) {
actions.push({ label: '编辑', handler: () => showDialog('edit', row), type: 'primary' })
}
if (hasAuth('package_series:delete')) {
actions.push({ label: '删除', handler: () => deleteSeries(row), type: 'danger' })
}
return actions
}
onMounted(() => {
getTableData()
})
// 套餐系列操作菜单项配置
const seriesOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
// 编辑
if (hasAuth('package_series:edit')) {
items.push({
key: 'edit',
label: '编辑'
})
}
// 删除
if (hasAuth('package_series:delete')) {
items.push({
key: 'delete',
label: '删除'
})
}
return items
})
// 显示套餐系列操作右键菜单
const showSeriesOperationMenu = (e: MouseEvent, row: PackageSeriesResponse) => {
e.preventDefault()
e.stopPropagation()
currentOperatingSeries.value = row
seriesOperationMenuRef.value?.show(e)
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PackageSeriesResponse, column: any, event: MouseEvent) => {
// 如果用户有编辑或删除权限,显示右键菜单
if (hasAuth('package_series:edit') || hasAuth('package_series:delete')) {
showSeriesOperationMenu(event, row)
}
}
// 处理套餐系列操作菜单选择
const handleSeriesOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingSeries.value) return
switch (item.key) {
case 'edit':
showDialog('edit', currentOperatingSeries.value)
break
case 'delete':
deleteSeries(currentOperatingSeries.value)
break
}
}
// 获取套餐系列列表
const getTableData = async () => {
loading.value = true
@@ -1121,27 +1059,10 @@
ElMessage.warning('您没有查看详情的权限')
}
}
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
</script>
<style lang="scss" scoped>
.package-series-page {
// 可以添加特定样式
}
.dialog-footer {
text-align: right;
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -120,7 +120,7 @@
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="启用状态">
<ElTag :type="detailData.force_recharge_enabled ? 'warning' : 'info'">
{{ detailData.force_recharge_enabled ? '已启用' : '未启用' }}
{{ getEnableStatusText(detailData.force_recharge_enabled || false) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="锁定状态">
@@ -314,6 +314,7 @@
PackageSeriesResponse
} from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { getEnableStatusText } from '@/config/constants'
defineOptions({ name: 'SeriesGrantsDetail' })
@@ -504,6 +505,7 @@
const handleSavePackage = async () => {
if (!packageFormRef.value || !detailData.value) return
const grantId = detailData.value.id // 保存到常量中避免类型检查问题
await packageFormRef.value.validate(async (valid) => {
if (!valid) return
@@ -525,7 +527,7 @@
})
}
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { packages })
await ShopSeriesGrantService.manageGrantPackages(grantId, { packages })
ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
packageDialogVisible.value = false

View File

@@ -35,29 +35,16 @@
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="160"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 套餐列表对话框 -->
<ElDialog
v-model="packageListDialogVisible"
@@ -777,17 +764,14 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import {
CommonStatus,
getStatusText,
frontendStatusToApi,
apiStatusToFrontend
apiStatusToFrontend,
STATUS_SELECT_OPTIONS,
getEnableStatusText
} from '@/config/constants'
defineOptions({ name: 'SeriesGrants' })
@@ -803,8 +787,6 @@
const packageLoading = ref(false)
const tableRef = ref()
const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<ShopSeriesGrantResponse | null>(null)
const currentStep = ref(0) // 当前步骤0 为第一步1 为第二步
// 套餐列表对话框相关
@@ -930,10 +912,7 @@
clearable: true,
placeholder: '请选择状态'
},
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 2 }
]
options: STATUS_SELECT_OPTIONS
}
])
@@ -1064,14 +1043,19 @@
label: '系列名称',
minWidth: 150,
formatter: (row: ShopSeriesGrantResponse) => {
const hasPermission = hasAuth('series_name_grants:detail')
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
style: hasPermission
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
: '',
onClick: hasPermission
? (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
: undefined
},
row.series_name
)
@@ -1137,7 +1121,7 @@
return h(
ElTag,
{ type: row.force_recharge_enabled ? 'warning' : 'info', size: 'small' },
() => (row.force_recharge_enabled ? '已启用' : '未启用')
() => getEnableStatusText(row.force_recharge_enabled || false)
)
}
},
@@ -1173,6 +1157,29 @@
}
])
// 操作列配置
const getActions = (row: ShopSeriesGrantResponse) => {
const actions: any[] = []
if (hasAuth('series_grants:detail')) {
actions.push({
label: '套餐列表',
handler: () => showPackageListDialog(row),
type: 'primary'
})
}
if (hasAuth('series_grants:edit')) {
actions.push({ label: '编辑', handler: () => showDialog('edit', row), type: 'primary' })
}
if (hasAuth('series_grants:delete')) {
actions.push({ label: '删除', handler: () => deleteAllocation(row), type: 'danger' })
}
return actions
}
// 构建树形结构数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
@@ -1865,50 +1872,6 @@
}
}
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('series_grants:detail')) {
items.push({ key: 'view_packages', label: '查看套餐列表' })
}
if (hasAuth('series_grants:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('series_grants:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: ShopSeriesGrantResponse, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'view_packages':
showPackageListDialog(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteAllocation(currentRow.value)
break
}
}
// 显示套餐列表对话框
const showPackageListDialog = async (row: ShopSeriesGrantResponse) => {
currentGrantId.value = row.id
@@ -2108,14 +2071,6 @@
}
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 下一步
const handleNextStep = async () => {
if (!formRef.value) return

View File

@@ -208,7 +208,7 @@
:total="logPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
style=" justify-content: flex-end;margin-top: 16px"
style="justify-content: flex-end; margin-top: 16px"
@size-change="getCleanupLogs"
@current-change="getCleanupLogs"
/>

View File

@@ -34,22 +34,17 @@
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="false"
:pagination="false"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="160"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -105,7 +100,7 @@
<ElFormItem label="所在地区" prop="region">
<ElCascader
v-model="formData.region"
:options="regionData"
:options="regionData as any"
placeholder="请选择省/市/区"
clearable
filterable
@@ -219,14 +214,6 @@
</template>
</ElDialog>
<!-- 店铺操作右键菜单 -->
<ArtMenuRight
ref="shopOperationMenuRef"
:menu-items="shopOperationMenuItems"
:menu-width="140"
@select="handleShopOperationMenuSelect"
/>
<!-- 店铺默认角色管理对话框 -->
<ElDialog
v-model="defaultRolesDialogVisible"
@@ -303,22 +290,17 @@
ElTreeSelect,
ElCascader
} from 'element-plus'
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
import { ShopService, RoleService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { ShopResponse, ShopRoleResponse } from '@/types/api'
import { RoleType } from '@/types/api'
import { RoleType, RoleStatus } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
import { RoutesAlias } from '@/router/routesAlias'
import { regionData } from '@/utils/constants/regionData'
defineOptions({ name: 'Shop' })
@@ -326,15 +308,6 @@
const { hasAuth } = useAuth()
const router = useRouter()
// 使
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const dialogType = ref('add')
const dialogVisible = ref(false)
const loading = ref(false)
@@ -353,10 +326,6 @@
const availableRoles = ref<ShopRoleResponse[]>([])
const selectedRoleId = ref<number | undefined>(undefined)
//
const shopOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingShop = ref<ShopResponse | null>(null)
//
const initialSearchState = {
shop_name: '',
@@ -511,7 +480,7 @@
}
//
nextTick(() => {
await nextTick(() => {
formRef.value?.clearValidate()
})
@@ -602,29 +571,48 @@
label: '创建时间',
width: 180,
formatter: (row: ShopResponse) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 110,
fixed: 'right',
formatter: (row: ShopResponse) => {
const buttons = []
if (hasAuth('shop:look_customer')) {
buttons.push(
h(ArtButtonTable, {
text: '账号列表',
onClick: () => viewCustomerAccounts(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
//
const getActions = (row: ShopResponse) => {
const actions: any[] = []
if (hasAuth('shop:look_customer')) {
actions.push({
label: '账号列表',
handler: () => viewCustomerAccounts(row),
type: 'primary'
})
}
if (hasAuth('shop:default_role')) {
actions.push({
label: '默认角色',
handler: () => showDefaultRolesDialog(row),
type: 'primary'
})
}
if (hasAuth('shop:edit')) {
actions.push({
label: '编辑',
handler: () => showDialog('edit', row),
type: 'primary'
})
}
if (hasAuth('shop:delete')) {
actions.push({
label: '删除',
handler: () => deleteShop(row),
type: 'danger'
})
}
return actions
}
//
const formRef = ref<FormInstance>()
@@ -658,8 +646,8 @@
}
//
const handleRegionChange = (value: string[]) => {
if (value && value.length === 3) {
const handleRegionChange = (value: any) => {
if (value && Array.isArray(value) && value.length === 3) {
formData.province = value[0]
formData.city = value[1]
formData.district = value[2]
@@ -677,8 +665,8 @@
const params: any = {
page: 1,
pageSize: 20,
role_type: 2, //
status: 1 //
role_type: RoleType.CUSTOMER, //
status: RoleStatus.ENABLED //
}
if (query) {
params.role_name = query
@@ -900,70 +888,6 @@
})
}
//
const shopOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
//
if (hasAuth('shop:default_role')) {
items.push({
key: 'defaultRoles',
label: '默认角色'
})
}
//
if (hasAuth('shop:edit')) {
items.push({
key: 'edit',
label: '编辑'
})
}
//
if (hasAuth('shop:delete')) {
items.push({
key: 'delete',
label: '删除'
})
}
return items
})
//
const showShopOperationMenu = (e: MouseEvent, row: ShopResponse) => {
e.preventDefault()
e.stopPropagation()
currentOperatingShop.value = row
shopOperationMenuRef.value?.show(e)
}
//
const handleRowContextMenu = (row: ShopResponse, column: any, event: MouseEvent) => {
// ,
if (hasAuth('shop:edit') || hasAuth('shop:delete')) {
showShopOperationMenu(event, row)
}
}
//
const handleShopOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingShop.value) return
switch (item.key) {
case 'defaultRoles':
showDefaultRolesDialog(currentOperatingShop.value)
break
case 'edit':
showDialog('edit', currentOperatingShop.value)
break
case 'delete':
deleteShop(currentOperatingShop.value)
break
}
}
//
const handleStatusChange = async (row: ShopResponse, newStatus: number) => {
const oldStatus = row.status
@@ -1016,9 +940,9 @@
try {
const res = await RoleService.getRoles({
page: 1,
page_size: 9999,
role_type: 2, //
status: 1 // RoleStatus.ENABLED
page_size: 100,
role_type: RoleType.CUSTOMER, //
status: RoleStatus.ENABLED //
})
if (res.code === 0) {
// PlatformRole ShopRoleResponse
@@ -1071,10 +995,6 @@
</script>
<style lang="scss" scoped>
.shop-page {
//
}
.current-role-display {
padding: 16px 20px;
margin-bottom: 24px;
@@ -1116,8 +1036,4 @@
color: #303133;
}
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -33,29 +33,14 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -163,18 +148,13 @@
<script setup lang="ts">
import { h, watch, nextTick } from 'vue'
import { CarrierService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { Carrier, CarrierType } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import {
CommonStatus,
@@ -191,17 +171,6 @@
const loading = ref(false)
const submitLoading = ref(false)
const tableRef = ref()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<any>(null)
// 使
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
//
const initialSearchState = {
@@ -388,6 +357,45 @@
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.created_at)
},
{
prop: 'actions',
label: '操作',
width: 160,
fixed: 'right',
formatter: (row: any) => {
const buttons: any[] = []
if (hasAuth('carrier:edit')) {
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => showDialog('edit', row)
},
() => '编辑'
)
)
}
if (hasAuth('carrier:delete')) {
buttons.push(
h(
ElButton,
{
type: 'danger',
link: true,
onClick: () => deleteCarrier(row)
},
() => '删除'
)
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
@@ -574,51 +582,10 @@
console.error(error)
}
}
//
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('carrier:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('carrier:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
//
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
//
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteCarrier(currentRow.value)
break
}
}
</script>
<style scoped lang="scss">
.carrier-page {
height: 100%;
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -1,661 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="menu-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
:showExpand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
:showZebra="false"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<!-- 按钮权限后端控制模式使用自定义指令 -->
<ElButton v-auth="'add'" @click="showModel('menu', null, true)" v-ripple>
添加菜单
</ElButton>
<ElButton @click="toggleExpand" v-ripple>
{{ isExpanded ? '收起' : '展开' }}
</ElButton>
<!-- 按钮权限前端控制模式使用 hasAuth 方法 -->
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
添加菜单
</ElButton> -->
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
rowKey="path"
ref="tableRef"
:loading="loading"
:data="filteredTableData"
:marginTop="10"
:stripe="false"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
<ElFormItem label="菜单类型">
<ElRadioGroup v-model="labelPosition" :disabled="disableMenuType">
<ElRadioButton value="menu" label="menu">菜单</ElRadioButton>
<ElRadioButton value="button" label="button">权限</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<template v-if="labelPosition === 'menu'">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="菜单名称" prop="name">
<ElInput v-model="form.name" placeholder="菜单名称"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="路由地址" prop="path">
<ElInput v-model="form.path" placeholder="路由地址"></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="权限标识" prop="label">
<ElInput v-model="form.label" placeholder="权限标识"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="图标" prop="icon">
<ArtIconSelector :iconType="iconType" :defaultIcon="form.icon" width="229px" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="菜单排序" prop="sort" style="width: 100%">
<ElInputNumber
v-model="form.sort"
style="width: 100%"
@change="handleChange"
:min="1"
controls-position="right"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="外部链接" prop="link">
<ElInput
v-model="form.link"
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="5">
<ElFormItem label="是否启用" prop="isEnable">
<ElSwitch v-model="form.isEnable"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="页面缓存" prop="keepAlive">
<ElSwitch v-model="form.keepAlive"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="是否显示" prop="isHidden">
<ElSwitch v-model="form.isHidden"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="是否内嵌" prop="isMenu">
<ElSwitch v-model="form.isIframe"></ElSwitch>
</ElFormItem>
</ElCol>
</ElRow>
</template>
<template v-if="labelPosition === 'button'">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="权限名称" prop="authName">
<ElInput v-model="form.authName" placeholder="权限名称"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="权限标识" prop="authLabel">
<ElInput v-model="form.authLabel" placeholder="权限标识"></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="权限排序" prop="authSort" style="width: 100%">
<ElInputNumber
v-model="form.authSort"
style="width: 100%"
@change="handleChange"
:min="1"
controls-position="right"
/>
</ElFormItem>
</ElCol>
</ElRow>
</template>
</ElForm>
<template #footer>
<span class="dialog-footer">
<ElButton @click="dialogVisible = false"> </ElButton>
<ElButton type="primary" @click="submitForm()"> </ElButton>
</span>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { useMenuStore } from '@/store/modules/menu'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { IconTypeEnum } from '@/enums/appEnum'
import { formatMenuTitle } from '@/router/utils/utils'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { ElPopover, ElButton } from 'element-plus'
import { AppRouteRecord } from '@/types/router'
import { useAuth } from '@/composables/useAuth'
import { SearchFormItem } from '@/types'
defineOptions({ name: 'Menus' })
const { hasAuth } = useAuth()
const { menuList } = storeToRefs(useMenuStore())
const loading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
name: '',
route: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
// 增加实际应用的搜索条件状态
const appliedFilters = reactive({ ...initialSearchState })
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
Object.assign(appliedFilters, { ...initialSearchState })
getTableData()
}
// 搜索处理
const handleSearch = () => {
// 将当前输入的筛选条件应用到实际搜索
Object.assign(appliedFilters, { ...formFilters })
getTableData()
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '菜单名称',
prop: 'name',
type: 'input',
config: {
clearable: true
}
},
{
label: '路由地址',
prop: 'route',
type: 'input',
config: {
clearable: true
}
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '用户名', prop: 'avatar' },
{ label: '手机号', prop: 'mobile' },
{ label: '性别', prop: 'sex' },
{ label: '部门', prop: 'dep' },
{ label: '状态', prop: 'status' },
{ label: '创建日期', prop: 'create_time' },
{ label: '操作', prop: 'operation' }
]
// 构建菜单类型标签
const buildMenuTypeTag = (row: AppRouteRecord) => {
if (row.children && row.children.length > 0) {
return 'info'
} else if (row.meta?.link && row.meta?.isIframe) {
return 'success'
} else if (row.path) {
return 'primary'
} else if (row.meta?.link) {
return 'warning'
}
}
// 构建菜单类型文本
const buildMenuTypeText = (row: AppRouteRecord) => {
if (row.children && row.children.length > 0) {
return '目录'
} else if (row.meta?.link && row.meta?.isIframe) {
return '内嵌'
} else if (row.path) {
return '菜单'
} else if (row.meta?.link) {
return '外链'
}
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'meta.title',
label: '菜单名称',
minWidth: 120,
formatter: (row: AppRouteRecord) => {
return formatMenuTitle(row.meta?.title)
}
},
{
prop: 'type',
label: '菜单类型',
formatter: (row: AppRouteRecord) => {
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
}
},
{
prop: 'path',
label: '路由',
formatter: (row: AppRouteRecord) => {
return row.meta?.link || row.path || ''
}
},
{
prop: 'meta.authList',
label: '可操作权限',
formatter: (row: AppRouteRecord) => {
return h(
'div',
{},
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
return h(
ElPopover,
{
placement: 'top-start',
title: '操作',
width: 200,
trigger: 'click',
key: index
},
{
default: () =>
h('div', { style: 'margin: 0; text-align: right' }, [
h(
ElButton,
{
size: 'small',
type: 'primary',
onClick: () => showModel('button', item)
},
{ default: () => '编辑' }
),
h(
ElButton,
{
size: 'small',
type: 'danger',
onClick: () => deleteAuth()
},
{ default: () => '删除' }
)
]),
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
}
)
})
)
}
},
{
prop: 'date',
label: '编辑时间',
formatter: () => '2022-3-12 12:00:00'
},
{
prop: 'status',
label: '隐藏菜单',
formatter: (row) => {
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
row.meta.isHide ? '是' : '否'
)
}
},
{
prop: 'operation',
label: '操作',
width: 180,
formatter: (row: AppRouteRecord) => {
return h('div', [
hasAuth('B_CODE1') &&
h(ArtButtonTable, {
type: 'add',
onClick: () => showModel('menu')
}),
hasAuth('B_CODE2') &&
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
hasAuth('B_CODE3') &&
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteMenu()
})
])
}
}
])
const handleRefresh = () => {
getTableData()
}
const dialogVisible = ref(false)
const form = reactive({
// 菜单
name: '',
path: '',
label: '',
icon: '',
isEnable: true,
sort: 1,
isMenu: true,
keepAlive: true,
isHidden: true,
link: '',
isIframe: false,
// 权限 (修改这部分)
authName: '',
authLabel: '',
authIcon: '',
authSort: 1
})
const iconType = ref(IconTypeEnum.UNICODE)
const labelPosition = ref('menu')
const rules = reactive<FormRules>({
name: [
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }],
label: [{ required: true, message: '输入权限标识', trigger: 'blur' }],
// 修改这部分
authName: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
authLabel: [{ required: true, message: '请输入权限权限标识', trigger: 'blur' }]
})
const tableData = ref<AppRouteRecord[]>([])
onMounted(() => {
getTableData()
})
const getTableData = () => {
loading.value = true
setTimeout(() => {
tableData.value = menuList.value
loading.value = false
}, 500)
}
// 过滤后的表格数据
const filteredTableData = computed(() => {
// 递归搜索函数
const searchMenu = (items: AppRouteRecord[]): AppRouteRecord[] => {
return items.filter((item) => {
// 获取搜索关键词,转换为小写并去除首尾空格
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
// 获取菜单标题和路径,确保它们存在
const menuTitle = formatMenuTitle(item.meta?.title || '').toLowerCase()
const menuPath = (item.path || '').toLowerCase()
// 使用 includes 进行模糊匹配
const nameMatch = !searchName || menuTitle.includes(searchName)
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
// 如果有子菜单,递归搜索
if (item.children && item.children.length > 0) {
const matchedChildren = searchMenu(item.children)
// 如果子菜单有匹配项,保留当前菜单
if (matchedChildren.length > 0) {
item.children = matchedChildren
return true
}
}
return nameMatch && routeMatch
})
}
return searchMenu(tableData.value)
})
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const dialogTitle = computed(() => {
const type = labelPosition.value === 'menu' ? '菜单' : '权限'
return isEdit.value ? `编辑${type}` : `新建${type}`
})
const showDialog = (type: string, row: AppRouteRecord) => {
showModel('menu', row, true)
}
const handleChange = () => {}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
// const menuStore = useMenuStore()
// const params =
// labelPosition.value === 'menu'
// ? {
// title: form.name,
// path: form.path,
// name: form.label,
// icon: form.icon,
// sort: form.sort,
// isEnable: form.isEnable,
// isMenu: form.isMenu,
// keepAlive: form.keepAlive,
// isHidden: form.isHidden,
// link: form.link
// }
// : {
// title: form.authName,
// name: form.authLabel,
// icon: form.authIcon,
// sort: form.authSort
// }
if (isEdit.value) {
// await menuStore.updateMenu(params)
} else {
// await menuStore.addMenu(params)
}
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
dialogVisible.value = false
} catch (error) {
console.error(error)
}
}
})
}
const showModel = (type: string, row?: any, lock: boolean = false) => {
dialogVisible.value = true
labelPosition.value = type
isEdit.value = false
lockMenuType.value = lock
resetForm()
if (row) {
isEdit.value = true
nextTick(() => {
// 回显数据
if (type === 'menu') {
// 菜单数据回显
form.name = formatMenuTitle(row.meta.title)
form.path = row.path
form.label = row.name
form.icon = row.meta.icon
form.sort = row.meta.sort || 1
form.isMenu = row.meta.isMenu
form.keepAlive = row.meta.keepAlive
form.isHidden = row.meta.isHidden || true
form.isEnable = row.meta.isEnable || true
form.link = row.meta.link
form.isIframe = row.meta.isIframe || false
} else {
// 权限按钮数据回显
form.authName = row.title
form.authLabel = row.auth_mark
form.authIcon = row.icon || ''
form.authSort = row.sort || 1
}
})
}
}
const resetForm = () => {
formRef.value?.resetFields()
Object.assign(form, {
// 菜单
name: '',
path: '',
label: '',
icon: '',
sort: 1,
isMenu: true,
keepAlive: true,
isHidden: true,
link: '',
isIframe: false,
// 权限
authName: '',
authLabel: '',
authIcon: '',
authSort: 1
})
}
const deleteMenu = async () => {
try {
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.success('删除成功')
} catch (error) {
if (error !== 'cancel') {
console.error(error)
}
}
}
const deleteAuth = async () => {
try {
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.success('删除成功')
} catch (error) {
if (error !== 'cancel') {
console.error(error)
}
}
}
// 修改计算属性,增加锁定控制参数
const disableMenuType = computed(() => {
// 编辑权限时锁定为权限类型
if (isEdit.value && labelPosition.value === 'button') return true
// 编辑菜单时锁定为菜单类型
if (isEdit.value && labelPosition.value === 'menu') return true
// 顶部添加菜单按钮时锁定为菜单类型
if (!isEdit.value && labelPosition.value === 'menu' && lockMenuType.value) return true
return false
})
// 添加一个控制变量
const lockMenuType = ref(false)
const isExpanded = ref(false)
const tableRef = ref()
const toggleExpand = () => {
isExpanded.value = !isExpanded.value
nextTick(() => {
if (tableRef.value) {
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
}
})
}
</script>
<style lang="scss" scoped>
.menu-page {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
:deep(.small-btn) {
height: 30px !important;
padding: 0 10px !important;
font-size: 12px !important;
}
}
</style>

View File

@@ -1,5 +0,0 @@
<template>
<div class="page-content">
<h1>菜单-1</h1>
</div>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<div class="page-content">
<h1>菜单-2-1</h1>
</div>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<div class="page-content">
<h1>菜单-3-1</h1>
</div>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<div class="page-content">
<h1>菜单-3-2-1</h1>
</div>
</template>

View File

@@ -31,27 +31,12 @@
:default-expand-all="false"
:marginTop="10"
:show-pagination="false"
:row-class-name="getRowClassName"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -134,6 +119,7 @@
ElTag,
ElRadio,
ElRadioGroup,
ElButton,
type FormInstance,
type FormRules
} from 'element-plus'
@@ -142,11 +128,6 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import {
PermissionType,
PERMISSION_TYPE_OPTIONS,
@@ -222,19 +203,8 @@
const tableRef = ref()
const dialogVisible = ref(false)
const dialogType = ref('add')
const currentRow = ref<PermissionTreeNode | null>(null)
const currentPermissionId = ref<number>(0)
const submitLoading = ref(false)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 表单引用和数据
const formRef = ref<FormInstance>()
@@ -342,6 +312,45 @@
prop: 'sort',
label: '排序',
width: 80
},
{
prop: 'actions',
label: '操作',
width: 160,
fixed: 'right',
formatter: (row: PermissionTreeNode) => {
const buttons: any[] = []
if (hasAuth('permission:edit')) {
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => showDialog('edit', row)
},
() => '编辑'
)
)
}
if (hasAuth('permission:delete')) {
buttons.push(
h(
ElButton,
{
type: 'danger',
link: true,
onClick: () => deletePermission(row)
},
() => '删除'
)
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
@@ -444,7 +453,6 @@
}
if (type === 'edit' && row) {
currentRow.value = row
currentPermissionId.value = row.id
// 从API获取完整的权限数据包含parent_id
try {
@@ -555,47 +563,6 @@
onMounted(() => {
getPermissionList()
})
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('permission:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('permission:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: PermissionTreeNode, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deletePermission(currentRow.value)
break
}
}
</script>
<style scoped lang="scss">
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>
<style scoped lang="scss"></style>

View File

@@ -32,29 +32,14 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -80,8 +65,12 @@
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<ElOption label="平台角色" :value="1" />
<ElOption label="客户角色" :value="2" />
<ElOption
v-for="item in ROLE_TYPE_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="状态">
@@ -240,13 +229,13 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants'
import {
CommonStatus,
getStatusText,
ROLE_TYPE_OPTIONS,
getRoleTypeLabel
} from '@/config/constants'
defineOptions({ name: 'Role' })
@@ -272,17 +261,6 @@
const leftTreeFilter = ref('') // 左侧树搜索关键字
const rightTreeFilter = ref('') // 右侧树搜索关键字
const isHandlingCheck = ref(false) // 标志位:是否正在处理勾选事件
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<PlatformRole | null>(null)
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 搜索表单初始值
const initialSearchState = {
@@ -313,10 +291,7 @@
clearable: true,
placeholder: '请选择角色类型'
},
options: () => [
{ label: '平台角色', value: 1 },
{ label: '客户角色', value: 2 }
]
options: () => ROLE_TYPE_OPTIONS
},
{
label: '状态',
@@ -342,7 +317,6 @@
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'ID' },
{ label: '角色名称', prop: 'role_name' },
{ label: '角色描述', prop: 'role_desc' },
{ label: '角色类型', prop: 'role_type' },
@@ -373,34 +347,25 @@
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'ID',
label: 'ID',
width: 80
},
{
prop: 'role_name',
label: '角色名称',
minWidth: 120
},
{
prop: 'role_desc',
label: '角色描述'
width: 180
},
{
prop: 'role_type',
label: '角色类型',
minWidth: 120,
width: 100,
formatter: (row: any) => {
return h(ElTag, { type: row.role_type === 1 ? 'primary' : 'success' }, () =>
row.role_type === 1 ? '平台角色' : '客户角色'
getRoleTypeLabel(row.role_type)
)
}
},
{
prop: 'status',
label: '状态',
minWidth: 100,
width: 100,
formatter: (row: any) => {
return h(ElSwitch, {
modelValue: row.status,
@@ -415,11 +380,68 @@
})
}
},
{
prop: 'role_desc',
label: '角色描述'
},
{
prop: 'CreatedAt',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.CreatedAt)
},
{
prop: 'actions',
label: '操作',
width: 220,
fixed: 'right',
formatter: (row: any) => {
const buttons: any[] = []
if (hasAuth('role:permission')) {
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => showPermissionDialog(row)
},
() => '分配权限'
)
)
}
if (hasAuth('role:edit')) {
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => showDialog('edit', row)
},
() => '编辑'
)
)
}
if (hasAuth('role:delete')) {
buttons.push(
h(
ElButton,
{
type: 'danger',
link: true,
onClick: () => deleteRole(row)
},
() => '删除'
)
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
@@ -442,33 +464,6 @@
return data.label.toLowerCase().includes(value.toLowerCase())
}
// 获取节点的所有子节点ID(包括子菜单和按钮)
const getAllChildrenIds = (node: any): number[] => {
const ids: number[] = []
const traverse = (n: any) => {
if (n.children && n.children.length > 0) {
n.children.forEach((child: any) => {
ids.push(child.id)
traverse(child)
})
}
}
traverse(node)
return ids
}
// 在树数据中查找节点
const findNodeInTree = (treeData: any[], nodeId: number): any => {
for (const node of treeData) {
if (node.id === nodeId) return node
if (node.children && node.children.length > 0) {
const found = findNodeInTree(node.children, nodeId)
if (found) return found
}
}
return null
}
// 更新父节点的勾选状态(计算应该半选的父节点)
const updateParentCheckStatus = (checkedKeys: number[]) => {
const checkedSet = new Set(checkedKeys)
@@ -502,14 +497,19 @@
const currentNodeChecked = checkedSet.has(node.id)
// 判断父节点应该显示的状态:
// 1. 如果有子节点被勾选(someChecked=true),但不是全部子节点被勾选或当前节点未被勾选 -> 半选
// 2. 如果有子节点被勾选当前节点被勾选 ->
if (someChecked) {
if (!allChecked || !currentNodeChecked) {
// 子节点部分被勾选,或者当前节点被勾选 -> 半选
// 1. 如果当前节点被勾选但不是所有子节点被勾选 -> 半选
// 2. 如果有子节点被勾选,但当前节点被勾选 ->
// 3. 如果所有子节点都被勾选且当前节点也被勾选 -> 全选
if (currentNodeChecked) {
// 当前节点被勾选
if (!allChecked) {
// 但不是所有子节点都被勾选 -> 半选
parentIdsToHalfCheck.add(node.id)
}
// 如果 allChecked && currentNodeChecked,则为全选,不需要设置半选
// 如果 allChecked则为全选不需要设置半选
} else if (someChecked) {
// 当前节点未勾选,但有子节点被勾选 -> 半选
parentIdsToHalfCheck.add(node.id)
}
return {
@@ -524,6 +524,34 @@
return Array.from(parentIdsToHalfCheck)
}
// 判断节点是否为一级菜单没有父节点或parent_id为0/null
const isFirstLevelMenu = (nodeId: number): boolean => {
const node = allPermissionsMap.value.get(nodeId)
if (!node) return false
return !node.parent_id || node.parent_id === 0
}
// 获取节点的所有子节点ID递归
const getAllChildrenIds = (nodeId: number): number[] => {
const childrenIds: number[] = []
const collectChildren = (id: number) => {
const node = allPermissionsMap.value.get(id)
if (!node || !node.children || node.children.length === 0) return
node.children.forEach((child: any) => {
childrenIds.push(child.id)
collectChildren(child.id)
})
}
collectChildren(nodeId)
return childrenIds
}
// 上一次勾选的keys用于判断是勾选还是取消勾选
const previousCheckedKeys = ref<number[]>([])
// 处理左侧树的勾选事件
const handleLeftTreeCheck = (data: any, checked: any) => {
if (isHandlingCheck.value) return
@@ -532,7 +560,52 @@
try {
// 获取当前勾选的keys
const currentChecked = checked.checkedKeys as number[]
let currentChecked = checked.checkedKeys as number[]
// 判断是勾选还是取消勾选通过对比当前和之前的keys
const isChecking =
currentChecked.includes(data.id) && !previousCheckedKeys.value.includes(data.id)
const isUnchecking =
!currentChecked.includes(data.id) && previousCheckedKeys.value.includes(data.id)
if (isChecking) {
// 正在勾选节点
// 判断当前节点是否为一级菜单
if (isFirstLevelMenu(data.id)) {
// 一级菜单:自动勾选所有子节点(包括所有子菜单和按钮)
const childrenIds = getAllChildrenIds(data.id)
const newCheckedKeys = [...new Set([...currentChecked, ...childrenIds])]
// 设置新的勾选状态
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
currentChecked = newCheckedKeys
} else {
// 二级及以下菜单/按钮:只勾选自己,不勾选子节点
const childrenIds = getAllChildrenIds(data.id)
// 从当前勾选中移除所有子节点(如果有的话)
const newCheckedKeys = currentChecked.filter((id) => !childrenIds.includes(id))
// 设置新的勾选状态
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
currentChecked = newCheckedKeys
}
} else if (isUnchecking) {
// 正在取消勾选节点
// 判断当前节点是否为一级菜单
if (isFirstLevelMenu(data.id)) {
// 一级菜单:自动取消所有子节点(包括所有子菜单和按钮)
const childrenIds = getAllChildrenIds(data.id)
const newCheckedKeys = currentChecked.filter((id) => !childrenIds.includes(id))
// 设置新的勾选状态
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
currentChecked = newCheckedKeys
}
// 二级及以下菜单/按钮:直接取消勾选,不需要额外处理
}
// 更新上一次勾选的keys
previousCheckedKeys.value = [...currentChecked]
// 计算应该半选的父节点
const halfCheckedIds = updateParentCheckStatus(currentChecked)
@@ -626,23 +699,6 @@
})
}
// 获取树中所有节点的ID包括父节点和子节点
const getAllNodeIds = (treeNodes: any[]): number[] => {
const ids: number[] = []
const traverse = (nodes: any[]) => {
nodes.forEach((node) => {
ids.push(node.id)
if (node.children && node.children.length > 0) {
traverse(node.children)
}
})
}
traverse(treeNodes)
return ids
}
// 保存原始权限树数据
const originalPermissionTree = ref<PermissionTreeNode[]>([])
@@ -765,6 +821,7 @@
// 清空左侧树的勾选状态
leftTreeRef.value?.setCheckedKeys([], false)
previousCheckedKeys.value = []
ElMessage.success('权限添加成功')
} catch (error) {
@@ -842,6 +899,7 @@
}
selectedPermissions.value = []
originalPermissions.value = []
previousCheckedKeys.value = []
leftTreeFilter.value = ''
rightTreeFilter.value = ''
@@ -999,6 +1057,7 @@
const data = {
role_name: form.role_name,
role_desc: form.role_desc,
role_type: form.role_type,
status: form.status
}
await RoleService.updateRole(form.id, data)
@@ -1031,57 +1090,9 @@
console.error(error)
}
}
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('role:permission')) {
items.push({ key: 'assignPermission', label: '分配权限' })
}
if (hasAuth('role:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('role:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'assignPermission':
showPermissionDialog(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteRole(currentRow.value)
break
}
}
</script>
<style scoped lang="scss">
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.dialog-header {
display: flex;
align-items: center;