Ingen beskrivning

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

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>

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

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

  // 移除可能存在的<?xml>声明和DOCTYPE
  svgContent = svgContent.replace(/<\?xml[^>]*>\s*/g, '').replace(/<!DOCTYPE[^>]*>\s*/g, '');

  return svgContent;
}

/**
 * 收集所有SVG文件中每个元素的transform变化
 * @param {string} workspaceDir - 工作目录路径
 * @param {Array} files - SVG文件列表
 * @param {Map} elementInfoMap - 元素信息Map
 * @returns {Promise<Map>} 更新后的元素信息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<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);
    }
  });
});

// 监听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);
    }
  });
};


// 导出方法以便外部调用
defineExpose({
  animateElement,
  animateByKey
});
</script>
<style scoped>
/* 可以添加一些基础样式 */
[data-anim-id] {
  transition: transform 3s;
}
</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文件`);

    // 读取第一个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();