首页 > 技术文章 > 完整表单全栈代码,有自定义上传功能,项扩展功能,数据回显,异步表单重构,上传文件变量管理,数据认证等

geekestli 2021-10-28 18:11 原文

读者须知:

文件是全代码文件,比较长,重点功能部分是前端的script标签的各种功能函数,代码量不大,其他大部分上下文代码作为全局参考

 

 

前端添加文件

extends ../layout/default
include ../components/top_header
include ../components/left_sidebar
include ../components/top_title
include ../components/showErrorMessage
include ../components/breadcrumb

block vars
  - var title ="新建广告材料"
  - origin_app_id = user.origin_app_id||''
  - channels = channels || []
  - applicationList = applicationList || []
  link(rel="stylesheet" type="text/css" href="/assets/css/material.css")
  link(rel="stylesheet" type="text/css" href="/assets/css/duallistbox.css")
  

block top
  +top_header(title,origin_app_id)

block leftbar
  +left_sidebar("materials/")

block top_title
  - var bread = [{path: "/admins/materials/", label: "广告材料管理"}, {label: title}]
  +top_title("新建广告材料")
    +breadcrumb(bread)


block content
  .card.card-border-color.card-border-color-primary
    .card-header.card-header-divider
        | 广告材料资料 
        span.card-subtitle 请填写广告材料相关资料
    .card-body
      +showErrorMessage(error_messages||messages)
      form#submit
        .row
          .col-12.col-md-6
            h4(style='font-weight:bold;') 广告材料归属
            //- .form-group.pt-2
            //-   label.col-3.text-right(for='origin_app_id' required="required") 上游APPID
            //-   input.col-9#origin_app_id.form-control(type="url" name='origin_app_id', placeholder='请输入上游APPID', style='display:inline-block;')
            //- .form-group.pt-2
            //-   label.col-3.text-right(for='origin_post_id') 上游广告位ID
            //-   input.col-9#origin_post_id.form-control(type="url" name='origin_post_id', placeholder='请输入上游广告位ID', style='display:inline-block;')
            .form-group.pt-2
              label.col-3.text-right(for='api_channel') API渠道
              select.col-9#api_channel.form-control(name='api_channel', style='display:inline-block;')
                each v,i in channels
                  option(value=i+1) #{v}
            .form-group.pt-2(style="display:flex;")
              label.col-3.text-right(for='applications') 应用选择
              select.col-9#applications.form-control(style='display:inline-block;' multiple size="5")
                each v,i in applicationList
                  option(value=v._id title=v._id) #{v.app_name}(#{v._id})
            .form-group.pt-2(style="display:flex;")
              label.col-3.text-right(for='unapplications') 应用排除选择
              select.col-9#unapplications.form-control(style='display:inline-block;' multiple size="5")
                each v,i in applicationList
                  option(value=v._id title=v._id) #{v.app_name}(#{v._id})
          
            hr.pt-2(style='width:74vw;')
            h4(style='font-weight:bold;') 广告材料名称
            .form-group.pt-2
              label.col-3.text-right(for='name') 
                span(style="color:red") *  
                span 广告材料名称
              input.col-9#name.form-control(type="text" name='name', placeholder='请输入广告材料名称', style='display:inline-block;' required)                                    
            
            hr.pt-2(style='width:74vw;')
            h4(style='font-weight:bold;') 广告材料制作
            .form-group.pt-2
              label.col-3.text-right.align-top(for='api_channel') 广告材料素材
              .col-9.figure.filels
                img.pr-1(src="/assets/images/mv.png", width="50%",onclick="upload('mv')")          
                img.pl-1(src="/assets/images/pic.png", width="50%",onclick="upload('pic')")
            .form-group.pt-2
              span.list-inline-item.col-3.text-right(for='operation') 素材链接操作
              span.col-9#operation 
                a(onclick="addinput()").btn.btn-space.btn-secondary 添加素材链接
                a(onclick="delinput()").btn.btn-space.btn-secondary 删除素材链接
            .form-group.pt-2.url
              label.col-3.text-right(for='url') 素材链接
              input.col-9#url.form-control.materurl(type="text", placeholder='请输入素材链接', style='display:inline-block;')                                    
            .form-group.pt-2.height
              label.col-3.text-right(for='height') 素材高度
              input.col-9#height.form-control.materheight(type="text", placeholder='请输入素材高度(单位:像素)', style='display:inline-block;')                                    
            .form-group.pt-2.width
              label.col-3.text-right(for='width') 素材宽度
              input.col-9#width.form-control.materwidth(type="text", placeholder='请输入素材宽度(单位:像素)', style='display:inline-block;')                                    
            .form-group.pt-2
              label.col-3.text-right(for='title') 广告标题
              input.col-9#title.form-control(type="text" name='title', placeholder='请输入广告标题', style='display:inline-block;')                                    
            .form-group.pt-2
              label.col-3.text-right(for='desc') 广告描述
              input.col-9#desc.form-control(type="text" name='desc', placeholder='请输入广告描述', style='display:inline-block;')
            .form-group.pt-2
              label.col-3.text-right(for='weight') 权重
              select.col-9#weight.form-control(name='weight', style='display:inline-block;')
                option(value=100) 100(最优先)
                option(value=90) 90(次优先)
                option(value=70) 70(较优先)
                option(value=50 selected="selected") 50(一般优先)
                option(value=30) 30(不优先)
                option(value=10) 10(非常不优先)
            .form-group.pt-2
              label.col-3.text-right(for='origin_config') 其它参数
              input.col-9#origin_config.form-control(type='text' name='origin_config', placeholder='请输入上游广告位所需的其它参数', style='display:inline-block;')
            .form-group.pt-2
              label.col-3.text-right(for='type') 类型
              select.col-9#type.form-control(name='type', style='display:inline-block;')
                option(value="1") 视频
                option(value="2") 单图
                option(value="3") 多图
                option(value="4") html文本
                option(value="5") 音频
            .form-group.pt-2
              label.col-3.text-right(for='orientation') 屏幕方向
              select.col-9#orientation.form-control(style='display:inline-block;')
                option(value="") 类型为视频,选择横竖屏
                option(value="1") 竖屏
                option(value="2") 横屏
            .form-group.pt-2
              label.col-3.text-right(for='ad_type') 广告类型
              select.col-9#ad_type.form-control(name='ad_type', style='display:inline-block;')
                option(value="1") 轮播图
                option(value="2") 视频
                option(value="3") 信息流
                option(value="4") 播放式广告
                option(value="5") 开屏
            .form-group.pt-2
              label.col-3.text-right(for='op_type') opType
              select.col-9#op_type.form-control(name='op_type', style='display:inline-block;')
                option(value="0") 无限制
                option(value="1") app下载
                option(value="2") H5
                option(value="3") Deeplink
                option(value="4") 电话广告
                option(value="5") 广点通下载广告
                option(value="6") 微信小程序拉起
                option(value="7") 广电通跳转
                option(value="8") 浏览器打开目标链接
            .form-group.pt-2
              label.col-3.text-right(for='switch_type') 运行开关
              select.col-9#switch_type.form-control(name='switch_type', style='display:inline-block;')
                option(value="1") 开
                option(value="0") 关
            
            hr.pt-2(style='width:74vw;')
            h4(style='font-weight:bold;') 转化页面
            .form-group.pt-2
              label.col-3.text-right(for='landing_page') 点击素材
              input.col-9#landing_page.form-control(type="url" name='landing_page', placeholder='请输入点击素材', style='display:inline-block;')
            .form-group.pt-2
              label.col-3.text-right(for='download_url') 应用下载地址
              input.col-9#download_url.form-control(type="url" name='download_url', placeholder='请输入应用下载地址', style='display:inline-block;')
            .form-group.pt-2
              label.col-3.text-right(for='deeplink_url') 
                span(style="color:red") *  
                span app唤醒地址
              input.col-9#deeplink_url.form-control(type='url' name='deeplink_url', placeholder='请输入app唤醒地址', style='display:inline-block;')
            
            //- hr.pt-2(style='width:74vw;')
            //- h4(style='font-weight:bold;') 第三方检测
            //- .form-group.pt-2
            //-   label.col-3.text-right(for='url_track_show') 曝光检测
            //-   input.col-9#url_track_show.form-control(type='text' name='url_track_show', placeholder='请输入曝光检测', style='display:inline-block;')            
            //- .form-group.pt-2
            //-   label.col-3.text-right(for='url_track_click') 点击检测
            //-   input.col-9#url_track_click.form-control(type='text' name='url_track_click', placeholder='请输入点击检测', style='display:inline-block;')            
            //- .form-group.pt-2
            //-   label.col-3.text-right(for='url_track_dplink') 唤醒检测
            //-   input.col-9#url_track_dplink.form-control(type='text' name='url_track_dplink', placeholder='请输入唤醒检测', style='display:inline-block;')                    
        
        input#num(type="text" style="display: none;" value=applicationList.length)
        input#mvfile(type="file" onchange="myupload('mv')" style="display: none;" accept="video/*")
        input#picfile(type="file" onchange="myupload('pic')" style="display: none;" accept="image/*")
        hr.pt-2(style='width:74vw;')
        .text-right
            a(href='/admins/materials/').btn.btn-space.btn-secondary 取消
            button.btn.btn-space.btn-primary.mr-3(onclick="asyncSubmit()") 提交

