├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── .DS_Store ├── favicon.ico ├── index.html └── static │ ├── 12c3946fa8c445e8a4f6de664893ae22.png │ ├── 3adc468aa17543e0902330f1a74c856a.92e9cd1b464643c7957426b8e2735ef8.jpg │ ├── 5690278ce1b44cdaab95a90684ec8803.png │ ├── 6acfc5f2a8f04981a8b785fde5b2a87c.png │ ├── 75abc05c659f4d5aa047b1c4e9bdd81a.png │ ├── 9206b8e038074cc9b6ff54330348cc01.png │ ├── c365ab6dbe274c1587df1ce13b1bbe29.png │ ├── f769bc0e1115441f99dc3468fe4e1db7.png │ └── new-pic.png ├── scripts ├── common.js ├── generateComponent │ ├── index.js │ └── template.js ├── generateStore │ ├── index.js │ └── template.js └── generateView │ ├── index.js │ └── template.js ├── src ├── App.vue ├── api │ ├── API.js │ └── index.js ├── assets │ ├── .DS_Store │ ├── images │ │ ├── lock-icon.svg │ │ ├── lock-open-icon.svg │ │ └── rotate-icon.svg │ ├── logo.png │ └── styles │ │ ├── base.css │ │ └── common.css ├── components │ ├── .DS_Store │ ├── MicroEdit │ │ ├── AddMaterials.vue │ │ ├── BannerEdit.vue │ │ ├── BottomOperate.vue │ │ ├── MicroLayer.vue │ │ ├── ParametersEdit.vue │ │ ├── TopOperate.vue │ │ ├── bannerEditCommon.js │ │ ├── fabricOperate.js │ │ ├── guideLines.js │ │ └── index.js │ └── index.js ├── main.js ├── router │ └── index.js ├── store │ ├── index.js │ └── mutationTypes.js ├── utils │ └── index.js └── views │ └── Home │ ├── Home.vue │ └── index.js └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | README.md 2 | src/.DS_Store 3 | /node_modules 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # onLinePS 2 | js基于canvas实现的一个类似ps的工具

