├── src ├── ingredients │ ├── components.ts │ ├── export │ │ ├── cssTemplate.ts │ │ ├── jsTemplate.ts │ │ ├── htmlTemplate.ts │ │ ├── index.ts │ │ └── resetCssTemplate.ts │ ├── elements │ │ ├── index.ts │ │ ├── Text.ts │ │ ├── Button.ts │ │ ├── Img.ts │ │ ├── Circle.ts │ │ └── Input.ts │ ├── components │ │ └── index.ts │ ├── layouts │ │ ├── LeftBox.ts │ │ ├── RightBox.ts │ │ ├── FixedBox.ts │ │ ├── BaseBox.ts │ │ ├── CeilingBox.ts │ │ ├── index.ts │ │ ├── PageTpl2.ts │ │ └── PageTpl1.ts │ └── cookEngine.ts ├── assets │ ├── img │ │ ├── wg.png │ │ ├── grid.png │ │ └── wg.svg │ ├── global.css │ └── normalize.css ├── pages │ ├── Preview │ │ ├── index.module.less │ │ └── index.tsx │ └── Home │ │ └── index.tsx ├── db │ ├── connect.ts │ ├── db_config.ts │ ├── state.ts │ └── use.ts ├── routes.ts ├── app.tsx ├── components │ ├── RulerLine │ │ ├── index.tsx │ │ └── index.module.less │ ├── EditPage │ │ ├── ActionPanel.tsx │ │ ├── UploadImg.tsx │ │ ├── ZoomPlugin.tsx │ │ ├── index.module.less │ │ ├── index.tsx │ │ ├── EditLineBox.tsx │ │ └── StylePanel.tsx │ ├── ToolBarBox │ │ ├── index.tsx │ │ └── index.module.less │ └── PageBoard │ │ ├── index.module.less │ │ └── index.tsx ├── utils │ ├── tool.js │ ├── idb.js │ ├── drag.js │ └── styleFormat.js └── typings.d.ts ├── .stylelintignore ├── public ├── fonts │ ├── fa-regular-400.ttf │ └── fa-solid-900.woff2 ├── index.html ├── favicon.png └── libs │ ├── FileSaver.min.js │ └── panzoom.min.js ├── .prettierrc.js ├── .stylelintrc.js ├── .prettierignore ├── .eslintignore ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── tsconfig.json ├── README.md └── package.json /src/ingredients/components.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/wg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullno/cook-web/HEAD/src/assets/img/wg.png -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # 忽略目录 2 | build/ 3 | tests/ 4 | demo/ 5 | 6 | # node 覆盖率文件 7 | coverage/ 8 | -------------------------------------------------------------------------------- /src/assets/img/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullno/cook-web/HEAD/src/assets/img/grid.png -------------------------------------------------------------------------------- /src/pages/Preview/index.module.less: -------------------------------------------------------------------------------- 1 | .cookPreview { 2 | width: 100%; 3 | overflow: hidden; 4 | } 5 | -------------------------------------------------------------------------------- /public/fonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullno/cook-web/HEAD/public/fonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /public/fonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullno/cook-web/HEAD/public/fonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const { getPrettierConfig } = require('@iceworks/spec'); 2 | 3 | module.exports = getPrettierConfig('react'); 4 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const { getStylelintConfig } = require('@iceworks/spec');; 2 | 3 | module.exports = getStylelintConfig('react'); 4 | -------------------------------------------------------------------------------- /src/db/connect.ts: -------------------------------------------------------------------------------- 1 | import Idb from 'idb-js' 2 | 3 | import db_config from './db_config'; 4 | 5 | export default Idb(db_config); 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | tests/ 3 | demo/ 4 | .ice/ 5 | coverage/ 6 | **/*-min.js 7 | **/*.min.js 8 | package-lock.json 9 | yarn.lock -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # 忽略目录 2 | build/ 3 | tests/ 4 | demo/ 5 | .ice/ 6 | 7 | # node 覆盖率文件 8 | coverage/ 9 | 10 | # 忽略文件 11 | **/*-min.js 12 | **/*.min.js 13 | -------------------------------------------------------------------------------- /src/ingredients/export/cssTemplate.ts: -------------------------------------------------------------------------------- 1 | export default (page, cssStr) => { 2 | return ` 3 | /* 4 | *@File css ${page.id} 5 | *@Date ${Date()} 6 | */ 7 | ${cssStr} 8 | ` 9 | } 10 | -------------------------------------------------------------------------------- /src/ingredients/export/jsTemplate.ts: -------------------------------------------------------------------------------- 1 | export default (page, jsStr) => { 2 | return ` 3 | /* 4 | *@File js ${page.id} 5 | *@Date ${Date()} 6 | *@Plugins [] 7 | */ 8 | ${jsStr} 9 | ` 10 | } 11 | -------------------------------------------------------------------------------- /src/ingredients/elements/index.ts: -------------------------------------------------------------------------------- 1 | const modules = import.meta.globEager("./*.ts"); 2 | 3 | const Elements: DomItem[] = []; 4 | for (let m in modules) { 5 | Elements.push(modules[m].default); 6 | } 7 | 8 | export default Elements; 9 | -------------------------------------------------------------------------------- /src/ingredients/components/index.ts: -------------------------------------------------------------------------------- 1 | const modules = import.meta.globEager("./*.ts"); 2 | 3 | const Components: DomItem[] = []; 4 | for (let m in modules) { 5 | Components.push(modules[m].default); 6 | } 7 | 8 | export default Components; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/ingredients/elements/Text.ts: -------------------------------------------------------------------------------- 1 | const Text = { 2 | type: 'Text', 3 | name: '文本', 4 | tag: 'span', 5 | text:'文本', 6 | style: { 7 | color: '#000', 8 | fontSize: '14px', 9 | outline:'none' 10 | }, 11 | stopAdd: true, 12 | edit: {}, 13 | }; 14 | export default Text; 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { getESLintConfig } = require('@iceworks/spec'); 2 | 3 | // https://www.npmjs.com/package/@iceworks/spec 4 | module.exports = getESLintConfig('react-ts', { 5 | rules: { 6 | 'react/jsx-filename-extension': 0, 7 | '@typescript-eslint/explicit-function-return-type': 0, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/ingredients/layouts/LeftBox.ts: -------------------------------------------------------------------------------- 1 | const LeftBox = { 2 | type: 'LeftBox', 3 | name: '居左', 4 | tag: 'div', 5 | dragDots: ['right', 'bottom'], 6 | style: { 7 | width: '100px', 8 | height: '100%', 9 | float: 'left', 10 | border: '1px dashed #EE771F', 11 | }, 12 | child: [], 13 | }; 14 | export default LeftBox; 15 | -------------------------------------------------------------------------------- /src/ingredients/layouts/RightBox.ts: -------------------------------------------------------------------------------- 1 | const RightBox = { 2 | type: 'RightBox', 3 | name: '居右', 4 | tag: 'div', 5 | dragDots: ['left', 'bottom'], 6 | style: { 7 | width: '100px', 8 | height: '100%', 9 | float: 'right', 10 | border: '1px dashed #EE771F', 11 | }, 12 | child: [], 13 | }; 14 | export default RightBox; 15 | -------------------------------------------------------------------------------- /src/ingredients/layouts/FixedBox.ts: -------------------------------------------------------------------------------- 1 | const FixedBox = { 2 | type: 'FixedBox', 3 | name: '悬浮框', 4 | tag: 'div', 5 | style: { 6 | width: '100px', 7 | height: '100px', 8 | backgroundColor: '#fff', 9 | border: '1px solid #EE771F', 10 | margin: '0 auto', 11 | position: 'fixed' 12 | }, 13 | child: [], 14 | }; 15 | export default FixedBox; 16 | -------------------------------------------------------------------------------- /src/ingredients/elements/Button.ts: -------------------------------------------------------------------------------- 1 | const Button = { 2 | type: 'Button', 3 | name: '按钮', 4 | tag: 'button', 5 | text: '按钮', 6 | style: { 7 | minWidth: '100px', 8 | height: '40px', 9 | backgroundColor: '#3388FF', 10 | color: '#fff', 11 | borderRadius: '3px', 12 | padding: '0 5px' 13 | }, 14 | edit: {}, 15 | }; 16 | export default Button; 17 | -------------------------------------------------------------------------------- /src/ingredients/layouts/BaseBox.ts: -------------------------------------------------------------------------------- 1 | const BaseBox = { 2 | type: 'BaseBox', 3 | name: '容器', 4 | tag: 'div', 5 | dragDots: ['left', 'right', 'bottom'], 6 | style: { 7 | width: '100%', 8 | height: '200px', 9 | backgroundColor: '#EDEDED', 10 | margin: '0 auto', 11 | overflow: 'hidden' 12 | }, 13 | child: [], 14 | }; 15 | export default BaseBox; 16 | -------------------------------------------------------------------------------- /src/ingredients/elements/Img.ts: -------------------------------------------------------------------------------- 1 | const Img = { 2 | type: 'Img', 3 | name: '图片', 4 | tag: 'img', 5 | src: 'https://resource.smartisan.com/resource/fdc4370d1ce14a67fadc35d74209ac0f.jpg?x-oss-process=image/resize,w_600/format,webp', 6 | dragDots: ['right', 'bottom'], 7 | stopAdd: true, 8 | style: { 9 | width: '100%', 10 | }, 11 | edit: {}, 12 | }; 13 | export default Img; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # production 7 | build/ 8 | dist/ 9 | DIST/ 10 | tmp/ 11 | lib/ 12 | 13 | # misc 14 | .idea/ 15 | .happypack 16 | .DS_Store 17 | *.swp 18 | *.dia~ 19 | .ice 20 | .vscode 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | index.module.scss.d.ts 26 | yarn.lock 27 | -------------------------------------------------------------------------------- /src/ingredients/elements/Circle.ts: -------------------------------------------------------------------------------- 1 | const Circle = { 2 | type: 'Circle', 3 | name: '圆形', 4 | tag: 'div', 5 | stopBefore: true, 6 | stopAfter: true, 7 | equalRatio: true, 8 | dragDots: ['right', 'bottom'], 9 | style: { 10 | width: '50px', 11 | height: '50px', 12 | backgroundColor: '#ccc', 13 | borderRadius: '100%', 14 | }, 15 | child: [], 16 | edit: {}, 17 | }; 18 | export default Circle; 19 | -------------------------------------------------------------------------------- /src/ingredients/elements/Input.ts: -------------------------------------------------------------------------------- 1 | const Input = { 2 | type: 'Input', 3 | name: '输入框', 4 | tag: 'input', 5 | placeholder: 'please input...', 6 | style: { 7 | minWidth: '100px', 8 | height: '40px', 9 | lineHeight: '40px', 10 | border: '1px solid #3388FF', 11 | borderRadius: '3px', 12 | padding: '0 5px', 13 | fontSize: '14px', 14 | color: '#666' 15 | }, 16 | edit: {}, 17 | }; 18 | export default Input; 19 | -------------------------------------------------------------------------------- /src/ingredients/layouts/CeilingBox.ts: -------------------------------------------------------------------------------- 1 | const CeilingBox = { 2 | type: 'CeilingBox', 3 | name: '吸顶栏', 4 | tag: 'div', 5 | dragDots: ['bottom'], 6 | style: { 7 | width: '100%', 8 | height: '80px', 9 | backgroundColor: '#fff', 10 | border: '1px solid #EE771F', 11 | margin: '0 auto', 12 | position: 'fixed', 13 | top: '0px', 14 | left: '0px', 15 | zIndex: '9' 16 | }, 17 | child: [], 18 | }; 19 | export default CeilingBox; 20 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | cook-web 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import { IRouterConfig, lazy } from 'ice'; 2 | 3 | const Home = lazy(() => import('@/pages/Home')); 4 | const Preview = lazy(() => import('@/pages/Preview')); 5 | 6 | const routerConfig: IRouterConfig[] = [ 7 | { 8 | path: '/', 9 | component: Home, 10 | exact: true, 11 | pageConfig: { 12 | title: 'cooking' 13 | } 14 | }, 15 | { 16 | path: '/preview/:pid', 17 | component: Preview, 18 | exact: true, 19 | // pageConfig: { 20 | // title: '预览' 21 | // } 22 | }, 23 | 24 | ]; 25 | 26 | export default routerConfig; 27 | -------------------------------------------------------------------------------- /src/ingredients/layouts/index.ts: -------------------------------------------------------------------------------- 1 | // const modules = import.meta.globEager("./*.ts"); 2 | 3 | // const Layouts: DomItem[] = []; 4 | // for (let m in modules) { 5 | // Layouts.push(modules[m].default); 6 | // } 7 | // export default Layouts; 8 | import BaseBox from "./BaseBox"; 9 | import LeftBox from "./LeftBox"; 10 | import RightBox from "./RightBox"; 11 | import FixedBox from "./FixedBox"; 12 | import CeilingBox from "./CeilingBox"; 13 | import PageTpl1 from "./PageTpl1"; 14 | import PageTpl2 from "./PageTpl2"; 15 | 16 | export default [BaseBox,LeftBox,RightBox,FixedBox,CeilingBox,PageTpl1,PageTpl2]; 17 | -------------------------------------------------------------------------------- /src/pages/Preview/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.less'; 2 | import { useParams } from 'ice'; 3 | import { useMount, useTitle } from 'ahooks'; 4 | import cookEngine from '@/ingredients/cookEngine' 5 | import STATE from '@/db/state'; 6 | 7 | interface pageParam { 8 | pid: string 9 | } 10 | const Preview = (props) => { 11 | const params: pageParam = useParams(); 12 | const page = STATE.DOM_DATA.pages[params.pid]; 13 | useTitle('预览-' + page.title); 14 | useMount(() => { 15 | cookEngine.preview(page, document.querySelector('#CookPreview')!) 16 | }) 17 | return
; 18 | }; 19 | 20 | export default Preview; 21 | -------------------------------------------------------------------------------- /src/ingredients/export/htmlTemplate.ts: -------------------------------------------------------------------------------- 1 | export default (page, htmlStr) => { 2 | return ` 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ${page.title} 16 | 17 | 18 |
19 | ${htmlStr} 20 |
21 | 22 | 23 | 24 | ` 25 | } 26 | -------------------------------------------------------------------------------- /src/db/db_config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | dbName: "cookEdit", // *数据库名称 3 | version: 1, // 数据库版本号(默认为当前时间戳) 4 | tables: [ // *数据库的表,即ObjectStore 5 | { 6 | tableName: "dom_sate", // *表名 7 | option: { keyPath: "id" }, // 表配置,即ObjectStore配置,此处指明主键为id 8 | indexs: [ // 数据库索引(建议加上索引) 9 | { 10 | key: "id", // *索引名 11 | option: { // 索引配置,此处表示该字段不允许重复 12 | unique: true 13 | } 14 | }, 15 | { 16 | key: "page_use" 17 | }, 18 | { 19 | key: "dom_tree" 20 | } 21 | ] 22 | }, 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: weijundong 3 | * @Date: 2022-04-19 15:12:59 4 | * @LastEditors: weijundong 5 | * @LastEditTime: 2022-05-27 09:33:19 6 | * @FilePath: \cook-web\src\pages\Home\index.tsx 7 | */ 8 | import { useEventEmitter } from 'ahooks'; 9 | 10 | import PageBoard from '@/components/PageBoard'; 11 | 12 | import RulerLine from '@/components/RulerLine'; 13 | 14 | import ToolBarBox from '@/components/ToolBarBox'; 15 | 16 | import EditPage from '@/components/EditPage'; 17 | 18 | const Home = () => { 19 | const $switchPage = useEventEmitter(); 20 | return
21 | {/* 页面管理 */} 22 | 23 | {/* 页面编辑容器 */} 24 | 25 | {/* 工具栏 */} 26 | 27 | {/* 辅助线 */} 28 | 29 | 30 |
; 31 | }; 32 | 33 | export default Home; 34 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { runApp, IAppConfig } from 'ice'; 2 | import { Spin } from 'antd'; 3 | import connectDB from './db/connect' 4 | import useDB from './db/use' 5 | import STATE from './db/state' 6 | 7 | import 'antd/dist/antd.css'; 8 | import '@/assets/global.css'; 9 | 10 | const appConfig: IAppConfig = { 11 | app: { 12 | rootId: 'ice-container', 13 | }, 14 | router: { 15 | type: 'hash', 16 | basename: '/', 17 | fallback: , 18 | modifyRoutes: (routes) => { 19 | return routes; 20 | } 21 | } 22 | }; 23 | 24 | connectDB.then(async (mydb) => { 25 | STATE.MyDB = mydb; 26 | const Res = await useDB.myStore() as DBStore; 27 | if (!Res) { 28 | await useDB.setStore(); 29 | } else { 30 | STATE.PAGE_USE = Res.page_use; 31 | STATE.DOM_DATA = Res.dom_tree; 32 | STATE.CUR_PAGE = STATE.DOM_DATA.pages[STATE.PAGE_USE]; 33 | } 34 | runApp(appConfig); 35 | }) 36 | 37 | 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "buildOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "build", 7 | "module": "esnext", 8 | "target": "es6", 9 | "jsx": "react-jsx", 10 | "moduleResolution": "node", 11 | "allowSyntheticDefaultImports": true, 12 | "lib": ["es6", "dom"], 13 | "sourceMap": true, 14 | "allowJs": true, 15 | "rootDir": "./", 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noImplicitAny": false, 20 | "importHelpers": true, 21 | "strictNullChecks": true, 22 | "suppressImplicitAnyIndexErrors": true, 23 | "noUnusedLocals": true, 24 | "skipLibCheck": true, 25 | "types": ["node","vite/client","react-ace"], 26 | "paths": { 27 | "@/*": ["./src/*"], 28 | "ice": [".ice"] 29 | } 30 | }, 31 | "include": ["src", ".ice"], 32 | "exclude": ["node_modules", "build", "public"] 33 | } 34 | -------------------------------------------------------------------------------- /src/components/RulerLine/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.less'; 2 | import { useSize } from 'ahooks'; 3 | // 辅助线 4 | const RulerLine = function () { 5 | const Screen = useSize(document.querySelector('html')); 6 | const Line = function (props) { 7 | const scaleEls: JSX.Element[] = []; 8 | for (var i = 0; i < props.size; i++) { 9 | if (i % 10 == 0 && i != 0) { 10 | let styleObj = props.direction == 'x' ? { left: (i - 1) + 'px', height: i % 50 == 0 ? '100%' : '50%' } : { top: (i - 1) + 'px', width: i % 50 == 0 ? '100%' : '50%' }; 11 | scaleEls.push({i % 50 == 0 ? {i} : ''}) 12 | } 13 | } 14 | return
{scaleEls}
; 15 | } 16 | return
17 | {/* 辅助线-X轴 */} 18 | 19 | {/* 辅助线-Y轴 */} 20 | 21 |
22 | } 23 | 24 | export default RulerLine; 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # cook-web(v1.0) 3 | 4 | > 一个简易的拖拽式网页构建平台 5 | 6 | ## 使用 7 | 8 | ```bash 9 | # 安装依赖 10 | $ yarn 11 | 12 | # 启动服务 13 | $ npm start # visit http://localhost:3333 14 | ``` 15 | 16 | [More docs](https://ice.work/docs/guide/about). 17 | 18 | ## 目录 19 | 20 | ```md 21 | ├── build/ # 构建产物 22 | ├── mock/ # 本地模拟数据 23 | │ ├── index.[j,t]s 24 | ├── public/ 25 | │ ├── index.html # 应用入口 HTML 26 | │ └── favicon.png # Favicon 27 | ├── src/ # 源码路径 28 | │ ├── components/ # 自定义业务组件 29 | │ │ └── Guide/ 30 | │ │ ├── index.[j,t]sx 31 | │ │ └── index.module.scss 32 | │ ├── pages/ # 页面 33 | │ │ └── index.tsx/ 34 | │ ├── global.scss # 全局样式 35 | │ └── app.[j,t]s[x] # 应用入口脚本 36 | ├── README.md 37 | ├── package.json 38 | ├── .editorconfig 39 | ├── .eslintignore 40 | ├── .eslintrc.[j,t]s 41 | ├── .gitignore 42 | ├── .stylelintignore 43 | ├── .stylelintrc.[j,t]s 44 | ├── .gitignore 45 | └── [j,t]sconfig.json 46 | ``` 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cook-web", 3 | "version": "0.1.0", 4 | "description": "前端页快速搭建工具", 5 | "dependencies": { 6 | "ace-builds": "^1.5.2", 7 | "ahooks": "^3.3.8", 8 | "antd": "^4.20.5", 9 | "idb-js": "^1.3.1", 10 | "jszip": "^3.9.1", 11 | "react": "^17.0.2", 12 | "react-ace": "^10.1.0", 13 | "react-dom": "^17.0.2" 14 | }, 15 | "devDependencies": { 16 | "@iceworks/spec": "^1.0.0", 17 | "@types/react": "^17.0.2", 18 | "@types/react-dom": "^17.0.2", 19 | "eslint": "^7.30.0", 20 | "ice.js": "^2.0.0", 21 | "stylelint": "^13.7.2" 22 | }, 23 | "scripts": { 24 | "start": "icejs start", 25 | "build": "icejs build", 26 | "lint": "npm run eslint && npm run stylelint", 27 | "eslint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./", 28 | "eslint:fix": "npm run eslint -- --fix", 29 | "stylelint": "stylelint \"**/*.{css,scss,less}\"" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/nullno/cook-web.git" 34 | }, 35 | "private": true, 36 | "originTemplate": "@alifd/scaffold-simple" 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/global.css: -------------------------------------------------------------------------------- 1 | @import './normalize.css'; 2 | 3 | .ant-input[type='color'] { 4 | height: 21px; 5 | } 6 | ::-webkit-scrollbar { 7 | width: 3px; 8 | height: 3px; 9 | } 10 | ::-webkit-scrollbar-track { 11 | width: 3px; 12 | background-color: rgba(0, 0, 0, 0); 13 | -webkit-border-radius: 2em; 14 | -moz-border-radius: 5px; 15 | border-radius: 2em; 16 | } 17 | ::-webkit-scrollbar-thumb { 18 | background-color: rgba(0, 0, 0, 0.3); 19 | background-clip: padding-box; 20 | min-height: 20px; 21 | -webkit-border-radius: 2em; 22 | -moz-border-radius: 2em; 23 | border-radius: 2em; 24 | } 25 | ::-webkit-scrollbar-thumb:hover { 26 | background-color: rgba(0, 0, 0, 1); 27 | } 28 | .loading-ready { 29 | width: 50px; 30 | height: 50px; 31 | position: fixed; 32 | top: 50%; 33 | left: 50%; 34 | margin-left: -25px; 35 | margin-top: -25px; 36 | } 37 | 38 | .cookApp { 39 | width: 100%; 40 | height: 100%; 41 | background-color: #fff; 42 | position: fixed; 43 | } 44 | 45 | @keyframes spinner { 46 | from { 47 | transform: rotate(0deg); 48 | } 49 | 50 | to { 51 | transform: rotate(360deg); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/RulerLine/index.module.less: -------------------------------------------------------------------------------- 1 | .ruler-Line { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | z-index: 9; 6 | opacity: 0.5; 7 | user-select: none; 8 | i { 9 | position: absolute; 10 | box-sizing: border-box; 11 | left: 0; 12 | top: 0; 13 | font-style: normal; 14 | font-size: 8px; 15 | background-color: #666; 16 | color: #e94337; 17 | span { 18 | position: absolute; 19 | top: 5px; 20 | left: 0; 21 | transform: scale(0.8); 22 | } 23 | span:hover { 24 | background-color: #e94337; 25 | padding: 0 5px; 26 | border-radius: 20px; 27 | color: #fff; 28 | transform: scale(1.5); 29 | cursor: crosshair; 30 | transition: all 0.1s; 31 | } 32 | } 33 | &.rulerX { 34 | width: 100%; 35 | height: 15px; 36 | i { 37 | width: 1px; 38 | height: 100%; 39 | } 40 | } 41 | &.rulerY { 42 | width: 15px; 43 | height: 100%; 44 | i { 45 | width: 100%; 46 | height: 1px; 47 | span { 48 | transform: scale(0.8) rotate(90deg); 49 | } 50 | span:hover { 51 | transform: scale(1.5) rotate(90deg); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/db/state.ts: -------------------------------------------------------------------------------- 1 | interface State { 2 | DOM_DATA: DomTree; // 页面数据 3 | DOM_MAP: { [propname: string]: DomItem }; // 扁平树 4 | CUR_EL: HTMLElement | null; // 当前编辑元素 5 | CUR_KEY: string; // 当前编辑key 6 | DRAG_EL: DomItem | null; // 拖动元素 7 | PAGE_USE: number;// 当前编辑页面索引 8 | CUR_PAGE: PageItem | null; // 当前页面数据 9 | MyRenderEngine: any; // 渲染引擎实例 10 | Panzoom: any; // 拖动缩放库实例 11 | MyDB: any; // 数据库 12 | } 13 | 14 | 15 | const State: State = { 16 | DOM_DATA: { 17 | appName: '测试应用', 18 | plugins: [], 19 | pages: [ 20 | { 21 | type: 'page', 22 | id: 'page1', 23 | title: '测试标题', 24 | style: { 25 | width: '1200px', 26 | minHeight: '500px', 27 | border: '1px dashed red' 28 | }, 29 | child: [] 30 | }, 31 | { 32 | type: 'page', 33 | id: 'page2', 34 | title: '测试标题', 35 | style: { 36 | width: '1200px', 37 | minHeight: '500px', 38 | border: '1px dashed red' 39 | }, 40 | child: [] 41 | } 42 | ], 43 | }, 44 | DOM_MAP: {}, 45 | CUR_EL: null, 46 | CUR_KEY: '', 47 | DRAG_EL: null, 48 | PAGE_USE: 0, 49 | CUR_PAGE: null, 50 | MyRenderEngine: null, 51 | Panzoom: null, 52 | MyDB: null, 53 | } 54 | 55 | State.CUR_PAGE = State.DOM_DATA.pages[State.PAGE_USE]; 56 | export default State 57 | -------------------------------------------------------------------------------- /src/utils/tool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 对象深拷贝 3 | * @param {*} obj 目标对象 4 | * @returns 5 | */ 6 | export const deepCopyObj = function (obj) { 7 | var result = Array.isArray(obj) ? [] : {}; 8 | for (var key in obj) { 9 | if (obj.hasOwnProperty(key)) { 10 | if (typeof obj[key] === "object" && obj[key] !== null) { 11 | result[key] = deepCopyObj(obj[key]); 12 | } else { 13 | result[key] = obj[key]; 14 | } 15 | } 16 | } 17 | return result; 18 | }; 19 | /** 20 | * 获取屏幕尺寸 21 | */ 22 | export const screen = { 23 | width: function() { 24 | return ( 25 | window.innerWidth || 26 | document.documentElement.clientWidth || 27 | document.body.clientWidth 28 | ); 29 | }, 30 | height: function() { 31 | return ( 32 | window.innerHeight || 33 | document.documentElement.clientHeight || 34 | document.body.clientHeight 35 | ); 36 | }, 37 | scrollTop: function() { 38 | return document.documentElement.scrollTop || document.body.scrollTop; 39 | }, 40 | scrollHeight: function() { 41 | return document.documentElement.scrollHeight || document.body.scrollHeight; 42 | }, 43 | resize: function(callback) { 44 | window.onresize = function() { 45 | callback({ 46 | width: screen.width(), 47 | height: screen.height(), 48 | }); 49 | }; 50 | window.onresize(); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/db/use.ts: -------------------------------------------------------------------------------- 1 | import STATE from '@/db/state'; 2 | 3 | export default { 4 | tableName: 'dom_sate', 5 | fixedId: 'cooker', 6 | myStore() { 7 | return new Promise((resolve, reject) => { 8 | STATE.MyDB.query_by_primaryKey({ 9 | tableName: this.tableName, 10 | target: this.fixedId, 11 | success: (res) => { 12 | res && (res.dom_tree = JSON.parse(res.dom_tree)); 13 | resolve(res) 14 | }, 15 | failed(err) { 16 | reject(null) 17 | } 18 | }) 19 | }) 20 | }, 21 | setStore() { 22 | return new Promise((resolve, reject) => { 23 | STATE.MyDB.insert({ 24 | tableName: this.tableName, 25 | data: { 26 | id: this.fixedId, 27 | page_use: STATE.PAGE_USE, 28 | dom_tree: JSON.stringify(STATE.DOM_DATA) 29 | }, 30 | success: (res) => { 31 | resolve(true) 32 | }, 33 | failed(err) { 34 | reject(false) 35 | } 36 | }) 37 | }); 38 | }, 39 | updateStore() { 40 | return new Promise((resolve, reject) => { 41 | STATE.MyDB.update_by_primaryKey({ 42 | tableName: this.tableName, 43 | target: this.fixedId, 44 | handle(val) { 45 | val.page_use = STATE.PAGE_USE 46 | val.dom_tree = JSON.stringify(STATE.DOM_DATA); 47 | }, 48 | success: res => { 49 | resolve(res); 50 | } 51 | }); 52 | }); 53 | }, 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/ingredients/export/index.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'antd'; 2 | import resetCssTemplate from './resetCssTemplate' 3 | import htmlTemplate from './htmlTemplate'; 4 | import cssTemplate from './cssTemplate'; 5 | import jsTemplate from './jsTemplate'; 6 | 7 | export default function (STATE, CoreHandler) { 8 | const loadHide = message.loading('正在导出源码..', 0); 9 | // @ts-ignore 10 | const zip = new JSZip(); 11 | const cookerFiles: any = zip.folder(".cooker"); 12 | const imgFiles: any = zip.folder("images"); 13 | const cssFiles = zip.folder("css"); 14 | const jsFiles = zip.folder("js"); 15 | cookerFiles.file("cooker." + Date.now() + ".json", JSON.stringify(STATE.DOM_DATA.pages)); 16 | cssFiles.file("normalize.css", resetCssTemplate()); 17 | STATE.DOM_DATA.pages.forEach(page => { 18 | const exportData = CoreHandler._exportPageHandler(page); 19 | // console.log(exportData); 20 | // img file 21 | for (let key in exportData.img) { 22 | const imgData = exportData.img[key]; 23 | imgFiles.file(key, imgData.substring(imgData.indexOf(',') + 1), { base64: true }); 24 | } 25 | // js file 26 | jsFiles.file(page.id + '.js', jsTemplate(page, exportData.js)); 27 | // css file 28 | cssFiles.file(page.id + '.css', cssTemplate(page, exportData.css)); 29 | // html file 30 | zip.file(page.id + '.html', htmlTemplate(page, exportData.html)); 31 | 32 | }) 33 | zip.generateAsync({ type: "blob" }) 34 | .then((content) => { 35 | // @ts-ignore see FileSaver.js 36 | saveAs(content, 'cook-web.zip'); 37 | loadHide(); 38 | message.success('导出成功!'); 39 | 40 | }).catch((err) => { 41 | loadHide(); 42 | message.error('导出失败:【' + err + '】'); 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/idb.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: weijundong wx_dda6ed3e7d4049b18953aab41af2bcd1@git.code.tencent.com 3 | * @Date: 2022-05-20 10:05:32 4 | * @LastEditors: weijundong wx_dda6ed3e7d4049b18953aab41af2bcd1@git.code.tencent.com 5 | * @LastEditTime: 2022-05-26 15:47:31 6 | * @FilePath: \ty-cooking\src\utils\idb.js 7 | * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE 8 | */ 9 | import { useState, useEffect } from 'react'; 10 | const dbp = new Promise((resolve, reject) => { 11 | const openreq = window.indexedDB.open('use-idb', 1); 12 | openreq.onerror = () => reject(openreq.error); 13 | openreq.onsuccess = () => resolve(openreq.result); 14 | openreq.onupgradeneeded = () => openreq.result.createObjectStore('idb'); 15 | }); 16 | 17 | const call = async (type, method, ...args) => { 18 | const db = await dbp; 19 | const transaction = db.transaction('idb', type); 20 | const store = transaction.objectStore('idb'); 21 | 22 | return new Promise((resolve, reject) => { 23 | const req = store[method](...args); 24 | transaction.oncomplete = () => resolve(req); 25 | transaction.onabort = transaction.onerror = () => reject(transaction.error); 26 | }); 27 | }; 28 | 29 | const get = async (key) => (await call('readonly', 'get', key)).result; 30 | const set = (key, value) => 31 | value === undefined ? call('readwrite', 'delete', key) : call('readwrite', 'put', value, key); 32 | const useIdb = (key, initialState) => { 33 | const [item, setItem] = useState(initialState); 34 | useEffect(() => { 35 | get(key).then((value) => value === undefined || setItem(value)); 36 | }, [key]); 37 | 38 | return [ 39 | item, 40 | (value) => { 41 | setItem(value); 42 | return set(key, value); 43 | }, 44 | ]; 45 | }; 46 | export default useIdb; 47 | -------------------------------------------------------------------------------- /src/ingredients/layouts/PageTpl2.ts: -------------------------------------------------------------------------------- 1 | const PageBox1 = { 2 | type: 'PageBox1', 3 | name: '模板2', 4 | tag: 'div', 5 | stopBefore: true, 6 | stopAfter: true, 7 | style: { 8 | width: '100%', 9 | height: 'auto', 10 | backgroundColor: '#F6F6F6', 11 | overflow: 'hidden' 12 | }, 13 | child: [ 14 | { 15 | type: 'PageBox1_head', 16 | name: '模板2-头部', 17 | tag: 'div', 18 | stopBefore: true, 19 | style: { 20 | width: '100%', 21 | height: '50px', 22 | backgroundColor: '#fff', 23 | margin: '0 auto', 24 | }, 25 | child: [], 26 | }, 27 | { 28 | type: 'PageBox1_banner', 29 | name: '模板2-banner', 30 | tag: 'div', 31 | style: { 32 | width: '100%', 33 | minHeight: '300px', 34 | backgroundColor: '#EDEDED', 35 | margin: '10px auto', 36 | overflow: 'hidden' 37 | }, 38 | child: [ 39 | { 40 | type: 'Img', 41 | name: '图片', 42 | tag: 'img', 43 | src: 'https://static.smartisanos.cn/delta/img/01@2x.4d19c6c.jpg?x-oss-process=image/format,webp', 44 | style: { 45 | width: '100%', 46 | }, 47 | } 48 | ], 49 | }, 50 | { 51 | type: 'PageBox1_main', 52 | name: '模板2-主体', 53 | tag: 'div', 54 | style: { 55 | width: '1200px', 56 | minHeight: '500px', 57 | backgroundColor: '#fff', 58 | margin: '10px auto', 59 | }, 60 | child: [], 61 | }, 62 | { 63 | type: 'PageBox1_footer', 64 | name: '模板2-底部', 65 | tag: 'div', 66 | stopAfter: true, 67 | style: { 68 | width: '100%', 69 | height: '200px', 70 | backgroundColor: '#fff', 71 | }, 72 | child: [], 73 | }, 74 | ], 75 | }; 76 | export default PageBox1; 77 | -------------------------------------------------------------------------------- /src/components/EditPage/ActionPanel.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.less'; 2 | import STATE from '@/db/state'; 3 | import useDB from '@/db/use'; 4 | import { useRef, useEffect } from 'react'; 5 | 6 | import { Tag, Button, message } from 'antd'; 7 | import { EnvironmentOutlined } from '@ant-design/icons'; 8 | // ace代码编辑器 9 | import AceEditor from 'react-ace'; 10 | 11 | 12 | const ActionPanel = (props) => { 13 | const aceCodeEditRef = useRef(null); 14 | const curWidget: DomItem = STATE.DOM_MAP[STATE.CUR_KEY]; 15 | 16 | // useEffect(()=>{}) 17 | let aceEditorValue = ''; 18 | 19 | const saveEventCode = () => { 20 | if (curWidget.events === aceEditorValue) return; 21 | curWidget.events = aceEditorValue; 22 | useDB.updateStore(); 23 | message.success('保存成功!'); 24 | } 25 | return
26 |
27 | } color="#f50">{STATE.CUR_KEY} 28 | 29 |
30 | 31 |
32 | aceEditorValue = value} 36 | value={curWidget.events || `var ${curWidget.className} = document.querySelector('.${curWidget.className}');`} 37 | name="UNIQUE_ID_JS_CODE" 38 | showGutter={true} 39 | fontSize={16} 40 | editorProps={{ $blockScrolling: true }} 41 | setOptions={{ 42 | useWorker: false, 43 | enableBasicAutocompletion: true, 44 | enableLiveAutocompletion: true, 45 | }} 46 | style={{ width: '100%', height: '100%' }} 47 | /> 48 |
49 | 50 |
51 | } 52 | export default ActionPanel; 53 | -------------------------------------------------------------------------------- /src/assets/img/wg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: weijundong wx_dda6ed3e7d4049b18953aab41af2bcd1@git.code.tencent.com 3 | * @Date: 2022-05-20 10:05:32 4 | * @LastEditors: weijundong wx_dda6ed3e7d4049b18953aab41af2bcd1@git.code.tencent.com 5 | * @LastEditTime: 2022-06-01 11:23:37 6 | * @FilePath: \ty-cooking\src\typings.d.ts 7 | * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE 8 | */ 9 | declare module 'react-ace'; 10 | 11 | declare module '*.module.less' { 12 | const classes: { [key: string]: string }; 13 | export default classes; 14 | } 15 | declare module '*.jpg' { 16 | const jpg: string; 17 | export default jpg; 18 | } 19 | declare module '*.png' { 20 | const png: string; 21 | export default png; 22 | } 23 | 24 | interface DBStore { 25 | id: string; 26 | page_use: number; 27 | dom_tree: any; 28 | } 29 | 30 | interface Screen { 31 | width: number; 32 | height: number; 33 | } 34 | 35 | interface ToolTab { 36 | name: string; 37 | icon: string; 38 | event: any; 39 | } 40 | 41 | interface DomItem { 42 | type: string; 43 | tag: 'div' | 'ul' | 'li' | 'button' | 'span' | 'image' | 'i'; 44 | name: string; 45 | style: Object; 46 | className?: string; 47 | child?: Array; 48 | text?: string; 49 | placeholder?: string; 50 | hide?: boolean; 51 | events?: any; 52 | plugins?: any; 53 | edit?: any; 54 | src?: string; 55 | stopBefore?: boolean; 56 | stopAfter?: boolean; 57 | stopAdd?: boolean; 58 | dragDots?: Array<'left' | 'right' | 'top' | 'bottom'>; 59 | } 60 | 61 | interface PageItem { 62 | type: 'page'; 63 | id: string; 64 | title: string; 65 | name?: string; 66 | style?: Object; 67 | child?: Array; 68 | hide?: boolean; 69 | events?: any; 70 | stopBefore?: boolean; 71 | stopAfter?: boolean; 72 | stopAdd?: boolean; 73 | dragDots?: Array<'left' | 'right' | 'top' | 'bottom'> 74 | } 75 | interface DomTree { 76 | appName: string; 77 | plugins: Array; 78 | pages: Array 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/components/EditPage/UploadImg.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Upload, message } from 'antd'; 3 | import { LoadingOutlined, PlusOutlined } from '@ant-design/icons'; 4 | import type { UploadChangeParam } from 'antd/es/upload'; 5 | import type { RcFile, UploadFile, UploadProps } from 'antd/es/upload/interface'; 6 | const getBase64 = (img: RcFile, callback: (url: string) => void) => { 7 | const reader = new FileReader(); 8 | reader.addEventListener('load', () => callback(reader.result as string)); 9 | reader.readAsDataURL(img); 10 | }; 11 | 12 | const beforeUpload = (file: RcFile) => { 13 | const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'; 14 | if (!isJpgOrPng) { 15 | message.error('只能选择 JPG/PNG 图片!'); 16 | } 17 | const isLt2M = file.size / 1024 / 1024 < 2; 18 | if (!isLt2M) { 19 | message.error('选择图片须小于2MB!'); 20 | } 21 | return isJpgOrPng && isLt2M; 22 | }; 23 | 24 | const UploadImg = (props) => { 25 | const [loading, setLoading] = useState(false); 26 | const [imageUrl, setImageUrl] = useState(props.defaultValue); 27 | 28 | const handleChange: UploadProps['onChange'] = (info: UploadChangeParam) => { 29 | if (info.file.status === 'uploading') { 30 | setLoading(true); 31 | return; 32 | } 33 | if (info.file) { 34 | // Get this url from response in real world. 35 | getBase64(info.file.originFileObj as RcFile, url => { 36 | setLoading(false); 37 | setImageUrl(url); 38 | props.onChange && props.onChange(url) 39 | }); 40 | } 41 | }; 42 | 43 | const uploadButton = ( 44 |
45 | {loading ? : } 46 |
本地图片
47 |
48 | ); 49 | 50 | return ( 51 | 52 | 61 | {imageUrl ? bg : uploadButton} 62 | 63 | ); 64 | }; 65 | 66 | export default UploadImg; 67 | -------------------------------------------------------------------------------- /src/components/EditPage/ZoomPlugin.tsx: -------------------------------------------------------------------------------- 1 | interface ViewState { 2 | disableMove: boolean; 3 | } 4 | 5 | import styles from './index.module.less'; 6 | import { useMount, useSetState, useFullscreen } from 'ahooks' 7 | import STATE from '@/db/state'; 8 | 9 | let myPanzoom: any; 10 | 11 | const ZoomPlugin = (props) => { 12 | 13 | const [viewState, setViewState] = useSetState({ 14 | disableMove: false, 15 | }); 16 | const [isFullscreen, { toggleFullscreen }] = useFullscreen(() => document.body); 17 | 18 | useMount(() => { 19 | const EditPageEl: HTMLDivElement = document.querySelector('#' + props.target)!; 20 | // @ts-ignore 21 | myPanzoom = Panzoom(EditPageEl, { startScale: 0.9, disablePan: viewState.disableMove }); 22 | // @ts-ignore 23 | EditPageEl.addEventListener('wheel', (event: any) => { 24 | if (!event.ctrlKey) return; 25 | myPanzoom.zoomWithWheel(event) 26 | }) 27 | STATE.Panzoom = myPanzoom; 28 | }) 29 | const setZoom = (f: string) => { 30 | if (f === 'add') { 31 | myPanzoom && myPanzoom.zoomIn(); 32 | } 33 | if (f === 'minus') { 34 | myPanzoom && myPanzoom.zoomOut(); 35 | } 36 | } 37 | 38 | const openMove = () => { 39 | const { disablePan } = myPanzoom.getOptions(); 40 | setViewState({ 41 | disableMove: !disablePan 42 | }) 43 | myPanzoom.setOptions({ disablePan: !disablePan }) 44 | } 45 | 46 | return
47 | 48 | 49 | myPanzoom.reset()}> 50 | setZoom('add')}> 51 | setZoom('minus')}> 52 | { 53 | window.open(location.protocol + '//' + location.host +location.pathname+ '#/preview/' + STATE.PAGE_USE); 54 | }}> 55 | { 56 | STATE.MyRenderEngine.download(); 57 | }}> 58 |
59 | 60 | } 61 | export default ZoomPlugin; 62 | -------------------------------------------------------------------------------- /src/ingredients/layouts/PageTpl1.ts: -------------------------------------------------------------------------------- 1 | const MangerBox = { 2 | type: 'MangerBox', 3 | name: '模板1', 4 | tag: 'div', 5 | stopBefore: true, 6 | stopAfter: true, 7 | stopAdd: true, 8 | style: { 9 | width: '100%', 10 | minWidth: '1200px', 11 | minHeight: '500px', 12 | height: '100%', 13 | display: 'flex', 14 | flexDirection: 'column', 15 | position: 'fixed', 16 | backgroundColor: '#fff', 17 | 18 | }, 19 | child: [ 20 | { 21 | type: 'MangerBox_head', 22 | name: '模板1-头部', 23 | tag: 'div', 24 | stopBefore: true, 25 | stopAfter: true, 26 | dragDots: ['bottom'], 27 | style: { 28 | width: '100%', 29 | height: '60px', 30 | maxHeight: '100px', 31 | overflow: 'hidden', 32 | backgroundColor: '#fff', 33 | padding: '10px' 34 | }, 35 | child: [], 36 | }, 37 | { 38 | type: 'MangerBox_main', 39 | name: '模板1-主体', 40 | tag: 'div', 41 | stopBefore: true, 42 | stopAfter: true, 43 | stopAdd: true, 44 | style: { 45 | width: '100%', 46 | height: '100%', 47 | flex: 1, 48 | overflow: 'hidden', 49 | display: 'flex', 50 | backgroundColor: '#F6F6F6', 51 | }, 52 | child: [ 53 | { 54 | type: 'MangerBox_left', 55 | name: '模板1-侧边栏', 56 | tag: 'div', 57 | stopBefore: true, 58 | stopAfter: true, 59 | dragDots: ['right'], 60 | style: { 61 | width: '100px', 62 | height: '99%', 63 | overflow: 'auto', 64 | backgroundColor: '#fff', 65 | marginTop: '10px', 66 | padding: '10px' 67 | }, 68 | child: [], 69 | }, 70 | { 71 | type: 'MangerBox_right', 72 | name: '模板1-内容', 73 | tag: 'div', 74 | stopBefore: true, 75 | stopAfter: true, 76 | style: { 77 | flex: 1, 78 | width: '98%', 79 | height: '99%', 80 | overflow: 'auto', 81 | marginLeft: '10px', 82 | marginTop: '10px', 83 | backgroundColor: '#fff', 84 | padding: '10px' 85 | }, 86 | child: [], 87 | } 88 | ], 89 | 90 | } 91 | ], 92 | }; 93 | 94 | export default MangerBox; 95 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR����g- 4 | fIDATx��klSe�_E��Cza�ʸ ��0�2@�>x ^?(bl;ƌZ���& &��/^h)0. 5 | ��4E�@� �Զnc�v�<>o��ҵ��zN����� �lky��{zΙ�hLv�-vM���q����q��M��c#?����鲺���8�� ���������=��o⏽�/X*i����hX�4���67�͢�N��$�*�}������#�)k90N/ �)O����&[��?�g���]9�M�**�v�����d���?�a����t.%��0�*�6�����A�r� �����S$����kO�-<�zT�����Y��/�$�>X��`��2�M��_���߹Zn�� r 6 | ���\�8d��g7�*i����K����&���g���D`l�����Cf.�1���@-��N���������M��+Ĺ�Fʷ���$,,]Zl.�W� 7��i�\�6!'y�-@��>��7�\������u��4L5XW�]����^��w�~�<��_C��3C�iMf�+����֒���0�1�4J�����0�9-@���/R\��{xfm�"���W�,2gF+ +��jK&�M��y^S�Y*�*@��F�z�%���@@E9����44.GJ6�9Ԥ�K@���4�T�;�� �8O��=ovC�Rj����x`�CE��y����Ʉ&�i嬓W� 3QZ{n������upy����'�|���Ȓ�M�5� �����o���8g�G�z�����0_z���=���=��Ùu�7�\�y����C�������ܦ؅��F�x*���5��h?�� `�.�[�UT(�B�qZZ�Z�v��A���-G�M��tD.)�?����DD�.�Y�g�N��V�;� M�V�Mɞ�xZI-���~5 7�,��!pe�/�������!�e����(��~.�IfRM'D2�����>���wt�g�vi�i��*l.�-2Ź+4�� �w�C1)ܕ2��"�N�/�}�����H�zǤd�� ����"Y�C��-J�t�����D 7 | �i��s��� �d�ݴ�~ȝ/(��&�ۂ4}�YZЖ��Hvq��6� ^~��T��<P�7HJj���9 %�p/���4A�S�sG�=�IiT���~Xў�����<�P 4�IY��o��0 8 | ���<�*}�S��͎m�je�p'�æ4����"WL��3��᧜��㇭ �����B/�;�r?lR����!���;�Y$� 9 | K�w���C���m�a�^���n�F뇾�k�`����3< �,q�C���q���<?��2_�WS�0:���i�,7C@���C�E�Q/�xR������W�f����嵍+ 10 | � ����yI�L0�J�c��!ݥ�EW���$[�����f)]6�&ݔnR�n 11 | �t��&I����,J�Ē5��a;�%[��N&�4rl����3�����^ )������y������Od?�i����<�v��91��wm�3�D(���{S�������~BFz5�!��Z���:*�j���I�o��_��"��7�W�H~0�@�<3��>��G��_�U���f�C����x�e��/ꇕOe?�:���~�~��督�^σ�&(��A�+�_��٦�1�����j�O�|bf8��8���@��):��r�{O@w�h���f��~PY� ���n��� ���l����+����ּHz�A�����!��f��Xncjd�ؐ! 12 | f�I���=�η�H.S<��\@���t��]lD���U���0�������������2������ �����d�0*V���U�$�p�Β7��D�CW����,nv����В��Y�������oӼ��?�%#��Ӧ�こ�hz�4�FsI�L����d�]��'krP�� ]��SW^��)�m}ѹ������f!kskz��:w8-Q��Q� �$�S[I��:��П~�������~����-��S�S� Q�t��x�$O�%^��&Z�@[j�r ��������!֋����"�Ʋ�I�r�M��m}PM�?�3b��)�r������X��jV��a�V��,�W�][� ^�͝3/ivOG����|G�>���25����xI�n&�o� �T̷����������w�+ ݪ]�X��rz��>�~K-O ��Μ�7dg�}12SX�m��-�t�{Ř[� 13 | l=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)}); 2 | 3 | //# sourceMappingURL=FileSaver.min.js.map -------------------------------------------------------------------------------- /src/components/ToolBarBox/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.less'; 2 | import { useRef, useState } from 'react'; 3 | import { useMount, useDrag, useSetState } from 'ahooks'; 4 | import drag from '@/utils/drag'; 5 | import STATE from '@/db/state'; 6 | 7 | import Layouts from '@/ingredients/layouts' 8 | import Elements from '@/ingredients/elements' 9 | import { deepCopyObj } from '@/utils/tool' 10 | 11 | interface ViewState { 12 | tabUse: number; 13 | tabs: Array 14 | } 15 | 16 | // 工具栏 17 | const ToolBarBox = function () { 18 | const [viewState, setViewState] = useSetState({ 19 | tabUse: 0, 20 | tabs: [ 21 | { name: '布局', icon: 'fa-columns', event: '' }, 22 | { name: '元素', icon: 'fa-cube', event: '' }, 23 | { name: '组件', icon: 'fa-cubes', event: '' }, 24 | ] 25 | }); 26 | 27 | const switchTab = (i: number) => { 28 | setViewState({ tabUse: i }) 29 | } 30 | 31 | useMount(() => { 32 | new drag(document.querySelector('#ToolBarBox')); 33 | }); 34 | 35 | const DragLayoutItems = function ({ data }) { 36 | const dragRef = useRef(null); 37 | const [dragging, setDragging] = useState(false); 38 | useDrag(data, dragRef, { 39 | onDragStart: () => { 40 | // setDragging(true); 41 | // @ts-ignore 42 | STATE.DRAG_EL = deepCopyObj(data); 43 | }, 44 | onDragEnd: () => { 45 | // setDragging(false); 46 | }, 47 | }); 48 | return
  • {data.name}
  • 49 | } 50 | 51 | return
    52 |
    53 |
      54 | {viewState.tabs.map((item, i) =>
    • switchTab(i)} 58 | > 59 | 60 | {item.name} 61 |
    • )} 62 |
    63 |
    64 |
      65 | {Layouts.map((item, i) => )} 66 |
    67 |
      68 | {Elements.map((item, i) => )} 69 |
    70 |
      71 | 组件 72 | {/* {Layouts.map((item, i) => )} */} 73 |
    74 | 75 |
    76 | } 77 | 78 | 79 | export default ToolBarBox; 80 | -------------------------------------------------------------------------------- /src/utils/drag.js: -------------------------------------------------------------------------------- 1 | const dragBox = function(el, option = {}) { 2 | this.option = Object.assign( 3 | { 4 | disableX: false, // 约束x轴移动 5 | disableY: false, // 约束y轴移动 6 | disable: false, // 禁用移动 7 | }, 8 | option 9 | ); 10 | 11 | this.$el = el; 12 | 13 | this.addEvent(); 14 | }; 15 | 16 | // 更新设置 17 | dragBox.prototype.updateSet = function(option) { 18 | this.option = Object.assign(this.option, option); 19 | }; 20 | 21 | // 获取坐标点 22 | dragBox.prototype.getPointXY = function(e) { 23 | // 触屏模式 || 鼠标模式 24 | const dragWay = e.targetTouches ? e.targetTouches[0] : e; 25 | var x1 = dragWay.clientX || dragWay.pageX; 26 | var y1 = dragWay.clientY || dragWay.pageY; 27 | var x2 = this.$el.offsetLeft; 28 | var y2 = this.$el.offsetTop; 29 | 30 | return { 31 | x: x1 - x2, 32 | y: y1 - y2, 33 | }; 34 | }; 35 | 36 | dragBox.prototype.addEvent = function() { 37 | const _scope = this; 38 | if (!this.$el.children[0]) return; 39 | const dragHandler = this.$el.children[0]; 40 | dragHandler.style.cursor = "move"; 41 | const isTouch = "ontouchstart" in window ? true : false; 42 | let dragFlag = false; 43 | let startX = 0, 44 | startY = 0; 45 | let setPosition = function(x, y) { 46 | if (_scope.option.disable) { 47 | return; 48 | } 49 | _scope.$el.style.marginLeft = "unset"; 50 | _scope.$el.style.marginTop = "unset"; 51 | 52 | if (!_scope.option.disableX) { 53 | _scope.$el.style.left = x + "px"; 54 | } 55 | if (!_scope.option.disableY) { 56 | _scope.$el.style.top = y + "px"; 57 | } 58 | _scope.option.onchange && 59 | _scope.option.onchange.call(_scope, { x: x, y: y }); 60 | }; 61 | 62 | dragHandler.addEventListener( 63 | isTouch ? "touchstart" : "mousedown", 64 | function(e) { 65 | dragFlag = true; 66 | const pointStart = _scope.getPointXY(e || window.event); 67 | startX = pointStart.x; 68 | startY = pointStart.y; 69 | document.body.style.touchAction = "none"; 70 | }, 71 | false 72 | ); 73 | document.body.addEventListener( 74 | isTouch ? "touchmove" : "mousemove", 75 | function(e) { 76 | if (!dragFlag) return; 77 | e.preventDefault(); 78 | e.stopPropagation(); 79 | 80 | const dragWay = e.targetTouches ? e.targetTouches[0] : e; 81 | var pageX = dragWay.clientX || dragWay.pageX; 82 | var pageY = dragWay.clientY || dragWay.pageY; 83 | setPosition(pageX - startX, pageY - startY); 84 | }, 85 | false 86 | ); 87 | document.body.addEventListener( 88 | isTouch ? "touchend" : "mouseup", 89 | function(e) { 90 | dragFlag = false; 91 | document.body.style.touchAction = "unset"; 92 | }, 93 | false 94 | ); 95 | }; 96 | 97 | export default dragBox; 98 | -------------------------------------------------------------------------------- /src/components/ToolBarBox/index.module.less: -------------------------------------------------------------------------------- 1 | .toolBarBox { 2 | position: fixed; 3 | width: 400px; 4 | height: 180px; 5 | bottom: 100px; 6 | left: 50%; 7 | margin-left: -150px; 8 | background: rgba(255, 255, 255, 0.5) url('@/assets/img/grid.png'); 9 | border: 1px solid rgba(0, 0, 0, 0.1); 10 | box-shadow: 0 0 5px rgba(255, 255, 255, 0.3); 11 | border-radius: 5px; 12 | animation: bounceInUp 1000ms both; 13 | // border-bottom: 3px solid #409eff; 14 | z-index: 99; 15 | .inner { 16 | position: absolute; 17 | width: 100%; 18 | height: 40px; 19 | bottom: 0; 20 | } 21 | .toolBarUl { 22 | position: absolute; 23 | border-radius: 30px; 24 | bottom: 5px; 25 | width: 300px; 26 | height: 30px; 27 | left: 50%; 28 | margin-left: -150px; 29 | background-color: rgba(0, 0, 0, 0.5); 30 | display: flex; 31 | // flex-flow: column; 32 | justify-content: space-between; 33 | align-items: center; 34 | border: 1px solid #e94337; 35 | overflow: hidden; 36 | li { 37 | box-sizing: border-box; 38 | display: block; 39 | overflow: hidden; 40 | width: 100px; 41 | height: 30px; 42 | line-height: 30px; 43 | text-align: center; 44 | background-color: #ffeae8; 45 | color: #e94337; 46 | cursor: pointer; 47 | // padding: 10px 3px; 48 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 49 | &.active { 50 | background-color: #e94337; 51 | color: #fff; 52 | } 53 | i { 54 | font-size: 10px; 55 | margin-right: 3px; 56 | } 57 | span { 58 | // display: block; 59 | font-size: 14px; 60 | } 61 | } 62 | } 63 | .wrap { 64 | width: 100%; 65 | height: 140px; 66 | overflow: auto; 67 | padding: 10px; 68 | // background-color:red; 69 | box-sizing: border-box; 70 | display: none; 71 | &.show { 72 | display: block; 73 | } 74 | li { 75 | width: 50px; 76 | height: 50px; 77 | box-sizing: border-box; 78 | background-color: #eaf0ff; 79 | border-radius: 5px; 80 | float: left; 81 | text-align: center; 82 | margin: 5px 6px; 83 | cursor: pointer; 84 | user-select: none; 85 | box-sizing: border-box; 86 | color: #409eff; 87 | border: 1px dashed #409eff; 88 | padding: 7px 0; 89 | i { 90 | font-size: 20px; 91 | } 92 | span { 93 | display: block; 94 | text-align: center; 95 | } 96 | } 97 | } 98 | } 99 | 100 | @keyframes bounceInUp { 101 | from, 102 | 60%, 103 | 75%, 104 | 90%, 105 | to { 106 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 107 | } 108 | 109 | from { 110 | opacity: 0; 111 | transform: translate3d(0, 3000px, 0); 112 | } 113 | 114 | 60% { 115 | opacity: 1; 116 | transform: translate3d(0, -20px, 0); 117 | } 118 | 119 | 75% { 120 | transform: translate3d(0, 10px, 0); 121 | } 122 | 123 | 90% { 124 | transform: translate3d(0, -5px, 0); 125 | } 126 | 127 | to { 128 | transform: translate3d(0, 0, 0); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/components/PageBoard/index.module.less: -------------------------------------------------------------------------------- 1 | .page-board { 2 | position: fixed; 3 | width: 100%; 4 | bottom: 0; 5 | border-bottom: 3px solid #409eff; 6 | z-index: 999; 7 | .page-list { 8 | overflow: hidden; 9 | width: 100%; 10 | display: flex; 11 | justify-content: start; 12 | padding: 0 5px; 13 | margin-bottom: 0; 14 | li { 15 | max-width: 150px; 16 | position: relative; 17 | // height:25px; 18 | margin: 0 30px; 19 | background-color: #eaf0ff; 20 | transition: margin-right 0.3s; 21 | animation: fadeInUp 300ms both; 22 | padding-right:20px; 23 | & > div { 24 | text-align: left; 25 | overflow: hidden; 26 | text-overflow: ellipsis; 27 | white-space: nowrap; 28 | text-align: center; 29 | // float: left; 30 | color: #409eff; 31 | border-radius: 5px 5px 0 0; 32 | padding: 5px 5px; 33 | cursor: pointer; 34 | font-size: 14px; 35 | i { 36 | padding-right: 3px; 37 | } 38 | .page-del { 39 | background-color: #ccc; 40 | margin-left: 15px; 41 | color: #fff; 42 | width: 15px; 43 | height: 15px; 44 | line-height: 14px; 45 | border-radius: 15px; 46 | cursor: pointer; 47 | position: absolute; 48 | overflow: hidden; 49 | right:3px; 50 | top:6px; 51 | i { 52 | padding: 0; 53 | font-size: 10px; 54 | pointer-events: none; 55 | } 56 | &:hover { 57 | background-color: #e94337; 58 | } 59 | } 60 | } 61 | &:after { 62 | content: ''; 63 | position: absolute; 64 | top: 0; 65 | left: -40px; 66 | width: 0; 67 | height: 0; 68 | border: 20px solid transparent; 69 | border-right-color: #eaf0ff; 70 | border-bottom-color: #eaf0ff; 71 | pointer-events: none; 72 | } 73 | &:before { 74 | content: ''; 75 | position: absolute; 76 | top: 0; 77 | right: -40px; 78 | width: 0; 79 | height: 0; 80 | border: 20px solid transparent; 81 | border-left-color: #eaf0ff; 82 | border-bottom-color: #eaf0ff; 83 | pointer-events: none; 84 | } 85 | &.active { 86 | background-color: #409eff; 87 | div { 88 | color: #fff; 89 | } 90 | z-index: 999 !important; 91 | &:after { 92 | border-right-color: #409eff; 93 | border-bottom-color: #409eff; 94 | } 95 | &:before { 96 | border-left-color: #409eff; 97 | border-bottom-color: #409eff; 98 | } 99 | } 100 | &.more { 101 | margin-right: -8px; 102 | } 103 | &.removed { 104 | animation: fadeOutDown 200ms both; 105 | } 106 | } 107 | } 108 | } 109 | 110 | @keyframes fadeInUp { 111 | from { 112 | opacity: 0; 113 | transform: translate3d(0, 100%, 0); 114 | } 115 | 116 | to { 117 | opacity: 1; 118 | transform: translate3d(0, 0, 0); 119 | } 120 | } 121 | 122 | @keyframes fadeOutDown { 123 | from { 124 | opacity: 1; 125 | } 126 | to { 127 | opacity: 0; 128 | transform: translate3d(0, 100%, 0); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/utils/styleFormat.js: -------------------------------------------------------------------------------- 1 | // 删除字符串头尾空白 2 | export function delStartAndEndSpace(st) { 3 | if (typeof st !== 'string') { 4 | return st; 5 | } 6 | let cpst = st; 7 | cpst = cpst.replace(/^\s*/, ''); 8 | cpst = cpst.replace(/\s*$/, ''); 9 | return cpst; 10 | } 11 | // 删除字符串头尾空白分号 12 | export function delStartAndEndsplit(st) { 13 | if (typeof st !== 'string') { 14 | return st; 15 | } 16 | let cpst = st; 17 | cpst = cpst.replace(/^(\s*:*\s*)*/, ''); 18 | cpst = cpst.replace(/(\s*:*\s*)*$/, ''); 19 | return cpst; 20 | } 21 | // 对象样式名转成代码样式名 22 | export function styleNameObjectToCode(name) { 23 | let splitName = name.split(/(?=[A-Z])/); 24 | let lowerSplitName = []; 25 | splitName.map((item) => { 26 | lowerSplitName.push(item.toLowerCase()); 27 | }); 28 | return lowerSplitName.join('-'); 29 | } 30 | // 代码样式名转成对象样式名 31 | export function styleNameCodeToObject(name) { 32 | let splitName = name.split('-'); 33 | let lowerSplitName = []; 34 | splitName.map((item, index) => { 35 | let newname = item; 36 | if (index >= 1 && item.length > 1) { 37 | // 首字母大写 38 | let firstName = item.slice(0, 1); 39 | let otherName = item.slice(1); 40 | firstName = firstName.toUpperCase(); 41 | otherName = otherName.toLowerCase(); 42 | newname = `${firstName}${otherName}`; 43 | } 44 | lowerSplitName.push(newname); 45 | }); 46 | return lowerSplitName.join(''); 47 | } 48 | // 将样式对象转成样式代码字符串 49 | export function styleObjectToCodeString(objStyle) { 50 | let styleCodeString = ''; 51 | for (const key in objStyle) { 52 | // 取出每个样式,过滤空格和分号,然后合并成字符串 53 | if (Object.prototype.hasOwnProperty.call(objStyle, key)) { 54 | let element = delStartAndEndSpace(objStyle[key]); 55 | element = delStartAndEndsplit(element); 56 | let keystring = delStartAndEndSpace(key); 57 | keystring = delStartAndEndsplit(keystring); 58 | if (element !== undefined) { 59 | styleCodeString += `${styleNameObjectToCode(keystring)}: ${element};\n`; 60 | } 61 | } 62 | } 63 | // 包裹元素是为了在编辑器里面可以有智能提示 64 | return `el{\n${styleCodeString}}`; 65 | } 66 | // 将代码字符串转成样式对象 67 | export function styleCodeStringToObject(CodeString) { 68 | let styleString = CodeString; 69 | // 去除包裹的原元素 前后空白符 70 | styleString = delStartAndEndSpace(styleString); 71 | styleString = styleString.replace(/^el\{/, ''); 72 | styleString = styleString.replace(/\}$/, ''); 73 | // 通过封号分割成单个样式 74 | const styleArr = styleString.split(';'); 75 | 76 | let styleObject = {}; 77 | styleArr.map((item) => { 78 | // 异常数据直接返回 79 | if (item === '' || !/:/.test(item)) { 80 | return; 81 | } 82 | // 去掉头尾空白符 83 | let itemStyle = delStartAndEndSpace(item); 84 | // 分成样式和值 85 | let name = itemStyle.match(/^.*\:/); 86 | let value = itemStyle.match(/:.*$/); 87 | if (!Array.isArray(name) || !Array.isArray(value)) { 88 | return; 89 | } 90 | name = delStartAndEndSpace(name[0]); 91 | name = delStartAndEndsplit(name); 92 | value = delStartAndEndSpace(value[0]); 93 | value = delStartAndEndsplit(value); 94 | styleObject[styleNameCodeToObject(name)] = value; 95 | }); 96 | return styleObject; 97 | } 98 | 99 | // 对比样式对象 没有的置 undefined 100 | export function styleObjectUndefined(oldStyleObject, newStyleObject) { 101 | let newObject = {}; 102 | for (const oldKey in oldStyleObject) { 103 | if (Object.hasOwnProperty.call(oldStyleObject, oldKey)) { 104 | newObject[oldKey] = newStyleObject[oldKey]; 105 | } 106 | } 107 | return { ...newObject, ...newStyleObject }; 108 | } 109 | -------------------------------------------------------------------------------- /src/components/PageBoard/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import styles from './index.module.less'; 3 | import { Modal, Input, message, Form } from 'antd'; 4 | import { ExclamationCircleOutlined } from '@ant-design/icons'; 5 | import { useSet, useSetState } from 'ahooks'; 6 | import STATE from '@/db/state'; 7 | import useDB from '@/db/use'; 8 | 9 | const { confirm } = Modal; 10 | 11 | interface ViewState { 12 | pageList: PageItem[]; 13 | pageUse: number; 14 | moreMax: number; 15 | modalVisible: boolean; 16 | newPageTitle: string; 17 | newPageId: string; 18 | isModify: boolean; 19 | modifyIndex: number | null; 20 | } 21 | 22 | const PageBoard = function (PB) { 23 | // const [pages, { add, remove }] = useSet(STATE.DOM_DATA.pages); 24 | const [viewState, setViewState] = useSetState({ 25 | pageList: STATE.DOM_DATA.pages, 26 | pageUse: STATE.PAGE_USE, 27 | moreMax: 6, 28 | modalVisible: false, 29 | newPageTitle: '', 30 | newPageId: '', 31 | isModify: false, 32 | modifyIndex: null, 33 | }); 34 | // const pageList: PageItem[] = Array.from(pages); 35 | // 新建页面 36 | const newPage = (): void => { 37 | if (!viewState.newPageTitle || !viewState.newPageId) { 38 | message.warning('请设置必填信息'); 39 | return 40 | } 41 | const isSamePage = STATE.DOM_DATA.pages.some(item => item.id === viewState.newPageId) 42 | if (isSamePage) { 43 | message.error('已存在相同的页面名称'); 44 | return 45 | } 46 | const Page: PageItem = { 47 | type: 'page', 48 | id: viewState.newPageId, 49 | title: viewState.newPageTitle, 50 | style: { 51 | width: '1200px', 52 | minHeight: '500px', 53 | border: '1px dashed red' 54 | }, 55 | child: [] 56 | }; 57 | // add(Page); 58 | STATE.DOM_DATA.pages.push(Page); 59 | useDB.updateStore(); 60 | setViewState({ pageList: STATE.DOM_DATA.pages, modalVisible: false, newPageTitle: '', newPageId: '' }); 61 | message.success('创建成功!'); 62 | } 63 | // 删除页面 64 | const delPage = (e: React.MouseEvent, Page: PageItem, i: number): void => { 65 | e.stopPropagation(); 66 | if (STATE.DOM_DATA.pages.length < 2) { 67 | message.error('至少保留一个页面'); 68 | return; 69 | }; 70 | confirm({ 71 | title: '确定要删除这个页面【' + Page.id + '】?', 72 | icon: , 73 | okText: '确定', 74 | cancelText: '考虑一下', 75 | onOk() { 76 | e.stopPropagation(); 77 | // remove(Page); 78 | STATE.DOM_DATA.pages.splice(i, 1); 79 | setViewState({ pageList: STATE.DOM_DATA.pages }); 80 | let pLen = viewState.pageList.length - 1; 81 | if (viewState.pageUse >= pLen) { 82 | switchPage(pLen - 1); 83 | } 84 | }, 85 | onCancel() { }, 86 | }); 87 | } 88 | // 修改页面 89 | const modifyPage = () => { 90 | if (typeof viewState.modifyIndex != 'number') return; 91 | Object.assign(STATE.DOM_DATA.pages[viewState.modifyIndex], { id: viewState.newPageId, title: viewState.newPageTitle }) 92 | setViewState({ pageList: STATE.DOM_DATA.pages, modalVisible: false, isModify: false, newPageTitle: '', newPageId: '', modifyIndex: null }); 93 | useDB.updateStore(); 94 | document.title = 'cooking-' + viewState.newPageTitle; 95 | message.success('修改成功!'); 96 | } 97 | // 切换页面 98 | const switchPage = (i: number): void => { 99 | if (STATE.PAGE_USE === i) return; 100 | STATE.PAGE_USE = i; 101 | STATE.CUR_PAGE = STATE.DOM_DATA.pages[i]; 102 | setViewState({ pageUse: i }); 103 | PB.$switchPage.emit(i); 104 | useDB.updateStore(); 105 | } 106 | 107 | return <>
    108 |
      109 | {viewState.pageList.map((item, idx) =>
    • = viewState.moreMax ? styles.more : '')} 113 | onClick={() => switchPage(idx)} 114 | onDoubleClick={() => setViewState({ modalVisible: true, newPageTitle: item.title, newPageId: item.id, isModify: true, modifyIndex: idx })} 115 | > 116 |
      117 | 118 | {item.id} 119 | 122 |
      123 |
    • )} 124 |
    • setViewState({ modalVisible: true })}>
      新建
    • 125 |
    126 |
    127 | { 135 | viewState.isModify ? modifyPage() : newPage(); 136 | }} 137 | onCancel={() => setViewState({ modalVisible: false, newPageTitle: '', newPageId: '', isModify: false, modifyIndex: null })} 138 | > 139 | { 140 | setViewState({ newPageId: e.target.value.replace(/[^\w\.\/]/ig, '') }) 141 | }} /> 142 | { 143 | setViewState({ newPageTitle: e.target.value }) 144 | }} /> 145 | 146 | 147 | } 148 | 149 | export default PageBoard; 150 | -------------------------------------------------------------------------------- /src/components/EditPage/index.module.less: -------------------------------------------------------------------------------- 1 | .edit-screen { 2 | width: 100%; 3 | min-height: 100%; 4 | background-color: #000; 5 | } 6 | .edit-page-main { 7 | width: 100%; 8 | min-height: 100%; 9 | background-color: #fff; 10 | position: fixed; 11 | // overflow: hidden; 12 | top: 0; 13 | left: 0; 14 | border: 1px dashed #666; 15 | cursor: auto !important; 16 | } 17 | 18 | .zoom-view { 19 | position: fixed; 20 | right: 20px; 21 | bottom: 50px; 22 | // background-color: rgba(0, 0, 0, 0.1); 23 | border-radius: 50px; 24 | z-index: 99; 25 | width: 30px; 26 | // height: 80px; 27 | overflow: hidden; 28 | text-align: center; 29 | i { 30 | display: block; 31 | font-size: 18px; 32 | margin: 20px 0; 33 | color: #666; 34 | &:hover, 35 | &.active { 36 | color: #409eff; 37 | cursor: pointer; 38 | } 39 | } 40 | } 41 | 42 | .edit-page-container { 43 | width: 100%; 44 | height: 100%; 45 | .drop-tip { 46 | pointer-events: none; 47 | position: absolute; 48 | top: 50%; 49 | left: 50%; 50 | transform: translate(-50%, -50%); 51 | color: #ccc; 52 | } 53 | } 54 | 55 | .edit-line-box { 56 | position: fixed; 57 | z-index: 9999; 58 | pointer-events: none; 59 | -webkit-user-drag: none; 60 | user-select: none; 61 | border: 2px solid #409eff; 62 | .line-dot { 63 | box-sizing: border-box; 64 | content: ''; 65 | width: 10px; 66 | height: 10px; 67 | position: absolute; 68 | background-color: #409eff; 69 | border-radius: 5px; 70 | pointer-events: auto; 71 | &.line-dot-top { 72 | top: -6px; 73 | left: 50%; 74 | margin-left: -5px; 75 | cursor: s-resize; 76 | .dot-drag > span { 77 | cursor: s-resize !important; 78 | } 79 | } 80 | &.line-dot-right { 81 | right: -6px; 82 | top: 50%; 83 | margin-top: -5px; 84 | cursor: w-resize; 85 | .dot-drag > span { 86 | cursor: w-resize !important; 87 | } 88 | } 89 | &.line-dot-bottom { 90 | bottom: -6px; 91 | left: 50%; 92 | margin-left: -5px; 93 | cursor: s-resize; 94 | .dot-drag > span { 95 | cursor: s-resize !important; 96 | } 97 | } 98 | &.line-dot-left { 99 | left: -6px; 100 | top: 50%; 101 | margin-top: -5px; 102 | cursor: w-resize; 103 | .dot-drag > span { 104 | cursor: w-resize !important; 105 | } 106 | } 107 | } 108 | .dot-drag { 109 | position: relative; 110 | width: 100%; 111 | height: 100%; 112 | top: 0; 113 | left: 0; 114 | // background-color: #409eff; 115 | span { 116 | width: 100%; 117 | height: 100%; 118 | transform: scale(2); 119 | position: absolute; 120 | } 121 | } 122 | .edit-option { 123 | position: absolute; 124 | display: flex; 125 | right: 0px; 126 | top: -32px; 127 | z-index: 999; 128 | // background-color:red; 129 | .edit-btn { 130 | width: 20px; 131 | height: 20px; 132 | box-sizing: border-box; 133 | margin: 3px 5px; 134 | border: 1px solid rgba(0, 0, 0, 0.1); 135 | background-color: #fff; 136 | border-radius: 25px; 137 | cursor: pointer; 138 | pointer-events: auto; 139 | color: #666; 140 | font-size: 10px; 141 | text-align: center; 142 | line-height: 0.8; 143 | transform: scale(1.2); 144 | i { 145 | transform: scale(0.8); 146 | } 147 | &:hover { 148 | color: #fff; 149 | background-color: #409eff; 150 | border: 1px solid #409eff; 151 | } 152 | } 153 | } 154 | .directive { 155 | height: 30px; 156 | padding: 5px; 157 | border-radius: 5px 5px 0 0; 158 | margin-left: 5px; 159 | white-space: nowrap; 160 | background-color: #eaf0ff; 161 | color: #409eff; 162 | // opacity: 0.8; 163 | } 164 | .insert-box-top, 165 | .insert-box-bottom { 166 | pointer-events: auto; 167 | -webkit-user-drag: unset; 168 | position: absolute; 169 | width: 100%; 170 | height: 30px; 171 | line-height: 30px; 172 | text-align: center; 173 | background-color: #fbc422; 174 | border: 1px dashed #666; 175 | opacity: 0.3; 176 | white-space: nowrap; 177 | padding: 0; 178 | overflow: hidden; 179 | } 180 | .insert-box-top { 181 | top: 0; 182 | } 183 | .insert-box-bottom { 184 | bottom: 0; 185 | } 186 | } 187 | 188 | .edit-style-panel { 189 | width: 400px; 190 | max-height: 400px; 191 | overflow-x: hidden; 192 | overflow-y: auto; 193 | .label-title { 194 | border-left: 3px solid #409eff; 195 | padding-left: 3px; 196 | margin-top: 10px; 197 | display: block; 198 | } 199 | .input-item { 200 | width: 100%; 201 | height: 32px; 202 | margin: 10px 0; 203 | overflow: hidden; 204 | label { 205 | padding: 10px; 206 | } 207 | } 208 | .item-group { 209 | padding: 10px; 210 | } 211 | } 212 | 213 | .edit-action-panel { 214 | width: 400px; 215 | max-height: 400px; 216 | .action-tit { 217 | display: flex; 218 | justify-content: space-between; 219 | align-items: center; 220 | } 221 | .edit-action-code { 222 | width: 100%; 223 | height: 300px; 224 | margin-top: 10px; 225 | border-radius: 5px; 226 | overflow: hidden; 227 | } 228 | } 229 | 230 | .edit-css { 231 | width: 100%; 232 | min-height: 300px; 233 | } 234 | .change-edit-btn { 235 | font-size: 16px; 236 | position: absolute; 237 | top: 18px; 238 | right: 15px; 239 | cursor: pointer; 240 | &:hover { 241 | transform: scale(1.1); 242 | color: #409eff; 243 | } 244 | } 245 | .edit-content { 246 | width: 100%; 247 | } 248 | -------------------------------------------------------------------------------- /src/components/EditPage/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.less'; 2 | import { useRef } from 'react'; 3 | import { useSetState, useMount, useDrop, useClickAway, useEventListener, useScroll } from 'ahooks'; 4 | import { screen } from '@/utils/tool' 5 | 6 | import ZoomPlugin from './ZoomPlugin'; 7 | import EditLineBox from './EditLineBox'; 8 | 9 | import cookEngine from '@/ingredients/cookEngine' 10 | 11 | import STATE from '@/db/state'; 12 | // ace代码编辑器 13 | import 'ace-builds/src-noconflict/mode-css'; 14 | import 'ace-builds/src-noconflict/mode-javascript'; 15 | import 'ace-builds/src-noconflict/theme-monokai'; 16 | import 'ace-builds/src-noconflict/ext-language_tools'; 17 | 18 | 19 | const SCREEN = { 20 | width: screen.width(), 21 | height: screen.height() 22 | }; 23 | 24 | // 页面编辑容器 25 | const EditPage = function (EP) { 26 | const [lineState, setLineState] = useSetState({ 27 | show: false, 28 | style: {}, 29 | mapKey: '', 30 | }); 31 | 32 | const EditMainRef = useRef(null); 33 | const EditPageRef = useRef(null); 34 | 35 | const EditPage: HTMLDivElement = EditPageRef.current!; 36 | // 转移焦点 37 | const tranFocus = () => { 38 | const InputBlur = document.querySelector('#InputBlur') as HTMLInputElement; 39 | if (InputBlur) { 40 | InputBlur && InputBlur.focus(); 41 | } else { 42 | const inputBlurEl = document.createElement('input'); 43 | inputBlurEl.style.cssText = 'height:0;width:0;opacity:0;'; 44 | inputBlurEl.id = "InputBlur"; 45 | inputBlurEl.focus(); 46 | document.body.appendChild(inputBlurEl); 47 | } 48 | } 49 | // 清除聚焦框 50 | const clearFocus = () => { 51 | STATE.DRAG_EL = null; 52 | setLineState({ show: false }); 53 | const DefaultTip = document.querySelector('#DefaultTip'); 54 | DefaultTip && DefaultTip.remove(); 55 | 56 | } 57 | // 拖动元素释放 58 | useDrop(EditMainRef, { 59 | onDragEnter: (e) => { 60 | if (!e || !STATE.DRAG_EL) return; 61 | }, 62 | onDrop(e) { 63 | if (!e || !STATE.DRAG_EL) return; 64 | const curDom = e.target as HTMLDivElement; 65 | const mapKey = curDom.getAttribute('data-key'); 66 | if (mapKey) { 67 | STATE.MyRenderEngine.insert(curDom.getAttribute('data-pos'), STATE.DRAG_EL, mapKey) 68 | } else if (curDom.className.indexOf(cookEngine.prefix) > -1) { 69 | STATE.MyRenderEngine.add(STATE.DRAG_EL, curDom.getAttribute('key') || '') 70 | } else { 71 | STATE.MyRenderEngine.add(STATE.DRAG_EL) 72 | } 73 | clearFocus(); 74 | } 75 | }); 76 | 77 | // 点击外部清除聚焦框 78 | useClickAway((e) => { 79 | const node = e.target as HTMLDivElement; 80 | node.id === 'EditScreen' && clearFocus(); 81 | }, EditMainRef); 82 | 83 | // 点击元素显示聚焦框 84 | useEventListener('click', (e) => { 85 | const cookDom = e.target as HTMLDivElement; 86 | if (!cookDom.className.includes(cookEngine.prefix)) { 87 | clearFocus(); 88 | tranFocus(); 89 | return; 90 | }; 91 | clearFocus(); 92 | STATE.CUR_EL = cookDom as HTMLDivElement; 93 | STATE.CUR_KEY = cookDom.getAttribute('key') || ''; 94 | !STATE.CUR_EL.getAttribute('canEdit') && tranFocus(); 95 | 96 | const pnode = cookDom.parentNode as HTMLDivElement; 97 | let topVal = cookDom.offsetTop - pnode.scrollTop; 98 | let leftVal = cookDom.offsetLeft - pnode.scrollLeft; 99 | // 父元素定位处理 100 | if (pnode.style.position && pnode.style.position != 'static') { 101 | topVal += pnode.offsetTop; 102 | leftVal += pnode.offsetLeft; 103 | } 104 | // transform 处理 105 | const transform = pnode.style.transform || cookDom.style.transform || 'none' 106 | 107 | setLineState({ 108 | show: true, 109 | mapKey: STATE.CUR_KEY, 110 | style: { 111 | width: (cookDom.offsetWidth) + 'px', 112 | height: (cookDom.offsetHeight) + 'px', 113 | top: topVal + 'px', 114 | left: leftVal + 'px', 115 | transform: transform 116 | }, 117 | }); 118 | pnode.onscroll = function (e) { 119 | setLineState({ 120 | style: { 121 | width: (cookDom.offsetWidth) + 'px', 122 | height: (cookDom.offsetHeight) + 'px', 123 | top: (cookDom.offsetTop - pnode.scrollTop) + 'px', 124 | left: (cookDom.offsetLeft - pnode.scrollLeft) + 'px', 125 | transform: transform 126 | } 127 | }) 128 | } 129 | }, { target: EditPageRef }); 130 | 131 | // 滚动聚焦框位置改变 132 | useScroll(EditPageRef, (val) => { 133 | (lineState.show && STATE.CUR_EL) && setLineState({ 134 | style: { 135 | ...lineState.style, 136 | top: (STATE.CUR_EL.offsetTop - val.top) + 'px', 137 | left: (STATE.CUR_EL.offsetLeft - val.left) + 'px' 138 | } 139 | }) 140 | return true; 141 | }); 142 | const createEngine = () => { 143 | STATE.MyRenderEngine = new cookEngine(document.querySelector('#EditPageContainer')!); 144 | console.log('渲染引擎->', STATE.MyRenderEngine) 145 | setTimeout(() => { 146 | STATE.CUR_PAGE && (document.title = 'cooking-' + STATE.CUR_PAGE.title); 147 | }) 148 | } 149 | 150 | useMount(() => { 151 | createEngine(); 152 | }) 153 | // 页面改变 154 | EP.$switchPage.useSubscription((index) => { 155 | STATE.Panzoom.reset(); 156 | clearFocus(); 157 | createEngine(); 158 | }); 159 | 160 | return
    161 |
    162 |
    163 |

    请拖动布局

    164 |
    165 | {lineState.show ? : null} 166 |
    167 | 168 |
    169 | } 170 | 171 | 172 | export default EditPage; 173 | -------------------------------------------------------------------------------- /src/components/EditPage/EditLineBox.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.less'; 2 | import { useMount, useKeyPress, useEventEmitter } from 'ahooks'; 3 | import { Popconfirm, Popover } from 'antd'; 4 | import StylePanel from './StylePanel'; 5 | import ActionPanel from './ActionPanel'; 6 | import drag from '@/utils/drag' 7 | import STATE from '@/db/state'; 8 | 9 | // 是否等比 10 | let isEqualRatio = false; 11 | // 添加尺寸拖动 12 | const addDragSizeEvent = function (dragDots, setLineState, EditPage) { 13 | dragDots.map((item) => { 14 | const useDom = STATE.CUR_EL as HTMLDivElement; 15 | const useWidget = STATE.DOM_MAP[STATE.CUR_KEY] 16 | const DragDotEl: HTMLDivElement = document.querySelector('#dot-drag-' + item)!; 17 | let OriginHeight = useDom.offsetHeight; 18 | let OriginWidth = useDom.offsetWidth; 19 | let ratio = OriginHeight / OriginWidth; 20 | new drag(DragDotEl, { 21 | disableX: !'left,right'.includes(item), 22 | disableY: !'top,bottom'.includes(item), 23 | onchange(pos) { 24 | const tempSize = { 25 | width: useWidget.style.width, 26 | height: useWidget.style.height 27 | }; 28 | 29 | DragDotEl.style.top = DragDotEl.style.left = '0px'; 30 | 31 | if (useDom.clientWidth < 5 || useDom.clientHeight < 5) return; 32 | 33 | // 正常模式 34 | if (!isEqualRatio) { 35 | if (this.option.disableX) { 36 | tempSize.height = (OriginHeight + ('bottom'.includes(item) ? + pos.y : -pos.y)) + 'px' 37 | // useDom.style.height = (OriginHeight + ('bottom'.includes(item) ? + pos.y : -pos.y)) + 'px'; 38 | // useWidget.style.height = useDom.style.height; 39 | } 40 | if (this.option.disableY) { 41 | tempSize.width = (OriginWidth + ('right'.includes(item) ? +pos.x : -pos.x)) + 'px'; 42 | // useDom.style.width = (OriginWidth + ('right'.includes(item) ? +pos.x : -pos.x)) + 'px'; 43 | // useWidget.style.width = useDom.style.width; 44 | } 45 | 46 | } 47 | // 等比模式 48 | if (isEqualRatio) { 49 | if (this.option.disableX) { 50 | const h = OriginHeight + pos.y; 51 | tempSize.height = h + 'px'; 52 | tempSize.width = h / ratio + 'px'; 53 | // useDom.style.height = h + 'px'; 54 | // useDom.style.width = h / ratio + 'px'; 55 | } 56 | if (this.option.disableY) { 57 | 58 | const w = OriginWidth + pos.x; 59 | tempSize.width = w + 'px'; 60 | tempSize.height = w * ratio + 'px'; 61 | // useDom.style.width = w + 'px'; 62 | // useDom.style.height = w * ratio + 'px'; 63 | } 64 | // useWidget.style.height = useDom.style.height; 65 | // useWidget.style.width = useDom.style.width; 66 | } 67 | STATE.MyRenderEngine.modify(tempSize); 68 | setLineState({ 69 | style: { 70 | width: (useDom.offsetWidth) + 'px', 71 | height: (useDom.offsetHeight) + 'px', 72 | top: (useDom.offsetTop - EditPage.scrollTop) + 'px', 73 | left: (useDom.offsetLeft - EditPage.scrollLeft) + 'px' 74 | } 75 | }) 76 | 77 | } 78 | }) 79 | }) 80 | } 81 | 82 | // 聚焦编辑线 83 | const EditLineBox = (props) => { 84 | const { lineState, setLineState, EditPage } = props; 85 | const curWidget = STATE.DOM_MAP[lineState.mapKey]; 86 | const showAddBefore = curWidget && curWidget.child && !curWidget.stopBefore; 87 | const showAddAfter = curWidget && curWidget.child && !curWidget.stopAfter; 88 | // 键盘监听-按住shift保持等比缩放 89 | useKeyPress(['shift'], (event) => { 90 | isEqualRatio = event.type === 'keydown'; 91 | }, { 92 | events: ['keydown', 'keyup'], 93 | }); 94 | 95 | // 编辑节点动作 96 | const optionsAction = (e): void => { 97 | switch (e.id) { 98 | case 'del': 99 | STATE.MyRenderEngine.remove(STATE.CUR_EL); 100 | setLineState({ show: false }); 101 | break; 102 | case 'copy': 103 | STATE.MyRenderEngine.clone(STATE.CUR_EL) 104 | break; 105 | } 106 | } 107 | // window.onresize = () => { 108 | // (lineState.show && STATE.CUR_EL) && setLineState({ 109 | // style: { 110 | // width: (STATE.CUR_EL.clientWidth + 2) + 'px', 111 | // height: (STATE.CUR_EL.clientHeight + 2) + 'px', 112 | // top: (STATE.CUR_EL.offsetTop - EditPage.scrollTop) + 'px', 113 | // left: (STATE.CUR_EL.offsetLeft - EditPage.scrollLeft) + 'px' 114 | // } 115 | // }) 116 | // } 117 | useMount(() => { 118 | curWidget.dragDots && addDragSizeEvent(curWidget.dragDots, setLineState, EditPage); 119 | }) 120 | 121 | return
    122 | {curWidget.dragDots && curWidget.dragDots.map((item, index) =>
    )} 123 |
    124 | optionsAction({ id: 'del' })}> 125 | 126 | 127 | 128 | } 131 | title="定制样式" 132 | trigger="click" 133 | className='ant-pop' 134 | > 135 | 136 | 137 | } 140 | title="动作交互" 141 | trigger="click" 142 | className='ant-pop' 143 | > 144 | 145 | 146 | 147 |
    148 | {curWidget && curWidget.name} 149 |
    150 |
    151 | { 152 | showAddBefore ?
    153 | 此元素前添加 154 |
    : null 155 | } 156 | { 157 | showAddAfter ?
    158 | 此元素后添加 159 |
    : null 160 | } 161 |
    162 | } 163 | 164 | export default EditLineBox; 165 | -------------------------------------------------------------------------------- /src/assets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | font-size: 14px; 15 | } 16 | 17 | /* Sections 18 | ========================================================================== */ 19 | 20 | /** 21 | * Remove the margin in all browsers. 22 | */ 23 | 24 | body { 25 | margin: 0; 26 | font-size: 14px; 27 | line-height:1.15; 28 | } 29 | 30 | /** 31 | * Render the `main` element consistently in IE. 32 | */ 33 | 34 | main { 35 | display: block; 36 | } 37 | 38 | /** 39 | * Correct the font size and margin on `h1` elements within `section` and 40 | * `article` contexts in Chrome, Firefox, and Safari. 41 | */ 42 | 43 | h1 { 44 | font-size: 2em; 45 | margin: 0.67em 0; 46 | } 47 | 48 | /* Grouping content 49 | ========================================================================== */ 50 | 51 | /** 52 | * 1. Add the correct box sizing in Firefox. 53 | * 2. Show the overflow in Edge and IE. 54 | */ 55 | 56 | hr { 57 | box-sizing: content-box; /* 1 */ 58 | height: 0; /* 1 */ 59 | overflow: visible; /* 2 */ 60 | } 61 | 62 | /** 63 | * 1. Correct the inheritance and scaling of font size in all browsers. 64 | * 2. Correct the odd `em` font sizing in all browsers. 65 | */ 66 | 67 | pre { 68 | font-family: monospace, monospace; /* 1 */ 69 | font-size: 1em; /* 2 */ 70 | } 71 | 72 | /* Text-level semantics 73 | ========================================================================== */ 74 | 75 | /** 76 | * Remove the gray background on active links in IE 10. 77 | */ 78 | 79 | a { 80 | background-color: transparent; 81 | } 82 | 83 | /** 84 | * 1. Remove the bottom border in Chrome 57- 85 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 86 | */ 87 | 88 | abbr[title] { 89 | border-bottom: none; /* 1 */ 90 | text-decoration: underline; /* 2 */ 91 | text-decoration: underline dotted; /* 2 */ 92 | } 93 | 94 | /** 95 | * Add the correct font weight in Chrome, Edge, and Safari. 96 | */ 97 | 98 | b, 99 | strong { 100 | font-weight: bolder; 101 | } 102 | 103 | /** 104 | * 1. Correct the inheritance and scaling of font size in all browsers. 105 | * 2. Correct the odd `em` font sizing in all browsers. 106 | */ 107 | 108 | code, 109 | kbd, 110 | samp { 111 | font-family: monospace, monospace; /* 1 */ 112 | font-size: 1em; /* 2 */ 113 | } 114 | 115 | /** 116 | * Add the correct font size in all browsers. 117 | */ 118 | 119 | small { 120 | font-size: 80%; 121 | } 122 | 123 | /** 124 | * Prevent `sub` and `sup` elements from affecting the line height in 125 | * all browsers. 126 | */ 127 | 128 | sub, 129 | sup { 130 | font-size: 75%; 131 | line-height: 0; 132 | position: relative; 133 | vertical-align: baseline; 134 | } 135 | 136 | sub { 137 | bottom: -0.25em; 138 | } 139 | 140 | sup { 141 | top: -0.5em; 142 | } 143 | 144 | /* Embedded content 145 | ========================================================================== */ 146 | 147 | /** 148 | * Remove the border on images inside links in IE 10. 149 | */ 150 | 151 | img { 152 | border-style: none; 153 | } 154 | 155 | /* Forms 156 | ========================================================================== */ 157 | 158 | /** 159 | * 1. Change the font styles in all browsers. 160 | * 2. Remove the margin in Firefox and Safari. 161 | */ 162 | 163 | button, 164 | input, 165 | optgroup, 166 | select, 167 | textarea { 168 | font-family: inherit; /* 1 */ 169 | font-size: 100%; /* 1 */ 170 | line-height: 1.15; /* 1 */ 171 | margin: 0; /* 2 */ 172 | outline: none; 173 | } 174 | button { 175 | border: 0; 176 | background: none; 177 | padding: 0; 178 | } 179 | /** 180 | * Show the overflow in IE. 181 | * 1. Show the overflow in Edge. 182 | */ 183 | 184 | button, 185 | input { 186 | /* 1 */ 187 | overflow: visible; 188 | } 189 | 190 | /** 191 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 192 | * 1. Remove the inheritance of text transform in Firefox. 193 | */ 194 | 195 | button, 196 | select { 197 | /* 1 */ 198 | text-transform: none; 199 | } 200 | 201 | /** 202 | * Correct the inability to style clickable types in iOS and Safari. 203 | */ 204 | 205 | button, 206 | [type='button'], 207 | [type='reset'], 208 | [type='submit'] { 209 | -webkit-appearance: button; 210 | } 211 | 212 | /** 213 | * Remove the inner border and padding in Firefox. 214 | */ 215 | 216 | button::-moz-focus-inner, 217 | [type='button']::-moz-focus-inner, 218 | [type='reset']::-moz-focus-inner, 219 | [type='submit']::-moz-focus-inner { 220 | border-style: none; 221 | padding: 0; 222 | } 223 | 224 | /** 225 | * Restore the focus styles unset by the previous rule. 226 | */ 227 | 228 | button:-moz-focusring, 229 | [type='button']:-moz-focusring, 230 | [type='reset']:-moz-focusring, 231 | [type='submit']:-moz-focusring { 232 | outline: 1px dotted ButtonText; 233 | } 234 | 235 | /** 236 | * Correct the padding in Firefox. 237 | */ 238 | 239 | fieldset { 240 | padding: 0.35em 0.75em 0.625em; 241 | } 242 | 243 | /** 244 | * 1. Correct the text wrapping in Edge and IE. 245 | * 2. Correct the color inheritance from `fieldset` elements in IE. 246 | * 3. Remove the padding so developers are not caught out when they zero out 247 | * `fieldset` elements in all browsers. 248 | */ 249 | 250 | legend { 251 | box-sizing: border-box; /* 1 */ 252 | color: inherit; /* 2 */ 253 | display: table; /* 1 */ 254 | max-width: 100%; /* 1 */ 255 | padding: 0; /* 3 */ 256 | white-space: normal; /* 1 */ 257 | } 258 | 259 | /** 260 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 261 | */ 262 | 263 | progress { 264 | vertical-align: baseline; 265 | } 266 | 267 | /** 268 | * Remove the default vertical scrollbar in IE 10+. 269 | */ 270 | 271 | textarea { 272 | overflow: auto; 273 | } 274 | 275 | /** 276 | * 1. Add the correct box sizing in IE 10. 277 | * 2. Remove the padding in IE 10. 278 | */ 279 | 280 | [type='checkbox'], 281 | [type='radio'] { 282 | box-sizing: border-box; /* 1 */ 283 | padding: 0; /* 2 */ 284 | } 285 | 286 | /** 287 | * Correct the cursor style of increment and decrement buttons in Chrome. 288 | */ 289 | 290 | [type='number']::-webkit-inner-spin-button, 291 | [type='number']::-webkit-outer-spin-button { 292 | height: auto; 293 | } 294 | 295 | /** 296 | * 1. Correct the odd appearance in Chrome and Safari. 297 | * 2. Correct the outline style in Safari. 298 | */ 299 | 300 | [type='search'] { 301 | -webkit-appearance: textfield; /* 1 */ 302 | outline-offset: -2px; /* 2 */ 303 | } 304 | 305 | /** 306 | * Remove the inner padding in Chrome and Safari on macOS. 307 | */ 308 | 309 | [type='search']::-webkit-search-decoration { 310 | -webkit-appearance: none; 311 | } 312 | 313 | /** 314 | * 1. Correct the inability to style clickable types in iOS and Safari. 315 | * 2. Change font properties to `inherit` in Safari. 316 | */ 317 | 318 | ::-webkit-file-upload-button { 319 | -webkit-appearance: button; /* 1 */ 320 | font: inherit; /* 2 */ 321 | } 322 | 323 | /* Interactive 324 | ========================================================================== */ 325 | 326 | /* 327 | * Add the correct display in Edge, IE 10+, and Firefox. 328 | */ 329 | 330 | details { 331 | display: block; 332 | } 333 | 334 | /* 335 | * Add the correct display in all browsers. 336 | */ 337 | 338 | summary { 339 | display: list-item; 340 | } 341 | 342 | /* Misc 343 | ========================================================================== */ 344 | 345 | /** 346 | * Add the correct display in IE 10+. 347 | */ 348 | 349 | template { 350 | display: none; 351 | } 352 | 353 | /** 354 | * Add the correct display in IE 10. 355 | */ 356 | 357 | [hidden] { 358 | display: none; 359 | } 360 | 361 | ul { 362 | margin: 0; 363 | padding: 0; 364 | } 365 | li { 366 | list-style: none; 367 | } 368 | 369 | div { 370 | box-sizing: border-box; 371 | } 372 | -------------------------------------------------------------------------------- /src/ingredients/export/resetCssTemplate.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ` 3 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 4 | 5 | /* Document 6 | ========================================================================== */ 7 | 8 | /** 9 | * 1. Correct the line height in all browsers. 10 | * 2. Prevent adjustments of font size after orientation changes in iOS. 11 | */ 12 | 13 | html { 14 | line-height: 1.15; /* 1 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | font-size: 14px; 17 | } 18 | 19 | /* Sections 20 | ========================================================================== */ 21 | 22 | /** 23 | * Remove the margin in all browsers. 24 | */ 25 | 26 | body { 27 | margin: 0; 28 | font-size: 14px; 29 | line-height:1.15; 30 | } 31 | 32 | /** 33 | * Render the --main-- element consistently in IE. 34 | */ 35 | 36 | main { 37 | display: block; 38 | } 39 | 40 | /** 41 | * Correct the font size and margin on --h1-- elements within --section-- and 42 | * --article-- contexts in Chrome, Firefox, and Safari. 43 | */ 44 | 45 | h1 { 46 | font-size: 2em; 47 | margin: 0.67em 0; 48 | } 49 | 50 | /* Grouping content 51 | ========================================================================== */ 52 | 53 | /** 54 | * 1. Add the correct box sizing in Firefox. 55 | * 2. Show the overflow in Edge and IE. 56 | */ 57 | 58 | hr { 59 | box-sizing: content-box; /* 1 */ 60 | height: 0; /* 1 */ 61 | overflow: visible; /* 2 */ 62 | } 63 | 64 | /** 65 | * 1. Correct the inheritance and scaling of font size in all browsers. 66 | * 2. Correct the odd --em-- font sizing in all browsers. 67 | */ 68 | 69 | pre { 70 | font-family: monospace, monospace; /* 1 */ 71 | font-size: 1em; /* 2 */ 72 | } 73 | 74 | /* Text-level semantics 75 | ========================================================================== */ 76 | 77 | /** 78 | * Remove the gray background on active links in IE 10. 79 | */ 80 | 81 | a { 82 | background-color: transparent; 83 | } 84 | 85 | /** 86 | * 1. Remove the bottom border in Chrome 57- 87 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 88 | */ 89 | 90 | abbr[title] { 91 | border-bottom: none; /* 1 */ 92 | text-decoration: underline; /* 2 */ 93 | text-decoration: underline dotted; /* 2 */ 94 | } 95 | 96 | /** 97 | * Add the correct font weight in Chrome, Edge, and Safari. 98 | */ 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | /** 106 | * 1. Correct the inheritance and scaling of font size in all browsers. 107 | * 2. Correct the odd --em-- font sizing in all browsers. 108 | */ 109 | 110 | code, 111 | kbd, 112 | samp { 113 | font-family: monospace, monospace; /* 1 */ 114 | font-size: 1em; /* 2 */ 115 | } 116 | 117 | /** 118 | * Add the correct font size in all browsers. 119 | */ 120 | 121 | small { 122 | font-size: 80%; 123 | } 124 | 125 | /** 126 | * Prevent --sub-- and --sup-- elements from affecting the line height in 127 | * all browsers. 128 | */ 129 | 130 | sub, 131 | sup { 132 | font-size: 75%; 133 | line-height: 0; 134 | position: relative; 135 | vertical-align: baseline; 136 | } 137 | 138 | sub { 139 | bottom: -0.25em; 140 | } 141 | 142 | sup { 143 | top: -0.5em; 144 | } 145 | 146 | /* Embedded content 147 | ========================================================================== */ 148 | 149 | /** 150 | * Remove the border on images inside links in IE 10. 151 | */ 152 | 153 | img { 154 | border-style: none; 155 | } 156 | 157 | /* Forms 158 | ========================================================================== */ 159 | 160 | /** 161 | * 1. Change the font styles in all browsers. 162 | * 2. Remove the margin in Firefox and Safari. 163 | */ 164 | 165 | button, 166 | input, 167 | optgroup, 168 | select, 169 | textarea { 170 | font-family: inherit; /* 1 */ 171 | font-size: 100%; /* 1 */ 172 | line-height: 1.15; /* 1 */ 173 | margin: 0; /* 2 */ 174 | outline: none; 175 | } 176 | button { 177 | border: 0; 178 | background: none; 179 | padding: 0; 180 | } 181 | /** 182 | * Show the overflow in IE. 183 | * 1. Show the overflow in Edge. 184 | */ 185 | 186 | button, 187 | input { 188 | /* 1 */ 189 | overflow: visible; 190 | } 191 | 192 | /** 193 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 194 | * 1. Remove the inheritance of text transform in Firefox. 195 | */ 196 | 197 | button, 198 | select { 199 | /* 1 */ 200 | text-transform: none; 201 | } 202 | 203 | /** 204 | * Correct the inability to style clickable types in iOS and Safari. 205 | */ 206 | 207 | button, 208 | [type='button'], 209 | [type='reset'], 210 | [type='submit'] { 211 | -webkit-appearance: button; 212 | } 213 | 214 | /** 215 | * Remove the inner border and padding in Firefox. 216 | */ 217 | 218 | button::-moz-focus-inner, 219 | [type='button']::-moz-focus-inner, 220 | [type='reset']::-moz-focus-inner, 221 | [type='submit']::-moz-focus-inner { 222 | border-style: none; 223 | padding: 0; 224 | } 225 | 226 | /** 227 | * Restore the focus styles unset by the previous rule. 228 | */ 229 | 230 | button:-moz-focusring, 231 | [type='button']:-moz-focusring, 232 | [type='reset']:-moz-focusring, 233 | [type='submit']:-moz-focusring { 234 | outline: 1px dotted ButtonText; 235 | } 236 | 237 | /** 238 | * Correct the padding in Firefox. 239 | */ 240 | 241 | fieldset { 242 | padding: 0.35em 0.75em 0.625em; 243 | } 244 | 245 | /** 246 | * 1. Correct the text wrapping in Edge and IE. 247 | * 2. Correct the color inheritance from --fieldset-- elements in IE. 248 | * 3. Remove the padding so developers are not caught out when they zero out 249 | * --fieldset-- elements in all browsers. 250 | */ 251 | 252 | legend { 253 | box-sizing: border-box; /* 1 */ 254 | color: inherit; /* 2 */ 255 | display: table; /* 1 */ 256 | max-width: 100%; /* 1 */ 257 | padding: 0; /* 3 */ 258 | white-space: normal; /* 1 */ 259 | } 260 | 261 | /** 262 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 263 | */ 264 | 265 | progress { 266 | vertical-align: baseline; 267 | } 268 | 269 | /** 270 | * Remove the default vertical scrollbar in IE 10+. 271 | */ 272 | 273 | textarea { 274 | overflow: auto; 275 | } 276 | 277 | /** 278 | * 1. Add the correct box sizing in IE 10. 279 | * 2. Remove the padding in IE 10. 280 | */ 281 | 282 | [type='checkbox'], 283 | [type='radio'] { 284 | box-sizing: border-box; /* 1 */ 285 | padding: 0; /* 2 */ 286 | } 287 | 288 | /** 289 | * Correct the cursor style of increment and decrement buttons in Chrome. 290 | */ 291 | 292 | [type='number']::-webkit-inner-spin-button, 293 | [type='number']::-webkit-outer-spin-button { 294 | height: auto; 295 | } 296 | 297 | /** 298 | * 1. Correct the odd appearance in Chrome and Safari. 299 | * 2. Correct the outline style in Safari. 300 | */ 301 | 302 | [type='search'] { 303 | -webkit-appearance: textfield; /* 1 */ 304 | outline-offset: -2px; /* 2 */ 305 | } 306 | 307 | /** 308 | * Remove the inner padding in Chrome and Safari on macOS. 309 | */ 310 | 311 | [type='search']::-webkit-search-decoration { 312 | -webkit-appearance: none; 313 | } 314 | 315 | /** 316 | * 1. Correct the inability to style clickable types in iOS and Safari. 317 | * 2. Change font properties to --inherit-- in Safari. 318 | */ 319 | 320 | ::-webkit-file-upload-button { 321 | -webkit-appearance: button; /* 1 */ 322 | font: inherit; /* 2 */ 323 | } 324 | 325 | /* Interactive 326 | ========================================================================== */ 327 | 328 | /* 329 | * Add the correct display in Edge, IE 10+, and Firefox. 330 | */ 331 | 332 | details { 333 | display: block; 334 | } 335 | 336 | /* 337 | * Add the correct display in all browsers. 338 | */ 339 | 340 | summary { 341 | display: list-item; 342 | } 343 | 344 | /* Misc 345 | ========================================================================== */ 346 | 347 | /** 348 | * Add the correct display in IE 10+. 349 | */ 350 | 351 | template { 352 | display: none; 353 | } 354 | 355 | /** 356 | * Add the correct display in IE 10. 357 | */ 358 | 359 | [hidden] { 360 | display: none; 361 | } 362 | 363 | ul { 364 | margin: 0; 365 | padding: 0; 366 | } 367 | li { 368 | list-style: none; 369 | } 370 | 371 | div { 372 | box-sizing: border-box; 373 | } 374 | ::-webkit-scrollbar { 375 | width: 3px; 376 | height: 3px; 377 | } 378 | ::-webkit-scrollbar-track { 379 | width: 3px; 380 | background-color: rgba(0, 0, 0, 0); 381 | -webkit-border-radius: 2em; 382 | -moz-border-radius: 5px; 383 | border-radius: 2em; 384 | } 385 | ::-webkit-scrollbar-thumb { 386 | background-color: rgba(0, 0, 0, 0.3); 387 | background-clip: padding-box; 388 | min-height: 20px; 389 | -webkit-border-radius: 2em; 390 | -moz-border-radius: 2em; 391 | border-radius: 2em; 392 | } 393 | ::-webkit-scrollbar-thumb:hover { 394 | background-color: rgba(0, 0, 0, 1); 395 | } 396 | ` 397 | 398 | } 399 | -------------------------------------------------------------------------------- /public/libs/panzoom.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Panzoom for panning and zooming elements using CSS transforms 3 | * Copyright Timmy Willison and other contributors 4 | * https://github.com/timmywil/panzoom/blob/main/MIT-License.txt 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Panzoom=e()}(this,function(){"use strict";var Y=function(){return(Y=Object.assign||function(t){for(var e,n=1,o=arguments.length;n 0) { 127 | e.child.forEach(n => { 128 | tempDom.appendChild(CoreHandler._domHandler(n, isType, exd)) 129 | }) 130 | } 131 | 132 | return tempFragments; 133 | } 134 | // 导出样式处理 135 | static _exportCssHandler(className: string, style: CSSStyleDeclaration, exd: exportData): string { 136 | let tempCss = ''; 137 | for (let key in style) { 138 | let val = style[key] ? style[key].toString() : ''; 139 | if (Util.IsImgBase64(val)) { 140 | const imgName = (((1 + Math.random()) * 0x10000000) | 0).toString(10); 141 | let ext = val.includes('data:image/jpeg') ? '.jpg' : '.png'; 142 | val = `url(../images/${imgName + ext})`; 143 | exd.img[imgName + ext] = Util.FlitterImgUrl(style[key]); 144 | } 145 | // 脏图片数据处理 146 | val = val.includes('data:') ? '' : val; 147 | tempCss += `${Util.FormatCssKey(key)}:${val};`; 148 | } 149 | return `.${className}{${tempCss}}\n`; 150 | } 151 | // 导出js处理 152 | static _exportJsHandler(events: string) { 153 | if (!events) return ''; 154 | // 事件转义 155 | return `${events}\n\n`; 156 | } 157 | // 导出源码处理 158 | static _exportPageHandler(page: PageItem): exportData { 159 | const tempRoot: HTMLDivElement = document.createElement('div'); 160 | const exportData = { html: '', css: '', js: '', img: {} }; 161 | page.child && page.child.forEach(e => { 162 | tempRoot.appendChild(CoreHandler._domHandler(e, 'export', exportData)); 163 | }); 164 | exportData.html = tempRoot.innerHTML; 165 | return exportData; 166 | } 167 | // 预览 168 | static preview(page: PageItem, target): void { 169 | if (!page.child) return; 170 | const previewData = { html: '', css: '', js: '', img: {} }; 171 | page.child.forEach(e => { 172 | target.appendChild(CoreHandler._domHandler(e, 'preview', previewData)) 173 | }); 174 | // 事件处理 175 | const script = document.createElement('script'); 176 | script.id = 'event_' + page.id; 177 | script.innerHTML = previewData.js; 178 | document.body.appendChild(script); 179 | 180 | } 181 | // 生成类名 182 | static _makeHashName(type): string { 183 | let hashName = type + '_' + (((1 + Math.random()) * 0x10000000) | 0).toString(16); 184 | hashName = !CoreHandler.testHashName.includes(hashName) ? hashName : hashName + 'x2'; 185 | CoreHandler.testHashName += hashName + '/'; 186 | return hashName; 187 | } 188 | // 开发模式-生成Dom节点 189 | _makeDom(e: DomItem): DocumentFragment { 190 | return CoreHandler._domHandler(e, 'dev'); 191 | } 192 | // 删除Dom节点 193 | _delDom(p: DomItem | PageItem, t: string): void { 194 | p.child && Util.CompareRemove(p.child, { a: 'className', b: t }) 195 | } 196 | // 更新节点 197 | _updateDom(dom) { 198 | let styleObject = {}; 199 | for (const key in STATE.DOM_MAP[STATE.CUR_KEY].style) { 200 | if (Object.prototype.hasOwnProperty.call(STATE.DOM_MAP[STATE.CUR_KEY].style, key)) { 201 | const element = STATE.DOM_MAP[STATE.CUR_KEY].style[key]; 202 | if (element !== undefined) { 203 | styleObject[key] = ['px'].indexOf(element) >= 0 ? '' : element; 204 | } else { 205 | styleObject[key] = ''; 206 | } 207 | } 208 | } 209 | Object.assign(dom.style, styleObject); 210 | } 211 | // 清空编辑框 212 | _clearAll() { 213 | this.$root.innerHTML = ''; 214 | } 215 | 216 | } 217 | 218 | export default class cookEngine extends CoreHandler { 219 | constructor(el: HTMLDivElement) { 220 | super(el); 221 | this.$root = el; 222 | this.name = 'cookEngine-' + cookEngine.cacheId; 223 | cookEngine.cacheId++; 224 | this.init(); 225 | } 226 | name: string; 227 | $root: HTMLDivElement; 228 | init() { 229 | // @ts-ignore 230 | const pChild = STATE.CUR_PAGE.child; 231 | if (!pChild) return; 232 | this._clearAll(); 233 | pChild.forEach(e => { 234 | this.$root.appendChild(this._makeDom(e)) 235 | }); 236 | 237 | } 238 | // 新增 239 | add(e: DomItem, mapKey?: string): void { 240 | const newDom = this._makeDom(e); 241 | if (!mapKey) { 242 | STATE.CUR_PAGE?.child?.push(e); 243 | this.$root.appendChild(newDom) 244 | useDB.updateStore(); 245 | return; 246 | } 247 | const useWidget = STATE.DOM_MAP[mapKey]; 248 | if (useWidget && useWidget.stopAdd) return; 249 | if (useWidget.child) { 250 | const cuSet = useWidget.child; 251 | cuSet.push(e); 252 | const curDom = document.querySelector('.' + mapKey) as HTMLElement; 253 | curDom.appendChild(newDom) 254 | } 255 | useDB.updateStore(); 256 | } 257 | // 插入 258 | insert(t: 'before' | 'after', e: DomItem, mapKey: string): void { 259 | const newDom = this._makeDom(e); 260 | const curDom = document.querySelector('.' + mapKey) as HTMLElement; 261 | 262 | let parentNode = curDom.parentNode as HTMLDivElement; 263 | let pMapKey = parentNode.getAttribute('key') || ''; 264 | let cuSet: Array = []; 265 | if (!pMapKey.includes(cookEngine.prefix)) { 266 | parentNode = this.$root; 267 | cuSet = STATE.CUR_PAGE?.child!; 268 | } else { 269 | const useWidget = STATE.DOM_MAP[pMapKey]; 270 | if (useWidget.stopAdd) return; 271 | cuSet = useWidget.child || []; 272 | } 273 | 274 | let fdx = cuSet.findIndex(item => item.className === mapKey); 275 | if (fdx === -1) return; 276 | if (t === 'before') { 277 | cuSet.splice(fdx - 1, 0, e); 278 | parentNode.insertBefore(newDom, curDom); 279 | } 280 | if (t === 'after') { 281 | cuSet.splice(fdx + 1, 0, e); 282 | !curDom.nextSibling ? parentNode.appendChild(newDom) : parentNode.insertBefore(newDom, curDom.nextSibling) 283 | } 284 | useDB.updateStore(); 285 | } 286 | // 删除 287 | remove(el: HTMLDivElement) { 288 | const mapKey = el.getAttribute('key') || ''; 289 | // @ts-ignore 290 | const pMapKey = el.parentNode.getAttribute('key') || ''; 291 | if (pMapKey.includes(cookEngine.prefix)) { 292 | const curItem = STATE.DOM_MAP[pMapKey]; 293 | curItem && this._delDom(curItem, mapKey); 294 | delete STATE.DOM_MAP[mapKey]; 295 | } else { 296 | STATE.CUR_PAGE && this._delDom(STATE.CUR_PAGE, mapKey); 297 | } 298 | el.remove(); 299 | useDB.updateStore(); 300 | } 301 | // 克隆 302 | clone(el: HTMLDivElement) { 303 | const mapKey = el.getAttribute('key') || ''; 304 | const curItem = STATE.DOM_MAP[mapKey]; 305 | if (!curItem) return; 306 | const newItem = deepCopyObj(curItem); 307 | (function removeClass(item) { 308 | delete item.className; 309 | if (item.child && item.child.length > 1) { 310 | item.child.forEach(n => removeClass(n)); 311 | } 312 | })(newItem) 313 | // @ts-ignore 314 | this.insert('after', newItem, mapKey) 315 | } 316 | // 修改(样式,绑定数据,事件等) 317 | modify(newStyle: CSSStyleDeclaration) { 318 | Object.assign(STATE.DOM_MAP[STATE.CUR_KEY].style, newStyle); 319 | this._updateDom(STATE.CUR_EL); 320 | useDB.updateStore(); 321 | } 322 | //打包输出源码 323 | download() { 324 | exportCode(STATE, CoreHandler); 325 | } 326 | } 327 | 328 | -------------------------------------------------------------------------------- /src/components/EditPage/StylePanel.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.less'; 2 | import { useSetState } from 'ahooks'; 3 | import { Space, Input, Select, Slider, Radio } from 'antd'; 4 | import { CodepenOutlined } from '@ant-design/icons'; 5 | import UploadImg from './UploadImg'; 6 | 7 | import STATE from '@/db/state'; 8 | import { styleObjectToCodeString, styleCodeStringToObject, styleObjectUndefined } from '@/utils/styleFormat'; 9 | // ace代码编辑器 10 | import AceEditor from 'react-ace'; 11 | 12 | const { Option } = Select; 13 | const Util = { 14 | FlitterNumber(str) { 15 | if (str === 'auto' || !str) { 16 | return ''; 17 | } 18 | const n = parseFloat(str); 19 | return isNaN(n) ? '' : n; 20 | }, 21 | FlitterUnit(str) { 22 | const res = this.FlitterNumber(str); 23 | return res == '' ? 'px' : str.replace(this.FlitterNumber(str), ''); 24 | }, 25 | FlitterImgUrl(str) { 26 | if (!str) return ''; 27 | let regBackgroundUrl = /url\("?'?.*"?'?\)/g; 28 | let regReplace = /"|'|url|\(|\)/g; 29 | return str.match(regBackgroundUrl)[0].replace(regReplace, ''); 30 | }, 31 | IsImgBase64(str) { 32 | if (!str) return false; 33 | return str.includes('base64,'); 34 | }, 35 | BgPos(str) { 36 | if (str === 'cover') { 37 | return 'full'; 38 | } 39 | if (str === 'unset') { 40 | return 'repeat'; 41 | } 42 | if (str === 'contain') { 43 | return 'center'; 44 | } 45 | return ''; 46 | }, 47 | }; 48 | 49 | const sizeArr = [ 50 | { text: '宽', key: 'width' }, 51 | { text: '高', key: 'height' }, 52 | { text: '最小宽', key: 'minWeight', disableAuto: true }, 53 | { text: '最大宽', key: 'maxWeight', disableAuto: true }, 54 | { text: '最小高', key: 'minHeight', disableAuto: true }, 55 | { text: '最大高', key: 'maxHeight', disableAuto: true }, 56 | ]; 57 | const directionArr = [ 58 | { text: '上', key: 'Top', key2: 'top' }, 59 | { text: '右', key: 'Right', key2: 'right' }, 60 | { text: '下', key: 'Bottom', key2: 'bottom' }, 61 | { text: '左', key: 'Left', key2: 'left' }, 62 | ]; 63 | 64 | const angleArr = [ 65 | { text: '上左', key: 'TopLeft' }, 66 | { text: '上右', key: 'TopRight' }, 67 | { text: '下右', key: 'BottomRight' }, 68 | { text: '下左', key: 'BottomLeft' }, 69 | ]; 70 | 71 | const StylePanel = (props) => { 72 | const curWidget = STATE.DOM_MAP[STATE.CUR_KEY]; 73 | const [useStyle, setUseStyle] = useSetState({ ...curWidget.style }); 74 | // console.log(useStyle) 75 | const [styleCode, setStyleCode] = useSetState({ string: '' }); 76 | const [edit, setEdit] = useSetState({ is: false }); 77 | 78 | // 样式对象转换成样式字符串 79 | const ObjectToCode = (style) => { 80 | let styleCodeString = styleObjectToCodeString(style); 81 | setStyleCode({ string: styleCodeString }); 82 | }; 83 | // 样式字符串转成样式对象 84 | const CodeToObject = (pStyleCodeString) => { 85 | let styleObj = styleCodeStringToObject(pStyleCodeString); 86 | let useStyleObject = styleObjectUndefined(useStyle, styleObj); 87 | setUseStyle(useStyleObject); 88 | STATE.MyRenderEngine.modify(useStyleObject); 89 | }; 90 | const UnitSelect = (p) => { 91 | return ( 92 | 98 | ); 99 | }; 100 | 101 | const updateStyle = { 102 | size(key, v, u) { 103 | const temp = {}; 104 | temp[key] = v + u; 105 | setUseStyle(temp); 106 | STATE.MyRenderEngine.modify(temp); 107 | }, 108 | bg(key, v) { 109 | let temp = {}; 110 | if (key != 'type') { 111 | temp[key] = v; 112 | } else { 113 | switch (v) { 114 | case 'full': 115 | temp = { 116 | backgroundRepeat: 'no-repeat', 117 | backgroundSize: 'cover', 118 | backgroundPosition: 'center', 119 | }; 120 | break; 121 | case 'repeat': 122 | temp = { 123 | backgroundRepeat: 'repeat', 124 | backgroundSize: 'unset', 125 | backgroundPosition: 'normal', 126 | }; 127 | break; 128 | case 'center': 129 | temp = { 130 | backgroundRepeat: 'no-repeat', 131 | backgroundSize: 'contain', 132 | backgroundPosition: 'center', 133 | }; 134 | break; 135 | } 136 | } 137 | setUseStyle(temp); 138 | STATE.MyRenderEngine.modify(temp); 139 | }, 140 | direction(key, v, n?) { 141 | const temp = {}; 142 | temp[key] = v + (!n ? 'px' : ''); 143 | setUseStyle(temp); 144 | STATE.MyRenderEngine.modify(temp); 145 | }, 146 | base(key, v) { 147 | const temp = {}; 148 | temp[key] = v; 149 | setUseStyle(temp); 150 | STATE.MyRenderEngine.modify(temp); 151 | }, 152 | }; 153 | return ( 154 |
    155 |
    { 159 | setEdit({ is: !edit.is }); 160 | ObjectToCode(useStyle); 161 | }} 162 | > 163 | 164 | {/* object/code */} 165 |
    166 | {edit.is ? ( 167 |
    168 | { 173 | setStyleCode({ string: value }); 174 | CodeToObject(value); 175 | }} 176 | value={styleCode.string} 177 | name="UNIQUE_ID_CSS_CODE" 178 | fontSize={16} 179 | showGutter={false} 180 | setOptions={{ 181 | useWorker: false, 182 | enableBasicAutocompletion: true, 183 | enableLiveAutocompletion: true, 184 | }} 185 | style={{ width: '100%', height: '400px' }} 186 | /> 187 |
    188 | ) : ( 189 | 190 | 191 | 192 | {sizeArr.map((item) => ( 193 | { 203 | updateStyle.size(item.key, Util.FlitterNumber(useStyle[item.key]), e); 204 | }} 205 | /> 206 | } 207 | type="number" 208 | onChange={(e) => { 209 | updateStyle.size(item.key, e.target.value, Util.FlitterUnit(useStyle[item.key])); 210 | }} 211 | /> 212 | ))} 213 | 214 | 215 | 216 | { 222 | updateStyle.bg('backgroundColor', e.target.value); 223 | }} 224 | /> 225 | { 230 | updateStyle.bg('backgroundColor', e.target.value); 231 | }} 232 | /> 233 |
    234 | 235 | { 242 | console.log(e); 243 | }} 244 | /> 245 |
    246 | { 251 | updateStyle.bg('backgroundImage', `url(${e.target.value})`); 252 | }} 253 | /> 254 | { 263 | updateStyle.bg('type', e.target.value); 264 | }} 265 | optionType="button" 266 | /> 267 | { 272 | updateStyle.bg('backgroundImage', `url(${e})`); 273 | }} 274 | /> 275 |
    276 | 277 | 278 | 279 |
    280 | 281 | 296 |
    297 |
    298 | 299 | 313 |
    314 |
    315 | 316 | 317 | 330 | 331 | 332 | 333 |
    334 | 335 | 340 |
    341 |
    342 | 343 | 348 |
    349 |
    350 |
    351 | 352 | 353 | 367 | 368 | 369 | 370 | {directionArr.map((item) => ( 371 | { 377 | updateStyle.base(item.key2, e.target.value); 378 | }} 379 | /> 380 | ))} 381 | 382 | 383 | 384 | 385 | { 391 | updateStyle.base('color', e.target.value); 392 | }} 393 | /> 394 | { 401 | updateStyle.base('fontSize', e.target.value + 'px'); 402 | }} 403 | /> 404 | { 411 | updateStyle.base('lineHeight', e.target.value + 'px'); 412 | }} 413 | /> 414 |
    415 | 416 | 428 |
    429 |
    430 | 431 | 447 |
    448 |
    449 | 450 | 451 | 452 | { 459 | updateStyle.direction('margin', e.target.value); 460 | }} 461 | /> 462 | { 469 | updateStyle.direction('padding', e.target.value); 470 | }} 471 | /> 472 | 473 | 474 | 475 | {directionArr.map((item) => ( 476 | { 484 | updateStyle.direction('margin' + item.key, e.target.value); 485 | }} 486 | /> 487 | ))} 488 | 489 | 490 | 491 | {directionArr.map((item) => ( 492 | { 500 | updateStyle.direction('padding' + item.key, e.target.value); 501 | }} 502 | /> 503 | ))} 504 | 505 | 506 | 507 | 508 | { 515 | updateStyle.direction('borderWidth', e.target.value); 516 | }} 517 | /> 518 | 531 | { 537 | updateStyle.direction('borderColor', e.target.value, true); 538 | }} 539 | /> 540 | 541 | 542 | 543 | {directionArr.map((item) => ( 544 | { 552 | updateStyle.direction('border' + item.key + 'Width', e.target.value); 553 | }} 554 | /> 555 | ))} 556 | 557 | 558 | 559 | {directionArr.map((item) => ( 560 | { 568 | updateStyle.direction('border' + item.key + 'Color', e.target.value, true); 569 | }} 570 | /> 571 | ))} 572 | 573 | 574 | 575 | { 581 | updateStyle.direction('borderRadius', e.target.value); 582 | }} 583 | /> 584 | 585 | 586 | {angleArr.map((item) => ( 587 | { 595 | updateStyle.direction('border' + item.key + 'Radius', e.target.value); 596 | }} 597 | /> 598 | ))} 599 | 600 |
    601 | )} 602 |
    603 | ); 604 | }; 605 | export default StylePanel; 606 | --------------------------------------------------------------------------------