From 4a24c08d5449d0fe861481ef1bb17daba48c39da Mon Sep 17 00:00:00 2001 From: douyage Date: Thu, 8 Oct 2026 09:46:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=99=BB=E5=BD=95=E5=90=8E?= =?UTF-8?q?=E9=87=8D=E5=AE=9A=E5=90=91=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E7=8B=AC=E8=A7=86=E9=A2=91=E9=A1=B5video-preview-embed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.agg | 12 +++ .env.htjc6 | 2 + package.json | 1 + public/index.html | 14 +++ src/api/login.js | 12 +++ src/components/AsyncImageFrame/index.vue | 1 + src/layout/components/Navbar.vue | 2 +- src/layout/index.vue | 3 +- src/permission.js | 11 +- src/router/index.js | 7 ++ src/utils/auth.js | 23 +++- src/utils/homePath.js | 5 + src/utils/request.js | 19 +++- src/utils/stationTitle.js | 4 +- .../cards/patrolReport/InspectionReporter.vue | 13 +-- .../cards/site_management/site_management.vue | 6 +- src/views/login.vue | 3 +- src/views/portal/index.vue | 37 ++++--- src/views/video/LivePreview.vue | 100 +++++++++++++----- src/views/video/LivePreviewEmbed.vue | 74 +++++++++++++ 20 files changed, 285 insertions(+), 64 deletions(-) create mode 100644 .env.agg create mode 100644 src/utils/homePath.js create mode 100644 src/views/video/LivePreviewEmbed.vue diff --git a/.env.agg b/.env.agg new file mode 100644 index 0000000..8fb0805 --- /dev/null +++ b/.env.agg @@ -0,0 +1,12 @@ +NODE_ENV = 'production' +ENV = 'production' + +VUE_APP_TITLE = 动环智能巡检系统 +VUE_APP_BASE_API = '/app/htjc6/api' +VUE_APP_NVR_API = '/nvr-api' + +VUE_APP_PUBLIC_PATH = '/app/htjc6/' +VUE_APP_OUTPUT_DIR = 'dist/app/htjc6' +VUE_APP_IMAGE_PATH_PREFIX = '' +VUE_APP_HOME_PATH = '/portal' +VUE_APP_AGG_VIDEO = true diff --git a/.env.htjc6 b/.env.htjc6 index 00fd2b5..861d807 100644 --- a/.env.htjc6 +++ b/.env.htjc6 @@ -8,3 +8,5 @@ VUE_APP_NVR_API = '/nvr-api' VUE_APP_PUBLIC_PATH = '/app/htjc6/' VUE_APP_OUTPUT_DIR = 'dist/app/htjc6' VUE_APP_IMAGE_PATH_PREFIX = '/app/htjc6/htjcImage' +VUE_APP_HOME_PATH = '/index' +VUE_APP_AGG_VIDEO = false diff --git a/package.json b/package.json index 14af78b..a2e60e7 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "vue-cli-service serve", "build:prod": "vue-cli-service build", "build:htjc6": "vue-cli-service build --mode htjc6", + "build:agg": "vue-cli-service build --mode agg", "build:stage": "vue-cli-service build --mode staging", "preview": "node build/index.js --preview", "lint": "eslint --ext .js,.vue src" diff --git a/public/index.html b/public/index.html index d1adedb..df422da 100644 --- a/public/index.html +++ b/public/index.html @@ -2,6 +2,11 @@ + @@ -26,6 +31,15 @@ padding: 0.2em 0; } + html.embed-preview, + html.embed-preview body { + background: #0b1248; + } + + html.embed-preview #loader-wrapper { + display: none; + } + #loader-wrapper { position: fixed; top: 0; diff --git a/src/api/login.js b/src/api/login.js index 3bc6456..881bc93 100644 --- a/src/api/login.js +++ b/src/api/login.js @@ -1,5 +1,17 @@ import request from '@/utils/request' +// 嵌入页换取访问 token,不走登录页 +export function getAuthToken(username, password) { + return request({ + url: '/auth/getToken', + headers: { + isToken: false + }, + method: 'get', + params: { username, password } + }) +} + // 登录方法 export function login(username, password, code, uuid) { console.log('login', username, password, code, uuid) diff --git a/src/components/AsyncImageFrame/index.vue b/src/components/AsyncImageFrame/index.vue index 63a3d02..5db2c04 100644 --- a/src/components/AsyncImageFrame/index.vue +++ b/src/components/AsyncImageFrame/index.vue @@ -538,6 +538,7 @@ export default { // ==================== FTP和URL处理 ==================== async ftpImageViewUrl(url) { + debugger; const ftpViewAddress = this.systemParams && this.systemParams.ftpViewAddress; if (!ftpViewAddress) { const systemParams = {}; diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue index ba8196c..7cceb4a 100644 --- a/src/layout/components/Navbar.vue +++ b/src/layout/components/Navbar.vue @@ -188,7 +188,7 @@ export default { pointer-events: none; background-repeat: no-repeat; background-position: center center; - background-size: 100% 100%; + background-size: auto 100%; background-color: #11194f; box-sizing: border-box; } diff --git a/src/layout/index.vue b/src/layout/index.vue index b849b38..50cbb7e 100644 --- a/src/layout/index.vue +++ b/src/layout/index.vue @@ -123,8 +123,7 @@ export default { { key: 'home', label: '首页', path: '/portal' }, { key: 'patrol', label: '动环巡检', path: '/index' }, // { key: 'system', label: '系统管理', path: '/system/user' } - { key: 'system', label: '系统管理', path: '' } - + // { key: 'system', label: '系统管理', path: '' } ] }, activeModule() { diff --git a/src/permission.js b/src/permission.js index 50b5f8b..7a30355 100644 --- a/src/permission.js +++ b/src/permission.js @@ -6,11 +6,12 @@ import "nprogress/nprogress.css"; import { getToken } from "@/utils/auth"; import { isEmbeddedSession } from "@/utils/embeddedAuth"; import { isRelogin } from "@/utils/request"; +import { homePath } from "@/utils/homePath"; import md5 from 'md5.js' NProgress.configure({ showSpinner: false }); -const whiteList = ["/login", "/auth-redirect", "/bind", "/register"]; +const whiteList = ["/login", "/auth-redirect", "/bind", "/register", "/video-preview-embed"]; router.beforeEach(async (to, from, next) => { // NProgress.start(); @@ -20,8 +21,10 @@ router.beforeEach(async (to, from, next) => { /* has token*/ if (to.path === "/login") { console.info("[GAB] to next"); - next({ path: "/xfdh/index", replace: true }); + next({ path: homePath, replace: true }); // NProgress.done(); + } else if (to.path === "/video-preview-embed") { + next(); } else if (embeddedSession) { // 外部系统跳转进入:宿主框架已提供登录用户,无需再走登录页, // 默认只挂载本地菜单(即非 admin 账号看到的默认内容)。 @@ -50,6 +53,10 @@ router.beforeEach(async (to, from, next) => { .catch(async (err) => { isRelogin.show = false; await store.dispatch("FedLogOut"); + if (to.path === "/video-preview-embed") { + next(); + return; + } Message.error((err && err.message) || "登录状态已失效,请重新登录"); next({ path: "/login", query: { redirect: to.fullPath }, replace: true }); }); diff --git a/src/router/index.js b/src/router/index.js index 5e789eb..f2d7b7c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -37,6 +37,13 @@ export const constantRoutes = [ hidden: true, meta: { title: '动环智能巡检系统' } }, + { + // 供外部系统 iframe 嵌入:无后台外壳,页面内静默登录 + path: '/video-preview-embed', + component: () => import('@/views/video/LivePreviewEmbed'), + hidden: true, + meta: { title: '实时监控' } + }, { path: '/xfdh', component: Layout, diff --git a/src/utils/auth.js b/src/utils/auth.js index bcbf6ba..f8f1ac0 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -3,17 +3,30 @@ import Cookies from 'js-cookie' const TokenKey = 'Admin-Token' const ExpiresInKey = "Admin-Expires-In"; + +// Cookie 不区分端口,同一 IP 下不同端口会串登录态。 +// localStorage 按源(协议 + 主机 + 端口)隔离,因此登录信息改存这里。 +function clearLegacyCookies() { + Cookies.remove(TokenKey) + Cookies.remove(ExpiresInKey) +} + export function getToken() { - return Cookies.get(TokenKey) + clearLegacyCookies() + return localStorage.getItem(TokenKey) } export function setToken(token) { - return Cookies.set(TokenKey, token) + clearLegacyCookies() + return localStorage.setItem(TokenKey, token) } export function removeToken() { - return Cookies.remove(TokenKey) + clearLegacyCookies() + return localStorage.removeItem(TokenKey) } + export function setExpiresIn(time) { - return Cookies.set(ExpiresInKey, time); -} \ No newline at end of file + clearLegacyCookies() + return localStorage.setItem(ExpiresInKey, time); +} diff --git a/src/utils/homePath.js b/src/utils/homePath.js new file mode 100644 index 0000000..04255c7 --- /dev/null +++ b/src/utils/homePath.js @@ -0,0 +1,5 @@ +const configuredHomePath = process.env.VUE_APP_HOME_PATH +const homePaths = ['/portal', '/index'] + +// 登录后的首页由打包模式决定,未配置或配置非法时进入门户 +export const homePath = homePaths.indexOf(configuredHomePath) >= 0 ? configuredHomePath : '/portal' diff --git a/src/utils/request.js b/src/utils/request.js index 5719f45..12366f9 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -10,6 +10,7 @@ import { saveAs } from 'file-saver' import { urlWhiteList } from '@/settings' import md5 from 'md5.js' import { isEmbeddedSession } from '@/utils/embeddedAuth' +import { homePath } from '@/utils/homePath' import Vue from 'vue' @@ -149,6 +150,17 @@ service.interceptors.response.use((res) => { console.log('[GAB] code: ' + code) if (code === 401) { isRelogin.show = false; + const currentPath = router.currentRoute && router.currentRoute.path + // iframe 嵌入页自己静默登录,401 不能把外层画面带到登录页或门户 + if (currentPath === "/video-preview-embed") { + return Promise.reject(new Error('无效的会话,或者会话已过期,请重新登录。')) + } + // 登录页会同时请求验证码。/code 不带 token,401 不能触发登出、静默登录或跳转, + // 否则会在登录成功跳往 /portal 之后把地址改掉。 + const requestUrl = (res.config && res.config.url) || "" + if (requestUrl === "/code" || requestUrl.indexOf("/code?") === 0) { + return Promise.reject(new Error('验证码获取失败')) + } if (isEmbeddedSession()) { // 外部系统跳转进入:宿主框架未提供本系统token,静默登录(默认普通账号)保证页面数据可用 const postData = new LoginEntity(); @@ -156,7 +168,12 @@ service.interceptors.response.use((res) => { postData.password = new md5().update("htjc@123").digest('hex') store.dispatch("Login", postData).then(() => { console.info("[GAB] Login then"); - router.push({ path: "/" }).catch(() => { }); + const loggedInPath = router.currentRoute && router.currentRoute.path + // 登录页自己会去首页;已经在首页上则不再改地址 + if (loggedInPath === homePath || loggedInPath === "/login") { + return + } + router.push({ path: homePath }).catch(() => { }); }) } else { // 本系统登录的用户:会话过期后回到登录页,不能悄悄换成别的账号 diff --git a/src/utils/stationTitle.js b/src/utils/stationTitle.js index fdd1d74..371632d 100644 --- a/src/utils/stationTitle.js +++ b/src/utils/stationTitle.js @@ -13,13 +13,13 @@ const stationConfigMap = { // 白杨坪站 "192.168.20.210": { titleImage: baiyangpingTitleImage, name: "白杨坪动环智能巡检系统" }, "192.168.134.10": { titleImage: baiyangpingTitleImage, name: "白杨坪动环智能巡检系统" }, - "192.168.129.2:32799": { titleImage: baiyangpingTitleImage, name: "白杨坪动环智能巡检系统" }, + "192.168.129.2:20001": { titleImage: baiyangpingTitleImage, name: "白杨坪动环智能巡检系统" }, // "192.168.1.90": { titleImage: xiaogaoshanTitleImage, name: "白杨坪智能巡检系统" }, // 白草坡站 "192.168.20.201": { titleImage: baicaopoTitleImage, name: "白草坡动环智能巡检系统" }, "192.168.147.13": { titleImage: baicaopoTitleImage, name: "白草坡动环智能巡检系统" }, - "192.168.129.2:20001": { titleImage: baicaopoTitleImage, name: "白草坡动环智能巡检系统" }, + "192.168.129.2:20002": { titleImage: baicaopoTitleImage, name: "白草坡动环智能巡检系统" }, // 小高山站 "192.168.20.222": { titleImage: xiaogaoshanTitleImage, name: "小高山动环智能巡检系统" }, diff --git a/src/views/cards/patrolReport/InspectionReporter.vue b/src/views/cards/patrolReport/InspectionReporter.vue index 52e0247..796656e 100644 --- a/src/views/cards/patrolReport/InspectionReporter.vue +++ b/src/views/cards/patrolReport/InspectionReporter.vue @@ -234,17 +234,18 @@ export default { }, ftpImageViewUrl(row, suffix) { // 添加安全检查 - if (!this.systemParams || !this.systemParams.ftpViewAddress) { - this.$modal.msgWarning("系统配置未加载,请稍后重试"); - console.error("systemParams为空:", this.systemParams); - return; - } + // if (!this.systemParams || !this.systemParams.ftpViewAddress) { + // this.$modal.msgWarning("系统配置未加载,请稍后重试"); + // console.error("systemParams为空:", this.systemParams); + // return; + // } console.log("使用FTP地址:", this.systemParams.ftpViewAddress); if (row.filePath) { var fileName = row.filePath.split(".")[0]; - const url = this.systemParams.ftpViewAddress + fileName + "." + suffix; + // const url = this.systemParams.ftpViewAddress + fileName + "." + suffix; + const url = row.filePath; console.log("导出URL:", url); const a = document.createElement("a"); a.setAttribute("href", url); diff --git a/src/views/cards/site_management/site_management.vue b/src/views/cards/site_management/site_management.vue index 2bab65a..59ddabe 100644 --- a/src/views/cards/site_management/site_management.vue +++ b/src/views/cards/site_management/site_management.vue @@ -1392,7 +1392,7 @@ import Algorith from "@/views/cards/site_management/algorithm.vue"; import ImageAnnotation from "./components/ImageAnnotation.vue"; // 设备树面板默认宽度(收窄后);存储 key 带 v2,避免老用户 localStorage 里的旧宽度盖住新默认 -const DEFAULT_LEFT_PANEL_WIDTH = 200; +const DEFAULT_LEFT_PANEL_WIDTH = 250; const LEFT_PANEL_WIDTH_STORAGE_KEY = "leftPanelWidthV2"; export default { @@ -3858,7 +3858,7 @@ export default { .search { width: 100%; position: relative; - margin-top: 0; + margin-top: 10px; margin-bottom: 10px; .el-input__inner { @@ -4010,7 +4010,7 @@ export default { gap: 12px; flex-shrink: 0; min-height: 34px; - padding-top: 3px; + padding-top: 10px; box-sizing: border-box; } diff --git a/src/views/login.vue b/src/views/login.vue index dff2312..e6d68e2 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -62,6 +62,7 @@ import { getCodeImg } from "@/api/login"; import Cookies from "js-cookie"; import { getToken } from "@/utils/auth"; +import { homePath } from "@/utils/homePath"; import { encrypt, decrypt } from '@/utils/jsencrypt' import md5 from 'md5.js' @@ -153,7 +154,7 @@ export default { this.$store.dispatch("Login", postData).then(() => { // 登录成功后清除手动登录标记,下次进入系统恢复免登录 sessionStorage.removeItem('manual-login'); - this.$router.push({ path: this.redirect || "/" }).catch(() => { }); + this.$router.push({ path: this.redirect || homePath }).catch(() => { }); }).catch(() => { this.loading = false; diff --git a/src/views/portal/index.vue b/src/views/portal/index.vue index 010a25c..a0e4a51 100644 --- a/src/views/portal/index.vue +++ b/src/views/portal/index.vue @@ -163,14 +163,12 @@
+ { - const station = this.stationList.find(item => String(item.id) === String(stationId)); - if (!station) return videos; - const matched = this.portalChannels.filter(video => String(video.stationId) === String(stationId)); - return videos.concat(matched); - }, []).slice(0, 4); + if (this.videoStationId === "" || this.videoStationId === null || this.videoStationId === undefined) { + return this.portalChannels.slice(0, 4); + } + const station = this.stationList.find(item => String(item.id) === String(this.videoStationId)); + if (!station) return []; + return this.portalChannels + .filter(video => String(video.stationId) === String(this.videoStationId)) + .slice(0, 4); }, portalVideoSlots() { return Array.from({ length: 4 }, (_, index) => this.videoList[index] || null); @@ -462,9 +461,10 @@ export default { trendQueryKey() { this.loadDefectTrend(); }, - videoStationIds(ids) { + videoStationId(id) { this.replacePortalVideos(this.filteredVideos); - if (ids && ids.length) this.openDeviceTree(); + this.loadChannelStats(); + if (id !== "" && id !== null && id !== undefined) this.openDeviceTree(); } }, mounted() { @@ -477,7 +477,7 @@ export default { }, methods: { selectMonitorStation(station) { - this.videoStationIds = [station.id]; + this.videoStationId = station.id; this.openDeviceTree(); }, openDeviceTree() { @@ -485,7 +485,7 @@ export default { this.treeVisible = true; }, isMonitorStationSelected(stationId) { - return this.videoStationIds.some(id => String(id) === String(stationId)); + return String(this.videoStationId) === String(stationId); }, selectTrendForStation(station, algCategory) { this.trendStationId = station.code; @@ -516,7 +516,10 @@ export default { }); }, loadChannelStats() { - return getChannelStats({}).then(response => { + const params = {}; + const station = this.stationList.find(item => String(item.id) === String(this.videoStationId)); + if (station && station.code) params.stationCode = station.code; + return getChannelStats(params).then(response => { const data = response && response.data ? response.data : {}; const counts = Array.isArray(data.deviceSourceCounts) ? data.deviceSourceCounts : []; const cameraCount = { ...EMPTY_CAMERA, total: toCount(data.totalCount) }; diff --git a/src/views/video/LivePreview.vue b/src/views/video/LivePreview.vue index 40403b7..e106c1a 100644 --- a/src/views/video/LivePreview.vue +++ b/src/views/video/LivePreview.vue @@ -1,8 +1,9 @@ @@ -168,6 +193,8 @@ + +