feat: 离线图标+在线图标

main
NoahLan 1 year ago
parent 256884d15b
commit c117a90750

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10a10 10 0 0 0 10-10c0-5.53-4.5-10-10-10M6.76 8.82l1.06-1.06l1.06 1.06l1.06-1.06L11 8.82L9.94 9.88L11 10.94L9.94 12l-1.06-1.06L7.82 12l-1.06-1.06l1.06-1.06l-1.06-1.06m.13 8.68C7.69 15.46 9.67 14 12 14c2.33 0 4.31 1.46 5.11 3.5H6.89m10.35-6.56L16.18 12l-1.06-1.06L14.06 12L13 10.94l1.06-1.06L13 8.82l1.06-1.06l1.06 1.06l1.06-1.06l1.06 1.06l-1.06 1.06l1.06 1.06Z"/></svg>

After

Width:  |  Height:  |  Size: 517 B

@ -0,0 +1 @@
保持此文件夹,用于生成文件

@ -0,0 +1,18 @@
import { addCollection } from '@iconify/vue'
import antdesignIcons from '@iconify/json/json/ant-design.json'
import gridiconsIcons from '@iconify/json/json/gridicons.json'
import mdiIcons from '@iconify/json/json/mdi.json'
import ionIcons from '@iconify/json/json/ion.json'
import carbonIcons from '@iconify/json/json/carbon.json'
import emojioneIcons from '@iconify/json/json/emojione.json'
import CustomSvgJSON from '/build/_generated/svg.json'
addCollection(antdesignIcons)
addCollection(gridiconsIcons)
addCollection(mdiIcons)
addCollection(ionIcons)
addCollection(carbonIcons)
addCollection(emojioneIcons)
addCollection(CustomSvgJSON)

@ -0,0 +1,15 @@
import antdesignIcons from '@iconify/json/json/ant-design.json'
import gridiconsIcons from '@iconify/json/json/gridicons.json'
import mdiIcons from '@iconify/json/json/mdi.json'
import ionIcons from '@iconify/json/json/ion.json'
import carbonIcons from '@iconify/json/json/carbon.json'
import emojioneIcons from '@iconify/json/json/emojione.json'
import CustomSvgJSON from '/build/_generated/svg.json'
const collections = [antdesignIcons, gridiconsIcons, mdiIcons, ionIcons, carbonIcons, emojioneIcons, CustomSvgJSON]
const ret: string[] = []
collections.forEach((item) => {
ret.push(...Object.keys(item.icons).map(key => `${item.prefix}:${key}`))
})
export default ret

@ -0,0 +1,4 @@
{
"icons": {},
"prefix": "nl-svg"
}

