This commit is contained in:
@@ -56,6 +56,7 @@
|
||||
"axios": "1.13.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"echarts": "^5.4.0",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "2.11.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlight.js": "^11.10.0",
|
||||
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -32,6 +32,9 @@ importers:
|
||||
echarts:
|
||||
specifier: ^5.4.0
|
||||
version: 5.5.1
|
||||
element-china-area-data:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
element-plus:
|
||||
specifier: 2.11.3
|
||||
version: 2.11.3(vue@3.5.15(typescript@5.6.3))
|
||||
@@ -1638,6 +1641,9 @@ packages:
|
||||
chardet@0.7.0:
|
||||
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
|
||||
|
||||
china-division@2.7.0:
|
||||
resolution: {integrity: sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
@@ -2056,6 +2062,9 @@ packages:
|
||||
echarts@5.5.1:
|
||||
resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==}
|
||||
|
||||
element-china-area-data@6.1.0:
|
||||
resolution: {integrity: sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==}
|
||||
|
||||
element-plus@2.11.3:
|
||||
resolution: {integrity: sha512-769xsjLR4B9Vf9cl5PDXnwTEdmFJvMgAkYtthdJKPhjVjU3hdAwTJ+gXKiO+PUyo2KWFwOYKZd4Ywh6PHfkbJg==}
|
||||
peerDependencies:
|
||||
@@ -6519,6 +6528,8 @@ snapshots:
|
||||
|
||||
chardet@0.7.0: {}
|
||||
|
||||
china-division@2.7.0: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
@@ -6987,6 +6998,10 @@ snapshots:
|
||||
tslib: 2.3.0
|
||||
zrender: 5.6.0
|
||||
|
||||
element-china-area-data@6.1.0:
|
||||
dependencies:
|
||||
china-division: 2.7.0
|
||||
|
||||
element-plus@2.11.3(vue@3.5.15(typescript@5.6.3)):
|
||||
dependencies:
|
||||
'@ctrl/tinycolor': 3.6.1
|
||||
|
||||
@@ -6,21 +6,18 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
AssetType,
|
||||
AssetResolveResponse,
|
||||
AssetRealtimeStatusResponse,
|
||||
AssetRefreshResponse,
|
||||
AssetPackageUsageRecord,
|
||||
AssetPackageListResponse,
|
||||
AssetPackageParams,
|
||||
AssetCurrentPackageResponse,
|
||||
DeviceStopResponse,
|
||||
DeviceStartResponse,
|
||||
CardStopResponse,
|
||||
CardStartResponse,
|
||||
AssetWalletTransactionListResponse,
|
||||
AssetWalletTransactionParams,
|
||||
AssetWalletResponse
|
||||
AssetWalletResponse,
|
||||
AssetOrdersParams,
|
||||
AssetOrdersResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class AssetService extends BaseService {
|
||||
@@ -36,148 +33,150 @@ export class AssetService extends BaseService {
|
||||
|
||||
/**
|
||||
* 读取资产实时状态(直接读 DB/Redis,不调网关)
|
||||
* GET /api/admin/assets/:asset_type/:id/realtime-status
|
||||
* @param assetType 资产类型 (card 或 device)
|
||||
* @param id 资产ID
|
||||
* GET /api/admin/assets/:identifier/realtime-status
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
*/
|
||||
static getRealtimeStatus(
|
||||
assetType: AssetType,
|
||||
id: number
|
||||
): Promise<BaseResponse<AssetRealtimeStatusResponse>> {
|
||||
static getRealtimeStatus(identifier: string): Promise<BaseResponse<AssetRealtimeStatusResponse>> {
|
||||
return this.getOne<AssetRealtimeStatusResponse>(
|
||||
`/api/admin/assets/${assetType}/${id}/realtime-status`
|
||||
`/api/admin/assets/${identifier}/realtime-status`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动调网关拉取最新数据后返回
|
||||
* POST /api/admin/assets/:asset_type/:id/refresh
|
||||
* POST /api/admin/assets/:identifier/refresh
|
||||
* 注意:设备有 30 秒冷却期,冷却中调用返回 429
|
||||
* @param assetType 资产类型 (card 或 device)
|
||||
* @param id 资产ID
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
*/
|
||||
static refreshAsset(
|
||||
assetType: AssetType,
|
||||
id: number
|
||||
): Promise<BaseResponse<AssetRefreshResponse>> {
|
||||
static refreshAsset(identifier: string): Promise<BaseResponse<AssetRefreshResponse>> {
|
||||
return this.post<BaseResponse<AssetRefreshResponse>>(
|
||||
`/api/admin/assets/${assetType}/${id}/refresh`,
|
||||
`/api/admin/assets/${identifier}/refresh`,
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询该资产所有套餐记录,含虚流量换算字段(分页)
|
||||
* GET /api/admin/assets/:asset_type/:id/packages?page=1&page_size=50
|
||||
* @param assetType 资产类型 (card 或 device)
|
||||
* @param id 资产ID
|
||||
* GET /api/admin/assets/:identifier/packages?page=1&page_size=50
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
* @param params 查询参数(可选分页参数)
|
||||
*/
|
||||
static getAssetPackages(
|
||||
assetType: AssetType,
|
||||
id: number,
|
||||
identifier: string,
|
||||
params?: AssetPackageParams
|
||||
): Promise<BaseResponse<AssetPackageListResponse>> {
|
||||
return this.get<BaseResponse<AssetPackageListResponse>>(
|
||||
`/api/admin/assets/${assetType}/${id}/packages`,
|
||||
`/api/admin/assets/${identifier}/packages`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前生效中的主套餐
|
||||
* GET /api/admin/assets/:asset_type/:id/current-package
|
||||
* GET /api/admin/assets/:identifier/current-package
|
||||
* 无生效套餐时返回 404
|
||||
* @param assetType 资产类型 (card 或 device)
|
||||
* @param id 资产ID
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
*/
|
||||
static getCurrentPackage(
|
||||
assetType: AssetType,
|
||||
id: number
|
||||
): Promise<BaseResponse<AssetCurrentPackageResponse>> {
|
||||
static getCurrentPackage(identifier: string): Promise<BaseResponse<AssetCurrentPackageResponse>> {
|
||||
return this.getOne<AssetCurrentPackageResponse>(
|
||||
`/api/admin/assets/${assetType}/${id}/current-package`
|
||||
`/api/admin/assets/${identifier}/current-package`
|
||||
)
|
||||
}
|
||||
|
||||
// ========== 设备停复机操作 ==========
|
||||
// ========== 资产停复机操作(统一接口)==========
|
||||
|
||||
/**
|
||||
* 批量停机设备下所有已实名卡
|
||||
* POST /api/admin/assets/device/:device_id/stop
|
||||
* 停机资产(卡或设备)
|
||||
* POST /api/admin/assets/:identifier/stop
|
||||
* 停机成功后设置 1 小时停机保护期(保护期内禁止复机)
|
||||
* @param deviceId 设备ID
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
*/
|
||||
static stopDevice(deviceId: number): Promise<BaseResponse<DeviceStopResponse>> {
|
||||
return this.post<BaseResponse<DeviceStopResponse>>(
|
||||
`/api/admin/assets/device/${deviceId}/stop`,
|
||||
static stopAsset(identifier: string): Promise<BaseResponse<DeviceStopResponse | void>> {
|
||||
return this.post<BaseResponse<DeviceStopResponse | void>>(
|
||||
`/api/admin/assets/${identifier}/stop`,
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量复机设备下所有已实名卡
|
||||
* POST /api/admin/assets/device/:device_id/start
|
||||
* 复机资产(卡或设备)
|
||||
* POST /api/admin/assets/:identifier/start
|
||||
* 复机成功后设置 1 小时复机保护期(保护期内禁止停机)
|
||||
* @param deviceId 设备ID
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
*/
|
||||
static startDevice(deviceId: number): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/assets/device/${deviceId}/start`, {})
|
||||
static startAsset(identifier: string): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/assets/${identifier}/start`, {})
|
||||
}
|
||||
|
||||
// ========== 单卡停复机操作 ==========
|
||||
// ========== 资产停用 ==========
|
||||
|
||||
/**
|
||||
* 手动停机单张卡(通过 ICCID)
|
||||
* POST /api/admin/assets/card/:iccid/stop
|
||||
* 若卡绑定的设备在复机保护期内,返回 403
|
||||
* @param iccid ICCID
|
||||
* 手动停用资产(卡或设备)
|
||||
* PATCH /api/admin/assets/:identifier/deactivate
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
*/
|
||||
static stopCard(iccid: string): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/assets/card/${iccid}/stop`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动复机单张卡(通过 ICCID)
|
||||
* POST /api/admin/assets/card/:iccid/start
|
||||
* 若卡绑定的设备在停机保护期内,返回 403
|
||||
* @param iccid ICCID
|
||||
*/
|
||||
static startCard(iccid: string): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/assets/card/${iccid}/start`, {})
|
||||
static deactivateAsset(identifier: string): Promise<BaseResponse> {
|
||||
return this.patch<BaseResponse>(`/api/admin/assets/${identifier}/deactivate`, {})
|
||||
}
|
||||
|
||||
// ========== 钱包查询 ==========
|
||||
|
||||
/**
|
||||
* 查询指定卡或设备的钱包余额概况
|
||||
* GET /api/admin/assets/:asset_type/:id/wallet
|
||||
* GET /api/admin/assets/:identifier/wallet
|
||||
* 企业账号禁止调用
|
||||
* @param assetType 资产类型 (card 或 device)
|
||||
* @param id 资产ID
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
*/
|
||||
static getAssetWallet(
|
||||
assetType: AssetType,
|
||||
id: number
|
||||
): Promise<BaseResponse<AssetWalletResponse>> {
|
||||
return this.getOne<AssetWalletResponse>(`/api/admin/assets/${assetType}/${id}/wallet`)
|
||||
static getAssetWallet(identifier: string): Promise<BaseResponse<AssetWalletResponse>> {
|
||||
return this.getOne<AssetWalletResponse>(`/api/admin/assets/${identifier}/wallet`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询指定资产的钱包收支流水
|
||||
* GET /api/admin/assets/:asset_type/:id/wallet/transactions
|
||||
* GET /api/admin/assets/:identifier/wallet/transactions
|
||||
* 企业账号禁止调用
|
||||
* @param assetType 资产类型 (card 或 device)
|
||||
* @param id 资产ID
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getWalletTransactions(
|
||||
assetType: AssetType,
|
||||
id: number,
|
||||
identifier: string,
|
||||
params?: AssetWalletTransactionParams
|
||||
): Promise<BaseResponse<AssetWalletTransactionListResponse>> {
|
||||
return this.get<BaseResponse<AssetWalletTransactionListResponse>>(
|
||||
`/api/admin/assets/${assetType}/${id}/wallet/transactions`,
|
||||
`/api/admin/assets/${identifier}/wallet/transactions`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
// ========== 轮询状态更新 ==========
|
||||
|
||||
/**
|
||||
* 更新资产轮询状态
|
||||
* PATCH /api/admin/assets/:identifier/polling-status
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
* @param data 轮询状态数据
|
||||
*/
|
||||
static updatePollingStatus(
|
||||
identifier: string,
|
||||
data: { enable_polling: boolean }
|
||||
): Promise<BaseResponse> {
|
||||
return this.patch<BaseResponse>(`/api/admin/assets/${identifier}/polling-status`, data)
|
||||
}
|
||||
|
||||
// ========== 资产历史订单 ==========
|
||||
|
||||
/**
|
||||
* 查询资产历史订单
|
||||
* GET /api/admin/assets/:identifier/orders
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getAssetOrders(
|
||||
identifier: string,
|
||||
params?: AssetOrdersParams
|
||||
): Promise<BaseResponse<AssetOrdersResponse>> {
|
||||
return this.get<BaseResponse<AssetOrdersResponse>>(
|
||||
`/api/admin/assets/${identifier}/orders`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,45 +56,48 @@ export class DeviceService extends BaseService {
|
||||
|
||||
/**
|
||||
* 删除设备
|
||||
* @param id 设备ID
|
||||
* @param virtualNo 设备虚拟号
|
||||
*/
|
||||
static deleteDevice(id: number): Promise<BaseResponse> {
|
||||
return this.remove(`/api/admin/devices/${id}`)
|
||||
static deleteDevice(virtualNo: string): Promise<BaseResponse> {
|
||||
return this.remove(`/api/admin/devices/${virtualNo}`)
|
||||
}
|
||||
|
||||
// ========== 设备卡绑定管理 ==========
|
||||
|
||||
/**
|
||||
* 获取设备绑定的卡列表
|
||||
* @param id 设备ID
|
||||
* @param virtualNo 设备虚拟号
|
||||
*/
|
||||
static getDeviceCards(id: number): Promise<BaseResponse<DeviceCardsResponse>> {
|
||||
return this.getOne<DeviceCardsResponse>(`/api/admin/devices/${id}/cards`)
|
||||
static getDeviceCards(virtualNo: string): Promise<BaseResponse<DeviceCardsResponse>> {
|
||||
return this.getOne<DeviceCardsResponse>(`/api/admin/devices/${virtualNo}/cards`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定卡到设备
|
||||
* @param id 设备ID
|
||||
* @param virtualNo 设备虚拟号
|
||||
* @param data 绑定参数
|
||||
*/
|
||||
static bindCard(
|
||||
id: number,
|
||||
virtualNo: string,
|
||||
data: BindCardToDeviceRequest
|
||||
): Promise<BaseResponse<BindCardToDeviceResponse>> {
|
||||
return this.post<BaseResponse<BindCardToDeviceResponse>>(`/api/admin/devices/${id}/cards`, data)
|
||||
return this.post<BaseResponse<BindCardToDeviceResponse>>(
|
||||
`/api/admin/devices/${virtualNo}/cards`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑设备上的卡
|
||||
* @param deviceId 设备ID
|
||||
* @param cardId IoT卡ID
|
||||
* @param virtualNo 设备虚拟号
|
||||
* @param iccid 卡ICCID
|
||||
*/
|
||||
static unbindCard(
|
||||
deviceId: number,
|
||||
cardId: number
|
||||
virtualNo: string,
|
||||
iccid: string
|
||||
): Promise<BaseResponse<UnbindCardFromDeviceResponse>> {
|
||||
return this.delete<BaseResponse<UnbindCardFromDeviceResponse>>(
|
||||
`/api/admin/devices/${deviceId}/cards/${cardId}`
|
||||
`/api/admin/devices/${virtualNo}/cards/${iccid}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -230,12 +233,4 @@ export class DeviceService extends BaseService {
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动停用设备
|
||||
* @param id 设备ID
|
||||
*/
|
||||
static deactivateDevice(id: number): Promise<BaseResponse> {
|
||||
return this.patch<BaseResponse>(`/api/admin/devices/${id}/deactivate`, {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,10 +66,10 @@ export class ExchangeService extends BaseService {
|
||||
static getExchanges(
|
||||
params?: ExchangeQueryParams
|
||||
): Promise<
|
||||
BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>
|
||||
BaseResponse<{ items: ExchangeResponse[]; page: number; page_size: number; total: number }>
|
||||
> {
|
||||
return this.get<
|
||||
BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>
|
||||
BaseResponse<{ items: ExchangeResponse[]; page: number; page_size: number; total: number }>
|
||||
>('/api/admin/exchanges', params)
|
||||
}
|
||||
|
||||
|
||||
@@ -279,19 +279,6 @@
|
||||
background-color: #fff !important;
|
||||
transform: scale(0.6) !important;
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 4px;
|
||||
margin: auto;
|
||||
border: 2px solid var(--el-checkbox-checked-icon-color);
|
||||
border-left: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,21 @@
|
||||
<!-- 动态列 -->
|
||||
<slot v-if="tableData.length"></slot>
|
||||
|
||||
<!-- 操作列固定在右侧 -->
|
||||
<el-table-column
|
||||
v-if="actions && tableData.length > 0"
|
||||
label="操作"
|
||||
:width="actionsWidth"
|
||||
fixed="right"
|
||||
align="left"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<component :is="renderActions(row)" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 空数据 -->
|
||||
<template #empty>
|
||||
<el-empty :description="emptyText" v-show="!loading" />
|
||||
@@ -78,10 +93,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElButton, ElDropdown, ElDropdownMenu, ElDropdownItem, ElIcon } from 'element-plus'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { useTableStore } from '@/store/modules/table'
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
|
||||
interface TableAction {
|
||||
label: string
|
||||
handler: (row: any) => void
|
||||
type?: 'primary' | 'danger'
|
||||
permission?: string
|
||||
}
|
||||
|
||||
interface TableProps {
|
||||
/** 表格数据源 */
|
||||
data?: any[]
|
||||
@@ -135,6 +160,10 @@
|
||||
treeProps?: { children?: string; hasChildren?: string }
|
||||
/** 是否默认展开所有行 */
|
||||
defaultExpandAll?: boolean
|
||||
/** 操作列配置 */
|
||||
actions?: ((row: any) => TableAction[]) | null
|
||||
/** 操作列宽度 */
|
||||
actionsWidth?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TableProps>(), {
|
||||
@@ -161,7 +190,9 @@
|
||||
showHeaderBackground: null,
|
||||
marginTop: 20,
|
||||
treeProps: undefined,
|
||||
defaultExpandAll: false
|
||||
defaultExpandAll: false,
|
||||
actions: null,
|
||||
actionsWidth: 140
|
||||
})
|
||||
|
||||
/*
|
||||
@@ -324,6 +355,78 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染操作列
|
||||
const renderActions = (row: any) => {
|
||||
if (!props.actions) return null
|
||||
|
||||
const actionList = props.actions(row)
|
||||
|
||||
// 如果操作数量 <= 2,直接显示所有按钮
|
||||
if (actionList.length <= 2) {
|
||||
return h(
|
||||
'div',
|
||||
{ style: 'display: flex; gap: 8px;' },
|
||||
actionList.map((action) =>
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: action.type === 'danger' ? 'danger' : 'primary',
|
||||
link: true,
|
||||
onClick: () => action.handler(row)
|
||||
},
|
||||
() => action.label
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// 如果操作数量 > 2,显示第一个 + 更多下拉菜单
|
||||
const firstAction = actionList[0]
|
||||
const moreActions = actionList.slice(1)
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 12px; align-items: center;' }, [
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: firstAction.type === 'danger' ? 'danger' : 'primary',
|
||||
link: true,
|
||||
onClick: () => firstAction.handler(row)
|
||||
},
|
||||
() => firstAction.label
|
||||
),
|
||||
h(
|
||||
ElDropdown,
|
||||
{
|
||||
onCommand: (handler: (row: any) => void) => handler(row)
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style:
|
||||
'display: flex; align-items: center; cursor: pointer; color: var(--el-color-primary);'
|
||||
},
|
||||
[h('span', '更多'), h(ElIcon, { style: 'margin-left: 4px;' }, () => h(ArrowDown))]
|
||||
),
|
||||
dropdown: () =>
|
||||
h(ElDropdownMenu, {}, () =>
|
||||
moreActions.map((action) =>
|
||||
h(
|
||||
ElDropdownItem,
|
||||
{
|
||||
command: () => action.handler(row),
|
||||
style: action.type === 'danger' ? 'color: var(--el-color-danger);' : ''
|
||||
},
|
||||
() => action.label
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
])
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -365,9 +468,15 @@
|
||||
}
|
||||
|
||||
// 解决el-image 和 el-table冲突层级冲突问题
|
||||
::v-deep(.el-table__cell) {
|
||||
// 只对包含图片的单元格应用,避免影响 fixed 列
|
||||
::v-deep(.el-table__cell:has(.el-image)) {
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
// 确保固定列能正常工作
|
||||
:deep(.el-table__fixed-right) {
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端分页
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="上行速率" prop="upload_speed">
|
||||
<ElInputNumber
|
||||
@@ -22,7 +22,7 @@
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="切换SIM卡" width="600px" @close="handleClose">
|
||||
<div v-if="loading" style=" padding: 40px;text-align: center">
|
||||
<div v-if="loading" style="padding: 40px; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
|
||||
</div>
|
||||
|
||||
@@ -185,6 +185,12 @@ export function useLogin() {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户类型 - 禁止企业账号登录
|
||||
if (response.data.user && response.data.user.user_type === 4) {
|
||||
ElMessage.error('企业账号无法登录后台管理系统')
|
||||
return
|
||||
}
|
||||
|
||||
// 保存 Token
|
||||
userStore.setToken(response.data.access_token, response.data.refresh_token)
|
||||
|
||||
|
||||
15
src/config/constants/enableStatus.ts
Normal file
15
src/config/constants/enableStatus.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 启用状态配置
|
||||
* 用于一次性佣金、强充等功能的启用/未启用状态
|
||||
*/
|
||||
|
||||
// 启用状态选项(用于搜索表单和下拉选择)
|
||||
export const ENABLE_STATUS_OPTIONS = [
|
||||
{ label: '已启用', value: true },
|
||||
{ label: '未启用', value: false }
|
||||
]
|
||||
|
||||
// 获取启用状态文本
|
||||
export function getEnableStatusText(enabled: boolean): string {
|
||||
return enabled ? '已启用' : '未启用'
|
||||
}
|
||||
@@ -25,3 +25,9 @@ export * from './carrierTypes'
|
||||
|
||||
// 套餐管理相关
|
||||
export * from './package'
|
||||
|
||||
// 角色类型相关
|
||||
export * from './roleTypes'
|
||||
|
||||
// 启用状态相关
|
||||
export * from './enableStatus'
|
||||
|
||||
@@ -132,6 +132,14 @@ export const SHELF_STATUS_OPTIONS = [
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 上架状态选项(精简版,用于搜索表单和下拉选择)
|
||||
*/
|
||||
export const SHELF_STATUS_SELECT_OPTIONS = [
|
||||
{ label: '上架', value: ShelfStatus.ON_SHELF },
|
||||
{ label: '下架', value: ShelfStatus.OFF_SHELF }
|
||||
]
|
||||
|
||||
/**
|
||||
* 上架状态映射
|
||||
*/
|
||||
|
||||
25
src/config/constants/roleTypes.ts
Normal file
25
src/config/constants/roleTypes.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 角色类型配置
|
||||
*/
|
||||
|
||||
import { RoleType } from '@/types/api'
|
||||
|
||||
// 角色类型选项
|
||||
export const ROLE_TYPE_OPTIONS = [
|
||||
{ label: '平台角色', value: RoleType.PLATFORM },
|
||||
{ label: '客户角色', value: RoleType.CUSTOMER }
|
||||
]
|
||||
|
||||
// 角色类型映射
|
||||
export const ROLE_TYPE_MAP = ROLE_TYPE_OPTIONS.reduce(
|
||||
(map, item) => {
|
||||
map[item.value] = item
|
||||
return map
|
||||
},
|
||||
{} as Record<RoleType, { label: string; value: RoleType }>
|
||||
)
|
||||
|
||||
// 获取角色类型标签
|
||||
export function getRoleTypeLabel(type: RoleType): string {
|
||||
return ROLE_TYPE_MAP[type]?.label || '未知'
|
||||
}
|
||||
@@ -348,6 +348,7 @@
|
||||
"title": "System Settings",
|
||||
"role": "Role Manage",
|
||||
"permission": "Permission Manage",
|
||||
"carrierManagement": "Carrier Management",
|
||||
"userCenter": "User Center",
|
||||
"menu": "Menu Manage",
|
||||
"nested": "Nested Menu",
|
||||
@@ -414,14 +415,9 @@
|
||||
"enterpriseCards": "Enterprise Card Management",
|
||||
"customerCommission": "Customer Commission"
|
||||
},
|
||||
"account": {
|
||||
"title": "Account Management",
|
||||
"customerAccount": "Customer Account",
|
||||
"withdrawal": "Commission Withdrawal",
|
||||
"withdrawalSettings": "Withdrawal Settings",
|
||||
"myAccount": "My Account",
|
||||
"carrierManagement": "Carrier Management",
|
||||
"orders": "Order Management",
|
||||
"orderManagement": {
|
||||
"title": "Order Management",
|
||||
"orderList": "Order List",
|
||||
"orderDetail": "Order Details"
|
||||
},
|
||||
"deviceManagement": {
|
||||
@@ -435,13 +431,17 @@
|
||||
"packageSeries": "Package Series Management",
|
||||
"packageList": "Package Management",
|
||||
"packageAssign": "Package Assignment",
|
||||
"shop": "Shop Management",
|
||||
"shopAccounts": "Shop Accounts"
|
||||
},
|
||||
"shopManagement": {
|
||||
"title": "Shop Management",
|
||||
"shopList": "Shop List"
|
||||
},
|
||||
"assetManagement": {
|
||||
"title": "Asset Management",
|
||||
"cardSearch": "IoT Card Query",
|
||||
"deviceSearch": "Device Search",
|
||||
"assetInformation": "Asset Information",
|
||||
"singleCard": "Single Card Info",
|
||||
"standaloneCardList": "IoT Card Management",
|
||||
"iotCardTask": "IoT Card Tasks",
|
||||
@@ -844,7 +844,8 @@
|
||||
"paymentMethod": {
|
||||
"wallet": "Wallet Payment",
|
||||
"wechat": "WeChat Payment",
|
||||
"alipay": "Alipay Payment"
|
||||
"alipay": "Alipay Payment",
|
||||
"offline": "Offline Payment"
|
||||
},
|
||||
"commissionStatus": {
|
||||
"notApplicable": "Not Applicable",
|
||||
|
||||
@@ -360,6 +360,7 @@
|
||||
"title": "系统管理",
|
||||
"role": "角色管理",
|
||||
"permission": "权限管理",
|
||||
"carrierManagement": "运营商管理",
|
||||
"userCenter": "个人中心",
|
||||
"menu": "菜单管理",
|
||||
"nested": "嵌套菜单",
|
||||
@@ -401,7 +402,7 @@
|
||||
"title": "套餐管理",
|
||||
"packageCreate": "新建套餐",
|
||||
"packageBatch": "批量管理",
|
||||
"packageList": "套餐管理",
|
||||
"packageList": "套餐列表",
|
||||
"packageDetail": "套餐详情",
|
||||
"packageChange": "套餐变更",
|
||||
"seriesGrants": "代理系列授权",
|
||||
@@ -429,15 +430,19 @@
|
||||
"simCard": "号卡管理",
|
||||
"simCardAssign": "号卡分配",
|
||||
"packageSeries": "套餐系列管理",
|
||||
"packageList": "套餐管理",
|
||||
"packageList": "套餐列表",
|
||||
"packageAssign": "套餐分配",
|
||||
"shop": "店铺管理",
|
||||
"shopAccounts": "店铺账号列表"
|
||||
},
|
||||
"shopManagement": {
|
||||
"title": "店铺管理",
|
||||
"shopList": "店铺列表"
|
||||
},
|
||||
"assetManagement": {
|
||||
"title": "资产管理",
|
||||
"cardSearch": "IoT卡查询",
|
||||
"deviceSearch": "设备查询",
|
||||
"assetInformation": "资产信息",
|
||||
"singleCard": "单卡信息",
|
||||
"standaloneCardList": "IoT卡管理",
|
||||
"iotCardDetail": "IoT卡详情",
|
||||
@@ -459,12 +464,9 @@
|
||||
"exchangeManagement": "换货管理",
|
||||
"exchangeDetail": "换货单详情"
|
||||
},
|
||||
"account": {
|
||||
"orderManagement": {
|
||||
"title": "订单管理",
|
||||
"customerAccount": "客户账号",
|
||||
"myAccount": "我的账户",
|
||||
"carrierManagement": "运营商管理",
|
||||
"orders": "订单管理",
|
||||
"orderList": "订单列表",
|
||||
"orderDetail": "订单详情"
|
||||
},
|
||||
"commission": {
|
||||
@@ -842,7 +844,8 @@
|
||||
"paymentMethod": {
|
||||
"wallet": "钱包支付",
|
||||
"wechat": "微信支付",
|
||||
"alipay": "支付宝支付"
|
||||
"alipay": "支付宝支付",
|
||||
"offline": "线下支付"
|
||||
},
|
||||
"commissionStatus": {
|
||||
"notApplicable": "不适用",
|
||||
|
||||
@@ -303,7 +303,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
name: 'CarrierManagement',
|
||||
component: RoutesAlias.CarrierManagement,
|
||||
meta: {
|
||||
title: 'menus.account.carrierManagement',
|
||||
title: 'menus.system.carrierManagement',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
@@ -790,7 +790,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
name: 'ShopManagement',
|
||||
component: RoutesAlias.Home,
|
||||
meta: {
|
||||
title: 'menus.product.shop',
|
||||
title: 'menus.shopManagement.title',
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
@@ -799,7 +799,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
name: 'Shop',
|
||||
component: RoutesAlias.Shop,
|
||||
meta: {
|
||||
title: 'menus.product.shop',
|
||||
title: 'menus.shopManagement.shopList',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
@@ -968,11 +968,11 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'single-card',
|
||||
name: 'SingleCard',
|
||||
component: RoutesAlias.SingleCard,
|
||||
path: 'asset-information',
|
||||
name: 'AssetInformation',
|
||||
component: RoutesAlias.AssetInformation,
|
||||
meta: {
|
||||
title: 'menus.cardManagement.singleCard',
|
||||
title: 'menus.assetManagement.assetInformation',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
@@ -1121,51 +1121,33 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
},
|
||||
|
||||
{
|
||||
path: '/account',
|
||||
name: 'Finance',
|
||||
path: '/order-management',
|
||||
name: 'OrderManagement',
|
||||
component: RoutesAlias.Home,
|
||||
meta: {
|
||||
title: 'menus.account.title',
|
||||
title: 'menus.orderManagement.title',
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
// {
|
||||
// path: 'customer-account',
|
||||
// name: 'CustomerAccountList',
|
||||
// component: RoutesAlias.CustomerAccountList,
|
||||
// meta: {
|
||||
// title: 'menus.account.customerAccount',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'OrderManagement',
|
||||
component: '/order-management/order-list',
|
||||
path: 'order-list',
|
||||
name: 'OrderList',
|
||||
component: RoutesAlias.OrderList,
|
||||
meta: {
|
||||
title: 'menus.account.orders',
|
||||
title: 'menus.orderManagement.orderList',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'orders/detail/:id',
|
||||
path: 'order-list/detail/:id',
|
||||
name: 'OrderDetail',
|
||||
component: '/order-management/order-list/detail',
|
||||
component: RoutesAlias.OrderDetail,
|
||||
meta: {
|
||||
title: 'menus.account.orderDetail',
|
||||
title: 'menus.orderManagement.orderDetail',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
}
|
||||
// {
|
||||
// path: 'my-account',
|
||||
// name: 'MyAccount',
|
||||
// component: RoutesAlias.MyAccount,
|
||||
// meta: {
|
||||
// title: 'menus.account.myAccount',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// }
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
@@ -2,18 +2,94 @@
|
||||
* 路由别名,方便快速找到页面,同时可以用作路由跳转
|
||||
*/
|
||||
export enum RoutesAlias {
|
||||
// 登录 布局容器
|
||||
Home = '/index/index', // 布局容器
|
||||
Login = '/auth/login', // 登录
|
||||
Register = '/auth/register', // 注册
|
||||
ForgetPassword = '/auth/forget-password', // 忘记密码
|
||||
|
||||
// 异常页面
|
||||
Exception403 = '/exception/403', // 403
|
||||
Exception404 = '/exception/404', // 404
|
||||
Exception500 = '/exception/500', // 500
|
||||
Success = '/result/success', // 成功
|
||||
Fail = '/result/fail', // 失败
|
||||
|
||||
// 仪表台
|
||||
Dashboard = '/dashboard/console', // 工作台
|
||||
Analysis = '/dashboard/analysis', // 分析页
|
||||
Ecommerce = '/dashboard/ecommerce', // 电子商务
|
||||
|
||||
// 系统管理
|
||||
Role = '/system/role', // 角色
|
||||
Permission = '/system/permission', // 权限管理
|
||||
CarrierManagement = '/system/carrier-management', // 运营商管理
|
||||
UserCenter = '/system/user-center', // 用户中心
|
||||
|
||||
// 套餐管理
|
||||
PackageList = '/package-management/package-list', // 套餐列表
|
||||
PackageDetail = '/package-management/package-list/detail', // 套餐列表详情
|
||||
PackageSeries = '/package-management/package-series', // 套餐系列
|
||||
PackageSeriesDetail = '/package-management/package-series/detail', // 套餐系列详情
|
||||
SeriesGrants = '/package-management/series-grants', // 代理系列授权
|
||||
SeriesGrantsDetail = '/package-management/series-grants/detail', // 代理系列授权详情
|
||||
|
||||
// 店铺管理
|
||||
Shop = '/shop-management/list', // 店铺列表
|
||||
|
||||
// 通用页面(店铺账号-企业账号)
|
||||
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表(通用)
|
||||
|
||||
// 账号管理
|
||||
Account = '/account-management/account', // 账号列表
|
||||
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
|
||||
EnterpriseCards = '/account-management/enterprise-cards', // 企业卡授权
|
||||
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备授权
|
||||
|
||||
// 资产管理
|
||||
AssetInformation = '/asset-management/asset-information', // 资产信息
|
||||
StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理
|
||||
DeviceList = '/asset-management/device-list', // 设备列表
|
||||
ExchangeManagement = '/asset-management/exchange-management', // 换货管理
|
||||
ExchangeDetail = '/asset-management/exchange-management/detail', // 换货单详情
|
||||
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
|
||||
AuthorizationRecordDetail = '/asset-management/authorization-records/detail', // 授权记录详情
|
||||
AssetAssign = '/asset-management/asset-assign', // 分配记录
|
||||
AssetAssignDetail = '/asset-management/asset-assign/detail', // 分配记录详情
|
||||
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
|
||||
DeviceTask = '/asset-management/device-task', // 设备任务
|
||||
TaskDetail = '/asset-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
||||
|
||||
// 订单管理
|
||||
OrderList = '/order-management/order-list', // 订单列表
|
||||
OrderDetail = '/order-management/order-list/detail', // 订单详情
|
||||
|
||||
// 财务管理
|
||||
AgentRecharge = '/finance/agent-recharge', // 代理充值
|
||||
AgentRechargeDetail = '/finance/agent-recharge/detail', // 代理充值详情
|
||||
RefundManagement = '/finance/refund', // 退款管理
|
||||
RefundDetail = '/finance/refund/detail', // 退款详情
|
||||
|
||||
// 佣金管理
|
||||
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
|
||||
CommissionWithdrawalSettings = '/finance/commission/withdrawal-settings', // 提现配置
|
||||
MyCommission = '/finance/commission/my-commission', // 我的佣金
|
||||
AgentCommission = '/finance/commission/agent-commission', // 代理商佣金管理
|
||||
|
||||
// 设置管理
|
||||
WechatConfig = '/settings/wechat-config', // 微信支付配置
|
||||
WechatConfigDetail = '/settings/wechat-config/detail', // 微信支付配置详情
|
||||
|
||||
// 轮询管理
|
||||
DataCleanup = '/polling-management/data-cleanup', // 数据清理
|
||||
AlertRules = '/polling-management/alert-rules', // 告警规则
|
||||
AlertHistory = '/polling-management/alert-history', // 告警历史
|
||||
PollingConcurrency = '/polling-management/concurrency', // 并发控制
|
||||
PollingConfig = '/polling-management/config', // 轮询配置
|
||||
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
|
||||
PollingMonitor = '/polling-management/monitor', // 轮询监控
|
||||
|
||||
// 其他页面------------------------------------------------暂时不知道有没有用到不要动
|
||||
Register = '/auth/register', // 注册
|
||||
ForgetPassword = '/auth/forget-password', // 忘记密码
|
||||
Success = '/result/success', // 成功
|
||||
Fail = '/result/fail', // 失败
|
||||
IconList = '/widgets/icon-list', // 图标列表
|
||||
IconSelector = '/widgets/icon-selector', // 图标选择器
|
||||
ImageCrop = '/widgets/image-crop', // 图片裁剪
|
||||
@@ -38,15 +114,6 @@ export enum RoutesAlias {
|
||||
ArticleDetail = '/article/detail', // 文章详情
|
||||
Comment = '/article/comment', // 评论
|
||||
ArticlePublish = '/article/publish', // 文章发布
|
||||
Role = '/system/role', // 角色
|
||||
Permission = '/system/permission', // 权限管理
|
||||
Shop = '/product/shop', // 店铺管理
|
||||
UserCenter = '/system/user-center', // 用户中心
|
||||
Menu = '/system/menu', // 菜单
|
||||
NestedMenu1 = '/system/nested/menu1', // 嵌套菜单1
|
||||
NestedMenu21 = '/system/nested/menu2', // 嵌套菜单2-1
|
||||
NestedMenu31 = '/system/nested/menu3', // 嵌套菜单3-1
|
||||
NestedMenu321 = '/system/nested/menu3/menu3-2', // 嵌套菜单3-2-1
|
||||
Server = '/safeguard/server', // 服务器
|
||||
ChangeLog = '/change/log', // 更新日志
|
||||
|
||||
@@ -54,7 +121,7 @@ export enum RoutesAlias {
|
||||
// 我的网卡
|
||||
CardList = '/card-management/card-list', // 网卡管理
|
||||
CardDetail = '/card-management/card-detail', // 网卡明细
|
||||
SingleCard = '/my-simcard/single-card', // 单卡信息
|
||||
|
||||
CardAssign = '/card-management/card-assign', // 网卡分配
|
||||
CardShutdown = '/card-management/card-shutdown', // 停机管理
|
||||
MyCards = '/card-management/my-cards', // 我的网卡
|
||||
@@ -64,28 +131,18 @@ export enum RoutesAlias {
|
||||
PackageGift = '/card-management/package-gift', // 套餐赠送
|
||||
CardChangeCard = '/card-management/card-change-card', // 换卡网卡
|
||||
|
||||
// 我的套餐
|
||||
// other
|
||||
PackageCreate = '/package-management/package-create', // 新建套餐
|
||||
PackageBatch = '/package-management/package-batch', // 批量管理
|
||||
PackageList = '/package-management/package-list', // 套餐管理
|
||||
PackageDetail = '/package-management/package-list/detail', // 套餐详情
|
||||
PackageChange = '/package-management/package-change', // 套餐变更
|
||||
SeriesGrants = '/package-management/series-grants', // 代理系列授权
|
||||
SeriesGrantsDetail = '/package-management/series-grants/detail', // 代理系列授权详情
|
||||
PackageSeries = '/package-management/package-series', // 套餐系列
|
||||
PackageSeriesDetail = '/package-management/package-series/detail', // 套餐系列详情
|
||||
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
|
||||
|
||||
// 账号管理
|
||||
Account = '/account-management/account', // 账号管理
|
||||
// PlatformAccount = '/account-management/platform-account', // 平台账号管理
|
||||
CustomerManagement = '/account-management/customer', // 客户管理
|
||||
CustomerRole = '/account-management/customer-role', // 客户角色
|
||||
AgentManagement = '/account-management/agent', // 代理商管理
|
||||
ShopAccount = '/account-management/shop-account', // 代理账号管理
|
||||
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
|
||||
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表(通用)
|
||||
EnterpriseCards = '/account-management/enterprise-cards', // 企业卡管理
|
||||
|
||||
CustomerCommission = '/account-management/customer-commission', // 客户账号佣金
|
||||
|
||||
// 产品管理
|
||||
@@ -94,55 +151,17 @@ export enum RoutesAlias {
|
||||
ShopAccounts = '/common/account-list', // 店铺账号列表(通用)
|
||||
|
||||
// 资产管理
|
||||
StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理
|
||||
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
|
||||
DeviceTask = '/asset-management/device-task', // 设备任务
|
||||
TaskDetail = '/asset-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
||||
DeviceList = '/asset-management/device-list', // 设备列表
|
||||
DeviceDetail = '/asset-management/device-detail', // 设备详情
|
||||
AssetAssign = '/asset-management/asset-assign', // 资产分配(分配记录)
|
||||
AssetAssignDetail = '/asset-management/asset-assign/detail', // 资产分配详情
|
||||
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
|
||||
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
|
||||
AuthorizationRecordDetail = '/asset-management/authorization-records/detail', // 授权记录详情
|
||||
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备列表
|
||||
ExchangeManagement = '/asset-management/exchange-management', // 换货管理
|
||||
ExchangeDetail = '/asset-management/exchange-management/detail', // 换货单详情
|
||||
|
||||
// 账户管理
|
||||
CustomerAccountList = '/finance/customer-account', // 客户账号
|
||||
MyAccount = '/finance/my-account', // 我的账户
|
||||
CarrierManagement = '/finance/carrier-management', // 运营商管理
|
||||
OrderList = '/account/orders', // 订单管理
|
||||
OrderDetail = '/account/orders/detail', // 订单详情
|
||||
AgentRecharge = '/finance/agent-recharge', // 代理充值
|
||||
AgentRechargeDetail = '/finance/agent-recharge/detail', // 代理充值详情
|
||||
|
||||
// 佣金管理
|
||||
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
|
||||
CommissionWithdrawalSettings = '/finance/commission/withdrawal-settings', // 提现配置
|
||||
MyCommission = '/finance/commission/my-commission', // 我的佣金
|
||||
AgentCommission = '/finance/commission/agent-commission', // 代理商佣金管理
|
||||
|
||||
// 设置管理
|
||||
PaymentMerchant = '/settings/payment-merchant', // 支付商户
|
||||
DeveloperApi = '/settings/developer-api', // 开发者API
|
||||
CommissionTemplate = '/settings/commission-template', // 分佣模板
|
||||
WechatConfig = '/settings/wechat-config', // 微信支付配置
|
||||
WechatConfigDetail = '/settings/wechat-config/detail', // 微信支付配置详情
|
||||
|
||||
// 退款管理
|
||||
RefundManagement = '/finance/refund', // 退款管理
|
||||
RefundDetail = '/finance/refund/detail', // 退款详情
|
||||
|
||||
// 轮询管理
|
||||
DataCleanup = '/polling-management/data-cleanup', // 数据清理
|
||||
AlertRules = '/polling-management/alert-rules', // 告警规则
|
||||
AlertHistory = '/polling-management/alert-history', // 告警历史
|
||||
PollingConcurrency = '/polling-management/concurrency', // 并发控制
|
||||
PollingConfig = '/polling-management/config', // 轮询配置
|
||||
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
|
||||
PollingMonitor = '/polling-management/monitor' // 轮询监控
|
||||
CommissionTemplate = '/settings/commission-template' // 分佣模板
|
||||
}
|
||||
|
||||
// 主页路由
|
||||
|
||||
@@ -47,6 +47,7 @@ export type UsageType = 'single_card' | 'device'
|
||||
export interface AssetResolveResponse {
|
||||
asset_type: AssetType // 资产类型:card 或 device
|
||||
asset_id: number // 数据库 ID
|
||||
identifier?: string // 原样回传本次查询所用的标识符
|
||||
virtual_no: string // 虚拟号
|
||||
status: number // 资产状态
|
||||
batch_no: string // 批次号
|
||||
@@ -97,6 +98,7 @@ export interface AssetResolveResponse {
|
||||
software_version?: string // 固件版本号(仅 device)
|
||||
switch_mode?: string // 切卡模式:"0"=自动, "1"=手动(仅 device)
|
||||
last_gateway_sync_at?: string | null // 最后 sync-info 同步时间(仅 device)
|
||||
extend?: any // 扩展信息字段
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -343,3 +345,64 @@ export interface AssetWalletResponse {
|
||||
created_at?: string // 创建时间(RFC3339)
|
||||
updated_at?: string // 更新时间(RFC3339)
|
||||
}
|
||||
|
||||
// ========== 资产历史订单 ==========
|
||||
|
||||
/**
|
||||
* 资产历史订单查询参数
|
||||
*/
|
||||
export interface AssetOrdersParams {
|
||||
page?: number // 页码,默认 1
|
||||
page_size?: number // 每页条数,默认 20,最大 100
|
||||
include_previous?: boolean // 是否追溯换货前代,默认 false
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单项
|
||||
*/
|
||||
export interface AssetOrderItem {
|
||||
package_name: string // 套餐名称
|
||||
quantity: number // 数量
|
||||
unit_price: number // 单价(分)
|
||||
amount: number // 金额(分)
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单信息
|
||||
*/
|
||||
export interface AssetOrder {
|
||||
order_no: string // 订单号
|
||||
order_type: string // 订单类型
|
||||
payment_status: number // 支付状态
|
||||
payment_status_text: string // 支付状态文本
|
||||
total_amount: number // 总金额(分)
|
||||
payment_method: string // 支付方式
|
||||
paid_at?: string // 支付时间
|
||||
generation?: number // 代数
|
||||
items: AssetOrderItem[] // 订单项
|
||||
created_at: string // 创建时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 代订单信息
|
||||
*/
|
||||
export interface GenerationOrders {
|
||||
generation: number // 代数
|
||||
identifier: string // 标识符
|
||||
asset_type: string // 资产类型
|
||||
exchange_no?: string // 换货单号(前代才有)
|
||||
exchanged_at?: string // 换货时间(前代才有)
|
||||
total: number // 订单总数
|
||||
page?: number // 页码(当前代才有)
|
||||
page_size?: number // 每页条数(当前代才有)
|
||||
items: AssetOrder[] // 订单列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产历史订单响应
|
||||
*/
|
||||
export interface AssetOrdersResponse {
|
||||
current_generation: GenerationOrders // 当前代订单
|
||||
previous_generations?: GenerationOrders[] // 前代订单(仅 include_previous=true 时有)
|
||||
truncated: boolean // 是否截断(换货链超过 10 代)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ export interface Device {
|
||||
software_version?: string // 固件版本号
|
||||
switch_mode?: SwitchMode // 切卡模式:"0"=自动, "1"=手动
|
||||
last_gateway_sync_at?: string | null // 最后 sync-info 同步时间
|
||||
imei?: string // IMEI(设备国际移动设备识别码)
|
||||
sn?: string // 设备序列号
|
||||
}
|
||||
|
||||
// 设备查询参数
|
||||
@@ -64,6 +66,7 @@ export interface DeviceQueryParams extends PaginationParams {
|
||||
created_at_start?: string // 创建时间起始
|
||||
created_at_end?: string // 创建时间结束
|
||||
series_id?: number // 套餐系列ID
|
||||
keyword?: string // 关键词搜索(虚拟号或IMEI)
|
||||
}
|
||||
|
||||
// 设备列表响应
|
||||
|
||||
@@ -52,6 +52,8 @@ export interface Order {
|
||||
commission_config_version: number
|
||||
device_id: number | null
|
||||
iot_card_id: number | null
|
||||
asset_identifier?: string // 资产标识符快照(ICCID 或 VirtualNo)
|
||||
asset_type?: string // 资产类型:single_card 或 device
|
||||
items: OrderItem[] | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -64,6 +66,7 @@ export interface OrderQueryParams {
|
||||
payment_status?: PaymentStatus
|
||||
order_type?: OrderType
|
||||
order_no?: string
|
||||
identifier?: string // 按资产标识符过滤
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
dateRange?: string[] | any // For date range picker in UI
|
||||
@@ -80,10 +83,8 @@ export interface OrderListResponse {
|
||||
|
||||
// 创建订单请求
|
||||
export interface CreateOrderRequest {
|
||||
order_type: OrderType
|
||||
package_ids: number[]
|
||||
iot_card_id?: number | null
|
||||
device_id?: number | null
|
||||
identifier: string // 资产标识符(ICCID 或 VirtualNo)
|
||||
package_ids: number[] // 套餐ID列表
|
||||
payment_method: OrderPaymentMethod // 支付方式
|
||||
}
|
||||
|
||||
@@ -93,8 +94,8 @@ export type CreateOrderResponse = Order
|
||||
// 套餐购买预检请求
|
||||
export interface PurchaseCheckRequest {
|
||||
order_type: OrderType // 订单类型 (single_card:单卡购买, device:设备购买)
|
||||
package_ids: number[] // 套餐ID列表
|
||||
resource_id: number // 资源ID (IoT卡ID或设备ID)
|
||||
package_ids: number[] // 套餐ID列表
|
||||
}
|
||||
|
||||
// 套餐购买预检响应
|
||||
|
||||
13
src/types/auto-imports.d.ts
vendored
13
src/types/auto-imports.d.ts
vendored
@@ -7,19 +7,6 @@
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElButton: (typeof import('element-plus/es'))['ElButton']
|
||||
const ElButton2: (typeof import('element-plus/es'))['ElButton2']
|
||||
const ElDropdown: (typeof import('element-plus/es'))['ElDropdown']
|
||||
const ElDropdown2: (typeof import('element-plus/es'))['ElDropdown2']
|
||||
const ElDropdownItem: (typeof import('element-plus/es'))['ElDropdownItem']
|
||||
const ElDropdownMenu: (typeof import('element-plus/es'))['ElDropdownMenu']
|
||||
const ElMessage: (typeof import('element-plus/es'))['ElMessage']
|
||||
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
|
||||
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
|
||||
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
|
||||
const ElPopover: (typeof import('element-plus/es'))['ElPopover']
|
||||
const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn']
|
||||
const ElTag: (typeof import('element-plus/es'))['ElTag']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
|
||||
@@ -27,7 +27,7 @@ export type KeyValue<T = any> = {
|
||||
// 选项类型
|
||||
export interface Option {
|
||||
label: string
|
||||
value: string | number
|
||||
value: string | number | boolean
|
||||
disabled?: boolean
|
||||
children?: Option[]
|
||||
}
|
||||
|
||||
20
src/types/components.d.ts
vendored
20
src/types/components.d.ts
vendored
@@ -61,7 +61,6 @@ declare module 'vue' {
|
||||
ArtSidebarMenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/index.vue')['default']
|
||||
ArtStatsCard: typeof import('./../components/core/cards/ArtStatsCard.vue')['default']
|
||||
ArtTable: typeof import('./../components/core/tables/ArtTable.vue')['default']
|
||||
ArtTableColumn: typeof import('./../components/core/tables/ArtTableColumn.vue')['default']
|
||||
ArtTableFullScreen: typeof import('./../components/core/tables/ArtTableFullScreen.vue')['default']
|
||||
ArtTableHeader: typeof import('./../components/core/tables/ArtTableHeader.vue')['default']
|
||||
ArtTextScroll: typeof import('./../components/core/text-effect/ArtTextScroll.vue')['default']
|
||||
@@ -86,12 +85,8 @@ declare module 'vue' {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
ElCalendar: typeof import('element-plus/es')['ElCalendar']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
@@ -107,40 +102,25 @@ declare module 'vue' {
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
||||
ElStep: typeof import('element-plus/es')['ElStep']
|
||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTransfer: typeof import('element-plus/es')['ElTransfer']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
ElWatermark: typeof import('element-plus/es')['ElWatermark']
|
||||
|
||||
@@ -1,220 +1,26 @@
|
||||
/**
|
||||
* 省市区三级联动数据
|
||||
* 简化版数据,包含主要省市区
|
||||
* 使用 element-china-area-data 包提供完整的中国省市区数据
|
||||
*/
|
||||
|
||||
import { regionData as chinaRegionData } from 'element-china-area-data'
|
||||
|
||||
export interface RegionItem {
|
||||
value: string
|
||||
label: string
|
||||
children?: RegionItem[]
|
||||
}
|
||||
|
||||
export const regionData: RegionItem[] = [
|
||||
{
|
||||
value: '北京市',
|
||||
label: '北京市',
|
||||
children: [
|
||||
{
|
||||
value: '北京市',
|
||||
label: '北京市',
|
||||
children: [
|
||||
{ value: '东城区', label: '东城区' },
|
||||
{ value: '西城区', label: '西城区' },
|
||||
{ value: '朝阳区', label: '朝阳区' },
|
||||
{ value: '丰台区', label: '丰台区' },
|
||||
{ value: '石景山区', label: '石景山区' },
|
||||
{ value: '海淀区', label: '海淀区' },
|
||||
{ value: '门头沟区', label: '门头沟区' },
|
||||
{ value: '房山区', label: '房山区' },
|
||||
{ value: '通州区', label: '通州区' },
|
||||
{ value: '顺义区', label: '顺义区' },
|
||||
{ value: '昌平区', label: '昌平区' },
|
||||
{ value: '大兴区', label: '大兴区' },
|
||||
{ value: '怀柔区', label: '怀柔区' },
|
||||
{ value: '平谷区', label: '平谷区' },
|
||||
{ value: '密云区', label: '密云区' },
|
||||
{ value: '延庆区', label: '延庆区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '上海市',
|
||||
label: '上海市',
|
||||
children: [
|
||||
{
|
||||
value: '上海市',
|
||||
label: '上海市',
|
||||
children: [
|
||||
{ value: '黄浦区', label: '黄浦区' },
|
||||
{ value: '徐汇区', label: '徐汇区' },
|
||||
{ value: '长宁区', label: '长宁区' },
|
||||
{ value: '静安区', label: '静安区' },
|
||||
{ value: '普陀区', label: '普陀区' },
|
||||
{ value: '虹口区', label: '虹口区' },
|
||||
{ value: '杨浦区', label: '杨浦区' },
|
||||
{ value: '闵行区', label: '闵行区' },
|
||||
{ value: '宝山区', label: '宝山区' },
|
||||
{ value: '嘉定区', label: '嘉定区' },
|
||||
{ value: '浦东新区', label: '浦东新区' },
|
||||
{ value: '金山区', label: '金山区' },
|
||||
{ value: '松江区', label: '松江区' },
|
||||
{ value: '青浦区', label: '青浦区' },
|
||||
{ value: '奉贤区', label: '奉贤区' },
|
||||
{ value: '崇明区', label: '崇明区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '广东省',
|
||||
label: '广东省',
|
||||
children: [
|
||||
{
|
||||
value: '广州市',
|
||||
label: '广州市',
|
||||
children: [
|
||||
{ value: '荔湾区', label: '荔湾区' },
|
||||
{ value: '越秀区', label: '越秀区' },
|
||||
{ value: '海珠区', label: '海珠区' },
|
||||
{ value: '天河区', label: '天河区' },
|
||||
{ value: '白云区', label: '白云区' },
|
||||
{ value: '黄埔区', label: '黄埔区' },
|
||||
{ value: '番禺区', label: '番禺区' },
|
||||
{ value: '花都区', label: '花都区' },
|
||||
{ value: '南沙区', label: '南沙区' },
|
||||
{ value: '从化区', label: '从化区' },
|
||||
{ value: '增城区', label: '增城区' }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '深圳市',
|
||||
label: '深圳市',
|
||||
children: [
|
||||
{ value: '罗湖区', label: '罗湖区' },
|
||||
{ value: '福田区', label: '福田区' },
|
||||
{ value: '南山区', label: '南山区' },
|
||||
{ value: '宝安区', label: '宝安区' },
|
||||
{ value: '龙岗区', label: '龙岗区' },
|
||||
{ value: '盐田区', label: '盐田区' },
|
||||
{ value: '龙华区', label: '龙华区' },
|
||||
{ value: '坪山区', label: '坪山区' },
|
||||
{ value: '光明区', label: '光明区' }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '东莞市',
|
||||
label: '东莞市',
|
||||
children: [
|
||||
{ value: '莞城区', label: '莞城区' },
|
||||
{ value: '南城区', label: '南城区' },
|
||||
{ value: '东城区', label: '东城区' },
|
||||
{ value: '万江区', label: '万江区' }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '佛山市',
|
||||
label: '佛山市',
|
||||
children: [
|
||||
{ value: '禅城区', label: '禅城区' },
|
||||
{ value: '南海区', label: '南海区' },
|
||||
{ value: '顺德区', label: '顺德区' },
|
||||
{ value: '三水区', label: '三水区' },
|
||||
{ value: '高明区', label: '高明区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '浙江省',
|
||||
label: '浙江省',
|
||||
children: [
|
||||
{
|
||||
value: '杭州市',
|
||||
label: '杭州市',
|
||||
children: [
|
||||
{ value: '上城区', label: '上城区' },
|
||||
{ value: '拱墅区', label: '拱墅区' },
|
||||
{ value: '西湖区', label: '西湖区' },
|
||||
{ value: '滨江区', label: '滨江区' },
|
||||
{ value: '萧山区', label: '萧山区' },
|
||||
{ value: '余杭区', label: '余杭区' },
|
||||
{ value: '临平区', label: '临平区' },
|
||||
{ value: '钱塘区', label: '钱塘区' },
|
||||
{ value: '富阳区', label: '富阳区' },
|
||||
{ value: '临安区', label: '临安区' }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '宁波市',
|
||||
label: '宁波市',
|
||||
children: [
|
||||
{ value: '海曙区', label: '海曙区' },
|
||||
{ value: '江北区', label: '江北区' },
|
||||
{ value: '北仑区', label: '北仑区' },
|
||||
{ value: '镇海区', label: '镇海区' },
|
||||
{ value: '鄞州区', label: '鄞州区' },
|
||||
{ value: '奉化区', label: '奉化区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '江苏省',
|
||||
label: '江苏省',
|
||||
children: [
|
||||
{
|
||||
value: '南京市',
|
||||
label: '南京市',
|
||||
children: [
|
||||
{ value: '玄武区', label: '玄武区' },
|
||||
{ value: '秦淮区', label: '秦淮区' },
|
||||
{ value: '建邺区', label: '建邺区' },
|
||||
{ value: '鼓楼区', label: '鼓楼区' },
|
||||
{ value: '浦口区', label: '浦口区' },
|
||||
{ value: '栖霞区', label: '栖霞区' },
|
||||
{ value: '雨花台区', label: '雨花台区' },
|
||||
{ value: '江宁区', label: '江宁区' },
|
||||
{ value: '六合区', label: '六合区' },
|
||||
{ value: '溧水区', label: '溧水区' },
|
||||
{ value: '高淳区', label: '高淳区' }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '苏州市',
|
||||
label: '苏州市',
|
||||
children: [
|
||||
{ value: '姑苏区', label: '姑苏区' },
|
||||
{ value: '虎丘区', label: '虎丘区' },
|
||||
{ value: '吴中区', label: '吴中区' },
|
||||
{ value: '相城区', label: '相城区' },
|
||||
{ value: '吴江区', label: '吴江区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '四川省',
|
||||
label: '四川省',
|
||||
children: [
|
||||
{
|
||||
value: '成都市',
|
||||
label: '成都市',
|
||||
children: [
|
||||
{ value: '锦江区', label: '锦江区' },
|
||||
{ value: '青羊区', label: '青羊区' },
|
||||
{ value: '金牛区', label: '金牛区' },
|
||||
{ value: '武侯区', label: '武侯区' },
|
||||
{ value: '成华区', label: '成华区' },
|
||||
{ value: '龙泉驿区', label: '龙泉驿区' },
|
||||
{ value: '青白江区', label: '青白江区' },
|
||||
{ value: '新都区', label: '新都区' },
|
||||
{ value: '温江区', label: '温江区' },
|
||||
{ value: '双流区', label: '双流区' },
|
||||
{ value: '郫都区', label: '郫都区' },
|
||||
{ value: '新津区', label: '新津区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
/**
|
||||
* 将 element-china-area-data 的数据格式转换为我们需要的格式
|
||||
* element-china-area-data 使用 code 作为 value,我们需要使用 label
|
||||
*/
|
||||
function convertRegionData(data: any[]): RegionItem[] {
|
||||
return data.map((item) => ({
|
||||
value: item.label,
|
||||
label: item.label,
|
||||
children: item.children ? convertRegionData(item.children) : undefined
|
||||
}))
|
||||
}
|
||||
|
||||
export const regionData: RegionItem[] = convertRegionData(chinaRegionData)
|
||||
|
||||
@@ -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