Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e5aa48cf9 | ||
|
|
21624f6c55 | ||
|
|
c4a799c99c | ||
|
|
62ed5cc1ce | ||
|
|
048a6ec969 |
@@ -10,7 +10,8 @@
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "eslint \"src/**/*.{js,vue}\""
|
||||
"lint": "eslint \"src/**/*.{js,vue}\"",
|
||||
"test:navigation": "node --test tests/navigation.test.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.3.2",
|
||||
@@ -50,4 +51,3 @@
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -58,22 +58,14 @@ import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
import { buildNavigation } from './navigation'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const collapsed = ref(false)
|
||||
const navigation = computed(() => {
|
||||
const root = router.getRoutes().find((route) => route.path === '/')
|
||||
const permissions = store.getters['sense-identity/permissions'] || []
|
||||
return (root?.children || [])
|
||||
.filter((route) => route.meta?.title && !route.meta?.hidden)
|
||||
.filter((route) => !route.meta?.permission || permissions.includes(route.meta.permission))
|
||||
.map((route) => ({
|
||||
path: route.path ? `/${route.path}`.replace('//', '/') : '/',
|
||||
title: route.meta.title,
|
||||
icon: route.meta.icon,
|
||||
order: route.meta.order || 0
|
||||
}))
|
||||
.sort((left, right) => left.order - right.order)
|
||||
return buildNavigation(router.getRoutes(), permissions)
|
||||
})
|
||||
|
||||
async function logout() {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
function buildNavigation(routes, permissions = []) {
|
||||
const layout = routes
|
||||
.filter((route) => route.path === '/' && Array.isArray(route.children) && route.children.length > 0)
|
||||
.sort((left, right) => right.children.length - left.children.length)[0]
|
||||
|
||||
return (layout?.children || [])
|
||||
.filter((route) => route.meta?.title && !route.meta?.hidden)
|
||||
.filter((route) => !route.meta?.permission || permissions.includes(route.meta.permission))
|
||||
.map((route) => ({
|
||||
path: route.path ? `/${route.path}`.replace('//', '/') : '/',
|
||||
title: route.meta.title,
|
||||
icon: route.meta.icon,
|
||||
order: route.meta.order || 0
|
||||
}))
|
||||
.sort((left, right) => left.order - right.order)
|
||||
}
|
||||
|
||||
module.exports = { buildNavigation }
|
||||
@@ -0,0 +1,65 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const test = require('node:test')
|
||||
|
||||
const { buildNavigation } = require('../src/layout/navigation.js')
|
||||
|
||||
test('selects the layout record when normalized routes contain duplicate root paths', () => {
|
||||
const routes = [
|
||||
{ path: '/', name: 'sense-dashboard', children: [], meta: { title: '工作台' } },
|
||||
{ path: '/devices', name: 'sense-devices', children: [], meta: { title: '设备管理' } },
|
||||
{
|
||||
path: '/',
|
||||
children: [
|
||||
{ path: 'devices', meta: { title: '设备管理', permission: 'device.read', order: 10 } },
|
||||
{ path: '', meta: { title: '工作台', order: 0 } },
|
||||
{ path: 'users', meta: { title: '用户与权限', permission: 'identity.users.read', order: 90 } }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
assert.deepEqual(buildNavigation(routes, ['device.read']), [
|
||||
{ path: '/', title: '工作台', icon: undefined, order: 0 },
|
||||
{ path: '/devices', title: '设备管理', icon: undefined, order: 10 }
|
||||
])
|
||||
})
|
||||
|
||||
test('returns no unauthorized or hidden entries', () => {
|
||||
const routes = [{
|
||||
path: '/',
|
||||
children: [
|
||||
{ path: '', meta: { title: '工作台' } },
|
||||
{ path: 'users', meta: { title: '用户与权限', permission: 'identity.users.read' } },
|
||||
{ path: 'internal', meta: { title: '内部页', hidden: true } }
|
||||
]
|
||||
}]
|
||||
|
||||
assert.deepEqual(buildNavigation(routes), [
|
||||
{ path: '/', title: '工作台', icon: undefined, order: 0 }
|
||||
])
|
||||
})
|
||||
|
||||
test('administrator receives every Sense MVP navigation entry', () => {
|
||||
const entries = [
|
||||
['', '工作台', null, 0],
|
||||
['devices', '设备管理', 'device.read', 10],
|
||||
['admission', '视频接入', 'device.write', 20],
|
||||
['media', '视频服务', 'media.read', 30],
|
||||
['liveview', '实时监看', 'media.read', 40],
|
||||
['areas', '区域与警戒线', 'area.read', 50],
|
||||
['users', '用户与权限', 'identity.users.read', 90],
|
||||
['audit', '操作记录', 'identity.audit.read', 91]
|
||||
]
|
||||
const routes = [{ path: '/', children: entries.map(([path, title, permission, order]) => ({
|
||||
path,
|
||||
meta: { title, permission, order }
|
||||
})) }]
|
||||
const permissions = [
|
||||
'device.read', 'device.write', 'media.read', 'area.read',
|
||||
'identity.users.read', 'identity.audit.read'
|
||||
]
|
||||
|
||||
assert.deepEqual(buildNavigation(routes, permissions).map((item) => item.title), [
|
||||
'工作台', '设备管理', '视频接入', '视频服务', '实时监看',
|
||||
'区域与警戒线', '用户与权限', '操作记录'
|
||||
])
|
||||
})
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Troubleshooting
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Troubleshooting
|
||||
wiki_revision: a076590915040eae0d165be6254c6302f308fc5a
|
||||
synchronized_at: 2026-08-12T10:22:09Z
|
||||
wiki_revision: b8e778ae4255ba3fc36a1cd6bcb735e29bd19eee
|
||||
synchronized_at: 2026-08-13T01:26:22Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 故障排查
|
||||
@@ -47,6 +47,7 @@ synchronized_at: 2026-08-12T10:22:09Z
|
||||
|---|---|
|
||||
| 启动提示缺少 `SENSE_DATABASE_URL` | 正式模式必须提供独立 PostgreSQL URL;只有测试/smoke 可显式使用 `memory`。 |
|
||||
| 登录统一提示用户名或密码错误 | 为避免枚举账户,失败响应不区分用户不存在、密码错误或账户停用;由管理员查看审计。 |
|
||||
| 管理员登录后侧栏没有模块 | 先确认 /api/v1/identity/me 返回角色与权限;若权限正常,检查前端是否从具有 children 的应用布局路由派生菜单,不得依赖重复 / 路由记录顺序。 |
|
||||
| `adapter_not_ready` | 当前设备类型尚无适配器,不代表网络故障;首期完整支持 video。 |
|
||||
| `discovery_unavailable` | 未设置获准的 `SENSE_ONVIF_DISCOVERY_IP`,或该 IP 不属于本机网卡。可改用手工 ONVIF 地址。 |
|
||||
| `authentication_failed` | 在设备管理中重新写入凭据后再次执行接入检查;不要把凭据写进地址。 |
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Task-44-Sense管理员侧栏模块为空修复
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-44-Sense%E7%AE%A1%E7%90%86%E5%91%98%E4%BE%A7%E6%A0%8F%E6%A8%A1%E5%9D%97%E4%B8%BA%E7%A9%BA%E4%BF%AE%E5%A4%8D.-
|
||||
wiki_revision: 9647fffe7d8b0ede13c53746e8966fffb3555820
|
||||
synchronized_at: 2026-08-13T01:31:35Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 44 Sense管理员侧栏模块为空修复
|
||||
|
||||
- 类型:缺陷
|
||||
- 所属 Epic:#7
|
||||
- 所属 MVP / 版本:#8
|
||||
- 状态:待验收
|
||||
- 日期:2026-08-13
|
||||
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/44
|
||||
- Wiki 页面:Task-44-Sense管理员侧栏模块为空修复
|
||||
- Wiki revision:见本地镜像头
|
||||
|
||||
## 背景与目标
|
||||
|
||||
最新 Windows 包中管理员可登录,但侧栏不显示任何模块。目标是在不放宽 RBAC 的前提下恢复 Sense MVP 导航,并防止 Vue Router 规范化记录顺序再次导致空菜单。
|
||||
|
||||
## 根因与最终方案
|
||||
|
||||
后端管理员会话正确返回 9 项权限。Vue Router `getRoutes()` 会同时返回父布局 `/` 和路径也为 `/` 的工作台子记录;原代码用首个 `path === '/'` 记录派生菜单,误选无 children 的工作台记录。
|
||||
|
||||
修复后只选择路径为 `/` 且具有 children 的布局记录,并在多个候选中优先 children 最完整者。权限过滤、隐藏入口过滤和排序逻辑保持不变,并提取为独立可测试函数。
|
||||
|
||||
## 修改文件
|
||||
|
||||
- `Sense/ui/src/layout/AppLayout.vue`:调用稳定的菜单派生函数。
|
||||
- `Sense/ui/src/layout/navigation.js`:定位布局、按权限过滤并排序。
|
||||
- `Sense/ui/tests/navigation.test.cjs`:重复根路径、权限/隐藏过滤和管理员八项导航测试。
|
||||
- `Sense/ui/package.json`:增加 `test:navigation`。
|
||||
- `docs/06-troubleshooting.md`:Wiki-first 排错说明。
|
||||
- `wiki-docs.json`、`docs/task/44-Sense管理员侧栏模块为空修复.md`:任务归档。
|
||||
|
||||
## 验收结果
|
||||
|
||||
| 验收标准 | 结果 |
|
||||
|---|---|
|
||||
| 管理员显示八项 Sense MVP 导航 | 自动回归通过 |
|
||||
| 不依赖重复 `/` 记录顺序 | 通过 |
|
||||
| 权限不足和隐藏入口仍被过滤 | 通过 |
|
||||
| 前端 test/lint/build | 通过;lint 0 error,存在既有 warning |
|
||||
| Windows ZIP 重建并以管理员登录 | 通过;角色 administrator,9 项权限,新前端资源已加载 |
|
||||
|
||||
## 测试
|
||||
|
||||
- `corepack pnpm@9.15.1 test:navigation`:3 项通过。
|
||||
- Vue Router 实际规范化记录探针:正确派生工作台和设备管理。
|
||||
- `corepack pnpm@9.15.1 lint`:0 error,既有 warning。
|
||||
- Windows 打包:通过;前端仅有既有 vendor bundle 体积警告。
|
||||
- 包内运行:登录角色 administrator、权限数 9,首页引用新脚本 `js/index.25411ed5.js`。
|
||||
- Harness:31 项、strict 与 Wiki 镜像检查通过。
|
||||
- ZIP:7,280,825 bytes;SHA-256 `0703B397D20EAAE54BCB6B5A83170EFF6816246A8C163276495CD93EEEB72B4F`。
|
||||
- **未验证部分**:未通过自动化真实浏览器截图断言侧栏 DOM;已要求用户 Ctrl+F5 后人工查看。
|
||||
|
||||
## 遗留问题
|
||||
|
||||
- 浏览器可能缓存旧前端资源,更新包后需要强制刷新。
|
||||
|
||||
## 相关提交
|
||||
|
||||
- `048a6ec` fix: restore Sense permission navigation (#44)
|
||||
- `62ed5cc` docs: add Sense empty navigation troubleshooting (#44)
|
||||
- `c4a799c` test: cover Sense administrator navigation (#44)
|
||||
- `21624f6` docs: create task 44 acceptance archive (#44)
|
||||
@@ -123,6 +123,10 @@
|
||||
{
|
||||
"page": "Task-42-Sense密码策略仅限制最短6位",
|
||||
"path": "docs/task/42-Sense密码策略仅限制最短6位.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-44-Sense管理员侧栏模块为空修复",
|
||||
"path": "docs/task/44-Sense管理员侧栏模块为空修复.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user