Label.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  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="{ color: tag.color }"
  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. };
  401. </script>
  402. <style lang="scss" scoped>
  403. .tag-edit {
  404. display: flex;
  405. flex-wrap: wrap;
  406. gap: 8px;
  407. :deep .el-tag {
  408. height: 32px;
  409. padding: 0 10px;
  410. line-height: 30px;
  411. color: var(--color1);
  412. background-color: #fff;
  413. border-color: var(--color1);
  414. border-radius: 2px;
  415. .el-tag__close:hover {
  416. // color: #fff;
  417. background-color: #ddd;
  418. }
  419. .el-tag__close {
  420. color: var(--color1);
  421. }
  422. }
  423. .button-new-tag {
  424. height: 32px;
  425. background-color: #f2f3f3;
  426. border: 1px dashed #c9cdd4;
  427. }
  428. .input-new-tag {
  429. width: 90px;
  430. }
  431. }
  432. .tag-manager {
  433. display: flex;
  434. flex-direction: column;
  435. row-gap: 10px;
  436. .tag-manager-box {
  437. display: flex;
  438. column-gap: 10px;
  439. align-items: center;
  440. justify-content: space-between;
  441. font-size: 14px;
  442. font-weight: 500;
  443. color: #000;
  444. cursor: pointer;
  445. .left-area {
  446. display: flex;
  447. column-gap: 10px;
  448. align-items: center;
  449. .tag-manager-title {
  450. color: rgba(0, 0, 0, 65%);
  451. cursor: text;
  452. }
  453. .tag-manager-text {
  454. display: flex;
  455. column-gap: 2px;
  456. align-items: center;
  457. }
  458. }
  459. .right-area {
  460. display: flex;
  461. column-gap: 10px;
  462. .search-box {
  463. width: 200px;
  464. :deep input {
  465. background-color: #fff;
  466. border-color: #dcdcdc;
  467. }
  468. }
  469. }
  470. }
  471. }
  472. </style>