```
feat(auth): 添加用户权限获取接口并完善JWT令牌角色信息 - 在JWT令牌中添加用户角色信息 - 新增get_my_permissions接口用于获取当前用户权限码列表 - 重构认证回调逻辑,增加错误日志记录 - 更新用户信息获取接口使用Authorization头验证 ```
This commit is contained in:
@@ -7,13 +7,14 @@
|
||||
<span class="page-subtitle">光线路终端设备配置与监控</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" size="small" @click="openAddDialog">
|
||||
<el-button v-if="!isMobile && can('olt.manage')" type="primary" size="small" @click="openAddDialog">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="margin-right: 5px">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
添加 OLT
|
||||
</el-button>
|
||||
<el-upload
|
||||
v-if="!isMobile && can('olt.manage')"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="handleImportFile"
|
||||
@@ -22,19 +23,19 @@
|
||||
>
|
||||
<el-button type="success" size="small" :loading="importing">批量导入</el-button>
|
||||
</el-upload>
|
||||
<el-button size="small" @click="downloadTemplate">下载模板</el-button>
|
||||
<el-button type="danger" plain size="small" @click="openDuplicateMacs">
|
||||
<el-button v-if="!isMobile && can('olt.manage')" size="small" @click="downloadTemplate">下载模板</el-button>
|
||||
<el-button v-if="!isMobile && can('olt.manage')" type="danger" plain size="small" @click="openDuplicateMacs">
|
||||
重复 MAC
|
||||
<el-badge v-if="duplicateCount > 0" :value="duplicateCount" style="margin-left: 4px" />
|
||||
</el-button>
|
||||
<el-button type="warning" plain size="small" @click="openNewDevices">
|
||||
<el-button v-if="can('olt.manage')" type="warning" plain size="small" @click="openNewDevices">
|
||||
新增设备
|
||||
<el-badge v-if="newDeviceCount > 0" :value="newDeviceCount" style="margin-left: 4px" />
|
||||
</el-button>
|
||||
<el-button type="danger" plain size="small" @click="runLoopbackDetection" :loading="loopDetecting">
|
||||
<el-button v-if="can('olt.loopback')" type="danger" plain size="small" @click="runLoopbackDetection" :loading="loopDetecting">
|
||||
环路检测
|
||||
</el-button>
|
||||
<el-button type="primary" plain size="small" @click="runQuickScan" :loading="quickScanning">
|
||||
<el-button v-if="can('olt.discover')" type="primary" plain size="small" @click="runQuickScan" :loading="quickScanning">
|
||||
快速扫描
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -42,7 +43,14 @@
|
||||
|
||||
<!-- OLT 列表 -->
|
||||
<div class="data-panel">
|
||||
<el-table :data="devices" size="small" style="width: 100%">
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar" v-if="!isMobile">
|
||||
<el-select v-model="filterRegion" placeholder="全部区域" clearable size="small" style="width: 160px" @change="fetchDevices">
|
||||
<el-option v-for="r in regions" :key="r" :label="r" :value="r" />
|
||||
</el-select>
|
||||
</div>
|
||||
<!-- 桌面端表格 -->
|
||||
<el-table v-if="!isMobile" :data="filteredDevices" size="small" style="width: 100%">
|
||||
<el-table-column type="index" label="#" width="52" align="center">
|
||||
<template #default="{ $index }">
|
||||
<span class="row-index">{{ $index + 1 }}</span>
|
||||
@@ -53,6 +61,11 @@
|
||||
<span class="ip-link" @click="openDetail(row)">{{ row.ip_address }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="region" label="区域" width="90">
|
||||
<template #default="{ row }">
|
||||
<span class="region-tag">{{ row.region || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户名" width="110">
|
||||
<template #default="{ row }">
|
||||
<span class="mono-val">{{ row.username }}</span>
|
||||
@@ -85,6 +98,50 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 移动端 OLT 卡片 -->
|
||||
<div v-if="isMobile" class="olt-cards">
|
||||
<div v-for="device in devices" :key="device.id" class="olt-card">
|
||||
<div class="olt-card-head">
|
||||
<div class="olt-card-head-left">
|
||||
<span class="olt-ip">{{ device.ip_address }}</span>
|
||||
<span class="olt-location">{{ device.location || '—' }}</span>
|
||||
</div>
|
||||
<div class="olt-card-head-right">
|
||||
<span
|
||||
v-if="loopStatusMap[device.id]"
|
||||
class="loop-badge"
|
||||
@click="showLoopDetail(device.id)"
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
环路 {{ loopStatusMap[device.id].length }}
|
||||
</span>
|
||||
<span v-else class="olt-status-ok">
|
||||
<span class="status-dot-sm online-dot"></span>正常
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="olt-card-actions">
|
||||
<button class="olt-action-btn" @click="openDetail(device)">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
快速扫描
|
||||
</button>
|
||||
<button v-if="can('olt.port_manage')" class="olt-action-btn" @click="openPortManagerFromCard(device)">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
||||
<circle cx="7" cy="9" r="1.2" fill="currentColor"/>
|
||||
</svg>
|
||||
端口管理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="devices.length === 0" class="olt-cards-empty">暂无 OLT 设备</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速扫描结果 -->
|
||||
@@ -206,6 +263,11 @@
|
||||
<el-form-item label="IP 地址"><el-input v-model="form.ip_address" :disabled="isEdit" /></el-form-item>
|
||||
<el-form-item label="用户名"><el-input v-model="form.username" /></el-form-item>
|
||||
<el-form-item label="密码"><el-input v-model="form.password" type="password" placeholder="不修改请留空" /></el-form-item>
|
||||
<el-form-item label="区域">
|
||||
<el-select v-model="form.region" style="width: 100%">
|
||||
<el-option v-for="r in regions" :key="r" :label="r" :value="r" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="安装位置"><el-input v-model="form.location" /></el-form-item>
|
||||
<el-form-item label="槽位命令"><el-input v-model="form.slot_command" /></el-form-item>
|
||||
<el-form-item label="描述"><el-input v-model="form.description" type="textarea" :rows="2" /></el-form-item>
|
||||
@@ -225,6 +287,7 @@
|
||||
<el-descriptions-item label="用户名">
|
||||
<span class="mono-val">{{ selectedDevice.username }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="区域">{{ selectedDevice.region || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="安装位置">{{ selectedDevice.location || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="槽位命令" :span="2">
|
||||
<span class="mono-val cmd">{{ selectedDevice.slot_command || '—' }}</span>
|
||||
@@ -236,7 +299,7 @@
|
||||
<div class="detail-actions">
|
||||
<!-- 第一行:设备操作 -->
|
||||
<div class="action-row">
|
||||
<el-button type="info" :loading="portsLoading" @click="openPortManager">
|
||||
<el-button v-if="can('olt.port_manage')" type="info" :loading="portsLoading" @click="openPortManager">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px;vertical-align:-2px">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
||||
<circle cx="7" cy="9" r="1.2" fill="currentColor"/><circle cx="11" cy="9" r="1.2" fill="currentColor"/>
|
||||
@@ -250,7 +313,7 @@
|
||||
</svg>
|
||||
扫描发现
|
||||
</el-button>
|
||||
<el-button type="warning" :loading="discovering" @click="discoverOlt">
|
||||
<el-button v-if="can('olt.discover')" type="warning" :loading="discovering" @click="discoverOlt">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px;vertical-align:-2px">
|
||||
<path d="M12 5v14M5 12l7 7 7-7"/>
|
||||
</svg>
|
||||
@@ -260,8 +323,8 @@
|
||||
<!-- 第二行:管理操作 + 关闭 -->
|
||||
<div class="action-row action-row--secondary">
|
||||
<div class="action-left">
|
||||
<el-button type="primary" @click="openEdit">编辑</el-button>
|
||||
<el-button type="danger" @click="confirmDelete">删除</el-button>
|
||||
<el-button v-if="can('olt.manage')" type="primary" @click="openEdit">编辑</el-button>
|
||||
<el-button v-if="can('olt.manage')" type="danger" @click="confirmDelete">删除</el-button>
|
||||
</div>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</div>
|
||||
@@ -298,7 +361,8 @@
|
||||
<div class="port-status-text">{{ portStatusText(port) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="port-hint">点击端口可切换开启/关闭状态</div>
|
||||
<div class="port-hint" v-if="can('olt.port_manage')">点击端口可切换开启/关闭状态</div>
|
||||
<div class="port-hint" v-else>仅查看端口状态(无管理权限)</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button :loading="portsLoading" @click="loadPorts">刷新</el-button>
|
||||
@@ -342,7 +406,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button type="warning" :loading="discovering" @click="discoverOlt">确认入库新设备</el-button>
|
||||
<el-button v-if="can('olt.discover')" type="warning" :loading="discovering" @click="discoverOlt">确认入库新设备</el-button>
|
||||
<el-button @click="scanVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -440,11 +504,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from '../utils/message'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
import { useMobile } from '../composables/useMobile'
|
||||
import { usePermission } from '../composables/usePermission'
|
||||
|
||||
const { isMobile } = useMobile()
|
||||
const { can } = usePermission()
|
||||
|
||||
const devices = ref([])
|
||||
const filterRegion = ref('')
|
||||
const regions = ref([])
|
||||
const filteredDevices = computed(() =>
|
||||
filterRegion.value ? devices.value.filter(d => d.region === filterRegion.value) : devices.value
|
||||
)
|
||||
const dialogVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const scanVisible = ref(false)
|
||||
@@ -471,7 +546,7 @@ const loopDetailInterfaces = ref([])
|
||||
const quickScanning = ref(false)
|
||||
const quickScanVisible = ref(false)
|
||||
const quickScanResult = ref({ total_online: 0, total_offline: 0, total_new: 0, results: [], errors: [] })
|
||||
const form = ref({ ip_address: '', username: '', password: '', slot_command: 'display onu slot', location: '', description: '' })
|
||||
const form = ref({ ip_address: '', username: '', password: '', slot_command: 'display onu slot', region: '城区', location: '', description: '' })
|
||||
|
||||
const portManagerVisible = ref(false)
|
||||
const portsLoading = ref(false)
|
||||
@@ -508,6 +583,7 @@ const openPortManager = async () => {
|
||||
}
|
||||
|
||||
const togglePort = async (port) => {
|
||||
if (!can('olt.port_manage')) return
|
||||
const isUp = port.status === 'up'
|
||||
const action = isUp ? 'shutdown' : 'undo shutdown'
|
||||
const actionText = isUp ? '关闭' : '开启'
|
||||
@@ -535,6 +611,13 @@ const togglePort = async (port) => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRegions = async () => {
|
||||
try {
|
||||
const { data } = await request.get('/olt/regions')
|
||||
regions.value = data
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const fetchDevices = async () => {
|
||||
const { data } = await request.get('/olt/devices')
|
||||
devices.value = data
|
||||
@@ -549,7 +632,7 @@ const fetchDuplicateCount = async () => {
|
||||
|
||||
const openAddDialog = () => {
|
||||
isEdit.value = false
|
||||
form.value = { ip_address: '', username: '', password: '', slot_command: 'display onu slot', location: '', description: '' }
|
||||
form.value = { ip_address: '', username: '', password: '', slot_command: 'display onu slot', region: regions.value[0] || '', location: '', description: '' }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -566,6 +649,7 @@ const openEdit = () => {
|
||||
username: selectedDevice.value.username,
|
||||
password: '',
|
||||
slot_command: selectedDevice.value.slot_command || 'display onu slot',
|
||||
region: selectedDevice.value.region || '城区',
|
||||
location: selectedDevice.value.location || '',
|
||||
description: selectedDevice.value.description || ''
|
||||
}
|
||||
@@ -794,7 +878,19 @@ const downloadTemplate = () => {
|
||||
window.open('/api/olt/template', '_blank')
|
||||
}
|
||||
|
||||
// 移动端卡片操作
|
||||
const openEditFromCard = (device) => {
|
||||
selectedDevice.value = { ...device }
|
||||
openEdit()
|
||||
}
|
||||
|
||||
const openPortManagerFromCard = async (device) => {
|
||||
selectedDevice.value = { ...device }
|
||||
await openPortManager()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRegions()
|
||||
fetchDevices()
|
||||
fetchDuplicateCount()
|
||||
fetchNewDeviceCount()
|
||||
@@ -851,6 +947,25 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.region-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
background: var(--accent-dim);
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 表格内元素 */
|
||||
.row-index {
|
||||
font-family: var(--font-mono);
|
||||
@@ -1148,4 +1263,147 @@ onMounted(() => {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ── 移动端 OLT 卡片 ── */
|
||||
.olt-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.olt-card {
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.olt-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.olt-card-head-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.olt-ip {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.olt-location {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.olt-card-head-right {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.olt-status-ok {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
color: var(--success);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 5px var(--success);
|
||||
}
|
||||
|
||||
.olt-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.olt-action-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
padding: 9px 6px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.olt-action-btn:active {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.olt-action-btn--edit {
|
||||
flex: 0 0 auto;
|
||||
padding: 9px 14px;
|
||||
}
|
||||
|
||||
.olt-cards-empty {
|
||||
text-align: center;
|
||||
padding: 48px 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 移动端页头 */
|
||||
@media (max-width: 767px) {
|
||||
.olt-manage {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-actions .el-button,
|
||||
.header-actions > * {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* 移动端按钮区只显示2列(移动端只剩新增设备、环路检测、快速扫描) */
|
||||
.header-actions {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
/* 端口管理对话框:限制高度支持滚动 */
|
||||
.switch-panel {
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user