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

This commit is contained in:
sexygoat
2026-03-11 17:09:35 +08:00
parent bd45f7a224
commit d43de4cd06
37 changed files with 2552 additions and 1696 deletions

View File

@@ -14,7 +14,9 @@
"Bash(npm run build:*)", "Bash(npm run build:*)",
"Bash(tree:*)", "Bash(tree:*)",
"Bash(npm run dev:*)", "Bash(npm run dev:*)",
"Bash(timeout:*)" "Bash(timeout:*)",
"Read(//d/**)",
"Bash(findstr:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -0,0 +1,38 @@
<!-- 表格右键菜单悬浮提示组件 -->
<template>
<div
v-show="visible"
class="table-context-menu-hint"
:style="{ left: position.x + 'px', top: position.y + 'px' }"
>
{{ text }}
</div>
</template>
<script setup lang="ts">
interface Props {
visible: boolean
position: { x: number; y: number }
text?: string
}
withDefaults(defineProps<Props>(), {
text: '右键查看更多操作'
})
</script>
<style scoped lang="scss">
.table-context-menu-hint {
position: fixed;
padding: 4px 10px;
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
font-size: 12px;
border-radius: 4px;
pointer-events: none;
white-space: nowrap;
z-index: 9999;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: opacity 0.2s ease;
}
</style>

View File

@@ -14,6 +14,7 @@
v-loading="loading" v-loading="loading"
:data="tableData" :data="tableData"
:row-key="rowKey" :row-key="rowKey"
:row-class-name="rowClassName"
:height="height" :height="height"
:max-height="maxHeight" :max-height="maxHeight"
:show-header="showHeader" :show-header="showHeader"
@@ -30,6 +31,8 @@
@row-click="handleRowClick" @row-click="handleRowClick"
@row-contextmenu="handleRowContextmenu" @row-contextmenu="handleRowContextmenu"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<!-- 序号列 --> <!-- 序号列 -->
<el-table-column <el-table-column
@@ -86,6 +89,8 @@
loading?: boolean loading?: boolean
/** 行数据的 Key用于标识每一行数据 */ /** 行数据的 Key用于标识每一行数据 */
rowKey?: string rowKey?: string
/** 行的 className 的回调方法 */
rowClassName?: ((data: { row: any; rowIndex: number }) => string) | string
/** 是否显示边框 */ /** 是否显示边框 */
border?: boolean | null border?: boolean | null
/** 是否使用斑马纹样式 */ /** 是否使用斑马纹样式 */
@@ -136,6 +141,7 @@
data: () => [], data: () => [],
loading: false, loading: false,
rowKey: 'id', rowKey: 'id',
rowClassName: undefined,
border: null, border: null,
stripe: null, stripe: null,
index: false, index: false,
@@ -178,7 +184,9 @@
'row-contextmenu', 'row-contextmenu',
'size-change', 'size-change',
'current-change', 'current-change',
'selection-change' 'selection-change',
'cell-mouse-enter',
'cell-mouse-leave'
]) ])
const tableStore = useTableStore() const tableStore = useTableStore()
@@ -281,6 +289,16 @@
emit('row-contextmenu', row, column, event) emit('row-contextmenu', row, column, event)
} }
// 单元格鼠标进入事件
const handleCellMouseEnter = (row: any, column: any, cell: any, event: any) => {
emit('cell-mouse-enter', row, column, cell, event)
}
// 单元格鼠标离开事件
const handleCellMouseLeave = (row: any, column: any, cell: any, event: any) => {
emit('cell-mouse-leave', row, column, cell, event)
}
// 选择变化事件 // 选择变化事件
const handleSelectionChange = (selection: any) => { const handleSelectionChange = (selection: any) => {
emit('selection-change', selection) emit('selection-change', selection)

View File

@@ -0,0 +1,63 @@
/**
* 表格右键菜单的组合式函数
* 提供右键菜单功能和鼠标悬浮提示
*/
import { ref, reactive } from 'vue'
export function useTableContextMenu() {
// 鼠标悬浮提示相关
const showContextMenuHint = ref(false)
const hintPosition = reactive({ x: 0, y: 0 })
let hintTimer: any = null
/**
* 为表格行添加类名
*/
const getRowClassName = ({ row, rowIndex }: { row: any; rowIndex: number }) => {
return 'table-row-with-context-menu'
}
/**
* 单元格鼠标进入事件处理
*/
const handleCellMouseEnter = (
row: any,
column: any,
cell: HTMLElement,
event: MouseEvent
) => {
// 清除之前的定时器
if (hintTimer) {
clearTimeout(hintTimer)
}
// 延迟显示提示,避免快速划过时闪烁
hintTimer = setTimeout(() => {
hintPosition.x = event.clientX + 15
hintPosition.y = event.clientY + 10
showContextMenuHint.value = true
}, 300)
}
/**
* 单元格鼠标离开事件处理
*/
const handleCellMouseLeave = () => {
if (hintTimer) {
clearTimeout(hintTimer)
}
showContextMenuHint.value = false
}
return {
// 状态
showContextMenuHint,
hintPosition,
// 方法
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
}
}

View File

@@ -450,11 +450,15 @@
"devices": "Device Management", "devices": "Device Management",
"deviceDetail": "Device Details", "deviceDetail": "Device Details",
"assetAssign": "Allocation Records", "assetAssign": "Allocation Records",
"assetAssignDetail": "Asset Allocation Details",
"allocationRecordDetail": "Allocation Record Details", "allocationRecordDetail": "Allocation Record Details",
"cardReplacementRequest": "Card Replacement Request", "cardReplacementRequest": "Card Replacement Request",
"authorizationRecords": "Authorization Records", "authorizationRecords": "Authorization Records",
"authorizationDetail": "Authorization Details", "authorizationDetail": "Authorization Details",
"enterpriseDevices": "Enterprise Devices" "authorizationRecordDetail": "Authorization Record Details",
"enterpriseDevices": "Enterprise Devices",
"recordsManagement": "Records Management",
"taskManagement": "Task Management"
}, },
"settings": { "settings": {
"title": "Settings Management", "title": "Settings Management",

View File

@@ -385,7 +385,7 @@
}, },
"cardManagement": { "cardManagement": {
"title": "我的网卡", "title": "我的网卡",
"singleCard": "单卡信息", "singleCard": "资产信息",
"cardList": "网卡管理", "cardList": "网卡管理",
"cardDetail": "网卡明细", "cardDetail": "网卡明细",
"cardAssign": "网卡分配", "cardAssign": "网卡分配",
@@ -453,7 +453,9 @@
"authorizationRecords": "授权记录", "authorizationRecords": "授权记录",
"authorizationDetail": "授权记录详情", "authorizationDetail": "授权记录详情",
"authorizationRecordDetail": "授权记录详情", "authorizationRecordDetail": "授权记录详情",
"enterpriseDevices": "企业设备列表" "enterpriseDevices": "企业设备列表",
"recordsManagement": "记录管理",
"taskManagement": "任务管理"
}, },
"account": { "account": {
"title": "账户管理", "title": "账户管理",

View File

@@ -51,6 +51,7 @@ export const asyncRoutes: AppRouteRecord[] = [
} }
] ]
}, },
// { // {
// path: '/widgets', // path: '/widgets',
// name: 'Widgets', // name: 'Widgets',
@@ -266,6 +267,7 @@ export const asyncRoutes: AppRouteRecord[] = [
// } // }
// ] // ]
// }, // },
{ {
path: '/system', path: '/system',
name: 'System', name: 'System',
@@ -429,6 +431,7 @@ export const asyncRoutes: AppRouteRecord[] = [
// } // }
] ]
}, },
// { // {
// path: '/article', // path: '/article',
// name: 'Article', // name: 'Article',
@@ -712,6 +715,7 @@ export const asyncRoutes: AppRouteRecord[] = [
// } // }
// ] // ]
// }, // },
{ {
path: '/package-management', path: '/package-management',
name: 'PackageManagement', name: 'PackageManagement',
@@ -780,6 +784,7 @@ export const asyncRoutes: AppRouteRecord[] = [
} }
] ]
}, },
{ {
path: '/shop-management', path: '/shop-management',
name: 'ShopManagement', name: 'ShopManagement',
@@ -800,6 +805,7 @@ export const asyncRoutes: AppRouteRecord[] = [
} }
] ]
}, },
{ {
path: '/account-management', path: '/account-management',
name: 'AccountManagement', name: 'AccountManagement',
@@ -885,6 +891,7 @@ export const asyncRoutes: AppRouteRecord[] = [
} }
] ]
}, },
// { // {
// path: '/product', // path: '/product',
// name: 'Product', // name: 'Product',
@@ -950,6 +957,7 @@ export const asyncRoutes: AppRouteRecord[] = [
// } // }
// ] // ]
// }, // },
{ {
path: '/asset-management', path: '/asset-management',
name: 'AssetManagement', name: 'AssetManagement',
@@ -977,44 +985,6 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true keepAlive: true
} }
}, },
{
path: 'iot-card-management/detail',
name: 'IotCardDetail',
component: RoutesAlias.StandaloneCardList + '/detail',
meta: {
title: 'menus.assetManagement.iotCardDetail',
isHide: true,
keepAlive: false
}
},
{
path: 'iot-card-task',
name: 'IotCardTask',
component: RoutesAlias.IotCardTask,
meta: {
title: 'menus.assetManagement.iotCardTask',
keepAlive: true
}
},
{
path: 'device-task',
name: 'DeviceTask',
component: RoutesAlias.DeviceTask,
meta: {
title: 'menus.assetManagement.deviceTask',
keepAlive: true
}
},
{
path: 'task-detail',
name: 'TaskDetail',
component: RoutesAlias.TaskDetail,
meta: {
title: 'menus.assetManagement.taskDetail',
isHide: true,
keepAlive: false
}
},
{ {
path: 'devices', path: 'devices',
name: 'DeviceList', name: 'DeviceList',
@@ -1024,63 +994,6 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true keepAlive: true
} }
}, },
{
path: 'device-detail',
name: 'DeviceDetail',
component: RoutesAlias.DeviceDetail,
meta: {
title: 'menus.assetManagement.deviceDetail',
isHide: true,
keepAlive: false
}
},
{
path: 'asset-assign',
name: 'AssetAssign',
component: RoutesAlias.AssetAssign,
meta: {
title: 'menus.assetManagement.assetAssign',
keepAlive: true
}
},
{
path: 'asset-assign/detail/:id',
name: 'AssetAssignDetail',
component: RoutesAlias.AssetAssignDetail,
meta: {
title: 'menus.assetManagement.assetAssignDetail',
isHide: true,
keepAlive: false
}
},
// {
// path: 'card-replacement-request',
// name: 'CardReplacementRequest',
// component: RoutesAlias.CardReplacementRequest,
// meta: {
// title: 'menus.assetManagement.cardReplacementRequest',
// keepAlive: true
// }
// },
{
path: 'authorization-records',
name: 'AuthorizationRecords',
component: RoutesAlias.AuthorizationRecords,
meta: {
title: 'menus.assetManagement.authorizationRecords',
keepAlive: true
}
},
{
path: 'authorization-records/detail/:id',
name: 'AuthorizationRecordDetail',
component: RoutesAlias.AuthorizationRecordDetail,
meta: {
title: 'menus.assetManagement.authorizationRecordDetail',
isHide: true,
keepAlive: false
}
},
{ {
path: 'enterprise-devices', path: 'enterprise-devices',
name: 'EnterpriseDevices', name: 'EnterpriseDevices',
@@ -1090,9 +1003,100 @@ export const asyncRoutes: AppRouteRecord[] = [
isHide: true, isHide: true,
keepAlive: false keepAlive: false
} }
},
// 记录管理
{
path: 'records',
name: 'RecordsManagement',
component: '',
meta: {
title: 'menus.assetManagement.recordsManagement',
keepAlive: true
},
children: [
{
path: 'asset-assign',
name: 'AssetAssign',
component: RoutesAlias.AssetAssign,
meta: {
title: 'menus.assetManagement.assetAssign',
keepAlive: true
}
},
{
path: 'asset-assign/detail/:id',
name: 'AssetAssignDetail',
component: RoutesAlias.AssetAssignDetail,
meta: {
title: 'menus.assetManagement.assetAssignDetail',
isHide: true,
keepAlive: false
}
},
{
path: 'authorization-records',
name: 'AuthorizationRecords',
component: RoutesAlias.AuthorizationRecords,
meta: {
title: 'menus.assetManagement.authorizationRecords',
keepAlive: true
}
},
{
path: 'authorization-records/detail/:id',
name: 'AuthorizationRecordDetail',
component: RoutesAlias.AuthorizationRecordDetail,
meta: {
title: 'menus.assetManagement.authorizationRecordDetail',
isHide: true,
keepAlive: false
}
}
]
},
// 任务管理
{
path: 'tasks',
name: 'TaskManagement',
component: '',
meta: {
title: 'menus.assetManagement.taskManagement',
keepAlive: true
},
children: [
{
path: 'iot-card-task',
name: 'IotCardTask',
component: RoutesAlias.IotCardTask,
meta: {
title: 'menus.assetManagement.iotCardTask',
keepAlive: true
}
},
{
path: 'device-task',
name: 'DeviceTask',
component: RoutesAlias.DeviceTask,
meta: {
title: 'menus.assetManagement.deviceTask',
keepAlive: true
}
},
{
path: 'task-detail',
name: 'TaskDetail',
component: RoutesAlias.TaskDetail,
meta: {
title: 'menus.assetManagement.taskDetail',
isHide: true,
keepAlive: false
}
}
]
} }
] ]
}, },
{ {
path: '/account', path: '/account',
name: 'Finance', name: 'Finance',
@@ -1141,6 +1145,7 @@ export const asyncRoutes: AppRouteRecord[] = [
// } // }
] ]
}, },
{ {
path: '/commission', path: '/commission',
name: 'CommissionManagement', name: 'CommissionManagement',
@@ -1192,6 +1197,7 @@ export const asyncRoutes: AppRouteRecord[] = [
} }
] ]
} }
// { // {
// path: '/settings', // path: '/settings',
// name: 'Settings', // name: 'Settings',

View File

