Explorar el Código

feat:报告编辑模块完成。接口调试完成,主题切换完成

韩洋 hace 10 meses
padre
commit
cd73bbec31
Se han modificado 33 ficheros con 1621 adiciones y 275 borrados
  1. 64 0
      src/assets/ai-editor/document_a.svg
  2. 64 0
      src/assets/ai-editor/document_b.svg
  3. 64 0
      src/assets/ai-editor/document_c.svg
  4. 14 0
      src/assets/ai-editor/insert-icon.svg
  5. 12 0
      src/assets/ai-editor/submit-ing_a.svg
  6. 12 0
      src/assets/ai-editor/submit-ing_b.svg
  7. 12 0
      src/assets/ai-editor/submit-ing_c.svg
  8. 13 0
      src/assets/ai-editor/submit-normal_a.svg
  9. 13 0
      src/assets/ai-editor/submit-normal_b.svg
  10. 13 0
      src/assets/ai-editor/submit-normal_c.svg
  11. 7 10
      src/modules/chat-item/Answer.vue
  12. 46 41
      src/modules/chat_echart/BarChart.vue
  13. 1 0
      src/modules/chat_echart/HomeChart.vue
  14. 1 0
      src/modules/conversation-chat/DialogInput.vue
  15. 1 1
      src/modules/conversation-chat/dialog-input/InputActions.vue
  16. 1 1
      src/modules/conversation-chat/dialog-input/RecommendModal.vue
  17. 97 0
      src/modules/conversation-dialog/api-dialog.js
  18. 212 51
      src/modules/report/AIModal.vue
  19. 449 23
      src/modules/report/ConversationZnws.vue
  20. 173 75
      src/modules/report/Editor.vue
  21. 8 2
      src/modules/report/EditorMenu.vue
  22. 20 6
      src/modules/report/conversation-znws/Answer.vue
  23. 6 0
      src/modules/report/conversation-znws/ChatItem.vue
  24. 6 0
      src/modules/report/conversation-znws/ChatList.vue
  25. 2 1
      src/modules/report/conversation-znws/DialogContent.vue
  26. 32 9
      src/modules/report/conversation-znws/DialogZnwsContent.vue
  27. 2 1
      src/modules/report/conversation-znws/Question.vue
  28. 18 0
      src/modules/report/use-input-clear.js
  29. 12 3
      src/views/Agent/AgentCentric.vue
  30. 1 0
      src/views/Report.vue
  31. 4 0
      src/views/layout/LayoutExperienceReport.vue
  32. 213 47
      src/views/report/ReportDetail.vue
  33. 28 4
      src/views/report/ReportHeader.vue

+ 64 - 0
src/assets/ai-editor/document_a.svg

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="31px" viewBox="0 0 34 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>文档- icon</title>
+    <defs>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
+            <stop stop-color="#1EA6A3" offset="0%"></stop>
+            <stop stop-color="#0A817E" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
+            <stop stop-color="#53C0BD" offset="0%"></stop>
+            <stop stop-color="#0A817E" offset="100%"></stop>
+        </linearGradient>
+        <path d="M0,8 C5,9.02532895 10,9.53799342 15,9.53799342 C20,9.53799342 25,9.02532895 30,8 L30,22 C30,26.418278 26.418278,30 22,30 L8,30 C3.581722,30 5.41083001e-16,26.418278 0,22 L0,8 L0,8 Z" id="path-3"></path>
+        <filter x="-11.7%" y="-20.5%" width="123.3%" height="131.8%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="-1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0.381079632   0 0 0 0 0.371391167  0 0 0 0.3 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="98.7682849%" id="linearGradient-5">
+            <stop stop-color="#AAEAE8" offset="0%"></stop>
+            <stop stop-color="#EAFCFB" offset="100%"></stop>
+        </linearGradient>
+        <circle id="path-6" cx="15" cy="15" r="3"></circle>
+        <filter x="0.0%" y="0.0%" width="100.0%" height="100.0%" filterUnits="objectBoundingBox" id="filter-7">
+            <feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="98.7682849%" id="linearGradient-8">
+            <stop stop-color="#FFFFFF" offset="0%"></stop>
+            <stop stop-color="#BEEEED" offset="100%"></stop>
+        </linearGradient>
+        <circle id="path-9" cx="15" cy="15" r="3"></circle>
+        <filter x="-25.0%" y="-25.0%" width="150.0%" height="150.0%" filterUnits="objectBoundingBox" id="filter-10">
+            <feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feMorphology radius="1" operator="erode" in="SourceAlpha" result="shadowInner"></feMorphology>
+            <feOffset dx="0" dy="0" in="shadowInner" result="shadowInner"></feOffset>
+            <feComposite in="shadowOffsetOuter1" in2="shadowInner" operator="out" result="shadowOffsetOuter1"></feComposite>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0.380392157   0 0 0 0 0.37254902  0 0 0 0.5 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-128.000000, -402.000000)">
+            <g id="文档--icon" transform="translate(130.000000, 402.000000)">
+                <rect id="矩形" fill="url(#linearGradient-1)" x="0" y="0" width="30" height="30" rx="8"></rect>
+                <rect id="矩形" fill="#FFFFFF" opacity="0.5" x="5" y="3" width="20" height="18" rx="4"></rect>
+                <rect id="矩形" fill="#FFFFFF" x="3" y="5" width="24" height="18" rx="4"></rect>
+                <g id="矩形">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
+                    <use fill="url(#linearGradient-2)" fill-rule="evenodd" xlink:href="#path-3"></use>
+                </g>
+                <g id="椭圆形">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-7)" xlink:href="#path-6"></use>
+                    <circle stroke="url(#linearGradient-5)" stroke-width="1.6" stroke-linejoin="square" cx="15" cy="15" r="2.2"></circle>
+                </g>
+                <g id="椭圆形">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-10)" xlink:href="#path-9"></use>
+                    <circle stroke="url(#linearGradient-8)" stroke-width="0.8" stroke-linejoin="square" cx="15" cy="15" r="2.6"></circle>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 64 - 0
src/assets/ai-editor/document_b.svg

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="31px" viewBox="0 0 34 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>文档- icon</title>
+    <defs>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
+            <stop stop-color="#1EA6A3" offset="0%"></stop>
+            <stop stop-color="#03547f" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
+            <stop stop-color="#53C0BD" offset="0%"></stop>
+            <stop stop-color="#03547f" offset="100%"></stop>
+        </linearGradient>
+        <path d="M0,8 C5,9.02532895 10,9.53799342 15,9.53799342 C20,9.53799342 25,9.02532895 30,8 L30,22 C30,26.418278 26.418278,30 22,30 L8,30 C3.581722,30 5.41083001e-16,26.418278 0,22 L0,8 L0,8 Z" id="path-3"></path>
+        <filter x="-11.7%" y="-20.5%" width="123.3%" height="131.8%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="-1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0.381079632   0 0 0 0 0.371391167  0 0 0 0.3 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="98.7682849%" id="linearGradient-5">
+            <stop stop-color="#AAEAE8" offset="0%"></stop>
+            <stop stop-color="#EAFCFB" offset="100%"></stop>
+        </linearGradient>
+        <circle id="path-6" cx="15" cy="15" r="3"></circle>
+        <filter x="0.0%" y="0.0%" width="100.0%" height="100.0%" filterUnits="objectBoundingBox" id="filter-7">
+            <feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="98.7682849%" id="linearGradient-8">
+            <stop stop-color="#FFFFFF" offset="0%"></stop>
+            <stop stop-color="#BEEEED" offset="100%"></stop>
+        </linearGradient>
+        <circle id="path-9" cx="15" cy="15" r="3"></circle>
+        <filter x="-25.0%" y="-25.0%" width="150.0%" height="150.0%" filterUnits="objectBoundingBox" id="filter-10">
+            <feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feMorphology radius="1" operator="erode" in="SourceAlpha" result="shadowInner"></feMorphology>
+            <feOffset dx="0" dy="0" in="shadowInner" result="shadowInner"></feOffset>
+            <feComposite in="shadowOffsetOuter1" in2="shadowInner" operator="out" result="shadowOffsetOuter1"></feComposite>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0.380392157   0 0 0 0 0.37254902  0 0 0 0.5 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-128.000000, -402.000000)">
+            <g id="文档--icon" transform="translate(130.000000, 402.000000)">
+                <rect id="矩形" fill="url(#linearGradient-1)" x="0" y="0" width="30" height="30" rx="8"></rect>
+                <rect id="矩形" fill="#FFFFFF" opacity="0.5" x="5" y="3" width="20" height="18" rx="4"></rect>
+                <rect id="矩形" fill="#FFFFFF" x="3" y="5" width="24" height="18" rx="4"></rect>
+                <g id="矩形">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
+                    <use fill="url(#linearGradient-2)" fill-rule="evenodd" xlink:href="#path-3"></use>
+                </g>
+                <g id="椭圆形">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-7)" xlink:href="#path-6"></use>
+                    <circle stroke="url(#linearGradient-5)" stroke-width="1.6" stroke-linejoin="square" cx="15" cy="15" r="2.2"></circle>
+                </g>
+                <g id="椭圆形">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-10)" xlink:href="#path-9"></use>
+                    <circle stroke="url(#linearGradient-8)" stroke-width="0.8" stroke-linejoin="square" cx="15" cy="15" r="2.6"></circle>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 64 - 0
src/assets/ai-editor/document_c.svg

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="31px" viewBox="0 0 34 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>文档- icon</title>
+    <defs>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
+            <stop stop-color="#1EA6A3" offset="0%"></stop>
+            <stop stop-color="#4977c8" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
+            <stop stop-color="#53C0BD" offset="0%"></stop>
+            <stop stop-color="#4977c8" offset="100%"></stop>
+        </linearGradient>
+        <path d="M0,8 C5,9.02532895 10,9.53799342 15,9.53799342 C20,9.53799342 25,9.02532895 30,8 L30,22 C30,26.418278 26.418278,30 22,30 L8,30 C3.581722,30 5.41083001e-16,26.418278 0,22 L0,8 L0,8 Z" id="path-3"></path>
+        <filter x="-11.7%" y="-20.5%" width="123.3%" height="131.8%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="-1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0.381079632   0 0 0 0 0.371391167  0 0 0 0.3 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="98.7682849%" id="linearGradient-5">
+            <stop stop-color="#AAEAE8" offset="0%"></stop>
+            <stop stop-color="#EAFCFB" offset="100%"></stop>
+        </linearGradient>
+        <circle id="path-6" cx="15" cy="15" r="3"></circle>
+        <filter x="0.0%" y="0.0%" width="100.0%" height="100.0%" filterUnits="objectBoundingBox" id="filter-7">
+            <feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="98.7682849%" id="linearGradient-8">
+            <stop stop-color="#FFFFFF" offset="0%"></stop>
+            <stop stop-color="#BEEEED" offset="100%"></stop>
+        </linearGradient>
+        <circle id="path-9" cx="15" cy="15" r="3"></circle>
+        <filter x="-25.0%" y="-25.0%" width="150.0%" height="150.0%" filterUnits="objectBoundingBox" id="filter-10">
+            <feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feMorphology radius="1" operator="erode" in="SourceAlpha" result="shadowInner"></feMorphology>
+            <feOffset dx="0" dy="0" in="shadowInner" result="shadowInner"></feOffset>
+            <feComposite in="shadowOffsetOuter1" in2="shadowInner" operator="out" result="shadowOffsetOuter1"></feComposite>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0.380392157   0 0 0 0 0.37254902  0 0 0 0.5 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-128.000000, -402.000000)">
+            <g id="文档--icon" transform="translate(130.000000, 402.000000)">
+                <rect id="矩形" fill="url(#linearGradient-1)" x="0" y="0" width="30" height="30" rx="8"></rect>
+                <rect id="矩形" fill="#FFFFFF" opacity="0.5" x="5" y="3" width="20" height="18" rx="4"></rect>
+                <rect id="矩形" fill="#FFFFFF" x="3" y="5" width="24" height="18" rx="4"></rect>
+                <g id="矩形">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
+                    <use fill="url(#linearGradient-2)" fill-rule="evenodd" xlink:href="#path-3"></use>
+                </g>
+                <g id="椭圆形">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-7)" xlink:href="#path-6"></use>
+                    <circle stroke="url(#linearGradient-5)" stroke-width="1.6" stroke-linejoin="square" cx="15" cy="15" r="2.2"></circle>
+                </g>
+                <g id="椭圆形">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-10)" xlink:href="#path-9"></use>
+                    <circle stroke="url(#linearGradient-8)" stroke-width="0.8" stroke-linejoin="square" cx="15" cy="15" r="2.6"></circle>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 14 - 0
src/assets/ai-editor/insert-icon.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>插入- icon</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="报告生成" transform="translate(-1452.000000, -730.000000)">
+            <g id="插入--icon" transform="translate(1461.000000, 739.000000) rotate(-90.000000) translate(-1461.000000, -739.000000) translate(1452.000000, 730.000000)">
+                <rect id="矩形" x="0" y="0" width="18" height="18"></rect>
+                <path d="M5.09522551,2.4 L4.69461273,2.4 C3.59004323,2.4 2.69461273,3.2954305 2.69461273,4.4 L2.69461273,12.4 C2.69461273,13.5045695 3.59004323,14.4 4.69461273,14.4 L12.6946127,14.4 C13.7991822,14.4 14.6946127,13.5045695 14.6946127,12.4 L14.6946127,4.4 C14.6946127,3.2954305 13.7991822,2.4 12.6946127,2.4 L12.0814454,2.4 L12.0814454,2.4" id="路径-18" stroke="#BBBBBB"></path>
+                <line x1="9" y1="2.4" x2="9" y2="9" id="路径-33" stroke="#BBBBBB"></line>
+                <path d="M6.75,7.125 L9,9.375 L11.25,7.125" id="路径-34" stroke="#BBBBBB"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 12 - 0
src/assets/ai-editor/submit-ing_a.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>暂停- icon</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="方案一#0A817E" transform="translate(-1567.000000, -952.000000)">
+            <g id="暂停--icon" transform="translate(1567.000000, 952.000000)">
+                <circle id="椭圆形" fill="#0A817E" cx="24" cy="24" r="24"></circle>
+                <rect id="矩形" fill="#FFFFFF" x="15" y="15" width="18" height="18" rx="4"></rect>
+            </g>
+        </g>
+    </g>
+</svg>

