落地页生成工具

2,219 阅读2分钟

背景

因为公司有很多落地页,以及埋点数据上报等等需求(数据上报直接插入到对应组件里面啦),所以需要做一个工具使简化同事的工作,也减少了来找开发同学的频率,各自欢喜~

分析需求

因为是投放的落地页,所以需要考虑一定的并发量以及网站的稳定性,为了防止意外发生直接生成静态页面比较好。 最终是要生成一个完整的 html 页面,因为是vue写的,所以最简单的方式就是通过 json 去渲染组件啦

我这里使用的技术栈是 vite + ts + vue-router

技术实现

  1. 左侧基础组件,点击添加基础组件 基础组件通过对象的方式去存储,每点击一次都 push 当前组件 type 到一个数组里面(为了落地页的展示,落地页根据这个数组渲染组件), 每点击一次基础组件,都生成一个唯一的随机 ID(为了存储每一个组件的配置信息),这个随机 ID 对映到中间渲染页面组件,这样编辑组件的时候就可以对应到每一个组件。
配置信息
{ 
  currentComponent: 'button', 
  listComponents: ['image-9592', 'button-317', 'video-2387']
}
  1. 中间渲染页面,点击组件弹出配置信息 中间页面各个组件点击的时候,实时记录当前点击的组件ID,弹出右侧相应的配置信息
{ 
  currentSetting: 'setting' 
}
  1. 右侧编辑组件,编辑组件配置 配置各个组件的基本信息,
vuex配置信息
state: {
  value: '',
  setting: { },
  button: {
    'button-317': {}
    ...
  },
  ...
},
  1. 生成页面,保存所有配置信息生成页面 将页面的配置信息通过请求发给后端同学,后端生成页面,把配置信息添加到 html 页面就大功告成了啦~

最终 html 页面代码,放到服务器上就可以访问啦

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试</title>
    <script>
        let data = {"componentsSetting":{"currentComponent":"button","listComponents":["image-9592","button-317","video-2387"],"saveHtml":"","previewVal":""},"currentCompenentsSetting":{"currentSetting":"setting"},"editSetting":{"value":"setting","setting":{"landPageName":"测试","title":"灵芝孢子粉"},"text":{},"button":{"button-317":{"button":"点击进入京东店铺","color":"#F2B00A","bgcolor":"#B80314","size":"20","align":"center","layoutStyle":"","sizeStyle":"margin-top:0px;margin-right:0px;margin-bottom:0px;margin-left:0px;width:100%;height:39px;line-height:39px;border-radius:27px;color:#F2B00A;font-size:20px;background:#B80314;float:none","h5Url":"https://shop.m.jd.com/?shopId=119921&utm_user=plusmember&gx=RnEwyjVYbjKMy9RG_sYoAXcAFA&ad_od=share&utm_source=androidapp&utm_medium=appshare&utm_campaign=t_335139774&utm_term=Wxfriends","schemesUrl":"","transform":[{"translationId":100004,"translationName":"点击按钮","translationTarget":"buy"}],"transformValue":"点击按钮","dividerSize":[{"title":"宽度","label":"width","value":100,"unit":"%"},{"title":"高度","label":"height","value":39,"min":32},{"title":"圆角","label":"border-radius","value":27,"min":0,"max":50}]}}},"userSetting":{"userInfo":{},"advPageInfo":{}},"post":{"id":"23","advAccountId":"670"}}
      window.localStorage.setItem('lp',JSON.stringify(data))
    </script>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

主要代码贴一下,vuex的就不贴了,相信大家都会用~

basic button code


<template>
  <div style="display:inline-block;text-align:center" :id="value" :style="sizeStyle" @click="handleTranslate">{{ post.button || '这是按钮'}}</div>
</template>

<script lang="ts">
export default defineComponent({
  props: {
    value: {
        type: String,
        default: () => '',
    },
  },
  setup(props) {
    const store = useStore()
    const route = useRoute()
    const state = reactive({
      style: '',
      button: '',
      post: {},
      sizeStyle:'',
      value: props.value
    })
    const getData = (d: any) => {
      let { value } = props
      let res = d && d[value]
      if(!res) return
      state.post = res
      state.sizeStyle = res.sizeStyle
    }
    const handleTranslate = () => {
      let{ h5Url, schemesUrl } = state.post
      nextTick(() => {
        if(Object.keys(state.post).includes('transform') && (state.post as any).transform){
        let { translationId, translationTarget } = (state.post as any).transform[0]
          transformEvent(translationTarget, translationId)
        }
       if(h5Url || schemesUrl){
          openApp(state.value, h5Url,schemesUrl)
        }
      })
    }

    watch(store.state.editSetting, (val) => {
      let { button } = val
      getData(button)
    });

    onMounted(() => {
      let { button } = store.state.editSetting
      getData(button)
    })

    return {
      ...toRefs(state),
      handleTranslate
    }
  },
})
</script>

setting button code