@ -0,0 +1,306 @@
/**
* This is an advanced example for creating icon bundles for Iconify SVG Framework.
*
* It creates a bundle from:
* - All SVG files in a directory.
* - Custom JSON files.
* - Iconify icon sets.
* - SVG framework.
*
* This example uses Iconify Tools to import and clean up icons.
* For Iconify Tools documentation visit https://docs.iconify.design/tools/tools2/
*/
import { promises as fs } from 'node:fs'
import { dirname } from 'node:path'
import type { IconifyJSON, IconifyMetaData } from '@iconify/types'
// Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify
import {
cleanupSVG,
importDirectory,
isEmptyColor,
parseColors,
runSVGO,
} from '@iconify/tools'
import { getIcons, minifyIconSet, stringToIcon } from '@iconify/utils'
import { buildUtilsReadFile, buildUtilsWriteFile } from '../../utils'
import {
IconLog,
SvgPrefix,
iconBundlePath,
iconListPath,
iconSVGPath,
} from '../src'
/**
* Script configuration
*/
interface BundleScriptCustomSVGConfig {
// Path to SVG files
dir: string
// True if icons should be treated as monotone: colors replaced with currentColor
monotone: boolean
// Icon set prefix
prefix: string
}
interface BundleScriptCustomJSONConfig {
// Path to JSON file
filename: string
// List of icons to import. If missing, all icons will be imported
icons?: string[]
}
interface BundleScriptConfig {
// Custom SVG to import and bundle
svg?: BundleScriptCustomSVGConfig[]
// Icons to bundled from @iconify/json packages
icons?: string[]
// List of JSON files to bundled
// Entry can be a string, pointing to filename or a BundleScriptCustomJSONConfig object (see type above)
// If entry is a string or object without 'icons' property, an entire JSON file will be bundled
json?: (string | BundleScriptCustomJSONConfig)[]
}
// Iconify component (this changes import statement in generated file)
// Available options: '@iconify/react' for React, '@iconify/vue' for Vue 3, '@iconify/vue2' for Vue 2, '@iconify/svelte' for Svelte
const component = '@iconify/vue'
// Set to true to use require() instead of import
const CommonJS = false
// File to save bundle to
const target = iconBundlePath
/**
* Do stuff!
*/
export async function generateIconUsedBundle() {
const iconPools = await buildUtilsReadFile(iconListPath)
const allIconsArr = Array.from<string>(
JSON.parse(iconPools.replace('export default ', '')),
)
const sources: BundleScriptConfig = {
// svg: [
// {
// dir: 'svg',
// monotone: true,
// prefix: 'svg',
// },
// {
// dir: 'emojis',
// monotone: false,
// prefix: 'emoji',
// },
// ],
icons: allIconsArr,
// json: [
// // Custom JSON file
// 'json/gg.json',
// // Iconify JSON file (@iconify/json is a package name, /json/ is directory where files are, then filename)
// require.resolve('@iconify/json/json/tabler.json'),
// // Custom file with only few icons
// {
// filename: require.resolve('@iconify/json/json/line-md.json'),
// icons: [
// 'home-twotone-alt',
// 'github',
// 'document-list',
// 'document-code',
// 'image-twotone',
// ],
// },
// ],
}
let bundle = CommonJS
? `const { addCollection } = require('${component}');\n\n`
: `import { addCollection } from '${component}';\n\n`
// Create directory for output if missing
const dir = dirname(target)
try {
await fs.mkdir(dir, {
recursive: true,
})
}
catch (err) {
//
}
/**
* Convert sources.icons to sources.json
*/
if (sources.icons) {
const sourcesJSON = sources.json ? sources.json : (sources.json = [])
// Sort icons by prefix
const organizedList = organizeIconsList(sources.icons)
for (const prefix in organizedList) {
if (prefix !== SvgPrefix) {
const filename = require.resolve(`@iconify/json/json/${prefix}.json`)
sourcesJSON.push({
filename,
icons: organizedList[prefix],
})
}
else {
sourcesJSON.push({
filename: iconSVGPath,
icons: organizedList[prefix],
})
}
}
}
/**
* Bundle JSON files
*/
if (sources.json) {
for (let i = 0; i < sources.json.length; i++) {
const item = sources.json[i]
// Load icon set
const filename = typeof item === 'string' ? item : item.filename
let content = JSON.parse(
await buildUtilsReadFile(filename),
) as IconifyJSON
// Filter icons
if (typeof item !== 'string' && item.icons?.length) {
const filteredContent = getIcons(content, item.icons)
if (!filteredContent)
throw new Error(`Cannot find required icons in ${filename}`)
content = filteredContent
}
// Remove metadata and add to bundle
removeMetaData(content)
minifyIconSet(content)
bundle += `addCollection(${JSON.stringify(content)});\n`
IconLog('Icon Bundle', `Bundled icons from ${filename}`)
}
}
/**
* Custom SVG
*/
if (sources.svg) {
for (let i = 0; i < sources.svg.length; i++) {
const source = sources.svg[i]
// Import icons
const iconSet = await importDirectory(source.dir, {
prefix: source.prefix,
})
// Validate, clean up, fix palette and optimise
await iconSet.forEach(async (name, type) => {
if (type !== 'icon')
return
// Get SVG instance for parsing
const svg = iconSet.toSVG(name)
if (!svg) {
// Invalid icon
iconSet.remove(name)
return
}
// Clean up and optimise icons
try {
// Clean up icon code
await cleanupSVG(svg)
if (source.monotone) {
// Replace color with currentColor, add if missing
// If icon is not monotone, remove this code
await parseColors(svg, {
defaultColor: 'currentColor',
callback: (attr, colorStr, color) => {
return !color || isEmptyColor(color) ? colorStr : 'currentColor'
},
})
}
// Optimise
runSVGO(svg)
}
catch (err) {
// Invalid icon
console.error(`Error parsing ${name} from ${source.dir}:`, err)
iconSet.remove(name)
return
}
// Update icon from SVG instance
iconSet.fromSVG(name, svg)
})
console.log(`Bundled ${iconSet.count()} icons from ${source.dir}`)
// Export to JSON
const content = iconSet.export()
bundle += `addCollection(${JSON.stringify(content)});\n`
}
}
// Save to file
await buildUtilsWriteFile(target, bundle)
IconLog(
'Icon Bundle',
`Saved bundle icons at: ${target} (${Number(bundle.length / 1024).toFixed(
2,
)} KB)`,
)
}
/**
* Remove metadata from icon set
*/
function removeMetaData(iconSet: IconifyJSON) {
const props: (keyof IconifyMetaData)[] = [
'info',
'chars',
'categories',
'themes',
'prefixes',
'suffixes',
]
props.forEach((prop) => {
delete iconSet[prop]
})
}
/**
* Sort icon names by prefix
*/
function organizeIconsList(icons: string[]): Record<string, string[]> {
const sorted: Record<string, string[]> = Object.create(null)
icons.forEach((icon) => {
const item = stringToIcon(icon)
if (!item)
return
const prefix = item.prefix
const prefixList = sorted[prefix] ? sorted[prefix] : (sorted[prefix] = [])
const name = item.name
if (!prefixList.includes(name))
prefixList.push(name)
})
return sorted
}