block script
  script(src="/assets/js/duallistbox.js")
  script.
    $(document).ready(function(){
      //- 穿梭框渲染
      acBoxRender()
    })

    //- 定义全局变量上传文件数组
    window.files = []
    l = console.log
    acBoxL = $('#num').val()

    //- 上传事件转发函数
    function upload(type){
      type == 'mv' ? $('#mvfile')[0].click() : $('#picfile')[0].click()
    }    

    //- 异步表单提交事件函数
    async function asyncSubmit(){
      event.preventDefault()
      dealurl()     // 处理素材链接框
      if(files.length<1) return alert('请添加素材')

      //- 表单必填项验证
        //- 屏幕方向参数验证
      if([1].includes(+$('#type').val())){
        if(!$('#orientation').val()) return alert('当类型为视频时,必须选择屏幕方向')
      }else{
        if($('#orientation').val()) return alert('当类型不为视频时,不能选择写屏幕方向')
      }
        //- 表单必填字段验证
      let formDatas = $("#submit").serializeArray()
      let optionField = ['name','deeplink_url']
      let required = formDatas.every(v=>{return optionField.includes(v.name) ? v.value : true})
      if(!required) return alert('带星号为必填项,请检查你的未填项继续填写')
        //- 素材合法性验证
      if($('#type').val() == 2){
        if(files.length != 1 || !files[0].width) return alert('当类型为单图,必须是只有一个图片素材')
        window.isfull = files[0].height > files[0].width
        let pass = isfull ? files[0].width > 539 && files[0].height > 959 : files[0].width > 959 && files[0].height > 539
        if(!pass && $('#ad_type').val() == 5) return alert('当类型为单图,像素必须是大于960X540')
      }
      if($('#type').val() == 1){
        if(files.length != 1) return alert('当类型为视频时,必须是只有一个视频素材')
      }
      if($('#applications').val().length == acBoxL){
        window.isall = true
      }
      let apps =  $('#applications').val()
      let unapps =  $('#unapplications').val()
      let combiapp = apps.concat(unapps)
      if(combiapp.length != new Set(combiapp).size){
        return alert('应用选择和应用排除含有相同项,请重新选择')
      }        

      //- 重构表单
      let formData = $("#submit").serialize()
        //- 为素材添加type类型参数
      let adtp =  Number($('#type').val())
      adtp === 3 && --adtp
      files.map((v,i)=>{files[i].type = adtp})
      formData += '&features='+JSON.stringify(files)
      formData += '&applications='+(window.isall ? '' : $('#applications').val().join(","))
      formData += '&unapplications='+$('#unapplications').val().join(",")
      formData += '&orientation='+(window.isfull != undefined ? (isfull ? 1 : 2) : +$('#orientation').val())
      let result = await $.post('/admins/materials/add',formData)
      if(result.success){
        alert('添加成功')
        location.href = '/admins/materials/'
      }else{
        if(result.message == '广告材料名称重复') return alert('广告材料名称重复,请修改后重试')
        if(result.message == '广告位id重复') return alert('广告位id重复,请修改后重试')
        if(result.message.msg) return alert(result.message.msg)
        alert('网络忙,请刷新后重试')
        location.reload()
      }
    }

    //- 上传功能事件函数
    function myupload(type){
      //- 重建表单数据
      let ele = type == 'mv' ? '#mvfile' : '#picfile'             
      let file =$(ele)[0].files[0]
      if(file == 'undefined' || file == undefined){return}
      let myform = new FormData()
      myform.append('file',file)

      //- 异步请求服务器
      console.log("正在上传数据,请稍后...")
      let ajax = $.ajax({url:'/admins/materials/upload',type:'post',data:myform,contentType: false,processData: false})
      ajax.then(function(res){        
        const { result, success } = res 
        if(!success) return alert("接口返回值错误")
        console.log("文件上传成功")
        
        //- 上传成功的文件名推入全局变量
        disposeParam = {...result.filePath,...result.fileSize}
        delete disposeParam.type
        files.push(disposeParam)

        //- 文件上传成功把附件局部更新到页面
        let txt = result.filePath.fileName
        let dels = $('[class^="del"]').length+1
        let nclass = 'del' + dels 
        $('.filels').append(`<div title="${result.filePath.filePath}"><span>${txt}</span>   <img  src="/assets/images/del.png", class="${nclass}", width="25""></img></div>`)
        $('.'+nclass).click(function () {delitem()})
      })                  
    } 

    //- 附件删除事件函数
    function delitem(){
      let ele = event.target.parentNode
      let name = ele.textContent.trim()   
      let sure = confirm(`确定要删除${ name }素材吗?`)
      if(sure){
        files.map((v,i) => {
          if(v.fileName == name){
            files.splice(i,1)               
          }
        })
        $(ele).remove()
      }
    }

    //- 媒体主穿梭框渲染
    function acBoxRender(){
      let config = {
        //- nonSelectedListLabel: '未选媒体主列表:',
        //- selectedListLabel: '已选媒体主列表:',
        preserveSelectionOnMove: 'moved',
        moveOnSelect: false,           // 出现一个剪头,表示可以一次选择一个
        filterTextClear: '展示所有',
        moveSelectedLabel: "添加",
        moveAllLabel: '添加所有',
        removeSelectedLabel: "移除",
        removeAllLabel: '移除所有',
        infoText: '共{0}个',
        showFilterInputs: false,       // 是否带搜索
        selectorMinimalHeight: 160
      }
      $('#applications').bootstrapDualListbox(config)
      $('#unapplications').bootstrapDualListbox(config)
    }

    //- 素材链接输入框添加
    function addinput(){
      let eles = Array.from($('.width'))
      let len = eles.length
      let endele = eles[len-1]
      let html = `
        <div class="form-group pt-2 url">
          <label class="col-3 text-right" style="margin-right: -4px;">素材链接${len}</label>
          <input class="col-9 form-control materurl" type="text" placeholder='请输入素材链接' style="display:inline-block;">
        </div>
        <div class="form-group pt-2 height">
          <label class="col-3 text-right" style="margin-right: -4px;">素材高度${len}</label>
          <input class="col-9 form-control materheight" type="text" placeholder='请输入素材高度(单位:像素)' style="display:inline-block;">
        </div>
        <div class="form-group pt-2 width">
          <label class="col-3 text-right" style="margin-right: -4px;">素材宽度${len}</label>
          <input class="col-9 form-control materwidth" type="text" placeholder='请输入素材宽度(单位:像素)' style="display:inline-block;">
        </div>
      `
      $(endele).after(html)
    }

    //- 素材链接输入框删除
    function delinput(){
      let eles = Array.from($('.url'))
      let eles1 = Array.from($('.width'))
      let eles2 = Array.from($('.height'))
      if(eles.length<2){
        return alert('素材链接输入框少于两个,不能删除')
      }
      if(confirm('确认删除最后一个素材链接输入框吗?')){
        let endele = eles.pop()
        let endele1 = eles1.pop()
        let endele2 = eles2.pop()
        let value = $(endele).find('input').val()
        files.map((v,i)=>{if(v.url==value) files.splice(i,1)})
        let parent = endele.parentNode
        let parent1 = endele1.parentNode
        let parent2 = endele2.parentNode
        parent.removeChild(endele)
        parent1.removeChild(endele1)
        parent2.removeChild(endele2)
      }
    }

    //- 素材链接输入框表单数据处理
    function dealurl(){
      let eles = Array.from($('.materurl'))
      let eles1 = Array.from($('.materheight'))
      let eles2 = Array.from($('.materwidth'))
      files = files.filter((v)=>{return v.name})
      eles.map((v,i)=>{
        if(v.value){
          files.push({url:v.value,height:+eles1[i].value,width:+eles2[i].value})
        }
      })
    }

 

 

