Browse Source

feat(maotu): 支持 App 内嵌组态预览免登录

- 接收 App 通过 URL fragment 传递的登录态和租户信息
- 组态预览路由跳过重复登录校验
- App 内切换组态时复用当前预览页面
Zimo 2 weeks ago
parent
commit
2689394e2a
3 changed files with 53 additions and 8 deletions
  1. 5 1
      src/permission.ts
  2. 40 0
      src/utils/auth.ts
  3. 8 7
      src/views/maotu/preview.vue

+ 5 - 1
src/permission.ts

@@ -12,6 +12,8 @@ import { usePermissionStoreWithOut } from '@/store/modules/permission'
 import * as LoginApi from '@/api/login'
 import * as authUtil from '@/utils/auth'
 
+authUtil.bootstrapAppWebtopoPreviewAuth()
+
 const { start, done } = useNProgress()
 
 const { loadStart, loadDone } = usePageLoading()
@@ -85,7 +87,9 @@ router.beforeEach(async (to, from, next) => {
   start()
   loadStart()
   if (getAccessToken()) {
-    if (to.path === '/login') {
+    if (authUtil.isAppWebtopoPreview() && to.path.startsWith('/maotu/preview/')) {
+      next()
+    } else if (to.path === '/login') {
       next({ path: '/' })
     } else if (to.fullPath.includes('portalLogin')) {
       // authUtil.removeToken()

+ 40 - 0
src/utils/auth.ts

@@ -7,12 +7,52 @@ const { wsCache: sessionCache } = useCache('sessionStorage')
 
 const AccessTokenKey = 'ACCESS_TOKEN'
 const RefreshTokenKey = 'REFRESH_TOKEN'
+const AppWebtopoPreviewKey = 'APP_WEBTOPO_PREVIEW'
 
 // 获取token
 export const getAccessToken = () => {
   return sessionCache.get(AccessTokenKey)
 }
 
+/**
+ * App 内嵌组态预览使用 URL fragment 传递短期登录态。
+ * fragment 不会发送到服务端,读取完成后立即从地址栏移除。
+ */
+export const bootstrapAppWebtopoPreviewAuth = () => {
+  if (typeof window === 'undefined' || !window.location.hash) {
+    return false
+  }
+
+  const params = new URLSearchParams(window.location.hash.slice(1))
+  if (params.get('appPreview') !== '1') {
+    return false
+  }
+
+  const accessToken = params.get('appAccessToken')
+  if (!accessToken) {
+    return false
+  }
+
+  sessionCache.set(AccessTokenKey, accessToken)
+  const tenantId = params.get('tenantId')
+  if (tenantId) {
+    localCache.set(CACHE_KEY.TenantId, tenantId)
+  }
+  sessionStorage.setItem(AppWebtopoPreviewKey, '1')
+  window.history.replaceState(
+    null,
+    document.title,
+    window.location.pathname + window.location.search
+  )
+  return true
+}
+
+export const isAppWebtopoPreview = () => {
+  return (
+    typeof sessionStorage !== 'undefined' && sessionStorage.getItem(AppWebtopoPreviewKey) === '1'
+  )
+}
+
 // 刷新token
 export const getRefreshToken = () => {
   return sessionCache.get(RefreshTokenKey)

+ 8 - 7
src/views/maotu/preview.vue

@@ -6,11 +6,7 @@
       type="button"
       aria-label="切换显示方向"
       @click="orientationSwitched = !orientationSwitched">
-      <svg
-        class="orientation-toggle__icon"
-        aria-hidden="true"
-        viewBox="0 0 24 24"
-        fill="none">
+      <svg class="orientation-toggle__icon" aria-hidden="true" viewBox="0 0 24 24" fill="none">
         <path
           d="M20 11a8 8 0 1 0-2.34 5.66M20 4v7h-7"
           stroke="currentColor"
@@ -27,6 +23,7 @@ import { WebtopoProjectApi, parseWebtopoDataModel } from '@/api/pms/maotu'
 import { IotDeviceApi } from '@/api/pms/device'
 import type { IExportJson } from '@/components/mt-edit/components/types'
 import { MtPreview } from '@/export'
+import { isAppWebtopoPreview } from '@/utils/auth'
 import { ElMessage } from 'element-plus'
 import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
@@ -171,8 +168,12 @@ const onEventCallBack = (type: string, item_id: string, targetProjectId?: number
   if (type == 'test-dialog') {
     ElMessage.success(`获取到了id:${item_id}`)
   } else if (type == 'openWebtopo' && targetProjectId) {
-    const routeInfo = router.resolve(`/maotu/preview/${targetProjectId}`)
-    window.open(routeInfo.href, '_blank')
+    if (isAppWebtopoPreview()) {
+      router.replace(`/maotu/preview/${targetProjectId}`)
+    } else {
+      const routeInfo = router.resolve(`/maotu/preview/${targetProjectId}`)
+      window.open(routeInfo.href, '_blank')
+    }
   }
 }