+ 12 - 0
src/assets/ai-editor/submit-ing_b.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>暂停- icon</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="方案一#0A817E" transform="translate(-1567.000000, -952.000000)">
+            <g id="暂停--icon" transform="translate(1567.000000, 952.000000)">
+                <circle id="椭圆形" fill="#03547f" cx="24" cy="24" r="24"></circle>
+                <rect id="矩形" fill="#FFFFFF" x="15" y="15" width="18" height="18" rx="4"></rect>
+            </g>
+        </g>
+    </g>
+</svg>

+ 12 - 0
src/assets/ai-editor/submit-ing_c.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>暂停- icon</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="方案一#0A817E" transform="translate(-1567.000000, -952.000000)">
+            <g id="暂停--icon" transform="translate(1567.000000, 952.000000)">
+                <circle id="椭圆形" fill="#4977c8" cx="24" cy="24" r="24"></circle>
+                <rect id="矩形" fill="#FFFFFF" x="15" y="15" width="18" height="18" rx="4"></rect>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/assets/ai-editor/submit-normal_a.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>发送-不可点击- icon备份 2</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-712.000000, -535.000000)">
+            <g id="发送-不可点击--icon备份-2" transform="translate(727.000000, 550.000000) rotate(-270.000000) translate(-727.000000, -550.000000) translate(712.000000, 535.000000)">
+                <circle id="椭圆形" fill="#0A817E" cx="15" cy="15" r="15"></circle>
+                <line x1="15" y1="21.875" x2="15" y2="9.375" id="路径-20" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"></line>
+                <polyline id="路径-21" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" points="8.75 13.75 15 8.125 21.25 13.75"></polyline>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/assets/ai-editor/submit-normal_b.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>发送-不可点击- icon备份 2</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-712.000000, -535.000000)">
+            <g id="发送-不可点击--icon备份-2" transform="translate(727.000000, 550.000000) rotate(-270.000000) translate(-727.000000, -550.000000) translate(712.000000, 535.000000)">
+                <circle id="椭圆形" fill="#03547f" cx="15" cy="15" r="15"></circle>
+                <line x1="15" y1="21.875" x2="15" y2="9.375" id="路径-20" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"></line>
+                <polyline id="路径-21" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" points="8.75 13.75 15 8.125 21.25 13.75"></polyline>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/assets/ai-editor/submit-normal_c.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>发送-不可点击- icon备份 2</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-712.000000, -535.000000)">
+            <g id="发送-不可点击--icon备份-2" transform="translate(727.000000, 550.000000) rotate(-270.000000) translate(-727.000000, -550.000000) translate(712.000000, 535.000000)">
+                <circle id="椭圆形" fill="#4977c8" cx="15" cy="15" r="15"></circle>
+                <line x1="15" y1="21.875" x2="15" y2="9.375" id="路径-20" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"></line>
+                <polyline id="路径-21" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" points="8.75 13.75 15 8.125 21.25 13.75"></polyline>
+            </g>
+        </g>
+    </g>
+</svg>

+ 7 - 10
src/modules/chat-item/Answer.vue

@@ -73,13 +73,7 @@
       <div class="answer-item-content" v-if="chat_type === 'znws'">
         <!-- 智能问数的组件渲染和报告生成的一致,先使用同一个,后续有变动再单独拆分 -->
         <DialogReportContent :is_loading="is_content_loading" :type="type" :result_list="report_content">
-          <DialogActions
-            v-if="answer_status == 2"
-            class="answer-actions-btns"
-            :type="type"
-            @copy="handleReportCopy"
-            @edit="handleReportEdit"
-          />
+          <DialogActions v-if="answer_status == 2" class="answer-actions-btns" :type="type" @copy="handleReportCopy" />
         </DialogReportContent>
       </div>
       <div class="answer-item-content" v-if="chat_type === 'bgcl'">
@@ -399,9 +393,12 @@ const handleUpdate = (icon) => {
 };
 
 const handleReportEdit = () => {
-  router.push({
-    path: '/report'
-  });
+  // console.log(props.dialog, 888);
+  if (props.dialog.report_files.record_id) {
+    router.push({
+      path: `/report/${props.dialog.report_files.record_id}`
+    });
+  }
 };
 const handleSelect = (v) => {
   if (v / 1 === 1) {

+ 46 - 41
src/modules/chat_echart/BarChart.vue

@@ -3,8 +3,8 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
-import * as echarts from 'echarts'
+import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
+import * as echarts from 'echarts';
 
 const props = defineProps({
   data: {
@@ -24,32 +24,32 @@ const props = defineProps({
     type: String,
     default: '#5470c6'
   }
-})
+});
 
-const chartDom = ref(null)
-let myChart = null
+const chartDom = ref(null);
+let myChart = null;
 
 const initChart = () => {
-  if (!chartDom.value) return
-  myChart = echarts.init(chartDom.value)
-  updateChart()
-}
+  if (!chartDom.value) return;
+  myChart = echarts.init(chartDom.value);
+  updateChart();
+};
 
 const formattedData = computed(() => {
-  return props.data.map(item => {
+  return props.data.map((item) => {
     if (typeof item.value === 'string' && item.value.endsWith('%')) {
       return {
         ...item,
         value: parseFloat(item.value) / 100,
         origin_value: item.value
-      }
+      };
     }
-    return item
-  })
-})
+    return item;
+  });
+});
 
 const updateChart = () => {
-  if (!myChart) return
+  if (!myChart) return;
   // console.log(props.data, 55555)
   const option = {
     title: {
@@ -57,14 +57,14 @@ const updateChart = () => {
       left: 'center'
     },
     tooltip: {
-      formatter: params => {
-        const dataItem = formattedData.value[params.dataIndex]
-        return `${params.name}<br/>${params.seriesName}: ${dataItem.origin_value || dataItem.value}`
+      formatter: (params) => {
+        const dataItem = formattedData.value[params.dataIndex];
+        return `${params.name}<br/>${params.seriesName}: ${dataItem.origin_value || dataItem.value}`;
       }
     },
     xAxis: {
       type: 'category',
-      data: props.xAxis.length ? props.xAxis : formattedData.value.map(item => item.name),
+      data: props.xAxis.length ? props.xAxis : formattedData.value.map((item) => item.name),
       axisLabel: {
         rotate: 45,
         interval: 0,
@@ -74,41 +74,46 @@ const updateChart = () => {
     yAxis: {
       type: 'value'
     },
-    series: [{
-      name: '数据',
-      type: 'bar',
-      data: formattedData.value.length ? formattedData.value : [],
-      itemStyle: {
-        color: props.color
-      },
-      barWidth: '60%'
-    }]
-  }
+    series: [
+      {
+        name: '数据',
+        type: 'bar',
+        data: formattedData.value.length ? formattedData.value : [],
+        itemStyle: {
+          color: props.color
+        },
+        barWidth: '60%'
+      }
+    ]
+  };
 
-  myChart.setOption(option)
-}
+  myChart.setOption(option);
+};
 
 const handleResize = () => {
-  myChart?.resize()
-}
+  myChart?.resize();
+};
 
 onMounted(() => {
-  initChart()
-  window.addEventListener('resize', handleResize)
-})
+  initChart();
+  window.addEventListener('resize', handleResize);
+  setTimeout(() => {
+    handleResize();
+  }, 300);
+});
 
 onBeforeUnmount(() => {
-  window.removeEventListener('resize', handleResize)
-  myChart?.dispose()
-})
+  window.removeEventListener('resize', handleResize);
+  myChart?.dispose();
+});
 
 watch(
   () => [props.formattedData, props.xAxis, props.title, props.color],
   () => {
-    updateChart()
+    updateChart();
   },
   { deep: true }
-)
+);
 </script>
 
 <style scoped>

+ 1 - 0
src/modules/chat_echart/HomeChart.vue

@@ -42,6 +42,7 @@
       :title="bar_and_line.title"
     ></BarAndLineChart2>
     <!-- <button @click="updateData">更新数据</button> -->
+    <slot name="insertBtn"></slot>
   </div>
 </template>
 

+ 1 - 0
src/modules/conversation-chat/DialogInput.vue

