HHoris优化
3a5f713f创建于 2025年12月4日历史提交
<template>
  <div class="menu flex-column-center">
    <el-button
      v-for="button in buttons"
      size="large"
      :key="button.name"
      @click="button.action"
    >
      {{ button.name }}
    </el-button>
    <el-button size="large" @click="() => (hotkeysDialogVisible = true)"
      >快捷键</el-button
    >
  </div>
  <el-dialog
    v-model="hotkeysDialogVisible"
    :show-close="false"
    :before-close="stopRecordKeyDown"
  >
    <template #header="{ titleClass, titleId }">
      <div class="hotkeys-header flex-space-between">
        <div :id="titleId" :class="titleClass">
          快捷键设置
          <span v-if="recordKeyDowning">
            <el-text> / 录入中 </el-text>
          </span>
        </div>
        <el-button
          :disabled="recordKeyDowning"
          @click="saveHotKeys"
          :icon="CircleCheckFilled"
          >保存</el-button
        >
      </div>
    </template>

    <div class="hotkeys-settings flex-column-center">
      <div
        v-for="(button, buttonIndex) in buttons"
        :key="button.name"
        class="hotkeys-item flex-space-between"
      >
        <span class="title"
          ><el-text>{{ button.name }}</el-text></span
        >
        <div class="hotkeys-item__content">
          <div v-for="(key, hotKeysIndex) in button.hotKeys" :key="key">
            <kbd>{{ key }}</kbd>
            <span v-if="hotKeysIndex + 1 < button.hotKeys.length">
              <el-text>+</el-text>
            </span>
          </div>
          <span v-if="button.hotKeys.length == 0">未设置</span>
        </div>
        <el-button
          :disabled="recordKeyDowning"
          text
          :icon="Edit"
          @click="recordKeyDown(buttonIndex)"
          >编辑</el-button
        >
      </div>
    </div>
  </el-dialog>
</template>

<script setup lang="ts">
import API from '@api'
import { CircleCheckFilled, Edit } from '@element-plus/icons-vue'
import hotkeys from 'hotkeys-js'
import { getSourceName, isInvaildSource, normalizeSource } from '../utils/souce'

const store = useSourceStore()
const pull = () => {
  const loadingMsg = ElMessage({
    message: '加载中……',
    showClose: true,
    duration: 0,
  })
  API.getSources()
    .then(({ data }) => {
      if (data.isSuccess) {
        store.changeTabName('editList')
        store.saveSources(data.data)
        ElMessage({
          message: `成功拉取${data.data.length}条源`,
          type: 'success',
        })
      } else {
        ElMessage({
          message: data.errorMsg ?? '后端错误',
          type: 'error',
        })
      }
    })
    .finally(() => loadingMsg.close())
}

const push = () => {
  const sources = store.sources
  store.changeTabName('editList')
  if (sources.length === 0) {
    return ElMessage({
      message: '空空如也',
      type: 'info',
    })
  }
  ElMessage({
    message: '正在推送中',
    type: 'info',
  })
  API.saveSources(sources).then(({ data }) => {
    if (data.isSuccess) {
      const okData = data.data
      if (Array.isArray(okData)) {
        let failMsg = ``
        if (sources.length > okData.length) {
          failMsg = '\n推送失败的源将用红色字体标注!'
          store.setPushReturnSources(okData)
        }
        ElMessage({
          message: `批量推送源到「阅读3.0APP」\n共计: ${
            sources.length
          } 条\n成功: ${okData.length} 条\n失败: ${
            sources.length - okData.length
          } 条${failMsg}`,
          type: 'success',
        })
      }
    } else {
      ElMessage({
        message: `批量推送源失败!\nErrorMsg: ${data.errorMsg}`,
        type: 'error',
      })
    }
  })
}

const conver2Tab = () => {
  store.changeTabName('editTab')
  store.changeEditTabSource(store.currentSource)
}
const conver2Source = () => {
  store.changeCurrentSource(store.editTabSource)
}

const undo = () => {
  store.editHistoryUndo()
}

const clearEdit = () => {
  store.clearEdit()
  ElMessage({
    message: '已清除',
    type: 'success',
  })
}

const redo = () => {
  store.clearEdit()
  store.clearAllHistory()
  ElMessage({
    message: '已清除所有历史记录',
    type: 'success',
  })
}

