diff --git a/package.json b/package.json index 9494fd4..fa321e8 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "jszip": "^3.7.1", "moment": "^2.27.0", "node-polyfill-webpack-plugin": "^2.0.1", + "streamsaver": "^2.0.6", "node-sass": "^4.14.1", "normalize.css": "7.0.0", "nprogress": "0.2.0", diff --git a/public/mitm.html b/public/mitm.html new file mode 100644 index 0000000..508a9c3 --- /dev/null +++ b/public/mitm.html @@ -0,0 +1,167 @@ + + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..dd75c1a --- /dev/null +++ b/public/sw.js @@ -0,0 +1,130 @@ +/* global self ReadableStream Response */ + +self.addEventListener('install', () => { + self.skipWaiting() +}) + +self.addEventListener('activate', event => { + event.waitUntil(self.clients.claim()) +}) + +const map = new Map() + +// This should be called once per download +// Each event has a dataChannel that the data will be piped through +self.onmessage = event => { + // We send a heartbeat every x second to keep the + // service worker alive if a transferable stream is not sent + if (event.data === 'ping') { + return + } + + const data = event.data + const downloadUrl = data.url || self.registration.scope + Math.random() + '/' + (typeof data === 'string' ? data : data.filename) + const port = event.ports[0] + const metadata = new Array(3) // [stream, data, port] + + metadata[1] = data + metadata[2] = port + + // Note to self: + // old streamsaver v1.2.0 might still use `readableStream`... + // but v2.0.0 will always transfer the stream through MessageChannel #94 + if (event.data.readableStream) { + metadata[0] = event.data.readableStream + } else if (event.data.transferringReadable) { + port.onmessage = evt => { + port.onmessage = null + metadata[0] = evt.data.readableStream + } + } else { + metadata[0] = createStream(port) + } + + map.set(downloadUrl, metadata) + port.postMessage({ download: downloadUrl }) +} + +function createStream (port) { + // ReadableStream is only supported by chrome 52 + return new ReadableStream({ + start (controller) { + // When we receive data on the messageChannel, we write + port.onmessage = ({ data }) => { + if (data === 'end') { + return controller.close() + } + + if (data === 'abort') { + controller.error('Aborted the download') + return + } + + controller.enqueue(data) + } + }, + cancel (reason) { + console.log('user aborted', reason) + port.postMessage({ abort: true }) + } + }) +} + +self.onfetch = event => { + const url = event.request.url + + // this only works for Firefox + if (url.endsWith('/ping')) { + return event.respondWith(new Response('pong')) + } + + const hijacke = map.get(url) + + if (!hijacke) return null + + const [ stream, data, port ] = hijacke + + map.delete(url) + + // Not comfortable letting any user control all headers + // so we only copy over the length & disposition + const responseHeaders = new Headers({ + 'Content-Type': 'application/octet-stream; charset=utf-8', + + // To be on the safe side, The link can be opened in a iframe. + // but octet-stream should stop it. + 'Content-Security-Policy': "default-src 'none'", + 'X-Content-Security-Policy': "default-src 'none'", + 'X-WebKit-CSP': "default-src 'none'", + 'X-XSS-Protection': '1; mode=block', + 'Cross-Origin-Embedder-Policy': 'require-corp' + }) + + let headers = new Headers(data.headers || {}) + + if (headers.has('Content-Length')) { + responseHeaders.set('Content-Length', headers.get('Content-Length')) + } + + if (headers.has('Content-Disposition')) { + responseHeaders.set('Content-Disposition', headers.get('Content-Disposition')) + } + + // data, data.filename and size should not be used anymore + if (data.size) { + console.warn('Depricated') + responseHeaders.set('Content-Length', data.size) + } + + let fileName = typeof data === 'string' ? data : data.filename + if (fileName) { + console.warn('Depricated') + // Make filename RFC5987 compatible + fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A') + responseHeaders.set('Content-Disposition', "attachment; filename*=UTF-8''" + fileName) + } + + event.respondWith(new Response(stream, { headers: responseHeaders })) + + port.postMessage({ debug: 'Download started' }) +} diff --git a/src/api/trials.js b/src/api/trials.js index f0b2689..0564097 100644 --- a/src/api/trials.js +++ b/src/api/trials.js @@ -3610,4 +3610,21 @@ export function getAuthorizationCodeInfo(params) { method: 'get', params }) +} + +// 获取下载信息 +export function getDownloadSubjectVisitStudyInfo(params) { + return request({ + url: `/Patient/getDownloadSubjectVisitStudyInfo`, + method: 'get', + params + }) +} +// 下载成功回调 +export function downloadImageSuccess(params) { + return request({ + url: `/Patient/downloadImageSuccess`, + method: 'get', + params + }) } \ No newline at end of file diff --git a/src/utils/stream.js b/src/utils/stream.js new file mode 100644 index 0000000..88f7d59 --- /dev/null +++ b/src/utils/stream.js @@ -0,0 +1,106 @@ +import streamSaver from "streamsaver"; +import "streamsaver/examples/zip-stream.js"; +// import store from '@/store' +streamSaver.mitm = `${window.location.origin}/mitm.html?version=2.0.0` +// 下载文件并压缩 +function zipFiles(zipName, files) { + return new Promise((resolve) => { + try { + console.log("同步下载打包开始时间:" + new Date()); + // store.dispatch('trials/setUnLock', true) + files = formatFiles(files) + // 创建压缩文件输出流 + const zipFileOutputStream = streamSaver.createWriteStream(zipName); + // 创建下载文件流 + const fileIterator = files.values(); + const readableZipStream = new ZIP({ + async pull(ctrl) { + const fileInfo = fileIterator.next(); + if (fileInfo.done) {//迭代终止 + ctrl.close(); + } else { + let { name, url } = fileInfo.value; + url = decodeUtf8(url); + return fetch(url).then(res => { + ctrl.enqueue({ + name, + stream: () => res.body + }); + }) + } + } + }); + if (window.WritableStream && readableZipStream.pipeTo) { + // 开始下载 + readableZipStream + .pipeTo(zipFileOutputStream) + .then(() => { + console.log("同步下载打包结束时间:" + new Date()); + // store.dispatch('trials/setUnLock', false) + resolve(true) + }).catch(err => { + console.log(err); + resolve(false) + }); + } else { + resolve(false) + } + } catch (err) { + console.log(err); + resolve(false) + } + }) + +} +// 下载文件并修改名称 +async function updateFile(file, name) { + return new Promise(async resolve => { + try { + // store.dispatch('trials/setUnLock', true) + const fileOutputStream = streamSaver.createWriteStream(name); + file = decodeUtf8(file); + let res = await fetch(file); + res.body.pipeTo(fileOutputStream).then(() => { + // store.dispatch('trials/setUnLock', true) + resolve(true) + }).catch(err => { + console.log(err) + resolve(false) + }); + } catch (err) { + console.log(err) + resolve(false) + } + }) +} +// 同名文件修改名称 +function formatFiles(files) { + let fileObj = {}; + files.forEach(file => { + let arr = Object.keys(fileObj); + if (!~arr.indexOf(file.name)) { + fileObj[file.name] = 1; + } else { + let name = file.name; + file.name = name.split(".")[0] + `(${fileObj[name]})` + name + .substring(name.lastIndexOf('.')) + .toLocaleLowerCase() + fileObj[name]++; + } + }) + return files; +} +function decodeUtf8(bytes) { + let str = bytes.split('?'); + let str2 = str[0].split('/'); + let name = str2[str2.length - 1]; + name = encodeURIComponent(name); + str.shift(); + str2.pop(); + return str2.join("/") + '/' + name; +} +export async function downLoadFile(file, name, type = 'file') { + if (type === 'zip') return await zipFiles(name, file); + return await updateFile(file, name) + +} \ No newline at end of file diff --git a/src/views/trials/trials-panel/hirVisit/index.vue b/src/views/trials/trials-panel/hirVisit/index.vue index e1e4819..bfa762f 100644 --- a/src/views/trials/trials-panel/hirVisit/index.vue +++ b/src/views/trials/trials-panel/hirVisit/index.vue @@ -83,7 +83,7 @@ - {{ $t("common:button:search") }} + {{ $t('common:button:search') }} - {{ $t("common:button:reset") }} + {{ $t('common:button:reset') }} @@ -164,7 +164,7 @@ sortable="custom" > @@ -213,12 +213,12 @@ ? 'danger' : '' " - >{{ $fd("SubmitState", Number(scope.row.SubmitState)) }}{{ $fd('SubmitState', Number(scope.row.SubmitState)) }} - {{ $fd("PackState", Number(scope.row.PackState)) }} - + --> @@ -330,14 +331,18 @@ diff --git a/src/views/trials/trials-panel/reading/read-task/index.vue b/src/views/trials/trials-panel/reading/read-task/index.vue index dd18eb8..137826e 100644 --- a/src/views/trials/trials-panel/reading/read-task/index.vue +++ b/src/views/trials/trials-panel/reading/read-task/index.vue @@ -122,7 +122,7 @@ icon="el-icon-search" @click="handleSearch" > - {{ $t("common:button:search") }} + {{ $t('common:button:search') }} - {{ $t("common:button:reset") }} + {{ $t('common:button:reset') }} @@ -205,19 +205,19 @@ > @@ -253,7 +253,7 @@ show-overflow-tooltip /> - {{ - $fd("PackState", Number(scope.row.PackState) || 0) + $fd('PackState', Number(scope.row.PackState) || 0) }} - + --> {{ - $t("trials:readTask:title:applyReason") + $t('trials:readTask:title:applyReason') }} {{ - $t("trials:readTask:option:errorRecords") + $t('trials:readTask:option:errorRecords') }} {{ - $t("trials:readTask:option:other") + $t('trials:readTask:option:other') }} @@ -442,7 +448,7 @@ {{ - $t("trials:readTask:title:influenceList") + $t('trials:readTask:title:influenceList') }}
{{ - $fd("TaskState", scope.row.TaskState) + $fd('TaskState', scope.row.TaskState) }} {{ - $fd("TaskState", scope.row.TaskState) + $fd('TaskState', scope.row.TaskState) }} {{ - $fd("TaskState", scope.row.TaskState) + $fd('TaskState', scope.row.TaskState) }} {{ - $fd("TaskState", scope.row.TaskState) + $fd('TaskState', scope.row.TaskState) }} {{ - $fd("TaskState", scope.row.TaskState) + $fd('TaskState', scope.row.TaskState) }} @@ -560,10 +566,10 @@ > @@ -576,13 +582,13 @@ > @@ -608,11 +614,11 @@ size="small" type="primary" @click=" - ApplyforReasonVisible = false; - ApplyforReasonForm = { Type: null, RequestReReadingReason: null }; + ApplyforReasonVisible = false + ApplyforReasonForm = { Type: null, RequestReReadingReason: null } " > - {{ $t("common:button:cancel") }} + {{ $t('common:button:cancel') }} - {{ $t("common:button:save") }} + {{ $t('common:button:save') }}
@@ -643,25 +649,29 @@ diff --git a/src/views/trials/trials-panel/reading/reading-tracking/index.vue b/src/views/trials/trials-panel/reading/reading-tracking/index.vue index 6f5449a..ce9ef89 100644 --- a/src/views/trials/trials-panel/reading/reading-tracking/index.vue +++ b/src/views/trials/trials-panel/reading/reading-tracking/index.vue @@ -104,7 +104,7 @@ - {{ $t("common:button:search") }} + {{ $t('common:button:search') }} - {{ $t("common:button:reset") }} + {{ $t('common:button:reset') }} @@ -192,7 +192,7 @@ scope.row.TaskState ] " - >{{ $fd("TaskState", scope.row.TaskState) }}{{ $fd('TaskState', scope.row.TaskState) }} @@ -217,7 +217,7 @@ @@ -236,7 +236,7 @@ sortable="custom" /> - {{ $fd("PackState", Number(scope.row.PackState)) }}{{ $fd('PackState', Number(scope.row.PackState)) }} - + -->