This commit is contained in:
@@ -19,7 +19,9 @@ import type {
|
|||||||
AssetOrdersParams,
|
AssetOrdersParams,
|
||||||
AssetOrdersResponse,
|
AssetOrdersResponse,
|
||||||
UpdateAssetRealnameStatusRequest,
|
UpdateAssetRealnameStatusRequest,
|
||||||
DtoUpdateAssetRealnameStatusResponse
|
DtoUpdateAssetRealnameStatusResponse,
|
||||||
|
AssetOperationLogsResponse,
|
||||||
|
AssetOperationLogsParams
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
export class AssetService extends BaseService {
|
export class AssetService extends BaseService {
|
||||||
@@ -231,4 +233,22 @@ 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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -434,3 +434,52 @@ 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
|
||||||
|
operation_type?: string // 操作类型
|
||||||
|
result_status?: 'success' | 'failed' | 'denied' // 执行结果
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
<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.operation_type"
|
||||||
|
placeholder="操作类型"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
@change="handleFilterChange"
|
||||||
|
>
|
||||||
|
<ElOption label="设备WiFi设置" value="device_set_wifi" />
|
||||||
|
<ElOption label="设备切卡" value="device_switch_card" />
|
||||||
|
<ElOption label="设备切卡模式" value="device_switch_mode" />
|
||||||
|
<ElOption label="设备停机" value="device_stop" />
|
||||||
|
<ElOption label="设备复机" value="device_start" />
|
||||||
|
<ElOption label="设备重启" value="device_reboot" />
|
||||||
|
<ElOption label="设备重置" value="device_reset" />
|
||||||
|
<ElOption label="卡分配" value="card_allocate" />
|
||||||
|
<ElOption label="卡回收" value="card_recall" />
|
||||||
|
<ElOption label="卡停机" value="card_stop" />
|
||||||
|
<ElOption label="卡复机" value="card_start" />
|
||||||
|
<ElOption label="设备分配" value="device_allocate" />
|
||||||
|
<ElOption label="设备回收" value="device_recall" />
|
||||||
|
<ElOption label="实名策略" value="asset_realname_policy" />
|
||||||
|
<ElOption label="实名状态" value="asset_realname_status" />
|
||||||
|
<ElOption label="轮询开关" value="asset_polling" />
|
||||||
|
</ElSelect>
|
||||||
|
<ElSelect
|
||||||
|
v-model="filterForm.result_status"
|
||||||
|
placeholder="执行结果"
|
||||||
|
clearable
|
||||||
|
style="width: 120px"
|
||||||
|
@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.operator_type) }}
|
||||||
|
</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="250">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="getChangeContent(row)" class="change-content">
|
||||||
|
<span
|
||||||
|
v-for="(change, index) in getChangeContent(row)"
|
||||||
|
:key="index"
|
||||||
|
class="change-item"
|
||||||
|
>
|
||||||
|
<span class="field-name">{{ change.fieldName }}</span>
|
||||||
|
<span class="field-value">
|
||||||
|
<template v-if="change.beforeValue !== undefined">
|
||||||
|
<span class="old-value">{{ formatValue(change.beforeValue) }}</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span class="new-value">{{ formatValue(change.afterValue) }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="new-value">{{ formatValue(change.afterValue) }}</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-else-if="row.fail_count > 0 && row.failed_items?.length" class="fail-reason">
|
||||||
|
失败 {{ row.fail_count }} 项
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
<ElPagination
|
||||||
|
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"
|
||||||
|
style="margin-top: 16px; justify-content: flex-end"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
ElCard,
|
||||||
|
ElTable,
|
||||||
|
ElTableColumn,
|
||||||
|
ElTag,
|
||||||
|
ElButton,
|
||||||
|
ElSelect,
|
||||||
|
ElOption,
|
||||||
|
ElPagination,
|
||||||
|
ElMessage
|
||||||
|
} from 'element-plus'
|
||||||
|
import { AssetService } from '@/api/modules'
|
||||||
|
import type { AssetOperationLogItem } from '@/types/api'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
assetIdentifier?: string
|
||||||
|
assetType?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChangeItem {
|
||||||
|
fieldName: string
|
||||||
|
beforeValue?: any
|
||||||
|
afterValue?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const logList = ref<AssetOperationLogItem[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
const filterForm = reactive({
|
||||||
|
operation_type: '',
|
||||||
|
result_status: '' as '' | 'success' | 'failed' | 'denied'
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
operation_type: filterForm.operation_type || undefined,
|
||||||
|
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.operation_type = ''
|
||||||
|
filterForm.result_status = ''
|
||||||
|
pagination.page = 1
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = () => {
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOperationTypeTag = (type: string) => {
|
||||||
|
const tagMap: Record<string, string> = {
|
||||||
|
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',
|
||||||
|
card_allocate: 'primary',
|
||||||
|
card_recall: 'warning',
|
||||||
|
card_stop: 'danger',
|
||||||
|
card_start: 'success',
|
||||||
|
asset_realname_policy: 'info',
|
||||||
|
asset_realname_status: 'info',
|
||||||
|
asset_polling: 'info'
|
||||||
|
}
|
||||||
|
return tagMap[type] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getResultStatusTag = (status: string) => {
|
||||||
|
const tagMap: Record<string, string> = {
|
||||||
|
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 getOperatorTypeText = (type: string) => {
|
||||||
|
const textMap: Record<string, string> = {
|
||||||
|
admin_user: '后台用户',
|
||||||
|
enterprise_user: '企业用户',
|
||||||
|
api_user: 'API用户'
|
||||||
|
}
|
||||||
|
return textMap[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (typeof value === 'object') return JSON.stringify(value)
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFieldValue = (key: string, value: any): string => {
|
||||||
|
if (key === 'enable_polling') return value ? '开启' : '关闭'
|
||||||
|
if (key === 'realname_policy') {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
none: '无需实名',
|
||||||
|
before_order: '先实名后充值',
|
||||||
|
after_order: '先充值后实名'
|
||||||
|
}
|
||||||
|
return map[value] || formatValue(value)
|
||||||
|
}
|
||||||
|
if (key === 'real_name_status') return value === 1 ? '已实名' : '未实名'
|
||||||
|
if (key === 'switch_mode') return value === 0 ? '自动' : '手动'
|
||||||
|
if (key === 'new_status') {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
allocated: '已分配',
|
||||||
|
recalled: '已回收'
|
||||||
|
}
|
||||||
|
return map[value] || formatValue(value)
|
||||||
|
}
|
||||||
|
return formatValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChangeContent = (row: AssetOperationLogItem): ChangeItem[] => {
|
||||||
|
const after = row.operation_content_after
|
||||||
|
const before = row.operation_content_before
|
||||||
|
const desc = row.operation_fields_desc || {}
|
||||||
|
|
||||||
|
if (!after || Object.keys(after).length === 0) return []
|
||||||
|
|
||||||
|
const changes: ChangeItem[] = []
|
||||||
|
for (const key of Object.keys(after)) {
|
||||||
|
const fieldName = desc[key] || key
|
||||||
|
changes.push({
|
||||||
|
fieldName,
|
||||||
|
beforeValue: before?.[key],
|
||||||
|
afterValue: after[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.assetIdentifier,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.operation-logs-card {
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table-wrapper {
|
||||||
|
.logs-table {
|
||||||
|
: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;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.field-name {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-value {
|
||||||
|
.old-value {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
color: var(--el-text-color-light);
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-value {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fail-reason {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -82,6 +82,14 @@
|
|||||||
@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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -162,6 +170,7 @@
|
|||||||
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 {
|
||||||
|
|||||||
@@ -672,7 +672,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'package_name',
|
prop: 'package_name',
|
||||||
label: '套餐名称',
|
label: '套餐名称',
|
||||||
minWidth: 160,
|
width: 200,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
formatter: (row: PackageResponse) => {
|
formatter: (row: PackageResponse) => {
|
||||||
const hasPermission = hasAuth('package:detail')
|
const hasPermission = hasAuth('package:detail')
|
||||||
|
|||||||
@@ -564,7 +564,8 @@
|
|||||||
{
|
{
|
||||||
prop: 'series_name',
|
prop: 'series_name',
|
||||||
label: '系列名称',
|
label: '系列名称',
|
||||||
minWidth: 150,
|
minWidth: 200,
|
||||||
|
showOverflowTooltip: true,
|
||||||
formatter: (row: PackageSeriesResponse) => {
|
formatter: (row: PackageSeriesResponse) => {
|
||||||
const hasPermission = hasAuth('package_series:detail')
|
const hasPermission = hasAuth('package_series:detail')
|
||||||
return h(
|
return h(
|
||||||
|
|||||||
@@ -1141,7 +1141,8 @@
|
|||||||
{
|
{
|
||||||
prop: 'series_name',
|
prop: 'series_name',
|
||||||
label: '系列名称',
|
label: '系列名称',
|
||||||
minWidth: 150,
|
width: 200,
|
||||||
|
showOverflowTooltip: true,
|
||||||
formatter: (row: ShopSeriesGrantResponse) => {
|
formatter: (row: ShopSeriesGrantResponse) => {
|
||||||
const hasPermission = hasAuth('series_grants:detail')
|
const hasPermission = hasAuth('series_grants:detail')
|
||||||
return h(
|
return h(
|
||||||
|
|||||||
Reference in New Issue
Block a user