@@ -349,6 +349,7 @@ defineExpose({
 
 <style lang="css" scoped>
 .dialog-input-area-container {
+  position: relative;
   display: flex;
   flex-direction: column;
 

+ 1 - 1
src/modules/conversation-chat/dialog-input/InputActions.vue

@@ -22,7 +22,7 @@
           <img class="icon_upload" src="../../../assets/chat-input/upload_pic.svg" alt="" />
         </a-tooltip> -->
       </div>
-      <a-tooltip :content="tip_content" position="top">
+      <a-tooltip :content="tip_content" position="left">
         <img v-if="btn_status == 0" class="action_btn_disabled" :src="submit_disabled" alt="禁用" />
         <img v-if="btn_status == 1" class="action_btn_normal" @click="handleSubmit" :src="submit_normal" alt="发送" />
         <img v-if="btn_status == 2" class="action_btn_stop" @click="handleStop" :src="submit_stop" alt="停止" />

+ 1 - 1
src/modules/conversation-chat/dialog-input/RecommendModal.vue

@@ -101,7 +101,7 @@ onUnmounted(() => {
 </script>
 <style scoped lang="css">
 .recommend-modal {
-  width: 600px;
+  width: 500px;
   height: 400px;
   border: 0.5px solid #dbdce0;
   background: #fff;

+ 97 - 0
src/modules/conversation-dialog/api-dialog.js

@@ -420,4 +420,101 @@ export const downExcelData1 = () => {
   });
 }
 
+// 获取富文本详情
 
+export const getReportHtmlDetail = (record_id) => {
+  return new Promise((resolve, reject) => {
+    ajax
+      .post(
+        `/iaserverapi/v1/report/aiEdit/detail?record_id=${record_id}`,
+        // { record_id: Number(record_id) },
+        {
+          headers: {
+            'Content-Type': 'application/json',
+          },
+        }
+      )
+      .then((res) => {
+        resolve({ code: res.code, data: res.data });
+      })
+      .catch((err) => {
+        reject(err);
+      });
+  });
+}
+/**
+ * AI润色
+ * origan_data: 要润色的原始数据
+ * rewrite_requirements:改写要求
+ * content_type:expand扩写 abbreviation缩写 rewrite改写
+ * */
+export const aiRewrite = ({ origan_data, rewrite_requirements, content_type }) => {
+  return new Promise((resolve, reject) => {
+    ajax
+      .post(
+        `/iaserverapi/v1/report/aiEdit/aiRewrite`,
+        { origan_data, rewrite_requirements, content_type },
+        {
+          headers: {
+            'Content-Type': 'application/json',
+          },
+        }
+      )
+      .then((res) => {
+        resolve({ code: res.code, data: res.data.rewrite_data });
+      })
+      .catch((err) => {
+        reject(err);
+      });
+  });
+}
+
+/**
+ * 富文本内容保存
+ * */
+export const saveHtml = ({ id, title, record_id, html_content }) => {
+  return new Promise((resolve, reject) => {
+    ajax
+      .post(
+        `/iaserverapi/v1/report/aiEdit/updateOrCreate`,
+        { id, title, record_id, html_content },
+        {
+          headers: {
+            'Content-Type': 'application/json',
+          },
+        }
+      )
+      .then((res) => {
+        resolve({ code: res.code, data: res.data });
+      })
+      .catch((err) => {
+        reject(err);
+      });
+  });
+}
+
+/**
+ * 生成图表图片
+ * */
+
+
+export const createChartPic = ({ type, data }) => {
+  return new Promise((resolve, reject) => {
+    ajax
+      .post(
+        `/iaserverapi/v1/chart/create`,
+        { table: JSON.stringify({ type, data }) },
+        {
+          headers: {
+            'Content-Type': 'application/json',
+          },
+        }
+      )
+      .then((res) => {
+        resolve({ code: res.code, data: res.data });
+      })
+      .catch((err) => {
+        reject(err);
+      });
+  });
+}

+ 212 - 51
src/modules/report/AIModal.vue

@@ -1,10 +1,16 @@
 <template>
-  <a-modal v-model:visible="visible" :mask-closable="false" simple :footer="false" :hide-title="true"
-    modal-class="my_custom_modal">
+  <a-modal
+    v-model:visible="visible"
+    :mask-closable="false"
+    simple
+    :footer="false"
+    :hide-title="true"
+    modal-class="my_custom_modal"
+  >
     <!-- <template #title> Title </template> -->
     <div class="header">
       <div class="header-left">
-        <img class="icon" src="../../assets/ai-editor/document.svg" alt="" />
+        <img class="icon" :src="document_icon" alt="" />
         <div class="title">文档AI</div>
       </div>
       <div class="header-right">
@@ -13,29 +19,42 @@
     </div>
     <div class="section">
       <div class="section-tabs">
-        <div class="tab-item" :class="[item.active ? 'tab-item-active' : '']" v-for="item in ai_tools" :key="item.id"
-          @click="changeTab(item)">
+        <div
+          class="tab-item"
+          :class="[item.active ? 'tab-item-active' : '']"
+          v-for="item in ai_tools"
+          :key="item.id"
+          @click="changeTab(item)"
+        >
           <img class="icon" :src="item.icon" />
           <div>{{ item.title }}</div>
         </div>
       </div>
       <div class="section-input">
-        <a-input v-model="input_txt" placeholder="你有什么其他要求告诉我哦~" />
-        <img class="icon" v-if="false" src="../../assets/ai-editor/submit-normal.svg" alt="">
-        <img class="icon" v-if="true" src="../../assets/ai-editor/submit-ing.svg" alt="">
-        <img class="icon" v-if="false" src="../../assets/submit_disabled.png" alt="">
+        <a-input
+          v-model="input_txt"
+          :disabled="input_disabled"
+          @keyup.enter="toSubmit"
+          placeholder="你有什么其他要求告诉我哦~"
+        />
+        <img class="icon" @click="toSubmit" v-if="!chat_status" :src="submit_normal" alt="" />
+        <img class="icon" @click="toStop" v-if="chat_status" :src="submit_ing" alt="" />
+        <!-- <img class="icon" v-if="false" src="../../assets/submit_disabled.png" alt="" /> -->
       </div>
-      <div class="section-output" ref="outputRef" v-html="outContent">
+      <div class="section-output" v-if="!is_empty" ref="outputRef" v-html="outStreamContent"></div>
+      <div class="section-output" v-else-if="!is_loading_ai" ref="outputRef">
+        <a-empty></a-empty>
+      </div>
+      <div class="section-output" v-else>
+        <div class="loading"><a-spin></a-spin> <span>正在思考中...</span></div>
       </div>
-
     </div>
     <div class="footer">
-      <a-button class="btn-primary submit-btn" @click="changeP">替换</a-button>
-      <a-button class="btn-primary" @click="insertP">插入</a-button>
-      <a-button class="btn-primary">重写</a-button>
+      <a-button class="btn-primary submit-btn" :disabled="disabled" @click="changeP">替换</a-button>
+      <a-button class="btn-primary" :disabled="disabled" @click="insertP">插入</a-button>
+      <a-button class="btn-primary" :disabled="disabled" @click="toSubmit">重写</a-button>
     </div>
   </a-modal>
-
 </template>
 
 <script setup>
@@ -43,43 +62,123 @@ import { ref, computed, onMounted } from 'vue';
 import rewrite from '../../assets/ai-editor/rewrite.svg';
 import winden from '../../assets/ai-editor/winden.svg';
 import shorten from '../../assets/ai-editor/shorten.svg';
+import submit_normal_a from '../../assets/ai-editor/submit-normal_a.svg';
+import submit_normal_b from '../../assets/ai-editor/submit-normal_b.svg';
+import submit_normal_c from '../../assets/ai-editor/submit-normal_c.svg';
+import submit_ing_a from '../../assets/ai-editor/submit-ing_a.svg';
+import submit_ing_b from '../../assets/ai-editor/submit-ing_b.svg';
+import submit_ing_c from '../../assets/ai-editor/submit-ing_c.svg';
+import document_a from '../../assets/ai-editor/document_a.svg';
+import document_b from '../../assets/ai-editor/document_b.svg';
+import document_c from '../../assets/ai-editor/document_c.svg';
+import { THEME_LIGHT, THEME_DARK, THEME_GREEN, THEME_PARTY } from '../../base/variables';
+import { useThemeStore } from '../../base/store/use-theme';
 
+import { aiRewrite } from '../../modules/conversation-dialog/api-dialog';
+import { Message } from '@arco-design/web-vue';
 const props = defineProps({
   target_active_pElement: Object,
   show_modal: Boolean
 });
-const emit = defineEmits(['close', 'changePElement', 'insertPElement'])
+const emit = defineEmits(['close', 'changePElement', 'insertPElement']);
+
+const themeStore = useThemeStore();
+
+const document_icon = computed(() => {
+  if (themeStore.value == THEME_GREEN) {
+    return document_a;
+  } else if (themeStore.value == THEME_PARTY) {
+    return document_b;
+  } else if (themeStore.value == THEME_LIGHT) {
+    return document_c;
+  } else {
+    return document_a;
+  }
+});
+
+const submit_normal = computed(() => {
+  if (themeStore.value == THEME_GREEN) {
+    return submit_normal_a;
+  } else if (themeStore.value == THEME_PARTY) {
+    return submit_normal_b;
+  } else if (themeStore.value == THEME_LIGHT) {
+    return submit_normal_c;
+  } else {
+    return submit_normal_a;
+  }
+});
+
+const submit_ing = computed(() => {
+  if (themeStore.value == THEME_GREEN) {
+    return submit_ing_a;
+  } else if (themeStore.value == THEME_PARTY) {
+    return submit_ing_b;
+  } else if (themeStore.value == THEME_LIGHT) {
+    return submit_ing_c;
+  } else {
+    return submit_ing_a;
+  }
+});
 
 const visible = computed(() => {
-  return props.show_modal
-})
+  return props.show_modal;
+});
+
 const ai_tools = ref([
   {
     id: 1,
     title: '改写',
     icon: rewrite,
-    active: true
+    active: true,
+    name: 'rewrite'
   },
   {
     id: 2,
     title: '扩写',
     icon: winden,
-    active: false
+    active: false,
+    name: 'expand'
   },
   {
     id: 3,
     title: '缩写',
     icon: shorten,
-    active: false
+    active: false,
+    name: 'abbreviation'
   }
 ]);
 
 const input_txt = ref('');
-const outputRef = ref(null)
-const outContent = ref(
-  '<p><span data-slate-node="text">10KV内容内容内容10KV内容内容内容10KV内容内容内容10KV内容内容内容10KV内容内容内容10KV内容内容内容</span></p>'
-);
+const outputRef = ref(null);
+// 流式输出的数据
+const outStreamContent = ref('');
+// 完整的输出数据
+const outAllContent = ref('');
+// 定时器
+let interval = null;
+// 问答状态 false:未问答,true:正在问答
+const chat_status = ref(false);
+
+// 是否正在请求问答接口
+const is_loading_ai = ref(false);
+
+// 回答框是否为空
+const is_empty = computed(() => {
+  return !outStreamContent.value;
+});
 
+const disabled = computed(() => {
+  return chat_status.value;
+});
+
+const input_disabled = computed(() => {
+  const item = ai_tools.value.find((item) => item.active);
+  if ([2, 3].includes(item.id)) {
+    return true;
+  } else {
+    return false;
+  }
+});
 const changeTab = (item) => {
   const id = item.id;
   ai_tools.value.forEach((it) => {
@@ -88,32 +187,89 @@ const changeTab = (item) => {
       it.active = true;
     }
   });
+  input_txt.value = '';
+};
+
+const toSubmit = () => {
+  const active_item = ai_tools.value.find((item) => item.active);
+  if (active_item.id / 1 === 1) {
+    if (!input_txt.value) {
+      return Message.warning('请输出改写要求');
+    }
+  }
+  outStreamContent.value = '';
+  aiRewriteFunc();
+};
+const toStop = () => {
+  clearTimeout(interval);
+  chat_status.value = false;
+};
+const aiRewriteFunc = async () => {
+  chat_status.value = true;
+  is_loading_ai.value = true;
+  const data = {
+    origan_data: props.target_active_pElement.innerText,
+    rewrite_requirements: input_txt.value,
+    content_type: ai_tools.value.find((item) => item.active).name
+  };
+  try {
+    const res = await aiRewrite(data);
+    if (res.code / 1 === 200) {
+      is_loading_ai.value = false;
+      outAllContent.value = res.data;
+      let streamStr = '';
+      let index = 0;
+      // streamEffect(outAllContent.value, streamStr, index);
+      function streamEffect() {
+        if (index < outAllContent.value.length) {
+          streamStr += outAllContent.value.charAt(index);
+          // outputElement.textContent = streamStr;
+          outStreamContent.value = '<p>' + streamStr + '</p>';
+          index++;
+          // console.log(index, outAllContent.value);
+          interval = setTimeout(streamEffect, 30); // 调整延迟时间控制速度
+        } else {
+          chat_status.value = false;
+          clearTimeout(interval);
+        }
+      }
+      streamEffect();
+    }
+  } catch (e) {
+    is_loading_ai.value = false;
+  } finally {
+    is_loading_ai.value = false;
+  }
 };
 
 const changeP = () => {
-  if (!outContent.value.trim()) return
-  // console.log(outContent, props.target_active_pElement, props.target_active_pElement.innerText)
-  // console.log(props.target_active_pElement.id)
-  // console.log(outputRef.value.querySelector('p').innerHTML)
+  if (!outStreamContent.value.trim()) return;
+  // console.log(outputRef.value?.querySelector('p'), 9999);s
+  // return;
   emit('changePElement', {
     pid: props.target_active_pElement.id,
     content: outputRef.value?.querySelector('p')
-  })
-
-}
+  });
+};
 const insertP = () => {
-  if (!outContent.value.trim()) return
+  // emit('insertPElement', {
+  //   pid: props.target_active_pElement.id,
+  //   content: '<p>插入内容</p>'
+  // });
+  if (!outStreamContent.value.trim()) return;
+  // console.log(outputRef.value?.querySelector('p'), 9999);
+  // return;
   emit('insertPElement', {
     pid: props.target_active_pElement.id,
     content: outputRef.value?.querySelector('p')
-  })
-}
+  });
+};
 
 const handleClose = () => {
-  emit('close')
+  emit('close');
 };
 