前端修改文件

extends ../layout/default
include ../components/top_header
include ../components/left_sidebar
include ../components/top_title
include ../components/showErrorMessage
include ../components/breadcrumb

block vars
  - var title ="修改广告材料"
  - origin_app_id = user.origin_app_id || ''
  - material = material || {}
  - material.applications = material.applications || []
  - channels = channels || []
  - applicationList = applicationList || []
  link(rel="stylesheet" type="text/css" href="/assets/css/material.css")
  link(rel="stylesheet" type="text/css" href="/assets/css/duallistbox.css")

block top
  +top_header(title,origin_app_id)

block leftbar
  +left_sidebar("materials/")

block top_title
  - var bread = [{path: "/admins/materials/", label: "广告材料管理"}, {label: title}]
  +top_title("修改广告材料")
    +breadcrumb(bread)


block content
  .card.card-border-color.card-border-color-primary
    .card-header.card-header-divider
        | 广告材料资料 
        span.card-subtitle 请填写广告材料相关资料
    .card-body
      +showErrorMessage(error_messages||messages)
      form#submit
        .row
          .col-12.col-md-6
            h4(style='font-weight:bold;') 广告材料归属
            //- .form-group.pt-2
            //-   label.col-md-3.text-right(for='origin_app_id' required="required") 上游APPID
            //-   input.col-md-9#origin_app_id.form-control(type="url" name='origin_app_id', value=material.origin_app_id, placeholder='请输入上游APPID', style='display:inline-block;')
            //- .form-group.pt-2
            //-   label.col-md-3.text-right(for='origin_post_id') 上游广告位ID
            //-   input.col-md-9#origin_post_id.form-control(type="url" name='origin_post_id', value=material.origin_post_id, placeholder='请输入上游广告位ID', style='display:inline-block;')
            .form-group.pt-2
              label.col-md-3.text-right(for='api_channel') API渠道
              select.col-md-9#api_channel.form-control(name='api_channel', style='display:inline-block;')
                each v,i in channels
                  option(value=i+1 selected = material.api_channel == i+1 ? true : false) #{v}
            .form-group.pt-2(style="display:flex;")
              label.col-3.text-right(for='applications') 应用选择
              select.col-9#applications.form-control(style='display:inline-block;' multiple size="5")
                each v,i in applicationList
                  option(value=v._id title=v._id selected = material.applications.includes(v._id) ? true : false) #{v.app_name}(#{v._id})
            .form-group.pt-2(style="display:flex;")
              label.col-3.text-right(for='unapplications') 应用排除选择
              select.col-9#unapplications.form-control(style='display:inline-block;' multiple size="5")
                each v,i in applicationList
                  option(value=v._id title=v._id selected = material.unapplications.includes(v._id) ? true : false) #{v.app_name}(#{v._id})


            hr.pt-2(style='width:74vw;')
            h4(style='font-weight:bold;') 广告材料名称
            .form-group.pt-2
              label.col-md-3.text-right(for='name')
                span(style="color:red") *  
                span 广告材料名称
              input.col-md-9#name.form-control(type="text" name='name', value=material.name, placeholder='请输入广告材料名称', style='display:inline-block;')                                    
            
            hr.pt-2(style='width:74vw;')
            h4(style='font-weight:bold;') 广告材料制作
            .form-group.pt-2
              label.col-md-3.text-right.align-top(for='api_channel') 广告材料素材
              .col-md-9.figure.filels
                img.pr-1(src="/assets/images/mv.png", width="50%",onclick="upload('mv')")          
                img.pl-1(src="/assets/images/pic.png", width="50%",onclick="upload('pic')")
                each v,i in material.features
                  if v.name
                    div 
                      span #{v.name}
                      img(src="/assets/images/del.png", class="del"+(i+1), width="25",onclick="delitem()")
            .form-group.pt-2
              span.list-inline-item.col-3.text-right(for='operation') 素材链接操作
              span.col-9#operation 
                a(onclick="addinput()").btn.btn-space.btn-secondary 添加素材链接
                a(onclick="delinput()").btn.btn-space.btn-secondary 删除素材链接
            each v,i in material.features
              if !v.name                        
                .form-group.pt-2.url
                  label.col-3.text-right(for='url') 素材链接#{i+1}
                  input.col-9#url.form-control.materurl(type="text", placeholder='请输入素材链接', value=v.url, style='display:inline-block;')                                    
                .form-group.pt-2.height
                  label.col-3.text-right(for='height') 素材高度#{i+1}
                  input.col-9#height.form-control.materheight(type="text", value=v.height, placeholder='请输入素材高度(单位:像素)', style='display:inline-block;')                                    
                .form-group.pt-2.width
                  label.col-3.text-right(for='width') 素材宽度#{i+1}
                  input.col-9#width.form-control.materwidth(type="text", value=v.width, placeholder='请输入素材宽度(单位:像素)', style='display:inline-block;')                                                    
            .form-group.pt-2
              label.col-md-3.text-right(for='title') 广告标题
              input.col-md-9#title.form-control(type="text" name='title', value=material.title, placeholder='请输入广告标题', style='display:inline-block;')                                    
            .form-group.pt-2
              label.col-md-3.text-right(for='desc') 广告描述
              input.col-md-9#desc.form-control(type="text" name='desc', value=material.desc, placeholder='请输入广告描述', style='display:inline-block;')
            .form-group.pt-2
              label.col-md-3.text-right(for='weight') 权重
              select.col-md-9#weight.form-control(name='weight', style='display:inline-block;')
                option(value="100" selected = material.weight == 100 ? true : false) 100(最优先)
                option(value="90" selected = material.weight == 90 ? true : false) 90(次优先)
                option(value="70" selected = material.weight == 70 ? true : false) 70(较优先)
                option(value="50" selected = material.weight == 50 ? true : false) 50(一般优先)
                option(value="30" selected = material.weight == 30 ? true : false) 30(不优先)
                option(value="10" selected = material.weight == 10 ? true : false) 10(非常不优先)
            .form-group.pt-2
              label.col-md-3.text-right(for='origin_config') 其它参数
              input.col-md-9#origin_config.form-control(type='text', name='origin_config', value=material.origin_config, placeholder='请输入上游广告位所需的其它参数', style='display:inline-block;')
            .form-group.pt-2
              label.col-md-3.text-right(for='type') 类型
              select.col-md-9#type.form-control(name='type', style='display:inline-block;')
                option(value="1" selected = material.type == 1 ? true : false) 视频
                option(value="2" selected = material.type == 2 ? true : false) 单图
                option(value="3" selected = material.type == 3 ? true : false) 多图
                option(value="4" selected = material.type == 4 ? true : false) html文本
                option(value="5" selected = material.type == 5 ? true : false) 音频
            .form-group.pt-2
              label.col-md-3.text-right(for='orientation') 屏幕方向
              select.col-md-9#orientation.form-control(style='display:inline-block;')
                option(value="") 类型为视频,选择横竖屏
                option(value="1" selected = material.features[0] && material.features[0].orientation == 1 ? true : false) 竖屏
                option(value="2" selected = material.features[0] && material.features[0].orientation == 2 ? true : false) 横屏
              //- input.col-md-9#orientation.form-control(type="number", value=material.features[0].orientation, placeholder='如果类型为单图或视频,竖1,横2', style='display:inline-block;')            
            .form-group.pt-2
              label.col-md-3.text-right(for='ad_type') 广告类型
              select.col-md-9#ad_type.form-control(name='ad_type', style='display:inline-block;')
                option(value="1" selected = material.ad_type == 1 ? true : false) 轮播图
                option(value="2" selected = material.ad_type == 2 ? true : false) 视频
                option(value="3" selected = material.ad_type == 3 ? true : false) 信息流
                option(value="4" selected = material.ad_type == 4 ? true : false) 播放式广告
                option(value="5" selected = material.ad_type == 5 ? true : false) 开屏
            .form-group.pt-2
              label.col-md-3.text-right(for='op_type') opType
              select.col-md-9#op_type.form-control(name='op_type', style='display:inline-block;')
                option(value="0" selected = material.op_type == 0 ? true : false) 无限制
                option(value="1" selected = material.op_type == 1 ? true : false) app下载
                option(value="2" selected = material.op_type == 2 ? true : false) H5
                option(value="3" selected = material.op_type == 3 ? true : false) Deeplink
                option(value="4" selected = material.op_type == 4 ? true : false) 电话广告
                option(value="5" selected = material.op_type == 5 ? true : false) 广点通下载广告
                option(value="6" selected = material.op_type == 6 ? true : false) 微信小程序拉起
                option(value="7" selected = material.op_type == 7 ? true : false) 广电通跳转
                option(value="8" selected = material.op_type == 8 ? true : false) 浏览器打开目标链接
            .form-group.pt-2
              label.col-md-3.text-right(for='switch_type') 运行开关
              select.col-md-9#switch_type.form-control(name='switch_type', style='display:inline-block;')
                option(value="1" selected = material.switch_type == 1 ? true : false) 开
                option(value="0" selected = material.switch_type == 0 ? true : false) 关
            
            hr.pt-2(style='width:74vw;')
            h4(style='font-weight:bold;') 转化页面
            .form-group.pt-2
              label.col-md-3.text-right(for='landing_page') 点击素材
              input.col-md-9#landing_page.form-control(type="url" name='landing_page', value=material.landing_page, placeholder='请输入点击素材', style='display:inline-block;')
            .form-group.pt-2
              label.col-md-3.text-right(for='download_url') 应用下载地址
              input.col-md-9#download_url.form-control(type="url" name='download_url', value=material.download_url, placeholder='请输入应用下载地址', style='display:inline-block;')
            .form-group.pt-2
              label.col-md-3.text-right(for='deeplink_url') 
                span(style="color:red") *  
                span app唤醒地址
              input.col-md-9#deeplink_url.form-control(type='url' name='deeplink_url', value=material.deeplink_url, placeholder='请输入app唤醒地址', style='display:inline-block;')
            
            //- hr.pt-2(style='width:74vw;')
            //- h4(style='font-weight:bold;') 第三方检测
            //- .form-group.pt-2
            //-   label.col-md-3.text-right(for='url_track_show') 曝光检测
            //-   input.col-md-9#url_track_show.form-control(type='text' name='url_track_show', value=material.url_track_show, placeholder='请输入曝光检测', style='display:inline-block;')            
            //- .form-group.pt-2
            //-   label.col-md-3.text-right(for='url_track_click') 点击检测
            //-   input.col-md-9#url_track_click.form-control(type='text' name='url_track_click', value=material.url_track_click, placeholder='请输入点击检测', style='display:inline-block;')            
            //- .form-group.pt-2
            //-   label.col-md-3.text-right(for='url_track_dplink') 唤醒检测
            //-   input.col-md-9#url_track_dplink.form-control(type='text' name='url_track_dplink', value=material.url_track_dplink, placeholder='请输入唤醒检测', style='display:inline-block;')                    
        
        input#num(type="text" style="display: none;" value=applicationList.length)
        input#mvfile(type="file" onchange="myupload('mv')" style="display: none;" accept="video/*")
        input#picfile(type="file" onchange="myupload('pic')" style="display: none;" accept="image/*")
        hr.pt-2(style='width:74vw;')
        .text-right
            a(href='/admins/materials/').btn.btn-space.btn-secondary 取消
            button.btn.btn-space.btn-primary.mr-3(onclick="asyncSubmit()") 保存