@@ -122,6 +122,8 @@ declare module 'vue' {
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem'] 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'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
@@ -152,6 +154,7 @@ declare module 'vue' {
SettingHeader: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default'] SettingHeader: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default']
SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default'] SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default']
SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default'] SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default']
TableContextMenuHint: typeof import('./../components/core/others/TableContextMenuHint.vue')['default']
ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default'] ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default']
} }
export interface ComponentCustomProperties { export interface ComponentCustomProperties {

View File

@@ -31,16 +31,22 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -191,8 +197,10 @@
import type { FormRules } from 'element-plus' import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { AccountService } from '@/api/modules/account' import { AccountService } from '@/api/modules/account'
import { RoleService } from '@/api/modules/role' import { RoleService } from '@/api/modules/role'
@@ -207,6 +215,15 @@
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const route = useRoute() const route = useRoute()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const dialogType = ref('add') const dialogType = ref('add')
const dialogVisible = ref(false) const dialogVisible = ref(false)
const roleDialogVisible = ref(false) const roleDialogVisible = ref(false)
@@ -865,6 +882,10 @@
// 账号管理页面样式 // 账号管理页面样式
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.dialog-header { .dialog-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -64,10 +64,13 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn type="selection" width="55" /> <ElTableColumn type="selection" width="55" />
@@ -75,6 +78,9 @@
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 授权卡对话框 --> <!-- 授权卡对话框 -->
<ElDialog <ElDialog
v-model="allocateDialogVisible" v-model="allocateDialogVisible"
@@ -232,8 +238,10 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { BgColorEnum } from '@/enums/appEnum' import { BgColorEnum } from '@/enums/appEnum'
@@ -251,6 +259,16 @@
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false) const loading = ref(false)
const allocateDialogVisible = ref(false) const allocateDialogVisible = ref(false)
const allocateLoading = ref(false) const allocateLoading = ref(false)
@@ -1064,4 +1082,8 @@
margin-top: 20px; margin-top: 20px;
} }
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -35,15 +35,21 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -221,8 +227,10 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
@@ -233,6 +241,15 @@
const router = useRouter() const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 判断是否是代理账号 (user_type === 3) // 判断是否是代理账号 (user_type === 3)
const isAgentAccount = computed(() => userStore.info.user_type === 3) const isAgentAccount = computed(() => userStore.info.user_type === 3)
@@ -811,3 +828,9 @@
} }
} }
</script> </script>
<style scoped lang="scss">
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -28,15 +28,21 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -57,16 +63,28 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card' import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
import { RoutesAlias } from '@/router/routesAlias' import { RoutesAlias } from '@/router/routesAlias'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
defineOptions({ name: 'AssetAllocationRecords' }) defineOptions({ name: 'AssetAllocationRecords' })
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter() const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false) const loading = ref(false)
const tableRef = ref() const tableRef = ref()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>() const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
@@ -374,4 +392,8 @@
.asset-allocation-records-page { .asset-allocation-records-page {
// Allocation records page styles // Allocation records page styles
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -28,15 +28,21 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -82,9 +88,11 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import type { import type {
AuthorizationItem, AuthorizationItem,
@@ -98,6 +106,16 @@
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter() const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false) const loading = ref(false)
const remarkDialogVisible = ref(false) const remarkDialogVisible = ref(false)
const remarkLoading = ref(false) const remarkLoading = ref(false)
@@ -442,4 +460,8 @@
.authorization-records-page { .authorization-records-page {
// Authorization records page styles // Authorization records page styles
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -1,446 +0,0 @@
<template>
<div class="device-detail">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">设备详情</h2>
</div>
<!-- 详情内容 -->
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
<!-- 绑定卡片列表 -->
<ElCard v-if="detailData" shadow="never" class="cards-section" style="margin-top: 20px">
<template #header>
<div class="section-header">
<span class="section-title">绑定的卡列表</span>
<ElButton
type="primary"
size="small"
@click="showBindDialog"
:disabled="!detailData || detailData.bound_card_count >= detailData.max_sim_slots"
>
绑定新卡
</ElButton>
</div>
</template>
<ElTable :data="cardList" border v-loading="cardsLoading">
<ElTableColumn prop="slot_position" label="插槽位置" width="100" align="center">
<template #default="{ row }">
<ElTag type="info" size="small">插槽 {{ row.slot_position }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="iccid" label="ICCID" minWidth="180" />
<ElTableColumn prop="msisdn" label="接入号" width="150">
<template #default="{ row }">
{{ row.msisdn || '-' }}
</template>
</ElTableColumn>
<ElTableColumn prop="carrier_name" label="运营商" width="120" />
<ElTableColumn prop="status" label="卡状态" width="100">
<template #default="{ row }">
<ElTag :type="cardStatusTypeMap[row.status]" size="small">
{{ cardStatusTextMap[row.status] || '未知' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="bind_time" label="绑定时间" width="180">
<template #default="{ row }">
{{ row.bind_time ? formatDateTime(row.bind_time) : '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<ElButton type="danger" text size="small" @click="handleUnbindCard(row)">
解绑
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<ElEmpty v-if="!cardList.length && !cardsLoading" description="暂无绑定的卡" />
</ElCard>
</ElCard>
<!-- 绑定卡对话框 -->
<ElDialog v-model="bindDialogVisible" title="绑定卡到设备" width="500px">
<ElForm ref="bindFormRef" :model="bindForm" :rules="bindRules" label-width="100px">
<ElFormItem label="选择卡" prop="iot_card_id">
<ElSelect
v-model="bindForm.iot_card_id"
placeholder="请搜索并选择卡"
filterable
remote
:remote-method="searchCards"
:loading="searchCardsLoading"
style="width: 100%"
>
<ElOption
v-for="card in availableCards"
:key="card.id"
:label="`${card.iccid} - ${card.carrier_name}`"
:value="card.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ card.iccid }}</span>
<ElTag size="small" type="info">{{ card.carrier_name }}</ElTag>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="插槽位置" prop="slot_position">
<ElSelect
v-model="bindForm.slot_position"
placeholder="请选择插槽位置"
style="width: 100%"
>
<ElOption
v-for="slot in availableSlots"
:key="slot"
:label="`插槽 ${slot}`"
:value="slot"
/>
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="bindDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleConfirmBind" :loading="bindLoading">
确认绑定
</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElMessageBox, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { DeviceService, CardService } from '@/api/modules'
import type { Device, DeviceCardBinding } from '@/types/api'
import type { FormInstance, FormRules } from 'element-plus'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'DeviceDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const cardsLoading = ref(false)
const bindLoading = ref(false)
const searchCardsLoading = ref(false)
const bindDialogVisible = ref(false)
const bindFormRef = ref<FormInstance>()
const detailData = ref<Device | null>(null)
const cardList = ref<DeviceCardBinding[]>([])
const availableCards = ref<any[]>([])
// 状态类型映射
const statusTypeMap: Record<number, any> = {
1: 'info',
2: 'primary',
3: 'success',
4: 'danger'
}
// 卡状态类型映射
const cardStatusTypeMap: Record<number, any> = {
1: 'info',
2: 'primary',
3: 'success',
4: 'danger'
}
// 卡状态文本映射
const cardStatusTextMap: Record<number, string> = {
1: '在库',
2: '已分销',
3: '已激活',
4: '已停用'
}
// 绑定表单
const bindForm = reactive({
iot_card_id: undefined as number | undefined,
slot_position: undefined as number | undefined
})
// 绑定表单验证规则
const bindRules = reactive<FormRules>({
iot_card_id: [{ required: true, message: '请选择卡', trigger: 'change' }],
slot_position: [{ required: true, message: '请选择插槽位置', trigger: 'change' }]
})
// 可用插槽
const availableSlots = computed(() => {
if (!detailData.value) return []
const occupiedSlots = cardList.value.map((card) => card.slot_position)
const allSlots = Array.from({ length: detailData.value.max_sim_slots }, (_, i) => i + 1)
return allSlots.filter((slot) => !occupiedSlots.includes(slot))
})
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
{ label: '设备ID', prop: 'id' },
{ label: '设备号', prop: 'device_no' },
{ label: '设备名称', prop: 'device_name', formatter: (value) => value || '-' },
{ label: '设备型号', prop: 'device_model', formatter: (value) => value || '-' },
{ label: '设备类型', prop: 'device_type', formatter: (value) => value || '-' },
{ label: '制造商', prop: 'manufacturer', formatter: (value) => value || '-' },
{ label: '最大插槽数', prop: 'max_sim_slots' },
{
label: '已绑定卡数',
render: (data: Device) => {
const color = data.bound_card_count > 0 ? '#67c23a' : '#909399'
return h('span', { style: { color, fontWeight: 'bold' } },
`${data.bound_card_count} / ${data.max_sim_slots}`)
}
},
{
label: '状态',
render: (data: Device) => {
const statusMap: Record<number, { text: string; type: any }> = {
1: { text: '在库', type: 'info' },
2: { text: '已分销', type: 'primary' },
3: { text: '已激活', type: 'success' },
4: { text: '已停用', type: 'danger' }
}
const status = statusMap[data.status] || { text: '未知', type: 'info' }
return h(ElTag, { type: status.type }, () => status.text)
}
},
{ label: '所属店铺', prop: 'shop_name', formatter: (value) => value || '平台库存' },
{ label: '批次号', prop: 'batch_no', formatter: (value) => value || '-' },
{
label: '激活时间',
prop: 'activated_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) }
]
}
]
// 返回上一页
const handleBack = () => {
router.back()
}
// 获取详情数据
const fetchDetail = async (id?: number, deviceNo?: string) => {
loading.value = true
try {
let res
if (id) {
res = await DeviceService.getDeviceById(id)
} else if (deviceNo) {
res = await DeviceService.getDeviceByImei(deviceNo)
} else {
ElMessage.error('缺少设备参数')
return
}
if (res.code === 0 && res.data) {
detailData.value = res.data
// 加载绑定的卡列表
if (res.data.id) {
loadDeviceCards(res.data.id)
}
} else {
ElMessage.error(res.message || '获取设备详情失败')
}
} catch (error) {
console.error(error)
ElMessage.error('获取设备详情失败')
} finally {
loading.value = false
}
}
// 加载设备绑定的卡列表
const loadDeviceCards = async (id: number) => {
cardsLoading.value = true
try {
const res = await DeviceService.getDeviceCards(id)
if (res.code === 0) {
cardList.value = res.data.bindings || []
}
} catch (error) {
console.error(error)
ElMessage.error('获取绑定卡列表失败')
} finally {
cardsLoading.value = false
}
}
// 搜索可用的卡
const searchCards = async (query: string) => {
if (!query) {
availableCards.value = []
return
}
searchCardsLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
iccid: query,
page: 1,
page_size: 20
})
if (res.code === 0) {
availableCards.value = res.data.items || []
}
} catch (error) {
console.error(error)
} finally {
searchCardsLoading.value = false
}
}
// 显示绑定对话框
const showBindDialog = () => {
bindForm.iot_card_id = undefined
bindForm.slot_position = undefined
availableCards.value = []
bindDialogVisible.value = true
}
// 确认绑定
const handleConfirmBind = async () => {
if (!bindFormRef.value || !detailData.value) return
await bindFormRef.value.validate(async (valid) => {
if (valid) {
bindLoading.value = true
try {
const data = {
iot_card_id: bindForm.iot_card_id!,
slot_position: bindForm.slot_position!
}
const res = await DeviceService.bindCard(detailData.value!.id, data)
if (res.code === 0) {
ElMessage.success('绑定成功')
bindDialogVisible.value = false
await fetchDetail(detailData.value!.id)
if (bindFormRef.value) {
bindFormRef.value.resetFields()
}
} else {
ElMessage.error(res.message || '绑定失败')
}
} catch (error) {
console.error(error)
ElMessage.error('绑定失败')
} finally {
bindLoading.value = false
}
}
})
}
// 解绑卡
const handleUnbindCard = (card: DeviceCardBinding) => {
ElMessageBox.confirm(`确定解绑卡 ${card.iccid} 吗?`, '解绑确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await DeviceService.unbindCard(detailData.value!.id, card.iot_card_id)
ElMessage.success('解绑成功')
await fetchDetail(detailData.value!.id)
} catch (error) {
console.error(error)
ElMessage.error('解绑失败')
}
})
.catch(() => {
// 用户取消
})
}
onMounted(() => {
const deviceId = route.query.id
const deviceNo = route.query.device_no
if (deviceId) {
fetchDetail(Number(deviceId))
} else if (deviceNo) {
fetchDetail(undefined, String(deviceNo))
} else {
ElMessage.error('缺少设备参数')
handleBack()
}
})
</script>
<style scoped lang="scss">
.device-detail {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
}
.cards-section {
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
</style>

View File

@@ -53,10 +53,13 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn type="selection" width="55" /> <ElTableColumn type="selection" width="55" />
@@ -64,6 +67,9 @@
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 批量分配对话框 --> <!-- 批量分配对话框 -->
<ElDialog v-model="allocateDialogVisible" title="批量分配设备" width="600px"> <ElDialog v-model="allocateDialogVisible" title="批量分配设备" width="600px">
<ElForm <ElForm
@@ -570,8 +576,10 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants' import { CommonStatus, getStatusText } from '@/config/constants'
@@ -581,6 +589,16 @@
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter() const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false) const loading = ref(false)
const allocateLoading = ref(false) const allocateLoading = ref(false)
const recallLoading = ref(false) const recallLoading = ref(false)
@@ -801,11 +819,11 @@
remark: '' remark: ''
}) })
// 跳转到设备详情页面 // 跳转到资产信息页面(原设备详情)
const goToDeviceSearchDetail = (deviceNo: string) => { const goToDeviceSearchDetail = (deviceNo: string) => {
if (hasAuth('device:view_detail')) { if (hasAuth('device:view_detail')) {
router.push({ router.push({
path: '/asset-management/device-detail', path: '/asset-management/single-card',
query: { query: {
device_no: deviceNo device_no: deviceNo
} }
@@ -1697,4 +1715,8 @@
.device-list-page { .device-list-page {
height: 100%; height: 100%;
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -39,15 +39,21 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -125,9 +131,11 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { StorageService } from '@/api/modules/storage' import { StorageService } from '@/api/modules/storage'
import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device' import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device'
@@ -137,6 +145,16 @@
const router = useRouter() const router = useRouter()
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false) const loading = ref(false)
const tableRef = ref() const tableRef = ref()
const uploadRef = ref<UploadInstance>() const uploadRef = ref<UploadInstance>()
@@ -679,4 +697,8 @@
} }
} }
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -1,312 +0,0 @@
<template>
<div class="iot-card-detail-page">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">IoT卡详情</h2>
<div class="header-actions">
<ElButton type="primary" @click="handleRefresh" :loading="loading">
<Icon name="refresh" /> 刷新
</ElButton>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading && !cardDetail" class="loading-container">
<ElIcon class="is-loading" :size="60"><Loading /></ElIcon>
<div class="loading-text">加载中...</div>
</div>
<!-- 详情内容 -->
<DetailPage v-if="cardDetail" :sections="detailSections" :data="cardDetail" />
<!-- 未找到卡片 -->
<div v-if="!cardDetail && !loading" class="empty-container">
<ElEmpty description="未找到该卡片信息" />
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { CardService } from '@/api/modules'
import { ElMessage, ElIcon, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/business/format'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
defineOptions({ name: 'IotCardDetail' })
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const cardDetail = ref<any>(null)
const iccid = ref<string>('')
onMounted(() => {
iccid.value = (route.query.iccid as string) || ''
if (iccid.value) {
loadCardDetail()
} else {
ElMessage.error('缺少ICCID参数')
}
})
// 加载卡片详情
const loadCardDetail = async () => {
loading.value = true
try {
const res = await CardService.getIotCardDetailByIccid(iccid.value)
if (res.code === 0) {
cardDetail.value = res.data
} else {
ElMessage.error(res.msg || '获取卡片详情失败')
}
} catch (error) {
console.error('获取卡片详情失败:', error)
ElMessage.error('获取卡片详情失败')
} finally {
loading.value = false
}
}
// 返回上一页
const handleBack = () => {
router.back()
}
// 刷新
const handleRefresh = () => {
loadCardDetail()
}
// 运营商类型文本
const getCarrierTypeText = (type: string) => {
const typeMap: Record<string, string> = {
CMCC: '中国移动',
CUCC: '中国联通',
CTCC: '中国电信',
CBN: '中国广电'
}
return typeMap[type] || type || '--'
}
// 卡业务类型文本
const getCardCategoryText = (category: string) => {
const categoryMap: Record<string, string> = {
normal: '普通卡',
industry: '行业卡'
}
return categoryMap[category] || category || '--'
}
// 状态文本
const getStatusText = (status: number) => {
const statusMap: Record<number, string> = {
1: '在库',
2: '已分销',
3: '已激活',
4: '已停用'
}
return statusMap[status] || '未知'
}
// 状态标签类型
const getStatusTagType = (status: number) => {
const typeMap: Record<number, any> = {
1: 'info',
2: 'warning',
3: 'success',
4: 'danger'
}
return typeMap[status] || 'info'
}
// 格式化价格
const formatCardPrice = (price: number) => {
return `¥${((price || 0) / 100).toFixed(2)}`
}
// DetailPage 配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
{ label: '卡ID', prop: 'id' },
{
label: 'ICCID',
render: (data) => {
return h(
'span',
{
style: {
padding: '3px 8px',
fontFamily: "'SF Mono', Monaco, Inconsolata, 'Roboto Mono', monospace",
fontSize: '13px',
fontWeight: '500',
color: 'var(--el-text-color-regular)',
background: 'var(--el-fill-color-light)',
border: '1px solid var(--el-border-color-lighter)',
borderRadius: '4px'
}
},
data.iccid
)
}
},
{
label: '卡接入号',
render: (data) => {
return h(
'span',
{
style: {
padding: '3px 8px',
fontFamily: "'SF Mono', Monaco, Inconsolata, 'Roboto Mono', monospace",
fontSize: '13px',
fontWeight: '500',
color: 'var(--el-text-color-regular)',
background: 'var(--el-fill-color-light)',
border: '1px solid var(--el-border-color-lighter)',
borderRadius: '4px'
}
},
data.msisdn || '--'
)
}
},
{ label: '运营商', prop: 'carrier_name', formatter: (value) => value || '--' },
{
label: '运营商类型',
prop: 'carrier_type',
formatter: (value) => getCarrierTypeText(value)
},
{
label: '卡业务类型',
prop: 'card_category',
formatter: (value) => getCardCategoryText(value)
},
{
label: '状态',
render: (data) => {
return h(ElTag, { type: getStatusTagType(data.status) }, () => getStatusText(data.status))
}
},
{ label: '套餐系列', prop: 'series_name', formatter: (value) => value || '--' },
{
label: '激活状态',
render: (data) => {
return h(
ElTag,
{ type: data.activation_status === 1 ? 'success' : 'info' },
() => (data.activation_status === 1 ? '已激活' : '未激活')
)
}
},
{
label: '实名状态',
render: (data) => {
return h(
ElTag,
{ type: data.real_name_status === 1 ? 'success' : 'warning' },
() => (data.real_name_status === 1 ? '已实名' : '未实名')
)
}
},
{
label: '网络状态',
render: (data) => {
return h(
ElTag,
{ type: data.network_status === 1 ? 'success' : 'danger' },
() => (data.network_status === 1 ? '开机' : '停机')
)
}
},
{
label: '累计流量使用',
prop: 'data_usage_mb',
formatter: (value) => `${value} MB`
},
{
label: '一次性佣金',
render: (data) => {
return h(
ElTag,
{ type: data.first_commission_paid ? 'success' : 'info' },
() => (data.first_commission_paid ? '已产生' : '未产生')
)
}
},
{
label: '累计充值',
prop: 'accumulated_recharge',
formatter: (value) => formatCardPrice(value)
},
{ label: '所属店铺', prop: 'shop_name', formatter: (value) => value || '--' },
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
]
}
]
</script>
<style lang="scss" scoped>
.iot-card-detail-page {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
flex: 1;
}
.header-actions {
margin-left: auto;
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
.loading-text {
font-size: 14px;
}
}
.empty-container {
padding: 60px 20px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -51,7 +51,7 @@
hasAuth('iot_card:batch_download') hasAuth('iot_card:batch_download')
" "
type="info" type="info"
@contextmenu.prevent="showMoreMenu" @click="showMoreMenuOnClick"
> >
更多操作 更多操作
</ElButton> </ElButton>
@@ -68,10 +68,13 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn type="selection" width="55" /> <ElTableColumn type="selection" width="55" />
@@ -79,6 +82,9 @@
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 批量分配对话框 --> <!-- 批量分配对话框 -->
<ElDialog <ElDialog
v-model="allocateDialogVisible" v-model="allocateDialogVisible"
@@ -597,8 +603,10 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import type { import type {
@@ -616,6 +624,16 @@
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter() const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false) const loading = ref(false)
const allocateDialogVisible = ref(false) const allocateDialogVisible = ref(false)
const allocateLoading = ref(false) const allocateLoading = ref(false)
@@ -910,11 +928,11 @@
} }
} }
// 跳转到IoT卡详情页面 // 跳转到资产信息页面(原IoT卡详情)
const goToCardDetail = (iccid: string) => { const goToCardDetail = (iccid: string) => {
if (hasAuth('iot_card:view_detail')) { if (hasAuth('iot_card:view_detail')) {
router.push({ router.push({
path: RoutesAlias.StandaloneCardList + '/detail', path: '/asset-management/single-card',
query: { query: {
iccid: iccid iccid: iccid
} }
@@ -1575,13 +1593,19 @@
return items return items
}) })
// 显示更多操作菜单 // 显示更多操作菜单 (右键)
const showMoreMenu = (e: MouseEvent) => { const showMoreMenu = (e: MouseEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
moreMenuRef.value?.show(e) moreMenuRef.value?.show(e)
} }
// 显示更多操作菜单 (左键点击)
const showMoreMenuOnClick = (e: MouseEvent) => {
e.stopPropagation()
moreMenuRef.value?.show(e)
}
// 处理更多操作菜单选择 // 处理更多操作菜单选择
const handleMoreMenuSelect = (item: MenuItemType) => { const handleMoreMenuSelect = (item: MenuItemType) => {
switch (item.key) { switch (item.key) {
@@ -1829,4 +1853,8 @@
.standalone-card-list-page { .standalone-card-list-page {
// Card list page styles // Card list page styles
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -39,15 +39,21 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -139,8 +145,10 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { StorageService } from '@/api/modules/storage' import { StorageService } from '@/api/modules/storage'
import { RoutesAlias } from '@/router/routesAlias' import { RoutesAlias } from '@/router/routesAlias'
@@ -152,6 +160,16 @@
const router = useRouter() const router = useRouter()
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false) const loading = ref(false)
const tableRef = ref() const tableRef = ref()
const uploadRef = ref<UploadInstance>() const uploadRef = ref<UploadInstance>()
@@ -733,4 +751,8 @@
} }
} }
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -1,6 +1,13 @@
<template> <template>
<div class="analysis-dashboard"> <div class="analysis-dashboard">
开发中敬请期待... <!-- 佣金概览 -->
<div class="section-title">佣金概览</div>
<CommissionSummary />
<!-- 提现配置 -->
<div class="section-title">提现配置</div>
<WithdrawalSettings />
<!--<el-row :gutter="20">--> <!--<el-row :gutter="20">-->
<!-- <el-col :xl="14" :lg="15" :xs="24">--> <!-- <el-col :xl="14" :lg="15" :xs="24">-->
<!-- <TodaySales />--> <!-- <TodaySales />-->
@@ -45,10 +52,24 @@
import TopProducts from './widget/TopProducts.vue' import TopProducts from './widget/TopProducts.vue'
import SalesMappingByCountry from './widget/SalesMappingByCountry.vue' import SalesMappingByCountry from './widget/SalesMappingByCountry.vue'
import VolumeServiceLevel from './widget/VolumeServiceLevel.vue' import VolumeServiceLevel from './widget/VolumeServiceLevel.vue'
import CommissionSummary from './widget/CommissionSummary.vue'
import WithdrawalSettings from './widget/WithdrawalSettings.vue'
defineOptions({ name: 'Analysis' }) defineOptions({ name: 'Analysis' })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@use './style'; @use './style';
.section-title {
margin-top: 24px;
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
&:first-child {
margin-top: 0;
}
}
</style> </style>

View File

@@ -31,7 +31,7 @@
} }
.el-card { .el-card {
border: 1px solid #e8ebf1; border: none;
box-shadow: none; box-shadow: none;
} }

View File

@@ -0,0 +1,175 @@
<template>
<ElRow :gutter="20" class="commission-summary-widget">
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4">
<ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
<i class="iconfont-sys">&#xe71d;</i>
</div>
<div class="stat-content">
<div class="stat-label">总佣金</div>
<div class="stat-value">{{ formatMoney(summary.total_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4">
<ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
>
<i class="iconfont-sys">&#xe71e;</i>
</div>
<div class="stat-content">
<div class="stat-label">可提现佣金</div>
<div class="stat-value">{{ formatMoney(summary.available_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4">
<ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
>
<i class="iconfont-sys">&#xe720;</i>
</div>
<div class="stat-content">
<div class="stat-label">冻结佣金</div>
<div class="stat-value">{{ formatMoney(summary.frozen_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4">
<ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)"
>
<i class="iconfont-sys">&#xe71f;</i>
</div>
<div class="stat-content">
<div class="stat-label">提现中佣金</div>
<div class="stat-value">{{ formatMoney(summary.withdrawing_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4">
<ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
>
<i class="iconfont-sys">&#xe721;</i>
</div>
<div class="stat-content">
<div class="stat-label">已提现佣金</div>
<div class="stat-value">{{ formatMoney(summary.withdrawn_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
</ElRow>
</template>
<script setup lang="ts">
import { CommissionService } from '@/api/modules'
import type { MyCommissionSummary } from '@/types/api/commission'
import { formatMoney } from '@/utils/business/format'
defineOptions({ name: 'CommissionSummaryWidget' })
// 佣金概览
const summary = ref<MyCommissionSummary>({
total_commission: 0,
available_commission: 0,
frozen_commission: 0,
withdrawing_commission: 0,
withdrawn_commission: 0
})
// 加载佣金概览
const loadSummary = async () => {
try {
const res = await CommissionService.getMyCommissionSummary()
if (res.code === 0) {
summary.value = res.data
}
} catch (error) {
console.error('获取佣金概览失败:', error)
}
}
onMounted(() => {
loadSummary()
})
</script>
<style lang="scss" scoped>
.commission-summary-widget {
.stat-card-wrapper {
margin-bottom: 20px;
:deep(.el-card__body) {
padding: 20px;
}
}
.stat-card {
display: flex;
gap: 16px;
align-items: center;
.stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
font-size: 24px;
color: white;
border-radius: 8px;
flex-shrink: 0;
}
.stat-content {
flex: 1;
min-width: 0;
.stat-label {
margin-bottom: 6px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
@media (max-width: 768px) {
.commission-summary-widget {
.stat-card-wrapper {
margin-bottom: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,223 @@
<template>
<ElCard shadow="never" class="withdrawal-settings-widget" v-if="currentSetting">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="header-title">当前生效配置</span>
<ElTag type="success" effect="dark" size="small">生效中</ElTag>
</div>
<div class="header-right">
<span class="creator-info"
>{{ currentSetting.creator_name || '-' }} 创建于
{{ formatDateTime(currentSetting.created_at) }}</span
>
</div>
</div>
</template>
<div class="setting-info">
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
<i class="el-icon">💰</i>
</div>
<div class="info-content">
<div class="info-label">最低提现金额</div>
<div class="info-value">{{ formatMoney(currentSetting.min_withdrawal_amount) }}</div>
</div>
</div>
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
>
<i class="el-icon">📊</i>
</div>
<div class="info-content">
<div class="info-label">手续费率</div>
<div class="info-value">{{ formatFeeRate(currentSetting.fee_rate) }}</div>
</div>
</div>
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
>
<i class="el-icon">🔢</i>
</div>
<div class="info-content">
<div class="info-label">每日提现次数</div>
<div class="info-value">{{ currentSetting.daily_withdrawal_limit }} </div>
</div>
</div>
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
>
<i class="el-icon"></i>
</div>
<div class="info-content">
<div class="info-label">到账天数</div>
<div class="info-value">{{
currentSetting.arrival_days === 0 ? '实时到账' : `${currentSetting.arrival_days}`
}}</div>
</div>
</div>
</div>
</ElCard>
<ElCard shadow="never" v-else class="withdrawal-settings-widget empty-state">
<div class="empty-content">
<i class="el-icon-info" style="font-size: 48px; color: var(--el-text-color-placeholder)"></i>
<p>暂无提现配置</p>
</div>
</ElCard>
</template>
<script setup lang="ts">
import { CommissionService } from '@/api/modules'
import { ElTag } from 'element-plus'
import type { WithdrawalSettingItem } from '@/types/api/commission'
import { formatDateTime, formatMoney, formatFeeRate } from '@/utils/business/format'
defineOptions({ name: 'WithdrawalSettingsWidget' })
// 当前生效的配置
const currentSetting = ref<WithdrawalSettingItem | null>(null)
// 加载当前生效配置
const loadCurrentSetting = async () => {
try {
const res = await CommissionService.getCurrentWithdrawalSetting()
if (res.code === 0 && res.data) {
currentSetting.value = res.data
}
} catch (error) {
console.error('获取当前配置失败:', error)
}
}
onMounted(() => {
loadCurrentSetting()
})
</script>
<style lang="scss" scoped>
.withdrawal-settings-widget {
:deep(.el-card__header) {
padding: 18px 20px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-lighter);
}
:deep(.el-card__body) {
padding: 20px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
.header-left {
display: flex;
gap: 10px;
align-items: center;
.header-title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.header-right {
.creator-info {
font-size: 13px;
color: var(--el-text-color-secondary);
}
}
}
.setting-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
.info-card {
display: flex;
gap: 12px;
align-items: center;
padding: 16px;
background: var(--el-fill-color-blank);
border-radius: 8px;
.info-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
font-size: 20px;
border-radius: 8px;
}
.info-content {
flex: 1;
min-width: 0;
.info-label {
margin-bottom: 4px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.info-value {
overflow: hidden;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
&.empty-state {
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--el-text-color-secondary);
p {
margin-top: 12px;
font-size: 14px;
}
}
}
}
@media (max-width: 768px) {
.withdrawal-settings-widget {
.card-header {
.header-left,
.header-right {
width: 100%;
}
}
.setting-info {
grid-template-columns: 1fr;
gap: 12px;
}
}
}
</style>

View File

@@ -33,14 +33,29 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -105,7 +120,11 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' 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 { formatDateTime } from '@/utils/business/format'
import { import {
CommonStatus, CommonStatus,
@@ -122,6 +141,17 @@
const loading = ref(false) const loading = ref(false)
const submitLoading = ref(false) const submitLoading = ref(false)
const tableRef = ref() const tableRef = ref()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<any>(null)
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
@@ -188,8 +218,7 @@
{ label: '运营商类型', prop: 'carrier_type' }, { label: '运营商类型', prop: 'carrier_type' },
{ label: '运营商描述', prop: 'description' }, { label: '运营商描述', prop: 'description' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' }
{ label: '操作', prop: 'operation' }
] ]
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
@@ -271,35 +300,6 @@
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: any) => formatDateTime(row.created_at) formatter: (row: any) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 150,
fixed: 'right',
formatter: (row: any) => {
const buttons = []
if (hasAuth('carrier:edit')) {
buttons.push(
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
})
)
}
if (hasAuth('carrier:delete')) {
buttons.push(
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteCarrier(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
@@ -469,10 +469,51 @@
console.error(error) 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.carrier-page { .carrier-page {
height: 100%; height: 100%;
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -46,15 +46,21 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -242,8 +248,10 @@
} from '@/types/api/commission' } from '@/types/api/commission'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime, formatMoney } from '@/utils/business/format' import { formatDateTime, formatMoney } from '@/utils/business/format'
import { import {
@@ -259,6 +267,15 @@
const route = useRoute() const route = useRoute()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 主表格状态 // 主表格状态
const loading = ref(false) const loading = ref(false)
const tableRef = ref() const tableRef = ref()
@@ -629,4 +646,8 @@
padding: 0; padding: 0;
} }
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -1,89 +1,5 @@
<template> <template>
<div class="my-commission-page"> <div class="my-commission-page">
<!-- 佣金概览卡片 -->
<ElRow :gutter="20" style="margin-bottom: 20px">
<ElCol :xs="24" :sm="12" :md="8" :lg="4">
<ElCard shadow="hover">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
<i class="iconfont-sys">&#xe71d;</i>
</div>
<div class="stat-content">
<div class="stat-label">总佣金</div>
<div class="stat-value">{{ formatMoney(summary.total_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="8" :lg="4">
<ElCard shadow="hover">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
>
<i class="iconfont-sys">&#xe71e;</i>
</div>
<div class="stat-content">
<div class="stat-label">可提现佣金</div>
<div class="stat-value">{{ formatMoney(summary.available_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="8" :lg="4">
<ElCard shadow="hover">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
>
<i class="iconfont-sys">&#xe720;</i>
</div>
<div class="stat-content">
<div class="stat-label">冻结佣金</div>
<div class="stat-value">{{ formatMoney(summary.frozen_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="8" :lg="4">
<ElCard shadow="hover">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)"
>
<i class="iconfont-sys">&#xe71f;</i>
</div>
<div class="stat-content">
<div class="stat-label">提现中佣金</div>
<div class="stat-value">{{ formatMoney(summary.withdrawing_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="8" :lg="4">
<ElCard shadow="hover">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
>
<i class="iconfont-sys">&#xe721;</i>
</div>
<div class="stat-content">
<div class="stat-label">已提现佣金</div>
<div class="stat-value">{{ formatMoney(summary.withdrawn_commission) }}</div>
</div>
</div>
</ElCard>
</ElCol>
</ElRow>
<!-- 标签页 --> <!-- 标签页 -->
<ElCard shadow="never"> <ElCard shadow="never">
<ElTabs v-model="activeTab"> <ElTabs v-model="activeTab">
@@ -841,37 +757,6 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.my-commission-page { .my-commission-page {
.stat-card { // 样式已移动到分析页的佣金概览组件
display: flex;
gap: 16px;
align-items: center;
.stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
font-size: 28px;
color: white;
border-radius: 12px;
}
.stat-content {
flex: 1;
.stat-label {
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
} }
</style> </style>

View File

@@ -1,78 +1,8 @@
<template> <template>
<ArtTableFullScreen> <ArtTableFullScreen>
<div class="withdrawal-settings-page" id="table-full-screen"> <div class="withdrawal-settings-page" id="table-full-screen">
<!-- 当前生效配置卡片 -->
<ElCard shadow="never" class="current-setting-card" v-if="currentSetting">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="header-title">当前生效配置</span>
<ElTag type="success" effect="dark">生效中</ElTag>
</div>
<div class="header-right">
<span class="creator-info"
>{{ currentSetting.creator_name || '-' }} 创建于
{{ formatDateTime(currentSetting.created_at) }}</span
>
</div>
</div>
</template>
<div class="setting-info">
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
<i class="el-icon">💰</i>
</div>
<div class="info-content">
<div class="info-label">最低提现金额</div>
<div class="info-value">{{ formatMoney(currentSetting.min_withdrawal_amount) }}</div>
</div>
</div>
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
>
<i class="el-icon">📊</i>
</div>
<div class="info-content">
<div class="info-label">手续费率</div>
<div class="info-value">{{ formatFeeRate(currentSetting.fee_rate) }}</div>
</div>
</div>
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
>
<i class="el-icon">🔢</i>
</div>
<div class="info-content">
<div class="info-label">每日提现次数</div>
<div class="info-value">{{ currentSetting.daily_withdrawal_limit }} </div>
</div>
</div>
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
>
<i class="el-icon"></i>
</div>
<div class="info-content">
<div class="info-label">到账天数</div>
<div class="info-value">{{
currentSetting.arrival_days === 0 ? '实时到账' : `${currentSetting.arrival_days}`
}}</div>
</div>
</div>
</div>
</ElCard>
<!-- 配置列表 --> <!-- 配置列表 -->
<ElCard shadow="never" class="art-table-card" style="margin-top: 20px"> <ElCard shadow="never" class="art-table-card">
<!-- 表格头部 --> <!-- 表格头部 -->
<ArtTableHeader <ArtTableHeader
:columnList="columnOptions" :columnList="columnOptions"
@@ -168,9 +98,6 @@
const tableRef = ref() const tableRef = ref()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
// 当前生效的配置
const currentSetting = ref<WithdrawalSettingItem | null>(null)
// 配置列表 // 配置列表
const settingsList = ref<WithdrawalSettingItem[]>([]) const settingsList = ref<WithdrawalSettingItem[]>([])
@@ -254,26 +181,9 @@
]) ])
onMounted(() => { onMounted(() => {
loadData() loadSettingsList()
}) })
// 加载数据
const loadData = async () => {
await Promise.all([loadCurrentSetting(), loadSettingsList()])
}
// 加载当前生效配置
const loadCurrentSetting = async () => {
try {
const res = await CommissionService.getCurrentWithdrawalSetting()
if (res.code === 0 && res.data) {
currentSetting.value = res.data
}
} catch (error) {
console.error('获取当前配置失败:', error)
}
}
// 加载配置列表 // 加载配置列表
const loadSettingsList = async () => { const loadSettingsList = async () => {
loading.value = true loading.value = true
@@ -291,7 +201,7 @@
// 刷新数据 // 刷新数据
const handleRefresh = () => { const handleRefresh = () => {
loadData() loadSettingsList()
} }
// 显示新增对话框 // 显示新增对话框
@@ -323,7 +233,7 @@
ElMessage.success('新增配置成功') ElMessage.success('新增配置成功')
dialogVisible.value = false dialogVisible.value = false
formEl.resetFields() formEl.resetFields()
loadData() loadSettingsList()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {
@@ -336,99 +246,6 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.withdrawal-settings-page { .withdrawal-settings-page {
.current-setting-card {
:deep(.el-card__header) {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.header-left {
display: flex;
gap: 12px;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 600;
color: #fff;
}
}
.header-right {
.creator-info {
font-size: 13px;
color: rgb(255 255 255 / 90%);
}
}
}
.setting-info {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
padding: 4px 0;
@media (width <= 1400px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 768px) {
grid-template-columns: 1fr;
}
.info-card {
display: flex;
gap: 16px;
align-items: center;
padding: 20px;
background: var(--el-fill-color-light);
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
transform: translateY(-2px);
}
.info-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
font-size: 24px;
border-radius: 12px;
}
.info-content {
flex: 1;
min-width: 0;
.info-label {
margin-bottom: 4px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.info-value {
overflow: hidden;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.form-tip { .form-tip {
margin-top: 4px; margin-top: 4px;
font-size: 12px; font-size: 12px;

View File

@@ -1,7 +1,16 @@
<template> <template>
<div class="single-card-page"> <div class="single-card-page">
<!-- 占位符用于在卡片固定时保持布局 -->
<div v-if="isSearchCardFixed" ref="searchCardPlaceholder" class="search-card-placeholder"></div>
<!-- ICCID查询区域 --> <!-- ICCID查询区域 -->
<ElCard shadow="never" class="search-card" style="margin-bottom: 24px"> <ElCard
ref="searchCardRef"
shadow="never"
class="search-card"
:class="{ 'is-fixed': isSearchCardFixed }"
:style="fixedCardStyle"
>
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>ICCID查询</span> <span>ICCID查询</span>
@@ -35,6 +44,60 @@
style="margin-left: 16px" style="margin-left: 16px"
>查询</ElButton >查询</ElButton
> >
<!-- 操作按钮组 -->
<div v-if="cardInfo" class="operation-button-group">
<ElDropdown trigger="click" @command="handleOperation">
<ElButton> 主要操作<i class="el-icon-arrow-down el-icon--right"></i> </ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="recharge">套餐充值</ElDropdownItem>
<ElDropdownItem command="activate">激活</ElDropdownItem>
<ElDropdownItem command="suspend">保号停机</ElDropdownItem>
<ElDropdownItem command="resume">保号复机</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<ElDropdown trigger="click" @command="handleOperation">
<ElButton> 查询记录<i class="el-icon-arrow-down el-icon--right"></i> </ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="trafficDetail">流量详单</ElDropdownItem>
<ElDropdownItem command="suspendRecord">停复机记录</ElDropdownItem>
<ElDropdownItem command="orderHistory">往期订单</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<ElDropdown trigger="click" @command="handleOperation">
<ElButton> 管理操作<i class="el-icon-arrow-down el-icon--right"></i> </ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="rebind">机卡重绑</ElDropdownItem>
<ElDropdownItem command="changeExpire">更改过期时间</ElDropdownItem>
<ElDropdownItem command="transferCard">转新卡</ElDropdownItem>
<ElDropdownItem command="adjustTraffic">增减流量</ElDropdownItem>
<ElDropdownItem command="speedLimit">单卡限速</ElDropdownItem>
<ElDropdownItem command="instantLimit">即时限速</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<ElDropdown trigger="click" @command="handleOperation">
<ElButton> 其他操作<i class="el-icon-arrow-down el-icon--right"></i> </ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="changeBalance">变更钱包余额</ElDropdownItem>
<ElDropdownItem command="resetPassword">重置支付密码</ElDropdownItem>
<ElDropdownItem command="renewRecharge">续充</ElDropdownItem>
<ElDropdownItem command="deviceOperation">设备操作</ElDropdownItem>
<ElDropdownItem command="recoverFromRoaming">窜卡复机</ElDropdownItem>
<ElDropdownItem command="roaming">窜卡</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</div> </div>
</ElCard> </ElCard>
@@ -42,47 +105,61 @@
<div v-if="cardInfo" class="card-content-area slide-in"> <div v-if="cardInfo" class="card-content-area slide-in">
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<div class="main-content-layout"> <div class="main-content-layout">
<!-- 第一行流量统计 --> <!-- 第一行当前套餐 -->
<div class="row full-width"> <div class="row full-width">
<ElCard shadow="never" class="info-card traffic-info"> <ElCard shadow="never" class="info-card traffic-info">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>流量统计</span> <span class="header-title">当前套餐: </span>
<span class="package-series-value">{{ cardInfo.packageSeries || '--' }}</span>
</div> </div>
</template> </template>
<!-- 流量使用情况 --> <!-- 流量使用情况 -->
<div class="traffic-overview horizontal"> <div class="traffic-overview">
<!-- 左侧主要流量指标 --> <!-- 流量进度条 -->
<div class="traffic-left"> <div class="traffic-progress-section">
<div class="traffic-stats-grid"> <!-- 进度条 -->
<ElCard shadow="never" class="stat-card"> <div class="progress-bar-wrapper">
<div class="stat-label">套餐系列</div> <div class="progress-info">
<div class="stat-value">{{ cardInfo.packageSeries || '--' }}</div> <span class="progress-label">流量使用情况</span>
</ElCard> <span class="progress-percentage">{{
<ElCard shadow="never" class="stat-card"> cardInfo.usedFlowPercentage || '0.00%'
<div class="stat-label">套餐总流量</div> }}</span>
<div class="stat-value">{{ cardInfo.packageTotalFlow || '--' }}</div> </div>
</ElCard> <ElProgress
<ElCard shadow="never" class="stat-card"> :percentage="parseFloat(cardInfo.usedFlowPercentage) || 0"
<div class="stat-label">已使用流量</div> :color="getProgressColor(parseFloat(cardInfo.usedFlowPercentage) || 0)"
<div class="stat-value">{{ cardInfo.usedFlow || '--' }}</div> :stroke-width="20"
</ElCard> :show-text="false"
<ElCard shadow="never" class="stat-card"> />
<div class="stat-label">已使用流量</div> <div class="progress-stats">
<div class="stat-value">{{ cardInfo.realUsedFlow || '--' }}</div> <div class="stat-item">
</ElCard> <span class="stat-label">总流量</span>
<ElCard shadow="never" class="stat-card"> <span class="stat-value">{{ cardInfo.packageTotalFlow || '0.00MB' }}</span>
<div class="stat-label">实际流量</div> </div>
<div class="stat-value">{{ cardInfo.actualFlow || '--' }}</div> <div class="stat-item">
</ElCard> <span class="stat-label">已使用</span>
<ElCard shadow="never" class="stat-card"> <span class="stat-value used">{{ cardInfo.usedFlow || '0.00MB' }}</span>
<div class="stat-label">剩余流量</div> </div>
<div class="stat-value">{{ cardInfo.remainFlow || '--' }}</div> <div class="stat-item">
</ElCard> <span class="stat-label">剩余</span>
<ElCard shadow="never" class="stat-card"> <span class="stat-value remaining">{{
<div class="stat-label">已使用流量百分比</div> cardInfo.remainFlow || '0.00MB'
<div class="stat-value">{{ cardInfo.usedFlowPercentage || '未设置' }}</div> }}</span>
</ElCard> </div>
</div>
</div>
</div>
<!-- 额外流量信息 -->
<div class="extra-traffic-info">
<div class="info-item">
<span class="label">已使用流量</span>
<span class="value">{{ cardInfo.realUsedFlow || '0.00MB' }}</span>
</div>
<div class="info-item">
<span class="label">实际流量</span>
<span class="value">{{ cardInfo.actualFlow || '0.00MB' }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -144,11 +221,6 @@
<ElDescriptionsItem label="运营商实名">{{ <ElDescriptionsItem label="运营商实名">{{
cardInfo?.operatorRealName || '--' cardInfo?.operatorRealName || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="国政通实名">
<ElTag :type="cardInfo?.realNameAuth ? 'success' : 'danger'" size="small">
{{ cardInfo?.realNameAuth ? '是' : '否' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="供应商">{{ <ElDescriptionsItem label="供应商">{{
cardInfo?.supplier || '--' cardInfo?.supplier || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
@@ -165,19 +237,19 @@
cardInfo?.walletPasswordStatus || '--' cardInfo?.walletPasswordStatus || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="导入时间">{{ <ElDescriptionsItem label="导入时间">{{
cardInfo?.importTime || '--' formatDateTime(cardInfo?.importTime)
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
</ElDescriptions> </ElDescriptions>
</ElCard> </ElCard>
</div> </div>
</div> </div>
<!-- 第三行当前套餐 --> <!-- 第三行套餐列表 -->
<div class="row full-width"> <div class="row full-width">
<ElCard shadow="never" class="info-card package-info"> <ElCard shadow="never" class="info-card package-info">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>当前套餐</span> <span>套餐列表</span>
</div> </div>
</template> </template>
<div class="package-table-wrapper"> <div class="package-table-wrapper">
@@ -204,124 +276,6 @@
</div> </div>
</ElCard> </ElCard>
</div> </div>
<!-- 第四行常规操作 -->
<div class="row two-columns">
<!-- 左侧操作 -->
<div class="col">
<ElCard shadow="never" class="info-card operation-card">
<div class="operations-grid">
<!-- 主要操作 -->
<div class="operation-group primary-operations">
<h4 class="group-title">主要操作</h4>
<div class="operation-buttons">
<ElButton
@click="handleOperation('recharge')"
:loading="operationLoading"
class="operation-btn"
>
套餐充值
</ElButton>
<ElButton
@click="handleOperation('activate')"
:loading="operationLoading"
class="operation-btn"
>
激活
</ElButton>
<ElButton
@click="handleOperation('suspend')"
:loading="operationLoading"
class="operation-btn"
>
保号停机
</ElButton>
<ElButton
@click="handleOperation('resume')"
:loading="operationLoading"
class="operation-btn"
>
保号复机
</ElButton>
</div>
</div>
<!-- 管理操作 -->
<div class="operation-group management-operations">
<h4 class="group-title">管理操作</h4>
<div class="operation-buttons">
<ElButton @click="handleOperation('rebind')" class="operation-btn"
>机卡重绑</ElButton
>
<ElButton @click="handleOperation('changeExpire')" class="operation-btn"
>更改过期时间</ElButton
>
<ElButton @click="handleOperation('transferCard')" class="operation-btn"
>转新卡</ElButton
>
<ElButton @click="handleOperation('adjustTraffic')" class="operation-btn"
>增减流量</ElButton
>
<ElButton @click="handleOperation('speedLimit')" class="operation-btn"
>单卡限速</ElButton
>
<ElButton @click="handleOperation('instantLimit')" class="operation-btn"
>即时限速</ElButton
>
</div>
</div>
</div>
</ElCard>
</div>
<!-- 右侧操作 -->
<div class="col">
<ElCard shadow="never" class="info-card operation-card">
<div class="operations-grid">
<!-- 查询操作 -->
<div class="operation-group query-operations">
<h4 class="group-title">查询记录</h4>
<div class="operation-buttons">
<ElButton @click="handleOperation('trafficDetail')" class="operation-btn"
>流量详单</ElButton
>
<ElButton @click="handleOperation('suspendRecord')" class="operation-btn"
>停复机记录</ElButton
>
<ElButton @click="handleOperation('orderHistory')" class="operation-btn"
>往期订单</ElButton
>
</div>
</div>
<!-- 其他操作 -->
<div class="operation-group other-operations">
<h4 class="group-title">其他操作</h4>
<div class="operation-buttons">
<ElButton @click="handleOperation('changeBalance')" class="operation-btn"
>变更钱包余额</ElButton
>
<ElButton @click="handleOperation('resetPassword')" class="operation-btn"
>重置支付密码</ElButton
>
<ElButton @click="handleOperation('renewRecharge')" class="operation-btn"
>续充</ElButton
>
<ElButton @click="handleOperation('deviceOperation')" class="operation-btn"
>设备操作</ElButton
>
<ElButton @click="handleOperation('recoverFromRoaming')" class="operation-btn"
>窜卡复机</ElButton
>
<ElButton @click="handleOperation('roaming')" class="operation-btn"
>窜卡</ElButton
>
</div>
</div>
</div>
</ElCard>
</div>
</div>
</div> </div>
</div> </div>
@@ -343,17 +297,27 @@
ElSkeleton, ElSkeleton,
ElDescriptions, ElDescriptions,
ElDescriptionsItem, ElDescriptionsItem,
ElMessageBox ElMessageBox,
ElDropdown,
ElDropdownMenu,
ElDropdownItem
} from 'element-plus' } from 'element-plus'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { EnterpriseService } from '@/api/modules/enterprise' import { EnterpriseService } from '@/api/modules/enterprise'
import { CardService } from '@/api/modules' import { CardService, DeviceService } from '@/api/modules'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'SingleCard' }) defineOptions({ name: 'SingleCard' })
const route = useRoute() const route = useRoute()
const loading = ref(false) const loading = ref(false)
const operationLoading = ref(false) const operationLoading = ref(false)
const isSearchCardFixed = ref(false)
const searchCardRef = ref<HTMLElement | null>(null)
const searchCardPlaceholder = ref<HTMLElement | null>(null)
const cardOriginalTop = ref(0)
const cardLeft = ref(0)
const cardWidth = ref(0)
// ICCID搜索相关 // ICCID搜索相关
const searchIccid = ref('') const searchIccid = ref('')
@@ -438,7 +402,7 @@
usedFlowPercentage: usedFlowPercentage:
data.data_usage_mb > 0 data.data_usage_mb > 0
? `${((data.current_month_usage_mb / data.data_usage_mb) * 100).toFixed(2)}%` ? `${((data.current_month_usage_mb / data.data_usage_mb) * 100).toFixed(2)}%`
: '未设置', : '0 %',
realUsedFlow: formatDataSize(data.current_month_usage_mb || 0), realUsedFlow: formatDataSize(data.current_month_usage_mb || 0),
actualFlow: formatDataSize(data.current_month_usage_mb || 0), actualFlow: formatDataSize(data.current_month_usage_mb || 0),
packageList: data.packages || [] packageList: data.packages || []
@@ -521,50 +485,144 @@
// 模拟卡片数据(保留作为参考) // 模拟卡片数据(保留作为参考)
const mockCardData = { const mockCardData = {
id: 1, // 卡片ID id: 1, // 卡片ID
iccid: '8986062357007989203', iccid: '',
accessNumber: '1440012345678', accessNumber: '',
imei: '860123456789012', imei: '',
expireTime: '2025-12-31', expireTime: '',
operator: '中国联通', operator: '',
cardStatus: '正常', cardStatus: '',
cardType: '流量卡', cardType: '',
supplier: '华为技术有限公司', supplier: '',
importTime: '2024-01-15 10:30:00', importTime: '',
phoneBind: '138****5678', phoneBind: '',
trafficPool: '全国流量池', trafficPool: '',
agent: '张丽丽', agent: '',
operatorStatus: '激活', operatorStatus: '',
operatorRealName: '已实名', operatorRealName: '',
walletBalance: '50.00元', walletBalance: '',
walletPasswordStatus: '已设置', walletPasswordStatus: '',
realNameAuth: true, realNameAuth: true,
virtualNumber: '10655****1234', virtualNumber: '',
// 流量信息 - 根据提供的数据更新 // 流量信息 - 根据提供的数据更新
packageSeries: 'UFI设备', packageSeries: '',
packageTotalFlow: '3072000MB', // 套餐总流量 packageTotalFlow: '', // 套餐总流量
usedFlow: '196.16MB', // 已使用流量 usedFlow: '', // 已使用流量
remainFlow: '3071803.84MB', // 剩余流量 remainFlow: '', // 剩余流量
usedFlowPercentage: '未设置', // 增加已使用流量百分比 usedFlowPercentage: '', // 增加已使用流量百分比
realUsedFlow: '196.16MB', // 已使用流量(真) realUsedFlow: '', // 已使用流量(真)
actualFlow: '196.16MB', // 实际流量 actualFlow: '', // 实际流量
packageList: [ packageList: []
{
packageName: '随意联畅玩年卡套餐12个月',
packageType: '年卡套餐',
totalFlow: '3072000MB',
usedFlow: '196.16MB',
remainFlow: '3071803.84MB',
expireTime: '2026-11-07',
status: '正常'
}
]
} }
// 页面初始化 - 不自动加载数据,等待用户输入ICCID查询 // 页面初始化 - 检查URL参数自动加载
onMounted(() => { onMounted(() => {
// 不再自动加载模拟数据,等待用户查询 autoLoadFromQuery()
// 初始化位置信息
nextTick(() => {
updateCardPosition()
})
window.addEventListener('scroll', handleScroll, true)
window.addEventListener('resize', updateCardPosition)
}) })
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll, true)
window.removeEventListener('resize', updateCardPosition)
})
// 更新卡片位置信息
const updateCardPosition = () => {
if (searchCardRef.value) {
const rect = searchCardRef.value.getBoundingClientRect()
cardOriginalTop.value = rect.top + window.scrollY
cardLeft.value = rect.left + window.scrollX
cardWidth.value = rect.width
}
}
// 处理滚动事件
const handleScroll = () => {
if (!searchCardRef.value) return
const scrollTop = window.scrollY || window.pageYOffset || document.documentElement.scrollTop
// 判断是否应该固定:当滚动超过卡片原始位置时
const shouldBeFixed = scrollTop > cardOriginalTop.value - 20
// 只在状态变化时更新
if (shouldBeFixed !== isSearchCardFixed.value) {
if (shouldBeFixed && !isSearchCardFixed.value) {
// 在变成固定之前,更新位置信息
updateCardPosition()
}
isSearchCardFixed.value = shouldBeFixed
}
}
// 计算固定时的样式
const fixedCardStyle = computed(() => {
if (isSearchCardFixed.value && cardWidth.value > 0) {
return {
left: `${cardLeft.value}px`,
width: `${cardWidth.value}px`
}
}
return {}
})
// 监听路由查询参数变化
watch(
() => route.query,
() => {
autoLoadFromQuery()
}
)
// 自动加载逻辑
const autoLoadFromQuery = () => {
const iccidFromQuery = route.query.iccid as string
const deviceNoFromQuery = route.query.device_no as string
if (iccidFromQuery) {
// 如果有ICCID参数,自动填充并搜索
searchIccid.value = iccidFromQuery
fetchCardDetailByIccid(iccidFromQuery)
} else if (deviceNoFromQuery) {
// 如果有设备号参数,先查询设备获取ICCID
searchIccid.value = deviceNoFromQuery
fetchCardDetailByDeviceNo(deviceNoFromQuery)
}
}
// 根据设备号获取卡片详情
const fetchCardDetailByDeviceNo = async (deviceNo: string) => {
try {
loading.value = true
const response = await DeviceService.getDeviceByImei(deviceNo)
if (response.code === 0 && response.data) {
const deviceData = response.data
// 如果设备有绑定的ICCID,使用ICCID查询卡片信息
if (deviceData.iccid) {
searchIccid.value = deviceData.iccid
await fetchCardDetailByIccid(deviceData.iccid)
} else {
ElMessage.warning(`设备 ${deviceNo} 未绑定SIM卡`)
cardInfo.value = null
}
} else {
ElMessage.error(response.msg || '查询设备失败')
cardInfo.value = null
}
} catch (error: any) {
console.error('获取设备信息失败:', error)
ElMessage.error(error?.message || '获取设备信息失败')
cardInfo.value = null
} finally {
loading.value = false
}
}
// 获取状态标签类型 // 获取状态标签类型
const getStatusType = (status: string) => { const getStatusType = (status: string) => {
switch (status) { switch (status) {
@@ -713,20 +771,44 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.single-card-page { .single-card-page {
padding: 20px 0; padding: 20px;
// 占位符
.search-card-placeholder {
height: 140px; // 大约等于搜索卡片的高度
margin-bottom: 20px;
}
// ICCID搜索卡片 // ICCID搜索卡片
.search-card { .search-card {
position: relative; margin-bottom: 20px;
overflow: visible; overflow: visible;
background: var(--el-bg-color, #fff);
transition: box-shadow 0.3s ease;
&.is-fixed {
position: fixed;
top: 20px;
z-index: 100;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
}
:deep(.el-card__header) {
padding: 16px 20px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-lighter);
}
:deep(.el-card__body) { :deep(.el-card__body) {
padding: 16px 20px;
overflow: visible; overflow: visible;
} }
.iccid-search { .iccid-search {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
flex-wrap: wrap;
gap: 12px;
overflow: visible; overflow: visible;
.iccid-input-wrapper { .iccid-input-wrapper {
@@ -739,6 +821,7 @@
top: calc(100% + 12px); top: calc(100% + 12px);
left: 0; left: 0;
white-space: nowrap; white-space: nowrap;
z-index: 1000;
// 三角箭头 - 上边框样式 // 三角箭头 - 上边框样式
.magnifier-arrow { .magnifier-arrow {
@@ -772,6 +855,37 @@
} }
} }
} }
// 操作按钮组
.operation-button-group {
display: flex;
gap: 10px;
margin-left: auto;
}
}
}
// 内容区域
.card-content-area,
.empty-state {
transition: all 0.3s ease;
}
// 移动端
@media (max-width: 768px) {
padding: 16px;
.search-card {
top: 16px;
margin-bottom: 16px;
:deep(.el-card__header) {
padding: 12px 16px;
}
:deep(.el-card__body) {
padding: 12px 16px;
}
} }
} }
@@ -873,6 +987,15 @@
font-weight: 600; font-weight: 600;
color: var(--el-text-color-primary, #1f2937); color: var(--el-text-color-primary, #1f2937);
.header-title {
color: var(--el-text-color-primary);
}
.package-series-value {
color: var(--el-color-primary);
font-weight: 500;
}
i { i {
font-size: 18px; font-size: 18px;
color: #667eea; color: #667eea;
@@ -889,130 +1012,106 @@
.traffic-info { .traffic-info {
// 流量概览 // 流量概览
.traffic-overview { .traffic-overview {
&.horizontal { display: flex;
flex-direction: column;
gap: 20px;
// 流量进度条区域
.traffic-progress-section {
display: flex; display: flex;
gap: 24px; flex-direction: column;
align-items: stretch;
}
// 左侧:流量统计网格 .progress-bar-wrapper {
.traffic-left { display: flex;
flex: 1; flex-direction: column;
min-width: 0;
.traffic-stats-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 12px; gap: 12px;
.stat-card { .progress-info {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
padding: 20px 16px;
:deep(.el-card__body) { .progress-label {
font-size: 15px;
font-weight: 500;
color: var(--el-text-color-regular);
}
.progress-percentage {
font-size: 24px;
font-weight: 700;
color: var(--el-text-color-primary);
}
}
:deep(.el-progress) {
margin: 4px 0;
}
.progress-stats {
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 8px;
.stat-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; gap: 4px;
justify-content: center; flex: 1;
width: 100%; padding: 12px;
height: 100%; background: var(--el-fill-color-light);
padding: 0; border-radius: 6px;
}
.stat-label {
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
color: var(--el-text-color-regular);
text-align: center; text-align: center;
}
.stat-value { .stat-label {
font-size: 16px; font-size: 12px;
font-weight: 600; color: var(--el-text-color-secondary);
line-height: 1.2; }
color: var(--el-text-color-primary);
text-align: center; .stat-value {
word-break: break-all; font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
&.used {
color: var(--el-color-warning);
}
&.remaining {
color: var(--el-color-success);
}
}
} }
} }
} }
} }
// 中间:使用率显示 // 额外流量信息
.traffic-center { .extra-traffic-info {
flex: 1; display: flex;
min-width: 0; gap: 16px;
.usage-display { .info-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; gap: 4px;
justify-content: center; flex: 1;
height: 100%; padding: 12px;
padding: 20px; background: var(--el-fill-color-light);
background: var(--el-bg-color, #fff); border-radius: 8px;
border: 1px solid var(--el-border-color-light);
border-radius: 12px;
.usage-title {
margin-bottom: 12px;
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-regular);
}
.usage-chart {
.chart-value {
font-size: 32px;
font-weight: 700;
line-height: 1;
color: #667eea;
}
}
}
}
// 右侧:套餐信息
.traffic-right {
flex: 1;
min-width: 0;
.package-display {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
padding: 20px;
text-align: center; text-align: center;
background: var(--el-bg-color, #fff);
border: 1px solid var(--el-border-color-light);
border-radius: 12px;
.package-label { .label {
margin-bottom: 8px; font-size: 13px;
font-size: 14px; color: var(--el-text-color-secondary);
font-weight: 500;
color: var(--el-text-color-regular);
} }
.package-name { .value {
margin-bottom: 8px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
line-height: 1.4;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
} }
.package-actual {
font-size: 14px;
font-weight: 500;
color: #667eea;
}
} }
} }
} }
@@ -1032,51 +1131,6 @@
} }
} }
// 操作卡片
.operation-card {
:deep(.el-card__body) {
display: flex;
flex-direction: column;
}
.operations-grid {
display: flex;
flex: 1;
flex-direction: column;
.operation-group {
margin-bottom: 24px;
&:last-child {
flex: 1;
margin-bottom: 0;
}
.group-title {
padding-bottom: 8px;
margin: 0 0 16px;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary, #374151);
border-bottom: 2px solid var(--el-border-color-light, #e5e7eb);
}
.operation-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: flex-start;
.operation-btn {
margin-right: 0;
margin-left: 0;
}
}
}
}
}
// 加载和空状态 // 加载和空状态
.loading-state, .loading-state,
.empty-state { .empty-state {
@@ -1115,51 +1169,14 @@
} }
.traffic-info .traffic-overview { .traffic-info .traffic-overview {
&.horizontal { .progress-stats {
flex-direction: column; flex-direction: column;
gap: 16px;
}
.traffic-left .traffic-stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 8px; gap: 8px;
.stat-card {
padding: 12px 8px;
:deep(.el-card__body) {
padding: 0;
}
.stat-label {
margin-bottom: 6px;
font-size: 12px;
}
.stat-value {
font-size: 14px;
}
}
} }
.traffic-center .usage-display { .extra-traffic-info {
padding: 16px; flex-direction: column;
gap: 12px;
.usage-chart .chart-value {
font-size: 24px;
}
}
.traffic-right .package-display {
padding: 16px;
.package-name {
font-size: 14px;
}
.package-actual {
font-size: 13px;
}
} }
} }
@@ -1169,20 +1186,16 @@
} }
@media (width <= 480px) { @media (width <= 480px) {
.traffic-left .traffic-stats-grid { .traffic-info .traffic-overview {
grid-template-columns: 1fr; .progress-percentage {
gap: 6px; font-size: 20px;
}
.stat-card { .progress-stats .stat-item {
padding: 10px 6px; padding: 10px;
.stat-label {
margin-bottom: 4px;
font-size: 11px;
}
.stat-value { .stat-value {
font-size: 13px; font-size: 14px;
} }
} }
} }

View File

@@ -34,15 +34,21 @@
:pageSize="pagination.page_size" :pageSize="pagination.page_size"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -311,8 +317,10 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias' import { RoutesAlias } from '@/router/routesAlias'
@@ -323,6 +331,15 @@
const router = useRouter() const router = useRouter()
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false) const loading = ref(false)
const createLoading = ref(false) const createLoading = ref(false)
const tableRef = ref() const tableRef = ref()
@@ -1108,4 +1125,8 @@
width: 140px; width: 140px;
} }
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -34,15 +34,21 @@
:pageSize="pagination.page_size" :pageSize="pagination.page_size"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -251,8 +257,10 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias' import { RoutesAlias } from '@/router/routesAlias'
@@ -283,6 +291,15 @@
const seriesOptions = ref<SeriesSelectOption[]>([]) const seriesOptions = ref<SeriesSelectOption[]>([])
const searchSeriesOptions = ref<SeriesSelectOption[]>([]) const searchSeriesOptions = ref<SeriesSelectOption[]>([])
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
package_name: '', package_name: '',
@@ -952,6 +969,10 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.package-list-page { .package-list-page {
// 可以添加特定样式 // 可以添加特定样式
} }

View File

@@ -35,15 +35,21 @@
:pageSize="pagination.page_size" :pageSize="pagination.page_size"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 套餐系列操作右键菜单 --> <!-- 套餐系列操作右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="seriesOperationMenuRef" ref="seriesOperationMenuRef"
@@ -245,7 +251,7 @@
style="width: 100%" style="width: 100%"
> >
<ElOption label="仅自己" value="self" /> <ElOption label="仅自己" value="self" />
<ElOption label="自己+下级" value="self_and_sub" /> <!--<ElOption label="自己+下级" value="self_and_sub" />-->
</ElSelect> </ElSelect>
</div> </div>
</div> </div>
@@ -396,8 +402,10 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { import {
@@ -1112,6 +1120,15 @@
ElMessage.warning('您没有查看详情的权限') ElMessage.warning('您没有查看详情的权限')
} }
} }
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1122,4 +1139,8 @@
.dialog-footer { .dialog-footer {
text-align: right; text-align: right;
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -91,13 +91,14 @@
<ElTableColumn label="统计范围" width="120"> <ElTableColumn label="统计范围" width="120">
<template #default="{ row }"> <template #default="{ row }">
<ElTag size="small" type="warning"> <ElTag size="small" type="warning">
{{ 仅自己
row.stat_scope === 'self' <!--{{-->
? '仅自己' <!-- row.stat_scope === 'self'-->
: row.stat_scope === 'self_and_sub' <!-- ? '仅自己'-->
? '自己+下级' <!-- : row.stat_scope === 'self_and_sub'-->
: '-' <!-- ? '自己+下级'-->
}} <!-- : '-'-->
<!--}}-->
</ElTag> </ElTag>
</template> </template>
</ElTableColumn> </ElTableColumn>

View File

@@ -35,15 +35,21 @@
:pageSize="pagination.page_size" :pageSize="pagination.page_size"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -52,60 +58,443 @@
@select="handleContextMenuSelect" @select="handleContextMenuSelect"
/> />
<!-- 套餐列表对话框 -->
<ElDialog
v-model="packageListDialogVisible"
title="套餐列表"
width="65%"
:close-on-click-modal="false"
@closed="handlePackageListDialogClosed"
>
<div class="package-list-dialog-content">
<!-- 添加授权套餐按钮 -->
<div class="package-list-header">
<ElButton
type="primary"
@click="showAddPackageDialog"
v-permission="'series_grants:manage_packages'"
>
添加授权套餐
</ElButton>
</div>
<!-- 套餐列表 -->
<ElTable
v-if="currentGrantPackages.length > 0"
:data="currentGrantPackages"
border
stripe
style="margin-top: 12px"
>
<ElTableColumn prop="package_name" label="套餐名称" />
<ElTableColumn prop="package_code" label="套餐编码" />
<ElTableColumn label="成本价">
<template #default="{ row }">
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="上架状态" align="center">
<template #default="{ row }">
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
<ElTag v-else-if="row.shelf_status === 2" type="info" size="small">下架</ElTag>
<span v-else>-</span>
</template>
</ElTableColumn>
<ElTableColumn label="状态" width="100" align="center">
<template #default="{ row }">
<ElTag v-if="row.status === 1" type="success" size="small">启用</ElTag>
<ElTag v-else-if="row.status === 2" type="danger" size="small">禁用</ElTag>
<span v-else>-</span>
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<ElButton
type="primary"
size="small"
link
@click="showEditPackageDialog(row)"
v-permission="'series_grants:edit_packages'"
>
编辑
</ElButton>
<ElButton
type="danger"
size="small"
link
@click="handleDeletePackage(row)"
v-permission="'series_grants:delete_packages'"
>
删除
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<ElEmpty v-else description="暂无套餐" :image-size="80" />
</div>
</ElDialog>
<!-- 添加/编辑套餐对话框 -->
<ElDialog
v-model="packageDialogVisible"
:title="packageDialogType === 'add' ? '添加套餐' : '编辑套餐'"
width="500px"
:close-on-click-modal="false"
@closed="handlePackageDialogClosed"
>
<ElForm
ref="packageFormRef"
:model="packageForm"
:rules="packageFormRules"
label-width="100px"
>
<!-- 添加模式选择套餐 -->
<ElFormItem label="选择套餐" prop="package_id" v-if="packageDialogType === 'add'">
<ElSelect
v-model="packageForm.package_id"
placeholder="请选择套餐"
style="width: 100%"
filterable
remote
:remote-method="searchAvailablePackages"
:loading="packageLoading"
clearable
>
<template
v-if="availablePackages.length === 0 && !packageLoading && currentGrantSeriesId"
>
<ElOption disabled value="" label="该系列没有可选套餐" />
</template>
<ElOption
v-for="pkg in availablePackages"
:key="pkg.id"
:label="`${pkg.package_name} (${pkg.package_code})`"
:value="pkg.id"
:disabled="isPackageAlreadyAdded(pkg.id)"
/>
</ElSelect>
</ElFormItem>
<!-- 编辑模式显示套餐信息 -->
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
<span>{{ packageForm.package_name }}</span>
</ElFormItem>
<ElFormItem label="套餐编码" v-if="packageDialogType === 'edit'">
<span>{{ packageForm.package_code }}</span>
</ElFormItem>
<!-- 成本价 -->
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
<ElInputNumber
v-model="packageForm.cost_price_yuan"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="packageDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSavePackage" :loading="packageSubmitLoading">
保存
</ElButton>
</div>
</template>
</ElDialog>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增代理系列授权' : '编辑代理系列授权'" :title="dialogType === 'add' ? '新增代理系列授权' : '编辑代理系列授权'"
width="50%" width="60%"
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleDialogClosed" @closed="handleDialogClosed"
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="130px"> <!-- 新增模式分步骤表单 -->
<!-- 新增模式基本信息 - 2列布局 --> <template v-if="dialogType === 'add'">
<div v-if="dialogType === 'add'"> <!-- 步骤条 -->
<ElRow :gutter="20"> <ElSteps
<ElCol :span="12"> :active="currentStep"
<ElFormItem label="选择套餐系列" prop="series_id"> finish-status="success"
align-center
style="margin-bottom: 30px"
>
<ElStep title="基本信息" description="选择系列和店铺" />
<ElStep title="佣金和充值配置" description="设置佣金和强制充值" />
</ElSteps>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="130px">
<!-- 第一步基本信息 -->
<div v-show="currentStep === 0">
<ElRow :gutter="20">
<ElCol :span="24">
<ElFormItem label="选择套餐系列" prop="series_id">
<ElSelect
v-model="form.series_id"
placeholder="请选择套餐系列"
style="width: 100%"
filterable
remote
:remote-method="searchSeries"
:loading="seriesLoading"
clearable
>
<ElOption
v-for="series in seriesOptions"
:key="series.id"
:label="`${series.series_name} (${series.series_code})`"
:value="series.id"
/>
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.series_id">
<ElCol :span="24">
<ElFormItem label="选择店铺" prop="shop_id">
<ElTreeSelect
v-model="form.shop_id"
:data="shopTreeData"
:props="{ label: 'shop_name', value: 'id', children: 'children' }"
placeholder="请选择店铺"
style="width: 100%"
filterable
clearable
:loading="shopLoading"
check-strictly
:render-after-expand="false"
/>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 套餐配置 -->
<div v-if="form.shop_id" class="form-section-title">
<span class="title-text">套餐配置可选</span>
</div>
<template v-if="form.shop_id">
<!-- 选择套餐 -->
<ElFormItem label="选择套餐">
<ElSelect <ElSelect
v-model="form.series_id" v-model="selectedPackageIds"
placeholder="请选择套餐系列" placeholder="请选择套餐"
style="width: 100%" style="width: 100%"
multiple
filterable filterable
remote remote
:remote-method="searchSeries" :remote-method="searchPackages"
:loading="seriesLoading" :loading="packageLoading"
clearable clearable
> >
<template
v-if="packageOptions.length === 0 && !packageLoading && form.series_id"
>
<ElOption disabled value="" label="该系列没有可选套餐" />
</template>
<ElOption <ElOption
v-for="series in seriesOptions" v-for="pkg in packageOptions"
:key="series.id" :key="pkg.id"
:label="`${series.series_name} (${series.series_code})`" :label="pkg.package_name"
:value="series.id" :value="pkg.id"
/> />
</ElSelect> </ElSelect>
<div class="form-tip">选择该授权下包含的套餐</div>
</ElFormItem> </ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="选择店铺" prop="shop_id">
<ElTreeSelect
v-model="form.shop_id"
:data="shopTreeData"
:props="{ label: 'shop_name', value: 'id', children: 'children' }"
placeholder="请选择店铺"
style="width: 100%"
filterable
clearable
:loading="shopLoading"
check-strictly
:render-after-expand="false"
/>
</ElFormItem>
</ElCol>
</ElRow>
</div>
<!-- 套餐成本价 -->
<ElFormItem label="套餐成本价" v-if="form.packages.length > 0">
<div class="package-list">
<div
v-for="(pkg, index) in form.packages"
:key="pkg.package_id"
class="package-item"
>
<span class="package-name">{{ getPackageName(pkg.package_id) }}</span>
<div class="cost-price-input-wrapper">
<ElInputNumber
v-model="pkg.cost_price"
:min="pkg.original_cost_price || 0"
:precision="2"
:step="0.01"
:controls="false"
placeholder="成本价(元)"
style="width: 150px"
/>
<span v-if="pkg.original_cost_price" class="min-cost-hint">
(成本价: ¥{{ pkg.original_cost_price.toFixed(2) }})
</span>
</div>
<ElButton type="danger" size="small" @click="removePackage(index)"
>删除</ElButton
>
</div>
</div>
<div class="form-tip"
>设置每个套餐的成本价单位不能低于套餐原始成本价</div
>
</ElFormItem>
</template>
</div>
<!-- 第二步佣金和强制充值配置 -->
<div v-show="currentStep === 1">
<!-- 一次性佣金配置 -->
<div class="form-section-title">
<span class="title-text">一次性佣金配置</span>
</div>
<!-- 佣金类型和金额 - 2列布局 -->
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="佣金类型">
<div class="commission-type-display">
<ElTag
:type="form.commission_type === 'fixed' ? 'success' : 'warning'"
size="large"
>
{{ form.commission_type === 'fixed' ? '固定佣金' : '梯度佣金' }}
</ElTag>
<span class="type-hint">从套餐系列配置继承</span>
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="form.commission_type === 'fixed'">
<ElFormItem label="佣金金额(元)" prop="one_time_commission_amount">
<ElInputNumber
v-model="form.one_time_commission_amount"
:min="0"
:max="form.series_max_commission_amount"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入固定佣金金额(元)"
/>
<div class="form-tip">
该代理能获得的固定佣金金额单位
<span v-if="form.series_max_commission_amount > 0" class="max-amount-hint">
<br />
该系列最大佣金金额
<span class="amount-value"
>¥{{ form.series_max_commission_amount.toFixed(2) }}</span
>
</span>
</div>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 梯度佣金配置 -->
<template v-if="form.commission_type === 'tiered'">
<ElFormItem label="梯度配置" prop="commission_tiers">
<ElTable :data="form.commission_tiers" border style="width: 100%">
<ElTableColumn label="比较运算符" width="100" align="center">
<template #default="{ row }">
<ElTag size="small" type="success">{{ row.operator || '>=' }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="达标阈值" width="120">
<template #default="{ row }">
<span class="readonly-value">{{ row.threshold }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="统计维度" width="120">
<template #default="{ row }">
<ElTag size="small" type="info">
{{ row.dimension === 'sales_count' ? '销量' : '销售额' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="统计范围" width="140">
<template #default="{ row }">
<ElTag size="small" type="warning"> 仅自己 </ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="佣金金额(元)" min-width="180">
<template #default="{ row }">
<div style="display: flex; flex-direction: column; gap: 4px">
<ElInputNumber
v-model="row.amount"
:min="0"
:max="row.max_amount"
:precision="2"
:step="0.01"
:controls="false"
placeholder="请输入佣金金额"
style="width: 100%"
/>
<span
v-if="row.max_amount"
style="font-size: 12px; color: var(--el-text-color-secondary)"
>
最大: ¥{{ row.max_amount.toFixed(2) }}
</span>
</div>
</template>
</ElTableColumn>
</ElTable>
<div class="form-tip" style="margin-top: 8px">
梯度配置从套餐系列继承达标阈值统计维度统计范围为只读只能修改佣金金额
</div>
</ElFormItem>
</template>
<!-- 强制充值配置 -->
<div class="form-section-title">
<span class="title-text">强制充值配置可选</span>
</div>
<!-- 启用强制充值和强充金额 - 2列布局 -->
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="启用强制充值">
<ElSwitch v-model="form.enable_force_recharge" />
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="form.enable_force_recharge">
<ElFormItem label="强充金额(元)" prop="force_recharge_amount">
<ElInputNumber
v-model="form.force_recharge_amount"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入强制充值金额(元)"
/>
<div class="form-tip">
用户需要达到的强制充值金额
<span
v-if="form.series_name && form.series_force_recharge_amount > 0"
class="series-force-hint"
>
<br />
可参考
<span class="amount-value">{{ form.series_name }}</span
>系列强充金额
<span class="amount-value"
>¥{{ form.series_force_recharge_amount.toFixed(2) }}</span
>
</span>
</div>
</ElFormItem>
</ElCol>
</ElRow>
</div>
</ElForm>
</template>
<!-- 编辑模式原有的表单布局 -->
<ElForm v-else ref="formRef" :model="form" :rules="rules" label-width="130px">
<!-- 编辑模式显示只读信息 --> <!-- 编辑模式显示只读信息 -->
<div v-if="dialogType === 'edit'" class="info-row"> <div class="info-row">
<div class="info-item"> <div class="info-item">
<span class="info-label">系列名称:</span> <span class="info-label">系列名称:</span>
<span class="info-value">{{ form.series_name || '-' }}</span> <span class="info-value">{{ form.series_name || '-' }}</span>
@@ -189,15 +578,7 @@
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="统计范围" width="140"> <ElTableColumn label="统计范围" width="140">
<template #default="{ row }"> <template #default="{ row }">
<ElTag size="small" type="warning"> <ElTag size="small" type="warning"> 仅自己 </ElTag>
{{
row.stat_scope === 'self'
? '仅自己'
: row.stat_scope === 'self_and_sub'
? '自己+下级'
: '-'
}}
</ElTag>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="佣金金额(元)" min-width="180"> <ElTableColumn label="佣金金额(元)" min-width="180">
@@ -271,13 +652,13 @@
</ElCol> </ElCol>
</ElRow> </ElRow>
<!-- 套餐配置 --> <!-- 套餐配置(编辑模式下禁用) -->
<div class="form-section-title"> <!-- <div class="form-section-title">
<span class="title-text">套餐配置可选</span> <span class="title-text">套餐配置可选</span>
</div> </div> -->
<!-- 选择套餐 --> <!-- 编辑模式下不显示套餐配置区域请使用右键菜单中的"添加授权套餐"功能 -->
<ElFormItem label="选择套餐"> <!-- <ElFormItem label="选择套餐">
<ElSelect <ElSelect
v-model="selectedPackageIds" v-model="selectedPackageIds"
placeholder="请选择套餐" placeholder="请选择套餐"
@@ -300,10 +681,10 @@
/> />
</ElSelect> </ElSelect>
<div class="form-tip">选择该授权下包含的套餐</div> <div class="form-tip">选择该授权下包含的套餐</div>
</ElFormItem> </ElFormItem> -->
<!-- 套餐成本价 --> <!-- 套餐成本价 -->
<ElFormItem label="套餐成本价" v-if="form.packages.length > 0"> <!-- <ElFormItem label="套餐成本价" v-if="form.packages.length > 0">
<div class="package-list"> <div class="package-list">
<div <div
v-for="(pkg, index) in form.packages" v-for="(pkg, index) in form.packages"
@@ -329,14 +710,32 @@
</div> </div>
</div> </div>
<div class="form-tip">设置每个套餐的成本价单位不能低于套餐原始成本价</div> <div class="form-tip">设置每个套餐的成本价单位不能低于套餐原始成本价</div>
</ElFormItem> </ElFormItem> -->
</ElForm> </ElForm>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton> <template v-if="dialogType === 'add'">
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading"> <ElButton v-if="currentStep > 0" @click="handlePrevStep">上一步</ElButton>
提交 <ElButton @click="dialogVisible = false">取消</ElButton>
</ElButton> <ElButton v-if="currentStep < 1" type="primary" @click="handleNextStep"
>下一步</ElButton
>
<ElButton
v-else
type="primary"
@click="handleSubmit(formRef)"
:loading="submitLoading"
>
提交
</ElButton>
</template>
<template v-else>
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
提交
</ElButton>
</template>
</div> </div>
</template> </template>
</ElDialog> </ElDialog>
@@ -346,7 +745,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h, ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { import {
ShopSeriesGrantService, ShopSeriesGrantService,
@@ -354,20 +753,34 @@
ShopService, ShopService,
PackageManageService PackageManageService
} from '@/api/modules' } from '@/api/modules'
import { ElMessage, ElMessageBox, ElSwitch, ElTag, ElRow, ElCol } from 'element-plus' import {
ElMessage,
ElMessageBox,
ElSwitch,
ElTag,
ElRow,
ElCol,
ElSteps,
ElStep,
ElEmpty
} from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { import type {
ShopSeriesGrantResponse, ShopSeriesGrantResponse,
PackageSeriesResponse, PackageSeriesResponse,
ShopResponse, ShopResponse,
PackageResponse, PackageResponse,
CommissionTier CommissionTier,
GrantPackageItem,
GrantPackageInfo
} from '@/types/api' } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { import {
@@ -392,6 +805,42 @@
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>() const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<ShopSeriesGrantResponse | null>(null) const currentRow = ref<ShopSeriesGrantResponse | null>(null)
const currentStep = ref(0) // 当前步骤0 为第一步1 为第二步
// 套餐列表对话框相关
const packageListDialogVisible = ref(false)
const currentGrantId = ref<number>(0)
const currentGrantSeriesId = ref<number>(0)
const currentGrantPackages = ref<GrantPackageInfo[]>([])
// 套餐管理相关
const packageDialogVisible = ref(false)
const packageDialogType = ref<'add' | 'edit'>('add')
const packageSubmitLoading = ref(false)
const availablePackages = ref<PackageResponse[]>([])
const packageFormRef = ref<FormInstance>()
// 套餐表单
const packageForm = ref<{
package_id?: number
package_name?: string
package_code?: string
cost_price_yuan: number
}>({
cost_price_yuan: 0
})
// 套餐表单验证规则
const packageFormRules = computed<FormRules>(() => ({
package_id: [
{ required: packageDialogType.value === 'add', message: '请选择套餐', trigger: 'change' }
],
cost_price_yuan: [
{ required: true, message: '请输入成本价', trigger: 'blur' },
{ type: 'number', min: 0, message: '成本价不能小于0', trigger: 'blur' }
]
}))
const seriesOptions = ref<PackageSeriesResponse[]>([]) const seriesOptions = ref<PackageSeriesResponse[]>([])
const shopOptions = ref<ShopResponse[]>([]) const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([]) const shopTreeData = ref<ShopResponse[]>([])
@@ -1277,6 +1726,8 @@
const handleDialogClosed = () => { const handleDialogClosed = () => {
// 清除表单验证状态 // 清除表单验证状态
formRef.value?.clearValidate() formRef.value?.clearValidate()
// 重置步骤
currentStep.value = 0
// 重置表单数据 // 重置表单数据
form.id = 0 form.id = 0
form.series_id = undefined form.series_id = undefined
@@ -1414,6 +1865,10 @@
const contextMenuItems = computed((): MenuItemType[] => { const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = [] const items: MenuItemType[] = []
if (hasAuth('series_grants:detail')) {
items.push({ key: 'view_packages', label: '查看套餐列表' })
}
if (hasAuth('series_grants:edit')) { if (hasAuth('series_grants:edit')) {
items.push({ key: 'edit', label: '编辑' }) items.push({ key: 'edit', label: '编辑' })
} }
@@ -1438,6 +1893,9 @@
if (!currentRow.value) return if (!currentRow.value) return
switch (item.key) { switch (item.key) {
case 'view_packages':
showPackageListDialog(currentRow.value)
break
case 'edit': case 'edit':
showDialog('edit', currentRow.value) showDialog('edit', currentRow.value)
break break
@@ -1446,6 +1904,242 @@
break break
} }
} }
// 显示套餐列表对话框
const showPackageListDialog = async (row: ShopSeriesGrantResponse) => {
currentGrantId.value = row.id
currentGrantSeriesId.value = row.series_id
// 获取该授权的详细信息,包括套餐列表
loading.value = true
try {
const res = await ShopSeriesGrantService.getShopSeriesGrantDetail(row.id)
if (res.code === 0) {
currentGrantPackages.value = res.data.packages || []
packageListDialogVisible.value = true
}
} catch (error) {
console.error('获取套餐列表失败:', error)
ElMessage.error('获取套餐列表失败')
} finally {
loading.value = false
}
}
// 显示添加套餐对话框
const showAddPackageDialog = () => {
packageDialogType.value = 'add'
packageForm.value = {
package_id: undefined,
cost_price_yuan: 0
}
// 加载可用套餐
if (currentGrantSeriesId.value) {
loadAvailablePackages()
}
packageDialogVisible.value = true
}
// 显示编辑套餐对话框
const showEditPackageDialog = (row: GrantPackageInfo) => {
packageDialogType.value = 'edit'
packageForm.value = {
package_id: row.package_id,
package_name: row.package_name,
package_code: row.package_code,
cost_price_yuan: row.cost_price / 100
}
packageDialogVisible.value = true
}
// 加载可用套餐
const loadAvailablePackages = async (packageName?: string) => {
if (!currentGrantSeriesId.value) return
packageLoading.value = true
try {
const params: any = {
page: 1,
page_size: 50,
series_id: currentGrantSeriesId.value
}
if (packageName) {
params.package_name = packageName
}
const res = await PackageManageService.getPackages(params)
if (res.code === 0) {
availablePackages.value = res.data.items
}
} catch (error) {
console.error('加载套餐选项失败:', error)
} finally {
packageLoading.value = false
}
}
// 搜索可用套餐
const searchAvailablePackages = (query: string) => {
if (query) {
loadAvailablePackages(query)
} else {
loadAvailablePackages()
}
}
// 检查套餐是否已添加
const isPackageAlreadyAdded = (packageId: number) => {
return currentGrantPackages.value.some((p) => p.package_id === packageId)
}
// 保存套餐
const handleSavePackage = async () => {
if (!packageFormRef.value || !currentGrantId.value) return
await packageFormRef.value.validate(async (valid) => {
if (!valid) return
packageSubmitLoading.value = true
try {
const packages: GrantPackageItem[] = []
if (packageDialogType.value === 'add') {
// 添加模式:添加新套餐
packages.push({
package_id: packageForm.value.package_id,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
})
} else {
// 编辑模式:更新套餐成本价
packages.push({
package_id: packageForm.value.package_id,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
})
}
await ShopSeriesGrantService.manageGrantPackages(currentGrantId.value, { packages })
ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
packageDialogVisible.value = false
// 刷新套餐列表
await refreshPackageList()
} catch (error) {
console.error(error)
ElMessage.error(packageDialogType.value === 'add' ? '添加失败' : '更新失败')
} finally {
packageSubmitLoading.value = false
}
})
}
// 删除套餐
const handleDeletePackage = (row: GrantPackageInfo) => {
ElMessageBox.confirm(`确定删除套餐 ${row.package_name} 的授权吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
if (!currentGrantId.value) return
packageSubmitLoading.value = true
try {
const packages: GrantPackageItem[] = [
{
package_id: row.package_id,
remove: true
}
]
await ShopSeriesGrantService.manageGrantPackages(currentGrantId.value, { packages })
ElMessage.success('删除成功')
// 刷新套餐列表
await refreshPackageList()
} catch (error) {
console.error(error)
ElMessage.error('删除失败')
} finally {
packageSubmitLoading.value = false
}
})
.catch(() => {
// 用户取消
})
}
// 刷新套餐列表
const refreshPackageList = async () => {
if (!currentGrantId.value) return
try {
const res = await ShopSeriesGrantService.getShopSeriesGrantDetail(currentGrantId.value)
if (res.code === 0) {
currentGrantPackages.value = res.data.packages || []
}
} catch (error) {
console.error('刷新套餐列表失败:', error)
}
}
// 关闭套餐对话框
const handlePackageDialogClosed = () => {
packageFormRef.value?.resetFields()
packageForm.value = {
cost_price_yuan: 0
}
availablePackages.value = []
}
// 关闭套餐列表对话框
const handlePackageListDialogClosed = () => {
currentGrantId.value = 0
currentGrantSeriesId.value = 0
currentGrantPackages.value = []
availablePackages.value = []
}
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 下一步
const handleNextStep = async () => {
if (!formRef.value) return
// 根据当前步骤验证不同的字段
let fieldsToValidate: string[] = []
if (currentStep.value === 0) {
// 第一步:验证系列和店铺选择
fieldsToValidate = ['series_id', 'shop_id']
}
try {
// 验证当前步骤的字段
await formRef.value.validateField(fieldsToValidate)
// 验证通过,进入下一步
currentStep.value++
} catch (error) {
console.error('表单验证失败:', error)
}
}
// 上一步
const handlePrevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1453,6 +2147,10 @@
// 可以添加特定样式 // 可以添加特定样式
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.dialog-footer { .dialog-footer {
text-align: right; text-align: right;
} }
@@ -1595,4 +2293,17 @@
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
font-weight: 500; font-weight: 500;
} }
.package-list-dialog-content {
.package-list-header {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.amount-value {
font-weight: 600;
color: var(--el-color-warning);
}
}
</style> </style>

View File

@@ -34,16 +34,22 @@
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="false" :default-expand-all="false"
:pagination="false" :pagination="false"
:row-class-name="getRowClassName"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -297,8 +303,10 @@
import type { FormRules } from 'element-plus' import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { ShopService, RoleService } from '@/api/modules' import { ShopService, RoleService } from '@/api/modules'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
@@ -313,6 +321,15 @@
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter() const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const dialogType = ref('add') const dialogType = ref('add')
const dialogVisible = ref(false) const dialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
@@ -1118,4 +1135,8 @@
margin-bottom: 4px; margin-bottom: 4px;
} }
} }
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style> </style>

View File

@@ -31,12 +31,27 @@
:default-expand-all="false" :default-expand-all="false"
:marginTop="10" :marginTop="10"
:show-pagination="false" :show-pagination="false"
:row-class-name="getRowClassName"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -127,7 +142,11 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' 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 { import {
PermissionType, PermissionType,
PERMISSION_TYPE_OPTIONS, PERMISSION_TYPE_OPTIONS,
@@ -191,10 +210,8 @@
{ label: '权限类型', prop: 'perm_type' }, { label: '权限类型', prop: 'perm_type' },
{ label: '菜单路径', prop: 'url' }, { label: '菜单路径', prop: 'url' },
{ label: '适用端口', prop: 'platform' }, { label: '适用端口', prop: 'platform' },
// { label: '可用角色类型', prop: 'available_for_role_types' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '排序', prop: 'sort' }, { label: '排序', prop: 'sort' }
{ label: '操作', prop: 'operation' }
] ]
// 权限列表(树形结构) // 权限列表(树形结构)
@@ -208,6 +225,16 @@
const currentRow = ref<PermissionTreeNode | null>(null) const currentRow = ref<PermissionTreeNode | null>(null)
const currentPermissionId = ref<number>(0) const currentPermissionId = ref<number>(0)
const submitLoading = ref(false) const submitLoading = ref(false)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 表单引用和数据 // 表单引用和数据
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
@@ -315,35 +342,6 @@
prop: 'sort', prop: 'sort',
label: '排序', label: '排序',
width: 80 width: 80
},
{
prop: 'operation',
label: '操作',
width: 120,
fixed: 'right',
formatter: (row: PermissionTreeNode) => {
const buttons = []
if (hasAuth('permission:edit')) {
buttons.push(
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
})
)
}
if (hasAuth('permission:delete')) {
buttons.push(
h(ArtButtonTable, {
type: 'delete',
onClick: () => deletePermission(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
@@ -557,4 +555,47 @@
onMounted(() => { onMounted(() => {
getPermissionList() 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> </script>
<style scoped lang="scss">
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -32,15 +32,21 @@
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu" @row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 --> <!-- 右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="contextMenuRef" ref="contextMenuRef"
@@ -234,8 +240,10 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.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 type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants' import { CommonStatus, getStatusText } from '@/config/constants'
@@ -267,6 +275,15 @@
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>() const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<PlatformRole | null>(null) const currentRow = ref<PlatformRole | null>(null)
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
role_name: '', role_name: '',
@@ -1056,6 +1073,10 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.dialog-header { .dialog-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

220
update_context_menu.py Normal file
View File

@@ -0,0 +1,220 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批量为表格页面添加右键菜单和悬浮提示功能
"""
import re
import os
def add_imports(content):
"""添加必要的导入"""
# 检查是否已经导入
if 'useTableContextMenu' in content:
return content
# 找到 useAuth 导入位置
import_pattern = r"(import\s+{\s+useAuth\s+}\s+from\s+'@/composables/useAuth')"
if re.search(import_pattern, content):
# 在 useAuth 后面添加 useTableContextMenu
content = re.sub(
import_pattern,
r"\1\n import { useTableContextMenu } from '@/composables/useTableContextMenu'",
content
)
# 添加 TableContextMenuHint 组件导入
arttable_pattern = r"(import\s+ArtButtonTable\s+from\s+'@/components/core/forms/ArtButtonTable\.vue')"
if re.search(arttable_pattern, content):
content = re.sub(
arttable_pattern,
r"\1\n import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'",
content
)
# 如果没有 ArtMenuRight,添加它
if 'ArtMenuRight' not in content:
content = re.sub(
arttable_pattern,
r"\1\n import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'\n import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'",
content
)
return content
def add_context_menu_usage(content):
"""添加 useTableContextMenu 的使用"""
if 'useTableContextMenu()' in content:
return content
# 查找 const currentRow = ref 或类似的变量声明
pattern = r"(const\s+currentRow\s*=\s*ref[^\n]+)"
usage_code = """
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()"""
if re.search(pattern, content):
content = re.sub(pattern, r"\1" + usage_code, content)
return content
def add_table_events(content):
"""为 ArtTable 添加事件监听"""
# 查找 ArtTable 标签
table_pattern = r"(<ArtTable[^>]*?)(\s*@row-contextmenu=\"[^\"]+\")?([^>]*>)"
if '@cell-mouse-enter' in content:
return content
# 添加必要的属性和事件
def replace_table(match):
prefix = match.group(1)
existing_contextmenu = match.group(2) or ''
suffix = match.group(3)
# 如果没有 row-class-name,添加它
if ':row-class-name' not in prefix and 'row-class-name' not in prefix:
prefix += '\n :row-class-name="getRowClassName"'
# 如果没有 row-contextmenu,添加它
if not existing_contextmenu and '@row-contextmenu' not in prefix:
existing_contextmenu = '\n @row-contextmenu="handleRowContextMenu"'
# 添加 cell mouse 事件
cell_events = '\n @cell-mouse-enter="handleCellMouseEnter"\n @cell-mouse-leave="handleCellMouseLeave"'
return prefix + existing_contextmenu + cell_events + suffix
content = re.sub(table_pattern, replace_table, content, flags=re.DOTALL)
return content
def add_hint_component(content):
"""添加悬浮提示组件"""
if 'TableContextMenuHint' in content and ':visible="showContextMenuHint"' in content:
return content
# 在 </ArtTable> 后添加提示组件
table_end_pattern = r"(</ArtTable>)"
hint_component = r"""\1
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />"""
content = re.sub(table_end_pattern, hint_component, content)
return content
def add_context_menu_component(content):
"""添加右键菜单组件"""
if 'ArtMenuRight' in content and 'contextMenuRef' in content:
return content
# 在提示组件后添加右键菜单
hint_pattern = r"(<TableContextMenuHint[^>]+/>)"
menu_component = r"""\1
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>"""
content = re.sub(hint_pattern, menu_component, content)
return content
def add_css_styles(content):
"""添加 CSS 样式"""
if 'table-row-with-context-menu' in content:
return content
# 查找 <style> 标签
style_pattern = r"(<style[^>]*>)"
css_code = r"""\1
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
"""
content = re.sub(style_pattern, css_code, content)
return content
def process_file(file_path):
"""处理单个文件"""
print(f"Processing: {file_path}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
# 执行所有转换
content = add_imports(content)
content = add_context_menu_usage(content)
content = add_table_events(content)
content = add_hint_component(content)
content = add_context_menu_component(content)
content = add_css_styles(content)
# 如果内容有变化,写回文件
if content != original_content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f" ✓ Updated")
return True
else:
print(f" - No changes needed")
return False
except Exception as e:
print(f" ✗ Error: {e}")
return False
def main():
"""主函数"""
# 定义需要处理的文件列表
files_to_process = [
"src/views/package-management/package-list/index.vue",
"src/views/account-management/account/index.vue",
"src/views/account-management/enterprise-customer/index.vue",
"src/views/account-management/enterprise-cards/index.vue",
"src/views/asset-management/iot-card-management/index.vue",
"src/views/asset-management/iot-card-task/index.vue",
"src/views/asset-management/device-task/index.vue",
"src/views/asset-management/asset-assign/index.vue",
"src/views/asset-management/authorization-records/index.vue",
"src/views/finance/commission/agent-commission/index.vue",
]
base_dir = os.path.dirname(os.path.abspath(__file__))
updated_count = 0
for file_rel_path in files_to_process:
file_path = os.path.join(base_dir, file_rel_path)
if os.path.exists(file_path):
if process_file(file_path):
updated_count += 1
else:
print(f"File not found: {file_path}")
print(f"\n完成! 更新了 {updated_count} 个文件")
if __name__ == '__main__':
main()