ToolBar.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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, buttonIndex) 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, hotKeysIndex) in button.hotKeys" :key="key">
  47. <kbd>{{ key }}</kbd>
  48. <span v-if="hotKeysIndex + 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(buttonIndex)"
  59. >编辑</el-button
  60. >
  61. </div>
  62. </div>
  63. </el-dialog>
  64. </template>
  65. <script setup lang="ts">
  66. import API from '@api'
  67. import { CircleCheckFilled, Edit } from '@element-plus/icons-vue'
  68. import hotkeys from 'hotkeys-js'
  69. import { getSourceName, 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. const 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. const 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. const source = store.currentSource
  161. if (isInvaildSource(source)) {
  162. API.saveSource(source).then(({ data }) => {
  163. const sourceName = getSourceName(source)
  164. if (data.isSuccess) {
  165. ElMessage({
  166. message: `源《${sourceName}》已成功保存到「阅读3.0APP」`,
  167. type: 'success',
  168. })
  169. //save to store
  170. store.saveCurrentSource()
  171. } else {
  172. ElMessage({
  173. message: `源《${sourceName}》保存失败!\nErrorMsg: ${data.errorMsg}`,
  174. type: 'error',
  175. })
  176. }
  177. })
  178. } else {
  179. ElMessage({
  180. message: `请检查<必填>项是否全部填写`,
  181. type: 'error',
  182. })
  183. }
  184. }
  185. const debug = () => {
  186. store.startDebug()
  187. }
  188. const buttons = ref<{ name: string; hotKeys: string[]; action: () => void }[]>(
  189. Array.of(
  190. { name: '⇈推送源', hotKeys: [], action: push },
  191. { name: '⇊拉取源', hotKeys: [], action: pull },
  192. { name: '⋙生成源', hotKeys: [], action: conver2Tab },
  193. { name: '⋘编辑源', hotKeys: [], action: conver2Source },
  194. { name: '✗清空表单', hotKeys: [], action: clearEdit },
  195. { name: '↶撤销操作', hotKeys: [], action: undo },
  196. { name: '↷重做操作', hotKeys: [], action: redo },
  197. { name: '⇏调试源', hotKeys: [], action: debug },
  198. { name: '✓保存源', hotKeys: [], action: saveSource },
  199. ),
  200. )
  201. const hotkeysDialogVisible = ref(true)
  202. const recordKeyDowning = ref(false)
  203. const recordKeyDownIndex = ref(-1)
  204. const stopRecordKeyDown = () => {
  205. if (!recordKeyDowning.value) {
  206. hotkeysDialogVisible.value = false
  207. }
  208. recordKeyDowning.value = false
  209. }
  210. watch(
  211. hotkeysDialogVisible,
  212. visibale => {
  213. if (!visibale) {
  214. hotkeys.unbind('*')
  215. readHotkeysConfig()
  216. bindHotKeys()
  217. return
  218. }
  219. readHotkeysConfig()
  220. hotkeys.unbind()
  221. /**监听按键 */
  222. hotkeys('*', event => {
  223. event.preventDefault()
  224. const pressedKeys = hotkeys.getPressedKeyString()
  225. if (pressedKeys.length == 1 && pressedKeys[0] == 'esc') {
  226. //单独按下esc 不录入
  227. return
  228. }
  229. if (recordKeyDowning.value && recordKeyDownIndex.value > -1)
  230. buttons.value[recordKeyDownIndex.value].hotKeys = pressedKeys
  231. })
  232. },
  233. { immediate: true },
  234. )
  235. const recordKeyDown = (index: number) => {
  236. recordKeyDowning.value = true
  237. ElMessage({
  238. message: '按ESC键或者点击空白处结束录入',
  239. type: 'info',
  240. })
  241. buttons.value[index].hotKeys = []
  242. recordKeyDownIndex.value = index
  243. }
  244. const saveHotKeys = () => {
  245. const hotKeysConfig: string[][] = []
  246. buttons.value.forEach(({ hotKeys }) => {
  247. hotKeysConfig.push(hotKeys)
  248. })
  249. saveHotkeysConfig(hotKeysConfig)
  250. hotkeysDialogVisible.value = false
  251. }
  252. const bindHotKeys = () => {
  253. // hotkeys默认过滤INPUT SELECT TEXTAREA
  254. hotkeys.filter = () => true
  255. buttons.value.forEach(({ hotKeys, action }) => {
  256. if (hotKeys.length == 0) return
  257. hotkeys(hotKeys.join('+'), event => {
  258. event.preventDefault()
  259. action.call(null)
  260. })
  261. })
  262. }
  263. const saveHotkeysConfig = (config: string[][]) => {
  264. localStorage.setItem('legado_web_hotkeys', JSON.stringify(config))
  265. }
  266. /**
  267. * 读取快捷键配置
  268. * @return 是否成功读取配置
  269. */
  270. function readHotkeysConfig() {
  271. try {
  272. const localStorageConfig = localStorage.getItem('legado_web_hotkeys')
  273. if (localStorageConfig === null) return false
  274. const config = JSON.parse(localStorageConfig)
  275. if (!Array.isArray(config) || config.length == 0) return false
  276. buttons.value.forEach((button, index) => (button.hotKeys = config[index]))
  277. return true
  278. } catch {
  279. ElMessage({ message: '快捷键配置错误', type: 'error' })
  280. localStorage.removeItem('legado_web_hotkeys')
  281. }
  282. return false
  283. }
  284. onMounted(() => {
  285. /**读取热键配置 */
  286. if (readHotkeysConfig()) {
  287. hotkeysDialogVisible.value = false
  288. }
  289. })
  290. </script>
  291. <style scoped>
  292. .flex-space-between {
  293. display: flex;
  294. justify-content: space-between;
  295. align-items: baseline;
  296. }
  297. .flex-column-center {
  298. display: flex;
  299. flex-direction: column;
  300. justify-content: center;
  301. }
  302. .menu > .el-button {
  303. margin: 4px;
  304. padding: 1em;
  305. width: 6em;
  306. }
  307. .hotkeys-item {
  308. .title {
  309. width: 5em;
  310. display: flex;
  311. justify-content: flex-end;
  312. margin-right: 1em;
  313. }
  314. .hotkeys-item__content {
  315. display: flex;
  316. flex-wrap: wrap;
  317. flex: 1;
  318. div {
  319. margin-bottom: 1em;
  320. }
  321. span {
  322. margin: 0.5em;
  323. }
  324. }
  325. }
  326. </style>