Compare commits

...

5 Commits

44 changed files with 3172 additions and 163 deletions
Split View
  1. +1
    -1
      .env.development
  2. +1
    -1
      .env.htjc6
  3. +1
    -1
      .env.production
  4. +1
    -1
      .env.staging
  5. +5
    -3
      src/api/inspectionDataManage/inspectionReporter.js
  6. +129
    -0
      src/api/portal/index.js
  7. +20
    -4
      src/api/videosMonitor/index.js
  8. BIN
      src/assets/images/header/xfdh-title-navbar.png
  9. BIN
      src/assets/portal/icon-donghuan.png
  10. BIN
      src/assets/portal/icon-enter.png
  11. BIN
      src/assets/portal/icon-fire.png
  12. BIN
      src/assets/portal/icon-security.png
  13. BIN
      src/assets/portal/icon-total.png
  14. BIN
      src/assets/portal/icon-triangle.png
  15. BIN
      src/assets/portal/panel-title.png
  16. BIN
      src/assets/portal/tree-collapse.png
  17. BIN
      src/assets/portal/tree-toggle.png
  18. +3
    -1
      src/layout/components/AppMain.vue
  19. +35
    -2
      src/layout/components/Navbar.vue
  20. +5
    -2
      src/layout/components/Sidebar/index.vue
  21. +102
    -7
      src/layout/index.vue
  22. +7
    -0
      src/router/index.js
  23. +5
    -0
      src/store/getters.js
  24. +3
    -1
      src/store/index.js
  25. +86
    -0
      src/store/modules/siteContext.js
  26. +54
    -1
      src/utils/request.js
  27. +2
    -2
      src/utils/stationTitle.js
  28. +91
    -84
      src/views/cards/defectRecord/index.vue
  29. +18
    -1
      src/views/cards/patrolPlan/notstart.vue
  30. +15
    -13
      src/views/cards/patrolPlan/summary_editTask.vue
  31. +20
    -14
      src/views/cards/patrolPlan/summary_newTask.vue
  32. +12
    -0
      src/views/cards/patrolReport/InspectionReporter.vue
  33. +7
    -1
      src/views/cards/patrolReport/patrolReportDetail.vue
  34. +46
    -6
      src/views/cards/site_management/site_management.vue
  35. +19
    -3
      src/views/cards/taskSummary/inspectionTask.vue
  36. +3
    -1
      src/views/cards/taskSummary/inspectionTaskDefectDetail.vue
  37. +3
    -0
      src/views/cards/taskSummary/newInspectionTaskDetail.vue
  38. +1
    -1
      src/views/login.vue
  39. +85
    -0
      src/views/portal/components/DefectTrendChart.vue
  40. +320
    -0
      src/views/portal/components/PortalDeviceTree.vue
  41. +1987
    -0
      src/views/portal/index.vue
  42. +1
    -1
      src/views/register.vue
  43. +76
    -9
      src/views/video/LivePreview.vue
  44. +8
    -2
      src/views/video/components/NvrDeviceTree.vue

+ 1
- 1
.env.development View File

@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 消防动环管理平台
VUE_APP_TITLE = 动环智能巡检系统
# 开发环境配置
ENV = 'development'


+ 1
- 1
.env.htjc6 View File

@ -1,7 +1,7 @@
NODE_ENV = 'production'
ENV = 'production'
VUE_APP_TITLE = 消防动环管理平台
VUE_APP_TITLE = 动环智能巡检系统
VUE_APP_BASE_API = '/app/htjc6/api'
VUE_APP_NVR_API = '/nvr-api'


+ 1
- 1
.env.production View File

@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 消防动环管理平台
VUE_APP_TITLE = 动环智能巡检系统
# 生产环境配置
ENV = 'production'


+ 1
- 1
.env.staging View File

@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 消防动环管理平台
VUE_APP_TITLE = 动环智能巡检系统
BABEL_ENV = production


+ 5
- 3
src/api/inspectionDataManage/inspectionReporter.js View File

@ -20,10 +20,12 @@ export function getPatrolReport(lineId,filter) {
});
}
export function getPatrolReportShaoxing(url, lineId, filter) {
export function getPatrolReportShaoxing(url, lineId, filter, stationCode) {
// 汇聚态:走聚合代理,stationCode 取点击的报告所属站
const prefix = stationCode ? "/agg/proxy" : "";
return request({
url: `/patrol/insreport/shaoxing/${url}/${lineId}/${filter}`,
url: `${prefix}/patrol/insreport/shaoxing/${url}/${lineId}/${filter}`,
method: "get",
params: {},
params: stationCode ? { stationCode } : {},
});
}

+ 129
- 0
src/api/portal/index.js View File

@ -0,0 +1,129 @@
// 汇聚门户(跨站点汇总)接口。
// 这些接口由中心端后端提供,返回"所有站"的汇总数据;
// 与单站业务接口不同,它们不依赖当前登录用户所属站。
// 后端未就绪时页面以空态展示,不影响其它功能。
import request from "@/utils/request";
// 场站列表
// 返回 data: [{ id, stationCode, stationName, baseUrl, portalUrl, enabled }]
export function getStationList(query) {
return request({
url: "/agg/station/all",
method: "get",
params: query,
noAlert: true
});
}
// 本轮巡检任务统计(表计/外观/红外)
// 返回 data: [{ algCategory, inspectedTaskCount, completedTaskCount, defectCount, ... }]
export function getInspectionOverview(query) {
return request({
url: "/agg/statistic/inspection",
method: "get",
params: query,
noAlert: true
});
}
// 缺陷发现趋势(最近 12 个月或 4 个季度)
// 返回 data: [{ date, minorCount, generalCount, seriousCount, urgentCount, ... }]
export function getDefectTrend(query) {
return request({
url: "/agg/statistic/defect",
method: "get",
params: query,
noAlert: true
});
}
// 通道设备来源统计
// 返回 data: { totalCount, deviceSourceCounts: [{ deviceSource, count }] }
export function getChannelStats(query) {
return request({
url: "/agg/statistic/channel",
method: "get",
params: query,
noAlert: true
});
}
// 通道树
// 返回 data: 区域、巡检设备、通道三级树
export function getChannelTree(query) {
return request({
url: "/agg/tree/channel",
method: "get",
params: query,
noAlert: true
});
}
// 汇聚侧实时视频启动
// 返回 data: { stationCode, cameraCode, streamId, hls, flv, status }
export function startRealtimeVideo(query) {
const { stationCode, ...data } = query || {};
return request({
url: "/agg/video/realtime/start",
method: "post",
params: { stationCode },
data,
noAlert: true
});
}
// 汇聚侧实时视频停止
// 入参按后端要求传 stationCode + cameraCode
export function stopRealtimeVideo(query) {
const { stationCode, ...data } = query || {};
return request({
url: "/agg/video/realtime/stop",
method: "post",
params: { stationCode },
data,
noAlert: true
});
}
// 汇聚侧云台控制:按摄像头所属站点代理转发
// stationCode 取当前控制摄像头所在站(而非顶部站名筛选),以查询串方式传递
export function aggPtzControl(payload) {
const { stationCode, ...data } = payload || {};
return request({
url: "/agg/proxy/patrol/preset/ptzControl",
method: "post",
params: { stationCode },
data,
noAlert: true
});
}
// 汇聚侧预置位列表:路径与单站一致(cameraCode 按 # 拆成 device/domain 放路径上),仅追加 ?stationCode
export function aggPresetList(payload) {
const { stationCode, cameraCode } = payload || {};
const value = String(cameraCode || "");
const separatorIndex = value.indexOf("#");
if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
return Promise.reject(new Error("摄像机编码格式不正确"));
}
const device = encodeURIComponent(value.slice(0, separatorIndex));
const domain = encodeURIComponent(value.slice(separatorIndex + 1));
return request({
url: `/agg/proxy/patrol/preset/presetList/${device}/${domain}`,
method: "get",
params: { stationCode },
noAlert: true
});
}
// 汇聚侧预置位保存:按摄像头所属站点代理转发
export function aggPresetCreate(payload) {
const { stationCode, ...data } = payload || {};
return request({
url: "/agg/proxy/patrol/preset/presetCreate",
method: "post",
params: { stationCode },
data,
noAlert: true
});
}

+ 20
- 4
src/api/videosMonitor/index.js View File

