|
@@ -219,11 +219,6 @@ function themifyScript(
|
|
|
上述的生成组件的脚本如下:
|
|
|
|
|
|
```javascript
|
|
|
-/**
|
|
|
- * 使用方式:node index.js --keys=key1,key2
|
|
|
- *
|
|
|
- * 输出位置:当前目录下的 workspace/animated-component.vue 文件
|
|
|
- */
|
|
|
const fs = require('fs');
|
|
|
const path = require('path');
|
|
|
const { parseString } = require('xml2js');
|
|
@@ -273,88 +268,74 @@ async function parseSVG(filePath) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 递归查找包含指定id的group元素
|
|
|
+ * 递归查找所有包含指定id引用的group元素
|
|
|
* @param {Object} node - XML节点对象
|
|
|
* @param {string} id - 要查找的id
|
|
|
- * @returns {Object|null} 找到的group元素或null
|
|
|
+ * @returns {Array} 找到的group元素数组
|
|
|
*/
|
|
|
-function findGroupWithId(node, id) {
|
|
|
- // 检查当前节点是否有g元素
|
|
|
- if (node.g && Array.isArray(node.g)) {
|
|
|
- for (const group of node.g) {
|
|
|
- // 检查group的id属性是否匹配
|
|
|
- if (group.$ && group.$.id === id) {
|
|
|
- return group;
|
|
|
- }
|
|
|
-
|
|
|
- // 递归检查子group
|
|
|
- if (group.g) {
|
|
|
- const result = findGroupWithId(group, id);
|
|
|
- if (result) return group; // 返回找到的父group
|
|
|
- }
|
|
|
-
|
|
|
- // 检查use元素是否引用了目标id
|
|
|
- if (group.use && Array.isArray(group.use)) {
|
|
|
- for (const use of group.use) {
|
|
|
- if (use.$ && use.$['xlink:href'] === `#${id}`) {
|
|
|
- return group;
|
|
|
+function findGroupsWithIdRef(node, id) {
|
|
|
+ const groups = [];
|
|
|
+
|
|
|
+ function traverse(currentNode) {
|
|
|
+ if (!currentNode) return;
|
|
|
+
|
|
|
+ // 检查当前节点是否为group且有use元素引用目标id
|
|
|
+ if (currentNode.g && Array.isArray(currentNode.g)) {
|
|
|
+ for (const group of currentNode.g) {
|
|
|
+ // 检查group是否有use元素引用目标id
|
|
|
+ if (group.use && Array.isArray(group.use)) {
|
|
|
+ for (const use of group.use) {
|
|
|
+ if (use.$ && use.$['xlink:href'] === `#${id}`) {
|
|
|
+ groups.push(currentNode);
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 递归检查子元素
|
|
|
+ traverse(group);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- return null;
|
|
|
-}
|
|
|
|
|
|
-/**
|
|
|
- * 从group元素中提取transform值
|
|
|
- * @param {Object} group - group元素对象
|
|
|
- * @returns {string|null} transform值或null
|
|
|
- */
|
|
|
-function extractTransform(group) {
|
|
|
- if (!group || !group.$ || !group.$.transform) return null;
|
|
|
- return group.$.transform;
|
|
|
+ traverse(node);
|
|
|
+ return groups;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 为SVG元素添加动态类绑定
|
|
|
+ * 为SVG元素添加唯一标识和初始transform
|
|
|
* @param {Object} svgData - 解析后的SVG对象
|
|
|
- * @param {Array<string>} keys - 需要添加绑定的key数组
|
|
|
- * @returns {Object} 修改后的SVG对象
|
|
|
+ * @param {Array<string>} keys - 需要处理的key数组
|
|
|
+ * @returns {Object} 修改后的SVG对象和元素映射
|
|
|
*/
|
|
|
-function addDynamicClassBinding(svgData, keys) {
|
|
|
+function addElementIdentifiers(svgData, keys) {
|
|
|
+ const elementInfoMap = new Map();
|
|
|
+
|
|
|
keys.forEach((key) => {
|
|
|
- const group = findGroupWithId(svgData, key);
|
|
|
- if (group && group.$) {
|
|
|
- // 添加动态类绑定
|
|
|
- group.$[':class'] = `{${key}_animate:!manager.${key},${key}_animate_reverse:manager.${key}}`;
|
|
|
- }
|
|
|
- });
|
|
|
- return svgData;
|
|
|
-}
|
|
|
+ const groups = findGroupsWithIdRef(svgData, key);
|
|
|
|
|
|
-/**
|
|
|
- * 生成CSS keyframes动画
|
|
|
- * @param {string} animationName - 动画名称
|
|
|
- * @param {Array<string>} transforms - 变换矩阵数组
|
|
|
- * @returns {string} 生成的CSS代码
|
|
|
- */
|
|
|
-function generateKeyframes(animationName, transforms) {
|
|
|
- const steps = transforms.length;
|
|
|
- let css = `@keyframes ${animationName} {\n`;
|
|
|
-
|
|
|
- transforms.forEach((transform, index) => {
|
|
|
- if (transform) {
|
|
|
- // 计算当前关键帧的百分比
|
|
|
- const percentage = (index / (steps - 1)) * 100;
|
|
|
- css += ` ${percentage.toFixed(0)}% {\n`;
|
|
|
- css += ` transform: ${transform};\n`;
|
|
|
- css += ` }\n`;
|
|
|
- }
|
|
|
+ groups.forEach((group, counter) => {
|
|
|
+ const elementId = `anim_${key}_${counter++}`;
|
|
|
+
|
|
|
+ // 确保group有属性对象
|
|
|
+ if (!group.$) group.$ = {};
|
|
|
+
|
|
|
+ // 添加唯一标识
|
|
|
+ group.$['data-anim-id'] = elementId;
|
|
|
+
|
|
|
+ // 保存初始transform
|
|
|
+ const transform = group.$.transform || '';
|
|
|
+
|
|
|
+ // 存储元素信息
|
|
|
+ elementInfoMap.set(elementId, {
|
|
|
+ key,
|
|
|
+ initialTransform: transform,
|
|
|
+ transforms: [], // 将在后续步骤填充
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
- css += '}\n';
|
|
|
- return css;
|
|
|
+ return { modifiedSvg: svgData, elementInfoMap };
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -379,50 +360,127 @@ function extractSVGContent(svgObj) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 生成Vue组件文件内容
|
|
|
- * @param {string} svgContent - SVG内容字符串
|
|
|
- * @param {Object} transformsByKey - 每个key的transform数组
|
|
|
- * @param {Object} firstTransforms - 第一个SVG文件中每个key的transform
|
|
|
- * @param {Object} lastTransforms - 最后一个SVG文件中每个key的transform
|
|
|
- * @param {Array<string>} keys - key数组
|
|
|
- * @returns {string} 生成的Vue组件内容
|
|
|
+ * 收集所有SVG文件中每个元素的transform变化
|
|
|
+ * @param {string} workspaceDir - 工作目录路径
|
|
|
+ * @param {Array} files - SVG文件列表
|
|
|
+ * @param {Map} elementInfoMap - 元素信息Map
|
|
|
+ * @returns {Promise<Map>} 更新后的元素信息Map
|
|
|
*/
|
|
|
-function generateVueComponent(svgContent, transformsByKey, firstTransforms, lastTransforms, keys) {
|
|
|
- let template = `<template>\n${svgContent}\n</template>\n\n`;
|
|
|
+async function collectTransforms(workspaceDir, files, elementInfoMap) {
|
|
|
+ // 为每个元素初始化transform序列
|
|
|
+ for (const [elementId, info] of elementInfoMap) {
|
|
|
+ info.transforms = [];
|
|
|
+ }
|
|
|
|
|
|
- let script = `<script setup lang="ts">\ndefineProps<{\nmanager:Record<string, boolean>;\n}>();\n</script>\n\n`;
|
|
|
+ // 按顺序处理所有SVG文件
|
|
|
+ for (const file of files) {
|
|
|
+ const filePath = path.join(workspaceDir, file);
|
|
|
+ const svgData = await parseSVG(filePath);
|
|
|
|
|
|
- let style = `<style scoped>\n`;
|
|
|
+ // 为每个元素查找对应的transform
|
|
|
+ for (const [elementId, info] of elementInfoMap) {
|
|
|
+ const key = info.key;
|
|
|
+ const groups = findGroupsWithIdRef(svgData.svg, key);
|
|
|
|
|
|
- // 为每个key生成样式
|
|
|
- keys.forEach((key) => {
|
|
|
- const animationName = key.replace(/^___/, '').replace(/_/g, '');
|
|
|
+ // 查找具有相同相对位置的group(假设顺序一致)
|
|
|
+ const elementIndex = parseInt(elementId.split('_').pop());
|
|
|
+ if (groups[elementIndex] && groups[elementIndex].$ && groups[elementIndex].$.transform) {
|
|
|
+ info.transforms.push(groups[elementIndex].$.transform);
|
|
|
+ } else {
|
|
|
+ // 如果找不到transform,使用前一个值或初始值
|
|
|
+ const lastTransform = info.transforms.length > 0 ? info.transforms[info.transforms.length - 1] : info.initialTransform;
|
|
|
+ info.transforms.push(lastTransform);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 添加keyframes
|
|
|
- style += generateKeyframes(animationName, transformsByKey[key]);
|
|
|
+ return elementInfoMap;
|
|
|
+}
|
|
|
|
|
|
- // 添加正向动画类
|
|
|
- style += `.${key}_animate {\n`;
|
|
|
- style += `transition: transform 3s;\n`;
|
|
|
- if (lastTransforms[key]) {
|
|
|
- style += `transform: ${lastTransforms[key]};\n`;
|
|
|
+/**
|
|
|
+ * 生成Vue组件文件内容
|
|
|
+ * @param {string} svgContent - SVG内容字符串
|
|
|
+ * @param {Map} elementInfoMap - 元素信息Map
|
|
|
+ * @param {Array<string>} keys - key数组
|
|
|
+ * @returns {string} 生成的Vue组件内容
|
|
|
+ */
|
|
|
+function generateVueComponent(svgContent, elementInfoMap, keys) {
|
|
|
+ return `
|
|
|
+<template>\n${svgContent}\n</template>\n\n
|
|
|
+<script setup lang="ts">
|
|
|
+import { watch, onMounted, defineExpose, defineProps } from "vue";
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ manager: Record<string, boolean>;
|
|
|
+}>();
|
|
|
+
|
|
|
+// 存储所有动画元素(不在模板中使用,不需要ref)
|
|
|
+const animElements = new Map<string, HTMLElement>();
|
|
|
+
|
|
|
+// 元素信息(常量数据,使用Map)
|
|
|
+const elementInfo = new Map([
|
|
|
+${Array.from(elementInfoMap.entries())
|
|
|
+ .map(([key, value]) => ` ["${key}", ${JSON.stringify(value)}]`)
|
|
|
+ .join(',\n')}
|
|
|
+]);
|
|
|
+
|
|
|
+// 初始化元素引用
|
|
|
+onMounted(() => {
|
|
|
+ elementInfo.forEach((info, elementId) => {
|
|
|
+ const el = document.querySelector(\`[data-anim-id="\${elementId}"]\`);
|
|
|
+ if (el) {
|
|
|
+ animElements.set(elementId, el as HTMLElement);
|
|
|
+ // 设置初始transform
|
|
|
+ const initialTransform = info.transforms[0] || '';
|
|
|
+ el.setAttribute('transform', initialTransform);
|
|
|
}
|
|
|
- style += `/*animation: ${animationName} 3s forwards;*/\n`;
|
|
|
- style += `}\n\n`;
|
|
|
-
|
|
|
- // 添加反向动画类
|
|
|
- style += `.${key}_animate_reverse {\n`;
|
|
|
- style += `transition: transform 3s;\n`;
|
|
|
- if (firstTransforms[key]) {
|
|
|
- style += `transform: ${firstTransforms[key]};\n`;
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// 监听manager变化并执行动画
|
|
|
+watch(() => props.manager, (newManager) => {
|
|
|
+ Object.keys(newManager).forEach(key => {
|
|
|
+ const isActive = newManager[key];
|
|
|
+
|
|
|
+ // 找到所有属于这个key的元素
|
|
|
+ animateByKey(key, isActive);
|
|
|
+ });
|
|
|
+}, { deep: true });
|
|
|
+
|
|
|
+// 直接控制动画的方法
|
|
|
+const animateElement = (elementId: string, toEnd: boolean, duration: number = 3000) => {
|
|
|
+ const el = animElements.get(elementId);
|
|
|
+ const info = elementInfo.get(elementId);
|
|
|
+
|
|
|
+ if (el && info && info.transforms.length > 1) {
|
|
|
+ el.style.transition = \`transform \${duration}ms\`;
|
|
|
+ el.setAttribute('transform', toEnd ? info.transforms[info.transforms.length - 1] : info.transforms[0]);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 批量控制同一key的所有元素
|
|
|
+const animateByKey = (key: string, toEnd: boolean, duration: number = 3000) => {
|
|
|
+ animElements.forEach((el, elementId) => {
|
|
|
+ const info = elementInfo.get(elementId);
|
|
|
+ if (info && info.key === key) {
|
|
|
+ animateElement(elementId, toEnd, duration);
|
|
|
}
|
|
|
- style += `/*animation: ${animationName} 3s forwards reverse;*/\n`;
|
|
|
- style += `}\n\n`;
|
|
|
});
|
|
|
+};
|
|
|
|
|
|
- style += `</style>`;
|
|
|
|
|
|
- return template + script + style;
|
|
|
+// 导出方法以便外部调用
|
|
|
+defineExpose({
|
|
|
+ animateElement,
|
|
|
+ animateByKey
|
|
|
+});
|
|
|
+</script>
|
|
|
+<style scoped>
|
|
|
+/* 可以添加一些基础样式 */
|
|
|
+[data-anim-id] {
|
|
|
+ transition: transform 3s;
|
|
|
+}
|
|
|
+</style>`;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -457,54 +515,26 @@ async function main() {
|
|
|
|
|
|
console.log(`找到 ${files.length} 个SVG文件`);
|
|
|
|
|
|
- // 为每个key创建transform数组
|
|
|
- const transformsByKey = {};
|
|
|
- const firstTransforms = {};
|
|
|
- const lastTransforms = {};
|
|
|
-
|
|
|
- keys.forEach((key) => {
|
|
|
- transformsByKey[key] = [];
|
|
|
- });
|
|
|
-
|
|
|
- // 按顺序处理所有SVG文件
|
|
|
- for (const file of files) {
|
|
|
- const filePath = path.join(workspaceDir, file);
|
|
|
- const svgData = await parseSVG(filePath);
|
|
|
-
|
|
|
- // 为每个key查找对应的group并提取transform
|
|
|
- for (const key of keys) {
|
|
|
- const group = findGroupWithId(svgData.svg, key);
|
|
|
- const transform = extractTransform(group);
|
|
|
- transformsByKey[key].push(transform);
|
|
|
-
|
|
|
- // 如果是第一个文件,保存transform
|
|
|
- if (file === files[0]) {
|
|
|
- firstTransforms[key] = transform;
|
|
|
- }
|
|
|
-
|
|
|
- // 如果是最后一个文件,保存transform
|
|
|
- if (file === files[files.length - 1]) {
|
|
|
- lastTransforms[key] = transform;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 读取第一个SVG文件并添加动态类绑定
|
|
|
+ // 读取第一个SVG文件
|
|
|
const firstSvgPath = path.join(workspaceDir, files[0]);
|
|
|
const firstSvgData = await parseSVG(firstSvgPath);
|
|
|
|
|
|
- // 添加动态类绑定
|
|
|
- const modifiedSvgData = addDynamicClassBinding(firstSvgData.svg, keys);
|
|
|
+ // 为SVG元素添加唯一标识
|
|
|
+ const { modifiedSvg, elementInfoMap } = addElementIdentifiers(firstSvgData.svg, keys);
|
|
|
+
|
|
|
+ // 收集所有SVG文件中每个元素的transform变化
|
|
|
+ const updatedElementInfoMap = await collectTransforms(workspaceDir, files, elementInfoMap);
|
|
|
|
|
|
// 提取SVG内容(不包含XML声明和根标签)
|
|
|
- const svgContent = extractSVGContent(modifiedSvgData);
|
|
|
+ const svgContent = extractSVGContent(modifiedSvg);
|
|
|
|
|
|
// 生成Vue组件
|
|
|
- const vueComponent = generateVueComponent(svgContent, transformsByKey, firstTransforms, lastTransforms, keys);
|
|
|
+ const vueComponent = generateVueComponent(svgContent, updatedElementInfoMap, keys);
|
|
|
|
|
|
// 写入Vue组件文件
|
|
|
fs.writeFileSync(outputFile, vueComponent);
|
|
|
console.log(`Vue组件已生成: ${outputFile}`);
|
|
|
+ console.log(`共找到 ${updatedElementInfoMap.size} 个动画元素`);
|
|
|
} catch (error) {
|
|
|
console.error('错误:', error.message);
|
|
|
process.exit(1);
|