@ -0,0 +1,50 @@
import { buildUtilsWriteFile } from '../../utils'
import {
IconBundleConfig,
SvgPrefix,
iconBundlePath,
iconListPath,
iconSVGPath,
} from '../src'
export async function generateIconDev() {
const names = IconBundleConfig.list.filter(i => i !== SvgPrefix)
const JSONName = (i: string) => `${i.replace('-', '')}Icons`
const customJSONName = 'CustomSvgJSON'
const importJSON = names.map(
i =>
`import ${JSONName(i)
} from '@iconify/json/json/${i}.json' \n`,
)
const addCollections = names.map(
i => `addCollection(${JSONName(i)}) \n`,
)
const generateListFromJSON = `const collections = [${names.map(i => JSONName(i))}, ${customJSONName}];
const ret: string[] = []
collections.forEach((item) => {
ret.push(...Object.keys(item.icons).map(key => \`\${item.prefix}:\${key}\`))
})
export default ret
`
const addImport = 'import { addCollection } from \'@iconify/vue\''
const importString = `
${importJSON.join('')}
import ${customJSONName} from '/${iconSVGPath}'\n
`
const addCollection = `${addCollections.join('')}
addCollection(${customJSONName})`
// addCollection bundle
await buildUtilsWriteFile(iconBundlePath, addImport + importString + addCollection)
// generate icon list
await buildUtilsWriteFile(iconListPath, importString + generateListFromJSON)
}

@ -0,0 +1,59 @@
import fg from 'fast-glob'
import { IconSet } from '@iconify/tools'
import type { IconifyJSON } from '@iconify/types'
import { buildUtilsReadFile, buildUtilsWriteFile } from '../../utils'
import {
IconBundleConfig,
IconLog,
SvgPrefix,
iconListPath,
iconSVGPath,
} from '../src'
export async function getIconListAllArray() {
let arr: string[] = []
const allCollectionPaths = await fg(
'node_modules/@iconify/json/json/*.json',
{},
)
await Promise.all(
IconBundleConfig.list.map(async (i) => {
const res = allCollectionPaths
.map(path => path.split('/').slice(-1)[0])
.filter(p => p === `${i}.json`)
const file = await buildUtilsReadFile(
i === SvgPrefix
? iconSVGPath
: `node_modules/@iconify/json/json/${res[0]}`,
)
const fileJSON: IconifyJSON = JSON.parse(file.toString())
const iconSet = new IconSet(fileJSON)
const iconArr = iconSet.list().map(i => `${fileJSON.prefix}:${i}`)
arr = arr.concat(iconArr)
}),
)
return arr
}
export async function generateIconListAll() {
const arr = await getIconListAllArray()
const str = JSON.stringify(arr)
await buildUtilsWriteFile(iconListPath, `export default ${str}`)
IconLog(
'Icon List',
`Generating icon list... Total number: ${arr.length}, writing into file: ${iconListPath}`,
)
}

