重构IconPicker组件

This commit is contained in:
jingrow 2025-11-01 20:54:32 +08:00
parent d7be207545
commit 5217ca41d2
2 changed files with 132 additions and 103 deletions

View File

@ -17,100 +17,109 @@
</template>
</n-button>
<!-- 图标选择弹窗 -->
<n-modal
<!-- 图标选择抽屉 -->
<n-drawer
v-model:show="showPicker"
preset="dialog"
:title="t('Select Icon')"
:positive-text="t('Confirm')"
:negative-text="t('Cancel')"
@positive-click="confirmSelection"
@negative-click="cancelSelection"
size="huge"
:bordered="false"
style="width: 90vw; max-width: 1200px;"
:width="900"
:placement="'right'"
:trap-focus="false"
:close-on-esc="true"
>
<div class="icon-picker-content">
<!-- 搜索栏和图标库选择 -->
<div class="search-section">
<div class="search-controls">
<n-input
v-model:value="searchQuery"
:placeholder="t('Search icon name...')"
clearable
size="large"
@update:value="handleSearch"
style="flex: 1;"
>
<template #prefix>
<Icon :icon="`${currentLibraryConfig.prefix}:search`" />
</template>
</n-input>
<n-select
v-model:value="currentLibrary"
:options="libraryOptions"
:placeholder="t('Icon Library')"
size="large"
style="width: 200px; margin-left: 12px;"
@update:value="handleLibraryChange"
/>
</div>
<div class="icon-count">
{{ t('Total') }} {{ filteredIcons.length }} {{ t('icons') }} ({{ currentLibraryConfig.displayName }})
<span v-if="loading" class="loading-text">{{ t('Loading...') }}</span>
</div>
</div>
<!-- 图标网格 -->
<div class="icon-grid" @scroll="handleScroll">
<div
v-for="icon in displayedIcons"
:key="icon"
class="icon-item"
:class="{ active: tempSelectedIcon === icon }"
@click="selectIcon(icon)"
:title="icon"
>
<!-- 复制按钮 -->
<button
class="copy-button"
@click.stop="copyIconName(icon)"
:title="t('Copy icon name')"
>
{{ t('Copy') }}
</button>
<div class="icon-wrapper">
<Icon
:icon="currentLibraryConfig.name === 'all' ? icon : `${currentLibraryConfig.prefix}:${icon}`"
:width="32"
:height="32"
@error="handleIconError"
/>
<n-drawer-content :title="t('Select Icon')" :closable="true">
<template #default>
<div class="icon-picker-content">
<!-- 搜索栏和图标库选择 -->
<div class="search-section">
<div class="search-controls">
<n-input
v-model:value="searchQuery"
:placeholder="t('Search icon name...')"
clearable
size="large"
@update:value="handleSearch"
style="flex: 1;"
>
<template #prefix>
<Icon :icon="`${currentLibraryConfig.prefix}:search`" />
</template>
</n-input>
<n-select
v-model:value="currentLibrary"
:options="libraryOptions"
:placeholder="t('Icon Library')"
size="large"
style="width: 200px; margin-left: 12px;"
@update:value="handleLibraryChange"
/>
</div>
<div class="icon-count">
{{ t('Total') }} {{ filteredIcons.length }} {{ t('icons') }} ({{ currentLibraryConfig.displayName }})
<span v-if="loading" class="loading-text">{{ t('Loading...') }}</span>
</div>
</div>
<!-- 图标网格 -->
<div class="icon-grid" @scroll="handleScroll">
<div
v-for="icon in displayedIcons"
:key="icon"
class="icon-item"
:class="{ active: tempSelectedIcon === icon }"
@click="selectIcon(icon)"
:title="icon"
>
<!-- 复制按钮 -->
<button
class="copy-button"
@click.stop="copyIconName(icon)"
:title="t('Copy icon name')"
>
{{ t('Copy') }}
</button>
<div class="icon-wrapper">
<Icon
:icon="currentLibraryConfig.name === 'all' ? icon : `${currentLibraryConfig.prefix}:${icon}`"
:width="32"
:height="32"
@error="handleIconError"
/>
</div>
<div class="icon-name">{{ currentLibraryConfig.name === 'all' ? icon.split(':')[1] || icon : icon }}</div>
</div>
<!-- 加载更多按钮 -->
<div v-if="hasMoreIcons && !loading" class="load-more" @click="loadMoreIcons">
<Icon :icon="`${currentLibraryConfig.prefix}:plus`" :width="24" :height="24" />
<span>{{ t('Load More') }}</span>
</div>
<!-- 加载中状态 -->
<div v-if="loading" class="loading-more">
<Icon :icon="`${currentLibraryConfig.prefix}:loader`" :width="24" :height="24" class="spinning" />
<span>{{ t('Loading...') }}</span>
</div>
</div>
<div class="icon-name">{{ currentLibraryConfig.name === 'all' ? icon.split(':')[1] || icon : icon }}</div>
</div>
<!-- 加载更多按钮 -->
<div v-if="hasMoreIcons && !loading" class="load-more" @click="loadMoreIcons">
<Icon :icon="`${currentLibraryConfig.prefix}:plus`" :width="24" :height="24" />
<span>{{ t('Load More') }}</span>
</template>
<!-- 底部操作按钮 -->
<template #footer>
<div class="drawer-footer">
<n-button @click="cancelSelection">{{ t('Cancel') }}</n-button>
<n-button type="primary" @click="confirmSelection" :disabled="!tempSelectedIcon">
{{ t('Confirm') }}
</n-button>
</div>
<!-- 加载中状态 -->
<div v-if="loading" class="loading-more">
<Icon :icon="`${currentLibraryConfig.prefix}:loader`" :width="24" :height="24" class="spinning" />
<span>{{ t('Loading...') }}</span>
</div>
</div>
</div>
</n-modal>
</template>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { NButton, NModal, NInput, NSelect, useMessage } from 'naive-ui'
import { ref, computed, onMounted, watch } from 'vue'
import { NButton, NDrawer, NDrawerContent, NInput, NSelect, useMessage } from 'naive-ui'
import { Icon } from '@iconify/vue'
import { getIconLibraryConfig, getAvailableIconLibraries, DEFAULT_ICON_LIBRARY } from '@/shared/utils/icon-libraries'
import { t } from '@/shared/i18n'
@ -414,10 +423,27 @@ async function copyIconName(iconName: string) {
message.success(`${t('Icon name copied to clipboard')}: ${fullIconName}`)
}
// modelValue
watch(() => props.modelValue, (newValue) => {
if (newValue !== selectedIcon.value) {
selectedIcon.value = newValue || ''
}
}, { immediate: true })
//
onMounted(() => {
loadAllIcons()
})
//
function open() {
openPicker()
}
// 使 defineExpose
defineExpose({
open
})
</script>
<style scoped>
@ -433,11 +459,18 @@ onMounted(() => {
}
.icon-picker-content {
height: 70vh;
height: calc(100vh - 120px);
display: flex;
flex-direction: column;
}
.drawer-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 16px;
}
.search-section {
margin-bottom: 20px;
display: flex;
@ -521,6 +554,7 @@ onMounted(() => {
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, nextTick } from 'vue'
import { computed, ref } from 'vue'
import { NInput } from 'naive-ui'
import { Icon } from '@iconify/vue'
import IconPicker from '@/core/components/IconPicker.vue'
@ -17,17 +17,12 @@ const iconValue = computed({
}
})
// IconPicker
// IconPicker
const iconPickerRef = ref<InstanceType<typeof IconPicker> | null>(null)
//
async function openIconPicker() {
await nextTick()
// IconPicker
const triggerButton = iconPickerRef.value?.$el?.querySelector('.icon-trigger') as HTMLElement
if (triggerButton) {
triggerButton.click()
}
function openIconPicker() {
iconPickerRef.value?.open()
}
</script>
@ -83,14 +78,14 @@ async function openIconPicker() {
</div>
</div>
<!-- 图标选择器隐藏触发按钮通过自定义按钮触发-->
<div v-if="canEdit" style="position: absolute; left: -9999px; opacity: 0; pointer-events: none;">
<IconPicker
ref="iconPickerRef"
:model-value="iconValue"
@update:model-value="iconValue = $event"
/>
</div>
<!-- 图标选择器不渲染触发按钮仅用于弹窗-->
<IconPicker
v-if="canEdit"
ref="iconPickerRef"
:model-value="iconValue"
@update:model-value="iconValue = $event"
style="display: none;"
/>
</div>
</template>