Browse Source

修改汇聚的交互,汇聚下csv的文件,修改接口会给浏览器卡住问题,修改汇聚下点位管理里面接口报错问题

inspection_yanyuan
douyage 1 month ago
parent
commit
2f1db1ec94
15 changed files with 333 additions and 78 deletions
  1. +10
    -6
      src/api/nvrVideo.js
  2. +35
    -17
      src/api/portal/index.js
  3. +2
    -1
      src/api/videosMonitor/index.js
  4. +18
    -3
      src/layout/index.vue
  5. +4
    -2
      src/permission.js
  6. +2
    -0
      src/store/getters.js
  7. +8
    -0
      src/utils/image-url.js
  8. +11
    -2
      src/utils/request.js
  9. +60
    -0
      src/utils/videoRequestLimit.js
  10. +10
    -1
      src/views/cards/components/ImagePreviewZoomTemperature/index.vue
  11. +13
    -10
      src/views/cards/site_management/site_management.vue
  12. +20
    -3
      src/views/cards/taskSummary/inspectionTask.vue
  13. +37
    -6
      src/views/portal/components/PortalDeviceTree.vue
  14. +91
    -22
      src/views/portal/index.vue
  15. +12
    -5
      src/views/video/LivePreview.vue

+ 10
- 6
src/api/nvrVideo.js View File

