阅片页面图像预览更改

uat
caiyiling 2025-02-21 16:13:17 +08:00
parent 551cdf0ed3
commit 6223eac81a
14 changed files with 2387 additions and 4 deletions

View File

@ -48,6 +48,7 @@ const getters = {
TotalNeedSignSystemDocCount: state => state.user.TotalNeedSignSystemDocCount,
TotalNeedSignTrialDocCount: state => state.user.TotalNeedSignTrialDocCount,
IsFirstSysDocNeedSign: state => state.user.IsFirstSysDocNeedSign,
TrialStatusStr: state => state.user.TrialStatusStr
TrialStatusStr: state => state.user.TrialStatusStr,
lastViewportTaskId: state => state.noneDicomReview.lastViewportTaskId
}
export default getters

View File

@ -12,7 +12,7 @@ import trials from './modules/trials'
import financials from './modules/financials'
import reading from './modules/reading'
import lang from './modules/lang'
import noneDicomReview from './modules/noneDicomReview'
Vue.use(Vuex)
const store = new Vuex.Store({
@ -27,7 +27,8 @@ const store = new Vuex.Store({
trials,
financials,
reading,
lang
lang,
noneDicomReview
},
getters
})

View File

@ -0,0 +1,24 @@
const getDefaultState = () => {
return {
lastViewportTaskId: null
}
}
const state = getDefaultState
const mutations = {
}
const actions = {
setLastViewportTaskId({ state }, id) {
state.lastViewportTaskId = id
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -1,6 +1,7 @@
import {
volumeLoader,
cornerstoneStreamingImageVolumeLoader
cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader
} from '@cornerstonejs/core';
export default function initVolumeLoader() {
@ -11,4 +12,8 @@ export default function initVolumeLoader() {
'cornerstoneStreamingImageVolume',
cornerstoneStreamingImageVolumeLoader
);
volumeLoader.registerVolumeLoader(
'cornerstoneStreamingDynamicImageVolume',
cornerstoneStreamingDynamicImageVolumeLoader
);
}

View File

@ -0,0 +1,76 @@
<template>
<div v-loading="loading" class="reading-viewer-container">
<!-- 访视阅片 -->
<visit-review v-if="taskInfo && taskInfo.ReadingCategory=== 1" />
<!-- 全局阅片 -->
<global-review v-else-if="taskInfo && taskInfo.ReadingCategory=== 2" />
<!-- 裁判阅片 -->
<ad-review v-else-if="taskInfo && taskInfo.ReadingCategory=== 4" />
<!-- 肿瘤学阅片 -->
<oncology-review v-else-if="taskInfo && taskInfo.ReadingCategory=== 5" />
</div>
</template>
<script>
import { getNextTask } from '@/api/trials'
import store from '@/store'
import VisitReview from '@/views/trials/trials-panel/reading/visit-review'
import GlobalReview from '@/views/trials/trials-panel/reading/global-review'
import AdReview from '@/views/trials/trials-panel/reading/ad-review'
import OncologyReview from '@/views/trials/trials-panel/reading/oncology-review'
export default {
name: 'ReadingViewer',
components: {
VisitReview,
GlobalReview,
AdReview,
OncologyReview
},
data() {
return {
taskInfo: null,
loading: false
}
},
mounted() {
this.getTaskInfo()
},
methods: {
async getTaskInfo() {
this.loading = true
try {
const params = {
subjectId: this.$route.query.subjectId,
trialId: this.$route.query.trialId,
subjectCode: this.$route.query.subjectCode,
visitTaskId: this.$route.query.visitTaskId,
trialReadingCriterionId: this.$route.query.TrialReadingCriterionId
}
const res = await getNextTask(params)
this.taskInfo = res.Result
localStorage.setItem('taskInfo', JSON.stringify(res.Result))
// if (res.Result.IsExistsClinicalData && res.Result.IsNeedReadClinicalData && !res.Result.IsReadClinicalData) {
// this.isFullscreen = false
// this.dialogVisible = true
// this.cdVisitTaskId = res.Result.visitTaskId
// }
// this.$nextTick(() => {
// if (res.Result.IsFirstChangeTask && res.Result.ReadingTaskState === 0) {
// this.tipVisible = true
// }
// })
this.loading = false
} catch (e) {
console.log(e)
store.dispatch('reading/setCurrentReadingTaskState', 2)
this.loading = false
}
}
}
}
</script>
<style lang="scss" scoped>
.reading-viewer-container {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,302 @@
<template>
<div v-loading="loading" class="ecrf-list-container">
<el-form
ref="questions"
size="small"
:model="questionForm"
class="ecrf-form"
>
<FormItem
v-for="question of questions"
:key="question.Id"
:question="question"
:question-form="questionForm"
:reading-task-state="readingTaskState"
:visit-task-id="visitTaskId"
:calculation-list="calculationList"
@setFormItemData="setFormItemData"
@resetFormItemData="resetFormItemData"
/>
<el-form-item v-if="readingTaskState < 2">
<div style="text-align:center;">
<el-button v-if="taskInfo && taskInfo.IseCRFShowInDicomReading" type="primary" @click="skipTask">
{{ $t('trials:readingReport:button:skip') }}
</el-button>
<el-button type="primary" @click="handleSave">
{{ $t('common:button:save') }}
</el-button>
<el-button v-if="taskInfo && taskInfo.IseCRFShowInDicomReading" type="primary" @click="handleSubmit">
{{ $t('common:button:submit') }}
</el-button>
</div>
</el-form-item>
</el-form>
<!-- 签名框 -->
<el-dialog
v-if="signVisible"
:visible.sync="signVisible"
:close-on-click-modal="false"
width="600px"
custom-class="base-dialog-wrapper"
>
<div slot="title">
<span style="font-size:18px;">{{ $t('common:dialogTitle:sign') }}</span>
<span style="font-size:12px;margin-left:5px">{{ `(${$t('common:label:sign')}${ currentUser })` }}</span>
</div>
<SignForm ref="signForm" :sign-code-enum="signCode" @closeDialog="closeSignDialog" />
</el-dialog>
</div>
</template>
<script>
import { getTrialReadingQuestion, saveVisitTaskQuestions, submitVisitTaskQuestionsInDto, getQuestionCalculateRelation } from '@/api/trials'
import { setSkipReadingCache } from '@/api/reading'
import const_ from '@/const/sign-code'
import FormItem from './FormItem'
import SignForm from '@/views/trials/components/newSignForm'
export default {
name: 'EcrfList',
components: {
FormItem,
SignForm
},
props: {
visitTaskInfo: {
type: Object,
required: true
}
},
data() {
return {
trialId: '',
criterionId: '',
visitTaskId: '',
loading: false,
questions: [],
questionForm: {},
publicQuestions: [],
signVisible: false,
signCode: null,
currentUser: zzSessionStorage.getItem('userName'),
readingTaskState: 2,
activeName: 0,
classArr: [],
calculationList: [],
taskInfo: null
}
},
watch: {
'visitTaskInfo.VisitTaskId': {
immediate: true,
handler(id) {
this.trialId = this.$route.query.trialId
this.criterionId = this.$route.query.TrialReadingCriterionId
if (!id) return
this.visitTaskId = id
this.getQuestionCalculateRelation()
this.getQuestions()
}
}
},
mounted() {
this.taskInfo = JSON.parse(localStorage.getItem('taskInfo'))
},
methods: {
async getQuestions() {
this.loading = true
try {
const param = {
readingQuestionCriterionTrialId: this.criterionId,
visitTaskId: this.visitTaskId
}
const res = await getTrialReadingQuestion(param)
if (res.IsSuccess) {
this.readingTaskState = res.OtherInfo.readingTaskState
res.Result.SinglePage.map((v) => {
if (v.Type === 'group' && v.Childrens.length === 0) return
if (!v.IsPage && v.Type !== 'group' && v.Type !== 'summary') {
this.$set(this.questionForm, v.Id, v.Answer ? v.Answer : null)
}
if (v.Type === 'class') {
this.classArr.push({ triggerId: v.ClassifyQuestionId, classId: v.Id, classifyAlgorithms: v.ClassifyAlgorithms, classifyType: v.ClassifyType })
}
if (v.Childrens.length > 0) {
this.setChild(v.Childrens)
}
})
this.questions = res.Result.SinglePage
}
this.loading = false
} catch (e) {
this.loading = false
}
},
setChild(obj) {
obj.forEach(i => {
if (i.Type !== 'group' && i.Type !== 'summary' && i.Id) {
this.$set(this.questionForm, i.Id, i.Answer ? i.Answer : null)
}
if (i.Type === 'class') {
this.classArr.push({ triggerId: i.ClassifyQuestionId, classId: i.Id, classifyAlgorithms: i.ClassifyAlgorithms, classifyType: i.ClassifyType })
}
if (i.Childrens && i.Childrens.length > 0) {
this.setChild(i.Childrens)
}
})
},
async getQuestionCalculateRelation() {
try {
const res = await getQuestionCalculateRelation({ TrialReadingCriterionId: this.criterionId })
this.calculationList = res.Result
} catch (e) {
console.log(e)
}
},
async handleSave() {
const valid = await this.$refs['questions'].validate()
if (!valid) return
this.loading = true
const answers = []
for (const k in this.questionForm) {
answers.push({ readingQuestionTrialId: k, answer: this.questionForm[k] })
}
const params = {
trialId: this.trialId,
visitTaskId: this.visitTaskId,
readingQuestionCriterionTrialId: this.criterionId,
answerList: answers
}
try {
const res = await saveVisitTaskQuestions(params)
if (res.IsSuccess) {
this.$message.success(this.$t('common:message:savedSuccessfully'))
}
this.loading = false
} catch (e) {
this.loading = false
}
},
async handleSubmit() {
const valid = await this.$refs['questions'].validate()
if (!valid) return
const { ImageAssessmentReportConfirmation } = const_.processSignature
this.signCode = ImageAssessmentReportConfirmation
this.signVisible = true
},
//
closeSignDialog(isSign, signInfo) {
if (isSign) {
this.signConfirm(signInfo)
} else {
this.signVisible = false
}
},
//
async signConfirm(signInfo) {
this.loading = true
var answers = []
for (const k in this.questionForm) {
answers.push({ readingQuestionTrialId: k, answer: this.questionForm[k] })
}
var params = {
data: {
trialId: this.trialId,
visitTaskId: this.visitTaskId,
readingQuestionCriterionTrialId: this.criterionId,
answerList: answers
},
signInfo: signInfo
}
try {
const res = await submitVisitTaskQuestionsInDto(params)
this.loading = false
if (res.IsSuccess) {
this.$message.success(this.$t('common:message:savedSuccessfully'))
this.isEdit = false
this.$refs['signForm'].btnLoading = false
this.signVisible = false
this.readingTaskState = 2
// window.opener.postMessage('refreshTaskList', window.location)
const confirm = await this.$confirm(
this.$t('trials:noneDicoms:message:msg1'),
{
type: 'warning',
distinguishCancelAndClose: true
}
)
if (confirm !== 'confirm') return
window.location.reload()
}
} catch (e) {
this.loading = false
this.$refs['signForm'].btnLoading = false
}
},
async skipTask() {
try {
//
const confirm = await this.$confirm(
this.$t('trials:readingReport:message:skipConfirm'),
{
type: 'warning',
distinguishCancelAndClose: true
}
)
if (confirm !== 'confirm') return
this.loading = true
const res = await setSkipReadingCache({ visitTaskId: this.visitTaskId })
this.loading = false
if (res.IsSuccess) {
window.location.reload()
}
} catch (e) {
this.loading = false
console.log(e)
}
},
resetFormItemData(v) {
this.questionForm[v] = null
},
setFormItemData(obj) {
this.$set(this.questionForm, obj.key, JSON.parse(JSON.stringify(obj.val)))
this.classArr.map(i => {
if (i.triggerId === obj.key) {
let answer = null
const list = JSON.parse(i.classifyAlgorithms)
if (i.classifyType === 0) {
const o = list.find(v => {
return (
parseFloat(obj.val) >= parseFloat(v.gt) &&
parseFloat(obj.val) < parseFloat(v.lt)
)
})
answer = o ? o.label : null
} else if (i.classifyType === 1) {
const o = list.find(v => {
return v.val.includes(obj.val)
})
answer = o ? o.label : null
}
this.$set(this.questionForm, i.classId, answer)
}
})
}
}
}
</script>
<style lang="scss" scoped>
.ecrf-list-container{
min-height:400px;
color: #ddd;
.ecrf-form{
::v-deep .el-form-item__label{
color: #ddd;
}
}
}
</style>

View File

@ -0,0 +1,588 @@
<template>
<div>
<!-- <div
v-if="!!question.GroupName && (questionForm[question.ParentId] === question.ParentTriggerValue) || question.ParentId===''||question.ParentId===null"
style="font-weight: bold;font-size: 16px;margin: 5px 0px;"
>
{{ question.GroupName }}
</div> -->
<div
v-if="!!question.GroupName && question.Type==='group'"
style="font-weight: bold;font-size: 16px;margin: 5px 0px;"
>
{{ question.GroupName }}
</div>
<template v-else>
<el-form-item
v-if="(question.ShowQuestion===1 && questionForm[question.ParentId] === question.ParentTriggerValue) || question.ShowQuestion===0"
:label="`${question.QuestionName}`"
:prop="question.Id"
:rules="[
{ required: (question.IsRequired === 0 || (question.IsRequired ===1 && question.RelevanceId && (question.RelevanceValueList.includes(isNaN(parseFloat(questionForm[question.RelevanceId])) ? questionForm[question.RelevanceId] : questionForm[question.RelevanceId].toString())))) && question.Type!=='group' && question.Type!=='summary',
message: $t('common:ruleMessage:specify'), trigger: ['blur', 'change']},
]"
:class="[question.Type==='group'?'mb':question.Type==='upload'?'uploadWrapper':'']"
>
<!-- 输入框 -->
<el-input
v-if="question.Type==='input'"
v-model="questionForm[question.Id]"
:disabled="readingTaskState >= 2"
/>
<!-- 多行文本输入框 -->
<el-input
v-if="question.Type==='textarea'"
v-model="questionForm[question.Id]"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4}"
:disabled="readingTaskState >= 2"
/>
<!-- 下拉框 -->
<!-- <el-select-->
<!-- v-if="question.Type==='select'"-->
<!-- v-model="questionForm[question.Id]"-->
<!-- :disabled="readingTaskState >= 2"-->
<!-- clearable-->
<!-- @change="((val)=>{formItemChange(val, question)})"-->
<!-- >-->
<!-- <el-option-->
<!-- v-for="val in question.TypeValue.split('|')"-->
<!-- :key="val"-->
<!-- :label="val"-->
<!-- :value="val"-->
<!-- />-->
<!-- </el-select>-->
<el-select
v-if="question.Type==='select'"
v-model="questionForm[question.Id]"
filterable
:placeholder="$t('common:placeholder:select')"
:disabled="readingTaskState >= 2"
@change="((val)=>{formItemChange(val, question)})"
>
<template v-if="question.DictionaryCode">
<el-option
v-for="item of $d[question.DictionaryCode]"
:key="item.id"
:value="item.value.toString()"
:label="item.label"
/>
</template>
<template v-else-if="question.TypeValue">
<el-option
v-for="val in question.TypeValue.split('|')"
:key="val.trim()"
:label="val.trim()"
:value="val.trim()"
/>
</template>
</el-select>
<!-- 单选 -->
<el-radio-group
v-if="question.Type==='radio'"
v-model="questionForm[question.Id]"
:disabled="readingTaskState >= 2"
@change="((val)=>{formItemChange(val, question)})"
>
<template v-if="question.DictionaryCode">
<el-radio
v-for="item of $d[question.DictionaryCode]"
:key="item.id"
:label="item.value.toString()"
>
{{ item.label }}
</el-radio>
</template>
<template v-else-if="question.TypeValue">
<el-radio
v-for="val in question.TypeValue.split('|')"
:key="val.trim()"
:label="val.trim()"
>
{{ val.trim() }}
</el-radio>
</template>
</el-radio-group>
<!-- 复选框 -->
<el-checkbox-group
v-if="question.Type==='checkbox'"
v-model="questionForm[question.Id]"
:disabled="readingTaskState >= 2"
>
<el-checkbox
v-for="val in question.TypeValue.split('|')"
:key="val.trim()"
:label="val.trim()"
>
{{ val.trim() }}
</el-checkbox>
</el-checkbox-group>
<!-- 数值 -->
<!-- :precision="2" :step="0.1" :max="10" -->
<template v-if="question.Type==='number'">
<!-- 数值 -->
<el-select
v-if="question.TypeValue"
v-model="questionForm[question.Id]"
clearable
@change="(val) => { formItemNumberChange(val, question) }"
>
<el-option
v-for="val in question.TypeValue.split('|')"
:key="val"
:label="val.trim()"
:value="val.trim()"
/>
</el-select>
<el-input
v-if="question.DataSource !== 1"
v-model="questionForm[question.Id]"
type="number"
onblur="value=parseFloat(value).toFixed(parseInt(localStorage.getItem('digitPlaces')));"
@change="(val) => { formItemNumberChange(val, question) }"
@input="limitInput($event, questionForm, question.Id)"
>
<template v-if="question.Unit !== 0" slot="append">{{ question.Unit !== 4 ? $fd('ValueUnit', question.Unit) : question.CustomUnit }}</template>
<template v-else-if="question.ValueType === 2" slot="append">%</template>
</el-input>
<el-input
v-if="question.DataSource === 1"
v-model="questionForm[question.Id]"
type="number"
onblur="value=parseFloat(value).toFixed(parseInt(localStorage.getItem('digitPlaces')));"
:disabled="question.DataSource === 1"
@input="limitInput($event, questionForm, question.Id)"
>
<template v-if="question.Unit !== 0" slot="append">{{ question.Unit !== 4 ? $fd('ValueUnit', question.Unit) : question.CustomUnit }}</template>
<template v-else-if="question.ValueType === 2" slot="append">%</template>
</el-input>
</template>
<!-- 自动分类 -->
<el-input
v-if="question.Type === 'class' && question.ClassifyShowType === 1"
v-model="questionForm[question.Id]"
:disabled="!question.ClassifyEditType"
/>
<el-select
v-if="question.Type === 'class' && question.ClassifyShowType === 2"
v-model="questionForm[question.Id]"
:disabled="!question.ClassifyEditType"
@change="(val) => { formItemChange(val, question) }"
>
<el-option
v-for="val in question.TypeValue.split('|')"
:key="val"
:label="val.trim()"
:value="val.trim()"
/>
</el-select>
<el-radio-group
v-if="question.Type === 'class' && question.ClassifyShowType === 3"
v-model="questionForm[question.Id]"
:disabled="!question.ClassifyEditType"
@change="(val) => { formItemChange(val, question) }"
>
<el-radio
v-for="item of question.TypeValue.split('|')"
:key="item.trim()"
:label="item.trim()"
>
{{ item.trim() }}
</el-radio>
</el-radio-group>
<el-input
v-if="question.Type === 'class' && question.ClassifyShowType === 4"
v-model="questionForm[question.Id]"
type="number"
:disabled="!question.ClassifyEditType"
@change="(val) => { formItemNumberChange(val, question) }"
/>
<!-- 上传图像 -->
<el-upload
v-if="question.Type==='upload'"
action
:accept="accept"
:limit="question.ImageCount"
:on-preview="handlePictureCardPreview"
:before-upload="handleBeforeUpload"
:http-request="uploadScreenshot"
list-type="picture-card"
:on-remove="handleRemove"
:file-list="fileList"
:class="{disabled:fileList.length >= question.ImageCount}"
:disabled="readingTaskState >= 2"
>
<i slot="default" class="el-icon-plus" />
<div slot="file" slot-scope="{file}">
<img
class="el-upload-list__item-thumbnail"
:src="OSSclientConfig.basePath + file.url"
alt=""
>
<span class="el-upload-list__item-actions">
<span
class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)"
>
<i class="el-icon-zoom-in" />
</span>
<span
v-if="readingTaskState < 2"
class="el-upload-list__item-delete"
@click="handleRemove(file)"
>
<i class="el-icon-delete" />
</span>
</span>
</div>
</el-upload>
<el-dialog
append-to-body
:visible.sync="imgVisible"
width="600px"
>
<el-image :src="OSSclientConfig.basePath + imageUrl" width="100%">
<div slot="placeholder" class="image-slot">
{{ $t('trials:readingUnit:qsList:message:loading') }}<span class="dot">...</span>
</div>
</el-image>
</el-dialog>
</el-form-item>
</template>
<FormItem
v-for="(item) in question.Childrens"
:key="item.Id"
:question="item"
:reading-task-state="readingTaskState"
:question-form="questionForm"
:visit-task-id="visitTaskId"
:calculation-list="calculationList"
@setFormItemData="setFormItemData"
@resetFormItemData="resetFormItemData"
@formItemNumberChange="formItemNumberChange"
/>
</div>
</template>
<script>
// import { uploadReadingAnswerImage } from '@/api/trials'
export default {
name: 'FormItem',
props: {
questionForm: {
type: Object,
default() {
return {}
}
},
question: {
type: Object,
default() {
return {}
}
},
readingTaskState: {
type: Number,
required: true
},
visitTaskId: {
type: String,
default: ''
},
calculationList: {
type: Array,
default() {
return []
}
}
},
data() {
return {
fileList: [],
accept: '.png,.jpg,.jpeg',
imgVisible: false,
imageUrl: '',
urls: [],
digitPlaces: null
}
},
watch: {
questionForm: {
deep: true,
immediate: true,
handler(v, oldv) {
try {
if (!v || !v[this.question.Id] || !oldv || !oldv[this.question.Id]) { return }
} catch (e) {
console.log(e, v)
}
if (this.question.Type === 'class') {
this.$emit('setFormItemData', { key: this.question.Id, val: v[this.question.Id], question: v })
}
this.formItemNumberChange(this.question.Id, false)
}
}
},
mounted() {
if (this.question.Type === 'upload') {
if (this.questionForm[this.question.Id]) {
this.urls = this.questionForm[this.question.Id].split('|')
this.fileList = []
this.urls.map(url => {
this.fileList.push({ name: '', url: `${url}` })
})
}
}
this.digitPlaces = localStorage.getItem('digitPlaces') ? parseInt(localStorage.getItem('digitPlaces')) : 0
},
methods: {
limitInput(value, a, b) {
if (value.indexOf('.') > -1) {
if (value.split('.')[1].length >= this.digitPlaces) {
this.$set(a, b, parseFloat(value).toFixed(this.digitPlaces))
}
}
},
logic(rules, num = 0) {
try {
if (rules.CalculateQuestionList.length === 0) {
return false
}
const dataArr = []
rules.CalculateQuestionList.forEach((o, i) => {
if (i === 0) {
if (rules.CustomCalculateMark > 4 && rules.CustomCalculateMark < 10) {
switch (rules.CustomCalculateMark) {
case 5:
this.questionForm[o.QuestionId].forEach((q, qi) => {
if (qi === 0) {
num = parseFloat(q[o.TableQuestionId])
} else {
num *= parseFloat(q[o.TableQuestionId])
}
})
break
case 6:
this.questionForm[o.QuestionId].forEach((q, qi) => {
if (qi === 0) {
num = parseFloat(q[o.TableQuestionId])
} else {
num += parseFloat(q[o.TableQuestionId])
}
})
break
case 7:
this.questionForm[o.QuestionId].forEach((q, qi) => {
if (qi === 0) {
num = parseFloat(q[o.TableQuestionId])
} else {
num += parseFloat(q[o.TableQuestionId])
}
})
num = this.questionForm[o.QuestionId].length === 0 ? 0 : num / this.questionForm[o.QuestionId].length
break
case 8:
const arr = []
this.questionForm[o.QuestionId].forEach(q => {
arr.push(q[o.TableQuestionId])
})
num = arr.length === 0 ? 0 : Math.max(...arr)
break
case 9:
const arr1 = []
this.questionForm[o.QuestionId].forEach(q => {
arr1.push(q[o.TableQuestionId])
})
num = arr1.length === 0 ? 0 : Math.min(...arr1)
break
}
} else {
num = parseFloat(this.questionForm[o.TableQuestionId])
if (!isNaN(num)) {
dataArr.push(num)
}
}
} else {
switch (rules.CustomCalculateMark) {
case 1:
num += parseFloat(this.questionForm[o.TableQuestionId])
break
case 2:
num -= parseFloat(this.questionForm[o.TableQuestionId])
break
case 3:
num *= parseFloat(this.questionForm[o.TableQuestionId])
break
case 4:
if (parseFloat(this.questionForm[o.TableQuestionId]) === 0) {
num = 0
} else {
num /= parseFloat(this.questionForm[o.TableQuestionId])
}
break
case 10:
if (!isNaN(parseFloat(this.questionForm[o.TableQuestionId]))) {
dataArr.push(parseFloat(this.questionForm[o.TableQuestionId]))
}
num = dataArr.length === 0 ? 0 : dataArr.reduce((acc, curr) => {
return acc + (typeof curr === 'number' ? curr : 0)
}, 0) / dataArr.length
break
case 11:
if (!isNaN(parseFloat(this.questionForm[o.TableQuestionId]))) {
dataArr.push(parseFloat(this.questionForm[o.TableQuestionId]))
}
num = Math.max(...dataArr)
break
case 12:
if (!isNaN(parseFloat(this.questionForm[o.TableQuestionId]))) {
dataArr.push(parseFloat(this.questionForm[o.TableQuestionId]))
}
num = Math.min(...dataArr)
break
case 13:
if (!isNaN(parseFloat(this.questionForm[o.TableQuestionId]))) {
dataArr.push(parseFloat(this.questionForm[o.TableQuestionId]))
}
num = dataArr.length === 0 ? 0 : dataArr.reduce((acc, curr) => acc && curr) ? 1 : 0
break
case 14:
if (!isNaN(parseFloat(this.questionForm[o.TableQuestionId]))) {
dataArr.push(parseFloat(this.questionForm[o.TableQuestionId]))
}
num = dataArr.length === 0 ? 0 : dataArr.reduce((acc, curr) => acc || curr, 0) ? 1 : 0
break
}
}
})
} catch (e) {
console.log(e)
}
var digitPlaces = parseInt(localStorage.getItem('digitPlaces'))
if (rules.ValueType === 2) {
num = num * 100
}
if (rules.CustomCalculateMark === 13 || rules.CustomCalculateMark === 14) {
return num
} else {
return num.toFixed(digitPlaces)
}
},
formItemNumberChange(questionId, isTable) {
if (isTable) {
this.calculationList.forEach((v, i) => {
var find = v.CalculateQuestionList.filter(o => {
return o.QuestionId === questionId
})
// findnumber
if (find) {
var num = this.logic(v)
if (num !== false) {
this.$emit('setFormItemData', { key: v.QuestionId, val: num, question: v })
}
}
})
} else {
this.calculationList.forEach(v => {
var find = v.CalculateQuestionList.filter(o => {
return o.TableQuestionId === questionId
})
// findnumber
if (find) {
var num = this.logic(v)
if (num !== false) {
this.$emit('setFormItemData', { key: v.QuestionId, val: num, question: v })
}
}
})
}
},
formItemChange(v, question) {
if (question.Childrens.length > 0) {
this.resetChild(question.Childrens)
} else {
this.$emit('setFormItemData', { key: question.Id, val: v, question: question })
}
},
resetChild(obj) {
obj.forEach(i => {
this.$emit('resetFormItemData', i.Id)
if (i.Childrens && i.Childrens.length > 0) {
this.resetChild(i.Childrens)
}
})
},
resetFormItemData(v) {
this.$emit('resetFormItemData', v)
},
setFormItemData(obj) {
this.$emit('setFormItemData', obj)
},
async uploadScreenshot(param) {
if (!this.visitTaskId) return
const loading = this.$loading({
target: document.querySelector('.ecrf-wrapper'),
fullscreen: false,
lock: true,
text: 'Loading',
spinner: 'el-icon-loading'
})
var file = await this.fileToBlob(param.file)
const res = await this.OSSclient.put(`/${this.trialId}/ReadAttachment/${this.subjectId}/${this.visitTaskId}/${param.file.name}`, file)
this.fileList.push({ name: param.file.name, url: this.$getObjectName(res.url) })
this.urls.push(this.$getObjectName(res.url))
this.$emit('setFormItemData', { key: this.question.Id, val: this.urls.length > 0 ? this.urls.join('|') : '' })
loading.close()
},
handleBeforeUpload(file) {
//
if (this.checkFileSuffix(file.name)) {
// this.fileList = []
return true
} else {
this.$alert(`必须是 ${this.accept} 格式`)
return false
}
},
checkFileSuffix(fileName) {
var index = fileName.lastIndexOf('.')
var suffix = fileName.substring(index + 1, fileName.length)
if (this.accept.toLocaleLowerCase().search(suffix.toLocaleLowerCase()) === -1) {
return false
} else {
return true
}
},
//
handlePictureCardPreview(file) {
this.imageUrl = this.OSSclientConfig.basePath + file.url
this.imgVisible = true
},
//
handleRemove(file, fileList) {
this.imageUrl = ''
this.fileList.splice(this.fileList.findIndex(f => f.url === file.url), 1)
this.urls.splice(this.fileList.findIndex(f => f === file.url), 1)
this.$emit('setFormItemData', { key: this.question.Id, val: this.urls.length > 0 ? this.urls.join('|') : '' })
}
}
}
</script>
<style lang="scss" scoped>
.mb{
margin-bottom: 0px;
}
.disabled{
::v-deep .el-upload--picture-card {
display: none;
}
}
.uploadWrapper{
display: flex;
flex-direction: column;
align-items: flex-start;
}
</style>

View File

@ -0,0 +1,509 @@
<template>
<div class="none-dicom-viewer">
<!-- tools -->
<div class="tools-wrapper">
<el-dropdown @command="handleCommand">
<span class="el-dropdown-link">
<i class="el-icon-menu" /><i class="el-icon-arrow-down el-icon--right" />
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="1*1">1*1</el-dropdown-item>
<el-dropdown-item command="1*2">1*2</el-dropdown-item>
<el-dropdown-item command="2*2">2*2</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<!-- viewports -->
<div class="viewports-wrapper">
<div class="grid-container" :style="gridStyle">
<div
v-for="(v, index) in viewportInfos"
v-show="index < cells.length"
:key="index"
:style="cellStyle"
:class="['grid-cell', index === activeCanvasIndex?'cell_active':'']"
@click="activeCanvas(index)"
@dblclick="toggleFullScreen(index)"
>
<div :ref="`canvas-${index}`" class="content">
<div class="left-top-text">
<div
v-if="v.taskInfo.IsExistsClinicalData"
class="cd-info"
:title="$t('trials:reading:button:clinicalData')"
@click.stop="viewCD($event)"
>
<svg-icon icon-class="documentation" class="svg-icon" />
</div>
<h2
v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo && v.taskInfo"
class="subject-info"
>
{{ `${taskInfo.SubjectCode} ${v.taskInfo.TaskBlindName} ` }}
</h2>
<div v-if="v.currentFileName">{{ v.currentFileName }}</div>
</div>
<div
v-if="taskInfo && taskInfo.IsReadingTaskViewInOrder === 1 && v.taskInfo"
class="top-center-tool"
>
<div class="toggle-visit-container">
<div
class="arrw_icon"
:style="{ cursor: v.taskInfo.VisitTaskNum !== 0 ? 'pointer' : 'not-allowed', color: v.taskInfo.VisitTaskNum !== 0 ? '#fff': '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, v.taskInfo.VisitTaskNum, -1)"
@dblclick.stop="preventDefault($event)"
>
<i class="el-icon-caret-left" />
</div>
<div class="arrow_text">
{{ v.taskInfo.TaskBlindName }}
</div>
<div
class="arrw_icon"
:style="{ cursor: v.taskInfo.VisitTaskNum < taskInfo.VisitNum ? 'pointer' : 'not-allowed', color: v.taskInfo.VisitTaskNum < taskInfo.VisitNum ? '#fff': '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, v.taskInfo.VisitTaskNum, 1)"
@dblclick.stop="preventDefault($event)"
>
<i class="el-icon-caret-right" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {
RenderingEngine,
Enums,
imageLoader,
metaData,
getRenderingEngine
} from '@cornerstonejs/core'
import * as cornerstoneTools from '@cornerstonejs/tools'
import initLibraries from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/initLibraries'
import hardcodedMetaDataProvider from './../js/hardcodedMetaDataProvider'
import registerWebImageLoader from './../js/registerWebImageLoader'
import { mapGetters } from 'vuex'
import store from '@/store'
import { readUint16 } from '../../../../../../../static/pdfjs/build/pdf.worker'
const { ViewportType } = Enums
const renderingEngineId = 'myRenderingEngine'
const { ToolGroupManager, Enums: csToolsEnums, StackScrollTool } = cornerstoneTools
const { MouseBindings } = csToolsEnums
export default {
name: 'ImageViewer',
props: {
relatedStudyInfo: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
rows: 1,
cols: 1,
fullScreenIndex: null,
imageIds: [],
activeCanvasIndex: 0,
layout: '1*2',
cellsMax: 4,
viewportInfos: [],
taskInfo: null
}
},
computed: {
gridStyle() {
return {
display: 'grid',
gridTemplateRows: `repeat(${this.rows}, 1fr)`,
gridTemplateColumns: `repeat(${this.cols}, 1fr)`,
height: '100%',
width: '100%'
}
},
cellStyle() {
return {
border: '1px dashed #ccc',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}
},
cells() {
return Array(this.rows * this.cols).fill(0)
},
...mapGetters(['lastViewportTaskId'])
},
watch: {
relatedStudyInfo: {
immediate: true,
handler(obj) {
if (!obj || Object.keys(obj).length === 0) return
this.updateViewportInfos(0, obj)
}
}
},
mounted() {
this.taskInfo = JSON.parse(localStorage.getItem('taskInfo'))
if (this.taskInfo.VisitNum > 0) {
this.rows = 1
this.cols = 2
this.activeCanvasIndex = 1
}
this.viewportInfos = Array.from({ length: this.cellsMax }, (_, index) => ({
index: index,
taskInfo: '',
currentImageIdIndex: 0,
viewportId: `canvas-${index}`,
currentFileName: '',
imageIds: []
}))
this.initLoader()
},
methods: {
//
async initLoader() {
registerWebImageLoader(imageLoader)
await initLibraries()
let renderingEngine = getRenderingEngine(renderingEngineId)
if (!renderingEngine) {
renderingEngine = new RenderingEngine(renderingEngineId)
}
const resizeObserver = new ResizeObserver(() => {
const renderingEngine = getRenderingEngine(renderingEngineId)
if (renderingEngine) {
renderingEngine.resize(true, false)
}
})
const element1 = this.$refs['canvas-0'][0]
const element2 = this.$refs['canvas-1'][0]
const element3 = this.$refs['canvas-2'][0]
const element4 = this.$refs['canvas-3'][0]
const elements = [
element1,
element2,
element3,
element4
]
elements.forEach((element, i) => {
element.oncontextmenu = (e) => e.preventDefault()
resizeObserver.observe(element)
element.addEventListener('CORNERSTONE_STACK_NEW_IMAGE', this.stackNewImage)
})
const viewportInputArray = [
{
viewportId: 'canvas-0',
type: ViewportType.STACK,
element: element1
},
{
viewportId: 'canvas-1',
type: ViewportType.STACK,
element: element2
},
{
viewportId: 'canvas-2',
type: ViewportType.STACK,
element: element3
},
{
viewportId: 'canvas-3',
type: ViewportType.STACK,
element: element4
}
]
const viewportIds = ['canvas-0', 'canvas-1', 'canvas-2', 'canvas-3']
renderingEngine.setViewports(viewportInputArray)
cornerstoneTools.addTool(StackScrollTool)
viewportIds.forEach((viewportId, i) => {
const toolGroupId = `canvas-${i}`
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId)
toolGroup.addViewport(viewportId, renderingEngineId)
toolGroup.addTool(StackScrollTool.toolName)
toolGroup.setToolActive(StackScrollTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Wheel }]
})
})
},
//
stackNewImage(e) {
const { detail } = e
const i = this.viewportInfos.findIndex(i => i.viewportId === detail.viewportId)
if (i === -1) return
this.viewportInfos[i].currentImageIdIndex = detail.imageIdIndex
const obj = this.viewportInfos[i].fileList[detail.imageIdIndex]
if (!obj) return
this.viewportInfos[i].currentFileName = obj.FileName
},
//
async renderImage(imageIds, canvasIndex, sliceIndex) {
metaData.addProvider((type, imageId) => hardcodedMetaDataProvider(type, imageId, imageIds), 10000)
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewport = renderingEngine.getViewport(`canvas-${canvasIndex}`)
await viewport.setStack(imageIds)
viewport.setImageIdIndex(sliceIndex)
viewport.render()
// this.updateViewportInfos()
},
setActiveCanvasImages(obj) {
if (!obj || Object.keys(obj).length === 0) return
const i = this.viewportInfos.findIndex(i => i.index === this.activeCanvasIndex)
if (i === -1) return
if (obj.visitTaskInfo.VisitTaskId === this.viewportInfos[i].taskInfo.VisitTaskId) {
this.sliceIndex(obj.fileIndex)
} else {
this.updateViewportInfos(this.activeCanvasIndex, obj)
store.dispatch('noneDicomReview/setLastViewportTaskId', obj.visitTaskInfo.VisitTaskId)
}
},
//
activeCanvas(index) {
if (this.activeCanvasIndex === index) return
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1) return
store.dispatch('noneDicomReview/setLastViewportTaskId', this.viewportInfos[i].taskInfo.VisitTaskId)
this.activeCanvasIndex = index
// this.$emit('toggleTask', this.viewportInfos[i].taskInfo)
},
//
updateViewportInfos(index, obj) {
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1) return
this.viewportInfos[i].taskInfo = obj.visitTaskInfo
this.viewportInfos[i].currentImageIdIndex = obj.fileIndex
this.viewportInfos[i].currentFileName = obj.fileInfo.FileName
this.viewportInfos[i].fileList = obj.fileList
const imageIds = []
for (let i = 0; i < obj.fileList.length; i++) {
const path = obj.fileList[i].Path
imageIds.push(`web:${this.OSSclientConfig.basePath}${path}`)
}
this.viewportInfos[i].imageIds = imageIds
if (imageIds.length > 0) {
this.renderImage(imageIds, index, obj.fileIndex)
}
},
//
sliceIndex(index) {
const i = this.viewportInfos.findIndex(i => i.index === this.activeCanvasIndex)
if (i === -1) return
if (index < 0 || index >= this.viewportInfos[i].imageIds.length) return
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportInfos[i].viewportId
)
viewport.setImageIdIndex(index)
viewport.render()
},
//
handleCommand(command) {
this.layout = command
this.rows = parseInt(command.split('*')[0])
this.cols = parseInt(command.split('*')[1])
// 1*1 1*2 线* 2*2
// const obj = this.viewportInfos.find(i => i.index === this.activeCanvasIndex)
// if (obj && this.rows === 1 && this.cols === 1) {
// this.viewportInfos = this.viewportInfos.map((v, i) => {
// if (i === 0) {
// v.taskInfo = obj.taskInfo
// v.currentImageIdIndex === obj.currentImageIdIndex
// v.currentFileName === obj.currentFileName
// v.imageIds === obj.imageIds
// } else {
// v.taskInfo = ''
// v.currentImageIdIndex === 0
// v.currentFileName === ''
// v.imageIds === []
// }
// return v
// })
// this.activeCanvasIndex = 0
// } else if (obj && this.rows === 1 && this.cols === 2 && this.taskInfo.IsReadingTaskViewInOrder === 1) {
// this.viewportInfos = this.viewportInfos.map((v, i) => {
// if (i === 0) {
// v.taskInfo = this.relatedStudyInfo.visitTaskInfo
// v.currentImageIdIndex === this.relatedStudyInfo.fileIndex
// v.currentFileName === this.relatedStudyInfo.fileInfo.FileName
// const imageIds = []
// for (let i = 0; i < this.relatedStudyInfo.fileList.length; i++) {
// const path = this.relatedStudyInfo.fileList[i].Path
// imageIds.push(`web:${this.OSSclientConfig.basePath}${path}`)
// }
// v.imageIds === imageIds
// } else if (i === 1) {
// v.taskInfo = obj.taskInfo
// v.currentImageIdIndex === obj.currentImageIdIndex
// v.currentFileName === obj.currentFileName
// v.imageIds === obj.imageIds
// } else {
// v.taskInfo = ''
// v.currentImageIdIndex === 0
// v.currentFileName === ''
// v.imageIds === []
// }
// return v
// })
// this.activeCanvasIndex = 1
// } else if (obj && this.rows === 1 && this.cols === 2 && this.taskInfo.IsReadingTaskViewInOrder !== 1) {
// this.viewportInfos = this.viewportInfos.map((v, i) => {
// if (i === 0 || i === 1) {
// v.taskInfo = obj.taskInfo
// v.currentImageIdIndex === obj.currentImageIdIndex
// v.currentFileName === obj.currentFileName
// v.imageIds === obj.imageIds
// } else {
// v.taskInfo = ''
// v.currentImageIdIndex === 0
// v.currentFileName === ''
// v.imageIds === []
// }
// return v
// })
// this.activeCanvasIndex = 1
// } else if (obj && (this.rows === 2 && this.cols === 2)) {
// this.viewportInfos = this.viewportInfos.map(v => {
// v.taskInfo = obj.taskInfo
// v.currentImageIdIndex === obj.currentImageIdIndex
// v.currentFileName === obj.currentFileName
// v.imageIds === obj.imageIds
// return v
// })
// this.activeCanvasIndex = 3
// }
// this.$nextTick(() => {
// this.viewportInfos.forEach(v => {
// if (v.imageIds.length > 0) {
// this.renderImage(v.imageIds, v.index, v.currentImageIdIndex)
// }
// })
// })
},
//
toggleFullScreen(index) {
this.fullScreenIndex = this.fullScreenIndex === index ? null : index
},
//
toggleTask(evt, visitTaskNum, i) {
const num = visitTaskNum + i
if (num >= 0 && num <= this.taskInfo.VisitNum) {
this.$emit('toggleTaskByViewer', num)
}
evt.stopImmediatePropagation()
evt.stopPropagation()
evt.preventDefault()
},
preventDefault(e) {
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
},
//
viewCD(e) {
}
}
}
</script>
<style lang="scss" scoped>
.none-dicom-viewer {
display: flex;
flex-direction: column;
width:100%;
height: 100%;
user-select: none;
.tools-wrapper {
height: 60px;
border-bottom: 1px solid #727272;
}
.viewports-wrapper {
flex: 1;
.grid-container {
display: grid;
height: 100%;
width: 100%;
position: relative;
}
.grid-cell {
border: 1px dashed #ccc;;
display: flex;
align-items: center;
justify-content: center;
}
.cell_active {
border-color: #fafa00!important;
}
.full-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.content {
width: 100%;
height: 100%;
position: relative;
.left-top-text {
position: absolute;
left: 5px;
top: 5px;
color: #ddd;
z-index: 1;
font-size: 12px;
.cd-info {
color: #ddd;
font-size: 18px;
cursor: pointer;
}
.subject-info {
color:#f44336;
padding: 5px 0px;
margin: 0;
}
}
.top-center-tool {
position: absolute;
left:50%;
top: 5px;
transform: translateX(-50%);
z-index: 1;
.toggle-visit-container {
display: flex;
}
.arrw_icon{
width: 20px;
height: 20px;
background-color: #3f3f3f;
text-align: center;
line-height: 20px;
border-radius: 10%;
}
.arrow_text{
height: 20px;
line-height: 20px;
background-color: #00000057;
color: #fff;
padding:0 10px;
font-size: 14px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,299 @@
<template>
<div v-loading="loading" class="read-page-container">
<!-- 检查列表 -->
<div class="left-panel">
<div class="task-container">
<div class="task-info">
<div
v-for="s in visitTaskList"
:key="s.VisitTaskId"
class="task-item"
:class="{'task-item-active': activeTaskVisitId==s.VisitTaskId}"
@click.prevent="toggleTask(s)"
>{{ s.TaskBlindName }}</div>
</div>
</div>
<div class="study-info">
<div
v-for="s in visitTaskList"
v-show="activeTaskVisitId === s.VisitTaskId"
:key="s.VisitTaskId"
style="height:100%;"
>
<study-list
v-if="selectArr.includes(s.VisitTaskId) && s.StudyList.length > 0"
:ref="s.VisitTaskId"
:visit-task-info="s"
@selectFile="selectFile"
/>
</div>
</div>
</div>
<!-- 图像 -->
<div class="middle-panel">
<image-viewer
ref="imageViewer"
:related-study-info="relatedStudyInfo"
@toggleTaskByViewer="toggleTaskByViewer"
@toggleTask="toggleTask"
/>
</div>
<!-- 表单 -->
<div class="right-panel">
<div
v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo"
class="text-info"
>
<h3>
<span
v-if="currentVisitInfo && currentVisitInfo.SubjectCode"
>
{{ currentVisitInfo.SubjectCode }}
</span>
<span
v-if="currentVisitInfo && currentVisitInfo.TaskBlindName"
style="margin-left:5px;"
>
{{ currentVisitInfo.TaskBlindName }}
</span>
</h3>
</div>
<ecrf-list
v-if="currentVisitInfo"
:visit-task-info="currentVisitInfo"
/>
</div>
</div>
</template>
<script>
import { getRelatedVisitTask, getReadingImageFile } from '@/api/trials'
import StudyList from './StudyList'
import ImageViewer from './ImageViewer'
import EcrfList from './EcrfList'
import { mapGetters } from 'vuex'
export default {
name: 'ReadPage',
components: {
StudyList,
ImageViewer,
EcrfList
},
data() {
return {
loading: false,
//
taskInfo: null,
// Id
activeTaskVisitId: '',
//
formData: {
name: '',
description: ''
},
//
visitTaskList: [],
selectArr: [],
fileType: 'image',
currentStudyInfo: null,
currentVisitInfo: null,
relatedStudyInfo: null
}
},
computed: {
...mapGetters(['lastViewportTaskId'])
},
watch: {
lastViewportTaskId: {
immediate: true,
handler(id) {
if (!id) return
const idx = this.visitTaskList.findIndex(i => i.VisitTaskId === id)
if (idx === -1) return
this.currentVisitInfo = this.visitTaskList[idx]
this.activeTaskVisitId = id
}
}
},
mounted() {
this.taskInfo = JSON.parse(localStorage.getItem('taskInfo'))
this.getRelatedTask()
},
methods: {
//
async getRelatedTask() {
this.loading = true
try {
const params = {
visitTaskId: this.taskInfo.VisitTaskId
}
const res = await getRelatedVisitTask(params)
this.visitTaskList = res.Result.map((item) => ({
...item,
StudyList: []
}))
const idx = res.Result.findIndex(i => i.IsCurrentTask)
if (idx > -1) {
await this.setActiveTaskVisitId(res.Result[idx].VisitTaskId)
this.$nextTick(() => {
this.$refs[res.Result[idx].VisitTaskId][0].setInitActiveFile()
})
}
if (this.taskInfo.IsReadingTaskViewInOrder === 1 && res.Result.length > 1) {
const i = this.visitTaskList.findIndex(i => i.IsBaseLineTask)
if (i > -1) {
await this.getReadingImageFile(res.Result[i].VisitTaskId, i)
const studyList = this.visitTaskList[i].StudyList
if (studyList.length > 0) {
const fileInfo = studyList[0].NoneDicomStudyFileList[0]
this.relatedStudyInfo = { fileInfo, visitTaskInfo: this.visitTaskList[i], fileList: studyList[0].NoneDicomStudyFileList, fileIndex: 0, studyId: studyList[0].Id }
}
}
}
this.loading = false
} catch (e) {
console.log(e)
this.loading = false
}
},
//
getInitStudyList() {
// 线
},
//
getReadingImageFile(visitTaskId, visitTaskIdx) {
return new Promise(async(resolve, reject) => {
this.loading = true
try {
const params = {
subjectId: this.taskInfo.SubjectId,
trialId: this.$route.query.trialId,
visistTaskId: visitTaskId
}
const res = await getReadingImageFile(params)
this.$set(this.visitTaskList[visitTaskIdx], 'StudyList', res.Result)
this.loading = false
resolve()
} catch (e) {
console.log(e)
this.loading = false
reject(e)
}
})
},
//
toggleTask(taskInfo) {
this.setActiveTaskVisitId(taskInfo.VisitTaskId)
},
// 访
async setActiveTaskVisitId(id, isInitActiveFile = false) {
if (!id) return
if (!this.selectArr.includes(id)) {
this.selectArr.push(id)
}
const idx = this.visitTaskList.findIndex(i => i.VisitTaskId === id)
if (idx === -1) return
if (this.visitTaskList[idx].StudyList.length === 0) {
await this.getReadingImageFile(id, idx)
}
this.activeTaskVisitId = id
if (isInitActiveFile) {
this.$refs[id][0].setInitActiveFile()
}
},
//
async toggleTaskByViewer(visitTaskNum) {
const i = this.visitTaskList.findIndex(v => v.VisitTaskNum === visitTaskNum)
if (i === -1) return
const visistTaskId = this.visitTaskList[i].VisitTaskId
this.setActiveTaskVisitId(visistTaskId, true)
},
selectFile(obj) {
this.$refs['imageViewer'].setActiveCanvasImages(obj)
}
}
}
</script>
<style lang="scss" scoped>
.read-page-container {
box-sizing: border-box;
height: 100%;
display: flex;
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
background: #d0d0d0;
}
.left-panel {
display: flex;
width: 200px;
border: 1px solid #727272;
color: #fff;
::-webkit-scrollbar {
width: 3px;
height: 3px;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
background: #d0d0d0;
}
.task-container {
position: relative;
width: 25px;
overflow-y: auto;
}
.task-info {
position: absolute;
top: 5px;
right: 20px;
transform-origin: right top;
transform: rotate(-90deg);
display: flex;
.task-item {
margin-left: 10px;
white-space: nowrap;
padding: 0px 4px;
border: 1px solid #999999;
border-bottom:none ;
text-align: center;
background-color: #4e4e4e;
color: #d5d5d5;
cursor: pointer;
}
.task-item-active {
background-color: #607d8b;
border: 1px solid #607d8b;
}
}
.study-info {
width: 170px;
border-left: 1px solid #727272;
}
}
.middle-panel {
flex: 1;
border: 1px solid #727272;
margin: 0 5px;
}
.right-panel {
width: 400px;
border: 1px solid #727272;
padding: 0 10px;
.text-info {
display: flex;
align-items: center;
justify-content: space-between;
color: #ddd;
}
}
}
</style>

View File

@ -0,0 +1,3 @@
<template>
<div>报告页</div>
</template>

View File

@ -0,0 +1,184 @@
<template>
<div v-loading="loading" class="study-wrapper">
<div class="study-info">
<div
v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo"
:title="taskInfo.SubjectCode"
>
{{ taskInfo.SubjectCode }}
</div>
<div
v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo"
:title="visitTaskInfo.TaskBlindName"
>
{{ visitTaskInfo.TaskBlindName }}
</div>
</div>
<div class="ps">
<el-collapse v-model="activeNames">
<el-collapse-item v-for="(study, index) in studyList" :key="`${study.Id}`" :name="`${study.Id}`">
<template slot="title">
<div
class="dicom-desc"
>
<div>{{ study.CodeView }}</div>
<div>
<span :title="study.BodyPart">{{ study.BodyPart }}</span>
<span style="margin-left: 5px;" :title="study.Modality">{{ study.Modality }}</span>
</div>
</div>
</template>
<div class="series">
<div
v-for="(k, i) in study.NoneDicomStudyFileList"
:key="i"
style="position:relative;margin-top:1px;"
series-type="current"
@click="selectFile(study, index, i)"
>
<div
:class="{'series-active': index === activeStudyIndex && i === activeFileIndex}"
class="series-wrapper"
:title="k.FileName"
>
{{ k.FileName }}
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script>
export default {
name: 'StudyList',
props: {
visitTaskInfo: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
activeNames: [],
activeStudyIndex: -1,
activeFileIndex: -1,
taskInfo: null,
studyList: []
}
},
mounted() {
this.taskInfo = JSON.parse(localStorage.getItem('taskInfo'))
this.studyList = this.visitTaskInfo.StudyList
if (this.studyList.length === 0) return
this.activeNames.push(this.studyList[0].Id)
},
methods: {
//
setInitActiveFile() {
if (this.studyList.length === 0) return
this.activeNames.push(this.studyList[0].Id)
this.selectFile(this.studyList[0].NoneDicomStudyFileList, 0, 0)
},
//
selectFile(study, studyIndex, fileIndex) {
this.activeStudyIndex = studyIndex
this.activeFileIndex = fileIndex
let fileList = study.NoneDicomStudyFileList
this.$emit('selectFile', { fileInfo: fileList[fileIndex], fileList, visitTaskInfo: this.visitTaskInfo, fileIndex: fileIndex, studyId: study.Id})
}
}
}
</script>
<style lang="scss" scoped>
.study-wrapper{
width:100%;
height: 100%;
overflow-y: hidden;
overflow-x: hidden;
display: flex;
flex-direction: column;
.study-info {
font-size: 16px;
font-weight: bold;
color: #ddd;
padding: 5px 0px;
margin: 0;
text-align: center;
background-color: #4c4c4c;
height: 50px;
}
.dicom-desc{
font-weight: bold;
font-size: 13px;
text-align: left;
color: #d0d0d0;
padding: 2px;
}
.ps {
flex: 1;
overflow-anchor: none;
touch-action: auto;
overflow-y: auto;
}
.series-active {
background-color: #607d8b!important;
border: 1px solid #607d8b!important;
}
::v-deep.el-progress__text{
color: #ccc;
font-size: 12px;
}
.series{
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
.series-wrapper {
width: 100%;
padding: 5px;
cursor: pointer;
background-color: #3a3a3a;
color: #ddd;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.el-progress__text{
display: none;
}
.el-progress-bar{
padding-right:0px;
}
}
}
::v-deep.el-collapse{
border: none;
.el-collapse-item{
background-color: #000!important;
color: #ddd;
}
.el-collapse-item__content{
padding-bottom:0px;
background-color: #000!important;
}
.el-collapse-item__header{
background-color: #000!important;
color: #ddd;
border-bottom-color:#5a5a5a;
padding-left: 5px;
height: 60px;
line-height: 20px;
}
}
::v-deep .el-progress-bar__inner{
transition: width 0s ease;
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="visit-review-container">
<el-tabs
v-model="activeName"
>
<!-- 阅片 -->
<el-tab-pane
v-if="taskInfo"
:label="$t('trials:reading:tabTitle:review')"
name="read"
>
<read-page />
</el-tab-pane>
<!-- 报告 -->
<el-tab-pane
v-if="taskInfo && !taskInfo.IseCRFShowInDicomReading"
:label="$t('trials:reading:tabTitle:report')"
name="report"
>
<report-page />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import ReadPage from './components/ReadPage'
import ReportPage from './components/ReportPage'
export default {
name: 'VisitReview',
components: {
ReadPage,
ReportPage
},
data() {
return {
activeName: 'read',
taskInfo: null
}
},
mounted() {
this.taskInfo = JSON.parse(localStorage.getItem('taskInfo'))
}
}
</script>
<style lang="scss" scoped>
.visit-review-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #000;
padding: 5px;
::v-deep .el-tabs {
height: 100%;
display: flex;
flex-direction: column;
.el-tabs__item {
color: #fff;
}
.el-tabs__item.is-active {
color: #428bca;
}
.el-tabs__item:hover {
color: #428bca;
}
.el-tabs__header {
height: 50px;
margin:0px;
}
.el-tabs__content {
flex: 1;
margin:0px;
}
.el-tab-pane {
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,68 @@
// Add hardcoded meta data provider for color images
export default function hardcodedMetaDataProvider(type, imageId, imageIds) {
const colonIndex = imageId.indexOf(':')
const scheme = imageId.substring(0, colonIndex)
if (scheme !== 'web') {
return
}
if (type === 'imagePixelModule') {
const imagePixelModule = {
pixelRepresentation: 0,
bitsAllocated: 24,
bitsStored: 24,
highBit: 24,
photometricInterpretation: 'RGB',
samplesPerPixel: 3
}
return imagePixelModule
} else if (type === 'generalSeriesModule') {
const generalSeriesModule = {
modality: 'SC',
seriesNumber: 1,
seriesDescription: 'Color',
seriesDate: '20190201',
seriesTime: '120000',
seriesInstanceUID: '1.2.276.0.7230010.3.1.4.83233.20190201120000.1'
}
return generalSeriesModule
} else if (type === 'imagePlaneModule') {
const index = imageIds.indexOf(imageId)
// console.warn(index);
const imagePlaneModule = {
imageOrientationPatient: [1, 0, 0, 0, 1, 0],
imagePositionPatient: [0, 0, index * 5],
pixelSpacing: [1, 1],
columnPixelSpacing: 1,
rowPixelSpacing: 1,
frameOfReferenceUID: 'FORUID',
columns: 2048,
rows: 1216,
rowCosines: [1, 0, 0],
columnCosines: [0, 1, 0],
// setting useDefaultValues to true signals the calibration values above cannot be trusted
// and units should be displayed in pixels
usingDefaultValues: true
}
return imagePlaneModule
} else if (type === 'voiLutModule') {
return {
// According to the DICOM standard, the width is the number of samples
// in the input, so 256 samples.
windowWidth: [256],
// The center is offset by 0.5 to allow for an integer value for even
// sample counts
windowCenter: [128]
}
} else if (type === 'modalityLutModule') {
return {
rescaleSlope: 1,
rescaleIntercept: 0
}
} else {
return undefined
}
}

View File

@ -0,0 +1,244 @@
import * as cornerstone from '@cornerstonejs/core'
const canvas = document.createElement('canvas')
let lastImageIdDrawn
// Todo: this loader should exist in a separate package in the same monorepo
/**
* creates a cornerstone Image object for the specified Image and imageId
*
* @param image - An Image
* @param imageId - the imageId for this image
* @returns Cornerstone Image Object
*/
function createImage(image, imageId) {
// extract the attributes we need
const rows = image.naturalHeight
const columns = image.naturalWidth
function getPixelData(targetBuffer) {
const imageData = getImageData()
let targetArray
// Check if targetBuffer is provided for volume viewports
if (targetBuffer) {
targetArray = new Uint8Array(
targetBuffer.arrayBuffer,
targetBuffer.offset,
targetBuffer.length
)
} else {
targetArray = new Uint8Array(imageData.width * imageData.height * 3)
}
// modify original image data and remove alpha channel (RGBA to RGB)
convertImageDataToRGB(imageData, targetArray)
return targetArray
}
function convertImageDataToRGB(imageData, targetArray) {
for (let i = 0, j = 0; i < imageData.data.length; i += 4, j += 3) {
targetArray[j] = imageData.data[i]
targetArray[j + 1] = imageData.data[i + 1]
targetArray[j + 2] = imageData.data[i + 2]
}
}
function getImageData() {
let context
if (lastImageIdDrawn === imageId) {
context = canvas.getContext('2d')
} else {
canvas.height = image.naturalHeight
canvas.width = image.naturalWidth
context = canvas.getContext('2d')
context.drawImage(image, 0, 0)
lastImageIdDrawn = imageId
}
return context.getImageData(0, 0, image.naturalWidth, image.naturalHeight)
}
function getCanvas() {
if (lastImageIdDrawn === imageId) {
return canvas
}
canvas.height = image.naturalHeight
canvas.width = image.naturalWidth
const context = canvas.getContext('2d')
context.drawImage(image, 0, 0)
lastImageIdDrawn = imageId
return canvas
}
// Extract the various attributes we need
return {
imageId,
minPixelValue: 0,
maxPixelValue: 255,
slope: 1,
intercept: 0,
windowCenter: 128,
windowWidth: 255,
getPixelData,
getCanvas,
getImage: () => image,
rows,
columns,
height: rows,
width: columns,
color: true,
// we converted the canvas rgba already to rgb above
rgba: false,
columnPixelSpacing: 1, // for web it's always 1
rowPixelSpacing: 1, // for web it's always 1
invert: false,
sizeInBytes: rows * columns * 3,
numberOfComponents: 3
}
}
function arrayBufferToImage(arrayBuffer) {
return new Promise((resolve, reject) => {
const image = new Image()
const arrayBufferView = new Uint8Array(arrayBuffer)
const blob = new Blob([arrayBufferView])
const urlCreator = window.URL || window.webkitURL
const imageUrl = urlCreator.createObjectURL(blob)
image.src = imageUrl
image.onload = () => {
resolve(image)
urlCreator.revokeObjectURL(imageUrl)
}
image.onerror = (error) => {
urlCreator.revokeObjectURL(imageUrl)
reject(error)
}
})
}
//
// This is a cornerstone image loader for web images such as PNG and JPEG
//
const options = {
// callback allowing customization of the xhr (e.g. adding custom auth headers, cors, etc)
beforeSend: (xhr) => {
// xhr
}
}
// Loads an image given a url to an image
function loadImage(uri, imageId) {
const xhr = new XMLHttpRequest()
xhr.open('GET', uri, true)
xhr.responseType = 'arraybuffer'
options.beforeSend(xhr)
xhr.onprogress = function(oProgress) {
if (oProgress.lengthComputable) {
// evt.loaded the bytes browser receive
// evt.total the total bytes set by the header
const loaded = oProgress.loaded
const total = oProgress.total
const percentComplete = Math.round((loaded / total) * 100)
const eventDetail = {
imageId,
loaded,
total,
percentComplete
}
cornerstone.triggerEvent(
cornerstone.eventTarget,
'cornerstoneimageloadprogress',
eventDetail
)
}
}
const promise = new Promise((resolve, reject) => {
xhr.onload = function() {
const imagePromise = arrayBufferToImage(this.response)
imagePromise
.then((image) => {
const imageObject = createImage(image, imageId)
resolve(imageObject)
}, reject)
.catch((error) => {
console.error(error)
})
}
xhr.onerror = function(error) {
reject(error)
}
xhr.send()
})
const cancelFn = () => {
xhr.abort()
}
return {
promise,
cancelFn
}
}
function registerWebImageLoader(imageLoader) {
imageLoader.registerImageLoader('web', _loadImageIntoBuffer)
}
/**
* Small stripped down loader from cornerstoneDICOMImageLoader
* Which doesn't create cornerstone images that we don't need
*/
function _loadImageIntoBuffer(imageId, options) {
const uri = imageId.replace('web:', '')
const promise = new Promise((resolve, reject) => {
// get the pixel data from the server
loadImage(uri, imageId)
.promise.then(
(image) => {
if (
!options?.targetBuffer?.length ||
!options?.targetBuffer?.offset
) {
resolve(image)
return
}
// @ts-ignore
image.getPixelData(options.targetBuffer)
resolve(true)
},
(error) => {
reject(error)
}
)
.catch((error) => {
reject(error)
})
})
return {
promise,
cancelFn: undefined
}
}
export default registerWebImageLoader