Нет описания

houzekong 12d94b8d9f [Doc 0000] 文档更新关于SVG动画的开发文档 4 часов назад
.github 0fc124ffda [Docs 0000] 更新README文档内容,添加提交模板 1 год назад
@ 4c310f07d7 1. 新增风窗瓦斯超限模拟 7 месяцев назад
build ac21340696 [Feat 0000] 为生产环境添加一些辨别版本的标记 1 год назад
js 2306d250f3 风机详情、安全监控 1 год назад
mock 6cc0a7207b [Feat 0000] 登陆过期后为自动登录用户添加续登录功能 1 год назад
public 406dd5e624 更新 1 месяц назад
src c859fd02da [Feat 0000] 风门SVG动画开发 4 часов назад
tests a2d39b4363 jeecgboot-vue 1.0.0 版本发布 3 лет назад
types 0b8baf20c4 [Mod 0000] 历史数据组件是否支持多选将由配置项决定 5 месяцев назад
.babelrc fecbaf166a 更新 1 год назад
.editorconfig 5a8812318e jeecgboot vue3 ui 3 лет назад
.env de3a4f910c 添加文件 4 месяцев назад
.env.development b94d515fde [Feat 0000]测风装置规程值设置功能 4 дней назад
.env.production 29bdccb57f 宝德瓦斯监测预警更新 2 месяцев назад
.eslintignore 5a8812318e jeecgboot vue3 ui 3 лет назад
.eslintrc.js e5f18c16a9 添加风门、调整echarts 2 лет назад
.gitignore f050171542 [Wip 0000] 瓦斯监测-添加测点监测/预测曲线切换功能 8 месяцев назад
.gitpod.yml 5a8812318e jeecgboot vue3 ui 3 лет назад
.prettierignore 5a8812318e jeecgboot vue3 ui 3 лет назад
.stylelintignore 5a8812318e jeecgboot vue3 ui 3 лет назад
Dockerfile 699c084b9f 提交文件 2 лет назад
LICENSE 3bff589e4c 项目升级 1 год назад
README.md 12d94b8d9f [Doc 0000] 文档更新关于SVG动画的开发文档 4 часов назад
commitlint.config.js 0fc124ffda [Docs 0000] 更新README文档内容,添加提交模板 1 год назад
index.html d838802449 [Wip 0000] 可配置首页主题化 8 месяцев назад
jest.config.mjs 5a8812318e jeecgboot vue3 ui 3 лет назад
npm 5a8812318e jeecgboot vue3 ui 3 лет назад
npminstall-debug.log e05880d0d8 预警管控-设备监测预警报警数量颜色修改-提交 9 месяцев назад
package-lock.json 5cfe0af95e [Feat 0000]主风机新增预警分析模块 3 месяцев назад
package.json da4f8b01fe [Feat 0000] 添加项目版本号标识 5 дней назад
pnpm-lock.yaml 8fd4f5892c 提交依赖安装文件 4 месяцев назад
postcss.config.js 5a8812318e jeecgboot vue3 ui 3 лет назад
prettier.config.js 5f5207b4aa 代码格式化格式调整 2 лет назад
stylelint.config.js 6b95ecd99b 1。 亚美大宁局部风机设备监测接口调整 10 месяцев назад
tsconfig.json 3bff589e4c 项目升级 1 год назад
vite.config.ts d6212f2e9e [Feat 0000] 新增均压二维巷道模型 3 месяцев назад

README.md

VentAnaly_2.0_front

系统v2.0前端代码仓库

前言

本项目以jeecgboot为模板,请先阅读此文档后继续!

开始

建议:安装 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/ 下添加

一般来说,主题化开发可以参考以下代码

// 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代码文本)

// 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钩子进行动画控制,例如<SVGAnimation :manager="animationManager" />

4、通过浏览器的元素检查功能,找到svg对应的group,复制group的id,并使用该id进行动画控制

使用示例:

<template>
  <SVGAni :manager="animationManager" />
</template>
<script setup>
  import SVGAni from 'path/to/SVGAnimation.vue';
  import useSvgAnimation from 'path/to/useSvgAnimation.ts';

  const { animationManager,triggerAnimation } = useSvgAnimation();

  onMounted(() => {
    // 根据情况触发动画
    if (condition) {
      triggerAnimation('id', false);
      } else {
      triggerAnimation('id', true);
      }
  })
</script>

上述的生成组件的脚本如下:

/**
 * 使用方式: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<Object>} 解析后的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<string>} 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<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`;
    }
  });

  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,
  });

  // 移除可能存在的<?xml>声明和DOCTYPE
  svgContent = svgContent.replace(/<\?xml[^>]*>\s*/g, '').replace(/<!DOCTYPE[^>]*>\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<string>} keys - key数组
 * @returns {string} 生成的Vue组件内容
 */
function generateVueComponent(svgContent, transformsByKey, firstTransforms, lastTransforms, keys) {
  let template = `<template>\n${svgContent}\n</template>\n\n`;

  let script = `<script setup lang="ts">\ndefineProps<{\nmanager:Record<string, boolean>;\n}>();\n</script>\n\n`;

  let style = `<style scoped>\n`;

  // 为每个key生成样式
  keys.forEach((key) => {
    const animationName = key.replace(/^___/, '').replace(/_/g, '');

    // 添加keyframes
    style += generateKeyframes(animationName, transformsByKey[key]);

    // 添加正向动画类
    style += `.${key}_animate {\n`;
    style += `transition: transform 3s;\n`;
    if (lastTransforms[key]) {
      style += `transform: ${lastTransforms[key]};\n`;
    }
    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`;
    }
    style += `/*animation: ${animationName} 3s forwards reverse;*/\n`;
    style += `}\n\n`;
  });

  style += `</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();