projectId 需要在项目页面点下载在http请求里获取
EGG_SESS_ICONFONT是httpOnly,无法通过js直接获取,去cookie下手动复制
fontClassPrefix是项目设置里的FontClass/Symbol前缀
fontFamily是项目设置里的Font Family
node ./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}`);
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`, '');
}
function toPascalCase(str) {
return str
.split('-')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
}
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 {
copyDirectorySync(sourcePath, targetPath);
let css = fs.readFileSync(iconfontPath, 'utf8');
css = removeDefaultFontSize(css);
fs.writeFileSync(iconfontPath, css);
const { enumContent, nameList } = generateEnumFromCSS(css);
fs.writeFileSync(iconfontEnumPath, enumContent);
createComponentFiles(nameList, componentsPath);
console.log('✅ 图标资源处理完成');
} catch (err) {
console.error('❌ 图标资源处理失败:', err.message);
process.exit(1);
}