阅片页面图像预览更改
parent
551cdf0ed3
commit
6223eac81a
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
})
|
||||
// find的自动计算值number
|
||||
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
|
||||
})
|
||||
// find的自动计算值number
|
||||
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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<div>报告页</div>
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue