From 1cd7e2449c208d182fb4a963341380b329dd1be5 Mon Sep 17 00:00:00 2001 From: Bob Du Date: Wed, 14 Jan 2026 10:56:15 +0800 Subject: [PATCH] feat: auto-import icon components Signed-off-by: Bob Du --- AGENTS.md | 7 +++ auto-imports.d.ts | 11 ++++ components.d.ts | 45 +++++++++++++ package.json | 3 +- pnpm-lock.yaml | 63 ++++++++++++++++--- src/components/common/PromptStore/index.vue | 17 +++-- .../common/Setting/BuiltInPrompt.vue | 7 +-- src/components/common/Setting/ChatRecord.vue | 3 +- src/components/common/Setting/General.vue | 18 +++--- src/components/common/Setting/Gift.vue | 3 +- src/components/common/Setting/Statistics.vue | 11 ++-- src/components/common/Setting/index.vue | 33 +++++----- src/components/common/SvgIcon/index.vue | 20 ------ src/components/common/index.ts | 3 +- src/hooks/useIconRender.ts | 35 ----------- src/views/chat/components/Header/index.vue | 14 ++--- src/views/chat/components/Message/index.vue | 16 ++--- src/views/chat/index.vue | 25 ++++---- src/views/chat/layout/sider/Footer.vue | 6 +- src/views/chat/layout/sider/List.vue | 11 ++-- tsconfig.json | 2 +- vite.config.ts | 15 +++++ 22 files changed, 212 insertions(+), 156 deletions(-) delete mode 100644 src/components/common/SvgIcon/index.vue delete mode 100644 src/hooks/useIconRender.ts diff --git a/AGENTS.md b/AGENTS.md index f03f8b7c8..7fc7c7821 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,13 @@ - ESLint is the source of truth; run `pnpm lint:fix` before commit. - Commit hooks are managed by Husky (`pnpm bootstrap`). +## Icons (unplugin-icons) +- Use `Icon*` components (e.g., ``) in templates; they are auto-imported. +- In script/render functions, `Icon*` components are also auto-imported via `unplugin-auto-import`; avoid manual `~icons/...` imports. +- If an icon name fails, switch to a valid icon in the same collection (e.g., `ri`), then rerun the app. +- Loader note: `@iconify/utils` uses `searchForIcon()` with `getPossibleIconNames()`, so numeric names like `settings4-line` can resolve to `settings-4-line` at load time. +- Rule: always use the `Icon*` component form (e.g., ``) for icons going forward. + ## i18n Keys & Usage - Locale files live in `src/locales/` (`en-US.json`, `zh-CN.json`, `zh-TW.json`, `ko-KR.json`). - `en-US.json` is the canonical schema; add new keys there first, then mirror them in other locales. diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 1b034cab7..800980ebb 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -7,6 +7,17 @@ export {} declare global { const EffectScope: typeof import('vue').EffectScope + const IconIcOutlineCode: typeof import('~icons/ic/outline-code').default + const IconIcOutlineCodeOff: typeof import('~icons/ic/outline-code-off').default + const IconMdiFormatVerticalAlignBottom: typeof import('~icons/mdi/format-vertical-align-bottom').default + const IconMdiFormatVerticalAlignTop: typeof import('~icons/mdi/format-vertical-align-top').default + const IconMdiReorderHorizontal: typeof import('~icons/mdi/reorder-horizontal').default + const IconRiContrastLine: typeof import('~icons/ri/contrast-line').default + const IconRiDeleteBinLine: typeof import('~icons/ri/delete-bin-line').default + const IconRiExternalLinkLine: typeof import('~icons/ri/external-link-line').default + const IconRiFileCopy2Line: typeof import('~icons/ri/file-copy2-line').default + const IconRiMoonFoggyLine: typeof import('~icons/ri/moon-foggy-line').default + const IconRiSunFoggyLine: typeof import('~icons/ri/sun-foggy-line').default const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate const computed: typeof import('vue').computed const createApp: typeof import('vue').createApp diff --git a/components.d.ts b/components.d.ts index e24602dbf..6c64221b4 100644 --- a/components.d.ts +++ b/components.d.ts @@ -11,6 +11,51 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + IconIcOutlineChat: typeof import('~icons/ic/outline-chat')['default'] + IconMageBoxUpload: typeof import('~icons/mage/box-upload')['default'] + IconMdiFormatVerticalAlignBottom: typeof import('~icons/mdi/format-vertical-align-bottom')['default'] + IconMdiFormatVerticalAlignTop: typeof import('~icons/mdi/format-vertical-align-top')['default'] + IconMdiGift: typeof import('~icons/mdi/gift')['default'] + IconMdiLightbulbOutline: typeof import('~icons/mdi/lightbulb-outline')['default'] + IconMdiReorderHorizontal: typeof import('~icons/mdi/reorder-horizontal')['default'] + IconMdiWeb: typeof import('~icons/mdi/web')['default'] + IconRiAddFill: typeof import('~icons/ri/add-fill')['default'] + IconRiAlignJustify: typeof import('~icons/ri/align-justify')['default'] + IconRiAlignRight: typeof import('~icons/ri/align-right')['default'] + IconRiArrowDownSLine: typeof import('~icons/ri/arrow-down-s-line')['default'] + IconRiBarChartBoxLine: typeof import('~icons/ri/bar-chart-box-line')['default'] + IconRiBubbleChartFill: typeof import('~icons/ri/bubble-chart-fill')['default'] + IconRiChatDownloadLine: typeof import('~icons/ri/chat-download-line')['default'] + IconRiChatHistoryLine: typeof import('~icons/ri/chat-history-line')['default'] + IconRiChatQuoteLine: typeof import('~icons/ri/chat-quote-line')['default'] + IconRiChatUploadLine: typeof import('~icons/ri/chat-upload-line')['default'] + IconRiCloseCircleLine: typeof import('~icons/ri/close-circle-line')['default'] + IconRiDeleteBack2Fill: typeof import('~icons/ri/delete-back2-fill')['default'] + IconRiDeleteBinLine: typeof import('~icons/ri/delete-bin-line')['default'] + IconRiDownload2Fill: typeof import('~icons/ri/download2-fill')['default'] + IconRiDownload2Line: typeof import('~icons/ri/download2-line')['default'] + IconRiDownloadLine: typeof import('~icons/ri/download-line')['default'] + IconRiEditLine: typeof import('~icons/ri/edit-line')['default'] + IconRiEqualizerLine: typeof import('~icons/ri/equalizer-line')['default'] + IconRiFileUserLine: typeof import('~icons/ri/file-user-line')['default'] + IconRiImageEditLine: typeof import('~icons/ri/image-edit-line')['default'] + IconRiInboxLine: typeof import('~icons/ri/inbox-line')['default'] + IconRiKey2Line: typeof import('~icons/ri/key2-line')['default'] + IconRiLink: typeof import('~icons/ri/link')['default'] + IconRiListSettingsLine: typeof import('~icons/ri/list-settings-line')['default'] + IconRiMailLine: typeof import('~icons/ri/mail-line')['default'] + IconRiMessage3Line: typeof import('~icons/ri/message3-line')['default'] + IconRiMore2Fill: typeof import('~icons/ri/more2-fill')['default'] + IconRiQuestionAnswerLine: typeof import('~icons/ri/question-answer-line')['default'] + IconRiRestartLine: typeof import('~icons/ri/restart-line')['default'] + IconRiSaveLine: typeof import('~icons/ri/save-line')['default'] + IconRiSendPlaneFill: typeof import('~icons/ri/send-plane-fill')['default'] + IconRiSettings4Line: typeof import('~icons/ri/settings4-line')['default'] + IconRiSettingsLine: typeof import('~icons/ri/settings-line')['default'] + IconRiStopCircleLine: typeof import('~icons/ri/stop-circle-line')['default'] + IconRiUpload2Fill: typeof import('~icons/ri/upload2-fill')['default'] + IconRiUser5Line: typeof import('~icons/ri/user5-line')['default'] + IconUilExit: typeof import('~icons/uil/exit')['default'] NAutoComplete: typeof import('naive-ui')['NAutoComplete'] NAvatar: typeof import('naive-ui')['NAvatar'] NButton: typeof import('naive-ui')['NButton'] diff --git a/package.json b/package.json index 2768b37b7..fa7544b7f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@antfu/eslint-config": "^6.3.0", "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", - "@iconify/vue": "^5.0.0", + "@iconify/json": "^2.2.397", "@tailwindcss/vite": "^4.1.17", "@types/katex": "^0.16.7", "@types/markdown-it": "^14.1.2", @@ -56,6 +56,7 @@ "tailwindcss": "^4.1.17", "typescript": "~5.9.3", "unplugin-auto-import": "^20.3.0", + "unplugin-icons": "^22.5.0", "unplugin-vue-components": "^30.0.0", "vite": "^7.2.6", "vue": "^3.5.25", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e4ee6c75..d3e8cef5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,9 @@ importers: '@commitlint/config-conventional': specifier: ^20.0.0 version: 20.0.0 - '@iconify/vue': - specifier: ^5.0.0 - version: 5.0.0(vue@3.5.25(typescript@5.9.3)) + '@iconify/json': + specifier: ^2.2.397 + version: 2.2.427 '@tailwindcss/vite': specifier: ^4.1.17 version: 4.1.17(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(yaml@2.8.2)) @@ -104,6 +104,9 @@ importers: unplugin-auto-import: specifier: ^20.3.0 version: 20.3.0(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3))) + unplugin-icons: + specifier: ^22.5.0 + version: 22.5.0(@vue/compiler-sfc@3.5.25) unplugin-vue-components: specifier: ^30.0.0 version: 30.0.0(@babel/parser@7.28.5)(vue@3.5.25(typescript@5.9.3)) @@ -567,13 +570,14 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@iconify/json@2.2.427': + resolution: {integrity: sha512-QyFXZmAzOvYbDInKKpDgPJJNWB6oXKuAFxzRTXeIEzRP1q/E9GJidvyg5f/Tt3KE8gEP+RVD2793tUn1foFbcw==} + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@iconify/vue@5.0.0': - resolution: {integrity: sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==} - peerDependencies: - vue: '>=3' + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} '@intlify/core-base@11.2.2': resolution: {integrity: sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==} @@ -2853,6 +2857,29 @@ packages: '@vueuse/core': optional: true + unplugin-icons@22.5.0: + resolution: {integrity: sha512-MBlMtT5RuMYZy4TZgqUL2OTtOdTUVsS1Mhj6G1pEzMlFJlEnq6mhUfoIt45gBWxHcsOdXJDWLg3pRZ+YmvAVWQ==} + peerDependencies: + '@svgr/core': '>=7.0.0' + '@svgx/core': ^1.0.1 + '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 + svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 + vue-template-compiler: ^2.6.12 + vue-template-es2015-compiler: ^1.9.0 + peerDependenciesMeta: + '@svgr/core': + optional: true + '@svgx/core': + optional: true + '@vue/compiler-sfc': + optional: true + svelte: + optional: true + vue-template-compiler: + optional: true + vue-template-es2015-compiler: + optional: true + unplugin-utils@0.3.1: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} @@ -3445,12 +3472,18 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@iconify/json@2.2.427': + dependencies: + '@iconify/types': 2.0.0 + pathe: 2.0.3 + '@iconify/types@2.0.0': {} - '@iconify/vue@5.0.0(vue@3.5.25(typescript@5.9.3))': + '@iconify/utils@3.1.0': dependencies: + '@antfu/install-pkg': 1.1.0 '@iconify/types': 2.0.0 - vue: 3.5.25(typescript@5.9.3) + mlly: 1.8.0 '@intlify/core-base@11.2.2': dependencies: @@ -5879,6 +5912,18 @@ snapshots: optionalDependencies: '@vueuse/core': 14.1.0(vue@3.5.25(typescript@5.9.3)) + unplugin-icons@22.5.0(@vue/compiler-sfc@3.5.25): + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/utils': 3.1.0 + debug: 4.4.3 + local-pkg: 1.1.2 + unplugin: 2.3.11 + optionalDependencies: + '@vue/compiler-sfc': 3.5.25 + transitivePeerDependencies: + - supports-color + unplugin-utils@0.3.1: dependencies: pathe: 2.0.3 diff --git a/src/components/common/PromptStore/index.vue b/src/components/common/PromptStore/index.vue index 1fa0b1edc..fb0f9f0c5 100644 --- a/src/components/common/PromptStore/index.vue +++ b/src/components/common/PromptStore/index.vue @@ -6,7 +6,6 @@ import { UserPrompt } from '@/components/common/Setting/model' import { useBasicLayout } from '@/hooks/useBasicLayout' import { useAuthStoreWithout, usePromptStore } from '@/store' import { rankBetween } from '@/utils/lexorank' -import { SvgIcon } from '..' import PromptRecommend from '../../../assets/recommend.json' const props = defineProps() @@ -477,7 +476,7 @@ function createColumns(): DataTableColumns { draggingId.value = null }, }, - { default: () => h(SvgIcon, { icon: 'mdi:reorder-horizontal' }) }, + { default: () => h(IconMdiReorderHorizontal) }, ), default: () => t('store.drag'), }, @@ -494,7 +493,7 @@ function createColumns(): DataTableColumns { disabled, onClick: () => moveToTop(row), }, - { default: () => h(SvgIcon, { icon: 'mdi:format-vertical-align-top' }) }, + { default: () => h(IconMdiFormatVerticalAlignTop) }, ), default: () => t('store.moveTop'), }, @@ -511,7 +510,7 @@ function createColumns(): DataTableColumns { disabled, onClick: () => moveToBottom(row), }, - { default: () => h(SvgIcon, { icon: 'mdi:format-vertical-align-bottom' }) }, + { default: () => h(IconMdiFormatVerticalAlignBottom) }, ), default: () => t('store.moveBottom'), }, @@ -768,7 +767,7 @@ async function handleGetUserPromptList() { @dragstart="handleMobileDragStart($event, item)" @dragend="handleMobileDragEnd" > - + {{ t('store.drag') }} @@ -781,7 +780,7 @@ async function handleGetUserPromptList() { :disabled="ordering || isSearchActive" @click="moveToTop(item)" > - + {{ t('store.moveTop') }} @@ -794,7 +793,7 @@ async function handleGetUserPromptList() { :disabled="ordering || isSearchActive" @click="moveToBottom(item)" > - + {{ t('store.moveBottom') }} @@ -848,11 +847,11 @@ async function handleGetUserPromptList() { :href="info.url" target="_blank" > - + - + diff --git a/src/components/common/Setting/BuiltInPrompt.vue b/src/components/common/Setting/BuiltInPrompt.vue index 04505e499..458dd845c 100644 --- a/src/components/common/Setting/BuiltInPrompt.vue +++ b/src/components/common/Setting/BuiltInPrompt.vue @@ -5,7 +5,6 @@ import { fetchBuiltInPromptList, fetchDeleteBuiltInPrompt, fetchUpsertBuiltInPro import { BuiltInPrompt } from '@/components/common/Setting/model' import { useBasicLayout } from '@/hooks/useBasicLayout' import { rankBetween } from '@/utils/lexorank' -import { SvgIcon } from '..' const { t } = useI18n() const message = useMessage() @@ -236,7 +235,7 @@ function createColumns(): DataTableColumns { draggingId.value = null }, }, - { default: () => h(SvgIcon, { icon: 'mdi:reorder-horizontal' }) }, + { default: () => h(IconMdiReorderHorizontal) }, ), default: () => t('store.drag'), }, @@ -253,7 +252,7 @@ function createColumns(): DataTableColumns { disabled, onClick: () => moveToTop(row), }, - { default: () => h(SvgIcon, { icon: 'mdi:format-vertical-align-top' }) }, + { default: () => h(IconMdiFormatVerticalAlignTop) }, ), default: () => t('store.moveTop'), }, @@ -270,7 +269,7 @@ function createColumns(): DataTableColumns { disabled, onClick: () => moveToBottom(row), }, - { default: () => h(SvgIcon, { icon: 'mdi:format-vertical-align-bottom' }) }, + { default: () => h(IconMdiFormatVerticalAlignBottom) }, ), default: () => t('store.moveBottom'), }, diff --git a/src/components/common/Setting/ChatRecord.vue b/src/components/common/Setting/ChatRecord.vue index 6aad582fb..dc1ac5462 100644 --- a/src/components/common/Setting/ChatRecord.vue +++ b/src/components/common/Setting/ChatRecord.vue @@ -1,7 +1,6 @@ - - diff --git a/src/components/common/index.ts b/src/components/common/index.ts index fa0765bc3..5cac3de45 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -4,8 +4,7 @@ import NaiveProvider from './NaiveProvider/index.vue' import PromptStore from './PromptStore/index.vue' import PromptTypeTag from './PromptTypeTag/index.vue' import Setting from './Setting/index.vue' -import SvgIcon from './SvgIcon/index.vue' import UserAvatar from './UserAvatar/index.vue' import Watermark from './Watermark/index.vue' -export { GithubSite, HoverButton, NaiveProvider, PromptStore, PromptTypeTag, Setting, SvgIcon, UserAvatar, Watermark } +export { GithubSite, HoverButton, NaiveProvider, PromptStore, PromptTypeTag, Setting, UserAvatar, Watermark } diff --git a/src/hooks/useIconRender.ts b/src/hooks/useIconRender.ts deleted file mode 100644 index 0e5caa6f6..000000000 --- a/src/hooks/useIconRender.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { SvgIcon } from '@/components/common' - -export function useIconRender() { - interface IconConfig { - icon: string - color?: string - fontSize?: number - } - - interface IconStyle { - color?: string - fontSize?: string - } - - const iconRender = (config: IconConfig) => { - const { color, fontSize, icon } = config - - const style: IconStyle = {} - - if (color) - style.color = color - - if (fontSize) - style.fontSize = `${fontSize}px` - - if (!icon) - window.console.warn('iconRender: icon is required') - - return () => h(SvgIcon, { icon, style }) - } - - return { - iconRender, - } -} diff --git a/src/views/chat/components/Header/index.vue b/src/views/chat/components/Header/index.vue index 44dc9aac1..6d2beac8e 100644 --- a/src/views/chat/components/Header/index.vue +++ b/src/views/chat/components/Header/index.vue @@ -1,5 +1,5 @@