|
6 timmar sedan | |
---|---|---|
.github | 1 år sedan | |
@ | 7 månader sedan | |
build | 1 år sedan | |
js | 1 år sedan | |
mock | 1 år sedan | |
public | 6 dagar sedan | |
src | 6 timmar sedan | |
tests | 3 år sedan | |
types | 5 månader sedan | |
.babelrc | 1 år sedan | |
.editorconfig | 3 år sedan | |
.env | 4 månader sedan | |
.env.development | 1 vecka sedan | |
.env.production | 2 månader sedan | |
.eslintignore | 3 år sedan | |
.eslintrc.js | 3 år sedan | |
.gitignore | 8 månader sedan | |
.gitpod.yml | 3 år sedan | |
.prettierignore | 3 år sedan | |
.stylelintignore | 3 år sedan | |
Dockerfile | 2 år sedan | |
LICENSE | 1 år sedan | |
README.md | 6 timmar sedan | |
commitlint.config.js | 1 år sedan | |
index.html | 9 månader sedan | |
jest.config.mjs | 3 år sedan | |
npm | 3 år sedan | |
npminstall-debug.log | 9 månader sedan | |
package-lock.json | 3 månader sedan | |
package.json | 1 vecka sedan | |
pnpm-lock.yaml | 4 månader sedan | |
postcss.config.js | 3 år sedan | |
prettier.config.js | 3 år sedan | |
stylelint.config.js | 10 månader sedan | |
tsconfig.json | 1 år sedan | |
vite.config.ts | 3 månader sedan |
系统v2.0前端代码仓库
本项目以jeecgboot为模板,请先阅读此文档后继续!
建议:安装 nvm 或其他 nodejs 版本管理器,使用 VSCode 作为 IDE,使用 Chrome/Edge 浏览器;
在项目目录下执行 pnpm install
在项目目录下执行 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二维模型动画开发,核心方法之一位置为/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();