Label.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. <template>
  2. <ModuleBase ref="base" :type="data.type">
  3. <template #content>
  4. <div>
  5. <div class="tag-edit">
  6. <template v-for="(tag, i) in data.dynamicTags">
  7. <el-input
  8. v-if="tag.isEditing"
  9. :key="'input-' + i"
  10. :ref="'inputRefs-' + i"
  11. v-model="tag.editName"
  12. class="input-new-tag"
  13. @blur="handleSave(i)"
  14. @keydown.enter.native="handleSave(i)"
  15. @keydown.escape.native="handleCancel(i)"
  16. />
  17. <el-tag
  18. v-else
  19. :key="'tag-' + i"
  20. closable
  21. :style="getTagStyle(tag)"
  22. class="dynamic-tag"
  23. @close="handleClose(i)"
  24. @dblclick.native="handleEdit(i)"
  25. >
  26. {{ tag.name }}
  27. </el-tag>
  28. </template>
  29. <el-input
  30. v-if="inputVisible"
  31. ref="saveTagInput"
  32. v-model="inputValue"
  33. class="input-new-tag"
  34. @keyup.enter.native="handleInputConfirm"
  35. @blur="handleInputConfirm"
  36. />
  37. <el-button
  38. v-else
  39. class="button-new-tag"
  40. :disabled="data.dynamicTags.length >= 16"
  41. @click="showAddLabelTextInput"
  42. >+ 添加标签</el-button
  43. >
  44. </div>
  45. </div>
  46. <hr style="margin: 16px 0; border-top: 1px solid #ededed" />
  47. <div class="tag-manager">
  48. <div class="tag-manager-box">
  49. <div class="left-area">
  50. <span class="tag-manager-title">常用标签 </span>
  51. <span class="tag-manager-text" :style="{ color: closable ? '#165dff' : '' }" @click="editCommonTags()">
  52. <SvgIcon icon-class="setup" size="14" />管理
  53. </span>
  54. <span
  55. class="tag-manager-text"
  56. :style="{ color: editLanguage ? '#165dff' : '' }"
  57. @click="editCommonTagsLanguage"
  58. >
  59. <SvgIcon icon-class="multilingual" size="14" />设置多语言
  60. </span>
  61. </div>
  62. <div class="right-area">
  63. <MultilingualFill
  64. v-if="multilingualVisible"
  65. :visible.sync="multilingualVisible"
  66. :text="editMultilingualTags.name"
  67. :translations="editMultilingualTags.mult_language_list"
  68. @SubmitTranslation="handleMultilingualTranslation"
  69. />
  70. <el-upload
  71. class=""
  72. action="no"
  73. :auto-upload="true"
  74. :before-upload="beforeUpload"
  75. :http-request="customUpload"
  76. :show-file-list="false"
  77. accept=".xls"
  78. >
  79. <el-button icon="el-icon-upload2">上传标签</el-button>
  80. </el-upload>
  81. <el-input v-model="labelName" placeholder="输入关键字" class="search-box">
  82. <el-button slot="append" icon="el-icon-search" @click="getLabelList()" />
  83. </el-input>
  84. </div>
  85. </div>
  86. <div class="tag-manager-common tag-edit">
  87. <el-tag
  88. v-for="(tag, i) in commonTags"
  89. :key="i"
  90. effect="plain"
  91. size="medium"
  92. :closable="closable"
  93. :style="{ cursor: 'pointer', 'background-color': editLanguage ? '#F2F3F3' : '' }"
  94. @close="handleCloseCommonTag(tag.id)"
  95. @click="viewToDynamicTags(i)"
  96. >
  97. {{ tag.name }}
  98. </el-tag>
  99. </div>
  100. <p v-if="loading">加载中...</p>
  101. <div class="block">
  102. <el-pagination
  103. background
  104. :current-page="cur_page"
  105. :page-sizes="[20, 40, 60, 80]"
  106. :page-size="page_capacity"
  107. layout="total, prev, pager, next, sizes, jumper"
  108. :total="total_count"
  109. @prev-click="changePage"
  110. @next-click="changePage"
  111. @current-change="changePage"
  112. @size-change="changePageSize"
  113. />
  114. </div>
  115. </div>
  116. </template>
  117. </ModuleBase>
  118. </template>
  119. <script>
  120. import { fileUpload } from '@/api/app';
  121. import { getLabelData, labelColorList } from '@/views/book/courseware/data/label';
  122. import { AddLabel, DeleteLabel, PageQueryLabelList, ImportLabel, UpdateLabel } from '@/api/project.js';
  123. import ModuleMixin from '../../common/ModuleMixin';
  124. export default {
  125. name: 'LabelPage',
  126. mixins: [ModuleMixin],
  127. data() {
  128. return {
  129. labelColorList,
  130. data: getLabelData(),
  131. inputVisible: false,
  132. inputValue: '',
  133. closable: false,
  134. commonTags: [], // 常用标签
  135. page_capacity: 20,
  136. cur_page: 1,
  137. total_count: 0,
  138. loading: false,
  139. labelName: '',
  140. editLanguage: false, // 编辑多语言
  141. editMultilingualTags: {},
  142. editLable: false,
  143. };
  144. },
  145. created() {
  146. this.getLabelList();
  147. },
  148. methods: {
  149. handleEdit(index, event) {
  150. this.data.dynamicTags
  151. .filter((x) => x.isEditing)
  152. .forEach((tag, i) => {
  153. if (tag.isEditing && i !== index) {
  154. this.$set(this.data.dynamicTags[i], 'isEditing', false);
  155. this.$set(this.data.dynamicTags[i], 'editName', null);
  156. }
  157. });
  158. this.$set(this.data.dynamicTags[index], 'isEditing', true);
  159. this.$set(this.data.dynamicTags[index], 'editName', this.data.dynamicTags[index].name);
  160. this.$nextTick(() => {
  161. const input = this.$refs[`inputRefs-${index}`];
  162. if (input && input.length > 0) {
  163. input[0].focus();
  164. }
  165. });
  166. this.editLable = true;
  167. },
  168. // 保存编辑
  169. handleSave(index) {
  170. if (!this.editLable) return;
  171. const en = this.data.dynamicTags[index];
  172. const newName = en.editName.trim();
  173. if (newName) {
  174. en.name = newName;
  175. UpdateLabel(en)
  176. .then(() => {
  177. this.$set(this.data.dynamicTags[index], 'isEditing', false);
  178. this.$set(this.data.dynamicTags[index], 'name', newName);
  179. this.$message.success('设置成功!');
  180. this.cur_page = 1;
  181. this.getLabelList();
  182. this.editLable = false;
  183. })
  184. .catch((e) => {
  185. console.error(e);
  186. });
  187. }
  188. },
  189. // 取消编辑
  190. handleCancel(index) {
  191. this.editLable = false;
  192. this.$set(this.data.dynamicTags[index], 'isEditing', false);
  193. },
  194. // 随机显示颜色
  195. getRandomColor() {
  196. let randomIndex = Math.floor(Math.random() * (this.labelColorList.length - 1)) + 1;
  197. return this.labelColorList[randomIndex].value;
  198. },
  199. // 删除动态标签
  200. handleClose(i) {
  201. this.data.dynamicTags.splice(i, 1);
  202. },
  203. // 显示编辑标签图标
  204. editCommonTags() {
  205. this.closable = !this.closable;
  206. this.editLanguage = false;
  207. },
  208. editCommonTagsLanguage() {
  209. this.editLanguage = !this.editLanguage;
  210. this.closable = false;
  211. },
  212. // 删除常用标签
  213. handleCloseCommonTag(id) {
  214. this.$confirm('确定要删除该标签吗?', '提示', {
  215. confirmButtonText: '确定',
  216. cancelButtonText: '取消',
  217. type: 'warning',
  218. })
  219. .then(() => {
  220. DeleteLabel({ id }).then(() => {
  221. this.data.dynamicTags = this.data.dynamicTags.filter((p) => p.id !== id);
  222. this.getLabelList();
  223. this.$message.success('删除成功!');
  224. });
  225. })
  226. .catch((e) => {
  227. console.error(e);
  228. });
  229. },
  230. // 显示新增标签输入框
  231. showAddLabelTextInput() {
  232. this.inputVisible = true;
  233. this.$nextTick(() => {
  234. this.$refs.saveTagInput.$refs.input.focus();
  235. });
  236. },
  237. // 新增标签
  238. handleInputConfirm() {
  239. let inputValue = this.inputValue.trim();
  240. let label_color = this.data.property.label_color;
  241. if (label_color === 'random') label_color = this.getRandomColor();
  242. let dynamicTags = this.data.dynamicTags;
  243. if (inputValue) {
  244. // 检查是否已存在相同标签
  245. const existingTag = dynamicTags.find((tag) => tag.name === inputValue);
  246. if (existingTag) {
  247. this.$message.warning('常用标签已包含该标签,无需重复添加');
  248. } else {
  249. // dynamicTags.push({
  250. // name: inputValue,
  251. // color: label_color,
  252. // });
  253. this.addLabel(inputValue, label_color);
  254. }
  255. }
  256. this.inputVisible = false;
  257. this.inputValue = '';
  258. },
  259. // 点击常用标签显示到动态标签里
  260. viewToDynamicTags(index) {
  261. let commonTagId = this.commonTags[index].id;
  262. let commonTagName = this.commonTags[index].name;
  263. if (this.closable) return;
  264. if (this.editLanguage) {
  265. this.openDialogLanguage(commonTagId);
  266. return;
  267. }
  268. if (this.data.dynamicTags.length >= 16) {
  269. this.$message.warning('最多添加16个标签!');
  270. return false;
  271. }
  272. // 检查是否已存在相同标签(不区分大小写)
  273. const existingTag = this.data.dynamicTags.find((tag) => tag.name === commonTagName);
  274. if (existingTag) {
  275. this.$message.warning('组件标签已包含该标签,无需重复加入');
  276. } else {
  277. let label_color = this.data.property.label_color;
  278. if (label_color === 'random') label_color = this.getRandomColor();
  279. this.data.dynamicTags.push({
  280. name: commonTagName,
  281. color: label_color,
  282. id: commonTagId,
  283. mult_language_list: this.commonTags[index].mult_language_list || [],
  284. });
  285. }
  286. },
  287. // 得到标签列表
  288. getLabelList() {
  289. let param_data = {
  290. page_capacity: this.page_capacity,
  291. cur_page: this.cur_page,
  292. name: this.labelName,
  293. };
  294. PageQueryLabelList(param_data)
  295. .then(({ total_count, label_list }) => {
  296. this.total_count = total_count;
  297. this.commonTags = label_list;
  298. })
  299. .catch(() => {
  300. // 如果请求失败,回退页码
  301. this.cur_page -= 1;
  302. })
  303. .finally(() => {
  304. this.loading = false;
  305. });
  306. },
  307. changePage(number) {
  308. this.cur_page = number;
  309. this.getLabelList();
  310. },
  311. changePageSize(size) {
  312. this.page_capacity = size;
  313. this.getLabelList();
  314. },
  315. // 存储标签到系统
  316. addLabel(name, color) {
  317. AddLabel({ name })
  318. .then((res) => {
  319. this.cur_page = 1;
  320. this.getLabelList();
  321. if (color) {
  322. this.data.dynamicTags.push({
  323. name,
  324. color,
  325. id: res.id,
  326. mult_language_list: [],
  327. });
  328. }
  329. })
  330. .catch((e) => {
  331. console.error(e);
  332. });
  333. },
  334. // 文件类型校验
  335. beforeUpload(file) {
  336. if (file.length > 0) {
  337. this.$message.warning('只能上传一个表格文件');
  338. return false;
  339. }
  340. const fileName = file.name;
  341. const suffix = fileName.slice(fileName.lastIndexOf('.') + 1, fileName.length).toLowerCase();
  342. // , 'xlsx', 'csv'
  343. if (!['xls'].includes(suffix)) {
  344. this.$message.warning('文件格式不正确');
  345. return false;
  346. }
  347. },
  348. // 上传标签
  349. customUpload(file) {
  350. fileUpload('Mid', file, { isGlobalprogress: true }).then(({ file_info_list }) => {
  351. if (file_info_list.length > 0) {
  352. let file_id = file_info_list[0].file_id;
  353. ImportLabel({ file_id })
  354. .then(() => {
  355. this.$message.success('导入成功!');
  356. this.cur_page = 1;
  357. this.getLabelList();
  358. })
  359. .catch((e) => {
  360. this.$message.error(e);
  361. console.error(e);
  362. });
  363. }
  364. });
  365. },
  366. // 多语言弹窗
  367. openDialogLanguage(commonTagId) {
  368. this.multilingualVisible = true;
  369. let en = { mult_language_list: [] };
  370. if (this.commonTags.some((p) => p.id === commonTagId)) {
  371. en = this.commonTags.find((p) => p.id === commonTagId);
  372. en.mult_language_list.map((p) => (p.translation = p.name));
  373. }
  374. this.editMultilingualTags = en;
  375. },
  376. // 提交多语言译文
  377. handleMultilingualTranslation(translations) {
  378. let data = this.editMultilingualTags;
  379. if (!translations.some((p) => p.type === 'ZH')) {
  380. translations.push({ translation: data.name, type: 'ZH' });
  381. }
  382. data['mult_language_list'] = translations.map((p) => {
  383. return { name: p.translation, type: p.type };
  384. });
  385. UpdateLabel(data)
  386. .then(() => {
  387. this.$message.success('设置成功!');
  388. this.cur_page = 1;
  389. let _index = this.data.dynamicTags.findIndex((p) => p.id === data.id);
  390. if (_index > -1) {
  391. this.$set(this.data.dynamicTags, _index, data);
  392. }
  393. this.getLabelList();
  394. })
  395. .catch((e) => {
  396. console.error(e);
  397. });
  398. },
  399. // 样式调整
  400. getTagStyle(tag) {
  401. const style = { color: tag.color };
  402. if (this.data.unified_attrib) {
  403. if (this.data.unified_attrib.font_size) {
  404. style['font-size'] = this.data.unified_attrib.font_size;
  405. }
  406. if (this.data.unified_attrib.font) {
  407. style['font-family'] = this.data.unified_attrib.font;
  408. }
  409. // if (this.data.unified_attrib.text_color) {
  410. // style['color'] = this.data.unified_attrib.text_color;
  411. // }
  412. // if (this.data.unified_attrib.line_height) {
  413. // style['line-height'] = this.data.unified_attrib.line_height;
  414. // }
  415. if (this.data.unified_attrib.font_weight) {
  416. style['font-weight'] = this.data.unified_attrib.font_weight;
  417. }
  418. }
  419. return style;
  420. },
  421. setRichFormat(type, val) {
  422. if (!this.data.unified_attrib) {
  423. this.$set(this.data, 'unified_attrib', {});
  424. }
  425. switch (type) {
  426. case 'fontSize':
  427. this.$set(this.data.unified_attrib, 'font_size', val);
  428. break;
  429. case 'fontFamily':
  430. this.$set(this.data.unified_attrib, 'font', val);
  431. break;
  432. // case 'color':
  433. // this.$set(this.data.unified_attrib, 'text_color', val);
  434. // break;
  435. // case 'lineHeight':
  436. // this.$set(this.data.unified_attrib, 'line_height', parseFloat(val));
  437. // break;
  438. case 'bold':
  439. this.$set(this.data.unified_attrib, 'font_weight', val ? 'bold' : 'normal');
  440. break;
  441. default:
  442. break;
  443. }
  444. },
  445. },
  446. };
  447. </script>
  448. <style lang="scss" scoped>
  449. .tag-edit {
  450. display: flex;
  451. flex-wrap: wrap;
  452. gap: 8px;
  453. :deep .el-tag {
  454. height: 32px;
  455. padding: 0 10px;
  456. line-height: 31px;
  457. color: var(--color1);
  458. background-color: #fff;
  459. border-color: var(--color1);
  460. border-radius: 2px;
  461. .el-tag__close:hover {
  462. // color: #fff;
  463. background-color: #ddd;
  464. }
  465. .el-tag__close {
  466. color: var(--color1);
  467. }
  468. }
  469. .button-new-tag {
  470. height: 32px;
  471. background-color: #f2f3f3;
  472. border: 1px dashed #c9cdd4;
  473. }
  474. .input-new-tag {
  475. width: 90px;
  476. }
  477. }
  478. .tag-manager {
  479. display: flex;
  480. flex-direction: column;
  481. row-gap: 10px;
  482. .tag-manager-box {
  483. display: flex;
  484. column-gap: 10px;
  485. align-items: center;
  486. justify-content: space-between;
  487. font-size: 14px;
  488. font-weight: 500;
  489. color: #000;
  490. cursor: pointer;
  491. .left-area {
  492. display: flex;
  493. column-gap: 10px;
  494. align-items: center;
  495. .tag-manager-title {
  496. color: rgba(0, 0, 0, 65%);
  497. cursor: text;
  498. }
  499. .tag-manager-text {
  500. display: flex;
  501. column-gap: 2px;
  502. align-items: center;
  503. }
  504. }
  505. .right-area {
  506. display: flex;
  507. column-gap: 10px;
  508. .search-box {
  509. width: 200px;
  510. :deep input {
  511. background-color: #fff;
  512. border-color: #dcdcdc;
  513. }
  514. }
  515. }
  516. }
  517. }
  518. </style>