Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions server/source/system/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "computeNode", Name: "computeNode", Component: "view/computenode/computeNode/computeNode.vue", Sort: 0, Meta: Meta{Title: "算力节点", Icon: "chicken"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "productSpec", Name: "productSpec", Component: "view/product/productSpec/productSpec.vue", Sort: 0, Meta: Meta{Title: "产品规格", Icon: "baseball"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "instance", Name: "instance", Component: "view/instance/instance/instance.vue", Sort: 0, Meta: Meta{Title: "实例管理", Icon: "briefcase"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "pcdn", Name: "pcdn", Component: "view/routerHolder.vue", Sort: 11, Meta: Meta{Title: "PCDN管理", Icon: "histogram"}},
}

// 先创建父级菜单(ParentId = 0 的菜单)
Expand Down Expand Up @@ -118,6 +119,12 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er

// portForward子菜单
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["portForward"], Path: "portForwardRules", Name: "portForwardRules", Component: "plugin/portforward/view/portForward.vue", Sort: 1, Meta: Meta{Title: "转发规则", Icon: "list"}},

// pcdn子菜单
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["pcdn"], Path: "pcdnDashboard", Name: "pcdnDashboard", Component: "view/pcdn/dashboard.vue", Sort: 1, Meta: Meta{Title: "PCDN概览", Icon: "data-analysis"}},
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["pcdn"], Path: "pcdnNodeManagement", Name: "pcdnNodeManagement", Component: "view/pcdn/nodeManagement.vue", Sort: 2, Meta: Meta{Title: "节点管理", Icon: "cpu"}},
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["pcdn"], Path: "pcdnPolicyManagement", Name: "pcdnPolicyManagement", Component: "view/pcdn/policyManagement.vue", Sort: 3, Meta: Meta{Title: "策略管理", Icon: "set-up"}},
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["pcdn"], Path: "pcdnDispatchTasks", Name: "pcdnDispatchTasks", Component: "view/pcdn/dispatchTasks.vue", Sort: 4, Meta: Meta{Title: "调度任务", Icon: "list"}},
}

// 创建子菜单
Expand Down
72 changes: 72 additions & 0 deletions web/src/api/pcdn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import service from '@/utils/request'

export const getPcdnNodeList = (params) => {
return service({
url: '/pcdn/node/list',
method: 'get',
params
})
}

export const getPcdnPolicyList = (params) => {
return service({
url: '/pcdn/policy/list',
method: 'get',
params
})
}

export const createPcdnPolicy = (data) => {
return service({
url: '/pcdn/policy/create',
method: 'post',
data
})
}

export const updatePcdnPolicyWeight = (data) => {
return service({
url: '/pcdn/policy/weight',
method: 'put',
data
})
}

export const publishPcdnPolicy = (data) => {
return service({
url: '/pcdn/policy/publish',
method: 'post',
data
})
}

export const rollbackPcdnPolicy = (data) => {
return service({
url: '/pcdn/policy/rollback',
method: 'post',
data
})
}

export const getPcdnDispatchTaskList = (params) => {
return service({
url: '/pcdn/dispatch/list',
method: 'get',
params
})
}

export const retryPcdnDispatchTask = (data) => {
return service({
url: '/pcdn/dispatch/retry',
method: 'post',
data
})
}

export const getPcdnDashboardMetrics = () => {
return service({
url: '/pcdn/dashboard/metrics',
method: 'get'
})
}
4 changes: 4 additions & 0 deletions web/src/pathInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
"/src/view/layout/setting/modules/layout/index.vue": "LayoutSettings",
"/src/view/layout/tabs/index.vue": "HistoryComponent",
"/src/view/login/index.vue": "Login",
"/src/view/pcdn/dashboard.vue": "PcdnDashboard",
"/src/view/pcdn/dispatchTasks.vue": "PcdnDispatchTasks",
"/src/view/pcdn/nodeManagement.vue": "PcdnNodeManagement",
"/src/view/pcdn/policyManagement.vue": "PcdnPolicyManagement",
"/src/view/person/person.vue": "Person",
"/src/view/product/productSpec/productSpec.vue": "ProductSpec",
"/src/view/product/productSpec/productSpecForm.vue": "ProductSpecForm",
Expand Down
29 changes: 29 additions & 0 deletions web/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,35 @@ const routes = [
},
component: () => import('@/view/example/upload/scanUpload.vue')
},

