Эх сурвалжийг харах

feat(OcrFavoriteView): 改进助记词输入方式

- 新增单词输入和选择功能,支持自动补全和键盘导航- 添加已选单词展示和移除功能
- 优化对话框布局,分为左右两个区域
- 增加BIP39英文单词列表支持
wui 9 сар өмнө
parent
commit
18d0f1e153

+ 275 - 15
src/views/OcrFavoriteView.vue

@@ -124,40 +124,66 @@
         :model="model"
         :model="model"
         :on-submit="submit"
         :on-submit="submit"
         @success="table.refresh()"
         @success="table.refresh()"
-        style="width: 80%"
+        style="width: 90%"
     >
     >
         <ElForm label-width="100px" label-position="right">
         <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>
                     </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>
                 </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" 
-                        :thumbnail="model.thumbnail"
-                        fit="cover" 
-                    />
+                <!-- 右侧图片区域 -->
+                <div class="right-section">
+                    <div class="image-container">
+                        <RImage :src="model.img" :thumbnail="model.thumbnail" fit="contain" class="preview-image" />
+                    </div>
                 </div>
                 </div>
             </div>
             </div>
         </ElForm>
         </ElForm>
     </EditDialog>
     </EditDialog>
 </template>
 </template>
 <script setup>
 <script setup>
-import { inject, ref, onMounted } from 'vue'
+import { inject, ref, onMounted, watch } from 'vue'
 import PagingTable from '@/components/PagingTable.vue'
 import PagingTable from '@/components/PagingTable.vue'
 import { useTimeFormatter } from '@/utils/formatter'
 import { useTimeFormatter } from '@/utils/formatter'
-import { Check, Edit, Plus, Search, Refresh, Star, StarOff, Heart, HeartBroken, DropletFilled } from '@vicons/tabler'
+import { Check, Edit, Plus, Search, Refresh, Star, StarOff, Heart, HeartBroken, DropletFilled, X } from '@vicons/tabler'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import EditDialog from '@/components/EditDialog.vue'
 import EditDialog from '@/components/EditDialog.vue'
 import { setupEditDialog } from '@/utils/editDialog'
 import { setupEditDialog } from '@/utils/editDialog'
 import { http } from '@/plugins/http'
 import { http } from '@/plugins/http'
 import { useClipboard } from '@vueuse/core'
 import { useClipboard } from '@vueuse/core'
+import { wordlists } from 'bip39'
 
 
 const query = ref({})
 const query = ref({})
 const timeFormatter = useTimeFormatter()
 const timeFormatter = useTimeFormatter()
@@ -168,6 +194,110 @@ const { copy } = useClipboard({ legacy: true })
 const isAdmin = inject('isAdminAndOperator')
 const isAdmin = inject('isAdminAndOperator')
 const channelOptions = ref([])
 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(' ')
+}
+
+// 滚动到选中项
+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
+        }
+    }
+}
+
 onMounted(async () => {
 onMounted(async () => {
     try {
     try {
         const response = await http.post('/ocrChannel/names')
         const response = await http.post('/ocrChannel/names')
@@ -371,4 +501,134 @@ async function handleFavoriteClick(row) {
         margin-top: 8px;
         margin-top: 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;
+}
+
+.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;
+}
+
+.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>
 </style>