This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(tree:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(timeout:*)"
|
||||
"Bash(timeout:*)",
|
||||
"Read(//d/**)",
|
||||
"Bash(findstr:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
38
src/components/core/others/TableContextMenuHint.vue
Normal file
38
src/components/core/others/TableContextMenuHint.vue
Normal 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>
|
||||
@@ -14,6 +14,7 @@
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
:row-key="rowKey"
|
||||
:row-class-name="rowClassName"
|
||||
:height="height"
|
||||
:max-height="maxHeight"
|
||||
:show-header="showHeader"
|
||||
@@ -30,6 +31,8 @@
|
||||
@row-click="handleRowClick"
|
||||
@row-contextmenu="handleRowContextmenu"
|
||||
@selection-change="handleSelectionChange"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<!-- 序号列 -->
|
||||
<el-table-column
|
||||
@@ -86,6 +89,8 @@
|
||||
loading?: boolean
|
||||
/** 行数据的 Key,用于标识每一行数据 */
|
||||
rowKey?: string
|
||||
/** 行的 className 的回调方法 */
|
||||
rowClassName?: ((data: { row: any; rowIndex: number }) => string) | string
|
||||
/** 是否显示边框 */
|
||||
border?: boolean | null
|
||||
/** 是否使用斑马纹样式 */
|
||||
@@ -136,6 +141,7 @@
|
||||
data: () => [],
|
||||
loading: false,
|
||||
rowKey: 'id',
|
||||
rowClassName: undefined,
|
||||
border: null,
|
||||
stripe: null,
|
||||
index: false,
|
||||
@@ -178,7 +184,9 @@
|
||||
'row-contextmenu',
|
||||
'size-change',
|
||||
'current-change',
|
||||
'selection-change'
|
||||
'selection-change',
|
||||
'cell-mouse-enter',
|
||||
'cell-mouse-leave'
|
||||
])
|
||||
|
||||
const tableStore = useTableStore()
|
||||
@@ -281,6 +289,16 @@
|
||||
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) => {
|
||||
emit('selection-change', selection)
|
||||
|
||||
63
src/composables/useTableContextMenu.ts
Normal file
63
src/composables/useTableContextMenu.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -450,11 +450,15 @@
|
||||
"devices": "Device Management",
|
||||
"deviceDetail": "Device Details",
|
||||
"assetAssign": "Allocation Records",
|
||||
"assetAssignDetail": "Asset Allocation Details",
|
||||
"allocationRecordDetail": "Allocation Record Details",
|
||||
"cardReplacementRequest": "Card Replacement Request",
|
||||
"authorizationRecords": "Authorization Records",
|
||||
"authorizationDetail": "Authorization Details",
|
||||
"enterpriseDevices": "Enterprise Devices"
|
||||
"authorizationRecordDetail": "Authorization Record Details",
|
||||
"enterpriseDevices": "Enterprise Devices",
|
||||
"recordsManagement": "Records Management",
|
||||
"taskManagement": "Task Management"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings Management",
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
},
|
||||
"cardManagement": {
|
||||
"title": "我的网卡",
|
||||
"singleCard": "单卡信息",
|
||||
"singleCard": "资产信息",
|
||||
"cardList": "网卡管理",
|
||||
"cardDetail": "网卡明细",
|
||||
"cardAssign": "网卡分配",
|
||||
@@ -453,7 +453,9 @@
|
||||
"authorizationRecords": "授权记录",
|
||||
"authorizationDetail": "授权记录详情",
|
||||
"authorizationRecordDetail": "授权记录详情",
|
||||
"enterpriseDevices": "企业设备列表"
|
||||
"enterpriseDevices": "企业设备列表",
|
||||
"recordsManagement": "记录管理",
|
||||
"taskManagement": "任务管理"
|
||||
},
|
||||
"account": {
|
||||
"title": "账户管理",
|
||||
|
||||
@@ -51,6 +51,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// {
|
||||
// path: '/widgets',
|
||||
// name: 'Widgets',
|
||||
@@ -266,6 +267,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
@@ -429,6 +431,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
// }
|
||||
]
|
||||
},
|
||||
|
||||
// {
|
||||
// path: '/article',
|
||||
// name: 'Article',
|
||||
@@ -712,6 +715,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
|
||||
{
|
||||
path: '/package-management',
|
||||
name: 'PackageManagement',
|
||||
@@ -780,6 +784,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/shop-management',
|
||||
name: 'ShopManagement',
|
||||
@@ -800,6 +805,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/account-management',
|
||||
name: 'AccountManagement',
|
||||
@@ -885,6 +891,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// {
|
||||
// path: '/product',
|
||||
// name: 'Product',
|
||||
@@ -950,6 +957,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
|
||||
{
|
||||
path: '/asset-management',
|
||||
name: 'AssetManagement',
|
||||
@@ -977,44 +985,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
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',
|
||||
name: 'DeviceList',
|
||||
@@ -1024,63 +994,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
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',
|
||||
name: 'EnterpriseDevices',
|
||||
@@ -1090,9 +1003,100 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
isHide: true,
|
||||
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',
|
||||
name: 'Finance',
|
||||
@@ -1141,6 +1145,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
// }
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/commission',
|
||||
name: 'CommissionManagement',
|
||||
@@ -1192,6 +1197,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// {
|
||||
// path: '/settings',
|
||||
// name: 'Settings',
|
||||
|
||||
3
src/types/components.d.ts
vendored
3
src/types/components.d.ts
vendored
@@ -122,6 +122,8 @@ declare module 'vue' {
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
||||
ElStep: typeof import('element-plus/es')['ElStep']
|
||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
@@ -152,6 +154,7 @@ declare module 'vue' {
|
||||
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']
|
||||
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']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
|
||||
@@ -31,16 +31,22 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -191,8 +197,10 @@
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { AccountService } from '@/api/modules/account'
|
||||
import { RoleService } from '@/api/modules/role'
|
||||
@@ -207,6 +215,15 @@
|
||||
const { hasAuth } = useAuth()
|
||||
const route = useRoute()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const roleDialogVisible = ref(false)
|
||||
@@ -865,6 +882,10 @@
|
||||
// 账号管理页面样式
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -64,10 +64,13 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
@@ -75,6 +78,9 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 授权卡对话框 -->
|
||||
<ElDialog
|
||||
v-model="allocateDialogVisible"
|
||||
@@ -232,8 +238,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { BgColorEnum } from '@/enums/appEnum'
|
||||
@@ -251,6 +259,16 @@
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const allocateDialogVisible = ref(false)
|
||||
const allocateLoading = ref(false)
|
||||
@@ -1064,4 +1082,8 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,15 +35,21 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -221,8 +227,10 @@
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
@@ -233,6 +241,15 @@
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 判断是否是代理账号 (user_type === 3)
|
||||
const isAgentAccount = computed(() => userStore.info.user_type === 3)
|
||||
|
||||
@@ -811,3 +828,9 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,15 +28,21 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -57,16 +63,28 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
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'
|
||||
|
||||
defineOptions({ name: 'AssetAllocationRecords' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
@@ -374,4 +392,8 @@
|
||||
.asset-allocation-records-page {
|
||||
// Allocation records page styles
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,15 +28,21 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -82,9 +88,11 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import 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 type {
|
||||
AuthorizationItem,
|
||||
@@ -98,6 +106,16 @@
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const remarkDialogVisible = ref(false)
|
||||
const remarkLoading = ref(false)
|
||||
@@ -442,4 +460,8 @@
|
||||
.authorization-records-page {
|
||||
// Authorization records page styles
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -53,10 +53,13 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
@@ -64,6 +67,9 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 批量分配对话框 -->
|
||||
<ElDialog v-model="allocateDialogVisible" title="批量分配设备" width="600px">
|
||||
<ElForm
|
||||
@@ -570,8 +576,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||
@@ -581,6 +589,16 @@
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const allocateLoading = ref(false)
|
||||
const recallLoading = ref(false)
|
||||
@@ -801,11 +819,11 @@
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 跳转到设备详情页面
|
||||
// 跳转到资产信息页面(原设备详情)
|
||||
const goToDeviceSearchDetail = (deviceNo: string) => {
|
||||
if (hasAuth('device:view_detail')) {
|
||||
router.push({
|
||||
path: '/asset-management/device-detail',
|
||||
path: '/asset-management/single-card',
|
||||
query: {
|
||||
device_no: deviceNo
|
||||
}
|
||||
@@ -1697,4 +1715,8 @@
|
||||
.device-list-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,15 +39,21 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -125,9 +131,11 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import 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 { StorageService } from '@/api/modules/storage'
|
||||
import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device'
|
||||
@@ -137,6 +145,16 @@
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
@@ -679,4 +697,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -51,7 +51,7 @@
|
||||
hasAuth('iot_card:batch_download')
|
||||
"
|
||||
type="info"
|
||||
@contextmenu.prevent="showMoreMenu"
|
||||
@click="showMoreMenuOnClick"
|
||||
>
|
||||
更多操作
|
||||
</ElButton>
|
||||
@@ -68,10 +68,13 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
@@ -79,6 +82,9 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 批量分配对话框 -->
|
||||
<ElDialog
|
||||
v-model="allocateDialogVisible"
|
||||
@@ -597,8 +603,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import type {
|
||||
@@ -616,6 +624,16 @@
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const allocateDialogVisible = ref(false)
|
||||
const allocateLoading = ref(false)
|
||||
@@ -910,11 +928,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到IoT卡详情页面
|
||||
// 跳转到资产信息页面(原IoT卡详情)
|
||||
const goToCardDetail = (iccid: string) => {
|
||||
if (hasAuth('iot_card:view_detail')) {
|
||||
router.push({
|
||||
path: RoutesAlias.StandaloneCardList + '/detail',
|
||||
path: '/asset-management/single-card',
|
||||
query: {
|
||||
iccid: iccid
|
||||
}
|
||||
@@ -1575,13 +1593,19 @@
|
||||
return items
|
||||
})
|
||||
|
||||
// 显示更多操作菜单
|
||||
// 显示更多操作菜单 (右键)
|
||||
const showMoreMenu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
moreMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 显示更多操作菜单 (左键点击)
|
||||
const showMoreMenuOnClick = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
moreMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 处理更多操作菜单选择
|
||||
const handleMoreMenuSelect = (item: MenuItemType) => {
|
||||
switch (item.key) {
|
||||
@@ -1829,4 +1853,8 @@
|
||||
.standalone-card-list-page {
|
||||
// Card list page styles
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,15 +39,21 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -139,8 +145,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
@@ -152,6 +160,16 @@
|
||||
const router = useRouter()
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
@@ -733,4 +751,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<template>
|
||||
<div class="analysis-dashboard">
|
||||
开发中敬请期待...
|
||||
<!-- 佣金概览 -->
|
||||
<div class="section-title">佣金概览</div>
|
||||
<CommissionSummary />
|
||||
|
||||
<!-- 提现配置 -->
|
||||
<div class="section-title">提现配置</div>
|
||||
<WithdrawalSettings />
|
||||
|
||||
<!--<el-row :gutter="20">-->
|
||||
<!-- <el-col :xl="14" :lg="15" :xs="24">-->
|
||||
<!-- <TodaySales />-->
|
||||
@@ -45,10 +52,24 @@
|
||||
import TopProducts from './widget/TopProducts.vue'
|
||||
import SalesMappingByCountry from './widget/SalesMappingByCountry.vue'
|
||||
import VolumeServiceLevel from './widget/VolumeServiceLevel.vue'
|
||||
import CommissionSummary from './widget/CommissionSummary.vue'
|
||||
import WithdrawalSettings from './widget/WithdrawalSettings.vue'
|
||||
|
||||
defineOptions({ name: 'Analysis' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@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>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border: 1px solid #e8ebf1;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
175
src/views/dashboard/analysis/widget/CommissionSummary.vue
Normal file
175
src/views/dashboard/analysis/widget/CommissionSummary.vue
Normal 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"></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"></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"></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"></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"></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>
|
||||
223
src/views/dashboard/analysis/widget/WithdrawalSettings.vue
Normal file
223
src/views/dashboard/analysis/widget/WithdrawalSettings.vue
Normal 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>
|
||||
@@ -33,14 +33,29 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -105,7 +120,11 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
CommonStatus,
|
||||
@@ -122,6 +141,17 @@
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
@@ -188,8 +218,7 @@
|
||||
{ label: '运营商类型', prop: 'carrier_type' },
|
||||
{ label: '运营商描述', prop: 'description' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
{ label: '创建时间', prop: 'created_at' }
|
||||
]
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
@@ -271,35 +300,6 @@
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('carrier:edit')) {
|
||||
items.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
|
||||
if (hasAuth('carrier:delete')) {
|
||||
items.push({ key: 'delete', label: '删除' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showDialog('edit', currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
deleteCarrier(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.carrier-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -46,15 +46,21 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -242,8 +248,10 @@
|
||||
} from '@/types/api/commission'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||
import {
|
||||
@@ -259,6 +267,15 @@
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 主表格状态
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
@@ -629,4 +646,8 @@
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,89 +1,5 @@
|
||||
<template>
|
||||
<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"></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"></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"></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"></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"></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">
|
||||
<ElTabs v-model="activeTab">
|
||||
@@ -841,37 +757,6 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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>
|
||||
|
||||
@@ -1,78 +1,8 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<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
|
||||
:columnList="columnOptions"
|
||||
@@ -168,9 +98,6 @@
|
||||
const tableRef = ref()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 当前生效的配置
|
||||
const currentSetting = ref<WithdrawalSettingItem | null>(null)
|
||||
|
||||
// 配置列表
|
||||
const settingsList = ref<WithdrawalSettingItem[]>([])
|
||||
|
||||
@@ -254,26 +181,9 @@
|
||||
])
|
||||
|
||||
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 () => {
|
||||
loading.value = true
|
||||
@@ -291,7 +201,7 @@
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
loadSettingsList()
|
||||
}
|
||||
|
||||
// 显示新增对话框
|
||||
@@ -323,7 +233,7 @@
|
||||
ElMessage.success('新增配置成功')
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
loadData()
|
||||
loadSettingsList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@@ -336,99 +246,6 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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 {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<template>
|
||||
<div class="single-card-page">
|
||||
<!-- 占位符,用于在卡片固定时保持布局 -->
|
||||
<div v-if="isSearchCardFixed" ref="searchCardPlaceholder" class="search-card-placeholder"></div>
|
||||
|
||||
<!-- 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>
|
||||
<div class="card-header">
|
||||
<span>ICCID查询</span>
|
||||
@@ -35,6 +44,60 @@
|
||||
style="margin-left: 16px"
|
||||
>查询</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>
|
||||
</ElCard>
|
||||
|
||||
@@ -42,47 +105,61 @@
|
||||
<div v-if="cardInfo" class="card-content-area slide-in">
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="main-content-layout">
|
||||
<!-- 第一行:流量统计 -->
|
||||
<!-- 第一行:当前套餐 -->
|
||||
<div class="row full-width">
|
||||
<ElCard shadow="never" class="info-card traffic-info">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>流量统计</span>
|
||||
<span class="header-title">当前套餐: </span>
|
||||
<span class="package-series-value">{{ cardInfo.packageSeries || '--' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 流量使用情况 -->
|
||||
<div class="traffic-overview horizontal">
|
||||
<!-- 左侧:主要流量指标 -->
|
||||
<div class="traffic-left">
|
||||
<div class="traffic-stats-grid">
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">套餐系列</div>
|
||||
<div class="stat-value">{{ cardInfo.packageSeries || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">套餐总流量</div>
|
||||
<div class="stat-value">{{ cardInfo.packageTotalFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">已使用流量</div>
|
||||
<div class="stat-value">{{ cardInfo.usedFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">已使用流量(真)</div>
|
||||
<div class="stat-value">{{ cardInfo.realUsedFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">实际流量</div>
|
||||
<div class="stat-value">{{ cardInfo.actualFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">剩余流量</div>
|
||||
<div class="stat-value">{{ cardInfo.remainFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">已使用流量百分比</div>
|
||||
<div class="stat-value">{{ cardInfo.usedFlowPercentage || '未设置' }}</div>
|
||||
</ElCard>
|
||||
<div class="traffic-overview">
|
||||
<!-- 流量进度条 -->
|
||||
<div class="traffic-progress-section">
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-bar-wrapper">
|
||||
<div class="progress-info">
|
||||
<span class="progress-label">流量使用情况</span>
|
||||
<span class="progress-percentage">{{
|
||||
cardInfo.usedFlowPercentage || '0.00%'
|
||||
}}</span>
|
||||
</div>
|
||||
<ElProgress
|
||||
:percentage="parseFloat(cardInfo.usedFlowPercentage) || 0"
|
||||
:color="getProgressColor(parseFloat(cardInfo.usedFlowPercentage) || 0)"
|
||||
:stroke-width="20"
|
||||
:show-text="false"
|
||||
/>
|
||||
<div class="progress-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总流量</span>
|
||||
<span class="stat-value">{{ cardInfo.packageTotalFlow || '0.00MB' }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">已使用</span>
|
||||
<span class="stat-value used">{{ cardInfo.usedFlow || '0.00MB' }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">剩余</span>
|
||||
<span class="stat-value remaining">{{
|
||||
cardInfo.remainFlow || '0.00MB'
|
||||
}}</span>
|
||||
</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>
|
||||
@@ -144,11 +221,6 @@
|
||||
<ElDescriptionsItem label="运营商实名">{{
|
||||
cardInfo?.operatorRealName || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="国政通实名">
|
||||
<ElTag :type="cardInfo?.realNameAuth ? 'success' : 'danger'" size="small">
|
||||
{{ cardInfo?.realNameAuth ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="供应商">{{
|
||||
cardInfo?.supplier || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
@@ -165,19 +237,19 @@
|
||||
cardInfo?.walletPasswordStatus || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入时间">{{
|
||||
cardInfo?.importTime || '--'
|
||||
formatDateTime(cardInfo?.importTime)
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三行:当前套餐 -->
|
||||
<!-- 第三行:套餐列表 -->
|
||||
<div class="row full-width">
|
||||
<ElCard shadow="never" class="info-card package-info">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>当前套餐</span>
|
||||
<span>套餐列表</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="package-table-wrapper">
|
||||
@@ -204,124 +276,6 @@
|
||||
</div>
|
||||
</ElCard>
|
||||
</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>
|
||||
|
||||
@@ -343,17 +297,27 @@
|
||||
ElSkeleton,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElMessageBox
|
||||
ElMessageBox,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem
|
||||
} from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
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' })
|
||||
|
||||
const route = useRoute()
|
||||
const loading = 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搜索相关
|
||||
const searchIccid = ref('')
|
||||
@@ -438,7 +402,7 @@
|
||||
usedFlowPercentage:
|
||||
data.data_usage_mb > 0
|
||||
? `${((data.current_month_usage_mb / data.data_usage_mb) * 100).toFixed(2)}%`
|
||||
: '未设置',
|
||||
: '0 %',
|
||||
realUsedFlow: formatDataSize(data.current_month_usage_mb || 0),
|
||||
actualFlow: formatDataSize(data.current_month_usage_mb || 0),
|
||||
packageList: data.packages || []
|
||||
@@ -521,50 +485,144 @@
|
||||
// 模拟卡片数据(保留作为参考)
|
||||
const mockCardData = {
|
||||
id: 1, // 卡片ID
|
||||
iccid: '8986062357007989203',
|
||||
accessNumber: '1440012345678',
|
||||
imei: '860123456789012',
|
||||
expireTime: '2025-12-31',
|
||||
operator: '中国联通',
|
||||
cardStatus: '正常',
|
||||
cardType: '流量卡',
|
||||
supplier: '华为技术有限公司',
|
||||
importTime: '2024-01-15 10:30:00',
|
||||
phoneBind: '138****5678',
|
||||
trafficPool: '全国流量池',
|
||||
agent: '张丽丽',
|
||||
operatorStatus: '激活',
|
||||
operatorRealName: '已实名',
|
||||
walletBalance: '50.00元',
|
||||
walletPasswordStatus: '已设置',
|
||||
iccid: '',
|
||||
accessNumber: '',
|
||||
imei: '',
|
||||
expireTime: '',
|
||||
operator: '',
|
||||
cardStatus: '',
|
||||
cardType: '',
|
||||
supplier: '',
|
||||
importTime: '',
|
||||
phoneBind: '',
|
||||
trafficPool: '',
|
||||
agent: '',
|
||||
operatorStatus: '',
|
||||
operatorRealName: '',
|
||||
walletBalance: '',
|
||||
walletPasswordStatus: '',
|
||||
realNameAuth: true,
|
||||
virtualNumber: '10655****1234',
|
||||
virtualNumber: '',
|
||||
// 流量信息 - 根据提供的数据更新
|
||||
packageSeries: 'UFI设备',
|
||||
packageTotalFlow: '3072000MB', // 套餐总流量
|
||||
usedFlow: '196.16MB', // 已使用流量
|
||||
remainFlow: '3071803.84MB', // 剩余流量
|
||||
usedFlowPercentage: '未设置', // 增加已使用流量百分比
|
||||
realUsedFlow: '196.16MB', // 已使用流量(真)
|
||||
actualFlow: '196.16MB', // 实际流量
|
||||
packageList: [
|
||||
{
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
packageType: '年卡套餐',
|
||||
totalFlow: '3072000MB',
|
||||
usedFlow: '196.16MB',
|
||||
remainFlow: '3071803.84MB',
|
||||
expireTime: '2026-11-07',
|
||||
status: '正常'
|
||||
}
|
||||
]
|
||||
packageSeries: '',
|
||||
packageTotalFlow: '', // 套餐总流量
|
||||
usedFlow: '', // 已使用流量
|
||||
remainFlow: '', // 剩余流量
|
||||
usedFlowPercentage: '', // 增加已使用流量百分比
|
||||
realUsedFlow: '', // 已使用流量(真)
|
||||
actualFlow: '', // 实际流量
|
||||
packageList: []
|
||||
}
|
||||
|
||||
// 页面初始化 - 不自动加载数据,等待用户输入ICCID查询
|
||||
// 页面初始化 - 检查URL参数自动加载
|
||||
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) => {
|
||||
switch (status) {
|
||||
@@ -713,20 +771,44 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.single-card-page {
|
||||
padding: 20px 0;
|
||||
padding: 20px;
|
||||
|
||||
// 占位符
|
||||
.search-card-placeholder {
|
||||
height: 140px; // 大约等于搜索卡片的高度
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
// ICCID搜索卡片
|
||||
.search-card {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
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) {
|
||||
padding: 16px 20px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.iccid-search {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
overflow: visible;
|
||||
|
||||
.iccid-input-wrapper {
|
||||
@@ -739,6 +821,7 @@
|
||||
top: calc(100% + 12px);
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
|
||||
// 三角箭头 - 上边框样式
|
||||
.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;
|
||||
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 {
|
||||
font-size: 18px;
|
||||
color: #667eea;
|
||||
@@ -889,130 +1012,106 @@
|
||||
.traffic-info {
|
||||
// 流量概览
|
||||
.traffic-overview {
|
||||
&.horizontal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
// 流量进度条区域
|
||||
.traffic-progress-section {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
}
|
||||
flex-direction: column;
|
||||
|
||||
// 左侧:流量统计网格
|
||||
.traffic-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.traffic-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
.progress-bar-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.stat-card {
|
||||
.progress-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 16px;
|
||||
justify-content: space-between;
|
||||
|
||||
: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;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-regular);
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: var(--el-text-color-primary);
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
// 额外流量信息
|
||||
.extra-traffic-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.usage-display {
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
background: var(--el-bg-color, #fff);
|
||||
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;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
background: var(--el-bg-color, #fff);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 12px;
|
||||
|
||||
.package-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
.label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.package-name {
|
||||
margin-bottom: 8px;
|
||||
.value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
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,
|
||||
.empty-state {
|
||||
@@ -1115,51 +1169,14 @@
|
||||
}
|
||||
|
||||
.traffic-info .traffic-overview {
|
||||
&.horizontal {
|
||||
.progress-stats {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.traffic-left .traffic-stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
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 {
|
||||
padding: 16px;
|
||||
|
||||
.usage-chart .chart-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-right .package-display {
|
||||
padding: 16px;
|
||||
|
||||
.package-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.package-actual {
|
||||
font-size: 13px;
|
||||
}
|
||||
.extra-traffic-info {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1169,20 +1186,16 @@
|
||||
}
|
||||
|
||||
@media (width <= 480px) {
|
||||
.traffic-left .traffic-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
.traffic-info .traffic-overview {
|
||||
.progress-percentage {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 10px 6px;
|
||||
|
||||
.stat-label {
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.progress-stats .stat-item {
|
||||
padding: 10px;
|
||||
|
||||
.stat-value {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,15 +34,21 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -311,8 +317,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
@@ -323,6 +331,15 @@
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
@@ -1108,4 +1125,8 @@
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,15 +34,21 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -251,8 +257,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
@@ -283,6 +291,15 @@
|
||||
const seriesOptions = ref<SeriesSelectOption[]>([])
|
||||
const searchSeriesOptions = ref<SeriesSelectOption[]>([])
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
package_name: '',
|
||||
@@ -952,6 +969,10 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.package-list-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
@@ -35,15 +35,21 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 套餐系列操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="seriesOperationMenuRef"
|
||||
@@ -245,7 +251,7 @@
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="仅自己" value="self" />
|
||||
<ElOption label="自己+下级" value="self_and_sub" />
|
||||
<!--<ElOption label="自己+下级" value="self_and_sub" />-->
|
||||
</ElSelect>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,8 +402,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
@@ -1112,6 +1120,15 @@
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
}
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -1122,4 +1139,8 @@
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -91,13 +91,14 @@
|
||||
<ElTableColumn label="统计范围" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag size="small" type="warning">
|
||||
{{
|
||||
row.stat_scope === 'self'
|
||||
? '仅自己'
|
||||
: row.stat_scope === 'self_and_sub'
|
||||
? '自己+下级'
|
||||
: '-'
|
||||
}}
|
||||
仅自己
|
||||
<!--{{-->
|
||||
<!-- row.stat_scope === 'self'-->
|
||||
<!-- ? '仅自己'-->
|
||||
<!-- : row.stat_scope === 'self_and_sub'-->
|
||||
<!-- ? '自己+下级'-->
|
||||
<!-- : '-'-->
|
||||
<!--}}-->
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
@@ -35,15 +35,21 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -52,60 +58,443 @@
|
||||
@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
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增代理系列授权' : '编辑代理系列授权'"
|
||||
width="50%"
|
||||
width="60%"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="130px">
|
||||
<!-- 新增模式:基本信息 - 2列布局 -->
|
||||
<div v-if="dialogType === 'add'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="选择套餐系列" prop="series_id">
|
||||
<!-- 新增模式:分步骤表单 -->
|
||||
<template v-if="dialogType === 'add'">
|
||||
<!-- 步骤条 -->
|
||||
<ElSteps
|
||||
:active="currentStep"
|
||||
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
|
||||
v-model="form.series_id"
|
||||
placeholder="请选择套餐系列"
|
||||
v-model="selectedPackageIds"
|
||||
placeholder="请选择套餐"
|
||||
style="width: 100%"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchSeries"
|
||||
:loading="seriesLoading"
|
||||
:remote-method="searchPackages"
|
||||
:loading="packageLoading"
|
||||
clearable
|
||||
>
|
||||
<template
|
||||
v-if="packageOptions.length === 0 && !packageLoading && form.series_id"
|
||||
>
|
||||
<ElOption disabled value="" label="该系列没有可选套餐" />
|
||||
</template>
|
||||
<ElOption
|
||||
v-for="series in seriesOptions"
|
||||
:key="series.id"
|
||||
:label="`${series.series_name} (${series.series_code})`"
|
||||
:value="series.id"
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.id"
|
||||
:label="pkg.package_name"
|
||||
:value="pkg.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="form-tip">选择该授权下包含的套餐</div>
|
||||
</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">
|
||||
<span class="info-label">系列名称:</span>
|
||||
<span class="info-value">{{ form.series_name || '-' }}</span>
|
||||
@@ -189,15 +578,7 @@
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="统计范围" width="140">
|
||||
<template #default="{ row }">
|
||||
<ElTag size="small" type="warning">
|
||||
{{
|
||||
row.stat_scope === 'self'
|
||||
? '仅自己'
|
||||
: row.stat_scope === 'self_and_sub'
|
||||
? '自己+下级'
|
||||
: '-'
|
||||
}}
|
||||
</ElTag>
|
||||
<ElTag size="small" type="warning"> 仅自己 </ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金金额(元)" min-width="180">
|
||||
@@ -271,13 +652,13 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 套餐配置 -->
|
||||
<div class="form-section-title">
|
||||
<!-- 套餐配置(编辑模式下禁用) -->
|
||||
<!-- <div class="form-section-title">
|
||||
<span class="title-text">套餐配置(可选)</span>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 选择套餐 -->
|
||||
<ElFormItem label="选择套餐">
|
||||
<!-- 编辑模式下不显示套餐配置区域,请使用右键菜单中的"添加授权套餐"功能 -->
|
||||
<!-- <ElFormItem label="选择套餐">
|
||||
<ElSelect
|
||||
v-model="selectedPackageIds"
|
||||
placeholder="请选择套餐"
|
||||
@@ -300,10 +681,10 @@
|
||||
/>
|
||||
</ElSelect>
|
||||
<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
|
||||
v-for="(pkg, index) in form.packages"
|
||||
@@ -329,14 +710,32 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-tip">设置每个套餐的成本价(单位:元),不能低于套餐原始成本价</div>
|
||||
</ElFormItem>
|
||||
</ElFormItem> -->
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
|
||||
提交
|
||||
</ElButton>
|
||||
<template v-if="dialogType === 'add'">
|
||||
<ElButton v-if="currentStep > 0" @click="handlePrevStep">上一步</ElButton>
|
||||
<ElButton @click="dialogVisible = false">取消</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>
|
||||
</template>
|
||||
</ElDialog>
|
||||
@@ -346,7 +745,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { h, ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
ShopSeriesGrantService,
|
||||
@@ -354,20 +753,34 @@
|
||||
ShopService,
|
||||
PackageManageService
|
||||
} 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 {
|
||||
ShopSeriesGrantResponse,
|
||||
PackageSeriesResponse,
|
||||
ShopResponse,
|
||||
PackageResponse,
|
||||
CommissionTier
|
||||
CommissionTier,
|
||||
GrantPackageItem,
|
||||
GrantPackageInfo
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
@@ -392,6 +805,42 @@
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
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 shopOptions = ref<ShopResponse[]>([])
|
||||
const shopTreeData = ref<ShopResponse[]>([])
|
||||
@@ -1277,6 +1726,8 @@
|
||||
const handleDialogClosed = () => {
|
||||
// 清除表单验证状态
|
||||
formRef.value?.clearValidate()
|
||||
// 重置步骤
|
||||
currentStep.value = 0
|
||||
// 重置表单数据
|
||||
form.id = 0
|
||||
form.series_id = undefined
|
||||
@@ -1414,6 +1865,10 @@
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('series_grants:detail')) {
|
||||
items.push({ key: 'view_packages', label: '查看套餐列表' })
|
||||
}
|
||||
|
||||
if (hasAuth('series_grants:edit')) {
|
||||
items.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
@@ -1438,6 +1893,9 @@
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'view_packages':
|
||||
showPackageListDialog(currentRow.value)
|
||||
break
|
||||
case 'edit':
|
||||
showDialog('edit', currentRow.value)
|
||||
break
|
||||
@@ -1446,6 +1904,242 @@
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -1453,6 +2147,10 @@
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1595,4 +2293,17 @@
|
||||
color: var(--el-text-color-primary);
|
||||
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>
|
||||
|
||||
@@ -34,16 +34,22 @@
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="false"
|
||||
:pagination="false"
|
||||
:row-class-name="getRowClassName"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -297,8 +303,10 @@
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { ShopService, RoleService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
@@ -313,6 +321,15 @@
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -1118,4 +1135,8 @@
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,12 +31,27 @@
|
||||
:default-expand-all="false"
|
||||
:marginTop="10"
|
||||
:show-pagination="false"
|
||||
:row-class-name="getRowClassName"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -127,7 +142,11 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import {
|
||||
PermissionType,
|
||||
PERMISSION_TYPE_OPTIONS,
|
||||
@@ -191,10 +210,8 @@
|
||||
{ label: '权限类型', prop: 'perm_type' },
|
||||
{ label: '菜单路径', prop: 'url' },
|
||||
{ label: '适用端口', prop: 'platform' },
|
||||
// { label: '可用角色类型', prop: 'available_for_role_types' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '排序', prop: 'sort' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
{ label: '排序', prop: 'sort' }
|
||||
]
|
||||
|
||||
// 权限列表(树形结构)
|
||||
@@ -208,6 +225,16 @@
|
||||
const currentRow = ref<PermissionTreeNode | null>(null)
|
||||
const currentPermissionId = ref<number>(0)
|
||||
const submitLoading = ref(false)
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 表单引用和数据
|
||||
const formRef = ref<FormInstance>()
|
||||
@@ -315,35 +342,6 @@
|
||||
prop: 'sort',
|
||||
label: '排序',
|
||||
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(() => {
|
||||
getPermissionList()
|
||||
})
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('permission:edit')) {
|
||||
items.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
|
||||
if (hasAuth('permission:delete')) {
|
||||
items.push({ key: 'delete', label: '删除' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: PermissionTreeNode, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showDialog('edit', currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
deletePermission(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,15 +32,21 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
@@ -234,8 +240,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||
@@ -267,6 +275,15 @@
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<PlatformRole | null>(null)
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
role_name: '',
|
||||
@@ -1056,6 +1073,10 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
220
update_context_menu.py
Normal file
220
update_context_menu.py
Normal 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()
|
||||
Reference in New Issue
Block a user