block script
  script(src="/assets/js/duallistbox.js")
  script.
    $(function(){
      //- $.fn.sspmaterials/Add()
      window.oldfiles = getFiles()      // 初始化获取素材文件并缓存
      //- 穿梭框渲染
      acBoxRender()
    })

    //- 定义全局变量上传文件数组
    window.files = []    
    l = console.log
    id = location.href.split('/').pop()
    acBoxL = $('#num').val()
 
    //- 上传事件转发函数
    function upload(type){
      type == 'mv' ? $('#mvfile')[0].click() : $('#picfile')[0].click()
    }    

    //- 异步表单提交事件函数
    async function asyncSubmit(){
      event.preventDefault()
      dealurl()     // 处理素材链接框
      //- return console.log(files)
      //- 表单必填项验证
      if([1].includes(+$('#type').val())){
        if(!$('#orientation').val()) return alert('当类型为视频时,必须选择屏幕方向')
      }
      let formDatas = $("#submit").serializeArray()
      let optionField = ['name','deeplink_url']
      let required = formDatas.every(v=>{return optionField.includes(v.name) ? v.value : true})
      if(!required) return alert('带星号为必填项,请检查你的未填项继续填写')     
      if($('#applications').val().length == acBoxL){
        window.isall = true
      }
      let apps =  $('#applications').val()
      let unapps =  $('#unapplications').val()
      let combiapp = apps.concat(unapps)
      if(combiapp.length != new Set(combiapp).size){
        return alert('应用选择和应用排除含有相同项,请重新选择')
      }   

      //- 重构表单
      let formData = $("#submit").serialize()
      let adtp =  Number($('#type').val())
      adtp === 3 && --adtp
      files.map((v,i)=>{files[i].type = adtp})
      formData += '&features='+JSON.stringify(files)
      formData += '&id='+id
      formData += '&dels='+getDelFiles().join(",")
      formData += '&applications='+(window.isall ? '' : $('#applications').val().join(","))
      formData += '&unapplications='+$('#unapplications').val().join(",")
      formData += '&orientation='+$('#orientation').val()
      let result = await $.post('/admins/materials/edit/'+id, formData)
      if(result.success){
        alert('修改成功')
        location.href = '/admins/materials/'
      }else{
        if(result.message == '广告材料名称重复') return alert('广告材料名称重复,请修改后重试')
        if(result.message == '广告位id重复') return alert('广告位id重复,请修改后重试')
        alert('网络忙,请刷新后重试')
        location.reload()
      }
    }

    //- 获取素材附件
    function getFiles(){
      let attachments = Array.from($('.filels div'))
      attachments = attachments.map(v=>{
        return v.textContent
      })
      return attachments
    }

    //- 获取删除已上传素材附件
    function getDelFiles(){
      let files = getFiles()
      let dels = []
      oldfiles.map(v=>{
        !files.includes(v) && dels.push(v.trim())
      })
      return dels
    }

    //- 上传功能事件函数
    function myupload(type){
      //- 重建表单数据
      let ele = type == 'mv' ? '#mvfile' : '#picfile'             
      let file =$(ele)[0].files[0]
      if(file == 'undefined' || file == undefined){return}
      let myform = new FormData()
      myform.append('file',file)

      //- 异步请求服务器
      console.log("正在上传数据,请稍后...")
      let ajax = $.ajax({url:'/admins/materials/upload',type:'post',data:myform,contentType: false,processData: false})
      ajax.then(function(res){        
        const { result, success } = res 
        if(!success) return alert("接口返回值错误")
        console.log("文件上传成功")
        
        //- 上传成功的文件名推入全局变量
        disposeParam = {...result.filePath,...result.fileSize}
        delete disposeParam.type
        files.push(disposeParam)

        //- 文件上传成功把附件局部更新到页面
        let txt = result.filePath.fileName
        let dels = $('[class^="del"]').length+1
        let nclass = 'del' + dels 
        $('.filels').append(`<div title="${result.filePath.filePath}"><span>${txt}</span>   <img  src="/assets/images/del.png", class="${nclass}", width="25""></img></div>`)
        $('.'+nclass).click(function () {delitem()})
      })                  
    } 

    //- 附件删除事件函数
    function delitem(){
      let ele = event.target.parentNode
      let name = ele.textContent.trim()   
      let sure = confirm(`确定要删除${ name }素材吗?`)
      if(sure){
        files.map((v,i) => {
          if(v.fileName == name){
            files.splice(i,1)               
          }
        })
        $(ele).remove()
      }
    }

    //- 媒体主穿梭框渲染
    function acBoxRender(){
      let config = {
        //- nonSelectedListLabel: '未选媒体主列表:',
        //- selectedListLabel: '已选媒体主列表:',
        preserveSelectionOnMove: 'moved',
        moveOnSelect: false,           // 出现一个剪头,表示可以一次选择一个
        filterTextClear: '展示所有',
        moveSelectedLabel: "添加",
        moveAllLabel: '添加所有',
        removeSelectedLabel: "移除",
        removeAllLabel: '移除所有',
        infoText: '共{0}个',
        showFilterInputs: false,       // 是否带搜索
        selectorMinimalHeight: 160
      }
      $('#applications').bootstrapDualListbox(config)
      $('#unapplications').bootstrapDualListbox(config)
    }

    //- 素材链接输入框添加
    function addinput(){
      let eles = Array.from($('.width'))
      let len = eles.length
      let endele = eles[len-1]
      let html = `
        <div class="form-group pt-2 url">
          <label class="col-3 text-right" style="margin-right: -4px;">素材链接${len}</label>
          <input class="col-9 form-control materurl" type="text" placeholder='请输入素材链接' style="display:inline-block;">
        </div>
        <div class="form-group pt-2 height">
          <label class="col-3 text-right" style="margin-right: -4px;">素材高度${len}</label>
          <input class="col-9 form-control materheight" type="text" placeholder='请输入素材高度(单位:像素)' style="display:inline-block;">
        </div>
        <div class="form-group pt-2 width">
          <label class="col-3 text-right" style="margin-right: -4px;">素材宽度${len}</label>
          <input class="col-9 form-control materwidth" type="text" placeholder='请输入素材宽度(单位:像素)' style="display:inline-block;">
        </div>
      `
      $(endele).after(html)
    }

    //- 素材链接输入框删除
    function delinput(){
      let eles = Array.from($('.url'))
      let eles1 = Array.from($('.width'))
      let eles2 = Array.from($('.height'))
      if(eles.length<2){
        return alert('素材链接输入框少于两个,不能删除')
      }
      if(confirm('确认删除最后一个素材链接输入框吗?')){
        let endele = eles.pop()
        let endele1 = eles1.pop()
        let endele2 = eles2.pop()
        let value = $(endele).find('input').val()
        files.map((v,i)=>{if(v.url==value) files.splice(i,1)})
        let parent = endele.parentNode
        let parent1 = endele1.parentNode
        let parent2 = endele2.parentNode
        parent.removeChild(endele)
        parent1.removeChild(endele1)
        parent2.removeChild(endele2)
      }
    }

    //- 素材链接输入框表单数据处理
    function dealurl(){
      let eles = Array.from($('.materurl'))
      let eles1 = Array.from($('.materheight'))
      let eles2 = Array.from($('.materwidth'))
      files = files.filter((v)=>{return v.name})
      eles.map((v,i)=>{
        if(v.value){
          files.push({url:v.value,height:+eles1[i].value,width:+eles2[i].value})
        }       
      })
    }
      

 

