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

View File

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