Преглед изворни кода

feat(OcrView): 实现助记词输入和图片识别功能- 新增助记词输入区域,支持单词提示和选择
- 添加图片预览区域
- 实现键盘事件处理,支持上下箭头和回车键选择单词- 添加删除已选单词的功能- 优化布局和样式,提高用户体验

wui пре 9 месеци
родитељ
комит
72dde2f3f4
3 измењених фајлова са 472 додато и 231 уклоњено
  1. 1 0
      package.json
  2. 351 11
      src/views/OcrView.vue
  3. 120 220
      yarn.lock

+ 1 - 0
package.json

@@ -18,6 +18,7 @@
     "@vueuse/sound": "^2.0.1",
     "ali-oss": "^6.18.1",
     "axios": "^1.3.6",
+    "bip39": "^3.1.0",
     "date-fns": "^2.29.3",
     "echarts": "^5.6.0",
     "element-plus": "^2.3.3",

+ 351 - 11
src/views/OcrView.vue

@@ -119,20 +119,64 @@
         :model="model"
         :on-submit="submit"
         @success="table.refresh()"
-        style="width: 80%"
+        style="width: 90%"
     >
         <ElForm label-width="100px" label-position="right">
-            <div style="display: flex; align-items: stretch; gap: 20px; height: 500px">
-                <div style="flex: 1; display: flex; flex-direction: column; justify-content: center">
-                    <ElFormItem prop="content" label="助记词内容">
-                        <ElInput v-model="model.content" placeholder="请输入内容" type="textarea" :rows="15" />
+            <div class="dialog-content">
+                <!-- 左侧输入和展示区域 -->
+                <div class="left-section">
+                    <ElFormItem prop="content" label="助记词内容: " class="input-section">
+                        <div class="input-wrapper">
+                            <ElInput
+                                v-model="inputWord"
+                                placeholder="输入助记词"
+                                @input="handleWordInput"
+                                @keydown="handleKeyDown"
+                                clearable
+                                class="word-input"
+                            />
+                            <div class="suggestions" v-if="suggestions.length > 0">
+                                <div
+                                    v-for="(word, index) in suggestions"
+                                    :key="word"
+                                    class="suggestion-item"
+                                    :class="{ 'suggestion-item-active': index === selectedIndex }"
+                                    @click="selectWord(word)"
+                                >
+                                    {{ word }}
+                                </div>
+                            </div>
+                        </div>
                     </ElFormItem>
+                    <div class="selected-words-container">
+                        <div
+                            v-for="(word, index) in selectedWords"
+                            :key="index"
+                            class="selected-word-item"
+                        >
+                            <span>{{ word }}</span>
+                            <ElButton
+                                type="danger"
+                                :icon="X"
+                                circle
+                                size="small"
+                                @click="removeWord(index)"
+                            />
+                        </div>
+                    </div>
                 </div>
 
-                <ElDivider direction="vertical" style="height: 100%" />
+                <ElDivider direction="vertical" class="divider" />
 
-                <div style="flex: 1; display: flex; justify-content: center; align-items: center">
-                    <RImage style="max-width: 100%; height: 100%; object-fit: cover" :src="model.img" fit="cover" />
+                <!-- 右侧图片区域 -->
+                <div class="right-section">
+                    <div class="image-container">
+                        <RImage 
+                            :src="model.img" 
+                            fit="contain" 
+                            class="preview-image"
+                        />
+                    </div>
                 </div>
             </div>
         </ElForm>
@@ -159,16 +203,17 @@
     </ElDialog>
 </template>
 <script setup>
-import { inject, ref, onMounted } from 'vue'
+import { inject, ref, onMounted, watch } from 'vue'
 import PagingTable from '@/components/PagingTable.vue'
 import { useTimeFormatter } from '@/utils/formatter'
-import { Check, Edit, Plus, Search, Refresh, Star, StarOff, ClearAll } from '@vicons/tabler'
+import { Check, Edit, Plus, Search, Refresh, Star, StarOff, ClearAll, X } from '@vicons/tabler'
 import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
 import EditDialog from '@/components/EditDialog.vue'
 import { setupEditDialog } from '@/utils/editDialog'
 import { http } from '@/plugins/http'
 import { useClipboard } from '@vueuse/core'
 import { format } from 'date-fns'
