XieXing 4 meses atrás
pai
commit
3c0e26a2c8

+ 31 - 32
README.md

@@ -1,12 +1,13 @@
 # 部署与上线指南
 
-本文档详细说明如何在服务器的 `/tmp/tmp` 目录下拉取 `dev` 分支代码,并完成前后端的编译与生产环境部署。
+本文档详细说明如何在服务器的 `/tmp/tmp` 目录下拉取 `dev` 分支代码并完成前后端的编译与生产环境部署。
 
 ## 1. 环境准备
 
-### 方案A:服务器直接编译(需要安装 Go)
+### 方案A:服务器直接编译(需要安装 Go)
+
+确保服务器已安装以下环境:
 
-确保服务器已安装以下环境:
 - **Git**: 用于拉取代码
 - **Node.js** (≥ 20.19) & **npm**: 用于前端打包
 - **Go** (≥ 1.24): 用于后端编译
@@ -31,13 +32,13 @@ source ~/.bashrc
 go version
 ```
 
-### 方案B:本地编译上传(推荐,无需服务器安装 Go)
+### 方案B:本地编译上传(推荐,无需服务器安装 Go)
 
-在本地 Windows 环境编译好二进制文件,然后上传到服务器。详见后续步骤。
+在本地 Windows 环境编译好二进制文件然后上传到服务器。详见后续步骤。
 
 ## 2. 拉取代码
 
-进入目标目录并拉取远程 `dev` 分支代码:
+进入目标目录并拉取远程 `dev` 分支代码
 
 ```bash
 # 1. 创建并进入目录
@@ -45,19 +46,19 @@ mkdir -p /tmp/tmp
 cd /tmp/tmp
 
 # 2. 拉取代码
-# 情况A:如果是首次部署(目录为空)
+# 情况A:如果是首次部署(目录为空)
 git clone -b dev http://192.168.0.3:3000/ShuDao-SafeAI/ShuDaoMAIN.git .
 
-# 情况B:如果目录已存在且已有仓库
+# 情况B如果目录已存在且已有仓库
 # git checkout dev
 # git pull origin dev
 ```
 
-> **注意**:请根据实际情况替换 git 仓库地址。
+> **注意**请根据实际情况替换 git 仓库地址。
 
 ## 3. 前端编译 (shudao-vue-frontend)
 
-进入前端目录,安装依赖并打包:
+进入前端目录,安装依赖并打包:
 
 ```bash
 # 1. 进入前端目录
@@ -70,11 +71,11 @@ npm install --registry=https://registry.npmmirror.com
 npm run build
 ```
 
-构建完成后,会在 `shudao-vue-frontend/dist` 目录下生成静态资源(包含 `assets` 目录和 `index.html`)
+构建完成后,会在 `shudao-vue-frontend/dist` 目录下生成静态资源(包含 `assets` 目录和 `index.html`)
 
 ## 4. 整合产物到后端
 
-将前端生成的静态资源复制到后端对应的目录中:
+将前端生成的静态资源复制到后端对应的目录中
 
 ```bash
 # 1. 回到项目根目录
@@ -83,7 +84,7 @@ cd ..
 # 2. 进入后端目录
 cd shudao-go-backend
 
-# 3. 清理旧资源(可选,建议执行以避免缓存问题)
+# 3. 清理旧资源(可选,建议执行以避免缓存问题)
 rm -rf assets
 rm -f views/index.html
 
@@ -95,29 +96,30 @@ cp -r ../shudao-vue-frontend/dist/assets .
 cp ../shudao-vue-frontend/dist/index.html views/
 ```
 
-> **说明**:
-> - `assets/` 目录存放 JS/CSS/图片等静态资源,后端通过 `beego.SetStaticPath("/assets", "assets")` 进行映射。
-> - `views/index.html` 是前端入口页面,后端控制器会渲染此模板。
+> **说明**:
+>
+> - `assets/` 目录存放 JS/CSS/图片等静态资源,后端通过 `beego.SetStaticPath("/assets", "assets")` 进行映射。
+> - `views/index.html` 是前端入口页面,后端控制器会渲染此模板。
 
 ## 5. 后端编译与打包 (shudao-go-backend)
 
-### 方法1:使用 Beego bee 工具打包(推荐)
+### 方法1:使用 Beego bee 工具打包(推荐)
 
 ```bash
 # 1. 进入后端目录
 cd shudao-go-backend
 