3 | 本系统是基于fabric.js实现的canvas版图片,文本编辑器,支持对图片的放大,缩小,旋转,镜面翻转,拖动,显示/隐藏图层,删除图层,替换图层等操作,对文本支持修改文本内容,颜色,字体,加粗,斜体,下划线,背景色等,同时支持图片已有的操作,拖动图层有辅助线功能,可对画布做放大缩小功能,多操作可撤销/回退功能,可直接导出图片,ps基本操作都已支持。 4 | 5 | 这个是项目中做的功能,由于过程比较坎坷,fabric.js库很强大,但是中文资料很少,导致解决问题的时候花费了很多功夫,所以摘出来供需要的人使用,由于有些东西需要后端配合,所以把一部分功能砍掉了,所有代码中有一些冗余代码,大家有需要那些功能的也可以宅后台配合把这些功能也做起来,前端的逻辑都已经实现了. 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ], 5 | "env":{ 6 | "development": { 7 | "plugins": ["dynamic-import-node"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mydemo1", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "new:view": "node ./scripts/generateView/index", 10 | "new:comp": "node ./scripts/generateComponent/index", 11 | "new:store": "node ./scripts/generateStore/index" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.21.2", 15 | "core-js": "^2.6.5", 16 | "element-ui": "^2.6.1", 17 | "fabric": "^4.3.0", 18 | "fontfaceobserver": "^2.1.0", 19 | "glfx.js__temp": "^1.0.2", 20 | "vue": "^2.5.2", 21 | "vue-router": "^3.0.1", 22 | "vuex": "^3.0.1" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-babel": "^3.12.1", 26 | "@vue/cli-plugin-eslint": "^3.5.0", 27 | "@vue/cli-service": "^5.0.8", 28 | "babel-eslint": "^10.0.1", 29 | "chalk": "^2.0.1", 30 | "compression-webpack-plugin": "^2.0.0", 31 | "css-loader": "^3.1.0", 32 | "eslint": "^5.16.0", 33 | "eslint-plugin-vue": "^5.0.0", 34 | "less": "^3.9.0", 35 | "less-loader": "^5.0.0", 36 | "style-loader": "^0.23.1", 37 | "uglifyjs-webpack-plugin": "^2.1.3", 38 | "vue-template-compiler": "^2.5.21" 39 | }, 40 | "eslintConfig": { 41 | "root": true, 42 | "env": { 43 | "node": true 44 | }, 45 | "extends": [ 46 | "plugin:vue/essential", 47 | "eslint:recommended" 48 | ], 49 | "rules": {}, 50 | "parserOptions": { 51 | "parser": "babel-eslint" 52 | } 53 | }, 54 | "postcss": { 55 | "plugins": { 56 | "autoprefixer": {} 57 | } 58 | }, 59 | "browserslist": [ 60 | "> 1%", 61 | "last 2 versions" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/.DS_Store -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | onlineps 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/static/12c3946fa8c445e8a4f6de664893ae22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/static/12c3946fa8c445e8a4f6de664893ae22.png -------------------------------------------------------------------------------- /public/static/3adc468aa17543e0902330f1a74c856a.92e9cd1b464643c7957426b8e2735ef8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/static/3adc468aa17543e0902330f1a74c856a.92e9cd1b464643c7957426b8e2735ef8.jpg -------------------------------------------------------------------------------- /public/static/5690278ce1b44cdaab95a90684ec8803.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/static/5690278ce1b44cdaab95a90684ec8803.png -------------------------------------------------------------------------------- /public/static/6acfc5f2a8f04981a8b785fde5b2a87c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/static/6acfc5f2a8f04981a8b785fde5b2a87c.png -------------------------------------------------------------------------------- /public/static/75abc05c659f4d5aa047b1c4e9bdd81a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/static/75abc05c659f4d5aa047b1c4e9bdd81a.png -------------------------------------------------------------------------------- /public/static/9206b8e038074cc9b6ff54330348cc01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/static/9206b8e038074cc9b6ff54330348cc01.png -------------------------------------------------------------------------------- /public/static/c365ab6dbe274c1587df1ce13b1bbe29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/static/c365ab6dbe274c1587df1ce13b1bbe29.png -------------------------------------------------------------------------------- /public/static/f769bc0e1115441f99dc3468fe4e1db7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/static/f769bc0e1115441f99dc3468fe4e1db7.png -------------------------------------------------------------------------------- /public/static/new-pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/public/static/new-pic.png -------------------------------------------------------------------------------- /scripts/common.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const resolvePath = (...file) => path.resolve(__dirname, ...file) 5 | const log = message => console.log(chalk.green(`${message}`)) 6 | const successLog = message => console.log(chalk.blue(`${message}`)) 7 | const errorLog = error => console.log(chalk.red(`${error}`)) 8 | const resolveComponentName = componentName => { 9 | componentName = componentName.toLowerCase(); 10 | //截取单词,按大驼峰命名拼接 11 | let componentNameStr = componentName.split("|"); 12 | if (componentNameStr.length > 1) { 13 | componentNameStr = componentNameStr.map((item) => { 14 | if (item.trim()) { 15 | return item.substring(0, 1).toUpperCase() + item.substring(1); 16 | } 17 | return ''; 18 | }); 19 | return componentNameStr.join(''); 20 | } else { 21 | errorLog('格式错误!名称各单词之间用|隔开,单个单词如home|,多个单词如home|index') 22 | return ''; 23 | } 24 | } 25 | 26 | // 递归创建目录 27 | const mkdirs=(directory, callback)=> { 28 | var exists = fs.existsSync(directory) 29 | if (exists) { 30 | callback() 31 | } else { 32 | mkdirs(path.dirname(directory), function () { 33 | fs.mkdirSync(directory) 34 | callback() 35 | }) 36 | } 37 | } 38 | 39 | module.exports ={ 40 | dotExistDirectoryCreate : (directory) => { 41 | return new Promise((resolve) => { 42 | mkdirs(directory, function () { 43 | resolve(true) 44 | }) 45 | }) 46 | }, 47 | generateFile : (path, data) => { 48 | if (fs.existsSync(path)) { 49 | errorLog(`${path}文件已存在`) 50 | return 51 | } 52 | return new Promise((resolve, reject) => { 53 | fs.writeFile(path, data, 'utf8', err => { 54 | if (err) { 55 | errorLog(err.message) 56 | reject(err) 57 | } else { 58 | resolve(true) 59 | } 60 | }) 61 | }) 62 | }, 63 | path: path, 64 | chalk: chalk, 65 | fs: fs, 66 | log: log, 67 | successLog: successLog, 68 | errorLog: errorLog, 69 | resolvePath: resolvePath, 70 | resolveComponentName: resolveComponentName 71 | 72 | } -------------------------------------------------------------------------------- /scripts/generateComponent/index.js: -------------------------------------------------------------------------------- 1 | // index.js` 2 | //导入公用方法 3 | const { 4 | dotExistDirectoryCreate, 5 | generateFile, 6 | fs, 7 | log, 8 | resolvePath, 9 | successLog, 10 | errorLog, 11 | resolveComponentName 12 | } = require('../common.js'); 13 | const { 14 | componentTemplate, 15 | entryTemplate, 16 | globalRegisterImportTemplate, 17 | globalRegisterExportTemplate 18 | } = require('./template') 19 | 20 | log('请输入要生成的组件名称、会生成在 components/目录下,如需生成全局组件,请加 g/ 前缀,名称各单词之间用|隔开,单个单词如home|,多个单词如home|index') 21 | let componentName = '' 22 | let isGlobal=false 23 | process.stdin.on('data', async chunk => { 24 | const inputName = String(chunk).trim().toString() 25 | if (inputName.includes('/')) { 26 | const inputArr = inputName.split('/') 27 | componentName = inputArr[inputArr.length - 1] 28 | if (inputArr[0]=='g') 29 | { 30 | isGlobal=true; 31 | } 32 | else{ 33 | isGlobal = false; 34 | errorLog("设置全局参数前缀错误,无法全局注册"); 35 | } 36 | } else { 37 | componentName = inputName 38 | } 39 | //判断名字是否包含|,不包含提示错误,重新输入,包含则按大驼峰拼接 40 | componentName = resolveComponentName(componentName); 41 | if (!componentName) { 42 | return; 43 | } 44 | /** 45 | * 组件目录路径 46 | */ 47 | const componentDirectory = resolvePath('../src/components', componentName) 48 | 49 | /** 50 | * vue组件路径 51 | */ 52 | const componentVueName = resolvePath(componentDirectory, `${componentName}.vue`) 53 | /** 54 | * 入口文件路径 55 | */ 56 | const entryComponentName = resolvePath(componentDirectory, 'index.js') 57 | 58 | const hasComponentDirectory = fs.existsSync(componentDirectory) 59 | if (hasComponentDirectory) { 60 | errorLog(`${componentName}组件目录已存在,请重新输入`) 61 | return 62 | } else { 63 | log(`正在生成 component 目录 ${componentDirectory}`) 64 | await dotExistDirectoryCreate(componentDirectory) 65 | } 66 | try { 67 | log(`正在生成 vue 文件 ${componentVueName}`) 68 | await generateFile(componentVueName, componentTemplate(componentName)) 69 | log(`正在生成 entry 文件 ${entryComponentName}`) 70 | await generateFile(entryComponentName, entryTemplate(componentName)) 71 | if (isGlobal) 72 | { 73 | log(`正在注册全局组件文件 ${componentName}`) 74 | await addGlobalComponent(componentName); 75 | } 76 | successLog('组件生成成功') 77 | } catch (e) { 78 | errorLog(e.message) 79 | } 80 | 81 | process.stdin.emit('end') 82 | }) 83 | process.stdin.on('end', () => { 84 | log('exit') 85 | process.exit() 86 | }) 87 | 88 | //添加全局注册 89 | function addGlobalComponent(fileName) { 90 | const file = resolvePath('../src/components', 'index.js'); 91 | return new Promise((resolve,reject)=>{ 92 | if (fs.existsSync(file)) { 93 | fs.readFile(file, 'utf-8', (err, data) => { 94 | if (err) { 95 | errorLog(err.message); 96 | reject(); 97 | } else { 98 | let dataStr = data.toString(); 99 | let importStr, exportStr; 100 | if (!dataStr) { 101 | emptyComponentAdd(file, fileName).then(()=>{ 102 | resolve(); 103 | }); 104 | return; 105 | } 106 | let dataList = dataStr.split("export"); 107 | if (dataList && dataList.length == 2) { 108 | importStr = dataList[0]; 109 | importStr += globalRegisterImportTemplate(fileName); 110 | exportStr = 'export' + dataList[1]; 111 | exportStr = exportStr.replace(/}/, globalRegisterExportTemplate(fileName)); 112 | let writeStr = importStr + '\n' + exportStr; 113 | writeDataToFile(file, writeStr).then(() => { 114 | resolve(); 115 | });; 116 | } else { 117 | errorLog("components/index.js格式不规范,请调整为标准格式后重试"); 118 | reject(); 119 | } 120 | } 121 | }); 122 | } else { 123 | emptyComponentAdd(file, fileName).then(()=>{ 124 | resolve(); 125 | }); 126 | } 127 | }) 128 | 129 | 130 | function emptyComponentAdd(file,fileName) { 131 | let importStr = globalRegisterImportTemplate(fileName); 132 | let exportStr = `export default (Vue) => {\n${globalRegisterExportTemplate(fileName)}`; 133 | let writeStr = importStr + '\n' + exportStr; 134 | return writeDataToFile(file, writeStr); 135 | } 136 | 137 | function writeDataToFile(file, data) { 138 | return new Promise((resolve,reject)=>{ 139 | fs.writeFile(file, data, { 140 | 'flag': 'w' 141 | }, function (err) { 142 | if (err) { 143 | errorLog(err.message); 144 | reject(); 145 | } 146 | successLog('全局注册成功!'); 147 | resolve(); 148 | }); 149 | }) 150 | 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /scripts/generateComponent/template.js: -------------------------------------------------------------------------------- 1 | // template.js 2 | module.exports = { 3 | componentTemplate: compoentName => { 4 | return ` 8 | 13 | ` 15 | }, 16 | entryTemplate: compoentName=> `import ${compoentName} from './${compoentName}.vue' 17 | export default ${compoentName}`, 18 | globalRegisterImportTemplate: compoentName => `import ${compoentName} from './${compoentName}'`, 19 | globalRegisterExportTemplate: compoentName => `Vue.component(${compoentName}.name, ${compoentName})\n}` 20 | } 21 | -------------------------------------------------------------------------------- /scripts/generateStore/index.js: -------------------------------------------------------------------------------- 1 | // index.js 2 | //导入公用方法 3 | const { 4 | dotExistDirectoryCreate, 5 | generateFile, 6 | fs, 7 | log, 8 | resolvePath, 9 | successLog, 10 | errorLog, 11 | resolveComponentName 12 | } = require('../common.js'); 13 | // 导入模板 14 | const { 15 | storeTemplate, 16 | storeRegisterImportTemplate, 17 | storeRegisterExportTemplate 18 | } = require('./template') 19 | 20 | log('请输入要生成的store名称、会生成在 store/modules/目录下,名称各单词之间用|隔开,单个单词如home|,多个单词如home|index') 21 | let componentName = '' 22 | process.stdin.on('data', async chunk => { 23 | // store名称 24 | const inputName = String(chunk).trim().toString() 25 | // 获取要创建的store名称 26 | if (inputName.includes('/')) { 27 | const inputArr = inputName.split('/') 28 | componentName = inputArr[inputArr.length - 1] 29 | } else { 30 | componentName = inputName 31 | } 32 | //判断名字是否包含|,不包含提示错误,重新输入,包含则按大驼峰拼接 33 | componentName = resolveComponentName(componentName); 34 | if (!componentName) { 35 | return; 36 | } 37 | //store用小驼峰 38 | componentName = componentName.substring(0, 1).toLowerCase() + componentName.substring(1); 39 | // store跟页面路径 40 | const storePath = resolvePath('../src', "store") 41 | if (!fs.existsSync(storePath)) { 42 | log(`正在生成 store 文件夹`) 43 | await dotExistDirectoryCreate(storePath) 44 | } 45 | // store跟页面路径 46 | const componentPath = resolvePath(storePath, "modules") 47 | if (!fs.existsSync(componentPath)) { 48 | log(`正在生成 modules 文件夹`) 49 | await dotExistDirectoryCreate(componentPath) 50 | } 51 | // store文件 52 | const storeFile = resolvePath(componentPath, `${componentName}.js`) 53 | 54 | // 判断要创建的store文件是否存在 55 | const hasComponentExists = fs.existsSync(storeFile) 56 | if (hasComponentExists) { 57 | errorLog(`${inputName}文件已存在,请重新输入`) 58 | return 59 | } 60 | try { 61 | log(`正在生成 store 文件 ${storeFile}`) 62 | await generateFile(storeFile, storeTemplate(componentName)) 63 | log(`正在注册store文件 ${storeFile}`) 64 | await addStore(componentName); 65 | successLog('store生成成功') 66 | } catch (e) { 67 | errorLog(e.message) 68 | } 69 | 70 | process.stdin.emit('end') 71 | }) 72 | process.stdin.on('end', () => { 73 | log('exit') 74 | process.exit() 75 | }) 76 | 77 | function addStore(fileName) { 78 | const file = resolvePath('../src/store', 'index.js'); 79 | let storeListItem; 80 | return new Promise((resolve,reject)=>{ 81 | if (fs.existsSync(file)) { 82 | fs.readFile(file, 'utf-8', (err, data) => { 83 | if (err) { 84 | errorLog(err.message); 85 | reject(err.message); 86 | } else { 87 | let dataStr = data.toString(); 88 | if (!dataStr) { 89 | emptyIndexAdd(file, fileName).then(() => { 90 | resolve(); 91 | });; 92 | } else { 93 | addStoreResgiter(dataStr, fileName).then(() => { 94 | resolve(); 95 | });; 96 | } 97 | } 98 | }); 99 | } else { 100 | emptyIndexAdd(file, fileName).then(()=>{ 101 | resolve(); 102 | }); 103 | } 104 | }) 105 | 106 | function addStoreResgiter(dataStr, fileName) { 107 | return new Promise((resolve,reject)=>{ 108 | //1.根据Vue.use(Vuex)分割,0是import注册部分,1是引入部分 109 | let storeRegisterList = dataStr.split('Vue.use(Vuex)'); 110 | if (storeRegisterList && storeRegisterList.length==2) 111 | { 112 | let storeRegisterImport = storeRegisterList[0]; 113 | storeRegisterImport +=storeRegisterImportTemplate(fileName); 114 | let storeRegisterUse = storeRegisterList[1]; 115 | //匹配modules:{},往modules对象中加内容 116 | let reg = /(modules\s*:\s*\{)((.|\s)*?)\}/; 117 | let storeList = storeRegisterUse.match(reg); 118 | if (storeList && storeList.input) { 119 | storeListItem = storeList[1] + storeList[2] + (storeList[2].trim()?",":"") + storeRegisterExportTemplate(fileName) + "\n}"; 120 | dataStr = storeRegisterUse.replace(reg, storeListItem); 121 | dataStr = storeRegisterImport + "\nVue.use(Vuex)" + dataStr; 122 | console.log(dataStr); 123 | writeDataToFile(file, dataStr).then(()=>{ 124 | resolve(); 125 | }); 126 | } else { 127 | errorLog("store/index.js页面结构不正确,本次注册store失败!"); 128 | reject(); 129 | } 130 | }else{ 131 | errorLog("store/index.js页面结构不正确,本次注册store失败!"); 132 | reject(); 133 | } 134 | }) 135 | 136 | } 137 | 138 | function emptyIndexAdd(file, fileName) { 139 | let importStr = `import Vue from 'vue' 140 | import Vuex from 'vuex' 141 | import types from './mutationTypes' 142 | import ${fileName} from './modules/${fileName}' 143 | Vue.use(Vuex) 144 | export default new Vuex.Store({ 145 | types, 146 | modules: { 147 | ${fileName} 148 | } 149 | })`; 150 | return writeDataToFile(file, importStr); 151 | } 152 | 153 | function writeDataToFile(file, data) { 154 | return new Promise((resolve,reject)=>{ 155 | fs.writeFile(file, data, function (err) { 156 | console.log(err); 157 | if (err) { 158 | errorLog(err.message); 159 | reject(err.message); 160 | } 161 | successLog('store注册成功!'); 162 | resolve(); 163 | }); 164 | }); 165 | 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /scripts/generateStore/template.js: -------------------------------------------------------------------------------- 1 | // template.js 2 | module.exports = { 3 | storeTemplate: compoenntName => { 4 | return `const ${compoenntName} = { 5 | state: { 6 | }, 7 | getters: { 8 | }, 9 | actions: { 10 | }, 11 | mutations: { 12 | } 13 | } 14 | export default {...${compoenntName}}` 15 | }, 16 | storeRegisterImportTemplate: compoenntName => { 17 | return `import ${compoenntName} from './modules/${compoenntName}'`; 18 | }, 19 | storeRegisterExportTemplate: compoenntName => { 20 | return `${compoenntName}`; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/generateView/index.js: -------------------------------------------------------------------------------- 1 | // index.js 2 | //导入公用方法 3 | const { 4 | dotExistDirectoryCreate, 5 | generateFile, 6 | fs, 7 | log, 8 | resolvePath, 9 | successLog, 10 | errorLog, 11 | resolveComponentName 12 | } = require('../common.js'); 13 | // 导入模板 14 | const { 15 | viewTemplate, 16 | entryTemplate, 17 | viewRouterTemplate 18 | } = require('./template'); 19 | 20 | 21 | log('请输入要生成的页面名称、会生成在 views/目录下,页面名称各单词之间用|隔开,单个单词如home|,多个单词如home|index') 22 | let componentName = '' 23 | process.stdin.on('data', async chunk => { 24 | // 组件名称 25 | const inputName = String(chunk).trim().toString() 26 | // 获取组件名 27 | if (inputName.includes('/')) { 28 | const inputArr = inputName.split('/') 29 | componentName = inputArr[inputArr.length - 1] 30 | } else { 31 | componentName = inputName 32 | } 33 | //判断名字是否包含|,不包含提示错误,重新输入,包含则按大驼峰拼接 34 | componentName = resolveComponentName(componentName); 35 | if (!componentName) 36 | { 37 | return; 38 | } 39 | // Vue页面组件路径 40 | const componentPath = resolvePath('../src/views', componentName) 41 | // vue文件 42 | const vueFile = resolvePath(componentPath, `${componentName}.vue`) 43 | // 入口文件 44 | const entryFile = resolvePath(componentPath, 'index.js') 45 | // 判断组件文件夹是否存在 46 | const hasComponentExists = fs.existsSync(componentPath) 47 | if (hasComponentExists) { 48 | errorLog(`${inputName}页面组件已存在,请重新输入`) 49 | return 50 | } else { 51 | log(`正在生成 view 目录 ${componentPath}`) 52 | await dotExistDirectoryCreate(componentPath) 53 | } 54 | try { 55 | log(`正在生成 vue 文件 ${vueFile}`) 56 | await generateFile(vueFile, viewTemplate(componentName)) 57 | log(`正在生成 entry 文件 ${entryFile}`) 58 | await generateFile(entryFile, entryTemplate(componentName)) 59 | log(`正在注册路由文件 ${componentName}`) 60 | await addRouter(componentName); 61 | successLog('页面生成成功') 62 | } catch (e) { 63 | errorLog(e.message) 64 | } 65 | 66 | process.stdin.emit('end') 67 | }) 68 | process.stdin.on('end', () => { 69 | log('exit') 70 | process.exit() 71 | }) 72 | 73 | function addRouter(fileName) { 74 | const file = resolvePath('../src/router', 'index.js'); 75 | let reg = /(routes\s*:\s*\[)((.|\s)*?)\]/; 76 | let routerListItem; 77 | return new Promise((resolve, reject) => { 78 | if (fs.existsSync(file)) { 79 | fs.readFile(file, 'utf-8', (err, data) => { 80 | if (err) { 81 | errorLog(err.message); 82 | reject(); 83 | } else { 84 | let dataStr = data.toString(); 85 | if (!dataStr) { 86 | emptyIndexAdd(file, fileName).then(()=>{ 87 | resolve(); 88 | }); 89 | } else { 90 | let routerList = dataStr.match(reg); 91 | if (routerList && routerList.input) { 92 | routerListItem = routerList[1] + routerList[2] + (routerList[2].trim() ? 93 | "," : '') + viewRouterTemplate(fileName) + "]"; 94 | dataStr = dataStr.replace(reg, routerListItem); 95 | writeDataToFile(file, dataStr).then(()=>{ 96 | resolve(); 97 | }); 98 | 99 | } else { 100 | errorLog("router/index.js页面结构不正确,本次注册路由失败!"); 101 | reject(); 102 | } 103 | } 104 | } 105 | 106 | }) 107 | } else { 108 | emptyIndexAdd(file, fileName).then(()=>{ 109 | resolve(); 110 | }); 111 | } 112 | }); 113 | 114 | function emptyIndexAdd(file, fileName) { 115 | 116 | let importStr = ` 117 | import Vue from 'vue' 118 | import Router from 'vue-router' 119 | Vue.use(Router) 120 | let router = new Router({ 121 | mode: 'hash', 122 | routes: [ 123 | { 124 | path: '/${fileName}', 125 | name: ${fileName}, 126 | component: () => import("@/views/${fileName}/${fileName}") 127 | } 128 | ] 129 | }) 130 | router.beforeEach((to, from, next) => { 131 | next(); 132 | }) 133 | export default router 134 | ` 135 | return writeDataToFile(file, importStr); 136 | } 137 | 138 | function writeDataToFile(file, data) { 139 | return new Promise((resolve,reject)=>{ 140 | fs.writeFile(file, data, { 141 | 'flag': 'w' 142 | }, function (err) { 143 | if (err) { 144 | errorLog(err.message); 145 | reject(err.message); 146 | } 147 | successLog('路由注册成功!'); 148 | resolve(); 149 | }); 150 | }) 151 | 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /scripts/generateView/template.js: -------------------------------------------------------------------------------- 1 | // template.js 2 | module.exports = { 3 | viewTemplate: compoenntName => { 4 | return ` 8 | 13 | ` 15 | }, 16 | entryTemplate: compoenntName => { 17 | return `import ${compoenntName} from './${compoenntName}.vue' 18 | export { 19 | ${compoenntName} 20 | }` 21 | }, 22 | viewRouterTemplate: compoenntName => `{ 23 | path: '/${compoenntName.toLocaleLowerCase?compoenntName.toLocaleLowerCase():compoenntName}', 24 | name: '${compoenntName.toLocaleLowerCase?compoenntName.toLocaleLowerCase():compoenntName}', 25 | component: () => import("@/views/${compoenntName}/${compoenntName}") 26 | }\n` 27 | } 28 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /src/api/API.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {Message} from 'element-ui' 3 | let errCode = ['400', '500', '502', '503', '504']; 4 | axios.defaults.baseURL = ''; 5 | axios.defaults.timeout = 10000 6 | axios.defaults.headers = { 7 | } 8 | 9 | // 请求拦截器 10 | axios.interceptors.request.use(config => { 11 | 12 | // config.responseType='blob'; 13 | return config; 14 | }, err => { 15 | return Promise.reject(err); 16 | }); 17 | 18 | 19 | // 响应拦截器 20 | axios.interceptors.response.use(res=>{ 21 | return res; 22 | },err=>{ 23 | let errStatus=err.response.status.toString(); 24 | let errCodeStr=errCode.join(','); 25 | if (errCodeStr.indexOf(errStatus)>=0) { 26 | Message({ 27 | messgae: '亲,当前访问服务的人太多,请稍后重试', 28 | type: 'warning' 29 | }); 30 | } 31 | else if (errStatus == '403') 32 | { 33 | Message({ 34 | messgae: '亲,您当前无权访问页面,请稍后重试', 35 | type: 'warning' 36 | }); 37 | } 38 | else if (errStatus == '404') { 39 | Message({ 40 | messgae: '亲,您的请求已经丢失,您可以逛逛别的页面', 41 | type: 'warning' 42 | }); 43 | } 44 | return Promise.reject(err); 45 | }); 46 | 47 | 48 | class API { 49 | constructor(url) { 50 | this.url = url 51 | } 52 | 53 | // 创建axios配置项 54 | static _createAxiosConfig({url, action, method = 'get', data, timeout, baseURL,responseType}) { 55 | var config = { 56 | method: method, 57 | url: url + action 58 | } 59 | if (baseURL) { 60 | config.baseURL = baseURL; 61 | } 62 | if (['post', 'put', 'patch'].includes(config.method)) { 63 | config.data = data 64 | } else { 65 | config.params = data 66 | } 67 | if (timeout!=undefined) { 68 | config.timeout = timeout 69 | } 70 | if(responseType) 71 | { 72 | config.responseType=responseType; 73 | } 74 | return config 75 | } 76 | 77 | // 请求url, data, method = 'get' 78 | static request(params, isAuth = true) { 79 | if (this instanceof API && !params.url) { 80 | params.url = this.url; 81 | } else { 82 | params.url = '' 83 | } 84 | return isAuth ? API._probeAuthStatusAndRequest(params) : axios(API._createAxiosConfig(params)) 85 | } 86 | 87 | 88 | 89 | 90 | //此处可以加鉴权处理,或者在每次请求之前加的处理逻辑 91 | static _probeAuthStatusAndRequest(params) { 92 | var promise = new Promise((resolve, reject) => { 93 | //这里可以添加权限验证,登录验证等 94 | axios(API._createAxiosConfig(params)).then(res => { 95 | return resolve(res) 96 | }).catch(err => { 97 | return reject(err) 98 | }) 99 | }) 100 | return promise 101 | } 102 | } 103 | export default API 104 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import API from './API' -------------------------------------------------------------------------------- /src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/src/assets/.DS_Store -------------------------------------------------------------------------------- /src/assets/images/lock-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/lock-open-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/rotate-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/styles/base.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | menu, 72 | nav, 73 | output, 74 | ruby, 75 | section, 76 | summary, 77 | time, 78 | mark, 79 | audio, 80 | video, 81 | input { 82 | margin: 0; 83 | padding: 0; 84 | border: 0; 85 | font-size: 100%; 86 | font-weight: normal; 87 | vertical-align: baseline; 88 | box-sizing: border-box; 89 | } 90 | 91 | /* HTML5 display-role reset for older browsers */ 92 | article, 93 | aside, 94 | details, 95 | figcaption, 96 | figure, 97 | footer, 98 | header, 99 | menu, 100 | nav, 101 | section { 102 | display: block; 103 | } 104 | 105 | body { 106 | line-height: 1; 107 | } 108 | 109 | blockquote, 110 | q { 111 | quotes: none; 112 | } 113 | 114 | blockquote:before, 115 | blockquote:after, 116 | q:before, 117 | q:after { 118 | content: none; 119 | } 120 | 121 | table { 122 | border-collapse: collapse; 123 | border-spacing: 0; 124 | } 125 | 126 | /* custom */ 127 | a { 128 | color: #7e8c8d; 129 | text-decoration: none; 130 | -webkit-backface-visibility: hidden; 131 | } 132 | 133 | li { 134 | list-style: none; 135 | } 136 | 137 | ::-webkit-scrollbar { 138 | width: 5px; 139 | height: 5px; 140 | } 141 | 142 | ::-webkit-scrollbar-track-piece { 143 | background-color: rgba(0, 0, 0, 0.2); 144 | -webkit-border-radius: 6px; 145 | } 146 | 147 | ::-webkit-scrollbar-thumb:vertical { 148 | height: 5px; 149 | background-color: rgba(125, 125, 125, 0.7); 150 | -webkit-border-radius: 6px; 151 | } 152 | 153 | ::-webkit-scrollbar-thumb:horizontal { 154 | width: 5px; 155 | background-color: rgba(125, 125, 125, 0.7); 156 | -webkit-border-radius: 6px; 157 | } 158 | 159 | html, 160 | body { 161 | width: 100%; 162 | } 163 | 164 | body { 165 | -webkit-text-size-adjust: none; 166 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 167 | } -------------------------------------------------------------------------------- /src/assets/styles/common.css: -------------------------------------------------------------------------------- 1 | .normal_btn,.focus_btn{ 2 | cursor: pointer; 3 | box-shadow: 0 6px 10px 0 rgba(66,0,193,0.24); 4 | border:1px solid; 5 | color: #1817E6; 6 | border-color:#5A59FF #4D1EA9; 7 | } 8 | .normal_btn:hover{ 9 | opacity: 0.92; 10 | background: #EEEDFF; 11 | box-shadow: 0 4px 7px -2px rgba(66,0,193,0.24); 12 | } 13 | .focus_btn{ 14 | background-image: linear-gradient(-40deg,#5A59FF 0%, #6128CF 100%); 15 | color: #fff!important; 16 | } 17 | .focus_btn:hover{ 18 | opacity: 0.8; 19 | box-shadow: 0 6px 10px 0 rgba(66,0,193,0.10); 20 | } 21 | .cl_both:before, 22 | .cl_both:after { 23 | content: " "; 24 | display: table; 25 | } 26 | .cl_both:after { 27 | clear: both; 28 | } 29 | .c_flex{ 30 | display: flex; 31 | display: -webkit-flex; 32 | } 33 | .cl_l { 34 | float: left; 35 | } 36 | 37 | .cl_r { 38 | float: right; 39 | } -------------------------------------------------------------------------------- /src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/src/components/.DS_Store -------------------------------------------------------------------------------- /src/components/MicroEdit/AddMaterials.vue: -------------------------------------------------------------------------------- 1 | 61 | 425 | 514 | 515 | 516 | -------------------------------------------------------------------------------- /src/components/MicroEdit/BannerEdit.vue: -------------------------------------------------------------------------------- 1 | 61 | 324 | 429 | 430 | 441 | -------------------------------------------------------------------------------- /src/components/MicroEdit/BottomOperate.vue: -------------------------------------------------------------------------------- 1 | 10 | 60 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/MicroEdit/MicroLayer.vue: -------------------------------------------------------------------------------- 1 | 23 | 233 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /src/components/MicroEdit/ParametersEdit.vue: -------------------------------------------------------------------------------- 1 | 98 | 237 | 406 | 407 | 408 | -------------------------------------------------------------------------------- /src/components/MicroEdit/TopOperate.vue: -------------------------------------------------------------------------------- 1 | 29 | 89 | 280 | 281 | -------------------------------------------------------------------------------- /src/components/MicroEdit/bannerEditCommon.js: -------------------------------------------------------------------------------- 1 | import 'glfx.js__temp' 2 | //canvas操作相关 3 | let fabricCommon = function (fabricObject,that) { 4 | //tojson包含自定义的属性内容 5 | let toJsonWithParams = [ 6 | "realLeft", 7 | "realTop", 8 | "realType", 9 | "realWidth", 10 | "realHeight", 11 | "order", 12 | "oldSrc", 13 | "oldId", 14 | "oldPictureName", 15 | "oldPictureNum", 16 | "oldPictType", 17 | "oldUserId", 18 | "lockUniScaling", 19 | "PictureName", 20 | "PictureNum", 21 | "id", 22 | "pictType", 23 | "userId", 24 | "aCoords", 25 | "oldWidth",//图片本身真实宽度 26 | "oldHeight", //图片本身真实高度 27 | "title", 28 | "oldRealUrl", 29 | "realUrl", 30 | "bboxWidth",//图片要显示的宽度 31 | "bboxHeight", //图片要显示的高度 32 | "realFontFamily",//用于存储选中的字体,在字体文件加载完成之后替换到fontFamiy 33 | 'lockScalingY', 34 | 'lockScalingX', 35 | 'lockMovementY', 36 | 'lockMovementX', 37 | 'layerIsEdit', 38 | 'isGif' 39 | ]; 40 | let _fabricInstance = fabricObject; 41 | _fabricInstance.setToJsonWithParams(toJsonWithParams); 42 | let _canvasObject = _fabricInstance.fabricInstance; 43 | 44 | //记录偏移量x,y的值 45 | let _offsetObj = {offsetX:0,offsetY:0}; 46 | 47 | //文本类型 48 | let txtList = ["3-1", "3-2", "3-3"]; 49 | //数据渲染 50 | let addObject = function (bannerMergeDataList) { 51 | //判断宽度是否大于1000或者高度是否大于600 52 | let objs = bannerMergeDataList.filter(e => { 53 | return e.type == "1-1"; 54 | }); 55 | if (objs && objs.length > 0) { 56 | 57 | // if (objs[0].bboxWidth > 1000 || objs[0].bboxHeight > 600) { 58 | // // setZoom(0.5); 59 | // if (that.initZoom) 60 | // { 61 | // that.initZoom(0.5); 62 | // } 63 | // _offsetObj.offsetX = (1000 - objs[0].bboxWidth) / 2; 64 | // _offsetObj.offsetY = (600 - objs[0].bboxHeight) / 2; 65 | // } else { 66 | // _offsetObj.offsetX = Math.abs((1000 - objs[0].bboxWidth) / 2); 67 | // _offsetObj.offsetY = Math.abs((600 - objs[0].bboxHeight) / 2); 68 | // } 69 | } 70 | console.log(_offsetObj); 71 | let cloneJson = JSON.parse(JSON.stringify(bannerMergeDataList)); 72 | loadDataToCanvas(cloneJson); 73 | _canvasObject.renderAll(); 74 | }; 75 | 76 | //创建图片,dataList不传则为新增 77 | let createImage = function (obj, dataList) { 78 | let order=(_canvasObject.getObjects()&&_canvasObject.getObjects().length)||0 79 | let item = obj; 80 | let url = '/static/'+item.layerPictureUrl; 81 | let top, left; 82 | if (item.type!="1-1") 83 | { 84 | left = parseInt(item.bboxX1) + parseInt(item.bboxWidth) / 2 + _offsetObj.offsetX; 85 | top = parseInt(item.bboxY1) + parseInt(item.bboxHeight) / 2 + _offsetObj.offsetY; 86 | } 87 | else{ 88 | left = parseInt(item.bboxWidth) / 2 + _offsetObj.offsetX; 89 | top = parseInt(item.bboxHeight) / 2 + _offsetObj.offsetY; 90 | } 91 | let imgConfig = { 92 | left: left, 93 | top: top, 94 | order: order, 95 | id: item.id, 96 | PictureName: item.PictureName, 97 | PictureNum: item.motherSetNumRef, 98 | userId: item.userId, 99 | pictType: item.pictType, 100 | angle: 0, 101 | realLeft: parseInt(item.bboxX1), 102 | realTop: parseInt(item.bboxY1), 103 | realType: item.type, 104 | title: item.title, 105 | realUrl: item.layerPictureUrl, 106 | bboxWidth: item.bboxWidth, 107 | bboxHeight: item.bboxHeight, 108 | lockScalingY:false, 109 | lockScalingX:false, 110 | lockMovementY:false, 111 | lockMovementX:false, 112 | layerIsEdit:item.layerIsEdit, 113 | isGif:item.isGif 114 | } 115 | _fabricInstance.createImage(url, imgConfig, !item.bboxWidth, item.type!='1-1').then(img=>{ 116 | if(!item.bboxWidth) 117 | { 118 | setActiveSelect(order); 119 | canvasObjectSelected(); 120 | } 121 | if(dataList) 122 | { 123 | loadDataCallback(dataList); 124 | } 125 | }); 126 | 127 | } 128 | 129 | //创建文本,dataList不传则为新增/或者替换 130 | let createTxt = function (item, dataList) { 131 | let order=(_canvasObject.getObjects()&&_canvasObject.getObjects().length)||0; 132 | if(that.getFont) 133 | { 134 | let result = that.getFont(item.fontType); 135 | if(!result) 136 | { 137 | item.fontType='FZZCHJW--GB1-0'; 138 | } 139 | } 140 | let scale=(!item.fontSize||item.fontSize>=12)?1:item.fontSize/12 141 | let txtConfig = { 142 | left: parseInt(item.bboxX1) + parseInt(item.bboxWidth) / 2 + _offsetObj.offsetX, //+parseInt(item.bboxWidth)/2 143 | top: parseInt(item.bboxY1) + parseInt(item.bboxHeight) / 2 + _offsetObj.offsetY, //+parseInt(item.bboxWidth)/2 144 | order: order, 145 | fill: item.color, //content.color 146 | fontSize: item.fontSize ? item.fontSize:20, 147 | angle: 0, 148 | fontFamily: "", //content.fontFamily 149 | realFontFamily: item.fontType, 150 | fontWeight: item.fontBold=="1" ? "bold" : "normal", 151 | fontStyle: item.fontItalic == "0" ? "normal" : "italic", 152 | userId: item.userId, 153 | realLeft: parseInt(item.bboxX1), 154 | realTop: parseInt(item.bboxY1), 155 | realType: item.type, 156 | title: item.title, 157 | realWidth: parseInt(item.bboxWidth), 158 | realHeight: parseInt(item.bboxHeight), 159 | // lockScalingY: true, 160 | // lockScalingX: true, 161 | lockMovementY: false, 162 | lockMovementX: false, 163 | width: parseInt(item.bboxWidth), 164 | height: parseInt(item.bboxHeight), 165 | scaleX:scale, 166 | scaleY:scale, 167 | layerIsEdit:item.layerIsEdit, 168 | isGif:item.isGif 169 | } 170 | let txtInstabce= _fabricInstance.createItext(item.text, txtConfig, !item.bboxWidth); 171 | if (item.fontType) { 172 | _fabricInstance.loadFont(txtInstabce,item.fontType); 173 | } 174 | txtInstabce.on("editing:exited", function () { 175 | let obj = _canvasObject.getActiveObject(); 176 | checkTxt(obj); 177 | }) 178 | if(!item.bboxWidth) 179 | { 180 | setActiveSelect(order); 181 | canvasObjectSelected(); 182 | } 183 | if(dataList) 184 | { 185 | loadDataCallback(dataList); 186 | } 187 | } 188 | 189 | //检查编辑的时候文本内容长度是否符合要求 190 | let checkTxt=function(obj){ 191 | if (obj && obj.text && obj.realType && that.copyRightLengthCheck) 192 | { 193 | that.copyRightLengthCheck({txt:obj.text,type: obj.realType}); 194 | } 195 | 196 | } 197 | 198 | //加载数据到画布的回调函数,适用于loadDataToCanvas方法 199 | let loadDataCallback = function (list) { 200 | list.shift(); 201 | loadDataToCanvas(list); 202 | } 203 | 204 | //处理加载过来的数据,递归加载 205 | let loadDataToCanvas = function (dataList) { 206 | if (dataList && dataList.length > 0) { 207 | let obj = dataList[0]; 208 | if (obj.type == "0-1") { 209 | loadDataCallback(dataList); 210 | return; 211 | } 212 | if (txtList.indexOf(obj.type) < 0) { 213 | //图片 214 | createImage(obj, dataList); 215 | } else { 216 | //文字 217 | createTxt(obj, dataList); 218 | } 219 | } else { 220 | _fabricInstance.firstAddToState(true); 221 | _fabricInstance.updateCanvasState(); 222 | } 223 | } 224 | 225 | //新增/修改文案信息 226 | let modifyCopyright = function (list) { 227 | let objectList = _canvasObject.getObjects(); 228 | for (let i = 0; i < list.length; i++) { 229 | let mainObj = objectList.filter(e => { 230 | return e.realType == list[i].type; 231 | }); 232 | if (mainObj && mainObj.length > 0) { //已有 233 | let title = list[i].text; 234 | let obj = { 235 | cssName: 'text', 236 | cssValue: title 237 | }; 238 | updateTxtCss(obj, mainObj[0]); 239 | _canvasObject.renderAll(); 240 | _fabricInstance.updateCanvasState(); 241 | } else { 242 | createTxt(list[i]); 243 | } 244 | } 245 | } 246 | 247 | let isMouseUp=true; 248 | 249 | //是否开启整体移动 250 | let isOpenAllMove=false; 251 | 252 | //用于整体拖动,记录拖动开始位置 253 | let point={x:0,y:0}; 254 | 255 | let selectedObj={}; 256 | _canvasObject.on("mouse:down", e => { 257 | if(isOpenAllMove) 258 | { 259 | Object.assign(point,e.absolutePointer); 260 | } 261 | isMouseUp=false; 262 | if (e && e.target && e.target.selectable) { 263 | //用于鼠标移动到的元素边框显示 264 | // isMouseUp=false; 265 | // clearDotted(); 266 | if (selectedObj != e.target) 267 | { 268 | selectedObj = e.target; 269 | } 270 | console.log(selectedObj); 271 | }else if(e&&!e.target){//点击到canvas中无图片渲染区域,重置图层选中状态为未选中 272 | let currentActive={order:-1} 273 | that.setActiveObjectCallback(currentActive); 274 | } 275 | }); 276 | _canvasObject.on("mouse:move", function (e) { 277 | if(isOpenAllMove&&!isMouseUp) 278 | { 279 | _canvasObject.relativePan({ x:e.absolutePointer.x- point.x, y: e.absolutePointer.y- point.y }); 280 | } 281 | if (e && e.target && isMouseUp) { 282 | //用于鼠标移动到的元素边框显示 283 | // if (lastMoved != e.target) 284 | // { 285 | // drawDottedBorder(e.target.aCoords); 286 | // lastMoved = e.target; 287 | // } 288 | } 289 | }); 290 | _canvasObject.on("mouse:up", e => { 291 | if (e && e.target) { 292 | canvasObjectSelected(); 293 | } 294 | //用于鼠标移动到的元素边框显示 295 | isMouseUp = true; 296 | }); 297 | _canvasObject.on("object:selected", e => { 298 | if (e && e.target) { 299 | } 300 | }); 301 | _canvasObject.on("object:modified", function (e) { 302 | if(e&&e.target) 303 | { 304 | console.log('modified'); 305 | } 306 | 307 | }); 308 | _canvasObject.on("object:scaled", e => { 309 | //文本类放大完成事件,把缩放率更新到字体大小 310 | if (e && e.target&&e.target.isType('i-text')) { 311 | let scale = e.target.scaleX; 312 | let fontSize = e.target.fontSize ? e.target.fontSize:20; 313 | fontSize =Math.round(fontSize * scale); 314 | let obj={ 315 | cssName:'scaleX', 316 | cssValue:1 317 | } 318 | updateTxtCss(obj,e.target); 319 | obj = { 320 | cssName: 'scaleY', 321 | cssValue: 1 322 | } 323 | updateTxtCss(obj, e.target); 324 | obj = { 325 | cssName: 'fontSize', 326 | cssValue: fontSize 327 | } 328 | updateTxtCss(obj, e.target); 329 | } 330 | }); 331 | 332 | //选中元素事件,重置realLeft和realTop 333 | let canvasObjectSelected = function () { 334 | let currentActive =_fabricInstance.getActiveObj(); 335 | if (currentActive) { 336 | let left, top, width, height; 337 | if (currentActive.type == "image") { 338 | //图片 339 | width = currentActive.oldWidth * currentActive.scaleX; 340 | height = currentActive.oldHeight * currentActive.scaleY; 341 | left = currentActive.left - _offsetObj.offsetX - currentActive.width * currentActive.scaleX / 2; 342 | top = currentActive.top - _offsetObj.offsetY - currentActive.height * currentActive.scaleY / 2; 343 | currentActive.realWidth = width; 344 | currentActive.realHeight = height; 345 | } else { 346 | //文本,不需要再去减二分之一的宽高 347 | left = currentActive.left - parseInt(currentActive.width) / 2 - _offsetObj.offsetX; 348 | top = currentActive.top - parseInt(currentActive.height) / 2 - _offsetObj.offsetY; 349 | } 350 | currentActive.realTop = top; 351 | currentActive.realLeft = left; 352 | _canvasObject.renderAll(); 353 | that.setActiveObjectCallback(currentActive); 354 | } 355 | }; 356 | 357 | //修改属性值 358 | let setCss = function (obj) { 359 | let currentActive = _canvasObject.getActiveObject(); 360 | if (!currentActive) { 361 | return; 362 | } 363 | //是否是背景,是否是编辑修改图片功能 364 | if (currentActive.realType=='1-1')//背景,只能替换或者恢复图片 365 | { 366 | if (obj.cssName != "src" && obj.cssName != "resetSrc") 367 | { 368 | return; 369 | } 370 | } 371 | //图片和文字都有的操作,需要重置位置的 372 | if (obj.cssName == "left" || obj.cssName == "top") { 373 | updateLeftOrTop(obj, currentActive); 374 | } 375 | else if (obj.cssName == "src") { //更换图片 376 | updatePic(obj, currentActive); 377 | return; 378 | } 379 | else if (obj.cssName == "resetSrc") { //恢复图片 380 | resetPic(currentActive); 381 | return; 382 | } 383 | else if (obj.cssName == "width" || obj.cssName == "height") { 384 | updateWidthOrHeight(obj, currentActive); 385 | } 386 | else { 387 | if (currentActive.isType("i-text"))//剩下的就是文本类的操作 388 | { 389 | updateTxtCss(obj, currentActive); 390 | } 391 | else{ 392 | currentActive.set(obj.cssName,obj.cssValue).setCoords(); 393 | } 394 | } 395 | _canvasObject.renderAll(); 396 | _fabricInstance.updateCanvasState(); 397 | }; 398 | 399 | //更新文本类属性值 400 | let updateTxtCss = function (obj, currentActive,isSetSelect=true) { 401 | //字体类的修改 402 | if (obj.cssName != "realFontFamily") { //不是修改字体样式 403 | currentActive.set(obj.cssName, obj.cssValue).setCoords(); 404 | // if (obj.cssName == "fontSize"&&obj.cssValue<12) {//修改字体大小,且字体大小是小于12的 405 | // let scale =obj.cssValue/12; 406 | // console.log(scale); 407 | // currentActive.scale(scale).setCoords(); 408 | // } 409 | // 解决切换颜色值,字体大小,下划线,加粗等无法重绘文字内容的问题 410 | let stroke = currentActive.stroke; 411 | if(stroke !== '#000000'){ 412 | currentActive.set('stroke','#000000'); 413 | }else{ 414 | currentActive.set('stroke','#ffffff'); 415 | } 416 | currentActive.set('stroke',stroke); 417 | // if (currentActive.fontFamily == "黑体") { 418 | // currentActive.set("fontFamily", "微软雅黑").setCoords(); 419 | // } else { 420 | // currentActive.set("fontFamily", "黑体").setCoords(); 421 | // } 422 | // currentActive.set("fontFamily", oldValue).setCoords(); 423 | }else { //修改字体样式 424 | currentActive.set(obj.cssName, obj.cssValue).setCoords(); 425 | console.log('_fabricInstance.loadFont'); 426 | _fabricInstance.loadFont(currentActive, obj.cssValue); 427 | } 428 | if (isSetSelect) 429 | { 430 | canvasObjectSelected(); 431 | } 432 | } 433 | 434 | //更换图片 435 | let updatePic = function (obj, currentActive) { 436 | let src = currentActive.getSrc(); 437 | //判断是否更换的同一张 438 | if (src == obj.cssValue) { 439 | return; 440 | } 441 | currentActive.set("oldSrc", src); //跨域转换后的url 442 | currentActive.set("oldId", currentActive.id); 443 | currentActive.set("oldPictureName", currentActive.PictureName); 444 | currentActive.set("oldPictureNum", currentActive.PictureNum); 445 | currentActive.set("oldPictType", currentActive.pictType); 446 | currentActive.set("oldUserId", currentActive.userId); 447 | let content = obj.cssValue; 448 | currentActive.set({ 449 | id: content.id, 450 | PictureName: content.PictureName, 451 | PictureNum: content.motherSetNumRef, 452 | userId: content.userId, 453 | pictType: content.type, 454 | oldRealUrl: currentActive.realUrl, //替换前的真实url 455 | realUrl: content.layerPictureUrl //替换后的真实url 456 | }); 457 | let width = currentActive.realWidth; 458 | let url = './app/bannerMaterialManager/getPicture?pictureUrl=' + content.layerPictureUrl; //跨域转换后的url 459 | currentActive.setSrc(url, function (e) { 460 | let scaleX = width / e.width; 461 | let scaleY = scaleX; 462 | if(currentActive.realType=='1-1') 463 | { 464 | scaleY=currentActive.realHeight/e.height; 465 | } 466 | currentActive.set("scaleX", scaleX).setCoords(); 467 | currentActive.set("scaleY", scaleY).setCoords(); 468 | currentActive.set("oldWidth", parseInt(e.width)).setCoords(); 469 | currentActive.set("oldHeight", parseInt(e.height)).setCoords(); 470 | _canvasObject.renderAll(); 471 | _fabricInstance.updateCanvasState(); 472 | canvasObjectSelected(); 473 | }); 474 | return; 475 | } 476 | 477 | //恢复图片 478 | let resetPic = function (currentActive) { 479 | let width = currentActive.realWidth; 480 | let height = currentActive.realHeight; 481 | currentActive.set({ 482 | id: currentActive.oldId, 483 | PictureName: currentActive.oldPictureName, 484 | PictureNum: currentActive.oldPictureNum, 485 | userId: currentActive.oldUserId, 486 | pictType: currentActive.oldPictType, 487 | realUrl: currentActive.oldRealUrl 488 | }); 489 | let url = currentActive.oldSrc; 490 | currentActive.set("oldSrc", ""); 491 | currentActive.set("oldId", ""); 492 | currentActive.set("oldPictureName", ""); 493 | currentActive.set("oldPictureNum", ""); 494 | currentActive.set("oldPictType", ""); 495 | currentActive.set("oldUserId", ""); 496 | currentActive.set("oldRealUrl", ""); 497 | 498 | currentActive.setSrc(url, function (e) { 499 | let scaleX = width / e.width; 500 | let scaleY = scaleX; 501 | if(currentActive.realType=='1-1') 502 | { 503 | scaleY=currentActive.realHeight/e.height; 504 | } 505 | currentActive.set("scaleX", scaleX).setCoords(); 506 | currentActive.set("scaleY", scaleY).setCoords(); 507 | currentActive.set("oldWidth", parseInt(e.width)).setCoords(); 508 | currentActive.set("oldHeight", parseInt(e.height)).setCoords(); 509 | _canvasObject.renderAll(); 510 | _fabricInstance.updateCanvasState(); 511 | canvasObjectSelected(); 512 | }); 513 | return; 514 | } 515 | 516 | //更新left/top的值 517 | let updateLeftOrTop = function (obj, currentActive) { 518 | let value; 519 | if (currentActive.isType("image")) { 520 | if (obj.cssName == "top") 521 | { 522 | value = currentActive.realTop + currentActive.height * currentActive.scaleY / 2 + _offsetObj.offsetY; 523 | } 524 | else if (obj.cssName == 'left') 525 | { 526 | value = currentActive.realLeft + currentActive.width * currentActive.scaleX / 2 + _offsetObj.offsetX; 527 | } 528 | } 529 | else { 530 | if (obj.cssName == "top") { 531 | value = currentActive.realTop + parseInt(currentActive.height) / 2 + _offsetObj.offsetY; 532 | } else if (obj.cssName == 'left') { 533 | value = currentActive.realLeft + parseInt(currentActive.width) / 2 + _offsetObj.offsetX; 534 | } 535 | } 536 | currentActive.set(obj.cssName, value).setCoords(); 537 | } 538 | 539 | //更新width/height的值 540 | let updateWidthOrHeight = function (obj, currentActive) { 541 | if (obj.cssName == "width") 542 | { 543 | if (currentActive.isType("image")) { 544 | let scaleX = currentActive.realWidth / currentActive.oldWidth; 545 | let scaleY = scaleX * currentActive.scaleY / currentActive.scaleX; 546 | currentActive.set("scaleX", scaleX).setCoords(); 547 | currentActive.set("scaleY", scaleY).setCoords(); 548 | canvasObjectSelected(); 549 | } 550 | } 551 | else{ 552 | if (currentActive.isType("image")) { 553 | let scaleY = currentActive.realHeight / currentActive.oldHeight; 554 | let scaleX = scaleY * currentActive.scaleX / currentActive.scaleY; 555 | currentActive.set("scaleY", scaleY).setCoords(); 556 | currentActive.set("scaleX", scaleX).setCoords(); 557 | canvasObjectSelected(); 558 | } 559 | } 560 | //文本类修改 561 | currentActive.set(obj.cssName, obj.cssValue).setCoords(); 562 | } 563 | 564 | //设置选中项 565 | let setActiveSelect = function (order) { 566 | _fabricInstance.setActiveSelect(order).then(res=>{ 567 | canvasObjectSelected(); 568 | }); 569 | }; 570 | 571 | //导出json数据 572 | let toJSon = function () { 573 | stopEditing(); 574 | let canvasList = _canvasObject.toJSON(toJsonWithParams).objects; 575 | let result=[]; 576 | canvasList.forEach(item=>{ 577 | console.log(item); 578 | let obj={}; 579 | let width = 0; 580 | let height = 0; 581 | if(item.realType.indexOf('3-')<0) 582 | { 583 | width = item.oldWidth * item.scaleX; 584 | height = item.oldHeight * item.scaleY; 585 | obj.layerPictureUrl=item.realUrl 586 | obj.motherSetNumRef=item.PictureNum 587 | obj.pictType=item.pictType 588 | obj.PictureName=item.PictureName 589 | obj.id=item.id 590 | obj.PictureName=item.PictureName 591 | }else{ 592 | width = item.width ? item.width : 0; 593 | height = item.height ? item.height : 0; 594 | obj.fontType=item.realFontFamily 595 | obj.color=item.fill 596 | obj.fontBold=item.fontWeight == "bold"?1:0 597 | obj.fontItalic=item.fontStyle != "normal"?1:0 598 | obj.fontSize=item.fontSize 599 | obj.text=item.text 600 | } 601 | obj.bboxWidth=width 602 | obj.bboxHeight=height 603 | obj.bboxX1=item.realLeft 604 | obj.bboxX2=item.realLeft+width 605 | obj.bboxY1=item.realTop 606 | obj.bboxY2=item.realTop+height 607 | obj.isGif=item.isGif 608 | obj.layerIsEdit=item.layerIsEdit 609 | obj.order=item.order 610 | obj.type=item.realType 611 | obj.userId=item.userId 612 | result.push(obj); 613 | }); 614 | return result; 615 | }; 616 | 617 | //导出图片 618 | let downLoadImage=function(){ 619 | let imgURL = _fabricInstance.getImgBase64Url(); 620 | dowmLoadFunc(imgURL); 621 | // glfxImg(imgURL).then(url=>{ 622 | // dowmLoadFunc(url); 623 | // }); 624 | } 625 | 626 | //使用glfx.js给图片加亮度 627 | const glfxImg = img=>{ 628 | return new Promise((resolve,reject)=>{ 629 | let imgInstance=document.createElement('img'); 630 | imgInstance.src=img; 631 | imgInstance.onload=()=>{ 632 | let url=''; 633 | try { 634 | var canvas = fx.canvas(); 635 | } catch (e) { 636 | alert(e); 637 | reject(); 638 | } 639 | var texture = canvas.texture(imgInstance); 640 | canvas.draw(texture).unsharpMask(1, 1.5).update(); 641 | url= canvas.toDataURL('image/png'); 642 | console.log(url); 643 | resolve(url); 644 | } 645 | }) 646 | } 647 | 648 | //下载通用方法,模拟a标签 649 | const dowmLoadFunc=(url,MIME_TYPE='image/png')=>{ 650 | //创建一个a链接,模拟点击下载 651 | let dlLink = document.createElement('a'); 652 | let filename = '合成图_' + (new Date()).getTime() + '.png'; 653 | dlLink.download = filename; 654 | dlLink.href = url; 655 | dlLink.dataset.downloadurl = [MIME_TYPE, dlLink.download, dlLink.href].join(':'); 656 | document.body.appendChild(dlLink); 657 | dlLink.click(); 658 | document.body.removeChild(dlLink); 659 | } 660 | 661 | //画布缩放 662 | let setZoom=function(value){ 663 | _fabricInstance.setZoom(value); 664 | } 665 | 666 | //退出编辑模式 667 | let stopEditing =function(){ 668 | if (selectedObj && selectedObj.isType && selectedObj.isType("i-text")) 669 | { 670 | if (selectedObj.exitEditing) 671 | { 672 | console.log('stopEditing'); 673 | selectedObj.exitEditing(); 674 | } 675 | } 676 | } 677 | 678 | return { 679 | addObject: addObject, 680 | toJSon: toJSon, 681 | setActiveSelect: setActiveSelect, 682 | setCss: setCss, 683 | setZoom: setZoom, 684 | createImage: createImage, 685 | createTxt: createTxt, 686 | modifyCopyright: modifyCopyright, 687 | downLoadImage: downLoadImage, 688 | undo: _fabricInstance.undo, 689 | redo: _fabricInstance.redo, 690 | initialize: _fabricInstance.initialize, 691 | toggleVisiable: _fabricInstance.toggleVisiable, 692 | updateLevel: _fabricInstance.updateLevel, 693 | delItem: _fabricInstance.delItem, 694 | flipActiveObj: _fabricInstance.flipActiveObj, 695 | stopEditing: stopEditing 696 | }; 697 | }; 698 | export { 699 | fabricCommon 700 | } 701 | -------------------------------------------------------------------------------- /src/components/MicroEdit/fabricOperate.js: -------------------------------------------------------------------------------- 1 | import FontFaceObserver from "fontfaceobserver"; 2 | import initAligningGuidelines from "./guideLines.js"; 3 | import "fabric"; 4 | import rotateIcon from "@/assets/images/rotate-icon.svg"; 5 | import lockIcon from "@/assets/images/lock-icon.svg"; 6 | import lockOpenIcon from "@/assets/images/lock-open-icon.svg"; 7 | const rotateImg = new Image(); 8 | rotateImg.src = rotateIcon; 9 | const lockImage = new Image(); 10 | lockImage.src = lockIcon; 11 | const lockOpenImage = new Image(); 12 | lockOpenImage.src = lockOpenIcon; 13 | const fabric = window.fabric; 14 | let fabricObject = function(that, id = "canvas") { 15 | /* 默认fabric配置 */ 16 | let _fabricConfig = { 17 | backgroundColor: "#F7F8FF", 18 | preserveObjectStacking: true, 19 | selection: false, //取消框选 20 | }; 21 | // 设置复制itext文本不带格式 22 | fabric.disableStyleCopyPaste = true; 23 | 24 | fabric.Object.prototype.set({ 25 | borderColor: "rgba(0,0,0,0.8)", 26 | cornerColor: "rgba(0,0,0,0.8)", //激活状态角落图标的填充颜色 27 | cornerStrokeColor: "rgba(0,0,0,0.8)", //激活状态角落图标的边框颜色 28 | borderOpacityWhenMoving: 1, 29 | borderScaleFactor: 1, 30 | cornerSize: 8, 31 | cornerStyle: "circle", //rect,circle 32 | centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 33 | centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 34 | transparentCorners: false, //激活状态角落的图标是否透明 35 | rotatingPointOffset: 20, //旋转距旋转体的距离 36 | originX: "center", 37 | originY: "center", 38 | lockUniScaling: false, //只显示四角的操作 39 | hasRotatingPoint: true, //是否显示旋转按钮 40 | showLock: true, // 控制是否显示lock 41 | }); 42 | 43 | // 不显示m 44 | const notShowControls = ["ml", "mr", "mb", "mt"]; 45 | notShowControls.forEach((control) => { 46 | fabric.Object.prototype.controls[control].visible = false; 47 | }); 48 | 49 | // 重写mtr 的render函数 50 | fabric.Object.prototype.controls.mtr.render = function( 51 | ctx, 52 | left, 53 | top, 54 | styleOverride, 55 | fabricObject 56 | ) { 57 | styleOverride = styleOverride || {}; 58 | const transparentCorners = 59 | typeof styleOverride.transparentCorners !== "undefined" 60 | ? styleOverride.transparentCorners 61 | : fabricObject.transparentCorners; 62 | const stroke = 63 | !transparentCorners && 64 | (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor); 65 | ctx.save(); 66 | ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; 67 | ctx.strokeStyle = 68 | styleOverride.strokeCornerColor || fabricObject.strokeCornerColor; 69 | ctx.lineWidth = 1; 70 | ctx.translate(left, top); 71 | ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); 72 | if (stroke) { 73 | ctx.drawImage(rotateImg, -10, -12, 22, 22); 74 | } 75 | ctx.restore(); 76 | }; 77 | 78 | const lockIcon = { 79 | width: 22, 80 | height: 22, 81 | }; 82 | // 扩展 增加lock操作图标 可以在controls中增加自定义control 83 | fabric.Object.prototype.controls.lock = new fabric.Control({ 84 | visible: true, 85 | x: -0.5, 86 | y: -0.5, 87 | offsetX: +lockIcon.width + 5, 88 | offsetY: +lockIcon.height + 5, 89 | sizeX: lockIcon.width, 90 | sizeY: lockIcon.height, 91 | actionName: "lockgroup", 92 | mouseDownHandler: function(eventData, transformData, x, y) { 93 | const current = transformData.target; 94 | const lockStatus = current.get("lockStatus"); 95 | if (lockStatus === 1) { 96 | current.set("lockStatus", 0); 97 | } else { 98 | current.set("lockStatus", 1); 99 | } 100 | }, 101 | cursorStyle: "pointer", 102 | render: function(ctx, left, top, styleOverride, fabricObject) { 103 | if (!fabricObject.showLock) { 104 | return; 105 | } 106 | styleOverride = styleOverride || {}; 107 | const transparentCorners = 108 | typeof styleOverride.transparentCorners !== "undefined" 109 | ? styleOverride.transparentCorners 110 | : fabricObject.transparentCorners; 111 | const stroke = 112 | !transparentCorners && 113 | (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor); 114 | ctx.save(); 115 | ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; 116 | ctx.strokeStyle = 117 | styleOverride.strokeCornerColor || fabricObject.strokeCornerColor; 118 | ctx.lineWidth = 1; 119 | ctx.translate(left, top); 120 | ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); 121 | if (stroke) { 122 | ctx.drawImage( 123 | fabricObject.lockStatus === 1 ? lockOpenImage : lockImage, 124 | -lockIcon.width / 2, 125 | -lockIcon.height / 2, 126 | lockIcon.width, 127 | lockIcon.height 128 | ); 129 | } 130 | ctx.restore(); 131 | }, 132 | }); 133 | 134 | //重绘旋转按钮 135 | 136 | //是否是初始状态 137 | let isInitialStatus = true; 138 | 139 | /* fabric对象 */ 140 | let _fabricObj = new fabric.Canvas(id, _fabricConfig); 141 | //添加辅助线 142 | initAligningGuidelines(_fabricObj); 143 | 144 | //对fabric.Canvas扩展 145 | fabric.Canvas.prototype.setZoomByCenter = function(value) { 146 | this.zoomToPoint( 147 | new fabric.Point(_fabricObj.width / 2, _fabricObj.height / 2), 148 | value 149 | ); 150 | return this; 151 | }; 152 | 153 | //最后一次鼠标滑到的对象--用于鼠标移动到的元素边框显示--用于鼠标移动到元素展示虚线 154 | let lastMoved = {}; 155 | 156 | /* 导出json时需要导出的自定义字段 */ 157 | let _toJsonWithParams = []; 158 | 159 | /* 初始化 */ 160 | const _initConfig = function(config = {}) { 161 | Object.assign(_fabricConfig, config); 162 | fabricObj.setOptions(_fabricConfig); 163 | }; 164 | 165 | /* 创建图片 */ 166 | const _createImage = function( 167 | url, 168 | imgConfig, 169 | centerObject = false, 170 | hasControls = true 171 | ) { 172 | return new Promise((resolve, reject) => { 173 | try { 174 | let imgInstance = new fabric.Image.fromURL(url, function(img) { 175 | img.set(imgConfig); 176 | img 177 | .set({ 178 | scaleX: imgConfig.bboxWidth 179 | ? parseInt(imgConfig.bboxWidth) / img.width 180 | : 1, 181 | scaleY: imgConfig.bboxHeight 182 | ? parseInt(imgConfig.bboxHeight) / img.height 183 | : 1, 184 | realWidth: imgConfig.bboxWidth 185 | ? parseInt(imgConfig.bboxWidth) 186 | : parseInt(img.width), 187 | realHeight: imgConfig.bboxHeight 188 | ? parseInt(imgConfig.bboxHeight) 189 | : parseInt(img.height), 190 | oldWidth: parseInt(img.width), 191 | oldHeight: parseInt(img.height), 192 | }) 193 | .setCoords(); 194 | if (!hasControls) { 195 | img.hasControls = hasControls; 196 | img.set("lockRotation", true); 197 | img.set("lockScalingX", true); 198 | img.set("lockScalingY", true); 199 | img.set("lockMovementX", true); 200 | img.set("lockMovementY", true); 201 | } 202 | if (centerObject) { 203 | _fabricObj.centerObject(img); 204 | } 205 | resolve(img); 206 | _fabricObj.add(img); 207 | _fabricObj.renderAll(); 208 | }); 209 | } catch (e) { 210 | reject(); 211 | } 212 | }); 213 | }; 214 | 215 | /* 创建文本 */ 216 | const _createItext = function(title, txtConfig, centerObject = false) { 217 | let textInstance = new fabric.IText(title, txtConfig); 218 | if (centerObject) { 219 | _fabricObj.centerObject(textInstance); 220 | } 221 | _fabricObj.add(textInstance); 222 | _fabricObj.renderAll(); 223 | return textInstance; 224 | }; 225 | 226 | /* 画虚线框相关开始 */ 227 | //画虚线的列表--用于鼠标移动到的元素边框显示 228 | let _dottedLineList = []; 229 | //画曲线--用于鼠标移动到的元素边框显示 230 | let _drawDottedLine = function(fromX, fromY, toX, toY) { 231 | let canvasObject = new fabric.Line([fromX, fromY, toX, toY], { 232 | strokeDashArray: [3, 1], 233 | stroke: "red", 234 | strokeWidth: 1, 235 | }); 236 | _dottedLineList.push(canvasObject); 237 | _fabricObj.add(canvasObject); 238 | _fabricObj.renderAll(); 239 | }; 240 | 241 | //画一个对象的虚线边框--用于鼠标移动到的元素边框显示 242 | let _drawDottedBorder = function(aCoords) { 243 | _clearDotted(); 244 | let fx = aCoords.tl.x; 245 | let fy = aCoords.tl.y; 246 | let tx = aCoords.tr.x; 247 | let ty = aCoords.tr.y; 248 | _drawDottedLine(fx, fy, tx, ty); 249 | fx = aCoords.tr.x; 250 | fy = aCoords.tr.y; 251 | tx = aCoords.br.x; 252 | ty = aCoords.br.y; 253 | _drawDottedLine(fx, fy, tx, ty); 254 | fx = aCoords.br.x; 255 | fy = aCoords.br.y; 256 | tx = aCoords.bl.x; 257 | ty = aCoords.bl.y; 258 | _drawDottedLine(fx, fy, tx, ty); 259 | fx = aCoords.bl.x; 260 | fy = aCoords.bl.y; 261 | tx = aCoords.tl.x; 262 | ty = aCoords.tl.y; 263 | _drawDottedLine(fx, fy, tx, ty); 264 | }; 265 | 266 | //清除虚线框--用于鼠标移动到的元素边框显示 267 | let _clearDotted = function() { 268 | for (let i = 0; i < _dottedLineList.length; i++) { 269 | _fabricObj.remove(_dottedLineList[i]); 270 | } 271 | _dottedLineList = []; 272 | }; 273 | /* 画虚线框相关结束 */ 274 | 275 | /* 鼠标事件监听相关--开始 */ 276 | _fabricObj.on("object:modified", function(e) { 277 | if (_config.isStart) updateCanvasState(); 278 | }); 279 | _fabricObj.on("object:added", function() { 280 | if (_config.isStart) updateCanvasState(); 281 | }); 282 | _fabricObj.on("object:removed", function() { 283 | updateCanvasState(); 284 | }); 285 | _fabricObj.on("object:rotating", function() { 286 | updateCanvasState(); 287 | }); 288 | _fabricObj.on("mouse:down", (e) => { 289 | if (e && e.target && e.target.selectable) { 290 | } 291 | }); 292 | _fabricObj.on("mouse:move", function(e) { 293 | if (e && e.target) { 294 | } 295 | }); 296 | _fabricObj.on("mouse:up", (e) => { 297 | if (e && e.target) { 298 | } 299 | }); 300 | _fabricObj.on("object:selected", (e) => { 301 | if (e && e.target) { 302 | } 303 | }); 304 | /* 鼠标监控相关--结束 */ 305 | 306 | //撤销回退相关操作状态配置表 307 | let _config = { 308 | canvasState: [], //存储各状态的json数据 309 | currentStateIndex: -1, 310 | undoStatus: false, //撤销 311 | redoStatus: false, //回退 312 | undoFinishedStatus: 1, //撤销中的状态 313 | redoFinishedStatus: 1, //回退中的状态 314 | undoBtnStatus: false, //撤销按钮可点击状态 315 | redoBtnStatus: false, //回退按钮可点击状态 316 | initialNum: 0, 317 | isStart: false, 318 | }; 319 | //记录每步操作,用户撤销和回退操作 320 | let updateCanvasState = function() { 321 | if (_config.undoStatus == false && _config.redoStatus == false) { 322 | let jsonData = _fabricObj.toJSON(_toJsonWithParams); 323 | let canvasAsJson = JSON.stringify(jsonData); 324 | if (_config.currentStateIndex < _config.canvasState.length - 1) { 325 | //回退操作更新 326 | let oldData = JSON.parse(JSON.stringify(_config.canvasState)); 327 | _config.canvasState = oldData.splice(0, _config.currentStateIndex + 1); 328 | _config.canvasState.push(canvasAsJson); 329 | } else { 330 | _config.canvasState.push(canvasAsJson); 331 | } 332 | _config.currentStateIndex = _config.canvasState.length - 1; 333 | updateUnRedoBtnStatus(); 334 | if ( 335 | _config.currentStateIndex == _config.canvasState.length - 1 && 336 | _config.currentStateIndex == 0 337 | ) { 338 | return; 339 | } 340 | if (isInitialStatus) { 341 | isInitialStatus = false; 342 | updateInitialStatus(isInitialStatus); 343 | } 344 | } 345 | }; 346 | 347 | //更新撤销/回退的操作状态 348 | let updateUnRedoBtnStatus = function() { 349 | if (!that.updateUnRedoStatus) { 350 | return; 351 | } 352 | //撤销按钮状态更新 353 | if (_config.currentStateIndex <= 0) { 354 | _config.undoBtnStatus = false; 355 | } else { 356 | _config.undoBtnStatus = true; 357 | } 358 | //回退按钮状态更新 359 | if ( 360 | _config.canvasState.length > _config.currentStateIndex + 1 && 361 | _config.canvasState.length > 1 362 | ) { 363 | _config.redoBtnStatus = true; 364 | } else { 365 | _config.redoBtnStatus = false; 366 | } 367 | let btnStatus = { 368 | undoStatus: _config.undoBtnStatus, 369 | redoStatus: _config.redoBtnStatus, 370 | }; 371 | //更新主页面的btn状态 372 | that.updateUnRedoStatus(btnStatus); 373 | }; 374 | 375 | //撤销 376 | let undo = function() { 377 | let currentTarget = _fabricObj.getActiveObject(); 378 | if (_config.undoFinishedStatus) { 379 | if (_config.currentStateIndex == 0) { 380 | _config.undoStatus = false; 381 | } else { 382 | if (_config.canvasState.length >= 2) { 383 | _config.undoFinishedStatus = 0; 384 | _config.undoStatus = true; 385 | _fabricObj.loadFromJSON( 386 | _config.canvasState[_config.currentStateIndex - 1], 387 | function() { 388 | _fabricObj.renderAll(); 389 | _config.undoStatus = false; 390 | _config.currentStateIndex -= 1; 391 | _config.undoFinishedStatus = 1; 392 | _fabricObj.renderAll.bind(_fabricObj); 393 | undoFinish(); 394 | }, 395 | canvasLoadFromJsonTxtLoadFont 396 | ); 397 | } 398 | } 399 | } 400 | }; 401 | //恢复 402 | let redo = function() { 403 | if (_config.redoFinishedStatus) { 404 | if (_config.currentStateIndex < _config.canvasState.length - 1) { 405 | if ( 406 | _config.canvasState.length > _config.currentStateIndex + 1 && 407 | _config.canvasState.length > 1 408 | ) { 409 | _config.redoFinishedStatus = 0; 410 | _config.redoStatus = true; 411 | _fabricObj.loadFromJSON( 412 | _config.canvasState[_config.currentStateIndex + 1], 413 | function() { 414 | _fabricObj.renderAll(); 415 | _config.redoStatus = false; 416 | _config.currentStateIndex += 1; 417 | _config.redoFinishedStatus = 1; 418 | _fabricObj.renderAll.bind(_fabricObj); 419 | undoFinish(); 420 | }, 421 | canvasLoadFromJsonTxtLoadFont 422 | ); 423 | } 424 | } 425 | } 426 | }; 427 | 428 | //获取最新的层级关系列表 429 | let getNewLevelList = function() { 430 | let list = []; 431 | let objList = _fabricObj.getObjects(); 432 | for (let i = 0; i < objList.length; i++) { 433 | let obj = {}; 434 | obj.visiable = objList[i].visible; 435 | obj.text = objList[i].title; 436 | obj.id = i + 1; 437 | obj.type = objList[i].realType; 438 | list.push(obj); 439 | } 440 | return list; 441 | }; 442 | 443 | //撤销/回退操作完成回调 444 | let undoFinish = function() { 445 | //banner微编辑 446 | if (that.undoSuccess) { 447 | that.undoSuccess(getNewLevelList()); 448 | } 449 | //gif动图微编辑 450 | that.initialSuccessCallback && 451 | that.initialSuccessCallback(getNewEditLevelList()); 452 | updateUnRedoBtnStatus(); 453 | }; 454 | 455 | //gif动图,更新可编辑图层信息 456 | const getNewEditLevelList = () => { 457 | let newObject = {}; 458 | let txtList = []; 459 | let imgList = []; 460 | 461 | let objList = _fabricObj.getObjects(); 462 | for (let i = 0; i < objList.length; i++) { 463 | if (!objList[i].layerIsEdit || objList[i].layerIsEdit == "0") { 464 | continue; 465 | } 466 | if (objList[i].realType.indexOf("3-") < 0) { 467 | let obj = { 468 | type: objList[i].realType, 469 | order: objList[i].order, 470 | layerPictureUrl: objList[i].layerPictureUrl, 471 | }; 472 | imgList.push(obj); 473 | } else { 474 | let obj = { 475 | type: objList[i].realType, 476 | order: objList[i].order, 477 | title: objList[i].text, 478 | }; 479 | txtList.push(obj); 480 | } 481 | } 482 | newObject.imgList = imgList; 483 | newObject.txtList = txtList; 484 | return newObject; 485 | }; 486 | 487 | //调整层级关系 488 | let updateLevel = function(moveOrder, movedOrder) { 489 | moveOrder = parseInt(moveOrder); 490 | movedOrder = parseInt(movedOrder); 491 | let upLevels = _fabricObj.getObjects(); 492 | let currentUpdate = {}; 493 | if (moveOrder < movedOrder) { 494 | //往外层移动 495 | for (let i = moveOrder; i <= movedOrder; i++) { 496 | if (upLevels[i].order > moveOrder) { 497 | upLevels[i].order = parseInt(upLevels[i].order) - 1; 498 | } else if (upLevels[i].order == moveOrder) { 499 | upLevels[i].order = movedOrder; 500 | currentUpdate = upLevels[i]; 501 | } 502 | } 503 | //移动图层,向外移动多少层 504 | for (let i = moveOrder; i < movedOrder; i++) { 505 | _fabricObj.bringForward(currentUpdate); //上移 506 | } 507 | } else { 508 | //往内层移动 509 | for (let i = movedOrder; i <= moveOrder; i++) { 510 | if (upLevels[i].order < moveOrder) { 511 | upLevels[i].order = parseInt(upLevels[i].order) + 1; 512 | } else if (upLevels[i].order == moveOrder) { 513 | upLevels[i].order = movedOrder; 514 | currentUpdate = upLevels[i]; 515 | } 516 | } 517 | //移动图层,向内移动多少层 518 | for (let i = movedOrder; i < moveOrder; i++) { 519 | _fabricObj.sendBackwards(currentUpdate); //下移 520 | } 521 | } 522 | _fabricObj.renderAll(); 523 | updateCanvasState(); 524 | }; 525 | 526 | //重新加载相关数据初始化 527 | let initialize = function() { 528 | if (isInitialStatus) { 529 | return; 530 | } 531 | let json = _config.canvasState[0]; 532 | _fabricObj.clear(); 533 | _config.isStart = false; 534 | _config.currentStateIndex = -1; 535 | _config.canvasState = []; 536 | isInitialStatus = true; 537 | _fabricObj.loadFromJSON( 538 | json, 539 | () => { 540 | firstAddToState(true); 541 | updateCanvasState(); 542 | undoFinish(); 543 | }, 544 | canvasLoadFromJsonTxtLoadFont 545 | ); 546 | updateInitialStatus(isInitialStatus); 547 | }; 548 | 549 | //更新是否是初始状态 550 | let updateInitialStatus = function() { 551 | if (that.updateInitialStatus) { 552 | that.updateInitialStatus(isInitialStatus); 553 | } 554 | }; 555 | 556 | //获取画布图片的base64 557 | let getImgBase64Url = function() { 558 | let currentActive = _fabricObj.getObjects()[0]; 559 | let width = currentActive.oldWidth * currentActive.scaleX; 560 | let height = currentActive.oldHeight * currentActive.scaleY; 561 | let zoom = _fabricObj.getZoom(); 562 | if (zoom != 1) { 563 | setZoom(1); 564 | } 565 | //转换成base64 566 | let imgURL = _fabricObj.toDataURL({ 567 | format: "jpeg", 568 | quality: 1, 569 | multiplier: 1, 570 | left: 0, 571 | top: 0, 572 | width: width, 573 | height: height, 574 | }); 575 | setZoom(zoom); 576 | return imgURL; 577 | }; 578 | 579 | //设置缩放 580 | let setZoom = function(num) { 581 | _fabricObj.setZoomByCenter(num); 582 | }; 583 | 584 | //设置导出json时需要导出的自定义字段 585 | let setToJsonWithParams = function(list) { 586 | _toJsonWithParams = _toJsonWithParams.concat(list); 587 | }; 588 | 589 | //设置是否显示 590 | let toggleVisiable = function(order, value) { 591 | let objs = _fabricObj.getObjects().filter((e) => { 592 | return e && e.order == order; 593 | }); 594 | if (objs && objs.length > 0) { 595 | objs[0].set("visible", value); 596 | _fabricObj.renderAll(); 597 | updateCanvasState(); 598 | } 599 | }; 600 | 601 | //删除元素 602 | let delItem = function(order) { 603 | let objs = _fabricObj.getObjects().filter((e) => { 604 | return e && e.order == order; 605 | }); 606 | if (objs && objs.length > 0) { 607 | _fabricObj.remove(objs[0]); 608 | _fabricObj.renderAll(); 609 | resetOrderToMapper(order); 610 | } 611 | }; 612 | 613 | //删除时候用,删除层级以上的全部降一,已于当前getobjects()的层级相对应 614 | let resetOrderToMapper = function(order) { 615 | order = parseInt(order); 616 | for (let i = order; i < _fabricObj.getObjects().length; i++) { 617 | if (parseInt(_fabricObj.getObjects()[i].order) > order) { 618 | _fabricObj.getObjects()[i].order = i; 619 | } 620 | } 621 | }; 622 | 623 | //开始记录操作,用于撤销回退 624 | let firstAddToState = function(isStart = false) { 625 | _config.isStart = isStart; 626 | }; 627 | 628 | //设置选中项 629 | let setActiveSelect = function(order) { 630 | return new Promise((resolve, reject) => { 631 | let objs = _fabricObj.getObjects().filter((e) => { 632 | return e && e.order == order; 633 | }); 634 | if (objs && objs.length > 0) { 635 | _fabricObj.setActiveObject(objs[0]); 636 | resolve(); 637 | } else { 638 | reject(); 639 | } 640 | }); 641 | }; 642 | 643 | //获取当前选中的元素 644 | let getActiveObj = function() { 645 | return _fabricObj.getActiveObject(); 646 | }; 647 | 648 | //设置元素属性值 649 | let setActiveObjCss = function(obj) { 650 | let currentActive = getActiveObj(); 651 | if (currentActive) { 652 | // currentActive.setOptions(obj) 653 | currentActive.set(obj.cssName, obj.cssVlaue).setCoords(); 654 | _fabricConfig.renderAll(); 655 | } 656 | }; 657 | 658 | //翻转元素,flip=flipX,水平翻转,flip=flipY垂直旋转 659 | let _flipActiveObj = function(flip) { 660 | let activeObj = getActiveObj(); 661 | if (activeObj) { 662 | let fX = activeObj[flip]; 663 | activeObj.set(flip, fX).setCoords(); 664 | } 665 | }; 666 | 667 | //加载json到canvas时加载每个元素的事件,用于异步加载字体 668 | let canvasLoadFromJsonTxtLoadFont = function(o, object) { 669 | if ( 670 | object.isType("i-text") && 671 | o.fontFamily == "" && 672 | o.realFontFamily != "" 673 | ) { 674 | _loadFont(object, o.realFontFamily); 675 | } 676 | }; 677 | 678 | //加载字体文件 679 | let _loadFont = function(instance, fontFamily) { 680 | let font = fontFamily; 681 | var myfont = new FontFaceObserver(font); 682 | myfont 683 | .load(null, 100000) 684 | .then(function() { 685 | console.log(instance, font); 686 | instance.set("fontFamily", font); 687 | _fabricObj.requestRenderAll(); 688 | _loadFontSuccess(instance); 689 | }) 690 | .catch(function(e) { 691 | console.log(e); 692 | }); 693 | }; 694 | 695 | //加载字体成功事件回调 696 | let _loadFontSuccess = function(currentActive) { 697 | let text = currentActive.text; 698 | currentActive.set("text", "").setCoords(); 699 | currentActive.set("text", text).setCoords(); 700 | _fabricObj.renderAll(); 701 | }; 702 | 703 | return { 704 | initFabricConfig: _initConfig, 705 | fabricInstance: _fabricObj, 706 | updateLevel: updateLevel, 707 | initialize: initialize, 708 | getImgBase64Url: getImgBase64Url, 709 | setZoom: setZoom, 710 | setToJsonWithParams: setToJsonWithParams, 711 | toggleVisiable: toggleVisiable, 712 | delItem: delItem, 713 | firstAddToState: firstAddToState, 714 | createImage: _createImage, 715 | createItext: _createItext, 716 | flipActiveObj: _flipActiveObj, 717 | updateCanvasState: updateCanvasState, 718 | setActiveSelect: setActiveSelect, 719 | getActiveObj: getActiveObj, 720 | setActiveObjCss: setActiveObjCss, 721 | undo: undo, 722 | redo: redo, 723 | loadFont: _loadFont, 724 | }; 725 | }; 726 | export { fabricObject }; 727 | -------------------------------------------------------------------------------- /src/components/MicroEdit/guideLines.js: -------------------------------------------------------------------------------- 1 | function initAligningGuidelines(canvas) { 2 | 3 | var ctx = canvas.getSelectionContext(), 4 | aligningLineOffset = 0, 5 | aligningLineMargin = 0, 6 | aligningLineWidth = 1, 7 | aligningLineColor = 'rgb(0,0,0)', 8 | viewportTransform, 9 | zoom = 1; 10 | 11 | function drawVerticalLine(coords) { 12 | drawLine( 13 | coords.x + 0.5, 14 | coords.y1 > coords.y2 ? coords.y2 : coords.y1, 15 | coords.x + 0.5, 16 | coords.y2 > coords.y1 ? coords.y2 : coords.y1); 17 | } 18 | 19 | function drawHorizontalLine(coords) { 20 | drawLine( 21 | coords.x1 > coords.x2 ? coords.x2 : coords.x1, 22 | coords.y + 0.5, 23 | coords.x2 > coords.x1 ? coords.x2 : coords.x1, 24 | coords.y + 0.5); 25 | } 26 | 27 | function drawLine(x1, y1, x2, y2) { 28 | ctx.save(); 29 | ctx.lineWidth = aligningLineWidth; 30 | ctx.strokeStyle = aligningLineColor; 31 | ctx.beginPath(); 32 | ctx.moveTo(((x1+viewportTransform[4])*zoom), ((y1+viewportTransform[5])*zoom)); 33 | ctx.lineTo(((x2+viewportTransform[4])*zoom), ((y2+viewportTransform[5])*zoom)); 34 | ctx.stroke(); 35 | ctx.restore(); 36 | } 37 | 38 | function isInRange(value1, value2) { 39 | value1 = Math.round(value1); 40 | value2 = Math.round(value2); 41 | for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { 42 | if (i === value2) { 43 | return true; 44 | } 45 | } 46 | return false; 47 | } 48 | 49 | var verticalLines = [], 50 | horizontalLines = []; 51 | 52 | canvas.on('mouse:down', function () { 53 | viewportTransform = canvas.viewportTransform; 54 | zoom = canvas.getZoom(); 55 | }); 56 | 57 | canvas.on('object:moving', function(e) { 58 | 59 | var activeObject = e.target, 60 | canvasObjects = canvas.getObjects(), 61 | activeObjectCenter = activeObject.getCenterPoint(), 62 | activeObjectLeft = activeObjectCenter.x, 63 | activeObjectTop = activeObjectCenter.y, 64 | activeObjectBoundingRect = activeObject.getBoundingRect(), 65 | activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], 66 | activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], 67 | horizontalInTheRange = false, 68 | verticalInTheRange = false, 69 | transform = canvas._currentTransform; 70 | 71 | if (!transform) return; 72 | 73 | // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, 74 | // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move 75 | 76 | for (var i = canvasObjects.length; i--; ) { 77 | 78 | if (canvasObjects[i] === activeObject) continue; 79 | 80 | var objectCenter = canvasObjects[i].getCenterPoint(), 81 | objectLeft = objectCenter.x, 82 | objectTop = objectCenter.y, 83 | objectBoundingRect = canvasObjects[i].getBoundingRect(), 84 | objectHeight = objectBoundingRect.height / viewportTransform[3], 85 | objectWidth = objectBoundingRect.width / viewportTransform[0]; 86 | 87 | // snap by the horizontal center line 88 | if (isInRange(objectLeft, activeObjectLeft)) { 89 | verticalInTheRange = true; 90 | verticalLines.push({ 91 | x: objectLeft, 92 | y1: (objectTop < activeObjectTop) 93 | ? (objectTop - objectHeight / 2 - aligningLineOffset) 94 | : (objectTop + objectHeight / 2 + aligningLineOffset), 95 | y2: (activeObjectTop > objectTop) 96 | ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) 97 | : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) 98 | }); 99 | activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); 100 | } 101 | 102 | // snap by the left edge 103 | if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { 104 | verticalInTheRange = true; 105 | verticalLines.push({ 106 | x: objectLeft - objectWidth / 2, 107 | y1: (objectTop < activeObjectTop) 108 | ? (objectTop - objectHeight / 2 - aligningLineOffset) 109 | : (objectTop + objectHeight / 2 + aligningLineOffset), 110 | y2: (activeObjectTop > objectTop) 111 | ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) 112 | : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) 113 | }); 114 | activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); 115 | } 116 | 117 | // snap by the right edge 118 | if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { 119 | verticalInTheRange = true; 120 | verticalLines.push({ 121 | x: objectLeft + objectWidth / 2, 122 | y1: (objectTop < activeObjectTop) 123 | ? (objectTop - objectHeight / 2 - aligningLineOffset) 124 | : (objectTop + objectHeight / 2 + aligningLineOffset), 125 | y2: (activeObjectTop > objectTop) 126 | ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) 127 | : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) 128 | }); 129 | activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); 130 | } 131 | 132 | // snap by the vertical center line 133 | if (isInRange(objectTop, activeObjectTop)) { 134 | horizontalInTheRange = true; 135 | horizontalLines.push({ 136 | y: objectTop, 137 | x1: (objectLeft < activeObjectLeft) 138 | ? (objectLeft - objectWidth / 2 - aligningLineOffset) 139 | : (objectLeft + objectWidth / 2 + aligningLineOffset), 140 | x2: (activeObjectLeft > objectLeft) 141 | ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) 142 | : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) 143 | }); 144 | activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); 145 | } 146 | 147 | // snap by the top edge 148 | if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { 149 | horizontalInTheRange = true; 150 | horizontalLines.push({ 151 | y: objectTop - objectHeight / 2, 152 | x1: (objectLeft < activeObjectLeft) 153 | ? (objectLeft - objectWidth / 2 - aligningLineOffset) 154 | : (objectLeft + objectWidth / 2 + aligningLineOffset), 155 | x2: (activeObjectLeft > objectLeft) 156 | ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) 157 | : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) 158 | }); 159 | activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); 160 | } 161 | 162 | // snap by the bottom edge 163 | if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { 164 | horizontalInTheRange = true; 165 | horizontalLines.push({ 166 | y: objectTop + objectHeight / 2, 167 | x1: (objectLeft < activeObjectLeft) 168 | ? (objectLeft - objectWidth / 2 - aligningLineOffset) 169 | : (objectLeft + objectWidth / 2 + aligningLineOffset), 170 | x2: (activeObjectLeft > objectLeft) 171 | ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) 172 | : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) 173 | }); 174 | activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); 175 | } 176 | } 177 | 178 | if (!horizontalInTheRange) { 179 | horizontalLines.length = 0; 180 | } 181 | 182 | if (!verticalInTheRange) { 183 | verticalLines.length = 0; 184 | } 185 | }); 186 | 187 | canvas.on('before:render', function() { 188 | if(canvas&&canvas.contextTop) 189 | { 190 | canvas.clearContext(canvas.contextTop); 191 | } 192 | }); 193 | 194 | canvas.on('after:render', function() { 195 | for (var i = verticalLines.length; i--; ) { 196 | drawVerticalLine(verticalLines[i]); 197 | } 198 | for (var i = horizontalLines.length; i--; ) { 199 | drawHorizontalLine(horizontalLines[i]); 200 | } 201 | 202 | verticalLines.length = horizontalLines.length = 0; 203 | }); 204 | 205 | canvas.on('mouse:up', function() { 206 | verticalLines.length = horizontalLines.length = 0; 207 | canvas.renderAll(); 208 | }); 209 | } 210 | 211 | export default initAligningGuidelines; -------------------------------------------------------------------------------- /src/components/MicroEdit/index.js: -------------------------------------------------------------------------------- 1 | import MicroEdit from './MicroEdit' 2 | export default MicroEdit -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | 2 | export default(Vue) => { 3 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from '@/router' 4 | import components from './components' 5 | import ElementUI,{Message} from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | import '@/assets/styles/base.css' 8 | import '@/assets/styles/common.css' 9 | 10 | Vue.config.productionTip = false 11 | Vue.use(components) 12 | Vue.use(ElementUI) 13 | Vue.prototype.$message = Message 14 | 15 | new Vue({ 16 | render: h => h(App), 17 | router 18 | }).$mount('#app') 19 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | let router = new Router({ 7 | mode: 'hash', 8 | routes: [ 9 | { 10 | path: '/home', 11 | name: 'home', 12 | component: () => import("@/views/Home/Home") 13 | }, 14 | { 15 | path: '*', 16 | component: () => import("@/views/Home/Home") 17 | } 18 | ] 19 | }) 20 | router.beforeEach((to, from, next) => { 21 | next(); 22 | }) 23 | export default router 24 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | 5 | Vue.use(Vuex) 6 | let state = {} 7 | let getters = {} 8 | let mutations = {} 9 | let actions = {} 10 | export default new Vuex.Store({ 11 | state, 12 | getters, 13 | mutations, 14 | actions, 15 | modules: { 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /src/store/mutationTypes.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeff-Bee/onLinePS/79737fa5333da831eeee28824c28204521f8fd55/src/store/mutationTypes.js -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | let qs = require('querystring') 2 | export const getQueryStringFromJson = function (json) { 3 | return qs.stringify(json) 4 | } 5 | 6 | // 时间格式化-yyyy-MM-dd 00or24 7 | export const getFormatYMDDateStr = function (date, isEnd = false) { 8 | if (!date) { 9 | return '' 10 | } 11 | var time = new Date(date) 12 | return `${time.getFullYear()}-${(time.getMonth() + 1)}-${time.getDate()} ${isEnd ? '23:59:59' : '00:00:00'}` 13 | } 14 | 15 | //格式化str时间为yyyy-MM-dd的字符串 16 | export const formatStrDataToYMDStr = function (date) { 17 | if (!date) { 18 | return '' 19 | } 20 | date = date.replace(/-/g, '/'); 21 | let time = new Date(date); 22 | return `${time.getFullYear()}-${(time.getMonth() + 1)}-${time.getDate()}` 23 | } 24 | 25 | // 判断是否是ie浏览器 26 | export const isIE = () => { 27 | if (window.navigator.userAgent.indexOf('MSIE') >= 1 || (!!window.ActiveXObject || 'ActiveXObject' in window)) { 28 | return true 29 | } 30 | return false 31 | } 32 | 33 | //创建全局遮罩层 34 | export const createMask=function(data){ 35 | let mask=document.createElement('div'); 36 | mask.style.cssText ="position: fixed;left: 0;top: 0;right: 0;bottom: 0;z-index: 1110;background:#fff;opacity:0;"; 37 | document.body.appendChild(mask); 38 | return mask; 39 | } 40 | 41 | // 判断是否是safari 42 | export const isSafari = () => { 43 | if (/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)) { 44 | return true 45 | } 46 | return false 47 | } 48 | // 判断是否是FF 49 | export const isFireFox = () => { 50 | if (/Firefox/.test(navigator.userAgent)) { 51 | return true 52 | } 53 | return false 54 | } 55 | 56 | // 判断是否是Chrome 57 | export const isChrome = () => { 58 | if (/Chrome/.test(navigator.userAgent)) { 59 | return true 60 | } 61 | return false 62 | } -------------------------------------------------------------------------------- /src/views/Home/Home.vue: -------------------------------------------------------------------------------- 1 | 11 | 49 | -------------------------------------------------------------------------------- /src/views/Home/index.js: -------------------------------------------------------------------------------- 1 | import Home from './Home.vue' 2 | export { 3 | Home 4 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 2 | const CompressionWebpackPlugin = require('compression-webpack-plugin'); 3 | 4 | module.exports = { 5 | publicPath:'/', 6 | assetsDir:'static', 7 | devServer: { 8 | disableHostCheck: true 9 | }, 10 | productionSourceMap: false, 11 | configureWebpack:{ 12 | optimization: { 13 | minimizer: [ 14 | new UglifyJsPlugin({ 15 | uglifyOptions: { 16 | warnings: false, 17 | compress: { 18 | drop_console: true,//console 19 | drop_debugger: true, 20 | pure_funcs: ['console.log']//移除console 21 | } 22 | } 23 | }) 24 | ] 25 | }, 26 | plugins: [ 27 | new CompressionWebpackPlugin({ 28 | test:/\.js$|\.html$|\.css$/, //匹配文件名 29 | threshold: 10240,//对超过10k的数据压缩 30 | deleteOriginalAssets: false //不删除源文件 31 | }) 32 | ] 33 | } 34 | } --------------------------------------------------------------------------------