后端koa文件

_ = require 'lodash'
path = require 'path'
fs = require 'fs'
url = require 'url'
os = require 'os'
debuglog = require('debug')("ssp::controllers:::material")
express = require "express"
router = express.Router()

# 导入数据模型
ModelMaterial = require '../models/material'
mongoose = require('mongoose')
MongoMaterial = mongoose.model('Material')

# 导入上传文件需要的依赖
multiparty = require('multiparty')
sizeOf = require('image-size')
profileName = process.env.DO_PROFILE
unless profileName
  throw new Error "DO SPACES INIT FAILED: Missing enviroment variable: DO_PROFILE"
pathToConfig = path.join os.homedir(), ".ssps3", profileName + ".json"
configRaw = fs.readFileSync pathToConfig
config = JSON.parse(configRaw)
PATH_PREFIX = config.priefix||'dev'
HOST = 'https://yjh-material.yunpro.cn/'



{
  sendSuccess,
  sendError,
  genResRender,
  getResRedirect,
  genResJson,
  renderItemsAsSuccess
} = require '../utils/response_util'


{
  requiresLogin
  isAdminRoleType
} = require '../middlewares/authorization'

{
  uploadFeature
} = require '../utils/dospace_util'

{
  generateUUID
} = require '../utils/string_util'