+import { wordlists } from 'bip39'
 
 const query = ref({
     width: 50,
@@ -226,6 +271,82 @@ const shortcuts = [
 
 const channelOptions = ref([])
 
+// 添加新的响应式变量
+const inputWord = ref('')
+const suggestions = ref([])
+const selectedWords = ref([])
+const selectedIndex = ref(-1)
+
+// 监听model变化,初始化selectedWords
+watch(() => model.value.content, (newVal) => {
+    if (newVal) {
+        selectedWords.value = newVal.split(' ').filter(Boolean)
+    }
+}, { immediate: true })
+
+// 使用BIP39的英文单词列表
+const bip39Words = wordlists.english
+
+// 处理单词输入
+function handleWordInput() {
+    if (!inputWord.value) {
+        suggestions.value = []
+        selectedIndex.value = -1
+        return
+    }
+    const input = inputWord.value.toLowerCase()
+    suggestions.value = bip39Words
+        .filter(word => word.startsWith(input))
+        .slice(0, 10)
+    selectedIndex.value = -1
+}
+
+// 处理键盘事件
+function handleKeyDown(e) {
+    if (suggestions.value.length === 0) return
+    
+    switch (e.key) {
+        case 'ArrowDown':
+            e.preventDefault()
+            selectedIndex.value = (selectedIndex.value + 1) % suggestions.value.length
+            scrollToSelected()
+            break
+        case 'ArrowUp':
+            e.preventDefault()
+            selectedIndex.value = (selectedIndex.value - 1 + suggestions.value.length) % suggestions.value.length
+            scrollToSelected()
+            break
+        case 'Enter':
+            e.preventDefault()
+            if (selectedIndex.value >= 0) {
+                selectWord(suggestions.value[selectedIndex.value])
+            }
+            break
+    }
+}
+
+// 选择单词
+function selectWord(word) {
+    if (!selectedWords.value.includes(word)) {
+        selectedWords.value.push(word)
+        updateContent()
+    }
+    inputWord.value = ''
+    suggestions.value = []
+    selectedIndex.value = -1
+}
+
+// 移除单词
+function removeWord(index) {
+    selectedWords.value.splice(index, 1)
+    updateContent()
+}
+
+// 更新内容
+function updateContent() {
+    model.value.content = selectedWords.value.join(' ')
+}
+
 onMounted(async () => {
     try {
         const response = await http.post('/ocrChannel/names')
@@ -364,6 +485,32 @@ async function confirmExport() {
     }
     exportDialog()
 }
+
+// 滚动到选中项
+function scrollToSelected() {
+    const suggestionsEl = document.querySelector('.suggestions')
+    const selectedEl = suggestionsEl?.querySelector('.suggestion-item-active')
+    if (selectedEl) {
+        const containerRect = suggestionsEl.getBoundingClientRect()
+        const selectedRect = selectedEl.getBoundingClientRect()
+        const itemHeight = selectedRect.height
+        const visibleHeight = containerRect.height
+        const scrollTop = suggestionsEl.scrollTop
+        const selectedTop = selectedRect.top - containerRect.top + scrollTop
+        
+        // 计算选中项在可视区域中的位置
+        const positionInViewport = selectedTop - scrollTop
+        
+        // 如果选中项在中间位置以下,向下滚动
+        if (positionInViewport > visibleHeight / 2) {
+            suggestionsEl.scrollTop = selectedTop - visibleHeight / 2 + itemHeight / 2
+        }
+        // 如果选中项在中间位置以上,向上滚动
+        else if (positionInViewport < visibleHeight / 2 - itemHeight) {
+            suggestionsEl.scrollTop = selectedTop - visibleHeight / 2 + itemHeight / 2
+        }
+    }
+}
 </script>
 
 <style scoped>
@@ -405,7 +552,7 @@ async function confirmExport() {
     align-items: center;
     justify-content: center;
     flex-shrink: 0;
-    margin-top: 24px; /* 调整这个值来对齐第二行 */
+    margin-top: 25px;
 }
 
 .detail-key {
@@ -465,4 +612,197 @@ async function confirmExport() {
         margin-top: 8px;
     }
 }
+
+.suggestions {
+    margin-top: 8px;
+    max-height: 200px;
+    overflow-y: auto;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+}
+
+.suggestion-item {
+    padding: 8px 12px;
+    cursor: pointer;
+    transition: all 0.3s;
+}
+
+.suggestion-item:hover,
+.suggestion-item-active {
+    background-color: #f5f7fa;
+    color: #409eff;
+    font-weight: 500;
+}
+
+.selected-words-container {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    padding: 10px;
+    min-height: 200px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+}
+
+.selected-word-item {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    padding: 4px 8px;
+    background-color: #f0f9eb;
+    border-radius: 4px;
+    border: 1px solid #e1f3d8;
+}
+
+.selected-word-item span {
+    color: #67c23a;
+    font-weight: 500;
+}
+
+.image-container {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: #f5f7fa;
+    border-radius: 4px;
+    overflow: hidden;
+}
+
+.preview-image {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+}
+
+.dialog-content {
+    display: flex;
+    align-items: stretch;
+    gap: 20px;
+    height: 500px;
+    padding: 10px;
+}
+
+.left-section {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    padding: 10px;
+    background-color: #f8f9fa;
+    border-radius: 8px;
+}
+
+.input-section {
+    margin-bottom: 0;
+}
+
+.input-wrapper {
+    position: relative;
+    width: 100%;
+}
+
+.word-input {
+    width: 100%;
+    margin-bottom: 8px;
+}
+
+.suggestions {
+    position: absolute;
+    top: 100%;
+    left: 0;
+    right: 0;
+    margin-top: 4px;
+    max-height: 300px;
+    overflow-y: auto;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    background-color: white;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    z-index: 1000;
+}
+
+.suggestion-item {
+    padding: 8px 12px;
+    cursor: pointer;
+    transition: all 0.2s;
+}
+
+.suggestion-item:hover,
+.suggestion-item-active {
+    background-color: #f5f7fa;
+    color: #409eff;
+    font-weight: 500;
+}
+
+.suggestion-item-active {
+    background-color: #ecf5ff;
+}
+
+.selected-words-container {
+    flex: 1;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    padding: 10px;
+    min-height: 300px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    background-color: white;
+    overflow-y: auto;
+}
+
+.selected-word-item {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    padding: 6px 12px;
+    background-color: #f0f9eb;
+    border-radius: 4px;
+    border: 1px solid #e1f3d8;
+    transition: all 0.3s;
+}
+
+.selected-word-item:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.selected-word-item span {
+    color: #67c23a;
+    font-weight: 500;
+}
+
+.divider {
+    margin: 0;
+    height: 100%;
+}
+
+.right-section {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    padding: 10px;
+    background-color: #f8f9fa;
+    border-radius: 8px;
+}
+
+.image-container {
+    flex: 1;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: white;
+    border-radius: 4px;
+    border: 1px solid #dcdfe6;
+    overflow: hidden;
+}
+
+.preview-image {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+}
 </style>

Разлика између датотеке није приказан због своје велике величине
+ 120 - 220
yarn.lock


Неке датотеке нису приказане због велике количине промена