{
path: '/pcdn',
name: 'Pcdn',
component: () => import('@/view/layout/index.vue'),
redirect: '/pcdn/dashboard',
children: [
{
path: 'dashboard',
name: 'pcdnDashboard',
component: () => import('@/view/pcdn/dashboard.vue')
},
{
path: 'node-management',
name: 'pcdnNodeManagement',
component: () => import('@/view/pcdn/nodeManagement.vue')
},
{
path: 'policy-management',
name: 'pcdnPolicyManagement',
component: () => import('@/view/pcdn/policyManagement.vue')
},
{
path: 'dispatch-tasks',
name: 'pcdnDispatchTasks',
component: () => import('@/view/pcdn/dispatchTasks.vue')
}
]
},
{
path: '/:catchAll(.*)',
meta: {
Expand Down
3 changes: 2 additions & 1 deletion web/src/view/dashboard/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ const quickNavs = ref([
{ name: '镜像库', icon: '📦', desc: '镜像管理', path: 'imageRegistry' },
{ name: '产品规格', icon: '💎', desc: '规格配置', path: 'productSpec' },
{ name: '端口转发', icon: '🔁', desc: '转发规则', path: 'portForward' },
{ name: 'K8s集群', icon: '☸️', desc: '集群管理', path: 'k8sCluster' }
{ name: 'K8s集群', icon: '☸️', desc: '集群管理', path: 'k8sCluster' },
{ name: 'PCDN管理', icon: '🌐', desc: '节点/策略/调度', path: 'pcdnDashboard' }
])

// 核心功能
Expand Down
128 changes: 128 additions & 0 deletions web/src/view/pcdn/dashboard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<template>
<div class="pcdn-page">
<el-row :gutter="16" class="mb-4">
<el-col :xs="24" :md="8">
<el-card shadow="hover">
<div class="metric-title">实时带宽(Gbps)</div>
<div class="metric-value">{{ summary.realtimeBandwidth }}</div>
</el-card>
</el-col>
<el-col :xs="24" :md="8">
<el-card shadow="hover">
<div class="metric-title">命中率</div>
<div class="metric-value">{{ summary.hitRate }}%</div>
</el-card>
</el-col>
<el-col :xs="24" :md="8">
<el-card shadow="hover">
<div class="metric-title">调度成功率</div>
<div class="metric-value">{{ summary.dispatchSuccessRate }}%</div>
</el-card>
</el-col>
</el-row>

<el-row :gutter="16">
<el-col :xs="24" :md="12">
<el-card shadow="never">
<template #header>实时带宽趋势</template>
<charts :options="bandwidthChartOption" height="280px" />
</el-card>
</el-col>
<el-col :xs="24" :md="12">
<el-card shadow="never">
<template #header>命中率与调度成功率</template>
<charts :options="rateChartOption" height="280px" />
</el-card>
</el-col>
</el-row>
</div>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import charts from '@/components/charts/index.vue'
import useChartOption from '@/hooks/charts'
import { getPcdnDashboardMetrics } from '@/api/pcdn'

defineOptions({ name: 'PcdnDashboard' })

const summary = ref({
realtimeBandwidth: 18.7,
hitRate: 92.8,
dispatchSuccessRate: 97.6
})

const xAxisData = ref(['10:00', '10:05', '10:10', '10:15', '10:20', '10:25', '10:30'])
const bandwidthData = ref([12.1, 13.4, 14.8, 15.6, 17.2, 18.4, 18.7])
const hitRateData = ref([89.2, 90.1, 91.5, 92.4, 92.8, 92.6, 92.8])
const dispatchRateData = ref([95.4, 95.9, 96.3, 96.8, 97.1, 97.5, 97.6])

const { chartOption: bandwidthChartOption } = useChartOption((isDark) => {
const color = isDark ? '#ddd' : '#303133'
return {
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: xAxisData.value, axisLabel: { color } },
yAxis: { type: 'value', axisLabel: { color } },
series: [{ data: bandwidthData.value, type: 'line', smooth: true, areaStyle: {} }]
}
})

const { chartOption: rateChartOption } = useChartOption((isDark) => {
const color = isDark ? '#ddd' : '#303133'
return {
tooltip: { trigger: 'axis' },
legend: { data: ['命中率', '调度成功率'], textStyle: { color } },
xAxis: { type: 'category', data: xAxisData.value, axisLabel: { color } },
yAxis: { type: 'value', axisLabel: { color } },
series: [
{ name: '命中率', data: hitRateData.value, type: 'line', smooth: true },
{ name: '调度成功率', data: dispatchRateData.value, type: 'line', smooth: true }
]
}
})

const updateMetrics = (data) => {
summary.value = {
realtimeBandwidth: data?.realtimeBandwidth ?? summary.value.realtimeBandwidth,
hitRate: data?.hitRate ?? summary.value.hitRate,
dispatchSuccessRate: data?.dispatchSuccessRate ?? summary.value.dispatchSuccessRate
}
xAxisData.value = data?.timeline || xAxisData.value
bandwidthData.value = data?.bandwidthTrend || bandwidthData.value
hitRateData.value = data?.hitRateTrend || hitRateData.value
dispatchRateData.value = data?.dispatchSuccessTrend || dispatchRateData.value
}

const loadMetrics = async () => {
try {
const res = await getPcdnDashboardMetrics()
updateMetrics(res?.data)
} catch (e) {
updateMetrics({})
}
}

onMounted(loadMetrics)
</script>

<style scoped lang="scss">
.pcdn-page {
padding: 20px;
}

.mb-4 {
margin-bottom: 16px;
}

.metric-title {
font-size: 14px;
color: #606266;
}

.metric-value {
margin-top: 10px;
font-size: 32px;
font-weight: 700;
color: #303133;
}
</style>
72 changes: 72 additions & 0 deletions web/src/view/pcdn/dispatchTasks.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<div class="pcdn-page">
<el-card shadow="never">
<template #header>调度任务流水</template>
<el-table :data="tableData" v-loading="loading" border>
<el-table-column prop="taskId" label="任务ID" min-width="180" />
<el-table-column prop="startTime" label="开始时间" min-width="180" />
<el-table-column prop="status" label="状态" min-width="100">
<template #default="scope">
<el-tag :type="statusTypeMap[scope.row.status] || 'info'">{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="reason" label="失败原因" min-width="220" />
<el-table-column prop="retryStatus" label="重试状态" min-width="120" />
<el-table-column label="操作" min-width="120" fixed="right">
<template #default="scope">
<el-button size="small" type="primary" :disabled="scope.row.status !== '失败'" @click="retry(scope.row)">重试</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getPcdnDispatchTaskList, retryPcdnDispatchTask } from '@/api/pcdn'

defineOptions({ name: 'PcdnDispatchTasks' })

const loading = ref(false)
const tableData = ref([])

const statusTypeMap = {
成功: 'success',
失败: 'danger',
进行中: 'warning'
}

const mockData = [
{ taskId: 'task-20260210-001', startTime: '2026-02-10 10:20:10', status: '成功', reason: '-', retryStatus: '无需重试' },
{ taskId: 'task-20260210-002', startTime: '2026-02-10 10:21:36', status: '失败', reason: '节点连接超时', retryStatus: '待重试' },
{ taskId: 'task-20260210-003', startTime: '2026-02-10 10:22:01', status: '进行中', reason: '-', retryStatus: '处理中' }
]

const loadData = async () => {
loading.value = true
try {
const res = await getPcdnDispatchTaskList()
tableData.value = res?.data?.list || mockData
} catch (e) {
tableData.value = mockData
} finally {
loading.value = false
}
}

const retry = async (row) => {
await retryPcdnDispatchTask({ taskId: row.taskId })
ElMessage.success('重试任务已提交')
loadData()
}

onMounted(loadData)
</script>

<style scoped lang="scss">
.pcdn-page {
padding: 20px;
}
</style>
Loading
Loading