1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        可視化搭建移動(dòng)端店鋪解決方案

        共 56094字,需瀏覽 113分鐘

         ·

        2021-07-07 10:27

        原文地址:https://juejin.cn/post/6979410699453726727    文章已經(jīng)過作者許可轉(zhuǎn)載

        關(guān)注并將「趣談前端」設(shè)為星標(biāo)

        每早08:30按時(shí)推送技術(shù)干貨/優(yōu)秀開源/技術(shù)思維

        前言

        經(jīng)過許久的深思熟慮與探索,同時(shí)也借鑒了行業(yè)內(nèi)不錯(cuò)的產(chǎn)品(如:有贊,H5-Dooring等),但跟列舉的產(chǎn)品還是有區(qū)別的(先賣個(gè)關(guān)子,后面再講有哪些區(qū)別)。其實(shí)這種功能在零售系統(tǒng)(目前我所在公司是零售行業(yè)的領(lǐng)頭羊)和電商系統(tǒng)應(yīng)該很常見,很多應(yīng)用場景都會(huì)用到,像產(chǎn)品營銷頁面、企業(yè)/個(gè)人微官網(wǎng)、H5活動(dòng)頁面等移動(dòng)端頁面,通過可視化配置快速搭建H5頁面,且提供豐富的頁面組件,更方便的為使用者搭建更強(qiáng)大的H5頁面。

        PC端界面如下:

        PC端界面

        移動(dòng)端(H5和小程序)界面如下:

        技術(shù)方案

        PC端 React 技術(shù)棧,移動(dòng)端 UniApp 跨平臺(tái)框架,功能的設(shè)計(jì)結(jié)構(gòu)圖如下:

        裝修頁面前端設(shè)計(jì)模式.png
        /*
         * @description: DecoratePage Context交互
         * @version: 分支號 20210629
         * @author: xuchao
         */
        import React, { PureComponent } from 'react';
        import { withRouter, router } from 'umi';
        import { Layout, Modal, Button } from 'antd';
        import { isEmpty, findIndex, isArray, find, every, cloneDeep } from 'lodash';
        import { DndProvider } from 'react-dnd';
        import { HTML5Backend } from 'react-dnd-html5-backend';
        import { showMsg } from '@/global';
        import Component from './components/Component';
        import Preview from './components/Preview';
        import Compiler from './components/Compiler';
        import { DecorateContext, components } from './utilities';
        import './style.less';

        const { Header } = Layout;

        export default class Decorate extends PureComponent {
            state = {
                compiler: 'PageSetting',
                pagename: '頁面標(biāo)題',
                selectIndex: 0,
                previewData: [],
            };

            getChildContext() {
                return {
                    ...this.state,
                    ...this.props,
                    setState: state => this.setState(state),
                };
            }

            render() {
                return (
                    <DecorateContext.Provider value={this.getChildContext()}>
                        <Layout className="decorate">
                            <Header className="header">
                                <span className="hand">
                                    返回首頁裝修
                                </span>
                                <Button type="primary" className="fr">
                                    發(fā)布
                                </Button>
                                <Button type="primary" className="fr mr10">
                                    保存
                                </Button>
                                <Button className="fr mr10">
                                    預(yù)覽
                                </Button>
                            </Header>
                            <DndProvider backend={HTML5Backend}>
                                <Layout className="container">
                                    <Component />
                                    <Preview />
                                    <Compiler />
                                </Layout>
                            </DndProvider>
                        </Layout>
                    </DecorateContext.Provider>
                );
            }

        數(shù)據(jù)

        前面說到與列舉的產(chǎn)品有哪些區(qū)別,區(qū)別在于PC端與移動(dòng)端的數(shù)據(jù)交互,它們都是通過 iframe 嵌套 H5 的頁面,通過 postmessage API 來做數(shù)據(jù)交互,而是我沒有這樣做,原因是項(xiàng)目特別緊,加上人員分配問題,所以采用數(shù)據(jù)定義模式。

        通過上面的設(shè)計(jì)結(jié)構(gòu)圖可以看出PC端最后會(huì)生成一份 schema 數(shù)據(jù)存儲(chǔ)服務(wù)端,移動(dòng)端從服務(wù)端獲取到 schema 數(shù)據(jù)進(jìn)行解析。數(shù)據(jù)格式如下:

        // 圖片廣告
        {
            component: 'ImageTextAd',
            options: {
                template: 'image', // image:一行一個(gè) carousel:輪播海報(bào) slide:大圖橫向滑動(dòng) zone:繪制熱區(qū)
                image: [
                    {
                        id: '',
                        url: '',
                        title: '',
                        linkCode: '',
                        linkName: '',
                        // 熱區(qū)
                        zones: [
                            {
                                x: 178,
                                y: 91,
                                width: 158,
                                height: 132,
                                code: '123',
                                text: '測試鏈接2',
                            }
                        ],
                    },
                    {
                        id: '',
                        url: '',
                        title: '',
                        linkCode: '',
                        linkName: '',
                        // 熱區(qū)
                        zones: [
                            {
                                x: 436,
                                y: 97,
                                width: 170,
                                height: 168,
                                code: '',
                                text: '',
                            }
                        ],
                    },
                ],
                indicator: 'dotted', // 指示器
                style: {
                    boxShadow: 'none',
                    borderRadius: 'none',
                    padding: '0',
                },
            },
        },
        // 公告
        {
            component: 'Notice',
            options: {
                content: '公告內(nèi)容',
                style: {
                    background: 'rgb(255, 248, 233)',
                    color: 'rgb(100, 101, 102)',
                },
            },
        },
        // 圖文導(dǎo)航
        {
            component: 'ImageTextNav',
            options: {
                template: 'image-nav', // image-nav:圖片導(dǎo)航 text-nav:文字導(dǎo)航
                images: [{
                    url: '',
                    title: '',
                    link: '',
                }],
                style: {
                    background: 'rgb(255, 248, 233)',
                    color: 'rgb(100, 101, 102)',
                },
            },
        },
        // 標(biāo)題欄
        {
            component: 'Title',
            options: {
                style: {
                    textAlign: 'left',
                    background: '#FFFFFF',
                },
                title: {
                    text: '',
                    style: {
                        fontSize: '16px',
                        fontWeight: 'bold',
                        color: '#323233',
                    },
                },
                content: {
                    text: '',
                    style: {
                        fontSize: '12px',
                        fontWeight: '400',
                        color: '#969799',
                    },
                },
            },
        },
        // 文本模塊
        {
            component: 'RichText',
            options: {
                content: '<html></html>',
                style: {
                    backgroundColor: '#F9F9F9',
                    padding: '10px 10px 0',
                },
            },
        },
        // 輔助分割
        {
            component: 'DivideLine',
            options: {
                template: 'block', // block:輔助空白 line:輔助線
                style: {
                    height: 30,
                    // borderTopWidth: '1px',
                    // borderTopStyle: 'dashed',
                    // borderTopColor: '#EBEDF0',
                    // margin: '10px 0 0',
                },
            },
        },
        // 商品搜索
        {
            component: 'GoodSearch',
            options: {
                style: {
                    backgroundColor: '#FFFFFF',
                },
                box: {
                    style: {
                        borderRadius: 'none',
                        textAlign: 'left',
                        height: 28,
                        backgroundColor: '#F7F8FA',
                        color: '#c8c9cc',
                    },
                },
            },
        },
        // 左右圖文
        {
            component: 'LRImageText',
            options: {
                template: 'lr', // lr:左圖右文 rl:左文右圖
                content: '', // 內(nèi)容
                image: {
                    url: '', // 圖片地址
                    linkCode: '', // 跳轉(zhuǎn)頁面code
                    linkName: '', // 跳轉(zhuǎn)頁面name
                    style: {
                        boxShadow: 'none',
                        borderRadius: 'none',
                    },
                },
            },
        },
        // 圖文導(dǎo)航
        {
            component: 'ImageTextNav',
            options: {
                template: 'image', // image:圖片導(dǎo)航 text:文字導(dǎo)航
                image: [
                    {
                        url: '',
                        title: '導(dǎo)航一',
                        linkCode: '',
                        linkName: '',
                    },
                    {
                        url: '',
                        title: '導(dǎo)航二',
                        linkCode: '',
                        linkName: '',
                    },
                    {
                        url: '',
                        title: '導(dǎo)航三',
                        linkCode: '',
                        linkName: '',
                    },
                    {
                        url: '',
                        title: '導(dǎo)航四',
                        linkCode: '',
                        linkName: '',
                    },
                    {
                        id: uuid(),
                        url: '',
                        title: '導(dǎo)航五',
                        linkCode: '',
                        linkName: '',
                    },
                ],
                style: {
                    backgroundColor: '#FFFFFF',
                    color: '#333333',
                },
            },
        },
        // 魔方
        {
            component: 'Cube',
            options: {
                template: 'row-one', // row-one:一行一個(gè) row-two:一行兩個(gè) row-four:一行四個(gè) row-col:一大兩小
                image: [
                    {
                        url: '',
                        linkType: '',
                        linkName: '',
                    },
                ],
                imageMargin: 0,
                layoutMargin: 0,
            },
        },
        // 定位菜單
        {
            component: 'PositionMenu',
            data: [], // 分組信息
            options: {
                template: 'tab-style-one', // tab-style-one:樣式1 tab-style-two:樣式2 tab-style-three:樣式3
                data: [
                    {
                        id: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
                        code: '',
                        name: '',
                        menuName: '',
                        comsize: 6,
                    },
                    {
                        id: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6c',
                        code: '',
                        name: '',
                        menuName: '',
                        comsize: 6,
                    },
                ],
                style: {
                    borderRadius: 'none',
                    fontWeight: '400',
                    paddingLeft: '5px',
                    paddingRight: '5px',
                },
                listStyle: 'row-one', // row-one:大圖模式 row-two:一行兩個(gè) row-three:一行三個(gè) row-col:詳細(xì)列表
                commodityStyle: 'no-border', // no-border:無邊白底 shadow:卡片投影 stroke:描邊白底 transparent:無邊透明底
                commodityName: true, // 商品名稱
                commodityDesc: true, // 商品描述
                commodityPrice: true, // 商品價(jià)格
                originalPrice: true, // 劃線價(jià)格
                buyButton: true, // 購買按鈕
                buyButtonStyle: 'style-1', // 購買按鈕樣式
                buyButtonText: '馬上搶', // 購買按鈕文字
                commoditySubscript: true, // 商品角標(biāo)
                commoditySubscriptStyle: 'new', // 商品角標(biāo)樣式
            },
        },
        // 普通商品
        {
            component: 'Goods',
            data: [], // 商品信息
            options: {
                template: 'large', // large:大圖模式 small:一行兩個(gè) three:一行三個(gè) list:詳細(xì)列表
                data: [], // 商品信息
                style: {
                    borderRadius: 'none',
                    fontWeight: '400',
                    paddingLeft: '5px',
                    paddingRight: '5px',
                },
                listStyle: 'row-one', // row-one:大圖模式 row-two:一行兩個(gè) row-three:一行三個(gè) row-col:詳細(xì)列表
                commodityStyle: 'no-border', // no-border:無邊白底 shadow:卡片投影 stroke:描邊白底 transparent:無邊透明底
                commodityName: true, // 商品名稱
                commodityDesc: true, // 商品描述
                commodityPrice: true, // 商品價(jià)格
                originalPrice: true, // 劃線價(jià)格
                buyButton: true, // 購買按鈕
                buyButtonStyle: 'style-1', // 購買按鈕樣式
                buyButtonText: '馬上搶', // 購買按鈕文字
                commoditySubscript: true, // 商品角標(biāo)
                commoditySubscriptStyle: 'new', // 商品角標(biāo)樣式
            },
        },
        // 限時(shí)折扣
        {
            template: 'row-one',
            data: [],
            style: {
                borderRadius: 'none',
                fontWeight: '400',
                padding: '0',
                margin: '0',
            },
            comsize: 10,
            tag: '限時(shí)折扣',
            commodityStyle: 'no-border',
            commodityName: true,
            commodityDesc: false,
            commodityPrice: true,
            originalPrice: true,
            lastStock: true,
            countdown: true,
            progressBar: true,
            buyButton: true,
            buyButtonStyle: 'style-1',
            buyButtonText: '即將開搶',

        拖拽

        拖拽依賴第三方庫react-dnd,提供的Hooks Api特別方便,上面的設(shè)計(jì)結(jié)構(gòu)圖 Component組件(DragSource) 和 Preview組件(DropTarget) 用到了拖拽,Preview組件不僅要支持上下拖拽,而且需要配合Compiler組件聯(lián)動(dòng)。

        /*
         * @description: DragSource 拖動(dòng)組件
         * @version: 分支號 20210629
         * @author: xuchao
         */
        import React, { useContext } from 'react';
        import { useDrag } from 'react-dnd';
        import { findIndex, some, isUndefined, filter } from 'lodash';
        import { v1 as uuid } from 'uuid';
        import { DecorateContext } from '../../utilities';
        import schema from '../Materials/schema';

        export default ({ component, name, icon, max, componentType, fixedIndex }) => {
            const { previewData = [], setState } = useContext(DecorateContext);
            const number = filter(previewData, { component }).length;

            const [, drag] = useDrag(
                () => ({
                    type'component',
                    options: {
                        dropEffect: 'copy',
                    },
                    item: {
                        type'add',
                        component,
                        name,
                        max,
                        componentType,
                        fixedIndex,
                    },
                    end: (item, monitor) => {
                        const hasPh = some(previewData, { component: 'placeholder' });
                        const phIndex = findIndex(previewData, { component: 'placeholder' });

                        if (!hasPh) return;

                        // 組件放置已達(dá)上限
                        if (number === max) {
                            previewData.splice(phIndex, 1);

                            setState({ previewData: [...previewData] });

                            return;
                        }

                        if (monitor.didDrop()) {
                            // 判斷拖拽放入Preview組件中,占位元素替換成組件元素
                            previewData.splice(phIndex, 1, {
                                id: uuid(),
                                component: item.component,
                                options: schema[component].defaultOptions,
                            });
                        } else {
                            // 判斷拖拽沒有放入Preview組件中,刪除占位元素
                            previewData.splice(phIndex, 1);
                        }

                        setState({
                            previewData: [...previewData],
                            selectIndex: phIndex,
                            compiler: item.component,
                        });
                    },
                }),
                [previewData],
            );

            /**
             * @description: 新增組件
             * @author: xuchao
             */
            const handleClick = () => {
                if (number === max) return;

                previewData.splice(!isUndefined(fixedIndex) ? fixedIndex : previewData.length, 0, {
                    id: uuid(),
                    component,
                    options: schema[component].defaultOptions,
                });

                setState({
                    previewData: [...previewData],
                    selectIndex: !isUndefined(fixedIndex) ? fixedIndex : previewData.length - 1,
                    compiler: component,
                });
            };

            return (
                <div ref={drag} className="item" onClick={handleClick}>
                    <i className={icon}></i>
                    <div className="name">{name}</div>
                    <div className="number">
                        {number}/{max}
                    </div>
                </div>
            );
        }; 

        /*
         * @description: DropTarget 放置組件
         * @version: 分支號 20210629
         * @author: xuchao
         */
        import React, { useContext, useCallback } from 'react';
        import { useDrop } from 'react-dnd';
        import { findIndex, some, isUndefined, filter } from 'lodash';
        import update from 'immutability-helper';
        import { DecorateContext } from '../../utilities';
        import Item from './Item';

        export default () => {
            const { previewData = [], selectIndex, setState } = useContext(DecorateContext);

            const [, drop] = useDrop(
                () => ({
                    accept: 'component',
                    hover: item => {
                        const limit = filter(previewData, { component: item.component }).length;
                        const hasPh = some(previewData, { component: 'placeholder' });
                        const spliceIndex = !isUndefined(item.fixedIndex)
                            ? item.fixedIndex
                            : previewData.length;

                        if (item.type === 'add' && !hasPh) {
                            // 判斷占位符是否已經(jīng)存在,若懸??瞻滋?,插入占位符
                            previewData.splice(spliceIndex, 0, {
                                component: 'placeholder',
                                limit: item.max === limit ? true : false,
                            });

                            setState({ previewData: [...previewData] });
                        }
                    },
                }),
                [previewData],
            );

            /**
             * @description: move callback
             * @param {number} dragIndex
             * @param {number} hoverIndex
             * @param {object} item
             * @author: xuchao
             */
            const handleMove = useCallback(
                (dragIndex, hoverIndex, item) => {
                    if (item.type === 'add' && !dragIndex) {
                        // 判斷拖拽是 Component 的組件,則 dragIndex 為 undefined,修改占位符的位置即可
                        const limit = filter(previewData, { component: item.component }).length;
                        const hasPh = some(previewData, { component: 'placeholder' });
                        const spliceIndex = !isUndefined(item.fixedIndex) ? item.fixedIndex : hoverIndex;

                        // 判斷占位符是否已經(jīng)存在,不再重復(fù)插入
                        if (hasPh) {
                            const phIndex = findIndex(previewData, {
                                component: 'placeholder',
                            });

                            setState({
                                previewData: update(previewData, {
                                    $splice: [
                                        [phIndex, 1],
                                        [
                                            spliceIndex,
                                            0,
                                            {
                                                component: 'placeholder',
                                                limit: item.max === limit ? true : false,
                                            },
                                        ],
                                    ],
                                }),
                            });

                            return;
                        }

                        setState({
                            previewData: update(previewData, {
                                $splice: [
                                    [
                                        spliceIndex,
                                        0,
                                        {
                                            component: 'placeholder',
                                            limit: item.max === limit ? true : false,
                                        },
                                    ],
                                ],
                            }),
                        });
                    } else {
                        // 判斷拖拽是 Preview 的組件,則 dragIndex 不為 undefined,替換 dragIndex 和 hoverIndex 位置的元素即可
                        setState({
                            previewData: update(previewData, {
                                $splice: [
                                    [dragIndex, 1],
                                    [hoverIndex, 0, previewData[dragIndex]],
                                ],
                            }),
                            selectIndex: dragIndex === selectIndex ? hoverIndex : dragIndex,
                        });
                    }
                },
                // eslint-disable-next-line react-hooks/exhaustive-deps
                [previewData],
            );

            /**
             * description: delete callback
             * param {object} event
             * param {number} index
             * author: xuchao
             */
            const handleDelete = (event, index) => {
                event.stopPropagation();

                previewData.splice(index, 1);

                setState({
                    previewData: [...previewData],
                    compiler: selectIndex === previewData.length ? undefined : previewData[index].compiler,
                });
            };

            return (
                <div ref={drop} className="content">
                    {previewData.map((item, index) => {
                        return (
                            <Item
                                key={item.id}
                                index={index}
                                selectIndex={selectIndex}
                                {...item}
                                onClick={() => setState({ selectIndex: index, compiler: item.component })}
                                onMove={handleMove}
                                onDelete={handleDelete}
                            />
                        );
                    })}
                </div>
            );
        }; 

        總結(jié)

        開發(fā)耗費(fèi)時(shí)間比較長的地方是怎么設(shè)計(jì)與移動(dòng)端同步數(shù)據(jù)和拖拽功能,最后還是迎刃而解。如果大家有什么疑問可以交流一下??

        ?? 看完三件事

        如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個(gè)小忙:

        • 點(diǎn)個(gè)【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容

        • 關(guān)注公眾號【趣談前端】,定期分享 工程化 可視化 / 低代碼 / 優(yōu)秀開源。



        Dooring可視化搭建平臺(tái)數(shù)據(jù)源設(shè)計(jì)剖析

        可視化搭建的一些思考和實(shí)踐

        基于Koa + React + TS從零開發(fā)全棧文檔編輯器(進(jìn)階實(shí)戰(zhàn))

        從零使用electron搭建桌面端Dooring


        點(diǎn)個(gè)在看你最好看

        瀏覽 66
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            中文字幕一区二区二三区四区 | 亚洲图片天堂 | 亚洲另类激情小说 | 天天舔夜夜爽 | 日韩特级毛片 | 特级婬片AAAAAAA级 | 香蕉人妻AV久久久久天天 | 9I国产精品视频 | 美女艹逼视频网站 | www.撸 |