-onMounted(() => { });
+onMounted(() => {});
 </script>
 <style scoped lang="css">
 .header {
@@ -129,12 +285,12 @@ onMounted(() => { });
   flex-direction: row;
   align-items: center;
   gap: 8px;
-  font-size: 20px;
+  font-size: 16px;
   color: #2c2c2c;
 }
 
 .header-left .icon {
-  width: 34px;
+  width: 28px;
 }
 
 .header-right .icon {
@@ -157,8 +313,8 @@ onMounted(() => { });
   align-items: center;
   justify-content: center;
   gap: 6px;
-  width: 80px;
-  height: 36px;
+  width: 100px;
+  height: 32px;
   border-radius: 18px;
   border: 1px solid #e5e5e5;
   cursor: pointer;
@@ -166,11 +322,11 @@ onMounted(() => { });
 }
 
 .tab-item:hover {
-  border-color: #11d6d0;
+  border-color: var(--editor_hover);
 }
 
 .tab-item-active {
-  border-color: #0a817e;
+  border-color: var(--primary-default);
 }
 
 .section-input {
@@ -182,7 +338,7 @@ onMounted(() => { });
 
 .section-input .arco-input-wrapper {
   height: 42px;
-  background: #e6f2f2;
+  background: var(--tip-bg-start);
   border-radius: 21px;
 }
 
@@ -192,7 +348,7 @@ onMounted(() => { });
 
 .section-input .arco-input-wrapper:focus-within,
 .section-input .arco-input-wrapper .arco-input-focus {
-  border-color: #0a817e;
+  border-color: var(--primary-default);
 }
 
 .section-input .icon {
@@ -201,24 +357,29 @@ onMounted(() => { });
   bottom: 25px;
   width: 30px;
   height: 30px;
+  cursor: pointer;
 }
 
 .section-output {
-  height: 150px;
+  height: 170px;
   border-radius: 20px;
   border: 1px solid #e5e5e5;
   margin: 0px 20px;
   padding: 20px;
   overflow-y: auto;
 }
+.section-input .loading {
+  display: flex;
+  align-items: center;
+}
 
 .btn-primary {
-  width: 100px;
-  height: 48px;
+  width: 80px;
+  height: 32px;
   border-radius: 24px;
-  border: 1px solid #E5E5E5;
-  font-size: 18px;
-  color: #2C2C2C;
+  border: 1px solid #e5e5e5;
+  /* font-size: 18px; */
+  color: #2c2c2c;
 }
 
 .btn-primary:hover {

+ 449 - 23
src/modules/report/ConversationZnws.vue

@@ -2,16 +2,22 @@
   <div class="conversation-chat">
     <div class="conversation-header">
       <div class="header-nav">
-        <img class="icon" :src="Icon" alt="" />
+        <img class="icon" :src="target_robot_icon" alt="" />
         <div class="title">{{ header_title }}</div>
       </div>
     </div>
     <div class="conversation-body">
-      <div class="chat-container">
-        <ChatList :chat_list="chat_display_list"></ChatList>
+      <div class="chat-container" ref="list_scroller">
+        <ChatList :chat_list="chat_display_list" :target_agent_icon="target_icon" @insertEchart="insert"></ChatList>
       </div>
       <div class="chat-input">
-        <DialogInput></DialogInput>
+        <DialogInput
+          ref="dialog_input"
+          :chat_type="chat_type"
+          :is_input_ing="input_ing"
+          @submit="handleSubmit"
+          @stop="stopResponse"
+        ></DialogInput>
         <DialogFooter></DialogFooter>
       </div>
     </div>
@@ -19,15 +25,59 @@
 </template>
 
 <script setup lang="js">
-import { ref,computed } from 'vue'
+import { ref,computed,nextTick,onMounted } from 'vue'
 import Icon from '../../assets/agent/icon_1.svg'
 import DialogInput from '../../modules/conversation-chat/DialogInput.vue'
 import DialogFooter from '../../modules/conversation-chat/DialogFooter.vue'
 import ChatList from './conversation-znws/ChatList.vue'
+import formatDialogQuestionByClient from '../../modules/conversation-dialog/format-dialog-question-by-client';
+import formatDialogAnswerForLoading from '../../modules/conversation-dialog/format-dialog-answer-for-loading';
+import formatDialogAnswerForException from '../../modules/conversation-dialog/format-dialog-answer-for-exception';
+import formatDialogAnswerFromSseReceiveMessageZnws from '../../modules/conversation-dialog/format-dialog-answer-from-sse_receive_message_znws';
+
+import { getUserName } from '../../utils/get-user-name';
+import { getUserId } from '../../utils/get-user-id';
+import { getIcon } from '../common/use-icon-map';
+import { useThemeStore } from '../../base/store/use-theme';
+import { THEME_LIGHT, THEME_DARK, THEME_GREEN, THEME_PARTY } from '../../base/variables';
+import useInputClear from './use-input-clear';
+import { useAgentUsedStore } from '../../base/store/use-agent-used';
+import {
+  getDialogSessionId_AL,
+  getAgentIcons,
+} from '../../modules/conversation-dialog/api-dialog';
+import useFetchEventSourceZnws from '../../modules/conversation-dialog/use-sse-znws';
+
 const props = defineProps({
 })
+const emit = defineEmits(['insertEchart']);
+
+const agentUsedStore = useAgentUsedStore();
 
 const header_title = ref('智能问数助手')
+const chat_type = ref('znws')
+// 是否正在对话
+const input_ing = ref(false)
+// 会话id阿里智能体(问数)
+const WS_session = ref({
+  conversation_id: '',
+  record_id: ''
+});
+const target_icon = ref('')
+const themeStore = useThemeStore();
+
+const target_robot_icon = computed(() => {
+  if (themeStore.value == THEME_GREEN) {
+    return getIcon(target_icon.value, 'a');
+  } else if (themeStore.value == THEME_PARTY) {
+    return getIcon(target_icon.value, 'b');
+  } else if (themeStore.value == THEME_LIGHT) {
+    return getIcon(target_icon.value, 'c');
+  } else {
+    return getIcon(target_icon.value, 'a');
+  }
+});
+
 
 // 聊天对话列表
 const moke_data = ref([
@@ -36,24 +86,140 @@ const moke_data = ref([
     answer:'你好,我是你的专属报告生成助手,有什么可以帮助您的么?',
     result_list:['你好,我是你的专属报告生成助手,有什么可以帮助您的么?']
   },
-  {
-    client_custom_item_type:'q',
-    question:'什么是大模型?'
-  },
-  {
-    client_custom_item_type:'a',
-    answer:'',
-    result_list:['不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据']
-  },
-  {
-    client_custom_item_type:'q',
-    question:'什么是大模型?'
-  },
-  {
-    client_custom_item_type:'a',
-    answer:'',
-    result_list:['不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据']
-  }
+//   {
+//     client_custom_item_type:'q',
+//     question:'什么是大模型?'
+//   },
+//   {
+//     client_custom_item_type:'a',
+//     answer:'',
+//     result_list:['不知道参测试数测试数据不知道参测试数测试',{
+//   "type": "bar",
+//   "title": "供电可靠率情况",
+//   "color": "xxx",
+//    'origin_chart':'</table>{\"type\": \"bar\", \"title\": \"供电可靠率情况\", \"color\": \"xxx\", \"chartData\": [{\"name\": \"成都\", \"type\": \"bar\", \"value\": \"99.96%\"}, {\"name\": \"宜宾\", \"type\": \"bar\", \"value\": \"99.94%\"}, {\"name\": \"天府\", \"type\": \"bar\", \"value\": \"99.94%\"}, {\"name\": \"攀枝花\", \"type\": \"bar\", \"value\": \"99.94%\"}, {\"name\": \"遂宁\", \"type\": \"bar\", \"value\": \"99.93%\"}, {\"name\": \"自贡\", \"type\": \"bar\", \"value\": \"99.92%\"}, {\"name\": \"内江\", \"type\": \"bar\", \"value\": \"99.90%\"}, {\"name\": \"南充\", \"type\": \"bar\", \"value\": \"99.90%\"}, {\"name\": \"德阳\", \"type\": \"bar\", \"value\": \"99.90%\"}, {\"name\": \"绵阳\", \"type\": \"bar\", \"value\": \"99.89%\"}, {\"name\": \"达州\", \"type\": \"bar\", \"value\": \"99.89%\"}, {\"name\": \"眉山\", \"type\": \"bar\", \"value\": \"99.87%\"}, {\"name\": \"广元\", \"type\": \"bar\", \"value\": \"99.87%\"}, {\"name\": \"巴中\", \"type\": \"bar\", \"value\": \"99.87%\"}, {\"name\": \"乐山\", \"type\": \"bar\", \"value\": \"99.86%\"}, {\"name\": \"泸州\", \"type\": \"bar\", \"value\": \"99.85%\"}, {\"name\": \"雅安\", \"type\": \"bar\", \"value\": \"99.85%\"}, {\"name\": \"广安\", \"type\": \"bar\", \"value\": \"99.85%\"}, {\"name\": \"资阳\", \"type\": \"bar\", \"value\": \"99.84%\"}, {\"name\": \"凉山\", \"type\": \"bar\", \"value\": \"99.65%\"}, {\"name\": \"阿坝\", \"type\": \"bar\", \"value\": \"99.34%\"}, {\"name\": \"甘孜\", \"type\": \"bar\", \"value\": \"98.81%\"}, {\"name\": \"岷江\", \"type\": \"bar\", \"value\": \"100.00%\"}]}<table/>',
+//   "chartData": [
+//     {
+//       "name": "成都",
+//       "type": "bar",
+//       "value": "99.96%"
+//     },
+//     {
+//       "name": "宜宾",
+//       "type": "bar",
+//       "value": "99.94%"
+//     },
+//     {
+//       "name": "天府",
+//       "type": "bar",
+//       "value": "99.94%"
+//     },
+//     {
+//       "name": "攀枝花",
+//       "type": "bar",
+//       "value": "99.94%"
+//     },
+//     {
+//       "name": "遂宁",
+//       "type": "bar",
+//       "value": "99.93%"
+//     },
+//     {
+//       "name": "自贡",
+//       "type": "bar",
+//       "value": "99.92%"
+//     },
+//     {
+//       "name": "内江",
+//       "type": "bar",
+//       "value": "99.90%"
+//     },
+//     {
+//       "name": "南充",
+//       "type": "bar",
+//       "value": "99.90%"
+//     },
+//     {
+//       "name": "德阳",
+//       "type": "bar",
+//       "value": "99.90%"
+//     },
+//     {
+//       "name": "绵阳",
+//       "type": "bar",
+//       "value": "99.89%"
+//     },
+//     {
+//       "name": "达州",
+//       "type": "bar",
+//       "value": "99.89%"
+//     },
+//     {
+//       "name": "眉山",
+//       "type": "bar",
+//       "value": "99.87%"
+//     },
+//     {
+//       "name": "广元",
+//       "type": "bar",
+//       "value": "99.87%"
+//     },
+//     {
+//       "name": "巴中",
+//       "type": "bar",
+//       "value": "99.87%"
+//     },
+//     {
+//       "name": "乐山",
+//       "type": "bar",
+//       "value": "99.86%"
+//     },
+//     {
+//       "name": "泸州",
+//       "type": "bar",
+//       "value": "99.85%"
+//     },
+//     {
+//       "name": "雅安",
+//       "type": "bar",
+//       "value": "99.85%"
+//     },
+//     {
+//       "name": "广安",
+//       "type": "bar",
+//       "value": "99.85%"
+//     },
+//     {
+//       "name": "资阳",
+//       "type": "bar",
+//       "value": "99.84%"
+//     },
+//     {
+//       "name": "凉山",
+//       "type": "bar",
+//       "value": "99.65%"
+//     },
+//     {
+//       "name": "阿坝",
+//       "type": "bar",
+//       "value": "99.34%"
+//     },
+//     {
+//       "name": "甘孜",
+//       "type": "bar",
+//       "value": "98.81%"
+//     },
+//     {
+//       "name": "岷江",
+//       "type": "bar",
+//       "value": "100.00%"
+//     }
+//   ]
+// }
+// ],
+//     origin_chart:'</table>{\"type\": \"bar\", \"title\": \"供电可靠率情况\", \"color\": \"xxx\", \"chartData\": [{\"name\": \"成都\", \"type\": \"bar\", \"value\": \"99.96%\"}, {\"name\": \"宜宾\", \"type\": \"bar\", \"value\": \"99.94%\"}, {\"name\": \"天府\", \"type\": \"bar\", \"value\": \"99.94%\"}, {\"name\": \"攀枝花\", \"type\": \"bar\", \"value\": \"99.94%\"}, {\"name\": \"遂宁\", \"type\": \"bar\", \"value\": \"99.93%\"}, {\"name\": \"自贡\", \"type\": \"bar\", \"value\": \"99.92%\"}, {\"name\": \"内江\", \"type\": \"bar\", \"value\": \"99.90%\"}, {\"name\": \"南充\", \"type\": \"bar\", \"value\": \"99.90%\"}, {\"name\": \"德阳\", \"type\": \"bar\", \"value\": \"99.90%\"}, {\"name\": \"绵阳\", \"type\": \"bar\", \"value\": \"99.89%\"}, {\"name\": \"达州\", \"type\": \"bar\", \"value\": \"99.89%\"}, {\"name\": \"眉山\", \"type\": \"bar\", \"value\": \"99.87%\"}, {\"name\": \"广元\", \"type\": \"bar\", \"value\": \"99.87%\"}, {\"name\": \"巴中\", \"type\": \"bar\", \"value\": \"99.87%\"}, {\"name\": \"乐山\", \"type\": \"bar\", \"value\": \"99.86%\"}, {\"name\": \"泸州\", \"type\": \"bar\", \"value\": \"99.85%\"}, {\"name\": \"雅安\", \"type\": \"bar\", \"value\": \"99.85%\"}, {\"name\": \"广安\", \"type\": \"bar\", \"value\": \"99.85%\"}, {\"name\": \"资阳\", \"type\": \"bar\", \"value\": \"99.84%\"}, {\"name\": \"凉山\", \"type\": \"bar\", \"value\": \"99.65%\"}, {\"name\": \"阿坝\", \"type\": \"bar\", \"value\": \"99.34%\"}, {\"name\": \"甘孜\", \"type\": \"bar\", \"value\": \"98.81%\"}, {\"name\": \"岷江\", \"type\": \"bar\", \"value\": \"100.00%\"}]}<table/>'
+//   },
+
 ]);
 const chat_new_list = ref([])
 const chat_log_list = ref([])
@@ -62,6 +228,266 @@ const chat_display_list = computed(() => {
   return [...moke_data.value].concat(chat_log_list.value, chat_new_list.value);
 });
 
+/**
+ * 新对话,客户端的提问数据
+ * 用以临时保存客户端的提问数据
+ * 每当用户确认进行提问时,将新提问赋给此值
+ * 一旦对话消息通过sse发送到服务器,此值可清空
+ */
+const chat_new_question = ref({});
+/**
+ * 对话区域滚动到底部
+ */
+const list_scroller = ref(null);
+const scroll2ListBottom = () => {
+  // if (!shouldAutoScroll.value) return;
+  nextTick(() => {
+    if (list_scroller.value) {
+      // list_scroller.value.scrollTop = list_scroller.value.scrollHeight;
+      list_scroller.value.scrollTo({
+        top: list_scroller.value.scrollHeight,
+        behavior: 'smooth' // 平滑滚动动画
+      });
+    }
+  });
+};
+
+/**
+ * input的一些操作方法
+ */
+const inputClearFn1 = useInputClear('dialog_input'); // 主表单清空
+
+// 初始化sse相关方法
+const { startZnwsStream, stopZnwsStream } = useFetchEventSourceZnws();
+
+/**
+ * 智能问数提交
+ * */
+const handleSubmit = async (options) => {
+  let ask_text = options.text; // 文本
+  let ask_files = options.files || [];
+  /**
+   * 当新消息发送时
+   * 意味着新一轮对话的开始
+   * 此时应该将之前一轮对话的信息进行清空
+   *
+   * 整理客户端提问的数据
+   * 组装Question数据
+   * */
+  const client_question = formatDialogQuestionByClient(ask_text, ask_files);
+  // 保存用户的提问信息
+  // 当消息正式发出后,将提问数据放入列表中
+  chat_new_question.value = client_question;
+  // 消息即将发送,记录用户的提问数据
+  chat_new_list.value.push(chat_new_question.value);
+  // 客户端答案数据(loading状态),客户端模拟的答案数据,依据此数据展示加载中的动画
+  // if (chat_type.value !== 'bgcl') {
+  const answerLoadingMsg = formatDialogAnswerForLoading();
+  chat_new_list.value.push(answerLoadingMsg);
+    // 清空文本数据和文件数据,避免下一次会话冲突
+  inputClearFn1();
+  // 清空已保存的提问数据(提问数据已经推入数组,此处无需继续保存)
+  chat_new_question.value = null;
+  // 文件上传、文本输入区域禁用
+  // input_disabled.value = true;
+  input_ing.value = true;
+  // 对话区域滚动到底部
+  scroll2ListBottom();
+  if (chat_type.value == 'znws') {
+    if (!WS_session.value.conversation_id) {
+      try {
+        const chatResult = await getDialogSessionId_AL({
+          app_id: window.APP_ID_AL_WS,
+          app_version: window.APP_VERSION_AL_WS
+        });
+        if (chatResult.code / 1 === 200) {
+          WS_session.value.conversation_id = chatResult.data.data.uniqueCode;
+          // session_id.value = chatResult.data.request_id;
+        } else {
+          // 失败
+          // 处理异常:直接在界面上显示
+          handleChatRequestionException({
+            message: `code: ${chatResult.errorCode} message: ${chatResult.errorMsg}`
+          });
+          return;
+        }
+      } catch (e) {
+        // 处理错误
+        handleChatRequestionException({
+          message: e.message
+        });
+        return;
+      }
+    }
+    agentUsedStore.updateAgentUsed('znws');
+    const username = await getUserName();
+    const power_id = await getUserId();
+    const query_params = {
+      conversation_id: WS_session.value.conversation_id,
+      user_id: convertToLowerCase(username),
+      query: ask_text,
+      app_id: window.APP_ID_AL_WS,
+      power_id
+    };
+    startZnwsStream(query_params, sseOnMessageZnws);
+    return;
+  }
+}
+let markdown_chart = null;
+//智能问数sse返回内容
+const sseOnMessageZnws = (event) => {
+  const res_data = JSON.parse(event.data);
+  // let markdown_chart = null;
+  if (res_data.metadata && res_data.metadata.markdown_chart) {
+    console.log(res_data.metadata.markdown_chart);
+    markdown_chart = JSON.parse(JSON.stringify(res_data.metadata.markdown_chart));
+    markdown_chart.origin_chart = `</table>${JSON.stringify(res_data.metadata.markdown_chart)}<table/>`
+  }
+  if (res_data.end) {
+    stopResponse();
+  }
+  if (res_data.object && res_data.object === 'error') {
+    handleChatRequestionException({
+      message: `code: ${res_data.content.errorCode} message: ${res_data.content.errorMsg}`
+    });
+  }
+  if(!res_data) {
+     handleChatRequestionException({
+      message: `message: 接口内容返回失败,请稍后重试`
+    });
+  }
+  // TODO: 将返回数据填充到页面
+
+  // 获取当前对话中最后一个对话数据
+  // 最后一个对话数据为当前正在进行的对话
+  const last_msg = chat_new_list.value[chat_new_list.value.length - 1];
+  /**
+   * 整理返回的数据
+   *
+   */
+  const currentChat = formatDialogAnswerFromSseReceiveMessageZnws(
+    res_data?.content,
+    last_msg,
+    res_data.end,
+    markdown_chart
+  );
+  const chat_status = currentChat.status;
+
+  // 处理对话数据
+  const handleChatData = () => {
+    const chat_data = currentChat.data; // 对话相关的数据项
+    // TODO: 为演示使用,判断是报告生成,新增两个字段一个是否有图标,另一个是文档数据
+    // 原来的最后一项弹出
+    chat_new_list.value.pop();
+    // 添加新的最后一项
+    chat_new_list.value.push(chat_data);
+
+    // 滚动到底部
+    scroll2ListBottom();
+  };
+
+  if (chat_status === 0) {
+    // 未开始: 此状态不存在,无需处理
+  }
+
+  if (chat_status === 1) {
+    // 此轮对话仍然在进行中
+    handleChatData();
+    // show_stop_sse.value = true;
+  }
+
+  if (chat_status === 2) {
+    // 此轮对话已结束
+    handleChatData();
+
+    // 对话已结束:文件上传、文本输入区域放开使用
+    // input_disabled.value = false;
+    input_ing.value = false;
+    // show_stop_sse.value = false;
+    emit('updateHistory');
+  }
+
+  if (chat_status === 3) {
+    // 此轮对话出错了
+    // TODO: 错误处理
+
+    // 此轮对话已结束
+    handleChatData();
+    // 对话已结束:文件上传、文本输入区域放开使用
+    input_disabled.value = false;
+    input_ing.value = false;
+    // show_stop_sse.value = false;
+    emit('updateHistory');
+  }
+
+  // 处理错误显示
+  if (
+    res_data.result === 'error' ||
+    res_data.result?.content === 'error' ||
+    res_data.result?.content?.outputs === 'error'
+  ) {
+    //sse错误,显示错误信息
+    handleChatRequestionException({
+      message: res_data.error
+    });
+    return;
+  }
+};
+
+/**
+ * 停止
+ * */
+const stopResponse = () => {
+  stopZnwsStream();
+  input_ing.value = false;
+  const lastItem = chat_new_list.value.pop();
+  if (lastItem) {
+    lastItem.client_custom_chat_status = 2;
+    chat_new_list.value.push(lastItem);
+  }
+  scroll2ListBottom();
+}
+const convertToLowerCase = (str) => {
+  return str.toLowerCase();
+};
+
+/**
+ * 异常处理
+ * 异常处理过程是一个会话模拟的过程)
+ * 先在界面上显示加载动画,然后紧接着显示错误提示
+ * 让用户感知到智能体有处理数据的过程
+ * 所以在异常处理阶段,有一些需要处理的关键点,比如欢迎界面的显示(如果在显示,则需要隐藏,否则对话显示不了)
+ * @param exceptionInfo
+ */
+const handleChatRequestionException = (exceptionInfo) => {
+  // 已结束
+  const answerData = formatDialogAnswerForException(exceptionInfo);
+
+  // 原来的最后一项弹出
+  chat_new_list.value.pop();
+  // 添加新的最后一项
+  chat_new_list.value.push(answerData);
+
+  // 对话已结束:文件上传、文本输入区域放开使用
+  input_ing.value = false;
+  // 对话区域滚动到底部
+  scroll2ListBottom();
+};
+
+const insert = (data) => {
+  emit('insertEchart', data);
+};
+//获取头像
+const getAgentIconsFun = async () => {
+  const username = await getUserName();
+  // target_agent_icon.value = '';
+  const res = await getAgentIcons({ username });
+  target_icon.value = res.data.user_icon['03']?.user_icon || 'icon_1'
+  console.log(target_icon.value)
+};
+onMounted(()=>{
+  getAgentIconsFun()
+})
 
 </script>
 <style scoped lang="css">

+ 173 - 75
src/modules/report/Editor.vue

@@ -18,7 +18,7 @@
       @onChange="handleChange"
     />
     <!-- 隐藏的图片上传input -->
-    <input type="file" ref="imageUploadRef" accept="image/*" style="display: none" @change="handleImageUpload" />
+    <!-- <input type="file" ref="imageUploadRef" accept="image/*" style="display: none" @change="handleImageUpload" /> -->
   </div>
   <AIModal
     v-if="show_ai_modal"
@@ -32,34 +32,55 @@
 <script setup>
 import '@wangeditor/editor/dist/css/style.css'; // 引入 css
 
-import { onBeforeUnmount, ref, shallowRef, onMounted, watch, nextTick } from 'vue';
+import { onBeforeUnmount, ref, shallowRef, onMounted, watch, nextTick, computed } from 'vue';
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
-import RobotIcon from '../../assets/tray-icon.png';
+// import RobotIcon from '../../assets/tray-icon.png';
+import RobotIcon_a from '../../assets/icon-robot/icon_green_a-1.png';
+import RobotIcon_b from '../../assets/icon-robot/icon_blue_b-1.png';
+import RobotIcon_c from '../../assets/icon-robot/icon_blue_c-1.png';
+import { THEME_LIGHT, THEME_DARK, THEME_GREEN, THEME_PARTY } from '../../base/variables';
 import { str_html } from './text';
-
+import { Message } from '@arco-design/web-vue';
 import AIModal from './AIModal.vue';
+import { useThemeStore } from '../../base/store/use-theme';
 
 const props = defineProps({
-  target_active_item: Object
+  target_active_item: Object,
+  html_content: String,
+  report_title: String
 });
 
-const emit = defineEmits(['showMenus']);
+const emit = defineEmits(['showMenus', 'changeSaveStatus']);
+
+const themeStore = useThemeStore();
 
 // 编辑器实例,必须用 shallowRef
 const editorRef = shallowRef();
 const toolbarRef = ref(null);
 const editorContentRef = ref();
 // 图片上传input的ref
-const imageUploadRef = ref(null);
+// const imageUploadRef = ref(null);
 
 // 内容 HTML
 const valueHtml = ref('');
+const pre_html_content = ref('');
 // 目录
 const menus = ref([]);
 
 // 当前要AI转写的P标签
 const target_active_pElement = ref(null);
 const show_ai_modal = ref(false);
+const RobotIcon = computed(() => {
+  if (themeStore.value == THEME_GREEN) {
+    return RobotIcon_a;
+  } else if (themeStore.value == THEME_PARTY) {
+    return RobotIcon_b;
+  } else if (themeStore.value == THEME_LIGHT) {
+    return RobotIcon_c;
+  } else {
+    return RobotIcon_a;
+  }
+});
 
 // 模拟 ajax 异步获取内容
 onMounted(() => {});
@@ -108,6 +129,13 @@ const editorConfig = {
         });
       }
     }