{
  API_CHANNEL_ENUMS_LABEL
} = require '../enums/api_channels'

{
  listAllApplications
} = require '../models/application'

# 上传表单解析方法
uploadBasic = (req, res, next) ->
  debuglog "[uploadBasic] start.", req.isAuthenticated()
  form = new multiparty.Form()
  form.parse req, (err, fields, files) ->
    if err?
      debuglog "[uploadBasic] ERROR: #{err}"
      sendError res, err
      return

    if _.isEmpty(files) or not files?
      debuglog "[uploadBasic] WARN files is empty."
      sendError res, "文件上传错误。"
      return

    keys = _.keys(files)
    file = files[keys[0]][0]
    if _.isEmpty(file)
      debuglog "[uploadBasic] WARN file is empty"
      sendError res, "文件上传错误。"
      return

    # TODO 确定判断上传文件类型和大小的条件
    options =
      size : file.sizegt
      originalFilename : file.originalFilename
      extName: path.extname(file.originalFilename)
      source: file.path
      contentType : file['headers']["content-type"]
      fields: fields||{}
    res.locals.uploadOptions = options
    debuglog("[uploadBasic] %j - %j", file, options)
    next()
    return
  return

# 文件上传到oss方法
uploadFileToOss = (req, res, next ) ->
  debuglog "[uploadFileToOss] start."
  uploadOptions = res.locals.uploadOptions||{}

  if _.isEmpty(uploadOptions)
    debuglog "[uploadFileToOss] ERROR: 数据上传错误。 userId:#{userId}"
    sendError res, "数据上传错误。"
    return

  {source, fields, extName, size, originalFilename, contentType} = uploadOptions
  oss_id = generateUUID()
  remoteFilePath = "#{oss_id}#{extName}"
  if contentType.match('image')
    res.locals.fileSize = sizeOf(source)
  uploadFeature remoteFilePath, source, contentType, (err) ->
    fs.unlinkSync source
    if err?
      debuglog "[uploadFileToOss] ERROR:#{err}"
      sendError res, err
      return
    res.locals.filePath =
      filePath : url.resolve HOST,path.join PATH_PREFIX,remoteFilePath
      fileName : originalFilename
    next()
    return
  return

