浏览代码

优化局域网打印

“shengjie.huang” 1 年之前
父节点
当前提交
138f4796da

+ 1 - 1
src/interceptors/request.ts

@@ -43,7 +43,7 @@ const httpInterceptor = {
       // TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
     }
     // 1. 请求超时
-    options.timeout = 20000 // 20s
+    options.timeout = options.timeout || 20000 // 20s
     // 2. (可选)添加小程序端请求头标识
     options.header = {
       platform, // 可选,与 uniapp 定义的平台一致,告诉后台来源

+ 7 - 0
src/pages.json

@@ -103,6 +103,13 @@
         "navigationBarTitleText": "打印任务详情"
       }
     },
+    {
+      "path": "pages/print/jobForIntranet",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "打印任务列表"
+      }
+    },
     {
       "path": "pages/printer/index",
       "type": "page",

+ 22 - 7
src/pages/assistant/index.vue

@@ -36,6 +36,7 @@
               <wd-tag :type="item.onlineStatus == 'online' ? 'success' : 'danger'" mark custom-class="ml-2">
                 {{ getLabel(item.onlineStatus || 'offline', onlineStatus) }}
               </wd-tag>
+              <wd-tag v-if="item.isIntranet" type="primary" mark custom-class="ml-2">局域网</wd-tag>
             </view>
             <view class="sub-text">
               {{ `添加方式: ${getLabel(item.addType, typeList)}` }}
@@ -70,7 +71,7 @@
 
 <script lang="ts" setup>
 import { useMessage, useToast } from 'wot-design-uni'
-import { getUserHubPage, deleteUserHub } from '@/service/api'
+import { getUserHubPage, deleteUserHub, testByIP } from '@/service/api'
 import { getLabel } from '@/utils'
 import { debounce } from 'wot-design-uni/components/common/util'
 
@@ -131,6 +132,16 @@ function getDataList() {
         } else {
           dataList.value.push(...(res.body.records || []))
         }
+        // 检查是否局域网
+        dataList.value.forEach(item => {
+          if (item.intranetIp) {
+            item.ipList = item.intranetIp.split(",")
+            // 调用test接口判断是否通
+            item.ipList.forEach(ip => {
+              checkIP(ip, item.id)
+            });
+          }
+        })
       }
     })
     .catch((e) => {})
@@ -139,6 +150,15 @@ function getDataList() {
     })
 }
 
