20260902173133
This commit is contained in:
1 parent
6b9cda5424
commit
db7813a2d2
16 files changed
+1350
-675
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { defineComponent, h } from 'vue'
|
||||
import * as LucideIcons from '@lucide/vue'
|
||||
|
||||
let iconMap = null
|
||||
@@ -19,11 +20,96 @@ function loadIcons() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步获取图标组件,传入 null/空字符串时返回 null。
|
||||
* @param {string | null | undefined} name lucide 图标名(首字母大写),如 "LayoutGrid"
|
||||
* 解析图标值,返回可渲染的 Vue 组件:
|
||||
* - 以 "lucide:" 开头(如 "lucide:chevron-down")→ 去掉前缀,名称归一化后匹配 lucide 图标
|
||||
* - 其他非空字符串 → 先尝试按 lucide 图标名匹配;匹配不到时仅当看起来是有效 SVG 标记才渲染
|
||||
* - 无效内容(既非 lucide 名也非 SVG 标记)→ 返回 null,不渲染
|
||||
* 传入 null/空字符串时返回 null。
|
||||
* @param {string | null | undefined} value
|
||||
*/
|
||||
export function getIcon(name) {
|
||||
export function getIcon(value) {
|
||||
if (!value || typeof value !== 'string') return null
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return null
|
||||
|
||||
if (!iconMap) loadIcons()
|
||||
if (!name || !iconMap) return null
|
||||
return iconMap[name] || null
|
||||
|
||||
// lucide 前缀:显式指定图标名,匹配不到返回 null
|
||||
if (trimmed.startsWith('lucide:')) {
|
||||
const name = normalizeIconName(trimmed.slice('lucide:'.length))
|
||||
return iconMap[name] || null
|
||||
}
|
||||
|
||||
// 无前缀:先尝试 lucide 图标名匹配
|
||||
const normalized = normalizeIconName(trimmed)
|
||||
if (iconMap[normalized]) return iconMap[normalized]
|
||||
|
||||
// 既非 lucide 名:仅当是有效 SVG 标记才渲染,否则视为无效不渲染
|
||||
return looksLikeSvg(trimmed) ? getSvgComponent(trimmed) : null
|
||||
}
|
||||
|
||||
// 把各种命名风格归一化为 lucide 的 PascalCase 导出名:
|
||||
// "chevron-down" / "chevron_down" / "chevronDown" / "CHEVRON DOWN" → "ChevronDown"
|
||||
function normalizeIconName(name) {
|
||||
const parts = String(name || '').split(/[-_\s]+/).filter(Boolean)
|
||||
if (parts.length === 0) return ''
|
||||
// 无分隔符(camelCase / 已是大写开头):直接首字母大写
|
||||
if (parts.length === 1) {
|
||||
const single = parts[0]
|
||||
return single.charAt(0).toUpperCase() + single.slice(1)
|
||||
}
|
||||
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
|
||||
}
|
||||
|
||||
// 判断字符串是否为有效 SVG 标记:用 DOMParser 按 SVG 文档解析,
|
||||
// 解析失败(存在 parsererror)或根元素不是 <svg> 时视为无效。
|
||||
// 无 DOMParser 环境(SSR/测试)回退到基础启发式判断。
|
||||
function looksLikeSvg(content) {
|
||||
const trimmed = String(content || '').trim()
|
||||
if (!trimmed) return false
|
||||
if (typeof DOMParser === 'undefined') {
|
||||
return /^<svg[\s>]/i.test(trimmed)
|
||||
}
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString(trimmed, 'image/svg+xml')
|
||||
if (doc.querySelector('parsererror')) return false
|
||||
const root = doc.documentElement
|
||||
return root && root.tagName.toLowerCase() === 'svg'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const svgComponentCache = new Map()
|
||||
|
||||
function getSvgComponent(svg) {
|
||||
let component = svgComponentCache.get(svg)
|
||||
if (!component) {
|
||||
component = defineComponent({
|
||||
name: 'RawSvgIcon',
|
||||
render() {
|
||||
return h('span', {
|
||||
class: 'raw-svg-icon',
|
||||
style: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 0,
|
||||
},
|
||||
innerHTML: svg,
|
||||
})
|
||||
},
|
||||
})
|
||||
svgComponentCache.set(svg, component)
|
||||
}
|
||||
return component
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 lucide 图标名称(按字母排序)。
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getIconNames() {
|
||||
if (!iconMap) loadIcons()
|
||||
return Object.keys(iconMap).toSorted()
|
||||
}
|
||||
Reference in new issue
Block a user