<template>
  <div class="text">
    <h3>基础设置</h3>
    <el-row :gutter="20">
      <el-col :span="5">点击跳转</el-col>
      <el-col :span="19">
        <el-radio v-model="radio" label="1">外链</el-radio>
      </el-col>
      <el-col :span="5"><div class="i-right-label">网页链接</div></el-col>
      <el-col :span="19">
        <el-input
          placeholder="http://"
          v-model="post.h5Url"
          @change="postDate"
        />
      </el-col>
      <el-col :span="5"><div class="i-right-label">直达链接</div></el-col>
      <el-col :span="19">
        <el-input
          placeholder="mttbrowser://url=https://www.qq.com"
          v-model="post.schemesUrl"
          @change="postDate"
        />
      </el-col>
    </el-row>
    <el-divider></el-divider>
    <h3>样式</h3>
    <el-row :gutter="20">
      <el-col :span="5">按钮文案</el-col>
      <el-col :span="19">
        <el-input
          placeholder="这是按钮"
          v-model="post.button"
          @change="postDate"
        />
      </el-col>
      <el-col :span="5"><div class="i-right-label">按钮颜色</div></el-col>
      <el-col :span="19">
        <div class="i-right-label">文字</div>
        <el-color-picker v-model="post.color" size="small" @change="postDate"> </el-color-picker>
        <div class="i-right-label" style="margin-left: 30px">背景</div>
        <el-color-picker v-model="post.bgcolor" size="small" @change="postDate" show-alpha> </el-color-picker>
      </el-col>
      <el-col :span="5"><div class="i-right-label">字号</div></el-col>
      <el-col :span="19">
        <el-select v-model="post.size" placeholder="请选择" style="width:70px" @change="postDate">
          <el-option
            v-for="item in options"
            :key="item"
            :label="item"
            :value="item">
          </el-option>
        </el-select>
      </el-col>
    </el-row>
    <Divider @handleDivider="getDividerSize" :data="dividerSize"/>
    <el-divider></el-divider>
    <h3>布局</h3>
    <el-row :gutter="20">
      <el-col :span="5"><div class="i-right-label">对齐方式</div></el-col>
      <el-col :span="19">
        <el-radio-group v-model="post.align" class="text-align" @change="postDate">
          <el-radio-button v-for="item in options2" :key="item" :label="item" value="item"></el-radio-button>
        </el-radio-group>
      </el-col>
    </el-row>
    <Divider @handleDivider="getDivider" :data="dividerData"/>
    <Transform @handleTranslate="getTranslate" :data="transformData"/>
  </div>
</template>

<script lang="ts">
export default defineComponent({
  components: { Transform, Divider },
    name: 'buttonSetting',
    setup() {
    const { proxy } = getCurrentInstance() as any;
    const store = useStore();
    const route = useRoute()
    const clearData = (params) => {
      return {
        dividerSize: [
          {
            title: '宽度',
            label: 'width',
            value: 80,
            unit: '%',
          },
          {
            title: '高度',
            label: 'height',
            value: 32,
            min: 32,
          },
          {
            title: '圆角',
            label: 'border-radius',
            value: 3,
            min: 0,
            max: 50,
          },
        ],
        dividerData: [
          {
            title: '上边距',
            label: 'margin-top',
            value: 0
          },
          {
            title: '右边距',
            label: 'margin-right',
            value: 0
          },
          {
            title: '下边距',
            label: 'margin-bottom',
            value: 0
          },
          {
            title: '左边距',
            label: 'margin-left',
            value: 0
          },
        ],
        post: {
          button: '这是按钮',
          color: '#fff',
          bgcolor: 'rgba(64, 158, 255, 1)',
          size: '16',
          align: 'center',
          layoutStyle: '',
          sizeStyle: '',
          h5Url: '',
          schemesUrl: '',
        },
        transform: []
      }[params]
    }
    
    const state = reactive({
      radio: '1',
      options: ['12', '14', '16', '18', '20', '24'],
      options1: ['normal', 'bold'],
      options2: ['left', 'center', 'right'],
      post: clearData('post'),
      dividerSize: clearData('dividerSize'),
      dividerData: clearData('dividerData'),
      transformData: clearData('transform'),
    })

    const value = computed(() => store.state.editSetting.value);
    const getData = (d: any) => {
      state.dividerData = clearData('dividerData')
      state.dividerSize = clearData('dividerSize')
      state.transformData = clearData('transform')
      state.post = clearData('post')
      if(!d || !value.value || !d[value.value]) return
      let data = d[value.value]
      state.dividerData = data.dividerData
      state.dividerSize = data.dividerSize
      state.transformData = data.transformValue
      state.post = data
    }

    watch(value, (val) => {
      if(val.indexOf('button') < 0) return
      let { button } = store.state.editSetting
      getData(button)
      postDate()
		});

    const postDate = () => {
      let { post } = state
      let { color,bgcolor, size, align } = post
      state.dividerData = post.dividerData ||  clearData('dividerData')
      state.dividerSize = post.dividerSize ||  clearData('dividerSize')
      let layoutStyle = styleTransform(state.dividerData)
      let sizeStyle = styleTransform(state.dividerSize)
      let style = `${layoutStyle}${sizeStyle}color:${color};font-size:${size}px;background:${bgcolor};float:${align=='center'?'none':align}`
      post.sizeStyle = style
      const params = {[value.value]: post}
      store.dispatch('editSetting/setButton', params);
    }

    const getTranslate = (val: object) => {
      Object.assign(state.post,val),postDate()
    }
    const getDivider = (val: object) => {
      let { data } = val
      Object.assign(state.post,{dividerData: data})
      postDate()
    }
    const getDividerSize = (val: object) => {
      let { data } = val
      Object.assign(state.post,{dividerSize: data})
      postDate()
    }

    const styleTransform = (data) => {
      let str = ''
      data.forEach(element => {
        str += `${element.label}:${element.value}${element.unit || 'px'};`
        if(element.label == 'height') str += `line-height:${element.value}${element.unit || 'px'};`
      })
      return str
    }

    onMounted(() => {
      let { button } = store.state.editSetting
      getData(button)
      postDate()
    })
    return {
      ...toRefs(state),
      postDate,
      getTranslate,
      getDivider,
      getDividerSize
    }
	},
});
</script>

贴个完成图

image.png

有思路了,实现起来也就容易了,完成的时候,后端同学还以为我用了三方插件,哈哈哈,看着挺复杂的,其实仔细分析,就很容易完成了,写这个主要是记录一下,如果各位小伙伴有问题,欢迎来讨论啊~