@ -1,4 +1,5 @@
import request from "@/utils/request";
import store from "@/store";
import { defectList } from "../home/index";
/**
@ -229,11 +230,13 @@ export function analysisDefectList(params) {
* @param {*} data
* @returns
*/
export function analysisDefectListShaoxing(params) {
export function analysisDefectListShaoxing(params, stationCode) {
// 汇聚态:走聚合代理,stationCode 取点击的任务所属站
const prefix = stationCode ? "/agg/proxy" : "";
return request({
url: `/patrol/analysis/list_v2_ex`,
url: `${prefix}/patrol/analysis/list_v2_ex`,
method: "get",
params,
params: stationCode ? { ...params, stationCode } : params,
});
}
@ -317,14 +320,27 @@ export function getDefectAlarmList(params = {}) {
});
}
/**
* 缺陷记录卡片的列表
* 缺陷记录卡片的列表支持 pageNum/pageSize/desc/pointName 等服务端筛选
* @param {*} data
* @returns
*/
export function defectRecordList(params) {
// 汇聚态由拦截器映射改写为 /agg/patrol/analysis/defect/list;单站保持原路径(服务端 algName 筛选)
return request({
url: `/patrol/analysis/defect/list`,
method: "get",
params,
});
}
/**
* 缺陷类型下拉选项汇聚态 descOptions单站 algNameOptions算法名称
* @returns
*/
export function defectDescOptions() {
const isAgg = store.getters.fromPortal;
return request({
url: isAgg ? `/agg/analysis/defect/algNameOptions` : `/patrol/analysis/defect/algNameOptions`,
method: "get",
});
}

BIN
src/assets/images/header/xfdh-title-navbar.png View File

Before After
Width: 1920  |  Height: 50  |  Size: 80 KiB Width: 1920  |  Height: 50  |  Size: 115 KiB

BIN
src/assets/portal/icon-donghuan.png View File

Before After
Width: 48  |  Height: 49  |  Size: 4.0 KiB

BIN
src/assets/portal/icon-enter.png View File

Before After
Width: 16  |  Height: 15  |  Size: 655 B

BIN
src/assets/portal/icon-fire.png View File

Before After
Width: 48  |  Height: 49  |  Size: 3.0 KiB

BIN
src/assets/portal/icon-security.png View File

Before After
Width: 48  |  Height: 49  |  Size: 3.5 KiB

BIN
src/assets/portal/icon-total.png View File

Before After
Width: 48  |  Height: 49  |  Size: 3.5 KiB

BIN
src/assets/portal/icon-triangle.png View File

Before After
Width: 27  |  Height: 20  |  Size: 1.2 KiB

BIN
src/assets/portal/panel-title.png View File

Before After
Width: 932  |  Height: 56  |  Size: 85 KiB

BIN
src/assets/portal/tree-collapse.png View File

Before After
Width: 50  |  Height: 217  |  Size: 6.2 KiB

BIN
src/assets/portal/tree-toggle.png View File

Before After
Width: 50  |  Height: 217  |  Size: 5.6 KiB

+ 3
- 1
src/layout/components/AppMain.vue View File

