fix: 代理系列授权和操作日志去掉
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m58s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m58s
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Change: 移除旧资产操作审计日志入口
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
IoT 卡管理、设备管理和资产信息页仍展示旧的资产操作审计日志入口与内容,与当前审计调查能力重复,且不再需要向用户提供该旧日志交互。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 移除 IoT 卡管理列表的“操作审计日志”操作项及其抽屉弹窗。
|
||||||
|
- 移除设备管理列表的“操作审计日志”操作项及其抽屉弹窗。
|
||||||
|
- 移除资产信息页内嵌的操作审计日志卡片。
|
||||||
|
- 清理上述页面对旧日志组件的引用;全局无其他前端调用时,删除旧日志组件、前端 API 方法及专用类型;仍有调用时保留共享实现。
|
||||||
|
- 不改变后端历史日志数据或接口。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `asset-audit-log-navigation`
|
||||||
|
- Affected code: `src/views/asset-management/iot-card-management/index.vue`, `src/views/asset-management/device-list/index.vue`, `src/views/asset-management/asset-information/index.vue`, and, only if unused, `src/components/business/OperationLogsDialog.vue`, `src/views/asset-management/asset-information/components/OperationLogsCard.vue`, `src/api/modules/asset.ts`, `src/types/api/asset.ts`
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: 旧资产操作审计日志入口
|
||||||
|
|
||||||
|
**Reason**: 旧资产操作审计日志交互已由当前审计调查能力替代,页面不再提供重复入口。
|
||||||
|
**Migration**: 需要审计调查的用户通过当前审计中心和资源审计时间线访问记录。
|
||||||
|
|
||||||
|
#### Scenario: 查看 IoT 卡、设备或资产信息
|
||||||
|
|
||||||
|
- **WHEN** 用户打开 IoT 卡管理、设备管理或资产信息页面
|
||||||
|
- **THEN** 页面不显示旧的“操作审计日志”入口、弹窗或内嵌日志卡片
|
||||||
|
- **AND** 前端不因这些页面加载或操作而请求旧资产操作日志接口
|
||||||
10
openspec/changes/remove-legacy-asset-operation-logs/tasks.md
Normal file
10
openspec/changes/remove-legacy-asset-operation-logs/tasks.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## 1. Remove User Interfaces
|
||||||
|
|
||||||
|
- [x] 1.1 Remove the IoT card management operation-log action, state and dialog.
|
||||||
|
- [x] 1.2 Remove the device management operation-log action, state and dialog.
|
||||||
|
- [x] 1.3 Remove the embedded operation-log card from asset information.
|
||||||
|
|
||||||
|
## 2. Cleanup and Verification
|
||||||
|
|
||||||
|
- [x] 2.1 Search remaining consumers of the legacy log component, front-end API method and dedicated types; delete only implementations with no consumers.
|
||||||
|
- [x] 2.2 Run lint and type checks for changed files.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Change: 使用授权套餐候选项接口创建代理系列授权
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
新增代理系列授权时,通用套餐分页接口不能按当前操作者、目标店铺和套餐系列返回真正可授权的套餐,也不能提供目标店铺已授权状态。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增代理系列授权弹窗的套餐选择改用 `GET /api/admin/shop-series-grants/package-options`。
|
||||||
|
- 仅在已选择目标店铺和套餐系列后请求候选项接口,并原样传递 `shop_id` 与 `series_id`。
|
||||||
|
- 以接口候选项作为可选集合,保留候选项的授权状态,且不再由前端额外过滤赠送套餐。
|
||||||
|
- 为候选项接口增加前端 API 方法与类型定义,并用其价格字段初始化套餐成本价和建议零售价校验数据。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `package-series-allocation`
|
||||||
|
- Affected code: `src/api/modules/shopSeriesGrant.ts`, `src/types/api/packageManagement.ts`, `src/views/package-management/series-grants/index.vue`
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 授权创建使用店铺系列套餐候选项
|
||||||
|
|
||||||
|
系统 SHALL 在新增代理系列授权时,使用 `GET /api/admin/shop-series-grants/package-options` 获取套餐候选项。前端 MUST 在目标店铺和套餐系列均已选择后,原样传递非空的 `shop_id` 与 `series_id`;接口返回的候选项 SHALL 作为可选套餐及其价格、授权状态的权威来源。
|
||||||
|
|
||||||
|
#### Scenario: 选择店铺和套餐系列后加载候选项
|
||||||
|
|
||||||
|
- **WHEN** 用户在新增代理系列授权弹窗中已选择目标店铺和套餐系列
|
||||||
|
- **THEN** 前端请求 `/api/admin/shop-series-grants/package-options` 并携带所选 `shop_id` 与 `series_id`
|
||||||
|
- **AND** 前端展示返回的候选套餐而不再调用通用套餐分页接口作为该弹窗的候选来源
|
||||||
|
|
||||||
|
#### Scenario: 更换店铺或套餐系列
|
||||||
|
|
||||||
|
- **WHEN** 用户更换目标店铺或套餐系列
|
||||||
|
- **THEN** 前端清空此前的套餐候选项和已选择套餐
|
||||||
|
- **AND** 仅使用新组合返回的候选项进行后续选择
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## 1. API and Types
|
||||||
|
|
||||||
|
- [x] 1.1 Add the package-options API method and response types defined by the August interface document.
|
||||||
|
|
||||||
|
## 2. Create Grant Dialog
|
||||||
|
|
||||||
|
- [x] 2.1 Load package candidates only after both shop and series are selected, passing their IDs unchanged.
|
||||||
|
- [x] 2.2 Use returned candidates for selection, package names, cost prices and price validation.
|
||||||
|
- [x] 2.3 Reset candidate and selected-package state when the shop or series changes.
|
||||||
|
|
||||||
|
## 3. Verification
|
||||||
|
|
||||||
|
- [x] 3.1 Verify request parameters, candidate rendering and selected-package submission.
|
||||||
|
- [x] 3.2 Run lint and type checks for changed files.
|
||||||
@@ -27,8 +27,6 @@ import type {
|
|||||||
AssetOrdersResponse,
|
AssetOrdersResponse,
|
||||||
UpdateAssetRealnameStatusRequest,
|
UpdateAssetRealnameStatusRequest,
|
||||||
DtoUpdateAssetRealnameStatusResponse,
|
DtoUpdateAssetRealnameStatusResponse,
|
||||||
AssetOperationLogsResponse,
|
|
||||||
AssetOperationLogsParams,
|
|
||||||
AssetPackageUsageRecord,
|
AssetPackageUsageRecord,
|
||||||
UpdateAssetPackageUsedDataRequest,
|
UpdateAssetPackageUsedDataRequest,
|
||||||
UpdateAssetPackageExpiresAtRequest,
|
UpdateAssetPackageExpiresAtRequest,
|
||||||
@@ -311,22 +309,4 @@ export class AssetService extends BaseService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 资产操作审计日志 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询资产操作审计日志
|
|
||||||
* GET /api/admin/assets/:identifier/operation-logs
|
|
||||||
* @param identifier 资产标识符
|
|
||||||
* @param params 查询参数
|
|
||||||
*/
|
|
||||||
static getOperationLogs(
|
|
||||||
identifier: string,
|
|
||||||
params?: AssetOperationLogsParams
|
|
||||||
): Promise<BaseResponse<AssetOperationLogsResponse>> {
|
|
||||||
return this.get<BaseResponse<AssetOperationLogsResponse>>(
|
|
||||||
`/api/admin/assets/${identifier}/operation-logs`,
|
|
||||||
params
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
CreateShopSeriesGrantRequest,
|
CreateShopSeriesGrantRequest,
|
||||||
UpdateShopSeriesGrantRequest,
|
UpdateShopSeriesGrantRequest,
|
||||||
ManageGrantPackagesRequest,
|
ManageGrantPackagesRequest,
|
||||||
|
ShopSeriesGrantPackageOptionsResponse,
|
||||||
BaseResponse,
|
BaseResponse,
|
||||||
PaginationResponse
|
PaginationResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
@@ -45,6 +46,20 @@ export class ShopSeriesGrantService extends BaseService {
|
|||||||
return this.getOne<ShopSeriesGrantResponse>(`/api/admin/shop-series-grants/${id}`)
|
return this.getOne<ShopSeriesGrantResponse>(`/api/admin/shop-series-grants/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取代理系列授权可选套餐
|
||||||
|
* GET /api/admin/shop-series-grants/package-options
|
||||||
|
*/
|
||||||
|
static getPackageOptions(
|
||||||
|
shopId: number,
|
||||||
|
seriesId: number
|
||||||
|
): Promise<BaseResponse<ShopSeriesGrantPackageOptionsResponse>> {
|
||||||
|
return this.getOne<ShopSeriesGrantPackageOptionsResponse>(
|
||||||
|
'/api/admin/shop-series-grants/package-options',
|
||||||
|
{ shop_id: shopId, series_id: seriesId }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新代理系列授权
|
* 更新代理系列授权
|
||||||
* PUT /api/admin/shop-series-grants/{id}
|
* PUT /api/admin/shop-series-grants/{id}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ElDrawer
|
|
||||||
v-model="dialogVisible"
|
|
||||||
title="操作审计日志"
|
|
||||||
direction="rtl"
|
|
||||||
size="70%"
|
|
||||||
:before-close="handleClose"
|
|
||||||
>
|
|
||||||
<OperationLogsCard :asset-identifier="identifier" :download-permission="downloadPermission" />
|
|
||||||
</ElDrawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { ElDrawer } from 'element-plus'
|
|
||||||
import OperationLogsCard from '@/views/asset-management/asset-information/components/OperationLogsCard.vue'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
modelValue: boolean
|
|
||||||
identifier?: string
|
|
||||||
downloadPermission?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: boolean]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const dialogVisible = ref(props.modelValue)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(val) => {
|
|
||||||
dialogVisible.value = val
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(dialogVisible, (val) => {
|
|
||||||
emit('update:modelValue', val)
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
dialogVisible.value = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -594,51 +594,3 @@ export interface DtoUpdateAssetRealnameStatusResponse {
|
|||||||
real_name_status: number // 更新后的实名状态 (0:未实名, 1:已实名)
|
real_name_status: number // 更新后的实名状态 (0:未实名, 1:已实名)
|
||||||
real_name_status_name: string // 实名状态名称(中文)
|
real_name_status_name: string // 实名状态名称(中文)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 资产操作审计日志 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作审计日志项
|
|
||||||
*/
|
|
||||||
export interface AssetOperationLogItem {
|
|
||||||
id: number // 日志ID
|
|
||||||
asset_id: number // 资产ID
|
|
||||||
asset_identifier: string // 资产标识符
|
|
||||||
asset_type: string // 资产类型
|
|
||||||
operation_type: string // 操作类型
|
|
||||||
operation_desc: string // 操作描述
|
|
||||||
result_status: 'success' | 'failed' | 'denied' // 执行结果
|
|
||||||
created_at: string // 创建时间
|
|
||||||
operator_id?: number // 操作人ID
|
|
||||||
operator_name: string // 操作人名称
|
|
||||||
operator_type: string // 操作人类型
|
|
||||||
before_data: Record<string, any> // 操作前数据(原始)
|
|
||||||
after_data: Record<string, any> // 操作后数据(原始)
|
|
||||||
operation_content_before?: Record<string, any> // 操作内容(变更前)
|
|
||||||
operation_content_after?: Record<string, any> // 操作内容(变更后)
|
|
||||||
operation_fields_desc?: Record<string, string> // 字段中文说明
|
|
||||||
batch_total: number // 批量总数
|
|
||||||
success_count: number // 成功数量
|
|
||||||
fail_count: number // 失败数量
|
|
||||||
failed_items?: Array<{ iccid?: string; reason?: string }> // 失败明细
|
|
||||||
reason?: string // 原因说明
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 资产操作审计日志响应
|
|
||||||
*/
|
|
||||||
export interface AssetOperationLogsResponse {
|
|
||||||
items: AssetOperationLogItem[] // 日志列表
|
|
||||||
page: number // 当前页码
|
|
||||||
page_size: number // 每页数量
|
|
||||||
total: number // 总记录数
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 资产操作审计日志查询参数
|
|
||||||
*/
|
|
||||||
export interface AssetOperationLogsParams {
|
|
||||||
page?: number // 页码,默认1
|
|
||||||
page_size?: number // 每页条数,默认20,最大100
|
|
||||||
result_status?: 'success' | 'failed' | 'denied' // 执行结果
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -325,6 +325,60 @@ export interface ShopSeriesGrantQueryParams extends PaginationParams {
|
|||||||
status?: number // 状态筛选
|
status?: number // 状态筛选
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理系列授权套餐候选项
|
||||||
|
*/
|
||||||
|
export interface ShopSeriesGrantPackageOption {
|
||||||
|
authorized: boolean
|
||||||
|
id: number
|
||||||
|
package_code: string
|
||||||
|
package_name: string
|
||||||
|
package_type: 'formal' | 'addon'
|
||||||
|
series_id: number | null
|
||||||
|
series_name: string | null
|
||||||
|
is_gift: boolean
|
||||||
|
status: number
|
||||||
|
status_name: string
|
||||||
|
shelf_status: number
|
||||||
|
shelf_status_name: string
|
||||||
|
calendar_type: 'natural_month' | 'by_day'
|
||||||
|
duration_days: number | null
|
||||||
|
duration_months: number
|
||||||
|
real_data_mb: number
|
||||||
|
virtual_data_mb: number
|
||||||
|
enable_virtual_data: boolean
|
||||||
|
virtual_ratio: number
|
||||||
|
data_reset_cycle: string
|
||||||
|
cost_price: number
|
||||||
|
retail_price: number | null
|
||||||
|
suggested_retail_price: number | null
|
||||||
|
effective_retail_price: number | null
|
||||||
|
expiry_base: string
|
||||||
|
expiry_base_override: PackageAllocationExpiryBaseOverride | null
|
||||||
|
expiry_base_override_name: string
|
||||||
|
default_expiry_base: string
|
||||||
|
default_expiry_base_name: string
|
||||||
|
effective_expiry_base: string
|
||||||
|
effective_expiry_base_name: string
|
||||||
|
price_config_status: number
|
||||||
|
price_config_status_name: string
|
||||||
|
retail_price_config_status: number | null
|
||||||
|
current_commission_rate: string
|
||||||
|
profit_margin: number | null
|
||||||
|
one_time_commission_amount: number | null
|
||||||
|
tier_info?: {
|
||||||
|
current_rate?: string
|
||||||
|
next_rate?: string
|
||||||
|
next_threshold?: number | null
|
||||||
|
}
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShopSeriesGrantPackageOptionsResponse {
|
||||||
|
items: ShopSeriesGrantPackageOption[] | null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建代理系列授权请求
|
* 创建代理系列授权请求
|
||||||
*/
|
*/
|
||||||
|
|||||||
1
src/types/components.d.ts
vendored
1
src/types/components.d.ts
vendored
@@ -148,7 +148,6 @@ declare module 'vue' {
|
|||||||
LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default']
|
LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default']
|
||||||
MenuLayoutSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue')['default']
|
MenuLayoutSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue')['default']
|
||||||
MenuStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue')['default']
|
MenuStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue')['default']
|
||||||
OperationLogsDialog: typeof import('./../components/business/OperationLogsDialog.vue')['default']
|
|
||||||
OperatorSelect: typeof import('./../components/business/OperatorSelect.vue')['default']
|
OperatorSelect: typeof import('./../components/business/OperatorSelect.vue')['default']
|
||||||
PackageSelector: typeof import('./../components/business/PackageSelector.vue')['default']
|
PackageSelector: typeof import('./../components/business/PackageSelector.vue')['default']
|
||||||
PaymentVoucherDialog: typeof import('./../components/business/PaymentVoucherDialog.vue')['default']
|
PaymentVoucherDialog: typeof import('./../components/business/PaymentVoucherDialog.vue')['default']
|
||||||
|
|||||||
@@ -1,692 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ElCard shadow="never" class="info-card operation-logs-card" v-loading="loading">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span class="header-title">操作审计日志</span>
|
|
||||||
<div class="filter-section">
|
|
||||||
<ElSelect
|
|
||||||
v-model="filterForm.result_status"
|
|
||||||
placeholder="执行结果"
|
|
||||||
clearable
|
|
||||||
class="result-status-select"
|
|
||||||
@change="handleFilterChange"
|
|
||||||
>
|
|
||||||
<ElOption label="成功" value="success" />
|
|
||||||
<ElOption label="失败" value="failed" />
|
|
||||||
<ElOption label="拒绝" value="denied" />
|
|
||||||
</ElSelect>
|
|
||||||
<ElButton type="primary" @click="handleQuery">查询</ElButton>
|
|
||||||
<ElButton @click="handleReset">重置</ElButton>
|
|
||||||
<ElTag type="info" size="small">共 {{ total }} 条</ElTag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="logs-table-wrapper">
|
|
||||||
<ElTable :data="logList" class="logs-table" border max-height="400">
|
|
||||||
<ElTableColumn label="操作时间" width="170" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ formatDateTime(row.created_at) }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="操作类型" width="160" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<ElTag :type="getOperationTypeTag(row.operation_type)" size="small">
|
|
||||||
{{ row.operation_desc }}
|
|
||||||
</ElTag>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="操作人" width="130" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.operator_name || '-' }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="操作人类型" width="110" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ getOperatorTypeText(row) }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="执行结果" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<ElTag :type="getResultStatusTag(row.result_status)" size="small">
|
|
||||||
{{ getResultStatusText(row.result_status) }}
|
|
||||||
</ElTag>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="变更内容" min-width="320">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div v-if="getDisplayContent(row).length" class="change-content">
|
|
||||||
<span
|
|
||||||
v-for="(item, index) in getDisplayContent(row)"
|
|
||||||
:key="`${item.type}-${item.key}-${index}`"
|
|
||||||
class="change-item"
|
|
||||||
>
|
|
||||||
<span class="field-name">{{ item.fieldName }}</span>
|
|
||||||
<span class="field-value">
|
|
||||||
<template v-if="item.type === 'change'">
|
|
||||||
<span class="new-value">{{ formatFieldValue(item.key, item.afterValue) }}</span>
|
|
||||||
<ElButton
|
|
||||||
v-if="canDownloadLogFile && isDownloadableFileKey(item.key, item.afterValue)"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
@click.stop="handleDownloadLogFile(item.afterValue)"
|
|
||||||
>
|
|
||||||
下载文件
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
<span v-else :class="['message-value', item.level === 'error' ? 'is-error' : '']">
|
|
||||||
{{ item.message }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
<ElPagination
|
|
||||||
class="logs-pagination"
|
|
||||||
v-if="total > 0"
|
|
||||||
v-model:current-page="pagination.page"
|
|
||||||
v-model:page-size="pagination.page_size"
|
|
||||||
:total="total"
|
|
||||||
:page-sizes="[10, 20, 50]"
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
@current-change="handlePageChange"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
|
||||||
import {
|
|
||||||
ElButton,
|
|
||||||
ElCard,
|
|
||||||
ElMessage,
|
|
||||||
ElOption,
|
|
||||||
ElPagination,
|
|
||||||
ElSelect,
|
|
||||||
ElTable,
|
|
||||||
ElTableColumn,
|
|
||||||
ElTag
|
|
||||||
} from 'element-plus'
|
|
||||||
import { AssetService, StorageService } from '@/api/modules'
|
|
||||||
import { useAuth } from '@/composables/useAuth'
|
|
||||||
import type { AssetOperationLogItem } from '@/types/api'
|
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
assetIdentifier?: string
|
|
||||||
assetType?: string
|
|
||||||
downloadPermission?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TagType = 'primary' | 'success' | 'warning' | 'info' | 'danger'
|
|
||||||
type ResultStatus = '' | 'success' | 'failed' | 'denied'
|
|
||||||
|
|
||||||
type OperationLogRow = AssetOperationLogItem & {
|
|
||||||
operator_type_code?: string
|
|
||||||
error_code?: string
|
|
||||||
error_msg?: string
|
|
||||||
request_id?: string
|
|
||||||
request_path?: string
|
|
||||||
request_method?: string
|
|
||||||
ip_address?: string
|
|
||||||
user_agent?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChangeItem {
|
|
||||||
type: 'change'
|
|
||||||
key: string
|
|
||||||
fieldName: string
|
|
||||||
beforeValue?: any
|
|
||||||
afterValue?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MessageItem {
|
|
||||||
type: 'message'
|
|
||||||
key: string
|
|
||||||
fieldName: string
|
|
||||||
message: string
|
|
||||||
level: 'info' | 'error'
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisplayItem = ChangeItem | MessageItem
|
|
||||||
|
|
||||||
const operationTypeTagMap: Record<string, TagType> = {
|
|
||||||
device_set_wifi: 'primary',
|
|
||||||
device_switch_card: 'success',
|
|
||||||
device_switch_mode: 'success',
|
|
||||||
device_stop: 'danger',
|
|
||||||
device_start: 'success',
|
|
||||||
device_reboot: 'warning',
|
|
||||||
device_reset: 'warning',
|
|
||||||
device_allocate: 'primary',
|
|
||||||
device_recall: 'warning',
|
|
||||||
device_bind_card: 'primary',
|
|
||||||
device_unbind_card: 'warning',
|
|
||||||
card_allocate: 'primary',
|
|
||||||
card_recall: 'warning',
|
|
||||||
card_stop: 'danger',
|
|
||||||
card_start: 'success',
|
|
||||||
card_manual_stop: 'danger',
|
|
||||||
card_manual_start: 'success',
|
|
||||||
card_auto_stop: 'danger',
|
|
||||||
card_auto_start: 'success',
|
|
||||||
card_polling_status: 'info',
|
|
||||||
asset_realname_policy: 'info',
|
|
||||||
asset_realname_status: 'info',
|
|
||||||
asset_polling: 'info',
|
|
||||||
asset_polling_status: 'info',
|
|
||||||
iot_card_import_task_create: 'primary'
|
|
||||||
}
|
|
||||||
|
|
||||||
const operatorTypeMap: Record<string, string> = {
|
|
||||||
system: '系统',
|
|
||||||
admin_user: '平台用户',
|
|
||||||
agent_user: '代理账号',
|
|
||||||
enterprise_user: '企业用户',
|
|
||||||
api_user: 'API用户'
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldNameMap: Record<string, string> = {
|
|
||||||
asset_type: '资产类型',
|
|
||||||
batch_no: '批次号',
|
|
||||||
card_category: '卡类型',
|
|
||||||
device_imei: '设备IMEI',
|
|
||||||
device_sn: '设备SN',
|
|
||||||
device_virtual_no: '设备虚拟号',
|
|
||||||
device_virtual_nos: '设备虚拟号',
|
|
||||||
enable_polling: '轮询开关',
|
|
||||||
file_key: '文件存储键',
|
|
||||||
first_realname_at: '首次实名时间',
|
|
||||||
iccid: 'ICCID',
|
|
||||||
iccids: 'ICCID',
|
|
||||||
network_status: '网络状态',
|
|
||||||
real_name_status: '实名状态',
|
|
||||||
realname_policy: '实名认证策略',
|
|
||||||
shop_name: '店铺',
|
|
||||||
source_service: '来源服务',
|
|
||||||
source_shop_name: '来源店铺',
|
|
||||||
status: '资产状态',
|
|
||||||
stop_reason: '停机原因',
|
|
||||||
switch_mode: '切卡模式',
|
|
||||||
target_shop_name: '目标店铺',
|
|
||||||
to_shop_name: '目标店铺'
|
|
||||||
}
|
|
||||||
|
|
||||||
const realnamePolicyMap: Record<string, string> = {
|
|
||||||
none: '无需实名',
|
|
||||||
before_order: '先实名后充值/购买',
|
|
||||||
after_order: '先充值/购买后实名'
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopReasonMap: Record<string, string> = {
|
|
||||||
'': '无',
|
|
||||||
no_package: '无可用套餐',
|
|
||||||
carrier_stopped: '运营商停机',
|
|
||||||
not_realname: '未实名'
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetTypeMap: Record<string, string> = {
|
|
||||||
iot_card: 'IoT卡',
|
|
||||||
device: '设备'
|
|
||||||
}
|
|
||||||
|
|
||||||
const cardCategoryMap: Record<string, string> = {
|
|
||||||
normal: '普通卡',
|
|
||||||
industry: '行业卡'
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetStatusMap: Record<string, string> = {
|
|
||||||
'1': '在库',
|
|
||||||
'2': '已分销',
|
|
||||||
'3': '已激活',
|
|
||||||
'4': '已停用'
|
|
||||||
}
|
|
||||||
|
|
||||||
const newStatusMap: Record<string, string> = {
|
|
||||||
allocated: '已分配',
|
|
||||||
recalled: '已回收'
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const { hasAuth } = useAuth()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const logList = ref<OperationLogRow[]>([])
|
|
||||||
const total = ref(0)
|
|
||||||
const canDownloadLogFile = computed(() => {
|
|
||||||
return props.downloadPermission ? hasAuth(props.downloadPermission) : false
|
|
||||||
})
|
|
||||||
const pagination = reactive({
|
|
||||||
page: 1,
|
|
||||||
page_size: 20
|
|
||||||
})
|
|
||||||
|
|
||||||
const filterForm = reactive({
|
|
||||||
result_status: '' as ResultStatus
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadLogs = async () => {
|
|
||||||
if (!props.assetIdentifier) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
const res = await AssetService.getOperationLogs(props.assetIdentifier, {
|
|
||||||
page: pagination.page,
|
|
||||||
page_size: pagination.page_size,
|
|
||||||
result_status: filterForm.result_status || undefined
|
|
||||||
})
|
|
||||||
if (res.code === 0 && res.data) {
|
|
||||||
logList.value = res.data.items || []
|
|
||||||
total.value = res.data.total || 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载操作日志失败:', error)
|
|
||||||
ElMessage.error('加载操作日志失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleQuery = () => {
|
|
||||||
pagination.page = 1
|
|
||||||
loadLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
filterForm.result_status = ''
|
|
||||||
pagination.page = 1
|
|
||||||
loadLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFilterChange = () => {
|
|
||||||
handleQuery()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePageChange = () => {
|
|
||||||
loadLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSizeChange = () => {
|
|
||||||
pagination.page = 1
|
|
||||||
loadLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOperationTypeTag = (type: string): TagType => {
|
|
||||||
return operationTypeTagMap[type] || 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getResultStatusTag = (status: string): TagType => {
|
|
||||||
const tagMap: Record<string, TagType> = {
|
|
||||||
success: 'success',
|
|
||||||
failed: 'danger',
|
|
||||||
denied: 'warning'
|
|
||||||
}
|
|
||||||
return tagMap[status] || 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getResultStatusText = (status: string) => {
|
|
||||||
const textMap: Record<string, string> = {
|
|
||||||
success: '成功',
|
|
||||||
failed: '失败',
|
|
||||||
denied: '拒绝'
|
|
||||||
}
|
|
||||||
return textMap[status] || status
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDownloadableFileKey = (key: string, value: unknown) => {
|
|
||||||
return key === 'file_key' && typeof value === 'string' && value.trim() !== ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDownloadLogFile = async (fileKey: string) => {
|
|
||||||
try {
|
|
||||||
await StorageService.downloadFileByKey(fileKey)
|
|
||||||
ElMessage.success('文件下载已开始')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('下载日志文件失败:', error)
|
|
||||||
ElMessage.error('下载文件失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOperatorTypeText = (row: OperationLogRow) => {
|
|
||||||
if (row.operator_type_code && operatorTypeMap[row.operator_type_code]) {
|
|
||||||
return operatorTypeMap[row.operator_type_code]
|
|
||||||
}
|
|
||||||
if (row.operator_type && operatorTypeMap[row.operator_type]) {
|
|
||||||
return operatorTypeMap[row.operator_type]
|
|
||||||
}
|
|
||||||
return row.operator_type || row.operator_type_code || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatValue = (value: any): string => {
|
|
||||||
if (value === null || value === undefined) return '空'
|
|
||||||
if (typeof value === 'boolean') return value ? '是' : '否'
|
|
||||||
if (typeof value === 'number') return String(value)
|
|
||||||
if (typeof value === 'string') return value || '空'
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (value.length === 0) return '空'
|
|
||||||
if (value.every((item) => ['string', 'number', 'boolean'].includes(typeof item))) {
|
|
||||||
return value.map((item) => formatValue(item)).join('、')
|
|
||||||
}
|
|
||||||
return JSON.stringify(value)
|
|
||||||
}
|
|
||||||
if (typeof value === 'object') return JSON.stringify(value)
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatFieldValue = (key: string, value: any): string => {
|
|
||||||
if (typeof value === 'string' && (key.endsWith('_at') || key.endsWith('_time'))) {
|
|
||||||
return formatDateTime(value)
|
|
||||||
}
|
|
||||||
if (key === 'enable_polling') return value ? '开启' : '关闭'
|
|
||||||
if (key === 'realname_policy') return realnamePolicyMap[value] || formatValue(value)
|
|
||||||
if (key === 'real_name_status') return value === 1 ? '已实名' : '未实名'
|
|
||||||
if (key === 'switch_mode') {
|
|
||||||
return String(value) === '0' ? '自动' : String(value) === '1' ? '手动' : formatValue(value)
|
|
||||||
}
|
|
||||||
if (key === 'new_status') return newStatusMap[value] || formatValue(value)
|
|
||||||
if (key === 'network_status') {
|
|
||||||
return Number(value) === 1 ? '正常' : Number(value) === 0 ? '停机' : formatValue(value)
|
|
||||||
}
|
|
||||||
if (key === 'stop_reason') return stopReasonMap[String(value ?? '')] ?? formatValue(value)
|
|
||||||
if (key === 'asset_type') return assetTypeMap[String(value)] || formatValue(value)
|
|
||||||
if (key === 'card_category') return cardCategoryMap[String(value)] || formatValue(value)
|
|
||||||
if (key === 'status') return assetStatusMap[String(value)] || formatValue(value)
|
|
||||||
if (key === 'source_service') return value === 'asset_polling' ? '资产轮询' : formatValue(value)
|
|
||||||
return formatValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanitizeFieldName = (fieldName: string): string => {
|
|
||||||
return fieldName
|
|
||||||
.trim()
|
|
||||||
.replace(/([^)]*)/g, '')
|
|
||||||
.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldHideIdField = (key: string, fieldName: string): boolean => {
|
|
||||||
const normalizedKey = key.trim()
|
|
||||||
const normalizedKeyLower = normalizedKey.toLowerCase()
|
|
||||||
const normalizedFieldName = sanitizeFieldName(fieldName)
|
|
||||||
const normalizedFieldNameUpper = normalizedFieldName.toUpperCase()
|
|
||||||
|
|
||||||
const visibleIdentifierKeys = new Set(['iccid', 'msisdn', 'imei', 'imsi', 'sn'])
|
|
||||||
const visibleIdentifierNames = ['ICCID', 'MSISDN', 'IMEI', 'IMSI', 'SN']
|
|
||||||
|
|
||||||
if (
|
|
||||||
visibleIdentifierKeys.has(normalizedKeyLower) ||
|
|
||||||
visibleIdentifierNames.some((name) => normalizedFieldNameUpper.endsWith(name))
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
normalizedKeyLower === 'id' ||
|
|
||||||
normalizedFieldName === '绑定记录ID' ||
|
|
||||||
normalizedFieldName === 'ID'
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
normalizedKeyLower.endsWith('_id') ||
|
|
||||||
/^[a-z][a-z0-9]*Id$/.test(normalizedKey) ||
|
|
||||||
normalizedFieldName.endsWith('ID')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractOperationContent = (
|
|
||||||
row: OperationLogRow,
|
|
||||||
type: 'before' | 'after'
|
|
||||||
): Record<string, any> => {
|
|
||||||
const directContent =
|
|
||||||
type === 'before' ? row.operation_content_before : row.operation_content_after
|
|
||||||
if (directContent && Object.keys(directContent).length > 0) {
|
|
||||||
return directContent
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackContainer = type === 'before' ? row.before_data : row.after_data
|
|
||||||
const nestedContent = fallbackContainer?.operation_content
|
|
||||||
if (nestedContent && Object.keys(nestedContent).length > 0) {
|
|
||||||
return nestedContent
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackContainer || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSameValue = (beforeValue: any, afterValue: any) => {
|
|
||||||
if (beforeValue === afterValue) return true
|
|
||||||
return JSON.stringify(beforeValue) === JSON.stringify(afterValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getChangeContent = (row: OperationLogRow): ChangeItem[] => {
|
|
||||||
const before = extractOperationContent(row, 'before')
|
|
||||||
const after = extractOperationContent(row, 'after')
|
|
||||||
const desc = row.operation_fields_desc || {}
|
|
||||||
const keys = new Set([...Object.keys(before), ...Object.keys(after)])
|
|
||||||
|
|
||||||
const changes: ChangeItem[] = []
|
|
||||||
for (const key of keys) {
|
|
||||||
const fieldName = sanitizeFieldName(desc[key] || fieldNameMap[key] || key)
|
|
||||||
if (shouldHideIdField(key, fieldName)) continue
|
|
||||||
|
|
||||||
const beforeValue = before[key]
|
|
||||||
const afterValue = after[key]
|
|
||||||
if (isSameValue(beforeValue, afterValue)) continue
|
|
||||||
|
|
||||||
changes.push({
|
|
||||||
type: 'change',
|
|
||||||
key,
|
|
||||||
fieldName,
|
|
||||||
beforeValue,
|
|
||||||
afterValue
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return changes
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMessageItems = (row: OperationLogRow): MessageItem[] => {
|
|
||||||
const items: MessageItem[] = []
|
|
||||||
const seen = new Set<string>()
|
|
||||||
|
|
||||||
const pushMessage = (
|
|
||||||
key: string,
|
|
||||||
fieldName: string,
|
|
||||||
message?: string,
|
|
||||||
level: 'info' | 'error' = 'info'
|
|
||||||
) => {
|
|
||||||
const normalizedMessage = message?.trim()
|
|
||||||
if (!normalizedMessage) return
|
|
||||||
|
|
||||||
const duplicateKey = `${fieldName}:${normalizedMessage}`
|
|
||||||
if (seen.has(duplicateKey)) return
|
|
||||||
seen.add(duplicateKey)
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
type: 'message',
|
|
||||||
key,
|
|
||||||
fieldName,
|
|
||||||
message: normalizedMessage,
|
|
||||||
level
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pushMessage('error_msg', '失败原因', row.error_msg, 'error')
|
|
||||||
pushMessage('reason', '原因说明', row.reason, row.result_status === 'failed' ? 'error' : 'info')
|
|
||||||
|
|
||||||
if (row.fail_count > 0) {
|
|
||||||
pushMessage('fail_count', '批量结果', `失败 ${row.fail_count} 项`, 'error')
|
|
||||||
}
|
|
||||||
|
|
||||||
row.failed_items?.slice(0, 3).forEach((item, index) => {
|
|
||||||
const detail = item?.iccid
|
|
||||||
? `${item.iccid}:${item.reason || '未知原因'}`
|
|
||||||
: item?.reason || ''
|
|
||||||
pushMessage(`failed_items_${index}`, '失败明细', detail, 'error')
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!items.length && row.result_status === 'failed') {
|
|
||||||
pushMessage('result_status', '执行结果', '执行失败', 'error')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!items.length && row.result_status === 'denied') {
|
|
||||||
pushMessage('result_status', '执行结果', '操作被拒绝', 'info')
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDisplayContent = (row: OperationLogRow): DisplayItem[] => {
|
|
||||||
return [...getChangeContent(row), ...getMessageItems(row)]
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.assetIdentifier,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
loadLogs()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.operation-logs-card {
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-section {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
.operation-type-select {
|
|
||||||
width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-status-select {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
.logs-table {
|
|
||||||
min-width: 980px;
|
|
||||||
|
|
||||||
:deep(.el-table__header-wrapper) {
|
|
||||||
th {
|
|
||||||
background-color: var(--el-fill-color-light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.change-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
.field-name {
|
|
||||||
min-width: 72px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-value {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
word-break: break-all;
|
|
||||||
|
|
||||||
.new-value {
|
|
||||||
color: var(--el-color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-value {
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
|
|
||||||
&.is-error {
|
|
||||||
color: var(--el-color-danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-pagination {
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= 1200px) {
|
|
||||||
.card-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
.filter-section {
|
|
||||||
justify-content: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= 768px) {
|
|
||||||
.card-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
.filter-section {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
:deep(.el-select),
|
|
||||||
.el-button,
|
|
||||||
.el-tag {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-table-wrapper {
|
|
||||||
padding: 0 12px;
|
|
||||||
margin-right: -12px;
|
|
||||||
margin-left: -12px;
|
|
||||||
|
|
||||||
:deep(.el-pagination) {
|
|
||||||
justify-content: flex-start !important;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -96,15 +96,6 @@
|
|||||||
@navigate-to-device="handleNavigateToDevice"
|
@navigate-to-device="handleNavigateToDevice"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 第五行:操作审计日志 -->
|
|
||||||
<div class="row full-width">
|
|
||||||
<OperationLogsCard
|
|
||||||
:asset-identifier="cardInfo.identifier"
|
|
||||||
:asset-type="cardInfo.asset_type"
|
|
||||||
download-permission="asset_info:download_log_file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -204,7 +195,6 @@
|
|||||||
import CurrentPackageCard from './components/CurrentPackageCard.vue'
|
import CurrentPackageCard from './components/CurrentPackageCard.vue'
|
||||||
import PackageListCard from './components/PackageListCard.vue'
|
import PackageListCard from './components/PackageListCard.vue'
|
||||||
import WalletTransactionCard from './components/WalletTransactionCard.vue'
|
import WalletTransactionCard from './components/WalletTransactionCard.vue'
|
||||||
import OperationLogsCard from './components/OperationLogsCard.vue'
|
|
||||||
|
|
||||||
// 引入对话框组件
|
// 引入对话框组件
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -703,12 +703,6 @@
|
|||||||
@confirm="handleConfirmBatchRealnamePolicy"
|
@confirm="handleConfirmBatchRealnamePolicy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 操作审计日志弹窗 -->
|
|
||||||
<OperationLogsDialog
|
|
||||||
v-model="operationLogsDialogVisible"
|
|
||||||
:identifier="operationLogsIdentifier"
|
|
||||||
download-permission="device:download_log_file"
|
|
||||||
/>
|
|
||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="enterpriseDeviceAuthorizeDialogVisible"
|
v-model="enterpriseDeviceAuthorizeDialogVisible"
|
||||||
title="授权设备给企业"
|
title="授权设备给企业"
|
||||||
@@ -1012,7 +1006,6 @@
|
|||||||
} from '@/types/api/enterpriseDevice'
|
} from '@/types/api/enterpriseDevice'
|
||||||
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
|
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
|
||||||
import BatchRealnamePolicyDialog from '@/components/business/BatchRealnamePolicyDialog.vue'
|
import BatchRealnamePolicyDialog from '@/components/business/BatchRealnamePolicyDialog.vue'
|
||||||
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
|
|
||||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||||
import type { AssetRealnamePolicy } from '@/types/api'
|
import type { AssetRealnamePolicy } from '@/types/api'
|
||||||
|
|
||||||
@@ -1045,8 +1038,6 @@
|
|||||||
const batchRealnamePolicyDialogVisible = ref(false)
|
const batchRealnamePolicyDialogVisible = ref(false)
|
||||||
const batchRealnamePolicyLoading = ref(false)
|
const batchRealnamePolicyLoading = ref(false)
|
||||||
const batchRealnamePolicyError = ref('')
|
const batchRealnamePolicyError = ref('')
|
||||||
const operationLogsDialogVisible = ref(false)
|
|
||||||
const operationLogsIdentifier = ref('')
|
|
||||||
const exportDialogVisible = ref(false)
|
const exportDialogVisible = ref(false)
|
||||||
const shopCascadeOptions = ref<any[]>([])
|
const shopCascadeOptions = ref<any[]>([])
|
||||||
const shopCascadeProps = {
|
const shopCascadeProps = {
|
||||||
@@ -3167,17 +3158,6 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAuth('device:operation_logs')) {
|
|
||||||
moreActions.push({
|
|
||||||
label: '操作审计日志',
|
|
||||||
handler: () => {
|
|
||||||
operationLogsIdentifier.value = row.virtual_no
|
|
||||||
operationLogsDialogVisible.value = true
|
|
||||||
},
|
|
||||||
type: 'primary'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const userType = Number(userStore.getUserInfo.user_type)
|
const userType = Number(userStore.getUserInfo.user_type)
|
||||||
const auditPermission =
|
const auditPermission =
|
||||||
userType === 3
|
userType === 3
|
||||||
|
|||||||
@@ -1001,13 +1001,6 @@
|
|||||||
@success="handleUpdateRealnameStatusSuccess"
|
@success="handleUpdateRealnameStatusSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 操作审计日志弹窗 -->
|
|
||||||
<OperationLogsDialog
|
|
||||||
v-model="operationLogsDialogVisible"
|
|
||||||
:identifier="operationLogsIdentifier"
|
|
||||||
download-permission="iot_card:download_log_file"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ExportTaskCreateDialog
|
<ExportTaskCreateDialog
|
||||||
v-model="exportDialogVisible"
|
v-model="exportDialogVisible"
|
||||||
scene="iot_card"
|
scene="iot_card"
|
||||||
@@ -1067,7 +1060,6 @@
|
|||||||
import type { AssetRealnamePolicy, PackageSeriesResponse } from '@/types/api'
|
import type { AssetRealnamePolicy, PackageSeriesResponse } from '@/types/api'
|
||||||
import type { EnterpriseItem } from '@/types/api/enterprise'
|
import type { EnterpriseItem } from '@/types/api/enterprise'
|
||||||
import type { AllocateCardsResponse, RecallCardsResponse } from '@/types/api/enterpriseCard'
|
import type { AllocateCardsResponse, RecallCardsResponse } from '@/types/api/enterpriseCard'
|
||||||
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'StandaloneCardList' })
|
defineOptions({ name: 'StandaloneCardList' })
|
||||||
|
|
||||||
@@ -1119,9 +1111,6 @@
|
|||||||
recalled_devices: null
|
recalled_devices: null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 操作审计日志弹窗
|
|
||||||
const operationLogsDialogVisible = ref(false)
|
|
||||||
const operationLogsIdentifier = ref('')
|
|
||||||
const exportDialogVisible = ref(false)
|
const exportDialogVisible = ref(false)
|
||||||
|
|
||||||
// 套餐系列绑定相关
|
// 套餐系列绑定相关
|
||||||
@@ -2323,17 +2312,6 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAuth('iot_card:operation_logs')) {
|
|
||||||
actions.push({
|
|
||||||
label: '操作审计日志',
|
|
||||||
handler: () => {
|
|
||||||
operationLogsIdentifier.value = row.iccid
|
|
||||||
operationLogsDialogVisible.value = true
|
|
||||||
},
|
|
||||||
type: 'primary'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const userType = Number(userStore.getUserInfo.user_type)
|
const userType = Number(userStore.getUserInfo.user_type)
|
||||||
const auditPermission =
|
const auditPermission =
|
||||||
userType === 3
|
userType === 3
|
||||||
|
|||||||
@@ -126,8 +126,6 @@
|
|||||||
multiple
|
multiple
|
||||||
:multiple-limit="100"
|
:multiple-limit="100"
|
||||||
filterable
|
filterable
|
||||||
remote
|
|
||||||
:remote-method="searchPackages"
|
|
||||||
:loading="packageLoading"
|
:loading="packageLoading"
|
||||||
clearable
|
clearable
|
||||||
>
|
>
|
||||||
@@ -139,8 +137,9 @@
|
|||||||
<ElOption
|
<ElOption
|
||||||
v-for="pkg in packageOptions"
|
v-for="pkg in packageOptions"
|
||||||
:key="pkg.id"
|
:key="pkg.id"
|
||||||
:label="pkg.package_name"
|
:label="`${pkg.package_name}${pkg.authorized ? '(已授权)' : ''}`"
|
||||||
:value="pkg.id"
|
:value="pkg.id"
|
||||||
|
:disabled="pkg.authorized"
|
||||||
/>
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
<div class="form-tip">首次创建必须选择 1~100 个不重复套餐</div>
|
<div class="form-tip">首次创建必须选择 1~100 个不重复套餐</div>
|
||||||
@@ -626,7 +625,14 @@
|
|||||||
const currentStep = ref(0) // 当前步骤,0 为第一步,1 为第二步
|
const currentStep = ref(0) // 当前步骤,0 为第一步,1 为第二步
|
||||||
|
|
||||||
const seriesOptions = ref<PackageSeriesResponse[]>([])
|
const seriesOptions = ref<PackageSeriesResponse[]>([])
|
||||||
const packageOptions = ref<PackageResponse[]>([])
|
type GrantPackageOptionView = {
|
||||||
|
id: number
|
||||||
|
package_name: string
|
||||||
|
cost_price: number
|
||||||
|
suggested_retail_price?: number | null
|
||||||
|
authorized: boolean
|
||||||
|
}
|
||||||
|
const packageOptions = ref<GrantPackageOptionView[]>([])
|
||||||
const selectedPackageIds = ref<number[]>([])
|
const selectedPackageIds = ref<number[]>([])
|
||||||
const searchSeriesOptions = ref<PackageSeriesResponse[]>([])
|
const searchSeriesOptions = ref<PackageSeriesResponse[]>([])
|
||||||
const searchShopOptions = ref<ShopResponse[]>([])
|
const searchShopOptions = ref<ShopResponse[]>([])
|
||||||
@@ -1082,7 +1088,7 @@
|
|||||||
isFirstActivation = false
|
isFirstActivation = false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载套餐选项(用于新增对话框)
|
// 加载套餐选项(用于编辑对话框)
|
||||||
const loadPackageOptions = async (packageName?: string) => {
|
const loadPackageOptions = async (packageName?: string) => {
|
||||||
packageLoading.value = true
|
packageLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -1104,8 +1110,17 @@
|
|||||||
page += 1
|
page += 1
|
||||||
} while (allPackages.length < total)
|
} while (allPackages.length < total)
|
||||||
|
|
||||||
// 过滤掉赠送套餐
|
packageOptions.value = allPackages
|
||||||
packageOptions.value = allPackages.filter((pkg) => !pkg.is_gift)
|
.filter((pkg) => !pkg.is_gift)
|
||||||
|
.map(
|
||||||
|
(pkg): GrantPackageOptionView => ({
|
||||||
|
id: pkg.id,
|
||||||
|
package_name: pkg.package_name,
|
||||||
|
cost_price: pkg.cost_price ?? 0,
|
||||||
|
suggested_retail_price: pkg.suggested_retail_price,
|
||||||
|
authorized: false
|
||||||
|
})
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载套餐选项失败:', error)
|
console.error('加载套餐选项失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1113,12 +1128,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索套餐
|
const loadGrantPackageOptions = async () => {
|
||||||
const searchPackages = (query: string) => {
|
const shopId = form.shop_id
|
||||||
if (query) {
|
const seriesId = form.series_id
|
||||||
loadPackageOptions(query)
|
if (dialogType.value !== 'add' || !shopId || !seriesId) return
|
||||||
} else {
|
|
||||||
loadPackageOptions()
|
packageLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await ShopSeriesGrantService.getPackageOptions(shopId, seriesId)
|
||||||
|
if (res.code === 0 && form.shop_id === shopId && form.series_id === seriesId) {
|
||||||
|
packageOptions.value = res.data.items || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载授权套餐候选项失败:', error)
|
||||||
|
} finally {
|
||||||
|
packageLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1263,8 +1287,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新加载该系列下的套餐
|
// 仅在已选择目标店铺后加载该组合的授权套餐候选项
|
||||||
loadPackageOptions()
|
loadGrantPackageOptions()
|
||||||
} else {
|
} else {
|
||||||
// 未选择系列,重置佣金类型
|
// 未选择系列,重置佣金类型
|
||||||
form.series_name = ''
|
form.series_name = ''
|
||||||
@@ -1278,6 +1302,17 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.shop_id,
|
||||||
|
(newShopId, oldShopId) => {
|
||||||
|
if (dialogType.value !== 'add' || newShopId === oldShopId) return
|
||||||
|
form.packages = []
|
||||||
|
selectedPackageIds.value = []
|
||||||
|
packageOptions.value = []
|
||||||
|
loadGrantPackageOptions()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 监听启用强制充值开关,自动设置默认强充金额
|
// 监听启用强制充值开关,自动设置默认强充金额
|
||||||
watch(
|
watch(
|
||||||
() => form.enable_force_recharge,
|
() => form.enable_force_recharge,
|
||||||
@@ -1778,16 +1813,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (packageItems.length > 0) {
|
if (packageItems.length > 0) {
|
||||||
// 验证是否包含赠送套餐
|
|
||||||
for (const pkg of form.packages) {
|
|
||||||
const pkgInfo = packageOptions.value.find((p) => p.id === pkg.package_id)
|
|
||||||
if (pkgInfo?.is_gift) {
|
|
||||||
ElMessage.error(`套餐 ${getPackageName(pkg.package_id)} 是赠送套餐,不能参与授权`)
|
|
||||||
submitLoading.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证成本价是否在允许范围内
|
// 验证成本价是否在允许范围内
|
||||||
for (const pkg of form.packages) {
|
for (const pkg of form.packages) {
|
||||||
const costPrice = pkg.cost_price ?? 0
|
const costPrice = pkg.cost_price ?? 0
|
||||||
|
|||||||
Reference in New Issue
Block a user