|
4 часов назад | |
---|---|---|
.github | 1 год назад | |
@ | 7 месяцев назад | |
build | 1 год назад | |
js | 1 год назад | |
mock | 1 год назад | |
public | 1 месяц назад | |
src | 4 часов назад | |
tests | 3 лет назад | |
types | 5 месяцев назад | |
.babelrc | 1 год назад | |
.editorconfig | 3 лет назад | |
.env | 4 месяцев назад | |
.env.development | 4 дней назад | |
.env.production | 2 месяцев назад | |
.eslintignore | 3 лет назад | |
.eslintrc.js | 2 лет назад | |
.gitignore | 8 месяцев назад | |
.gitpod.yml | 3 лет назад | |
.prettierignore | 3 лет назад | |
.stylelintignore | 3 лет назад | |
Dockerfile | 2 лет назад | |
LICENSE | 1 год назад | |
README.md | 4 часов назад | |
commitlint.config.js | 1 год назад | |
index.html | 8 месяцев назад | |
jest.config.mjs | 3 лет назад | |
npm | 3 лет назад | |
npminstall-debug.log | 9 месяцев назад | |
package-lock.json | 3 месяцев назад | |
package.json | 5 дней назад | |
pnpm-lock.yaml | 4 месяцев назад | |
postcss.config.js | 3 лет назад | |
prettier.config.js | 2 лет назад | |
stylelint.config.js | 10 месяцев назад | |
tsconfig.json | 1 год назад | |
vite.config.ts | 3 месяцев назад |
系统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>
上述的生成组件的脚本如下:
/**
* 使用方式: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();