## VentAnaly_2.0_front 系统v2.0前端代码仓库 ### 前言 本项目以[jeecgboot](https://github.com/jeecgboot/jeecgboot-vue3)为模板,请先阅读此文档后继续! ### 开始 建议:安装 nvm 或其他 nodejs 版本管理器,使用 VSCode 作为 IDE,使用 Chrome/Edge 浏览器; 1. 在项目目录下执行 `pnpm install` 2. 在项目目录下执行 `git config commit.template .github/COMMIT_TEMPLATE` ### 开发 开发的基本流程,部分内容可忽略 `git pull` # 可以rebase `git checkout [branch]` # 可选 `nvm use 20` # 建议,高版本node自带pnpm包管理器 `pnpm dev` # 必选,dddd `git add .` `git commit` # 公用模板见.github/COMMIT_TEMPLATE `git push origin [branch]` # 目前master分支无保护,可直接推 ### 构建 `pnpm build` ### 主题及主题化 主题可以在 /views/vent/sys/setting/index.vue 中找到设置入口 常规的颜色、变量在 /design/color.less 或 /design/themify/ 下添加,图片资源应在 /assets/images/themify/ 下添加 一般来说,主题化开发可以参考以下代码 ```less // import theme variables @import '/@/design/theme.less'; // scope the selector @{theme-deepblue} { .your-component-root-class-name { --image-a: url('/@/assets/images/themify/deepblue/a.png'); --bg-a: #ffffff; } } // scope the selector @{theme-dark} { .your-component-root-class-name { --image-a: url('/@/assets/images/themify/dark/a.png'); --bg-a: #000000; } } // default css .your-component-root-class-name { --image-a: url('/@/assets/images/a.png'); --bg-a: #888888; .class-name-a { background-image: var(--image-a); background-color: var(--bg-a); } } ``` 更加详细的页面主题化标准模板可以参考登录页 /views/sys/login/Login.vue 如果已经写了css代码,想要支持主题化,则下面是配合主题化使用的工具,用于将已有样式代码输出为主题化代码。输入页面的 css 样式,即可输出标准模板所需的资源(返回的数据第一项为变量集合,第二项为css代码文本) ```javascript // Take note of requires below before using this function: // 1. replace all url patterns to patterns like `url(/@/xxx/xxx.xxx)`. // 2. remove all the in-line comments(//) or replace them to block comments(/** */). // 3. replace all rbg/rgba/hsa or any other css color functions to hex colors(#xxxxxx). function themifyScript( str, options = { color: false, gradient: false, url: true, } ) { const keySet = new Set(); let strcopy = str; let varstr = ''; // process url, extract all css url and replace them to css var and record them. { keySet.clear(); const regexp = /url\('?(\/[@|0-9|a-z|A-Z|\-|_]+)+.(png|svg)'?\)/g; let res = regexp.exec(str); while (res) { const [url, image] = res; const varname = `--image-${image.replace('/', '')}`; if (!keySet.has(image)) { keySet.add(image); varstr += `${varname}: ${url};`; } if (options.url) { strcopy = strcopy.replace(url, `var(${varname})`); } res = regexp.exec(str); } } // process gradient, extract all css gradient and replace them to css var and record them. { keySet.clear(); let key = 0; const regexp = /linear-gradient\([0-9|a-z|A-Z|#|,|\s|%|.]+\)/g; let res = regexp.exec(str); while (res) { const [gradient] = res; const varname = `--gradient-${key}`; if (!keySet.has(gradient)) { keySet.add(gradient); varstr += `${varname}: ${gradient};`; } if (options.gradient) { strcopy = strcopy.replace(gradient, `var(${varname})`); } res = regexp.exec(str); key += 1; } } { keySet.clear(); let key = 0; const regexp = /radial-gradient\([0-9|a-z|A-Z|#|,|\s|%|.]+\)/g; let res = regexp.exec(str); while (res) { const [gradient] = res; const varname = `--gradient-${key}`; if (!keySet.has(gradient)) { keySet.add(gradient); varstr += `${varname}: ${gradient};`; } if (options.gradient) { strcopy = strcopy.replace(gradient, `var(${varname})`); } res = regexp.exec(str); key += 1; } } // process color, extract all css colors and replace them to css var and record them. { keySet.clear(); let key = 0; const regexp = /#[0-9|a-z|A-Z]{3,8}/g; let res = regexp.exec(str); while (res) { const [color] = res; const varname = `--color-${key}`; if (!keySet.has(color)) { keySet.add(color); varstr += `${varname}: ${color};`; } if (options.color) { strcopy = strcopy.replace(color, `var(${varname})`); } res = regexp.exec(str); key += 1; } } return [varstr, strcopy]; } ``` ### SVG模型动画开发 关于svg二维模型动画开发,核心方法之一位置为/mky-vent-base/src/hooks/vent/useSvgAnimation.ts,说明如下: 1、设计团队提供svg图组,通常是一组svg图片,每张图片对应一个动画帧 2、将图组放入指定脚本的工作区内生成可用的vue组件,脚本可见项目的README文档,然后自行新建文件、复制代码、安装依赖并运行 3、将可用的vue组件引入到需要使用动画的页面中,并使用useSvgAnimation钩子进行动画控制,例如`` 4、通过浏览器的元素检查功能,找到svg对应的group,复制group的id,并使用该id进行动画控制 使用示例: ```vue ``` 上述的生成组件的脚本如下: ```javascript const fs = require('fs'); const path = require('path'); const { parseString } = require('xml2js'); const { Builder } = require('xml2js'); /** * 解析命令行参数 * 支持 --keys=key1,key2 格式的参数 * @returns {Object} 包含解析后参数的对象 */ function parseArgs() { const args = process.argv.slice(2); const params = {}; args.forEach((arg) => { if (arg.startsWith('--keys=')) { // 分割并处理keys参数 params.keys = arg.split('=')[1].split(','); } }); return params; } /** * 读取并解析SVG文件 * @param {string} filePath - SVG文件路径 * @returns {Promise} 解析后的SVG对象 */ async function parseSVG(filePath) { return new Promise((resolve, reject) => { fs.readFile(filePath, 'utf8', (err, data) => { if (err) { reject(new Error(`读取文件失败: ${filePath}, 错误: ${err.message}`)); return; } parseString(data, (err, result) => { if (err) { reject(new Error(`解析SVG失败: ${filePath}, 错误: ${err.message}`)); return; } resolve(result); }); }); }); } /** * 递归查找所有包含指定id引用的group元素 * @param {Object} node - XML节点对象 * @param {string} id - 要查找的id * @returns {Array} 找到的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); } } } traverse(node); return groups; } /** * 为SVG元素添加唯一标识和初始transform * @param {Object} svgData - 解析后的SVG对象 * @param {Array} keys - 需要处理的key数组 * @returns {Object} 修改后的SVG对象和元素映射 */ function addElementIdentifiers(svgData, keys) { const elementInfoMap = new Map(); keys.forEach((key) => { const groups = findGroupsWithIdRef(svgData, key); 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: [], // 将在后续步骤填充 }); }); }); return { modifiedSvg: svgData, elementInfoMap }; } /** * 从SVG对象中提取SVG内容(去除XML声明和根标签) * @param {Object} svgObj - SVG对象 * @returns {string} SVG内容字符串 */ function extractSVGContent(svgObj) { const builder = new Builder({ headless: true, // 不包含XML声明 }); // 构建SVG内容,但不包含根标签 let svgContent = builder.buildObject({ svg: svgObj, }); // 移除可能存在的声明和DOCTYPE svgContent = svgContent.replace(/<\?xml[^>]*>\s*/g, '').replace(/]*>\s*/g, ''); return svgContent; } /** * 收集所有SVG文件中每个元素的transform变化 * @param {string} workspaceDir - 工作目录路径 * @param {Array} files - SVG文件列表 * @param {Map} elementInfoMap - 元素信息Map * @returns {Promise} 更新后的元素信息Map */ async function collectTransforms(workspaceDir, files, elementInfoMap) { // 为每个元素初始化transform序列 for (const [elementId, info] of elementInfoMap) { info.transforms = []; } // 按顺序处理所有SVG文件 for (const file of files) { const filePath = path.join(workspaceDir, file); const svgData = await parseSVG(filePath); // 为每个元素查找对应的transform for (const [elementId, info] of elementInfoMap) { const key = info.key; const groups = findGroupsWithIdRef(svgData.svg, key); // 查找具有相同相对位置的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); } } } return elementInfoMap; } /** * 生成Vue组件文件内容 * @param {string} svgContent - SVG内容字符串 * @param {Map} elementInfoMap - 元素信息Map * @param {Array} keys - key数组 * @returns {string} 生成的Vue组件内容 */ function generateVueComponent(svgContent, elementInfoMap, keys) { return ` \n\n `; } /** * 主函数 - 协调整个流程 */ async function main() { try { // 解析命令行参数 const { keys } = parseArgs(); if (!keys || keys.length === 0) { throw new Error('请提供keys参数,例如: --keys=key1,key2'); } const workspaceDir = path.join(process.cwd(), 'workspace'); const outputFile = path.join(workspaceDir, 'animated-component.vue'); // 检查workspace目录是否存在 if (!fs.existsSync(workspaceDir)) { throw new Error('workspace目录不存在'); } // 读取并过滤SVG文件 const files = fs .readdirSync(workspaceDir) .filter((file) => file.endsWith('.svg')) .sort(); // 按字母顺序排序以确保正确的动画顺序 if (files.length === 0) { throw new Error('workspace目录下没有找到SVG文件'); } console.log(`找到 ${files.length} 个SVG文件`); // 读取第一个SVG文件 const firstSvgPath = path.join(workspaceDir, files[0]); const firstSvgData = await parseSVG(firstSvgPath); // 为SVG元素添加唯一标识 const { modifiedSvg, elementInfoMap } = addElementIdentifiers(firstSvgData.svg, keys); // 收集所有SVG文件中每个元素的transform变化 const updatedElementInfoMap = await collectTransforms(workspaceDir, files, elementInfoMap); // 提取SVG内容(不包含XML声明和根标签) const svgContent = extractSVGContent(modifiedSvg); // 生成Vue组件 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); } } // 执行主函数 main(); ```