Notes.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. <template>
  2. <ModuleBase ref="base" :type="data.type">
  3. <template #content>
  4. <!-- <label>标题:</label>
  5. <RichText
  6. ref="richText"
  7. v-model="data.title_con"
  8. :inline="true"
  9. :placeholder="'输入标题'"
  10. :font-size="data?.unified_attrib?.font_size"
  11. :font-family="data?.unified_attrib?.font"
  12. toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
  13. class="title-box"
  14. /> -->
  15. <el-table :key="refreshKey" :data="data.option" border style="width: 100%">
  16. <el-table-column prop="number" label="序号" width="70">
  17. <template slot-scope="scope">
  18. <el-input v-model="scope.row.number" />
  19. </template>
  20. </el-table-column>
  21. <el-table-column prop="con" label="内容" width="200">
  22. <template slot-scope="scope">
  23. <RichText
  24. v-if="property.isGetContent"
  25. ref="richText"
  26. :key="'richtext-1-' + scope.$index"
  27. v-model="scope.row.con"
  28. :inline="true"
  29. :item-index="scope.$index"
  30. :font-size="data?.unified_attrib?.font_size"
  31. :font-family="data?.unified_attrib?.font"
  32. :font-color="data?.unified_attrib?.text_color"
  33. toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
  34. @handleRichTextBlur="handleBlurCon"
  35. />
  36. </template>
  37. </el-table-column>
  38. <el-table-column v-if="isEnable(data.property.view_pinyin)" prop="con" label="拼音" width="150">
  39. <template slot-scope="scope">
  40. <RichText
  41. v-if="property.isGetContent"
  42. ref="richText"
  43. :key="'richtext-2-' + scope.$index"
  44. v-model="scope.row.pinyin"
  45. :inline="true"
  46. :font-size="data?.unified_attrib?.pinyin_size"
  47. :font-family="'League'"
  48. :font-color="data?.unified_attrib?.text_color"
  49. toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
  50. />
  51. </template>
  52. </el-table-column>
  53. <el-table-column prop="interpret" label="翻译" width="200">
  54. <template slot-scope="scope">
  55. <RichText
  56. v-if="property.isGetContent"
  57. ref="richText"
  58. :key="'richtext-3-' + scope.$index"
  59. v-model="scope.row.interpret"
  60. :inline="true"
  61. :font-size="data?.unified_attrib?.font_size"
  62. :font-family="data?.unified_attrib?.font"
  63. :font-color="data?.unified_attrib?.text_color"
  64. toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
  65. />
  66. </template>
  67. </el-table-column>
  68. <el-table-column prop="note" label="注释" width="200">
  69. <template slot-scope="scope">
  70. <RichText
  71. v-if="property.isGetContent"
  72. ref="richText"
  73. :key="'richtext-4-' + scope.$index"
  74. v-model="scope.row.note"
  75. :inline="true"
  76. :font-size="data?.unified_attrib?.font_size"
  77. :font-family="data?.unified_attrib?.font"
  78. :font-color="data?.unified_attrib?.text_color"
  79. toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
  80. />
  81. </template>
  82. </el-table-column>
  83. <el-table-column prop="img_list" label="图片" width="300">
  84. <template slot-scope="scope">
  85. <UploadPicture
  86. :file-info="scope.row.file_list[0]"
  87. :item-index="scope.$index"
  88. :show-upload="!scope.row.file_list[0]"
  89. @upload="uploadPic"
  90. @deleteFile="deletePic"
  91. />
  92. </template>
  93. </el-table-column>
  94. <el-table-column prop="match_jump" label="关联跳转" width="300">
  95. <template slot-scope="scope">
  96. <template v-if="scope.row.match_jump.type === 'outer' && scope.row.match_jump.con">
  97. 已关联:{{ scope.row.match_jump.con }}
  98. </template>
  99. <template v-else-if="scope.row.match_jump.type === 'inner' && scope.row.match_jump.source_courseware_name">
  100. 已关联:{{ scope.row.match_jump.source_courseware_name }}
  101. </template>
  102. <div class="match-btn" @click="handleMatch(scope.row)"><SvgIcon icon-class="match" /><span>关联</span></div>
  103. </template>
  104. </el-table-column>
  105. <el-table-column label="操作" width="150">
  106. <template slot-scope="scope">
  107. <el-button size="mini" type="text" @click="handleDelete(scope.$index)">删除</el-button>
  108. <el-button size="mini" type="text" @click="moveElement(scope.row, scope.$index, 'up')">上移</el-button>
  109. <el-button size="mini" type="text" @click="moveElement(scope.row, scope.$index, 'down')">下移</el-button>
  110. </template>
  111. </el-table-column>
  112. </el-table>
  113. <el-button icon="el-icon-plus" style="margin: 24px 0" @click="addElement">增加一个</el-button>
  114. <el-button @click="handleMultilingual">多语言</el-button>
  115. <MultilingualFill
  116. :visible.sync="multilingualVisible"
  117. :text="multilingualText"
  118. :translations="data.multilingual"
  119. @SubmitTranslation="handleMultilingualTranslation"
  120. />
  121. <el-dialog
  122. v-if="dialogFlag"
  123. :visible.sync="dialogFlag"
  124. :close-on-click-modal="false"
  125. :modal-append-to-body="false"
  126. :modal="false"
  127. width="600px"
  128. class="Notes-edit-dialog"
  129. title="关联跳转"
  130. >
  131. <div class="check-box">
  132. <el-form ref="form" :model="matchObj" label-width="80px">
  133. <el-form-item label="类型">
  134. <el-radio-group v-model="matchObj.type">
  135. <el-radio label="outer">外部跳转</el-radio>
  136. <el-radio label="inner">内部跳转</el-radio>
  137. </el-radio-group>
  138. </el-form-item>
  139. <el-form-item v-if="matchObj.type === 'outer'" label="关联链接">
  140. <el-input v-model="matchObj.con" />
  141. </el-form-item>
  142. <el-form-item v-else label="关联节点">
  143. <el-cascader
  144. :ref="refId"
  145. v-model="matchObj.source_courseware_id_list"
  146. :options="courseware_node_list"
  147. :show-all-levels="false"
  148. clearable
  149. @change="handleChangeCourseware"
  150. />
  151. </el-form-item>
  152. </el-form>
  153. <div class="btn-box">
  154. <el-button type="info" size="small" @click="cancleDialog">取消</el-button>
  155. <el-button type="primary" size="small" @click="sureMatch">确定</el-button>
  156. </div>
  157. </div>
  158. </el-dialog>
  159. </template>
  160. </ModuleBase>
  161. </template>
  162. <script>
  163. import { isEnable } from '@/views/book/courseware/data/common';
  164. import ModuleMixin from '../../common/ModuleMixin';
  165. import { ChapterGetBookChapterStruct, PinyinBuild_OldFormat } from '@/api/book';
  166. import UploadPicture from '../new_word/components/UploadPicture.vue';
  167. import { getNotesData, getOption } from '@/views/book/courseware/data/notes';
  168. import cnchar from 'cnchar';
  169. export default {
  170. name: 'NotesPage',
  171. components: { UploadPicture },
  172. mixins: [ModuleMixin],
  173. data() {
  174. return {
  175. isEnable,
  176. data: getNotesData(),
  177. multilingualText: '',
  178. refreshKey: '',
  179. dialogFlag: false,
  180. matchObj: null,
  181. activeMatchRow: null,
  182. courseware_node_list: [],
  183. refId: `Notes-${Math.random().toString(36).substring(2, 12)}`,
  184. };
  185. },
  186. watch: {
  187. 'data.option': 'handleMindMap',
  188. 'data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case': {
  189. handler(val, oldVal) {
  190. if (val === oldVal) return;
  191. if (isEnable(this.data.property.view_pinyin)) {
  192. this.handlePinyin();
  193. }
  194. },
  195. deep: true,
  196. },
  197. },
  198. created() {
  199. this.getBookChapterStructExpandList();
  200. },
  201. methods: {
  202. /**
  203. * 得到教材章节结构展开列表
  204. */
  205. getBookChapterStructExpandList() {
  206. ChapterGetBookChapterStruct({
  207. book_id: this.$route.query.project_id,
  208. node_deep_mode: 0,
  209. is_contain_producer: 'false',
  210. is_contain_auditor: 'false',
  211. }).then(({ nodes }) => {
  212. this.courseware_node_list = nodes;
  213. nodes.forEach((item) => {
  214. item.label = item.name;
  215. item.pid = '';
  216. item.nodexIndex = 0;
  217. item.isLeaf = item.is_leaf === 'true';
  218. item.value = item.id;
  219. if (item.nodes) {
  220. item.children = item.nodes;
  221. item.lists = item.nodes;
  222. this.handleData(item, 1);
  223. }
  224. });
  225. });
  226. },
  227. // 递归
  228. handleData(data, nodeIndex) {
  229. if (data.nodes) {
  230. data.nodes.forEach((item) => {
  231. item.label = item.name;
  232. item.pid = data.id;
  233. item.nodexIndex = nodeIndex;
  234. item.isLeaf = item.is_leaf === 'true';
  235. item.value = item.id;
  236. if (item.nodes) {
  237. item.children = item.nodes;
  238. item.lists = item.nodes;
  239. this.handleData(item, nodeIndex + 1);
  240. }
  241. });
  242. }
  243. },
  244. handleChangeCourseware(val) {
  245. this.matchObj.source_courseware_id = val[val.length - 1];
  246. this.matchObj.source_courseware_name = this.$refs[this.refId].getCheckedNodes()[0].pathLabels.join('/');
  247. },
  248. // 删除行
  249. handleDelete(index) {
  250. this.data.option.splice(index, 1);
  251. },
  252. // 上移下移
  253. moveElement(dItem, index, type) {
  254. let obj = JSON.parse(JSON.stringify(dItem));
  255. if (type === 'up' && index > 0) {
  256. this.data.option.splice(index - 1, 0, obj);
  257. this.data.option.splice(index + 1, 1);
  258. }
  259. if (type === 'down' && index < this.data.option.length - 1) {
  260. this.data.option[index] = this.data.option.splice(index + 1, 1, this.data.option[index])[0];
  261. }
  262. },
  263. // 增加
  264. addElement() {
  265. this.data.option.push(getOption());
  266. },
  267. handleMindMap() {
  268. // 思维导图数据
  269. let node_list = [];
  270. this.data.option.forEach((item) => {
  271. node_list.push({
  272. name: item.con.replace(/<[^>]*>?/gm, ''),
  273. id: Math.random().toString(36).substring(2, 12),
  274. });
  275. if (!item.hasOwnProperty('match_jump')) {
  276. this.$set(item, 'match_jump', { type: 'outer', con: '', source_courseware_id_list: [] });
  277. }
  278. });
  279. this.data.mind_map.node_list = node_list;
  280. },
  281. async handleBlurCon(i) {
  282. let text = this.data.option[i].con.replace(/<[^>]+>/g, '');
  283. if (text) {
  284. this.data.option[i].pinyin = await this.handlePinyins(
  285. text,
  286. this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case,
  287. );
  288. }
  289. // this.data.option[i].pinyin = cnchar
  290. // .spell(
  291. // text,
  292. // 'array',
  293. // this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case === 'true' ? 'high' : 'low',
  294. // 'tone',
  295. // )
  296. // .join(' ');
  297. this.handleMindMap();
  298. },
  299. async handlePinyin() {
  300. for (const item of this.data.option) {
  301. let text = item.con.replace(/<[^>]+>/g, '');
  302. if (text) {
  303. item.pinyin = await this.handlePinyins(
  304. text,
  305. this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case,
  306. );
  307. }
  308. }
  309. // this.data.option.forEach((item) => {
  310. // let text = item.con.replace(/<[^>]+>/g, '');
  311. // item.pinyin = cnchar
  312. // .spell(
  313. // text,
  314. // 'array',
  315. // this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case === 'true' ? 'high' : 'low',
  316. // 'tone',
  317. // )
  318. // .join(' ');
  319. // });
  320. },
  321. // 自动生成拼音
  322. handlePinyins(text, flag) {
  323. let data = {
  324. text,
  325. is_rich_text: 'false',
  326. is_first_sentence_first_hz_pinyin_first_char_upper_case: flag || 'false',
  327. is_fill_space: 'false',
  328. is_custom_fc: 'false',
  329. };
  330. return new Promise((resolve, reject) => {
  331. PinyinBuild_OldFormat(data).then((res) => {
  332. if (res.parsed_text) {
  333. let mergedData = '';
  334. res.parsed_text.paragraph_list.map((outerArr, i) =>
  335. outerArr.map((innerArr, j) =>
  336. innerArr.map((newItem, k) => {
  337. mergedData += `${newItem.pinyin} `;
  338. }),
  339. ),
  340. );
  341. resolve(mergedData);
  342. }
  343. });
  344. });
  345. },
  346. handleMultilingual() {
  347. this.multilingualText = this.data.title_con ? this.data.title_con : '<p>&nbsp;</p>';
  348. this.data.option.forEach((item) => {
  349. this.multilingualText += item.con ? item.con : '<p>&nbsp;</p>';
  350. this.multilingualText += item.note ? item.note : '<p>&nbsp;</p>';
  351. });
  352. this.multilingualVisible = true;
  353. },
  354. // 获取拼音解析文本
  355. createParsedTextInfoPinyin(text) {
  356. if (text === '') {
  357. this.data.paragraph_list_parameter.pinyin_proofread_word_list = [];
  358. return;
  359. }
  360. this.data.paragraph_list_parameter.text = text.replace(/<[^>]+>/g, '');
  361. this.data.paragraph_list_parameter.is_first_sentence_first_hz_pinyin_first_char_upper_case =
  362. this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case;
  363. PinyinBuild_OldFormat(this.data.paragraph_list_parameter).then((res) => {
  364. if (res.parsed_text) {
  365. const mergedData = res.parsed_text.paragraph_list.map((outerArr, i) =>
  366. outerArr.map((innerArr, j) =>
  367. innerArr.map((newItem, k) => {
  368. // 从 originalData 中找到对应的项
  369. const originalItem = this.data.paragraph_list[i]?.[j]?.[k];
  370. // 如果 originalItem 有 activeTextStyle,就合并到 newItem
  371. if (originalItem?.activeTextStyle) {
  372. return {
  373. ...newItem,
  374. activeTextStyle: originalItem.activeTextStyle,
  375. };
  376. }
  377. // 否则直接返回 newItem
  378. return newItem;
  379. }),
  380. ),
  381. );
  382. this.data.paragraph_list = mergedData;
  383. let pinyin_index = 0;
  384. this.data.option.forEach((items, index) => {
  385. items.model_pinyin = [];
  386. if (items.content && mergedData[pinyin_index] && mergedData[pinyin_index][0]) {
  387. mergedData[pinyin_index][0].forEach((itemP) => {
  388. items.model_pinyin.push(itemP);
  389. });
  390. pinyin_index++;
  391. }
  392. });
  393. }
  394. });
  395. },
  396. // 填充校对后的拼音
  397. fillCorrectPinyin({ selectContent: { text, pinyin, activeTextStyle }, i, j, k }) {
  398. this.data.paragraph_list_parameter.pinyin_proofread_word_list.push({
  399. paragraph_index: i,
  400. sentence_index: j,
  401. word_index: k,
  402. word: text,
  403. pinyin,
  404. });
  405. if (pinyin) this.data.paragraph_list[i][j][k].pinyin = pinyin;
  406. if (activeTextStyle) this.data.paragraph_list[i][j][k].activeTextStyle = activeTextStyle;
  407. },
  408. uploadPic(file_id, index, file) {
  409. this.data.option[index].file_list[0] = file;
  410. this.data.file_id_list.push(file_id);
  411. this.refreshKey = Math.random();
  412. },
  413. deletePic(file_id, index) {
  414. this.data.option[index].file_list[0] = '';
  415. this.data.file_id_list = this.data.file_id_list.filter((item) => item !== file_id);
  416. },
  417. // 点击关联按钮
  418. handleMatch(row) {
  419. this.matchObj = JSON.parse(JSON.stringify(row.match_jump));
  420. this.activeMatchRow = row;
  421. this.dialogFlag = true;
  422. },
  423. // 确定关联
  424. sureMatch() {
  425. this.dialogFlag = false;
  426. this.activeMatchRow.match_jump = JSON.parse(JSON.stringify(this.matchObj));
  427. this.matchObj = null;
  428. },
  429. cancleDialog() {
  430. this.dialogFlag = false;
  431. this.matchObj = null;
  432. this.activeMatchRow = null;
  433. },
  434. },
  435. };
  436. </script>
  437. <style lang="scss" scoped>
  438. .upload-file {
  439. display: flex;
  440. column-gap: 12px;
  441. align-items: center;
  442. margin: 8px 0;
  443. .file-name {
  444. display: flex;
  445. column-gap: 14px;
  446. align-items: center;
  447. justify-content: space-between;
  448. max-width: 360px;
  449. padding: 8px 12px;
  450. font-size: 14px;
  451. color: #1d2129;
  452. background-color: #f7f8fa;
  453. span {
  454. display: flex;
  455. column-gap: 14px;
  456. align-items: center;
  457. }
  458. }
  459. .svg-icon {
  460. cursor: pointer;
  461. }
  462. }
  463. .title-box {
  464. padding: 5px;
  465. margin: 5px 0;
  466. background-color: #f2f3f5;
  467. border: 1px solid #f2f3f5;
  468. border-radius: 4px;
  469. :deep p {
  470. margin: 0;
  471. }
  472. }
  473. .match-btn {
  474. display: flex;
  475. column-gap: 12px;
  476. align-items: center;
  477. width: 233px;
  478. padding: 4px 12px;
  479. font-size: 14px;
  480. color: #1d2129;
  481. cursor: pointer;
  482. background-color: #f2f3f5;
  483. }
  484. </style>
  485. <style lang="scss">
  486. .tox .tox-editor-header {
  487. z-index: 3 !important;
  488. }
  489. .Notes-edit-dialog {
  490. .el-dialog__body {
  491. padding: 0;
  492. }
  493. .check-box {
  494. padding: 24px;
  495. background: #fff;
  496. border-radius: 4px;
  497. box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 25%);
  498. .btn-box {
  499. display: block;
  500. text-align: right;
  501. .el-button {
  502. padding: 2px 12px;
  503. font-size: 12px;
  504. line-height: 20px;
  505. }
  506. }
  507. }
  508. }
  509. </style>