From 2eab7c53654d3a91516ec9d2ad7934855a24c7d0 Mon Sep 17 00:00:00 2001 From: Seven Date: Tue, 30 Dec 2025 17:41:51 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E7=BB=84=E4=BB=B6=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD=EF=BC=9B=E6=9B=B4=E6=96=B0=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E9=85=8D=E7=BD=AE=E5=92=8C=E7=B1=BB=E5=9E=8B=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=EF=BC=9B=E4=BF=AE=E5=A4=8D=E4=BE=9D=E8=B5=96=E5=9C=B0?= =?UTF-8?q?=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- pnpm-lock.yaml | 33 ++- src/components/custom/icon-picker.vue | 226 +++++++++++++++++ src/router/elegant/routes.ts | 22 +- src/typings/components.d.ts | 2 + src/typings/icon.d.ts | 21 ++ src/utils/icon-loader.ts | 149 +++++++++++ src/views/home/index.vue | 19 +- .../home/modules/icon-picker-example.vue | 237 ++++++++++++++++++ 9 files changed, 696 insertions(+), 18 deletions(-) create mode 100644 src/components/custom/icon-picker.vue create mode 100644 src/typings/icon.d.ts create mode 100644 src/utils/icon-loader.ts create mode 100644 src/views/home/modules/icon-picker-example.vue diff --git a/package.json b/package.json index 638839e..49dd075 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d4d4be..eb7dcd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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)) diff --git a/src/components/custom/icon-picker.vue b/src/components/custom/icon-picker.vue new file mode 100644 index 0000000..7ed43e7 --- /dev/null +++ b/src/components/custom/icon-picker.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/src/router/elegant/routes.ts b/src/router/elegant/routes.ts index 5a04d22..14cfd7f 100644 --- a/src/router/elegant/routes.ts +++ b/src/router/elegant/routes.ts @@ -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 - } - }, + } ]; diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 092613b..2d2dc9b 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -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'] diff --git a/src/typings/icon.d.ts b/src/typings/icon.d.ts new file mode 100644 index 0000000..ea1a1f9 --- /dev/null +++ b/src/typings/icon.d.ts @@ -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; +} diff --git a/src/utils/icon-loader.ts b/src/utils/icon-loader.ts new file mode 100644 index 0000000..60baaaa --- /dev/null +++ b/src/utils/icon-loader.ts @@ -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; + /** 加载时间戳 */ + timestamp: number; +} + +const iconCacheMap = new Map(); + +/** + * 加载 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 = {}; + 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}`; +} diff --git a/src/views/home/index.vue b/src/views/home/index.vue index 91f8e84..2aa527e 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -1,6 +1,7 @@