job.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. <!-- 使用 type="home" 属性设置首页,其他页面不需要设置,默认为page;推荐使用json5,更强大,且允许注释 -->
  2. <route lang="json5">
  3. {
  4. style: {
  5. navigationBarTitleText: '打印任务列表',
  6. },
  7. }
  8. </route>
  9. <template>
  10. <view class="page-list">
  11. <wd-sticky>
  12. <view class="sticky-box">
  13. <wd-tabs v-model="queryParams.completed" auto-line-width @change="resetDataList">
  14. <wd-tab v-for="item of completedList" :key="item.value" :title="item.label" :name="item.value">
  15. </wd-tab>
  16. </wd-tabs>
  17. <!-- <wd-search
  18. placeholder="请输入打印任务名称"
  19. placeholder-left
  20. hide-cancel
  21. v-model="queryParams.title"
  22. @change="onSearch"
  23. @clear="onSearch"
  24. >
  25. <template v-slot:prefix>
  26. <wd-drop-menu custom-class="search-menu">
  27. <wd-drop-menu-item v-model="queryParams.completed" :options="completedList" @change="onSearch" />
  28. </wd-drop-menu>
  29. </template>
  30. </wd-search> -->
  31. </view>
  32. </wd-sticky>
  33. <view class="list-contain">
  34. <view v-if="queryParams.printer" class="printer">{{ `当前打印机: ${queryParams.printer} ${queryParams.asname ? `(${queryParams.asname})` : ''}` }}</view>
  35. <template v-if="dataList.length > 0 || loadStatus == 'loading'">
  36. <view v-for="item of dataList" :key="item.id" class="item-contain" @click="toDetail(item)">
  37. <view class="item-info">
  38. <view class="image">
  39. <wd-img
  40. :src="getFileType(item.title)"
  41. :width="60"
  42. :height="60"
  43. mode="aspectFit"
  44. ></wd-img>
  45. </view>
  46. <view class="name">
  47. <view class="main-text">
  48. <view>{{ item.title }}</view>
  49. </view>
  50. <view class="sub-text">
  51. <text>任务状态: </text>
  52. <text :class="`color-${getLabelColor(item.jobStatus, jobStatus)}`">{{ getLabel(item.jobStatus, jobStatus) + (item.statusMsg ? `(${item.statusMsg})` : "") }}</text>
  53. </view>
  54. <view class="sub-text">
  55. {{ `创建时间: ${item.createTime}` }}
  56. </view>
  57. <!-- <view class="sub-text flex-center">
  58. <view class="flex-1">xxx</view>
  59. <view class="flex-1">xxx</view>
  60. </view> -->
  61. </view>
  62. </view>
  63. <view class="item-opt">
  64. <view v-if="item.reprint == '1'" class="opt-btn" @click.stop="handleReprint(item.id)">重打</view>
  65. <view v-if="['3', '4', '5', '6'].includes(item.jobStatus)" class="opt-btn" @click.stop="handleCancel(item.id)">取消</view>
  66. <view class="opt-btn delete-btn" @click.stop="handleDelete(item)">删除</view>
  67. </view>
  68. </view>
  69. <!-- 加载更多 -->
  70. <wd-loadmore custom-class="loadmore" :state="loadStatus" />
  71. </template>
  72. <wd-status-tip v-else image="content" tip="暂无内容" />
  73. </view>
  74. </view>
  75. </template>
  76. <script lang="ts" setup>
  77. import { useMessage, useToast } from 'wot-design-uni'
  78. import { getUserHubJobsPage, reprintUserHubJobs, cancelUserHubJobs, deleteUserHubJobs } from '@/service/api'
  79. import { getLabel, getLabelColor } from '@/utils'
  80. import { debounce } from 'wot-design-uni/components/common/util'
  81. import { useUserStore } from '@/store'
  82. import { toLoginWait } from '@/utils'
  83. let socketBaseUrl = import.meta.env.VITE_SOCKET_BASEURL
  84. const userStore = useUserStore()
  85. const token = userStore.token
  86. const message = useMessage()
  87. const toast = useToast()
  88. const total = ref(0)
  89. const queryParams: any = reactive({
  90. pageNo: 1,
  91. pageSize: 10,
  92. completed: "all",
  93. title: '',
  94. printer: "",
  95. hubId: '',
  96. asname: '', // 打印助手名称
  97. })
  98. const completedList: any = ref([
  99. { label: '全部', value: 'all' },
  100. { label: '当前任务', value: '0' },
  101. { label: '历史任务', value: '1' }
  102. ])
  103. // 0连接中 3 等待中 4 已暂停 5 处理中 6已停止 7已取消 8已中止 9已完成
  104. const jobStatus: any = ref([
  105. { label: '连接中', value: '0', color: 'primary' },
  106. { label: '等待中', value: '3', color: 'info' },
  107. { label: '已暂停', value: '4', color: 'info' },
  108. { label: '处理中', value: '5', color: 'primary' },
  109. { label: '已停止', value: '6', color: 'danger' },
  110. { label: '已取消', value: '7', color: 'danger' },
  111. { label: '已中止', value: '8', color: 'danger' },
  112. { label: '已完成', value: '9', color: 'success' },
  113. ])
  114. const dataList = ref([])
  115. // 加载中: loading; 没有数据: finished; 错误: error;
  116. const loadStatus: any = ref('loading')
  117. const isToTop = ref(false)
  118. // 文件类型
  119. const imageSrc = '/static/images/image.png'
  120. const docSrc = '/static/images/doc.png'
  121. const xlsSrc = '/static/images/xls.png'
  122. const pptSrc = '/static/images/ppt.png'
  123. const pdfSrc = '/static/images/pdf.png'
  124. const txtSrc = '/static/images/txt.png'
  125. const otherSrc = '/static/images/other.png'
  126. // websocket连接对象
  127. const socketTask = ref(null)
  128. // 是否正在重连
  129. const isReconnecting = ref(false)
  130. // 当前重连次数
  131. const reconnectAttempts = ref(0)
  132. // 最大重连次数
  133. const maxReconnectAttempts = ref(5)
  134. // 重连间隔时间
  135. const reconnectInterval = 1000
  136. defineOptions({
  137. name: 'Job',
  138. })
  139. // 根据文件类型获取图片
  140. // 允许的文件类型 allowType = ['txt', 'pdf', 'doc', 'docx', 'xlsx', 'xls', 'ppt', 'pptx', 'gif', 'png', 'jpg', 'jpeg', 'webp']
  141. function getFileType(fileName) {
  142. let src = otherSrc
  143. let fileType = ""
  144. if (fileName) {
  145. const lastIndex = fileName.lastIndexOf('.')
  146. if (lastIndex != -1) fileType = fileName.substring(lastIndex + 1)
  147. switch (fileType) {
  148. case "doc":
  149. case "docx":
  150. src = docSrc
  151. break;
  152. case "xls":
  153. case "xlsx":
  154. src = xlsSrc;
  155. break;
  156. case "ppt":
  157. case "pptx":
  158. src = pptSrc;
  159. break;
  160. case "png":
  161. case "jpg":
  162. case "jpeg":
  163. case "gif":
  164. case "webp":
  165. src = imageSrc;
  166. break;
  167. case "txt":
  168. src = txtSrc;
  169. break;
  170. case "pdf":
  171. src = pdfSrc;
  172. break;
  173. }
  174. }
  175. return src
  176. }
  177. // 获取列表数据
  178. function getDataList() {
  179. loadStatus.value = 'loading'
  180. const params = { ...queryParams }
  181. if (params.title) {
  182. params.title = '*' + params.title + '*'
  183. } else {
  184. delete params.title
  185. }
  186. if (params.completed === '' || params.completed === 'all') delete params.completed
  187. if (!params.printer) delete params.printer
  188. if (!params.hubId) delete params.hubId
  189. getUserHubJobsPage(params)
  190. .then((res: any) => {
  191. if (res.code === 0 && res.body) {
  192. total.value = res.body.total
  193. if (queryParams.pageNo === 1) {
  194. dataList.value = res.body.records || []
  195. } else {
  196. dataList.value.push(...(res.body.records || []))
  197. }
  198. }
  199. })
  200. .catch((e) => {})
  201. .finally(() => {
  202. loadStatus.value = 'finished'
  203. })
  204. }
  205. const onSearch = debounce(() => {
  206. queryParams.pageNo = 1
  207. getDataList()
  208. }, 500)
  209. // 查看详情
  210. function toDetail(item: any) {
  211. if (['0'].includes(item.jobStatus)) return
  212. uni.navigateTo({
  213. url: `/pages/print/jobDetail?id=${item.id}`,
  214. })
  215. }
  216. // 重打任务
  217. function handleReprint(id: string) {
  218. reprintUserHubJobs({id}).then((res: any) => {
  219. if (res.code === 0) {
  220. toast.success('已发送重新打印')
  221. getDataList()
  222. }
  223. })
  224. .catch((error) => {
  225. console.log(error)
  226. })
  227. }
  228. // 取消任务
  229. function handleCancel(id: string) {
  230. cancelUserHubJobs({id}).then((res: any) => {
  231. if (res.code === 0) {
  232. toast.success('取消打印任务成功')
  233. getDataList()
  234. }
  235. })
  236. .catch((error) => {
  237. console.log(error)
  238. })
  239. }
  240. // 删除任务
  241. function handleDelete(item) {
  242. message
  243. .confirm({
  244. title: '提示',
  245. msg: `是否删除打印任务: ${item.title} ?`,
  246. })
  247. .then(() => {
  248. deleteUserHubJobs({id: item.id})
  249. .then((res: any) => {
  250. if (res.code === 0) {
  251. toast.success('删除成功')
  252. queryParams.pageNo = 1
  253. getDataList()
  254. }
  255. })
  256. .catch((error) => {
  257. console.log(error)
  258. })
  259. })
  260. .catch((error) => {
  261. console.log(error)
  262. })
  263. }
  264. // 查询列表, 滚动到顶部, 返回第一页
  265. function resetDataList() {
  266. uni.pageScrollTo({
  267. scrollTop: 0,
  268. duration: 300,
  269. })
  270. dataList.value = []
  271. onSearch()
  272. }
  273. // 页面加载
  274. onLoad((option) => {
  275. initWebSocket()
  276. // 指定打印机 & 助手ID
  277. if (option && option.printer && option.hubId) {
  278. queryParams.printer = option.printer
  279. queryParams.hubId = option.hubId
  280. queryParams.asname = option.asname
  281. }
  282. if (option && option.tab) {
  283. queryParams.completed = option.tab
  284. }
  285. getDataList()
  286. // 监听刷新
  287. uni.$on('refreshData', () => {
  288. resetDataList()
  289. isToTop.value = true
  290. })
  291. })
  292. onUnload(() => {
  293. closeWebSocket()
  294. })
  295. // 页面显示
  296. onShow(() => {
  297. if (isToTop.value) {
  298. uni.pageScrollTo({
  299. scrollTop: 0,
  300. duration: 300,
  301. })
  302. isToTop.value = false
  303. }
  304. })
  305. // 滚动到底部
  306. onReachBottom(() => {
  307. if (dataList.value.length < total.value) {
  308. queryParams.pageNo++
  309. getDataList()
  310. } else {
  311. loadStatus.value = 'finished'
  312. }
  313. })
  314. // 创建websocket连接
  315. function initWebSocket() {
  316. socketTask.value = uni.connectSocket({
  317. url: `${socketBaseUrl}/print/ws`,
  318. header: {
  319. 'token': token
  320. },
  321. success: () => {
  322. console.log("连接成功");
  323. },
  324. fail: (err) => {
  325. console.log("连接失败: ", err);
  326. handleReconnect()
  327. }
  328. })
  329. // 监听 WebSocket 连接打开事件
  330. socketTask.value.onOpen(() => {
  331. console.log('WebSocket 连接已打开');
  332. // 发送数据到服务器(心跳)
  333. // socketTask.send({
  334. // data: JSON.stringify({ message: 'Hello, Server!' }),
  335. // success: () => {
  336. // console.log('消息发送成功');
  337. // },
  338. // fail: (err) => {
  339. // console.error('消息发送失败', err);
  340. // }
  341. // });
  342. })
  343. // 监听 WebSocket 接收到消息事件
  344. socketTask.value.onMessage((res) => {
  345. console.log('Received message:', res.data);
  346. // 处理接收到的数据
  347. const lines = res.data.split(/\r?\n|\r/).filter(line => line.trim() !== '');
  348. const message = lines.reduce((obj, item) => {
  349. const [key, value] = item.split(/:(.*)/);
  350. obj[key] = key == "data" ? JSON.parse(value) : value;
  351. return obj;
  352. }, {});
  353. console.log('解析后的消息:', message);
  354. handleWebsocketMessage(message)
  355. })
  356. // 监听 WebSocket 错误事件
  357. socketTask.value.onError((err) => {
  358. console.error('WebSocket 发生错误', err);
  359. });
  360. // 监听 WebSocket 连接关闭事件 (线上存在一分钟就断开重连的问题)
  361. socketTask.value.onClose((res) => {
  362. console.log('WebSocket 连接已关闭:', res); // 异常断开 {code: 1006, reason: "abnormal closure"}
  363. if (res.code !== 1000) handleReconnect()
  364. });
  365. }
  366. // 断开websocket连接
  367. function closeWebSocket() {
  368. if (socketTask.value) {
  369. socketTask.value.close({
  370. success: () => {
  371. console.log('断开 WebSocket 连接成功');
  372. },
  373. fail: (err) => {
  374. console.error('断开 WebSocket 连接失败', err);
  375. },
  376. });
  377. socketTask.value = null
  378. }
  379. }
  380. // 处理重连机制
  381. function handleReconnect() {
  382. console.log('reconnectAttempts.value: ', reconnectAttempts.value);
  383. if (isReconnecting.value || reconnectAttempts.value >= maxReconnectAttempts.value) {
  384. console.log('已达到最大重连次数,停止重连');
  385. return;
  386. }
  387. isReconnecting.value = true
  388. reconnectAttempts.value = reconnectAttempts.value++
  389. setTimeout(() => {
  390. initWebSocket();
  391. isReconnecting.value = false;
  392. }, reconnectInterval)
  393. }
  394. // 处理Websocket消息
  395. function handleWebsocketMessage(message: any) {
  396. switch (message.event) {
  397. case "job-update":
  398. handleUpdate(message.data)
  399. break;
  400. case "not-verify":
  401. toast.warning('token失效, 请重新登录')
  402. toLoginWait(1500)
  403. break;
  404. }
  405. }
  406. // 处理更新数据
  407. function handleUpdate(data) {
  408. if (data.id && dataList.value.findIndex(i => i.id == data.id) !== -1) {
  409. // 更新状态
  410. let target: any = dataList.value.find(i => i.id == data.id)
  411. target.jobStatus = String(data.jobStatus)
  412. target.statusMsg = data.statusMsg
  413. // 通知
  414. switch (String(data.jobStatus)) {
  415. case "9":
  416. toast.success(`打印完成: ${data.title}`)
  417. break;
  418. case "6":
  419. toast.error(`打印错误: ${data.title}`)
  420. break;
  421. default:
  422. toast.info(`打印信息: ${data.title} ${getLabel(data.jobStatus, jobStatus.value)}`)
  423. break;
  424. }
  425. }
  426. }
  427. </script>
  428. <style lang="scss" scoped>
  429. .sticky-box {
  430. width: 100vw;
  431. height: 100rpx;
  432. background-color: #ffffff;
  433. }
  434. .search-type {
  435. position: relative;
  436. height: 60rpx;
  437. padding: 0 16rpx 0 32rpx;
  438. line-height: 60rpx;
  439. }
  440. .search-type::after {
  441. position: absolute;
  442. top: 10rpx;
  443. right: 0;
  444. bottom: 10rpx;
  445. width: 2rpx;
  446. content: '';
  447. background: rgba(0, 0, 0, 0.25);
  448. }
  449. .search-type {
  450. :deep(.icon-arrow) {
  451. display: inline-block;
  452. font-size: 40rpx;
  453. vertical-align: middle;
  454. }
  455. }
  456. .printer {
  457. margin-bottom: 20rpx;
  458. }
  459. .list-contain {
  460. padding: 10rpx 30rpx 30rpx;
  461. overflow-y: auto;
  462. .item-contain {
  463. padding: 30rpx;
  464. margin-bottom: 30rpx;
  465. border: 2rpx solid #eee;
  466. .item-info {
  467. display: flex;
  468. align-items: flex-start;
  469. .image {
  470. display: flex;
  471. padding-right: 14rpx;
  472. }
  473. .name {
  474. flex: 1;
  475. .main-text {
  476. display: flex;
  477. align-items: center;
  478. word-break: break-all;
  479. }
  480. .sub-text {
  481. margin-top: 14rpx;
  482. font-size: 28rpx;
  483. color: #00000073;
  484. }
  485. }
  486. .opt {
  487. width: 80rpx;
  488. text-align: center;
  489. }
  490. }
  491. .item-opt {
  492. margin-top: 20rpx;
  493. border-top: 2rpx solid #eee;
  494. padding-top: 20rpx;
  495. text-align: right;
  496. font-size: 28rpx;
  497. .opt-btn {
  498. display: inline-block;
  499. padding: 4rpx 20rpx;
  500. margin-left: 20rpx;
  501. color: var(--wot-color-theme);
  502. border: 2rpx solid var(--wot-color-theme);
  503. border-radius: 8rpx;
  504. }
  505. .opt-btn:first-of-type {
  506. margin-left: 0;
  507. }
  508. .delete-btn {
  509. color: #fa4350;
  510. border-color: #fa4350;
  511. }
  512. }
  513. }
  514. }
  515. //搜索下拉菜单
  516. :deep(.search-menu .wd-drop-menu__list) {
  517. background: none;
  518. }
  519. :deep(.search-menu .wd-drop-menu__item) {
  520. height: 62rpx;
  521. line-height: 62rpx;
  522. }
  523. :deep(.search-menu .wd-drop-menu__item-title::after) {
  524. content: none;
  525. }
  526. :deep(.wd-message-box__content) {
  527. word-break: break-all;
  528. }
  529. </style>