-# 2. 安装 bee 工具(如果未安装)
+# 2. 安装 bee 工具(如果未安装)
 go install github.com/beego/bee/v2@latest
 
-# 3. 使用 bee pack 打包(会自动交叉编译为 Linux 并打包所有资源)
+# 3. 使用 bee pack 打包(会自动交叉编译为 Linux 并打包所有资源)
 bee pack -be GOOS=linux -be GOARCH=amd64
 
-# 4. 生成的压缩包:shudao-go-backend.tar.gz
-# 将此文件上传到服务器 /opt/www/shudao_main/shudao-go-backend/ 目录
+# 4. 生成的压缩包shudao-go-backend.tar.gz
+# 将此文件上传到服务器 /tmp/tmp 目录
 ```
 
-### 方法2:直接使用 go build 编译
+### 方法2直接使用 go build 编译
 
 ```bash
 # 1. 进入后端目录
@@ -138,9 +140,9 @@ GOOS=linux GOARCH=amd64 go build -o shudao-go-backend main.go
 
 ```bash
 # 1. 进入部署目录
-cd /opt/www/shudao_main/shudao-go-backend
+cd /tmp/tmp
 
-# 2. 解压(如果使用 bee pack)
+# 2. 解压(如果使用 bee pack)
 tar -xzf shudao-go-backend.tar.gz
 
 # 3. 进入后端目录
@@ -149,24 +151,21 @@ cd shudao-go-backend
 # 4. 赋予执行权限
 chmod +x shudao-go-backend
 
-# 5. 停止旧服务(如果存在)
+# 5. 停止旧服务(如果存在)
 ps -ef | grep shudao-go-backend
-# 找到进程ID后执行:kill -9 <PID>
+# 找到进程ID后执行kill -9 <PID>
 
 # 6. 启动新服务 (后台运行)
-nohup ./shudao-go-backend > nohup.out 2>&1 &
-
-# 7. 查看启动日志
-tail -f nohup.out
+nohup ./shudao-go-backend > nohup.out 2>&1 &tail -f nohup.out
 ```
 
 ## 6. 验证部署
 
-- 检查进程是否存在:
+- 检查进程是否存在
   ```bash
   ps -ef | grep shudao-go-backend
   ```
-- 访问服务地址,确认页面加载正常且 API 调用成功。
+- 访问服务地址确认页面加载正常且 API 调用成功。
 
 ---
 

+ 1 - 1
shudao-go-backend/controllers/hazard.go