@ -0,0 +1,41 @@
import fg from 'fast-glob'
import { buildUtilsReadFile, buildUtilsWriteFile } from '../../utils'
import { IconLog, iconListPath } from '../src/index'
import { getIconListAllArray } from './icon-list-all'
export async function generateIconListScan() {
const iconPool = await getIconListAllArray()
const files = await fg('src/**/*.{vue,ts,tsx}', { dot: true })
IconLog(
'Icon Scan',
`Scanning all 'vue, ts, tsx' files under 'src'. File number: ${files.length}`,
)
const buffers = await Promise.all(files.map(i => buildUtilsReadFile(i)))
const ret = [
...new Set(
buffers
.map(i =>
iconPool
.map(q => i.toString().match(new RegExp(q, 'gm')))
.filter(i => i),
)
.filter(i => i.length !== 0)
.flat(Number.POSITIVE_INFINITY),
),
]
await buildUtilsWriteFile(
iconListPath,
`export default ${JSON.stringify(ret)}`,
)
IconLog(
'Icon Scan',
`Detected ${ret.length} matched icons, writing into file: ${iconListPath}`,
)
}

@ -0,0 +1,23 @@
import { buildUtilsReadFile, buildUtilsWriteFile } from '../../utils'
import { IconLog, SvgPrefix, iconBundlePath, iconListPath, iconSVGPath } from '../src'
import { generateSvgJSON } from './icon-svg-json'
export async function writeSvgJSONBundle() {
const result = `import { addCollection } from '@iconify/vue'
import CustomSvgJson from '/${iconSVGPath}'\n
addCollection(CustomSvgJson);`
IconLog('Svg Bundle', `Bundle svg icons from ${iconSVGPath}`)
await buildUtilsWriteFile(iconBundlePath, result)
}
export async function rewriteSvgJSON() {
const usedIconList = await buildUtilsReadFile(iconListPath)
const arr = Array.from<string>(JSON.parse(usedIconList.replace('export default ', '')))
const usedSvgIcons: string[] = arr.filter(i => i.startsWith(SvgPrefix))
await generateSvgJSON(usedSvgIcons.map(i => i.replace(`${SvgPrefix}:`, '')))
}

@ -0,0 +1,91 @@
import {
cleanupSVG,
importDirectory,
parseColors,
runSVGO,
} from '@iconify/tools'
import { buildUtilsReadFile, buildUtilsWriteFile } from '../../utils'
import { IconLog, SvgPrefix, iconSVGPath } from '../src'
export async function generateSvgJSON(whiteList?: string[]) {
// build the empty json file
await buildUtilsWriteFile(
iconSVGPath,
JSON.stringify({ icons: {}, prefix: SvgPrefix }),
)
// Import icons
const iconSet = await importDirectory('.svg', {
prefix: SvgPrefix,
})
// Validate, clean up, fix palette and optimise
await iconSet.forEach(async (name, type) => {
if (type !== 'icon')
return
// white list limit
if (whiteList && !whiteList.includes(name))
iconSet.remove(name)
const svg = iconSet.toSVG(name)
if (!svg) {
// Invalid icon
iconSet.remove(name)
return
}
// Clean up and optimise icons
try {
// Cleanup icon code
await cleanupSVG(svg)
// Assume icon is monotone: replace color with currentColor, add if missing
// If icon is not monotone, remove this code
await parseColors(svg, {
defaultColor: 'currentColor',
// eslint-disable-next-line unused-imports/no-unused-vars
callback: (attr, colorStr, color) => {
// return !color || isEmptyColor(color) ? colorStr : 'currentColor'
return colorStr || 'currentColor'
},
})
// Optimise
runSVGO(svg)
}
catch (err) {
// Invalid icon
console.error(`Error parsing ${name}:`, err)
iconSet.remove(name)
return
}
// Update icon
iconSet.fromSVG(name, svg)
})
const data = JSON.parse(await buildUtilsReadFile(iconSVGPath))
// Generate to icon list
await iconSet.forEach((name) => {
// auto remove width and height
const body = String(iconSet.toString(name))
.replaceAll(/width="(.[0-9]*)"/gi, '')
.replaceAll(/height="(.[0-9]*)"/gi, '')
data.icons[name] = {
body,
}
})
// write into json
await buildUtilsWriteFile(iconSVGPath, JSON.stringify(data, null, 2))
IconLog(
'Icon SVG',
`Detecting ${iconSet.count()} custom svg icon, writing into file: ${iconSVGPath}`,
)
}

@ -0,0 +1,9 @@
import process from 'node:process'
import { writeIntoLog } from '../utils'
import { iconLogPath } from './src/path'
writeIntoLog(
'Icon Bundle',
`npx esno build/icon/src/core.ts ${process.argv.slice(-1)[0]}`,
iconLogPath,
)

