feat(admin): back the SPA icon picker with the server catalog
FaIconPicker drops its duplicated hard-coded FA subset and loads GET /aksrt/v1/icons/catalog: full solid/regular/brands sets, the built-in zibsvg sprite tab and an inline custom SVG tab. Props/emit surface is unchanged; the system-notice title icon schema drift (input vs icon) is fixed on the JS side.
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Icon catalog API — serves the universal icon picker (FA5 + sprite + custom).
|
||||||
|
*/
|
||||||
|
export const iconsApiMethods = {
|
||||||
|
getIconCatalog() {
|
||||||
|
return this.request('/icons/catalog')
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { paymentApiMethods } from './payment.js'
|
|||||||
import { extensionsApiMethods } from './extensions.js'
|
import { extensionsApiMethods } from './extensions.js'
|
||||||
import { toolsApiMethods } from './tools.js'
|
import { toolsApiMethods } from './tools.js'
|
||||||
import { schemaApiMethods } from './schema.js'
|
import { schemaApiMethods } from './schema.js'
|
||||||
|
import { iconsApiMethods } from './icons.js'
|
||||||
|
|
||||||
export const apiMethods = {
|
export const apiMethods = {
|
||||||
...globalApiMethods,
|
...globalApiMethods,
|
||||||
@@ -16,4 +17,5 @@ export const apiMethods = {
|
|||||||
...extensionsApiMethods,
|
...extensionsApiMethods,
|
||||||
...toolsApiMethods,
|
...toolsApiMethods,
|
||||||
...schemaApiMethods,
|
...schemaApiMethods,
|
||||||
|
...iconsApiMethods,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-fa-icon-picker">
|
<div class="aksrt-fa-icon-picker">
|
||||||
<n-popover trigger="click" placement="bottom-start" :width="360" :style="{ padding: 0 }">
|
<n-popover trigger="click" placement="bottom-start" :width="400" :style="{ padding: 0 }">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-input
|
<n-input
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@@ -10,7 +10,20 @@
|
|||||||
@clear="$emit('update:modelValue', '')"
|
@clear="$emit('update:modelValue', '')"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FaIcon v-if="modelValue" :icon="modelValue" />
|
<!-- FA value -->
|
||||||
|
<FaIcon v-if="isFaValue(modelValue)" :icon="modelValue" />
|
||||||
|
<!-- Built-in sprite value -->
|
||||||
|
<span
|
||||||
|
v-else-if="isZibsvgValue(modelValue)"
|
||||||
|
class="aksrt-icon-picker__preview-svg"
|
||||||
|
v-html="zibsvgMarkup(modelValue)"
|
||||||
|
></span>
|
||||||
|
<!-- Inline custom SVG value -->
|
||||||
|
<span
|
||||||
|
v-else-if="isCustomSvgValue(modelValue)"
|
||||||
|
class="aksrt-icon-picker__preview-svg"
|
||||||
|
v-html="cleanSvg(modelValue.slice(10))"
|
||||||
|
></span>
|
||||||
</template>
|
</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<FaIcon icon="fa fas fa-chevron-down" :size="12" />
|
<FaIcon icon="fa fas fa-chevron-down" :size="12" />
|
||||||
@@ -20,12 +33,7 @@
|
|||||||
|
|
||||||
<div class="aksrt-icon-picker__panel">
|
<div class="aksrt-icon-picker__panel">
|
||||||
<div class="aksrt-icon-picker__search">
|
<div class="aksrt-icon-picker__search">
|
||||||
<n-input
|
<n-input v-model:value="search" placeholder="搜索图标..." size="small" clearable>
|
||||||
v-model:value="search"
|
|
||||||
placeholder="搜索图标..."
|
|
||||||
size="small"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FaIcon icon="fa fas fa-search" :size="12" />
|
<FaIcon icon="fa fas fa-search" :size="12" />
|
||||||
</template>
|
</template>
|
||||||
@@ -33,23 +41,51 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="aksrt-icon-picker__tabs">
|
<div class="aksrt-icon-picker__tabs">
|
||||||
<n-radio-group v-model:value="activePrefix" size="small">
|
<n-radio-group v-model:value="activeTab" size="small">
|
||||||
<n-radio-button value="fas">Solid</n-radio-button>
|
<n-radio-button v-for="tab in tabs" :key="tab.id" :value="tab.id">
|
||||||
<n-radio-button value="far">Regular</n-radio-button>
|
{{ tab.title }}
|
||||||
<n-radio-button value="fab">Brands</n-radio-button>
|
</n-radio-button>
|
||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="aksrt-icon-picker__grid" ref="gridRef">
|
<!-- Custom SVG tab -->
|
||||||
|
<div v-if="activeTab === 'custom'" class="aksrt-icon-picker__custom">
|
||||||
|
<n-input v-model:value="customCode" type="textarea" placeholder='<svg viewBox="0 0 1024 1024"><path d="…"></path></svg>' :rows="4" />
|
||||||
|
<div class="aksrt-icon-picker__custom-actions">
|
||||||
|
<span class="aksrt-icon-picker__custom-preview" v-html="customPreview"></span>
|
||||||
|
<n-button size="small" type="primary" :disabled="!customPreview" @click="insertCustom">
|
||||||
|
确认插入
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="sessionCustom.length" class="aksrt-icon-picker__grid aksrt-icon-picker__grid--session">
|
||||||
|
<div
|
||||||
|
v-for="(svg, i) in sessionCustom"
|
||||||
|
:key="i"
|
||||||
|
class="aksrt-icon-picker__item"
|
||||||
|
:class="{ 'is-active': modelValue === `customsvg-${svg}` }"
|
||||||
|
title="customsvg"
|
||||||
|
@click="$emit('update:modelValue', `customsvg-${svg}`)"
|
||||||
|
v-html="cleanSvg(svg)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Catalog tab -->
|
||||||
|
<div v-else class="aksrt-icon-picker__grid" ref="gridRef">
|
||||||
<div
|
<div
|
||||||
v-for="icon in filteredIcons"
|
v-for="icon in filteredIcons"
|
||||||
:key="icon"
|
:key="icon"
|
||||||
class="aksrt-icon-picker__item"
|
class="aksrt-icon-picker__item"
|
||||||
:class="{ 'is-active': modelValue === `fa ${activePrefix} ${icon}` }"
|
:class="{ 'is-active': modelValue === composeIconValue(activeGroup, icon) }"
|
||||||
:title="icon"
|
:title="icon"
|
||||||
@click="selectIcon(icon)"
|
@click="selectIcon(icon)"
|
||||||
>
|
>
|
||||||
<i :class="['fa', activePrefix, icon]"></i>
|
<i v-if="activeGroup.type === 'fa'" :class="['fa', activeTab, icon]"></i>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="aksrt-icon-picker__preview-svg"
|
||||||
|
v-html="zibsvgMarkup(`zibsvg-${icon}`)"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="filteredIcons.length === 0" class="aksrt-icon-picker__empty">
|
<div v-if="filteredIcons.length === 0" class="aksrt-icon-picker__empty">
|
||||||
未找到匹配图标
|
未找到匹配图标
|
||||||
@@ -62,11 +98,21 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
/**
|
/**
|
||||||
* FaIconPicker — searchable FA5 icon picker.
|
* FaIconPicker — universal icon picker backed by the server catalog
|
||||||
* Renders an input that opens a popover grid of FA5 icons.
|
* (GET /aksrt/v1/icons/catalog): full FA5 free set, the theme's built-in
|
||||||
|
* SVG sprite keys and inline custom SVG values.
|
||||||
*/
|
*/
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import FaIcon from './FaIcon.vue'
|
import FaIcon from './FaIcon.vue'
|
||||||
|
import api from '@/api/index.js'
|
||||||
|
import {
|
||||||
|
cleanSvg,
|
||||||
|
isFaValue,
|
||||||
|
isZibsvgValue,
|
||||||
|
isCustomSvgValue,
|
||||||
|
zibsvgMarkup,
|
||||||
|
composeIconValue,
|
||||||
|
} from '@/utils/icons.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: String, default: '' },
|
modelValue: { type: String, default: '' },
|
||||||
@@ -75,92 +121,54 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const FALLBACK_TABS = [
|
||||||
|
{ id: 'fas', title: 'Solid', type: 'fa', icons: [] },
|
||||||
|
{ id: 'far', title: 'Regular', type: 'fa', icons: [] },
|
||||||
|
{ id: 'fab', title: 'Brands', type: 'fa', icons: [] },
|
||||||
|
{ id: 'zibsvg', title: '内置 SVG', type: 'svg', icons: [] },
|
||||||
|
{ id: 'custom', title: '自定义', type: 'custom', icons: [] },
|
||||||
|
]
|
||||||
|
|
||||||
|
const tabs = ref(FALLBACK_TABS)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const activePrefix = ref('fas')
|
const activeTab = ref('fas')
|
||||||
const gridRef = ref(null)
|
const customCode = ref('')
|
||||||
|
const sessionCustom = ref([])
|
||||||
|
|
||||||
// Common FA5 icons subset — a curated list covering most admin UI needs.
|
const activeGroup = computed(() => tabs.value.find(t => t.id === activeTab.value) || tabs.value[0])
|
||||||
// Curated FA5 subsets for the three bundled font families.
|
|
||||||
const SOLID_ICONS = [
|
|
||||||
'fa-home', 'fa-cog', 'fa-cogs', 'fa-wrench', 'fa-sliders-h', 'fa-wrench',
|
|
||||||
'fa-search', 'fa-filter', 'fa-sort', 'fa-sync-alt', 'fa-redo-alt', 'fa-undo',
|
|
||||||
'fa-plus', 'fa-minus', 'fa-times', 'fa-check', 'fa-exclamation', 'fa-info',
|
|
||||||
'fa-question', 'fa-bell', 'fa-envelope', 'fa-comment', 'fa-comments',
|
|
||||||
'fa-user', 'fa-users', 'fa-user-plus', 'fa-user', 'fa-user-circle',
|
|
||||||
'fa-lock', 'fa-unlock', 'fa-key', 'fa-shield-alt', 'fa-id-badge',
|
|
||||||
'fa-globe', 'fa-globe', 'fa-link', 'fa-external-link-alt', 'fa-anchor',
|
|
||||||
'fa-image', 'fa-image', 'fa-camera', 'fa-video', 'fa-music', 'fa-file',
|
|
||||||
'fa-folder', 'fa-folder-open', 'fa-download', 'fa-upload', 'fa-cloud',
|
|
||||||
'fa-database', 'fa-server', 'fa-hdd', 'fa-microchip', 'fa-microchip',
|
|
||||||
'fa-desktop', 'fa-laptop', 'fa-mobile-alt', 'fa-tablet-alt',
|
|
||||||
'fa-chart-bar', 'fa-chart-line', 'fa-chart-pie', 'fa-chart-area',
|
|
||||||
'fa-tachometer-alt', 'fa-tasks', 'fa-clipboard', 'fa-clipboard',
|
|
||||||
'fa-list', 'fa-list-alt', 'fa-list-ol', 'fa-list-ul', 'fa-th', 'fa-th-large',
|
|
||||||
'fa-tags', 'fa-tag', 'fa-bookmark', 'fa-star', 'fa-heart', 'fa-thumbs-up',
|
|
||||||
'fa-share', 'fa-share-alt', 'fa-external-link-alt', 'fa-clipboard',
|
|
||||||
'fa-eye', 'fa-eye-slash', 'fa-lightbulb', 'fa-magic', 'fa-paint-brush',
|
|
||||||
'fa-paint-brush', 'fa-font', 'fa-text-height', 'fa-bold', 'fa-italic',
|
|
||||||
'fa-code', 'fa-terminal', 'fa-bug', 'fa-exclamation-triangle',
|
|
||||||
'fa-exclamation-circle', 'fa-check-circle', 'fa-info-circle',
|
|
||||||
'fa-spinner', 'fa-circle-notch', 'fa-hourglass-half', 'fa-clock',
|
|
||||||
'fa-calendar', 'fa-calendar', 'fa-history', 'fa-hourglass',
|
|
||||||
'fa-map-marker-alt', 'fa-map', 'fa-road', 'fa-compass',
|
|
||||||
'fa-shopping-cart', 'fa-shopping-bag', 'fa-credit-card', 'fa-money-bill',
|
|
||||||
'fa-database', 'fa-dollar-sign', 'fa-credit-card', 'fa-file-alt',
|
|
||||||
'fa-newspaper', 'fa-rss', 'fa-podcast', 'fa-bullhorn',
|
|
||||||
'fa-flag', 'fa-trophy', 'fa-trophy', 'fa-trophy', 'fa-gift',
|
|
||||||
'fa-fire', 'fa-bolt', 'fa-sun', 'fa-moon', 'fa-cloud',
|
|
||||||
'fa-male', 'fa-female', 'fa-child', 'fa-id-card',
|
|
||||||
'fa-paper-plane', 'fa-paperclip', 'fa-at', 'fa-hashtag',
|
|
||||||
'fa-ellipsis-h', 'fa-ellipsis-v', 'fa-bars', 'fa-columns',
|
|
||||||
'fa-window-maximize', 'fa-window-minimize', 'fa-window-close',
|
|
||||||
'fa-expand', 'fa-compress', 'fa-arrows-alt', 'fa-crosshairs',
|
|
||||||
'fa-sign-in-alt', 'fa-sign-out-alt', 'fa-sign-in-alt',
|
|
||||||
'fa-plug', 'fa-power-off', 'fa-redo-alt', 'fa-sync-alt',
|
|
||||||
]
|
|
||||||
|
|
||||||
const REGULAR_ICONS = [
|
|
||||||
'fa-heart', 'fa-star', 'fa-bookmark', 'fa-user', 'fa-envelope',
|
|
||||||
'fa-comment', 'fa-comments', 'fa-bell', 'fa-clock', 'fa-calendar',
|
|
||||||
'fa-clipboard', 'fa-file', 'fa-folder', 'fa-image', 'fa-eye',
|
|
||||||
'fa-thumbs-up', 'fa-flag', 'fa-share-square', 'fa-hand-point-right',
|
|
||||||
'fa-life-ring', 'fa-lightbulb', 'fa-question-circle', 'fa-check-circle',
|
|
||||||
'fa-times-circle', 'fa-dot-circle', 'fa-circle', 'fa-square',
|
|
||||||
'fa-moon', 'fa-sun', 'fa-snowflake', 'fa-lemon', 'fa-gem',
|
|
||||||
'fa-copy', 'fa-edit', 'fa-trash-alt', 'fa-save', 'fa-window-maximize',
|
|
||||||
'fa-window-minimize', 'fa-window-close', 'fa-caret-square-right',
|
|
||||||
'fa-address-book', 'fa-address-card', 'fa-id-badge',
|
|
||||||
'fa-building', 'fa-chart-bar', 'fa-newspaper', 'fa-map',
|
|
||||||
]
|
|
||||||
|
|
||||||
const BRAND_ICONS = [
|
|
||||||
'fa-github', 'fa-gitlab', 'fa-bitbucket', 'fa-google', 'fa-google-plus',
|
|
||||||
'fa-facebook', 'fa-facebook-f', 'fa-twitter', 'fa-instagram',
|
|
||||||
'fa-linkedin', 'fa-linkedin-in', 'fa-youtube', 'fa-weixin', 'fa-qq',
|
|
||||||
'fa-weibo', 'fa-telegram', 'fa-telegram-plane', 'fa-discord', 'fa-slack',
|
|
||||||
'fa-whatsapp', 'fa-skype', 'fa-apple', 'fa-windows', 'fa-linux',
|
|
||||||
'fa-android', 'fa-chrome', 'fa-firefox', 'fa-edge', 'fa-safari',
|
|
||||||
'fa-opera', 'fa-html5', 'fa-css3-alt', 'fa-js', 'fa-node', 'fa-react',
|
|
||||||
'fa-vuejs', 'fa-angular', 'fa-php', 'fa-python', 'fa-java', 'fa-rust',
|
|
||||||
'fa-docker', 'fa-aws', 'fa-digital-ocean', 'fa-cloudflare', 'fa-wordpress',
|
|
||||||
'fa-joomla', 'fa-drupal', 'fa-shopify', 'fa-paypal', 'fa-stripe',
|
|
||||||
'fa-alipay', 'fa-tiktok', 'fa-snapchat', 'fa-pinterest', 'fa-reddit',
|
|
||||||
'fa-stack-overflow', 'fa-codepen', 'fa-npm', 'fa-yarn', 'fa-twitch',
|
|
||||||
'fa-spotify', 'fa-soundcloud',
|
|
||||||
]
|
|
||||||
|
|
||||||
const iconSets = { fas: SOLID_ICONS, far: REGULAR_ICONS, fab: BRAND_ICONS }
|
|
||||||
|
|
||||||
const filteredIcons = computed(() => {
|
const filteredIcons = computed(() => {
|
||||||
|
const group = activeGroup.value
|
||||||
|
if (!group || group.type === 'custom') return []
|
||||||
const q = search.value.toLowerCase().replace(/\s+/g, '')
|
const q = search.value.toLowerCase().replace(/\s+/g, '')
|
||||||
const set = iconSets[activePrefix.value] || SOLID_ICONS
|
if (!q) return group.icons
|
||||||
if (!q) return set
|
return group.icons.filter(i => i.includes(q))
|
||||||
return set.filter(i => i.includes(q))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const customPreview = computed(() => cleanSvg(customCode.value))
|
||||||
|
|
||||||
function selectIcon(icon) {
|
function selectIcon(icon) {
|
||||||
emit('update:modelValue', `fa ${activePrefix.value} ${icon}`)
|
emit('update:modelValue', composeIconValue(activeGroup.value, icon))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertCustom() {
|
||||||
|
const svg = cleanSvg(customCode.value)
|
||||||
|
if (!svg) return
|
||||||
|
if (!sessionCustom.value.includes(svg)) sessionCustom.value.push(svg)
|
||||||
|
emit('update:modelValue', `customsvg-${svg}`)
|
||||||
|
customCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.getIconCatalog()
|
||||||
|
if (res?.success && Array.isArray(res.groups) && res.groups.length) {
|
||||||
|
tabs.value = res.groups
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Keep fallback tabs; the panel stays usable once the endpoint answers.
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -207,6 +215,18 @@ function selectIcon(icon) {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aksrt-icon-picker__item :deep(svg),
|
||||||
|
.aksrt-icon-picker__preview-svg :deep(svg),
|
||||||
|
.aksrt-icon-picker__preview-svg svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-icon-picker__preview-svg {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__empty {
|
.aksrt-icon-picker__empty {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -214,4 +234,27 @@ function selectIcon(icon) {
|
|||||||
color: var(--aksrt-color-muted);
|
color: var(--aksrt-color-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aksrt-icon-picker__custom-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-icon-picker__custom-preview {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px dashed var(--aksrt-color-muted);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-icon-picker__grid--session {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--aksrt-color-muted);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const systemNoticeSchema = {
|
|||||||
},
|
},
|
||||||
{ key: 'system_notice_radius', label: '按钮圆角', type: 'switch', default: false, dependsOn: { key: 'system_notice_enabled', value: true }, help: '开启后弹窗按钮使用圆角样式' },
|
{ key: 'system_notice_radius', label: '按钮圆角', type: 'switch', default: false, dependsOn: { key: 'system_notice_enabled', value: true }, help: '开启后弹窗按钮使用圆角样式' },
|
||||||
{ key: 'system_notice_class', label: '公告样式类', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
|
{ key: 'system_notice_class', label: '公告样式类', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
|
||||||
{ key: 'system_notice_title_icon', label: '标题图标', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true }, help: '如 fa fas fa-bullhorn' },
|
{ key: 'system_notice_title_icon', label: '标题图标', type: 'icon', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
|
||||||
{ key: 'system_notice_title_class', label: '标题背景配色', type: 'select', default: '', dependsOn: { key: 'system_notice_enabled', value: true }, options: [
|
{ key: 'system_notice_title_class', label: '标题背景配色', type: 'select', default: '', dependsOn: { key: 'system_notice_enabled', value: true }, options: [
|
||||||
{ label: '无', value: '' },
|
{ label: '无', value: '' },
|
||||||
{ label: '炫彩黄', value: 'jb-yellow' },
|
{ label: '炫彩黄', value: 'jb-yellow' },
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Icon value helpers shared by admin UI.
|
||||||
|
*
|
||||||
|
* Supported stored formats:
|
||||||
|
* "fa fas fa-star" — FA5 class string
|
||||||
|
* "zibsvg-hot" — theme built-in SVG sprite key (window.aksrt_svgs)
|
||||||
|
* "customsvg-<svg>" — inline user SVG markup
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Clean pasted SVG markup: strip authoring leftovers, reject unsafe content. */
|
||||||
|
export function cleanSvg(raw) {
|
||||||
|
const text = String(raw || '').trim()
|
||||||
|
if (!text) return ''
|
||||||
|
let doc
|
||||||
|
try {
|
||||||
|
doc = new DOMParser().parseFromString(text, 'image/svg+xml')
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (doc.querySelector('parsererror')) return ''
|
||||||
|
const svg = doc.querySelector('svg')
|
||||||
|
if (!svg || svg.querySelector('script, foreignObject, animate')) return ''
|
||||||
|
;['class', 'id', 'style', 't', 'version', 'p-id'].forEach((attr) => svg.removeAttribute(attr))
|
||||||
|
Array.from(svg.attributes).forEach((attr) => {
|
||||||
|
if (attr.name.indexOf('data-') === 0) svg.removeAttribute(attr.name)
|
||||||
|
})
|
||||||
|
const keep = new XMLSerializer().serializeToString(svg)
|
||||||
|
return /<path|<use|<g\b|<circle|<rect|<polygon|<ellipse|<line/i.test(keep) ? keep : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFaValue(value) {
|
||||||
|
return /^fa[\s-]/.test(String(value || '').trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isZibsvgValue(value) {
|
||||||
|
return String(value || '').trim().startsWith('zibsvg-')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCustomSvgValue(value) {
|
||||||
|
return String(value || '').trim().startsWith('customsvg-')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sprite key of a zibsvg value, e.g. "zibsvg-hot" → "hot". */
|
||||||
|
export function zibsvgKey(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.trim()
|
||||||
|
.slice(7)
|
||||||
|
.replace(/[^a-zA-Z0-9_-]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inline markup for a zibsvg sprite preview (sprite injected by svg-icon.js). */
|
||||||
|
export function zibsvgMarkup(value) {
|
||||||
|
const key = zibsvgKey(value)
|
||||||
|
return key ? `<svg class="icon" aria-hidden="true"><use xlink:href="#icon-${key}"></use></svg>` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Value of a picker selection composed from a catalog group + icon name. */
|
||||||
|
export function composeIconValue(group, name) {
|
||||||
|
if (group.type === 'fa') return `fa ${group.id} fa-${name}`
|
||||||
|
if (group.type === 'svg') return `zibsvg-${name}`
|
||||||
|
return String(name || '')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user