Browse Source

修改样式问题

inspection_yanyuan
douyage 4 weeks ago
parent
commit
330c39f181
12 changed files with 324 additions and 161 deletions
  1. +35
    -0
      src/assets/styles/element-ui.scss
  2. +1
    -1
      src/assets/styles/sidebar.scss
  3. +8
    -8
      src/layout/components/AppMain.vue
  4. +2
    -2
      src/layout/components/Navbar.vue
  5. +1
    -1
      src/layout/components/Sidebar/Logo.vue
  6. +171
    -40
      src/layout/components/TagsView/index.vue
  7. +4
    -1
      src/main.js
  8. +29
    -81
      src/utils/viewportScale.js
  9. +24
    -17
      src/views/cards/patrolReport/InspectionReporter.vue
  10. +8
    -3
      src/views/portal/components/PortalDeviceTree.vue
  11. +39
    -5
      src/views/portal/index.vue
  12. +2
    -2
      src/views/video/LivePreview.vue

+ 35
- 0
src/assets/styles/element-ui.scss View File

@ -73,6 +73,41 @@
} }
} }
// 下拉和提示挂在 body 上,不吃汇聚页的 zoom。
// 不用 zoom 去改它们:zoom 会把下拉列表高度算成 0,选项就变成一条空带子。
// 字号、行高按汇聚页同一比例走,浏览器再放大时会和页面一起被压回去。
html[data-portal-scale] {
.el-message {
height: calc(48px * var(--portal-scale)) !important;
min-width: calc(380px * var(--portal-scale));
padding: 0 calc(16px * var(--portal-scale)) !important;
font-size: calc(14px * var(--portal-scale));
.el-message__icon {
font-size: calc(16px * var(--portal-scale));
margin-right: calc(10px * var(--portal-scale));
}
.el-message__content {
font-size: calc(14px * var(--portal-scale));
line-height: calc(18px * var(--portal-scale)) !important;
max-height: calc(48px * var(--portal-scale));
}
}
.el-select-dropdown__item {
height: calc(34px * var(--portal-scale));
line-height: calc(34px * var(--portal-scale));
padding: 0 calc(20px * var(--portal-scale));
font-size: calc(14px * var(--portal-scale));
}
.el-select-dropdown__empty {
padding: calc(10px * var(--portal-scale)) 0;
font-size: calc(14px * var(--portal-scale));
}
}
// refine element ui upload // refine element ui upload
.upload-container { .upload-container {
.el-upload { .el-upload {


+ 1
- 1
src/assets/styles/sidebar.scss View File

@ -50,7 +50,7 @@
&.has-logo { &.has-logo {
.el-scrollbar { .el-scrollbar {
height: calc(100% - 50px);
height: calc(100% - 5.5vh);
} }
} }


+ 8
- 8
src/layout/components/AppMain.vue View File

@ -46,8 +46,8 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.app-main { .app-main {
/* 50 = navbar */
height: calc(100vh - 50px);
/* 5.5vh = navbar,和别家标题栏同一高度 */
height: calc(100vh - 5.5vh);
min-height: 0; min-height: 0;
width: 100%; width: 100%;
position: relative; position: relative;
@ -55,25 +55,25 @@ export default {
} }
.fixed-header + .app-main { .fixed-header + .app-main {
padding-top: 50px;
padding-top: 5.5vh;
} }
.app-main.hasTagsView { .app-main.hasTagsView {
/* 130 = navbar + subbar + tags view = 50 + 40 + 40 */
height: calc(100vh - 130px);
/* 5.5vh + 80px = navbar + subbar + tags view */
height: calc(100vh - 5.5vh - 80px);
min-height: 0; min-height: 0;
} }
.fixed-header + .app-main.hasTagsView { .fixed-header + .app-main.hasTagsView {
padding-top: 130px;
padding-top: calc(5.5vh + 80px);
} }
.app-main.patrolWorkspace.hasTagsView { .app-main.patrolWorkspace.hasTagsView {
height: calc(100vh - 130px);
height: calc(100vh - 5.5vh - 80px);
} }
.fixed-header + .app-main.patrolWorkspace.hasTagsView { .fixed-header + .app-main.patrolWorkspace.hasTagsView {
padding-top: 130px;
padding-top: calc(5.5vh + 80px);
} }
</style> </style>


+ 2
- 2
src/layout/components/Navbar.vue View File

@ -175,7 +175,7 @@ export default {
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.navbar { .navbar {
height: 50px;
height: 5.5vh;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
display: flex; display: flex;
@ -195,7 +195,7 @@ export default {
pointer-events: none; pointer-events: none;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center center; background-position: center center;
background-size: auto 100%;
background-size: 100% 100%;
background-color: #11194f; background-color: #11194f;
box-sizing: border-box; box-sizing: border-box;
} }


+ 1
- 1
src/layout/components/Sidebar/Logo.vue View File

@ -33,7 +33,7 @@ export default {
.sidebar-logo-container { .sidebar-logo-container {
position: relative; position: relative;
width: 100%; width: 100%;
height: 50px;
height: 5.5vh;
background: transparent; background: transparent;
overflow: hidden; overflow: hidden;
display: flex; display: flex;


+ 171
- 40
src/layout/components/TagsView/index.vue View File

@ -40,20 +40,19 @@
v-for="tag in visitedViews" v-for="tag in visitedViews"
ref="tag" ref="tag"
:key="tag.path" :key="tag.path"
:data-path="tag.path"
:class="[isActive(tag) ? 'active' : '', { 'is-dragging': dragTag && dragTag.path === tag.path }]" :class="[isActive(tag) ? 'active' : '', { 'is-dragging': dragTag && dragTag.path === tag.path }]"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span" tag="span"
class="tags-view-item" class="tags-view-item"
draggable="true"
@dragstart.native="onTagDragStart(tag, $event)"
@dragover.native.prevent="onTagDragOver(tag, $event)"
@drop.native.prevent="onTagDrop(tag)"
@dragend.native="onTagDragEnd"
@pointerdown.native="onTagPointerDown(tag, $event)"
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''" @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
@contextmenu.prevent.native="openMenu(tag,$event)" @contextmenu.prevent.native="openMenu(tag,$event)"
> >
{{ titleFn(tag.title) }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
<span class="tags-view-face">
{{ titleFn(tag.title) }}
<span v-if="!isAffix(tag)" class="el-icon-close" @pointerdown.stop @click.prevent.stop="closeSelectedTag(tag)" />
</span>
</router-link> </router-link>
</scroll-pane> </scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu"> <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
@ -80,6 +79,8 @@ export default {
left: 0, left: 0,
selectedTag: {}, selectedTag: {},
dragTag: null, dragTag: null,
dropPath: '',
pointerDrag: null,
affixTags: [], affixTags: [],
videoLayout: 9, videoLayout: 9,
videoControlPanelExpanded: false, videoControlPanelExpanded: false,
@ -134,6 +135,7 @@ export default {
this.$root.$on('video-control-panel-updated', this.updateVideoControlPanel) this.$root.$on('video-control-panel-updated', this.updateVideoControlPanel)
}, },
beforeDestroy() { beforeDestroy() {
this.detachTagPointer()
this.$root.$off('video-layout-updated', this.updateVideoLayout) this.$root.$off('video-layout-updated', this.updateVideoLayout)
this.$root.$off('video-control-panel-updated', this.updateVideoControlPanel) this.$root.$off('video-control-panel-updated', this.updateVideoControlPanel)
}, },
@ -314,31 +316,146 @@ export default {
handleScroll() { handleScroll() {
this.closeMenu() this.closeMenu()
}, },
onTagDragStart(tag, event) {
this.dragTag = tag
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', tag.path)
// 不用 HTML5 拖拽。现场大屏会给 html 加 zoom,原生拖拽要截整页快照,缩放不是 1 时会把页面卡住,松手也不换位。
onTagPointerDown(tag, event) {
if (event.button !== 0) return
if (event.target.closest && event.target.closest('.el-icon-close')) return
if (this.isAffix(tag)) return
this.detachTagPointer()
const rect = event.currentTarget.getBoundingClientRect()
this.pointerDrag = {
tag,
el: event.currentTarget,
startX: event.clientX,
startY: event.clientY,
offsetX: event.clientX - rect.left,
toPath: tag.path,
active: false,
pointerId: event.pointerId,
userSelect: document.body.style.userSelect
} }
},
onTagDragOver(tag, event) {
if (!this.dragTag || this.dragTag.path === tag.path) return
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
},
onTagDrop(tag) {
const from = this.dragTag
this.dragTag = null
if (!from || from.path === tag.path) return
const el = event.currentTarget
if (el && el.setPointerCapture) {
try { el.setPointerCapture(event.pointerId) } catch (err) { /* 部分现场浏览器不支持捕获 */ }
}
document.addEventListener('pointermove', this.onTagPointerMove)
document.addEventListener('pointerup', this.onTagPointerUp, true)
document.addEventListener('pointercancel', this.onTagPointerUp, true)
document.addEventListener('mouseup', this.onTagPointerUp, true)
window.addEventListener('blur', this.onTagDragCancel)
},
onTagPointerMove(event) {
const drag = this.pointerDrag
if (!drag || event.pointerId !== drag.pointerId) return
const movedX = Math.abs(event.clientX - drag.startX)
const movedY = Math.abs(event.clientY - drag.startY)
if (!drag.active) {
if (movedX < 2 && movedY < 2) return
drag.active = true
this.dragTag = drag.tag
document.body.style.userSelect = 'none'
}
event.preventDefault()
this.followTagPointer(drag, event.clientX)
this.scrollTagsWhileDragging(event.clientX)
},
onTagPointerUp(event) {
const drag = this.pointerDrag
if (!drag) return
if (event && event.type !== 'mouseup' && event.pointerId !== undefined && event.pointerId !== drag.pointerId) return
const active = drag.active
const fromPath = drag.tag.path
const toPath = drag.toPath
document.body.style.userSelect = drag.userSelect || ''
this.detachTagPointer()
if (!active) return
const blockClick = (e) => {
e.preventDefault()
e.stopPropagation()
window.removeEventListener('click', blockClick, true)
}
window.addEventListener('click', blockClick, true)
setTimeout(() => window.removeEventListener('click', blockClick, true), 0)
const views = this.visitedViews const views = this.visitedViews
const fromIndex = views.findIndex(item => item.path === from.path)
const toIndex = views.findIndex(item => item.path === tag.path)
if (fromIndex < 0 || toIndex < 0) return
const affixCount = views.filter(item => item.meta && item.meta.affix).length
if (fromIndex < affixCount || toIndex < affixCount) return
this.$store.dispatch('tagsView/moveVisitedView', { from: fromIndex, to: toIndex })
},
onTagDragEnd() {
const fromIndex = views.findIndex(item => item.path === fromPath)
const toIndex = views.findIndex(item => item.path === toPath)
if (fromIndex >= 0 && toIndex >= 0 && fromIndex !== toIndex) {
const affixCount = views.filter(item => item.meta && item.meta.affix).length
if (fromIndex >= affixCount && toIndex >= affixCount) {
this.$store.dispatch('tagsView/moveVisitedView', { from: fromIndex, to: toIndex })
}
}
this.$nextTick(() => this.clearTagTransforms())
},
// 按页面上现在的标签位置拖。换过顺序后组件引用的顺序不会跟着变,不能再用它定位。
followTagPointer(drag, clientX) {
const current = drag.el
if (!current || !current.isConnected) return
this.clearTagTransforms()
const layoutLeft = current.getBoundingClientRect().left
const width = current.offsetWidth
const dx = clientX - drag.offsetX - layoutLeft
this.setTagShift(current, dx)
drag.toPath = this.resolveTagPathAt(layoutLeft + dx + width / 2, drag.tag.path)
},
resolveTagPathAt(center, fromPath) {
const items = this.tagElements()
const affixCount = this.visitedViews.filter(item => item.meta && item.meta.affix).length
let target = ''
for (let i = affixCount; i < items.length; i++) {
const rect = items[i].getBoundingClientRect()
if (center >= rect.left && center <= rect.right) {
target = items[i].dataset.path || ''
break
}
}
if (!target) {
const first = items[affixCount]
const last = items[items.length - 1]
if (first && center < first.getBoundingClientRect().left) target = first.dataset.path || ''
else if (last && center > last.getBoundingClientRect().right) target = last.dataset.path || ''
}
return target || fromPath
},
tagElements() {
const pane = this.$refs.scrollPane
const root = pane && pane.$el
if (!root) return []
return Array.from(root.querySelectorAll('.tags-view-item'))
},
setTagShift(el, dx) {
const face = el.querySelector('.tags-view-face')
if (!face) return
face.style.transform = dx ? `translateX(${dx}px)` : ''
},
clearTagTransforms() {
this.tagElements().forEach(el => this.setTagShift(el, 0))
},
onTagDragCancel() {
const drag = this.pointerDrag
if (!drag) return
document.body.style.userSelect = drag.userSelect || ''
this.detachTagPointer()
},
detachTagPointer() {
document.removeEventListener('pointermove', this.onTagPointerMove)
document.removeEventListener('pointerup', this.onTagPointerUp, true)
document.removeEventListener('pointercancel', this.onTagPointerUp, true)
document.removeEventListener('mouseup', this.onTagPointerUp, true)
window.removeEventListener('blur', this.onTagDragCancel)
this.clearTagTransforms()
this.pointerDrag = null
this.dragTag = null this.dragTag = null
this.dropPath = ''
},
scrollTagsWhileDragging(clientX) {
const pane = this.$refs.scrollPane
if (!pane || !pane.$el || !pane.scrollWrapper) return
const rect = pane.$el.getBoundingClientRect()
const wrap = pane.scrollWrapper
const edge = 28
if (clientX > rect.right - edge) wrap.scrollLeft += 16
else if (clientX < rect.left + edge) wrap.scrollLeft -= 16
}, },
titleFn(item, children) { titleFn(item, children) {
let str = ""; let str = "";
@ -498,19 +615,29 @@ export default {
display: inline-block; display: inline-block;
position: relative; position: relative;
cursor: grab; cursor: grab;
user-select: none;
touch-action: none;
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
border: 0; border: 0;
color: #c0c5db; color: #c0c5db;
background: transparent; background: transparent;
padding: 0 26px;
padding: 0;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
vertical-align: top; vertical-align: top;
box-sizing: border-box; box-sizing: border-box;
transition: color .2s, background-color .2s; transition: color .2s, background-color .2s;
// 相邻标签之间的青色短竖线(设计稿的分割线样式)
.tags-view-face {
position: relative;
display: block;
height: 40px;
padding: 0 26px;
line-height: 40px;
}
// 分隔线留在标签格子上,拖动时只移动里面的文字和关闭按钮
& + .tags-view-item::after { & + .tags-view-item::after {
content: ''; content: '';
position: absolute; position: absolute;
@ -532,8 +659,9 @@ export default {
} }
&.is-dragging { &.is-dragging {
opacity: 0.45;
z-index: 3;
cursor: grabbing; cursor: grabbing;
transition: none;
} }
&.active { &.active {
@ -572,21 +700,24 @@ export default {
.tags-view-wrapper { .tags-view-wrapper {
.tags-view-item { .tags-view-item {
.el-icon-close { .el-icon-close {
margin-left: 4px;
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
position: absolute;
top: 0;
right: 0;
width: 26px;
height: 40px;
margin-left: 0;
line-height: 40px;
border-radius: 0;
text-align: center; text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%;
cursor: default;
transition: background-color .2s, color .2s;
&:before { &:before {
transform: scale(.6); transform: scale(.6);
display: inline-block; display: inline-block;
vertical-align: -3px; vertical-align: -3px;
} }
&:hover { &:hover {
background-color: rgba(84, 255, 255, 0.25);
background: transparent;
color: #fff; color: #fff;
} }
} }


+ 4
- 1
src/main.js View File

@ -52,9 +52,12 @@ import 'vue-video-player/src/custom-theme.css'
import i18n from './lang' import i18n from './lang'
// 站点配置:按访问 IP 切换标题头横幅与浏览器标签页站名 // 站点配置:按访问 IP 切换标题头横幅与浏览器标签页站名
import { applyStationTitle } from '@/utils/stationTitle' import { applyStationTitle } from '@/utils/stationTitle'
import { bindViewportScale } from '@/utils/viewportScale'
import { applyViewportScale, bindViewportScale } from '@/utils/viewportScale'
applyStationTitle() applyStationTitle()
bindViewportScale() bindViewportScale()
router.afterEach(() => {
applyViewportScale()
})
// 全局方法挂载 // 全局方法挂载
Vue.prototype.getDicts = getDicts Vue.prototype.getDicts = getDicts


+ 29
- 81
src/utils/viewportScale.js View File

@ -1,27 +1,6 @@
// 1920×1080 是页面原来的基准,这个尺寸保持原样铺满。
// 只有窗口明显更大时才整体缩小。缩小后把布局盒按比例拉大,视觉上仍然铺满窗口。
const DESIGN_WIDTH = 1920
const DESIGN_HEIGHT = 1080
const MIN_SCALE = 0.72
function readWindowBox() {
const rawWidth = window.innerWidth || DESIGN_WIDTH
const rawHeight = window.innerHeight || DESIGN_HEIGHT
const applied = Number(document.documentElement.dataset.appScale || 1) || 1
// 已经比设计尺寸大,读到的是缩放前的窗口,不能再除一次。
if (rawWidth > DESIGN_WIDTH + 40 || rawHeight > DESIGN_HEIGHT + 40) {
return { width: rawWidth, height: rawHeight }
}
return { width: rawWidth / applied, height: rawHeight / applied }
}
export function computeViewportScale() {
const { width, height } = readWindowBox()
// 1920×1080 附近(含浏览器边框吃掉的几十像素)保持 1,页面按原来的方式铺满。
if (width <= DESIGN_WIDTH + 40 && height <= DESIGN_HEIGHT + 40) return 1
const scale = Math.min(width / DESIGN_WIDTH, height / DESIGN_HEIGHT, 1)
return Math.max(MIN_SCALE, Math.round(scale * 1000) / 1000)
}
// 汇聚页、动环巡检、站端都按浏览器 100% 的原始布局显示。
// 不再根据窗口或浏览器缩放去改 zoom,避免放大后页面被压成一条、回到 100% 也恢复不了。
const BASE_DPR_KEY = "viewport-base-dpr"
function ensureScaleStyle() { function ensureScaleStyle() {
let style = document.getElementById("app-viewport-scale") let style = document.getElementById("app-viewport-scale")
@ -33,6 +12,10 @@ function ensureScaleStyle() {
} }
function clearPortalCanvas() { function clearPortalCanvas() {
const root = document.documentElement
root.removeAttribute("data-portal-scale")
root.style.removeProperty("--portal-scale")
root.removeAttribute("data-app-scale")
const screen = document.querySelector(".portal-screen") const screen = document.querySelector(".portal-screen")
if (!screen) return if (!screen) return
;["width", "height", "minWidth", "minHeight", "transform", "transformOrigin", "position", "left", "top", "zoom", "marginTop", "marginBottom", "overflow"].forEach(name => { ;["width", "height", "minWidth", "minHeight", "transform", "transformOrigin", "position", "left", "top", "zoom", "marginTop", "marginBottom", "overflow"].forEach(name => {
@ -42,71 +25,36 @@ function clearPortalCanvas() {
if (body) body.style.columnGap = "" if (body) body.style.columnGap = ""
} }
// 汇聚首页铺满窗口,同时按能放下 1920×1080 的比例整体放大。
// 大屏上字号和模块一起变大,不会再出现字很小、卡片和视频被单独拉得很开。
function fitPortalCanvas() {
const screen = document.querySelector(".portal-screen")
if (!screen) return false
const width = window.innerWidth || DESIGN_WIDTH
const height = window.innerHeight || DESIGN_HEIGHT
const scale = Math.min(width / DESIGN_WIDTH, height / DESIGN_HEIGHT) || 1
const body = screen.querySelector(".portal-body")
screen.style.zoom = String(scale)
screen.style.width = `${width / scale}px`
screen.style.height = `${height / scale}px`
screen.style.minWidth = "0px"
screen.style.minHeight = "0px"
screen.style.position = "absolute"
screen.style.transform = "none"
screen.style.transformOrigin = "left top"
screen.style.left = "0px"
screen.style.top = "0px"
screen.style.overflow = "hidden"
screen.style.marginTop = "0px"
screen.style.marginBottom = "0px"
if (body) body.style.columnGap = ""
return true
}
export function applyViewportScale() { export function applyViewportScale() {
const portalHome = /\/portal\/?$/.test(window.location.pathname || "")
if (portalHome && fitPortalCanvas()) {
const root = document.documentElement
root.dataset.appScale = "1"
root.style.zoom = ""
ensureScaleStyle().textContent = ""
return 1
}
try { localStorage.removeItem(BASE_DPR_KEY) } catch (err) { /* 清掉之前记下的错误比例 */ }
clearPortalCanvas() clearPortalCanvas()
const scale = computeViewportScale()
const root = document.documentElement const root = document.documentElement
root.dataset.appScale = String(scale)
root.style.zoom = "" root.style.zoom = ""
ensureScaleStyle().textContent = scale === 1
? ""
: [
`html { zoom: ${scale}; }`,
`#app { width: ${(100 / scale).toFixed(4)}%; height: ${(100 / scale).toFixed(4)}%; }`
].join("\n")
return scale
ensureScaleStyle().textContent = ""
return 1
} }
export function bindViewportScale() {
let frame = 0
const update = () => {
frame = 0
applyViewportScale()
function blockPageZoom(event) {
if (event.type === "wheel" || event.type === "mousewheel") {
if (event.ctrlKey) event.preventDefault()
return
} }
const schedule = () => {
if (frame) return
frame = window.requestAnimationFrame(update)
if (!(event.ctrlKey || event.metaKey) || event.altKey) return
const key = event.key
// 拦住继续放大缩小。Ctrl+0 仍然留给浏览器,用来回到 100%。
if (key === "+" || key === "-" || key === "=" || key === "_") {
event.preventDefault()
} }
schedule()
window.addEventListener("resize", schedule)
window.addEventListener("orientationchange", schedule)
}
export function bindViewportScale() {
applyViewportScale()
window.addEventListener("wheel", blockPageZoom, { passive: false })
window.addEventListener("mousewheel", blockPageZoom, { passive: false })
window.addEventListener("keydown", blockPageZoom)
return () => { return () => {
if (frame) window.cancelAnimationFrame(frame)
window.removeEventListener("resize", schedule)
window.removeEventListener("orientationchange", schedule)
window.removeEventListener("wheel", blockPageZoom)
window.removeEventListener("mousewheel", blockPageZoom)
window.removeEventListener("keydown", blockPageZoom)
} }
} }

+ 24
- 17
src/views/cards/patrolReport/InspectionReporter.vue View File

@ -232,25 +232,36 @@ export default {
(this.queryParams.pageNum - 1) * this.queryParams.pageSize + index + 1 (this.queryParams.pageNum - 1) * this.queryParams.pageSize + index + 1
); );
}, },
openReportFile(url) {
console.log("导出URL:", url);
const a = document.createElement("a");
a.setAttribute("href", url);
a.setAttribute("target", "_blank");
a.click();
},
ftpImageViewUrl(row, suffix) { ftpImageViewUrl(row, suffix) {
// 汇聚下列表来自各个站,本站 FTP 地址对不上,直接用报告自己的文件地址。
if (this.$store.getters.fromPortal && !this.$store.getters.inSite) {
if (!row.filePath) {
this.$modal.msgWarning("巡检报告正在生成,请稍后");
return;
}
this.openReportFile(row.filePath);
return;
}
// 添加安全检查 // 添加安全检查
// if (!this.systemParams || !this.systemParams.ftpViewAddress) {
// this.$modal.msgWarning("系统配置未加载,请稍后重试");
// console.error("systemParams为空:", this.systemParams);
// return;
// }
if (!this.systemParams || !this.systemParams.ftpViewAddress) {
this.$modal.msgWarning("系统配置未加载,请稍后重试");
console.error("systemParams为空:", this.systemParams);
return;
}
console.log("使用FTP地址:", this.systemParams.ftpViewAddress); console.log("使用FTP地址:", this.systemParams.ftpViewAddress);
if (row.filePath) { if (row.filePath) {
var fileName = row.filePath.split(".")[0]; var fileName = row.filePath.split(".")[0];
// const url = this.systemParams.ftpViewAddress + fileName + "." + suffix;
const url = row.filePath;
console.log("导出URL:", url);
const a = document.createElement("a");
a.setAttribute("href", url);
a.setAttribute("target", "_blank");
a.click();
const url = this.systemParams.ftpViewAddress + fileName + "." + suffix;
this.openReportFile(url);
} else { } else {
this.isLock = true; this.isLock = true;
this.getList(() => { this.getList(() => {
@ -259,11 +270,7 @@ export default {
}); });
if (it && it.filePath) { if (it && it.filePath) {
const url = this.systemParams.ftpViewAddress + it.filePath; const url = this.systemParams.ftpViewAddress + it.filePath;
console.log("导出URL:", url);
const a = document.createElement("a");
a.setAttribute("href", url);
a.setAttribute("target", "_blank");
a.click();
this.openReportFile(url);
this.isLock = false; this.isLock = false;
} else { } else {
this.isLock = true; this.isLock = true;


+ 8
- 3
src/views/portal/components/PortalDeviceTree.vue View File

@ -347,12 +347,17 @@ export default {
<style lang="scss"> <style lang="scss">
.portal-tree-tooltip.el-tooltip__popper { .portal-tree-tooltip.el-tooltip__popper {
max-width: 420px;
padding: 8px 12px;
color: #eaf6ff; color: #eaf6ff;
background: #176bc2;
border: 1px solid #4ba3ff;
background: #0b205b;
border: 1px solid #3f7fd8;
border-radius: 4px;
font-size: calc(14px * var(--portal-scale, 1));
line-height: 1.4;
} }
.portal-tree-tooltip.el-tooltip__popper[x-placement^="top"] .popper__arrow { .portal-tree-tooltip.el-tooltip__popper[x-placement^="top"] .popper__arrow {
border-top-color: #176bc2;
border-top-color: #0b205b;
} }
</style> </style>

+ 39
- 5
src/views/portal/index.vue View File

@ -96,7 +96,14 @@
:value="station.code" :value="station.code"
/> />
</el-select> </el-select>
<el-select v-model="trendType" class="trend-select" size="mini" placeholder="巡检类型">
<el-select
v-model="trendType"
class="trend-select trend-select-type"
size="mini"
multiple
collapse-tags
placeholder="巡检类型"
>
<el-option <el-option
v-for="item in overviewList" v-for="item in overviewList"
:key="item.algCategory" :key="item.algCategory"
@ -107,6 +114,7 @@
<el-select v-model="trendGranularity" class="trend-select trend-select-short" size="mini" placeholder="周期"> <el-select v-model="trendGranularity" class="trend-select trend-select-short" size="mini" placeholder="周期">
<el-option label="月" value="month" /> <el-option label="月" value="month" />
<el-option label="季度" value="quarter" /> <el-option label="季度" value="quarter" />
<el-option label="年度" value="year" />
</el-select> </el-select>
<div class="trend-legend"> <div class="trend-legend">
<span v-for="item in trendLegend" :key="item.name" class="legend-item"> <span v-for="item in trendLegend" :key="item.name" class="legend-item">
@ -395,7 +403,7 @@ export default {
defectCarouselTimer: null, defectCarouselTimer: null,
overviewStationId: "", overviewStationId: "",
trendStationId: "", trendStationId: "",
trendType: "表计",
trendType: ["表计"],
trendGranularity: "month", trendGranularity: "month",
trendLegend: TREND_LEGEND, trendLegend: TREND_LEGEND,
overviewList: createEmptyOverview(), overviewList: createEmptyOverview(),
@ -434,7 +442,7 @@ export default {
return this.defectSlides[this.defectSlideIndex]; return this.defectSlides[this.defectSlideIndex];
}, },
trendQueryKey() { trendQueryKey() {
return [this.trendStationId, this.trendType, this.trendGranularity].join("|");
return [this.trendStationId, this.trendType.join(","), this.trendGranularity].join("|");
}, },
cameraStats() { cameraStats() {
return [ return [
@ -500,7 +508,7 @@ export default {
}, },
selectTrendForStation(station, algCategory) { selectTrendForStation(station, algCategory) {
this.trendStationId = station.code; this.trendStationId = station.code;
this.trendType = algCategory;
this.trendType = [algCategory];
}, },
portalRequestConfig() { portalRequestConfig() {
if (!this.portalRequestSource) this.portalRequestSource = axios.CancelToken.source(); if (!this.portalRequestSource) this.portalRequestSource = axios.CancelToken.source();
@ -898,7 +906,7 @@ export default {
loadDefectTrend() { loadDefectTrend() {
const requestSerial = ++this.trendRequestSerial; const requestSerial = ++this.trendRequestSerial;
const params = { const params = {
algCategory: this.trendType,
algCategory: this.trendType.join(","),
dateType: this.trendGranularity dateType: this.trendGranularity
}; };
if (this.trendStationId) params.stationCode = this.trendStationId; if (this.trendStationId) params.stationCode = this.trendStationId;
@ -1214,6 +1222,10 @@ $panel-border: rgba(84, 130, 255, 0.22);
width: 126px; width: 126px;
} }
.trend-select-type {
width: 168px;
}
.trend-select-short { .trend-select-short {
width: 72px; width: 72px;
} }
@ -2065,6 +2077,28 @@ $panel-border: rgba(84, 130, 255, 0.22);
height: 28px; height: 28px;
line-height: 28px; line-height: 28px;
} }
.el-select__tags {
flex-wrap: nowrap;
max-width: calc(100% - 28px) !important;
}
.el-tag {
height: 20px;
margin: 2px 0 2px 4px;
line-height: 18px;
color: #dcecff;
background: rgba(0, 42, 127, 0.85);
border-color: rgba(57, 136, 255, 0.7);
}
.el-tag .el-select__tags-text {
max-width: 72px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
}
} }
} }
</style> </style>

+ 2
- 2
src/views/video/LivePreview.vue View File

@ -121,7 +121,7 @@
</div> </div>
</section> </section>
<section class="control-section"> <section class="control-section">
<div class="section-title"><span>云台控制</span><small>{{ selectedChannelName }}</small></div>
<div class="section-title section-title-stack"><span>云台控制</span><small>{{ selectedChannelName }}</small></div>
<div class="ptz-grid"> <div class="ptz-grid">
<button <button
v-for="button in ptzButtons" v-for="button in ptzButtons"
@ -1163,7 +1163,7 @@ const PTZ_CODES = {
.empty-slot { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: #466f9c; background: radial-gradient(circle at center, #0b2a61, #050b24 70%); user-select: none; }.empty-slot i { font-size: 28px; }.empty-slot span { font-size: 11px; } .empty-slot { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: #466f9c; background: radial-gradient(circle at center, #0b2a61, #050b24 70%); user-select: none; }.empty-slot i { font-size: 28px; }.empty-slot span { font-size: 11px; }
.control-panel { position: absolute; top: 0; right: 0; bottom: 0; z-index: 4; width: 242px; min-width: 242px; height: auto; margin: 0; overflow: visible; border: 1px solid rgba(63,127,216,.72); border-radius: 2px; background: rgba(7, 24, 78, .82); box-sizing: border-box; cursor: move; user-select: none; } .control-panel { position: absolute; top: 0; right: 0; bottom: 0; z-index: 4; width: 242px; min-width: 242px; height: auto; margin: 0; overflow: visible; border: 1px solid rgba(63,127,216,.72); border-radius: 2px; background: rgba(7, 24, 78, .82); box-sizing: border-box; cursor: move; user-select: none; }
.control-panel-body { height: 100%; padding: 10px; overflow-x: hidden; overflow-y: auto; box-sizing: border-box; } .control-panel-body { height: 100%; padding: 10px; overflow-x: hidden; overflow-y: auto; box-sizing: border-box; }
.control-section { padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid rgba(39, 128, 220, .28); }.section-title { height: 30px; display: flex; align-items: center; justify-content: space-between; color: #54ffff; }.section-title small { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #719bc8; }.section-title button { border: 0; color: #75bfff; background: transparent; cursor: pointer; }
.control-section { padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid rgba(39, 128, 220, .28); }.section-title { height: 30px; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 8px; overflow: hidden; color: #54ffff; }.section-title span { flex: none; white-space: nowrap; }.section-title small { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; color: #719bc8; }.section-title-stack { height: auto; flex-direction: column; align-items: flex-start; gap: 2px; overflow: visible; }.section-title-stack small { width: 100%; white-space: normal; text-align: left; line-height: 1.4; overflow: visible; text-overflow: unset; }.section-title button { border: 0; color: #75bfff; background: transparent; cursor: pointer; }
.ptz-grid { width: 168px; margin: 6px auto 12px; display: grid; grid-template-columns: repeat(3, 50px); gap: 9px; }.ptz-grid button { width: 50px; height: 50px; border: 1px solid #3988ff; border-radius: 7px; color: #43c0ff; background: #01016b; box-shadow: none; cursor: pointer; transition: color .15s, background .15s, transform .15s; }.ptz-grid button i { font-size: 18px; font-weight: 700; }.ptz-grid button:hover { color: #43c0ff; background: #01016b; }.ptz-grid button:active { color: #ffffff; background: #01016b; transform: scale(.96); } .ptz-grid { width: 168px; margin: 6px auto 12px; display: grid; grid-template-columns: repeat(3, 50px); gap: 9px; }.ptz-grid button { width: 50px; height: 50px; border: 1px solid #3988ff; border-radius: 7px; color: #43c0ff; background: #01016b; box-shadow: none; cursor: pointer; transition: color .15s, background .15s, transform .15s; }.ptz-grid button i { font-size: 18px; font-weight: 700; }.ptz-grid button:hover { color: #43c0ff; background: #01016b; }.ptz-grid button:active { color: #ffffff; background: #01016b; transform: scale(.96); }
.speed-row { display: grid; grid-template-columns: 34px minmax(0, 1fr) 48px; align-items: center; gap: 7px; color: #8eb7df; font-size: 12px; }.speed-row > input[type="range"] { min-width: 0; height: 8px; box-sizing: border-box; margin: 0; appearance: none; border: 1px solid #2787ed; border-radius: 4px; outline: none; cursor: pointer; }.speed-row > input[type="range"]::-webkit-slider-runnable-track { height: 6px; border: 0; border-radius: 3px; background: transparent; }.speed-row > input[type="range"]::-webkit-slider-thumb { width: 14px; height: 14px; margin-top: -4px; appearance: none; border: 0; border-radius: 50%; background: #2787ed; }.speed-row > input[type="range"]::-moz-range-track { height: 6px; border: 0; border-radius: 3px; background: transparent; }.speed-row > input[type="range"]::-moz-range-progress { background: transparent; }.speed-row > input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border: 0; border-radius: 50%; background: #2787ed; }.speed-input { width: 48px; } .speed-row { display: grid; grid-template-columns: 34px minmax(0, 1fr) 48px; align-items: center; gap: 7px; color: #8eb7df; font-size: 12px; }.speed-row > input[type="range"] { min-width: 0; height: 8px; box-sizing: border-box; margin: 0; appearance: none; border: 1px solid #2787ed; border-radius: 4px; outline: none; cursor: pointer; }.speed-row > input[type="range"]::-webkit-slider-runnable-track { height: 6px; border: 0; border-radius: 3px; background: transparent; }.speed-row > input[type="range"]::-webkit-slider-thumb { width: 14px; height: 14px; margin-top: -4px; appearance: none; border: 0; border-radius: 50%; background: #2787ed; }.speed-row > input[type="range"]::-moz-range-track { height: 6px; border: 0; border-radius: 3px; background: transparent; }.speed-row > input[type="range"]::-moz-range-progress { background: transparent; }.speed-row > input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border: 0; border-radius: 50%; background: #2787ed; }.speed-input { width: 48px; }
.preset-quick-actions { display: flex; align-items: center; gap: 7px; margin: 9px 0 0 1px; }.preset-quick-actions button { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 1px solid rgba(48, 151, 248, .5); color: #58bfff; background: transparent; cursor: pointer; }.preset-quick-actions button:hover { background: rgba(31, 126, 222, .25); }.preset-quick-actions img { width: 18px; height: 18px; object-fit: contain; } .preset-quick-actions { display: flex; align-items: center; gap: 7px; margin: 9px 0 0 1px; }.preset-quick-actions button { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 1px solid rgba(48, 151, 248, .5); color: #58bfff; background: transparent; cursor: pointer; }.preset-quick-actions button:hover { background: rgba(31, 126, 222, .25); }.preset-quick-actions img { width: 18px; height: 18px; object-fit: contain; }


Loading…
Cancel
Save