+    // image: {
+    //   // 清空 image 元素的 hoverbar
+    //   menuKeys: ['imageWidth30', 'imageWidth50', 'imageWidth100', 'deleteImage']
+    // },
+    // link: {
+    //   menuKeys: []
+    // }
   }
 };
 
@@ -120,7 +148,7 @@ onBeforeUnmount(() => {
 
 const handleCreated = (editor) => {
   editorRef.value = editor; // 记录 editor 实例,重要!
-
+  // console.log(editor);
   // 修改工具栏配置,保留图片上传按钮
   toolbarConfig.excludeKeys = [
     'insertImage',
@@ -135,19 +163,25 @@ const handleCreated = (editor) => {
     'insertTable',
     'editImage',
     'editLink',
-    'viewImageLink'
+    'viewImageLink',
+    'codeBlock'
   ];
-  console.log(editor.getAllMenuKeys());
+  // console.log(editor.getConfig().hoverbarKeys);
+  // console.log(editor.getAllMenuKeys());
 
-  valueHtml.value = str_html;
+  // valueHtml.value = str_html;
+  // valueHtml.value = props.html_content;
   // 让所有的h1居中
-  toAlginCenterByH1();
+  // toAlginCenterByH1();
   nextTick(() => {
     bindPElementEvents();
   });
 };
 
 const handleChange = () => {
+  if (pre_html_content.value !== valueHtml.value) {
+    emit('changeSaveStatus');
+  }
   menus.value = generateTableOfContents();
   emit('showMenus', menus);
 
@@ -198,19 +232,21 @@ const toAlginCenterByH1 = () => {
     }
     count += 1;
     if (editorRef.value.getHtml()) {
-      console.log(document.querySelectorAll('.editor_content h1'));
+      // console.log(document.querySelectorAll('.editor_content h1'));
       const headers = document.querySelectorAll('.editor_content h1');
       headers.forEach((item, index) => {
         if (index + 1 == item.id.split('-')[1]) {
           item.setAttribute('style', 'text-align: center ');
         }
         if (item.id.split('-')[1] / 1 === 1) {
-          console.log(item.innerText);
+          if (props.report_title) return;
           emit('renderTitle', item.innerText);
         }
       });
       editorRef.value.updateView();
       count = 11;
+      pre_html_content.value = valueHtml.value;
+      // console.log(pre_html_content.value == valueHtml.value, 99);
     }
   }, 300);
 };
@@ -239,11 +275,12 @@ const generateTableOfContents = () => {
 };
 
 const getHTML = () => {
-  console.log(editorRef.value.getHtml());
+  // console.log(editorRef.value.getHtml());
   menus.value = generateTableOfContents();
   emit('showMenus', menus);
 };
 
+// 点击目录 锚点定位编辑器内容位置
 const handleItemClick = (item) => {
   const editorContent = document.querySelector('.editor_content');
   const targetElement = document.getElementById(item.id);
@@ -271,49 +308,54 @@ const getAllPTags = (parentNode) => {
 };
 
 const handleMouseEnter = (pElement) => {
+  // TODO 这里后面可以改一下,当鼠标移入是不加光标,当点击左侧按钮时再加入光标
   const hasImg = pElement.querySelector('img') !== null;
-  if (!hasImg) {
-    // 添加光标到段落末尾
-    const range = document.createRange();
-    const selection = window.getSelection();
-    range.selectNodeContents(pElement);
-    range.collapse(false); // 折叠到元素末尾
-    selection.removeAllRanges();
-    selection.addRange(range);
-  }
 
-  setTimeout(() => {
-    if (!pElement.innerText.trim()) {
-      return;
-    }
-    pElement.style.background = 'rgba(10, 129, 126, 0.3)';
-    // 若已存在按钮,则不再添加
-    const existingBtn = pElement.querySelector('.ai-btn');
-    if (!existingBtn) {
-      const btn = document.createElement('img');
-      btn.classList.add('ai-btn');
-      btn.style.position = 'absolute';
-      btn.style.height = '40px';
-      btn.style.top = '-10px';
-      btn.style.left = '-45px';
-      btn.style.padding = '5px 10px';
-      btn.src = RobotIcon;
-      btn.style.color = '#fff';
-      btn.style.border = 'none';
-      btn.style.cursor = 'pointer';
-      btn.style.borderRadius = '4px';
-      btn.addEventListener('click', () => {
+  // setTimeout(() => {
+  if (!pElement.innerText.trim()) {
+    return;
+  }
+  pElement.style.background = 'var(--editor_hover)';
+  // 若已存在按钮,则不再添加
+  const existingBtn = pElement.querySelector('.ai-btn');
+  if (!existingBtn) {
+    // console.log(RobotIcon, 666);
+    const btn = document.createElement('img');
+    btn.classList.add('ai-btn');
+    btn.style.position = 'absolute';
+    btn.style.height = '40px';
+    btn.style.top = '-10px';
+    btn.style.left = '-45px';
+    btn.style.padding = '5px 10px';
+    btn.src = RobotIcon.value;
+    btn.style.color = '#fff';
+    btn.style.border = 'none';
+    btn.style.cursor = 'pointer';
+    btn.style.borderRadius = '4px';
+    btn.style.zIndex = '9999';
+    btn.addEventListener('click', () => {
+      if (!hasImg) {
+        // 添加光标到段落末尾
+        const range = document.createRange();
+        const selection = window.getSelection();
+        range.selectNodeContents(pElement);
+        range.collapse(false); // 折叠到元素末尾
+        selection.removeAllRanges();
+        selection.addRange(range);
+      }
+      setTimeout(() => {
         target_active_pElement.value = pElement;
         show_ai_modal.value = true;
-      });
-      //设置p元素为相对定位,确保按钮能相对于P元素定位
-      pElement.style.position = 'relative';
-      pElement.appendChild(btn);
-      btn.addEventListener('mouseleave', () => handleMouseLeave(pElement));
-      setTimeout(() => {}, 50);
-      console.log(editorRef.value.selection, 9999);
-    }
-  }, 50);
+      }, 50);
+    });
+    //设置p元素为相对定位,确保按钮能相对于P元素定位
+    pElement.style.position = 'relative';
+    pElement.appendChild(btn);
+    btn.addEventListener('mouseleave', () => handleMouseLeave(pElement));
+    // setTimeout(() => {}, 50);
+    // console.log(editorRef.value.selection, 9999);
+  }
+  // }, 50);
 };
 
 const handleMouseLeave = (pElement) => {
@@ -339,7 +381,7 @@ let offset = 0;
 const changePElement = (obj) => {
   if (editorRef.value) {
     const editor = editorRef.value;
-    console.log(editor.selection);
+    // console.log(editor.selection);
     const targetP = document.querySelector(`p#${obj.pid}`);
     if (targetP) {
       // 创建一个临时Range来选中整个p元素
@@ -349,7 +391,7 @@ const changePElement = (obj) => {
       // 将DOM范围转换为编辑器选区
       editor.restoreSelection(range);
 
-      offset = Number(editor.selection.focus.offset);
+      offset = Number(editor.selection.focus.offset) || 0;
       // 删除选中内容
       for (let i = 0; i <= offset; i++) {
         editor.deleteBackward();
@@ -359,7 +401,7 @@ const changePElement = (obj) => {
       // 替换为新内容
       editor.dangerouslyInsertHtml(obj.content.innerHTML);
       // targetP.innerHTML = obj.content.innerHTML;
-      console.log(editor.getHtml());
+      // console.log(editor.getHtml());
     }
     // }
   }
@@ -375,9 +417,10 @@ const insertPElement = (obj) => {
     const targetP = editorContentRef.value.$el.querySelector(`p#${obj.pid}`);
     if (targetP) {
       // targetP.innerHTML += obj.content.innerHTML;
+      // console.log(obj);
       editor.dangerouslyInsertHtml(obj.content.innerHTML);
       editor.updateView();
-      console.log(editor.getHtml());
+      // console.log(editor.getHtml());
     }
   }
   closeAIModal();
@@ -403,34 +446,74 @@ const convertImageToBase64 = (file) => {
 };
 
 // 处理图片上传
-const handleImageUpload = async (e) => {
-  const file = e.target.files[0];
-  if (!file) return;
+// const handleImageUpload = async (e) => {
+//   const file = e.target.files[0];
+//   if (!file) return;
+//   console.log(11111111111111);
+//   try {
+//     const editor = editorRef.value;
+//     if (!editor) {
+//       console.error('编辑器实例不存在');
+//       return;
+//     }
+
+//     // 转换为base64
+//     const base64Url = await convertImageToBase64(file);
+//     console.log(editor);
+//     // 插入图片到编辑器
+//     editor.insertImage(base64Url, file.name);
+
+//     // 图片插入后重新绑定事件
+//     nextTick(() => {
+//       bindPElementEvents();
+//     });
+//   } catch (error) {
+//     console.error('图片上传失败:', error);
+//     alert('图片上传失败,请重试');
+//   }
+
+//   // 重置input值,以便可以重复上传同一文件
+//   e.target.value = '';
+// };
+// 插入图片
+const insertPic = async (base64) => {
+  // 1. 验证编辑器实例是否初始化
+  if (!editorRef.value) {
+    console.error('编辑器尚未初始化');
+    return false;
+  }
+
+  // 2. 验证base64格式是否有效
+  if (!base64 || !base64.startsWith('data:image/')) {
+    console.error('无效的base64图片格式(需以data:image/开头)');
+    return false;
+  }
 
   try {
     const editor = editorRef.value;
-    if (!editor) {
-      console.error('编辑器实例不存在');
+
+    // 3. 如果没有选中区域,将光标移到文档末尾
+    if (!editor.selection) {
+      // editor.moveCursorToEnd(); // 移动光标到末尾
+      // alert('请选择插入位置');
+      Message.error('请选择图片插入位置');
       return;
     }
 
-    // 转换为base64
-    const base64Url = await convertImageToBase64(file);
-
-    // 插入图片到编辑器
-    editor.insertImage(base64Url, file.name);
+    // 4. 生成图片HTML标签(使用dangerouslyInsertHtml插入)
+    const imgHtml = `<img src="${base64}" alt="插入的图片" style="max-width:100%;height:auto;margin:10px 0;" />`;
+    editor.dangerouslyInsertHtml(imgHtml); // 核心:插入图片HTML
 
-    // 图片插入后重新绑定事件
+    // 5. 插入后重新绑定事件(保持原有逻辑)
     nextTick(() => {
       bindPElementEvents();
     });
+
+    return true;
   } catch (error) {
-    console.error('图片上传失败:', error);
-    alert('图片上传失败,请重试');
+    console.error('插入图片失败:', error);
+    return false;
   }
-
-  // 重置input值,以便可以重复上传同一文件
-  e.target.value = '';
 };
 
 watch(
@@ -441,6 +524,14 @@ watch(
     }
   }
 );
+watch(
+  () => props.html_content,
+  (newValue) => {
+    valueHtml.value = newValue;
+    pre_html_content.value = valueHtml.value;
+    toAlginCenterByH1();
+  }
+);
 defineExpose({
   getHtml: () => {
     if (editorRef.value) {
@@ -448,7 +539,11 @@ defineExpose({
     } else {
       return '';
     }
-  }
+  },
+  // insertEchartPic: ()=>{
+  // 插入base64图片到富文本
+  insertEchartPic: insertPic
+  // }
 });
 </script>
 <style scoped lang="css">
@@ -476,6 +571,9 @@ defineExpose({
   -ms-overflow-style: none;
   /* scrollbar-width: none; */
 }
+:deep(.w-e-text-container .w-e-text-placeholder) {
+  padding: 20px 40px;
+}
 
 /* 为图片添加一些样式 */
 :deep(.w-e-image-container img) {

+ 8 - 2
src/modules/report/EditorMenu.vue

@@ -12,6 +12,9 @@
         }}</a>
       </li>
     </ul>
+    <div class="empty" v-if="!editor_menus.length">
+      <a-empty></a-empty>
+    </div>
   </div>
 </template>
 
@@ -84,11 +87,14 @@ watch(
   padding: 5px 10px;
 }
 .list-item a:hover {
-  background: rgba(10, 129, 126, 0.1);
+  background: var(--question_bg);
   border-radius: 8px;
 }
 .active {
-  background: rgba(10, 129, 126, 0.3);
+  background: var(--editor_hover);
   border-radius: 8px;
 }
+.empty {
+  margin-top: 120px;
+}
 </style>

+ 20 - 6
src/modules/report/conversation-znws/Answer.vue

@@ -6,7 +6,12 @@
       </div>
       <div class="answer-item-content" v-if="chat_type === 'znws'">
         <!-- 智能问数的组件渲染和报告生成的一致,先使用同一个,后续有变动再单独拆分 -->
-        <DialogZnwsContent :is_loading="is_content_loading" :type="type" :result_list="report_content">
+        <DialogZnwsContent
+          :is_loading="is_content_loading"
+          :type="type"
+          :result_list="report_content"
+          @insertEchart="insert"
+        >
           <DialogActions
             v-if="answer_status == 2"
             class="answer-actions-btns"
@@ -50,6 +55,8 @@ const props = defineProps({
   }
 });
 
+const emit = defineEmits(['insertEchart']);
+
 const themeStore = useThemeStore();
 
 const target_icon = computed(() => {
@@ -63,10 +70,9 @@ const target_icon = computed(() => {
     return getIcon(props.target_agent_icon, 'a');
   }
 });
-console.log(themeStore.value, THEME_PARTY, props.target_agent_icon, 909);
 
 const report_content = computed(() => {
-  // console.log(props.dialog)
+  console.log(props.dialog, 222);
 
   const result_list = props.dialog.result_list;
   // console.log(result_list)
@@ -105,12 +111,16 @@ const is_content_loading = computed(() => {
   return answer_status.value === 0;
 });
 const handleReportCopy = () => {
-  const result_list = props.dialog.result_list;
-  const copy_text = result_list.filter((item) => typeof item == 'string').join('');
+  const result_list = props.dialog.result_list || [];
+  const copy_text = result_list?.filter((item) => typeof item == 'string').join('');
   copyText(copy_text);
   Message.success('复制成功');
 };
 
+const insert = (data) => {
+  emit('insertEchart', data);
+};
+
 onMounted(() => {});
 </script>
 <style scoped lang="css">
@@ -133,6 +143,7 @@ onMounted(() => {});
 }
 
 .answer-item-content {
+  width: 100%;
   max-width: calc(100% - 62px - 62px + 55px);
 }
 
@@ -155,10 +166,13 @@ onMounted(() => {});
 .answer-item-icon img {
   margin-right: 22px;
   /* border-radius: 50%; */
-  width: 40px;
+  width: 30px;
   /* height: 30px; */
   flex-shrink: 0;
   cursor: pointer;
   margin-top: 15px;
 }
+.answer-actions-btns {
+  margin-top: 20px;
+}
 </style>

+ 6 - 0
src/modules/report/conversation-znws/ChatItem.vue

@@ -6,6 +6,7 @@
       :dialog="dialog"
       :type="type"
       :target_agent_icon="target_agent_icon"
+      @insertEchart="insert"
     ></Answer>
   </div>
 </template>
@@ -30,6 +31,11 @@ const props = defineProps({
     default: 'icon_1'
   }
 });
+const emit = defineEmits(['insertEchart']);
+
+const insert = (data) => {
+  emit('insertEchart', data);
+};
 </script>
 <style scoped lang="css">
 .chat-item {

+ 6 - 0
src/modules/report/conversation-znws/ChatList.vue

@@ -6,6 +6,7 @@
       :dialog="item"
       :type="type"
       :target_agent_icon="target_agent_icon"
+      @insertEchart="insert"
     ></ChatItem>
   </div>
 </template>
@@ -27,6 +28,11 @@ const props = defineProps({
     default: 'icon_1'
   }
 });
+const emit = defineEmits(['insertEchart']);
+
+const insert = (data) => {
+  emit('insertEchart', data);
+};
 </script>
 <style scoped lang="css">
 .chat-list {

+ 2 - 1
src/modules/report/conversation-znws/DialogContent.vue

@@ -74,7 +74,8 @@ onMounted(() => {});
   line-height: 24px;
 }
 
-.loading {
+.dialog-chat-answer-content .loading {
+  margin-top: 20px;
   display: flex;
   align-items: center;
   gap: 10px;

+ 32 - 9
src/modules/report/conversation-znws/DialogZnwsContent.vue

@@ -3,11 +3,21 @@
     <div v-if="is_loading && type == 'znws'" class="loading"><a-spin></a-spin> <span>正在思考中...</span></div>
     <div class="answer-content-text answer-content-report" v-for="(item, index) in result_list" :key="index">
       <div class="answer-content" v-if="typeof item === 'string'" v-html="renderHtml(item)"></div>
-      <HomeChart v-if="typeof item === 'object'" :data="item"></HomeChart>
+      <HomeChart v-if="typeof item === 'object'" :data="item">
+        <template #insertBtn>
+          <a-button @click="toInsert(item)" :loading="loading" :disabled="loading">
+            <img :src="InsertIcon" alt="" v-if="!loading" />
+            <span>插入</span>
+          </a-button>
+        </template>
+      </HomeChart>
     </div>
+    <!-- <div class="answer-content-text answer-content-report" v-if="result_list && !result_list.length" :key="index">
+      <div class="answer-content" v-html="暂无内容"></div>
+    </div> -->
     <slot></slot>
     <div class="report-loading" v-if="answer_status === 1">
-      <img src="../../assets/chat-report/report-ing.png" alt="" />
+      <img src="../../../assets/chat-report/report-ing.png" alt="" />
       <!-- <span>报告生成中...</span> -->
     </div>
   </div>
@@ -15,9 +25,11 @@
 </template>
 
 <script setup>
-import { computed, onMounted } from 'vue';
+import { ref, computed, onMounted } from 'vue';
 import HomeChart from '../../chat_echart/HomeChart.vue';
 import marked from '../../../3rd-libs/marked';
+import InsertIcon from '../../../assets/ai-editor/insert-icon.svg';
+import { createChartPic } from '../../../modules/conversation-dialog/api-dialog';
 // 接收markdown数据,进行一些转换
 const props = defineProps({
   /**
@@ -38,6 +50,10 @@ const props = defineProps({
   answer_status: Boolean
 });
 
+const emit = defineEmits(['insertEchart']);
+
+const loading = ref(false);
+
 const content_style = computed(() => {
   if (props.type === 'q') {
     return {
@@ -46,13 +62,7 @@ const content_style = computed(() => {
     };
   }
 });
-// const html_1 = computed(()=>{
-//   const demo =
-// "\n  \n  ## 五、配电自动化应用建设情况\n  上周全省FA有效覆盖率60.04%,其中较高的单位有:攀枝花(77.17%),遂宁(76.22%),自贡(75.79%);较低的单位有:内江(45.78%),甘孜(32.47%),岷江(0.0%)。\n\n 上周全省全自动FA覆盖率,其中较高的单位有:雅安(51.42%),成都(29.32%),眉山(17.18%);较低的单位有:遂宁(0.0%),天府(0.0%),岷江(0.0%)。\n上周全省遥控使用率31.50%,其中较高的单位有:遂宁(95.65%),广安(92.86%),眉山(86.52%);较低的单位有:德阳(0%),甘孜(0.0%),岷江(0%)。 \n上周全省FA动作正确率51.86%,其中较高的单位有:攀枝花(76.56%),自贡(70.0%),泸州(69.33%);较低的单位有:乐山(20.83%),德阳(0%),岷江(0%)。\n  \n "
-//   console.log(marked.parse(demo),5555555555)
-// return marked.parse(demo)
 
-// })
 const renderHtml = (item) => {
   let renderContent = marked.parse(item);
   // renderContent = renderContent.replace(/^ {2}/gm, '&nbsp;&nbsp;&nbsp;&nbsp;');
@@ -70,6 +80,18 @@ const renderHtml = (item) => {
   //   .replace(/<\/code><\/pre>/gs, '</p>');
   return renderContent;
 };
+
+const toInsert = async (item) => {
+  loading.value = true;
+  const params = {
+    type: 'table',
+    data: item.origin_chart
+  };
+  const res = await createChartPic(params);
+  // console.log(res.data.chart_buf);
+  emit('insertEchart', res.data.chart_buf);
+  loading.value = false;
+};
 onMounted(() => {});
 </script>
 
@@ -107,6 +129,7 @@ p {
 }
 
 .loading {
+  margin-top: 20px;
   display: flex;
   align-items: center;
   gap: 10px;

+ 2 - 1
src/modules/report/conversation-znws/Question.vue

@@ -6,7 +6,7 @@
         <DialogContent :content="dialog.question" :type="dialog.client_custom_item_type"></DialogContent>
       </div>
       <div class="question-item-icon">
-        <img src="../../assets/user.png" alt="" />
+        <!-- <img src="../../assets/user.png" alt="" /> -->
       </div>
     </div>
   </div>
@@ -31,6 +31,7 @@ const props = defineProps({
   display: flex;
   justify-content: flex-end;
   align-items: flex-start;
+  margin-top: 15px;
 }
 
 .question-file-list {

+ 18 - 0
src/modules/report/use-input-clear.js

@@ -0,0 +1,18 @@
+import { ref, getCurrentInstance, onMounted, onUnmounted, computed } from 'vue';
+
+/**
+ * 获取对话区域的DOM元素尺寸
+ * @param {string} ref_name
+ * @returns
+ */
+export default function useInputClear(ref_name) {
+  const { proxy } = getCurrentInstance();
+
+  return () => {
+    const target = proxy.$refs[ref_name];
+
+    if (target && target.clear && typeof target.clear === 'function') {
+      target.clear();
+    }
+  };
+}

+ 12 - 3
src/views/Agent/AgentCentric.vue

@@ -54,7 +54,10 @@ const getAuthFun = async () => {
         opinion_work_order_target_value: res.data.AI_BGSC_YJ || 0, //意见工单目标值管理
         ftu_value: res.data.AI_BGSC_FTU || 1, // FTU
         dtu_value: res.data.AI_BGSC_DTU || 1, // dTU
-        main_sub_station_value: res.data.AI_BGSC_STATION || 1 // dTU
+        main_sub_station_value: res.data.AI_BGSC_STATION || 1, // station
+        smart_integrated_terminal: res.data.AI_SIT || 1, //智能融合终端采购
+        leakage_protection_online: res.data.AI_LPO || 1, //漏保在线监测模块采购
+        power_monitoring_terminal: res.data.AI_PMT || 1 //动环监测终端采购
       };
       sessionStorage.setItem('btn_auth', JSON.stringify(auth.value));
       btnAuthStore.updateBtnAuthObj(auth.value);
@@ -69,7 +72,10 @@ const getAuthFun = async () => {
         opinion_work_order_target_value: res.data.AI_BGSC_YJ || 0, //意见工单目标值管理
         ftu_value: res.data.AI_BGSC_FTU || 1, // FTU
         dtu_value: res.data.AI_BGSC_DTU || 1, // dTU
-        main_sub_station_value: res.data.AI_BGSC_STATION || 1 // dTU
+        main_sub_station_value: res.data.AI_BGSC_STATION || 1, // station
+        smart_integrated_terminal: res.data.AI_SIT || 1, //智能融合终端采购
+        leakage_protection_online: res.data.AI_LPO || 1, //漏保在线监测模块采购
+        power_monitoring_terminal: res.data.AI_PMT || 1 //动环监测终端采购
       };
       sessionStorage.setItem('btn_auth', JSON.stringify(auth.value));
       btnAuthStore.updateBtnAuthObj(auth.value);
@@ -86,7 +92,10 @@ const getAuthFun = async () => {
       opinion_work_order_target_value: 1, //意见工单目标值管理
       ftu_value: res.data.AI_BGSC_FTU || 1, // FTU
       dtu_value: res.data.AI_BGSC_DTU || 1, // dTU
-      main_sub_station_value: res.data.AI_BGSC_STATION || 1 // station
+      main_sub_station_value: res.data.AI_BGSC_STATION || 1, // station
+      smart_integrated_terminal: res.data.AI_SIT || 1, //智能融合终端采购
+      leakage_protection_online: res.data.AI_LPO || 1, //漏保在线监测模块采购
+      power_monitoring_terminal: res.data.AI_PMT || 1 //动环监测终端采购
     };
     sessionStorage.setItem('btn_auth', JSON.stringify(auth.value));
     btnAuthStore.updateBtnAuthObj(auth.value);

+ 1 - 0
src/views/Report.vue

@@ -25,6 +25,7 @@ const is_electron = computed(() => {
 </script>
 <style scoped lang="css">
 .container-1 {
+  width: 100%;
   position: relative;
   display: flex;
   flex-direction: row;

+ 4 - 0
src/views/layout/LayoutExperienceReport.vue

@@ -54,6 +54,7 @@ if (window.parent !== window) {
 
 onMounted(() => {
   const newTheme = window.parent?.document?.body?.className;
+  console.log(newTheme, 111);
   if (newTheme.includes('themea')) {
     setThemeGreen();
   } else if (newTheme.includes('themeb')) {
@@ -63,6 +64,8 @@ onMounted(() => {
   } else {
     // 默認
     setThemeGreen();
+    // setThemeParty();
+    // setThemeLight();
   }
 });
 </script>
@@ -72,6 +75,7 @@ onMounted(() => {
   position: relative;
   display: flex;
   flex-direction: row;
+  width: 100%;
   height: 100vh;
   /* background-image: url('../../assets/bg/bg-green.svg');
   background-repeat: no-repeat;

+ 213 - 47
src/views/report/ReportDetail.vue

@@ -1,65 +1,119 @@
 <template>
   <div class="report-detail">
-    <ReportHeader :report_title="report_title" @updateTitle="renderTitle"></ReportHeader>
-    <div class="editor-detail">
-      <EditorMenu :editor_menus="editor_menus" @itemClick="itemClick"></EditorMenu>
-      <div class="editor-detail-editor">
-        <div class="editor-tools">
-          <div class="tools-left">
-            <div class="title">{{ report_title }}</div>
-            <div class="save">已保存{{ save_time }}</div>
-          </div>
-          <div class="tools-right">
-            <a-tooltip content="复制">
-              <img class="icon" @click="toCopy" src="../../assets/chat-report-detail/icon_copy.svg" alt="" />
-            </a-tooltip>
-            <a-tooltip content="下载">
-              <img class="icon" @click="toDownload" src="../../assets/chat-report-detail/icon_download.svg" alt="" />
-            </a-tooltip>
-            <!-- <img class="icon" src="../../assets/chat-report-detail/icon_send.svg" alt="" /> -->
+    <ReportHeader ref="reportHeaderRef" :report_title="report_title" @updateTitle="updateTitle"></ReportHeader>
+    <a-spin
+      :loading="is_loading"
+      tip="报告内容加载中..."
+      style="width: 100%; height: calc(100% - 53px); box-sizing: border-box"
+    >
+      <div class="editor-detail">
+        <EditorMenu :editor_menus="editor_menus" @itemClick="itemClick"></EditorMenu>
+        <div class="editor-detail-editor">
+          <div class="editor-tools">
+            <div class="tools-left">
+              <div class="title">{{ report_title }}</div>
+              <div class="save" v-if="save_status == -1"></div>
+              <div class="save" v-if="save_status == 0">
+                未保存
+                <a-tooltip content="使用Ctrl+S快捷键保存">
+                  <span class="shortcut-hint" @click="saveHtmlFunc">(Ctrl+S)</span>
+                </a-tooltip>
+              </div>
+              <div class="save" v-if="save_status == 1"><a-spin />保存中</div>
+              <div class="save" v-if="save_status == 2">已保存{{ save_time }}</div>
+            </div>
+            <div class="tools-right">
+              <a-tooltip content="复制">
+                <img class="icon" @click="toCopy" src="../../assets/chat-report-detail/icon_copy.svg" alt="" />
+              </a-tooltip>
+              <a-tooltip content="下载">
+                <img class="icon" @click="toDownload" src="../../assets/chat-report-detail/icon_download.svg" alt="" />
+              </a-tooltip>
+              <!-- <img class="icon" src="../../assets/chat-report-detail/icon_send.svg" alt="" /> -->
+            </div>
           </div>
+          <Editor
+            ref="my_editor"
+            :target_active_item="target_active_item"
+            :html_content="html_content"
+            :report_title="report_title"
+            @showMenus="showMenus"
+            @changeSaveStatus="changeSaveStatus"
+            @renderTitle="renderTitle"
+          ></Editor>
         </div>
-        <Editor
-          ref="my_editor"
-          :target_active_item="target_active_item"
-          @showMenus="showMenus"
-          @renderTitle="renderTitle"
-        ></Editor>
-      </div>
-      <div class="right-znws">
-        <!-- <router-view ref="conversationContainer" :mainStyle="mainStyle" /> -->
-        <!-- <ConversationChatContainer origin="report_detail" :mainStyle="mainStyle"> </ConversationChatContainer>
+
+        <div class="right-znws">
+          <!-- <router-view ref="conversationContainer" :mainStyle="mainStyle" /> -->
+          <!-- <ConversationChatContainer origin="report_detail" :mainStyle="mainStyle"> </ConversationChatContainer>
          -->
-        <ConversationZnwd></ConversationZnwd>
+          <ConversationZnwd @insertEchart="insert"></ConversationZnwd>
+        </div>
       </div>
-    </div>
+    </a-spin>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
+import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
+import { useRoute } from 'vue-router';
 import ReportHeader from './ReportHeader.vue';
 import EditorMenu from '../../modules/report/EditorMenu.vue';
 import Editor from '../../modules/report/Editor.vue';
 import ConversationChatContainer from '../conversation/ConversationChatContainer.vue';
 import ConversationZnwd from '../../modules/report/ConversationZnws.vue';
 
+import { getReportHtmlDetail, saveHtml } from '../../modules/conversation-dialog/api-dialog';
+
 import { asBlob } from 'html-docx-js-typescript';
 import { saveAs } from 'file-saver';
 import copyText from '../../3rd-libs/clipboard';
 import { Message } from '@arco-design/web-vue';
 
+const route = useRoute();
+
 const editor_menus = ref([]);
 const target_active_item = ref({});
 
-const report_title = ref('国网四川省电力公司供电可靠性周报');
-const save_time = ref('10:20');
+const report_title = ref('');
+
+// 保存时使用
+const id = ref('');
+const target_record_id = ref('');
 
 const my_editor = ref();
 
+const html_content = ref('');
+const is_loading = ref(false);
+/**
+ * 保存状态
+ * -1:不显示保存状态
+ * 0:未保存
+ * 1:保存中
+ * 2:已保存
+ * */
+const save_status = ref(-1);
+// 保存时间
+const save_time = ref('10:20');
+// 定时器,定时保存文本内容
+let timeout_interval = null;
+
+const reportHeaderRef = ref(null);
+
 const showMenus = (menus) => {
   editor_menus.value = menus.value;
-  console.log(editor_menus.value);
+  // console.log(editor_menus.value);
+};
+
+const changeSaveStatus = () => {
+  save_status.value = 0;
+  if (timeout_interval) {
+    clearTimeout(timeout_interval);
+    timeout_interval = null;
+  }
+  timeout_interval = setTimeout(() => {
+    saveHtmlFunc();
+  }, 1000 * 10);
 };
 const itemClick = (item) => {
   target_active_item.value = item;
@@ -74,6 +128,10 @@ const toCopy = () => {
 const renderTitle = (title) => {
   report_title.value = title;
 };
+const updateTitle = (title) => {
+  renderTitle(title);
+  saveHtmlFunc();
+};
 
 const toDownload = (item) => {
   const html = my_editor.value.getHtml();
@@ -91,26 +149,122 @@ const toDownload = (item) => {
       ${fullHtml}
     </body>
     </html>`;
-  // const page = `<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body> <h1>Welcome</h1></body></html>`;
   try {
-    console.log();
     asBlob(htmlString).then((data) => {
-      console.log(data);
-      saveAs(data, 'file.docx'); // save as docx file
-    }); // asBlob() return Promise<Blob|Buffer>
-    // 转换 HTML 为 docx blob
-    // const docxBlob = htmlDocx.asBlob(page);
-    // console.log(docxBlob);
-    // // 下载文件
-    // saveAs(docxBlob, `导出文档_${new Date().toLocaleDateString()}.docx`);
+      // console.log(data);
+      saveAs(data, `${report_title.value}.docx`); // save as docx file
+    });
   } catch (error) {
     console.error('导出 docx 失败:', error);
     alert('导出失败,可能存在过大的图片,请压缩后重试');
   }
 };
+
+const getReportHtml = async (record_id) => {
+  is_loading.value = true;
+  try {
+    const res = await getReportHtmlDetail(record_id);
+    // console.log(res);
+    if (res.code / 1 === 200) {
+      html_content.value = res.data.html_content;
+      id.value = res.data.id || 0;
+      // if (res.data?.record_id) {
+      //   record_id.value = res.data.record_id;
+      // }
+      if (res.data?.title) {
+        report_title.value = res.data.title;
+      }
+      // console.log(html_content.value, report_title.value, 99);
+    }
+  } catch (e) {
+    console.log(e, '错误');
+  } finally {
+    is_loading.value = false;
+  }
+};
+
+const getReportHtmlForSave = async () => {
+  const res = await getReportHtmlDetail(target_record_id.value);
+  // console.log(res);
+  if (res.code / 1 === 200) {
+    html_content.value = res.data.html_content;
+    id.value = res.data.id || 0;
+    // if (res.data?.record_id) {
+    //   record_id.value = res.data.record_id;
+    // }
+    if (res.data?.title) {
+      report_title.value = res.data.title;
+    }
+    // console.log(html_content.value, report_title.value, 99);
+  }
+};
+/**
+ * 富文本内容保存
+ *id, title, record_id, html_content
+ * */
+const saveHtmlFunc = async () => {
+  clearTimeout(timeout_interval);
+  timeout_interval = null;
+  if (save_status.value / 1 == 2) return;
+  save_status.value = 1;
+  const data = {
+    id: id.value,
+    title: report_title.value,
+    record_id: target_record_id.value,
+    html_content: my_editor.value.getHtml()
+  };
+  try {
+    const res = await saveHtml(data);
+    if (res.code / 1 === 200) {
+      // console.log(res);
+      save_status.value = 2;
+      save_time.value = getCurrentTime();
+      nextTick(() => {
+        getReportHtmlForSave();
+      });
+    }
+  } catch (e) {
+    save_status.value = 0;
+    Message.error('保存失败,请稍候重试');
+  } finally {
+  }
+};
+
+const handleSave = (e) => {
+  // 判断是否按下了Ctrl+S
+  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
+    e.preventDefault(); // 阻止浏览器默认的保存行为
+    saveHtmlFunc();
+    // 这里编写你的保存逻辑
+    // console.log('执行保存操作');
+    // 例如:调用API保存数据
+    // saveData();
+  }
+};
+
+function getCurrentTime() {
+  const now = new Date();
+  return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
+}
+
+const insert = (data) => {
+  // console.log('数据', data);
+  let base64 = 'data:image/png;base64,' + data;
+  my_editor.value.insertEchartPic(base64);
+};
+
+onMounted(() => {
+  target_record_id.value = route.params.record_id;
+  getReportHtml(target_record_id.value);
+  window.addEventListener('keydown', handleSave);
+});
+onUnmounted(() => {
+  document.removeEventListener('keydown', handleSave);
+});
 </script>
 <style scoped lang="css">
 .report-detail {
+  width: 100%;
   /* flex-basis: 1; */
   flex-grow: 1;
   background: #f5fafa;
@@ -120,7 +274,7 @@ const toDownload = (item) => {
 
 .editor-detail {
   width: 100%;
-  height: calc(100% - 50px);
+  height: calc(100%);
   display: flex;
   flex-direction: row;
 }
@@ -148,16 +302,16 @@ const toDownload = (item) => {
 }
 
 .tools-left .title {
-  font-family: SourceHanSansCN, SourceHanSansCN;
+  font-family: '思源黑体';
   /* font-weight: 500; */
-  font-size: 20px;
+  font-size: 16px;
   color: #181d1d;
 }
 
 .tools-left .save {
   font-family: SourceHanSansCN, SourceHanSansCN;
   font-weight: 400;
-  font-size: 16px;
+  font-size: 12px;
   color: #b0bebd;
 }
 
@@ -177,4 +331,16 @@ const toDownload = (item) => {
 .right-znws {
   width: 30%;
 }
+.shortcut-hint {
+  margin-left: 8px;
+  font-size: 12px;
+  color: #666;
+  padding: 2px 6px;
+  border-radius: 4px;
+  background-color: #f5f5f5;
+  cursor: pointer;
+}
+.shortcut-hint:hover {
+  opacity: 0.8;
+}
 </style>

+ 28 - 4
src/views/report/ReportHeader.vue

@@ -1,16 +1,22 @@
 <template>
   <div class="report-header">
-    <div class="left-title">
+    <div class="left-title" ref="headerTitleRef">
       <!-- <icon-left class="icon" @click="toBack" /> -->
       <img src="../../assets/chat-report-detail/icon_back.svg" class="icon" @click="toBack" alt="" />
       <div class="title">
-        <a-input ref="inputRef" :style="{ width: '320px' }" v-model="title" v-if="is_edit_title"></a-input>
+        <a-input
+          ref="inputRef"
+          :style="{ width: '320px' }"
+          v-model="title"
+          v-if="is_edit_title"
+          @blur="handleBlur"
+        ></a-input>
         <span v-else>{{ report_title }}</span>
       </div>
       <img
         src="../../assets/chat-report-detail/icon_edit.svg"
         alt=""
-        v-if="!is_edit_title"
+        v-if="!is_edit_title && show_edit"
         class="icon"
         @click="changeEdit"
       />
@@ -32,9 +38,10 @@
 </template>
 
 <script setup>
-import { ref, nextTick } from 'vue';
+import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue';
 import { useRouter } from 'vue-router';
 import Template from '../../modules/report/Template.vue';
+import { Message } from '@arco-design/web-vue';
 
 const router = useRouter();
 
@@ -47,6 +54,11 @@ const emit = defineEmits(['updateTitle']);
 const title_origin = ref('');
 const title = ref();
 const is_edit_title = ref(false);
+const headerTitleRef = ref(null);
+
+const show_edit = computed(() => {
+  return props.report_title;
+});
 
 const inputRef = ref(null);
 
@@ -62,6 +74,7 @@ const changeEdit = () => {
 };
 
 const changeCheck = () => {
+  if (!title.value) return Message.error('标题名称不能为空');
   emit('updateTitle', title.value);
   title_origin.value = title.value;
   is_edit_title.value = false;
@@ -75,6 +88,16 @@ const changeCancel = () => {
 const toBack = () => {
   router.go(-1);
 };
+
+const handleBlur = (e) => {
+  setTimeout(() => {
+    if ((is_edit_title.value = true)) {
+      changeCancel();
+    }
+  }, 500);
+};
+
+onMounted(() => {});
 </script>
 <style scoped lang="css">
 .report-header {
@@ -90,6 +113,7 @@ const toBack = () => {
   align-items: center;
   font-size: 18px;
   gap: 10px;
+  padding-left: 20px;
 }
 
 .icon {