feat: 新增图标选择器组件及相关功能;更新路由配置和类型定义;修复依赖地址
This commit is contained in:
@@ -50,7 +50,7 @@
|
||||
"@better-scroll/core": "2.5.1",
|
||||
"@elysiajs/eden": "^1.4.5",
|
||||
"@iconify/vue": "5.0.0",
|
||||
"@riwa/api-types": "http://192.168.1.27:9527/api/riwa-api-types-0.0.67.tgz",
|
||||
"@riwa/api-types": "http://192.168.1.3:9527/api/riwa-api-types-0.0.67.tgz",
|
||||
"@sa/axios": "workspace:*",
|
||||
"@sa/color": "workspace:*",
|
||||
"@sa/hooks": "workspace:*",
|
||||
@@ -74,7 +74,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify-json/cryptocurrency-color": "^1.2.4",
|
||||
"@iconify-json/material-symbols": "^1.2.50",
|
||||
"@iconify/json": "2.2.414",
|
||||
"@iconify/types": "^2.0.0",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.7.4",
|
||||
|
||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@@ -18,8 +18,8 @@ importers:
|
||||
specifier: 5.0.0
|
||||
version: 5.0.0(vue@3.5.25(typescript@5.9.3))
|
||||
'@riwa/api-types':
|
||||
specifier: http://192.168.1.27:9527/api/riwa-api-types-0.0.67.tgz
|
||||
version: http://192.168.1.27:9527/api/riwa-api-types-0.0.67.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))
|
||||
specifier: http://192.168.1.3:9527/api/riwa-api-types-0.0.67.tgz
|
||||
version: http://192.168.1.3:9527/api/riwa-api-types-0.0.67.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))
|
||||
'@sa/axios':
|
||||
specifier: workspace:*
|
||||
version: link:packages/axios
|
||||
@@ -84,9 +84,18 @@ importers:
|
||||
'@elegant-router/vue':
|
||||
specifier: 0.3.8
|
||||
version: 0.3.8
|
||||
'@iconify-json/cryptocurrency-color':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4
|
||||
'@iconify-json/material-symbols':
|
||||
specifier: ^1.2.50
|
||||
version: 1.2.50
|
||||
'@iconify/json':
|
||||
specifier: 2.2.414
|
||||
version: 2.2.414
|
||||
'@iconify/types':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@sa/scripts':
|
||||
specifier: workspace:*
|
||||
version: link:packages/scripts
|
||||
@@ -892,6 +901,12 @@ packages:
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@iconify-json/cryptocurrency-color@1.2.4':
|
||||
resolution: {integrity: sha512-8vjIfTAAMg0zo3/CdVWV7YjViY1L/q4TFfjROmqRPCRPhM6iVecW4TzMFS8hxm48S2Ge69SNM1yC8FmHT+jfHw==}
|
||||
|
||||
'@iconify-json/material-symbols@1.2.50':
|
||||
resolution: {integrity: sha512-71tjHR70h46LHtBFab3fAd2V/wPTO7JMV5lKnRn3IcF303LaFgAlO0BZeTJDcmCv9d0snRZmnoLZAJVD7/eisw==}
|
||||
|
||||
'@iconify/json@2.2.414':
|
||||
resolution: {integrity: sha512-7aYnEansnTQCuwK0HQjcxWWH/mzSDGbnRgtHnF3PNO0QXhylOpIPHuC+l2vBBIxC7CMrQd87U3UpI+1jfB5nag==}
|
||||
|
||||
@@ -1068,8 +1083,8 @@ packages:
|
||||
'@quansync/fs@0.1.6':
|
||||
resolution: {integrity: sha512-zoA8SqQO11qH9H8FCBR7NIbowYARIPmBz3nKjgAaOUDi/xPAAu1uAgebtV7KXHTc6CDZJVRZ1u4wIGvY5CWYaw==}
|
||||
|
||||
'@riwa/api-types@http://192.168.1.27:9527/api/riwa-api-types-0.0.67.tgz':
|
||||
resolution: {tarball: http://192.168.1.27:9527/api/riwa-api-types-0.0.67.tgz}
|
||||
'@riwa/api-types@http://192.168.1.3:9527/api/riwa-api-types-0.0.67.tgz':
|
||||
resolution: {tarball: http://192.168.1.3:9527/api/riwa-api-types-0.0.67.tgz}
|
||||
version: 0.0.67
|
||||
peerDependencies:
|
||||
'@elysiajs/eden': ^1.4.5
|
||||
@@ -4896,6 +4911,14 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@iconify-json/cryptocurrency-color@1.2.4':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/material-symbols@1.2.50':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/json@2.2.414':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
@@ -5057,7 +5080,7 @@ snapshots:
|
||||
dependencies:
|
||||
quansync: 0.3.0
|
||||
|
||||
'@riwa/api-types@http://192.168.1.27:9527/api/riwa-api-types-0.0.67.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
||||
'@riwa/api-types@http://192.168.1.3:9527/api/riwa-api-types-0.0.67.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
||||
dependencies:
|
||||
'@elysiajs/eden': 1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||
|
||||
|
||||
226
src/components/custom/icon-picker.vue
Normal file
226
src/components/custom/icon-picker.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { NButton, NEmpty, NInput, NPopover, NScrollbar, NSpin, NTabPane, NTabs } from 'naive-ui';
|
||||
import { getIconPath, loadIconCollection, searchIcons } from '@/utils/icon-loader';
|
||||
import type { IconCollection } from '@/typings/icon';
|
||||
|
||||
defineOptions({
|
||||
name: 'IconPicker'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 当前选中的图标 */
|
||||
modelValue?: string;
|
||||
/** 图标集前缀 */
|
||||
prefix?: IconCollection;
|
||||
/** 每页显示数量 */
|
||||
pageSize?: number;
|
||||
/** 弹窗宽度 */
|
||||
width?: number;
|
||||
/** 图标尺寸 */
|
||||
iconSize?: string;
|
||||
/** 可选的图标集列表 */
|
||||
collections?: IconCollection[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
prefix: 'material-symbols',
|
||||
pageSize: 60,
|
||||
width: 600,
|
||||
iconSize: '24px',
|
||||
collections: () => ['material-symbols', 'cryptocurrency-color']
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const showPopover = ref(false);
|
||||
const searchValue = ref('');
|
||||
const loading = ref(false);
|
||||
const allIcons = ref<string[]>([]);
|
||||
const currentPage = ref(1);
|
||||
const activeCollection = ref<IconCollection>(props.prefix);
|
||||
|
||||
// 图标集显示名称映射
|
||||
const collectionLabels: Record<IconCollection, string> = {
|
||||
'material-symbols': 'Material Symbols',
|
||||
'cryptocurrency-color': 'Cryptocurrency Color'
|
||||
};
|
||||
|
||||
// 过滤图标
|
||||
const filteredIcons = computed(() => {
|
||||
return searchIcons(allIcons.value, searchValue.value);
|
||||
});
|
||||
|
||||
// 分页计算
|
||||
const pageCount = computed(() => {
|
||||
return Math.ceil(filteredIcons.value.length / props.pageSize);
|
||||
});
|
||||
|
||||
const currentPageIcons = computed(() => {
|
||||
const start = (currentPage.value - 1) * props.pageSize;
|
||||
const end = start + props.pageSize;
|
||||
return filteredIcons.value.slice(start, end);
|
||||
});
|
||||
|
||||
// 监听搜索值变化,重置到第一页
|
||||
watch(searchValue, () => {
|
||||
currentPage.value = 1;
|
||||
});
|
||||
|
||||
// 监听图标集切换
|
||||
watch(activeCollection, () => {
|
||||
searchValue.value = '';
|
||||
currentPage.value = 1;
|
||||
loadIcons();
|
||||
});
|
||||
|
||||
// 加载图标集
|
||||
function loadIcons() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const icons = loadIconCollection(activeCollection.value);
|
||||
allIcons.value = icons;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to load icons:', error);
|
||||
window.$message?.error('图标加载失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 选择图标
|
||||
function selectIcon(icon: string) {
|
||||
const fullIcon = getIconPath(activeCollection.value, icon);
|
||||
emit('update:modelValue', fullIcon);
|
||||
showPopover.value = false;
|
||||
}
|
||||
|
||||
// 清除选择
|
||||
function clearSelection() {
|
||||
emit('update:modelValue', '');
|
||||
}
|
||||
|
||||
// 弹窗打开时加载图标
|
||||
function handlePopoverShow(show: boolean) {
|
||||
if (show && allIcons.value.length === 0) {
|
||||
loadIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// 上一页
|
||||
function prevPage() {
|
||||
if (currentPage.value > 1) {
|
||||
currentPage.value -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 下一页
|
||||
function nextPage() {
|
||||
if (currentPage.value < pageCount.value) {
|
||||
currentPage.value += 1;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 如果需要预加载,可以在这里调用
|
||||
loadIcons();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NPopover
|
||||
v-model:show="showPopover"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
:width="width"
|
||||
@update:show="handlePopoverShow"
|
||||
>
|
||||
<template #trigger>
|
||||
<div
|
||||
class="inline-flex cursor-pointer items-center gap-2 border border-gray-300 rounded px-3 py-2 transition-colors dark:border-gray-700 hover:border-primary"
|
||||
>
|
||||
<template v-if="modelValue">
|
||||
<SvgIcon :icon="modelValue" :style="{ fontSize: iconSize }" />
|
||||
<span class="text-sm">{{ modelValue.split(':')[1] || modelValue }}</span>
|
||||
<span class="ml-2 text-gray-400 transition-colors hover:text-red-500" @click.stop="clearSelection">✕</span>
|
||||
</template>
|
||||
<span v-else class="text-gray-400">选择图标</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-2">
|
||||
<!-- Tabs 切换图标集 -->
|
||||
<div v-if="collections.length > 1" class="px-3 pb-3">
|
||||
<NTabs v-model:value="activeCollection" type="line" size="small" animated>
|
||||
<NTabPane
|
||||
v-for="collection in collections"
|
||||
:key="collection"
|
||||
:name="collection"
|
||||
:tab="collectionLabels[collection]"
|
||||
/>
|
||||
</NTabs>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="px-3 pb-3">
|
||||
<NInput v-model:value="searchValue" placeholder="搜索图标名称..." clearable>
|
||||
<template #prefix>
|
||||
<SvgIcon icon="material-symbols:search" class="text-lg" />
|
||||
</template>
|
||||
</NInput>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="px-3 pb-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
共 {{ filteredIcons.length }} 个图标
|
||||
<template v-if="searchValue">,第 {{ currentPage }}/{{ pageCount }} 页</template>
|
||||
</div>
|
||||
|
||||
<!-- 图标列表 -->
|
||||
<NSpin :show="loading">
|
||||
<NScrollbar class="max-h-360px">
|
||||
<div v-if="currentPageIcons.length" class="grid grid-cols-8 gap-2 px-3">
|
||||
<div
|
||||
v-for="icon in currentPageIcons"
|
||||
:key="icon"
|
||||
class="group flex flex-col cursor-pointer items-center justify-center rounded p-2 transition-all hover:bg-primary/10"
|
||||
:class="{
|
||||
'bg-primary/20 ring-2 ring-primary': modelValue === getIconPath(activeCollection, icon)
|
||||
}"
|
||||
:title="icon"
|
||||
@click="selectIcon(icon)"
|
||||
>
|
||||
<SvgIcon :icon="getIconPath(activeCollection, icon)" :style="{ fontSize: iconSize }" class="mb-1" />
|
||||
<span
|
||||
class="w-full truncate text-center text-xs text-gray-600 dark:text-gray-400 group-hover:text-primary"
|
||||
>
|
||||
{{ icon.length > 8 ? icon.slice(0, 8) + '...' : icon }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<NEmpty v-else description="未找到匹配的图标" class="py-8" />
|
||||
</NScrollbar>
|
||||
</NSpin>
|
||||
|
||||
<!-- 分页控制 -->
|
||||
<div
|
||||
v-if="pageCount > 1"
|
||||
class="flex items-center justify-between border-t border-gray-200 px-3 pt-3 dark:border-gray-700"
|
||||
>
|
||||
<NButton size="small" :disabled="currentPage === 1" @click="prevPage">上一页</NButton>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ currentPage }} / {{ pageCount }}</span>
|
||||
<NButton size="small" :disabled="currentPage === pageCount" @click="nextPage">下一页</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</NPopover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加自定义样式 */
|
||||
</style>
|
||||
@@ -39,6 +39,16 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'asset',
|
||||
path: '/asset',
|
||||
component: 'layout.base$view.asset',
|
||||
meta: {
|
||||
title: 'asset',
|
||||
i18nKey: 'route.asset',
|
||||
order: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'deposit',
|
||||
path: '/deposit',
|
||||
@@ -234,15 +244,5 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'asset',
|
||||
path: '/asset',
|
||||
component: 'layout.base$view.asset',
|
||||
meta: {
|
||||
title: 'asset',
|
||||
i18nKey: 'route.asset',
|
||||
order: 5
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@@ -34,6 +34,7 @@ declare module 'vue' {
|
||||
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
IconPicker: typeof import('./../components/custom/icon-picker.vue')['default']
|
||||
IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
|
||||
IconUilSearch: typeof import('~icons/uil/search')['default']
|
||||
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
|
||||
@@ -124,6 +125,7 @@ declare global {
|
||||
const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
const IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
const IconPicker: typeof import('./../components/custom/icon-picker.vue')['default']
|
||||
const IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
|
||||
const IconUilSearch: typeof import('~icons/uil/search')['default']
|
||||
const LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
|
||||
|
||||
21
src/typings/icon.d.ts
vendored
Normal file
21
src/typings/icon.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 图标相关类型定义
|
||||
*/
|
||||
|
||||
/** 图标集名称 */
|
||||
export type IconCollection = 'material-symbols' | 'cryptocurrency-color';
|
||||
|
||||
/** 图标完整路径 */
|
||||
export type IconPath = `${IconCollection}:${string}`;
|
||||
|
||||
/** 图标选择器配置 */
|
||||
export interface IconPickerOptions {
|
||||
/** 图标集前缀 */
|
||||
prefix?: IconCollection;
|
||||
/** 每页显示数量 */
|
||||
pageSize?: number;
|
||||
/** 弹窗宽度 */
|
||||
width?: number;
|
||||
/** 图标尺寸 */
|
||||
iconSize?: string;
|
||||
}
|
||||
149
src/utils/icon-loader.ts
Normal file
149
src/utils/icon-loader.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { IconifyJSON } from '@iconify/types';
|
||||
import materialSymbolsIcons from '@iconify-json/material-symbols/icons.json';
|
||||
import cryptoCurrencyIcons from '@iconify-json/cryptocurrency-color/icons.json';
|
||||
|
||||
/**
|
||||
* 图标集缓存
|
||||
*/
|
||||
interface IconCache {
|
||||
/** 图标名称列表 */
|
||||
icons: string[];
|
||||
/** 图标别名映射 */
|
||||
aliases: Record<string, string>;
|
||||
/** 加载时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const iconCacheMap = new Map<string, IconCache>();
|
||||
|
||||
/**
|
||||
* 加载 Material Symbols 图标集
|
||||
* @returns 图标名称数组
|
||||
*/
|
||||
export function loadMaterialSymbolsIcons(): string[] {
|
||||
const collectionName = 'material-symbols';
|
||||
|
||||
// 返回缓存的图标
|
||||
if (iconCacheMap.has(collectionName)) {
|
||||
const cache = iconCacheMap.get(collectionName)!;
|
||||
return cache.icons;
|
||||
}
|
||||
|
||||
try {
|
||||
const iconSet = materialSymbolsIcons as unknown as IconifyJSON;
|
||||
|
||||
// 获取所有图标名称
|
||||
const icons = Object.keys(iconSet.icons || {});
|
||||
|
||||
// 处理别名
|
||||
const aliases: Record<string, string> = {};
|
||||
if (iconSet.aliases) {
|
||||
Object.entries(iconSet.aliases).forEach(([alias, config]) => {
|
||||
if (typeof config === 'object' && 'parent' in config) {
|
||||
aliases[alias] = config.parent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
iconCacheMap.set(collectionName, {
|
||||
icons,
|
||||
aliases,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return icons;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to load material symbols icons:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 cryptocurrency-color 图标集
|
||||
* @returns 图标名称数组
|
||||
*/
|
||||
export function loadCryptoCurrencyIcons(): string[] {
|
||||
const collectionName = 'cryptocurrency-color';
|
||||
|
||||
// 返回缓存的图标
|
||||
if (iconCacheMap.has(collectionName)) {
|
||||
const cache = iconCacheMap.get(collectionName)!;
|
||||
return cache.icons;
|
||||
}
|
||||
|
||||
try {
|
||||
const iconSet = cryptoCurrencyIcons as IconifyJSON;
|
||||
|
||||
// 获取所有图标名称
|
||||
const icons = Object.keys(iconSet.icons || {});
|
||||
|
||||
// 缓存结果
|
||||
iconCacheMap.set(collectionName, {
|
||||
icons,
|
||||
aliases: {},
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return icons;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to load material icon theme icons:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用图标集加载器
|
||||
* @param collectionName 图标集名称,如 'material-symbols'
|
||||
* @returns 图标名称数组
|
||||
*/
|
||||
export function loadIconCollection(collectionName: string): string[] {
|
||||
// 根据图标集名称调用对应的加载函数
|
||||
switch (collectionName) {
|
||||
case 'material-symbols':
|
||||
return loadMaterialSymbolsIcons();
|
||||
case 'cryptocurrency-color':
|
||||
return loadCryptoCurrencyIcons();
|
||||
default:
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Unknown icon collection: ${collectionName}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索图标
|
||||
* @param icons 图标列表
|
||||
* @param keyword 搜索关键词
|
||||
* @returns 匹配的图标列表
|
||||
*/
|
||||
export function searchIcons(icons: string[], keyword: string): string[] {
|
||||
if (!keyword.trim()) return icons;
|
||||
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
return icons.filter(icon => icon.toLowerCase().includes(lowerKeyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除图标缓存
|
||||
* @param collectionName 可选,指定清除的图标集,不传则清除所有
|
||||
*/
|
||||
export function clearIconCache(collectionName?: string): void {
|
||||
if (collectionName) {
|
||||
iconCacheMap.delete(collectionName);
|
||||
} else {
|
||||
iconCacheMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标完整路径
|
||||
* @param collection 图标集名称
|
||||
* @param iconName 图标名称
|
||||
* @returns 完整图标路径,如 'material-symbols:home'
|
||||
*/
|
||||
export function getIconPath(collection: string, iconName: string): string {
|
||||
return `${collection}:${iconName}`;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import IconPicker from '@/components/custom/icon-picker.vue';
|
||||
import HeaderBanner from './modules/header-banner.vue';
|
||||
import CardData from './modules/card-data.vue';
|
||||
import LineChart from './modules/line-chart.vue';
|
||||
@@ -11,10 +12,26 @@ import CreativityBanner from './modules/creativity-banner.vue';
|
||||
const appStore = useAppStore();
|
||||
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
const selectedIcon = ref('');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- Icon Picker 测试区域 -->
|
||||
<NCard title="Icon Picker 测试" :bordered="false" class="card-wrapper">
|
||||
<NSpace vertical :size="12">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm">选择图标:</span>
|
||||
<IconPicker v-model="selectedIcon" />
|
||||
</div>
|
||||
<div v-if="selectedIcon" class="flex items-center gap-3 rounded bg-gray-100 p-3 dark:bg-gray-800">
|
||||
<SvgIcon :icon="selectedIcon" class="text-2xl" />
|
||||
<code class="text-sm text-primary">{{ selectedIcon }}</code>
|
||||
</div>
|
||||
<div v-else class="text-sm text-gray-400">未选择图标</div>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<NAlert :title="$t('common.tip')" type="warning">
|
||||
{{ $t('page.home.branchDesc') }}
|
||||
</NAlert>
|
||||
|
||||
237
src/views/home/modules/icon-picker-example.vue
Normal file
237
src/views/home/modules/icon-picker-example.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { NButton, NCard, NDescriptions, NDescriptionsItem, NInput, NSpace } from 'naive-ui';
|
||||
import IconPicker from '@/components/custom/icon-picker.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'IconPickerExample'
|
||||
});
|
||||
|
||||
// 基础使用
|
||||
const selectedIcon1 = ref('');
|
||||
|
||||
// 默认值
|
||||
const selectedIcon2 = ref('material-symbols:home');
|
||||
|
||||
// 自定义配置
|
||||
const selectedIcon3 = ref('');
|
||||
|
||||
// 单个图标集
|
||||
const selectedIcon4 = ref('material-icon-theme:folder-aws');
|
||||
|
||||
// 表单集成示例
|
||||
interface FormData {
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const formData = ref<FormData>({
|
||||
name: '',
|
||||
icon: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('表单数据:', formData.value);
|
||||
window.$message?.success(`提交成功!图标:${formData.value.icon}`);
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formData.value = {
|
||||
name: '',
|
||||
icon: '',
|
||||
description: ''
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<NSpace vertical :size="24">
|
||||
<!-- 标题 -->
|
||||
<div>
|
||||
<h1 class="mb-2 text-2xl font-bold">Icon Picker 图标选择器</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">支持多个图标集切换,实时搜索和分页</p>
|
||||
</div>
|
||||
|
||||
<!-- 基础使用 - 多图标集切换 -->
|
||||
<NCard title="多图标集切换" :bordered="false" class="rounded-lg shadow">
|
||||
<NSpace vertical :size="16">
|
||||
<div>
|
||||
<p class="mb-3 text-sm text-gray-600">通过 Tabs 在不同的图标集之间切换</p>
|
||||
<IconPicker v-model="selectedIcon1" />
|
||||
</div>
|
||||
<NDescriptions :column="1" size="small" bordered>
|
||||
<NDescriptionsItem label="选中的图标">
|
||||
<code class="text-primary">{{ selectedIcon1 || '未选择' }}</code>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- 带默认值 -->
|
||||
<NCard title="带默认值" :bordered="false" class="rounded-lg shadow">
|
||||
<NSpace vertical :size="16">
|
||||
<div>
|
||||
<p class="mb-3 text-sm text-gray-600">设置默认图标值</p>
|
||||
<IconPicker v-model="selectedIcon2" />
|
||||
</div>
|
||||
<NDescriptions :column="1" size="small" bordered>
|
||||
<NDescriptionsItem label="选中的图标">
|
||||
<code class="text-primary">{{ selectedIcon2 }}</code>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- 自定义配置 -->
|
||||
<NCard title="自定义配置" :bordered="false" class="rounded-lg shadow">
|
||||
<NSpace vertical :size="16">
|
||||
<div>
|
||||
<p class="mb-3 text-sm text-gray-600">自定义弹窗宽度、每页数量和图标尺寸</p>
|
||||
<IconPicker v-model="selectedIcon3" :width="700" :page-size="80" icon-size="28px" />
|
||||
</div>
|
||||
<NDescriptions :column="1" size="small" bordered>
|
||||
<NDescriptionsItem label="选中的图标">
|
||||
<code class="text-primary">{{ selectedIcon3 || '未选择' }}</code>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- 单个图标集 -->
|
||||
<NCard title="单个图标集" :bordered="false" class="rounded-lg shadow">
|
||||
<NSpace vertical :size="16">
|
||||
<div>
|
||||
<p class="mb-3 text-sm text-gray-600">只显示 Cryptocurrency Color 图标集</p>
|
||||
<IconPicker v-model="selectedIcon4" prefix="cryptocurrency-color" :collections="['cryptocurrency-color']" />
|
||||
</div>
|
||||
<NDescriptions :column="1" size="small" bordered>
|
||||
<NDescriptionsItem label="选中的图标">
|
||||
<code class="text-primary">{{ selectedIcon4 }}</code>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- 表单集成 -->
|
||||
<NCard title="表单集成示例" :bordered="false" class="rounded-lg shadow">
|
||||
<NSpace vertical :size="16">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">名称</label>
|
||||
<NInput v-model:value="formData.name" placeholder="请输入名称" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">图标</label>
|
||||
<IconPicker v-model="formData.icon" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">描述</label>
|
||||
<NInput v-model:value="formData.description" type="textarea" placeholder="请输入描述" :rows="3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NSpace>
|
||||
<NButton type="primary" @click="handleSubmit">提交</NButton>
|
||||
<NButton @click="handleReset">重置</NButton>
|
||||
</NSpace>
|
||||
|
||||
<NDescriptions :column="1" size="small" bordered>
|
||||
<NDescriptionsItem label="表单数据">
|
||||
<code class="text-primary">{{ JSON.stringify(formData, null, 2) }}</code>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- API 文档 -->
|
||||
<NCard title="Props API" :bordered="false" class="rounded-lg shadow">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left">属性</th>
|
||||
<th class="px-4 py-2 text-left">类型</th>
|
||||
<th class="px-4 py-2 text-left">默认值</th>
|
||||
<th class="px-4 py-2 text-left">说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-t dark:border-gray-700">
|
||||
<td class="px-4 py-2"><code>modelValue</code></td>
|
||||
<td class="px-4 py-2"><code>string</code></td>
|
||||
<td class="px-4 py-2"><code>''</code></td>
|
||||
<td class="px-4 py-2">选中的图标值(v-model)</td>
|
||||
</tr>
|
||||
<tr class="border-t dark:border-gray-700">
|
||||
<td class="px-4 py-2"><code>prefix</code></td>
|
||||
<td class="px-4 py-2"><code>IconCollection</code></td>
|
||||
<td class="px-4 py-2"><code>'material-symbols'</code></td>
|
||||
<td class="px-4 py-2">默认图标集前缀</td>
|
||||
</tr>
|
||||
<tr class="border-t dark:border-gray-700">
|
||||
<td class="px-4 py-2"><code>collections</code></td>
|
||||
<td class="px-4 py-2"><code>IconCollection[]</code></td>
|
||||
<td class="px-4 py-2"><code>['material-symbols', 'material-icon-theme']</code></td>
|
||||
<td class="px-4 py-2">可选的图标集列表</td>
|
||||
</tr>
|
||||
<tr class="border-t dark:border-gray-700">
|
||||
<td class="px-4 py-2"><code>pageSize</code></td>
|
||||
<td class="px-4 py-2"><code>number</code></td>
|
||||
<td class="px-4 py-2"><code>60</code></td>
|
||||
<td class="px-4 py-2">每页显示的图标数量</td>
|
||||
</tr>
|
||||
<tr class="border-t dark:border-gray-700">
|
||||
<td class="px-4 py-2"><code>width</code></td>
|
||||
<td class="px-4 py-2"><code>number</code></td>
|
||||
<td class="px-4 py-2"><code>600</code></td>
|
||||
<td class="px-4 py-2">弹窗宽度(px)</td>
|
||||
</tr>
|
||||
<tr class="border-t dark:border-gray-700">
|
||||
<td class="px-4 py-2"><code>iconSize</code></td>
|
||||
<td class="px-4 py-2"><code>string</code></td>
|
||||
<td class="px-4 py-2"><code>'24px'</code></td>
|
||||
<td class="px-4 py-2">图标显示尺寸</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<!-- 功能特性 -->
|
||||
<NCard title="功能特性" :bordered="false" class="rounded-lg shadow">
|
||||
<ul class="list-disc list-inside text-sm space-y-2">
|
||||
<li>✅ 支持多个图标集切换(Tabs)</li>
|
||||
<li>✅ Material Symbols(3000+ 图标)</li>
|
||||
<li>✅ Material Icon Theme(VSCode 文件图标)</li>
|
||||
<li>✅ 支持实时搜索过滤</li>
|
||||
<li>✅ 分页展示,避免一次性渲染大量图标</li>
|
||||
<li>✅ 缓存机制,避免重复加载</li>
|
||||
<li>✅ 响应式设计,支持深色模式</li>
|
||||
<li>✅ TypeScript 类型支持</li>
|
||||
<li>✅ 支持清除选择</li>
|
||||
<li>✅ 图标预览和名称提示</li>
|
||||
</ul>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
@apply rounded bg-gray-100 px-1.5 py-0.5 text-xs font-mono dark:bg-gray-800;
|
||||
}
|
||||
|
||||
table {
|
||||
@apply border-collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
@apply font-semibold;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user