@ -0,0 +1,17 @@
export const SvgPrefix = 'nl'
export const IconBundleConfig: IconConfig = {
online: false,
treeShake: true,
cache: false,
// see full icon in https://icon-sets.iconify.design
list: [
SvgPrefix,
'ant-design',
'gridicons',
'mdi',
'ion',
'carbon',
'emojione',
],
}

@ -0,0 +1,64 @@
import process from 'node:process'
import { generateIconUsedBundle } from '../generate/icon-bundle'
import { generateIconListAll } from '../generate/icon-list-all'
import { generateIconListScan } from '../generate/icon-list-scan'
import { generateSvgJSON } from '../generate/icon-svg-json'
import { generateIconDev } from '../generate/icon-dev'
import { rewriteSvgJSON, writeSvgJSONBundle } from '../generate/icon-svg-bundle'
import { buildUtilsWarn } from '../../utils'
import { IconBundleConfig } from './config'
(async () => {
const arg = process.argv.slice(-1)[0]
// always excute the logic to transform custom svg icons into iconify json
await generateSvgJSON()
// dev env, just bundle all collection icons
if (arg === 'dev') {
// when dev, just import json from `node_modules` and call `addCollection` api
// also generate icon list from imported jsons
await generateIconDev()
}
else {
if (IconBundleConfig.online) {
// online but not treeshake
if (!IconBundleConfig.treeShake) {
// Step 1 - Icon List should be all
await generateIconListAll()
}
// online also treeshake
if (IconBundleConfig.treeShake) {
// Step 1 - Use fast-glob to scan and write the used icon into icon list
await generateIconListScan()
// Step 2 - Used last step generated icon list to rewrite the svg json file
await rewriteSvgJSON()
}
// Add svg icons with `addCollection`
await writeSvgJSONBundle()
}
else {
// not online and not treeshake
// WARNING - highly not recommended to do so
// cause it will bundle all icons into final output which would increase the bundle size a lot
if (!IconBundleConfig.treeShake) {
// not recomended to do so
buildUtilsWarn('Not online and Not treeshake \n WARNING - It\'s highly not recommended to do so \n Cause it will bundle all icons into final output which would increase the bundle size a lot \n Normally more than 1MB')
process.exitCode = 1
}
// not online but treeshake
if (IconBundleConfig.treeShake) {
// Step 1 - Use fast-glob to scan and write the used icon into icon list
await generateIconListScan()
// Step 2 - Excute the iconify bundle logic from
// https://docs.iconify.design/icon-components/bundles/examples/component-full.html
await generateIconUsedBundle()
}
}
}
})()

@ -0,0 +1,3 @@
export * from './path'
export * from './log'
export * from './config'

@ -0,0 +1,15 @@
import { buildUtilsLog } from '../../utils'
export function IconLog(title: string, data: string) {
buildUtilsLog(
`
/**
* ==============================================
* =============== ${title} ==================
* ==============================================
* ${data}
* ==============================================
*/
`,
)
}

@ -0,0 +1,4 @@
export const iconListPath = 'build/_generated/icon-list.ts'
export const iconBundlePath = 'build/_generated/icon-bundle.ts'
export const iconSVGPath = 'build/_generated/svg.json'
export const iconLogPath = 'build/_generated/icon-build.log'

@ -0,0 +1,10 @@
export {}
declare global {
interface IconConfig {
online: boolean
treeShake: boolean
cache: boolean
list: string[]
}
}

@ -0,0 +1,17 @@
import { promisify } from 'node:util'
import { exec } from 'node:child_process'
import { buildUtilsLog } from './'
const promisifyExec = promisify(exec)
export async function buildUtilsExec(file: string) {
const { stdout, stderr } = await promisifyExec(file)
buildUtilsLog(stdout)
if (stderr) {
return false
}
return true
}

@ -0,0 +1,14 @@
import { promises as fs } from 'node:fs'
import { buildUtilsLog } from './'
const stage = 'File'
export async function buildUtilsReadFile(path: string) {
buildUtilsLog(`Reading File: ${path}`, stage)
return await fs.readFile(path, 'utf8')
}
export async function buildUtilsWriteFile(path: string, data: any) {
buildUtilsLog(`Writing data into File: ${path}`, stage)
return await fs.writeFile(path, data, 'utf8')
}