@ -1,4 +1,5 @@
import request from '@/utils/request' import request from '@/utils/request'
import { limitVideoRequest } from '@/utils/videoRequestLimit'
import axios from 'axios' import axios from 'axios'
const ivsRequest = axios.create({ const ivsRequest = axios.create({
@ -29,30 +30,33 @@ export function getNvrConfig() {
} }
export function startLive(channel, streamType = 1) { export function startLive(channel, streamType = 1) {
return unwrap(request({
return unwrap(limitVideoRequest(() => request({
url: '/patrol/video/realtime/start', url: '/patrol/video/realtime/start',
method: 'post', method: 'post',
timeout: 8000,
data: { data: {
cameraCode: channel.cameraCode || channel.channelCode, cameraCode: channel.cameraCode || channel.channelCode,
streamType streamType
} }
}))
})))
} }
export function getLiveStatus(cameraCode) { export function getLiveStatus(cameraCode) {
return unwrap(request({
return unwrap(limitVideoRequest(() => request({
url: '/patrol/video/realtime/status', url: '/patrol/video/realtime/status',
method: 'get', method: 'get',
timeout: 8000,
params: { cameraCode } params: { cameraCode }
}))
})))
} }
export function stopLive(cameraCode) { export function stopLive(cameraCode) {
return unwrap(request({
return unwrap(limitVideoRequest(() => request({
url: '/patrol/video/realtime/stop', url: '/patrol/video/realtime/stop',
method: 'post', method: 'post',
timeout: 8000,
data: { cameraCode } data: { cameraCode }
}))
})))
} }
export function controlPtz(payload) { export function controlPtz(payload) {


+ 35
- 17
src/api/portal/index.js View File

@ -2,78 +2,95 @@
// 这些接口由中心端后端提供,返回"所有站"的汇总数据; // 这些接口由中心端后端提供,返回"所有站"的汇总数据;
// 与单站业务接口不同,它们不依赖当前登录用户所属站。 // 与单站业务接口不同,它们不依赖当前登录用户所属站。
// 后端未就绪时页面以空态展示,不影响其它功能。 // 后端未就绪时页面以空态展示,不影响其它功能。
import axios from "axios";
import request from "@/utils/request"; import request from "@/utils/request";
import { limitVideoRequest } from "@/utils/videoRequestLimit";
// 场站列表 // 场站列表
// 返回 data: [{ id, stationCode, stationName, baseUrl, portalUrl, enabled }] // 返回 data: [{ id, stationCode, stationName, baseUrl, portalUrl, enabled }]
export function getStationList(query) {
export function getStationList(query, config = {}) {
return request({ return request({
url: "/agg/station/all", url: "/agg/station/all",
method: "get", method: "get",
params: query, params: query,
noAlert: true
noAlert: true,
cancelToken: config.cancelToken
}); });
} }
// 本轮巡检任务统计(表计/外观/红外) // 本轮巡检任务统计(表计/外观/红外)
// 返回 data: [{ algCategory, inspectedTaskCount, completedTaskCount, defectCount, ... }] // 返回 data: [{ algCategory, inspectedTaskCount, completedTaskCount, defectCount, ... }]
export function getInspectionOverview(query) {
export function getInspectionOverview(query, config = {}) {
return request({ return request({
url: "/agg/statistic/inspection", url: "/agg/statistic/inspection",
method: "get", method: "get",
params: query, params: query,
noAlert: true
noAlert: true,
cancelToken: config.cancelToken
}); });
} }
// 缺陷发现趋势(最近 12 个月或 4 个季度) // 缺陷发现趋势(最近 12 个月或 4 个季度)
// 返回 data: [{ date, minorCount, generalCount, seriousCount, urgentCount, ... }] // 返回 data: [{ date, minorCount, generalCount, seriousCount, urgentCount, ... }]
export function getDefectTrend(query) {
export function getDefectTrend(query, config = {}) {
return request({ return request({
url: "/agg/statistic/defect", url: "/agg/statistic/defect",
method: "get", method: "get",
params: query, params: query,
noAlert: true
noAlert: true,
cancelToken: config.cancelToken
}); });
} }
// 通道设备来源统计 // 通道设备来源统计
// 返回 data: { totalCount, deviceSourceCounts: [{ deviceSource, count }] } // 返回 data: { totalCount, deviceSourceCounts: [{ deviceSource, count }] }
export function getChannelStats(query) {
export function getChannelStats(query, config = {}) {
return request({ return request({
url: "/agg/statistic/channel", url: "/agg/statistic/channel",
method: "get", method: "get",
params: query, params: query,
noAlert: true
noAlert: true,
cancelToken: config.cancelToken
}); });
} }
// 通道树 // 通道树
// 返回 data: 区域、巡检设备、通道三级树 // 返回 data: 区域、巡检设备、通道三级树
export function getChannelTree(query) {
export function getChannelTree(query, config = {}) {
return request({ return request({
url: "/agg/tree/channel", url: "/agg/tree/channel",
method: "get", method: "get",
params: query, params: query,
noAlert: true
noAlert: true,
skipWorkspaceStation: !!config.skipWorkspaceStation,
cancelToken: config.cancelToken
}); });
} }
// 汇聚侧实时视频启动 // 汇聚侧实时视频启动
// 返回 data: { stationCode, cameraCode, streamId, hls, flv, status } // 返回 data: { stationCode, cameraCode, streamId, hls, flv, status }
// 拉流会占住浏览器到同一地址的连接,必须可取消,否则切页后连接一直 pending。
export function startRealtimeVideo(query) { export function startRealtimeVideo(query) {
const { stationCode, ...data } = query || {}; const { stationCode, ...data } = query || {};
return request({
url: "/agg/video/realtime/start",
method: "post",
params: { stationCode },
data,
noAlert: true
const source = axios.CancelToken.source();
const promise = limitVideoRequest(registerAbort => {
registerAbort(() => source.cancel("video request cancelled"));
return request({
url: "/agg/video/realtime/start",
method: "post",
params: { stationCode },
data,
timeout: 8000,
noAlert: true,
cancelToken: source.token
});
}); });
return promise;
} }
// 汇聚侧实时视频停止 // 汇聚侧实时视频停止
// 入参按后端要求传 stationCode + cameraCode
// 入参按后端要求传 stationCode + cameraCode。
// 停流不进拉流队列:离开首页时拉流可能还占着名额,停流再排队就会和业务请求一起卡住。
export function stopRealtimeVideo(query) { export function stopRealtimeVideo(query) {
const { stationCode, ...data } = query || {}; const { stationCode, ...data } = query || {};
return request({ return request({
@ -81,6 +98,7 @@ export function stopRealtimeVideo(query) {
method: "post", method: "post",
params: { stationCode }, params: { stationCode },
data, data,
timeout: 8000,
noAlert: true noAlert: true
}); });
} }


+ 2
- 1
src/api/videosMonitor/index.js View File

@ -28,11 +28,12 @@ export function getAreaEqubookTreeSelect(params) {
* @param {*} data * @param {*} data
* @returns * @returns
*/ */
export function list(params) {
export function list(params, config = {}) {
return request({ return request({
url: "/patrol/taskstatus/list", url: "/patrol/taskstatus/list",
method: "get", method: "get",
params, params,
cancelToken: config.cancelToken,
}); });
} }
/** /**


+ 18
- 3
src/layout/index.vue View File

@ -57,6 +57,7 @@ import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler' import ResizeMixin from './mixin/ResizeHandler'
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import variables from '@/assets/styles/variables.scss' import variables from '@/assets/styles/variables.scss'
import axios from 'axios'
import { getStationList } from '@/api/portal' import { getStationList } from '@/api/portal'
export default { export default {
@ -74,7 +75,8 @@ export default {
// 侧边栏宽度档位:0=160px,1=200px,2=240px,3=280px,随子菜单展开层级加深 // 侧边栏宽度档位:0=160px,1=200px,2=240px,3=280px,随子菜单展开层级加深
sidebarWidthLevel: 0, sidebarWidthLevel: 0,
// 汇聚态站名筛选选项 // 汇聚态站名筛选选项
stationOptions: []
stationOptions: [],
stationRequestSource: null
} }
}, },
mixins: [ResizeMixin], mixins: [ResizeMixin],
@ -137,12 +139,25 @@ export default {
mounted() { mounted() {
if (this.fromPortal) this.loadStationOptions() if (this.fromPortal) this.loadStationOptions()
}, },
beforeDestroy() {
this.cancelStationOptions()
},
methods: { methods: {
cancelStationOptions() {
if (!this.stationRequestSource) return
this.stationRequestSource.cancel('layout station list cancelled')
this.stationRequestSource = null
},
// 站名下拉选项:与汇聚首页同款接口 // 站名下拉选项:与汇聚首页同款接口
loadStationOptions() { loadStationOptions() {
getStationList({}).then(response => {
this.cancelStationOptions()
this.stationRequestSource = axios.CancelToken.source()
getStationList({}, { cancelToken: this.stationRequestSource.token }).then(response => {
this.stationRequestSource = null
this.stationOptions = Array.isArray(response && response.data) ? response.data : [] this.stationOptions = Array.isArray(response && response.data) ? response.data : []
}).catch(() => {
}).catch((error) => {
if (axios.isCancel(error)) return
this.stationRequestSource = null
this.stationOptions = [] this.stationOptions = []
}) })
}, },


+ 4
- 2
src/permission.js View File

@ -21,7 +21,9 @@ router.beforeEach(async (to, from, next) => {
/* has token*/ /* has token*/
if (to.path === "/login") { if (to.path === "/login") {
console.info("[GAB] to next"); console.info("[GAB] to next");
next({ path: homePath, replace: true });
// 已经登录时回到进来前的地址;没有记录才去打包配置的首页
const redirect = to.query && to.query.redirect
next({ path: redirect || homePath, replace: true });
// NProgress.done(); // NProgress.done();
} else if (to.path === "/video-preview-embed") { } else if (to.path === "/video-preview-embed") {
next(); next();
@ -70,7 +72,7 @@ router.beforeEach(async (to, from, next) => {
// 在免登录白名单,直接进入 // 在免登录白名单,直接进入
next(); next();
} else { } else {
next(`/login`); // 否则全部重定向到登录页
next({ path: "/login", query: { redirect: to.fullPath } }); // 记住进来的地址,登录后回到这一页
// NProgress.done(); // NProgress.done();
} }
} }


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

@ -20,6 +20,8 @@ const getters = {
siteId: state => state.siteContext.siteId, siteId: state => state.siteContext.siteId,
siteName: state => state.siteContext.siteName, siteName: state => state.siteContext.siteName,
inSite: state => state.siteContext.siteId !== undefined && state.siteContext.siteId !== null, inSite: state => state.siteContext.siteId !== undefined && state.siteContext.siteId !== null,
// 从汇聚门户点进动环巡检后保留的会话标记:横向导航、聚合接口都靠它。
// 直接打开 /index 不会带这个标记,所以仍是站端。
fromPortal: state => state.siteContext.fromPortal, fromPortal: state => state.siteContext.fromPortal,
workspaceStationCode: state => state.siteContext.workspaceStationCode, workspaceStationCode: state => state.siteContext.workspaceStationCode,
} }


+ 8
- 0
src/utils/image-url.js View File

@ -1,3 +1,5 @@
import store from "@/store";
const imagePathPrefix = process.env.VUE_APP_IMAGE_PATH_PREFIX || ""; const imagePathPrefix = process.env.VUE_APP_IMAGE_PATH_PREFIX || "";
function joinPath(left, right) { function joinPath(left, right) {
@ -33,6 +35,12 @@ export function buildImageUrl(ftpViewAddress, imagePath) {
return path; return path;
} }
// 汇聚接口直接返回各站完整图片地址,由 nginx 代理,前端不再拼前缀。
// 站端仍走下面的前缀拼接。
if (store.getters.fromPortal && !store.getters.inSite) {
return path;
}
if (/^https?:\/\//i.test(path)) { if (/^https?:\/\//i.test(path)) {
try { try {
const absoluteUrl = new URL(path); const absoluteUrl = new URL(path);


+ 11
- 2
src/utils/request.js View File

@ -68,7 +68,8 @@ service.interceptors.request.use(config => {
if (mapped) config.url = "/" + mapped if (mapped) config.url = "/" + mapped
// 工作台顶部站名下拉:选中站后给列表/树查询带上 stationCode("全部场站"不传) // 工作台顶部站名下拉:选中站后给列表/树查询带上 stationCode("全部场站"不传)
const wsCode = store.getters.workspaceStationCode const wsCode = store.getters.workspaceStationCode
if (wsCode && config.method === "get" && AGG_STATION_CODE_PATHS.some(p => config.url.indexOf(p) === 0)) {
// 首页设备树自己带场站,不能被动环巡检右上角的选择覆盖
if (wsCode && !config.skipWorkspaceStation && config.method === "get" && AGG_STATION_CODE_PATHS.some(p => config.url.indexOf(p) === 0)) {
config.params = Object.assign({}, config.params, { stationCode: wsCode }) config.params = Object.assign({}, config.params, { stationCode: wsCode })
} }
} }
@ -179,7 +180,8 @@ service.interceptors.response.use((res) => {
// 本系统登录的用户:会话过期后回到登录页,不能悄悄换成别的账号 // 本系统登录的用户:会话过期后回到登录页,不能悄悄换成别的账号
store.dispatch("FedLogOut").then(() => { store.dispatch("FedLogOut").then(() => {
Message.error("登录状态已过期,请重新登录"); Message.error("登录状态已过期,请重新登录");
router.push({ path: "/login" }).catch(() => { });
const returnPath = router.currentRoute && router.currentRoute.fullPath
router.push({ path: "/login", query: returnPath && returnPath !== "/login" ? { redirect: returnPath } : undefined }).catch(() => { });
}); });
} }
return Promise.reject(new Error('无效的会话,或者会话已过期,请重新登录。')) return Promise.reject(new Error('无效的会话,或者会话已过期,请重新登录。'))
@ -208,8 +210,15 @@ service.interceptors.response.use((res) => {
} }
}, },
error => { error => {
// 页面切走时主动取消的请求不是失败,不能弹错误,也不能当成异常去重试。
if (axios.isCancel(error)) {
return Promise.reject(error)
}
console.log('[GAB] err: ' + error) console.log('[GAB] err: ' + error)
let { message } = error; let { message } = error;
if (!message) {
return Promise.reject(error)
}
if (message == "Network Error") { if (message == "Network Error") {
message = "后端接口连接异常"; message = "后端接口连接异常";
} }


+ 60
- 0
src/utils/videoRequestLimit.js View File

@ -0,0 +1,60 @@
// 浏览器对同一地址的并发连接有上限(HTTP/1.1 通常是 6)。
// 首页四路拉流一旦卡住就会占满连接,巡检列表等业务请求只能一直 pending,页面跟着卡死。
// 拉流单独排队,同时最多两路;离开页面时把还没发出的请求丢掉,并取消已经发出的请求。
const MAX_CONCURRENT = 2
let running = 0
const queue = []
function pump() {
while (running < MAX_CONCURRENT && queue.length) {
const job = queue.shift()
if (job.cancelled) continue
running += 1
let settled = false
const finish = () => {
if (settled) return
settled = true
running -= 1
pump()
}
Promise.resolve()
.then(() => job.task(signal => {
job.abort = signal
}))
.then(
value => {
finish()
if (!job.cancelled) job.resolve(value)
},
error => {
finish()
if (!job.cancelled) job.reject(error)
}
)
}
}
function cancelledError() {
const error = new Error("video request cancelled")
error.code = "ERR_CANCELED"
return error
}
export function limitVideoRequest(task) {
let job
const promise = new Promise((resolve, reject) => {
job = { task, resolve, reject, cancelled: false, abort: null }
queue.push(job)
pump()
})
// 还在排队就直接丢掉;已经发出去的交给调用方注册的 abort 取消。
promise.cancel = () => {
if (!job || job.cancelled) return
job.cancelled = true
const index = queue.indexOf(job)
if (index !== -1) queue.splice(index, 1)
if (typeof job.abort === "function") job.abort()
job.reject(cancelledError())
}
return promise
}

+ 10
- 1
src/views/cards/components/ImagePreviewZoomTemperature/index.vue View File

@ -31,7 +31,7 @@
--> -->
<img <img
ref="previewImage" ref="previewImage"
:src="'/app/htjc6/htjcImage/'+selectImgUrl"
:src="previewImageSrc"
style="width: 100%; height: 100%; object-fit: contain" style="width: 100%; height: 100%; object-fit: contain"
@load="handleImageLoad" @load="handleImageLoad"
@error="handleImageError" @error="handleImageError"
@ -97,6 +97,7 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex";
import { getTemperatureValue } from "@/api/device"; import { getTemperatureValue } from "@/api/device";
import { downloadCsv } from "@/api/basedata/patrolpointmnt/patrolpointpreset.js"; import { downloadCsv } from "@/api/basedata/patrolpointmnt/patrolpointpreset.js";
import Papa from "papaparse"; import Papa from "papaparse";
@ -178,6 +179,14 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(["fromPortal", "inSite"]),
previewImageSrc() {
if (!this.selectImgUrl) return "";
if (/^(https?:|data:|blob:)/i.test(this.selectImgUrl)) return this.selectImgUrl;
// 汇聚直接使用后端返回的完整地址;站端继续拼图片前缀
if (this.fromPortal && !this.inSite) return this.selectImgUrl;
return "/app/htjc6/htjcImage/" + this.selectImgUrl;
},
selectImgUrl() { selectImgUrl() {
return this.dataImageUrls.length > this.selectIndex return this.dataImageUrls.length > this.selectIndex
? this.dataImageUrls[this.selectIndex] ? this.dataImageUrls[this.selectIndex]


+ 13
- 10
src/views/cards/site_management/site_management.vue View File

@ -139,8 +139,9 @@
</el-option> </el-option>
</el-select> </el-select>
<!-- 设备类型筛选下拉框 -->
<!-- 设备类型筛选下拉框:汇聚下不使用 -->
<el-select <el-select
v-if="!$store.getters.fromPortal"
v-model="selectedDeviceType" v-model="selectedDeviceType"
placeholder="全部设备类型" placeholder="全部设备类型"
clearable clearable
@ -1680,7 +1681,7 @@ export default {
created() { created() {
this.testobjBackup = lodash.cloneDeep(this.testobj); this.testobjBackup = lodash.cloneDeep(this.testobj);
this.presetDataBackup = lodash.cloneDeep(this.presetData); this.presetDataBackup = lodash.cloneDeep(this.presetData);
listPatroltype().then((res) => {
if (!this.$store.getters.fromPortal) listPatroltype().then((res) => {
const list = []; const list = [];
let childs = []; let childs = [];
let ptype = {}; let ptype = {};
@ -1714,14 +1715,16 @@ export default {
this.getAreaList1(); this.getAreaList1();
this.getAlgCategory(); this.getAlgCategory();
getBdconfigMainsystem().then((res) => {
const config = res.data.find((x) => x.systemType.includes("xsxt"));
if (config) {
this.updata = {
sendCode: config.mainSystemCode,
};
}
});
if (!this.$store.getters.fromPortal) {
getBdconfigMainsystem().then((res) => {
const config = res.data.find((x) => x.systemType.includes("xsxt"));
if (config) {
this.updata = {
sendCode: config.mainSystemCode,
};
}
});
}
// 从localStorage加载保存的宽度 // 从localStorage加载保存的宽度
this.loadLeftPanelWidth(); this.loadLeftPanelWidth();


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

@ -490,6 +490,7 @@ import InspectionTaskDefectDetail from "./inspectionTaskDefectDetail.vue";
import PatrolAbnormalStatistics from "@/views/cards/error_statistics/patrolAbnormalStatistics.vue"; import PatrolAbnormalStatistics from "@/views/cards/error_statistics/patrolAbnormalStatistics.vue";
import debounce from "lodash.debounce"; import debounce from "lodash.debounce";
import moment from "moment"; import moment from "moment";
import axios from "axios";
import { openDialog } from "../common.js"; import { openDialog } from "../common.js";
import { list, areaList, taskStatusChange } from "@/api/videosMonitor/index"; import { list, areaList, taskStatusChange } from "@/api/videosMonitor/index";
import { listData } from "@/api/system/dict/data"; import { listData } from "@/api/system/dict/data";
@ -516,6 +517,7 @@ export default {
data() { data() {
return { return {
timer: null, // 用于存储定时器的引用 timer: null, // 用于存储定时器的引用
listCancelSource: null,
// 遮罩层 // 遮罩层
loading: false, loading: false,
// 总条数 // 总条数
@ -625,10 +627,12 @@ export default {
}, },
computed: {}, computed: {},
beforeDestroy() { beforeDestroy() {
// 在组件销毁前清除定时器
// 在组件销毁前清除定时器,并中止还没返回的列表请求。
// 巡检列表和首页拉流打的是同一地址,不取消就会一直占着浏览器连接。
if (this.timer) { if (this.timer) {
clearInterval(this.timer); clearInterval(this.timer);
} }
this.cancelListRequest();
}, },
created() { created() {
// 防抖 // 防抖
@ -711,6 +715,7 @@ export default {
clearInterval(this.timer); clearInterval(this.timer);
this.timer = null; this.timer = null;
} }
this.cancelListRequest();
}, },
destroyed() { destroyed() {
this.setTableHeight.cancel(); this.setTableHeight.cancel();
@ -747,6 +752,11 @@ export default {
this.$refs.tableCardRef.clientHeight - 52 this.$refs.tableCardRef.clientHeight - 52
); );
}, },
cancelListRequest() {
if (!this.listCancelSource) return;
this.listCancelSource.cancel("inspection list cancelled");
this.listCancelSource = null;
},
getList(showLoading = false) { getList(showLoading = false) {
// 保存当前选中的任务ID // 保存当前选中的任务ID
const selectedIds = this.pendingSelectedIds.length const selectedIds = this.pendingSelectedIds.length
@ -761,8 +771,13 @@ export default {
if (showLoading) { if (showLoading) {
this.loading = true; this.loading = true;
} }
list(params)
// 轮询和手动刷新会叠在上一次还没回来的请求上,先取消旧的。
this.cancelListRequest();
const source = axios.CancelToken.source();
this.listCancelSource = source;
list(params, { cancelToken: source.token })
.then(({ rows, total }) => { .then(({ rows, total }) => {
if (this.listCancelSource === source) this.listCancelSource = null;
this.loading = false; this.loading = false;
this.dataList = rows; this.dataList = rows;
this.total = total; this.total = total;
@ -780,7 +795,9 @@ export default {
this.pendingSelectedIds = []; this.pendingSelectedIds = [];
}); });
}) })
.catch(() => {
.catch((error) => {
if (axios.isCancel(error)) return;
if (this.listCancelSource === source) this.listCancelSource = null;
this.loading = false; this.loading = false;
}); });
}, },


+ 37
- 6
src/views/portal/components/PortalDeviceTree.vue View File

@ -79,7 +79,9 @@ function filterTree(nodes, keyword, onlineStatus) {
export default { export default {
name: "PortalDeviceTree", name: "PortalDeviceTree",
props: { props: {
selectedCode: { type: String, default: "" }
selectedCode: { type: String, default: "" },
// 只跟首页场站选择,不吃动环巡检右上角的站
stationCode: { type: String, default: "" }
}, },
data() { data() {
return { return {
@ -89,6 +91,7 @@ export default {
keywords: "", keywords: "",
onlineStatus: "1", onlineStatus: "1",
internalLoading: false, internalLoading: false,
treeRequestId: 0,
treeProps: { children: "children", label: "label" } treeProps: { children: "children", label: "label" }
}; };
}, },
@ -99,6 +102,9 @@ export default {
}, },
watch: { watch: {
onlineStatus: "refreshTree", onlineStatus: "refreshTree",
stationCode() {
this.loadTree();
},
selectedCode(value) { selectedCode(value) {
const node = this.channelNodes.find(item => this.toCameraCode(item) === value); const node = this.channelNodes.find(item => this.toCameraCode(item) === value);
if (node && this.$refs.treeRef) this.$refs.treeRef.setCurrentKey(node.id); if (node && this.$refs.treeRef) this.$refs.treeRef.setCurrentKey(node.id);
@ -109,12 +115,19 @@ export default {
}, },
methods: { methods: {
async loadTree() { async loadTree() {
const requestId = ++this.treeRequestId;
this.internalLoading = true; this.internalLoading = true;
this.treeNodes = [];
this.treeData = [];
this.expandedKeys = [];
try { 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 => {
const query = { onlineStatus: this.onlineStatus, patroldeviceTypeFlag: "ipc" };
if (this.stationCode) query.stationCode = this.stationCode;
const response = await getChannelTree(query, { skipWorkspaceStation: true });
if (requestId !== this.treeRequestId) return;
const nodes = Array.isArray(response && response.data) ? response.data : [];
this.treeNodes = this.stationCode ? this.keepStation(nodes, this.stationCode) : nodes;
const walk = items => (items || []).forEach(node => {
if (node.children && node.children.length) { if (node.children && node.children.length) {
this.expandedKeys.push(node.id); this.expandedKeys.push(node.id);
walk(node.children); walk(node.children);
@ -124,13 +137,31 @@ export default {
this.refreshTree(); this.refreshTree();
this.$emit("channels-loaded", this.channelNodes.map(this.normalizeChannel)); this.$emit("channels-loaded", this.channelNodes.map(this.normalizeChannel));
} catch (error) { } catch (error) {
if (requestId !== this.treeRequestId) return;
this.treeNodes = []; this.treeNodes = [];
this.treeData = []; this.treeData = [];
this.$message.error(error.message || "设备树加载失败"); this.$message.error(error.message || "设备树加载失败");
} finally { } finally {
this.internalLoading = false;
if (requestId === this.treeRequestId) this.internalLoading = false;
} }
}, },
keepStation(nodes, stationCode) {
const matched = this.filterStation(nodes, stationCode);
return matched.length ? matched : nodes;
},
filterStation(nodes, stationCode) {
return (nodes || []).reduce((result, node) => {
if (node.channelFlag) {
if (String(node.stationCode || "") === String(stationCode)) result.push(node);
return result;
}
const children = this.filterStation(node.children, stationCode);
if (String(node.stationCode || "") === String(stationCode) || children.length) {
result.push({ ...node, children });
}
return result;
}, []);
},
refreshTree() { refreshTree() {
this.treeData = filterTree(this.treeNodes, this.keywords, this.onlineStatus); this.treeData = filterTree(this.treeNodes, this.keywords, this.onlineStatus);
}, },


+ 91
- 22
src/views/portal/index.vue View File

@ -237,7 +237,9 @@
<aside v-if="treeMounted" v-show="treeVisible" class="portal-device-tree"> <aside v-if="treeMounted" v-show="treeVisible" class="portal-device-tree">
<portal-device-tree <portal-device-tree
:key="portalTreeStationCode || 'all'"
:selected-code="selectedPortalCameraCode" :selected-code="selectedPortalCameraCode"
:station-code="portalTreeStationCode"
@channels-loaded="handlePortalTreeChannels" @channels-loaded="handlePortalTreeChannels"
@open="openPortalChannel" @open="openPortalChannel"
/> />
@ -272,6 +274,7 @@ import {
} from "@/api/portal"; } from "@/api/portal";
import Hls from "hls.js"; import Hls from "hls.js";
import flvjs from "flv.js"; import flvjs from "flv.js";
import axios from "axios";
import DefectTrendChart from "./components/DefectTrendChart"; import DefectTrendChart from "./components/DefectTrendChart";
import PortalDeviceTree from "./components/PortalDeviceTree.vue"; import PortalDeviceTree from "./components/PortalDeviceTree.vue";
import Navbar from "@/layout/components/Navbar"; import Navbar from "@/layout/components/Navbar";
@ -406,6 +409,10 @@ export default {
treeMounted: false, treeMounted: false,
portalPlayers: {}, portalPlayers: {},
portalStartTimers: {}, portalStartTimers: {},
portalStartRequests: {},
portalRequestSource: null,
portalAlive: true,
portalReleased: false,
overviewRequestSerial: 0, overviewRequestSerial: 0,
trendRequestSerial: 0 trendRequestSerial: 0
}; };
@ -446,6 +453,10 @@ export default {
.filter(video => String(video.stationId) === String(this.videoStationId)) .filter(video => String(video.stationId) === String(this.videoStationId))
.slice(0, 4); .slice(0, 4);
}, },
portalTreeStationCode() {
const station = this.stationList.find(item => String(item.id) === String(this.videoStationId));
return station && station.code ? station.code : "";
},
portalVideoSlots() { portalVideoSlots() {
return Array.from({ length: 4 }, (_, index) => this.videoList[index] || null); return Array.from({ length: 4 }, (_, index) => this.videoList[index] || null);
}, },
@ -472,8 +483,7 @@ export default {
this.loadPortalData(); this.loadPortalData();
}, },
beforeDestroy() { beforeDestroy() {
this.pauseDefectCarousel();
this.stopPortalVideos();
this.releasePortalPage();
}, },
methods: { methods: {
selectMonitorStation(station) { selectMonitorStation(station) {
@ -491,6 +501,18 @@ export default {
this.trendStationId = station.code; this.trendStationId = station.code;
this.trendType = algCategory; this.trendType = algCategory;
}, },
portalRequestConfig() {
if (!this.portalRequestSource) this.portalRequestSource = axios.CancelToken.source();
return { cancelToken: this.portalRequestSource.token };
},
cancelPortalRequests() {
if (!this.portalRequestSource) return;
this.portalRequestSource.cancel("portal requests cancelled");
this.portalRequestSource = null;
},
isPortalCancel(error) {
return !this.portalAlive || axios.isCancel(error);
},
loadPortalData() { loadPortalData() {
return this.loadStations().then(() => Promise.all([ return this.loadStations().then(() => Promise.all([
this.loadInspectionOverview(), this.loadInspectionOverview(),
@ -500,7 +522,7 @@ export default {
])); ]));
}, },
loadStations() { loadStations() {
return getStationList({}).then(response => {
return getStationList({}, this.portalRequestConfig()).then(response => {
const rows = Array.isArray(response && response.data) ? response.data : []; const rows = Array.isArray(response && response.data) ? response.data : [];
this.stationList = rows.map(item => ({ this.stationList = rows.map(item => ({
id: item.id, id: item.id,
@ -511,7 +533,8 @@ export default {
enabled: String(item.enabled), enabled: String(item.enabled),
status: String(item.enabled) === "1" ? "normal" : "abnormal" status: String(item.enabled) === "1" ? "normal" : "abnormal"
})); }));
}).catch(() => {
}).catch((error) => {
if (this.isPortalCancel(error)) return;
this.stationList = []; this.stationList = [];
}); });
}, },
@ -519,7 +542,7 @@ export default {
const params = {}; const params = {};
const station = this.stationList.find(item => String(item.id) === String(this.videoStationId)); const station = this.stationList.find(item => String(item.id) === String(this.videoStationId));
if (station && station.code) params.stationCode = station.code; if (station && station.code) params.stationCode = station.code;
return getChannelStats(params).then(response => {
return getChannelStats(params, this.portalRequestConfig()).then(response => {
const data = response && response.data ? response.data : {}; const data = response && response.data ? response.data : {};
const counts = Array.isArray(data.deviceSourceCounts) ? data.deviceSourceCounts : []; const counts = Array.isArray(data.deviceSourceCounts) ? data.deviceSourceCounts : [];
const cameraCount = { ...EMPTY_CAMERA, total: toCount(data.totalCount) }; const cameraCount = { ...EMPTY_CAMERA, total: toCount(data.totalCount) };
@ -532,19 +555,21 @@ export default {
else cameraCount.security += count; else cameraCount.security += count;
}); });
this.cameraCount = cameraCount; this.cameraCount = cameraCount;
}).catch(() => {
}).catch((error) => {
if (this.isPortalCancel(error)) return;
this.cameraCount = { ...EMPTY_CAMERA }; this.cameraCount = { ...EMPTY_CAMERA };
}); });
}, },
loadVideoChannels() { loadVideoChannels() {
return getChannelTree({ onlineStatus: "1", patroldeviceTypeFlag: "ipc" }).then(response => {
return getChannelTree({ onlineStatus: "1", patroldeviceTypeFlag: "ipc" }, { skipWorkspaceStation: true, ...this.portalRequestConfig() }).then(response => {
const nodes = flattenNodes(Array.isArray(response && response.data) ? response.data : []); const nodes = flattenNodes(Array.isArray(response && response.data) ? response.data : []);
const videos = nodes const videos = nodes
.filter(node => node.channelFlag && (String(node.patroldeviceOnlineStatus || "1") === "1")) .filter(node => node.channelFlag && (String(node.patroldeviceOnlineStatus || "1") === "1"))
.map(this.normalizePortalVideo); .map(this.normalizePortalVideo);
this.portalChannels = videos; this.portalChannels = videos;
return this.replacePortalVideos(videos); return this.replacePortalVideos(videos);
}).catch(() => {
}).catch((error) => {
if (this.isPortalCancel(error)) return;
this.portalChannels = []; this.portalChannels = [];
return this.replacePortalVideos([]); return this.replacePortalVideos([]);
}); });
@ -563,6 +588,8 @@ export default {
}, },
replacePortalVideos(videos) { replacePortalVideos(videos) {
return this.stopPortalVideos().then(() => { return this.stopPortalVideos().then(() => {
// 切页后停流才返回时,不能再把播放器挂回去。
if (!this.portalAlive) return;
this.videoList = (videos || []).slice(0, 4); this.videoList = (videos || []).slice(0, 4);
this.activeVideoIndex = 0; this.activeVideoIndex = 0;
this.$nextTick(() => this.startDefaultPortalVideos()); this.$nextTick(() => this.startDefaultPortalVideos());
@ -574,19 +601,25 @@ export default {
}); });
}, },
async startPortalVideo(video, index) { async startPortalVideo(video, index) {
if (!video || !video.stationCode || !video.cameraCode) return;
if (!this.portalAlive || !video || !video.stationCode || !video.cameraCode) return;
this.clearPortalStartTimer(index); this.clearPortalStartTimer(index);
this.cancelPortalStart(index);
this.destroyPortalPlayer(index); this.destroyPortalPlayer(index);
this.$set(video, "loading", true); this.$set(video, "loading", true);
this.$set(video, "playbackError", false); this.$set(video, "playbackError", false);
this.$set(video, "playUrl", ""); this.$set(video, "playUrl", "");
this.$set(video, "flvUrl", ""); this.$set(video, "flvUrl", "");
const request = startRealtimeVideo({
stationCode: video.stationCode,
cameraCode: video.cameraCode
});
this.$set(this.portalStartRequests, index, request);
try { try {
const response = await startRealtimeVideo({
stationCode: video.stationCode,
cameraCode: video.cameraCode
});
if (this.videoList[index] !== video) return;
const response = await request;
if (this.portalStartRequests[index] === request) {
this.$delete(this.portalStartRequests, index);
}
if (!this.portalAlive || this.videoList[index] !== video) return;
const stream = pickPortalStream(response); const stream = pickPortalStream(response);
this.$set(video, "streamId", stream.streamId); this.$set(video, "streamId", stream.streamId);
this.$set(video, "playUrl", stream.hlsUrl); this.$set(video, "playUrl", stream.hlsUrl);
@ -598,6 +631,11 @@ export default {
} }
this.$nextTick(() => this.attachPortalPlayer(video, index)); this.$nextTick(() => this.attachPortalPlayer(video, index));
} catch (error) { } catch (error) {
if (this.portalStartRequests[index] === request) {
this.$delete(this.portalStartRequests, index);
}
// 切页取消的请求不要再改状态,组件可能已经销毁。
if (!this.portalAlive || (error && error.code === "ERR_CANCELED")) return;
this.$set(video, "loading", false); this.$set(video, "loading", false);
this.$set(video, "playbackError", true); this.$set(video, "playbackError", true);
} }
@ -605,10 +643,14 @@ export default {
attachPortalPlayer(video, index) { attachPortalPlayer(video, index) {
const ref = this.$refs[`portalVideo${index}`]; const ref = this.$refs[`portalVideo${index}`];
const element = Array.isArray(ref) ? ref[0] : ref; const element = Array.isArray(ref) ? ref[0] : ref;
if (!video || this.videoList[index] !== video) return;
if (!this.portalAlive || !video || this.videoList[index] !== video) return;
if (!element) { if (!element) {
// 切走时视频节点会被拆掉。这里再排一次 nextTick,离开后会一直空转。
this.$nextTick(() => { this.$nextTick(() => {
if (this.videoList[index] === video) this.attachPortalPlayer(video, index);
if (!this.portalAlive || this.videoList[index] !== video) return;
const retryRef = this.$refs[`portalVideo${index}`];
const retryElement = Array.isArray(retryRef) ? retryRef[0] : retryRef;
if (retryElement) this.attachPortalPlayer(video, index);
}); });
return; return;
} }
@ -763,10 +805,22 @@ export default {
window.clearTimeout(this.portalStartTimers[index]); window.clearTimeout(this.portalStartTimers[index]);
this.$delete(this.portalStartTimers, index); this.$delete(this.portalStartTimers, index);
}, },
cancelPortalStart(index) {
const request = this.portalStartRequests[index];
if (!request) return;
this.$delete(this.portalStartRequests, index);
if (typeof request.cancel === "function") request.cancel();
},
cancelPortalStarts() {
Object.keys(this.portalStartRequests).forEach(index => this.cancelPortalStart(index));
},
stopPortalVideo(video, index) { stopPortalVideo(video, index) {
this.clearPortalStartTimer(index); this.clearPortalStartTimer(index);
this.cancelPortalStart(index);
this.destroyPortalPlayer(index); this.destroyPortalPlayer(index);
// 拉流还没成功就切走时,没有可停的流。再发 stop 只会继续占浏览器连接。
if (!video || !video.stationCode || !video.cameraCode) return Promise.resolve(null); if (!video || !video.stationCode || !video.cameraCode) return Promise.resolve(null);
if (!video.streamId && !video.playUrl && !video.flvUrl) return Promise.resolve(null);
return stopRealtimeVideo({ stationCode: video.stationCode, cameraCode: video.cameraCode }).catch(() => null); return stopRealtimeVideo({ stationCode: video.stationCode, cameraCode: video.cameraCode }).catch(() => null);
}, },
stopPortalVideos() { stopPortalVideos() {
@ -801,7 +855,7 @@ export default {
loadInspectionOverview() { loadInspectionOverview() {
const requestSerial = ++this.overviewRequestSerial; const requestSerial = ++this.overviewRequestSerial;
const params = this.overviewStationId ? { stationCode: this.overviewStationId } : {}; const params = this.overviewStationId ? { stationCode: this.overviewStationId } : {};
return getInspectionOverview(params).then(response => {
return getInspectionOverview(params, this.portalRequestConfig()).then(response => {
if (requestSerial !== this.overviewRequestSerial) return; if (requestSerial !== this.overviewRequestSerial) return;
const rows = Array.isArray(response && response.data) ? response.data : []; const rows = Array.isArray(response && response.data) ? response.data : [];
const rowByCategory = rows.reduce((result, item) => { const rowByCategory = rows.reduce((result, item) => {
@ -823,7 +877,8 @@ export default {
importantDefects: toCount(item.seriousCount) + toCount(item.urgentCount) importantDefects: toCount(item.seriousCount) + toCount(item.urgentCount)
}; };
}); });
}).catch(() => {
}).catch((error) => {
if (this.isPortalCancel(error)) return;
if (requestSerial === this.overviewRequestSerial) { if (requestSerial === this.overviewRequestSerial) {
this.overviewList = createEmptyOverview(); this.overviewList = createEmptyOverview();
} }
@ -836,7 +891,7 @@ export default {
dateType: this.trendGranularity dateType: this.trendGranularity
}; };
if (this.trendStationId) params.stationCode = this.trendStationId; if (this.trendStationId) params.stationCode = this.trendStationId;
return getDefectTrend(params).then(response => {
return getDefectTrend(params, this.portalRequestConfig()).then(response => {
if (requestSerial !== this.trendRequestSerial) return; if (requestSerial !== this.trendRequestSerial) return;
const rows = (Array.isArray(response && response.data) ? response.data : []) const rows = (Array.isArray(response && response.data) ? response.data : [])
.slice() .slice()
@ -850,7 +905,8 @@ export default {
{ name: "重大缺陷", data: rows.map(item => toCount(item.urgentCount)) } { name: "重大缺陷", data: rows.map(item => toCount(item.urgentCount)) }
] ]
}; };
}).catch(() => {
}).catch((error) => {
if (this.isPortalCancel(error)) return;
if (requestSerial === this.trendRequestSerial) { if (requestSerial === this.trendRequestSerial) {
this.trend = createEmptyTrend(); this.trend = createEmptyTrend();
} }
@ -897,15 +953,28 @@ export default {
this.defectCarouselTimer = null; this.defectCarouselTimer = null;
} }
}, },
releasePortalPage() {
// 点击切换和组件销毁都会走到这里,只释放一次,避免停流发两遍。
if (this.portalReleased) return;
this.portalReleased = true;
// 先标记页面已离开,再取消拉流。否则慢返回的 start 还会去挂播放器。
this.portalAlive = false;
this.pauseDefectCarousel();
this.cancelPortalRequests();
this.cancelPortalStarts();
this.stopPortalVideos();
},
onNavClick(item) { onNavClick(item) {
if (item.key === "home") { if (item.key === "home") {
this.$router.push("/portal");
if (this.$route.path !== "/portal") this.$router.push("/portal").catch(() => undefined);
} else if (item.key === "patrol") { } else if (item.key === "patrol") {
// 动环巡检:进入单站系统(即原后台,默认当前部署站的首页), // 动环巡检:进入单站系统(即原后台,默认当前部署站的首页),
// 打上来源标记后单站内保留 module-subbar 横向导航,便于返回门户 // 打上来源标记后单站内保留 module-subbar 横向导航,便于返回门户
this.$store.dispatch("siteContext/markFromPortal"); this.$store.dispatch("siteContext/markFromPortal");
this.$store.dispatch("siteContext/exitSite"); this.$store.dispatch("siteContext/exitSite");
this.$router.push("/index");
// 先把首页还没回来的请求清掉,再跳转。否则拉流会占住浏览器连接,动环巡检的列表一直 pending。
this.releasePortalPage();
this.$router.push("/index").catch(() => undefined);
} else if (item.key === "system") { } else if (item.key === "system") {
this.$router.push("/system/user"); this.$router.push("/system/user");
} }


+ 12
- 5
src/views/video/LivePreview.vue View File

@ -326,11 +326,10 @@ const PTZ_CODES = {
} }
}, },
computed: { computed: {
// 打包模式决定视频接口:build:agg 走汇聚接口,build:htjc6 走单站接口。
// 其它未配置的模式仍按是否从门户进入判断。
// 嵌入页,以及从汇聚门户进入的动环巡检,走聚合视频接口;直接打开的站端走单站接口。
isAggMode() { isAggMode() {
if (process.env.VUE_APP_AGG_VIDEO === 'true') return true
if (process.env.VUE_APP_AGG_VIDEO === 'false') return false
const path = this.$route && this.$route.path
if (path === '/video-preview-embed') return true
return this.$store.getters.fromPortal return this.$store.getters.fromPortal
}, },
capacity() { return this.layout }, capacity() { return this.layout },
@ -549,7 +548,7 @@ const PTZ_CODES = {
? startRealtimeVideo({ stationCode: channel.stationCode, cameraCode: channel.cameraCode }) ? startRealtimeVideo({ stationCode: channel.stationCode, cameraCode: channel.cameraCode })
: startLive(channel, streamType) : startLive(channel, streamType)
const result = await slot.startPromise const result = await slot.startPromise
if (this.slots[index] !== slot) return
if (!this.pageActive || this.slots[index] !== slot) return
this.applyLiveResult(index, slot, result) this.applyLiveResult(index, slot, result)
if (!slot.streamSource.hlsUrl && !slot.streamSource.flvUrl) { if (!slot.streamSource.hlsUrl && !slot.streamSource.flvUrl) {
if (this.isAggMode) { if (this.isAggMode) {
@ -562,6 +561,8 @@ const PTZ_CODES = {
} }
if (index === this.activeIndex) this.loadPresets(true) if (index === this.activeIndex) this.loadPresets(true)
} catch (error) { } catch (error) {
// 切页取消的拉流不是播放失败,不能再弹错误,也不能继续占着画面。
if (!this.pageActive || (error && error.code === 'ERR_CANCELED')) return
if (this.slots[index] === slot) { if (this.slots[index] === slot) {
this.$set(this.slots, index, null) this.$set(this.slots, index, null)
this.$message.error(error.message) this.$message.error(error.message)
@ -605,6 +606,12 @@ const PTZ_CODES = {
this.presets = [] this.presets = []
this.presetId = null this.presetId = null
} }
const startWasPending = slot.loading && slot.startPromise && typeof slot.startPromise.cancel === 'function'
if (startWasPending) {
// 拉流还在排队或卡住时直接取消,不能等它回来再停流,否则浏览器连接一直被占着。
slot.startPromise.cancel()
return
}
if (slot.startPromise) { if (slot.startPromise) {
try { await slot.startPromise } catch (_) { /* no stream session was created */ } try { await slot.startPromise } catch (_) { /* no stream session was created */ }
} }


Loading…
Cancel
Save