164164 <div v-else-if =" rowType==='footer'" style =" margin-top :6px ;color :#909399 ;font-size :11px ;line-height :1.6 " >
165165 渲染 1 次(数据行下方)。绑定数据集后支持聚合:<code >${金额:SUM}</code >、<code >${字段:AVG}</code >、MIN / MAX / COUNT。
166166 </div >
167+
168+ <p style =" margin :10px 0 4px " >边框:</p >
169+ <div style =" display :flex ;gap :4px ;flex-wrap :wrap " >
170+ <el-button size =" small" :type =" bdState.top?'primary':''" @click =" setBorderSide('top')" >上</el-button >
171+ <el-button size =" small" :type =" bdState.bottom?'primary':''" @click =" setBorderSide('bottom')" >下</el-button >
172+ <el-button size =" small" :type =" bdState.left?'primary':''" @click =" setBorderSide('left')" >左</el-button >
173+ <el-button size =" small" :type =" bdState.right?'primary':''" @click =" setBorderSide('right')" >右</el-button >
174+ <el-button size =" small" @click =" setBorderAll(true)" >全部</el-button >
175+ <el-button size =" small" @click =" setBorderAll(false)" >无</el-button >
176+ <el-button size =" small" :type =" bdState.diagonal?'primary':''" @click =" setBorderDiagonal" >交叉</el-button >
177+ </div >
178+ <div style =" display :flex ;gap :6px ;margin-top :6px ;align-items :center " >
179+ <el-select v-model =" bdStyle" size =" small" style =" width :80px " @change =" applyBorderMeta" >
180+ <el-option value =" solid" label =" 实线" />
181+ <el-option value =" dashed" label =" 虚线" />
182+ <el-option value =" dotted" label =" 点线" />
183+ <el-option value =" double" label =" 双线" />
184+ </el-select >
185+ <el-select v-model =" bdWidth" size =" small" style =" width :66px " @change =" applyBorderMeta" >
186+ <el-option :value =" 1" label =" 1px" />
187+ <el-option :value =" 2" label =" 2px" />
188+ <el-option :value =" 3" label =" 3px" />
189+ </el-select >
190+ <el-color-picker v-model =" bdColor" size =" small" @change =" applyBorderMeta" />
191+ </div >
192+
193+ <p style =" margin :10px 0 4px " >单元格背景图:</p >
194+ <el-input v-model =" bgImage" size =" small" placeholder =" 图片 URL(可留空清除)" clearable @change =" applyBgImage" />
195+ <p style =" margin :10px 0 4px " >背景图填充:</p >
196+ <el-select v-model =" bgImageSize" size =" small" style =" width :100% " @change =" applyBgImage" >
197+ <el-option value =" cover" label =" 铺满(cover)" />
198+ <el-option value =" contain" label =" 完整(contain)" />
199+ <el-option value =" auto" label =" 原始大小(auto)" />
200+ </el-select >
167201 </div >
168202 <el-empty v-else description =" 点击单元格查看属性" />
169203 </div >
181215 <el-form-item label =" 页边距(px)" ><el-input v-model =" pageMargin.top" placeholder =" 上" style =" width :60px " /><span style =" margin :0 4px " >-</span ><el-input v-model =" pageMargin.right" placeholder =" 右" style =" width :60px " /><span style =" margin :0 4px " >-</span ><el-input v-model =" pageMargin.bottom" placeholder =" 下" style =" width :60px " /><span style =" margin :0 4px " >-</span ><el-input v-model =" pageMargin.left" placeholder =" 左" style =" width :60px " /></el-form-item >
182216 <el-form-item label =" 页眉" ><el-input v-model =" pageHeader" placeholder =" 如: " ; 销售报表 - ${date}" ; " /></el-form-item >
183217 <el-form-item label =" 页脚" ><el-input v-model =" pageFooter" placeholder =" 如: " ; 第 ${page} 页 / 共 ${total} 页" ; " /></el-form-item >
218+ <el-form-item label =" 背景图" ><el-input v-model =" pageBgImage" placeholder =" 文档背景图片 URL(可留空)" clearable /></el-form-item >
184219 </el-form >
185220 <template #footer ><el-button @click =" pageSettingsVisible=false" >确定</el-button ></template >
186221 </el-dialog >
@@ -393,6 +428,12 @@ const selectedCols = ref(new Set<number>())
393428const ctrlDown = ref (false )
394429const boldActive = ref (false ), italicActive = ref (false )
395430const selFontSize = ref (12 ), selFontName = ref (' Microsoft YaHei' ), selColor = ref (' ' ), selBgColor = ref (' ' )
431+ // 边框设置(分边/粗细/颜色/线型/交叉斜线)
432+ const bdState = reactive ({ top: true , bottom: true , left: true , right: true , diagonal: false })
433+ const bdStyle = ref (' solid' ), bdWidth = ref (1 ), bdColor = ref (' #cccccc' )
434+ // 单元格背景图 / 文档背景图
435+ const bgImage = ref (' ' ), bgImageSize = ref (' cover' )
436+ const pageBgImage = ref (' ' )
396437const cellValue = ref (' ' ), rowType = ref (' data' )
397438// 行类型与数据集绑定(完整行语义模型)
398439const rowTypes = ref <string []>([]) // 每行类型: title/header/data/footer
@@ -640,6 +681,7 @@ function loadLayoutJson() {
640681 pageSize .value = layout .page ?.size || ' A4' ; pageOrientation .value = layout .page ?.orientation || ' portrait'
641682 if (layout .page ?.margin ) Object .assign (pageMargin , layout .page .margin )
642683 pageHeader .value = layout .page ?.header || ' ' ; pageFooter .value = layout .page ?.footer || ' '
684+ pageBgImage .value = layout .page ?.bgImage || ' '
643685 cells .value = {}; if (layout .cells ) for (const c of layout .cells ) cells .value [` ${c .r },${c .c } ` ] = c
644686 previewParams .value = layout .params || []
645687 if (layout .datasets ) datasets .value = layout .datasets .map ((d : any )=> ({... d ,expanded:true ,loading:false }))
@@ -661,7 +703,20 @@ function getCellStyle(r:number,c:number) {
661703 if (s .color ) style += ` color:${s .color }; `
662704 if (s .bgColor ) style += ` background-color:${s .bgColor }; `
663705 if (s .align ) style += ` text-align:${s .align }; `
664- if (s .border === false ) style += ' border:none;'
706+ // 边框:false=无;对象=分边/粗细/颜色/线型/交叉斜线;缺省=默认细边框
707+ const b = s .border
708+ if (b === false ) style += ' border:none;'
709+ else if (b && typeof b === ' object' ) {
710+ const w= b .width || 1 , col= b .color || ' #333' , st= b .style || ' solid'
711+ style += ' border:none;'
712+ if (b .top ) style += ` border-top:${w }px ${st } ${col }; `
713+ if (b .bottom ) style += ` border-bottom:${w }px ${st } ${col }; `
714+ if (b .left ) style += ` border-left:${w }px ${st } ${col }; `
715+ if (b .right ) style += ` border-right:${w }px ${st } ${col }; `
716+ if (b .diagonal ) style += ` background-image:linear-gradient(to top right,transparent calc(50% - ${w * 0.5 }px),${col },transparent calc(50% + ${w * 0.5 }px)),linear-gradient(to bottom right,transparent calc(50% - ${w * 0.5 }px),${col },transparent calc(50% + ${w * 0.5 }px)); `
717+ }
718+ // 单元格背景图
719+ if (s .bgImage ) style += ` background-image:url('${s .bgImage }');background-size:${s .bgImageSize || ' cover' };background-position:center;background-repeat:no-repeat; `
665720 return style
666721}
667722
@@ -694,6 +749,7 @@ function onCellMouseDown(_e:MouseEvent, r:number, c:number) {
694749 italicActive .value = cell ?.style ?.italic || false
695750 selFontSize .value = cell ?.style ?.fontSize || 12
696751 selColor .value = cell ?.style ?.color || ' ' ; selBgColor .value = cell ?.style ?.bgColor || ' '
752+ loadBorderState (cell )
697753}
698754
699755function commitEdit() {
@@ -798,7 +854,57 @@ function onStyleChange(prop: 'color' | 'bgColor' | 'fontName' | 'fontSize') {
798854 applyStyle (prop , val )
799855}
800856
801- function toggleBorder() { pushHistory (); forEachSelected ((r ,c )=> {const key= ` ${r },${c } ` ,ex= cells .value [key ]|| {value:' ' },s= ex .style || {}; s .border = s .border === false ? true : false ; cells .value [key ]= {... ex ,style:s }}) }
857+ // ===== 边框设置(分边/粗细/颜色/线型/交叉斜线) =====
858+ function loadBorderState(cell : any ) {
859+ const b = cell ?.style ?.border
860+ if (b && typeof b === ' object' ) {
861+ bdState .top = !! b .top ; bdState .bottom = !! b .bottom ; bdState .left = !! b .left ; bdState .right = !! b .right ; bdState .diagonal = !! b .diagonal
862+ bdStyle .value = b .style || ' solid' ; bdWidth .value = b .width || 1 ; bdColor .value = b .color || ' #cccccc'
863+ } else {
864+ bdState .top = bdState .bottom = bdState .left = bdState .right = true ; bdState .diagonal = false
865+ bdStyle .value = ' solid' ; bdWidth .value = 1 ; bdColor .value = ' #cccccc'
866+ }
867+ bgImage .value = cell ?.style ?.bgImage || ' '
868+ bgImageSize .value = cell ?.style ?.bgImageSize || ' cover'
869+ }
870+
871+ function applyBorder() {
872+ pushHistory ()
873+ forEachSelected ((r : number , c : number ) => {
874+ const key= ` ${r },${c } ` , existing= cells .value [key ]|| {value:' ' }
875+ const style = {... (existing .style || {})}
876+ style .border = { top:bdState .top , bottom:bdState .bottom , left:bdState .left , right:bdState .right , diagonal:bdState .diagonal , width:bdWidth .value , style:bdStyle .value , color:bdColor .value }
877+ cells .value [key ] = {... existing , style }
878+ })
879+ }
880+ function setBorderSide(side : ' top' | ' bottom' | ' left' | ' right' ) { bdState [side ] = ! bdState [side ]; applyBorder () }
881+ function setBorderAll(on : boolean ) { bdState .top = bdState .bottom = bdState .left = bdState .right = on ; applyBorder () }
882+ function setBorderDiagonal() { bdState .diagonal = ! bdState .diagonal ; applyBorder () }
883+ function applyBorderMeta() { applyBorder () }
884+
885+ /** 单元格背景图 */
886+ function applyBgImage() {
887+ pushHistory ()
888+ forEachSelected ((r : number , c : number ) => {
889+ const key= ` ${r },${c } ` , existing= cells .value [key ]|| {value:' ' }
890+ const style = {... (existing .style || {})}
891+ if (bgImage .value ) { style .bgImage = bgImage .value ; style .bgImageSize = bgImageSize .value }
892+ else { delete style .bgImage ; delete style .bgImageSize }
893+ cells .value [key ] = {... existing , style }
894+ })
895+ }
896+
897+ function toggleBorder() {
898+ pushHistory ()
899+ forEachSelected ((r : number ,c : number ) => {
900+ const key= ` ${r },${c } ` , ex= cells .value [key ]|| {value:' ' }, s= {... (ex .style || {})}
901+ const b = s .border
902+ const hasAny = b === undefined || b === true || (typeof b === ' object' && (b .top || b .bottom || b .left || b .right ))
903+ s .border = hasAny ? false : { top:true ,bottom:true ,left:true ,right:true ,width:1 ,style:' solid' ,color:' #cccccc' }
904+ cells .value [key ] = {... ex , style:s }
905+ })
906+ loadBorderState (getCell (selR .value , selC .value ))
907+ }
802908
803909function mergeSelected() {
804910 if (selectedCells .value .length < 2 ) return
@@ -944,7 +1050,7 @@ function saveLayoutJson() {
9441050 const cellArr= Object .entries (cells .value ).map (([k ,v ]: any )=> ({r:Number (k .split (' ,' )[0 ]),c:Number (k .split (' ,' )[1 ]),... v }))
9451051 const dsArr = datasets .value .map (d => ({id:d .id ,name:d .name ,sourceType:d .sourceType ,sourceRef:d .sourceRef ,customSql:d .customSql ,dataSourceId:d .dataSourceId ,fields:d .fields }))
9461052 form .layoutJson = JSON .stringify ({
947- page:{size:pageSize .value ,orientation:pageOrientation .value ,margin:{... pageMargin },header:pageHeader .value ,footer:pageFooter .value },
1053+ page:{size:pageSize .value ,orientation:pageOrientation .value ,margin:{... pageMargin },header:pageHeader .value ,footer:pageFooter .value ,bgImage: pageBgImage . value },
9481054 params:previewParams .value , rows , cols , cells:cellArr ,
9491055 datasets: dsArr
9501056 })
0 commit comments