const saveSource = () => {
  const source = store.currentSource
  if (isInvaildSource(source)) {
    normalizeSource(source)
    API.saveSource(source).then(({ data }) => {
      const sourceName = getSourceName(source)
      if (data.isSuccess) {
        ElMessage({
          message: `源《${sourceName}》已成功保存到「阅读3.0APP」`,
          type: 'success',
        })
        //save to store
        store.saveCurrentSource()
      } else {
        ElMessage({
          message: `源《${sourceName}》保存失败!\nErrorMsg: ${data.errorMsg}`,
          type: 'error',
        })
      }
    })
  } else {
    ElMessage({
      message: `请检查<必填>项是否全部填写`,
      type: 'error',
    })
  }
}

const debug = () => {
  store.startDebug()
}

const buttons = ref<{ name: string; hotKeys: string[]; action: () => void }[]>(
  Array.of(
    { name: '⇈推送源', hotKeys: [], action: push },
    { name: '⇊拉取源', hotKeys: [], action: pull },
    { name: '⋙生成源', hotKeys: [], action: conver2Tab },
    { name: '⋘编辑源', hotKeys: [], action: conver2Source },
    { name: '✗清空表单', hotKeys: [], action: clearEdit },
    { name: '↶撤销操作', hotKeys: [], action: undo },
    { name: '↷重做操作', hotKeys: [], action: redo },
    { name: '⇏调试源', hotKeys: [], action: debug },
    { name: '✓保存源', hotKeys: [], action: saveSource },
  ),
)
const hotkeysDialogVisible = ref(true)

const recordKeyDowning = ref(false)

const recordKeyDownIndex = ref(-1)

const stopRecordKeyDown = () => {
  if (!recordKeyDowning.value) {
    hotkeysDialogVisible.value = false
  }
  recordKeyDowning.value = false
}

watch(
  hotkeysDialogVisible,
  visibale => {
    if (!visibale) {
      hotkeys.unbind('*')
      readHotkeysConfig()
      bindHotKeys()
      return
    }
    readHotkeysConfig()
    hotkeys.unbind()
    /**监听按键 */
    hotkeys('*', event => {
      event.preventDefault()
      const pressedKeys = hotkeys.getPressedKeyString()
      if (pressedKeys.length == 1 && pressedKeys[0] == 'esc') {
        //单独按下esc 不录入
        return
      }
      if (recordKeyDowning.value && recordKeyDownIndex.value > -1)
        buttons.value[recordKeyDownIndex.value].hotKeys = pressedKeys
    })
  },
  { immediate: true },
)

const recordKeyDown = (index: number) => {
  recordKeyDowning.value = true
  ElMessage({
    message: '按ESC键或者点击空白处结束录入',
    type: 'info',
  })
  buttons.value[index].hotKeys = []
  recordKeyDownIndex.value = index
}

const saveHotKeys = () => {
  const hotKeysConfig: string[][] = []
  buttons.value.forEach(({ hotKeys }) => {
    hotKeysConfig.push(hotKeys)
  })
  saveHotkeysConfig(hotKeysConfig)
  hotkeysDialogVisible.value = false
}

const bindHotKeys = () => {
  // hotkeys默认过滤INPUT SELECT TEXTAREA
  hotkeys.filter = () => true
  buttons.value.forEach(({ hotKeys, action }) => {
    if (hotKeys.length == 0) return
    hotkeys(hotKeys.join('+'), event => {
      event.preventDefault()
      action.call(null)
    })
  })
}
const saveHotkeysConfig = (config: string[][]) => {
  localStorage.setItem('legado_web_hotkeys', JSON.stringify(config))
}

/**
 * 读取快捷键配置
 * @return 是否成功读取配置
 */
function readHotkeysConfig() {
  try {
    const localStorageConfig = localStorage.getItem('legado_web_hotkeys')
    if (localStorageConfig === null) return false
    const config = JSON.parse(localStorageConfig)
    if (!Array.isArray(config) || config.length == 0) return false
    buttons.value.forEach((button, index) => (button.hotKeys = config[index]))
    return true
  } catch {
    ElMessage({ message: '快捷键配置错误', type: 'error' })
    localStorage.removeItem('legado_web_hotkeys')
  }
  return false
}

onMounted(() => {
  /**读取热键配置 */
  if (readHotkeysConfig()) {
    hotkeysDialogVisible.value = false
  }
})
</script>

<style lang="scss" scoped>
.flex-space-between {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}
.flex-column-center {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.menu > .el-button {
  margin: 4px;
  padding: 1em;
  width: 6em;
}

.hotkeys-item {
  .title {
    width: 5em;
    display: flex;
    justify-content: flex-end;
    margin-right: 1em;
  }
  .hotkeys-item__content {
    display: flex;
    flex-wrap: wrap;
    flex: 1;
    div {
      margin-bottom: 1em;
    }
    span {
      margin: 0.5em;
    }
  }
}
</style>