## 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 /** * 使用方式:node index.js --keys=key1,key2 * * 输出位置:当前目录下的 workspace/animated-component.vue 文件 */ 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 {Object|null} 找到的group元素或null */ 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; } } } } } 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; } /** * 为SVG元素添加动态类绑定 * @param {Object} svgData - 解析后的SVG对象 * @param {Array} keys - 需要添加绑定的key数组 * @returns {Object} 修改后的SVG对象 */ function addDynamicClassBinding(svgData, keys) { keys.forEach((key) => { const group = findGroupWithId(svgData, key); if (group && group.$) { // 添加动态类绑定 group.$[':class'] = `{${key}_animate:!manager.${key},${key}_animate_reverse:manager.${key}}`; } }); return svgData; } /** * 生成CSS keyframes动画 * @param {string} animationName - 动画名称 * @param {Array} 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`; } }); css += '}\n'; return css; } /** * 从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; } /** * 生成Vue组件文件内容 * @param {string} svgContent - SVG内容字符串 * @param {Object} transformsByKey - 每个key的transform数组 * @param {Object} firstTransforms - 第一个SVG文件中每个key的transform * @param {Object} lastTransforms - 最后一个SVG文件中每个key的transform * @param {Array} keys - key数组 * @returns {string} 生成的Vue组件内容 */ function generateVueComponent(svgContent, transformsByKey, firstTransforms, lastTransforms, keys) { let template = `\n\n`; let script = `\n\n`; let style = ``; return template + script + style; } /** * 主函数 - 协调整个流程 */ 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文件`); // 为每个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文件并添加动态类绑定 const firstSvgPath = path.join(workspaceDir, files[0]); const firstSvgData = await parseSVG(firstSvgPath); // 添加动态类绑定 const modifiedSvgData = addDynamicClassBinding(firstSvgData.svg, keys); // 提取SVG内容(不包含XML声明和根标签) const svgContent = extractSVGContent(modifiedSvgData); // 生成Vue组件 const vueComponent = generateVueComponent(svgContent, transformsByKey, firstTransforms, lastTransforms, keys); // 写入Vue组件文件 fs.writeFileSync(outputFile, vueComponent); console.log(`Vue组件已生成: ${outputFile}`); } catch (error) { console.error('错误:', error.message); process.exit(1); } } // 执行主函数 main(); ```