├── .browserslistrc ├── result.png ├── babel.config.js ├── src ├── components │ ├── LImage │ │ ├── index.ts │ │ └── LImage.vue │ ├── LText │ │ ├── index.ts │ │ └── LText.vue │ ├── LShape │ │ ├── index.ts │ │ └── LShape.vue │ └── FinalPage │ │ ├── index.ts │ │ └── FinalPage.vue ├── main.ts ├── shims-vue.d.ts ├── hooks │ ├── useComponentClick.ts │ └── useStylePick.ts ├── index.ts ├── App.vue └── defaultProps.ts ├── jest.config.js ├── .gitignore ├── .eslintrc.js ├── public └── index.html ├── tests └── unit │ ├── LImage.spec.ts │ └── LText.spec.ts ├── tsconfig.json ├── .travis.yml ├── README.md └── package.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imooc-lego/lego-components/HEAD/result.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/components/LImage/index.ts: -------------------------------------------------------------------------------- 1 | import LImage from './LImage.vue' 2 | LImage.install = (app: any) => { 3 | app.component(LImage.name, LImage) 4 | } 5 | 6 | export default LImage -------------------------------------------------------------------------------- /src/components/LText/index.ts: -------------------------------------------------------------------------------- 1 | import LText from './LText.vue' 2 | 3 | LText.install = (app: any) => { 4 | app.component(LText.name, LText) 5 | } 6 | 7 | export default LText -------------------------------------------------------------------------------- /src/components/LShape/index.ts: -------------------------------------------------------------------------------- 1 | import LShape from './LShape.vue' 2 | 3 | LShape.install = (app: any) => { 4 | app.component(LShape.name, LShape) 5 | } 6 | 7 | export default LShape -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import Lego from './index' 3 | import App from './App.vue' 4 | const app = createApp(App) 5 | app.use(Lego) 6 | app.mount('#app') 7 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { defineComponent } from 'vue' 3 | const component: ReturnType 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /src/components/FinalPage/index.ts: -------------------------------------------------------------------------------- 1 | import FinalPage from './FinalPage.vue' 2 | 3 | FinalPage.install = (app: any) => { 4 | app.component(FinalPage.name, FinalPage) 5 | } 6 | 7 | export default FinalPage -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript', 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest' 5 | }, 6 | transformIgnorePatterns: [ 7 | "/!node_modules\\/lodash-es/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/useComponentClick.ts: -------------------------------------------------------------------------------- 1 | const useComponentClick = (props: any) => { 2 | const handleClick = () => { 3 | if (props.actionType && props.url && !props.isEditing) { 4 | window.location.href = props.url 5 | } 6 | } 7 | return handleClick 8 | } 9 | 10 | export default useComponentClick 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/hooks/useStylePick.ts: -------------------------------------------------------------------------------- 1 | import { pick, without } from 'lodash-es' 2 | import { computed } from 'vue' 3 | import { textDefaultProps } from '../defaultProps' 4 | 5 | export const defaultStyles = without(Object.keys(textDefaultProps), 'actionType', 'url', 'text') 6 | const useStylePick = (props: any, pickStyles = defaultStyles) => { 7 | return computed(() => pick(props, pickStyles)) 8 | } 9 | 10 | export default useStylePick 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import LText from './components/LText' 2 | import LImage from './components/LImage' 3 | import LShape from './components/LShape' 4 | import FinalPage from './components/FinalPage' 5 | const components = [ 6 | LText, 7 | LImage, 8 | LShape, 9 | FinalPage 10 | ] 11 | 12 | const install = (app: any) => { 13 | components.map(component => { 14 | app.use(component) 15 | }) 16 | } 17 | 18 | export { 19 | install, 20 | LText, 21 | LImage, 22 | LShape, 23 | FinalPage 24 | } 25 | 26 | export default { 27 | install 28 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/FinalPage/FinalPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /tests/unit/LImage.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import LImage from '../../src/components/LImage' 3 | import { componentsDefaultProps } from '../../src/defaultProps' 4 | describe('LImage.vue', () => { 5 | it('default LImage render', () => { 6 | const srcPath = 'test.jpg' 7 | const props = { 8 | ...componentsDefaultProps['l-image'], 9 | imageSrc: srcPath 10 | } 11 | const wrapper = shallowMount(LImage, { props }) 12 | const style = wrapper.attributes().style 13 | const src = wrapper.attributes().src 14 | // should match the path 15 | expect(src).toMatch(srcPath) 16 | // should be Img tag 17 | expect(wrapper.element.tagName).toBe('IMG') 18 | // should not have prop has been filtered 19 | expect(style.includes('imageSrc')).toBeFalsy() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env", 16 | "jest" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | cache: 5 | directories: 6 | - node_modules 7 | deploy: 8 | provider: npm 9 | skip_cleanup: true 10 | email: vikingmute@gmail.com 11 | api_key: 12 | secure: dHs0HC9aOtDCwdfbiV5PD2T3hN0g10/looEs8fO7GnRlwH8wGsy9NaVrz8pxZeG9eqVYV/oMDjoo+Cmy1H+bpyPRjS4EbzDSBSmGXkj38j3nQ60HH2ViwX0XMROP4hJFqTdbKy5ALDwJIhq+vGPNYZSiUtc1fOTWp/a1dloprmK8GuxchEA2B4AFGureswtP1EO8cPYdI9UNDh7Z0L9O6ewLyx53mYaRMrNKV4qAi8UxPUBsWj4EFHQ8PCBKY2hAa2UnLEcXKZ5o43v7YymMtYEgfpO9I8eyxsYnDsk0Fur0+NDwW0UlCWJKnwPEFEI/TgL7daKjkG3MP9QOWy7fED8sDzGxjgElxCOtkH9m/g9uEn8H/hJ+ryYpDcKcepDsqARW0a5ZbjB4iPqTFaQpoNWZZvqeOi6IvQcXbtg229buQ6zQdKW9rmiVns1cAP3U2utIJ4Ac517LkFr2AvXO+bxkRvyPcJYwjHhQlsf8GV4WmXN7pCEFepeAUqGjJ3UmX66etkG4phl2+6RPf93pRAD1cpSfcROEVQvh8tLKDvKRL47F0u+l6mosZQuF7dIhXqzzj/YjtwTB874N29Xx74e4YXsHJeixdPgC9oxvBTOiEAtGY1RHnWgqNVBY7eZXFPzfOf6iXtl3kmqsL1rm7S4ZxG9bxd5purUQ3VpR1Wg= 13 | on: 14 | tags: true 15 | -------------------------------------------------------------------------------- /src/components/LShape/LShape.vue: -------------------------------------------------------------------------------- 1 | 5 | 28 | -------------------------------------------------------------------------------- /src/components/LImage/LImage.vue: -------------------------------------------------------------------------------- 1 | 8 | 29 | 30 | 35 | -------------------------------------------------------------------------------- /src/components/LText/LText.vue: -------------------------------------------------------------------------------- 1 | 6 | 36 | 37 | 50 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Imooc Lego 业务组件库 2 | 3 | ### 提供一个业务组件库在编辑器和 H5 页面中都可以使用 4 | 该组件包导出两种格式的模块,供不同情况下使用 5 | 6 | ```javascript 7 | // umd 格式 8 | "main": "dist/lego-components.umd.js", 9 | // es modules 格式 10 | "module": "dist/lego-components.esm.js", 11 | ``` 12 | 13 | ## 安装和使用 14 | 15 | ```bash 16 | // 安装 17 | npm install lego-components --save 18 | ``` 19 | 20 | ```javascript 21 | import Lego from 'lego-components' 22 | // 加载样式 23 | import 'lego-components/dist/lego-components.css' 24 | 25 | const app = createApp(App) 26 | // 全局引入 目前包括 FinalPage, LText, LImage , Lshape三个组件 27 | app.use(Lego) 28 | 29 | app.mount('#app') 30 | ``` 31 | 32 | ### 渲染最终页面 33 | ```javascript 34 | setup() { 35 | // 使用 finalPage 组件进行渲染,使用我们预定义好的数据结构 36 | const testData = { 37 | // 页面上面一个个组件的属性 38 | components: [ 39 | {id: '123', name: 'l-text', props: { text: 'hello', top: '0', left: '20px'}}, 40 | {id: '234', name: 'l-image', props: { imageSrc: 'http://vue-maker.oss-cn-hangzhou.aliyuncs.com/vue-marker/5f6338e666336111f73d220c.png', top: '30px', left: '20px'}}, 41 | {id: '235', name: 'l-shape', props: { backgroundColor: 'red', top: '50px', left: '20px', width: '100px', height: '100px'}}, 42 | // 这是一个链接 43 | {id: '345', name: 'l-text', props: { backgroundColor: "#1890ff", color: "#ffffff", text: "按钮内容", width: "100px", actionType: "to", url: "http://www.baidu.com", top: '200px', left: '150px', 44 | }} 45 | ] 46 | } 47 | return { 48 | testData 49 | } 50 | } 51 | ``` 52 | 53 | ```html 54 | 55 | ``` 56 | 57 | ## 最终页面效果 58 | 59 | ![最终页面效果](./result.png) 60 | 61 | 62 | -------------------------------------------------------------------------------- /tests/unit/LText.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import LText from '../../src/components/LText' 3 | import { componentsDefaultProps } from '../../src/defaultProps' 4 | describe('LText.vue', () => { 5 | const { location } = window 6 | beforeEach((): void => { 7 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 8 | // @ts-ignore 9 | delete window.location; 10 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 11 | // @ts-ignore 12 | window.location = { 13 | href: '', 14 | } 15 | }) 16 | afterEach((): void => { 17 | window.location = location; 18 | }) 19 | it('default LText render', () => { 20 | const msg = 'test' 21 | const props = { 22 | ...componentsDefaultProps['l-text'], 23 | text: msg 24 | } 25 | const wrapper = shallowMount(LText, { props }) 26 | const style = wrapper.attributes().style 27 | // should have the right text 28 | expect(wrapper.text()).toMatch(msg) 29 | // should be p tag 30 | expect(wrapper.element.tagName).toBe('P') 31 | // should have one css attr 32 | expect(style.includes('font-size')).toBeTruthy() 33 | // should not have prop has been filtered 34 | expect(style.includes('actionType')).toBeFalsy() 35 | }) 36 | it('LText with actionType and URL should trigger location href change', () => { 37 | const props = { 38 | ...componentsDefaultProps['l-text'], 39 | actionType: 'to', 40 | url: 'http://dummy.url', 41 | tag: 'h2' 42 | } 43 | const wrapper = shallowMount(LText, { props }) 44 | // should be h2 45 | expect(wrapper.element.tagName).toBe('H2') 46 | // trigger the element 47 | wrapper.trigger('click') 48 | expect(window.location.href).toBe(props.url) 49 | }) 50 | it('LText with isEditing should not trigger location change', () => { 51 | const props = { 52 | ...componentsDefaultProps['l-text'], 53 | actionType: 'to', 54 | url: 'http://dummy.url', 55 | tag: 'h2', 56 | isEditing: true 57 | } 58 | const wrapper = shallowMount(LText, { props }) 59 | // trigger the element 60 | wrapper.trigger('click') 61 | expect(window.location.href).not.toBe(props.url) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lego-components", 3 | "version": "0.1.7", 4 | "private": false, 5 | "main": "dist/lego-components.umd.js", 6 | "module": "dist/lego-components.esm.js", 7 | "types": "dist/index.d.ts", 8 | "description": "Lego Vue3 components library", 9 | "author": "Viking Zhang", 10 | "license": "MIT", 11 | "keywords": [ 12 | "Component", 13 | "UI", 14 | "Vue" 15 | ], 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "serve": "vue-cli-service serve", 21 | "build": "npm run build:es && npm run build:umd", 22 | "lint": "vue-cli-service lint", 23 | "build:es": "rollup --config build/rollup.esm.config.js", 24 | "build:umd": "rollup --config build/rollup.umd.config.js", 25 | "test": "vue-cli-service test:unit", 26 | "test:watch": "vue-cli-service test:unit --watch", 27 | "prepublishOnly": "npm run test && npm run build" 28 | }, 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "npm run test" 32 | } 33 | }, 34 | "dependencies": { 35 | "lodash-es": "^4.17.15" 36 | }, 37 | "peerDependencies": { 38 | "vue": "^3.0.0-0" 39 | }, 40 | "devDependencies": { 41 | "@rollup/plugin-commonjs": "^15.1.0", 42 | "@rollup/plugin-node-resolve": "^9.0.0", 43 | "@rollup/plugin-typescript": "^6.1.0", 44 | "@types/jest": "^26.0.15", 45 | "@types/lodash-es": "^4.17.3", 46 | "@typescript-eslint/eslint-plugin": "^4.22.0", 47 | "@typescript-eslint/parser": "^4.22.0", 48 | "@vue/cli-plugin-babel": "~4.5.0", 49 | "@vue/cli-plugin-eslint": "~4.5.0", 50 | "@vue/cli-plugin-typescript": "^4.5.0", 51 | "@vue/cli-plugin-unit-jest": "^4.5.8", 52 | "@vue/cli-service": "~4.5.0", 53 | "@vue/compiler-sfc": "^3.0.0-0", 54 | "@vue/eslint-config-typescript": "^7.0.0", 55 | "@vue/test-utils": "^2.0.0-beta.8", 56 | "babel-eslint": "^10.1.0", 57 | "cross-env": "^7.0.2", 58 | "eslint": "^6.7.2", 59 | "eslint-plugin-vue": "^7.0.0-0", 60 | "husky": "^4.3.0", 61 | "rollup": "^2.28.2", 62 | "rollup-plugin-css-only": "^2.1.0", 63 | "rollup-plugin-exclude-dependencies-from-bundle": "^1.1.13", 64 | "rollup-plugin-typescript2": "^0.29.0", 65 | "rollup-plugin-vue": "^6.0.0-beta.10", 66 | "typescript": "^4.0.5", 67 | "vue": "^3.0.0-0", 68 | "vue-jest": "^5.0.0-alpha.5" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/defaultProps.ts: -------------------------------------------------------------------------------- 1 | import { mapValues } from 'lodash-es' 2 | 3 | export interface ComponentData { 4 | props: { [key: string]: any }; 5 | id: string; 6 | name: string; 7 | layerName?: string; 8 | isHidden?: boolean; 9 | isLocked?: boolean; 10 | } 11 | 12 | export interface PageData { 13 | props: { [key: string]: any }; 14 | setting: { [key: string]: any }; 15 | id?: number; 16 | title?: string; 17 | desc?: string; 18 | coverImg?: string; 19 | uuid?: string; 20 | latestPublishAt?: string; 21 | updatedAt?: string; 22 | isTemplate?: boolean; 23 | isHot?: boolean; 24 | isNew?: boolean; 25 | author?: string; 26 | status?: string; 27 | } 28 | 29 | // the common default props, all the components should have these props 30 | export const commonDefaultProps = { 31 | // actions 32 | actionType: '', 33 | url: '', 34 | // size 35 | height: '', 36 | width: '318px', 37 | paddingLeft: '0px', 38 | paddingRight: '0px', 39 | paddingTop: '0px', 40 | paddingBottom: '0px', 41 | // border type 42 | borderStyle: 'none', 43 | borderColor: '#000', 44 | borderWidth: '0', 45 | borderRadius: '0', 46 | // shadow and opacity 47 | boxShadow: '0 0 0 #000000', 48 | opacity: 1, 49 | // position and x,y 50 | position: 'absolute', 51 | left: '0', 52 | top: '0', 53 | right: '0' 54 | } 55 | export const textDefaultProps = { 56 | // basic props - font styles 57 | text: '正文内容', 58 | fontSize: '14px', 59 | fontFamily: '', 60 | fontWeight: 'normal', 61 | fontStyle: 'normal', 62 | textDecoration: 'none', 63 | lineHeight: '1', 64 | textAlign: 'left', 65 | color: '#000000', 66 | backgroundColor: '', 67 | ...commonDefaultProps 68 | } 69 | 70 | export const imageDefaultProps = { 71 | imageSrc: '', 72 | ...commonDefaultProps 73 | } 74 | 75 | export const shapeDefaultProps = { 76 | backgroundColor: '', 77 | ...commonDefaultProps 78 | } 79 | // this contains all default props for all the components 80 | // useful for inserting new component into the store 81 | export const componentsDefaultProps = { 82 | 'l-text': { 83 | props: textDefaultProps 84 | }, 85 | 'l-image': { 86 | props: imageDefaultProps 87 | }, 88 | 'l-shape': { 89 | props: shapeDefaultProps 90 | } 91 | } 92 | 93 | export const isEditingProp = { 94 | isEditing: { 95 | type: Boolean, 96 | default: false 97 | } 98 | } 99 | 100 | export const transformToComponentProps = (props: { [key: string]: any }, extraProps?: { [key: string]: any }) => { 101 | const mapProps = mapValues(props, (item) => { 102 | return { 103 | type: item.constructor, 104 | default: item 105 | } 106 | }) 107 | if (extraProps) { 108 | return { ...mapProps, ...extraProps } 109 | } else { 110 | return mapProps 111 | } 112 | } 113 | export default componentsDefaultProps 114 | --------------------------------------------------------------------------------