feat(auth): 添加用户权限获取接口并完善JWT令牌角色信息

- 在JWT令牌中添加用户角色信息
- 新增get_my_permissions接口用于获取当前用户权限码列表
- 重构认证回调逻辑,增加错误日志记录
- 更新用户信息获取接口使用Authorization头验证
```
This commit is contained in:
2026-04-06 00:40:08 +08:00
parent dfa8fa62a8
commit f1f8518985
71 changed files with 9402 additions and 191 deletions
+275 -17
View File
@@ -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>