ImageTextPreview.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. <!-- eslint-disable vue/no-v-html -->
  2. <template>
  3. <div class="imageText-preview" :style="[getAreaStyle(), getComponentStyle()]">
  4. <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
  5. <template v-if="data.mp3_list && data.mp3_list.length > 0 && mp3_url">
  6. <AudioLine
  7. ref="audioLine"
  8. audio-id="Audio"
  9. :mp3="mp3_url"
  10. :get-cur-time="getCurTime"
  11. :duration="data.mp3_list[0].media_duration"
  12. :mp3-source="data.mp3_list[0].source"
  13. :width="audio_width"
  14. :ed="ed"
  15. type="audioLine"
  16. :attrib="data.unified_attrib"
  17. @emptyEd="emptyEd"
  18. />
  19. </template>
  20. <div
  21. v-if="image_url"
  22. class="img-box"
  23. :style="{
  24. background: 'url(' + image_url + ') center / contain no-repeat',
  25. width: isMobile ? '100%' : data.image_width + 'px',
  26. height: data.image_height + 'px',
  27. }"
  28. >
  29. <div
  30. v-for="(itemP, indexP) in data.text_list"
  31. :key="'text' + indexP"
  32. :class="['position-item', sentIndex === indexP ? 'active' : '']"
  33. :style="{
  34. width: itemP.width,
  35. height: itemP.height,
  36. left: itemP.x,
  37. top: itemP.y,
  38. }"
  39. @click="handleChangePosition(indexP)"
  40. ></div>
  41. <div
  42. v-for="(itemP, indexP) in data.input_list"
  43. :key="'input' + indexP"
  44. :class="['position-item position-item-input', 'active', ...computedAnswerClass(indexP)]"
  45. :style="{
  46. width: itemP.width,
  47. height: itemP.height,
  48. left: itemP.x,
  49. top: itemP.y,
  50. }"
  51. >
  52. <el-input
  53. v-model="answer.answer_list[indexP].text"
  54. :disabled="disabled"
  55. type="textarea"
  56. style="height: 100%"
  57. :placeholder="convertText('请输入')"
  58. />
  59. </div>
  60. </div>
  61. <PreviewOperation
  62. v-if="data.input_list.length > 0"
  63. @showAnswerAnalysis="showAnswerAnalysis"
  64. @judgeCorrect="judgeCorrect"
  65. @retry="retry"
  66. />
  67. <AnswerCorrect
  68. :answer-correct="data?.answer_correct"
  69. :visible.sync="visibleAnswerCorrect"
  70. :is-check-correct="isCheckCorrect"
  71. @closeAnswerCorrect="closeAnswerCorrect"
  72. />
  73. <AnswerAnalysis
  74. :visible.sync="visibleAnswerAnalysis"
  75. :answer-list="data.answer_list"
  76. :analysis-list="data.analysis_list"
  77. @closeAnswerAnalysis="closeAnswerAnalysis"
  78. >
  79. <div slot="right-answer" class="right-answer">
  80. <div
  81. v-if="image_url"
  82. class="img-box"
  83. :style="{
  84. background: 'url(' + image_url + ') center / contain no-repeat',
  85. width: data.image_width + 'px',
  86. height: data.image_height + 'px',
  87. }"
  88. >
  89. <div
  90. v-for="(itemP, indexP) in data.input_list"
  91. :key="'input' + indexP"
  92. :class="['position-item position-item-input', 'active']"
  93. :style="{
  94. width: itemP.width,
  95. height: itemP.height,
  96. left: itemP.x,
  97. top: itemP.y,
  98. }"
  99. >
  100. <el-input
  101. v-model="itemP.text"
  102. :disabled="disabled"
  103. type="textarea"
  104. style="height: 100%"
  105. :placeholder="convertText('请输入')"
  106. />
  107. </div>
  108. </div>
  109. </div>
  110. </AnswerAnalysis>
  111. <el-dialog
  112. v-if="mageazineDetailShow"
  113. :visible.sync="mageazineDetailShow"
  114. :show-close="false"
  115. :close-on-click-modal="false"
  116. :width="isMobile ? '100%' : '80%'"
  117. class="login-dialog magazine-detail-dialog"
  118. :class="[isMobile ? 'magazine-detail-dialog-phone' : '']"
  119. :modal="false"
  120. >
  121. <magazine-sentence
  122. :font-size="fontSize"
  123. :sentence-theme="sentenceTheme"
  124. :data="data.word_time"
  125. :text-list="data.text_list"
  126. :active-index="sentIndex"
  127. :mp3-url="mp3_url"
  128. :multilingual-text-list="showLang && multilingualTextList[getLang()] ? multilingualTextList[getLang()] : []"
  129. :property="data.property"
  130. :attrib="data.unified_attrib"
  131. @closeWord="closeMagazineSentence"
  132. @changeTheme="changeTheme"
  133. />
  134. </el-dialog>
  135. </div>
  136. </template>
  137. <script>
  138. import PreviewMixin from '../common/PreviewMixin';
  139. import AudioLine from '../voice_matrix/components/AudioLine.vue';
  140. import { getImageTextData } from '@/views/book/courseware/data/imageText';
  141. import MagazineSentence from './components/MagazineSentence.vue';
  142. export default {
  143. name: 'ImageTextPreview',
  144. components: { AudioLine, MagazineSentence },
  145. mixins: [PreviewMixin],
  146. props: {
  147. isMobile: {
  148. type: Boolean,
  149. default: false,
  150. },
  151. },
  152. data() {
  153. return {
  154. data: getImageTextData(),
  155. curTime: 0,
  156. paraIndex: -1, // 段落索引
  157. sentIndex: -1, // 句子索引
  158. ed: undefined,
  159. mp3_url: '',
  160. image_url: '',
  161. audio_width: 0,
  162. mageazineDetailIndex: null, // 当前高亮第几个
  163. mageazineDetailShow: false,
  164. inputIndex: null,
  165. fontSize: 20,
  166. sentenceTheme: 0,
  167. multilingualTextList: {},
  168. };
  169. },
  170. watch: {
  171. 'data.image_list': {
  172. handler() {
  173. this.initData();
  174. },
  175. },
  176. },
  177. created() {
  178. this.initData();
  179. },
  180. mounted() {
  181. this.audio_width = document.getElementsByClassName('imageText-preview')[0].clientWidth - 150;
  182. },
  183. methods: {
  184. initData() {
  185. if (!this.isJudgingRightWrong) {
  186. this.answer.answer_list = [];
  187. this.data.input_list.forEach((item) => {
  188. let obj = {
  189. text: '',
  190. answer: item.text,
  191. id: item.id,
  192. };
  193. this.answer.answer_list.push(obj);
  194. });
  195. }
  196. this.data.multilingual.forEach((item) => {
  197. let trans_arr = item.translation.split('\n');
  198. this.$set(this.multilingualTextList, item.type, trans_arr);
  199. });
  200. this.data.image_list.forEach((item) => {
  201. this.image_url = item.file_url;
  202. // GetFileURLMap({ file_id_list: [item.file_id] }).then(({ url_map }) => {
  203. // this.image_url = url_map[item.file_id];
  204. // });
  205. });
  206. this.data.mp3_list.forEach((item) => {
  207. this.mp3_url = item.temporary_url;
  208. // GetFileURLMap({ file_id_list: [item.file_id] }).then(({ url_map }) => {
  209. // this.mp3_url = url_map[item.file_id];
  210. // });
  211. });
  212. if (this.isMobile) {
  213. setTimeout(() => {
  214. let totalWidth = document.querySelector('.imageText-preview').offsetWidth;
  215. let rate = totalWidth / this.data.image_width;
  216. this.data.image_height *= rate;
  217. this.data.input_list.forEach((item) => {
  218. item.width = `${item.width.replace('px', '') * rate}px`;
  219. item.height = `${item.height.replace('px', '') * rate}px`;
  220. item.x = `${item.x.replace('px', '') * rate}px`;
  221. item.y = `${item.y.replace('px', '') * rate}px`;
  222. });
  223. this.data.text_list.forEach((item) => {
  224. item.width = `${item.width.replace('px', '') * rate}px`;
  225. item.height = `${item.height.replace('px', '') * rate}px`;
  226. item.x = `${item.x.replace('px', '') * rate}px`;
  227. item.y = `${item.y.replace('px', '') * rate}px`;
  228. });
  229. }, 50);
  230. }
  231. },
  232. /**
  233. * 计算答题对错选项字体颜色
  234. * @param {string} mark 选项标识
  235. */
  236. computedAnswerClass(i) {
  237. if (!this.isJudgingRightWrong && !this.isShowRightAnswer) {
  238. return '';
  239. }
  240. let answerOption = this.data.input_list[i] ? this.data.input_list[i].text : '';
  241. let selectValue = this.answer.answer_list[i].text ? this.answer.answer_list[i].text.trim() : '';
  242. let classList = [];
  243. let isRight = answerOption && answerOption === selectValue;
  244. if (this.isJudgingRightWrong && answerOption) {
  245. isRight ? classList.push('right') : classList.push('wrong');
  246. }
  247. if (this.isShowRightAnswer && !isRight) {
  248. classList.push('show-right-answer');
  249. }
  250. return classList;
  251. },
  252. getCurTime(curTime) {
  253. this.curTime = curTime * 1000;
  254. this.getSentIndex(this.curTime);
  255. },
  256. getSentIndex(curTime) {
  257. for (let i = 0; i < this.data.word_time.length; i++) {
  258. let bg = this.data.word_time[i].bg;
  259. let ed = this.data.word_time[i].ed;
  260. if (curTime >= bg && curTime <= ed) {
  261. this.sentIndex = i;
  262. break;
  263. }
  264. }
  265. },
  266. emptyEd() {
  267. this.ed = undefined;
  268. },
  269. // 切换画刊里面的卡片
  270. handleChangePosition(index) {
  271. if (this.$refs.audioLine && this.$refs.audioLine.audio.playing) {
  272. this.$refs.audioLine.PlayAudio();
  273. }
  274. this.sentIndex = index;
  275. this.mageazineDetailShow = true;
  276. },
  277. // 关闭画刊卡片
  278. closeMagazineSentence() {
  279. this.mageazineDetailShow = false;
  280. },
  281. // 切换主题色和文字大小
  282. changeTheme(theme, size) {
  283. if (theme !== '') this.sentenceTheme = theme;
  284. if (size) this.fontSize = size;
  285. },
  286. // 重做
  287. retry() {
  288. this.isJudgingRightWrong = false;
  289. this.isShowRightAnswer = false;
  290. this.initData();
  291. },
  292. /**
  293. * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
  294. */
  295. getNoTextContentData() {
  296. let noTextContentData = JSON.parse(JSON.stringify(this.data));
  297. const resetFieldMap = {
  298. mp3_list: [], // 音频列表
  299. detail: [],
  300. image_list: [], // 图片列表
  301. image_info_list: [],
  302. image_id_list: [], // 文件 id
  303. image_width: 500, // 图片宽度px
  304. image_height: 500, // 图片高度px
  305. text_list: [], // 文字框列表
  306. input_list: [], // 输入框列表
  307. file_id_list: [], // 文件 id
  308. word_time: [], // 时间字幕
  309. answer_list: [], // 答案列表
  310. analysis_list: [], // 解析列表
  311. };
  312. Object.assign(noTextContentData, resetFieldMap);
  313. if (noTextContentData.answer) {
  314. noTextContentData.answer.answer_list = [];
  315. noTextContentData.answer.reference_answer = '';
  316. }
  317. return noTextContentData;
  318. },
  319. },
  320. };
  321. </script>
  322. <style lang="scss" scoped>
  323. @use '@/styles/mixin.scss' as *;
  324. .imageText-preview {
  325. max-width: 100%;
  326. overflow: auto;
  327. }
  328. .position-item {
  329. position: absolute;
  330. z-index: 1;
  331. font-size: 0;
  332. cursor: pointer;
  333. border: 3px solid transparent;
  334. &.active {
  335. border-color: #ff1616;
  336. }
  337. &:hover {
  338. border-color: #ff1616;
  339. }
  340. &.position-item-input {
  341. border-color: #f90;
  342. &.right {
  343. border-color: $right-color;
  344. :deep .el-textarea__inner {
  345. color: $right-color;
  346. }
  347. }
  348. &.wrong {
  349. border-color: $error-color;
  350. :deep .el-textarea__inner {
  351. color: $error-color;
  352. }
  353. }
  354. }
  355. :deep .el-textarea__inner {
  356. height: 100%;
  357. padding: 5px;
  358. font-family: 'League', '楷体';
  359. line-height: 1;
  360. text-align: center;
  361. resize: none;
  362. background: transparent;
  363. border: none;
  364. }
  365. }
  366. .img-box {
  367. position: relative;
  368. margin: 20px auto;
  369. }
  370. .right-answer {
  371. .title {
  372. margin: 24px 0;
  373. }
  374. }
  375. :deep .el-slider {
  376. flex: 1;
  377. width: auto !important;
  378. }
  379. </style>
  380. <style lang="scss">
  381. .magazine-detail-dialog {
  382. .el-dialog__header,
  383. .el-dialog__body {
  384. padding: 0;
  385. }
  386. .el-dialog {
  387. position: absolute;
  388. bottom: 50px;
  389. left: 50%;
  390. margin-left: -40%;
  391. border: none;
  392. border-radius: 16px;
  393. box-shadow:
  394. 0 6px 30px 5px rgba(0, 0, 0, 5%),
  395. 0 16px 24px 2px rgba(0, 0, 0, 4%),
  396. 0 8px 10px -5px rgba(0, 0, 0, 8%);
  397. }
  398. &-phone {
  399. .el-dialog {
  400. margin-left: -50%;
  401. }
  402. }
  403. }
  404. </style>