index.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897
  1. <!-- 使用 type="home" 属性设置首页,其他页面不需要设置,默认为page;推荐使用json5,更强大,且允许注释 -->
  2. <route lang="json5">
  3. {
  4. style: {
  5. navigationBarTitleText: '远程打印',
  6. },
  7. }
  8. </route>
  9. <template>
  10. <view class="page-form">
  11. <!-- 选择打印机--获取打印参数--上传打印 -->
  12. <wd-form ref="form" :model="formData">
  13. <wd-cell-group custom-class="form-group" border>
  14. <wd-cell title="上传文件"
  15. title-width="100px"
  16. required
  17. prop="fileList"
  18. custom-class="my-cell">
  19. <view class="file-list">
  20. <view v-for="(item, index) of fileList"
  21. :key="item.filePath"
  22. class="file-item"
  23. @click="previewFile(item.name, item.filePath)">
  24. <view class="item-icon">
  25. <wd-img
  26. :src="getFileIcon(item.name)"
  27. :width="20"
  28. :height="20"
  29. mode="aspectFit"
  30. ></wd-img>
  31. </view>
  32. <view class="item-name">{{ item.name }}</view>
  33. <view class="item-del" @click.stop="removeFile(index)">
  34. <wd-img
  35. :src="delSrc"
  36. :width="20"
  37. :height="20"
  38. mode="aspectFit"
  39. ></wd-img>
  40. </view>
  41. </view>
  42. <!-- 选择文件: 支持多选多个 (系统文件另外传入) -->
  43. <wd-button v-if="accept != 'all'" size="small" @click="selectFile()">选择文件</wd-button>
  44. </view>
  45. </wd-cell>
  46. <wd-select-picker
  47. v-model="formData.printer"
  48. label="打印机"
  49. label-width="100px"
  50. type="radio"
  51. :columns="printerList"
  52. value-key="id"
  53. label-key="showName"
  54. :max="1"
  55. :show-confirm="false"
  56. filterable
  57. placeholder="请选择打印机"
  58. prop="printer"
  59. :rules="[{ required: true, message: '请选择打印机' }]"
  60. @change="handlePrinterChange"
  61. />
  62. <!-- 动态属性 -->
  63. <template v-for="option of printerOptions" :key="option.key">
  64. <!-- type: select 下拉选项 -->
  65. <wd-select-picker
  66. v-if="option.type == 'select'"
  67. v-model="formData[option.key]"
  68. :label="option.text"
  69. label-width="100px"
  70. type="radio"
  71. :columns="option.values"
  72. value-key="key"
  73. label-key="text"
  74. :max="1"
  75. :show-confirm="false"
  76. filterable
  77. :placeholder="`请选择${option.text}`"
  78. :prop="option.key"
  79. :rules="[{ required: true, message: `请选择${option.text}` }]"
  80. />
  81. <!-- type: radio 单选项 -->
  82. <wd-cell v-if="option.type == 'radio'"
  83. :title="option.text"
  84. title-width="100px"
  85. custom-class="my-cell"
  86. :prop="option.key"
  87. :rules="[{ required: true, message: `请选择${option.text}` }]">
  88. <wd-radio-group v-model="formData[option.key]"
  89. shape="dot"
  90. inline>
  91. <wd-radio v-for="item of option.values"
  92. :key="item.key"
  93. :value="item.key">
  94. {{ item.text }}
  95. </wd-radio>
  96. </wd-radio-group>
  97. </wd-cell>
  98. <!-- type: rang 范围值, 传参是拼接字符串'xxx_str,xxx_end', 默认全部all不用传 -->
  99. <wd-cell v-if="option.type == 'rang'"
  100. :title="option.text"
  101. title-width="100px"
  102. custom-class="my-cell"
  103. :prop="option.key"
  104. required>
  105. <!-- 1. 先radio选择 all / range, 默认all全部, 仅单个文件时支持range选项 -->
  106. <wd-radio-group v-model="formData[option.key]"
  107. shape="dot"
  108. inline>
  109. <wd-radio value="all">所有页</wd-radio>
  110. <wd-radio v-if="fileList.length == 1 && !isImg()" value="range">输入范围</wd-radio>
  111. </wd-radio-group>
  112. <!-- 2. 当上传单个文件(非图片)且radio选择 range 时允许输入范围, 从 XXX 到 XXX) -->
  113. <view v-if="!isImg() && formData[option.key] == 'range'" style="text-align: left; margin-top: 10px">
  114. <view class="inline-txt" style="margin-left: 0">从</view>
  115. <wd-input
  116. custom-style="display: inline-block; width: 70px; vertical-align: middle"
  117. placeholder="起始页"
  118. v-model="formData[option.key + '_str']"
  119. />
  120. <view class="inline-txt">到</view>
  121. <wd-input
  122. custom-style="display: inline-block; width: 70px; vertical-align: middle"
  123. placeholder="结束页"
  124. v-model="formData[option.key + '_end']"
  125. />
  126. </view>
  127. </wd-cell>
  128. <!-- type: number 数字 -->
  129. <wd-cell v-if="option.type == 'number'"
  130. :title="option.text"
  131. title-width="100px"
  132. custom-class="my-cell"
  133. :rules="[{ required: true, message: `请输入${option.text}` }]">
  134. <wd-input-number v-model="formData[option.key]"
  135. :min="option.min"
  136. :max="option.max"
  137. :step="1"
  138. step-strictly
  139. input-width="100px" />
  140. </wd-cell>
  141. </template>
  142. </wd-cell-group>
  143. <view class="form-footer">
  144. <wd-button type="primary" size="large" block @click="handleSubmit()">确认打印</wd-button>
  145. </view>
  146. </wd-form>
  147. </view>
  148. </template>
  149. <script lang="ts" setup>
  150. import { useToast, useMessage } from 'wot-design-uni'
  151. import {
  152. getUserHubPrints,
  153. getUserHubAttr,
  154. getInvoiceBatch,
  155. getUserHubPage,
  156. testByIP,
  157. getAttrByLocal
  158. } from '@/service/api'
  159. import { getEnvBaseUrl, redirectToUpload, reLaunchToHome } from '@/utils'
  160. import { useUserStore } from '@/store'
  161. const userStore = useUserStore()
  162. const token = userStore.token
  163. const baseUrl = getEnvBaseUrl()
  164. const toast = useToast()
  165. const message = useMessage()
  166. // 系统文件--all, 聊天文件--msg, 相册--image, 发票--invoice
  167. const accept = ref("all")
  168. const fileList = ref([])
  169. const form = ref()
  170. const formData: any = ref({
  171. printer: "",
  172. })
  173. const printerList: any = ref([])
  174. const printerOptions: any = ref([])
  175. const allowType = ['txt', 'pdf', 'doc', 'docx', 'xlsx', 'xls', 'ppt', 'pptx', 'gif', 'png', 'jpg', 'jpeg', 'webp']
  176. const maxSize = 10 * 1024 * 1024
  177. // 文件类型
  178. const imageSrc = '/static/images/image.png'
  179. const docSrc = '/static/images/doc.png'
  180. const xlsSrc = '/static/images/xls.png'
  181. const pptSrc = '/static/images/ppt.png'
  182. const pdfSrc = '/static/images/pdf.png'
  183. const txtSrc = '/static/images/txt.png'
  184. const otherSrc = '/static/images/other.png'
  185. const delSrc = '/static/images/deleteIcon.png'
  186. // 文件类型图片匹配
  187. const srcMap = {
  188. 'doc': docSrc,
  189. 'docx': docSrc,
  190. 'xls': xlsSrc,
  191. 'xlsx': xlsSrc,
  192. 'ppt': pptSrc,
  193. 'pptx': pptSrc,
  194. 'png': imageSrc,
  195. 'jpg': imageSrc,
  196. 'jpeg': imageSrc,
  197. 'gif': imageSrc,
  198. 'webp': imageSrc,
  199. 'txt': txtSrc,
  200. 'pdf': pdfSrc,
  201. }
  202. // 上传失败的任务
  203. const failedFiles = ref([])
  204. // 助手列表
  205. const userHubList = ref([])
  206. // 判断环境是否局域网
  207. const isLocal = ref(false)
  208. const intranetIp = ref("")
  209. defineOptions({
  210. name: 'Print',
  211. })
  212. // 根据打印助手的内网ip字段"intranetIp" 检查是否局域网 (采用局域网接口)
  213. function checkUserHub() {
  214. let params = {
  215. pageNo: 1,
  216. pageSize: 999,
  217. }
  218. getUserHubPage(params).then((res: any) => {
  219. if (res.code === 0 && res.body) {
  220. userHubList.value = (res.body.records || [])
  221. userHubList.value.forEach(item => {
  222. item.isIntranet = false
  223. if (item.intranetIp) {
  224. item.ipList = item.intranetIp.split(',')
  225. // 调用test接口判断是否通
  226. item.ipList.forEach(ip => {
  227. // 是否检测局域网, 注释就用远程打印的逻辑
  228. checkIP(ip, item.id)
  229. });
  230. }
  231. });
  232. }
  233. }).catch(() => {
  234. })
  235. }
  236. // 检查传入IP是否有连通, 标志为局域网助手
  237. function checkIP(ip, userHubId) {
  238. testByIP(ip).then((res: any) => {
  239. if (res.code == 200) {
  240. userHubList.value.find(i => i.id == userHubId).isIntranet = true
  241. updateShowName()
  242. }
  243. }).catch(() => {})
  244. }
  245. // 更新打印机展示名称
  246. function updateShowName() {
  247. printerList.value.forEach(item => {
  248. if (item.userHubId && userHubList.value.find(i => i.id == item.userHubId).isIntranet && !item.showName.startsWith('【局域网】')) {
  249. item.showName = `【局域网】${item.showName}`
  250. }
  251. })
  252. }
  253. // 获取打印机 (兼容远程+局域网, 打印机均可选择, 只是选择局域网打印机的处理逻辑不同)
  254. function getPrinterList() {
  255. let params = { id: "" }
  256. getUserHubPrints(params)
  257. .then((res: any) => {
  258. if (res.code === 0 && res.body) {
  259. // printerList.value = res.body && res.body.length > 0 ? res.body.filter(i => i['printer-type'] == 'normal' ) : []
  260. printerList.value = res.body || []
  261. printerList.value.forEach(item => {
  262. item.showName = `${item.name} (${item.userHubName})`
  263. })
  264. }
  265. })
  266. .catch((e) => {})
  267. }
  268. // 处理更换打印机, 获取配置参数
  269. function handlePrinterChange(item: any) {
  270. let obj = printerList.value.find(i => i.id == item.value) || {}
  271. // 判断所选打印机是否局域网
  272. let matchUserHub = userHubList.value.find(i => i.id == obj.userHubId) || {}
  273. isLocal.value = matchUserHub.isIntranet
  274. intranetIp.value = matchUserHub.isIntranet ? matchUserHub.ipList[0] : ''
  275. // 构造打印选项参数
  276. printerOptions.value = []
  277. formData.value = {
  278. printer: item.value
  279. }
  280. let params = {
  281. ip: intranetIp.value, // 用于局域网打印
  282. id: obj.userHubId,
  283. printer: obj.name,
  284. }
  285. const getFn = () => {
  286. return isLocal.value ? getAttrByLocal : getUserHubAttr
  287. }
  288. getFn()(params)
  289. .then((res: any) => {
  290. if ([0, 200].includes(res.code) && res.body) {
  291. printerOptions.value = res.body || []
  292. let _formData = {
  293. printer: item.value,
  294. userHubId: obj.userHubId,
  295. printerName: obj.name,
  296. asname: obj.userHubName,
  297. }
  298. if (printerOptions.value.length) {
  299. printerOptions.value.forEach(option => {
  300. switch(option.type) {
  301. case "rang":
  302. // 默认全部, 同时创建 xxx_str 和 xxx_end
  303. let defList = option.def ? option.def.split(',') : [option.min || '', '']
  304. _formData[option.key] = "all"
  305. _formData[option.key + '_str'] = defList[0] || ''
  306. _formData[option.key + '_end'] = defList[1] || ''
  307. break;
  308. default:
  309. let firstValue = option.values && option.values[0] ? option.values[0].key : ""
  310. _formData[option.key] = option.def || firstValue || ""
  311. }
  312. });
  313. formData.value = _formData
  314. }
  315. }
  316. })
  317. .catch((e) => {
  318. })
  319. .finally(() => {})
  320. }
  321. // 获取文件格式
  322. function getFileType(fileName) {
  323. let type = ""
  324. const lastIndex = fileName.lastIndexOf('.')
  325. if (lastIndex != -1) {
  326. type = fileName.substring(lastIndex + 1)
  327. }
  328. return type
  329. }
  330. // 获取文件图标
  331. function getFileIcon(fileName) {
  332. return srcMap[getFileType(fileName)] || otherSrc
  333. }
  334. // 处理选择文件
  335. function selectFile() {
  336. switch(accept.value) {
  337. case "wxImg":
  338. uni.chooseMessageFile({
  339. count: 10,
  340. type: "image",
  341. success (res) {
  342. console.log('chooseMessageFile IMG res: ', res);
  343. if (res.errMsg == "chooseMessageFile:ok") {
  344. let failList = []
  345. res.tempFiles.forEach(item => {
  346. if (item.size > maxSize || !allowType.includes(getFileType(item.name))) {
  347. failList.push(item)
  348. } else {
  349. fileList.value.push({
  350. name: item.name,
  351. filePath: item.path,
  352. })
  353. handleFileListChange()
  354. }
  355. })
  356. if (failList.length > 0) toast.warning(`文件大小限制为10MB, 文件格式仅支持 ${allowType.join('、')}, 所选文件有 ${failList.length} 格式不符合`)
  357. } else {
  358. toast.warning("选择微信图片异常, 请重试")
  359. }
  360. },
  361. fail () {
  362. toast.warning("选择微信图片失败, 请重试")
  363. }
  364. })
  365. break;
  366. case "wxFile":
  367. uni.chooseMessageFile({
  368. count: 10,
  369. type: "file",
  370. extension: allowType,
  371. success (res) {
  372. console.log('chooseMessageFile FILE res: ', res);
  373. if (res.errMsg == "chooseMessageFile:ok") {
  374. let failList = []
  375. res.tempFiles.forEach(item => {
  376. if (item.size > maxSize || !allowType.includes(getFileType(item.name))) {
  377. failList.push(item)
  378. } else {
  379. fileList.value.push({
  380. name: item.name,
  381. filePath: item.path,
  382. })
  383. handleFileListChange()
  384. }
  385. })
  386. if (failList.length > 0) toast.warning(`文件大小限制为10MB, 文件格式仅支持 ${allowType.join('、')}, 所选文件有 ${failList.length} 格式不符合`)
  387. } else {
  388. toast.warning("选择微信文件异常, 请重试")
  389. }
  390. },
  391. fail () {
  392. toast.warning("选择微信文件失败, 请重试")
  393. }
  394. })
  395. break;
  396. case "image":
  397. uni.chooseImage({
  398. count: 10,
  399. sourceType: ['album', 'camera'],
  400. sizeType: ['original', 'compressed'],
  401. extension: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
  402. success (res) {
  403. console.log('chooseImage res: ', res);
  404. if (res.errMsg == "chooseImage:ok") {
  405. let failList = []
  406. res.tempFiles.forEach((item, index) => {
  407. if (item.size > maxSize) {
  408. failList.push(item)
  409. } else {
  410. fileList.value.push({
  411. // name: item.path.substring(item.path.lastIndexOf("/") + 1),
  412. name: `图片-${new Date().getTime() + index}.${getFileType(item.path)}`,
  413. filePath: item.path,
  414. })
  415. handleFileListChange()
  416. }
  417. })
  418. if (failList.length > 0) toast.warning(`文件大小限制为10MB, 所选文件有 ${failList.length} 格式不符合`)
  419. } else {
  420. toast.warning("选择相册图片异常, 请重试")
  421. }
  422. },
  423. fail () {
  424. toast.warning("选择相册图片失败, 请重试")
  425. }
  426. })
  427. break;
  428. case "invoice":
  429. uni.chooseInvoice({
  430. success (res) {
  431. console.log('chooseInvoice res: ', res);
  432. if (res.errMsg == "chooseInvoice:ok") {
  433. let list = JSON.parse(res.invoiceInfo)
  434. if (list && list.length > 0) {
  435. let params = {
  436. cardId: list.map(i => i.card_id).join(","),
  437. encryptCode: list.map(i => i.encrypt_code).join(","),
  438. }
  439. getInvoiceBatch(params).then(res => {
  440. if (res.code == 0 && res.body) {
  441. // 遍历下载pdf 存储在 fileList 打印
  442. res.body.forEach(item => {
  443. if (item.userInfo.pdfUrl) {
  444. downloadFile(`发票-${item.payee}.png`, item.userInfo.pdfUrl)
  445. } else {
  446. toast.warning(`发票-${item.payee} PDF地址异常`)
  447. }
  448. });
  449. }
  450. }).catch(() => {
  451. toast.warning("获取发票信息异常")
  452. })
  453. }
  454. } else {
  455. toast.warning("选择微信发票异常, 请重试")
  456. }
  457. },
  458. fail (err) {
  459. let tips = "选择微信发票失败, 请重试"
  460. switch (err.errMsg) {
  461. case "chooseInvoice:fail cancel":
  462. tips = "已取消选择发票"
  463. break;
  464. case "chooseInvoice:fail auth deny":
  465. tips = "已拒绝授权,无法访问发票信息"
  466. break;
  467. case "chooseInvoice:fail system error":
  468. tips = "系统错误"
  469. break;
  470. }
  471. toast.warning(tips)
  472. }
  473. })
  474. break;
  475. }
  476. }
  477. // 判断是否选图片, 或者选一个文件 并且图片类型 ['png', 'jpg', 'jpeg', 'gif', 'webp']
  478. function isImg() {
  479. return accept.value == "image" || (fileList.value.length == 1 && ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(getFileType(fileList.value[0].name)))
  480. }
  481. // 处理文件列表增减
  482. function handleFileListChange() {
  483. // 处理打印选项参数
  484. if (printerOptions.value && printerOptions.value.length > 0) {
  485. printerOptions.value.forEach(option => {
  486. if (option.type == "rang" && formData.value[option.key] != "all") {
  487. let defList = option.def ? option.def.split(',') : [option.min || '', '']
  488. formData.value[option.key] = "all"
  489. formData.value[option.key + '_str'] = defList[0] || ''
  490. formData.value[option.key + '_end'] = defList[1] || ''
  491. }
  492. });
  493. }
  494. }
  495. // 下载文件(用于处理发票)
  496. function downloadFile(fileName, fileUrl) {
  497. uni.downloadFile({
  498. url: fileUrl,
  499. filePath: uni.env.USER_DATA_PATH + '/' + fileName,
  500. success: (res) => {
  501. fileList.value.push({
  502. name: fileName,
  503. filePath: res.filePath,
  504. })
  505. handleFileListChange()
  506. },
  507. fail () {
  508. toast.warning("下载文件失败, 请重试")
  509. }
  510. })
  511. }
  512. // 预览文件 (区分文件&图片)
  513. function previewFile(fileName, filePath) {
  514. let fileType = getFileType(fileName)
  515. if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(fileType)) {
  516. // 图片预览
  517. let imgUrl = filePath
  518. uni.previewImage({
  519. current: 0,
  520. urls: [imgUrl],
  521. })
  522. } else {
  523. // 文件预览
  524. uni.openDocument({
  525. filePath: filePath,
  526. success () {},
  527. fail () {
  528. toast.warning("打开文件失败, 请重试")
  529. }
  530. })
  531. }
  532. }
  533. // 移除文件
  534. function removeFile(index) {
  535. fileList.value.splice(index, 1)
  536. handleFileListChange()
  537. if (accept.value == "all" && fileList.value.length == 0) {
  538. message.confirm({
  539. msg: "文件已全部移除, 是否重新选择?",
  540. closeOnClickModal: false,
  541. }).then(() => {
  542. redirectToUpload()
  543. }).catch((error) => {
  544. reLaunchToHome()
  545. })
  546. }
  547. }
  548. // 批量打印文件
  549. async function handleBatchPrint() {
  550. try {
  551. failedFiles.value = []; // 记录上传失败的文件
  552. for (let i = 0; i < fileList.value.length; i++) {
  553. const fileName = fileList.value[i].name;
  554. const filePath = fileList.value[i].filePath;
  555. toast.loading({
  556. loadingType: 'ring',
  557. msg: `开始上传文件 ${i + 1}: ${fileName}`
  558. })
  559. try {
  560. // 上传文件并监听进度
  561. const uploadRes = await handlePrint(filePath, (progress) => {
  562. toast.loading({
  563. loadingType: 'ring',
  564. msg: `文件 ${i + 1} 上传进度: ${progress}%`
  565. })
  566. });
  567. toast.success(`文件 ${i + 1} 上传成功`)
  568. } catch (error) {
  569. toast.error(`文件 ${i + 1} 上传失败`)
  570. failedFiles.value.push(fileList.value[i]); // 记录失败的文件
  571. }
  572. }
  573. // 检查是否有失败的文件
  574. if (failedFiles.value.length > 0) {
  575. message.confirm({
  576. title: '上传失败',
  577. msg: `有 ${failedFiles.value.length} 个文件上传失败,是否重新上传?`,
  578. closeOnClickModal: false,
  579. }).then(async () => {
  580. await retryUploadFiles(failedFiles.value); // 重新上传失败的文件
  581. }).catch((error) => {
  582. msgConfirm("上传完成", "取消重新上传, 其余文件已上传成功!")
  583. })
  584. } else {
  585. toast.success("所有文件上传成功")
  586. msgConfirm("上传完成", "所有文件上传成功")
  587. }
  588. } catch(error) {
  589. console.log("上传过程中发生错误", error);
  590. }
  591. }
  592. async function retryUploadFiles(failedFiles) {
  593. const newFailedFiles = []; // 记录重新上传失败的文件
  594. // 逐个重新上传失败的文件
  595. for (let i = 0; i < failedFiles.length; i++) {
  596. const fileName = failedFiles[i].name;
  597. const filePath = failedFiles[i].filePath;
  598. toast.loading({
  599. loadingType: 'ring',
  600. msg: `重新上传文件 ${i + 1}: ${fileName}`
  601. })
  602. try {
  603. const uploadRes = await handlePrint(filePath, (progress) => {
  604. toast.loading({
  605. loadingType: 'ring',
  606. msg: `文件 ${i + 1} 上传进度: ${progress}%`
  607. })
  608. });
  609. toast.success(`文件 ${i + 1} 重新上传成功`)
  610. } catch (error) {
  611. toast.error(`文件 ${i + 1} 重新上传成功`)
  612. newFailedFiles.push(failedFiles[i]); // 记录重新上传失败的文件
  613. }
  614. }
  615. // 检查是否还有失败的文件
  616. if (newFailedFiles.length > 0) {
  617. // 修改当前文件列表
  618. fileList.value = newFailedFiles
  619. message.alert({
  620. title: '重新上传失败',
  621. msg: `有 ${newFailedFiles.length} 个文件重新上传失败,请检查网络或文件后重试。`,
  622. })
  623. } else {
  624. toast.success("所有失败文件重新上传成功")
  625. msgConfirm("上传完成", "所有失败文件重新上传成功!")
  626. }
  627. }
  628. // 打印文件 (单个打印任务)
  629. function handlePrint(filePath, onProgress) {
  630. return new Promise((resolve, reject) => {
  631. // 构造提交数据
  632. let params = {
  633. id: formData.value.userHubId,
  634. printer: formData.value.printerName,
  635. }
  636. printerOptions.value.forEach(option => {
  637. switch(option.type) {
  638. case "rang":
  639. // 打印页数范围 all 时候不传
  640. if (formData.value[option.key] !== "all") {
  641. params[option.key] = `${formData.value[option.key + '_str']},${formData.value[option.key + '_end']}`
  642. }
  643. break;
  644. default:
  645. params[option.key] = formData.value[option.key]
  646. }
  647. })
  648. const queryParams = Object.entries(params).map(([key, value]) => {
  649. return `${key}=${encodeURIComponent(value)}`;
  650. }).join('&');
  651. // 上传接口地址, 判断是否局域网
  652. const getURL = () => {
  653. return isLocal.value ? `http://${intranetIp.value}:5002/api/print-file?${queryParams}` : `${baseUrl}/sys/wx/userHub/print?${queryParams}`
  654. }
  655. const uploadTask = uni.uploadFile({
  656. url: getURL(),
  657. filePath: filePath,
  658. name: 'file', // 文件对应的 key
  659. formData: {
  660. // 其他表单数据
  661. },
  662. success: (res) => {
  663. if (res.statusCode === 200 && ((isLocal.value && JSON.parse(res.data).code === 200) || (!isLocal.value && JSON.parse(res.data).code === 0))) {
  664. resolve(res.data);
  665. } else {
  666. reject(new Error(`上传失败,状态码: ${res.statusCode}`));
  667. }
  668. },
  669. fail: (err) => {
  670. reject(err);
  671. },
  672. });
  673. // 监听上传进度
  674. uploadTask.onProgressUpdate((res) => {
  675. if (onProgress) {
  676. onProgress(res.progress); // 回调上传进度
  677. }
  678. });
  679. });
  680. }
  681. // 处理提交打印
  682. function handleSubmit() {
  683. if (fileList.value.length == 0) {
  684. toast.warning('请先上传文件')
  685. return
  686. }
  687. // 校验打印机属性 范围最小最大 item.min item.max
  688. for (let item of printerOptions.value) {
  689. if (item.type == 'rang' && formData.value[item.key] == "range") {
  690. let strValue = formData.value[item.key + "_str"]
  691. let endValue = formData.value[item.key + "_end"]
  692. if (!strValue) {
  693. toast.warning(`请输入${item.text}起始页`)
  694. return
  695. } else if (item.min && strValue < item.min) {
  696. toast.warning(`${item.text} 起始页的最小值为${item.min}`)
  697. return
  698. } else if (item.max && strValue > item.max) {
  699. toast.warning(`${item.text} 起始页的最大值为${item.max}`)
  700. return
  701. }
  702. if (!endValue) {
  703. toast.warning(`请输入${item.text}结束页`)
  704. return
  705. } else if (item.min && endValue < item.min) {
  706. toast.warning(`${item.text} 结束页的最小值为${item.min}`)
  707. return
  708. } else if (item.max && endValue > item.max) {
  709. toast.warning(`${item.text} 结束页的最大值为${item.max}`)
  710. return
  711. } else if (strValue && endValue < strValue) {
  712. toast.warning(`${item.text} 结束页不能小于起始页`)
  713. return
  714. }
  715. }
  716. }
  717. form.value.validate().then(({ valid }) => {
  718. if (valid) {
  719. // 不区分单个/批量, 以批量打印处理
  720. handleBatchPrint()
  721. }
  722. })
  723. }
  724. // 打印完成后提示词
  725. function msgConfirm(title="提示", msg="文件已上传成功!") {
  726. message.confirm({
  727. title,
  728. msg,
  729. confirmButtonText: "查看任务",
  730. cancelButtonText: "继续打印",
  731. }).then(() => {
  732. // 页面跳转--查看任务: 区分远程/局域网打印任务列表
  733. if (intranetIp.value) {
  734. uni.redirectTo({
  735. url: `/pages/print/jobForIntranet?ip=${intranetIp.value}&printer=${formData.value.printerName}&asname=${formData.value.asname}`,
  736. })
  737. } else {
  738. uni.redirectTo({
  739. url: `/pages/print/job?tab=0`,
  740. })
  741. }
  742. }).catch(() => {
  743. // 继续打印
  744. failedFiles.value = []
  745. fileList.value = []
  746. printerOptions.value = []
  747. formData.value = {
  748. printer: ""
  749. }
  750. if (accept.value == "all") {
  751. redirectToUpload()
  752. }
  753. })
  754. }
  755. // 处理系统文件webview传入
  756. function base64ToTempFilePath(fileName, base64Data, success, fail) {
  757. const fs = uni.getFileSystemManager()
  758. // const fileName = 'temp_' + Date.now() + '.png' // 自定义文件名,可根据需要修改
  759. const filePath = uni.env.USER_DATA_PATH + '/' + fileName
  760. const buffer = uni.base64ToArrayBuffer(base64Data)
  761. fs.writeFile({
  762. filePath,
  763. data: buffer,
  764. encoding: 'binary',
  765. success() {
  766. success && success(filePath)
  767. },
  768. fail() {
  769. fail && fail()
  770. }
  771. })
  772. }
  773. // 打开SSE webview页面
  774. // function openWebViewSSE() {
  775. // let list = JSON.stringify(fileList.value.map(item => item.name).join(",") || "")
  776. // const url = `https://service.1ai.ltd/webview-sse/index.html?token=${token}&list=${list}&t=${new Date().getTime()}`
  777. // uni.navigateTo({
  778. // url: `/pages/webview/sse?url=${encodeURIComponent(url)}`,
  779. // })
  780. // }
  781. onLoad((option) => {
  782. if (option && option.accept) {
  783. accept.value = option.accept
  784. // 处理系统文件--all: 从webview上传的文件
  785. if (option.accept == "all" && uni.getStorageSync('fileList')) {
  786. fileList.value = uni.getStorageSync('fileList');
  787. uni.removeStorageSync('fileList');
  788. fileList.value.forEach(item => {
  789. item.file = item.file.split('base64,')[1]
  790. base64ToTempFilePath(item.name, item.file, (filePath) => {
  791. console.log('转换成功,临时地址为:', filePath)
  792. item.filePath = filePath
  793. }, function() {
  794. toast.warning('文件转换失败,请重试')
  795. })
  796. })
  797. }
  798. }
  799. getPrinterList()
  800. checkUserHub()
  801. })
  802. </script>
  803. <style lang="scss" scoped>
  804. :deep(.my-cell) {
  805. .wd-cell__value {
  806. text-align: left !important;
  807. }
  808. }
  809. :deep(.wd-radio-group) {
  810. padding: 0 !important;
  811. font-size: unset !important;
  812. .wd-radio {
  813. padding-top: 0 !important;
  814. }
  815. }
  816. :deep(.wd-upload__mask) {
  817. display: none !important;
  818. }
  819. :deep(.img-btn .wd-img__image) {
  820. width: 160rpx;
  821. height: 160rpx;
  822. border-radius: 40rpx;
  823. }
  824. .file-item {
  825. margin-bottom: 8rpx;
  826. display: flex;
  827. align-items: center;
  828. .item-icon {
  829. display: flex;
  830. width: 40rpx;
  831. height: 40rpx;
  832. margin-right: 8rpx;
  833. }
  834. .item-name {
  835. flex: 1;
  836. word-break: break-all;
  837. }
  838. .item-del {
  839. display: flex;
  840. width: 40rpx;
  841. height: 40rpx;
  842. margin-left: 8rpx;
  843. }
  844. }
  845. .inline-txt {
  846. display: inline-block;
  847. font-size: 28rpx;
  848. margin: 0 16rpx;
  849. color: rgba(0, 0, 0, 0.45);
  850. vertical-align: middle;
  851. }
  852. </style>