@ -0,0 +1,4 @@
export * from './log'
export * from './cp'
export * from './fs'
export * from './paths'

@ -0,0 +1,83 @@
import process from 'node:process'
import fs from 'node:fs'
import cp from 'node:child_process'
import chalk from 'chalk'
import { getNow } from 'easy-fns-ts/dist/lib'
import pkg from '../../package.json'
function title(stage: string) {
return chalk.magenta.bgBlack(
`[${pkg.name.toUpperCase()}] - [${getNow()}] - [${stage}]`,
)
}
export function buildUtilsLog(msg: string, stage = 'log') {
console.log(`${title(stage)}: ${msg}`)
}
export function buildUtilsWarn(warns: string) {
console.warn(chalk.yellow(`[${pkg.name.toUpperCase()} Warning] - [${getNow()}] \n ${warns}`))
}
export function writeIntoLog(title: string, command: string, path: string) {
const prefix = (msg: string, emoji: string) => `
/**
* ==============================================
* ==============================================
* ${title} - ${msg} - ${emoji} - ${getNow()}
* ==============================================
* ==============================================
*/
`
const log_file = fs.createWriteStream(path, {
flags: 'w',
})
const start = new Date().getTime()
cp.exec(command, (error, stdout, stderr) => {
prefix(`Excuting command: ${command}`, '⚡⚡⚡⚡⚡')
const end = new Date().getTime()
const cost = new Date(end - start).getSeconds()
buildUtilsLog(chalk.blue(`${title} done in ${cost}s`))
if (error) {
process.exitCode = 1
log_file.write(
prefix('Error', '😈😈😈😈😈') + JSON.stringify(error),
() => {
// // recover icon file if failed
// cp.execSync('npm run postbuild')
buildUtilsLog(
chalk.red.bgBlack(`${title} Failed, see more in ${path}`),
)
},
)
}
else {
buildUtilsLog(chalk.green.bgBlack(`${title} Successfully! ✨✨✨✨✨ `))
}
if (stdout) {
log_file.write(prefix('Std Out', '✨✨✨✨✨') + stdout, () => {
buildUtilsLog(
chalk.magenta.bgBlack(`${title} Std out, see more in ${path}`),
)
})
}
if (stderr) {
log_file.write(prefix('Std Err', '💊💊💊💊💊') + stderr, () => {
buildUtilsLog(
chalk.yellow.bgBlack(`${title} Std Error, see more in ${path}`),
)
})
}
})
}

@ -0,0 +1,18 @@
export const stagingBuildLogPath = 'report/stage.log'
export const typesCheckLogPath = 'report/tsc.log'
export const bundleSizeStatsLogPath = 'report/stats.html'
// paths.ts
export const generatedPathsFilePath = 'build/_generated/paths.ts'
// publish.bat
export const generatedPublishBatFileName = 'build/_generated/publish.bat'
// vscode setting file path
export const VScodeSettingsFilePath = '.vscode/settings.json'
// app setting file path
export const AppSettingsJSONFilePath = 'src/settings.json'
// app setting interface file path
export const AppSettingsInterfaceFilePath = 'types/settings.d.ts'

@ -0,0 +1,12 @@
import { IconBundleConfig } from '/build/icon/src/config'
import { disableCache, enableCache } from '@iconify/vue'
import '/build/_generated/icon-bundle.ts'
if (!IconBundleConfig.cache) {
disableCache('all')
}
else {
enableCache('local')
}
export { default } from './src/icon.vue'

