fix: 资产相关页面微调

This commit is contained in:
zxr
2026-08-01 16:24:52 +08:00
parent dbda9f76fb
commit 7353c4eede
35 changed files with 273 additions and 271 deletions

View File

@@ -1,22 +1,27 @@
import { request } from '@/api/request'
/** 许可证配置(与 DC-Control `LicenceConfig` / 接口 `data` 一致;字段按实际响应可能部分缺失) */
export interface LicenceConfig {
title?: string
version?: string
company_name?: string
create_time?: string
expire_time?: string
machine_code?: string
max_database?: number
max_middleware?: number
max_pc?: number
max_server?: number
max_client?: number
max_user?: number
max_role?: number
max_permission?: number
max_menu?: number
export interface LicenceQuotas {
max_database: number
max_middleware: number
max_network_device: number
max_security: number
max_storage: number
max_pc: number
max_server: number
max_user: number
max_role: number
max_permission: number
max_menu: number
}
export interface LicenceInfo {
id: string
platform_name: string
workspace: string
issued_on: string
valid_from: string
expires_on: string
quotas: LicenceQuotas
}
/** 获取 采集器 */
@@ -39,7 +44,8 @@ export const updateCollector = (data: any) => request.put(`/DC-Control/v1/collec
export const fetchCollectorStatistics = () => request.get('/DC-Control/v1/statistics')
/** 获取 许可证信息 */
export const fetchLicenseInfo = () => request.get<{ code?: number; data?: LicenceConfig; message?: string }>('/DC-Control/v1/license')
export const fetchLicenseInfo = () =>
request.get<{ code: number; details: LicenceInfo; message: string; timeseq: number }>('/DC-Control/v1/license')
export interface PageResult<T> {
total: number
@@ -182,7 +188,9 @@ export const fetchControlResources = (params?: ResourceListParams) =>
request.get<{ code?: number; details?: PageResult<ControlResource>; message?: string }>('/DC-Control/v1/resources', { params })
export const fetchControlResourceOptions = (params?: { resource_category?: string }) =>
request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/resources/options', { params })
request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/resources/options', {
params,
})
export const createControlResource = (data: ControlResourcePayload) =>
request.post<{ code?: number; details?: ControlResource; message?: string }>('/DC-Control/v1/resources', data)
@@ -203,10 +211,9 @@ export const fetchControlResourceTypeOptions = () =>
request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/resource-types/options')
export const fetchControlMetricDefinitions = (params?: { page?: number; size?: number; keyword?: string; resource_category?: string }) =>
request.get<{ code?: number; details?: PageResult<ControlMetricDefinition>; message?: string }>(
'/DC-Control/v1/metric-definitions',
{ params }
)
request.get<{ code?: number; details?: PageResult<ControlMetricDefinition>; message?: string }>('/DC-Control/v1/metric-definitions', {
params,
})
export const fetchControlMetricDefinitionOptions = (params?: { resource_category?: string }) =>
request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>(
@@ -218,10 +225,14 @@ export const fetchControlMetricSeries = (params?: MetricSeriesListParams) =>
request.get<{ code?: number; details?: PageResult<ControlMetricSeries>; message?: string }>('/DC-Control/v1/metric-series', { params })
export const fetchControlBusinessSystems = (params?: { page?: number; size?: number; keyword?: string; status?: string }) =>
request.get<{ code?: number; details?: PageResult<ControlBusinessSystem>; message?: string }>('/DC-Control/v1/business-systems', { params })
request.get<{ code?: number; details?: PageResult<ControlBusinessSystem>; message?: string }>('/DC-Control/v1/business-systems', {
params,
})
export const fetchControlBusinessSystemOptions = () =>
request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/business-systems/options')
request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>(
'/DC-Control/v1/business-systems/options'
)
export const fetchCollectionStatus = (params?: {
page?: number
@@ -231,7 +242,6 @@ export const fetchCollectionStatus = (params?: {
resource_uid?: string
status?: string
}) =>
request.get<{ code?: number; details?: PageResult<ControlCollectionStatus>; message?: string }>(
'/DC-Control/v1/collection/status',
{ params }
)
request.get<{ code?: number; details?: PageResult<ControlCollectionStatus>; message?: string }>('/DC-Control/v1/collection/status', {
params,
})

View File

@@ -1,5 +1,13 @@
import { request } from '@/api/request'
/** IPAM服务统一响应 */
export interface IPAMResponse<T> {
code: number
details: T
message?: string
timeseq?: number
}
/** IP地址运行状态 */
export type IPStatus = 'online' | 'offline' | 'unknown'
@@ -118,7 +126,8 @@ export interface IPGroupListParams {
/** IP分组列表响应 */
export interface IPGroupListResponse {
data: IPGroupItem[]
list: IPGroupItem[]
count: number
}
/** IP分组表单数据 */
@@ -359,92 +368,96 @@ export interface IPAnomalyFormData {
/** ========== 概览 API ========== */
/** 获取IPAM概览统计 */
export const fetchIPAMOverview = () => request.get<IPAMOverview>('/DC-Control/v1/ipam/overview')
export const fetchIPAMOverview = () => request.get<IPAMResponse<IPAMOverview>>('/DC-Control/v1/ipam/overview')
/** ========== IP地址 API ========== */
/** 获取IP地址列表 */
export const fetchIPAddressList = (params?: IPAddressListParams) =>
request.get<IPAddressListResponse>('/DC-Control/v1/ipaddresses', { params })
request.get<IPAMResponse<IPAddressListResponse>>('/DC-Control/v1/ipaddresses', { params })
/** 获取IP地址详情 */
export const fetchIPAddressDetail = (id: number) => request.get<IPAddressItem>(`/DC-Control/v1/ipaddresses/${id}`)
export const fetchIPAddressDetail = (id: number) => request.get<IPAMResponse<IPAddressItem>>(`/DC-Control/v1/ipaddresses/${id}`)
/** 创建IP地址 */
export const createIPAddress = (data: IPAddressFormData) => request.post<IPAddressItem>('/DC-Control/v1/ipaddresses', data)
export const createIPAddress = (data: IPAddressFormData) => request.post<IPAMResponse<IPAddressItem>>('/DC-Control/v1/ipaddresses', data)
/** 更新IP地址 */
export const updateIPAddress = (id: number, data: Partial<IPAddressFormData>) =>
request.put<{ message: string }>(`/DC-Control/v1/ipaddresses/${id}`, data)
request.put<IPAMResponse<{ message: string }>>(`/DC-Control/v1/ipaddresses/${id}`, data)
/** 删除IP地址 */
export const deleteIPAddress = (id: number) => request.delete<{ message: string }>(`/DC-Control/v1/ipaddresses/${id}`)
export const deleteIPAddress = (id: number) => request.delete<IPAMResponse<{ message: string }>>(`/DC-Control/v1/ipaddresses/${id}`)
/** ========== IP分组 API ========== */
/** 获取IP分组列表树形 */
export const fetchIPGroupList = (params?: IPGroupListParams) => request.get<IPGroupListResponse>('/DC-Control/v1/ip-groups', { params })
export const fetchIPGroupList = (params?: IPGroupListParams) =>
request.get<IPAMResponse<IPGroupListResponse>>('/DC-Control/v1/ip-groups', { params })
/** 创建IP分组 */
export const createIPGroup = (data: IPGroupFormData) => request.post<IPGroupItem>('/DC-Control/v1/ip-groups', data)
export const createIPGroup = (data: IPGroupFormData) => request.post<IPAMResponse<IPGroupItem>>('/DC-Control/v1/ip-groups', data)
/** 更新IP分组 */
export const updateIPGroup = (id: number, data: Partial<IPGroupFormData>) =>
request.put<{ message: string }>(`/DC-Control/v1/ip-groups/${id}`, data)
request.put<IPAMResponse<{ message: string }>>(`/DC-Control/v1/ip-groups/${id}`, data)
/** 删除IP分组 */
export const deleteIPGroup = (id: number) => request.delete<{ message: string }>(`/DC-Control/v1/ip-groups/${id}`)
export const deleteIPGroup = (id: number) => request.delete<IPAMResponse<{ message: string }>>(`/DC-Control/v1/ip-groups/${id}`)
/** ========== IP子网 API ========== */
/** 获取IP子网列表 */
export const fetchIPSubnetList = (params?: IPSubnetListParams) => request.get<IPSubnetListResponse>('/DC-Control/v1/ip-subnets', { params })
export const fetchIPSubnetList = (params?: IPSubnetListParams) =>
request.get<IPAMResponse<IPSubnetListResponse>>('/DC-Control/v1/ip-subnets', { params })
/** 获取IP子网详情 */
export const fetchIPSubnetDetail = (id: number) => request.get<IPSubnetItem>(`/DC-Control/v1/ip-subnets/${id}`)
export const fetchIPSubnetDetail = (id: number) => request.get<IPAMResponse<IPSubnetItem>>(`/DC-Control/v1/ip-subnets/${id}`)
/** 创建IP子网 */
export const createIPSubnet = (data: IPSubnetFormData) => request.post<IPSubnetItem>('/DC-Control/v1/ip-subnets', data)
export const createIPSubnet = (data: IPSubnetFormData) => request.post<IPAMResponse<IPSubnetItem>>('/DC-Control/v1/ip-subnets', data)
/** 更新IP子网 */
export const updateIPSubnet = (id: number, data: Partial<IPSubnetFormData>) =>
request.put<{ message: string }>(`/DC-Control/v1/ip-subnets/${id}`, data)
request.put<IPAMResponse<{ message: string }>>(`/DC-Control/v1/ip-subnets/${id}`, data)
/** 删除IP子网 */
export const deleteIPSubnet = (id: number) => request.delete<{ message: string }>(`/DC-Control/v1/ip-subnets/${id}`)
export const deleteIPSubnet = (id: number) => request.delete<IPAMResponse<{ message: string }>>(`/DC-Control/v1/ip-subnets/${id}`)
/** ========== DHCP租约 API ========== */
/** 获取DHCP租约列表 */
export const fetchDHCPLeaseList = (params?: DHCPLeaseListParams) =>
request.get<DHCPLeaseListResponse>('/DC-Control/v1/ipam/dhcp-leases', { params })
request.get<IPAMResponse<DHCPLeaseListResponse>>('/DC-Control/v1/ipam/dhcp-leases', { params })
/** 创建DHCP租约 */
export const createDHCPLease = (data: DHCPLeaseFormData) => request.post<DHCPLeaseItem>('/DC-Control/v1/ipam/dhcp-leases', data)
export const createDHCPLease = (data: DHCPLeaseFormData) =>
request.post<IPAMResponse<DHCPLeaseItem>>('/DC-Control/v1/ipam/dhcp-leases', data)
/** ========== IP冲突 API ========== */
/** 获取IP冲突列表 */
export const fetchIPConflictList = (params?: IPConflictListParams) =>
request.get<IPConflictListResponse>('/DC-Control/v1/ipam/conflicts', { params })
request.get<IPAMResponse<IPConflictListResponse>>('/DC-Control/v1/ipam/conflicts', { params })
/** 创建IP冲突记录 */
export const createIPConflict = (data: IPConflictFormData) => request.post<IPConflictItem>('/DC-Control/v1/ipam/conflicts', data)
export const createIPConflict = (data: IPConflictFormData) =>
request.post<IPAMResponse<IPConflictItem>>('/DC-Control/v1/ipam/conflicts', data)
/** ========== IP变更 API ========== */
/** 获取IP变更列表 */
export const fetchIPChangeList = (params?: IPChangeListParams) =>
request.get<IPChangeListResponse>('/DC-Control/v1/ipam/changes', { params })
request.get<IPAMResponse<IPChangeListResponse>>('/DC-Control/v1/ipam/changes', { params })
/** 创建IP变更记录 */
export const createIPChange = (data: IPChangeFormData) => request.post<IPChangeItem>('/DC-Control/v1/ipam/changes', data)
export const createIPChange = (data: IPChangeFormData) => request.post<IPAMResponse<IPChangeItem>>('/DC-Control/v1/ipam/changes', data)
/** ========== IP异常 API ========== */
/** 获取IP异常列表 */
export const fetchIPAnomalyList = (params?: IPAnomalyListParams) =>
request.get<IPAnomalyListResponse>('/DC-Control/v1/ipam/anomalies', { params })
request.get<IPAMResponse<IPAnomalyListResponse>>('/DC-Control/v1/ipam/anomalies', { params })
/** 创建IP异常记录 */
export const createIPAnomaly = (data: IPAnomalyFormData) => request.post<IPAnomalyItem>('/DC-Control/v1/ipam/anomalies', data)
export const createIPAnomaly = (data: IPAnomalyFormData) => request.post<IPAMResponse<IPAnomalyItem>>('/DC-Control/v1/ipam/anomalies', data)

View File

@@ -9,7 +9,7 @@ export interface RoomDeviceItem {
service_identity: string
name: string
description: string
room_id: string
room_id: number
device_code?: string
device_category: string
type?: string
@@ -69,7 +69,7 @@ export interface RoomDeviceCreateData {
service_identity?: string
name: string
description?: string
room_id: string
room_id: number
device_category: string
agent_config?: string
collect_method?: 'api' | 'snmp'
@@ -97,7 +97,7 @@ export interface RoomDeviceCreateData {
export interface RoomDeviceUpdateData {
name?: string
description?: string
room_id?: string
room_id?: number
device_category?: string
agent_config?: string
collect_method?: 'api' | 'snmp'

View File

@@ -67,7 +67,7 @@ export interface StorageListParams {
/** 创建存储设备请求参数 */
export interface StorageCreateData {
service_identity: string
service_identity?: string
name: string
category?: string
type: string

View File

@@ -15,6 +15,7 @@ export interface LoginData {
/** 用户信息 */
export interface UserItem {
id?: number
user_id?: number
account?: string
password?: string
confirmPassword?: string

View File

@@ -93,7 +93,8 @@
<li>
<a-dropdown trigger="click">
<a-avatar :size="32" :style="{ marginRight: '8px' }">
<img alt="avatar" :src="avatar" />
<img v-if="avatar" alt="avatar" :src="avatar" />
<icon-user v-else />
</a-avatar>
<template #content>
<a-doption>
@@ -149,7 +150,7 @@ const { changeLocale, currentLocale }: any = useLocale()
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen()
const locales = [...LOCALE_OPTIONS]
const avatar = computed(() => {
return userStore.avatar || '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image'
return userStore.avatar
})
const theme = computed(() => {
return appStore.theme

View File

@@ -25,6 +25,7 @@
import { ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { resetUserPassword } from '@/api/module/user'
import type { UserItem } from '@/api/types'
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
interface Props {
@@ -79,12 +80,12 @@ const handleSavePassword = async () => {
try {
// 从 SafeStorage 获取登录用户信息
const userInfo = SafeStorage.get(AppStorageKey.USER_INFO) || {}
const userInfo = SafeStorage.get<UserItem>(AppStorageKey.USER_INFO)
const res = await resetUserPassword({
account: userInfo.account || '',
account: userInfo?.account || '',
code: '123456', // 暂时没校验,随便传
password: form.value.newPassword,
phone: userInfo.phone || '13800138000', // 暂时没校验,随便传
phone: userInfo?.phone || '13800138000', // 暂时没校验,随便传
})
if (res.code === 0) {

View File

@@ -29,6 +29,7 @@
import { ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { modifyUser } from '@/api/module/user'
import type { UserItem } from '@/api/types'
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
interface Props {
@@ -56,11 +57,11 @@ watch(
visible.value = val
if (val) {
// 打开时从存储中获取用户信息
const userInfo = SafeStorage.get(AppStorageKey.USER_INFO) || {}
const userInfo = SafeStorage.get<UserItem>(AppStorageKey.USER_INFO)
form.value = {
account: userInfo.account || '',
name: userInfo.name || '',
email: userInfo.email || '',
account: userInfo?.account || '',
name: userInfo?.name || '',
email: userInfo?.email || '',
}
}
}
@@ -76,11 +77,11 @@ const handleSaveProfile = async () => {
loading.value = true
try {
const userInfo = SafeStorage.get(AppStorageKey.USER_INFO) || {}
const userInfo = SafeStorage.get<UserItem>(AppStorageKey.USER_INFO)
const updatedUserInfo = {
...userInfo,
...form.value,
id: userInfo.user_id,
id: userInfo?.user_id ?? userInfo?.id,
}
const res = await modifyUser(updatedUserInfo)

View File

@@ -19,8 +19,14 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import bannerImage from '@/assets/images/login-banner.png'
interface CarouselItem {
slogan?: string
subSlogan?: string
image: string
}
const { t } = useI18n()
const carouselItem = computed(() => [
const carouselItem = computed<CarouselItem[]>(() => [
{
// slogan: t('login.banner.slogan1'),
// subSlogan: t('login.banner.subSlogan1'),

View File

@@ -89,7 +89,7 @@
<a-button type="text" size="small" :disabled="isActionDisabled(record, 'comment')" @click.stop="handleComment(record)">
评论
</a-button>
<a-dropdown @select="(v) => handleMoreSelect(v, record)">
<a-dropdown @select="handleMoreSelect($event, record)">
<a-button type="text" size="small" @click.stop>更多</a-button>
<template #content>
<a-doption value="detail">详情</a-doption>

View File

@@ -163,7 +163,7 @@
<!-- 分配U位对话框 -->
<allocate-unit-dialog
v-model:visible="allocateVisible"
:rack-id="selectedRackId"
:rack-id="selectedRackId!"
:rack-height="rackInfo.height"
@success="handleRefresh"
/>
@@ -171,7 +171,7 @@
<!-- 预留U位对话框 -->
<reserve-unit-dialog
v-model:visible="reserveVisible"
:rack-id="selectedRackId"
:rack-id="selectedRackId!"
:rack-height="rackInfo.height"
@success="handleRefresh"
/>

View File

@@ -265,7 +265,7 @@ const loadPolicyOptions = async () => {
const loadRoomOptions = async () => {
try {
const response: any = await fetchRoomOptions({ enabled: true })
const response: any = await fetchRoomOptions()
if (Array.isArray(response)) {
roomOptions.value = response
} else if (response && response.details) {
@@ -347,6 +347,11 @@ watch(
const handleOk = async () => {
try {
await formRef.value?.validate()
const roomId = formData.room_id
if (!roomId) {
Message.warning('请选择机房')
return
}
if (formData.collect_method === 'api' && !formData.agent_config?.trim()) {
Message.warning('API 模式下请填写采集地址')
return
@@ -384,7 +389,7 @@ const handleOk = async () => {
const updateData: RoomDeviceUpdateData = {
name: formData.name,
description: formData.description,
room_id: formData.room_id,
room_id: roomId,
device_category: formData.device_category,
agent_config: formData.agent_config,
collect_method: formData.collect_method,
@@ -417,7 +422,7 @@ const handleOk = async () => {
const createData: RoomDeviceCreateData = {
name: formData.name,
description: formData.description,
room_id: formData.room_id,
room_id: roomId,
device_category: formData.device_category,
agent_config: formData.agent_config,
collect_method: formData.collect_method,

View File

@@ -157,7 +157,7 @@ const handleViewMetrics = async () => {
}
}
const formatTime = (time?: string) => {
const formatTime = (time?: string | null) => {
if (!time) return '-'
if (time.startsWith('0001-01-01')) return '-'
const date = new Date(time)

View File

@@ -160,7 +160,7 @@ const handleViewMetrics = async () => {
}
}
const formatTime = (time?: string) => {
const formatTime = (time?: string | null) => {
if (!time) return '-'
if (time.startsWith('0001-01-01')) return '-'
const date = new Date(time)

View File

@@ -443,7 +443,7 @@ async function loadHardware() {
if (isHostHardwareApiSuccess(colRes)) {
const col = unwrapHostHardwareDetails(colRes)
const did = col?.device_id
deviceId.value = col?.device_id
deviceId.value = did ?? null
if (did) {
const detailOk = await loadDeviceDetailIntoForm(did, gen)
if (gen !== hardwareLoadGeneration) return

View File

@@ -181,7 +181,9 @@ let assetSearchTimer: ReturnType<typeof setTimeout> | null = null
const isEdit = computed(() => !!props.record?.id)
const formData = reactive<ServerFormData>({
type ServerDialogFormData = ServerFormData & Required<Pick<ServerFormData, 'host'>>
const formData = reactive<ServerDialogFormData>({
server_identity: '',
name: '',
host: '',

View File

@@ -331,16 +331,16 @@ function formatDateTime(v?: string) {
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function formatStatusText(status: string) {
function formatStatusText(status?: string) {
const m: Record<string, string> = {
online: '在线',
offline: '离线',
unknown: '未知',
}
return m[status] || status || '-'
return m[status || ''] || status || '-'
}
function getStatusColor(status: string) {
function getStatusColor(status?: string) {
const colorMap: Record<string, string> = {
online: 'green',
offline: 'red',
@@ -348,7 +348,7 @@ function getStatusColor(status: string) {
warning: 'orange',
success: 'green',
}
return colorMap[status] || 'gray'
return colorMap[status || ''] || 'gray'
}
function isMetricsTarget(d: RoomDeviceItem) {

View File

@@ -409,7 +409,15 @@ function mergeMonitorOptionAsRow(opt: StorageMonitorOptionItem) {
collect_method: 'api',
snmp_target: '',
snmp_port: 161,
snmp_version: 'v2c',
snmp_community: '',
snmp_v3_security_level: '',
snmp_v3_security_name: '',
snmp_v3_auth_protocol: '',
snmp_v3_auth_password: '',
snmp_v3_priv_protocol: '',
snmp_v3_priv_password: '',
snmp_v3_context_name: '',
snmp_timeout_ms: 3000,
snmp_retries: 1,
snmp_oids: '[]',

View File

@@ -476,7 +476,7 @@ const handleScan = async () => {
}
}
const formatRange = (a?: string, b?: string) => {
const formatRange = (a?: string, b?: string | null) => {
if (!a) return ''
const sa = dayjs(a).format('YYYY-MM-DD HH:mm:ss')
const sb = b ? dayjs(b).format('YYYY-MM-DD HH:mm:ss') : '进行中'

View File

@@ -81,7 +81,14 @@ const pagination = reactive({
total: 0,
})
const formModel = reactive({
interface AnomalySearchForm {
keyword: string
subnet_id: number | ''
anomaly_type: string
status: string
}
const formModel = reactive<AnomalySearchForm>({
keyword: '',
subnet_id: '',
anomaly_type: '',
@@ -144,7 +151,7 @@ const loadSubnets = async () => {
try {
const response = await fetchIPSubnetList({ size: 1000 })
if (response && response.code === 0) {
subnets.value = response.details?.data || response.data || []
subnets.value = response.details.data || []
}
} catch (error) {
console.error('Failed to load subnets:', error)
@@ -157,7 +164,10 @@ const loadData = async () => {
const params: IPAnomalyListParams = {
page: pagination.current,
size: pagination.pageSize,
...formModel,
keyword: formModel.keyword,
subnet_id: formModel.subnet_id || undefined,
anomaly_type: formModel.anomaly_type,
status: formModel.status,
}
Object.keys(params).forEach((key) => {
@@ -168,8 +178,8 @@ const loadData = async () => {
const response = await fetchIPAnomalyList(params)
if (response && response.code === 0) {
tableData.value = response.details?.data || response.data || []
pagination.total = response.details?.total || response.total || 0
tableData.value = response.details.data || []
pagination.total = response.details.total || 0
}
} catch (error) {
console.error('Failed to load anomalies:', error)
@@ -178,7 +188,7 @@ const loadData = async () => {
}
}
const handleFormModelUpdate = (model: typeof formModel) => {
const handleFormModelUpdate = (model: Record<string, unknown>) => {
Object.assign(formModel, model)
}

View File

@@ -79,7 +79,13 @@ const pagination = reactive({
total: 0,
})
const formModel = reactive({
interface ChangeSearchForm {
keyword: string
subnet_id: number | ''
change_type: string
}
const formModel = reactive<ChangeSearchForm>({
keyword: '',
subnet_id: '',
change_type: '',
@@ -130,7 +136,7 @@ const loadSubnets = async () => {
try {
const response = await fetchIPSubnetList({ size: 1000 })
if (response && response.code === 0) {
subnets.value = response.details?.data || response.data || []
subnets.value = response.details.data || []
}
} catch (error) {
console.error('Failed to load subnets:', error)
@@ -143,7 +149,9 @@ const loadData = async () => {
const params: IPChangeListParams = {
page: pagination.current,
size: pagination.pageSize,
...formModel,
keyword: formModel.keyword,
subnet_id: formModel.subnet_id || undefined,
change_type: formModel.change_type,
}
Object.keys(params).forEach((key) => {
@@ -154,8 +162,8 @@ const loadData = async () => {
const response = await fetchIPChangeList(params)
if (response && response.code === 0) {
tableData.value = response.details?.data || response.data || []
pagination.total = response.details?.total || response.total || 0
tableData.value = response.details.data || []
pagination.total = response.details.total || 0
}
} catch (error) {
console.error('Failed to load changes:', error)
@@ -164,7 +172,7 @@ const loadData = async () => {
}
}
const handleFormModelUpdate = (model: typeof formModel) => {
const handleFormModelUpdate = (model: Record<string, unknown>) => {
Object.assign(formModel, model)
}

View File

@@ -80,7 +80,13 @@ const pagination = reactive({
total: 0,
})
const formModel = reactive({
interface ConflictSearchForm {
keyword: string
subnet_id: number | ''
status: string
}
const formModel = reactive<ConflictSearchForm>({
keyword: '',
subnet_id: '',
status: '',
@@ -132,7 +138,7 @@ const loadSubnets = async () => {
try {
const response = await fetchIPSubnetList({ size: 1000 })
if (response && response.code === 0) {
subnets.value = response.details?.data || response.data || []
subnets.value = response.details.data || []
}
} catch (error) {
console.error('Failed to load subnets:', error)
@@ -145,7 +151,9 @@ const loadData = async () => {
const params: IPConflictListParams = {
page: pagination.current,
size: pagination.pageSize,
...formModel,
keyword: formModel.keyword,
subnet_id: formModel.subnet_id || undefined,
status: formModel.status,
}
Object.keys(params).forEach((key) => {
@@ -156,8 +164,8 @@ const loadData = async () => {
const response = await fetchIPConflictList(params)
if (response && response.code === 0) {
tableData.value = response.details?.data || response.data || []
pagination.total = response.details?.total || response.total || 0
tableData.value = response.details.data || []
pagination.total = response.details.total || 0
}
} catch (error) {
console.error('Failed to load conflicts:', error)
@@ -166,7 +174,7 @@ const loadData = async () => {
}
}
const handleFormModelUpdate = (model: typeof formModel) => {
const handleFormModelUpdate = (model: Record<string, unknown>) => {
Object.assign(formModel, model)
}

View File

@@ -61,7 +61,12 @@ const pagination = reactive({
total: 0,
})
const formModel = reactive({
interface DHCPLeaseSearchForm {
keyword: string
subnet_id: number | ''
}
const formModel = reactive<DHCPLeaseSearchForm>({
keyword: '',
subnet_id: '',
})
@@ -114,7 +119,7 @@ const loadSubnets = async () => {
try {
const response = await fetchIPSubnetList({ size: 1000 })
if (response && response.code === 0) {
subnets.value = response.details?.data || response.data || []
subnets.value = response.details.data || []
}
} catch (error) {
console.error('Failed to load subnets:', error)
@@ -127,7 +132,8 @@ const loadData = async () => {
const params: DHCPLeaseListParams = {
page: pagination.current,
size: pagination.pageSize,
...formModel,
keyword: formModel.keyword,
subnet_id: formModel.subnet_id || undefined,
}
Object.keys(params).forEach((key) => {
@@ -138,8 +144,8 @@ const loadData = async () => {
const response = await fetchDHCPLeaseList(params)
if (response && response.code === 0) {
tableData.value = response.details?.data || response.data || []
pagination.total = response.details?.total || response.total || 0
tableData.value = response.details.data || []
pagination.total = response.details.total || 0
}
} catch (error) {
console.error('Failed to load DHCP leases:', error)
@@ -148,7 +154,7 @@ const loadData = async () => {
}
}
const handleFormModelUpdate = (model: typeof formModel) => {
const handleFormModelUpdate = (model: Record<string, unknown>) => {
Object.assign(formModel, model)
}

View File

@@ -100,7 +100,15 @@ const pagination = reactive({
total: 0,
})
const formModel = reactive({
interface IPAddressSearchForm {
keyword: string
subnet_id: number | ''
allocation_status: AllocationStatus | ''
status: IPStatus | ''
usage_status: UsageStatus | ''
}
const formModel = reactive<IPAddressSearchForm>({
keyword: '',
subnet_id: '',
allocation_status: '',
@@ -169,7 +177,7 @@ const loadSubnets = async () => {
try {
const response = await fetchIPSubnetList({ size: 1000 })
if (response && response.code === 0) {
subnets.value = response.details?.data || response.data || []
subnets.value = response.details.data || []
}
} catch (error) {
console.error('Failed to load subnets:', error)
@@ -182,7 +190,11 @@ const loadData = async () => {
const params: IPAddressListParams = {
page: pagination.current,
size: pagination.pageSize,
...formModel,
keyword: formModel.keyword,
subnet_id: formModel.subnet_id || undefined,
allocation_status: formModel.allocation_status || undefined,
status: formModel.status || undefined,
usage_status: formModel.usage_status || undefined,
}
Object.keys(params).forEach((key) => {
@@ -193,8 +205,8 @@ const loadData = async () => {
const response = await fetchIPAddressList(params)
if (response && response.code === 0) {
tableData.value = response.details?.data || response.data || []
pagination.total = response.details?.total || response.total || 0
tableData.value = response.details.data || []
pagination.total = response.details.total || 0
}
} catch (error) {
console.error('Failed to load IP addresses:', error)
@@ -203,7 +215,7 @@ const loadData = async () => {
}
}
const handleFormModelUpdate = (model: typeof formModel) => {
const handleFormModelUpdate = (model: Record<string, unknown>) => {
Object.assign(formModel, model)
}

View File

@@ -148,7 +148,7 @@ const loadData = async () => {
try {
const response = await fetchIPAMOverview()
if (response && response.code === 0) {
overview.value = response.details || response.data || response
overview.value = response.details
}
} catch (error) {
console.error('Failed to load overview:', error)

View File

@@ -84,7 +84,12 @@ const pagination = reactive({
total: 0,
})
const formModel = reactive({
interface SubnetSearchForm {
keyword: string
group_id: number | ''
}
const formModel = reactive<SubnetSearchForm>({
keyword: '',
group_id: '',
})
@@ -113,7 +118,7 @@ const loadGroups = async () => {
try {
const response = await fetchIPGroupList()
if (response && response.code === 0) {
groups.value = response.details?.data || response.data || []
groups.value = response.details.list || []
}
} catch (error) {
console.error('Failed to load groups:', error)
@@ -126,7 +131,8 @@ const loadData = async () => {
const params: IPSubnetListParams = {
page: pagination.current,
size: pagination.pageSize,
...formModel,
keyword: formModel.keyword,
group_id: formModel.group_id || undefined,
}
Object.keys(params).forEach((key) => {
@@ -137,8 +143,8 @@ const loadData = async () => {
const response = await fetchIPSubnetList(params)
if (response && response.code === 0) {
tableData.value = response.details?.data || response.data || []
pagination.total = response.details?.total || response.total || 0
tableData.value = response.details.data || []
pagination.total = response.details.total || 0
}
} catch (error) {
console.error('Failed to load subnets:', error)
@@ -147,7 +153,7 @@ const loadData = async () => {
}
}
const handleFormModelUpdate = (model: typeof formModel) => {
const handleFormModelUpdate = (model: Record<string, unknown>) => {
Object.assign(formModel, model)
}

View File

@@ -187,7 +187,7 @@ const formData = reactive<Partial<TopologyGroup> & { parent_id: number }>({
name: '',
description: '',
sort: 0,
enable: 2,
enable: true,
parent_id: 0,
})

View File

@@ -1,7 +1,6 @@
import { computed, ComputedRef, unref, Ref } from 'vue'
import { Edge } from '@vue-flow/core'
type EdgeType = 'default' | 'straight' | 'step' | 'simplebezier'
import type { EdgeType } from '../types'
/**
* 边样式计算Hook

View File

@@ -140,7 +140,7 @@ import '@vue-flow/core/dist/theme-default.css'
import { Message } from '@arco-design/web-vue'
import * as TopoAPI from '@/api/ops/netarchTopo'
import { NodeData, DeviceType } from './types'
import { NodeData, DeviceType, EdgeType } from './types'
import { DEVICE_TYPE_CONFIG } from './config'
import { CustomNode } from './components'
import { useTopoLayout, useEdgeStyles } from './hooks'
@@ -184,7 +184,7 @@ const edges = ref<any[]>([])
// UI控制状态
const selectedGroup = ref<string | null>(null)
const expandedGroups = ref<Set<string>>(new Set())
const edgeType = ref<'default' | 'straight' | 'step' | 'simplebezier'>('default')
const edgeType = ref<EdgeType>('default')
// 节点操作状态
const selectedNode = ref<any>(null)
@@ -422,7 +422,7 @@ const handleLayout = (value: string | number | Record<string, any> | undefined)
// 设置边类型
const setEdgeType = (value: string | number | Record<string, any> | undefined) => {
const type = value as 'default' | 'straight' | 'step' | 'smoothstep' | 'simplebezier'
const type = value as EdgeType
edgeType.value = type
}

View File

@@ -42,4 +42,4 @@ export interface LinkData {
}
// 链路类型(用于边样式)
export type EdgeType = 'default' | 'straight' | 'step' | 'simplebezier'
export type EdgeType = 'default' | 'straight' | 'step' | 'smoothstep' | 'simplebezier'

View File

@@ -555,7 +555,7 @@ const loadTrafficData = async () => {
if (!dashboard?.protocols?.length && latest?.protocol_stats) {
const protocol = parseJsonObject(latest.protocol_stats)
if (protocol && typeof protocol === 'object') {
const total = Object.values(protocol).reduce((sum, v) => sum + Number(v || 0), 0)
const total = Object.values(protocol).reduce<number>((sum, v) => sum + Number(v || 0), 0)
if (total > 0) {
protocolData.value = Object.entries(protocol).map(([name, value], index) => ({
name,

View File

@@ -10,7 +10,7 @@
<div class="stat-title">
<span class="stat-name">{{ card.title }}</span>
<a-tag v-if="card.title === '警告' || card.title === '工单' || card.title === '审核'" size="small" class="stat-tag">
待处理/总数
{{ card.title === '警告' ? '告警中/总数' : '待处理/总数' }}
</a-tag>
<a-tag v-else size="small" class="stat-tag">启用/总数</a-tag>
</div>
@@ -506,11 +506,11 @@ const handleChange = (value: string) => {
// 处理统计卡片点击
const handleCardClick = (cardTitle: string) => {
const routeMap: any = {
服务器及PC: '/ops/monitor/server',
数据库服务: '/ops/dc/database',
网络设备: '/ops/monitor/network',
服务器及PC: '/dc/server',
数据库服务: '/dc/database',
网络设备: '/monitor/network',
警告: '/alert/tackle',
工单: '/ops/ticket',
工单: '/feedback/all',
审核: '/kb/review',
}
const route = routeMap[cardTitle]
@@ -559,7 +559,7 @@ const loadStatistics = async () => {
fetchAlertCount().catch((e) => ({ error: e, success: false })),
fetchCollectorStatistics().catch((e) => ({ error: e, success: false })),
fetchFeedbackTicketStatistics().catch((e) => ({ error: e, success: false })),
fetchHistories({ page: 1, page_size: 5, status: 'pending' }).catch((e) => ({ error: e, success: false })),
fetchHistories({ page: 1, page_size: 5, status: 'firing' }).catch((e) => ({ error: e, success: false })),
fetchReviewStats({ resource_type: 'all' }).catch((e: unknown) => ({ error: e, success: false as const })),
fetchNetworkDeviceList({ page: 1, size: 1 }).catch((e) => ({ error: e, success: false })),
fetchNetworkDeviceList({ page: 1, size: 1, enabled: true }).catch((e) => ({ error: e, success: false })),
@@ -591,7 +591,7 @@ const loadStatistics = async () => {
// 处理告警统计数据
if (alertData.status === 'fulfilled' && !isFailedRequest(alertData.value)) {
statistics.alert = {
pending: alertData.value?.details?.status_counts?.pending || 0,
pending: alertData.value?.details?.status_counts?.firing || 0,
total: alertData.value?.details?.total || 0,
}
} else {
@@ -667,6 +667,7 @@ const getStatusColor = (status: any) => {
const statusStr = typeof status === 'object' ? status?.code || status?.name : status
const colorMap: any = {
pending: '#FF4D4F',
firing: '#FF4D4F',
processing: '#1890FF',
resolved: '#52C41A',
closed: '#8C8C8C',
@@ -679,6 +680,7 @@ const getStatusTagColor = (status: any) => {
const statusStr = typeof status === 'object' ? status?.code || status?.name : status
const colorMap: any = {
pending: 'red',
firing: 'red',
processing: 'blue',
resolved: 'green',
closed: 'gray',
@@ -693,6 +695,7 @@ const getStatusText = (status: any) => {
}
const statusMap: any = {
pending: '待处理',
firing: '告警中',
processing: '处理中',
resolved: '已解决',
closed: '已关闭',

View File

@@ -35,7 +35,7 @@ import { computed, ref } from 'vue'
export interface PermissionItem {
id: number
name: string
title: string
children?: PermissionItem[]
}

View File

@@ -26,7 +26,7 @@
<div class="kv-list">
<div v-for="row in basicRows" :key="row.key" class="basic-row">
<span class="label">{{ row.label }}</span>
<span class="value" :class="{ 'value-code': row.key === 'machine_code' }">{{ row.display }}</span>
<span class="value" :class="{ 'value-code': row.key === 'id' }">{{ row.display }}</span>
</div>
</div>
</a-card>
@@ -35,7 +35,7 @@
<template #title>
<span class="section-title">资源限制</span>
</template>
<p class="quota-hint">配额 0 表示该维度不按许可证限制数量由业务逻辑决定</p>
<p class="quota-hint">配额为 0 表示该资源未授权不能新增</p>
<div class="kv-list">
<div v-for="row in quotaRows" :key="row.key" class="quota-row">
<span class="label">{{ row.label }}</span>
@@ -53,135 +53,42 @@
import { computed, onMounted, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconRefresh } from '@arco-design/web-vue/es/icon'
import { fetchLicenseInfo, type LicenceConfig } from '@/api/ops/dcControl'
import { fetchLicenseInfo, type LicenceInfo } from '@/api/ops/dcControl'
const loading = ref(false)
const loadError = ref(false)
const license = ref<LicenceConfig | null>(null)
const license = ref<LicenceInfo | null>(null)
const dash = (v: string | undefined | null) => (v != null && String(v).length > 0 ? String(v) : '—')
const formatQuota = (n: number | undefined) => {
if (n == null || Number.isNaN(Number(n))) return '—'
const v = Number(n)
return v > 0 ? String(v) : '无限制'
}
function pickStr(o: Record<string, unknown>, snake: string, camel: string): string | undefined {
const v = o[snake] ?? o[camel]
return typeof v === 'string' ? v : v != null ? String(v) : undefined
}
function pickNum(o: Record<string, unknown>, snake: string, camel: string): number | undefined {
const v = o[snake] ?? o[camel]
if (v == null || v === '') return undefined
const n = Number(v)
return Number.isNaN(n) ? undefined : n
}
/** 将接口对象统一为 LicenceConfig兼容 snake_case / camelCase */
function normalizeLicensePayload(raw: Record<string, unknown>): LicenceConfig {
return {
title: pickStr(raw, 'title', 'title'),
version: pickStr(raw, 'version', 'version'),
company_name: pickStr(raw, 'company_name', 'companyName'),
create_time: pickStr(raw, 'create_time', 'createTime'),
expire_time: pickStr(raw, 'expire_time', 'expireTime'),
machine_code: pickStr(raw, 'machine_code', 'machineCode'),
max_database: pickNum(raw, 'max_database', 'maxDatabase'),
max_middleware: pickNum(raw, 'max_middleware', 'maxMiddleware'),
max_pc: pickNum(raw, 'max_pc', 'maxPc'),
max_server: pickNum(raw, 'max_server', 'maxServer'),
max_client: pickNum(raw, 'max_client', 'maxClient'),
max_user: pickNum(raw, 'max_user', 'maxUser'),
max_role: pickNum(raw, 'max_role', 'maxRole'),
max_permission: pickNum(raw, 'max_permission', 'maxPermission'),
max_menu: pickNum(raw, 'max_menu', 'maxMenu'),
}
}
function isLicensePayloadRaw(v: unknown): v is Record<string, unknown> {
if (v == null || typeof v !== 'object' || Array.isArray(v)) return false
const o = v as Record<string, unknown>
const strHit = (s: string, c: string) => {
const x = o[s] ?? o[c]
return typeof x === 'string' && x.length > 0
}
const numHit = (s: string, c: string) => {
const x = o[s] ?? o[c]
return typeof x === 'number' && !Number.isNaN(x)
}
return (
strHit('company_name', 'companyName') ||
strHit('machine_code', 'machineCode') ||
strHit('title', 'title') ||
strHit('version', 'version') ||
numHit('max_database', 'maxDatabase') ||
numHit('max_middleware', 'maxMiddleware') ||
numHit('max_pc', 'maxPc') ||
numHit('max_server', 'maxServer') ||
numHit('max_client', 'maxClient') ||
numHit('max_user', 'maxUser') ||
numHit('max_role', 'maxRole') ||
numHit('max_permission', 'maxPermission') ||
numHit('max_menu', 'maxMenu')
)
}
function parseLicenseResponse(res: unknown): {
code?: number
success?: boolean
data: unknown
message?: string
} {
if (res == null || typeof res !== 'object') {
return { data: undefined, message: undefined }
}
const r = res as Record<string, unknown>
const code = typeof r.code === 'number' ? r.code : undefined
const success = typeof r.success === 'boolean' ? r.success : undefined
const message = typeof r.message === 'string' ? r.message : typeof r.msg === 'string' ? r.msg : undefined
let data: unknown = r.data ?? r.details ?? r.result
if (data == null && isLicensePayloadRaw(r)) {
data = r
}
return { code, success, data, message }
}
function responseIndicatesSuccess(code: number | undefined, success: boolean | undefined): boolean {
if (success === false) return false
if (success === true) return true
if (code === undefined) return true
return code === 200 || code === 0
}
const formatQuota = (value: number) => (value === 0 ? '未授权' : String(value))
const basicRows = computed(() => {
const L = license.value
if (!L) return []
const current = license.value
if (!current) return []
return [
{ key: 'company_name', label: '公司名称', display: dash(L.company_name) },
{ key: 'title', label: '版本标题', display: dash(L.title) },
{ key: 'version', label: '版本号', display: dash(L.version) },
{ key: 'machine_code', label: '机器码', display: dash(L.machine_code) },
{ key: 'create_time', label: '创建时间', display: dash(L.create_time) },
{ key: 'expire_time', label: '过期时间', display: dash(L.expire_time) },
{ key: 'id', label: '许可证 ID', display: current.id },
{ key: 'platform_name', label: '平台名称', display: current.platform_name },
{ key: 'workspace', label: 'Workspace', display: current.workspace },
{ key: 'issued_on', label: '签发日期', display: current.issued_on },
{ key: 'valid_from', label: '生效日期', display: current.valid_from },
{ key: 'expires_on', label: '到期日期', display: current.expires_on },
]
})
const quotaRows = computed(() => {
const L = license.value
if (!L) return []
const quotas = license.value?.quotas
if (!quotas) return []
return [
{ key: 'max_database', label: '数据库', display: formatQuota(L.max_database) },
{ key: 'max_middleware', label: '中间件', display: formatQuota(L.max_middleware) },
{ key: 'max_pc', label: 'PC', display: formatQuota(L.max_pc) },
{ key: 'max_server', label: '服务器', display: formatQuota(L.max_server) },
{ key: 'max_client', label: '客户端', display: formatQuota(L.max_client) },
{ key: 'max_user', label: '用户', display: formatQuota(L.max_user) },
{ key: 'max_role', label: '角色', display: formatQuota(L.max_role) },
{ key: 'max_permission', label: '权限', display: formatQuota(L.max_permission) },
{ key: 'max_menu', label: '菜单', display: formatQuota(L.max_menu) },
{ key: 'max_database', label: '数据库', display: formatQuota(quotas.max_database) },
{ key: 'max_middleware', label: '中间件', display: formatQuota(quotas.max_middleware) },
{ key: 'max_network_device', label: '网络设备', display: formatQuota(quotas.max_network_device) },
{ key: 'max_security', label: '安全设备', display: formatQuota(quotas.max_security) },
{ key: 'max_storage', label: '存储设备', display: formatQuota(quotas.max_storage) },
{ key: 'max_pc', label: 'PC', display: formatQuota(quotas.max_pc) },
{ key: 'max_server', label: '服务器', display: formatQuota(quotas.max_server) },
{ key: 'max_user', label: '用户', display: formatQuota(quotas.max_user) },
{ key: 'max_role', label: '角色', display: formatQuota(quotas.max_role) },
{ key: 'max_permission', label: '权限', display: formatQuota(quotas.max_permission) },
{ key: 'max_menu', label: '菜单', display: formatQuota(quotas.max_menu) },
]
})
@@ -189,28 +96,22 @@ async function loadLicense() {
loading.value = true
loadError.value = false
try {
const res = await fetchLicenseInfo()
const { code, success, data, message } = parseLicenseResponse(res)
const ok = responseIndicatesSuccess(code, success)
if (data != null && typeof data === 'object' && !Array.isArray(data) && isLicensePayloadRaw(data) && ok) {
license.value = normalizeLicensePayload(data as Record<string, unknown>)
loadError.value = false
} else {
loadError.value = true
Message.error(message || '获取许可证失败')
const response = await fetchLicenseInfo()
if (response.code !== 0) {
throw new Error(response.message || '获取许可证失败')
}
} catch (e) {
license.value = response.details
} catch (error) {
loadError.value = true
console.error('[license-center] fetchLicenseInfo', e)
Message.error('获取许可证失败')
license.value = null
console.error('[license-center] fetchLicenseInfo', error)
Message.error(error instanceof Error ? error.message : '获取许可证失败')
} finally {
loading.value = false
}
}
onMounted(() => {
loadLicense()
})
onMounted(loadLicense)
</script>
<script lang="ts">

View File

@@ -4,6 +4,7 @@
"module": "ES2020",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"jsx": "preserve",
"allowJs": true,
"sourceMap": true,