完整表单全栈代码,有自定义上传功能,项扩展功能,数据回显,异步表单重构,上传文件变量管理,数据认证等
读者须知:
文件是全代码文件,比较长,重点功能部分是前端的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