diff --git a/src/api/nvrVideo.js b/src/api/nvrVideo.js index 6c1d081..1072b13 100644 --- a/src/api/nvrVideo.js +++ b/src/api/nvrVideo.js @@ -28,13 +28,13 @@ export function getNvrConfig() { return unwrap(request({ url: '/patrol/video/config', method: 'get' })) } -export function startLive(channel) { +export function startLive(channel, streamType = 1) { return unwrap(request({ url: '/patrol/video/realtime/start', method: 'post', data: { cameraCode: channel.cameraCode || channel.channelCode, - streamType: 1 + streamType } })) } diff --git a/src/assets/styles/sidebar.scss b/src/assets/styles/sidebar.scss index ca5a4aa..9642955 100644 --- a/src/assets/styles/sidebar.scss +++ b/src/assets/styles/sidebar.scss @@ -238,6 +238,17 @@ } } } + + // 收起时仅展示图标:隐藏菜单文字(保留图标),图标保持居中。 + // 菜单图标与文字被 .menu-item-inner 包裹,需只隐藏其中的文字 span。 + .el-menu > div > a > .submenu-title-noDropdown .menu-item-inner, + .el-menu > div > .el-submenu > .el-submenu__title .menu-item-inner { + overflow: visible; + + > span:not(.menu-icon-img):not(.menu-icon-placeholder) { + display: none !important; + } + } } .el-menu--collapse .el-menu .el-submenu { diff --git a/src/router/index.js b/src/router/index.js index 53c4f8e..6728349 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -170,12 +170,13 @@ export const constantRoutes = [ { path: '/task-summary', component: Layout, - redirect: '/task-summary/inspectionTask', + // 与默认首页 /index 渲染同一组件,重定向过去复用固定的巡视任务页签,避免出现两个同名页签 + redirect: '/index', meta: { title: '巡视任务' }, children: [ { path: 'inspectionTask', - component: () => import('@/views/cards/taskSummary/inspectionTask'), + redirect: '/index', name: 'InspectionTaskSummary', meta: { title: '巡视任务', activeMenu: '/task-summary/inspectionTask' } } diff --git a/src/views/cards/assets/styles/index.scss b/src/views/cards/assets/styles/index.scss index d9febcb..c2d5542 100644 --- a/src/views/cards/assets/styles/index.scss +++ b/src/views/cards/assets/styles/index.scss @@ -555,7 +555,8 @@ } .site-point-add-dialog { - height: 92vh; + // 高度随内容自适应,避免表单内容少时弹框底部出现大片空白 + height: auto; max-height: 92vh; margin-top: 4vh; display: flex; @@ -563,8 +564,15 @@ border: 0; box-shadow: none; + // 去掉表单自带的深色背景,与弹窗底色保持一致 + // (ruoyi.scss 对 .card-page 的背景使用了 !important,这里需同样处理) + .el-form.card-page { + background-color: transparent !important; + border: 0 !important; + } + .el-dialog__body { - flex: 1 1 auto; + flex: 0 0 auto; min-height: 0; height: auto; max-height: calc(92vh - 135px); diff --git a/src/views/cards/defectRecord/index.vue b/src/views/cards/defectRecord/index.vue index 026a033..613c6e2 100644 --- a/src/views/cards/defectRecord/index.vue +++ b/src/views/cards/defectRecord/index.vue @@ -4,6 +4,62 @@ :showTitle="false" >
+ +
+
+ 总缺陷数 + {{ total }} +
+ + + + + + 搜索 + 重置 +
{ + const desc = (item.desc || "").trim(); + if (desc) set.add(desc); + }); + return Array.from(set).sort((a, b) => a.localeCompare(b, "zh")); + }, + }, created() { - this.getList(); + this.loadData(); window.addEventListener("resize", this.setTableHeight); }, mounted() { @@ -139,7 +209,91 @@ export default { handleQuery() { this.queryParams.pageNum = 1; - this.getList(); + this.applyFilters(); + }, + + // 重置查询条件(筛选在前端完成,无需重新请求后端) + resetQuery() { + this.defectTypeList = []; + this.queryParams.pointName = ""; + this.dateRange = []; + this.handleQuery(); + }, + + // 一次性拉取全量数据,之后的筛选、分页都在前端完成 + loadData() { + this.loading = true; + // 重置图片计数器 + this.imageLoadCount = 0; + this.expectedImageCount = 0; + + defectRecordList({ pageNum: 1, pageSize: 9999 }) + .then((res) => { + this.allDataList = res.rows || []; + this.applyFilters(); + }) + .catch((error) => { + console.error("获取缺陷记录失败:", error); + this.allDataList = []; + this.applyFilters(); + }) + .finally(() => { + this.loading = false; + }); + }, + + // 前端筛选:缺陷结果、点位名称、采集时间,随后本地分页 + applyFilters() { + const keyword = (this.queryParams.pointName || "").trim().toLowerCase(); + const types = this.defectTypeList || []; + const range = + this.dateRange && this.dateRange.length === 2 ? this.dateRange : null; + + this.filteredList = this.allDataList.filter((row) => { + if (types.length && !types.includes((row.desc || "").trim())) { + return false; + } + if ( + keyword && + (row.pointName || "").toLowerCase().indexOf(keyword) === -1 + ) { + return false; + } + if (range) { + const day = (row.patrolTime || "").slice(0, 10); + if (!day || day < range[0] || day > range[1]) { + return false; + } + } + return true; + }); + + this.total = this.filteredList.length; + // 当前页超出范围时回到第一页 + const maxPage = Math.max( + 1, + Math.ceil(this.total / (this.queryParams.pageSize || 20)) + ); + if (this.queryParams.pageNum > maxPage) { + this.queryParams.pageNum = 1; + } + const start = (this.queryParams.pageNum - 1) * this.queryParams.pageSize; + this.dataList = this.filteredList.slice( + start, + start + this.queryParams.pageSize + ); + + // 计算需要加载的图片数量 + this.expectedImageCount = this.dataList.filter( + (item) => item.imgAnalyse + ).length; + + // 如果没有图片需要加载,直接刷新表格 + if (this.expectedImageCount === 0) { + this.forceRefreshTable(); + } else { + this.$nextTick(this.setTableHeight); + } }, linkUrl() { @@ -201,42 +355,13 @@ export default { }); }, + // 翻页:数据已在前端,直接本地切片(首次进入时才请求后端) getList() { - this.loading = true; - // 重置图片计数器 - this.imageLoadCount = 0; - this.expectedImageCount = 0; - - const params = { ...this.queryParams }; - if (this.dateRange && this.dateRange.length === 2) { - params.beginTime = this.dateRange[0]; - params.endTime = this.dateRange[1]; + if (!this.allDataList.length) { + this.loadData(); + return; } - - defectRecordList(params) - .then((res) => { - this.dataList = res.rows || []; - this.total = res.total || 0; - this.$nextTick(this.setTableHeight); - - // 计算需要加载的图片数量 - this.expectedImageCount = this.dataList.filter( - (item) => item.imgAnalyse - ).length; - - // 如果没有图片需要加载,直接刷新表格 - if (this.expectedImageCount === 0) { - this.forceRefreshTable(); - } - }) - .catch((error) => { - console.error("获取缺陷记录失败:", error); - this.dataList = []; - this.total = 0; - }) - .finally(() => { - this.loading = false; - }); + this.applyFilters(); }, onSubmit() { @@ -252,6 +377,67 @@ export default { display: flex; flex-direction: column; } +// 查询栏:总缺陷数 + 条件筛选 +.query-bar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; + padding: 12px 14px; + margin-bottom: 12px; + border: 1px solid #0b2567; + border-radius: 2px; + background: rgba(11, 37, 103, 0.35); + + .total-chip { + display: flex; + align-items: center; + gap: 10px; + padding: 5px 14px; + border: 1px solid rgba(63, 127, 216, 0.72); + border-radius: 2px; + background: rgba(11, 37, 103, 0.55); + + .total-label { + font-size: 14px; + color: #dcecff; + } + + .total-value { + font-size: 20px; + font-weight: 700; + color: #54ffff; + line-height: 1.2; + } + } + + .query-type { + width: 220px; + } + + .query-input { + width: 180px; + } + + .query-date { + width: 260px; + } + + // 重置按钮:与巡视方案页“添加”按钮同款配色 + .query-reset-btn { + background: linear-gradient(#01016b, #01016b) padding-box, + linear-gradient(135deg, #3988ff, #0351c6) border-box !important; + border: 1px solid transparent !important; + color: #43c0ff !important; + + &:hover, + &:focus { + background: linear-gradient(#01016b, #01016b) padding-box, + linear-gradient(135deg, #3988ff, #0351c6) border-box !important; + color: #43c0ff !important; + } + } +} .card-page { height: 100%; min-height: 0; diff --git a/src/views/cards/patrolPlan/summary_editTask.vue b/src/views/cards/patrolPlan/summary_editTask.vue index 71b96da..6daebf2 100644 --- a/src/views/cards/patrolPlan/summary_editTask.vue +++ b/src/views/cards/patrolPlan/summary_editTask.vue @@ -1195,6 +1195,11 @@ export default { align-items: flex-start; margin-bottom: 24px; + // 间隔类型下拉去掉禁用态默认的深色背景,与普通下拉外观一致 + ::v-deep .el-select .el-input.is-disabled .el-input__inner { + background-color: rgba(27, 42, 94, 0.94) !important; + } + .form-label { width: 130px; flex-shrink: 0; diff --git a/src/views/cards/patrolPlan/summary_newTask.vue b/src/views/cards/patrolPlan/summary_newTask.vue index 09f5194..5402c50 100644 --- a/src/views/cards/patrolPlan/summary_newTask.vue +++ b/src/views/cards/patrolPlan/summary_newTask.vue @@ -1042,6 +1042,11 @@ export default { align-items: flex-start; margin-bottom: 24px; + // 间隔类型为禁用下拉,去掉禁用态默认的深色背景,与普通下拉外观一致 + ::v-deep .el-select .el-input.is-disabled .el-input__inner { + background-color: rgba(27, 42, 94, 0.94) !important; + } + .form-label { width: 130px; flex-shrink: 0; diff --git a/src/views/cards/patrolReport/patrolReportDetail.vue b/src/views/cards/patrolReport/patrolReportDetail.vue index 8d0339b..b9e3166 100644 --- a/src/views/cards/patrolReport/patrolReportDetail.vue +++ b/src/views/cards/patrolReport/patrolReportDetail.vue @@ -19,13 +19,13 @@ :show-header="false" :span-method="headerSpan" > - + - + diff --git a/src/views/video/LivePreview.vue b/src/views/video/LivePreview.vue index a3c24cc..b6d67a4 100644 --- a/src/views/video/LivePreview.vue +++ b/src/views/video/LivePreview.vue @@ -199,6 +199,7 @@ export default { streamCleanupPromise: null, selectedCode: '', layout: 9, + fullscreenIndex: -1, layouts: [ { value: 1, label: '单画面' }, { value: 4, label: '四画面' }, { value: 9, label: '九画面' }, { value: 16, label: '十六画面' } @@ -259,6 +260,8 @@ export default { visibleSlots() { return this.slots.slice(0, this.capacity) }, activeSlot() { return this.slots[this.activeIndex] || null }, activeMuted() { return this.controlTarget ? this.controlTarget.muted !== false : true }, + // 单画面用主码流,4/9/16 宫格用辅码流,减轻多路视频卡顿 + streamType() { return this.layout === 1 ? 1 : 2 }, gridStyle() { const columns = { 1: 1, 4: 2, 9: 3, 16: 4 }[this.layout] return { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`, gridTemplateRows: `repeat(${columns}, minmax(0, 1fr))` } @@ -269,6 +272,7 @@ export default { created() { this.$root.$on('video-layout-change', this.changeLayout) this.$root.$on('video-control-panel-change', this.changeControlPanel) + document.addEventListener('fullscreenchange', this.handleFullscreenChange) this.$root.$emit('video-layout-updated', this.layout) this.$root.$emit('video-control-panel-updated', this.controlPanelExpanded) this.loadConfig() @@ -293,6 +297,7 @@ export default { this.stopTreeResize() this.lifecycleId += 1 this.pageActive = false + document.removeEventListener('fullscreenchange', this.handleFullscreenChange) this.$root.$off('video-layout-change', this.changeLayout) this.$root.$off('video-control-panel-change', this.changeControlPanel) this.closeAll() @@ -421,11 +426,11 @@ export default { this.selectChannel(channel) this.startChannel(channel) }, - async startChannel(channel, targetIndex = this.activeIndex, attemptedCameraCodes = []) { + async startChannel(channel, targetIndex = this.activeIndex, attemptedCameraCodes = [], streamType = this.streamType) { if (!this.pageActive || !channel || !channel.cameraCode) return const index = targetIndex const current = this.slots[index] - if (current && current.cameraCode === channel.cameraCode) { + if (current && current.cameraCode === channel.cameraCode && current.streamType === streamType) { this.controlTarget = current return } @@ -439,7 +444,8 @@ export default { muted: true, errorNotified: false, attemptedCameraCodes: [...attemptedCameraCodes, channel.cameraCode], - timer: null + timer: null, + streamType } this.$set(this.slots, index, slot) if (index === this.activeIndex) { @@ -449,7 +455,7 @@ export default { this.presets = [] this.presetId = null try { - slot.startPromise = startLive(channel) + slot.startPromise = startLive(channel, streamType) const result = await slot.startPromise if (this.slots[index] !== slot) return this.applyLiveResult(index, slot, result) @@ -587,6 +593,29 @@ export default { const card = this.$refs.cameraCards && this.$refs.cameraCards[index] if (card && card.requestFullscreen) card.requestFullscreen() }, + // 放大(全屏)观看时切主码流,退出全屏后恢复当前布局对应的码流 + async handleFullscreenChange() { + const fsElement = document.fullscreenElement + if (fsElement) { + const cards = this.$refs.cameraCards || [] + const index = cards.indexOf(fsElement) + if (index >= 0) { + this.fullscreenIndex = index + await this.switchSlotStream(index, 1) + } + } else if (this.fullscreenIndex >= 0) { + const index = this.fullscreenIndex + this.fullscreenIndex = -1 + await this.switchSlotStream(index, this.streamType) + } + }, + async switchSlotStream(index, streamType) { + const slot = this.slots[index] + if (!this.pageActive || !slot || slot.streamType === streamType) return + await this.closeSlot(index, false) + if (!this.pageActive) return + await this.startChannel(slot, index, [], streamType) + }, openPlayback(slot) { this.$router.push({ name: 'VideoHistoryPlayback', query: { cameraCode: slot.cameraCode }}) }, diff --git a/src/views/video/components/NvrDeviceTree.vue b/src/views/video/components/NvrDeviceTree.vue index f248924..2bb7789 100644 --- a/src/views/video/components/NvrDeviceTree.vue +++ b/src/views/video/components/NvrDeviceTree.vue @@ -155,7 +155,7 @@ export default { .tree-filters .el-input { flex: 1; min-width: 0; } .tree-filters .el-button { flex: 0 0 32px; padding: 0; border-color: #2787ed; color: #54ffff; background: #0a2b67; } .tree-filters .el-button:hover, .tree-filters .el-button:focus { border-color: #2787ed; color: #ffffff; background: #176bc2; } -.tree-content { flex: 1; min-height: 0; overflow-x: auto; overflow-y: auto; padding: 0 8px 8px; scrollbar-width: thin; scrollbar-color: #2f7fe8 rgba(6,27,79,.7); } +.tree-content { flex: 1; min-height: 0; overflow-x: auto; overflow-y: auto; padding: 0 0 8px; scrollbar-width: thin; scrollbar-color: #2f7fe8 rgba(6,27,79,.7); } .tree-content::-webkit-scrollbar { width: 8px; height: 8px; } .tree-content::-webkit-scrollbar-track { background: rgba(6,27,79,.7); } .tree-content::-webkit-scrollbar-thumb { background: #2f7fe8; border: 1px solid #0d327d; border-radius: 4px; } @@ -169,7 +169,11 @@ export default { ::v-deep .custom-tree-node, ::v-deep .node-label { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif !important; font-weight: 400 !important; } ::v-deep .el-tree-node__content { display: flex; } -::v-deep .el-tree-node__content { height: 34px; } +/* 行高与点位管理设备树保持一致,展开后更紧凑 */ +::v-deep .el-tree-node__content { height: 24px; } +/* 叶子节点没有展开小三角,隐形三角占位收窄一半,名称略向左靠, + 与前几级的间距节奏保持一致(同点位管理设备树的处理) */ +::v-deep .el-tree-node__expand-icon.is-leaf { display: inline-block; width: 12px; } ::v-deep .el-tree-node__content:hover, ::v-deep .el-tree-node:focus > .el-tree-node__content { background: transparent; } ::v-deep .el-tree-node.is-current > .el-tree-node__content { color: #54ffff; background: transparent; }