# 广告添加方法
postAddMaterial = (req, res) ->
  body = req.body

  # 素材名称格式化
  features = String(body.features)
  features =features.replace(/filePath/g,'url')
  features = features.replace(/fileName/g,'name')
  body.features = JSON.parse(features)
 
  # 应用列表格式化
  body.applications = String(body.applications).split(",")
  body.unapplications = String(body.unapplications).split(",")

  # 字符串去空格
  for k,v of body
    if typeof v is 'string' then body[k] = v.trim()

  # 添加手动输入屏幕方向到素材对象
  orientation = body.orientation
  delete body.orientation
  if orientation and body.features[0]
    body.features[0].orientation = Number(orientation)

  ModelMaterial.insertData body, (cb) ->
    status = cb[0]
    log = cb[1]
    if status
      sendError res,log
    else
      sendSuccess res,log

# 广告修改方法
postEditMaterial = (req, res) ->
  body = req.body
  features = String(body.features)
  features =features.replace(/filePath/g,'url')
  features = features.replace(/fileName/g,'name')
  body.features = JSON.parse(features)

  # 应用列表格式化
  body.applications = String(body.applications).split(",")
  body.unapplications = String(body.unapplications).split(",")

  for k,v of body
    if typeof v is 'string' then body[k] = v.trim()

  id = body.id
  delete body.id

  dels = if body.dels then body.dels.split(",") else false
  delete body.dels

  oldFiles = res.locals.material.features
  oldFiles = oldFiles.filter ->
    v.name
  if dels
    for v in dels
      for v1,i in oldFiles
        oldFiles.splice i,1 if v is (v1 && v1.name)

  body.features = body.features.concat oldFiles

  orientation = body.orientation
  delete body.orientation
  if orientation
    if body.features[0] then body.features[0].orientation = Number(orientation)

  result = await MongoMaterial.updateOne _id:id,body
  if result.nModified is 1
    sendSuccess res,'修改成功'
  else
    sendError res,'修改失败'

