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 @@