@ -0,0 +1,175 @@
<script setup lang="ts" name="MyIcon">
import { Icon as Iconify, iconExists, loadIcon } from '@iconify/vue'
import type { CSSProperties } from 'vue'
import { createNamespace } from '~/utils'
defineOptions({ name: 'LIcon' })
const props = defineProps({
icon: { type: String as PropType<string> },
size: { type: [String, Number] as PropType<string | number>, default: 16 },
color: { type: String },
depth: { type: Number as PropType<1 | 2 | 3 | 4 | 5> },
spin: { type: Boolean, default: false },
transition: { type: Boolean, default: false },
})
const emit = defineEmits(['click'])
const { VITE_ICON_UNOCSS_PREFIX: UnoCSSPrefix, VITE_ICON_LOCAL_PREFIX: LocalPrefix } = import.meta.env
function getPrefixRegex(prefix: string) {
return new RegExp(`^${prefix}[:-]`)
}
// ^nl[-:]
const UnoCSSPrefixRegex = getPrefixRegex(UnoCSSPrefix)
const LocalPrefixRegex = getPrefixRegex(LocalPrefix)
enum IconType {
Unknown = 0,
UnoCSS = 1,
Local = 2,
Iconify = 3,
}
let loaded = $ref(false)
function click(e: MouseEvent) {
emit('click', e)
}
const getIconTypeRef = computed((): IconType => {
const { icon } = props
if (!icon) {
return IconType.Unknown
}
if (LocalPrefixRegex.test(icon)) {
return IconType.Local
}
if (UnoCSSPrefixRegex.test(icon)) {
return IconType.UnoCSS
}
return IconType.Iconify
})
function checkAndLoad(icon: string) {
if (unref(getIconTypeRef) === IconType.Iconify) {
const isLoaded = (loaded = iconExists(icon))
if (!isLoaded) {
loadIcon(icon)
.then(() => loaded = true)
.catch((_) => {})
}
}
}
// icon
watch(
() => props.icon,
v => checkAndLoad(v!),
{ immediate: true },
)
const getIconRef = computed((): string => {
const iconType = unref(getIconTypeRef)
switch (iconType) {
case IconType.Local:
{
let icon = props.icon || 'no-icon'
if (icon.startsWith(LocalPrefix)) {
icon = icon.slice(icon.indexOf(LocalPrefix) + LocalPrefix.length + 1)
}
return `#${LocalPrefix}-${icon}`
}
case IconType.UnoCSS:
{
let icon = props.icon!
if (icon.startsWith(UnoCSSPrefix)) {
icon = icon.slice(icon.indexOf(UnoCSSPrefix) + UnoCSSPrefix.length + 1)
}
return `${UnoCSSPrefix}-${icon}`
}
case IconType.Iconify:
return props.icon!
default:
return ''
}
})
const attrs = useAttrs()
const bindAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || '',
}))
const getStyle = computed((): CSSProperties => {
const { size } = props
let s = `${size}`
s = `${s.replace('px', '')}px`
return {
width: s,
height: s,
}
})
const getClass = computed(() => {
return {
'animate-spin animate-duration-1000': props.spin,
'transition-all ease-in-out-300': props.transition,
}
})
const { bem } = createNamespace('l-icon')
</script>
<template>
<div :class="$attrs.class || {}" @click="click($event)">
<template v-if="getIconTypeRef === IconType.Unknown">
<NSkeleton animated circle :width="size" :height="size" class="inline-block" />
</template>
<template v-else>
<template v-if="getIconTypeRef === IconType.Local">
<svg
:class="[bem(), getClass]"
:style="getStyle"
aria-hidden="true"
v-bind="bindAttrs"
>
<use :xlink:href="getIconRef" />
</svg>
</template>
<template v-if="getIconTypeRef === IconType.Iconify">
<NIcon
v-if="loaded"
:class="[bem(), getClass]"
:style="getStyle"
:color="color"
:depth="depth"
:size="size"
v-bind="bindAttrs"
>
<Iconify :icon="getIconRef" />
</NIcon>
<NSkeleton v-else animated circle :width="size" :height="size" class="inline-block" />
</template>
<template v-if="getIconTypeRef === IconType.UnoCSS">
<NIcon
:class="[bem(), getClass]"
:style="getStyle"
:color="color"
:depth="depth"
:size="size"
v-bind="bindAttrs"
>
<div :class="getIconRef" />
</NIcon>
</template>
</template>
</div>
</template>
<style lang="less" scoped>
.l-icon {
display: inline-block;
overflow: hidden;
fill: currentcolor;
vertical-align: -0.15em;
}
</style>

@ -0,0 +1,29 @@
import LIcon from '~/components/LIcon'
export function useIcon() {
interface IconRenderOption {
icon?: string
color?: string
size?: string | number
depth?: 1 | 2 | 3 | 4 | 5
spin?: boolean
transition?: boolean
}
function renderIcon(val: IconRenderOption | string) {
let opt: IconRenderOption = {}
if (val instanceof String) {
opt = { icon: val as string }
}
else {
opt = val as IconRenderOption
}
if (opt.icon) {
return h(LIcon, { ...opt })
}
return null
}
return { renderIcon }
}
Loading…
Cancel
Save