+// 根据打印助手的内网ip字段"intranetIp" 检查是否局域网 (采用局域网接口)
+function checkIP(ip, userHubId) {
+  testByIP(ip).then((res: any) => {
+    if (res.code == 200) {
+      dataList.value.find(i => i.id == userHubId).isIntranet = true
+    }
+  }).catch(() => {})
+}
+
 const onSearch = debounce(() => {
   queryParams.pageNo = 1
   getDataList()
@@ -178,15 +198,10 @@ function handleDelete() {
 // 跳转到打印机列表
 function toPrinter() {
   uni.navigateTo({
-    url: `/pages/printer/index?id=${currentData.value.id || ''}&asname=${currentData.value.asname}`,
+    url: `/pages/printer/index?id=${currentData.value.id || ''}&asname=${currentData.value.asname}&ip=${currentData.value.isIntranet && currentData.value.ipList[0]}`,
   })
 }
 
-// TODO: 根据打印助手的内网ip字段"intranetIp" 检查是否局域网 (采用局域网接口)
-function checkStatus() {
-
-}
-
 function showActions(data: any) {
   actionsTitle.value = data.asname ? `打印助手: ${data.asname}` : '操作'
   currentData.value = data || {}

+ 84 - 21
src/pages/print/index.vue

@@ -148,7 +148,14 @@
 
 <script lang="ts" setup>
 import { useToast, useMessage } from 'wot-design-uni'
-import { getUserHubPrints, getUserHubAttr, getInvoiceBatch } from '@/service/api'
+import {
+  getUserHubPrints,
+  getUserHubAttr,
+  getInvoiceBatch,
+  getUserHubPage,
+  testByIP,
+  getAttrByLocal
+} from '@/service/api'
 import { getEnvBaseUrl, redirectToUpload, reLaunchToHome } from '@/utils'
 import { useUserStore } from '@/store'
 
@@ -194,17 +201,62 @@ const srcMap = {
   'txt': txtSrc,
   'pdf': pdfSrc,
 }
-
-// 上传成功的任务
-const successPrint = ref([])
 // 上传失败的任务
 const failedFiles = ref([])
+// 助手列表
+const userHubList = ref([])
+// 判断环境是否局域网
+const isLocal = ref(false)
+const intranetIp = ref("")
 
 defineOptions({
   name: 'Print',
 })
 
-// 获取打印机
+// 根据打印助手的内网ip字段"intranetIp" 检查是否局域网 (采用局域网接口)
+function checkUserHub() {
+  let params = {
+    pageNo: 1,
+    pageSize: 999,
+  }
+  getUserHubPage(params).then((res: any) => {
+    if (res.code === 0 && res.body) {
+      userHubList.value = (res.body.records || [])
+      userHubList.value.forEach(item => {
+        item.isIntranet = false
+        if (item.intranetIp) {
+          item.ipList = item.intranetIp.split(',')
+          // 调用test接口判断是否通
+          item.ipList.forEach(ip => {
+            checkIP(ip, item.id)
+          });
+        }
+      });
+    }
+  }).catch(() => {
+  })
+}
+
+// 检查传入IP是否有连通, 标志为局域网助手
+function checkIP(ip, userHubId) {
+  testByIP(ip).then((res: any) => {
+    if (res.code == 200) {
+      userHubList.value.find(i => i.id == userHubId).isIntranet = true
+      updateShowName()
+    }
+  }).catch(() => {})
+}
+
+// 更新打印机展示名称
+function updateShowName() {
+  printerList.value.forEach(item => {
+    if (item.userHubId && userHubList.value.find(i => i.id == item.userHubId).isIntranet && !item.showName.startsWith('【局域网】')) {
+      item.showName = `【局域网】 ${item.showName}`
+    }
+  })
+}
+
+// 获取打印机 (兼容远程+局域网, 打印机均可选择, 只是选择局域网打印机的处理逻辑不同)
 function getPrinterList() {
   let params = { id: "" }
   getUserHubPrints(params)
@@ -219,20 +271,29 @@ function getPrinterList() {
     .catch((e) => {})
 }
 
-// 处理更换打印机
+// 处理更换打印机, 获取配置参数
 function handlePrinterChange(item: any) {
+  let obj = printerList.value.find(i => i.id == item.value) || {}
+  // 判断所选打印机是否局域网
+  let matchUserHub = userHubList.value.find(i => i.id == obj.userHubId) || {}
+  isLocal.value = matchUserHub.isIntranet
+  intranetIp.value = matchUserHub.isIntranet ? matchUserHub.ipList[0] : ''
+  // 构造打印选项参数
   printerOptions.value = []
   formData.value = {
     printer: item.value
   }
-  let obj = printerList.value.find(i => i.id == item.value) || {}
   let params = {
+    ip: intranetIp.value, // 用于局域网打印
     id: obj.userHubId,
     printer: obj.name,
   }
-  getUserHubAttr(params)
+  const getFn = () => {
+    return isLocal.value ? getAttrByLocal : getUserHubAttr
+  }
+  getFn()(params)
     .then((res: any) => {
-      if (res.code === 0 && res.body) {
+      if ([0, 200].includes(res.code) && res.body) {
         printerOptions.value = res.body || []
         let _formData = {
           printer: item.value,
@@ -524,16 +585,19 @@ function handlePrint(filePath, onProgress) {
     const queryParams = Object.entries(params).map(([key, value]) => {
       return `${key}=${encodeURIComponent(value)}`;
     }).join('&');
-
+    // 上传接口地址, 判断是否局域网
+    const getURL = () => {
+      return isLocal.value ? `http://${intranetIp.value}:5002/api/print-file?${queryParams}` : `${baseUrl}/sys/wx/userHub/print?${queryParams}`
+    }
     const uploadTask = uni.uploadFile({
-      url: `${baseUrl}/sys/wx/userHub/print?${queryParams}`, // 上传接口地址
+      url: getURL(),
       filePath: filePath,
       name: 'file', // 文件对应的 key
       formData: {
         // 其他表单数据
       },
       success: (res) => {
-        if (res.statusCode === 200 && JSON.parse(res.data).code === 0) {
+        if (res.statusCode === 200 && ((isLocal.value && JSON.parse(res.data).code === 200) || (!isLocal.value && JSON.parse(res.data).code === 0))) {
           resolve(res.data);
         } else {
           reject(new Error(`上传失败,状态码: ${res.statusCode}`));
@@ -613,15 +677,13 @@ function base64ToTempFilePath(fileName, base64Data, success, fail) {
 }
 
 // 打开SSE webview页面
-function openWebViewSSE() {
-  // let list = JSON.stringify(fileList.value.map(item => item.name).join(",") || "")
-  console.log('list: ', fileList.value);
-  let list = JSON.stringify(fileList.value)
-  const url = `https://service.1ai.ltd/webview-sse/index.html?token=${token}&list=${list}&t=${new Date().getTime()}`
-  uni.navigateTo({
-    url: `/pages/webview/sse?url=${encodeURIComponent(url)}`,
-  })
-}
+// function openWebViewSSE() {
+//   let list = JSON.stringify(fileList.value.map(item => item.name).join(",") || "")
+//   const url = `https://service.1ai.ltd/webview-sse/index.html?token=${token}&list=${list}&t=${new Date().getTime()}`
+//   uni.navigateTo({
+//     url: `/pages/webview/sse?url=${encodeURIComponent(url)}`,
+//   })
+// }
 
 onLoad((option) => {
   if (option && option.accept) {
@@ -643,6 +705,7 @@ onLoad((option) => {
     }
   }
   getPrinterList()
+  checkUserHub()
 })
 </script>
 

+ 33 - 65
src/pages/print/job.vue

@@ -62,12 +62,8 @@
                 <view class="flex-1">xxx</view>
               </view> -->
             </view>
-            <!-- <view class="opt" @click.stop="showActions(item)">
-              <wd-button type="icon" icon="more"></wd-button>
-            </view> -->
           </view>
           <view class="item-opt">
-            <!-- <view class="opt-btn">详情</view> -->
             <view v-if="item.reprint == '1'" class="opt-btn" @click.stop="handleReprint(item.id)">重打</view>
             <view v-if="['3', '4', '5', '6'].includes(item.jobStatus)" class="opt-btn" @click.stop="handleCancel(item.id)">取消</view>
             <view class="opt-btn delete-btn" @click.stop="handleDelete(item)">删除</view>
@@ -78,15 +74,6 @@
       </template>
       <wd-status-tip v-else image="content" tip="暂无内容" />
     </view>
-    
-    <!-- 动作面板 -->
-    <wd-action-sheet
-      v-model="isShowActions"
-      :actions="currentActions"
-      :title="actionsTitle"
-      @close="isShowActions = false"
-      @select="selectActions"
-    />
   </view>
 </template>
 
@@ -131,13 +118,8 @@ const jobStatus: any = ref([
   { label: '已完成', value: '9', color: 'success' },
 ])
 const dataList = ref([])
-const currentData: any = ref({})
 // 加载中: loading; 没有数据: finished; 错误: error;
 const loadStatus: any = ref('loading')
-// 动作面板
-const isShowActions = ref(false)
-const actionsTitle = ref('')
-const currentActions: any = ref([{ loading: true }])
 const isToTop = ref(false)
 // 文件类型
 const imageSrc = '/static/images/image.png'
@@ -149,6 +131,14 @@ const txtSrc = '/static/images/txt.png'
 const otherSrc = '/static/images/other.png'
 // websocket连接对象
 const socketTask = ref(null)
+// 是否正在重连
+const isReconnecting = ref(false)
+// 当前重连次数
+const reconnectAttempts = ref(0)
+// 最大重连次数
+const maxReconnectAttempts = ref(5)
+// 重连间隔时间
+const reconnectInterval = 1000
 
 defineOptions({
   name: 'Job',
@@ -191,16 +181,6 @@ function getFileType(fileName) {
   return src
 }
 
-// 根据状态 构造操作面板
-function buildCurrentActions() {
-  currentActions.value = [
-    { name: '查看' },
-    { name: '重打' },
-    { name: '取消' },
-    { name: '删除', color: '#fa4350' }
-  ]
-}
-
 // 获取列表数据
 function getDataList() {
   loadStatus.value = 'loading'
@@ -294,32 +274,6 @@ function handleDelete(item) {
     })
 }
 
-// 显示操作
-function showActions(data: any) {
-  actionsTitle.value = data.title ? `打印任务: ${data.title}` : '操作'
-  currentData.value = data || {}
-  buildCurrentActions()
-  isShowActions.value = true
-}
-
-// 处理操作
-function selectActions(data: any) {
-  switch (data.item.name) {
-    case '查看':
-      toDetail(currentData.value)
-      break
-    case '重打':
-      handleReprint(currentData.value.id)
-      break
-    case '取消':
-      handleCancel(currentData.value.id)
-      break
-    case '删除':
-      handleDelete(currentData.value)
-      break
-  }
-}
-
 // 查询列表, 滚动到顶部, 返回第一页
 function resetDataList() {
   uni.pageScrollTo({
@@ -378,8 +332,7 @@ onReachBottom(() => {
 // 创建websocket连接
 function initWebSocket() {
   socketTask.value = uni.connectSocket({
-    // url: `${socketBaseUrl}/print/ws`,
-    url: `wss://service.1ai.ltd/maoer-api/print/ws`,
+    url: `${socketBaseUrl}/print/ws`,
     header: {
       'token': token
     },
@@ -388,6 +341,7 @@ function initWebSocket() {
     },
     fail: (err) => {
       console.log("连接失败: ", err);
+      handleReconnect()
     }
   })
 
@@ -428,13 +382,10 @@ function initWebSocket() {
     console.error('WebSocket 发生错误', err);
   });
 
-  // 监听 WebSocket 连接关闭事件
-  socketTask.value.onClose(() => {
-    console.log('WebSocket 连接已关闭');
-    // 重连机制
-    setTimeout(() => {
-      initWebSocket()
-    }, 3000)
+  // 监听 WebSocket 连接关闭事件 (线上存在一分钟就断开重连的问题)
+  socketTask.value.onClose((res) => {
+    console.log('WebSocket 连接已关闭:', res); // 异常断开 {code: 1006, reason: "abnormal closure"}
+    if (res.code !== 1000) handleReconnect()
   });
 }
 
@@ -443,15 +394,32 @@ function closeWebSocket() {
   if (socketTask.value) {
     socketTask.value.close({
       success: () => {
-        console.log('WebSocket 连接已关闭');
+        console.log('断开 WebSocket 连接成功');
       },
       fail: (err) => {
-        console.error('关闭 WebSocket 连接失败', err);
+        console.error('断开 WebSocket 连接失败', err);
       },
     });
+    socketTask.value = null
   }
 }
 
+// 处理重连机制
+function handleReconnect() {
+  if (isReconnecting.value || reconnectAttempts.value >= maxReconnectAttempts.value) {
+    console.log('已达到最大重连次数,停止重连');
+    return;
+  }
+
+  isReconnecting.value = true
+  reconnectAttempts.value = reconnectAttempts.value++
+  
+  setTimeout(() => {
+    initWebSocket();
+    isReconnecting.value = false;
+  }, reconnectInterval)
+}
+
 // 处理Websocket消息
 function handleWebsocketMessage(message: any) {
   switch (message.event) {

+ 564 - 0
src/pages/print/jobForIntranet.vue

@@ -0,0 +1,564 @@
+<!-- 使用 type="home" 属性设置首页,其他页面不需要设置,默认为page;推荐使用json5,更强大,且允许注释 -->
+<route lang="json5">
+{
+  style: {
+    navigationBarTitleText: '打印任务列表',
+  },
+}
+</route>
+<template>
+  <view class="page-list">
+    <wd-sticky>
+      <view class="sticky-box">
+        <wd-tabs v-model="queryParams.completed" auto-line-width @change="resetDataList">
+          <wd-tab v-for="item of completedList" :key="item.value" :title="item.label" :name="item.value">
+          </wd-tab>
+        </wd-tabs>
+
+        <!-- <wd-search
+          placeholder="请输入打印任务名称"
+          placeholder-left
+          hide-cancel
+          v-model="queryParams.title"
+          @change="onSearch"
+          @clear="onSearch"
+        >
+          <template v-slot:prefix>
+            <wd-drop-menu custom-class="search-menu">
+              <wd-drop-menu-item v-model="queryParams.completed" :options="completedList" @change="onSearch" />
+            </wd-drop-menu>
+          </template>
+        </wd-search> -->
+      </view>
+    </wd-sticky>
+
+    <view class="list-contain">
+      <view v-if="queryParams.printer" class="printer">{{ `当前打印机: ${queryParams.printer} ${queryParams.asname ? `(${queryParams.asname})` : ''}` }}</view>
+
+      <template v-if="dataList.length > 0 || loadStatus == 'loading'">
+        <view v-for="item of dataList" :key="item.id" class="item-contain" @click="toDetail(item)">
+          <view class="item-info">
+            <view class="image">
+              <wd-img
+                :src="getFileType(item.title)"
+                :width="60"
+                :height="60"
+                mode="aspectFit"
+              ></wd-img>
+            </view>
+            <view class="name">
+              <view class="main-text">
+                <view>{{ item.title }}</view>
+              </view>
+              <view class="sub-text">
+                <text>任务状态: </text>
+                <text :class="`color-${getLabelColor(item.jobStatus, jobStatus)}`">{{ getLabel(item.jobStatus, jobStatus) + (item.statusMsg ? `(${item.statusMsg})` : "") }}</text>
+              </view>
+              <view class="sub-text">
+                {{ `创建时间: ${item.createTime}` }}
+              </view>
+              <!-- <view class="sub-text flex-center">
+                <view class="flex-1">xxx</view>
+                <view class="flex-1">xxx</view>
+              </view> -->
+            </view>
+          </view>
+          <view class="item-opt">
+            <view v-if="item.reprint == '1'" class="opt-btn" @click.stop="handleReprint(item.id)">重打</view>
+            <view v-if="['3', '4', '5', '6'].includes(item.jobStatus)" class="opt-btn" @click.stop="handleCancel(item.id)">取消</view>
+            <view class="opt-btn delete-btn" @click.stop="handleDelete(item)">删除</view>
+          </view>
+        </view>
+        <!-- 加载更多 -->
+        <wd-loadmore custom-class="loadmore" :state="loadStatus" />
+      </template>
+      <wd-status-tip v-else image="content" tip="暂无内容" />
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { useMessage, useToast } from 'wot-design-uni'
+import { getUserHubJobsPage, reprintUserHubJobs, cancelUserHubJobs, deleteUserHubJobs } from '@/service/api'
+import { getLabel, getLabelColor } from '@/utils'
+import { debounce } from 'wot-design-uni/components/common/util'
+import { useUserStore } from '@/store'
+import { handleError } from 'vue'
+import { toLoginWait } from '@/utils'
+
+let socketBaseUrl = import.meta.env.VITE_SOCKET_BASEURL
+const userStore = useUserStore()
+const token = userStore.token
+const message = useMessage()
+const toast = useToast()
+const total = ref(0)
+const queryParams: any = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  completed: "all",
+  title: '',
+  printer: "",
+  hubId: '',
+  asname: '', // 打印助手名称
+})
+const completedList: any = ref([
+  { label: '全部', value: 'all' },
+  { label: '当前任务', value: '0' },
+  { label: '历史任务', value: '1' }
+])
+//  0连接中 3 等待中 4 已暂停 5 处理中 6已停止 7已取消 8已中止 9已完成
+const jobStatus: any = ref([
+  { label: '连接中', value: '0', color: 'primary' },
+  { label: '等待中', value: '3', color: 'info' },
+  { label: '已暂停', value: '4', color: 'info' },
+  { label: '处理中', value: '5', color: 'primary' },
+  { label: '已停止', value: '6', color: 'danger' },
+  { label: '已取消', value: '7', color: 'danger' },
+  { label: '已中止', value: '8', color: 'danger' },
+  { label: '已完成', value: '9', color: 'success' },
+])
+const dataList = ref([])
+// 加载中: loading; 没有数据: finished; 错误: error;
+const loadStatus: any = ref('loading')
+const isToTop = ref(false)
+// 文件类型
+const imageSrc = '/static/images/image.png'
+const docSrc = '/static/images/doc.png'
+const xlsSrc = '/static/images/xls.png'
+const pptSrc = '/static/images/ppt.png'
+const pdfSrc = '/static/images/pdf.png'
+const txtSrc = '/static/images/txt.png'
+const otherSrc = '/static/images/other.png'
+// websocket连接对象
+const socketTask = ref(null)
+// 是否正在重连
+const isReconnecting = ref(false)
+// 当前重连次数
+const reconnectAttempts = ref(0)
+// 最大重连次数
+const maxReconnectAttempts = ref(5)
+// 重连间隔时间
+const reconnectInterval = 1000
+
+defineOptions({
+  name: 'Job',
+})
+
+// 根据文件类型获取图片
+// 允许的文件类型 allowType = ['txt', 'pdf', 'doc', 'docx', 'xlsx', 'xls', 'ppt', 'pptx', 'gif', 'png', 'jpg', 'jpeg', 'webp']
+function getFileType(fileName) {
+  let src = otherSrc
+  let fileType = ""
+  const lastIndex = fileName.lastIndexOf('.')
+  if (lastIndex != -1) fileType = fileName.substring(lastIndex + 1)
+  switch (fileType) {
+    case "doc":
+    case "docx":
+      src = docSrc
+      break;
+    case "xls":
+    case "xlsx":
+      src = xlsSrc;
+      break;
+    case "ppt":
+    case "pptx":
+      src = pptSrc;
+      break;
+    case "png":
+    case "jpg":
+    case "jpeg":
+    case "gif":
+    case "webp":
+      src = imageSrc;
+      break;
+    case "txt":
+      src = txtSrc;
+      break;
+    case "pdf":
+      src = pdfSrc;
+      break;
+  }
+  return src
+}
+
+// 获取列表数据
+function getDataList() {
+  loadStatus.value = 'loading'
+  const params = { ...queryParams }
+  if (params.title) {
+    params.title = '*' + params.title + '*'
+  } else {
+    delete params.title
+  }
+  if (params.completed === '' || params.completed === 'all') delete params.completed
+  if (!params.printer) delete params.printer
+  if (!params.hubId) delete params.hubId
+  getUserHubJobsPage(params)
+    .then((res: any) => {
+      if (res.code === 0 && res.body) {
+        total.value = res.body.total
+        if (queryParams.pageNo === 1) {
+          dataList.value = res.body.records || []
+        } else {
+          dataList.value.push(...(res.body.records || []))
+        }
+      }
+    })
+    .catch((e) => {})
+    .finally(() => {
+      loadStatus.value = 'finished'
+    })
+}
+
+const onSearch = debounce(() => {
+  queryParams.pageNo = 1
+  getDataList()
+}, 500)
+
+// 查看详情
+function toDetail(item: any) {
+  if (['0'].includes(item.jobStatus)) return
+  uni.navigateTo({
+    url: `/pages/print/jobDetail?id=${item.id}`,
+  })
+}
+
+// 重打任务
+function handleReprint(id: string) {
+  reprintUserHubJobs({id}).then((res: any) => {
+    if (res.code === 0) {
+      toast.success('已发送重新打印')
+      getDataList()
+    }
+  })
+  .catch((error) => {
+    console.log(error)
+  })
+}
+
+// 取消任务
+function handleCancel(id: string) {
+  cancelUserHubJobs({id}).then((res: any) => {
+    if (res.code === 0) {
+      toast.success('取消打印任务成功')
+      getDataList()
+    }
+  })
+  .catch((error) => {
+    console.log(error)
+  })
+}
+
+// 删除任务
+function handleDelete(item) {
+  message
+    .confirm({
+      title: '提示',
+      msg: `是否删除打印任务: ${item.title} ?`,
+    })
+    .then(() => {
+      deleteUserHubJobs({id: item.id})
+        .then((res: any) => {
+          if (res.code === 0) {
+            toast.success('删除成功')
+            queryParams.pageNo = 1
+            getDataList()
+          }
+        })
+        .catch((error) => {
+          console.log(error)
+        })
+    })
+    .catch((error) => {
+      console.log(error)
+    })
+}
+
+// 查询列表, 滚动到顶部, 返回第一页
+function resetDataList() {
+  uni.pageScrollTo({
+    scrollTop: 0,
+    duration: 300,
+  })
+  dataList.value = []
+  onSearch()
+}
+
+// 页面加载
+onLoad((option) => {
+  initWebSocket()
+  // 指定打印机 & 助手ID
+  if (option && option.printer && option.hubId) {
+    queryParams.printer = option.printer
+    queryParams.hubId = option.hubId
+    queryParams.asname = option.asname
+  }
+  if (option && option.tab) {
+    queryParams.completed = option.tab
+  }
+  getDataList()
+  // 监听刷新
+  uni.$on('refreshData', () => {
+    resetDataList()
+    isToTop.value = true
+  })
+})
+
+onUnload(() => {
+  closeWebSocket()
+})
+
+// 页面显示
+onShow(() => {
+  if (isToTop.value) {
+    uni.pageScrollTo({
+      scrollTop: 0,
+      duration: 300,
+    })
+    isToTop.value = false
+  }
+})
+
+// 滚动到底部
+onReachBottom(() => {
+  if (dataList.value.length < total.value) {
+    queryParams.pageNo++
+    getDataList()
+  } else {
+    loadStatus.value = 'finished'
+  }
+})
+
+// 创建websocket连接
+function initWebSocket() {
+  socketTask.value = uni.connectSocket({
+    url: `${socketBaseUrl}/print/ws`,
+    header: {
+      'token': token
+    },
+    success: () => {
+      console.log("连接成功");
+    },
+    fail: (err) => {
+      console.log("连接失败: ", err);
+      handleReconnect()
+    }
+  })
+
+  // 监听 WebSocket 连接打开事件
+  socketTask.value.onOpen(() => {
+    console.log('WebSocket 连接已打开');
+
+    // 发送数据到服务器(心跳)
+    // socketTask.send({
+    //   data: JSON.stringify({ message: 'Hello, Server!' }),
+    //   success: () => {
+    //     console.log('消息发送成功');
+    //   },
+    //   fail: (err) => {
+    //     console.error('消息发送失败', err);
+    //   }
+    // });
+  })
+
+  // 监听 WebSocket 接收到消息事件
+  socketTask.value.onMessage((res) => {
+    console.log('Received message:', res.data);
+
+    // 处理接收到的数据
+    const lines = res.data.split(/\r?\n|\r/).filter(line => line.trim() !== '');
+    const message = lines.reduce((obj, item) => {
+      const [key, value] = item.split(/:(.*)/);
+      obj[key] = key == "data" ? JSON.parse(value) : value;
+      return obj;
+    }, {});
+    console.log('解析后的消息:', message);
+
+    handleWebsocketMessage(message)
+  })
+
+  // 监听 WebSocket 错误事件
+  socketTask.value.onError((err) => {
+    console.error('WebSocket 发生错误', err);
+  });
+
+  // 监听 WebSocket 连接关闭事件 (线上存在一分钟就断开重连的问题)
+  socketTask.value.onClose((res) => {
+    console.log('WebSocket 连接已关闭:', res); // 异常断开 {code: 1006, reason: "abnormal closure"}
+    if (res.code !== 1000) handleReconnect()
+  });
+}
+
+// 断开websocket连接 
+function closeWebSocket() {
+  if (socketTask.value) {
+    socketTask.value.close({
+      success: () => {
+        console.log('断开 WebSocket 连接成功');
+      },
+      fail: (err) => {
+        console.error('断开 WebSocket 连接失败', err);
+      },
+    });
+    socketTask.value = null
+  }
+}
+
+// 处理重连机制
+function handleReconnect() {
+  if (isReconnecting.value || reconnectAttempts.value >= maxReconnectAttempts.value) {
+    console.log('已达到最大重连次数,停止重连');
+    return;
+  }
+
+  isReconnecting.value = true
+  reconnectAttempts.value = reconnectAttempts.value++
+  
+  setTimeout(() => {
+    initWebSocket();
+    isReconnecting.value = false;
+  }, reconnectInterval)
+}
+
+// 处理Websocket消息
+function handleWebsocketMessage(message: any) {
+  switch (message.event) {
+    case "job-update":
+      handleUpdate(message.data)
+      break;
+    case "not-verify":
+      toast.warning('token失效, 请重新登录')
+      toLoginWait(1500)
+      break;
+  }
+}
+
+// 处理更新数据
+function handleUpdate(data) {
+  if (data.id && dataList.value.findIndex(i => i.id == data.id) !== -1) {
+    // 更新状态
+    let target: any = dataList.value.find(i => i.id == data.id)
+    target.jobStatus = String(data.jobStatus)
+    target.statusMsg = data.statusMsg
+    // 通知
+    switch (String(data.jobStatus)) {
+      case "9":
+        toast.success(`打印完成: ${data.title}`)
+        break;
+      case "6":
+        toast.error(`打印错误: ${data.title}`)
+        break;
+      default:
+        toast.info(`打印信息: ${data.title} ${getLabel(data.jobStatus, jobStatus.value)}`)
+        break; 
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.sticky-box {
+  width: 100vw;
+  height: 100rpx;
+  background-color: #ffffff;
+}
+.search-type {
+  position: relative;
+  height: 60rpx;
+  padding: 0 16rpx 0 32rpx;
+  line-height: 60rpx;
+}
+.search-type::after {
+  position: absolute;
+  top: 10rpx;
+  right: 0;
+  bottom: 10rpx;
+  width: 2rpx;
+  content: '';
+  background: rgba(0, 0, 0, 0.25);
+}
+.search-type {
+  :deep(.icon-arrow) {
+    display: inline-block;
+    font-size: 40rpx;
+    vertical-align: middle;
+  }
+}
+.printer {
+  margin-bottom: 20rpx;
+}
+.list-contain {
+  padding: 10rpx 30rpx 30rpx;
+  overflow-y: auto;
+
+  .item-contain {
+    padding: 30rpx;
+    margin-bottom: 30rpx;
+    border: 2rpx solid #eee;
+
+    .item-info {
+      display: flex;
+      align-items: flex-start;
+      .image {
+        display: flex;
+        padding-right: 14rpx;
+      }
+      .name {
+        flex: 1;
+        .main-text {
+          display: flex;
+          align-items: center;
+          word-break: break-all;
+        }
+        .sub-text {
+          margin-top: 14rpx;
+          font-size: 28rpx;
+          color: #00000073;
+        }
+      }
+      .opt {
+        width: 80rpx;
+        text-align: center;
+      }
+    }
+
+    .item-opt {
+      margin-top: 20rpx;
+      border-top: 2rpx solid #eee;
+      padding-top: 20rpx;
+      text-align: right;
+      font-size: 28rpx;
+      
+      .opt-btn {
+        display: inline-block;
+        padding: 4rpx 20rpx;
+        margin-left: 20rpx;
+        color: var(--wot-color-theme);
+        border: 2rpx solid var(--wot-color-theme);
+        border-radius: 8rpx;
+      }
+      .opt-btn:first-of-type {
+        margin-left: 0;
+      }
+      .delete-btn {
+        color: #fa4350;
+        border-color: #fa4350;
+      }
+    }
+  }
+}
+//搜索下拉菜单
+:deep(.search-menu .wd-drop-menu__list) {
+  background: none;
+}
+:deep(.search-menu .wd-drop-menu__item) {
+  height: 62rpx;
+  line-height: 62rpx;
+}
+:deep(.search-menu .wd-drop-menu__item-title::after) {
+  content: none;
+}
+:deep(.wd-message-box__content) {
+  word-break: break-all;
+}
+</style>

+ 18 - 5
src/pages/printer/index.vue

@@ -67,14 +67,11 @@
 </template>
 
 <script lang="ts" setup>
-import { useMessage, useToast } from 'wot-design-uni'
 import { getUserHubPrints } from '@/service/api'
 import { getLabel } from '@/utils'
 import { debounce } from 'wot-design-uni/components/common/util'
 
 const asname = ref("")
-const message = useMessage()
-const toast = useToast()
 const total = ref(0)
 const queryParams: any = reactive({
   id: "",
@@ -102,6 +99,8 @@ const isShowActions = ref(false)
 const actionsTitle = ref('')
 const currentActions: any = ref([{ loading: true }])
 const isToTop = ref(false)
+// 判断环境是否局域网
+const intranetIp = ref("")
 
 defineOptions({
   name: 'Printer',
@@ -112,6 +111,9 @@ function buildCurrentActions() {
   currentActions.value = [
     { name: '查看打印任务' },
   ]
+  if (intranetIp.value && intranetIp.value != 'undefined') {
+    currentActions.value.push({ name: '查看局域网打印任务' })
+  }
 }
 
 function getDataList() {
@@ -143,10 +145,17 @@ const onSearch = debounce(() => {
 // 查看打印任务
 function toJob(data?: any) {
   uni.navigateTo({
-    url: `/pages/print/job?printer=${data.name}&hubId=${data.userHubId}&asname=${data.userHubName || asname.value}`,
+    url: `/pages/print/job?printer=${data.name}&hubId=${data.hubId}&asname=${data.userHubName || asname.value}`,
   })
 }
 
+// 查看局域网打印任务
+function toIntranetJob(data?: any) {
+  // uni.navigateTo({
+  //   url: `/pages/print/jobForIntranet?printer=${data.name}&asname=${data.userHubName || asname.value}&ip=${intranetIp.value}`,
+  // })
+}
+
 function showActions(data: any) {
   actionsTitle.value = data.name ? `打印机: ${data.name}` : '操作'
   currentData.value = data || {}
@@ -158,7 +167,10 @@ function selectActions(data: any) {
   switch (data.item.name) {
     case '查看打印任务':
       toJob(currentData.value)
-      break
+      break;
+    case '查看局域网打印任务':
+      toIntranetJob(currentData.value)
+      break;
   }
 }
 
@@ -173,6 +185,7 @@ function resetDataList() {
 
 // 页面加载
 onLoad((option) => {
+  if (option && option.ip) intranetIp.value = option.ip || ""
   if (option && option.asname) asname.value = option.asname || ""
   if (option && option.id) {
     queryParams.id = option.id || ""

+ 68 - 0
src/service/api/index.ts

@@ -202,6 +202,7 @@ export const testConnect = () => {
   return http<any>({
     url: "http://192.168.90.1:5002/api/test",
     method: "GET",
+    hideErrorToast: true,
   })
 }
 
@@ -225,4 +226,71 @@ export const connectWifi = (data: any) => {
     method: "POST",
     data: data,
   })
+}
+
+// ---------------------- 局域网打印相关 ----------------------
+// 局域网打印 直接在 uni.uploadFile 中传入 http://${ip}:5002/api/print-file
+
+// 测试是否连通
+export const testByIP = (ip) => {
+  return http<any>({
+    url: `http://${ip}:5002/api/test`,
+    method: "GET",
+    hideErrorToast: true,
+    timeout: 5000,
+  })
+}
+
+// 获取打印助手的打印机列表
+export const getPrinterByLocal = (data) => {
+  return http<any>({
+    url: `http://${data.ip}:5002/api/printer`,
+    method: "GET",
+    query: data
+  })
+}
+
+// 获取打印助手参数
+export const getAttrByLocal = (data) => {
+  return http<any>({
+    url: `http://${data.ip}:5002/api/print-attr`,
+    method: 'GET',
+    query: data
+  })
+}
+
+// 打印任务记录
+export const getJobsCompleted = (data) => {
+  return http<any>({
+    url: `http://${data.ip}:5002/api/jobs`,
+    method: 'GET',
+    query: data
+  })
+}
+
+// 进行中的打印任务队列
+export const getJobsNotCompleted = (data) => {
+  return http<any>({
+    url: `http://${data.ip}:5002/api/jobs-not-completed`,
+    method: 'GET',
+    query: data
+  })
+}
+
+// 取消打印任务
+export const cancelJobByLocal = (data) => {
+  return http<any>({
+    url: `http://${data.ip}:5002/api/job-cancel`,
+    method: 'GET',
+    query: data
+  })
+}
+
+// 重打打印任务
+export const restartJobByLocal = (data) => {
+  return http<any>({
+    url: `http://${data.ip}:5002/api/job-restart`,
+    method: 'GET',
+    query: data
+  })
 }

+ 1 - 0
src/types/uni-pages.d.ts

@@ -13,6 +13,7 @@ interface NavigateToOptions {
        "/pages/print/index" |
        "/pages/print/job" |
        "/pages/print/jobDetail" |
+       "/pages/print/jobForIntranet" |
        "/pages/printer/index" |
        "/pages/test/index" |
        "/pages/webview/index" |

+ 6 - 4
src/utils/http.ts

@@ -34,10 +34,12 @@ export const http = <T>(options: CustomRequestOptions) => {
       },
       // 响应失败
       fail(err) {
-        uni.showToast({
-          icon: 'none',
-          title: '网络错误,换个网络试试',
-        })
+        if (!options.hideErrorToast) {
+          uni.showToast({
+            icon: 'none',
+            title: '网络错误,换个网络试试',
+          })
+        }
         reject(err)
       },
     })