@ -20,7 +20,9 @@ export default {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
//
const stationCode = this.$store.getters.workspaceStationCode || ''
return this.$route.path + (stationCode ? '||' + stationCode : '')
}
},
watch: {


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

@ -3,11 +3,17 @@
<div v-if="isXfdh" class="xfdh-title-art" :style="{ backgroundImage: `url(${xfdhTitleNavbar})` }" />
<div class="navbar-brand">
<img :src="isXfdh ? xfdhBrand : headerLogo" class="header-logo" alt="" />
<span class="platform-title">消防动环管理平台</span>
<span class="platform-title">动环智能巡检系统</span>
</div>
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav" />
<div v-else class="navbar-spacer" />
<!-- 当前站点上下文从门户跳转进入单站时显示 -->
<div v-if="inSite" class="site-context-tag">
当前站{{ siteName }}
<i class="el-icon-close" title="退出单站视图" @click="exitStation" />
</div>
<div class="right-menu">
<template v-if="device !== 'mobile'">
<!-- <search id="header-search" class="right-menu-item" />
@ -73,7 +79,7 @@ export default {
};
},
computed: {
...mapGetters(["name", "device"]),
...mapGetters(["name", "device", "inSite", "siteName"]),
topNav: {
get() {
return this.$store.state.settings.topNav;
@ -104,6 +110,10 @@ export default {
if (this.frameworkUserPoller) window.clearInterval(this.frameworkUserPoller)
},
methods: {
exitStation() {
this.$store.dispatch("siteContext/exitSite");
this.$router.push("/portal");
},
loadFrameworkUser() {
// (宿) name
const user = getEmbeddedUser();
@ -260,6 +270,29 @@ export default {
vertical-align: top;
}
.site-context-tag {
display: flex;
align-items: center;
gap: 6px;
margin-left: 12px;
margin-right: 10px;
padding: 4px 10px;
border-radius: 13px;
background: rgba(18, 215, 248, 0.12);
border: 1px solid rgba(18, 215, 248, 0.35);
font-size: 12px;
color: #7fe6f8;
white-space: nowrap;
.el-icon-close {
cursor: pointer;
&:hover {
color: #ffffff;
}
}
}
.right-menu {
height: 100%;
display: flex;


+ 5
- 2
src/layout/components/Sidebar/index.vue View File

@ -50,7 +50,7 @@ export default {
},
computed: {
...mapState(["settings"]),
...mapGetters(["sidebarRouters", "sidebar", "roles"]),
...mapGetters(["sidebarRouters", "sidebar", "roles", "fromPortal"]),
// admin
isAdminAccount() {
if ((this.roles || []).indexOf("admin") !== -1) return true;
@ -80,7 +80,10 @@ export default {
const isTaskManagement = ["巡检任务管理", "巡视任务管理"].includes(title);
const isImplementationPoints = !this.isAdminAccount && isImplementationPointsMenu(route);
const isTemporarilyHiddenRootMenu = depth === 0 && temporarilyHiddenRootMenus.has(title);
if (isTaskManagement || isImplementationPoints || isTemporarilyHiddenRootMenu) return result;
//
const isHistoryPlaybackHidden = this.fromPortal &&
["/video-playback", "/inspection-workspace/video-playback"].includes(route.path);
if (isTaskManagement || isImplementationPoints || isTemporarilyHiddenRootMenu || isHistoryPlaybackHidden) return result;
result.push({
...route,


+ 102
- 7
src/layout/index.vue View File

@ -11,6 +11,7 @@
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
<div class="layout-header" :class="{'fixed-header':fixedHeader}">
<navbar />
<!-- 横向导航条位置始终保留仅从汇聚门户跳转进入时显示导航按钮 -->
<div class="module-subbar">
<div class="module-links">
<button
@ -22,7 +23,23 @@
@click="goModule(item.path)"
>{{ item.label }}</button>
</div>
<!-- station selector removed -->
<!-- 汇聚态站名筛选接口与首页一致/agg/station/all默认全部场站 -->
<el-select
v-if="fromPortal"
v-model="stationCode"
class="module-station-select"
size="mini"
placeholder="全部场站"
clearable
>
<el-option label="全部场站" value="" />
<el-option
v-for="station in stationOptions"
:key="station.stationCode"
:label="station.stationName"
:value="station.stationCode"
/>
</el-select>
</div>
<tags-view v-if="needTagsView"/>
</div>
@ -38,8 +55,9 @@
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
import { mapState, mapGetters } from 'vuex'
import variables from '@/assets/styles/variables.scss'
import { getStationList } from '@/api/portal'
export default {
name: 'Layout',
@ -55,9 +73,8 @@ export default {
return {
// 0=160px1=200px2=240px3=280px
sidebarWidthLevel: 0,
moduleLinks: [
{ key: 'home', label: '', path: '/index' }
]
//
stationOptions: []
}
},
mixins: [ResizeMixin],
@ -85,14 +102,51 @@ export default {
isPatrolWorkspace() {
return this.$route.matched.some(route => route.meta && route.meta.patrolWorkspace)
},
// 1
sidebarWide() {
return this.sidebarWidthLevel >= 1
},
...mapGetters(['fromPortal', 'workspaceStationCode']),
// storesessionStorage
stationCode: {
get() {
return this.workspaceStationCode || ''
},
set(value) {
this.$store.dispatch('siteContext/setWorkspaceStation', value)
}
},
//
moduleLinks() {
if (!this.fromPortal) return []
return [
{ key: 'home', label: '首页', path: '/portal' },
{ key: 'patrol', label: '动环巡检', path: '/index' },
// { key: 'system', label: '', path: '/system/user' }
{ key: 'system', label: '系统管理', path: '' }
]
},
activeModule() {
return 'home'
if (this.$route.path.startsWith('/system')) return 'system'
return 'patrol'
},
variables() {
return variables;
}
},
mounted() {
if (this.fromPortal) this.loadStationOptions()
},
methods: {
//
loadStationOptions() {
getStationList({}).then(response => {
this.stationOptions = Array.isArray(response && response.data) ? response.data : []
}).catch(() => {
this.stationOptions = []
})
},
goModule(path) {
if (this.$route.path !== path) this.$router.push({ path }).catch(() => undefined)
},
@ -175,6 +229,34 @@ export default {
align-items: stretch;
}
/* 汇聚态站名筛选:靠行最右 */
.module-station-select {
width: 150px;
::v-deep .el-input__inner {
height: 28px;
line-height: 28px;
background: rgba(16, 30, 84, 0.65);
border-color: rgba(84, 130, 255, 0.35);
color: #eef4ff;
font-size: 12px;
&::placeholder {
color: rgba(159, 180, 232, 0.65);
}
}
::v-deep .el-input__suffix {
display: flex;
align-items: center;
height: 28px;
}
::v-deep .el-input__icon {
line-height: 28px;
}
}
.module-link {
position: relative;
min-width: 90px;
@ -217,10 +299,23 @@ export default {
font-size: 14px;
letter-spacing: 1px;
font-weight: 900;
background: transparent;
//
background: radial-gradient(ellipse 90% 78% at 50% 108%, rgba(63, 255, 254, 0.28), rgba(63, 255, 254, 0.08) 52%, transparent 76%);
box-shadow: none;
}
.module-link.active::after {
content: "";
position: absolute;
left: 14%;
right: 14%;
bottom: 0;
height: 2px;
border-radius: 2px;
background: linear-gradient(90deg, rgba(63, 255, 254, 0) 0%, #3ffef0 50%, rgba(63, 255, 254, 0) 100%);
box-shadow: 0 0 14px rgba(63, 255, 254, 0.85);
}
.fixed-header {
position: fixed;
top: 0;


+ 7
- 0
src/router/index.js View File

@ -30,6 +30,13 @@ import Layout from '@/layout'
// 公共路由
export const constantRoutes = [
{
// 汇聚门户首页:跨站点汇总大屏,独立于后台 Layout 的全屏页面
path: '/portal',
component: () => import('@/views/portal/index'),
hidden: true,
meta: { title: '动环智能巡检系统' }
},
{
path: '/xfdh',
component: Layout,


+ 5
- 0
src/store/getters.js View File

@ -17,5 +17,10 @@ const getters = {
sidebarRouters:state => state.permission.sidebarRouters,
language: state => state.app.language,
systemParams: state => state.app.systemParams,
siteId: state => state.siteContext.siteId,
siteName: state => state.siteContext.siteName,
inSite: state => state.siteContext.siteId !== undefined && state.siteContext.siteId !== null,
fromPortal: state => state.siteContext.fromPortal,
workspaceStationCode: state => state.siteContext.workspaceStationCode,
}
export default getters

+ 3
- 1
src/store/index.js View File

@ -6,6 +6,7 @@ import user from './modules/user'
import tagsView from './modules/tagsView'
import permission from './modules/permission'
import settings from './modules/settings'
import siteContext from './modules/siteContext'
import getters from './getters'
Vue.use(Vuex)
@ -17,7 +18,8 @@ const store = new Vuex.Store({
user,
tagsView,
permission,
settings
settings,
siteContext
},
getters
})


+ 86
- 0
src/store/modules/siteContext.js View File

@ -0,0 +1,86 @@
// 汇聚门户的站点上下文:
// 门户首页(汇聚视图)展示所有站的数据;从"场站系统入口"进入某个站后,
// siteContext 记录当前站,后续业务请求由 request 拦截器统一携带 siteId,
// 后端据此只返回该站的数据(接口未支持该参数前会自动忽略,行为不变)。
const SITE_KEY = "portal_site";
// 是否从汇聚门户跳转进入单站视图(会话级标记,直接输地址打开时不带)
const FROM_KEY = "portal_from";
// 汇聚门户工作台顶部"站名"下拉选中的站(会话级)
const WORKSPACE_STATION_KEY = "portal_ws_station";
function readSite() {
try {
const raw = sessionStorage.getItem(SITE_KEY);
return raw ? JSON.parse(raw) : null;
} catch (e) {
return null;
}
}
const siteContext = {
namespaced: true,
state: () => {
const site = readSite();
return {
siteId: site ? site.id : undefined,
siteName: site ? site.name : "",
fromPortal: sessionStorage.getItem(FROM_KEY) === "1",
workspaceStationCode: sessionStorage.getItem(WORKSPACE_STATION_KEY) || ""
};
},
mutations: {
SET_FROM_PORTAL(state, value) {
state.fromPortal = !!value;
if (value) {
sessionStorage.setItem(FROM_KEY, "1");
} else {
sessionStorage.removeItem(FROM_KEY);
}
},
SET_SITE(state, site) {
state.siteId = site && site.id !== undefined ? site.id : undefined;
state.siteName = (site && site.name) || "";
if (site && site.id !== undefined) {
sessionStorage.setItem(SITE_KEY, JSON.stringify({ id: site.id, name: site.name || "" }));
} else {
sessionStorage.removeItem(SITE_KEY);
}
},
SET_WORKSPACE_STATION(state, code) {
state.workspaceStationCode = code || "";
if (code) {
sessionStorage.setItem(WORKSPACE_STATION_KEY, code);
} else {
sessionStorage.removeItem(WORKSPACE_STATION_KEY);
}
}
},
actions: {
// 标记本次会话由门户跳转进入
markFromPortal({ commit }) {
commit("SET_FROM_PORTAL", true);
},
// 工作台顶部站名下拉选择
setWorkspaceStation({ commit }, code) {
commit("SET_WORKSPACE_STATION", code);
},
// 进入单站视图
enterSite({ commit }, site) {
commit("SET_SITE", site);
},
// 退出单站视图,回到汇聚门户
exitSite({ commit }) {
commit("SET_SITE", null);
}
},
getters: {
fromPortal: state => state.fromPortal,
siteId: state => state.siteId,
siteName: state => state.siteName,
workspaceStationCode: state => state.workspaceStationCode,
// 是否处于单站视图
inSite: state => state.siteId !== undefined && state.siteId !== null
}
};
export default siteContext;

+ 54
- 1
src/utils/request.js View File

@ -18,6 +18,39 @@ let downloadLoadingInstance;
export let isRelogin = { show: false };
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 汇聚门户(动环巡检)只读会话的接口改写映射:
// 从门户进入且未进入单站视图时,动环业务查询统一走后端聚合接口(agg)。
// key/value 不含查询参数,按 url(忽略首部斜杠)全等匹配,
// 因此 /relation/right、/patrol/insreport/{id}/{filter} 等衍生接口不会被误改。
const AGG_API_MAP = {
'patrol/taskstatus/list': 'agg/taskstatus/list',
'patrol/task/taskInfo_v2': 'agg/proxy/patrol/task/taskInfo_v2',
'patrol/task/getPatrolPointName': 'agg/proxy/patrol/task/getPatrolPointName',
'patrol/task/list': 'agg/task/list',
'basedata/prodevmnt/device/treeAreaDeviceSelectFilterByPreset/relation': 'patrol/agg/tree/device',
'basedata/patrolpointmnt/patrolpoint/list/monitor/relation': 'agg/patrolpoint/list/monitor',
'patrol/insreport/list': 'agg/insreport/list',
'patrol/calender/year': 'agg/calender/year',
'patrol/calender/month': 'agg/calender/month',
'patrol/calender/day': 'agg/calender/day',
'patrol/analysis/defect/list': 'agg/analysis/defect/list'
}
// 汇聚门户工作台顶部"站名"下拉:选中站后,下列查询接口统一携带 stationCode
// (前缀匹配;缺陷类型下拉选项 algNameOptions 也按选中站过滤)
const AGG_STATION_CODE_PATHS = [
'/agg/taskstatus/list',
'/agg/task/list',
'/agg/insreport/list',
'/agg/calender/',
'/agg/tree/channel',
'/patrol/agg/tree/device',
'/agg/patrolpoint/list/monitor',
'/agg/analysis/defect/list',
'/agg/analysis/defect/algNameOptions'
]
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
@ -28,6 +61,16 @@ const service = axios.create({
// request拦截器
service.interceptors.request.use(config => {
// 汇聚门户只读会话:动环业务接口改写为聚合接口(单站/嵌入会话不受影响)
if (store.getters.fromPortal && !store.getters.inSite && typeof config.url === "string") {
const mapped = AGG_API_MAP[config.url.replace(/^\/+/, "")]
if (mapped) config.url = "/" + mapped
// 工作台顶部站名下拉:选中站后给列表/树查询带上 stationCode("全部场站"不传)
const wsCode = store.getters.workspaceStationCode
if (wsCode && config.method === "get" && AGG_STATION_CODE_PATHS.some(p => config.url.indexOf(p) === 0)) {
config.params = Object.assign({}, config.params, { stationCode: wsCode })
}
}
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
@ -36,6 +79,16 @@ service.interceptors.request.use(config => {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
config.headers['ticket'] = store.getters.secondTicket;
// 站点上下文:从汇聚门户进入单站视图后,业务请求统一携带 siteId 供后端按站过滤。
// 汇总接口(/portal/*、/agg/*)本身面向全部站,不注入;接口未支持该参数的后端会自动忽略。
const siteId = store.getters.siteId;
if (siteId !== undefined && siteId !== null && !config.url.startsWith('/portal') && !config.url.startsWith('/agg/') && (config.headers || {}).noSiteId !== true) {
if (config.method === 'get') {
config.params = Object.assign({}, config.params, { siteId })
} else if (config.data && typeof config.data === 'object' && !(config.data instanceof FormData)) {
config.data = Object.assign({}, config.data, { siteId })
}
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
@ -79,7 +132,7 @@ service.interceptors.request.use(config => {
function LoginEntity() {
let username;
let password;
let password;
}


+ 2
- 2
src/utils/stationTitle.js View File

@ -2,7 +2,7 @@
// 后续接入新站点:把切图放到 src/assets/images/header/ 下,
// 在 stationConfigMap 里加一行 "<该站IP或域名>: { titleImage, name }" 即可;
// 同一 IP 不同端口是不同站点时,key 写成 "IP:端口"(如 "192.168.129.2:32799");
// 未匹配到的站点使用默认横幅和默认标题"消防动环管理平台"。
// 未匹配到的站点使用默认横幅和默认标题"动环智能巡检系统"。
import defaultTitleImage from "@/assets/images/header/xfdh-title-navbar.png";
import baiyangpingTitleImage from "@/assets/images/header/baiyangping-title.png";
import baicaopoTitleImage from "@/assets/images/header/baicaopo-title.png";
@ -29,7 +29,7 @@ const stationConfigMap = {
const defaultStationConfig = {
titleImage: defaultTitleImage,
// 默认站名(环境变量 VUE_APP_TITLE 是"智能巡检",与需求不符,故直接写死)
name: "消防动环管理平台",
name: "动环智能巡检系统",
};
export function getStationConfig() {


+ 91
- 84
src/views/cards/defectRecord/index.vue View File

@ -11,13 +11,11 @@
<span class="total-value">{{ total }}</span>
</div>
<el-select
v-model="defectTypeList"
v-model="defectType"
class="query-item query-type"
multiple
collapse-tags
filterable
clearable
placeholder="缺陷类型"
placeholder="算法类型"
@change="handleQuery"
>
<el-option
@ -77,7 +75,24 @@
align="center"
:index="table_index"
/>
<el-table-column
label="任务名称"
align="center"
prop="taskName"
min-width="140"
show-overflow-tooltip
/>
<el-table-column label="算法类型" align="center" prop="algName" />
<el-table-column label="区域" align="center" prop="areaName" />
<!-- 站名列汇聚门户聚合数据下展示 -->
<el-table-column
v-if="$store.getters.fromPortal"
label="站名"
align="center"
prop="stationName"
min-width="100"
show-overflow-tooltip
/>
<el-table-column label="点位" align="center" prop="pointName" />
<el-table-column label="采集时间" align="center" prop="patrolTime" />
<el-table-column label="缺陷照片" align="center">
@ -128,7 +143,7 @@
<script>
import CardBox from "../components/cardBox.vue";
import CardPagination from "../components/CardPagination/index.vue";
import { defectRecordList } from "@/api/videosMonitor/index";
import { defectRecordList, defectDescOptions } from "@/api/videosMonitor/index";
import ImagePreview from "@/views/cards/components/ImagePreview/index.vue";
import AsyncImage from "@/components/AsyncImage/index.vue";
export default {
@ -137,9 +152,8 @@ export default {
data() {
return {
queryParams: { pageNum: 1, pageSize: 20, pointName: "" },
defectTypeList: [],
allDataList: [], //
filteredList: [], //
defectType: "", //
defectTypeOptions: [], // descOptions
dataList: [],
loading: false,
total: 0,
@ -165,18 +179,9 @@ export default {
previewImagePos: 0,
};
},
computed: {
//
defectTypeOptions() {
const set = new Set();
this.allDataList.forEach((item) => {
const desc = (item.desc || "").trim();
if (desc) set.add(desc);
});
return Array.from(set).sort((a, b) => a.localeCompare(b, "zh"));
},
},
computed: {},
created() {
this.loadDescOptions();
this.loadData();
window.addEventListener("resize", this.setTableHeight);
},
@ -209,92 +214,97 @@ export default {
handleQuery() {
this.queryParams.pageNum = 1;
this.applyFilters();
this.loadData();
},
//
//
resetQuery() {
this.defectTypeList = [];
this.defectType = "";
this.queryParams.pointName = "";
this.dateRange = [];
this.handleQuery();
},
//
// algName
loadData() {
this.loading = true;
//
this.imageLoadCount = 0;
this.expectedImageCount = 0;
// 20
defectRecordList({ pageNum: 1, pageSize: 20 })
const params = {
pageNum: this.queryParams.pageNum,
pageSize: this.queryParams.pageSize,
pointName: this.queryParams.pointName || undefined,
algName: this.defectType || undefined,
beginTime:
this.dateRange && this.dateRange.length === 2
? this.dateRange[0]
: undefined,
endTime:
this.dateRange && this.dateRange.length === 2
? this.dateRange[1]
: undefined,
};
defectRecordList(params)
.then((res) => {
this.allDataList = res.rows || [];
this.applyFilters();
this.dataList = res.rows || [];
this.total = res.total || 0;
//
if (!this.defectTypeOptions.length) {
this.deriveDescOptions(this.dataList);
}
this.expectedImageCount = this.dataList.filter(
(item) => item.imgAnalyse
).length;
if (this.expectedImageCount === 0) {
this.forceRefreshTable();
}
})
.catch((error) => {
console.error("获取缺陷记录失败:", error);
this.allDataList = [];
this.applyFilters();
this.dataList = [];
this.total = 0;
this.expectedImageCount = 0;
this.forceRefreshTable();
})
.finally(() => {
this.loading = false;
//
this.$nextTick(this.setTableHeight);
});
},
//
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;
// descOptions algNameOptions
loadDescOptions() {
defectDescOptions()
.then((res) => {
let options = res && res.data;
if (!Array.isArray(options)) options = [];
this.defectTypeOptions = options
.map((item) =>
typeof item === "string"
? item
: (item &&
(item.algName || item.desc || item.name || item.label)) ||
""
)
.filter(Boolean)
.sort((a, b) => a.localeCompare(b, "zh"));
})
.catch(() => {
this.defectTypeOptions = [];
});
},
deriveDescOptions(rows) {
const set = new Set();
(rows || []).forEach((item) => {
const desc = (item.desc || "").trim();
if (desc) set.add(desc);
});
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.defectTypeOptions = Array.from(set).sort((a, b) =>
a.localeCompare(b, "zh")
);
//
this.expectedImageCount = this.dataList.filter(
(item) => item.imgAnalyse
).length;
//
if (this.expectedImageCount === 0) {
this.forceRefreshTable();
} else {
this.$nextTick(this.setTableHeight);
}
},
linkUrl() {
@ -357,12 +367,9 @@ export default {
},
//
//
getList() {
if (!this.allDataList.length) {
this.loadData();
return;
}
this.applyFilters();
this.loadData();
},
onSubmit() {


+ 18
- 1
src/views/cards/patrolPlan/notstart.vue View File

@ -75,7 +75,9 @@
@click="handleQuery"
>搜索</el-button
>
<!-- 添加方案按钮汇聚门户进入的只读视图下隐藏 -->
<el-button
v-if="!$store.getters.fromPortal"
class="task-toolbar-btn"
type="primary"
icon="el-icon-plus"
@ -109,6 +111,15 @@
:resizable="false"
/>
<el-table-column label="方案名称" align="left" prop="taskName" min-width="360" header-align="center" show-overflow-tooltip :resizable="true" />
<!-- 站名列汇聚门户聚合数据下展示 -->
<el-table-column
v-if="$store.getters.fromPortal"
label="站名"
align="center"
prop="stationName"
min-width="100"
show-overflow-tooltip
/>
<el-table-column label="点位数" align="center" prop="totalPoints" width="90" :resizable="true" />
<el-table-column label="时间" align="center" prop="createTime" width="170" show-overflow-tooltip :resizable="true" />
<el-table-column
@ -143,7 +154,13 @@
prop="taskCycle"
:formatter="stateTask"
/> -->
<el-table-column label="操作" align="center" width="150" :resizable="true">
<el-table-column
v-if="!$store.getters.fromPortal"
label="操作"
align="center"
width="150"
:resizable="true"
>
<template slot-scope="scope">
<el-tooltip content="立即执行" placement="bottom">
<i


+ 15
- 13
src/views/cards/patrolPlan/summary_editTask.vue View File

@ -284,21 +284,21 @@
/>
</div>
<!-- 点位类型 - 复选框组 -->
<!-- 点位类型 - 单选默认外观 -->
<div class="form-row checkbox-row">
<div class="form-label">点位类型 <span>*</span></div>
<div class="form-input checkbox-group-wrapper">
<el-checkbox-group
v-model="formData.pointTypeList"
<el-radio-group
v-model="formData.pointType"
@change="handlePointTypeChange"
>
<el-checkbox
<el-radio
v-for="item in pointTypeOptions"
:key="String(item.dictCode)"
:label="String(item.dictCode)"
>{{ item.dictLabel }}</el-checkbox
>{{ item.dictLabel }}</el-radio
>
</el-checkbox-group>
</el-radio-group>
</div>
</div>
@ -415,7 +415,7 @@ export default {
taskName: "",
priority: "1",
type: null,
pointTypeList: [],
pointType: "",
deviceTypeList: [],
executionMode: true,
executionType: "1",
@ -481,7 +481,7 @@ export default {
return findSpecialPatrol(this.listPatroltype);
},
submitPointType() {
return this.formData.pointTypeList.join(",");
return this.formData.pointType;
},
submitDeviceType() {
return this.formData.deviceTypeList.join(",");
@ -610,15 +610,17 @@ export default {
this.formData.isEnabled = data.isenable || "0";
if (data.pointType) {
this.formData.pointTypeList = data.pointType
//
const codes = String(data.pointType)
.split(",")
.filter((code) =>
this.pointTypeOptions.some(
(opt) => String(opt.dictCode) === String(code)
)
);
this.formData.pointType = codes.length ? codes[0] : "";
} else {
this.formData.pointTypeList = [];
this.formData.pointType = "";
}
if (data.deviceType) {
@ -653,8 +655,8 @@ export default {
if (this.formData.deviceTypeList.length) {
params.devTypeCodeList = this.formData.deviceTypeList;
}
if (this.formData.pointTypeList.length) {
params.algTypeCodeList = this.formData.pointTypeList;
if (this.formData.pointType) {
params.algTypeCodeList = [this.formData.pointType];
}
params.filterByPreset = true;
return treeAreaDeviceSelectFilterByPresetRight(params)
@ -966,7 +968,7 @@ export default {
this.$modal.msgError("请选择任务类型");
return false;
}
if (this.formData.pointTypeList.length === 0) {
if (!this.formData.pointType) {
this.$modal.msgError("请选择点位类型");
return false;
}


+ 20
- 14
src/views/cards/patrolPlan/summary_newTask.vue View File

@ -282,21 +282,21 @@
/>
</div>
<!-- 点位类型 - 复选框组 -->
<!-- 点位类型 - 单选默认外观 -->
<div class="form-row checkbox-row">
<div class="form-label">点位类型 <span>*</span></div>
<div class="form-input checkbox-group-wrapper">
<el-checkbox-group
v-model="pointTypeList"
<el-radio-group
v-model="pointType"
@change="handlePointTypeChange"
>
<el-checkbox
<el-radio
v-for="item in pointTypeOptions"
:key="item.dictCode"
:label="item.dictCode"
>{{ item.dictLabel }}</el-checkbox
>{{ item.dictLabel }}</el-radio
>
</el-checkbox-group>
</el-radio-group>
</div>
</div>
@ -370,7 +370,7 @@ export default {
// "0"
isEnabled: "0",
pointTypeOptions: [],
pointTypeList: [],
pointType: "",
deviceTypeOptions: [],
deviceTypeList: [],
skipWatch: false, // watch
@ -467,7 +467,7 @@ export default {
return findSpecialPatrol(this.listPatroltype);
},
submitPointType() {
return this.pointTypeList.join(",");
return this.pointType;
},
submitDeviceType() {
return this.deviceTypeList.join(",");
@ -513,9 +513,15 @@ export default {
//
this.pointTypeOptions = pointTypeRes.rows || [];
this.pointTypeList = (pointTypeRes.rows || []).map(
(item) => item.dictCode
// ""退
const appearance = this.pointTypeOptions.find(
(item) => item.dictLabel === "外观"
);
this.pointType = appearance
? appearance.dictCode
: this.pointTypeOptions.length
? this.pointTypeOptions[0].dictCode
: "";
this.deviceTypeOptions = deviceTypeRes.rows || [];
this.deviceTypeList = (deviceTypeRes.rows || []).map(
@ -542,9 +548,9 @@ export default {
params.devTypeCodeList = this.deviceTypeList;
}
//
if (this.pointTypeList && this.pointTypeList.length > 0) {
params.algTypeCodeList = this.pointTypeList;
//
if (this.pointType) {
params.algTypeCodeList = [this.pointType];
}
params.filterByPreset = true
treeAreaDeviceSelectFilterByPresetRight(params)
@ -806,7 +812,7 @@ export default {
this.$modal.msgError("请选择任务类型");
return false;
}
if (this.pointTypeList.length === 0) {
if (!this.pointType) {
this.$modal.msgError("请选择点位类型");
return false;
}


+ 12
- 0
src/views/cards/patrolReport/InspectionReporter.vue View File

@ -78,6 +78,15 @@
width="620"
header-align="center"
/>
<!-- 站名列汇聚门户聚合数据下展示 -->
<el-table-column
v-if="$store.getters.fromPortal"
label="站名"
align="center"
prop="stationName"
min-width="100"
show-overflow-tooltip
/>
<el-table-column
label="巡检开始时间"
align="center"
@ -343,6 +352,8 @@ export default {
sessionStorage.setItem("reportLineId", JSON.stringify(info.lineId));
sessionStorage.setItem("reportType", JSON.stringify(info.filter));
//
sessionStorage.setItem("reportStationCode", info.stationCode || "");
this.$router.push({
name: this.$route.meta.patrolWorkspace
? "InspectionWorkspaceReportDetail"
@ -351,6 +362,7 @@ export default {
lineId: info.lineId,
filter: info.filter,
reportType: info.filter,
stationCode: info.stationCode || undefined,
fromPage: "InspectionReporter",
fromComponent: "InspectionReporter",
},


+ 7
- 1
src/views/cards/patrolReport/patrolReportDetail.vue View File

@ -219,6 +219,7 @@ export default {
return {
lineId: null,
reportType: 0,
stationCode: "",
showPreviewImages: false,
showImgesUrls: [],
previewImagePos: 0,
@ -257,6 +258,11 @@ export default {
const savedReportType = sessionStorage.getItem("reportType");
this.reportType = savedReportType ? JSON.parse(savedReportType) : 0;
}
// stationCode
this.stationCode =
this.$route.query.stationCode ||
sessionStorage.getItem("reportStationCode") ||
"";
this.getDetail();
if (
@ -281,7 +287,7 @@ export default {
} else {
url = `report`;
}
getPatrolReportShaoxing(url, this.lineId, this.reportType || 0).then((res) => {
getPatrolReportShaoxing(url, this.lineId, this.reportType || 0, this.stationCode).then((res) => {
// Some deployments return detail as a JSON string; normalize it before grouping.
const report = res && res.data ? res.data : res;
if (report && typeof report.detail === "string") {


+ 46
- 6
src/views/cards/site_management/site_management.vue View File

@ -70,7 +70,9 @@
<div class="form" style="padding-top: 10px" ref="form">
<div class="line">
<div class="left-buttons">
<!-- 添加点位按钮汇聚门户进入的只读视图下隐藏 -->
<el-button
v-if="!$store.getters.fromPortal"
type="primary"
class="task-toolbar-btn"
@click="addAlertShow1"
@ -83,8 +85,9 @@
icon="el-icon-plus"
>添加</el-button
>
<!-- 批量删除模式切换按钮 -->
<!-- 批量删除模式切换按钮汇聚门户进入的只读视图下隐藏 -->
<el-button
v-if="!$store.getters.fromPortal"
class="task-toolbar-btn"
:type="batchDeleteMode ? 'info' : 'danger'"
@click="toggleBatchDeleteMode"
@ -95,7 +98,7 @@
</el-button>
<!-- 确认删除按钮 - 仅在批量删除模式下显示 -->
<el-button
v-if="batchDeleteMode"
v-if="batchDeleteMode && !$store.getters.fromPortal"
type="danger"
class="task-toolbar-btn"
@click="confirmBatchDelete"
@ -215,6 +218,7 @@
</template>
</el-table-column>
<el-table-column
prop="patrolpointName"
header-align="center"
@ -225,7 +229,13 @@
<span>点位名称 <span style="color: red">*</span> </span>
</template>
<template slot-scope="scope">
<!-- 汇聚门户只读视图点位名称展示纯文本不允许修改 -->
<span
v-if="$store.getters.fromPortal"
class="point-name-text"
>{{ scope.row.patrolpointName || "-" }}</span>
<el-input
v-else
v-model="scope.row.patrolpointName"
@change="handleChangePointCode(scope.row)"
@input="handlePatrolpointNameInput($event, scope.row)"
@ -235,6 +245,17 @@
></el-input>
</template>
</el-table-column>
<!-- 站名列汇聚门户聚合数据下展示 -->
<el-table-column
v-if="$store.getters.fromPortal"
label="站名"
align="center"
header-align="center"
class-name="station-name-col"
prop="stationName"
min-width="100"
>
</el-table-column>
<el-table-column
prop="algSubtypeName"
@ -247,9 +268,9 @@
:class="[
'nowarp',
'algorithm-setting-link',
{ 'is-disabled': batchDeleteMode },
{ 'is-disabled': batchDeleteMode || $store.getters.fromPortal },
]"
@click="!batchDeleteMode && chooseAlgorithm(scope.row)"
@click="!batchDeleteMode && !$store.getters.fromPortal && chooseAlgorithm(scope.row)"
:title="scope.row.algSubtypeName"
>
{{
@ -258,8 +279,9 @@
</a>
</template>
</el-table-column>
<!-- 新增设备类型列 -->
<el-table-column
v-if="!$store.getters.fromPortal"
prop="pointTypeName"
header-align="center"
align="center"
@ -271,6 +293,7 @@
</template>
</el-table-column>
<el-table-column
v-if="!$store.getters.fromPortal"
align="center"
prop="patrolContent"
header-align="center"
@ -312,7 +335,12 @@
</el-select>
</template>
</el-table-column> -->
<el-table-column label="操作" align="center" width="150">
<el-table-column
v-if="!$store.getters.fromPortal"
label="操作"
align="center"
width="150"
>
<template slot-scope="scope">
<el-button
class="task-row-btn"
@ -488,7 +516,9 @@
<el-tabs v-model="activeTab" :stretch="true" class="right-tabs">
<el-tab-pane label="视频设备预置点" name="first">
<div class="eq-a2">
<!-- 绑定入口汇聚门户进入的只读视图下隐藏 -->
<el-button
v-if="!$store.getters.fromPortal"
class="task-toolbar-btn"
@click="visibleShow1"
title="一个监控点位只允许绑定一个预置点"
@ -498,6 +528,7 @@
>绑定可见光监测点</el-button
>
<el-button
v-if="!$store.getters.fromPortal"
class="task-toolbar-btn"
@click="infraredShow1"
title="一个监控点位只允许绑定一个预置点"
@ -539,6 +570,7 @@
show-overflow-tooltip
></el-table-column>
<el-table-column
v-if="!$store.getters.fromPortal"
label="操作"
width="150"
fixed="right"
@ -832,6 +864,7 @@
>
</div>
<img
v-if="!$store.getters.fromPortal"
slot="reference"
class="alert1_img1"
src="@/assets/images/r7.png"
@ -845,6 +878,7 @@
placeholder="请输入内容"
></el-input>
<img
v-if="!$store.getters.fromPortal"
class="alert1_img1"
@click="editPresetDot1"
src="@/assets/images/r8.png"
@ -4197,6 +4231,12 @@ export default {
white-space: nowrap;
}
/* 站名列强制居中(覆盖可能的对齐/宽度冲突样式) */
::v-deep .el-table th.station-name-col .cell,
::v-deep .el-table td.station-name-col .cell {
text-align: center !important;
}
::v-deep .el-table__row td.operation-column .cell,
::v-deep .el-table__fixed-right td.operation-column .cell {
overflow: visible;


+ 19
- 3
src/views/cards/taskSummary/inspectionTask.vue View File

@ -84,8 +84,9 @@
>下一天
<i class="el-icon-arrow-right"></i>
</el-button>
<!-- 添加批量归档按钮 -->
<!-- 添加批量归档按钮汇聚门户进入的只读视图下隐藏 -->
<el-button
v-if="!$store.getters.fromPortal"
class="task-toolbar-btn task-toolbar-btn-archive"
type="primary"
icon="el-icon-folder-add"
@ -115,8 +116,9 @@
@selection-change="handleSelectionChange"
@header-dragend="handleHeaderDragend"
>
<!-- 添加复选框列 -->
<!-- 添加复选框列汇聚门户进入的只读视图下隐藏 -->
<el-table-column
v-if="!$store.getters.fromPortal"
type="selection"
width="55"
align="center"
@ -145,6 +147,15 @@
</el-tooltip>
</template>
</el-table-column>
<!-- 站名列汇聚门户聚合数据下展示 -->
<el-table-column
v-if="$store.getters.fromPortal"
label="站名"
align="center"
prop="stationName"
min-width="100"
show-overflow-tooltip
/>
<el-table-column
label="执行时间"
align="center"
@ -240,7 +251,12 @@
</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="140">
<el-table-column
v-if="!$store.getters.fromPortal"
label="操作"
align="center"
min-width="140"
>
<template slot-scope="scope">
<el-button
class="task-row-btn"


+ 3
- 1
src/views/cards/taskSummary/inspectionTaskDefectDetail.vue View File

@ -112,13 +112,15 @@ export default {
}
this.queryParams.taskPatrolledId = this.taskInfo.taskPatrolledId;
this.queryParams.filter = this.taskInfo.defectType;
// stationCode
this.queryParams.stationCode = this.taskInfo.stationCode;
this.getList();
},
beforeDestroy() {},
methods: {
//
getList() {
analysisDefectListShaoxing(this.queryParams).then((res) => {
analysisDefectListShaoxing(this.queryParams, this.queryParams.stationCode).then((res) => {
this.dataList = res.rows;
this.total = res.total;
});


+ 3
- 0
src/views/cards/taskSummary/newInspectionTaskDetail.vue View File

@ -550,6 +550,8 @@ export default {
this.queryParams.taskPatrolledId = this.taskInfo.taskPatrolledId;
this.queryParams.posType = this.taskInfo.posType;
this.queryParams.lineId = this.taskInfo.lineId;
// stationCode axios
this.queryParams.stationCode = this.taskInfo.stationCode;
this.getPointList();
this.initBaseInfo();
},
@ -742,6 +744,7 @@ export default {
taskPatrolledId: this.queryParams.taskPatrolledId,
posType: this.queryParams.posType,
lineId: this.queryParams.lineId,
stationCode: this.queryParams.stationCode,
};
getPatrolPointName(params).then((res) => {
this.searchSourceData.listPointName = res.data?.listPointName || [];


+ 1
- 1
src/views/login.vue View File

@ -1,7 +1,7 @@
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">消防动环管理系统</h3>
<h3 class="title">动环智能巡检系统</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"


+ 85
- 0
src/views/portal/components/DefectTrendChart.vue View File

@ -0,0 +1,85 @@
<template>
<div ref="chartEl" class="defect-trend-chart" />
</template>
<script>
import * as echarts from "echarts";
import resizeMixin from "@/views/dashboard/mixins/resize";
// ///
const SERIES_STYLE = {
轻微缺陷: "#2f7bff",
一般缺陷: "#3ad07a",
严重缺陷: "#f0c14a",
重大缺陷: "#ef5b5b"
};
export default {
name: "DefectTrendChart",
mixins: [resizeMixin],
props: {
months: { type: Array, default: () => [] },
series: { type: Array, default: () => [] }
},
data() {
return { chart: null };
},
computed: {
option() {
return {
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" },
backgroundColor: "rgba(13,22,72,.92)",
borderColor: "rgba(84,130,255,.4)",
textStyle: { color: "#dce7ff", fontSize: 12 }
},
legend: { show: false },
grid: { left: 8, right: 12, top: 12, bottom: 4, containLabel: true },
xAxis: {
type: "category",
data: this.months,
axisLine: { lineStyle: { color: "rgba(84,130,255,.35)" } },
axisTick: { show: false },
axisLabel: { interval: 0, color: "#9fb4e8", fontSize: 11 }
},
yAxis: {
type: "value",
splitLine: { lineStyle: { color: "rgba(84,130,255,.14)", type: "dashed" } },
axisLabel: { color: "#9fb4e8", fontSize: 11 }
},
series: this.series.map(item => ({
name: item.name,
type: "bar",
stack: "total",
barMaxWidth: 28,
itemStyle: { color: SERIES_STYLE[item.name] || "#4d8bff" },
data: item.data
}))
};
}
},
mounted() {
this.chart = echarts.init(this.$refs.chartEl);
this.chart.setOption(this.option);
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
},
watch: {
option() {
if (this.chart) this.chart.setOption(this.option, true);
}
}
};
</script>
<style scoped>
.defect-trend-chart {
width: 100%;
height: 100%;
}
</style>

+ 320
- 0
src/views/portal/components/PortalDeviceTree.vue View File

@ -0,0 +1,320 @@
<template>
<aside class="portal-tree">
<div class="tree-title">
<span><i class="el-icon-s-platform" /> 设备列表</span>
<strong>{{ channelNodes.length }}</strong>
</div>
<div class="tree-filters">
<el-select v-model="onlineStatus" size="small" placeholder="状态" clearable>
<el-option label="全部" value="" />
<el-option label="在线" value="1" />
<el-option label="离线" value="0" />
</el-select>
<el-input
v-model.trim="keywords"
size="small"
clearable
prefix-icon="el-icon-search"
placeholder="搜索"
@input="refreshTree"
/>
<el-button size="small" icon="el-icon-refresh" title="刷新列表" @click="loadTree" />
</div>
<div v-loading="internalLoading" class="tree-content">
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
node-key="id"
highlight-current
:default-expanded-keys="expandedKeys"
@node-click="handleNodeClick"
>
<el-tooltip
slot-scope="{ data }"
:content="data.label || data.name || ''"
placement="top-start"
:offset="6"
:open-delay="300"
:append-to-body="true"
popper-class="portal-tree-tooltip"
effect="dark"
>
<span class="custom-tree-node">
<i :class="nodeIcon(data)" />
<span class="node-label">{{ data.label || data.name }}</span>
</span>
</el-tooltip>
</el-tree>
<div v-if="!internalLoading && !treeData.length" class="tree-empty">
<i class="el-icon-video-camera-solid" />
<span>没有匹配的设备通道</span>
</div>
</div>
</aside>
</template>
<script>
import { getChannelTree } from "@/api/portal";
function flatten(nodes, result = []) {
(nodes || []).forEach(node => {
result.push(node);
if (Array.isArray(node.children)) flatten(node.children, result);
});
return result;
}
function filterTree(nodes, keyword, onlineStatus) {
return (nodes || []).reduce((result, node) => {
const children = filterTree(node.children, keyword, onlineStatus);
const text = `${node.label || ""} ${node.name || ""} ${node.stationName || ""} ${node.patroldeviceCode || ""} ${node.channelCode || ""}`.toLowerCase();
const matchesKeyword = !keyword || text.includes(keyword.toLowerCase());
const matchesStatus = node.areaFlag === "Yes" || !onlineStatus || String(node.patroldeviceOnlineStatus || "") === onlineStatus;
if ((matchesKeyword && matchesStatus) || children.length) result.push({ ...node, children });
return result;
}, []);
}
export default {
name: "PortalDeviceTree",
props: {
selectedCode: { type: String, default: "" }
},
data() {
return {
treeNodes: [],
treeData: [],
expandedKeys: [],
keywords: "",
onlineStatus: "1",
internalLoading: false,
treeProps: { children: "children", label: "label" }
};
},
computed: {
channelNodes() {
return flatten(this.treeNodes).filter(node => node.channelFlag);
}
},
watch: {
onlineStatus: "refreshTree",
selectedCode(value) {
const node = this.channelNodes.find(item => this.toCameraCode(item) === value);
if (node && this.$refs.treeRef) this.$refs.treeRef.setCurrentKey(node.id);
}
},
mounted() {
this.loadTree();
},
methods: {
async loadTree() {
this.internalLoading = true;
try {
const response = await getChannelTree({ onlineStatus: this.onlineStatus, patroldeviceTypeFlag: "ipc" });
this.treeNodes = Array.isArray(response && response.data) ? response.data : [];
this.expandedKeys = [];
const walk = nodes => (nodes || []).forEach(node => {
if (node.children && node.children.length) {
this.expandedKeys.push(node.id);
walk(node.children);
}
});
walk(this.treeNodes);
this.refreshTree();
this.$emit("channels-loaded", this.channelNodes.map(this.normalizeChannel));
} catch (error) {
this.treeNodes = [];
this.treeData = [];
this.$message.error(error.message || "设备树加载失败");
} finally {
this.internalLoading = false;
}
},
refreshTree() {
this.treeData = filterTree(this.treeNodes, this.keywords, this.onlineStatus);
},
toCameraCode(node) {
return node.cameraCode || node.channelCode || "";
},
normalizeChannel(node) {
return {
...node,
cameraCode: this.toCameraCode(node),
cameraName: node.channelName || node.label || node.name,
name: node.channelName || node.label || node.name,
channel: node.channelCode
};
},
handleNodeClick(node) {
if (!node.channelFlag) return;
this.$emit("open", this.normalizeChannel(node));
},
nodeIcon(node) {
if (node.channelFlag) return "el-icon-video-camera";
if (node.patroldeviceType === 13) return "el-icon-position";
if (node.areaFlag === "Yes") return "el-icon-location-outline";
return "el-icon-connection";
}
}
};
</script>
<style lang="scss" scoped>
.portal-tree {
width: 100%;
min-width: 260px;
height: 100%;
display: flex;
flex-direction: column;
border: 1px solid rgba(63, 127, 216, 0.72);
border-radius: 2px;
background: transparent;
color: #d7e9ff;
box-sizing: border-box;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif !important;
}
.tree-title {
height: 46px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(41, 139, 255, 0.35);
color: #54ffff;
font-size: 14px;
}
.tree-title i {
margin-right: 6px;
}
.tree-title strong {
min-width: 28px;
text-align: center;
color: #b7d8ff;
}
.tree-filters {
display: flex;
gap: 6px;
padding: 10px 8px;
}
.tree-filters .el-select {
width: 78px;
}
.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-content {
flex: 1;
min-height: 0;
overflow: auto;
padding: 8px;
scrollbar-width: thin;
scrollbar-color: #2f7fe8 rgba(6, 27, 79, 0.7);
}
.tree-content::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.tree-content::-webkit-scrollbar-track {
background: rgba(6, 27, 79, 0.7);
}
.tree-content::-webkit-scrollbar-thumb {
background: #2f7fe8;
border: 1px solid #0d327d;
border-radius: 4px;
}
.custom-tree-node {
display: flex;
align-items: center;
width: max-content;
min-width: 100%;
gap: 6px;
font-size: 14px;
white-space: nowrap;
}
.node-label {
white-space: nowrap;
}
.tree-empty {
padding: 40px 0;
text-align: center;
color: #7298c3;
}
.tree-empty i {
display: block;
margin-bottom: 10px;
font-size: 28px;
}
::v-deep .el-tree {
display: inline-block;
width: max-content;
min-width: 100%;
background: transparent;
color: inherit;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif !important;
font-size: 14px;
}
::v-deep .el-tree-node__content,
::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;
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;
}
</style>
<style lang="scss">
.portal-tree-tooltip.el-tooltip__popper {
color: #eaf6ff;
background: #176bc2;
border: 1px solid #4ba3ff;
}
.portal-tree-tooltip.el-tooltip__popper[x-placement^="top"] .popper__arrow {
border-top-color: #176bc2;
}
</style>

+ 1987
- 0
src/views/portal/index.vue
File diff suppressed because it is too large
View File


+ 1
- 1
src/views/register.vue View File

@ -1,7 +1,7 @@
<template>
<div class="register">
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">消防动环管理系统</h3>
<h3 class="title">动环智能巡检系统</h3>
<el-form-item prop="username">
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />


+ 76
- 9
src/views/video/LivePreview.vue View File

@ -7,6 +7,7 @@
:channels="channels"
:selected-code="selectedCode"
:loading="loading"
:use-agg-api="isAggMode"
@channels-loaded="handleTreeChannels"
@select="selectChannel"
@open="openChannel"
@ -171,6 +172,7 @@ import {
getNvrConfig, startLive, getLiveStatus, stopLive, controlPtz,
getPresetList, createPreset, createSnapshot, downloadSnapshot, controlWiper
} from '@/api/nvrVideo'
import { startRealtimeVideo, stopRealtimeVideo, aggPtzControl, aggPresetList, aggPresetCreate } from '@/api/portal'
const PTZ_CODES = {
UP: 2, DOWN: 3, LEFT: 4, UP_LEFT: 5, DOWN_LEFT: 6,
@ -256,6 +258,8 @@ export default {
}
},
computed: {
//
isAggMode() { return this.$store.getters.fromPortal },
capacity() { return this.layout },
visibleSlots() { return this.slots.slice(0, this.capacity) },
activeSlot() { return this.slots[this.activeIndex] || null },
@ -330,6 +334,13 @@ export default {
this.loading = true
this.connectionText = '读取配置'
try {
if (this.isAggMode) {
// NVR
this.config = {}
this.connectionText = '汇聚模式'
this.connectionState = 'online'
return
}
const config = await getNvrConfig()
this.config = config || {}
// Keep config as metadata only until the device tree has supplied the
@ -446,12 +457,20 @@ export default {
this.presets = []
this.presetId = null
try {
slot.startPromise = startLive(channel, streamType)
// hls/flvapplyLiveResult
slot.startPromise = this.isAggMode
? startRealtimeVideo({ stationCode: channel.stationCode, cameraCode: channel.cameraCode })
: startLive(channel, streamType)
const result = await slot.startPromise
if (this.slots[index] !== slot) return
this.applyLiveResult(index, slot, result)
if (!slot.streamSource.hlsUrl && !slot.streamSource.flvUrl) {
this.pollLive(index, slot, 0)
if (this.isAggMode) {
slot.loading = false
this.$message.error('视频流启动失败,请稍后重试')
} else {
this.pollLive(index, slot, 0)
}
return
}
if (index === this.activeIndex) this.loadPresets(true)
@ -507,7 +526,13 @@ export default {
if (slot.startPromise) {
try { await slot.startPromise } catch (_) { /* no stream session was created */ }
}
try { await stopLive(slot.cameraCode) } catch (_) { /* release the UI slot first */ }
try {
if (this.isAggMode && slot.stationCode) {
await stopRealtimeVideo({ stationCode: slot.stationCode, cameraCode: slot.cameraCode })
} else {
await stopLive(slot.cameraCode)
}
} catch (_) { /* release the UI slot first */ }
if (notify) this.$message.success('视频通道已关闭')
},
closeAll() {
@ -572,7 +597,13 @@ export default {
}
}
this.$set(this.slots, index, null)
try { await stopLive(failedSlot.cameraCode) } catch (_) { /* continue switching cameras */ }
try {
if (this.isAggMode && failedSlot.stationCode) {
await stopRealtimeVideo({ stationCode: failedSlot.stationCode, cameraCode: failedSlot.cameraCode })
} else {
await stopLive(failedSlot.cameraCode)
}
} catch (_) { /* continue switching cameras */ }
if (!this.pageActive) return
if (nextChannel) {
await this.startChannel(nextChannel, index, Array.from(attemptedCodes))
@ -611,10 +642,23 @@ export default {
this.$router.push({ name: 'VideoHistoryPlayback', query: { cameraCode: slot.cameraCode }})
},
async sendPtz(controlCode, controlPara1 = '', controlPara2 = '', cameraCode = '') {
const targetCode = cameraCode || (this.controlTarget && this.controlTarget.cameraCode)
const target = cameraCode
? this.slots.find(slot => slot && slot.cameraCode === cameraCode) || this.controlTarget
: this.controlTarget
const targetCode = (target && target.cameraCode) || cameraCode
if (!targetCode) { this.$message.warning('请先选择有视频的画面'); return }
const result = await controlPtz({ cameraCode: targetCode, controlCode, controlPara1, controlPara2 })
if (!result || Number(result.resultCode) !== 0) throw new Error('云台控制失败')
let result
if (this.isAggMode) {
// stationCode
const stationCode = target && target.stationCode
if (!stationCode) { this.$message.warning('未获取到该摄像头所属站点信息'); return }
result = await aggPtzControl({ stationCode, cameraCode: targetCode, controlCode, controlPara1, controlPara2 })
} else {
result = await controlPtz({ cameraCode: targetCode, controlCode, controlPara1, controlPara2 })
}
// dataresultCode 0
const body = result && Object.prototype.hasOwnProperty.call(result, 'data') && result.data ? result.data : result
if (body && body.resultCode !== undefined && Number(body.resultCode) !== 0) throw new Error('云台控制失败')
},
startPtz(action) {
if (action === 'STOP') {
@ -651,7 +695,17 @@ export default {
const cameraCode = this.controlTarget && this.controlTarget.cameraCode
if (!cameraCode) return
try {
const result = await getPresetList(cameraCode)
let result
if (this.isAggMode) {
// stationCode
const stationCode = this.controlTarget && this.controlTarget.stationCode
if (!stationCode) { this.$message.warning('未获取到该摄像头所属站点信息'); return }
result = await aggPresetList({ stationCode, cameraCode })
} else {
result = await getPresetList(cameraCode)
}
// data
result = result && Object.prototype.hasOwnProperty.call(result, 'data') && result.data ? result.data : result
if (!this.controlTarget || this.controlTarget.cameraCode !== cameraCode) return
const source = result && result.ptzPresetInfoList && result.ptzPresetInfoList.ptzPresetInfo
this.presets = (Array.isArray(source) ? source : []).map(item => ({ id: Number(item.presetIndex), name: item.presetName || `预置位 ${item.presetIndex}` })).filter(item => item.id > 0)
@ -687,7 +741,20 @@ export default {
}
try {
await this.$confirm(`确认保存预置位 ${this.newPresetId}${this.newPresetName ? ` · ${this.newPresetName}` : ''}`, '保存预置位')
await createPreset({ cameraCode: channel.cameraCode, nvrIp: channel.nvrIp, channel: Number(channel.channel), preset: this.newPresetId, presetName: this.newPresetName || null })
if (this.isAggMode) {
// stationCode
if (!channel.stationCode) { this.$message.warning('未获取到该摄像头所属站点信息'); return }
await aggPresetCreate({
stationCode: channel.stationCode,
cameraCode: channel.cameraCode,
nvrIp: channel.nvrIp,
channel: Number(channel.channel),
preset: this.newPresetId,
presetName: this.newPresetName || null
})
} else {
await createPreset({ cameraCode: channel.cameraCode, nvrIp: channel.nvrIp, channel: Number(channel.channel), preset: this.newPresetId, presetName: this.newPresetName || null })
}
await this.loadPresets(true)
this.$message.success('预置位保存成功')
} catch (error) { if (error !== 'cancel' && error !== 'close') this.$message.error(error.message) }


+ 8
- 2
src/views/video/components/NvrDeviceTree.vue View File

@ -56,6 +56,7 @@
<script>
import { getAreaEqubookTreeSelect } from '@/api/videosMonitor/index'
import { getChannelTree } from '@/api/portal'
function flatten(nodes, result = []) {
const source = nodes || []
@ -84,7 +85,9 @@ export default {
channels: { type: Array, default: () => [] },
selectedCode: { type: String, default: '' },
loading: { type: Boolean, default: false },
openHint: { type: String, default: '单击通道打开实时预览' }
openHint: { type: String, default: '单击通道打开实时预览' },
// /agg/tree/channel stationCode
useAggApi: { type: Boolean, default: false }
},
data() {
return {
@ -113,7 +116,10 @@ export default {
async loadTree() {
this.internalLoading = true
try {
const response = await getAreaEqubookTreeSelect({ patroldeviceName: '', onlineStatus: '', patroldeviceTypeFlag: 'ipc' })
const queryParams = { patroldeviceName: '', onlineStatus: '', patroldeviceTypeFlag: 'ipc' }
const response = this.useAggApi
? await getChannelTree(queryParams)
: await getAreaEqubookTreeSelect(queryParams)
this.treeNodes = Array.isArray(response.data) ? response.data : []
this.expandedKeys = []
const walk = nodes => (nodes || []).forEach(node => { if (node.children && node.children.length) { this.expandedKeys.push(node.id); walk(node.children) } })


Loading…
Cancel
Save