|
|
@@ -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>
|