Browse Source

智能报告编辑;预览页兼容样式

Karsa 6 months ago
parent
commit
662bf17ed3

+ 1 - 0
package.json

@@ -38,6 +38,7 @@
     "vant": "^4.6.4",
     "vconsole": "^3.15.0",
     "vue": "^3.2.47",
+    "vue-qr": "^4.0.9",
     "vue-router": "^4.1.6",
     "vue3-clipboard": "^1.0.0",
     "vue3-tree-org": "^4.2.2",

+ 9 - 0
src/api/common.js

@@ -84,4 +84,13 @@ export function apiCheckClassify(params){
  */
 export function getSystemInfo(){
     return get('/system/sysuser/detail')
+}
+
+/**
+ * 上传图片
+ * @param {*} params 
+ * @returns 
+ */
+export function uploadImgAPi(params) {
+    return post('/banner/upload',params)
 }

+ 5 - 0
src/api/report.js

@@ -242,6 +242,11 @@ export default {
         return post("/smart_report/get_pdf_url",params)
     },
 
+     // 资源库列表
+    imgReourceList:params=>{
+        return get('/smart_report/resource/list',params)
+    },
+
         
     /* v2=============================== */
     /**

BIN
src/assets/imgs/report/icon_copy.png


BIN
src/assets/imgs/report/icon_wx_black.png


+ 30 - 0
src/assets/svg/add-comp.svg

@@ -0,0 +1,30 @@
+<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g filter="url(#filter0_ddd_2477_12714)">
+<rect x="16" y="13" width="96" height="96" rx="48" fill="#0052D9"/>
+<path d="M62.0499 62.9499V74.5H65.9499V62.9499H77.5V59.0499H65.9499V47.5H62.0499V59.0499H50.5V62.9499H62.0499Z" fill="white"/>
+</g>
+<defs>
+<filter id="filter0_ddd_2477_12714" x="0" y="0" width="128" height="128" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feMorphology radius="3" operator="erode" in="SourceAlpha" result="effect1_dropShadow_2477_12714"/>
+<feOffset dy="5"/>
+<feGaussianBlur stdDeviation="2.5"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2477_12714"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feMorphology radius="1" operator="dilate" in="SourceAlpha" result="effect2_dropShadow_2477_12714"/>
+<feOffset dy="8"/>
+<feGaussianBlur stdDeviation="5"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
+<feBlend mode="normal" in2="effect1_dropShadow_2477_12714" result="effect2_dropShadow_2477_12714"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feMorphology radius="2" operator="dilate" in="SourceAlpha" result="effect3_dropShadow_2477_12714"/>
+<feOffset dy="3"/>
+<feGaussianBlur stdDeviation="7"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
+<feBlend mode="normal" in2="effect2_dropShadow_2477_12714" result="effect3_dropShadow_2477_12714"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_2477_12714" result="shape"/>
+</filter>
+</defs>
+</svg>

+ 3 - 0
src/hooks/useAuthBtn.js

@@ -7,6 +7,9 @@ const authBtnStore = useAuthBtnStore()
 export const reportManageBtn = {
     reportManage_sendMsg:'reportManage:sendMsg',//推送消息/已推送消息
     reportManage_reportView:'reportManage:reportView',//研报预览:即是否能点击研报名称跳转预览页面
+    reportManage_reportView_wechartShare:'reportManage:reportView:wechartShare',//研报预览页面-微信分享
+    reportManage_reportView_copyWechat:'reportManage:reportView:copyWechat',//研报预览页面-复制链接
+    
     reportManage_reportDel:'reportManage:reportDel',//删除研报
     reportManage_reportEdit:'reportManage:reportEdit',//编辑研报
     reportManage_cancelPublish:'reportManage:cancelPublish',//取消发布

+ 4 - 4
src/hooks/useFroalaEditor.js

@@ -5,7 +5,7 @@ export function useInitFroalaEditor() {
 	let lastFocusPosition=ref(null)//最后焦点位置
 	let imgUploadFlag=ref(true)//图片是否上传完成
 
-	const options = {
+	let options = {
 		toolbarButtons: [
 			"insertImage",
 			"insertVideo",
@@ -31,7 +31,7 @@ export function useInitFroalaEditor() {
 			"undo",
 			"redo",
 		],
-		height: 400,
+		height: 500,
 		fontSize: ["6","8","10","12", "13", "14", "15", "16", "18", "20", "24", "28", "32", "36", "40"],
 		
 		fontSizeDefaultSelection: "16",
@@ -101,8 +101,8 @@ export function useInitFroalaEditor() {
 		// })
 
 		// 方案二
-		options.height=opts?.height??500
-		options.height=options.height<350?350:options.height
+		options={...options,...opts}
+		// options.height=options.height<350?350:options.height
 		console.log(options);
 		return new FroalaEditor(el, options);
 	};

+ 169 - 47
src/views/report/PreviewDetail.vue

@@ -1,11 +1,17 @@
 <script setup name="ReportPreview">
-import {ref} from 'vue'
+import { ref,computed, nextTick, reactive,toRefs } from 'vue'
 import { useRoute, useRouter } from "vue-router";
 import apiReport from '@/api/report'
 import AudioBox from './components/AudioBox.vue'
 import {showToast} from 'vant'
 import {reportManageBtn,useAuthBtn} from '@/hooks/useAuthBtn'
+import { copyText } from 'vue3-clipboard'
+import {usePublicSettingStore} from '@/store/modules/publicSetting'
+import vueQr from 'vue-qr/src/packages/vue-qr.vue'
+
+
 const {checkAuthBtn} = useAuthBtn()
+const publicSettingStore = usePublicSettingStore()
 
 const route=useRoute()
 const router=useRouter()
@@ -13,11 +19,34 @@ const router=useRouter()
 
 // 获取报告详情
 let reportInfo=ref(null)
+const smartState = reactive({
+    bgColor:'',
+    headImgStyle:null,//版头style
+    endImgStyle:null,//版尾style
+    layoutBaseInfo:{
+        研报标题:'',
+        研报作者:'',
+        创建时间:''
+    }
+})
 async function getReportDetail(){
     const res=await apiReport.getReportDetail({ReportId:Number(route.query.id)})
     if(res.Ret===200){
         reportInfo.value=res.Data
         document.title=res.Data.Title
+
+        smartState.bgColor=res.Data.CanvasColor
+        smartState.headImgStyle=reportInfo.value.HeadStyle?JSON.parse(reportInfo.value.HeadStyle):[]
+        smartState.headImgStyle.map(st =>{
+            st.value=st.value || st.label
+        })
+        smartState.endImgStyle=reportInfo.value.EndStyle?JSON.parse(reportInfo.value.EndStyle):[]
+        smartState.endImgStyle.map(st =>{
+            st.value=st.value || st.label
+        })
+        smartState.layoutBaseInfo['研报标题']=reportInfo.value.Title
+        smartState.layoutBaseInfo['研报作者']=reportInfo.value.Author
+        smartState.layoutBaseInfo['创建时间']=[2,6].includes(reportInfo.value.State)?reportInfo.value.PublishTime:''
     }
 }
 if(route.query.id==-1){
@@ -29,59 +58,69 @@ if(route.query.id==-1){
     getReportDetail()
 }
 
+const { bgColor,headImgStyle,endImgStyle,layoutBaseInfo } = toRefs(smartState)
 
-//去编辑
-async function goEdit(){
-    // 先mark一下
-    const markRes=await apiReport.reportMark({
-        Status:1,
-        ReportId:reportInfo.value.Id
-    })
-    if(markRes.Ret===200){
-        if(markRes.Data.Status===1){
-            showToast(markRes.Data.Msg || '该研报正在编辑,不可重复编辑')
-            return
-        }
-    }else{
-        showToast(markRes.ErrMsg || '未知错误,请稍后重试')
-        return
-    }
 
-    if(reportInfo.value.CollaborateType===2){
-        router.push({
-            path:"/report/chapter/list",
-            query:{
-                id:reportInfo.value.Id
-            }
-        })
-    }else{
-        // 研报
-        router.push({
-            path:reportInfo.value.ReportLayout===1 ? '/report/edit' : "/smart_report/edit",
-            query:{
-                id:reportInfo.value.Id,
-                coopType: reportInfo.value.CollaborateType
-            }
-        })
+const showImgPop = ref(false)
+const linkUrl = computed(() =>{
+    console.log(publicSettingStore)
+    let str=''
+    const baseUrl= publicSettingStore.publicSetting.ReportViewUrl;
+    if(reportInfo.value.ReportCode){
+        // 设置水印文案
+        let waterMarkStr= '';
+
+        str= reportInfo.value.ReportLayout===1 
+            ? `${baseUrl}/reportshare_crm_report?code=${reportInfo.value.ReportCode}&flag=${waterMarkStr}& ${reportInfo.value.Title}`
+            : `${baseUrl}/reportshare_smart_report?code=${reportInfo.value.ReportCode}& ${reportInfo.value.Title}`
     }
-}
+    
+    return str
+})
+function handleCopyLink() {
+    copyText(linkUrl.value,undefined,(error,event)=>{
+        if(error){
+            showToast('复制链接成功')
 
+            throw new Error('复制数据失败'+JSON.stringify(error))
+        }else{
+            showToast('复制链接成功')
+        }
+    })
+}
 </script>
 
 <template>
-    <div class="report-detail-page" v-if="reportInfo">
+    <div class="report-detail-page" v-if="reportInfo" :style="{backgroundColor:bgColor}">
         <div class="top-stage-box" v-if="$route.query.id!=-1">
             <span class="stage">第{{reportInfo.Stage}}期 / {{reportInfo.Frequency}}</span>
-            <img v-if="reportInfo.State==1&&checkAuthBtn(reportManageBtn.reportManage_reportEdit)" class="edit-icon" src="@/assets/imgs/report/icon_edit2.png" alt="" @click="goEdit">
+            <!-- <img v-if="reportInfo.State==1&&checkAuthBtn(reportManageBtn.reportManage_reportEdit)" class="edit-icon" src="@/assets/imgs/report/icon_edit2.png" alt="" @click="goEdit"> -->
         </div>
-        <h1 class="report-title">{{reportInfo.Title}}</h1>
-        <div class="auth-box">
-            <span>{{reportInfo.Author}}</span>
-            <span>{{reportInfo.PublishTime}}</span>
+
+        <!-- 版头 -->
+        <div class="html-head-img-box" v-if="reportInfo && reportInfo.HeadImg">
+            <img :src="reportInfo.HeadImg" alt="" style="display:block;width:100%">
+            <div class="head-layout-item" v-for="item in headImgStyle" :key="item.value"
+            :style="{fontFamily:item.family,fontSize:(item.size*2)+'px',fontWeight:item.weight,textAlign:item.align,color:item.color,
+                width:item.width,height:item.height,left:item.left,top:item.top
+            }">
+                {{ layoutBaseInfo[item.value] }}
+            </div>
         </div>
-        <!-- 晨报/周报 -->
-        <template v-if="['day','week'].includes(reportInfo.ChapterType)">
-            <div class="report-abstract" v-if="reportInfo.Abstract">摘要:{{reportInfo.Abstract}}</div>
+
+        <template v-if="reportInfo&&(!reportInfo.HeadImg) && (!reportInfo.EndImg)">
+            <h1 class="report-title">{{reportInfo.Title}}</h1>
+            <div class="auth-box">
+                <span>{{reportInfo.Author}}</span>
+                <span>{{reportInfo.PublishTime}}</span>
+            </div>
+        </template>
+        <!-- 音频 -->
+        <AudioBox :url="reportInfo.VideoUrl" v-if="reportInfo.VideoUrl"/>
+        <div class="report-abstract" v-if="reportInfo.Abstract">摘要:{{reportInfo.Abstract}}</div>
+        
+        <!-- 章节 -->
+        <template v-if="reportInfo.CollaborateType===2">
             <ul class="chapter-list-wrap">
                 <li class="chapter-item-box" v-for="item in reportInfo.ChapterList" :key="item.ReportChapterId">
                     <div class="type-box">
@@ -96,17 +135,47 @@ async function goEdit(){
         </template>
         <!-- 研报 -->
         <template v-else>
-            <!-- 音频 -->
-            <AudioBox :url="reportInfo.VideoUrl" v-if="reportInfo.VideoUrl"/>
-            <div class="report-abstract" v-if="reportInfo.Abstract">摘要:{{reportInfo.Abstract}}</div>
             <div class="report-html-wrap" v-html="reportInfo.Content"></div>
         </template>
+
+
+          <!-- 板尾 -->
+        <div class="html-end-img-box" v-if="reportInfo && reportInfo.EndImg">
+            <img :src="reportInfo.EndImg" alt="" style="display:block;width:100%">
+            <div class="head-layout-item" v-for="item in endImgStyle" :key="item.value"
+            :style="{fontFamily:item.family,fontSize:(item.size*2)+'px',fontWeight:item.weight,textAlign:item.align,color:item.color,
+                width:item.width,height:item.height,left:item.left,top:item.top
+            }">
+                {{ layoutBaseInfo[item.value] }}
+            </div>
+        </div> 
+
+        <!-- 浮动操作 -->
+        <div class="fix-bot-action-box">
+            <div class="item" @click="handleCopyLink" v-permission="reportManageBtn.reportManage_reportView_copyWechat">
+                <img class="icon" src="@/assets/imgs/report/icon_copy.png" alt="">
+                <div>复制链接</div>
+            </div>
+            <div class="item" @click="showImgPop=true" v-permission="reportManageBtn.reportManage_reportView_wechartShare">
+                <img class="icon" src="@/assets/imgs/report/icon_wx_black.png" alt="">
+                <div>微信分享</div>
+            </div>
+        </div>
     </div>
+
+
+    <van-popup 
+        v-model:show="showImgPop" 
+        round
+    >
+        <vue-qr :text="linkUrl" colorDark="#333" colorLight="#fff" :dotScale="1"></vue-qr>
+    </van-popup>
 </template>
 
 <style lang="scss" scoped>
 .report-detail-page{
     padding: 30px 34px;
+    margin-bottom: 112px;
     .report-title{
         margin: 30px 0;
         font-weight: 600;
@@ -150,6 +219,20 @@ async function goEdit(){
             }
         }
     }
+
+    .report-drag-item-wrap{
+        padding: 6px;
+        margin-bottom: 3px;
+    }
+    .html-head-img-box,.html-end-img-box{
+        position: relative;
+        margin: 20px 0;
+        .head-layout-item{
+            position: absolute;
+            overflow: hidden;
+            box-sizing: border-box
+        }
+    }
 }
 .top-stage-box{
     .stage{
@@ -168,10 +251,38 @@ async function goEdit(){
     }
 }
 
+
+.fix-bot-action-box{
+    position: fixed;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    z-index: 99;
+    background-color: #fff;
+    border-top: 1px solid $border-color;
+    height: 112px;
+    display: flex;
+    align-items: center;
+    .item{
+        height: 100%;
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        font-size: 20px;
+        .icon{
+            width: 40px;
+            height: 40px;
+            margin-bottom: 5px;
+        }
+    }
+}
+
 @media screen and (min-width:$media-width){
     .report-detail-page{
         max-width: 800px;
-        margin: 0 auto;
+        margin: 0 auto 110px;
         padding: 30px;
         .report-title{
             margin: 15px 0;
@@ -216,5 +327,16 @@ async function goEdit(){
             height: 35px;
         }
     }
+
+    .fix-bot-action-box{ 
+        height: 110px;
+        .item {
+            font-size: 18px;
+            .icon{
+                width: 40px;
+                height: 40px;
+            }
+        }
+    }
 }
 </style>

+ 225 - 111
src/views/report/chapter/List.vue

@@ -2,13 +2,15 @@
 import {computed, nextTick, reactive, ref} from 'vue'
 import { useRoute, useRouter } from "vue-router";
 import apiReport from '@/api/report'
-import {apiGetWXQRCodeImg} from '@/api/common'
+import { apiGetWXQRCodeImg,getSystemInfo } from '@/api/common'
 import moment from 'moment';
 import {Base64} from 'js-base64'
 import { showToast,showDialog } from 'vant';
 import { useWindowSize } from '@vueuse/core'
 import {useCachedViewsStore} from '@/store/modules/cachedViews'
+import {usePublicSettingStore} from '@/store/modules/publicSetting'
 import EditBaseInfo from './components/EidtBaseInfo.vue'
+import ReportPublishTimeSet from '../components/ReportPublishTimeSet.vue'
 import html2canvas from "html2canvas";
 import {transfImgTobase64} from '@/hooks/common'
 import draggable from 'vuedraggable'
@@ -16,8 +18,10 @@ import {reportManageBtn,useAuthBtn} from '@/hooks/useAuthBtn'
 import {useReportApprove} from '@/hooks/useReportApprove'
 import AddReportBaseInfoV2 from '../components/AddReportBaseInfoV2.vue'
 import EditChapterBaseInfo from './components/EditChapterBaseInfo.vue'
+import { useReportHandles } from '../hooks/useReport'
 
 const cachedViewsStore=useCachedViewsStore()
+const publicSettingStore = usePublicSettingStore()
 const { width, height } = useWindowSize()
 
 const route=useRoute()
@@ -124,6 +128,25 @@ async function getChapterList(){
 }
 
 
+// 水印
+const waterMarkStr=ref('')
+function getSystemInfoFun(){
+    getSystemInfo().then(res=>{
+        if(res.Ret===200){
+          const systemUserInfo=res.Data
+          // 设置水印文案
+          let waterMarkString=''
+          if(systemUserInfo){
+            waterMarkString=`${systemUserInfo.RealName}${systemUserInfo.Mobile?systemUserInfo.Mobile:systemUserInfo.Email}`
+            waterMarkString=encodeURIComponent(waterMarkString)
+            waterMarkStr.value=Base64.encode(waterMarkString)
+          }
+        }
+    })
+}
+getSystemInfoFun()
+
+
 /* 章节操作弹窗 */
 const showItemOpt = ref(false)
 const activeItem = ref(null)
@@ -146,6 +169,161 @@ function handleChapterBaseInfoSave() {
 }
 
 
+const { handleSubmitReport } = useReportHandles()
+/* 发布报告 */
+async function  handlePublishReportCheck(tp) {
+    //校验章节是否都已发布
+    let res = await apiReport.checkChaterPublishState({
+    ReportId: Number(route.query.id)
+    })
+    if(res.Data&&res.Data.length>0){
+        let str = res.Data.map(_ =>_.Title).join(',')
+        return showToast(`${str}内容未提交`)
+    } 
+
+    handlePublishReport(tp)
+}
+
+//发布 定时 提交
+async function handlePublishReport(tp) {
+    cachedViewsStore.removeCaches('ReportList')
+
+    if(tp==='dsfb'){
+        showDSFBTime.value=true
+		return
+    }else if(tp==='submit'){
+        handleSubmitReport({id:Number(route.query.id)})
+        return
+    }
+
+    let sendMsg = reportInfo.value.MsgIsSend;
+    if(sendMsg===1){
+        reportPublish({sendMsg: false})
+    }else {
+        const isPost = checkAuthBtn(reportManageBtn.reportManage_sendMsg)
+
+        showDialog({
+            title: '发布提示',
+            showCancelButton: true,
+            message: isPost?'发布后,是否推送模板消息?':'是否立即发布报告?',
+            confirmButtonText: isPost?'推送':'发布',
+            cancelButtonText: isPost?'不推送':'取消',      
+        }).then(() => {
+            reportPublish({sendMsg: isPost?true:false})
+        })
+        .catch(() => {
+            if(isPost) reportPublish({sendMsg: false});
+        });
+
+    }
+}
+
+async function reportPublish({sendMsg}){
+    const res=await apiReport.reportPublish({ReportIds:String(route.query.id),ReportUrl:generatePdfLinks()})
+    if(res.Ret===200){
+        sendMsg && apiReport.reportMessageSend({ReportId: Number(route.query.id)})
+
+        showToast('发布成功')
+        console.log('back');
+        setTimeout(() => {
+            router.back()
+        },1000)
+    }
+}
+function generatePdfLinks(){
+    let code = reportInfo.value.ReportCode
+
+    return `${publicSettingStore.publicSetting.ReportViewUrl}/reportshare_pdf?code=${code}&flag=${waterMarkStr.value}`
+}
+
+
+// 定时发布报告选择时间
+const showDSFBTime=ref(false)
+function onConfirmDSFBTime(time){
+
+    if(reportInfo.value.MsgIsSend===1){//已经推送过了
+        apiReport.reportPublishTimeSet({
+            ReportId:reportInfo.value.Id,
+            PrePublishTime:time,
+            PreMsgSend:0,
+            ReportUrl:generatePdfLinks()
+        }).then(res=>{
+            if(res.Ret===200){
+                showToast('定时发布成功!')
+                setTimeout(() => {
+                    router.back()
+                }, 1000);
+            }
+        })
+        return
+    }
+    const isAuthPushMsg=checkAuthBtn(reportManageBtn.reportManage_sendMsg)
+    showDialog({
+        title: '提示',
+        message:isAuthPushMsg?'是否发布定时报告,并推送模板消息?':'是否发布定时报告',
+        confirmButtonText:isAuthPushMsg?'推送':'确定',
+        cancelButtonText:isAuthPushMsg?'不推送':'取消',
+        showCancelButton:true
+    }).then(()=>{
+        if(!isAuthPushMsg){
+            apiReport.reportPublishTimeSet({
+                ReportId:reportInfo.value.Id,
+                PrePublishTime:time,
+                PreMsgSend:0,
+                ReportUrl:generatePdfLinks()
+            }).then(res=>{
+                if(res.Ret===200){
+                    showToast('定时发布成功!')
+                    setTimeout(() => {
+                        router.back()
+                    }, 1000);
+                }
+            })
+            return
+        }
+        //推送
+        apiReport.reportPublishTimeSet({
+            ReportId:reportInfo.value.Id,
+            PrePublishTime:time,
+            PreMsgSend:1,
+            ReportUrl:generatePdfLinks()
+        }).then(res=>{
+            if(res.Ret===200){
+                showToast('定时发布成功!')
+                setTimeout(() => {
+                    router.back()
+                }, 1000);
+            }
+        })
+    }).catch(()=>{
+        if(!isAuthPushMsg) return
+        //不推送
+        apiReport.reportPublishTimeSet({
+            ReportId:reportInfo.value.Id,
+            PrePublishTime:time,
+            PreMsgSend:0,
+            ReportUrl:generatePdfLinks()
+        }).then(res=>{
+            if(res.Ret===200){
+                showToast('定时发布成功!')
+                setTimeout(() => {
+                    router.back()
+                }, 1000);
+            }
+        })
+    })
+}
+
+/* 预览报告 */
+function handlePreviewReport() {
+    router.push({
+        path:"/report/preview",
+        query:{
+            id:reportInfo.value.Id
+        }
+    })
+}
+
  /* 删除章节 */
 function handleDelChapter(item) {
     showItemOpt.value = false
@@ -173,7 +351,8 @@ function handleUploadChapterAudio(item) {
     showItemOpt.value = false
 }
 
-async function  handleMoveChapter({oldIndex,newIndex}) {
+//章节拖动
+async function handleMoveChapter({oldIndex,newIndex}) {
     console.log(oldIndex,newIndex)
     console.log(chapterList.value)
     
@@ -239,114 +418,50 @@ function handleConfirmEditTrendTag(){
 
 
 // 跳转章节详情
-function goChapterDetail(item){
-    router.push({
-        path:'/report/chapter/detail',
-        query:{
-            id:item.ReportChapterId
-        }
-    })
-}
+async function goChapterDetail(item){
 
-
-// 保存报告信息
-async function handleSaveReportInfo(){
-    if(!reportInfo.value.Title){
-        showToast('请填写报告标题')
-        return
-    }
-    const params={
-        ReportId:reportInfo.value.Id,
-        Title:reportInfo.value.Title,
-        ReportType:reportInfo.value.ClassifyNameFirst=='周报'?'week':'day',
-        CreateTime:moment(reportInfo.value.CreateTime).format('YYYY-MM-DD'),
-        Author:reportInfo.value.Author
-    }
-    const res=await apiReport.editDayWeekReport(params)
-    if(res.Ret===200){
-        showToast('保存成功')
-    }
-}
-
-// 周报校验音频是否都上传了
-async function handlePublishValid(){
-    if(reportInfo.value.ClassifyNameFirst==='周报'){
-        const validRes=await apiReport.weekReportValidAudio({ReportId:Number(reportInfo.value.Id)})
-        if(validRes.Ret===200){
-            if(validRes.Data){
-                showDialog({
-                    title: '发布提示',
-                    message: `报告未上传录音:${validRes.Data.join('、')},确定发布吗?`,
-                    showCancelButton:true
-                }).then(() => {
-                    // on close
-                    handlePublishReport()
-                }).catch(()=>{})
-            }
+    if(item.IsAuth) {
+        //编辑前标记一下
+        const res = await apiReport.reportMark({
+            Status: 1,
+            ReportId: Number(reportInfo.value.Id),
+            ReportChapterId: item.ReportChapterId
+        });
+        
+        if (res.Ret === 200) {
+        if (res.Data.Status === 1) {
+            showToast(res.Data.Msg || '该研报正在编辑,不可重复编辑')
+            item.CanEdit = false;
+            item.Editor = res.Data.Editor || "";
+            return;
+        } else if (res.Data.Status === 0) {
+            item.CanEdit = true;
+            item.Editor = res.Data.Editor || "";
         }
-        return
-    }else{
-        handlePublishReport()
-    }
-}
-
-// 发布报告 晨报提示是否推送客群
-async function handlePublishReport(){
-    const res=await apiReport.publishDayOrWeekReport({ReportId:Number(reportInfo.value.Id)})
-    if(res.Ret!=200)return
-    // 清除列表缓存
-    cachedViewsStore.removeCaches('ReportList')
-
-    if(reportInfo.value.ClassifyNameFirst==='周报'){
-        showToast('发布成功')
-        router.back()
-        return
-    }
-    // 晨报
-    if(res.Data){
-        showDialog({
-            title: '发布提示',
-            message: res.Data,
-        }).then(()=>{
-            showDialog({
-                title: '发布提示',
-                message: `发布后,是否推送模板消息和客户群?`,
-                showCancelButton:true,
-                confirmButtonText:'推送',
-                cancelButtonText:'不推送'
-            }).then(async() => {
-                // on close
-                const pushRes=await apiReport.reportMessageSend({ReportId:Number(reportInfo.value.Id)})
-                if(pushRes.Ret===200){
-                    showToast('推送成功')
-                    router.back()
-                }
-            }).catch(()=>{
-                showToast('发布成功')
-                router.back()
-            })
+        } else {
+            showToast(res.ErrMsg || "未知错误,请稍后重试");
+            return;
+        }
+        
+        router.push({
+            path:reportInfo.value.ReportLayout===1 ? '/report/edit' : "/smart_report/edit",
+            query:{
+                id:reportInfo.value.Id,
+                coopType: reportInfo.value.CollaborateType
+            }
         })
-    }else{
-        showDialog({
-            title: '发布提示',
-            message: `发布后,是否推送模板消息和客户群?`,
-            showCancelButton:true,
-            confirmButtonText:'推送',
-            cancelButtonText:'不推送'
-        }).then(async() => {
-            // on close
-            const pushRes=await apiReport.reportMessageSend({ReportId:Number(reportInfo.value.Id)})
-            if(pushRes.Ret===200){
-                showToast('推送成功')
-                router.back()
+    }else {
+        router.push({
+            path:'/report/chapter/detail',
+            query:{
+                id:item.ReportChapterId
             }
-        }).catch(()=>{
-            showToast('发布成功')
-            router.back()
         })
     }
+
 }
 
+
 // 显示海报
 const showChapterItemPoster=ref(false)
 const chapterItemPosterInfo=ref(null)
@@ -399,7 +514,6 @@ async function handleShowPoster(item){
                 @end="handleMoveChapter"
                 tag="ul"
             >
-                    <!-- v-for="item in chapterList" :key="item.ReportChapterId" -->
                 <template #item="{element}">
                     <li class="item"  @click="goChapterDetail(element)">
                         <div class="item-top">
@@ -450,22 +564,22 @@ async function handleShowPoster(item){
                     <img src="@/assets/imgs/report/icon_info.png" alt="">
                     <span>基础信息</span>
                 </div>
-                <div class="item" @click="handleReportOpt('yl')" v-permission="reportManageBtn.reportManage_reportView">
+                <div class="item" @click="handlePreviewReport" v-permission="reportManageBtn.reportManage_reportView">
                     <img src="@/assets/imgs/report/icon_preview.png" alt="">
                     <span>预览</span>
                 </div>
                 <template v-if="!isApprove||!hasApproveFlow">
-                    <div class="item" @click="handleReportOpt('dsfb')" v-permission="reportManageBtn.reportManage_publish">
+                    <div class="item" @click="handlePublishReportCheck('dsfb')" v-permission="reportManageBtn.reportManage_publish">
                         <img src="@/assets/imgs/report/icon_time.png" alt="">
                         <span>定时发布</span>
                     </div>
-                    <div class="item" @click="handleReportOpt('fb')" v-permission="reportManageBtn.reportManage_publish">
+                    <div class="item" @click="handlePublishReportCheck('fb')" v-permission="reportManageBtn.reportManage_publish">
                         <img src="@/assets/imgs/report/icon_publish3.png" alt="">
                         <span>发布</span>
                     </div>
                 </template>
                 <template v-if="isApprove&&hasApproveFlow">
-                    <div class="item" @click="handleReportOpt('submit')" v-permission="reportManageBtn.reportManage_publish">
+                    <div class="item" @click="handlePublishReportCheck('submit')" v-permission="reportManageBtn.reportManage_publish">
                         <img src="@/assets/imgs/report/icon_publish3.png" alt="">
                         <span>提交</span>
                     </div>
@@ -579,6 +693,9 @@ async function handleShowPoster(item){
         </div>
     </van-action-sheet>
 
+    <!-- 定时发布选择时间 -->
+    <ReportPublishTimeSet v-model="showDSFBTime" :prePublishTime="reportInfo?.PrePublishTime" @confirm="onConfirmDSFBTime" />
+
     <!-- 海报dom模块 -->
         <div v-if="chapterItemPosterInfo" class="select-text-disabled chapter-poster-box" id="chapter-poster-box">
             <img class="bg" :src="chapterItemPosterInfo.TypeEditImg" alt="">
@@ -834,13 +951,10 @@ async function handleShowPoster(item){
                     right: 60px;
                 }
 
-                .icon{
+                .bottom .icon{
                     width: 35px;
                     height: 35px;
                 }
-                .icon-wx{
-                    right: 54px;
-                }
             }
         }
     }

+ 10 - 0
src/views/report/chapter/Preview.vue

@@ -0,0 +1,10 @@
+<script setup>
+import { ref } from 'vue'
+
+</script>
+<template>
+  <div></div>
+</template>
+<style scoped lang="scss">
+
+</style>

+ 2 - 1
src/views/report/components/reportInsert/Index.vue

@@ -19,7 +19,8 @@ function handleSelectChart(data){
 
 function handleConfirmInsert(){
     if(['图表插入','批量插入'].includes(activeType.value)){
-        let filterList = activeType.value === '批量插入' ? list.value.filter(_ => _.HaveOperaAuth) : list.value;
+        console.log(list.value)
+        let filterList = list.value;
 
         emits('insert',{list: filterList,type:'iframe',chartType:'chart'})
     }else if(activeType.value==='表格插入'){

+ 0 - 1
src/views/report/hooks/useReport.js

@@ -81,7 +81,6 @@ export function useReportHandles() {
 
   },1000)
 
-
   /* 提交报告 */
   function handleSubmitReport({id}) {
     showDialog({

+ 554 - 114
src/views/report/smartReport/EditReport.vue

@@ -1,8 +1,10 @@
 <script setup>
-import { nextTick, reactive, ref,toRefs } from 'vue'
+import { nextTick, reactive, ref,toRefs, watch,onMounted,onUnmounted } from 'vue'
+import { V3ColorPicker } from "v3-color-picker-teleport"
 import ReportInsertContent from '../components/reportInsert/Index.vue'
 // import ReportPublishTimeSet from './components/ReportPublishTimeSet.vue'
 import apiReport from '@/api/report'
+import _ from 'lodash'
 import {getSystemInfo} from '@/api/common'
 import moment from 'moment'
 import { showToast,showDialog } from 'vant'
@@ -15,10 +17,13 @@ import {Base64} from 'js-base64'
 import AddReportBaseInfoV2 from '../components/AddReportBaseInfoV2.vue'
 import { useReportHandles } from '../hooks/useReport'
 import draggable from 'vuedraggable'
+import TextEditor from './components/TextEditor.vue'
+import ImgEditor from  './components/ImgEditor.vue'
 import TextComp from './components/TextComp.vue'
 import ChartComp from './components/ChartComp.vue'
 import ImgComp from './components/ImgComp.vue'
 import SheetComp from './components/SheetComp.vue'
+import ReportLayoutImg from './components/ReportLayoutImg.vue'
 
 
 const cachedViewsStore=useCachedViewsStore()
@@ -30,6 +35,7 @@ const {checkAuthBtn} = useAuthBtn()
 
 // 获取报告详情
 const reportInfo=ref(null)
+const reportCoopType = ref(Number(route.query.coopType))
 const contentChange = ref(false)//内容是否发生变化
 const smartState = reactive({
   conList: [],//内容列表
@@ -113,6 +119,18 @@ async function getReportDetail(){
 }
 getReportDetail()
 
+
+watch(
+	() => smartState.conList,
+	() => {
+		contentChange.value = true;
+	},
+	{
+		deep: true
+	}
+)
+
+
 /* map类型对应组件 */
 function getComponentName(item){
   const temMap=new Map([
@@ -154,7 +172,7 @@ function handleResizeP(e,index){
 		}
 		document.onmouseup=(el)=>{
 				console.log(targetBox.style.cssText);
-				conList.value[index].style = targetBox.style.cssText
+				smartState.conList[index].style = targetBox.style.cssText
 				el.preventDefault()
 				document.onmousemove=null
 				setTimeout(() => {
@@ -165,7 +183,7 @@ function handleResizeP(e,index){
 }
 // 内部元素的缩放
 function handleResizeC(e,index,cindex,type){
-		this.isDragResize=true
+		isDragResize.value=true
 		e.preventDefault()
 		const parentBox=e.target.parentNode.parentNode
 		const parentBoxWidth=parentBox.offsetWidth
@@ -209,34 +227,185 @@ function handleResizeC(e,index,cindex,type){
 						})
 						// 存储修改的值
 						parentBox.childNodes.forEach((item,idx)=>{
-								this.$set(this.conList[index].child[idx],'style',item.style.cssText)
+								smartState.conList[index].child[idx].style = item.style.cssText
 						})
 				}
 				el.preventDefault()
 				document.onmousemove=null
 				setTimeout(() => {
-						this.isDragResize=false
+						isDragResize.value=false
 						document.onmouseup=null
 				}, 50);
 		}
 }
+//新增内部元素
+function handleChildAdd(e,parent,parentIndex){
+		console.log('child-onAdd操作------------------->');
+
+		const {item,newDraggableIndex}=e
+
+		const compData=JSON.parse(item.getAttribute('comp-data'))
+
+		console.log(compData,newDraggableIndex);
+
+		const index=parentIndex
+		// if(index>-1){
+				let obj=_.cloneDeep(smartState.conList[index]) 
+
+				console.log(obj);
+
+				if(obj.child&&obj.child.length===1&&obj.id){
+						if(compData){
+								obj={
+										child:[
+												{
+														compId:obj.compId,
+														compType:obj.compType,
+														id:obj.id,
+														content:obj.content,
+														style:obj.compType==='chart'?'height:350px':'',
+														child:[]
+												},
+												{
+														compId:compData.compId,
+														compType:compData.compType,
+														content:compData.content||'',
+														id:getCompId(compData.compType),
+														style:compData.compType==='chart'?'height:350px':'',
+														child:[]
+												}
+										]
+								}
+						}else{//是内容区域拖动排序的
+								const temItem=_.cloneDeep(obj.child[0])
+								if(temItem.child.length>0){//如果拖动的盒子里面有子元素则不能进入
+										obj={
+												...obj,
+												child:[]
+										}
+										setTimeout(() => {
+												smartState.conList.splice(index,0,temItem)
+										}, 50);
+								}else{
+										obj={
+												child:[
+														{
+																compId:obj.compId,
+																compType:obj.compType,
+																id:obj.id,
+																content:obj.content,
+																style:obj.compType==='chart'?'height:350px':'',
+																child:[]
+														},
+														{
+																...temItem
+														}
+												]
+										}
+								}
+						}
+						
+				}else{
+						if(compData){//如果是从内容区域拖入的没有compData
+								obj.child.splice(newDraggableIndex,1,{
+										compId:compData.compId,
+										compType:compData.compType,
+										content:compData.content||'',
+										id:getCompId(compData.compType),
+										style:compData.compType==='chart'?'height:350px':'',
+										child:[]
+								})
+						}
+				}
+				console.log(obj);
+
+				smartState.conList.splice(index,1,obj)
+		// }
+}
+//移除内部元素事件 
+function handleChildRemove(e,arr){
+		console.log('child-remove操作------------------->');
 
-// 点击删除某个
+		// 如果当前移出的这个child还有两个的话则重置他们的宽度
+		arr.forEach(item=>{
+				if(item.style){
+						const styleArr=item.style.split(';').filter(s=>s&&(s.indexOf('width')===-1&&s.indexOf('flex')===-1)).join(';')
+						item.style=styleArr
+				}
+		})
+
+
+		// 如果child只剩一个了则移出来
+		smartState.conList=smartState.conList.map(_item=>{
+				if(_item.child&&_item.child.length===1){
+						const obj=_item.child[0]
+						if(obj.style){
+								const styleArr=obj.style.split(';').filter(s=>s&&(s.indexOf('width')===-1&&s.indexOf('flex')===-1)).join(';')
+								obj.style=styleArr
+						}
+						return obj
+				}else{
+						return _item
+				}
+		})
+}
+
+
+
+// 点击删除某个元素
 function handleDelItem(pindex,cindex){
 		if(cindex===-1){
-				conList.value.splice(pindex,1)
+				smartState.conList.splice(pindex,1)
 		}else{//删除子盒子
-				conList.value[pindex].child.splice(cindex,1)
-				if(conList.value[pindex].child.length===1){//只剩一个子盒子了则变成一个大盒子
-						const styleArr=conList.value[pindex].child[0].style.split(';').filter(s=>s&&(s.indexOf('width')===-1&&s.indexOf('flex')===-1)).join(';')
-						conList.value[pindex]=conList.value[pindex].child[0]
-						conList.value[pindex].style=styleArr
+				smartState.conList[pindex].child.splice(cindex,1)
+				if(smartState.conList[pindex].child.length===1){//只剩一个子盒子了则变成一个大盒子
+						const styleArr=smartState.conList[pindex].child[0].style.split(';').filter(s=>s&&(s.indexOf('width')===-1&&s.indexOf('flex')===-1)).join(';')
+						smartState.conList[pindex]=smartState.conList[pindex].child[0]
+						smartState.conList[pindex].style=styleArr
 				}
 		}
 }
 
+// 删除版头版尾
+function deleteLayoutPic(type){
+		if(type===1){
+				smartState.headImg=''
+				smartState.headImgId = 0
+		}else{
+				smartState.endImg=''
+				smartState.endImgId = 0
+		}
+		contentChange.value=true
+}
+
+
+
+const currentState = reactive({
+	activeId: 0,
+	activeContent: '',
+	activePindex: '',
+	activeCindex: ''
+})
+// 当前再编辑哪个
+function handleChoose(item,index,cindex){
+		//{item:数据,index:父序号,cindex:子序号}
+		if(!item.id||isDragResize.value||!['text','img'].includes(item.compType)) return
+		currentState.activeId=item.id
+		currentState.activeContent=item.content||''
+		currentState.activePindex=index
+		currentState.activeCindex=cindex>=0?cindex:''
+
+		showInsertCompType.value = item.compId;
+		showReportInsertPop.value = true
+		if(item.compId === 1) {//文字组件
+			temTextVal.value = item.content
+		}else if(item.compId === 2) { //图片组件
+			temImgVal.value = item.content
+		}
+}
 
-const showPopover = ref(true)
+
+const showPopover = ref(false)
 const showReportInsertPop = ref(false)
 const showInsertCompType=ref(0)
 const  compType = [
@@ -264,11 +433,13 @@ function handleOpenComPop(e) {
   console.log(e)
 
   showInsertCompType.value = e.compId;
+	temTextVal.value = ''
+	temImgVal.value = ''
   showReportInsertPop.value = true
 }
 /* 插入图表表格 */
 function handleChartInsert({list,type,chartType}){
-
+		console.log(list,type,chartType)
     let tempCompDataArr=[]
     
     if(type==='iframe'){
@@ -304,26 +475,122 @@ function handleChartInsert({list,type,chartType}){
 						compId:2,
             compType:'img',
             id:getCompId(2),
-            content:``,
-            titleText: item.ExcelName,
+            content:item,
+            titleText: '',
             style:'',
             child:[]
 				}))
     }
 
-    conList.value.splice(0,1,...tempCompDataArr)
-		console.log(conList.value)
+    smartState.conList.splice(0,0,...tempCompDataArr)
     showReportInsertPop.value=false
 }
 //插入文本
-function handleInsertText() {
+const temTextVal = ref('')
+function handleInsertText(content) {
+	if(temTextVal.value) { //编辑
+		currentState.activeContent=content
+		if(currentState.activeCindex>=0&&currentState.activeCindex!==''){
+				smartState.conList[currentState.activePindex].child[currentState.activeCindex].content=content
+		}else{
+				smartState.conList[currentState.activePindex].content = content
+		}
+	}else { //新增
+
+		let tempCompData = {
+				compId:1,
+				compType:'text',
+				id:getCompId(1),
+				content:content,
+				style:'',
+				child:[]
+		}
+		smartState.conList.push(tempCompData)
+	}
+
+	console.log(smartState.conList)
+	showReportInsertPop.value = false
+}
+//插入图片
+const temImgVal=ref('')
+function handleInsertImg(e) {
+	if(temImgVal.value) { //编辑
+		currentState.activeContent=e
+		if(currentState.activeCindex>=0&&currentState.activeCindex!==''){
+				smartState.conList[currentState.activePindex].child[currentState.activeCindex].content=e
+		}else{
+				smartState.conList[currentState.activePindex].content = e
+		}
+	}else { //新增
+		let tempCompData = {
+				compId:2,
+				compType:'img',
+				id:getCompId(2),
+				content:e,
+				style:'',
+				child:[]
+		}
+		smartState.conList.push(tempCompData)
+	}
 
+		showReportInsertPop.value = false
 }
 function getCompId(type) {
 	return type+new Date().getTime()
 }
 
+// 更新sheet表格高度
+function reInitSheetIframe(e){
+    const { height,code } = e.data;
+    let iframeDom = document.getElementsByClassName(`iframe${code}`)
+    Array.prototype.forEach.call(iframeDom, function (ele) {
+        ele.height = `${height}px`;
+    });
+}
+onMounted(()=>{
+    window.addEventListener('message',reInitSheetIframe)
+})
+onUnmounted(()=>{
+    window.removeEventListener('message',reInitSheetIframe)
+})
+
 
+
+//更多设置弹窗
+const showMoreHandlePop = ref(false)
+const showLayoutPop = ref(false)
+const showBgPop = ref(false)
+const temBgVar=ref("")
+//设置版图
+function handleConfirmSetLayout(e) {
+		if(e.type=='1'){
+				showToast('版头设置成功')
+				smartState.headImg=e.data.ImgUrl
+				smartState.headImgId = e.data.ResourceId
+				smartState.headImgStyle=e.data.Style?JSON.parse(e.data.Style):[]
+		}else{
+				showToast('版尾设置成功')
+				smartState.endImg=e.data.ImgUrl
+				smartState.endImgId = e.data.ResourceId
+				smartState.endImgStyle=e.data.Style?JSON.parse(e.data.Style):[]
+		}
+		contentChange.value=true
+		showLayoutPop.value = false
+}
+function handleOpenBgPop() {
+	temBgVar.value=smartState.bgColor||''
+	showMoreHandlePop.value=false 
+	showBgPop.value=true;
+}
+//设置背景色
+function handleConfirmBgColor() {
+		smartState.bgColor=temBgVar.value||''
+  	contentChange.value=true
+		showBgPop.value = false
+}
+
+/* 报告流程操作 */
+const { handleRefresh,handleSubmitReport } = useReportHandles()
 // 报告基本信息
 const showReportBaseInfo=ref(false)
 let reportBaseInfoData={
@@ -351,6 +618,67 @@ async function handleReportBaseInfoChange(e){
     showReportBaseInfo.value=false
 }
 
+// 刷新所有图表
+async function handleRefreshAllChart(){
+  handleRefresh({ id: reportInfo.value.Id,chapterId:reportInfo.value.ChapterId })
+}
+
+//预览
+async function handlePreviewReport() {
+		if(document.getElementById('report-html-content')) { 
+				// 存一次草稿
+				const saveRes=await autoSaveReportContent("auto")
+				if(!saveRes) return
+		}
+	  const routerEl=router.resolve({
+        path:'/report/preview',
+        query:{
+            id: reportInfo.value.Id
+        }
+    })
+    window.open(routerEl.href,'_blank')
+    return
+}
+
+/* 保存 定时保存 */
+function autoSaveReportContent(type="auto") {
+	  if(!route.query.id) return
+
+		const html=document.getElementById('report-html-content').outerHTML.replace(/contenteditable="true"/g,'contenteditable="false"');
+    return new Promise(async (resolve,reject)=>{
+
+				let imgParams = {
+						HeadImg: reportCoopType.value===1?smartState.headImg:'',
+						EndImg:reportCoopType.value===1?smartState.endImg:'',
+						HeadResourceId:reportCoopType.value===1?smartState.headImgId:'',
+						EndResourceId:reportCoopType.value===1?smartState.endImgId:'',
+						CanvasColor:reportCoopType.value===1?smartState.bgColor:''
+				}
+
+        const res=await apiReport.reportContentSave({
+            ReportId:Number(route.query.id),
+            Content:html,
+						ContentStruct:JSON.stringify(smartState.conList),
+            NoChange:contentChange.value?0:1,
+						...imgParams
+        })
+        if(res.Ret === 200) {
+            resolve(true)
+            type==='save' && showToast("保存成功");
+            contentChange.value=false
+        }
+    })
+}
+
+
+async function handlePublishReport(tp) {
+	 	if(document.getElementById('report-html-content')) { 
+				// 存一次草稿
+				const saveRes=await autoSaveReportContent("auto")
+				if(!saveRes) return
+		}
+}
+
 
 const { 
   conList,
@@ -369,10 +697,34 @@ const {
     <div class="main-wrap">
 
       <div class="report-content-box" id="report-content-box" :style="{backgroundColor:bgColor}">
+
+					<div class="add-comp-wrapper">
+						<van-popover v-model:show="showPopover" @select="handleOpenComPop">
+							<van-grid
+								square
+								clickable
+								:border="false"
+								column-num="3"
+								style="width: 240px;"
+							>
+								<van-grid-item
+									v-for="i in compType"
+									:key="i.id"
+									:text="i.text"
+									:icon="i.icon"
+									@click="handleOpenComPop(i)"
+								/>
+							</van-grid>
+							<template #reference>
+									<svg-icon name="add-comp" size="40"/>
+							</template>
+						</van-popover>
+					</div>
+
           <!-- 版头 -->
           <div class="html-head-img-box">
-              <div class="opt-btn-box">
-                  <div class="del-btn" @click.stop="deleteLayoutPic(1)"></div>
+              <div class="opt-btn-box"  style="display: none;">
+									<van-icon name="delete-o" @click.stop="deleteLayoutPic(1)" color="#f00"/>
               </div>
               <img :src="headImg" alt="" style="display:block;width:100%">
               <div class="head-layout-item" v-for="item in headImgStyle" :key="item.value"
@@ -384,21 +736,18 @@ const {
           </div>
           <draggable
               :list="conList"
+							item-key="id"
               :group="{ name: 'component', pull: true, put: true }"
               class="report-html-wrap"
               id="report-html-content"
               animation="300"
               tag="div"
-              handle=".drag-btn_p"
-              @add="handleParentAdd"
-              @remove="handleParentRemove"
-              :move="handleParentMove"
           >
             <template #item="{element,index}">
               <div 
                   :class="[
                       'report-drag-item-wrap',
-                      activeId===element.id?'blue-bg':'',
+                      currentState.activeId===element.id?'blue-bg':'',
                       element.child&&!element.child.length?'report-drag-item-out':''
                   ]"
                   :comp-type="element.compType"
@@ -406,10 +755,8 @@ const {
                   :style="element.style"
               >
                   <div class="resize-drag-box" @mousedown.stop="handleResizeP($event,index)"></div>
-                  <div class="opt-btn-box">
-                      <div class="drag-btn drag-btn_p"></div>
-                      <!-- <div class="del-btn" @click.stop="handleDelItem(index,-1)"></div> -->
-											<van-icon name="delete-o" @click.stop="handleDelItem(index,-1)"/>
+                  <div class="opt-btn-box"  style="display: none;">
+											<van-icon name="delete-o" @click.stop="handleDelItem(index,-1)" color="#f00"/>
                   </div>
                   <div 
                       v-if="element.child&&!element.child.length"
@@ -419,14 +766,47 @@ const {
                   >
                       <component :is="getComponentName(element)" :compData="element"/>
                   </div>
+
+									<draggable
+											:list="element.child"
+											item-key="id"
+											:group="{ name: 'component', pull: true, put: element.child&&element.child.length<3?true:false }"
+											animation="300"
+											tag="div"
+											class="report-drag-item-wrap_child-wrap"
+											@add="handleChildAdd($event,element,index)"
+											@remove="handleChildRemove($event,element.child)"
+											style="display: flex;gap: 3px;align-items: flex-start;"
+									>		
+										<template #item="child">
+											<div 
+													:class="['report-drag-item-wrap_child_content',currentState.activeId===child.element.id?'blue-bg':'']" 
+													:comp-type="child.element.compType"
+													:data-id="child.element.id"
+													@click.stop="handleChoose(child.element,index,child.index)"
+													style="flex:1"
+													:style="child.element.style"
+											>
+													<div class="opt-btn-box2" style="display: none;">
+															<van-icon name="delete-o" @click.stop="handleDelItem(index,child.index)" color="#f00"/>
+													</div>
+													<!-- 拖动按钮 -->
+													<div class="resize-drag-box_lb" @mousedown.stop="handleResizeC($event,index,child.index,'lb')"></div>
+													<div class="resize-drag-box_rb" @mousedown.stop="handleResizeC($event,index,child.index,'rb')"></div>
+													<component :is="getComponentName(child.element)" :compData="child.element"/>
+													<!--  -->
+													<div class="mark-box" v-if="isDragResize" style="position: absolute;left:0;right:0;top:0;bottom: 0;z-index: 10;"></div>
+											</div>
+										 </template>
+									</draggable>
               </div>
             </template>
           </draggable>
           
           <!-- 版尾 -->
           <div class="html-end-img-box">
-              <div class="opt-btn-box">
-                  <div class="del-btn" @click.stop="deleteLayoutPic(2)"></div>
+              <div class="opt-btn-box"  style="display: none;">
+									<van-icon name="delete-o" @click.stop="deleteLayoutPic(2)" color="#f00"/>
               </div>
               <img :src="endImg" alt="" style="display:block;width:100%">
               <div class="head-layout-item" v-for="item in endImgStyle" :key="item.value"
@@ -436,30 +816,6 @@ const {
                   {{ layoutBaseInfo[item.value] }}
               </div>
           </div>
-
-        
-        <div class="add-comp-wrapper">
-          <van-popover v-model:show="showPopover" @select="handleOpenComPop">
-            <van-grid
-              square
-              clickable
-              :border="false"
-              column-num="3"
-              style="width: 240px;"
-            >
-              <van-grid-item
-                v-for="i in compType"
-                :key="i.id"
-                :text="i.text"
-                :icon="i.icon"
-                @click="handleOpenComPop(i)"
-              />
-            </van-grid>
-            <template #reference>
-                <van-icon name="add-o" size="24" class="add-ico"/>
-            </template>
-          </van-popover>
-        </div>
       </div>
     </div>
 
@@ -474,20 +830,20 @@ const {
                 <img src="@/assets/imgs/report/icon_refresh.png" alt="">
                 <span>刷新</span>
             </div>
-            <div class="item" @click="handlePreviewReport" >
+            <div class="item" @click="handlePreviewReport" v-permission="reportManageBtn.reportManage_reportView">
                 <img src="@/assets/imgs/report/icon_preview.png" alt="">
                 <span>预览</span>
             </div>
-            <div class="item" @click="autoSaveReportContent('save')">
+            <div class="item" @click="autoSaveReportContent('save')" v-permission="reportManageBtn.reportManage_publish">
                 <img src="@/assets/imgs/report/icon_save2.png" alt="">
                 <span>保存</span>
             </div>
             <template v-if="!isApprove||!hasApproveFlow">
-                <div class="item" @click="handlePublishReport('dsfb')" >
+                <div class="item" @click="handlePublishReport('dsfb')" v-permission="reportManageBtn.reportManage_publish">
                     <img src="@/assets/imgs/report/icon_time.png" alt="">
                     <span>定时发布</span>
                 </div>
-                <div class="item" @click="handlePublishReport('fb')">
+                <div class="item" @click="handlePublishReport('fb')" v-permission="reportManageBtn.reportManage_publish">
                     <img src="@/assets/imgs/report/icon_publish3.png" alt="">
                     <span>发布</span>
                 </div>
@@ -498,22 +854,38 @@ const {
                     <span>提交</span>
                 </div>
             </template>
-            <div class="item" @click="handlePreviewReport" >
-                <img src="@/assets/imgs/report/icon_preview.png" alt="">
-                <span>更多设置</span>
+            <div class="item" @click="showMoreHandlePop=true" v-if="reportInfo&&!reportInfo.ReportChapterId">
+								<van-icon name="ellipsis" size="24"/>
+                <div>更多设置</div>
             </div>
         </div>
     </div>
   </div>
 
   <!-- 报告插入数据模块 -->
-  <van-popup
-      v-model:show="showReportInsertPop"
-      position="bottom"
-      round
-  >
-      <ReportInsertContent v-if="showInsertCompType===3" @insert="handleChartInsert"/>
-  </van-popup>
+    <van-popup
+        v-model:show="showReportInsertPop"
+        position="bottom"
+        round
+    >
+				<!-- 文字插入 -->
+				<TextEditor 
+					v-if="showInsertCompType===1"
+					:defaultVal="temTextVal"
+					@close="showReportInsertPop=false;temTextVal=''"
+					@confirm="handleInsertText"
+				/>
+
+        <!-- 图片 -->
+				<ImgEditor
+					v-else-if="showInsertCompType===2"
+					:defaultVal="temImgVal"
+					@close="showReportInsertPop=false;temImgVal=''"
+					@confirm="handleInsertImg"
+				/>
+        <!-- 图表资源 -->
+        <ReportInsertContent v-if="showInsertCompType===3" @insert="handleChartInsert"/>
+    </van-popup>
 
   <!-- 报告基础信息 -->
   <van-popup
@@ -527,6 +899,49 @@ const {
           :defaultData="reportBaseInfoData"
           @confirm="handleReportBaseInfoChange"
       />
+  </van-popup>
+
+	<!-- 更多操作 背景色 版图 -->
+	<van-action-sheet 
+			teleport="body"
+			v-model:show="showMoreHandlePop"
+			cancel-text="取消"
+			close-on-click-action
+	>
+			<div class="report-item-action-box">
+					
+					<div class="item" @click="showLayoutPop=true;showMoreHandlePop=false">版图选择</div>
+					<div class="item" @click="handleOpenBgPop">背景色设置</div>
+			</div>
+	</van-action-sheet>
+
+	<!-- 版图设置 -->
+  <van-popup
+      v-model:show="showLayoutPop"
+      position="bottom"
+  >
+      <ReportLayoutImg 
+				v-if="showLayoutPop" 
+				@close="showLayoutPop=false" 
+				@confirm="handleConfirmSetLayout"
+			/>
+  </van-popup>
+
+	<!-- 背景色设置 -->
+  <van-popup
+      v-model:show="showBgPop"
+      position="bottom"
+			:style="{ height: '50%' }"
+  >
+		<div class="top-box">
+        <span style="color:#666666" @click="showBgPop=false">取消</span>
+        <span style="font-size:18px;font-weight:bold">背景色设置</span>
+        <span style="color:#0052D9" @click="handleConfirmBgColor">确定</span>
+    </div>
+		<div class="color-box">
+				<span>颜色选择</span>
+				<V3ColorPicker v-model:value="temBgVar" :zIndex="9999"/>
+		</div>
   </van-popup>
 </template>
 <style scoped lang="scss">
@@ -544,7 +959,6 @@ const {
 		overflow-y: auto;
     margin-top: 30px;
     border: 1px solid #DCDFE6;
-		pointer-events:auto;
 
 		.report-drag-item-wrap{
 				width: 100%;
@@ -567,24 +981,6 @@ const {
 						left: -36px;
 						padding-right: 8px;
 						top: 0;
-						.drag-btn::after{
-								content: '';
-								display: block;
-								width: 28px;
-								height: 28px;
-								background-image: url('~@/assets/img/smartReport/icon12.png');
-								background-size: cover;
-								cursor: pointer;
-						}
-						.del-btn::after{
-								content: '';
-								display: block;
-								width: 28px;
-								height: 28px;
-								background-image: url('~@/assets/img/smartReport/icon13.png');
-								background-size: cover;
-								cursor: pointer;
-						}
 				}
 				.resize-drag-box{
 						position: absolute;
@@ -606,9 +1002,7 @@ const {
 						height: 100%;
 				}
 		}
-		.report-drag-item-wrap_child-wrap{
-				// min-height: 30px;
-		}
+
 		.report-drag-item-wrap_child_content{
 				min-height: 80px;
 				border: 1px dashed #0052D9;
@@ -631,24 +1025,6 @@ const {
 						right: -15px;
 						top: 0;
 						z-index: 10;
-						.drag-btn::after{
-								content: '';
-								display: block;
-								width: 28px;
-								height: 28px;
-								background-image: url('~@/assets/img/smartReport/icon12.png');
-								background-size: cover;
-								cursor: pointer;
-						}
-						.del-btn::after{
-								content: '';
-								display: block;
-								width: 28px;
-								height: 28px;
-								background-image: url('~@/assets/img/smartReport/icon13.png');
-								background-size: cover;
-								cursor: pointer;
-						}
 				}
 				.resize-drag-box_lt,
 				.resize-drag-box_lb,
@@ -688,8 +1064,26 @@ const {
 .report-content-box{
     width: 100%;
     margin: 0 auto;
-    height: 100%;
 		padding: 20px 20px 20px 44px;
+		.html-head-img-box,.html-end-img-box{
+				position: relative;
+				&:hover{
+						.opt-btn-box{
+								display: block !important;
+						}
+				}
+				.opt-btn-box{
+						position: absolute;
+						left: -36px;
+						padding-right: 8px;
+						top: 0;
+				}
+				.head-layout-item{
+						position: absolute;
+						overflow: hidden;
+						box-sizing: border-box
+				}
+		}
 }
 .add-comp-wrapper {
   display: flex;
@@ -744,6 +1138,35 @@ const {
         }
     }
 }
+.report-item-action-box{
+    .item{
+			 	padding: 20px;
+        text-align: center;
+        font-size: 32px;
+        border-top: 1px solid $border-color;
+    }
+}
+
+.top-box{
+    padding:32px;
+    display: flex;
+    justify-content: space-between;
+    border-bottom: 1px solid #DCDFE6;
+    .close{
+        color:#666666;
+    }
+    .title{
+        font-size: 36px;
+    }
+    .add-btn{
+        color:$theme-color;
+    }
+}
+.color-box {
+	padding: 20px;
+	display: flex;
+	align-items: center;
+}
 @media screen and (min-width:$media-width){
     .add-report-page{
         height: calc(100dvh - 60px);
@@ -779,10 +1202,27 @@ const {
     }
     .main-wrap{ 
       margin-top: 20px;
+			max-width: 800px;
+			.report-content-box {
+				padding: 20px 20px 20px 44px;
+			}
+			.report-drag-item-wrap {
+				.opt-btn-box{
+					left: -30px;
+				}
+			}
+
+			.report-drag-item-wrap_child_content{ 
+				.opt-btn-box2{
+					right: -10px;
+				}
+			}
     }
-    .report-content-box{ 
-      max-width: 800px;
-    }
+		.report-item-action-box{
+			.item{
+				font-size: 16px;
+			}
+		}	
 }
 
 </style>

+ 87 - 0
src/views/report/smartReport/components/ImgEditor.vue

@@ -0,0 +1,87 @@
+<script setup>
+import { ref, watch } from 'vue'
+import { uploadImgAPi } from '@/api/common'
+import { showToast } from 'vant'
+
+const props = defineProps({
+  defaultVal: ''
+})
+const emit = defineEmits(['close','confirm'])
+
+watch(() => props.defaultVal,
+  (newval) => {
+    fileList.value = newval ? [{url:newval}] : []
+  }
+)
+
+const fileList = ref(props.defaultVal?[{url: props.defaultVal}]:[])
+async function uploadAfter(e){
+    // console.log(e);
+   
+    let form = new FormData();
+        form.append('file',e.file);  //hostfile.name
+
+    const res = await uploadImgAPi(form)
+    if( res.Ret !== 200 ) return
+    fileList.value = [{url:res.Data.ResourceUrl}]
+}
+
+
+
+function handleCancle() {
+  fileList.value = []
+  emit('close')
+}
+
+function handleConfirm() {
+  if(!fileList.value[0]) return showToast('请上传图片')
+  emit('confirm',fileList.value[0].url)
+}
+</script>
+<template>
+  <div>
+  <div class="top-box">
+      <span style="color:#666666" @click="handleCancle">取消</span>
+      <span style="font-size:18px;font-weight:bold">插入图片</span>
+      <span style="color:#0052D9" @click="handleConfirm">确定</span>
+  </div>
+  <div class="img-box">
+    <van-uploader 
+      v-model="fileList"
+      accept="image/*"
+      :after-read="uploadAfter"
+      :max-count="1"
+    />
+  </div>
+</div>
+</template>
+<style scoped lang="scss">
+.top-box{
+    padding:32px;
+    display: flex;
+    justify-content: space-between;
+    border-bottom: 1px solid #DCDFE6;
+    .close{
+        color:#666666;
+    }
+    .title{
+        font-size: 36px;
+    }
+    .add-btn{
+        color:$theme-color;
+    }
+    
+}
+.img-box {
+  height: 50vh;
+  padding: 10px;
+}
+@media screen and (min-width:$media-width){
+  .top-box{
+      padding:16px;
+      .title{
+          font-size: 18px;
+      }
+  }
+}
+</style>

+ 171 - 0
src/views/report/smartReport/components/ReportLayoutImg.vue

@@ -0,0 +1,171 @@
+<script setup>
+import { reactive, ref } from 'vue'
+import apiReport from '@/api/report'
+import { vInfiniteScroll } from '@vueuse/components'
+
+
+
+const props = defineProps({
+  defaultVal: ''
+})
+const emit = defineEmits(['close','confirm'])
+
+const imgTypeOpts = [
+  { text:'版头',value: 1 },
+  { text:'版尾',value: 2 },
+]
+
+const list = ref([])
+const filterState = reactive({
+    type: 1,
+    page:1,
+    pageSize:20,
+    finished:false,
+    searchVal: ''
+})            
+async function getList() {
+  const res=await apiReport.imgReourceList({
+      CurrentIndex:filterState.page,
+      PageSize:filterState.pageSize,
+      Type:filterState.type,
+      Keyword:filterState.searchVal
+  })
+  if(res.Ret===200){
+      const arr = res.Data.List || [];
+      list.value =
+          filterState.page === 1
+          ? arr
+          : [...this.list.value, ...arr];
+      filterState.finished =  res.Data.Paging.IsEnd;
+  }
+}
+getList()
+
+function initList() {
+  filterState.page = 1;
+  getList()
+}
+
+function onLoadMore() {
+  if(filterState.finished) return
+
+  filterState.page++
+  getList()
+}
+
+
+const selectItem = ref(null)
+function handleSelect(e) {
+  selectItem.value = e
+}
+
+function handleCancle() {
+  emit('close')
+}
+function handleConfirm() {
+
+  emit('confirm',{
+    type:filterState.type,
+    data:selectItem.value
+  })
+}
+
+</script>
+<template>
+  <div class="img-source-cont">
+    <div class="top-box">
+        <span style="color:#666666" @click="handleCancle">取消</span>
+        <van-dropdown-menu>
+          <van-dropdown-item v-model="filterState.type" :options="imgTypeOpts" @change="initList"/>
+        </van-dropdown-menu>
+        <span style="color:#0052D9" @click="handleConfirm">确定</span>
+    </div>
+
+    <div class="container">
+      <div class="top">
+
+        <van-search v-model="filterState.searchVal" placeholder="请输入搜索关键词" @search="initList"/>
+
+      </div>
+      <ul class="img-list" v-infinite-scroll="[onLoadMore, { 'distance' : 10 }]" v-if="list.length">
+        <li 
+            :class="['item',selectItem&&selectItem.ResourceId===item.ResourceId?'active':'']" 
+            v-for="item in list" 
+            :key="item.ResourceId"
+            @click="handleSelect(item)"
+        >
+            
+            <img :src="item.ImgUrl" alt="">
+            <div class="title">{{item.ImgName}}</div>
+            <svg v-if="selectItem&&selectItem.ResourceId===item.ResourceId" width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M14.5 28C22.232 28 28.5 21.732 28.5 14C28.5 6.26801 22.232 0 14.5 0C6.76801 0 0.5 6.26801 0.5 14C0.5 21.732 6.76801 28 14.5 28ZM7.5 14.413L8.913 13L12.5 16.586L20.085 9L21.5 10.415L12.5 19.414L7.5 14.413Z" fill="#0052D9"/>
+            </svg>
+        </li>
+      </ul>
+
+      <div v-if="!list.length&&filterState.finished">
+          <img class="list-empty-img" src="https://hzstatic.hzinsights.com/static/ETA_mobile/empty_img.png" alt="">
+          <p style="text-align:center;color:#999999;font-size:12px">暂无图片</p>
+      </div>
+    </div>
+  </div>
+</template>
+<style scoped lang="scss">
+.img-source-cont {
+  display: flex;
+  flex-direction: column;
+}
+.top-box{
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 116px;
+    border-bottom: 1px solid $border-color;
+    padding: 0 34px;
+}
+.img-list{
+    height: 800px;
+    padding: 4px;
+    flex: 1;
+    overflow-y: auto;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    .item{
+        width: 48%;
+        background: #FFFFFF;
+        border: 1px solid $border-color;
+        box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.03);
+        border-radius: 4px;
+        margin-bottom: 30px;
+        overflow: hidden;
+        padding: 14px;
+        box-sizing: border-box;
+        position: relative;
+        max-height: 400px;
+        img{
+            width: 100%;
+        }
+    }
+    .active{
+        border-color: $theme-color;
+        svg{
+            width: 28px;
+            height: 28px;
+            position: absolute;
+            right: 22px;
+            bottom: 22px;
+        }
+    }
+}
+
+@media screen and (min-width:$media-width){
+  .top-box{
+    height: 60px;
+    padding: 0 30px;
+  }
+  .img-list{
+    height: 600px;
+  }  
+}
+</style>

+ 1 - 2
src/views/report/smartReport/components/SheetComp.vue

@@ -11,8 +11,7 @@ const props = defineProps({
       style="width:100%;overflow: hidden;"
   >
       <div 
-          style="padding-left:10px;font-size:17px" 
-          contenteditable="true"
+          style="padding-left:10px;font-size:17px"
           v-html="compData.titleText"
           v-if="compData.titleText"
       ></div>

+ 109 - 0
src/views/report/smartReport/components/TextEditor.vue

@@ -0,0 +1,109 @@
+<script setup>
+import { nextTick, onMounted, ref, watch } from 'vue'
+import {useInitFroalaEditor} from '@/hooks/useFroalaEditor'
+
+const props = defineProps({
+  defaultVal: ''
+})
+const emit = defineEmits(['close','confirm'])
+
+const {lastFocusPosition,initFroalaEditor,imgUploadFlag,frolaEditorContentChange}=useInitFroalaEditor()
+
+let reportContentEditorIns=null//报告内容编辑器实例
+
+
+onMounted(() => {
+  reportContentEditorIns = initFroalaEditor('#editor',{
+    height:400, 
+    toolbarButtons: [
+      "textColor",
+      "bold",
+      "italic",
+      "underline",
+      "strikeThrough",
+      "subscript",
+      "superscript",
+      "fontFamily",
+      "fontSize",
+      "color",
+      "inlineClass",
+      "inlineStyle",
+      "paragraphStyle",
+      "lineHeight",
+      "paragraphFormat",
+      "align",
+      "formatOL",
+      "formatUL",
+      "outdent",
+      "indent",
+      "quote",
+      "specialCharacters",
+      "insertHR",
+      "selectAll",
+      "clearFormatting",
+      /* "html", */
+      "undo",
+      "redo",
+    ]
+  })
+
+  setTimeout(() => {
+    props.defaultVal&&reportContentEditorIns.html.set(props.defaultVal);
+  })
+})
+
+watch(() => props.defaultVal,
+  (nval) => {
+    reportContentEditorIns.html.set(nval)
+  }
+)
+
+
+function handleCancle() {
+  emit('close')
+}
+
+function handleConfirm() {
+  emit('confirm',$('.fr-element').html())
+}
+</script>
+<template>
+<div>
+  <div class="top-box">
+      <span style="color:#666666" @click="handleCancle">取消</span>
+      <span style="font-size:18px;font-weight:bold">编辑文字</span>
+      <span style="color:#0052D9" @click="handleConfirm">确定</span>
+  </div>
+  <div class="editor-box" id="editor"></div>
+</div>
+</template>
+<style scoped lang="scss">
+.top-box{
+    padding:32px;
+    display: flex;
+    justify-content: space-between;
+    border-bottom: 1px solid #DCDFE6;
+    .close{
+        color:#666666;
+    }
+    .title{
+        font-size: 36px;
+    }
+    .add-btn{
+        color:$theme-color;
+    }
+}
+.editor-box {
+  width: 100%;
+  height: 100%;
+  padding: 10px;
+}
+@media screen and (min-width:$media-width){
+  .top-box{
+      padding:16px;
+      .title{
+          font-size: 18px;
+      }
+  }
+}
+</style>