# 获取api渠道方法
getChannels = (req, res, next) ->
  dics = []
  for i,v of API_CHANNEL_ENUMS_LABEL
    dics.push v
  res.locals.channels = dics
  next()
  return

# 获取应用列表方法
getApplicationList = (req, res, next) ->
  listAllApplications (err, applications) -> 
    if err?
      debuglog "[getApplicationList] ERROR:#{err}"
      next(err)
      return
    res.locals.applicationList = applications||[]
    next()
    return
  return

# 广告列表分页获取数据
getPageListMaterial = (req, res, next) ->
  {page} = req.params
  {switch_type} = req.query||{}
  ModelMaterial.pageListMaterial page, req.query, (err, materials) ->
    if err?
      debuglog "[getPageListMaterial:pageListMaterial] ERROR:#{err}."
      next(err)
      return

    res.locals.materials = materials||[]
    if switch_type?
      res.locals.switch_type = switch_type
    next()
    return
  return

# 广告开关状态更新方法
postUpdateSwitchType = (req, res, next) ->
  {_id} = req.params
  user = req.user || {}
  {material} = res.locals||{}
  referer = (req.headers||{}).referer
  # console.log(res.locals)
  debuglog "[postUpdateSwitchType] start. material:#{_id} user:#{user._id}"

  if _.isEmpty(material)
    debuglog "[postUpdateSwitchType] WARN material is empty. material:#{_id} user:#{user._id}"
    # sendError res, 'materialUser is empty'
    req.flash('error', "没有对应的广告")
    res.redirect referer||"/admins/materials"
    return
  switchType = if material.switch_type == 0 then 1 else 0
  ModelMaterial.updateSwitchTypeById _id, switchType, (err, newMaterial) ->
    if err?
      debuglog "[postUpdateSwitchType:updateSwitchTypeById] ERROR:#{err}. material:#{_id} user:#{user._id}"
      # sendError res, err
      req.flash('error', "#{err}")
      res.redirect referer||"/admins/materials"
      return
    # sendSuccess res, newmaterialUser
    res.redirect referer||"/admins/materials"
    return
  return

# 通过广告id获取广告方法
getMaterialDeatilById = (req, res, next) ->
  {_id} = req.params
  debuglog "[getMaterialDeatilById] start. Material:#{_id}"
  ModelMaterial.findMaterialById _id, (err, material) ->
    if err?
      debuglog "[getMaterialDeatilById:findMaterialById] ERROR:#{err}. material:#{_id}"
      next(err)
      return
    res.locals.material = material||{}
    next()
    return
  return

# 添加直投广告页面
router.get '/add',
  requiresLogin,
  isAdminRoleType,
  getChannels,
  getApplicationList,
  genResRender("materials/add")

# 添加直投广告接口
router.post '/add',
  requiresLogin,
  isAdminRoleType,
  postAddMaterial

# 修改直投广告页面
router.get '/edit/:_id',
  requiresLogin,
  isAdminRoleType,
  getMaterialDeatilById,
  getChannels,
  getApplicationList,
  genResRender("materials/edit")

# 修改直投广告接口
router.post '/edit/:_id',
  requiresLogin,
  isAdminRoleType,
  getMaterialDeatilById,
  postEditMaterial

# 直投广告上传接口
router.post "/upload",
  requiresLogin
  isAdminRoleType
  uploadBasic
  uploadFileToOss
  (req,res) ->
    sendSuccess(res,res.locals)


# 广告材料列表
router.get '/:page?',
  requiresLogin,
  isAdminRoleType,
  getPageListMaterial,
  getChannels
  # genResJson()
  genResRender("materials/index")

#列表switch修改
router.post '/switch/update/:_id',
  requiresLogin,
  isAdminRoleType,
  getMaterialDeatilById,
  postUpdateSwitchType

module.exports = exports = router

 

推荐阅读