@@ -0,0 +1,495 @@
/* 可视化页面编辑器(可复用):三栏布局(图标栏 | 编辑画布 | 标签页右栏)
* 右栏:预览(只读实时模拟)/ 属性 / 素材(可删除)
* 支持元素类型:text 文字, image 图片, button 链接按钮, buy 产品购买, price 产品价格, specs 产品规格
* 对齐:左/中/右 + 竖(文字=竖排 writing-mode,图片=垂直居中)
* 依赖全局(由后台 partial 注入):
* __PB_INIT__ 初始布局数组
* __PB_MODULE__ page|product|news|case|category
* __PB_MEDIA__ 素材列表接口
* __PB_UPLOAD__ 上传接口
* __PB_DELETE__ 删除接口前缀(拼接文件名)
* __PB_CSRF__ CSRF 令牌
*/
( function ( ) {
'use strict' ;
var STAGE _W = 720 ;
var stage = document . getElementById ( 'pbStage' ) ;
if ( ! stage ) return ;
var MODULE = ( window . _ _PB _MODULE _ _ || 'page' ) ;
var elements = Array . isArray ( window . _ _PB _INIT _ _ ) ? window . _ _PB _INIT _ _ . slice ( ) : [ ] ;
var selectedId = null ;
var zCounter = elements . reduce ( function ( m , e ) { return Math . max ( m , e . z || 0 ) ; } , 0 ) ;
function uid ( ) { return 'el_' + Date . now ( ) . toString ( 36 ) + Math . floor ( Math . random ( ) * 1e4 ) . toString ( 36 ) ; }
function nextZ ( ) { return ++ zCounter ; }
function findEl ( id ) { for ( var i = 0 ; i < elements . length ; i ++ ) if ( elements [ i ] . id === id ) return elements [ i ] ; return null ; }
function nodeOf ( id ) { return stage . querySelector ( '[data-id="' + id + '"]' ) ; }
function ptOf ( ev ) { return ev . touches && ev . touches [ 0 ] ? ev . touches [ 0 ] : ev ; }
function on ( id , evt , fn ) { var el = document . getElementById ( id ) ; if ( el ) el . addEventListener ( evt , fn ) ; }
/* ---------- 对齐样式(文字:text-align + 竖排;图片:flex 水平 + 垂直居中) ---------- */
function styleAlign ( el , node ) {
node . style . textAlign = '' ;
node . style . writingMode = '' ;
node . style . display = '' ;
node . style . flexDirection = '' ;
node . style . alignItems = '' ;
node . style . justifyContent = '' ;
if ( el . type === 'image' ) {
node . style . display = 'flex' ;
node . style . flexDirection = 'column' ;
node . style . alignItems = ( el . align === 'center' ) ? 'center' : ( el . align === 'right' ) ? 'flex-end' : 'flex-start' ;
node . style . justifyContent = el . v ? 'center' : 'flex-start' ;
} else if ( el . type === 'text' ) {
node . style . textAlign = el . align || 'left' ;
if ( el . v ) node . style . writingMode = 'vertical-rl' ;
}
}
/* ---------- 通用指针拖拽(鼠标 + 触摸) ---------- */
function onPointer ( ev , handlers ) {
ev . preventDefault ( ) ;
var move = function ( e ) { if ( handlers . move ) handlers . move ( ptOf ( e ) ) ; } ;
var up = function ( ) {
document . removeEventListener ( 'mousemove' , move ) ;
document . removeEventListener ( 'mouseup' , up ) ;
document . removeEventListener ( 'touchmove' , move ) ;
document . removeEventListener ( 'touchend' , up ) ;
if ( handlers . end ) handlers . end ( ) ;
} ;
document . addEventListener ( 'mousemove' , move ) ;
document . addEventListener ( 'mouseup' , up ) ;
document . addEventListener ( 'touchmove' , move , { passive : false } ) ;
document . addEventListener ( 'touchend' , up ) ;
if ( handlers . start ) handlers . start ( ptOf ( ev ) ) ;
}
/* ---------- 构建元素内部内容 ---------- */
function buildInner ( el ) {
if ( el . type === 'image' ) {
var im = document . createElement ( 'img' ) ;
im . className = 'pb-img' ; im . src = el . src || '' ; im . alt = '' ; im . draggable = false ;
im . addEventListener ( 'load' , updateStageSize ) ;
return im ;
}
if ( el . type === 'button' || el . type === 'buy' ) {
var a = document . createElement ( 'a' ) ;
a . className = 'pb-static pb-btn-prev' + ( el . type === 'buy' ? ' pb-buy' : '' ) ;
a . textContent = el . type === 'buy' ? '立即购买' : ( el . text || '按钮' ) ;
if ( el . type === 'button' ) {
if ( el . bg ) a . style . background = el . bg ;
if ( el . tc ) a . style . color = el . tc ;
a . style . fontSize = ( el . size === 'lg' ? '18px' : el . size === 'sm' ? '13px' : '15px' ) ;
}
return a ;
}
if ( el . type === 'price' ) {
var d = document . createElement ( 'div' ) ;
d . className = 'pb-static pb-price-prev' ;
d . textContent = '¥ — 起/套' ;
if ( el . color ) d . style . color = el . color ;
return d ;
}
if ( el . type === 'specs' ) {
var t = document . createElement ( 'div' ) ;
t . className = 'pb-static pb-specs-prev' ;
t . innerHTML = '<div class="sp-row"><span>参数 1</span><span>值 1</span></div><div class="sp-row"><span>参数 2</span><span>值 2</span></div>' ;
return t ;
}
var tx = document . createElement ( 'div' ) ;
tx . className = 'pb-text' ;
tx . textContent = el . text != null ? el . text : '' ;
tx . style . fontSize = ( el . fontSize || 18 ) + 'px' ;
tx . style . color = el . color || '#0f172a' ;
tx . style . fontWeight = el . bold ? '700' : '400' ;
tx . style . textAlign = el . align || 'left' ;
tx . style . lineHeight = '1.4' ;
return tx ;
}
/* ---------- 渲染单个元素 ---------- */
function renderEl ( el ) {
var wrap = document . createElement ( 'div' ) ;
wrap . className = 'pb-el' ;
wrap . setAttribute ( 'data-id' , el . id ) ;
wrap . style . left = ( el . x || 0 ) + 'px' ;
wrap . style . top = ( el . y || 0 ) + 'px' ;
wrap . style . width = ( el . w || 200 ) + 'px' ;
wrap . style . zIndex = el . z || 1 ;
wrap . appendChild ( buildInner ( el ) ) ;
styleAlign ( el , wrap ) ;
var h = document . createElement ( 'span' ) ;
h . className = 'pb-handle' ;
h . addEventListener ( 'mousedown' , function ( e ) { startResize ( e , el ) ; } ) ;
h . addEventListener ( 'touchstart' , function ( e ) { startResize ( e , el ) ; } , { passive : false } ) ;
wrap . appendChild ( h ) ;
wrap . addEventListener ( 'mousedown' , function ( e ) { startDrag ( e , el ) ; } ) ;
wrap . addEventListener ( 'touchstart' , function ( e ) { startDrag ( e , el ) ; } , { passive : false } ) ;
wrap . addEventListener ( 'click' , function ( e ) { e . stopPropagation ( ) ; selectEl ( el . id ) ; } ) ;
if ( el . type === 'text' ) {
wrap . addEventListener ( 'dblclick' , function ( e ) { e . stopPropagation ( ) ; startEdit ( el , wrap . firstChild ) ; } ) ;
}
return wrap ;
}
function renderAll ( ) {
stage . innerHTML = '' ;
elements . forEach ( function ( el ) { stage . appendChild ( renderEl ( el ) ) ; } ) ;
if ( selectedId && nodeOf ( selectedId ) ) nodeOf ( selectedId ) . classList . add ( 'sel' ) ;
updateStageSize ( ) ;
renderPreview ( ) ;
}
/* 画布高度自适应:容纳所有元素底部 */
function updateStageSize ( ) {
var maxB = 480 ;
elements . forEach ( function ( el ) {
var n = nodeOf ( el . id ) ;
if ( ! n ) return ;
var b = ( el . y || 0 ) + n . offsetHeight ;
if ( b > maxB ) maxB = b ;
} ) ;
stage . style . minHeight = maxB + 'px' ;
}
function applyEl ( el ) {
var n = nodeOf ( el . id ) ;
if ( ! n ) return ;
n . style . width = ( el . w || 200 ) + 'px' ;
styleAlign ( el , n ) ;
var inner = n . firstChild ;
if ( el . type === 'image' ) {
inner . src = el . src || '' ;
} else if ( el . type === 'text' ) {
inner . textContent = el . text != null ? el . text : '' ;
inner . style . fontSize = ( el . fontSize || 18 ) + 'px' ;
inner . style . color = el . color || '#0f172a' ;
inner . style . fontWeight = el . bold ? '700' : '400' ;
inner . style . textAlign = el . align || 'left' ;
inner . style . writingMode = el . v ? 'vertical-rl' : '' ;
} else if ( el . type === 'button' ) {
inner . textContent = el . text || '按钮' ;
if ( el . bg ) inner . style . background = el . bg ;
if ( el . tc ) inner . style . color = el . tc ;
inner . style . fontSize = ( el . size === 'lg' ? '18px' : el . size === 'sm' ? '13px' : '15px' ) ;
} else if ( el . type === 'price' ) {
if ( el . color ) inner . style . color = el . color ;
}
updateStageSize ( ) ;
renderPreview ( ) ;
}
/* ---------- 拖拽移动 ---------- */
function startDrag ( e , el ) {
if ( e . target && e . target . classList . contains ( 'pb-handle' ) ) return ;
e . stopPropagation ( ) ;
selectEl ( el . id ) ;
var p = ptOf ( e ) ;
var sx = p . clientX , sy = p . clientY , ox = el . x || 0 , oy = el . y || 0 ;
onPointer ( e , {
move : function ( p2 ) {
el . x = Math . max ( 0 , ox + ( p2 . clientX - sx ) ) ;
el . y = Math . max ( 0 , oy + ( p2 . clientY - sy ) ) ;
var n = nodeOf ( el . id ) ;
if ( n ) { n . style . left = el . x + 'px' ; n . style . top = el . y + 'px' ; }
}
} ) ;
}
/* ---------- 缩放(右下角) ---------- */
function startResize ( e , el ) {
e . stopPropagation ( ) ; e . preventDefault ( ) ;
selectEl ( el . id ) ;
var p = ptOf ( e ) ;
var sx = p . clientX , sw = el . w || 200 ;
onPointer ( e , {
move : function ( p2 ) {
el . w = Math . max ( 40 , sw + ( p2 . clientX - sx ) ) ;
var n = nodeOf ( el . id ) ;
if ( n ) n . style . width = el . w + 'px' ;
}
} ) ;
}
/* ---------- 双击编辑文字 ---------- */
function startEdit ( el , node ) {
node . setAttribute ( 'contenteditable' , 'true' ) ;
node . focus ( ) ;
try { document . getSelection ( ) . selectAllChildren ( node ) ; } catch ( x ) { }
function done ( ) {
node . removeAttribute ( 'contenteditable' ) ;
el . text = node . textContent ;
node . removeEventListener ( 'blur' , done ) ;
if ( selectedId === el . id ) { syncProps ( ) ; renderPreview ( ) ; }
}
node . addEventListener ( 'blur' , done ) ;
}
/* ---------- 选中 / 属性面板 ---------- */
function selectEl ( id ) {
selectedId = id ;
Array . prototype . forEach . call ( stage . querySelectorAll ( '.pb-el' ) , function ( n ) {
n . classList . toggle ( 'sel' , n . getAttribute ( 'data-id' ) === id ) ;
} ) ;
renderProps ( findEl ( id ) ) ;
if ( id ) switchTab ( 'props' ) ;
}
function alignGroup ( el , types ) {
var html = '<label>对齐</label><div class="pb-align">' ;
html += '<button type="button" class="pb-ab" data-a="left">左</button>' ;
html += '<button type="button" class="pb-ab" data-a="center">中</button>' ;
html += '<button type="button" class="pb-ab" data-a="right">右</button>' ;
html += '<button type="button" class="pb-ab" data-a="v">竖</button>' ;
html += '</div>' ;
return html ;
}
function wireAlign ( el ) {
document . querySelectorAll ( '#pbPropsBody .pb-ab' ) . forEach ( function ( b ) {
var a = b . getAttribute ( 'data-a' ) ;
var on = ( a === 'v' ) ? ! ! el . v : ( ( el . align || 'left' ) === a ) ;
b . classList . toggle ( 'active' , on ) ;
b . onclick = function ( ) {
if ( a === 'v' ) el . v = ! el . v ; else el . align = a ;
wireAlign ( el ) ; applyEl ( el ) ; renderPreview ( ) ;
} ;
} ) ;
}
function renderProps ( el ) {
var body = document . getElementById ( 'pbPropsBody' ) ;
if ( ! el ) { body . className = 'pb-empty' ; body . textContent = '在画布中选择一个元素以编辑属性' ; return ; }
body . className = '' ;
if ( el . type === 'text' ) {
body . innerHTML =
'<label>文字内容</label><textarea id="pp_text" rows="3"></textarea>' +
'<label>字号 (px)</label><input type="number" id="pp_size" min="10" max="160">' +
'<label>颜色</label><input type="color" id="pp_color">' +
'<label style="display:flex;align-items:center;gap:6px"><input type="checkbox" id="pp_bold"> 加粗</label>' +
alignGroup ( el ) +
'<button class="pb-del" id="pp_del" type="button">删除该元素</button>' ;
document . getElementById ( 'pp_text' ) . value = el . text != null ? el . text : '' ;
document . getElementById ( 'pp_size' ) . value = el . fontSize || 18 ;
document . getElementById ( 'pp_color' ) . value = el . color || '#0f172a' ;
document . getElementById ( 'pp_bold' ) . checked = ! ! el . bold ;
bind ( 'pp_text' , 'input' , function ( v ) { el . text = v ; applyEl ( el ) ; } ) ;
bind ( 'pp_size' , 'input' , function ( v ) { el . fontSize = parseInt ( v , 10 ) || 18 ; applyEl ( el ) ; } ) ;
bind ( 'pp_color' , 'input' , function ( v ) { el . color = v ; applyEl ( el ) ; } ) ;
bind ( 'pp_bold' , 'change' , function ( v ) { el . bold = ! ! v ; applyEl ( el ) ; } ) ;
wireAlign ( el ) ;
} else if ( el . type === 'image' ) {
body . innerHTML =
'<label>图片宽度 (px)</label><input type="number" id="pp_w" min="40" max="3000">' +
alignGroup ( el ) +
'<label>替换图片(点击素材)</label><div class="pb-lib" id="pp_lib"></div>' +
'<button class="pb-del" id="pp_del" type="button">删除该元素</button>' ;
document . getElementById ( 'pp_w' ) . value = el . w || 200 ;
bind ( 'pp_w' , 'input' , function ( v ) { el . w = Math . max ( 40 , parseInt ( v , 10 ) || 200 ) ; applyEl ( el ) ; } ) ;
fillLib ( document . getElementById ( 'pp_lib' ) , function ( url ) { el . src = url ; applyEl ( el ) ; } ) ;
wireAlign ( el ) ;
} else if ( el . type === 'button' ) {
body . innerHTML =
'<label>按钮文字</label><input type="text" id="pp_text">' +
'<label>链接地址</label><input type="text" id="pp_href" placeholder="如 /contact 或 https://...">' +
'<label>背景色</label><input type="color" id="pp_bg">' +
'<label>文字颜色</label><input type="color" id="pp_tc">' +
'<label>尺寸</label><select id="pp_size"><option value="sm">小</option><option value="md">中</option><option value="lg">大</option></select>' +
'<button class="pb-del" id="pp_del" type="button">删除该元素</button>' ;
document . getElementById ( 'pp_text' ) . value = el . text || '按钮' ;
document . getElementById ( 'pp_href' ) . value = el . href || '' ;
document . getElementById ( 'pp_bg' ) . value = el . bg || '#0ea5e9' ;
document . getElementById ( 'pp_tc' ) . value = el . tc || '#ffffff' ;
document . getElementById ( 'pp_size' ) . value = el . size || 'md' ;
bind ( 'pp_text' , 'input' , function ( v ) { el . text = v ; applyEl ( el ) ; } ) ;
bind ( 'pp_href' , 'input' , function ( v ) { el . href = v ; } ) ;
bind ( 'pp_bg' , 'input' , function ( v ) { el . bg = v ; applyEl ( el ) ; } ) ;
bind ( 'pp_tc' , 'input' , function ( v ) { el . tc = v ; applyEl ( el ) ; } ) ;
bind ( 'pp_size' , 'change' , function ( v ) { el . size = v ; applyEl ( el ) ; } ) ;
} else if ( el . type === 'price' ) {
body . innerHTML =
'<p class="pb-empty" style="margin-bottom:10px">自动显示本产品「价格」,无需填写。</p>' +
'<label>价格文字颜色</label><input type="color" id="pp_color">' +
'<button class="pb-del" id="pp_del" type="button">删除该元素</button>' ;
document . getElementById ( 'pp_color' ) . value = el . color || '#0f172a' ;
bind ( 'pp_color' , 'input' , function ( v ) { el . color = v ; applyEl ( el ) ; } ) ;
} else if ( el . type === 'buy' ) {
body . innerHTML = '<p class="pb-empty">自动链接到本产品的「立即购买」下单页。</p><button class="pb-del" id="pp_del" type="button">删除该元素</button>' ;
} else if ( el . type === 'specs' ) {
body . innerHTML = '<p class="pb-empty">自动显示本产品的「规格参数」。</p><button class="pb-del" id="pp_del" type="button">删除该元素</button>' ;
}
var del = document . getElementById ( 'pp_del' ) ;
if ( del ) del . addEventListener ( 'click' , function ( ) { removeEl ( el . id ) ; } ) ;
}
function bind ( id , evt , fn ) {
var el = document . getElementById ( id ) ;
if ( ! el ) return ;
el . addEventListener ( evt , function ( ) { fn ( el . value !== undefined ? el . value : el . checked ) ; } ) ;
}
function syncProps ( ) { var el = findEl ( selectedId ) ; if ( el ) renderProps ( el ) ; }
function removeEl ( id ) {
elements = elements . filter ( function ( e ) { return e . id !== id ; } ) ;
if ( selectedId === id ) selectedId = null ;
renderAll ( ) ;
switchTab ( 'preview' ) ;
if ( ! selectedId ) renderProps ( null ) ;
}
/* ---------- 标签页切换 ---------- */
function switchTab ( name ) {
document . querySelectorAll ( '.pb-tab' ) . forEach ( function ( t ) {
t . classList . toggle ( 'active' , t . getAttribute ( 'data-tab' ) === name ) ;
} ) ;
var panes = { preview : 'panePreview' , props : 'paneProps' , material : 'paneMaterial' } ;
Object . keys ( panes ) . forEach ( function ( k ) {
document . getElementById ( panes [ k ] ) . classList . toggle ( 'hidden' , k !== name ) ;
} ) ;
if ( name === 'preview' ) fitPreview ( ) ;
}
document . querySelectorAll ( '.pb-tab' ) . forEach ( function ( t ) {
t . addEventListener ( 'click' , function ( ) { switchTab ( t . getAttribute ( 'data-tab' ) ) ; } ) ;
} ) ;
/* ---------- 实时预览(克隆只读 + 自适应缩放) ---------- */
function renderPreview ( ) {
var pv = document . getElementById ( 'pbPreview' ) ;
if ( ! pv ) return ;
pv . innerHTML = stage . innerHTML ;
pv . querySelectorAll ( '.pb-handle' ) . forEach ( function ( h ) { h . remove ( ) ; } ) ;
pv . querySelectorAll ( '.pb-el' ) . forEach ( function ( n ) { n . classList . remove ( 'sel' ) ; n . style . outline = '' ; } ) ;
pv . querySelectorAll ( '[contenteditable]' ) . forEach ( function ( n ) { n . removeAttribute ( 'contenteditable' ) ; } ) ;
fitPreview ( ) ;
}
function fitPreview ( ) {
var wrap = document . getElementById ( 'pbPreviewWrap' ) ;
var pv = document . getElementById ( 'pbPreview' ) ;
if ( ! wrap || ! pv ) return ;
var avail = wrap . clientWidth || STAGE _W ;
var scale = Math . min ( 1 , avail / STAGE _W ) ;
var m = 420 ;
pv . querySelectorAll ( '.pb-el' ) . forEach ( function ( n ) {
var y = parseInt ( n . style . top , 10 ) || 0 ;
var b = y + n . offsetHeight ;
if ( b > m ) m = b ;
} ) ;
pv . style . transform = 'scale(' + scale + ')' ;
pv . style . transformOrigin = 'top left' ;
pv . style . height = m + 'px' ;
wrap . style . height = ( m * scale ) + 'px' ;
}
/* ---------- 素材库(含删除) ---------- */
function fillLib ( container , onPick ) {
if ( ! container ) return ;
container . innerHTML = '<span style="font-size:12px;color:#94a3b8">加载中…</span>' ;
fetch ( window . _ _PB _MEDIA _ _ )
. then ( function ( r ) { return r . json ( ) ; } )
. then ( function ( j ) {
container . innerHTML = '' ;
var items = ( j && j . items ) || [ ] ;
if ( ! items . length ) { container . innerHTML = '<span style="font-size:12px;color:#94a3b8">暂无素材,先上传</span>' ; return ; }
items . forEach ( function ( it ) {
var cell = document . createElement ( 'div' ) ;
cell . className = 'pb-lib-item' ;
var img = document . createElement ( 'img' ) ;
img . src = it . url ; img . title = it . name ;
img . addEventListener ( 'click' , function ( ) { onPick ( it . url ) ; } ) ;
var del = document . createElement ( 'button' ) ;
del . className = 'pb-lib-del' ; del . type = 'button' ; del . textContent = '× ' ; del . title = '删除素材' ;
del . addEventListener ( 'click' , function ( e ) { e . stopPropagation ( ) ; deleteMedia ( it . name ) ; } ) ;
cell . appendChild ( img ) ; cell . appendChild ( del ) ;
container . appendChild ( cell ) ;
} ) ;
} )
. catch ( function ( ) { container . innerHTML = '<span style="font-size:12px;color:#dc2626">加载失败</span>' ; } ) ;
}
function deleteMedia ( name ) {
if ( ! confirm ( '确定删除素材「' + name + '」?此操作不可撤销。' ) ) return ;
fetch ( window . _ _PB _DELETE _ _ + encodeURIComponent ( name ) , {
method : 'POST' ,
headers : { 'X-CSRF-TOKEN' : window . _ _PB _CSRF _ _ }
} )
. then ( function ( r ) { return r . json ( ) ; } )
. then ( function ( j ) {
if ( j && j . ok ) {
loadLib ( ) ;
var pp = document . getElementById ( 'pp_lib' ) ;
if ( pp ) fillLib ( pp , function ( url ) { var el = findEl ( selectedId ) ; if ( el ) { el . src = url ; applyEl ( el ) ; } } ) ;
} else { alert ( ( j && j . msg ) || '删除失败' ) ; }
} )
. catch ( function ( ) { alert ( '删除失败,请重试' ) ; } ) ;
}
function loadLib ( ) { fillLib ( document . getElementById ( 'pbLib' ) , function ( url ) { addImage ( url ) ; } ) ; }
/* ---------- 添加元素 ---------- */
function addText ( ) {
var el = { id : uid ( ) , type : 'text' , x : 40 , y : 40 , w : 320 , text : '双击编辑文字' , fontSize : 20 , color : '#0f172a' , bold : false , align : 'left' , v : false , z : nextZ ( ) } ;
elements . push ( el ) ; renderAll ( ) ; selectEl ( el . id ) ;
}
function addImage ( url ) {
var el = { id : uid ( ) , type : 'image' , x : 40 , y : 40 , w : 280 , src : url , align : 'left' , v : false , z : nextZ ( ) } ;
elements . push ( el ) ; renderAll ( ) ; selectEl ( el . id ) ;
}
function addButton ( ) {
var el = { id : uid ( ) , type : 'button' , x : 40 , y : 40 , w : 200 , text : '按钮' , href : '#' , bg : '#0ea5e9' , tc : '#ffffff' , size : 'md' , z : nextZ ( ) } ;
elements . push ( el ) ; renderAll ( ) ; selectEl ( el . id ) ;
}
function addBuy ( ) {
var el = { id : uid ( ) , type : 'buy' , x : 40 , y : 40 , w : 200 , z : nextZ ( ) } ;
elements . push ( el ) ; renderAll ( ) ; selectEl ( el . id ) ;
}
function addPrice ( ) {
var el = { id : uid ( ) , type : 'price' , x : 40 , y : 40 , w : 240 , color : '#0f172a' , z : nextZ ( ) } ;
elements . push ( el ) ; renderAll ( ) ; selectEl ( el . id ) ;
}
function addSpecs ( ) {
var el = { id : uid ( ) , type : 'specs' , x : 40 , y : 40 , w : 360 , z : nextZ ( ) } ;
elements . push ( el ) ; renderAll ( ) ; selectEl ( el . id ) ;
}
/* 图标栏动作 */
document . querySelectorAll ( '.pb-rail-btn' ) . forEach ( function ( b ) {
b . addEventListener ( 'click' , function ( ) {
var act = b . getAttribute ( 'data-act' ) ;
if ( act === 'material' ) { switchTab ( 'material' ) ; loadLib ( ) ; }
else if ( act === 'image' ) { switchTab ( 'material' ) ; }
else if ( act === 'text' ) { addText ( ) ; }
else if ( act === 'button' ) { addButton ( ) ; }
else if ( act === 'buy' ) { addBuy ( ) ; }
else if ( act === 'price' ) { addPrice ( ) ; }
else if ( act === 'specs' ) { addSpecs ( ) ; }
} ) ;
} ) ;
stage . addEventListener ( 'click' , function ( ) { selectEl ( null ) ; } ) ;
/* ---------- 上传素材 ---------- */
on ( 'pbFile' , 'change' , function ( ) {
var file = this . files && this . files [ 0 ] ;
if ( ! file ) return ;
var fd = new FormData ( ) ;
fd . append ( 'file' , file ) ;
fetch ( window . _ _PB _UPLOAD _ _ , { method : 'POST' , body : fd , headers : { 'X-CSRF-TOKEN' : window . _ _PB _CSRF _ _ } } )
. then ( function ( r ) { return r . json ( ) ; } )
. then ( function ( j ) { if ( j && j . ok ) { addImage ( j . url ) ; loadLib ( ) ; } else alert ( ( j && j . msg ) || '上传失败' ) ; } )
. catch ( function ( ) { alert ( '上传失败,请重试' ) ; } ) ;
this . value = '' ;
} ) ;
/* ---------- 保存:序列化到隐藏字段 ---------- */
on ( 'pbForm' , 'submit' , function ( ) {
var f = document . getElementById ( 'pbLayout' ) ;
if ( f ) f . value = JSON . stringify ( elements ) ;
} ) ;
/* ---------- 启动 ---------- */
renderAll ( ) ;
loadLib ( ) ;
window . addEventListener ( 'resize' , fitPreview ) ;
} ) ( ) ;