fix
This commit is contained in:
@@ -1,5 +1,24 @@
|
||||
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 const fetchCollectors = (data: { page: number, size: number, keyword?: string }) => request.get("/DC-Control/v1/collectors", { params: data });
|
||||
|
||||
@@ -19,4 +38,5 @@ 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("/DC-Control/v1/license");
|
||||
export const fetchLicenseInfo = () =>
|
||||
request.get<{ code?: number; data?: LicenceConfig; message?: string }>("/DC-Control/v1/license");
|
||||
|
||||
@@ -32,6 +32,7 @@ export default {
|
||||
'menu.ops.systemSettings': 'System Settings',
|
||||
'menu.ops.systemSettings.menuManagement': 'Menu Management',
|
||||
'menu.ops.systemSettings.systemLogs': 'System Logs',
|
||||
'menu.ops.systemSettings.licenseCenter': 'License Center',
|
||||
'menu.ops.webTest': 'Web Test',
|
||||
'menu.ops.report': 'Report Management',
|
||||
'menu.ops.report.history': 'Report History',
|
||||
|
||||
@@ -32,6 +32,7 @@ export default {
|
||||
'menu.ops.systemSettings': '系统设置',
|
||||
'menu.ops.systemSettings.menuManagement': '菜单管理',
|
||||
'menu.ops.systemSettings.systemLogs': '系统日志',
|
||||
'menu.ops.systemSettings.licenseCenter': '许可授权中心',
|
||||
'menu.ops.webTest': '网页测试',
|
||||
'menu.ops.report': '报表管理',
|
||||
'menu.ops.report.history': '报表历史',
|
||||
|
||||
@@ -181,6 +181,11 @@ function extractRelativePath(childPath: string, parentPath: string): string {
|
||||
* @param parentIsFull 父级菜单的 is_full 字段
|
||||
* @returns 子路由配置数组
|
||||
*/
|
||||
/** 服务端未配置 component 时按 menu_path 绑定视图(避免误用 redirect 导致白屏) */
|
||||
const MENU_PATH_COMPONENT_FALLBACK: { test: (fullPath: string) => boolean; component: string }[] = [
|
||||
{ test: (p) => p.includes('license-center'), component: 'ops/pages/system-settings/license-center' },
|
||||
]
|
||||
|
||||
function transformChildRoutes(
|
||||
children: ServerMenuItem[],
|
||||
parentComponent?: string,
|
||||
@@ -188,10 +193,16 @@ function transformChildRoutes(
|
||||
parentIsFull?: boolean
|
||||
): AppRouteRecordRaw[] {
|
||||
return children.map((child) => {
|
||||
// 优先使用子菜单自己的 component,否则继承父级的 component
|
||||
const componentPath = child.component || parentComponent
|
||||
// 计算子路由的相对路径
|
||||
// 计算子路由的相对路径(需先于 component 解析,供 path 兜底使用)
|
||||
const childFullPath = child.menu_path || child.path || ''
|
||||
|
||||
// 优先使用子菜单自己的 component,否则继承父级的 component;再按路径兜底
|
||||
let componentPath = child.component || parentComponent
|
||||
const matchedFallback = MENU_PATH_COMPONENT_FALLBACK.find((fb) => fb.test(childFullPath))
|
||||
if (matchedFallback) {
|
||||
componentPath = matchedFallback.component
|
||||
}
|
||||
|
||||
const relativePath = extractRelativePath(childFullPath, parentPath || '')
|
||||
|
||||
const route: AppRouteRecordRaw = {
|
||||
|
||||
@@ -32,6 +32,16 @@ const OPS: AppRouteRecordRaw = {
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'license-center',
|
||||
name: 'LicenseCenter',
|
||||
component: () => import('@/views/ops/pages/system-settings/license-center/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.ops.systemSettings.licenseCenter',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'web-test',
|
||||
name: 'WebTest',
|
||||
|
||||
379
src/views/ops/pages/system-settings/license-center/index.vue
Normal file
379
src/views/ops/pages/system-settings/license-center/index.vue
Normal file
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.licenseCenter']" />
|
||||
|
||||
<a-card class="license-page-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="card-head-title">
|
||||
<div class="page-title">许可证信息</div>
|
||||
<div class="page-subtitle">{{ license?.company_name || '—' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button type="outline" :loading="loading" :disabled="loading" @click="loadLicense">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-spin :loading="loading" class="page-spin">
|
||||
<a-empty v-if="!loading && loadError && !license" description="加载失败,请稍后重试" />
|
||||
|
||||
<template v-else-if="license">
|
||||
<a-card class="section-card" :bordered="false">
|
||||
<template #title>
|
||||
<span class="section-title">基本信息</span>
|
||||
</template>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card class="section-card section-card--quota" :bordered="false">
|
||||
<template #title>
|
||||
<span class="section-title">资源限制</span>
|
||||
</template>
|
||||
<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>
|
||||
<span class="value">{{ row.display }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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'
|
||||
|
||||
const loading = ref(false)
|
||||
const loadError = ref(false)
|
||||
const license = ref<LicenceConfig | 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 basicRows = computed(() => {
|
||||
const L = license.value
|
||||
if (!L) 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) },
|
||||
]
|
||||
})
|
||||
|
||||
const quotaRows = computed(() => {
|
||||
const L = license.value
|
||||
if (!L) 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) },
|
||||
]
|
||||
})
|
||||
|
||||
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 || '获取许可证失败')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError.value = true
|
||||
console.error('[license-center] fetchLicenseInfo', e)
|
||||
Message.error('获取许可证失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLicense()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'LicenseCenter',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* 与 system-settings / 报表等运维页一致 */
|
||||
.container {
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
.license-page-card {
|
||||
:deep(.arco-card-body) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-head-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: var(--color-text-2);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.page-spin {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
margin-bottom: 16px;
|
||||
background: var(--color-fill-1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-card-header) {
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 0;
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
}
|
||||
|
||||
.section-card--quota {
|
||||
:deep(.arco-card-body) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.quota-hint {
|
||||
margin: 0;
|
||||
padding: 12px 20px 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.5;
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
|
||||
.kv-list {
|
||||
padding: 0;
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
|
||||
.basic-row,
|
||||
.quota-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--color-border-1);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.quota-row {
|
||||
align-items: center;
|
||||
|
||||
.value {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
.value-code {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.container {
|
||||
padding: 0 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.arco-card-header) {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.basic-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
|
||||
.label {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.quota-row .label {
|
||||
width: auto;
|
||||
min-width: 72px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user