iconfont 组件封装

26 阅读2分钟

projectId 需要在项目页面点下载在http请求里获取

EGG_SESS_ICONFONThttpOnly,无法通过js直接获取,去cookie下手动复制

fontClassPrefix是项目设置里的FontClass/Symbol前缀

fontFamily是项目设置里的Font Family

node ./scripts/iconfont.js

# scripts/iconfont.js
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';

import dotenv from 'dotenv';

dotenv.config({
  path: '.env.local',
});

const EGG_SESS_ICONFONT = process.env['EGG_SESS_ICONFONT'];

const projectId = 10000000;
const fontClassPrefix = 'iconfont-';
const fontFamily = 'iconfont';

const dirname = process.cwd();

// 工具函数:路径拼接
const resolvePath = (...paths) => path.join(dirname, ...paths);


// 路径定义
const sourcePath = resolvePath('scripts/iconfont');
const targetPath = resolvePath('src/assets/font/iconfont');
const iconfontPath = resolvePath('src/assets/font/iconfont/iconfont.css');
const iconfontEnumPath = resolvePath('src/components/Iconfont/constants.ts');
const vueDir = resolvePath('src/components/Iconfont');
const vuePath = path.join(vueDir, 'Iconfont.vue');
const vueTypePath = path.join(vueDir, 'type.ts');
const componentsPath = path.join(vueDir, 'components');
const vueIndexPath = path.join(vueDir, 'index.ts');

// 创建下载目录
fs.mkdirSync(sourcePath, { recursive: true });
fs.mkdirSync(targetPath, { recursive: true });
fs.mkdirSync(componentsPath, { recursive: true });

// 下载文件
console.log(`开始下载`);

execSync(
  `curl -# -o ${path.join(
    sourcePath,
    'download.zip',
  )} 'https://www.iconfont.cn/api/project/download.zip?pid=${projectId}' -b 'EGG_SESS_ICONFONT=${EGG_SESS_ICONFONT}'`,
  { stdio: 'inherit' },
);
console.log(`下载完成`);

const zipFilePath = path.join(sourcePath, 'download.zip');

console.log(`开始解压文件: ${zipFilePath}`);
execSync(`unzip -o '${zipFilePath}' -d '${sourcePath}'`, { stdio: 'inherit' });
console.log(`文件已成功解压到: ${sourcePath}`);
const entries = fs.readdirSync(sourcePath, { withFileTypes: true });
const subDirObj = entries.find((entry) => entry.isDirectory());
if (!subDirObj) {
  throw new Error('未找到解压后的子目录');
}
const subDir = path.join(sourcePath, subDirObj.name);
console.log(`找到子目录: ${subDir}`);
// 移动所有内容到上级目录
const items = fs.readdirSync(subDir, { withFileTypes: true });

for (const item of items) {
  const aPath = path.join(subDir, item.name);
  const bPath = path.join(sourcePath, item.name);

  // 移动文件或目录(覆盖已存在的文件)
  fs.renameSync(aPath, bPath);
  console.log(`已移动: ${item.name}`);
}

// 删除空的源目录
fs.rmdirSync(subDir);
console.log(`已删除空目录: ${subDir}`);

// 删除ZIP文件
fs.unlinkSync(zipFilePath);
console.log(`已删除ZIP文件: ${zipFilePath}`);

// 复制图标资源到目标目录
function copyDirectorySync(src, dest) {
  if (!fs.existsSync(dest)) {
    fs.mkdirSync(dest, { recursive: true });
  }
  const files = fs.readdirSync(src);
  files.forEach((file) => {
    const srcFile = path.join(src, file);
    const destFile = path.join(dest, file);
    fs.copyFileSync(srcFile, destFile);
  });
}

// 移除默认字体大小
function removeDefaultFontSize(css) {
  return css.replace(`  font-size: 16px;\n`, '');
}

// 类名转 PascalCase
function toPascalCase(str) {
  return str
    .split('-')
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join('');
}

// 根据 CSS 类名生成枚举内容
function generateEnumFromCSS(css) {
  const reg = new RegExp(`${fontClassPrefix}[a-zA-Z0-9-]+`, 'g');
  const classNames = css.match(reg) || [];
  const nameList = [];

  console.log('classNames\n', classNames);
  const enumStr = classNames
    .map((className) => {
      const reg = new RegExp(`^${fontClassPrefix}`);
      const name = className.replace(reg, '');
      const key = toPascalCase(name);
      nameList.push(key);
      return `  ${key} = '${className}'`;
    })
    .join(',\n');
  console.log('enumStr\n', enumStr);
  return {
    enumContent: `export enum IconfontEnum {\n${enumStr},\n}\n`,
    nameList,
  };
}

// 创建组件文件
function createComponentFiles(nameList, componentsPath) {
  if (fs.existsSync(componentsPath)) {
    fs.rmSync(componentsPath, { recursive: true });
  }
  fs.mkdirSync(componentsPath);

  fs.writeFileSync(
    vueTypePath,
    `import { IconfontEnum } from './constants';

export interface IconfontProps {
  name: IconfontEnum;
  size?: string;
  width?: string;
  height?: string;
}

export interface IconfontComProps {
  size?: string;
  width?: string;
  height?: string;
}
`,
  );

  fs.writeFileSync(
    vuePath,
    `<script lang="ts" setup>
  import { computed } from 'vue';

  import type { IconfontProps } from './type';

  const props = withDefaults(defineProps<IconfontProps>(), {});
  const getClass = computed(() => {
    return \`${fontFamily} \${props.name}\`;
  });
  const customStyle = computed(() => {
    return {
      fontSize: props.size,
      width: props.width || props.size,
      height: props.height || props.size,
      lineHeight: props.height || props.size,
    };
  });
</script>

<template>
  <span :class="getClass" :style="customStyle"></span>
</template>

<style lang="less" scoped></style>
`,
  );

  const indexTsList = [`export { default as Iconfont } from './Iconfont.vue';`];

  for (const name of nameList) {
    const comName = `${name}Icon`;
    const comPath = path.join(componentsPath, `${comName}.vue`);
    const content = `<script lang="ts" setup>
  import { IconfontEnum } from '../constants';
  import Iconfont from '../Iconfont.vue';
  import type { IconfontComProps } from '../type';

  const props = defineProps<IconfontComProps>();
</script>

<template>
  <Iconfont :name="IconfontEnum.${name}" :size="size" :width="width" :height="height" />
</template>
`;

    fs.writeFileSync(comPath, content);
    indexTsList.push(`export { default as ${comName} } from './components/${comName}.vue';`);
  }

  indexTsList.push('');
  fs.writeFileSync(vueIndexPath, indexTsList.join('\n'));
}

// 主流程
try {
  // 1. 复制图标资源
  copyDirectorySync(sourcePath, targetPath);

  // 2. 处理 CSS 文件
  let css = fs.readFileSync(iconfontPath, 'utf8');
  css = removeDefaultFontSize(css);
  fs.writeFileSync(iconfontPath, css);

  // 3. 生成枚举文件
  const { enumContent, nameList } = generateEnumFromCSS(css);
  fs.writeFileSync(iconfontEnumPath, enumContent);

  // 4. 创建组件文件
  createComponentFiles(nameList, componentsPath);

  console.log('✅ 图标资源处理完成');
} catch (err) {
  console.error('❌ 图标资源处理失败:', err.message);
  process.exit(1);
}