This commit is contained in:
@@ -31,30 +31,17 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="160"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '添加账号' : '编辑账号'"
|
||||
@@ -197,10 +184,6 @@
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { AccountService } from '@/api/modules/account'
|
||||
import { RoleService } from '@/api/modules/role'
|
||||
import { ShopService, EnterpriseService } from '@/api/modules'
|
||||
@@ -214,25 +197,13 @@
|
||||
const { hasAuth } = useAuth()
|
||||
const route = useRoute()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const roleDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const roleSubmitLoading = ref(false)
|
||||
const currentAccountId = ref<number>(0)
|
||||
const currentAccountName = ref<string>('')
|
||||
const currentAccountType = ref<number>(0)
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<any | null>(null)
|
||||
const selectedRoles = ref<number[]>([])
|
||||
const allRoles = ref<PlatformRole[]>([])
|
||||
const rolesToAdd = ref<number[]>([])
|
||||
@@ -493,6 +464,37 @@
|
||||
}
|
||||
])
|
||||
|
||||
// 操作列配置
|
||||
const getActions = (row: any) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('account:patch_role')) {
|
||||
actions.push({
|
||||
label: '分配角色',
|
||||
handler: () => showRoleDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('account:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => showDialog('edit', row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('account:delete')) {
|
||||
actions.push({
|
||||
label: '删除',
|
||||
handler: () => deleteAccount(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
@@ -678,7 +680,12 @@
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入账号名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_-]+$/,
|
||||
message: '用户名只能包含字母、数字、下划线和横线',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
@@ -692,7 +699,7 @@
|
||||
user_type: [{ required: true, message: '请选择账号类型', trigger: 'change' }],
|
||||
shop_id: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
validator: (_rule: any, value: any, callback: any) => {
|
||||
if (formData.user_type === 3 && !value) {
|
||||
callback(new Error('代理账号必须关联店铺'))
|
||||
} else {
|
||||
@@ -704,7 +711,7 @@
|
||||
],
|
||||
enterprise_id: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
validator: (_rule: any, value: any, callback: any) => {
|
||||
if (formData.user_type === 4 && !value) {
|
||||
callback(new Error('企业账号必须关联企业'))
|
||||
} else {
|
||||
@@ -832,59 +839,9 @@
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('account:patch_role')) {
|
||||
items.push({ key: 'assignRole', label: '分配角色' })
|
||||
}
|
||||
|
||||
if (hasAuth('account:edit')) {
|
||||
items.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
|
||||
if (hasAuth('account:delete')) {
|
||||
items.push({ key: 'delete', label: '删除' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'assignRole':
|
||||
showRoleDialog(currentRow.value)
|
||||
break
|
||||
case 'edit':
|
||||
showDialog('edit', currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
deleteAccount(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.account-page {
|
||||
// 账号管理页面样式
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -35,26 +35,21 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="160"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增企业客户' : '编辑企业客户'"
|
||||
width="600px"
|
||||
width="40%"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<ElFormItem label="企业编号" prop="enterprise_code">
|
||||
@@ -204,14 +199,6 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 企业客户操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="enterpriseOperationMenuRef"
|
||||
:menu-items="enterpriseOperationMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleEnterpriseOperationMenuSelect"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -228,12 +215,7 @@
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { regionData } from '@/utils/constants/regionData'
|
||||
|
||||
@@ -244,15 +226,6 @@
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 判断是否是代理账号 (user_type === 3)
|
||||
const isAgentAccount = computed(() => userStore.info.user_type === 3)
|
||||
|
||||
@@ -266,10 +239,6 @@
|
||||
const currentEnterpriseId = ref<number>(0)
|
||||
const shopTreeData = ref<ShopResponse[]>([])
|
||||
|
||||
// 右键菜单
|
||||
const enterpriseOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentOperatingEnterprise = ref<EnterpriseItem | null>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
enterprise_name: '',
|
||||
@@ -414,7 +383,7 @@
|
||||
{
|
||||
prop: 'enterprise_code',
|
||||
label: '企业编号',
|
||||
minWidth: 150,
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
@@ -482,47 +451,56 @@
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: EnterpriseItem) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
formatter: (row: EnterpriseItem) => {
|
||||
const buttons = []
|
||||
|
||||
if (hasAuth('enterprise_customer:look_customer')) {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '账号列表',
|
||||
onClick: () => viewCustomerAccounts(row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:card_authorization')) {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '卡授权',
|
||||
onClick: () => manageCards(row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:device_authorization')) {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '设备授权',
|
||||
onClick: () => manageDevices(row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 操作列配置
|
||||
const getActions = (row: EnterpriseItem) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('enterprise_customer:look_customer')) {
|
||||
actions.push({
|
||||
label: '账号列表',
|
||||
handler: () => viewCustomerAccounts(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:card_authorization')) {
|
||||
actions.push({
|
||||
label: '卡授权',
|
||||
handler: () => manageCards(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:device_authorization')) {
|
||||
actions.push({
|
||||
label: '设备授权',
|
||||
handler: () => manageDevices(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => showDialog('edit', row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:update_pwd')) {
|
||||
actions.push({
|
||||
label: '修改密码',
|
||||
handler: () => showPasswordDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
// 只有非代理账号才需要加载店铺列表
|
||||
@@ -840,59 +818,6 @@
|
||||
}
|
||||
|
||||
// 企业客户操作菜单项配置
|
||||
const enterpriseOperationMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('enterprise_customer:edit')) {
|
||||
items.push({
|
||||
key: 'edit',
|
||||
label: '编辑'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:update_pwd')) {
|
||||
items.push({
|
||||
key: 'updatePassword',
|
||||
label: '修改密码'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 显示企业客户操作右键菜单
|
||||
const showEnterpriseOperationMenu = (e: MouseEvent, row: EnterpriseItem) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
currentOperatingEnterprise.value = row
|
||||
enterpriseOperationMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 处理企业客户操作菜单选择
|
||||
const handleEnterpriseOperationMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentOperatingEnterprise.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showDialog('edit', currentOperatingEnterprise.value)
|
||||
break
|
||||
case 'updatePassword':
|
||||
showPasswordDialog(currentOperatingEnterprise.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: EnterpriseItem, column: any, event: MouseEvent) => {
|
||||
// 如果用户有编辑或修改密码权限,显示右键菜单
|
||||
if (hasAuth('enterprise_customer:edit') || hasAuth('enterprise_customer:update_pwd')) {
|
||||
showEnterpriseOperationMenu(event, row)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -60,10 +60,31 @@
|
||||
<ElCard shadow="never" class="info-card basic-info">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ cardInfo?.asset_type === 'card' ? '卡信息' : '设备信息' }}</span>
|
||||
<ElTag :type="cardInfo?.asset_type === 'card' ? 'success' : 'primary'" size="small">
|
||||
{{ cardInfo?.asset_type === 'card' ? 'IoT卡' : '设备' }}
|
||||
</ElTag>
|
||||
<div class="card-header-left">
|
||||
<span>{{ cardInfo?.asset_type === 'card' ? '卡信息' : '设备信息' }}</span>
|
||||
<ElTag
|
||||
:type="cardInfo?.asset_type === 'card' ? 'success' : 'primary'"
|
||||
size="small"
|
||||
>
|
||||
{{ cardInfo?.asset_type === 'card' ? 'IoT卡' : '设备' }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="card-header-right">
|
||||
<ElTooltip content="开启后系统将自动轮询更新资产状态" placement="top">
|
||||
<div class="polling-switch-wrapper">
|
||||
<span class="polling-label">自动轮询</span>
|
||||
<ElSwitch
|
||||
v-model="pollingEnabled"
|
||||
:loading="pollingLoading"
|
||||
@change="handlePollingStatusChange"
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
inline-prompt
|
||||
size="default"
|
||||
/>
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ElDescriptions :column="4" border>
|
||||
@@ -222,17 +243,17 @@
|
||||
>
|
||||
<ElTableColumn prop="iccid" label="ICCID" />
|
||||
<ElTableColumn prop="msisdn" label="MSISDN" />
|
||||
<ElTableColumn prop="slot_position" label="卡槽位置" align="center">
|
||||
<ElTableColumn prop="slot_position" label="卡槽位置" align="center" width="130">
|
||||
<template #default="scope">
|
||||
<div
|
||||
style="display: flex; gap: 4px; align-items: center; justify-content: center"
|
||||
style="display: flex; gap: 10px; align-items: center; justify-content: center"
|
||||
>
|
||||
<span>{{ scope.row.slot_position }}</span>
|
||||
<ElTag v-if="scope.row.is_current" type="success" size="small">当前</ElTag>
|
||||
<span>SIM-{{ scope.row.slot_position }}</span>
|
||||
<ElTag v-if="scope.row.is_current" type="primary" size="small">当前</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="网络状态" align="center">
|
||||
<ElTableColumn label="网络状态" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag
|
||||
:type="scope.row.network_status === 1 ? 'success' : 'danger'"
|
||||
@@ -242,14 +263,14 @@
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="实名状态" align="center">
|
||||
<ElTableColumn label="实名状态" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getRealNameStatusType(scope.row.real_name_status)" size="small">
|
||||
{{ getRealNameStatusName(scope.row.real_name_status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="280" align="center">
|
||||
<ElTableColumn label="操作" width="200" align="center">
|
||||
<template #default="scope">
|
||||
<!-- 根据网络状态显示启用或停用按钮 -->
|
||||
<ElButton
|
||||
@@ -575,10 +596,9 @@
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="package-actions">
|
||||
<ElButton type="primary">套餐充值</ElButton>
|
||||
<ElButton type="primary" @click="showRechargeDialog">套餐充值</ElButton>
|
||||
<ElButton type="warning">更改过期时间</ElButton>
|
||||
<ElButton type="info">往期订单</ElButton>
|
||||
<ElButton type="success">增减流量</ElButton>
|
||||
<ElButton type="info" @click="showOrderHistoryDialog">往期订单</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- 流量使用进度条 -->
|
||||
@@ -1091,6 +1111,92 @@
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 套餐充值对话框 -->
|
||||
<ElDialog
|
||||
v-model="rechargeDialogVisible"
|
||||
title="套餐充值"
|
||||
width="600px"
|
||||
@closed="handleRechargeDialogClosed"
|
||||
>
|
||||
<ElForm
|
||||
ref="rechargeFormRef"
|
||||
:model="rechargeForm"
|
||||
:rules="rechargeRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="资产信息">
|
||||
<span style="font-weight: bold; color: #409eff">
|
||||
{{ cardInfo?.asset_type === 'card' ? cardInfo?.iccid : cardInfo?.imei }} ({{
|
||||
cardInfo?.virtual_no
|
||||
}})
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="选择套餐" prop="package_ids">
|
||||
<ElSelect
|
||||
v-model="rechargeForm.package_ids"
|
||||
placeholder="请选择套餐"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="handleRechargePackageSearch"
|
||||
:loading="packageSearchLoading"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.id"
|
||||
:label="`${pkg.package_name} (¥${(pkg.cost_price / 100).toFixed(2)})`"
|
||||
:value="pkg.id"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ pkg.package_name }}</span>
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
¥{{ (pkg.cost_price / 100).toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="支付方式" prop="payment_method">
|
||||
<ElSelect
|
||||
v-model="rechargeForm.payment_method"
|
||||
placeholder="请选择支付方式"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="钱包支付" value="wallet" />
|
||||
<!-- 只有平台用户(user_type 1:超级管理员, 2:平台用户)才显示线下支付选项 -->
|
||||
<ElOption
|
||||
v-if="userStore.info.user_type === 1 || userStore.info.user_type === 2"
|
||||
label="线下支付"
|
||||
value="offline"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
<template v-if="rechargeForm.payment_method === 'wallet'">
|
||||
提示: 使用钱包支付时,订单将直接完成
|
||||
</template>
|
||||
<template v-else-if="rechargeForm.payment_method === 'offline'">
|
||||
提示: 线下支付订单需要手动确认支付
|
||||
</template>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="rechargeDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleRechargeOrder(rechargeFormRef)"
|
||||
:loading="rechargeLoading"
|
||||
>
|
||||
确认充值
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 流量详单对话框 -->
|
||||
<ElDialog
|
||||
v-model="dailyRecordsDialogVisible"
|
||||
@@ -1183,6 +1289,142 @@
|
||||
<ElEmpty description="暂无流量详单数据" />
|
||||
</div>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 往期订单对话框 -->
|
||||
<ElDialog v-model="orderHistoryDialogVisible" title="往期订单" width="60%" destroy-on-close>
|
||||
<div v-if="orderHistoryLoading" style="padding: 40px 0; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
</div>
|
||||
<div v-else-if="orderHistoryData">
|
||||
<!-- 当前代订单 -->
|
||||
<div class="generation-section">
|
||||
<h3 style="margin-bottom: 16px">
|
||||
当前代 ({{ orderHistoryData.current_generation.identifier }})
|
||||
</h3>
|
||||
<ElTable :data="orderHistoryData.current_generation.items" border>
|
||||
<ElTableColumn prop="order_no" label="订单号" width="200" />
|
||||
<ElTableColumn prop="order_type" label="订单类型" width="120" />
|
||||
<ElTableColumn prop="payment_status_text" label="支付状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getPaymentStatusType(row.payment_status)">
|
||||
{{ row.payment_status_text }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="total_amount" label="总金额" width="120">
|
||||
<template #default="{ row }"> ¥{{ (row.total_amount / 100).toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="payment_method" label="支付方式" width="120" />
|
||||
<ElTableColumn prop="paid_at" label="支付时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.paid_at ? formatDateTime(row.paid_at) : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="订单项" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, idx) in row.items" :key="idx" style="margin-bottom: 4px">
|
||||
{{ item.package_name }} × {{ item.quantity }}
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created_at" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="margin-top: 16px; text-align: right">
|
||||
<ElPagination
|
||||
v-model:current-page="orderHistoryPagination.page"
|
||||
v-model:page-size="orderHistoryPagination.page_size"
|
||||
:total="orderHistoryData.current_generation.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleOrderHistorySizeChange"
|
||||
@current-change="handleOrderHistoryPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 前代订单 -->
|
||||
<div
|
||||
v-if="
|
||||
orderHistoryData.previous_generations &&
|
||||
orderHistoryData.previous_generations.length > 0
|
||||
"
|
||||
style="margin-top: 32px"
|
||||
>
|
||||
<div
|
||||
v-for="gen in orderHistoryData.previous_generations"
|
||||
:key="gen.generation"
|
||||
class="generation-section"
|
||||
style="margin-bottom: 24px"
|
||||
>
|
||||
<h3 style="margin-bottom: 16px">
|
||||
第{{ gen.generation }}代 ({{ gen.identifier }})
|
||||
<span
|
||||
v-if="gen.exchange_no"
|
||||
style="margin-left: 12px; font-size: 14px; color: #909399"
|
||||
>
|
||||
换货单号: {{ gen.exchange_no }}
|
||||
</span>
|
||||
<span
|
||||
v-if="gen.exchanged_at"
|
||||
style="margin-left: 12px; font-size: 14px; color: #909399"
|
||||
>
|
||||
换货时间: {{ formatDateTime(gen.exchanged_at) }}
|
||||
</span>
|
||||
</h3>
|
||||
<ElTable :data="gen.items" border>
|
||||
<ElTableColumn prop="order_no" label="订单号" width="200" />
|
||||
<ElTableColumn prop="order_type" label="订单类型" width="120" />
|
||||
<ElTableColumn prop="payment_status_text" label="支付状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getPaymentStatusType(row.payment_status)">
|
||||
{{ row.payment_status_text }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="total_amount" label="总金额" width="120">
|
||||
<template #default="{ row }"> ¥{{ (row.total_amount / 100).toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="payment_method" label="支付方式" width="120" />
|
||||
<ElTableColumn prop="paid_at" label="支付时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.paid_at ? formatDateTime(row.paid_at) : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="订单项" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, idx) in row.items" :key="idx" style="margin-bottom: 4px">
|
||||
{{ item.package_name }} × {{ item.quantity }}
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created_at" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<ElAlert
|
||||
v-if="orderHistoryData.truncated"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
style="margin-top: 16px"
|
||||
>
|
||||
换货链超过10代,仅显示最近10代的订单记录
|
||||
</ElAlert>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="padding: 40px 0; text-align: center">
|
||||
<ElEmpty description="暂无订单数据" />
|
||||
</div>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1220,18 +1462,29 @@
|
||||
import { Loading, Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { EnterpriseService } from '@/api/modules/enterprise'
|
||||
import { CardService, AssetService, DeviceService } from '@/api/modules'
|
||||
import {
|
||||
CardService,
|
||||
AssetService,
|
||||
DeviceService,
|
||||
OrderService,
|
||||
PackageManageService
|
||||
} from '@/api/modules'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type {
|
||||
AssetWalletTransactionItem,
|
||||
TransactionType,
|
||||
AssetWalletResponse
|
||||
AssetWalletResponse,
|
||||
CreateOrderRequest,
|
||||
PackageResponse,
|
||||
PurchaseCheckRequest
|
||||
} from '@/types/api'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'SingleCard' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const loading = ref(false)
|
||||
const operationLoading = ref(false)
|
||||
const refreshLoading = ref(false)
|
||||
@@ -1243,6 +1496,10 @@
|
||||
const disableCardLoading = ref(false)
|
||||
const manualDeactivateCardLoading = ref(false)
|
||||
|
||||
// 轮询状态
|
||||
const pollingEnabled = ref(false)
|
||||
const pollingLoading = ref(false)
|
||||
|
||||
// 设备操作loading
|
||||
const rebootDeviceLoading = ref(false)
|
||||
const resetDeviceLoading = ref(false)
|
||||
@@ -1295,6 +1552,21 @@
|
||||
]
|
||||
})
|
||||
|
||||
// 套餐充值相关
|
||||
const rechargeDialogVisible = ref(false)
|
||||
const rechargeLoading = ref(false)
|
||||
const rechargeFormRef = ref<FormInstance>()
|
||||
const rechargeForm = reactive({
|
||||
package_ids: [] as number[],
|
||||
payment_method: 'wallet' as 'wallet' | 'offline'
|
||||
})
|
||||
const rechargeRules: FormRules = {
|
||||
package_ids: [{ required: true, message: '请选择套餐', trigger: 'change' }],
|
||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }]
|
||||
}
|
||||
const packageOptions = ref<PackageResponse[]>([])
|
||||
const packageSearchLoading = ref(false)
|
||||
|
||||
// 流量详单相关
|
||||
const dailyRecordsDialogVisible = ref(false)
|
||||
const dailyRecordsLoading = ref(false)
|
||||
@@ -1304,6 +1576,15 @@
|
||||
const selectedMonth = ref<string>('')
|
||||
const filteredRecords = ref<any[]>([])
|
||||
|
||||
// 往期订单相关
|
||||
const orderHistoryDialogVisible = ref(false)
|
||||
const orderHistoryLoading = ref(false)
|
||||
const orderHistoryData = ref<any>(null)
|
||||
const orderHistoryPagination = reactive({
|
||||
page: 1,
|
||||
page_size: 20
|
||||
})
|
||||
|
||||
// ICCID搜索相关
|
||||
const searchIccid = ref('')
|
||||
const iccidInputFocused = ref(false)
|
||||
@@ -1443,15 +1724,19 @@
|
||||
packageList: [] // 套餐列表需要单独调用API获取
|
||||
}
|
||||
|
||||
// 如果有asset_id和asset_type,并行加载其他数据
|
||||
if (data.asset_type && data.asset_id) {
|
||||
// 设置轮询状态
|
||||
pollingEnabled.value = data.enable_polling || false
|
||||
|
||||
// 获取资产标识符并加载其他数据
|
||||
const identifier = data.asset_type === 'card' ? data.iccid : data.virtual_no
|
||||
if (identifier) {
|
||||
// 并行调用5个接口: 当前生效套餐、套餐列表、实时状态、钱包概况、钱包流水
|
||||
Promise.all([
|
||||
loadCurrentPackage(data.asset_type, data.asset_id),
|
||||
loadPackageList(data.asset_type, data.asset_id),
|
||||
loadRealtimeStatus(data.asset_type, data.asset_id),
|
||||
loadAssetWallet(data.asset_type, data.asset_id),
|
||||
loadWalletTransactions(data.asset_type, data.asset_id)
|
||||
loadCurrentPackage(identifier),
|
||||
loadPackageList(identifier),
|
||||
loadRealtimeStatus(identifier, data.asset_type),
|
||||
loadAssetWallet(identifier),
|
||||
loadWalletTransactions(identifier)
|
||||
])
|
||||
}
|
||||
} else {
|
||||
@@ -1474,9 +1759,9 @@
|
||||
}
|
||||
|
||||
// 加载套餐列表
|
||||
const loadPackageList = async (assetType: string, assetId: number) => {
|
||||
const loadPackageList = async (identifier: string) => {
|
||||
try {
|
||||
const response = await AssetService.getAssetPackages(assetType, assetId, {
|
||||
const response = await AssetService.getAssetPackages(identifier, {
|
||||
page: 1,
|
||||
page_size: 50
|
||||
})
|
||||
@@ -1490,12 +1775,12 @@
|
||||
}
|
||||
|
||||
// 加载当前生效套餐
|
||||
const loadCurrentPackage = async (assetType: string, assetId: number) => {
|
||||
const loadCurrentPackage = async (identifier: string) => {
|
||||
try {
|
||||
// 清空之前的错误信息
|
||||
currentPackageErrorMsg.value = ''
|
||||
|
||||
const response = await AssetService.getCurrentPackage(assetType, assetId)
|
||||
const response = await AssetService.getCurrentPackage(identifier)
|
||||
if (response.code === 0) {
|
||||
if (response.data) {
|
||||
const pkg = response.data
|
||||
@@ -1549,9 +1834,9 @@
|
||||
}
|
||||
|
||||
// 加载实时状态
|
||||
const loadRealtimeStatus = async (assetType: string, assetId: number) => {
|
||||
const loadRealtimeStatus = async (identifier: string, assetType: string) => {
|
||||
try {
|
||||
const response = await AssetService.getRealtimeStatus(assetType, assetId)
|
||||
const response = await AssetService.getRealtimeStatus(identifier)
|
||||
if (response.code === 0 && response.data) {
|
||||
const data = response.data
|
||||
// 更新实时状态数据
|
||||
@@ -1605,9 +1890,9 @@
|
||||
}
|
||||
|
||||
// 加载钱包概况
|
||||
const loadAssetWallet = async (assetType: string, assetId: number) => {
|
||||
const loadAssetWallet = async (identifier: string) => {
|
||||
try {
|
||||
const response = await AssetService.getAssetWallet(assetType, assetId)
|
||||
const response = await AssetService.getAssetWallet(identifier)
|
||||
if (response.code === 0 && response.data) {
|
||||
walletInfo.value = response.data
|
||||
}
|
||||
@@ -1985,12 +2270,14 @@
|
||||
|
||||
try {
|
||||
refreshLoading.value = true
|
||||
await AssetService.refreshAsset(cardInfo.value.asset_type, cardInfo.value.asset_id)
|
||||
const identifier =
|
||||
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
|
||||
await AssetService.refreshAsset(identifier)
|
||||
ElMessage.success('刷新成功')
|
||||
|
||||
// 刷新成功后重新加载实时状态数据
|
||||
if (cardInfo.value.asset_type && cardInfo.value.asset_id) {
|
||||
await loadRealtimeStatus(cardInfo.value.asset_type, cardInfo.value.asset_id)
|
||||
if (identifier) {
|
||||
await loadRealtimeStatus(identifier, cardInfo.value.asset_type)
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 检查429错误(冷却期)
|
||||
@@ -2004,6 +2291,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 处理轮询状态变更
|
||||
const handlePollingStatusChange = async (value: boolean) => {
|
||||
if (!cardInfo.value) {
|
||||
ElMessage.warning('无法更新轮询状态:缺少资产信息')
|
||||
// 恢复原状态
|
||||
pollingEnabled.value = !value
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
pollingLoading.value = true
|
||||
const identifier =
|
||||
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
|
||||
|
||||
const response = await AssetService.updatePollingStatus(identifier, {
|
||||
enable_polling: value
|
||||
})
|
||||
|
||||
if (response.code === 0) {
|
||||
ElMessage.success(value ? '已开启自动轮询' : '已关闭自动轮询')
|
||||
// 更新本地状态
|
||||
if (cardInfo.value) {
|
||||
cardInfo.value.enable_polling = value
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(response.msg || '更新轮询状态失败')
|
||||
// 恢复原状态
|
||||
pollingEnabled.value = !value
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('更新轮询状态失败:', error)
|
||||
ElMessage.error(error?.message || '更新轮询状态失败')
|
||||
// 恢复原状态
|
||||
pollingEnabled.value = !value
|
||||
} finally {
|
||||
pollingLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleOperation = async (operation: string) => {
|
||||
if (!cardInfo.value) {
|
||||
@@ -2072,7 +2398,7 @@
|
||||
)
|
||||
|
||||
stopLoading.value = true
|
||||
await AssetService.stopCard(cardInfo.value.iccid)
|
||||
await AssetService.stopAsset(cardInfo.value.iccid)
|
||||
ElMessage.success('停机操作成功')
|
||||
|
||||
// 刷新卡片信息
|
||||
@@ -2111,7 +2437,7 @@
|
||||
)
|
||||
|
||||
startLoading.value = true
|
||||
await AssetService.startCard(cardInfo.value.iccid)
|
||||
await AssetService.startAsset(cardInfo.value.iccid)
|
||||
ElMessage.success('复机操作成功')
|
||||
|
||||
// 刷新卡片信息
|
||||
@@ -2150,7 +2476,7 @@
|
||||
)
|
||||
|
||||
stopLoading.value = true
|
||||
const response = await AssetService.stopDevice(cardInfo.value.asset_id)
|
||||
const response = await AssetService.stopAsset(cardInfo.value.virtual_no)
|
||||
|
||||
// 显示停机结果
|
||||
if (response.code === 0 && response.data) {
|
||||
@@ -2201,7 +2527,7 @@
|
||||
)
|
||||
|
||||
startLoading.value = true
|
||||
await AssetService.startDevice(cardInfo.value.asset_id)
|
||||
await AssetService.startAsset(cardInfo.value.virtual_no)
|
||||
ElMessage.success('设备复机操作成功')
|
||||
|
||||
// 刷新设备信息
|
||||
@@ -2220,14 +2546,10 @@
|
||||
// ========== 钱包流水相关函数 ==========
|
||||
|
||||
// 加载钱包流水列表
|
||||
const loadWalletTransactions = async (assetType: string, assetId: number) => {
|
||||
const loadWalletTransactions = async (identifier: string) => {
|
||||
try {
|
||||
walletLoading.value = true
|
||||
const response = await AssetService.getWalletTransactions(
|
||||
assetType,
|
||||
assetId,
|
||||
walletQueryParams.value
|
||||
)
|
||||
const response = await AssetService.getWalletTransactions(identifier, walletQueryParams.value)
|
||||
if (response.code === 0 && response.data) {
|
||||
walletTransactionList.value = response.data.list || []
|
||||
walletTransactionTotal.value = response.data.total || 0
|
||||
@@ -2263,8 +2585,10 @@
|
||||
// 处理筛选条件变化
|
||||
const handleWalletFilterChange = () => {
|
||||
walletQueryParams.value.page = 1 // 重置到第一页
|
||||
if (cardInfo.value?.asset_type && cardInfo.value?.asset_id) {
|
||||
loadWalletTransactions(cardInfo.value.asset_type, cardInfo.value.asset_id)
|
||||
const identifier =
|
||||
cardInfo.value?.asset_type === 'card' ? cardInfo.value?.iccid : cardInfo.value?.virtual_no
|
||||
if (identifier) {
|
||||
loadWalletTransactions(identifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2290,8 +2614,10 @@
|
||||
end_time: null
|
||||
}
|
||||
walletDateRange.value = null
|
||||
if (cardInfo.value?.asset_type && cardInfo.value?.asset_id) {
|
||||
loadWalletTransactions(cardInfo.value.asset_type, cardInfo.value.asset_id)
|
||||
const identifier =
|
||||
cardInfo.value?.asset_type === 'card' ? cardInfo.value?.iccid : cardInfo.value?.virtual_no
|
||||
if (identifier) {
|
||||
loadWalletTransactions(identifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2299,16 +2625,20 @@
|
||||
const handleWalletPageSizeChange = (size: number) => {
|
||||
walletQueryParams.value.page_size = size
|
||||
walletQueryParams.value.page = 1 // 重置到第一页
|
||||
if (cardInfo.value?.asset_type && cardInfo.value?.asset_id) {
|
||||
loadWalletTransactions(cardInfo.value.asset_type, cardInfo.value.asset_id)
|
||||
const identifier =
|
||||
cardInfo.value?.asset_type === 'card' ? cardInfo.value?.iccid : cardInfo.value?.virtual_no
|
||||
if (identifier) {
|
||||
loadWalletTransactions(identifier)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理页码变化
|
||||
const handleWalletPageChange = (page: number) => {
|
||||
walletQueryParams.value.page = page
|
||||
if (cardInfo.value?.asset_type && cardInfo.value?.asset_id) {
|
||||
loadWalletTransactions(cardInfo.value.asset_type, cardInfo.value.asset_id)
|
||||
const identifier =
|
||||
cardInfo.value?.asset_type === 'card' ? cardInfo.value?.iccid : cardInfo.value?.virtual_no
|
||||
if (identifier) {
|
||||
loadWalletTransactions(identifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2335,7 +2665,7 @@
|
||||
})
|
||||
|
||||
enableCardLoading.value = true
|
||||
const res = await CardService.enableIotCard(cardInfo.value.iccid)
|
||||
const res = await AssetService.startAsset(cardInfo.value.iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('启用成功')
|
||||
await handleRefreshAsset()
|
||||
@@ -2358,7 +2688,7 @@
|
||||
})
|
||||
|
||||
disableCardLoading.value = true
|
||||
const res = await CardService.disableIotCard(cardInfo.value.iccid)
|
||||
const res = await AssetService.stopAsset(cardInfo.value.iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('停用成功')
|
||||
await handleRefreshAsset()
|
||||
@@ -2387,7 +2717,7 @@
|
||||
)
|
||||
|
||||
manualDeactivateCardLoading.value = true
|
||||
const res = await CardService.deactivateIotCard(cardInfo.value.asset_id)
|
||||
const res = await AssetService.deactivateAsset(cardInfo.value.iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('手动停用成功')
|
||||
await handleRefreshAsset()
|
||||
@@ -2411,7 +2741,7 @@
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await CardService.enableIotCard(card.iccid)
|
||||
const res = await AssetService.startAsset(card.iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('启用成功')
|
||||
await handleRefreshAsset()
|
||||
@@ -2431,7 +2761,7 @@
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await CardService.disableIotCard(card.iccid)
|
||||
const res = await AssetService.stopAsset(card.iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('停用成功')
|
||||
await handleRefreshAsset()
|
||||
@@ -2457,7 +2787,7 @@
|
||||
}
|
||||
)
|
||||
|
||||
const res = await CardService.deactivateIotCard(card.id)
|
||||
const res = await AssetService.deactivateAsset(card.iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('手动停用成功')
|
||||
await handleRefreshAsset()
|
||||
@@ -2644,6 +2974,221 @@
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 套餐充值 ==========
|
||||
|
||||
// 显示套餐充值对话框
|
||||
const showRechargeDialog = async () => {
|
||||
if (!cardInfo.value) {
|
||||
ElMessage.warning('请先查询资产信息')
|
||||
return
|
||||
}
|
||||
|
||||
rechargeDialogVisible.value = true
|
||||
// 加载可用套餐列表
|
||||
await loadRechargePackages()
|
||||
}
|
||||
|
||||
// 加载可充值套餐列表
|
||||
const loadRechargePackages = async () => {
|
||||
if (!cardInfo.value) return
|
||||
|
||||
try {
|
||||
packageSearchLoading.value = true
|
||||
|
||||
const response = await PackageManageService.getPackages({
|
||||
series_id: cardInfo.value.series_id,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: 1, // 只获取启用的套餐
|
||||
shelf_status: 1 // 只获取已上架的套餐
|
||||
})
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
packageOptions.value = response.data.items || []
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载套餐列表失败:', error)
|
||||
ElMessage.error(error?.message || '加载套餐列表失败')
|
||||
} finally {
|
||||
packageSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索套餐
|
||||
const handleRechargePackageSearch = async (query: string) => {
|
||||
if (!cardInfo.value) return
|
||||
|
||||
try {
|
||||
packageSearchLoading.value = true
|
||||
|
||||
const response = await PackageManageService.getPackages({
|
||||
package_name: query || undefined,
|
||||
series_id: cardInfo.value.series_id,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: 1, // 只获取启用的套餐
|
||||
shelf_status: 1 // 只获取已上架的套餐
|
||||
})
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
packageOptions.value = response.data.items || []
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('搜索套餐失败:', error)
|
||||
} finally {
|
||||
packageSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交充值订单
|
||||
const handleRechargeOrder = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl || !cardInfo.value) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
rechargeLoading.value = true
|
||||
try {
|
||||
// 获取订单类型和资源ID
|
||||
const orderType: 'single_card' | 'device' =
|
||||
cardInfo.value.asset_type === 'card' ? 'single_card' : 'device'
|
||||
const resourceId = cardInfo.value.asset_id
|
||||
const identifier =
|
||||
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
|
||||
|
||||
// 调用套餐购买预检接口
|
||||
const checkData: PurchaseCheckRequest = {
|
||||
order_type: orderType,
|
||||
resource_id: resourceId,
|
||||
package_ids: rechargeForm.package_ids
|
||||
}
|
||||
|
||||
const checkResponse = await OrderService.purchaseCheck(checkData)
|
||||
|
||||
// 检查预检结果
|
||||
if (checkResponse.code === 0 && checkResponse.data) {
|
||||
const checkResult = checkResponse.data
|
||||
|
||||
// 如果需要强充,显示确认对话框
|
||||
if (checkResult.need_force_recharge) {
|
||||
const confirmMessage = `${checkResult.message || '钱包余额不足'}\n\n套餐总价: ¥${(checkResult.total_package_amount! / 100).toFixed(2)}\n需要强充: ¥${(checkResult.force_recharge_amount! / 100).toFixed(2)}\n钱包到账: ¥${(checkResult.wallet_credit! / 100).toFixed(2)}\n实际支付: ¥${(checkResult.actual_payment! / 100).toFixed(2)}\n\n是否继续创建订单?`
|
||||
|
||||
await ElMessageBox.confirm(confirmMessage, '购买预检提示', {
|
||||
confirmButtonText: '继续创建',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 预检通过或用户确认后,创建订单
|
||||
const data: CreateOrderRequest = {
|
||||
identifier,
|
||||
package_ids: rechargeForm.package_ids,
|
||||
payment_method: rechargeForm.payment_method
|
||||
}
|
||||
|
||||
await OrderService.createOrder(data)
|
||||
|
||||
// 根据支付方式显示不同的成功消息
|
||||
if (rechargeForm.payment_method === 'wallet') {
|
||||
ElMessage.success('订单创建成功,已自动完成支付')
|
||||
} else {
|
||||
ElMessage.success('订单创建成功')
|
||||
}
|
||||
|
||||
rechargeDialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
|
||||
// 刷新资产信息
|
||||
if (searchIccid.value) {
|
||||
await fetchCardDetailByIccid(searchIccid.value)
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 用户取消确认对话框
|
||||
if (error === 'cancel') {
|
||||
return
|
||||
}
|
||||
console.error('创建订单失败:', error)
|
||||
} finally {
|
||||
rechargeLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭充值对话框
|
||||
const handleRechargeDialogClosed = () => {
|
||||
rechargeFormRef.value?.resetFields()
|
||||
rechargeForm.package_ids = []
|
||||
rechargeForm.payment_method = 'wallet'
|
||||
packageOptions.value = []
|
||||
}
|
||||
|
||||
// ========== 往期订单 ==========
|
||||
|
||||
// 显示往期订单对话框
|
||||
const showOrderHistoryDialog = async () => {
|
||||
if (!cardInfo.value) {
|
||||
ElMessage.warning('请先查询资产信息')
|
||||
return
|
||||
}
|
||||
|
||||
orderHistoryDialogVisible.value = true
|
||||
orderHistoryPagination.page = 1
|
||||
orderHistoryPagination.page_size = 20
|
||||
await loadOrderHistory()
|
||||
}
|
||||
|
||||
// 加载往期订单
|
||||
const loadOrderHistory = async () => {
|
||||
if (!cardInfo.value) return
|
||||
|
||||
try {
|
||||
orderHistoryLoading.value = true
|
||||
|
||||
// 使用资产标识符(ICCID 或 VirtualNo)
|
||||
const identifier =
|
||||
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
|
||||
|
||||
const response = await AssetService.getAssetOrders(identifier, {
|
||||
page: orderHistoryPagination.page,
|
||||
page_size: orderHistoryPagination.page_size,
|
||||
include_previous: true // 包含换货前代的订单
|
||||
})
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
orderHistoryData.value = response.data
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载往期订单失败:', error)
|
||||
ElMessage.error(error?.message || '加载往期订单失败')
|
||||
} finally {
|
||||
orderHistoryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleOrderHistorySizeChange = (size: number) => {
|
||||
orderHistoryPagination.page_size = size
|
||||
loadOrderHistory()
|
||||
}
|
||||
|
||||
const handleOrderHistoryPageChange = (page: number) => {
|
||||
orderHistoryPagination.page = page
|
||||
loadOrderHistory()
|
||||
}
|
||||
|
||||
// 获取支付状态标签类型
|
||||
const getPaymentStatusType = (status: number): 'success' | 'warning' | 'danger' | 'info' => {
|
||||
const statusMap: Record<number, 'success' | 'warning' | 'danger' | 'info'> = {
|
||||
1: 'warning', // 待支付
|
||||
2: 'success', // 已支付
|
||||
3: 'danger', // 已取消
|
||||
4: 'info' // 已关闭
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 手动停用设备
|
||||
const handleManualDeactivateDevice = async () => {
|
||||
try {
|
||||
@@ -2658,7 +3203,9 @@
|
||||
)
|
||||
|
||||
manualDeactivateDeviceLoading.value = true
|
||||
const res = await DeviceService.deactivateDevice(cardInfo.value.asset_id)
|
||||
const identifier =
|
||||
cardInfo.value.asset_type === 'card' ? cardInfo.value.iccid : cardInfo.value.virtual_no
|
||||
const res = await AssetService.deactivateAsset(identifier)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('手动停用成功')
|
||||
await handleRefreshAsset()
|
||||
@@ -2965,10 +3512,53 @@
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary, #1f2937);
|
||||
|
||||
.card-header-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.polling-switch-wrapper {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
.polling-label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
:deep(.el-switch) {
|
||||
height: 26px;
|
||||
|
||||
.el-switch__core {
|
||||
height: 26px;
|
||||
min-width: 50px;
|
||||
|
||||
.el-switch__inner {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-switch__action {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
@@ -3439,4 +4029,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 往期订单样式
|
||||
.generation-section {
|
||||
padding: 16px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 8px;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -53,13 +53,11 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="180"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
@@ -67,9 +65,6 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 批量分配对话框 -->
|
||||
<ElDialog v-model="allocateDialogVisible" title="批量分配设备" width="600px">
|
||||
<ElForm
|
||||
@@ -284,7 +279,7 @@
|
||||
|
||||
<!-- 设备详情弹窗 -->
|
||||
<ElDialog v-model="deviceDetailDialogVisible" title="设备详情" width="1000px">
|
||||
<div v-if="deviceDetailLoading" style=" padding: 40px 0;text-align: center">
|
||||
<div v-if="deviceDetailLoading" style="padding: 40px 0; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
</div>
|
||||
<div v-else-if="currentDeviceDetail">
|
||||
@@ -296,17 +291,17 @@
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="设备名称">{{
|
||||
currentDeviceDetail.device_name || '--'
|
||||
currentDeviceDetail.device_name || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备型号">{{
|
||||
currentDeviceDetail.device_model || '--'
|
||||
currentDeviceDetail.device_model || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备类型">{{
|
||||
currentDeviceDetail.device_type || '--'
|
||||
currentDeviceDetail.device_type || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="制造商">{{
|
||||
currentDeviceDetail.manufacturer || '--'
|
||||
currentDeviceDetail.manufacturer || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最大插槽数">{{
|
||||
currentDeviceDetail.max_sim_slots
|
||||
@@ -321,11 +316,11 @@
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="批次号" :span="2">{{
|
||||
currentDeviceDetail.batch_no || '--'
|
||||
currentDeviceDetail.batch_no || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="创建时间" :span="3">{{
|
||||
formatDateTime(currentDeviceDetail.created_at) || '--'
|
||||
formatDateTime(currentDeviceDetail.created_at) || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</div>
|
||||
@@ -398,7 +393,7 @@
|
||||
</ElCard>
|
||||
|
||||
<!-- 已绑定卡片列表 -->
|
||||
<div v-if="deviceCardsLoading" style=" padding: 40px 0;text-align: center">
|
||||
<div v-if="deviceCardsLoading" style="padding: 40px 0; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -406,15 +401,15 @@
|
||||
<ElTableColumn prop="slot_position" label="插槽位置" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<div
|
||||
style="display: flex; gap: 4px; align-items: center; justify-content: center"
|
||||
style="display: flex; gap: 10px; align-items: center; justify-content: center"
|
||||
>
|
||||
<span>{{ row.slot_position }}</span>
|
||||
<span>SIM-{{ row.slot_position }}</span>
|
||||
<ElTag v-if="row.is_current" type="success" size="small">当前</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="iccid" label="ICCID" min-width="180" />
|
||||
<ElTableColumn prop="msisdn" label="接入号" width="140" />
|
||||
<ElTableColumn prop="iccid" label="ICCID" min-width="120" />
|
||||
<ElTableColumn prop="msisdn" label="接入号" width="160" />
|
||||
<ElTableColumn prop="network_status" label="网络状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getNetworkStatusTagType(row.network_status)">
|
||||
@@ -424,7 +419,7 @@
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="bind_time" label="绑定时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.bind_time) || '--' }}
|
||||
{{ formatDateTime(row.bind_time) || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="100" fixed="right" align="center">
|
||||
@@ -437,21 +432,13 @@
|
||||
</ElTable>
|
||||
<div
|
||||
v-if="deviceCards.length === 0"
|
||||
style=" padding: 20px; color: #909399;text-align: center"
|
||||
style="padding: 20px; color: #909399; text-align: center"
|
||||
>
|
||||
暂无设备绑定的卡
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 设备操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="deviceOperationMenuRef"
|
||||
:menu-items="deviceOperationMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleDeviceOperationMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 设置限速对话框 -->
|
||||
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="50%">
|
||||
<ElForm
|
||||
@@ -471,7 +458,7 @@
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="上行速率" prop="upload_speed">
|
||||
<ElInputNumber
|
||||
@@ -481,7 +468,7 @@
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -494,7 +481,7 @@
|
||||
|
||||
<!-- 切换SIM卡对话框 -->
|
||||
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="600px">
|
||||
<div v-if="loadingDeviceCards" style=" padding: 40px;text-align: center">
|
||||
<div v-if="loadingDeviceCards" style="padding: 40px; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
|
||||
</div>
|
||||
@@ -647,11 +634,7 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||
import type { PackageSeriesResponse } from '@/types/api'
|
||||
@@ -661,15 +644,6 @@
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const allocateLoading = ref(false)
|
||||
const recallLoading = ref(false)
|
||||
@@ -738,11 +712,8 @@
|
||||
]
|
||||
})
|
||||
|
||||
// 设备操作右键菜单
|
||||
const deviceOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentOperatingDeviceNo = ref<string>('')
|
||||
const currentOperatingDevice = ref<Device | null>(null)
|
||||
const currentOperatingImei = ref<string>('') // 用于存储当前操作设备的IMEI
|
||||
const currentOperatingDevice = ref<Device | null>(null)
|
||||
|
||||
const switchCardDialogVisible = ref(false)
|
||||
const switchCardLoading = ref(false)
|
||||
@@ -901,7 +872,7 @@
|
||||
const goToDeviceSearchDetail = (deviceNo: string) => {
|
||||
if (hasAuth('device:view_detail')) {
|
||||
router.push({
|
||||
path: '/asset-management/single-card',
|
||||
path: '/asset-management/asset-information',
|
||||
query: {
|
||||
virtual_no: deviceNo
|
||||
}
|
||||
@@ -920,14 +891,14 @@
|
||||
bindCardForm.iot_card_id = undefined
|
||||
bindCardForm.slot_position = 1
|
||||
// 加载设备卡列表和默认IoT卡列表
|
||||
await Promise.all([loadDeviceCards(device.id), loadDefaultIotCards()])
|
||||
await Promise.all([loadDeviceCards(device.virtual_no), loadDefaultIotCards()])
|
||||
}
|
||||
|
||||
// 加载设备绑定的卡列表
|
||||
const loadDeviceCards = async (deviceId: number) => {
|
||||
const loadDeviceCards = async (virtualNo: string) => {
|
||||
deviceCardsLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDeviceCards(deviceId)
|
||||
const res = await DeviceService.getDeviceCards(virtualNo)
|
||||
if (res.code === 0 && res.data) {
|
||||
deviceCards.value = res.data.bindings || []
|
||||
console.log('设备绑定的卡列表:', deviceCards.value)
|
||||
@@ -995,7 +966,7 @@
|
||||
if (valid) {
|
||||
bindCardLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.bindCard(currentDeviceDetail.value.id, {
|
||||
const res = await DeviceService.bindCard(currentDeviceDetail.value.virtual_no, {
|
||||
iot_card_id: bindCardForm.iot_card_id!,
|
||||
slot_position: bindCardForm.slot_position
|
||||
})
|
||||
@@ -1006,7 +977,7 @@
|
||||
bindCardForm.slot_position = 1
|
||||
bindCardFormRef.value.resetFields()
|
||||
// 重新加载卡列表
|
||||
await loadDeviceCards(currentDeviceDetail.value.id)
|
||||
await loadDeviceCards(currentDeviceDetail.value.virtual_no)
|
||||
// 刷新设备详情以更新绑定卡数量
|
||||
const detailRes = await AssetService.resolveAsset(currentDeviceDetail.value.virtual_no)
|
||||
if (detailRes.code === 0 && detailRes.data) {
|
||||
@@ -1031,11 +1002,14 @@
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await DeviceService.unbindCard(currentDeviceDetail.value.id, row.iot_card_id)
|
||||
const res = await DeviceService.unbindCard(
|
||||
currentDeviceDetail.value.virtual_no,
|
||||
row.iccid
|
||||
)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('解绑成功')
|
||||
// 重新加载卡列表
|
||||
await loadDeviceCards(currentDeviceDetail.value.id)
|
||||
await loadDeviceCards(currentDeviceDetail.value.virtual_no)
|
||||
// 刷新设备详情以更新绑定卡数量
|
||||
const detailRes = await AssetService.resolveAsset(currentDeviceDetail.value.virtual_no)
|
||||
if (detailRes.code === 0 && detailRes.data) {
|
||||
@@ -1367,7 +1341,7 @@
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
await DeviceService.deleteDevice(row.id)
|
||||
await DeviceService.deleteDevice(row.virtual_no)
|
||||
ElMessage.success('删除成功')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
@@ -1581,13 +1555,16 @@
|
||||
}
|
||||
|
||||
// 设备操作路由
|
||||
const handleDeviceOperation = async (command: string, deviceNo: string) => {
|
||||
const handleDeviceOperation = async (command: string, deviceNo: string, device?: Device) => {
|
||||
// 设置当前操作的设备
|
||||
currentOperatingDevice.value = device || null
|
||||
|
||||
switch (command) {
|
||||
case 'view-cards':
|
||||
handleViewCardsByDeviceNo(deviceNo)
|
||||
break
|
||||
case 'reboot': {
|
||||
const imei = await getDeviceImei(deviceNo)
|
||||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||||
if (!imei) {
|
||||
ElMessage.error('无法获取设备IMEI,无法执行重启操作')
|
||||
return
|
||||
@@ -1596,7 +1573,7 @@
|
||||
break
|
||||
}
|
||||
case 'reset': {
|
||||
const imei = await getDeviceImei(deviceNo)
|
||||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||||
if (!imei) {
|
||||
ElMessage.error('无法获取设备IMEI,无法执行重置操作')
|
||||
return
|
||||
@@ -1605,7 +1582,7 @@
|
||||
break
|
||||
}
|
||||
case 'speed-limit': {
|
||||
const imei = await getDeviceImei(deviceNo)
|
||||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||||
if (!imei) {
|
||||
ElMessage.error('无法获取设备IMEI,无法执行限速操作')
|
||||
return
|
||||
@@ -1614,7 +1591,7 @@
|
||||
break
|
||||
}
|
||||
case 'switch-card': {
|
||||
const imei = await getDeviceImei(deviceNo)
|
||||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||||
if (!imei) {
|
||||
ElMessage.error('无法获取设备IMEI,无法执行切卡操作')
|
||||
return
|
||||
@@ -1623,7 +1600,7 @@
|
||||
break
|
||||
}
|
||||
case 'set-wifi': {
|
||||
const imei = await getDeviceImei(deviceNo)
|
||||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||||
if (!imei) {
|
||||
ElMessage.error('无法获取设备IMEI,无法执行WiFi设置操作')
|
||||
return
|
||||
@@ -1669,7 +1646,7 @@
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await DeviceService.deactivateDevice(device.id)
|
||||
const res = await AssetService.deactivateAsset(device.virtual_no)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('手动停用成功')
|
||||
loadDeviceList()
|
||||
@@ -1798,7 +1775,7 @@
|
||||
// 加载设备绑定的卡列表
|
||||
loadingDeviceCards.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDeviceCards(device.id)
|
||||
const res = await DeviceService.getDeviceCards(device.virtual_no)
|
||||
if (res.code === 0 && res.data) {
|
||||
deviceBindingCards.value = res.data.bindings || []
|
||||
|
||||
@@ -1884,89 +1861,76 @@
|
||||
}
|
||||
|
||||
// 设备操作菜单项配置
|
||||
const deviceOperationMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
// 表格操作列配置
|
||||
const getActions = (row: Device) => {
|
||||
const actions: any[] = []
|
||||
|
||||
// 第一个按钮:切换SIM卡
|
||||
if (hasAuth('device:switch_sim')) {
|
||||
actions.push({
|
||||
label: '切换SIM卡',
|
||||
handler: () => handleDeviceOperation('switch-card', row.virtual_no, row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
// 其他操作放到"更多"下拉菜单中
|
||||
const moreActions: any[] = []
|
||||
|
||||
// 添加设备绑定的卡到菜单最前面
|
||||
if (hasAuth('device:view_cards')) {
|
||||
items.push({
|
||||
key: 'view-cards',
|
||||
label: '设备绑定的卡'
|
||||
moreActions.push({
|
||||
label: '设备绑定的卡',
|
||||
handler: () => handleDeviceOperation('view-cards', row.virtual_no, row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('device:reboot')) {
|
||||
items.push({
|
||||
key: 'reboot',
|
||||
label: '重启设备'
|
||||
moreActions.push({
|
||||
label: '重启设备',
|
||||
handler: () => handleDeviceOperation('reboot', row.virtual_no, row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('device:factory_reset')) {
|
||||
items.push({
|
||||
key: 'reset',
|
||||
label: '恢复出厂'
|
||||
})
|
||||
}
|
||||
|
||||
// if (hasAuth('device:set_speed_limit')) {
|
||||
// items.push({
|
||||
// key: 'speed-limit',
|
||||
// label: '设置限速'
|
||||
// })
|
||||
// }
|
||||
|
||||
if (hasAuth('device:switch_sim')) {
|
||||
items.push({
|
||||
key: 'switch-card',
|
||||
label: '切换SIM卡'
|
||||
moreActions.push({
|
||||
label: '恢复出厂',
|
||||
handler: () => handleDeviceOperation('reset', row.virtual_no, row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('device:set_wifi')) {
|
||||
items.push({
|
||||
key: 'set-wifi',
|
||||
label: '设置WiFi'
|
||||
moreActions.push({
|
||||
label: '设置WiFi',
|
||||
handler: () => handleDeviceOperation('set-wifi', row.virtual_no, row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('device:manual_deactivate')) {
|
||||
items.push({
|
||||
key: 'manual-deactivate',
|
||||
label: '手动停用'
|
||||
moreActions.push({
|
||||
label: '手动停用',
|
||||
handler: () => handleDeviceOperation('manual-deactivate', row.virtual_no, row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('device:delete')) {
|
||||
items.push({
|
||||
key: 'delete',
|
||||
label: '删除设备'
|
||||
moreActions.push({
|
||||
label: '删除设备',
|
||||
handler: () => handleDeviceOperation('delete', row.virtual_no, row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
// 如果有更多操作,添加到 actions 中
|
||||
if (moreActions.length > 0) {
|
||||
actions.push(...moreActions)
|
||||
}
|
||||
|
||||
// 显示设备操作菜单
|
||||
const showDeviceOperationMenu = (e: MouseEvent, deviceNo: string, device?: Device) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
currentOperatingDeviceNo.value = deviceNo
|
||||
currentOperatingDevice.value = device || null
|
||||
deviceOperationMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 处理设备操作菜单选择
|
||||
const handleDeviceOperationMenuSelect = (item: MenuItemType) => {
|
||||
const deviceNo = currentOperatingDeviceNo.value
|
||||
if (!deviceNo) return
|
||||
|
||||
handleDeviceOperation(item.key, deviceNo)
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: Device, column: any, event: MouseEvent) => {
|
||||
showDeviceOperationMenu(event, row.virtual_no, row)
|
||||
return actions
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -37,13 +37,18 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showAllocateDialog">{{
|
||||
$t('enterpriseDevices.buttons.allocateDevices')
|
||||
}}</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="showAllocateDialog"
|
||||
v-permission="'enterprise_device:allocate'"
|
||||
>
|
||||
{{ $t('enterpriseDevices.buttons.allocateDevices') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="warning"
|
||||
:disabled="selectedDevices.length === 0"
|
||||
@click="showRecallDialog"
|
||||
v-permission="'enterprise_device:recall'"
|
||||
>
|
||||
{{ $t('enterpriseDevices.buttons.recallDevices') }}
|
||||
</ElButton>
|
||||
|
||||
@@ -75,16 +75,73 @@
|
||||
v-model="createForm.old_asset_type"
|
||||
placeholder="请选择旧资产类型"
|
||||
style="width: 100%"
|
||||
@change="handleOldAssetTypeChange"
|
||||
>
|
||||
<ElOption label="IoT卡" value="iot_card" />
|
||||
<ElOption label="设备" value="device" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="旧资产标识符" prop="old_identifier">
|
||||
<ElInput
|
||||
<ElFormItem
|
||||
v-if="createForm.old_asset_type === 'iot_card'"
|
||||
label="旧资产标识符"
|
||||
prop="old_identifier"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="createForm.old_identifier"
|
||||
placeholder="请输入ICCID/虚拟号/IMEI/SN"
|
||||
/>
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="请输入ICCID搜索"
|
||||
:remote-method="searchOldIotCards"
|
||||
:loading="oldCardSearchLoading"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="card in oldIotCardOptions"
|
||||
:key="card.id"
|
||||
:label="`${card.iccid} (${card.msisdn || '无接入号'})`"
|
||||
:value="card.iccid"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ card.iccid }}</span>
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
{{ card.msisdn || '无接入号' }}
|
||||
</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="createForm.old_asset_type === 'device'"
|
||||
label="旧资产标识符"
|
||||
prop="old_identifier"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="createForm.old_identifier"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="请输入虚拟号或IMEI搜索"
|
||||
:remote-method="searchOldDevices"
|
||||
:loading="oldDeviceSearchLoading"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="device in oldDeviceOptions"
|
||||
:key="device.id"
|
||||
:label="`${device.virtual_no} (${device.device_name || device.imei})`"
|
||||
:value="device.virtual_no"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ device.virtual_no }}</span>
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
{{ device.device_name || device.imei }}
|
||||
</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
@@ -129,7 +186,7 @@
|
||||
</ElFormItem>
|
||||
<ElFormItem label="是否迁移数据">
|
||||
<ElSwitch v-model="shipForm.migrate_data" />
|
||||
<div style=" margin-top: 4px;font-size: 12px; color: #909399">
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #909399">
|
||||
开启后将迁移钱包余额、套餐记录到新资产
|
||||
</div>
|
||||
</ElFormItem>
|
||||
@@ -152,8 +209,9 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ExchangeService } from '@/api/modules'
|
||||
import { ExchangeService, CardService, DeviceService } from '@/api/modules'
|
||||
import type { ExchangeResponse } from '@/api/modules/exchange'
|
||||
import type { StandaloneIotCard, Device } from '@/types/api'
|
||||
import { ElMessage, ElTag, ElButton, ElMessageBox, ElSwitch } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
@@ -208,9 +266,15 @@
|
||||
const createRules = reactive<FormRules>({
|
||||
exchange_reason: [{ required: true, message: '请输入换货原因', trigger: 'blur' }],
|
||||
old_asset_type: [{ required: true, message: '请选择旧资产类型', trigger: 'change' }],
|
||||
old_identifier: [{ required: true, message: '请输入旧资产标识符', trigger: 'blur' }]
|
||||
old_identifier: [{ required: true, message: '请选择旧资产标识符', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 旧资产搜索相关
|
||||
const oldIotCardOptions = ref<StandaloneIotCard[]>([])
|
||||
const oldCardSearchLoading = ref(false)
|
||||
const oldDeviceOptions = ref<Device[]>([])
|
||||
const oldDeviceSearchLoading = ref(false)
|
||||
|
||||
// 发货表单
|
||||
const shipForm = reactive({
|
||||
new_identifier: '',
|
||||
@@ -311,7 +375,7 @@
|
||||
prop: 'new_asset_identifier',
|
||||
label: '新资产标识符',
|
||||
width: 180,
|
||||
formatter: (row: any) => row.new_asset_identifier || '--'
|
||||
formatter: (row: any) => row.new_asset_identifier
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
@@ -327,8 +391,8 @@
|
||||
}
|
||||
])
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
const types: Record<number, string> = {
|
||||
const getStatusType = (status: number): 'success' | 'info' | 'warning' | 'danger' | 'primary' => {
|
||||
const types: Record<number, 'success' | 'info' | 'warning' | 'danger' | 'primary'> = {
|
||||
1: 'warning',
|
||||
2: 'info',
|
||||
3: 'primary',
|
||||
@@ -363,7 +427,7 @@
|
||||
|
||||
const res = await ExchangeService.getExchanges(params)
|
||||
if (res.code === 0 && res.data) {
|
||||
exchangeList.value = res.data.list || []
|
||||
exchangeList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -406,6 +470,123 @@
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 旧资产类型变更
|
||||
const handleOldAssetTypeChange = async () => {
|
||||
// 清空旧资产标识符
|
||||
createForm.old_identifier = ''
|
||||
oldIotCardOptions.value = []
|
||||
oldDeviceOptions.value = []
|
||||
|
||||
// 根据类型加载默认数据
|
||||
if (createForm.old_asset_type === 'iot_card') {
|
||||
await loadDefaultIotCards()
|
||||
} else if (createForm.old_asset_type === 'device') {
|
||||
await loadDefaultDevices()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载默认IoT卡列表
|
||||
const loadDefaultIotCards = async () => {
|
||||
oldCardSearchLoading.value = true
|
||||
try {
|
||||
const res = await CardService.getStandaloneIotCards({
|
||||
page: 1,
|
||||
page_size: 10
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
oldIotCardOptions.value = res.data.items || []
|
||||
if (oldIotCardOptions.value.length === 0) {
|
||||
ElMessage.info('暂无可用的IoT卡')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载IoT卡列表失败:', error)
|
||||
oldIotCardOptions.value = []
|
||||
} finally {
|
||||
oldCardSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载默认设备列表
|
||||
const loadDefaultDevices = async () => {
|
||||
oldDeviceSearchLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDevices({
|
||||
page: 1,
|
||||
page_size: 10
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
oldDeviceOptions.value = res.data.items || []
|
||||
if (oldDeviceOptions.value.length === 0) {
|
||||
ElMessage.info('暂无可用的设备')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设备列表失败:', error)
|
||||
oldDeviceOptions.value = []
|
||||
} finally {
|
||||
oldDeviceSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索旧IoT卡
|
||||
const searchOldIotCards = async (query: string) => {
|
||||
if (!query) {
|
||||
// 如果清空搜索,加载默认列表
|
||||
await loadDefaultIotCards()
|
||||
return
|
||||
}
|
||||
|
||||
oldCardSearchLoading.value = true
|
||||
try {
|
||||
const res = await CardService.getStandaloneIotCards({
|
||||
iccid: query,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
oldIotCardOptions.value = res.data.items || []
|
||||
if (oldIotCardOptions.value.length === 0) {
|
||||
ElMessage.info('未找到匹配的IoT卡')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索IoT卡失败:', error)
|
||||
oldIotCardOptions.value = []
|
||||
} finally {
|
||||
oldCardSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索旧设备
|
||||
const searchOldDevices = async (query: string) => {
|
||||
if (!query) {
|
||||
// 如果清空搜索,加载默认列表
|
||||
await loadDefaultDevices()
|
||||
return
|
||||
}
|
||||
|
||||
oldDeviceSearchLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDevices({
|
||||
keyword: query,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
oldDeviceOptions.value = res.data.items || []
|
||||
if (oldDeviceOptions.value.length === 0) {
|
||||
ElMessage.info('未找到匹配的设备')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索设备失败:', error)
|
||||
oldDeviceOptions.value = []
|
||||
} finally {
|
||||
oldDeviceSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 确认创建
|
||||
const handleConfirmCreate = () => {
|
||||
createFormRef.value?.validate(async (valid) => {
|
||||
@@ -417,7 +598,7 @@
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('换货单创建成功')
|
||||
createDialogVisible.value = false
|
||||
loadExchangeList()
|
||||
await loadExchangeList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建换货单失败:', error)
|
||||
@@ -430,6 +611,9 @@
|
||||
// 关闭创建对话框
|
||||
const handleCloseCreateDialog = () => {
|
||||
createFormRef.value?.resetFields()
|
||||
// 清空搜索选项
|
||||
oldIotCardOptions.value = []
|
||||
oldDeviceOptions.value = []
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
@@ -492,7 +676,7 @@
|
||||
})
|
||||
|
||||
// 处理右键菜单
|
||||
const handleRowContextMenu = (row: ExchangeResponse, column: any, event: MouseEvent) => {
|
||||
const handleRowContextMenu = (row: ExchangeResponse, _column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentExchangeRow.value = row
|
||||
@@ -531,7 +715,7 @@
|
||||
const res = await ExchangeService.cancelExchange(row.id, { remark: value })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('取消成功')
|
||||
loadExchangeList()
|
||||
await loadExchangeList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消换货失败:', error)
|
||||
@@ -562,7 +746,7 @@
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('发货成功')
|
||||
shipDialogVisible.value = false
|
||||
loadExchangeList()
|
||||
await loadExchangeList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发货失败:', error)
|
||||
@@ -589,7 +773,7 @@
|
||||
const res = await ExchangeService.completeExchange(row.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('操作成功')
|
||||
loadExchangeList()
|
||||
await loadExchangeList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('确认完成失败:', error)
|
||||
@@ -610,7 +794,7 @@
|
||||
const res = await ExchangeService.renewExchange(row.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('操作成功')
|
||||
loadExchangeList()
|
||||
await loadExchangeList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('旧资产转新失败:', error)
|
||||
|
||||
@@ -68,13 +68,11 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="160"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
@@ -82,9 +80,6 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 批量分配对话框 -->
|
||||
<ElDialog
|
||||
v-model="allocateDialogVisible"
|
||||
@@ -126,26 +121,32 @@
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="allocateForm.selection_type === 'list'" label="ICCID列表">
|
||||
<ElFormItem
|
||||
v-if="allocateForm.selection_type === CardSelectionType.LIST"
|
||||
label="ICCID列表"
|
||||
>
|
||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="allocateForm.selection_type === 'range'"
|
||||
v-if="allocateForm.selection_type === CardSelectionType.RANGE"
|
||||
label="起始ICCID"
|
||||
prop="iccid_start"
|
||||
>
|
||||
<ElInput v-model="allocateForm.iccid_start" placeholder="请输入起始ICCID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="allocateForm.selection_type === 'range'"
|
||||
v-if="allocateForm.selection_type === CardSelectionType.RANGE"
|
||||
label="结束ICCID"
|
||||
prop="iccid_end"
|
||||
>
|
||||
<ElInput v-model="allocateForm.iccid_end" placeholder="请输入结束ICCID" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="运营商">
|
||||
<ElFormItem
|
||||
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
|
||||
label="运营商"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="allocateForm.carrier_id"
|
||||
placeholder="请选择运营商"
|
||||
@@ -157,7 +158,10 @@
|
||||
<ElOption label="中国电信" :value="3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="卡状态">
|
||||
<ElFormItem
|
||||
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
|
||||
label="卡状态"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="allocateForm.status"
|
||||
placeholder="请选择状态"
|
||||
@@ -170,7 +174,10 @@
|
||||
<ElOption label="已停用" :value="4" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="批次号">
|
||||
<ElFormItem
|
||||
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
|
||||
label="批次号"
|
||||
>
|
||||
<ElInput v-model="allocateForm.batch_no" placeholder="请输入批次号" />
|
||||
</ElFormItem>
|
||||
|
||||
@@ -209,26 +216,32 @@
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="recallForm.selection_type === 'list'" label="ICCID列表">
|
||||
<ElFormItem
|
||||
v-if="recallForm.selection_type === CardSelectionType.LIST"
|
||||
label="ICCID列表"
|
||||
>
|
||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="recallForm.selection_type === 'range'"
|
||||
v-if="recallForm.selection_type === CardSelectionType.RANGE"
|
||||
label="起始ICCID"
|
||||
prop="iccid_start"
|
||||
>
|
||||
<ElInput v-model="recallForm.iccid_start" placeholder="请输入起始ICCID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="recallForm.selection_type === 'range'"
|
||||
v-if="recallForm.selection_type === CardSelectionType.RANGE"
|
||||
label="结束ICCID"
|
||||
prop="iccid_end"
|
||||
>
|
||||
<ElInput v-model="recallForm.iccid_end" placeholder="请输入结束ICCID" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="recallForm.selection_type === 'filter'" label="运营商">
|
||||
<ElFormItem
|
||||
v-if="recallForm.selection_type === CardSelectionType.FILTER"
|
||||
label="运营商"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="recallForm.carrier_id"
|
||||
placeholder="请选择运营商"
|
||||
@@ -240,7 +253,10 @@
|
||||
<ElOption label="中国电信" :value="3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="recallForm.selection_type === 'filter'" label="批次号">
|
||||
<ElFormItem
|
||||
v-if="recallForm.selection_type === CardSelectionType.FILTER"
|
||||
label="批次号"
|
||||
>
|
||||
<ElInput v-model="recallForm.batch_no" placeholder="请输入批次号" />
|
||||
</ElFormItem>
|
||||
|
||||
@@ -385,7 +401,7 @@
|
||||
|
||||
<!-- 卡详情对话框 -->
|
||||
<ElDialog v-model="cardDetailDialogVisible" title="卡片详情" width="900px">
|
||||
<div v-if="cardDetailLoading" style=" padding: 40px;text-align: center">
|
||||
<div v-if="cardDetailLoading" style="padding: 40px; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
<div style="margin-top: 16px">加载中...</div>
|
||||
</div>
|
||||
@@ -465,7 +481,7 @@
|
||||
|
||||
<!-- 流量使用查询对话框 -->
|
||||
<ElDialog v-model="flowUsageDialogVisible" title="流量使用查询" width="500px">
|
||||
<div v-if="flowUsageLoading" style=" padding: 40px;text-align: center">
|
||||
<div v-if="flowUsageLoading" style="padding: 40px; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
<div style="margin-top: 16px">查询中...</div>
|
||||
</div>
|
||||
@@ -494,7 +510,7 @@
|
||||
|
||||
<!-- 实名状态查询对话框 -->
|
||||
<ElDialog v-model="realnameStatusDialogVisible" title="实名认证状态" width="500px">
|
||||
<div v-if="realnameStatusLoading" style=" padding: 40px;text-align: center">
|
||||
<div v-if="realnameStatusLoading" style="padding: 40px; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
<div style="margin-top: 16px">查询中...</div>
|
||||
</div>
|
||||
@@ -517,7 +533,7 @@
|
||||
|
||||
<!-- 卡实时状态查询对话框 -->
|
||||
<ElDialog v-model="cardStatusDialogVisible" title="卡实时状态" width="500px">
|
||||
<div v-if="cardStatusLoading" style=" padding: 40px;text-align: center">
|
||||
<div v-if="cardStatusLoading" style="padding: 40px; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
<div style="margin-top: 16px">查询中...</div>
|
||||
</div>
|
||||
@@ -548,14 +564,6 @@
|
||||
:menu-width="180"
|
||||
@select="handleMoreMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 表格行操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="cardOperationMenuRef"
|
||||
:menu-items="cardOperationMenuItems"
|
||||
:menu-width="160"
|
||||
@select="handleCardOperationMenuSelect"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -571,12 +579,9 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import type {
|
||||
StandaloneIotCard,
|
||||
StandaloneCardStatus,
|
||||
@@ -585,23 +590,14 @@
|
||||
AllocateStandaloneCardsResponse,
|
||||
BatchSetCardSeriesBindingResponse
|
||||
} from '@/types/api/card'
|
||||
import { CardSelectionType } from '@/types/api/card'
|
||||
import type { PackageSeriesResponse } from '@/types/api'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'StandaloneCardList' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const allocateDialogVisible = ref(false)
|
||||
const allocateLoading = ref(false)
|
||||
@@ -660,18 +656,21 @@
|
||||
|
||||
// 更多操作右键菜单
|
||||
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const cardOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentOperatingIccid = ref<string>('')
|
||||
const currentOperatingCard = ref<StandaloneIotCard | null>(null)
|
||||
|
||||
// 店铺相关
|
||||
const targetShopLoading = ref(false)
|
||||
const fromShopLoading = ref(false)
|
||||
const targetShopList = ref<any[]>([])
|
||||
const fromShopList = ref<any[]>([])
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
const initialSearchState: {
|
||||
status: undefined | number
|
||||
carrier_id: undefined | number
|
||||
iccid: string
|
||||
msisdn: string
|
||||
virtual_no: string
|
||||
is_distributed: undefined | number
|
||||
[key: string]: any
|
||||
} = {
|
||||
status: undefined,
|
||||
carrier_id: undefined,
|
||||
iccid: '',
|
||||
@@ -685,7 +684,7 @@
|
||||
|
||||
// 批量分配表单
|
||||
const allocateForm = reactive<Partial<AllocateStandaloneCardsRequest>>({
|
||||
selection_type: 'list',
|
||||
selection_type: CardSelectionType.LIST,
|
||||
to_shop_id: undefined,
|
||||
iccids: [],
|
||||
iccid_start: '',
|
||||
@@ -703,8 +702,8 @@
|
||||
iccid_start: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (allocateForm.selection_type === 'range' && !value) {
|
||||
validator: (_rule, value, callback) => {
|
||||
if (allocateForm.selection_type === CardSelectionType.RANGE && !value) {
|
||||
callback(new Error('请输入起始ICCID'))
|
||||
} else {
|
||||
callback()
|
||||
@@ -716,8 +715,8 @@
|
||||
iccid_end: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (allocateForm.selection_type === 'range' && !value) {
|
||||
validator: (_rule, value, callback) => {
|
||||
if (allocateForm.selection_type === CardSelectionType.RANGE && !value) {
|
||||
callback(new Error('请输入结束ICCID'))
|
||||
} else {
|
||||
callback()
|
||||
@@ -730,7 +729,7 @@
|
||||
|
||||
// 批量回收表单
|
||||
const recallForm = reactive<Partial<RecallStandaloneCardsRequest>>({
|
||||
selection_type: 'list',
|
||||
selection_type: CardSelectionType.LIST,
|
||||
iccids: [],
|
||||
iccid_start: '',
|
||||
iccid_end: '',
|
||||
@@ -745,8 +744,8 @@
|
||||
iccid_start: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (recallForm.selection_type === 'range' && !value) {
|
||||
validator: (_rule, value, callback) => {
|
||||
if (recallForm.selection_type === CardSelectionType.RANGE && !value) {
|
||||
callback(new Error('请输入起始ICCID'))
|
||||
} else {
|
||||
callback()
|
||||
@@ -758,8 +757,8 @@
|
||||
iccid_end: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (recallForm.selection_type === 'range' && !value) {
|
||||
validator: (_rule, value, callback) => {
|
||||
if (recallForm.selection_type === CardSelectionType.RANGE && !value) {
|
||||
callback(new Error('请输入结束ICCID'))
|
||||
} else {
|
||||
callback()
|
||||
@@ -907,7 +906,7 @@
|
||||
const goToCardDetail = (iccid: string) => {
|
||||
if (hasAuth('iot_card:view_detail')) {
|
||||
router.push({
|
||||
path: '/asset-management/single-card',
|
||||
path: '/asset-management/asset-information',
|
||||
query: {
|
||||
iccid: iccid
|
||||
}
|
||||
@@ -983,13 +982,13 @@
|
||||
{
|
||||
prop: 'msisdn',
|
||||
label: '卡接入号',
|
||||
width: 130
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'virtual_no',
|
||||
label: '虚拟号',
|
||||
width: 130,
|
||||
formatter: (row: StandaloneIotCard) => row.virtual_no || '-'
|
||||
width: 160,
|
||||
formatter: (row: StandaloneIotCard) => row.virtual_no
|
||||
},
|
||||
{
|
||||
prop: 'card_category',
|
||||
@@ -1081,6 +1080,63 @@
|
||||
}
|
||||
])
|
||||
|
||||
// 操作列配置
|
||||
const getActions = (row: StandaloneIotCard) => {
|
||||
const actions: any[] = []
|
||||
|
||||
// 查询流量按钮
|
||||
if (hasAuth('iot_card:query_flow')) {
|
||||
actions.push({
|
||||
label: '查询流量',
|
||||
handler: () => showFlowUsageDialog(row.iccid),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
// 其他操作放到更多中
|
||||
if (hasAuth('iot_card:query_realname_status')) {
|
||||
actions.push({
|
||||
label: '查询实名状态',
|
||||
handler: () => handleCardOperation('realname-status', row.iccid),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('iot_card:query_card_status')) {
|
||||
actions.push({
|
||||
label: '查询卡状态',
|
||||
handler: () => handleCardOperation('card-status', row.iccid),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('iot_card:start_card')) {
|
||||
actions.push({
|
||||
label: '启用此卡',
|
||||
handler: () => handleCardOperation('start-card', row.iccid),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('iot_card:stop_card')) {
|
||||
actions.push({
|
||||
label: '停用此卡',
|
||||
handler: () => handleCardOperation('stop-card', row.iccid),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('iot_card:manual_deactivate')) {
|
||||
actions.push({
|
||||
label: '手动停用',
|
||||
handler: () => handleCardOperation('manual-deactivate', row.iccid),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
@@ -1089,7 +1145,7 @@
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
const params: Record<string, any> = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...formFilters
|
||||
@@ -1174,33 +1230,6 @@
|
||||
await loadTargetShops(query || undefined)
|
||||
}
|
||||
|
||||
// 加载来源店铺列表
|
||||
const loadFromShops = async (shopName?: string) => {
|
||||
fromShopLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
if (shopName) {
|
||||
params.shop_name = shopName
|
||||
}
|
||||
const res = await ShopService.getShops(params)
|
||||
if (res.code === 0) {
|
||||
fromShopList.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取来源店铺列表失败:', error)
|
||||
} finally {
|
||||
fromShopLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索来源店铺
|
||||
const searchFromShops = async (query: string) => {
|
||||
await loadFromShops(query || undefined)
|
||||
}
|
||||
|
||||
// 显示批量分配对话框
|
||||
const showAllocateDialog = () => {
|
||||
if (selectedCards.value.length === 0) {
|
||||
@@ -1209,7 +1238,7 @@
|
||||
}
|
||||
allocateDialogVisible.value = true
|
||||
Object.assign(allocateForm, {
|
||||
selection_type: 'list',
|
||||
selection_type: CardSelectionType.LIST,
|
||||
to_shop_id: undefined,
|
||||
iccids: selectedCards.value.map((card) => card.iccid),
|
||||
iccid_start: '',
|
||||
@@ -1234,7 +1263,7 @@
|
||||
}
|
||||
recallDialogVisible.value = true
|
||||
Object.assign(recallForm, {
|
||||
selection_type: 'list',
|
||||
selection_type: CardSelectionType.LIST,
|
||||
iccids: selectedCards.value.map((card) => card.iccid),
|
||||
iccid_start: '',
|
||||
iccid_end: '',
|
||||
@@ -1274,16 +1303,16 @@
|
||||
remark: allocateForm.remark
|
||||
}
|
||||
|
||||
if (allocateForm.selection_type === 'list') {
|
||||
if (allocateForm.selection_type === CardSelectionType.LIST) {
|
||||
params.iccids = selectedCards.value.map((card) => card.iccid)
|
||||
if (params.iccids.length === 0) {
|
||||
ElMessage.warning('请先选择要分配的卡')
|
||||
return
|
||||
}
|
||||
} else if (allocateForm.selection_type === 'range') {
|
||||
} else if (allocateForm.selection_type === CardSelectionType.RANGE) {
|
||||
params.iccid_start = allocateForm.iccid_start
|
||||
params.iccid_end = allocateForm.iccid_end
|
||||
} else if (allocateForm.selection_type === 'filter') {
|
||||
} else if (allocateForm.selection_type === CardSelectionType.FILTER) {
|
||||
if (allocateForm.carrier_id) params.carrier_id = allocateForm.carrier_id
|
||||
if (allocateForm.status) params.status = allocateForm.status
|
||||
if (allocateForm.batch_no) params.batch_no = allocateForm.batch_no
|
||||
@@ -1327,16 +1356,16 @@
|
||||
remark: recallForm.remark
|
||||
}
|
||||
|
||||
if (recallForm.selection_type === 'list') {
|
||||
if (recallForm.selection_type === CardSelectionType.LIST) {
|
||||
params.iccids = selectedCards.value.map((card) => card.iccid)
|
||||
if (params.iccids.length === 0) {
|
||||
ElMessage.warning('请先选择要回收的卡')
|
||||
return
|
||||
}
|
||||
} else if (recallForm.selection_type === 'range') {
|
||||
} else if (recallForm.selection_type === CardSelectionType.RANGE) {
|
||||
params.iccid_start = recallForm.iccid_start
|
||||
params.iccid_end = recallForm.iccid_end
|
||||
} else if (recallForm.selection_type === 'filter') {
|
||||
} else if (recallForm.selection_type === CardSelectionType.FILTER) {
|
||||
if (recallForm.carrier_id) params.carrier_id = recallForm.carrier_id
|
||||
if (recallForm.batch_no) params.batch_no = recallForm.batch_no
|
||||
}
|
||||
@@ -1358,7 +1387,7 @@
|
||||
// 刷新列表
|
||||
await getTableData()
|
||||
} else {
|
||||
// code !== 0 才是真正的失败(接口调用失败)
|
||||
// code !== 0 才是真正失败(接口调用失败)
|
||||
ElMessage.error(res.msg || '批量回收失败,请重试')
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1501,60 +1530,6 @@
|
||||
})
|
||||
|
||||
// 卡操作菜单项配置
|
||||
const cardOperationMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('iot_card:query_flow')) {
|
||||
items.push({
|
||||
key: 'query-flow',
|
||||
label: '查询流量'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('iot_card:query_realname_status')) {
|
||||
items.push({
|
||||
key: 'realname-status',
|
||||
label: '查询实名状态'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('iot_card:query_card_status')) {
|
||||
items.push({
|
||||
key: 'card-status',
|
||||
label: '查询卡状态'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('iot_card:start_card')) {
|
||||
items.push({
|
||||
key: 'start-card',
|
||||
label: '启用此卡'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('iot_card:stop_card')) {
|
||||
items.push({
|
||||
key: 'stop-card',
|
||||
label: '停用此卡'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('iot_card:manual_deactivate')) {
|
||||
items.push({
|
||||
key: 'manual-deactivate',
|
||||
label: '手动停用'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 显示更多操作菜单 (右键)
|
||||
const showMoreMenu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
moreMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 显示更多操作菜单 (左键点击)
|
||||
const showMoreMenuOnClick = (e: MouseEvent) => {
|
||||
@@ -1583,27 +1558,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 显示卡操作菜单
|
||||
const showCardOperationMenu = (e: MouseEvent, iccid: string, card?: StandaloneIotCard) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
currentOperatingIccid.value = iccid
|
||||
currentOperatingCard.value = card || null
|
||||
cardOperationMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 处理卡操作菜单选择
|
||||
const handleCardOperationMenuSelect = (item: MenuItemType) => {
|
||||
const iccid = currentOperatingIccid.value
|
||||
if (!iccid) return
|
||||
|
||||
if (item.key === 'query-flow') {
|
||||
showFlowUsageDialog(iccid)
|
||||
} else {
|
||||
handleCardOperation(item.key, iccid)
|
||||
}
|
||||
}
|
||||
|
||||
// 网卡分销 - 正在开发中
|
||||
const cardDistribution = () => {
|
||||
ElMessage.info('功能正在开发中')
|
||||
@@ -1645,7 +1599,7 @@
|
||||
handleStopCard(iccid)
|
||||
break
|
||||
case 'manual-deactivate':
|
||||
handleManualDeactivate()
|
||||
handleManualDeactivate(iccid)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1668,7 +1622,7 @@
|
||||
extend: res.data.extend
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.message || '查询失败')
|
||||
ElMessage.error(res.msg || '查询失败')
|
||||
flowUsageDialogVisible.value = false
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -1704,7 +1658,7 @@
|
||||
extend: res.data.extend
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.message || '查询失败')
|
||||
ElMessage.error(res.msg || '查询失败')
|
||||
realnameStatusDialogVisible.value = false
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -1732,7 +1686,7 @@
|
||||
extend: res.data.extend
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.message || '查询失败')
|
||||
ElMessage.error(res.msg || '查询失败')
|
||||
cardStatusDialogVisible.value = false
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -1752,11 +1706,11 @@
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
// 使用新接口:直接通过 ICCID 启用此卡
|
||||
const res = await AssetService.startCard(iccid)
|
||||
// 使用统一接口:通过 identifier (ICCID) 启用资产
|
||||
const res = await AssetService.startAsset(iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('启用成功')
|
||||
getTableData()
|
||||
await getTableData()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('启用此卡失败:', error)
|
||||
@@ -1776,11 +1730,11 @@
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
// 使用新接口:直接通过 ICCID 停用此卡
|
||||
const res = await AssetService.stopCard(iccid)
|
||||
// 使用统一接口:通过 identifier (ICCID) 停用资产
|
||||
const res = await AssetService.stopAsset(iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('停用成功')
|
||||
getTableData()
|
||||
await getTableData()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('停用此卡失败:', error)
|
||||
@@ -1792,15 +1746,9 @@
|
||||
}
|
||||
|
||||
// 手动停用 IoT 卡(资产层面的停用)
|
||||
const handleManualDeactivate = () => {
|
||||
const card = currentOperatingCard.value
|
||||
if (!card) {
|
||||
ElMessage.error('未找到卡片信息')
|
||||
return
|
||||
}
|
||||
|
||||
const handleManualDeactivate = (iccid: string) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要手动停用卡片 ${card.iccid} 吗?此操作将在资产管理层面停用该卡。`,
|
||||
`确定要手动停用卡片 ${iccid} 吗?此操作将在资产管理层面停用该卡。`,
|
||||
'确认手动停用',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
@@ -1810,7 +1758,7 @@
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await CardService.deactivateIotCard(card.id)
|
||||
const res = await AssetService.deactivateAsset(iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('手动停用成功')
|
||||
await getTableData()
|
||||
@@ -1823,18 +1771,9 @@
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: StandaloneIotCard, column: any, event: MouseEvent) => {
|
||||
showCardOperationMenu(event, row.iccid, row)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.standalone-card-list-page {
|
||||
// Card list page styles
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
align-center
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="failDataLoading" style=" padding: 40px;text-align: center">
|
||||
<div v-if="failDataLoading" style="padding: 40px; text-align: center">
|
||||
<ElSkeleton :rows="5" animated />
|
||||
</div>
|
||||
<div v-else-if="failedItems.length > 0">
|
||||
@@ -92,7 +92,7 @@
|
||||
<ElTableColumn prop="message" label="失败原因" min-width="200" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</div>
|
||||
<div v-else style=" padding: 40px;text-align: center">
|
||||
<div v-else style="padding: 40px; text-align: center">
|
||||
<ElEmpty description="暂无失败数据" />
|
||||
</div>
|
||||
|
||||
@@ -118,8 +118,10 @@
|
||||
<p>1. 请先下载 Excel 模板文件,按照模板格式填写IoT卡信息</p>
|
||||
<p>2. 仅支持 Excel 格式(.xlsx),单次最多导入 1000 条</p>
|
||||
<p>3. 列格式请设置为文本格式,避免长数字被转为科学计数法</p>
|
||||
<p>4. 必填字段:ICCID、MSISDN(手机号);可选字段:virtual_no(虚拟号)</p>
|
||||
<p>5. 必须选择运营商</p>
|
||||
<p>4. <strong>必填字段:ICCID、MSISDN(手机号)、virtual_no(虚拟号)</strong></p>
|
||||
<p>5. <strong>Excel 文件必须包含 virtual_no 列,否则整批导入将失败</strong></p>
|
||||
<p>6. virtual_no 为空将导入失败</p>
|
||||
<p>7. 必须选择运营商</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
@@ -17,13 +17,17 @@
|
||||
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
||||
<p
|
||||
>4.
|
||||
必填字段:virtual_no(设备号)、device_name(设备名称)、device_model(设备型号)</p
|
||||
<strong>必填字段:virtual_no(设备虚拟号)</strong
|
||||
>、device_name(设备名称)、device_model(设备型号)</p
|
||||
>
|
||||
<p
|
||||
>5.
|
||||
可选字段:device_type(设备类型)、manufacturer(制造商)、max_sim_slots(最大插槽数,默认1)</p
|
||||
>
|
||||
<p>6. 设备号重复将自动跳过,导入后可在任务管理中查看详情</p>
|
||||
<p
|
||||
>6. virtual_no
|
||||
为空将导入失败,设备号重复将自动跳过,导入后可在任务管理中查看详情</p
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
@@ -14,8 +14,14 @@
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载模板文件,按照模板格式填写网卡信息</p>
|
||||
<p>2. 支持 Excel 格式(.xlsx, .xls),单次最多导入 1000 条</p>
|
||||
<p>3. 必填字段:ICCID、运营商、套餐类型、流量规格</p>
|
||||
<p>4. 导入后系统将自动校验数据,重复 ICCID 将跳过</p>
|
||||
<p
|
||||
>3.
|
||||
<strong
|
||||
>必填字段:ICCID、virtual_no(虚拟号)、运营商、套餐类型、流量规格</strong
|
||||
></p
|
||||
>
|
||||
<p>4. <strong>Excel 文件必须包含 virtual_no 列,否则整批导入将失败</strong></p>
|
||||
<p>5. virtual_no 为空将导入失败,重复 ICCID 将跳过</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
@@ -480,7 +480,12 @@
|
||||
const accountRules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_-]+$/,
|
||||
message: '用户名只能包含字母、数字、下划线和横线',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
style="width: 100%"
|
||||
placeholder="请输入入账金额(分)"
|
||||
/>
|
||||
<div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-secondary)">
|
||||
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
金额单位为分,例如:100元 = 10000分
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
@closed="handleCreateDialogClosed"
|
||||
>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||
<ElFormItem label="关联订单ID" prop="order_id">
|
||||
<ElFormItem label="订单号" prop="order_id">
|
||||
<ElSelect
|
||||
v-model="createForm.order_id"
|
||||
filterable
|
||||
@@ -73,6 +73,7 @@
|
||||
:loading="orderSearchLoading"
|
||||
placeholder="请输入订单号搜索"
|
||||
style="width: 100%"
|
||||
@focus="handleOrderSelectFocus"
|
||||
>
|
||||
<ElOption
|
||||
v-for="order in orderSearchOptions"
|
||||
@@ -677,17 +678,18 @@
|
||||
|
||||
// 搜索订单(用于创建弹窗)
|
||||
const searchOrders = async (query: string) => {
|
||||
if (!query) {
|
||||
orderSearchOptions.value = []
|
||||
return
|
||||
}
|
||||
orderSearchLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_no: query
|
||||
page_size: 20
|
||||
}
|
||||
|
||||
// 如果有搜索关键词,则按订单号搜索
|
||||
if (query) {
|
||||
params.order_no = query
|
||||
}
|
||||
|
||||
const res = await OrderService.getOrders(params)
|
||||
if (res.code === 0) {
|
||||
orderSearchOptions.value = res.data.items || []
|
||||
@@ -756,6 +758,14 @@
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 订单选择框获取焦点时加载默认数据
|
||||
const handleOrderSelectFocus = () => {
|
||||
// 如果还没有数据,则加载默认的20条
|
||||
if (orderSearchOptions.value.length === 0) {
|
||||
searchOrders('')
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框关闭后的清理
|
||||
const handleCreateDialogClosed = () => {
|
||||
createFormRef.value?.resetFields()
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ card.iccid }}</span>
|
||||
<span style=" font-size: 12px;color: var(--el-text-color-secondary)">
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
{{ card.msisdn || '无接入号' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -130,7 +130,7 @@
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ device.virtual_no }}</span>
|
||||
<span style=" font-size: 12px;color: var(--el-text-color-secondary)">
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
{{ device.device_name }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -163,7 +163,7 @@
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ pkg.package_name }}</span>
|
||||
<span style=" font-size: 12px;color: var(--el-text-color-secondary)">
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
¥{{ (pkg.cost_price / 100).toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -364,7 +364,6 @@
|
||||
payment_status: undefined,
|
||||
order_type: undefined,
|
||||
purchase_role: undefined,
|
||||
is_expired: undefined,
|
||||
start_time: '',
|
||||
end_time: ''
|
||||
}
|
||||
@@ -437,19 +436,6 @@
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '超时状态',
|
||||
prop: 'is_expired',
|
||||
type: 'select',
|
||||
placeholder: '请选择超时状态',
|
||||
options: [
|
||||
{ label: '已超时', value: true },
|
||||
{ label: '未超时', value: false }
|
||||
],
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('orderManagement.searchForm.dateRange'),
|
||||
prop: 'dateRange',
|
||||
@@ -480,8 +466,6 @@
|
||||
{ label: '购买备注', prop: 'purchase_remark' },
|
||||
{ label: '操作者', prop: 'operator_name' },
|
||||
{ label: t('orderManagement.table.paymentStatus'), prop: 'payment_status' },
|
||||
{ label: '超时状态', prop: 'is_expired' },
|
||||
{ label: '超时时间', prop: 'expires_at' },
|
||||
{ label: t('orderManagement.table.totalAmount'), prop: 'total_amount' },
|
||||
{ label: '实付金额', prop: 'actual_paid_amount' },
|
||||
{ label: t('orderManagement.table.paymentMethod'), prop: 'payment_method' },
|
||||
@@ -656,7 +640,8 @@
|
||||
const methodMap: Record<OrderPaymentMethod, string> = {
|
||||
wallet: t('orderManagement.paymentMethod.wallet'),
|
||||
wechat: t('orderManagement.paymentMethod.wechat'),
|
||||
alipay: t('orderManagement.paymentMethod.alipay')
|
||||
alipay: t('orderManagement.paymentMethod.alipay'),
|
||||
offline: t('orderManagement.paymentMethod.offline')
|
||||
}
|
||||
return methodMap[method] || method
|
||||
}
|
||||
@@ -758,25 +743,6 @@
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'is_expired',
|
||||
label: '超时状态',
|
||||
width: 100,
|
||||
formatter: (row: Order) => {
|
||||
if (row.is_expired === undefined || row.is_expired === null) return '-'
|
||||
if (row.is_expired) {
|
||||
return h(ElTag, { type: 'danger', size: 'small' }, () => '已超时')
|
||||
} else {
|
||||
return h(ElTag, { type: 'success', size: 'small' }, () => '未超时')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'expires_at',
|
||||
label: '超时时间',
|
||||
width: 180,
|
||||
formatter: (row: Order) => (row.expires_at ? formatDateTime(row.expires_at) : '-')
|
||||
},
|
||||
{
|
||||
prop: 'total_amount',
|
||||
label: t('orderManagement.table.totalAmount'),
|
||||
@@ -863,7 +829,6 @@
|
||||
payment_status: searchForm.payment_status,
|
||||
order_type: searchForm.order_type,
|
||||
purchase_role: searchForm.purchase_role,
|
||||
is_expired: searchForm.is_expired,
|
||||
start_time: searchForm.start_time || undefined,
|
||||
end_time: searchForm.end_time || undefined
|
||||
}
|
||||
@@ -1009,20 +974,41 @@
|
||||
if (valid) {
|
||||
createLoading.value = true
|
||||
try {
|
||||
// 获取资源ID (IoT卡ID或设备ID)
|
||||
const resourceId =
|
||||
createForm.order_type === 'single_card' ? createForm.iot_card_id : createForm.device_id
|
||||
|
||||
if (!resourceId) {
|
||||
ElMessage.error('请选择IoT卡或设备')
|
||||
return
|
||||
// 获取 identifier (ICCID 或 VirtualNo)
|
||||
let identifier = ''
|
||||
if (createForm.order_type === 'single_card') {
|
||||
if (!createForm.iot_card_id) {
|
||||
ElMessage.error('请选择IoT卡')
|
||||
return
|
||||
}
|
||||
const selectedCard = iotCardOptions.value.find(
|
||||
(card) => card.id === createForm.iot_card_id
|
||||
)
|
||||
if (!selectedCard?.iccid) {
|
||||
ElMessage.error('未找到选中的卡信息')
|
||||
return
|
||||
}
|
||||
identifier = selectedCard.iccid
|
||||
} else {
|
||||
if (!createForm.device_id) {
|
||||
ElMessage.error('请选择设备')
|
||||
return
|
||||
}
|
||||
const selectedDevice = deviceOptions.value.find(
|
||||
(dev) => dev.id === createForm.device_id
|
||||
)
|
||||
if (!selectedDevice?.virtual_no) {
|
||||
ElMessage.error('未找到选中的设备信息')
|
||||
return
|
||||
}
|
||||
identifier = selectedDevice.virtual_no
|
||||
}
|
||||
|
||||
// 调用套餐购买预检接口
|
||||
const checkData: PurchaseCheckRequest = {
|
||||
order_type: createForm.order_type,
|
||||
package_ids: createForm.package_ids,
|
||||
resource_id: resourceId
|
||||
resource_id: createForm.order_type === 'single_card' ? createForm.iot_card_id! : createForm.device_id!,
|
||||
package_ids: createForm.package_ids
|
||||
}
|
||||
|
||||
const checkResponse = await OrderService.purchaseCheck(checkData)
|
||||
@@ -1045,11 +1031,9 @@
|
||||
|
||||
// 预检通过或用户确认后,创建订单
|
||||
const data: CreateOrderRequest = {
|
||||
order_type: createForm.order_type,
|
||||
identifier,
|
||||
package_ids: createForm.package_ids,
|
||||
iot_card_id: createForm.order_type === 'single_card' ? createForm.iot_card_id : null,
|
||||
device_id: createForm.order_type === 'device' ? createForm.device_id : null,
|
||||
payment_method: createForm.payment_method // 必填字段
|
||||
payment_method: createForm.payment_method
|
||||
}
|
||||
|
||||
await OrderService.createOrder(data)
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
import { PackageManageService } from '@/api/modules'
|
||||
import type { PackageResponse } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getStatusLabel, getShelfStatusText } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'PackageDetail' })
|
||||
|
||||
@@ -55,7 +56,7 @@
|
||||
{
|
||||
label: '套餐类型',
|
||||
formatter: (_, data) => {
|
||||
const typeMap = {
|
||||
const typeMap: Record<string, string> = {
|
||||
formal: '正式套餐',
|
||||
addon: '附加套餐'
|
||||
}
|
||||
@@ -72,7 +73,7 @@
|
||||
label: '套餐周期类型',
|
||||
formatter: (_, data) => {
|
||||
if (!data.calendar_type) return '-'
|
||||
const typeMap = {
|
||||
const typeMap: Record<string, string> = {
|
||||
natural_month: '自然月',
|
||||
by_day: '按天'
|
||||
}
|
||||
@@ -90,7 +91,7 @@
|
||||
label: '流量重置周期',
|
||||
formatter: (_, data) => {
|
||||
if (!data.data_reset_cycle) return '-'
|
||||
const cycleMap = {
|
||||
const cycleMap: Record<string, string> = {
|
||||
daily: '每日',
|
||||
monthly: '每月',
|
||||
yearly: '每年',
|
||||
@@ -109,13 +110,13 @@
|
||||
{
|
||||
label: '状态',
|
||||
formatter: (_, data) => {
|
||||
return data.status === 1 ? '启用' : '禁用'
|
||||
return getStatusLabel(data.status ?? 0)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '上架状态',
|
||||
formatter: (_, data) => {
|
||||
return data.shelf_status === 1 ? '上架' : '下架'
|
||||
return getShelfStatusText(data.shelf_status ?? 0)
|
||||
}
|
||||
},
|
||||
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
|
||||
|
||||
@@ -34,29 +34,15 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -204,14 +190,20 @@
|
||||
<!-- 真流量额度 -->
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="真流量额度(MB)" prop="real_data_mb">
|
||||
<ElFormItem label="真流量额度(GB)" prop="real_data_gb">
|
||||
<ElInputNumber
|
||||
v-model="form.real_data_mb"
|
||||
v-model="realDataGb"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入真流量额度"
|
||||
@change="handleRealDataChange"
|
||||
/>
|
||||
<div v-if="realDataGb" style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
{{ realDataGb }}GB = {{ form.real_data_mb }}MB
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
@@ -228,14 +220,20 @@
|
||||
<!-- 虚流量额度和到期计时基准 -->
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12" v-if="form.enable_virtual_data">
|
||||
<ElFormItem label="虚流量额度(MB)" prop="virtual_data_mb">
|
||||
<ElFormItem label="虚流量额度(GB)" prop="virtual_data_gb">
|
||||
<ElInputNumber
|
||||
v-model="form.virtual_data_mb"
|
||||
v-model="virtualDataGb"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入虚流量额度"
|
||||
@change="handleVirtualDataChange"
|
||||
/>
|
||||
<div v-if="virtualDataGb" style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
{{ virtualDataGb }}GB = {{ form.virtual_data_mb }}MB
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
@@ -363,16 +361,13 @@
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { PackageManageService, PackageSeriesService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton, ElInputNumber } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElInputNumber } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { PackageResponse, SeriesSelectOption } from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import {
|
||||
@@ -382,13 +377,17 @@
|
||||
apiStatusToFrontend,
|
||||
PACKAGE_TYPE_OPTIONS,
|
||||
getPackageTypeLabel,
|
||||
getPackageTypeTag
|
||||
getPackageTypeTag,
|
||||
STATUS_SELECT_OPTIONS,
|
||||
SHELF_STATUS_SELECT_OPTIONS,
|
||||
getShelfStatusText
|
||||
} from '@/config/constants'
|
||||
import { generatePackageCode } from '@/utils/codeGenerator'
|
||||
|
||||
defineOptions({ name: 'PackageList' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
@@ -397,8 +396,6 @@
|
||||
const seriesLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<PackageResponse | null>(null)
|
||||
const seriesOptions = ref<SeriesSelectOption[]>([])
|
||||
const searchSeriesOptions = ref<SeriesSelectOption[]>([])
|
||||
|
||||
@@ -419,14 +416,6 @@
|
||||
})
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
package_name: '',
|
||||
@@ -490,10 +479,7 @@
|
||||
clearable: true,
|
||||
placeholder: '请选择上架状态'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '上架', value: 1 },
|
||||
{ label: '下架', value: 2 }
|
||||
]
|
||||
options: SHELF_STATUS_SELECT_OPTIONS
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
@@ -503,10 +489,7 @@
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 2 }
|
||||
]
|
||||
options: STATUS_SELECT_OPTIONS
|
||||
}
|
||||
])
|
||||
|
||||
@@ -559,7 +542,7 @@
|
||||
baseRules.virtual_data_mb = [
|
||||
{ required: true, message: '请输入虚流量额度', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
validator: (_rule: any, value: any, callback: any) => {
|
||||
if (value && form.real_data_mb && value > form.real_data_mb) {
|
||||
callback(new Error('虚流量额度不能超过真流量额度'))
|
||||
} else {
|
||||
@@ -602,6 +585,27 @@
|
||||
description: ''
|
||||
})
|
||||
|
||||
// GB 输入值(用于界面显示)
|
||||
const realDataGb = ref<number>(0)
|
||||
const virtualDataGb = ref<number>(0)
|
||||
|
||||
// GB 转 MB 处理
|
||||
const handleRealDataChange = (value: number | null) => {
|
||||
if (value === null || value === undefined) {
|
||||
form.real_data_mb = 0
|
||||
return
|
||||
}
|
||||
form.real_data_mb = Math.round(value * 1024)
|
||||
}
|
||||
|
||||
const handleVirtualDataChange = (value: number | null) => {
|
||||
if (value === null || value === undefined) {
|
||||
form.virtual_data_mb = 0
|
||||
return
|
||||
}
|
||||
form.virtual_data_mb = Math.round(value * 1024)
|
||||
}
|
||||
|
||||
const packageList = ref<PackageResponse[]>([])
|
||||
const dialogType = ref('add')
|
||||
|
||||
@@ -619,14 +623,19 @@
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PackageResponse) => {
|
||||
const hasPermission = hasAuth('package:detail')
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
style: hasPermission
|
||||
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
|
||||
: '',
|
||||
onClick: hasPermission
|
||||
? (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
row.package_name
|
||||
)
|
||||
@@ -652,13 +661,29 @@
|
||||
prop: 'real_data_mb',
|
||||
label: '真流量',
|
||||
width: 120,
|
||||
formatter: (row: PackageResponse) => `${row.real_data_mb}MB`
|
||||
formatter: (row: PackageResponse) => {
|
||||
const mb = row.real_data_mb ?? 0
|
||||
if (mb >= 1024) {
|
||||
return `${(mb / 1024).toFixed(2)}GB`
|
||||
}
|
||||
return `${mb}MB`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'virtual_data_mb',
|
||||
label: '虚流量',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => `${row.virtual_data_mb}MB`
|
||||
width: 120,
|
||||
formatter: (row: PackageResponse) => {
|
||||
// 如果没有启用虚流量,显示 "-"
|
||||
if (!row.enable_virtual_data) {
|
||||
return '-'
|
||||
}
|
||||
const mb = row.virtual_data_mb ?? 0
|
||||
if (mb >= 1024) {
|
||||
return `${(mb / 1024).toFixed(2)}GB`
|
||||
}
|
||||
return `${mb}MB`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'virtual_ratio',
|
||||
@@ -666,8 +691,10 @@
|
||||
width: 120,
|
||||
formatter: (row: PackageResponse) => {
|
||||
// 如果启用虚流量且虚流量大于0,计算比例
|
||||
if (row.enable_virtual_data && row.virtual_data_mb > 0) {
|
||||
const ratio = row.real_data_mb / row.virtual_data_mb
|
||||
const virtualData = row.virtual_data_mb ?? 0
|
||||
const realData = row.real_data_mb ?? 0
|
||||
if (row.enable_virtual_data && virtualData > 0) {
|
||||
const ratio = realData / virtualData
|
||||
return ratio.toFixed(2)
|
||||
}
|
||||
// 否则返回 1.0
|
||||
@@ -684,14 +711,16 @@
|
||||
prop: 'cost_price',
|
||||
label: '成本价',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => `¥${(row.cost_price / 100).toFixed(2)}`
|
||||
formatter: (row: PackageResponse) => `¥${((row.cost_price ?? 0) / 100).toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'suggested_retail_price',
|
||||
label: '建议售价',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) =>
|
||||
row.suggested_retail_price ? `¥${(row.suggested_retail_price / 100).toFixed(2)}` : '-'
|
||||
row.suggested_retail_price
|
||||
? `¥${((row.suggested_retail_price ?? 0) / 100).toFixed(2)}`
|
||||
: '-'
|
||||
},
|
||||
{
|
||||
prop: 'shelf_status',
|
||||
@@ -700,8 +729,8 @@
|
||||
formatter: (row: PackageResponse) => {
|
||||
return h(ElSwitch, {
|
||||
modelValue: row.shelf_status === 1,
|
||||
activeText: '上架',
|
||||
inactiveText: '下架',
|
||||
activeText: getShelfStatusText(1),
|
||||
inactiveText: getShelfStatusText(2),
|
||||
inlinePrompt: true,
|
||||
disabled: !hasAuth('package:update_away'),
|
||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||
@@ -714,7 +743,7 @@
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => {
|
||||
const frontendStatus = apiStatusToFrontend(row.status)
|
||||
const frontendStatus = apiStatusToFrontend(row.status ?? 0)
|
||||
return h(ElSwitch, {
|
||||
modelValue: frontendStatus,
|
||||
activeValue: CommonStatus.ENABLED,
|
||||
@@ -736,33 +765,29 @@
|
||||
}
|
||||
])
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
// 操作列配置
|
||||
const getActions = (row: PackageResponse) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('package:edit')) {
|
||||
items.push({
|
||||
key: 'edit',
|
||||
label: '编辑'
|
||||
})
|
||||
actions.push({ label: '编辑', handler: () => showDialog('edit', row), type: 'primary' })
|
||||
}
|
||||
|
||||
if (hasAuth('package:update_retail_price')) {
|
||||
items.push({
|
||||
key: 'update-retail-price',
|
||||
label: '修改零售价'
|
||||
// 只有代理账号(user_type=3)才显示修改零售价
|
||||
if (hasAuth('package:update_retail_price') && userStore.getUserInfo.user_type === 3) {
|
||||
actions.push({
|
||||
label: '修改零售价',
|
||||
handler: () => showRetailPriceDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('package:delete')) {
|
||||
items.push({
|
||||
key: 'delete',
|
||||
label: '删除'
|
||||
})
|
||||
actions.push({ label: '删除', handler: () => deletePackage(row), type: 'danger' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
return actions
|
||||
}
|
||||
|
||||
// 监听虚流量开关变化,关闭时重置虚流量额度
|
||||
watch(
|
||||
@@ -950,7 +975,10 @@
|
||||
form.expiry_base = row.expiry_base || 'from_activation'
|
||||
form.real_data_mb = row.real_data_mb || 0
|
||||
form.virtual_data_mb = row.virtual_data_mb || 0
|
||||
form.cost_price = row.cost_price / 100 // 分转换为元显示
|
||||
// MB 转换为 GB 显示
|
||||
realDataGb.value = form.real_data_mb ? Number((form.real_data_mb / 1024).toFixed(2)) : 0
|
||||
virtualDataGb.value = form.virtual_data_mb ? Number((form.virtual_data_mb / 1024).toFixed(2)) : 0
|
||||
form.cost_price = (row.cost_price ?? 0) / 100 // 分转换为元显示
|
||||
form.suggested_retail_price = row.suggested_retail_price
|
||||
? row.suggested_retail_price / 100
|
||||
: undefined
|
||||
@@ -969,6 +997,9 @@
|
||||
form.expiry_base = 'from_activation'
|
||||
form.real_data_mb = 0
|
||||
form.virtual_data_mb = 0
|
||||
// 重置 GB 值
|
||||
realDataGb.value = 0
|
||||
virtualDataGb.value = 0
|
||||
form.cost_price = 0
|
||||
form.suggested_retail_price = undefined
|
||||
form.description = ''
|
||||
@@ -1008,6 +1039,9 @@
|
||||
form.expiry_base = 'from_activation'
|
||||
form.real_data_mb = 0
|
||||
form.virtual_data_mb = 0
|
||||
// 重置 GB 值
|
||||
realDataGb.value = 0
|
||||
virtualDataGb.value = 0
|
||||
form.cost_price = 0
|
||||
form.suggested_retail_price = undefined
|
||||
form.description = ''
|
||||
@@ -1147,20 +1181,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: PackageResponse, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 显示零售价修改对话框
|
||||
const showRetailPriceDialog = (row: PackageResponse) => {
|
||||
retailPriceForm.id = row.id
|
||||
retailPriceForm.package_name = row.package_name
|
||||
// 将分转换为元显示
|
||||
retailPriceForm.retail_price = row.retail_price ? row.retail_price / 100 : 0
|
||||
retailPriceForm.retail_price = row.suggested_retail_price ? row.suggested_retail_price / 100 : 0
|
||||
retailPriceDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -1177,7 +1203,7 @@
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('零售价修改成功')
|
||||
retailPriceDialogVisible.value = false
|
||||
loadPackageList()
|
||||
await getTableData()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('修改零售价失败:', error)
|
||||
@@ -1192,34 +1218,9 @@
|
||||
retailPriceDialogVisible.value = false
|
||||
retailPriceFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showDialog('edit', currentRow.value)
|
||||
break
|
||||
case 'update-retail-price':
|
||||
showRetailPriceDialog(currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
deletePackage(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.package-list-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
import { PackageSeriesService } from '@/api/modules'
|
||||
import type { PackageSeriesResponse } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getEnableStatusText } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'PackageSeriesDetail' })
|
||||
|
||||
@@ -72,7 +73,7 @@
|
||||
{
|
||||
label: '启用状态',
|
||||
formatter: (_, data) => {
|
||||
return data.one_time_commission_config?.enable ? '已启用' : '未启用'
|
||||
return getEnableStatusText(data.one_time_commission_config?.enable || false)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -151,7 +152,9 @@
|
||||
{
|
||||
label: '启用状态',
|
||||
formatter: (_, data) => {
|
||||
return data.one_time_commission_config?.enable_force_recharge ? '已启用' : '未启用'
|
||||
return getEnableStatusText(
|
||||
data.one_time_commission_config?.enable_force_recharge || false
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -180,7 +183,7 @@
|
||||
formatter: (_, data) => {
|
||||
const config = data.one_time_commission_config
|
||||
if (!config?.validity_type) return '-'
|
||||
const typeMap = {
|
||||
const typeMap: Record<string, string> = {
|
||||
permanent: '永久',
|
||||
fixed_date: '固定日期',
|
||||
relative: '相对时长'
|
||||
|
||||
@@ -35,29 +35,15 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 套餐系列操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="seriesOperationMenuRef"
|
||||
:menu-items="seriesOperationMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleSeriesOperationMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -404,18 +390,16 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
CommonStatus,
|
||||
getStatusText,
|
||||
frontendStatusToApi,
|
||||
apiStatusToFrontend
|
||||
apiStatusToFrontend,
|
||||
STATUS_SELECT_OPTIONS,
|
||||
ENABLE_STATUS_OPTIONS,
|
||||
getEnableStatusText
|
||||
} from '@/config/constants'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -430,15 +414,11 @@
|
||||
const tableRef = ref()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 右键菜单
|
||||
const seriesOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentOperatingSeries = ref<PackageSeriesResponse | null>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
series_name: '',
|
||||
status: undefined as number | undefined,
|
||||
enable_one_time_commission: undefined as boolean | undefined
|
||||
status: undefined,
|
||||
enable_one_time_commission: undefined
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
@@ -463,10 +443,7 @@
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 2 }
|
||||
]
|
||||
options: STATUS_SELECT_OPTIONS
|
||||
},
|
||||
{
|
||||
label: '一次性佣金',
|
||||
@@ -476,10 +453,7 @@
|
||||
clearable: true,
|
||||
placeholder: '请选择'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '已启用', value: true },
|
||||
{ label: '未启用', value: false }
|
||||
]
|
||||
options: ENABLE_STATUS_OPTIONS
|
||||
}
|
||||
]
|
||||
|
||||
@@ -557,14 +531,19 @@
|
||||
label: '系列名称',
|
||||
minWidth: 150,
|
||||
formatter: (row: PackageSeriesResponse) => {
|
||||
const hasPermission = hasAuth('package_series:detail')
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
style: hasPermission
|
||||
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
|
||||
: '',
|
||||
onClick: hasPermission
|
||||
? (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
row.series_name
|
||||
)
|
||||
@@ -582,10 +561,10 @@
|
||||
return h(
|
||||
ElTag,
|
||||
{
|
||||
type: row.one_time_commission_config.enable ? 'success' : 'info',
|
||||
type: row.one_time_commission_config?.enable ? 'success' : 'info',
|
||||
size: 'small'
|
||||
},
|
||||
() => (row.one_time_commission_config.enable ? '已启用' : '未启用')
|
||||
() => getEnableStatusText(row.one_time_commission_config?.enable || false)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -641,10 +620,10 @@
|
||||
return h(
|
||||
ElTag,
|
||||
{
|
||||
type: row.one_time_commission_config.enable_force_recharge ? 'warning' : 'info',
|
||||
type: row.one_time_commission_config?.enable_force_recharge ? 'warning' : 'info',
|
||||
size: 'small'
|
||||
},
|
||||
() => (row.one_time_commission_config.enable_force_recharge ? '已启用' : '未启用')
|
||||
() => getEnableStatusText(row.one_time_commission_config?.enable_force_recharge || false)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -738,63 +717,22 @@
|
||||
}
|
||||
])
|
||||
|
||||
// 操作列配置
|
||||
const getActions = (row: PackageSeriesResponse) => {
|
||||
const actions: any[] = []
|
||||
if (hasAuth('package_series:edit')) {
|
||||
actions.push({ label: '编辑', handler: () => showDialog('edit', row), type: 'primary' })
|
||||
}
|
||||
if (hasAuth('package_series:delete')) {
|
||||
actions.push({ label: '删除', handler: () => deleteSeries(row), type: 'danger' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
// 套餐系列操作菜单项配置
|
||||
const seriesOperationMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
// 编辑
|
||||
if (hasAuth('package_series:edit')) {
|
||||
items.push({
|
||||
key: 'edit',
|
||||
label: '编辑'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除
|
||||
if (hasAuth('package_series:delete')) {
|
||||
items.push({
|
||||
key: 'delete',
|
||||
label: '删除'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 显示套餐系列操作右键菜单
|
||||
const showSeriesOperationMenu = (e: MouseEvent, row: PackageSeriesResponse) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
currentOperatingSeries.value = row
|
||||
seriesOperationMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: PackageSeriesResponse, column: any, event: MouseEvent) => {
|
||||
// 如果用户有编辑或删除权限,显示右键菜单
|
||||
if (hasAuth('package_series:edit') || hasAuth('package_series:delete')) {
|
||||
showSeriesOperationMenu(event, row)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理套餐系列操作菜单选择
|
||||
const handleSeriesOperationMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentOperatingSeries.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showDialog('edit', currentOperatingSeries.value)
|
||||
break
|
||||
case 'delete':
|
||||
deleteSeries(currentOperatingSeries.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 获取套餐系列列表
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
@@ -1121,27 +1059,10 @@
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
}
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-series-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="启用状态">
|
||||
<ElTag :type="detailData.force_recharge_enabled ? 'warning' : 'info'">
|
||||
{{ detailData.force_recharge_enabled ? '已启用' : '未启用' }}
|
||||
{{ getEnableStatusText(detailData.force_recharge_enabled || false) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="锁定状态">
|
||||
@@ -314,6 +314,7 @@
|
||||
PackageSeriesResponse
|
||||
} from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getEnableStatusText } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'SeriesGrantsDetail' })
|
||||
|
||||
@@ -504,6 +505,7 @@
|
||||
const handleSavePackage = async () => {
|
||||
if (!packageFormRef.value || !detailData.value) return
|
||||
|
||||
const grantId = detailData.value.id // 保存到常量中避免类型检查问题
|
||||
await packageFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
@@ -525,7 +527,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { packages })
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId, { packages })
|
||||
ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
packageDialogVisible.value = false
|
||||
|
||||
|
||||
@@ -35,29 +35,16 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="160"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 套餐列表对话框 -->
|
||||
<ElDialog
|
||||
v-model="packageListDialogVisible"
|
||||
@@ -777,17 +764,14 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
CommonStatus,
|
||||
getStatusText,
|
||||
frontendStatusToApi,
|
||||
apiStatusToFrontend
|
||||
apiStatusToFrontend,
|
||||
STATUS_SELECT_OPTIONS,
|
||||
getEnableStatusText
|
||||
} from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'SeriesGrants' })
|
||||
@@ -803,8 +787,6 @@
|
||||
const packageLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<ShopSeriesGrantResponse | null>(null)
|
||||
const currentStep = ref(0) // 当前步骤,0 为第一步,1 为第二步
|
||||
|
||||
// 套餐列表对话框相关
|
||||
@@ -930,10 +912,7 @@
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 2 }
|
||||
]
|
||||
options: STATUS_SELECT_OPTIONS
|
||||
}
|
||||
])
|
||||
|
||||
@@ -1064,14 +1043,19 @@
|
||||
label: '系列名称',
|
||||
minWidth: 150,
|
||||
formatter: (row: ShopSeriesGrantResponse) => {
|
||||
const hasPermission = hasAuth('series_name_grants:detail')
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
style: hasPermission
|
||||
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
|
||||
: '',
|
||||
onClick: hasPermission
|
||||
? (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
row.series_name
|
||||
)
|
||||
@@ -1137,7 +1121,7 @@
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.force_recharge_enabled ? 'warning' : 'info', size: 'small' },
|
||||
() => (row.force_recharge_enabled ? '已启用' : '未启用')
|
||||
() => getEnableStatusText(row.force_recharge_enabled || false)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -1173,6 +1157,29 @@
|
||||
}
|
||||
])
|
||||
|
||||
// 操作列配置
|
||||
const getActions = (row: ShopSeriesGrantResponse) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('series_grants:detail')) {
|
||||
actions.push({
|
||||
label: '套餐列表',
|
||||
handler: () => showPackageListDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('series_grants:edit')) {
|
||||
actions.push({ label: '编辑', handler: () => showDialog('edit', row), type: 'primary' })
|
||||
}
|
||||
|
||||
if (hasAuth('series_grants:delete')) {
|
||||
actions.push({ label: '删除', handler: () => deleteAllocation(row), type: 'danger' })
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
// 构建树形结构数据
|
||||
const buildTreeData = (items: ShopResponse[]) => {
|
||||
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
|
||||
@@ -1865,50 +1872,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('series_grants:detail')) {
|
||||
items.push({ key: 'view_packages', label: '查看套餐列表' })
|
||||
}
|
||||
|
||||
if (hasAuth('series_grants:edit')) {
|
||||
items.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
|
||||
if (hasAuth('series_grants:delete')) {
|
||||
items.push({ key: 'delete', label: '删除' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: ShopSeriesGrantResponse, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'view_packages':
|
||||
showPackageListDialog(currentRow.value)
|
||||
break
|
||||
case 'edit':
|
||||
showDialog('edit', currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
deleteAllocation(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 显示套餐列表对话框
|
||||
const showPackageListDialog = async (row: ShopSeriesGrantResponse) => {
|
||||
currentGrantId.value = row.id
|
||||
@@ -2108,14 +2071,6 @@
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 下一步
|
||||
const handleNextStep = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
:total="logPagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style=" justify-content: flex-end;margin-top: 16px"
|
||||
style="justify-content: flex-end; margin-top: 16px"
|
||||
@size-change="getCleanupLogs"
|
||||
@current-change="getCleanupLogs"
|
||||
/>
|
||||
|
||||
@@ -34,22 +34,17 @@
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="false"
|
||||
:pagination="false"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="160"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -105,7 +100,7 @@
|
||||
<ElFormItem label="所在地区" prop="region">
|
||||
<ElCascader
|
||||
v-model="formData.region"
|
||||
:options="regionData"
|
||||
:options="regionData as any"
|
||||
placeholder="请选择省/市/区"
|
||||
clearable
|
||||
filterable
|
||||
@@ -219,14 +214,6 @@
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 店铺操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="shopOperationMenuRef"
|
||||
:menu-items="shopOperationMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleShopOperationMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 店铺默认角色管理对话框 -->
|
||||
<ElDialog
|
||||
v-model="defaultRolesDialogVisible"
|
||||
@@ -303,22 +290,17 @@
|
||||
ElTreeSelect,
|
||||
ElCascader
|
||||
} from 'element-plus'
|
||||
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
|
||||
import { ShopService, RoleService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { ShopResponse, ShopRoleResponse } from '@/types/api'
|
||||
import { RoleType } from '@/types/api'
|
||||
import { RoleType, RoleStatus } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { regionData } from '@/utils/constants/regionData'
|
||||
|
||||
defineOptions({ name: 'Shop' })
|
||||
@@ -326,15 +308,6 @@
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -353,10 +326,6 @@
|
||||
const availableRoles = ref<ShopRoleResponse[]>([])
|
||||
const selectedRoleId = ref<number | undefined>(undefined)
|
||||
|
||||
// 右键菜单
|
||||
const shopOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentOperatingShop = ref<ShopResponse | null>(null)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
shop_name: '',
|
||||
@@ -511,7 +480,7 @@
|
||||
}
|
||||
|
||||
// 再次确保清除验证状态
|
||||
nextTick(() => {
|
||||
await nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
|
||||
@@ -602,29 +571,48 @@
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: ShopResponse) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 110,
|
||||
fixed: 'right',
|
||||
formatter: (row: ShopResponse) => {
|
||||
const buttons = []
|
||||
|
||||
if (hasAuth('shop:look_customer')) {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '账号列表',
|
||||
onClick: () => viewCustomerAccounts(row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 操作列配置
|
||||
const getActions = (row: ShopResponse) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('shop:look_customer')) {
|
||||
actions.push({
|
||||
label: '账号列表',
|
||||
handler: () => viewCustomerAccounts(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('shop:default_role')) {
|
||||
actions.push({
|
||||
label: '默认角色',
|
||||
handler: () => showDefaultRolesDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('shop:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => showDialog('edit', row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('shop:delete')) {
|
||||
actions.push({
|
||||
label: '删除',
|
||||
handler: () => deleteShop(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
@@ -658,8 +646,8 @@
|
||||
}
|
||||
|
||||
// 处理地区选择变化
|
||||
const handleRegionChange = (value: string[]) => {
|
||||
if (value && value.length === 3) {
|
||||
const handleRegionChange = (value: any) => {
|
||||
if (value && Array.isArray(value) && value.length === 3) {
|
||||
formData.province = value[0]
|
||||
formData.city = value[1]
|
||||
formData.district = value[2]
|
||||
@@ -677,8 +665,8 @@
|
||||
const params: any = {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
role_type: 2, // 仅客户角色
|
||||
status: 1 // 仅启用的角色
|
||||
role_type: RoleType.CUSTOMER, // 仅客户角色
|
||||
status: RoleStatus.ENABLED // 仅启用的角色
|
||||
}
|
||||
if (query) {
|
||||
params.role_name = query
|
||||
@@ -900,70 +888,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 店铺操作菜单项配置
|
||||
const shopOperationMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
// 默认角色
|
||||
if (hasAuth('shop:default_role')) {
|
||||
items.push({
|
||||
key: 'defaultRoles',
|
||||
label: '默认角色'
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑
|
||||
if (hasAuth('shop:edit')) {
|
||||
items.push({
|
||||
key: 'edit',
|
||||
label: '编辑'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除
|
||||
if (hasAuth('shop:delete')) {
|
||||
items.push({
|
||||
key: 'delete',
|
||||
label: '删除'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 显示店铺操作右键菜单
|
||||
const showShopOperationMenu = (e: MouseEvent, row: ShopResponse) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
currentOperatingShop.value = row
|
||||
shopOperationMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: ShopResponse, column: any, event: MouseEvent) => {
|
||||
// 如果用户有编辑或删除权限,显示右键菜单
|
||||
if (hasAuth('shop:edit') || hasAuth('shop:delete')) {
|
||||
showShopOperationMenu(event, row)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理店铺操作菜单选择
|
||||
const handleShopOperationMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentOperatingShop.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'defaultRoles':
|
||||
showDefaultRolesDialog(currentOperatingShop.value)
|
||||
break
|
||||
case 'edit':
|
||||
showDialog('edit', currentOperatingShop.value)
|
||||
break
|
||||
case 'delete':
|
||||
deleteShop(currentOperatingShop.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
const handleStatusChange = async (row: ShopResponse, newStatus: number) => {
|
||||
const oldStatus = row.status
|
||||
@@ -1016,9 +940,9 @@
|
||||
try {
|
||||
const res = await RoleService.getRoles({
|
||||
page: 1,
|
||||
page_size: 9999,
|
||||
role_type: 2, // 仅客户角色
|
||||
status: 1 // RoleStatus.ENABLED
|
||||
page_size: 100,
|
||||
role_type: RoleType.CUSTOMER, // 仅客户角色
|
||||
status: RoleStatus.ENABLED // 仅启用的角色
|
||||
})
|
||||
if (res.code === 0) {
|
||||
// 将 PlatformRole 转换为与 ShopRoleResponse 兼容的格式
|
||||
@@ -1071,10 +995,6 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shop-page {
|
||||
// 店铺管理页面样式
|
||||
}
|
||||
|
||||
.current-role-display {
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 24px;
|
||||
@@ -1116,8 +1036,4 @@
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -33,29 +33,14 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -163,18 +148,13 @@
|
||||
<script setup lang="ts">
|
||||
import { h, watch, nextTick } from 'vue'
|
||||
import { CarrierService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { Carrier, CarrierType } from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
CommonStatus,
|
||||
@@ -191,17 +171,6 @@
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
@@ -388,6 +357,45 @@
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: any) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'actions',
|
||||
label: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
formatter: (row: any) => {
|
||||
const buttons: any[] = []
|
||||
|
||||
if (hasAuth('carrier:edit')) {
|
||||
buttons.push(
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => showDialog('edit', row)
|
||||
},
|
||||
() => '编辑'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (hasAuth('carrier:delete')) {
|
||||
buttons.push(
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'danger',
|
||||
link: true,
|
||||
onClick: () => deleteCarrier(row)
|
||||
},
|
||||
() => '删除'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -574,51 +582,10 @@
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('carrier:edit')) {
|
||||
items.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
|
||||
if (hasAuth('carrier:delete')) {
|
||||
items.push({ key: 'delete', label: '删除' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showDialog('edit', currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
deleteCarrier(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.carrier-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -1,661 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="menu-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
:showZebra="false"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
|
||||
<ElButton v-auth="'add'" @click="showModel('menu', null, true)" v-ripple>
|
||||
添加菜单
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
|
||||
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
|
||||
添加菜单
|
||||
</ElButton> -->
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
rowKey="path"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data="filteredTableData"
|
||||
:marginTop="10"
|
||||
:stripe="false"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
|
||||
<ElFormItem label="菜单类型">
|
||||
<ElRadioGroup v-model="labelPosition" :disabled="disableMenuType">
|
||||
<ElRadioButton value="menu" label="menu">菜单</ElRadioButton>
|
||||
<ElRadioButton value="button" label="button">权限</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<template v-if="labelPosition === 'menu'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="菜单名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="菜单名称"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="路由地址" prop="path">
|
||||
<ElInput v-model="form.path" placeholder="路由地址"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限标识" prop="label">
|
||||
<ElInput v-model="form.label" placeholder="权限标识"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="图标" prop="icon">
|
||||
<ArtIconSelector :iconType="iconType" :defaultIcon="form.icon" width="229px" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="菜单排序" prop="sort" style="width: 100%">
|
||||
<ElInputNumber
|
||||
v-model="form.sort"
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="外部链接" prop="link">
|
||||
<ElInput
|
||||
v-model="form.link"
|
||||
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
|
||||
></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否启用" prop="isEnable">
|
||||
<ElSwitch v-model="form.isEnable"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="页面缓存" prop="keepAlive">
|
||||
<ElSwitch v-model="form.keepAlive"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否显示" prop="isHidden">
|
||||
<ElSwitch v-model="form.isHidden"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否内嵌" prop="isMenu">
|
||||
<ElSwitch v-model="form.isIframe"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<template v-if="labelPosition === 'button'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限名称" prop="authName">
|
||||
<ElInput v-model="form.authName" placeholder="权限名称"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限标识" prop="authLabel">
|
||||
<ElInput v-model="form.authLabel" placeholder="权限标识"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限排序" prop="authSort" style="width: 100%">
|
||||
<ElInputNumber
|
||||
v-model="form.authSort"
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取 消</ElButton>
|
||||
<ElButton type="primary" @click="submitForm()">确 定</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { IconTypeEnum } from '@/enums/appEnum'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { ElPopover, ElButton } from 'element-plus'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'Menus' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const { menuList } = storeToRefs(useMenuStore())
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
route: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 增加实际应用的搜索条件状态
|
||||
const appliedFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
Object.assign(appliedFilters, { ...initialSearchState })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 将当前输入的筛选条件应用到实际搜索
|
||||
Object.assign(appliedFilters, { ...formFilters })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '菜单名称',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '路由地址',
|
||||
prop: 'route',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '用户名', prop: 'avatar' },
|
||||
{ label: '手机号', prop: 'mobile' },
|
||||
{ label: '性别', prop: 'sex' },
|
||||
{ label: '部门', prop: 'dep' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建日期', prop: 'create_time' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 构建菜单类型标签
|
||||
const buildMenuTypeTag = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return 'info'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return 'success'
|
||||
} else if (row.path) {
|
||||
return 'primary'
|
||||
} else if (row.meta?.link) {
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单类型文本
|
||||
const buildMenuTypeText = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return '目录'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return '内嵌'
|
||||
} else if (row.path) {
|
||||
return '菜单'
|
||||
} else if (row.meta?.link) {
|
||||
return '外链'
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'meta.title',
|
||||
label: '菜单名称',
|
||||
minWidth: 120,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return formatMenuTitle(row.meta?.title)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'type',
|
||||
label: '菜单类型',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'path',
|
||||
label: '路由',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return row.meta?.link || row.path || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'meta.authList',
|
||||
label: '可操作权限',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
|
||||
return h(
|
||||
ElPopover,
|
||||
{
|
||||
placement: 'top-start',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
trigger: 'click',
|
||||
key: index
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h('div', { style: 'margin: 0; text-align: right' }, [
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => showModel('button', item)
|
||||
},
|
||||
{ default: () => '编辑' }
|
||||
),
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'danger',
|
||||
onClick: () => deleteAuth()
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
)
|
||||
]),
|
||||
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'date',
|
||||
label: '编辑时间',
|
||||
formatter: () => '2022-3-12 12:00:00'
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '隐藏菜单',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
|
||||
row.meta.isHide ? '是' : '否'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h('div', [
|
||||
hasAuth('B_CODE1') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'add',
|
||||
onClick: () => showModel('menu')
|
||||
}),
|
||||
hasAuth('B_CODE2') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
hasAuth('B_CODE3') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteMenu()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const form = reactive({
|
||||
// 菜单
|
||||
name: '',
|
||||
path: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
isEnable: true,
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHidden: true,
|
||||
link: '',
|
||||
isIframe: false,
|
||||
// 权限 (修改这部分)
|
||||
authName: '',
|
||||
authLabel: '',
|
||||
authIcon: '',
|
||||
authSort: 1
|
||||
})
|
||||
const iconType = ref(IconTypeEnum.UNICODE)
|
||||
|
||||
const labelPosition = ref('menu')
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }],
|
||||
label: [{ required: true, message: '输入权限标识', trigger: 'blur' }],
|
||||
// 修改这部分
|
||||
authName: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
|
||||
authLabel: [{ required: true, message: '请输入权限权限标识', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const tableData = ref<AppRouteRecord[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
const getTableData = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
tableData.value = menuList.value
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 过滤后的表格数据
|
||||
const filteredTableData = computed(() => {
|
||||
// 递归搜索函数
|
||||
const searchMenu = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||
return items.filter((item) => {
|
||||
// 获取搜索关键词,转换为小写并去除首尾空格
|
||||
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
|
||||
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
|
||||
|
||||
// 获取菜单标题和路径,确保它们存在
|
||||
const menuTitle = formatMenuTitle(item.meta?.title || '').toLowerCase()
|
||||
const menuPath = (item.path || '').toLowerCase()
|
||||
|
||||
// 使用 includes 进行模糊匹配
|
||||
const nameMatch = !searchName || menuTitle.includes(searchName)
|
||||
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
|
||||
|
||||
// 如果有子菜单,递归搜索
|
||||
if (item.children && item.children.length > 0) {
|
||||
const matchedChildren = searchMenu(item.children)
|
||||
// 如果子菜单有匹配项,保留当前菜单
|
||||
if (matchedChildren.length > 0) {
|
||||
item.children = matchedChildren
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return nameMatch && routeMatch
|
||||
})
|
||||
}
|
||||
|
||||
return searchMenu(tableData.value)
|
||||
})
|
||||
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogTitle = computed(() => {
|
||||
const type = labelPosition.value === 'menu' ? '菜单' : '权限'
|
||||
return isEdit.value ? `编辑${type}` : `新建${type}`
|
||||
})
|
||||
|
||||
const showDialog = (type: string, row: AppRouteRecord) => {
|
||||
showModel('menu', row, true)
|
||||
}
|
||||
|
||||
const handleChange = () => {}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// const menuStore = useMenuStore()
|
||||
// const params =
|
||||
// labelPosition.value === 'menu'
|
||||
// ? {
|
||||
// title: form.name,
|
||||
// path: form.path,
|
||||
// name: form.label,
|
||||
// icon: form.icon,
|
||||
// sort: form.sort,
|
||||
// isEnable: form.isEnable,
|
||||
// isMenu: form.isMenu,
|
||||
// keepAlive: form.keepAlive,
|
||||
// isHidden: form.isHidden,
|
||||
// link: form.link
|
||||
// }
|
||||
// : {
|
||||
// title: form.authName,
|
||||
// name: form.authLabel,
|
||||
// icon: form.authIcon,
|
||||
// sort: form.authSort
|
||||
// }
|
||||
|
||||
if (isEdit.value) {
|
||||
// await menuStore.updateMenu(params)
|
||||
} else {
|
||||
// await menuStore.addMenu(params)
|
||||
}
|
||||
|
||||
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
|
||||
dialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showModel = (type: string, row?: any, lock: boolean = false) => {
|
||||
dialogVisible.value = true
|
||||
labelPosition.value = type
|
||||
isEdit.value = false
|
||||
lockMenuType.value = lock
|
||||
resetForm()
|
||||
|
||||
if (row) {
|
||||
isEdit.value = true
|
||||
nextTick(() => {
|
||||
// 回显数据
|
||||
if (type === 'menu') {
|
||||
// 菜单数据回显
|
||||
form.name = formatMenuTitle(row.meta.title)
|
||||
form.path = row.path
|
||||
form.label = row.name
|
||||
form.icon = row.meta.icon
|
||||
form.sort = row.meta.sort || 1
|
||||
form.isMenu = row.meta.isMenu
|
||||
form.keepAlive = row.meta.keepAlive
|
||||
form.isHidden = row.meta.isHidden || true
|
||||
form.isEnable = row.meta.isEnable || true
|
||||
form.link = row.meta.link
|
||||
form.isIframe = row.meta.isIframe || false
|
||||
} else {
|
||||
// 权限按钮数据回显
|
||||
form.authName = row.title
|
||||
form.authLabel = row.auth_mark
|
||||
form.authIcon = row.icon || ''
|
||||
form.authSort = row.sort || 1
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
Object.assign(form, {
|
||||
// 菜单
|
||||
name: '',
|
||||
path: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHidden: true,
|
||||
link: '',
|
||||
isIframe: false,
|
||||
// 权限
|
||||
authName: '',
|
||||
authLabel: '',
|
||||
authIcon: '',
|
||||
authSort: 1
|
||||
})
|
||||
}
|
||||
|
||||
const deleteMenu = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAuth = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改计算属性,增加锁定控制参数
|
||||
const disableMenuType = computed(() => {
|
||||
// 编辑权限时锁定为权限类型
|
||||
if (isEdit.value && labelPosition.value === 'button') return true
|
||||
// 编辑菜单时锁定为菜单类型
|
||||
if (isEdit.value && labelPosition.value === 'menu') return true
|
||||
// 顶部添加菜单按钮时锁定为菜单类型
|
||||
if (!isEdit.value && labelPosition.value === 'menu' && lockMenuType.value) return true
|
||||
return false
|
||||
})
|
||||
|
||||
// 添加一个控制变量
|
||||
const lockMenuType = ref(false)
|
||||
|
||||
const isExpanded = ref(false)
|
||||
const tableRef = ref()
|
||||
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-page {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
:deep(.small-btn) {
|
||||
height: 30px !important;
|
||||
padding: 0 10px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-2-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-3-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-3-2-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -31,27 +31,12 @@
|
||||
:default-expand-all="false"
|
||||
:marginTop="10"
|
||||
:show-pagination="false"
|
||||
:row-class-name="getRowClassName"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -134,6 +119,7 @@
|
||||
ElTag,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElButton,
|
||||
type FormInstance,
|
||||
type FormRules
|
||||
} from 'element-plus'
|
||||
@@ -142,11 +128,6 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import {
|
||||
PermissionType,
|
||||
PERMISSION_TYPE_OPTIONS,
|
||||
@@ -222,19 +203,8 @@
|
||||
const tableRef = ref()
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const currentRow = ref<PermissionTreeNode | null>(null)
|
||||
const currentPermissionId = ref<number>(0)
|
||||
const submitLoading = ref(false)
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 表单引用和数据
|
||||
const formRef = ref<FormInstance>()
|
||||
@@ -342,6 +312,45 @@
|
||||
prop: 'sort',
|
||||
label: '排序',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'actions',
|
||||
label: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
formatter: (row: PermissionTreeNode) => {
|
||||
const buttons: any[] = []
|
||||
|
||||
if (hasAuth('permission:edit')) {
|
||||
buttons.push(
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => showDialog('edit', row)
|
||||
},
|
||||
() => '编辑'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (hasAuth('permission:delete')) {
|
||||
buttons.push(
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'danger',
|
||||
link: true,
|
||||
onClick: () => deletePermission(row)
|
||||
},
|
||||
() => '删除'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -444,7 +453,6 @@
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
currentRow.value = row
|
||||
currentPermissionId.value = row.id
|
||||
// 从API获取完整的权限数据(包含parent_id)
|
||||
try {
|
||||
@@ -555,47 +563,6 @@
|
||||
onMounted(() => {
|
||||
getPermissionList()
|
||||
})
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('permission:edit')) {
|
||||
items.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
|
||||
if (hasAuth('permission:delete')) {
|
||||
items.push({ key: 'delete', label: '删除' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: PermissionTreeNode, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showDialog('edit', currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
deletePermission(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -32,29 +32,14 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -80,8 +65,12 @@
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
>
|
||||
<ElOption label="平台角色" :value="1" />
|
||||
<ElOption label="客户角色" :value="2" />
|
||||
<ElOption
|
||||
v-for="item in ROLE_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
@@ -240,13 +229,13 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||
import {
|
||||
CommonStatus,
|
||||
getStatusText,
|
||||
ROLE_TYPE_OPTIONS,
|
||||
getRoleTypeLabel
|
||||
} from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'Role' })
|
||||
|
||||
@@ -272,17 +261,6 @@
|
||||
const leftTreeFilter = ref('') // 左侧树搜索关键字
|
||||
const rightTreeFilter = ref('') // 右侧树搜索关键字
|
||||
const isHandlingCheck = ref(false) // 标志位:是否正在处理勾选事件
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<PlatformRole | null>(null)
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
@@ -313,10 +291,7 @@
|
||||
clearable: true,
|
||||
placeholder: '请选择角色类型'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '平台角色', value: 1 },
|
||||
{ label: '客户角色', value: 2 }
|
||||
]
|
||||
options: () => ROLE_TYPE_OPTIONS
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
@@ -342,7 +317,6 @@
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: 'ID', prop: 'ID' },
|
||||
{ label: '角色名称', prop: 'role_name' },
|
||||
{ label: '角色描述', prop: 'role_desc' },
|
||||
{ label: '角色类型', prop: 'role_type' },
|
||||
@@ -373,34 +347,25 @@
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'ID',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'role_name',
|
||||
label: '角色名称',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'role_desc',
|
||||
label: '角色描述'
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'role_type',
|
||||
label: '角色类型',
|
||||
minWidth: 120,
|
||||
width: 100,
|
||||
formatter: (row: any) => {
|
||||
return h(ElTag, { type: row.role_type === 1 ? 'primary' : 'success' }, () =>
|
||||
row.role_type === 1 ? '平台角色' : '客户角色'
|
||||
getRoleTypeLabel(row.role_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
minWidth: 100,
|
||||
width: 100,
|
||||
formatter: (row: any) => {
|
||||
return h(ElSwitch, {
|
||||
modelValue: row.status,
|
||||
@@ -415,11 +380,68 @@
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'role_desc',
|
||||
label: '角色描述'
|
||||
},
|
||||
{
|
||||
prop: 'CreatedAt',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: any) => formatDateTime(row.CreatedAt)
|
||||
},
|
||||
{
|
||||
prop: 'actions',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
formatter: (row: any) => {
|
||||
const buttons: any[] = []
|
||||
|
||||
if (hasAuth('role:permission')) {
|
||||
buttons.push(
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => showPermissionDialog(row)
|
||||
},
|
||||
() => '分配权限'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (hasAuth('role:edit')) {
|
||||
buttons.push(
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => showDialog('edit', row)
|
||||
},
|
||||
() => '编辑'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (hasAuth('role:delete')) {
|
||||
buttons.push(
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'danger',
|
||||
link: true,
|
||||
onClick: () => deleteRole(row)
|
||||
},
|
||||
() => '删除'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -442,33 +464,6 @@
|
||||
return data.label.toLowerCase().includes(value.toLowerCase())
|
||||
}
|
||||
|
||||
// 获取节点的所有子节点ID(包括子菜单和按钮)
|
||||
const getAllChildrenIds = (node: any): number[] => {
|
||||
const ids: number[] = []
|
||||
const traverse = (n: any) => {
|
||||
if (n.children && n.children.length > 0) {
|
||||
n.children.forEach((child: any) => {
|
||||
ids.push(child.id)
|
||||
traverse(child)
|
||||
})
|
||||
}
|
||||
}
|
||||
traverse(node)
|
||||
return ids
|
||||
}
|
||||
|
||||
// 在树数据中查找节点
|
||||
const findNodeInTree = (treeData: any[], nodeId: number): any => {
|
||||
for (const node of treeData) {
|
||||
if (node.id === nodeId) return node
|
||||
if (node.children && node.children.length > 0) {
|
||||
const found = findNodeInTree(node.children, nodeId)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 更新父节点的勾选状态(计算应该半选的父节点)
|
||||
const updateParentCheckStatus = (checkedKeys: number[]) => {
|
||||
const checkedSet = new Set(checkedKeys)
|
||||
@@ -502,14 +497,19 @@
|
||||
const currentNodeChecked = checkedSet.has(node.id)
|
||||
|
||||
// 判断父节点应该显示的状态:
|
||||
// 1. 如果有子节点被勾选(someChecked=true),但不是全部子节点被勾选或当前节点未被勾选 -> 半选
|
||||
// 2. 如果所有子节点都被勾选且当前节点也被勾选 -> 全选
|
||||
if (someChecked) {
|
||||
if (!allChecked || !currentNodeChecked) {
|
||||
// 子节点部分被勾选,或者当前节点未被勾选 -> 半选
|
||||
// 1. 如果当前节点被勾选,但不是所有子节点都被勾选 -> 半选
|
||||
// 2. 如果有子节点被勾选,但当前节点未被勾选 -> 半选
|
||||
// 3. 如果所有子节点都被勾选且当前节点也被勾选 -> 全选
|
||||
if (currentNodeChecked) {
|
||||
// 当前节点被勾选
|
||||
if (!allChecked) {
|
||||
// 但不是所有子节点都被勾选 -> 半选
|
||||
parentIdsToHalfCheck.add(node.id)
|
||||
}
|
||||
// 如果 allChecked && currentNodeChecked,则为全选,不需要设置半选
|
||||
// 如果 allChecked,则为全选,不需要设置半选
|
||||
} else if (someChecked) {
|
||||
// 当前节点未勾选,但有子节点被勾选 -> 半选
|
||||
parentIdsToHalfCheck.add(node.id)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -524,6 +524,34 @@
|
||||
return Array.from(parentIdsToHalfCheck)
|
||||
}
|
||||
|
||||
// 判断节点是否为一级菜单(没有父节点或parent_id为0/null)
|
||||
const isFirstLevelMenu = (nodeId: number): boolean => {
|
||||
const node = allPermissionsMap.value.get(nodeId)
|
||||
if (!node) return false
|
||||
return !node.parent_id || node.parent_id === 0
|
||||
}
|
||||
|
||||
// 获取节点的所有子节点ID(递归)
|
||||
const getAllChildrenIds = (nodeId: number): number[] => {
|
||||
const childrenIds: number[] = []
|
||||
|
||||
const collectChildren = (id: number) => {
|
||||
const node = allPermissionsMap.value.get(id)
|
||||
if (!node || !node.children || node.children.length === 0) return
|
||||
|
||||
node.children.forEach((child: any) => {
|
||||
childrenIds.push(child.id)
|
||||
collectChildren(child.id)
|
||||
})
|
||||
}
|
||||
|
||||
collectChildren(nodeId)
|
||||
return childrenIds
|
||||
}
|
||||
|
||||
// 上一次勾选的keys,用于判断是勾选还是取消勾选
|
||||
const previousCheckedKeys = ref<number[]>([])
|
||||
|
||||
// 处理左侧树的勾选事件
|
||||
const handleLeftTreeCheck = (data: any, checked: any) => {
|
||||
if (isHandlingCheck.value) return
|
||||
@@ -532,7 +560,52 @@
|
||||
|
||||
try {
|
||||
// 获取当前勾选的keys
|
||||
const currentChecked = checked.checkedKeys as number[]
|
||||
let currentChecked = checked.checkedKeys as number[]
|
||||
|
||||
// 判断是勾选还是取消勾选(通过对比当前和之前的keys)
|
||||
const isChecking =
|
||||
currentChecked.includes(data.id) && !previousCheckedKeys.value.includes(data.id)
|
||||
const isUnchecking =
|
||||
!currentChecked.includes(data.id) && previousCheckedKeys.value.includes(data.id)
|
||||
|
||||
if (isChecking) {
|
||||
// 正在勾选节点
|
||||
// 判断当前节点是否为一级菜单
|
||||
if (isFirstLevelMenu(data.id)) {
|
||||
// 一级菜单:自动勾选所有子节点(包括所有子菜单和按钮)
|
||||
const childrenIds = getAllChildrenIds(data.id)
|
||||
const newCheckedKeys = [...new Set([...currentChecked, ...childrenIds])]
|
||||
|
||||
// 设置新的勾选状态
|
||||
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
|
||||
currentChecked = newCheckedKeys
|
||||
} else {
|
||||
// 二级及以下菜单/按钮:只勾选自己,不勾选子节点
|
||||
const childrenIds = getAllChildrenIds(data.id)
|
||||
// 从当前勾选中移除所有子节点(如果有的话)
|
||||
const newCheckedKeys = currentChecked.filter((id) => !childrenIds.includes(id))
|
||||
|
||||
// 设置新的勾选状态
|
||||
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
|
||||
currentChecked = newCheckedKeys
|
||||
}
|
||||
} else if (isUnchecking) {
|
||||
// 正在取消勾选节点
|
||||
// 判断当前节点是否为一级菜单
|
||||
if (isFirstLevelMenu(data.id)) {
|
||||
// 一级菜单:自动取消所有子节点(包括所有子菜单和按钮)
|
||||
const childrenIds = getAllChildrenIds(data.id)
|
||||
const newCheckedKeys = currentChecked.filter((id) => !childrenIds.includes(id))
|
||||
|
||||
// 设置新的勾选状态
|
||||
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
|
||||
currentChecked = newCheckedKeys
|
||||
}
|
||||
// 二级及以下菜单/按钮:直接取消勾选,不需要额外处理
|
||||
}
|
||||
|
||||
// 更新上一次勾选的keys
|
||||
previousCheckedKeys.value = [...currentChecked]
|
||||
|
||||
// 计算应该半选的父节点
|
||||
const halfCheckedIds = updateParentCheckStatus(currentChecked)
|
||||
@@ -626,23 +699,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 获取树中所有节点的ID(包括父节点和子节点)
|
||||
const getAllNodeIds = (treeNodes: any[]): number[] => {
|
||||
const ids: number[] = []
|
||||
|
||||
const traverse = (nodes: any[]) => {
|
||||
nodes.forEach((node) => {
|
||||
ids.push(node.id)
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
traverse(treeNodes)
|
||||
return ids
|
||||
}
|
||||
|
||||
// 保存原始权限树数据
|
||||
const originalPermissionTree = ref<PermissionTreeNode[]>([])
|
||||
|
||||
@@ -765,6 +821,7 @@
|
||||
|
||||
// 清空左侧树的勾选状态
|
||||
leftTreeRef.value?.setCheckedKeys([], false)
|
||||
previousCheckedKeys.value = []
|
||||
|
||||
ElMessage.success('权限添加成功')
|
||||
} catch (error) {
|
||||
@@ -842,6 +899,7 @@
|
||||
}
|
||||
selectedPermissions.value = []
|
||||
originalPermissions.value = []
|
||||
previousCheckedKeys.value = []
|
||||
leftTreeFilter.value = ''
|
||||
rightTreeFilter.value = ''
|
||||
|
||||
@@ -999,6 +1057,7 @@
|
||||
const data = {
|
||||
role_name: form.role_name,
|
||||
role_desc: form.role_desc,
|
||||
role_type: form.role_type,
|
||||
status: form.status
|
||||
}
|
||||
await RoleService.updateRole(form.id, data)
|
||||
@@ -1031,57 +1090,9 @@
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('role:permission')) {
|
||||
items.push({ key: 'assignPermission', label: '分配权限' })
|
||||
}
|
||||
|
||||
if (hasAuth('role:edit')) {
|
||||
items.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
|
||||
if (hasAuth('role:delete')) {
|
||||
items.push({ key: 'delete', label: '删除' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'assignPermission':
|
||||
showPermissionDialog(currentRow.value)
|
||||
break
|
||||
case 'edit':
|
||||
showDialog('edit', currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
deleteRole(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user