@@ -195,7 +195,7 @@ func (c *HazardController) Hazard() {
 	if len(yoloResp.Labels) == 0 {
 		c.Data["json"] = HazardResponse{
 			Code: 500,
-			Msg:  "没有识别到任何隐患",
+			Msg:  "当前照片与场景不匹配",
 		}
 		c.ServeJSON()
 		return

+ 10 - 4
shudao-vue-frontend/src/components/MobileHeader.vue

@@ -1,7 +1,7 @@
 <template>
   <header class="mobile-header">
-    <button class="back-button" @click="goHome">
-      <img src="@/assets/index/2.jpg" alt="回到首页" class="back-icon">
+    <button class="back-button" @click="goBack">
+      <img src="@/assets/index/2.jpg" alt="回" class="back-icon">
     </button>
     <h1 class="page-title">{{ title }}</h1>
     <button v-if="showMenu" class="hamburger-btn" @click="$emit('menu')">
@@ -26,8 +26,14 @@ defineProps({
 })
 
 const router = useRouter()
-const goHome = () => {
-  router.push('/')
+const goBack = () => {
+  // 检查是否有历史记录
+  if (window.history.length > 1) {
+    router.go(-1)
+  } else {
+    // 如果没有历史记录,返回首页
+    router.push('/')
+  }
 }
 </script>
 

+ 84 - 37
shudao-vue-frontend/src/components/MobilePdfViewer.vue

@@ -2,13 +2,18 @@
   <div class="mobile-pdf-viewer">
     <div v-if="loading" class="loading-container">
       <div class="loading-spinner"></div>
-      <div class="loading-text">正在加载文档...</div>
+      <div class="loading-text">
+        {{ progress > 0 ? `正在加载文档... ${progress}%` : '正在加载文档...' }}
+      </div>
+      <div v-if="retryCount > 0" class="retry-info">
+        重试中 ({{ retryCount }}/{{ maxRetries }})
+      </div>
     </div>
     
-    <div v-if="error" class="error-container">
+    <div v-if="error && !loading" class="error-container">
       <div class="error-icon">!</div>
       <div class="error-text">{{ error }}</div>
-      <button class="retry-btn" @click="loadPdf">重试</button>
+      <button class="retry-btn" @click="() => { retryCount = 0; loadPdf(true); }">重试</button>
     </div>
 
     <div ref="pdfContainer" class="pdf-container">
@@ -22,11 +27,7 @@ import { ref, onMounted, watch, onUnmounted } from 'vue';
 import * as pdfjsLib from 'pdfjs-dist';
 
 // 设置 worker
-// 注意:在 Vite 中,我们需要正确引入 worker
-// 使用 CDN 或者本地构建的 worker
-// 这里尝试使用 import 方式,如果失败则回退到 CDN
 try {
-  // 尝试动态导入 worker
   const workerUrl = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).href;
   pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl;
 } catch (e) {
@@ -46,14 +47,32 @@ const error = ref(null);
 const progress = ref(0);
 const pdfContainer = ref(null);
 const currentUrl = ref('');
+const isLoading = ref(false); // 防止重复加载的标志
+const retryCount = ref(0); // 重试次数
+const maxRetries = 3; // 最大重试次数
 let pdfDoc = null;
 
-const loadPdf = async () => {
-  if (!props.url) return;
-  // 如果正在加载当前URL,或者是已加载完成的URL,则跳过
-  if (loading.value && props.url === currentUrl.value) return;
+const loadPdf = async (isRetry = false) => {
+  if (!props.url) {
+    console.log('📄 [MobilePdfViewer] 没有URL,跳过加载');
+    return;
+  }
+  
+  // 防止重复加载:如果正在加载相同的URL,直接返回
+  if (isLoading.value && props.url === currentUrl.value) {
+    console.log('📄 [MobilePdfViewer] 正在加载相同URL,跳过重复请求:', props.url);
+    return;
+  }
+  
+  // 如果URL没有变化且已经加载完成,跳过
+  if (!isRetry && props.url === currentUrl.value && !loading.value && !error.value) {
+    console.log('📄 [MobilePdfViewer] URL未变化且已加载完成,跳过:', props.url);
+    return;
+  }
   
+  console.log('📄 [MobilePdfViewer] 开始加载PDF:', props.url);
   currentUrl.value = props.url;
+  isLoading.value = true;
   loading.value = true;
   error.value = null;
   progress.value = 0;
@@ -68,46 +87,65 @@ const loadPdf = async () => {
     
     loadingTask.onProgress = (p) => {
       if (p.total > 0) {
-        progress.value = p.loaded / p.total;
+        progress.value = Math.floor((p.loaded / p.total) * 100);
       }
     };
 
     pdfDoc = await loadingTask.promise;
-    console.log(`PDF 加载成功,共 ${pdfDoc.numPages} 页`);
+    console.log(`📄 [MobilePdfViewer] PDF 加载成功,共 ${pdfDoc.numPages} 页`);
     
-    // 渲染所有页面
-    for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) {
-      await renderPage(pageNum);
+    // 分块加载策略:先加载第一页快速显示,然后逐页加载剩余页面
+    if (pdfDoc.numPages > 0) {
+      // 第一页优先加载
+      await renderPage(1);
+      progress.value = Math.floor((1 / pdfDoc.numPages) * 100);
+      
+      // 加载剩余页面
+      for (let pageNum = 2; pageNum <= pdfDoc.numPages; pageNum++) {
+        await renderPage(pageNum);
+        progress.value = Math.floor((pageNum / pdfDoc.numPages) * 100);
+      }
     }
     
     loading.value = false;
+    isLoading.value = false;
+    retryCount.value = 0; // 重置重试次数
+    console.log('📄 [MobilePdfViewer] PDF 加载完成');
   } catch (err) {
-    console.error('PDF 加载失败:', err);
-    error.value = '文档加载失败,请稍后重试';
-    loading.value = false;
+    console.error('📄 [MobilePdfViewer] PDF 加载失败:', err);
+    isLoading.value = false;
+    
+    // 错误重试机制
+    if (retryCount.value < maxRetries) {
+      retryCount.value++;
+      console.log(`📄 [MobilePdfViewer] 准备第 ${retryCount.value} 次重试...`);
+      error.value = `加载失败,正在重试 (${retryCount.value}/${maxRetries})...`;
+      
+      // 指数退避策略:等待时间随重试次数增加
+      const delay = Math.min(1000 * Math.pow(2, retryCount.value - 1), 5000);
+      setTimeout(() => {
+        loadPdf(true);
+      }, delay);
+    } else {
+      error.value = '文档加载失败,请点击重试按钮';
+      loading.value = false;
+      retryCount.value = 0;
+    }
   }
 };
 
 const renderPage = async (pageNum) => {
-  if (!pdfDoc) return; // 确保文档存在
+  if (!pdfDoc) return;
   
   try {
     const page = await pdfDoc.getPage(pageNum);
-    
-    // 计算缩放比例,使页面宽度适应屏幕
-    const containerWidth = pdfContainer.value ? pdfContainer.value.clientWidth : window.innerWidth;
-    // 默认使用 1.5 倍缩放以获得更好的清晰度,然后通过 CSS 缩小
     const viewport = page.getViewport({ scale: 1.5 });
     
-    // 创建 canvas
     const canvas = document.createElement('canvas');
     const context = canvas.getContext('2d');
     
-    // 设置 canvas 尺寸
     canvas.height = viewport.height;
     canvas.width = viewport.width;
-    
-    // 设置 CSS 样式以适应容器宽度
     canvas.style.width = '100%';
     canvas.style.height = 'auto';
     canvas.style.display = 'block';
@@ -118,7 +156,6 @@ const renderPage = async (pageNum) => {
       pdfContainer.value.appendChild(canvas);
     }
     
-    // 渲染页面
     const renderContext = {
       canvasContext: context,
       viewport: viewport
@@ -126,29 +163,32 @@ const renderPage = async (pageNum) => {
     
     await page.render(renderContext).promise;
   } catch (err) {
-    console.error(`渲染第 ${pageNum} 页失败:`, err);
+    console.error(`📄 [MobilePdfViewer] 渲染第 ${pageNum} 页失败:`, err);
   }
 };
 
+// 监听URL变化,只在真正变化时重新加载
 watch(() => props.url, (newUrl, oldUrl) => {
-  if (newUrl !== oldUrl) {
+  if (newUrl && newUrl !== oldUrl) {
+    console.log('📄 [MobilePdfViewer] URL变化,重新加载:', { oldUrl, newUrl });
+    retryCount.value = 0; // 重置重试次数
     loadPdf();
   }
 });
 
 onMounted(() => {
-  // 稍微延迟加载,确保容器已准备好
-  setTimeout(() => {
-    if (props.url) {
-      loadPdf();
-    }
-  }, 100);
+  // 组件挂载后立即检查并加载
+  if (props.url) {
+    console.log('📄 [MobilePdfViewer] 组件挂载,开始加载');
+    loadPdf();
+  }
 });
 
 onUnmounted(() => {
   if (pdfDoc) {
     pdfDoc.destroy();
   }
+  isLoading.value = false;
 });
 </script>
 
@@ -191,6 +231,13 @@ onUnmounted(() => {
 .loading-text {
   color: #6b7280;
   font-size: 12px;
+  margin-bottom: 4px;
+}
+
+.retry-info {
+  color: #f59e0b;
+  font-size: 11px;
+  font-weight: 500;
 }
 
 .error-container {

+ 77 - 51
shudao-vue-frontend/移动客户端与H5对接规范.md

@@ -1,51 +1,77 @@
-移动客户端与H5对接规范
-一、概述
-在移动互联网技术发展成熟的今天,为了更好的满足app快速研发、及时更新、模块分离等需要;在原生应用中集成H5页面也越来越重要和常见。因此需要制定良好的规范以提高软件整体质量及研发效率。
-二、集成形式及系统环境
-(一)集成形式
-简单来说,就是把H5放到客户端中加载。为了更好的提升体验,客户端要保证H5容器的稳定和性能,
-客户端需要为H5提供加载容器及基本的能力支持,如:上传文件、定位、错误处理。
-(二)宿主系统环境
-Android:最低支持Android 8.0 系统,浏览器内核为Chrome;
-iOS:最低支持iOS 13系统,浏览器内核为WebKit。
-针对上述宿主环境,H5业务系统需要做兼容性适配。
-三、集成要求
-(一)单点登录
-1.app访问H5业务系统时,首先调用该系统接口获取授权token,然后按照该业务系统要求携带相关参数访问业务系统h5链接,实现单点登录。
-2.针对业务系统无权限用户,需要返回给app单独的错误码和错误提示信息。
-3.如使用4a作为认证,在访问业务系统时,无权限账户要进行友好提示。
-(二)导航栏设计
-1.建议使用app原生导航栏,app会监听h5页面标题的变化并进行展示。
-2.如受到业务系统限制不能使用app原生导航栏,需要调用交互关闭导航栏。
-3.H5页面自定义导航栏的情况下,需要适配安全区并且需要增加关闭原生页面的交互。
-(三) H5与app交互
-为了保证业务系统功能完善、性能优良,对接时往往需要支持多种交互。
-3.1 返回上一级与关闭页面
-在使用app原生导航栏的情况下,建议同时添加“返回webGoBack()”和“关闭页面(nativeClosePage())”这两个交互来实现返回上一级、关闭页面的功能;如业务系统评估不需要增加,则app根据webview返回栈调用返回和关闭。
-3.2 当前支持的交互
-当前支持的交互内容如下表所列,H5业务系统可以按照需要使用;如需要增加新的交互,可协商添加。
-移动端集成H5原生交互协议
-序号	功能	交互名称	动作(需求)表述	交互集成建议	备注
-1
-
-
-	返回与关闭
-	webGoBack()	H5提供JS返回方法,供原生调用	建议集成
-	 点击原生返回按钮时调用,由h5执行返回逻辑
-		finishPage()	JS调用原生交互关闭当前页面	建议集成
-	
-2
-	原生导航控制
-	showNativeNav(show)
-	JS调用原生方法关闭/显示导航栏	按需	参数show:0隐藏,1显示
-3
-	下载文件	downloadFile(url)
-	JS调用原生方法下载文件;下载后自动预览	按需
-	对于H5不支持查看(或需要下载)的文件,需要通知原生进行下载查看。参数url:下载地址的全路径。
-4	扫描二维码	startScan() 	JS调用原生方法扫描二维码	按需	 
-5		setScanResult(String result)	原生将扫码结果传递给h5	按需	
-6	请求定位权限
-	requestLocPerm()
-	JS调用原生方法请求获取定位权限	按需	
-		getLocationCallback()	原生通知定位权限获取成功	按需	
-7	打电话	callPhone(tel)	JS调用原生方法拨打电话	按需	
+
+# 移动客户端与 H5 对接规范
+
+## 一、概述
+
+在移动互联网技术成熟落地的今天,为满足 App **快速研发、及时更新、模块分离** 等需求,在原生应用中集成 H5 页面已成为常态。
+制定并遵循统一的对接规范,可显著提升软件质量与研发效率。
+
+---
+
+## 二、集成形式及系统环境
+
+### (一)集成形式
+
+- 将 H5 页面内嵌至客户端 WebView 中加载。
+- 客户端需保证 H5 容器的**稳定性与高性能**,并提供以下基础能力:
+  - 文件上传
+  - 定位
+  - 错误处理
+
+### (二)宿主系统环境
+
+| 平台    | 最低系统版本 | 浏览器内核 |
+| ------- | ------------ | ---------- |
+| Android | 8.0          | Chrome     |
+| iOS     | 13           | WebKit     |
+
+&gt; H5 业务系统须针对以上宿主环境做兼容性适配。
+
+---
+
+## 三、集成要求
+
+### (一)单点登录
+
+1. App 访问 H5 业务系统时,先调用后台接口获取授权 `token`。随后按业务方约定携带相关参数访问 H5 链接,完成单点登录。
+2. 对无权限用户,业务系统须返回**专用错误码**及**友好提示**。
+3. 若采用 4A 统一认证,无权限账号需给予明确、友好提示。
+
+---
+
+### (二)导航栏设计
+
+| 场景         | 建议方案                     | 补充说明                                                         |
+| ------------ | ---------------------------- | ---------------------------------------------------------------- |
+| 常规场景     | 使用**App 原生导航栏** | App 监听 `document.title` 变化并同步展示                       |
+| 受限场景     | 必须调用 JS 关闭原生导航栏   | 需保证隐藏后 UI 无遮挡                                           |
+| 自定义导航栏 | H5 自行实现                  | 1. 适配安全区&lt;br&gt;2. 提供 **关闭当前原生页面** 的交互 |
+
+---
+
+### (三)H5 与 App 交互
+
+#### 3.1 返回 / 关闭页面
+
+- **使用原生导航栏时**建议 H5 同时暴露以下两个方法,供原生调用:
+  - `webGoBack()` —— H5 自行处理返回逻辑
+  - `nativeClosePage()` —— 关闭当前 WebView
+    若业务评估无需暴露,则 App 按 WebView 返回栈自动处理。
+
+#### 3.2 当前支持的交互协议
+
+| 序号 | 功能         | 交互名称                  | 动作描述                                | 集成建议           | 备注                        |
+| ---- | ------------ | ------------------------- | --------------------------------------- | ------------------ | --------------------------- |
+| 1    | 返回与关闭   | `webGoBack()`           | H5 提供 JS 返回方法,供原生返回按钮调用 | **建议集成** | 由 H5 执行返回逻辑          |
+|      |              | `finishPage()`          | JS 调用原生关闭当前页面                 | **建议集成** | —                          |
+| 2    | 原生导航控制 | `showNativeNav(show)`   | JS 调用原生隐藏/显示导航栏              | 按需               | `show=0` 隐藏,`1` 显示 |
+| 3    | 下载文件     | `downloadFile(url)`     | JS 调用原生下载并自动预览               | 按需               | url 为完整下载地址          |
+| 4    | 扫描二维码   | `startScan()`           | JS 调用原生扫码                         | 按需               | —                          |
+|      |              | `setScanResult(result)` | 原生将扫码结果回传 H5                   | 按需               | —                          |
+| 5    | 请求定位权限 | `requestLocPerm()`      | JS 调用原生申请定位权限                 | 按需               | —                          |
+|      |              | `getLocationCallback()` | 原生通知权限获取成功                    | 按需               | —                          |
+| 6    | 打电话       | `callPhone(tel)`        | JS 调用原生拨号                         | 按需               | tel 为电话号码              |
+
+&gt; 如需新增交互,可经评审后扩展。
+
+---