SourceList.vue 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. <template>
  2. <el-input
  3. v-model="searchKey"
  4. class="search"
  5. :prefix-icon="Search"
  6. placeholder="筛选源"
  7. />
  8. <div class="tool">
  9. <el-button @click="importSourceFile" :icon="Folder">打开</el-button>
  10. <el-button
  11. :disabled="sourcesFiltered.length === 0"
  12. @click="outExport"
  13. :icon="Download"
  14. >
  15. 导出</el-button
  16. >
  17. <el-button
  18. type="danger"
  19. :icon="Delete"
  20. @click="deleteSelectSources"
  21. :disabled="sourceSelect.length === 0"
  22. >删除</el-button
  23. >
  24. <el-button
  25. type="danger"
  26. :icon="Delete"
  27. @click="clearAllSources"
  28. :disabled="sources.length === 0"
  29. >清空</el-button
  30. >
  31. </div>
  32. <el-checkbox-group id="source-list" v-model="sourceUrlSelect">
  33. <virtual-list
  34. style="height: 100%; overflow-y: auto; overflow-x: hidden"
  35. :data-key="(source) => source.bookSourceUrl || source.sourceUrl"
  36. :data-sources="sourcesFiltered"
  37. :data-component="SourceItem"
  38. :estimate-size="45"
  39. />
  40. </el-checkbox-group>
  41. </template>
  42. <script setup>
  43. import API from "@api";
  44. import { Folder, Delete, Download, Search } from "@element-plus/icons-vue";
  45. import {
  46. isSourceMatches,
  47. getSourceUniqueKey,
  48. convertSourcesToMap,
  49. } from "@utils/souce";
  50. import VirtualList from "vue3-virtual-scroll-list";
  51. import SourceItem from "./SourceItem.vue";
  52. const store = useSourceStore();
  53. const sourceUrlSelect = ref([]);
  54. const searchKey = ref("");
  55. const sources = computed(() => store.sources);
  56. // 筛选源
  57. /** @type Ref<import('@/source').Source[]> */
  58. const sourcesFiltered = computed(() => {
  59. const key = searchKey.value;
  60. if (key === "") return sources.value;
  61. return (
  62. sources.value
  63. // @ts-ignore
  64. .filter((source) => isSourceMatches(source, key))
  65. );
  66. });
  67. // 计算当前筛选关键词下的选中源
  68. /** @type Ref<import('@/source').Source[]> */
  69. const sourceSelect = computed(() => {
  70. const urls = sourceUrlSelect.value;
  71. if (urls.length == 0) return [];
  72. const sourcesFilteredMap =
  73. searchKey.value == ""
  74. ? store.sourcesMap
  75. : convertSourcesToMap(sourcesFiltered.value);
  76. return urls.reduce((sources, sourceUrl) => {
  77. const source = sourcesFilteredMap.get(sourceUrl);
  78. if (source) sources.push(source);
  79. return sources;
  80. }, []);
  81. });
  82. const deleteSelectSources = () => {
  83. const sourceSelectValue = sourceSelect.value;
  84. API.deleteSource(sourceSelectValue).then(({ data }) => {
  85. if (!data.isSuccess) return ElMessage.error(data.errorMsg);
  86. store.deleteSources(sourceSelectValue);
  87. const sourceUrlSelectRawValue = toRaw(sourceUrlSelect.value);
  88. sourceSelectValue.forEach((source) => {
  89. const index = sourceUrlSelectRawValue.indexOf(getSourceUniqueKey(source));
  90. if (index > -1) sourceUrlSelectRawValue.splice(index, 1);
  91. });
  92. sourceUrlSelect.value = sourceUrlSelectRawValue;
  93. });
  94. };
  95. const clearAllSources = () => {
  96. store.clearAllSource();
  97. sourceUrlSelect.value = [];
  98. };
  99. //导入本地文件
  100. const importSourceFile = () => {
  101. const input = document.createElement("input");
  102. input.type = "file";
  103. input.accept = ".json,.txt";
  104. input.addEventListener("change", (e) => {
  105. // @ts-ignore
  106. const file = e.target.files[0];
  107. const reader = new FileReader();
  108. reader.readAsText(file);
  109. reader.onload = () => {
  110. try {
  111. // @ts-ignore
  112. const jsonData = JSON.parse(reader.result);
  113. store.saveSources(jsonData);
  114. } catch {
  115. ElMessage({
  116. message: "上传的源格式错误",
  117. type: "error",
  118. });
  119. }
  120. };
  121. });
  122. input.click();
  123. };
  124. const isBookSource = /bookSource/.test(window.location.href);
  125. const outExport = () => {
  126. const exportFile = document.createElement("a");
  127. let sources =
  128. sourceUrlSelect.value.length === 0
  129. ? sourcesFiltered.value
  130. : sourceSelect.value,
  131. sourceType = isBookSource ? "BookSource" : "RssSource";
  132. exportFile.download = `${sourceType}_${Date()
  133. .replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, "$2$1$3")
  134. .replace(/:/g, "")}.json`;
  135. let myBlob = new Blob([JSON.stringify(sources, null, 4)], {
  136. type: "application/json",
  137. });
  138. exportFile.href = window.URL.createObjectURL(myBlob);
  139. exportFile.click();
  140. window.URL.revokeObjectURL(exportFile.href); //avoid memory leak
  141. };
  142. </script>
  143. <style lang="scss" scoped>
  144. .tool {
  145. display: flex;
  146. margin: 4px 0;
  147. justify-content: center;
  148. }
  149. #source-list {
  150. margin-top: 6px;
  151. height: calc(100vh - 112px - 7px);
  152. :deep(.el-checkbox) {
  153. margin-bottom: 4px;
  154. width: 100%;
  155. }
  156. }
  157. </style>