ToolBar.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <template>
  2. <div class="menu flex-column-center">
  3. <el-button
  4. v-for="button in buttons"
  5. size="large"
  6. :key="button.name"
  7. @click="button.action"
  8. >
  9. {{ button.name }}
  10. </el-button>
  11. <el-button size="large" @click="() => (hotkeysDialogVisible = true)"
  12. >快捷键</el-button
  13. >
  14. </div>
  15. <el-dialog
  16. v-model="hotkeysDialogVisible"
  17. :show-close="false"
  18. :before-close="stopRecordKeyDown"
  19. >
  20. <template #header="{ titleClass, titleId }">
  21. <div class="hotkeys-header flex-space-between">
  22. <div :id="titleId" :class="titleClass">
  23. 快捷键设置
  24. <span v-if="recordKeyDowning">
  25. <el-text> / 录入中 </el-text>
  26. </span>
  27. </div>
  28. <el-button
  29. :disabled="recordKeyDowning"
  30. @click="saveHotKeys"
  31. :icon="CircleCheckFilled"
  32. >保存</el-button
  33. >
  34. </div>
  35. </template>
  36. <div class="hotkeys-settings flex-column-center">
  37. <div
  38. v-for="(button, index) in buttons"
  39. :key="button.name"
  40. class="hotkeys-item flex-space-between"
  41. >
  42. <span class="title"
  43. ><el-text>{{ button.name }}</el-text></span
  44. >
  45. <div class="hotkeys-item__content">
  46. <div v-for="(key, index) in button.hotKeys" :key="key">
  47. <kbd>{{ key }}</kbd>
  48. <span v-if="index + 1 < button.hotKeys.length">
  49. <el-text>+</el-text>
  50. </span>
  51. </div>
  52. <span v-if="button.hotKeys.length == 0">未设置</span>
  53. </div>
  54. <el-button
  55. :disabled="recordKeyDowning"
  56. text
  57. :icon="Edit"
  58. @click="recordKeyDown(index)"
  59. >编辑</el-button
  60. >
  61. </div>
  62. </div>
  63. </el-dialog>
  64. </template>
  65. <script setup>
  66. import API from "@api";
  67. import { CircleCheckFilled, Edit } from "@element-plus/icons-vue";
  68. import hotkeys from "hotkeys-js";
  69. import { isInvaildSource } from "../utils/souce";
  70. const store = useSourceStore();
  71. const pull = () => {
  72. const loadingMsg = ElMessage({
  73. message: "加载中……",
  74. showClose: true,
  75. duration: 0,
  76. });
  77. API.getSources()
  78. .then(({ data }) => {
  79. if (data.isSuccess) {
  80. store.changeTabName("editList");
  81. store.saveSources(data.data);
  82. ElMessage({
  83. message: `成功拉取${data.data.length}条源`,
  84. type: "success",
  85. });
  86. } else {
  87. ElMessage({
  88. message: data.errorMsg ?? "后端错误",
  89. type: "error",
  90. });
  91. }
  92. })
  93. .finally(() => loadingMsg.close());
  94. };
  95. const push = () => {
  96. let sources = store.sources;
  97. store.changeTabName("editList");
  98. if (sources.length === 0) {
  99. return ElMessage({
  100. message: "空空如也",
  101. type: "info",
  102. });
  103. }
  104. ElMessage({
  105. message: "正在推送中",
  106. type: "info",
  107. });
  108. API.saveSources(sources).then(({ data }) => {
  109. if (data.isSuccess) {
  110. let okData = data.data;
  111. if (Array.isArray(okData)) {
  112. let failMsg = ``;
  113. if (sources.length > okData.length) {
  114. failMsg = "\n推送失败的源将用红色字体标注!";
  115. store.setPushReturnSources(okData);
  116. }
  117. ElMessage({
  118. message: `批量推送源到「阅读3.0APP」\n共计: ${
  119. sources.length
  120. } 条\n成功: ${okData.length} 条\n失败: ${
  121. sources.length - okData.length
  122. } 条${failMsg}`,
  123. type: "success",
  124. });
  125. }
  126. } else {
  127. ElMessage({
  128. message: `批量推送源失败!\nErrorMsg: ${data.errorMsg}`,
  129. type: "error",
  130. });
  131. }
  132. });
  133. };
  134. const conver2Tab = () => {
  135. store.changeTabName("editTab");
  136. store.changeEditTabSource(store.currentSource);
  137. };
  138. const conver2Source = () => {
  139. store.changeCurrentSource(store.editTabSource);
  140. };
  141. const undo = () => {
  142. store.editHistoryUndo();
  143. };
  144. const clearEdit = () => {
  145. store.clearEdit();
  146. ElMessage({
  147. message: "已清除",
  148. type: "success",
  149. });
  150. };
  151. const redo = () => {
  152. store.clearEdit();
  153. store.clearAllHistory();
  154. ElMessage({
  155. message: "已清除所有历史记录",
  156. type: "success",
  157. });
  158. };
  159. const saveSource = () => {
  160. let isBookSource = /bookSource/.test(location.href),
  161. /** @type {import("@/source.js").Source} */
  162. source = store.currentSource;
  163. if (isInvaildSource(source)) {
  164. API.saveSource(source).then(({ data }) => {
  165. if (data.isSuccess) {
  166. ElMessage({
  167. message: `源《${
  168. isBookSource ? source.bookSourceName : source.sourceName
  169. }》已成功保存到「阅读3.0APP」`,
  170. type: "success",
  171. });
  172. //save to store
  173. store.saveCurrentSource();
  174. } else {
  175. ElMessage({
  176. message: `源《${
  177. isBookSource ? source.bookSourceName : source.sourceName
  178. }》保存失败!\nErrorMsg: ${data.errorMsg}`,
  179. type: "error",
  180. });
  181. }
  182. });
  183. } else {
  184. ElMessage({
  185. message: `请检查<必填>项是否全部填写`,
  186. type: "error",
  187. });
  188. }
  189. };
  190. const debug = () => {
  191. store.startDebug();
  192. };
  193. const buttons = ref(
  194. Array.of(
  195. { name: "⇈推送源", hotKeys: [], action: push },
  196. { name: "⇊拉取源", hotKeys: [], action: pull },
  197. { name: "⋙生成源", hotKeys: [], action: conver2Tab },
  198. { name: "⋘编辑源", hotKeys: [], action: conver2Source },
  199. { name: "✗清空表单", hotKeys: [], action: clearEdit },
  200. { name: "↶撤销操作", hotKeys: [], action: undo },
  201. { name: "↷重做操作", hotKeys: [], action: redo },
  202. { name: "⇏调试源", hotKeys: [], action: debug },
  203. { name: "✓保存源", hotKeys: [], action: saveSource },
  204. ),
  205. );
  206. const hotkeysDialogVisible = ref(true);
  207. const recordKeyDowning = ref(false);
  208. const recordKeyDownIndex = ref(-1);
  209. const stopRecordKeyDown = () => {
  210. if (!recordKeyDowning.value) {
  211. hotkeysDialogVisible.value = false;
  212. }
  213. recordKeyDowning.value = false;
  214. };
  215. watch(
  216. hotkeysDialogVisible,
  217. (visibale) => {
  218. if (!visibale) {
  219. hotkeys.unbind("*");
  220. readHotkeysConfig();
  221. bindHotKeys();
  222. return;
  223. }
  224. readHotkeysConfig();
  225. hotkeys.unbind();
  226. /**监听按键 */
  227. hotkeys("*", (event) => {
  228. event.preventDefault();
  229. let pressedKeys = hotkeys.getPressedKeyString();
  230. if (pressedKeys.length == 1 && pressedKeys[0] == "esc") {
  231. //单独按下esc 不录入
  232. return;
  233. }
  234. if (recordKeyDowning.value && recordKeyDownIndex.value > -1)
  235. buttons.value[recordKeyDownIndex.value].hotKeys = pressedKeys;
  236. });
  237. },
  238. { immediate: true },
  239. );
  240. const recordKeyDown = (index) => {
  241. recordKeyDowning.value = true;
  242. ElMessage({
  243. message: "按ESC键或者点击空白处结束录入",
  244. type: "info",
  245. });
  246. buttons.value[index].hotKeys = [];
  247. recordKeyDownIndex.value = index;
  248. };
  249. const saveHotKeys = () => {
  250. const hotKeysConfig = [];
  251. buttons.value.forEach(({ hotKeys }) => {
  252. hotKeysConfig.push(hotKeys);
  253. });
  254. saveHotkeysConfig(hotKeysConfig);
  255. hotkeysDialogVisible.value = false;
  256. };
  257. const bindHotKeys = () => {
  258. // hotkeys默认过滤INPUT SELECT TEXTAREA
  259. hotkeys.filter = () => true;
  260. buttons.value.forEach(({ hotKeys, action }) => {
  261. if (hotKeys.length == 0) return;
  262. hotkeys(hotKeys.join("+"), (event) => {
  263. event.preventDefault();
  264. action.call(null);
  265. });
  266. });
  267. };
  268. const saveHotkeysConfig = (config) => {
  269. localStorage.setItem("legado_web_hotkeys", JSON.stringify(config));
  270. };
  271. /**
  272. * 读取快捷键配置
  273. * @return 是否成功读取配置
  274. */
  275. function readHotkeysConfig() {
  276. try {
  277. const config = JSON.parse(localStorage.getItem("legado_web_hotkeys"));
  278. if (!Array.isArray(config) || config.length == 0) return false;
  279. buttons.value.forEach((button, index) => (button.hotKeys = config[index]));
  280. return true;
  281. } catch {
  282. ElMessage({ message: "快捷键配置错误", type: "error" });
  283. localStorage.removeItem("legado_web_hotkeys");
  284. }
  285. return false;
  286. }
  287. onMounted(() => {
  288. /**读取热键配置 */
  289. if (readHotkeysConfig()) {
  290. hotkeysDialogVisible.value = false;
  291. }
  292. });
  293. </script>
  294. <style lang="scss" scoped>
  295. .flex-space-between {
  296. display: flex;
  297. justify-content: space-between;
  298. align-items: baseline;
  299. }
  300. .flex-column-center {
  301. display: flex;
  302. flex-direction: column;
  303. justify-content: center;
  304. }
  305. .menu > .el-button {
  306. margin: 4px;
  307. padding: 1em;
  308. width: 6em;
  309. }
  310. .hotkeys-item {
  311. .title {
  312. width: 5em;
  313. display: flex;
  314. justify-content: flex-end;
  315. margin-right: 1em;
  316. }
  317. &__content {
  318. display: flex;
  319. flex-wrap: wrap;
  320. flex: 1;
  321. div {
  322. margin-bottom: 1em;
  323. }
  324. span {
  325. margin: 0.5em;
  326. }
  327. }
  328. }
  329. </style>