Преглед изворни кода

feat:测试富文本数据展示,实现导出功能,进入自动识别标题,标题居中等,解决AI替换和插入内容bug等

韩洋 пре 10 месеци
родитељ
комит
3a73f7bf58
32 измењених фајлова са 1927 додато и 161 уклоњено
  1. 51 0
      package-lock.json
  2. 2 1
      package.json
  3. 1 0
      src/assets/chat-report/selected.svg
  4. 1 0
      src/assets/chat-report/selected_b.svg
  5. 1 0
      src/assets/chat-report/selected_c.svg
  6. 1 0
      src/base/theme/theme-green.css
  7. 1 0
      src/base/theme/theme-light.css
  8. 1 0
      src/base/theme/theme-party.css
  9. 6 0
      src/modules/conversation-chat/dialog-input/InputActions.vue
  10. 8 3
      src/modules/conversation-chat/dialog-input/SettingActions.vue
  11. 191 0
      src/modules/conversation-chat/dialog-input/TemplateConfig.vue
  12. 105 0
      src/modules/conversation-chat/dialog-input/template-config/TemplateMenu.vue
  13. 89 0
      src/modules/conversation-chat/dialog-input/template-config/TemplateModal.vue
  14. 33 0
      src/modules/conversation-chat/dialog-input/template-config/TemplateSection.vue
  15. 14 0
      src/modules/conversation-chat/dialog-input/template-config/TemplateSetting.vue
  16. 1 1
      src/modules/report/AIModal.vue
  17. 116 0
      src/modules/report/ConversationZnws.vue
  18. 274 0
      src/modules/report/Editor copy.vue
  19. 330 103
      src/modules/report/Editor.vue
  20. 3 1
      src/modules/report/EditorMenu.vue
  21. 164 0
      src/modules/report/conversation-znws/Answer.vue
  22. 42 0
      src/modules/report/conversation-znws/ChatItem.vue
  23. 36 0
      src/modules/report/conversation-znws/ChatList.vue
  24. 117 0
      src/modules/report/conversation-znws/DialogContent.vue
  25. 156 0
      src/modules/report/conversation-znws/DialogZnwsContent.vue
  26. 60 0
      src/modules/report/conversation-znws/Question.vue
  27. 0 0
      src/modules/report/text.js
  28. 1 1
      src/views/layout/LayoutExperienceReport.vue
  29. 79 20
      src/views/report/ReportDetail.vue
  30. 40 28
      src/views/report/ReportHeader.vue
  31. 1 1
      src/views/routes.js
  32. 2 2
      vite.config.mjs

+ 51 - 0
package-lock.json

@@ -19,10 +19,13 @@
         "docx-preview": "0.3.3",
         "echarts": "^5.6.0",
         "electron-store": "^8.2.0",
+        "file-saver": "^2.0.5",
         "filesize": "10.1.6",
         "highlight.js": "11.10.0",
+        "html-docx-js-typescript": "^0.1.5",
         "js-cookie": "3.0.5",
         "jsencrypt": "^3.3.1",
+        "katex": "^0.16.22",
         "marked": "13.0.0",
         "md5": "2.3.0",
         "mitt": "3.0.1",
@@ -2189,6 +2192,11 @@
         "concat-map": "0.0.1"
       }
     },
+    "node_modules/browser-or-node": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/browser-or-node/-/browser-or-node-1.3.0.tgz",
+      "integrity": "sha512-0F2z/VSnLbmEeBcUrSuDH5l0HxTXdQQzLjkmBR4cYfvg1zJrKSlmIZFqyFR8oX0NrwPhy3c3HQ6i3OxMbew4Tg=="
+    },
     "node_modules/buffer-alloc": {
       "version": "1.2.0",
       "dev": true,
@@ -2491,6 +2499,14 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/commander": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
+      "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
     "node_modules/compare-version": {
       "version": "0.1.2",
       "dev": true,
@@ -3769,6 +3785,11 @@
         "node": ">=16.0.0"
       }
     },
+    "node_modules/file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+    },
     "node_modules/filesize": {
       "version": "10.1.6",
       "license": "BSD-3-Clause",
@@ -4055,6 +4076,21 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/html-docx-js-typescript": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmmirror.com/html-docx-js-typescript/-/html-docx-js-typescript-0.1.5.tgz",
+      "integrity": "sha512-GNojWFDYbpHSIgKml6/0oAom8mtHrHRTWKMyLRdeJQHO/CyeM6H39DYgzYvPp4OhBp2Ti8dxMKFq0/FkpYD4bg==",
+      "dependencies": {
+        "browser-or-node": "^1.2.1",
+        "jszip": "^3.4.0",
+        "tslib": "^1.13.0"
+      }
+    },
+    "node_modules/html-docx-js-typescript/node_modules/tslib": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz",
+      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+    },
     "node_modules/html-void-elements": {
       "version": "3.0.0",
       "dev": true,
@@ -4313,6 +4349,21 @@
         "setimmediate": "^1.0.5"
       }
     },
+    "node_modules/katex": {
+      "version": "0.16.23",
+      "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.23.tgz",
+      "integrity": "sha512-7VlC1hsEEolL9xNO05v9VjrvWZePkCVBJqj8ruICxYjZfHaHbaU53AlP+PODyFIXEnaEIEWi3wJy7FPZ95JAVg==",
+      "funding": [
+        "https://opencollective.com/katex",
+        "https://github.com/sponsors/katex"
+      ],
+      "dependencies": {
+        "commander": "^8.3.0"
+      },
+      "bin": {
+        "katex": "cli.js"
+      }
+    },
     "node_modules/keyv": {
       "version": "4.5.4",
       "dev": true,

+ 2 - 1
package.json

@@ -37,8 +37,10 @@
     "docx-preview": "0.3.3",
     "echarts": "^5.6.0",
     "electron-store": "^8.2.0",
+    "file-saver": "^2.0.5",
     "filesize": "10.1.6",
     "highlight.js": "11.10.0",
+    "html-docx-js-typescript": "^0.1.5",
     "js-cookie": "3.0.5",
     "jsencrypt": "^3.3.1",
     "katex": "^0.16.22",
@@ -79,6 +81,5 @@
       "createStartMenuShortcut": true,
       "shortcutName": "智库问答助手"
     }
-
   }
 }

+ 1 - 0
src/assets/chat-report/selected.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g><path d="M0,12C0,5.3833041,5.3833041,0,12,0C18.616695,0,24,5.3833041,24,12C24,18.616695,18.616695,24,12,24C5.3833041,24,0,18.616695,0,12ZM10.911103,17.69931L18.714809,9.6993198C19.101124,9.3048019,19.094208,8.6715136,18.699203,8.2852974C18.305708,7.8990092,17.671909,7.9064107,17.285206,8.3009043L10.188511,15.578205L6.7070189,12.097811C6.316421,11.707115,5.6836195,11.707115,5.2930217,12.097811C4.9023261,12.488408,4.9023261,13.121696,5.2930217,13.511807L9.4892902,17.707127C9.6768112,17.894623,9.9312115,18.000118,10.196301,18.000118L10.201706,18.000118C10.468792,17.998608,10.724606,17.890217,10.911103,17.69931Z" fill-rule="evenodd" fill="#09807D" fill-opacity="1"/></g></svg>

+ 1 - 0
src/assets/chat-report/selected_b.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g><path d="M0,12C0,5.3833041,5.3833041,0,12,0C18.616695,0,24,5.3833041,24,12C24,18.616695,18.616695,24,12,24C5.3833041,24,0,18.616695,0,12ZM10.911103,17.69931L18.714809,9.6993198C19.101124,9.3048019,19.094208,8.6715136,18.699203,8.2852974C18.305708,7.8990092,17.671909,7.9064107,17.285206,8.3009043L10.188511,15.578205L6.7070189,12.097811C6.316421,11.707115,5.6836195,11.707115,5.2930217,12.097811C4.9023261,12.488408,4.9023261,13.121696,5.2930217,13.511807L9.4892902,17.707127C9.6768112,17.894623,9.9312115,18.000118,10.196301,18.000118L10.201706,18.000118C10.468792,17.998608,10.724606,17.890217,10.911103,17.69931Z" fill-rule="evenodd" fill="#03547f" fill-opacity="1"/></g></svg>

+ 1 - 0
src/assets/chat-report/selected_c.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g><path d="M0,12C0,5.3833041,5.3833041,0,12,0C18.616695,0,24,5.3833041,24,12C24,18.616695,18.616695,24,12,24C5.3833041,24,0,18.616695,0,12ZM10.911103,17.69931L18.714809,9.6993198C19.101124,9.3048019,19.094208,8.6715136,18.699203,8.2852974C18.305708,7.8990092,17.671909,7.9064107,17.285206,8.3009043L10.188511,15.578205L6.7070189,12.097811C6.316421,11.707115,5.6836195,11.707115,5.2930217,12.097811C4.9023261,12.488408,4.9023261,13.121696,5.2930217,13.511807L9.4892902,17.707127C9.6768112,17.894623,9.9312115,18.000118,10.196301,18.000118L10.201706,18.000118C10.468792,17.998608,10.724606,17.890217,10.911103,17.69931Z" fill-rule="evenodd" fill="#4977c8" fill-opacity="1"/></g></svg>

+ 1 - 0
src/base/theme/theme-green.css

@@ -15,6 +15,7 @@ html body[dmx-theme='green'] {
   --tip-bg-end:rgba(230,242,242,0);
   --question_bg:rgba(10,129,126,0.1);
   --agent_hover:rgba(10, 129, 126,0.5);
+  --editor_hover:rgba(10, 129, 126,0.3);
   --textarea-input-border:#c7eceb;
   --alert-top-bg:#e6f2f2;
   --table-th-bg:#E6F2F2;

+ 1 - 0
src/base/theme/theme-light.css

@@ -6,6 +6,7 @@ html body[dmx-theme='light'] {
   --tip-bg-end:rgba(235,240,248,0) ;
   --question_bg: rgba(59,107,191,0.1);
   --agent_hover:rgba(73, 119, 200,0.5);
+  --editor_hover:rgba(73, 119, 200,0.3);
   --textarea-input-border:#C4D2EB;
   --alert-top-bg:#EBF0F8;
   --table-th-bg:#EBF0F8;

+ 1 - 0
src/base/theme/theme-party.css

@@ -13,6 +13,7 @@ html body[dmx-theme='party'] {
   --tip-bg-end: rgba(236,243,246,0) ;
   --question_bg:rgba(3,84,127,0.1);
   --agent_hover:rgba(3, 84, 127,0.5);
+  --editor_hover:rgba(3, 84, 127,0.3);
   --textarea-input-border:#CEE7F4;
   --alert-top-bg:##EBF2F7 ;
   --table-th-bg:#EBF2F7;

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

@@ -1,6 +1,7 @@
 <template>
   <div class="dialog-input-actions">
     <div class="dialog-input-action-left">
+      <TemplateConfig v-if="has_template"></TemplateConfig>
       <RuleActions v-if="has_rules" :is_merge_table="is_merge_table"></RuleActions>
       <SettingActions v-if="has_settings" :btn_auth="btn_auth"></SettingActions>
       <RecommendActions v-if="has_recommend" @click="handleRecommend"></RecommendActions>
@@ -39,6 +40,7 @@ import SettingActions from './SettingActions.vue';
 import RuleActions from './RuleActions.vue';
 import RecommendActions from './RecommendActions.vue';
 import RecommendModal from './RecommendModal.vue';
+import TemplateConfig from './TemplateConfig.vue';
 import { Message } from '@arco-design/web-vue';
 import submit_normal_a from '../../../assets/chat-input/submit_normal.png';
 import submit_normal_b from '../../../assets/chat-input/submit_normal_b.png';
@@ -214,6 +216,9 @@ const has_recommend = computed(() => {
 const show_upload = computed(() => {
   return props.chat_type === 'bgcl';
 });
+const has_template = computed(() => {
+  return props.chat_type === 'bgcs' && false;
+});
 
 /**
  * 按钮状态
@@ -293,6 +298,7 @@ onMounted(() => {
 
 .dialog-input-action-left {
   display: flex;
+  gap: 10px;
 }
 
 .dialog-input-action-right {

+ 8 - 3
src/modules/conversation-chat/dialog-input/SettingActions.vue

@@ -3,7 +3,7 @@
   <!-- <img class="icon" src="../../../assets/chat-report/settings.svg" alt="" @click="handleSettingClick"> -->
   <!-- </a-tooltip> -->
 
-  <a-dropdown position="top" @select="handleSelect">
+  <a-dropdown position="top" @select="handleSelect" class="my_custom_drop">
     <a-button class="btn">
       <img :src="icon_table" alt="" />
       <span>数据管理</span>
@@ -139,11 +139,16 @@ onMounted(() => {});
 }
 
 .dopt-item {
-  padding: 0px 20px;
+  padding: 0px 10px;
 }
 
 .arco-dropdown-open .arco-icon-down {
   transform: rotate(180deg);
 }
 </style>
-<style></style>
+<style>
+.my_custom_drop .arco-dropdown {
+  padding: 10px 0;
+  border-radius: 12px;
+}
+</style>

+ 191 - 0
src/modules/conversation-chat/dialog-input/TemplateConfig.vue

@@ -0,0 +1,191 @@
+<template>
+  <a-dropdown position="top" @select="handleSelect" class="my_custom_drop">
+    <a-button class="btn">
+      <img :src="icon_table" alt="" />
+      <span>报告模板</span>
+      <icon-down />
+    </a-button>
+    <template #content>
+      <a-doption v-for="item in template_types" :key="item.value" :value="item.value">
+        <div class="dop">
+          <div class="dop-item">
+            <div class="item-title">{{ item.title }}</div>
+            <div class="item-desc">{{ item.desc }}</div>
+          </div>
+          <div class="dop-item-selected" v-if="item.is_selected">
+            <img :src="icon_selected" alt="" />
+          </div>
+        </div>
+      </a-doption>
+    </template>
+  </a-dropdown>
+  <TemplateModal
+    v-if="show_setting_modal"
+    :showModel="show_setting_modal"
+    :type="target_type"
+    :title="target_title"
+    @close="handleCloseModal"
+  >
+  </TemplateModal>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue';
+import TemplateModal from './template-config/TemplateModal.vue';
+import icon_file_a from '../../../assets/chat-report/rule.svg';
+import icon_file_b from '../../../assets/chat-report/rule_b.svg';
+import icon_file_c from '../../../assets/chat-report/rule_c.svg';
+
+import icon_selected_a from '../../../assets/chat-report/selected.svg';
+import icon_selected_b from '../../../assets/chat-report/selected_b.svg';
+import icon_selected_c from '../../../assets/chat-report/selected_c.svg';
+
+import { useThemeStore } from '../../../base/store/use-theme';
+import { THEME_LIGHT, THEME_DARK, THEME_GREEN, THEME_PARTY } from '../../../base/variables';
+
+const themeStore = useThemeStore();
+
+const icon_table = computed(() => {
+  if (themeStore.value == THEME_GREEN) {
+    return icon_file_a;
+  } else if (themeStore.value == THEME_PARTY) {
+    return icon_file_b;
+  } else if (themeStore.value == THEME_LIGHT) {
+    return icon_file_c;
+  } else {
+    return icon_file_a;
+  }
+});
+const icon_selected = computed(() => {
+  if (themeStore.value == THEME_GREEN) {
+    return icon_selected_a;
+  } else if (themeStore.value == THEME_PARTY) {
+    return icon_selected_b;
+  } else if (themeStore.value == THEME_LIGHT) {
+    return icon_selected_c;
+  } else {
+    return icon_selected_a;
+  }
+});
+
+const template_types = ref([
+  {
+    value: 1,
+    title: '配网运行分析周报',
+    desc: '这是描述内容',
+    is_selected: 0
+  },
+  {
+    value: 2,
+    title: '配网运行分析月报',
+    desc: '这是描述内容',
+    is_selected: 0
+  },
+  {
+    value: 3,
+    title: '配网数字化月报',
+    desc: '这是描述内容',
+    is_selected: 0
+  }
+]);
+
+const show_setting_modal = ref(false);
+const target_type = ref('');
+const target_title = ref('');
+
+const handleSelect = (type) => {
+  console.log(type);
+  template_types.value.forEach((item) => {
+    item.is_selected = 0;
+  });
+  const idx = template_types.value.findIndex((item) => item.value == type);
+  if (idx !== -1) {
+    template_types.value[idx].is_selected = 1;
+    target_title.value = template_types.value[idx].title;
+  }
+  target_type.value = type;
+
+  show_setting_modal.value = true;
+};
+
+const handleCloseModal = () => {
+  target_type.value = '';
+  show_setting_modal.value = false;
+  template_types.value.forEach((item) => {
+    item.is_selected = 0;
+  });
+  target_title.value = false;
+};
+onMounted(() => {});
+</script>
+<style scoped lang="css">
+.icon {
+  cursor: pointer;
+  width: 24px;
+}
+
+.btn {
+  background: var(--bg-title);
+  border-radius: 20px;
+  font-weight: 500;
+  font-size: 14px;
+  color: var(--primary-default);
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+
+.btn:hover {
+  background: var(--bg-title);
+  color: var(--primary-default);
+  opacity: 0.8;
+}
+
+.dopt-item {
+  padding: 0px 20px;
+}
+
+.arco-dropdown-open .arco-icon-down {
+  transform: rotate(180deg);
+}
+.dop {
+  width: 160px;
+  display: flex;
+  justify-content: space-between;
+  padding: 5px;
+  border-bottom: 1px solid #e5e5e5;
+}
+.dop .dop-item {
+  display: flex;
+  flex-direction: column;
+}
+.dop-item .item-title {
+  font-family: 思源黑体;
+  font-weight: 600;
+  line-height: normal;
+  letter-spacing: 0px;
+  color: #2c2c2c;
+}
+.dop-item .item-desc {
+  font-family: 思源黑体;
+  font-size: 12px;
+  font-weight: normal;
+  line-height: normal;
+  letter-spacing: 0px;
+  color: #687272;
+}
+.dop-item-selected {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.dop-item-selected img {
+  width: 16px;
+}
+</style>
+<style>
+.my_custom_drop .arco-dropdown {
+  padding: 10px 0;
+  border-radius: 12px;
+}
+</style>

+ 105 - 0
src/modules/conversation-chat/dialog-input/template-config/TemplateMenu.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="template-menu">
+    <div class="title bg_title">{{ title }}</div>
+    <!-- <div class="menus"> -->
+    <div class="menus-list">
+      <div class="menus-list-item" v-for="item in menu_data" :key="item.id">
+        <div class="date">{{ item.date }}</div>
+        <div class="menus-list-item-menu" v-for="it in item.menus" :key="it.id">
+          {{ it.title }}
+        </div>
+        <!-- </div> -->
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import {ref} from 'vue'
+const title = ref('国网四川省电力公司供电可靠性周报')
+
+const menu_data = [
+  {
+    id:1,
+    date:'2025年4月22日',
+    menus:[
+      {
+        id:'1-1',
+        title:'一.停电时户数目标管控'
+      },
+      {
+        id:'1-2',
+        title:'二.停电时户数目标管控'
+      },
+      {
+        id:'1-3',
+        title:'三.停电时户数目标管控'
+      },
+    ]
+  },
+  {
+    id:2,
+    date:'2025年4月22日',
+    menus:[
+      {
+        id:'2-1',
+        title:'一.停电时户数目标管控'
+      },
+      {
+        id:'2-2',
+        title:'二.停电时户数目标管控'
+      },
+      {
+        id:'2-3',
+        title:'三.停电时户数目标管控'
+      },
+    ]
+  },
+]
+</script>
+<style scoped lang="css">
+.template-menu {
+  padding: 10px;
+  width: 16%;
+  height: 100%;
+  box-sizing: border-box;
+  /* overflow-y: auto; */
+}
+.title {
+  padding: 5px 10px;
+  border-radius: 6px;
+  font-family: 思源黑体;
+
+  font-weight: 500;
+  line-height: normal;
+  letter-spacing: 0px;
+
+  color: #181d1d;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis; /* 内容过长时显示省略号 */
+}
+.menus-list {
+  height: calc(100% - 40px);
+  margin-top: 10px;
+  overflow-y: auto;
+}
+.menus-list-item {
+  /* padding: 10px 0; */
+}
+.menus-list-item .date {
+  padding: 5px 0;
+}
+.menus-list-item .menus-list-item-menu {
+  padding: 5px 0;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis; /* 内容过长时显示省略号 */
+  cursor: pointer;
+  padding: 0px 5px;
+}
+.menus-list-item-menu:hover {
+  background: var(--bg-title);
+  border-radius: 6px;
+}
+</style>

+ 89 - 0
src/modules/conversation-chat/dialog-input/template-config/TemplateModal.vue

@@ -0,0 +1,89 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    width="90%"
+    top="5vh"
+    :align-center="false"
+    :mask-closable="false"
+    title-align="start"
+    :footer="false"
+    :modalClass="'customSettingClass'"
+    @cancel="handleCancel"
+  >
+    <template #title>
+      <div class="header">
+        {{ title + '模板配置' }}
+      </div>
+    </template>
+    <div class="body">
+      <TemplateMenu></TemplateMenu>
+      <TemplateSection></TemplateSection>
+      <TemplateSetting></TemplateSetting>
+    </div>
+  </a-modal>
+</template>
+
+<script setup>
+import { Message } from '@arco-design/web-vue';
+import { ref, computed, onMounted, reactive, nextTick } from 'vue';
+import TemplateMenu from './TemplateMenu.vue';
+import TemplateSection from './TemplateSection.vue';
+import TemplateSetting from './TemplateSetting.vue';
+
+const props = defineProps({
+  showModel: Boolean,
+  type: String,
+  title: String
+});
+
+const emit = defineEmits(['close']);
+
+const visible = computed(() => {
+  return props.showModel;
+});
+const handleCancel = () => {
+  emit('close');
+};
+
+onMounted(() => {});
+</script>
+<style scoped lang="css">
+.header {
+  /* padding: 0px 10px; */
+}
+
+.body {
+  height: 100%;
+  box-sizing: border-box;
+  padding: 10px;
+  display: flex;
+  /* grid-template-columns: 1fr 4fr 1.2fr; */
+}
+</style>
+<style>
+.customSettingClass {
+  height: 90vh;
+}
+
+.customSettingClass .arco-modal-header {
+  border: none;
+  padding-left: 10px;
+  padding-right: 10px;
+  /* height: 60px;
+  background: linear-gradient(180deg, #E6f2f2 0%, #FFFFFF 100%);
+  box-shadow: 0px 0px 20px 0px rgba(0, 50, 49, 0.1); */
+}
+.customSettingClass .arco-modal-body {
+  height: calc(100% - 48px);
+  padding: 0;
+}
+
+.customSettingClass .arco-modal-header .arco-modal-title {
+  margin-top: 15px;
+  font-weight: bold;
+}
+
+.customSettingClass .arco-modal-close-btn {
+  font-size: 16px;
+}
+</style>

+ 33 - 0
src/modules/conversation-chat/dialog-input/template-config/TemplateSection.vue

@@ -0,0 +1,33 @@
+<template>
+  <div class="template-section">
+    <div class="section-header">
+      <div class="header-title">
+        <span>{{ title }}</span>
+        <span>{{ save_time }}</span>
+      </div>
+      <div class="header-tools">
+        <a-button>保存</a-button>
+        <a-button type="primary">生成报告</a-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import {ref} from 'vue'
+
+const title = ref('国网四川省电力公司供电可靠性周报')
+const save_time = ref('10.20')
+
+</script>
+<style scoped lang="css">
+.template-section {
+  width: 65%;
+  height: 100%;
+  overflow-y: auto;
+  border-left: 1px solid #e5e5e5;
+  border-right: 1px solid #e5e5e5;
+  padding: 10px 20px;
+  box-sizing: border-box;
+}
+</style>

+ 14 - 0
src/modules/conversation-chat/dialog-input/template-config/TemplateSetting.vue

@@ -0,0 +1,14 @@
+<template>
+  <div class="template-section"></div>
+</template>
+
+<script setup lang="js">
+
+</script>
+<style scoped lang="css">
+.template-section {
+  width: 19%;
+  height: 100%;
+  overflow-y: auto;
+}
+</style>

+ 1 - 1
src/modules/report/AIModal.vue

@@ -77,7 +77,7 @@ const ai_tools = ref([
 const input_txt = ref('');
 const outputRef = ref(null)
 const outContent = ref(
-  '<p><span>10KV内容内容内容10KV内容内容内容10KV内容内容内容10KV内容内容内容10KV内容内容内容10KV内容内容内容</span></p>'
+  '<p><span data-slate-node="text">10KV内容内容内容10KV内容内容内容10KV内容内容内容10KV内容内容内容10KV内容内容内容10KV内容内容内容</span></p>'
 );
 
 const changeTab = (item) => {

+ 116 - 0
src/modules/report/ConversationZnws.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="conversation-chat">
+    <div class="conversation-header">
+      <div class="header-nav">
+        <img class="icon" :src="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>
+      <div class="chat-input">
+        <DialogInput></DialogInput>
+        <DialogFooter></DialogFooter>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref,computed } 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'
+const props = defineProps({
+})
+
+const header_title = ref('智能问数助手')
+
+// 聊天对话列表
+const moke_data = ref([
+  {
+    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:['不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据不知道参测试数测试数据']
+  }
+]);
+const chat_new_list = ref([])
+const chat_log_list = ref([])
+// 对话展示列表
+const chat_display_list = computed(() => {
+  return [...moke_data.value].concat(chat_log_list.value, chat_new_list.value);
+});
+
+
+</script>
+<style scoped lang="css">
+.conversation-chat {
+  width: 100%;
+  height: 100%;
+  background: #fff;
+  border-top-right-radius: 20px;
+  border-bottom-right-radius: 20px;
+  border: 1px solid #e5e6eb;
+  box-sizing: border-box;
+}
+.conversation-header {
+  height: 50px;
+  border-bottom: 1px solid #e5e6eb;
+}
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+}
+
+.header-nav .title {
+  padding: 6px 20px;
+  background-color: var(--bg-title);
+  border-radius: 20px;
+  z-index: 1;
+}
+.header-nav .icon {
+  width: 32px;
+  margin-right: -15px;
+  z-index: 2;
+}
+.conversation-body {
+  width: 100%;
+  height: calc(100% - 51px);
+  box-sizing: border-box;
+}
+.chat-container {
+  width: 100%;
+  height: calc(100% - 200px);
+  box-sizing: border-box;
+  padding: 20px;
+  overflow-y: auto;
+}
+.chat-input {
+  height: 200px;
+  padding: 20px 10px 0px 10px;
+  box-sizing: border-box;
+}
+</style>

+ 274 - 0
src/modules/report/Editor copy.vue

@@ -0,0 +1,274 @@
+src/modules/report/Editor.vue<template>
+  <div class="editor">
+    <Toolbar
+      class="tool_bar"
+      ref="toolbarRef"
+      style="position: sticky; top: 0; z-index: 1; border-bottom: 1px solid #ccc"
+      :editor="editorRef"
+      :defaultConfig="toolbarConfig"
+      :mode="mode"
+    />
+    <Editor
+      class="editor_content"
+      ref="editorContentRef"
+      v-model="valueHtml"
+      :defaultConfig="editorConfig"
+      :mode="mode"
+      @onCreated="handleCreated"
+      @onChange="handleChange"
+    />
+  </div>
+  <AIModal
+    v-if="show_ai_modal"
+    :show_modal="show_ai_modal"
+    :target_active_pElement="target_active_pElement"
+    @close="closeAIModal"
+    @changePElement="changePElement"
+    @insertPElement="insertPElement"
+  ></AIModal>
+</template>
+<script setup>
+import '@wangeditor/editor/dist/css/style.css'; // 引入 css
+
+import { onBeforeUnmount, ref, shallowRef, onMounted, watch, nextTick } from 'vue';
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
+import RobotIcon from '../../assets/tray-icon.png';
+import { str_html } from './text';
+
+import AIModal from './AIModal.vue';
+
+const props = defineProps({
+  target_active_item: Object
+});
+
+const emit = defineEmits(['showMenus']);
+
+// 编辑器实例,必须用 shallowRef
+const editorRef = shallowRef();
+
+const toolbarRef = ref(null);
+
+// 正文内容
+const editorContentRef = ref();
+
+// 内容 HTML
+const valueHtml = ref('<p>hello</p>');
+// 目录
+const menus = ref([]);
+
+// 当前要AI转写的P标签
+const target_active_pElement = ref(null);
+
+const show_ai_modal = ref(false);
+
+// 模拟 ajax 异步获取内容
+onMounted(() => {});
+
+const toolbarConfig = {};
+const editorConfig = {
+  placeholder: '请输入内容...'
+};
+
+// 组件销毁时,也及时销毁编辑器
+onBeforeUnmount(() => {
+  const editor = editorRef.value;
+  if (editor == null) return;
+  editor.destroy();
+});
+
+const handleCreated = (editor) => {
+  editorRef.value = editor; // 记录 editor 实例,重要!
+  toolbarConfig.excludeKeys = ['insertImage', 'fullScreen', 'enter', 'emotion', 'group-video', 'todo'];
+  console.log('工具栏默认配置:', editor.getAllMenuKeys());
+  console.log('工具栏默认配置:', editor.getConfig());
+  console.log('工具栏默认配置:', Toolbar);
+
+  // console.log(toolbarRef,.getConfig().toolbarKeys)
+  valueHtml.value = str_html;
+  nextTick(() => {
+    const pTags = getAllPTags(editorContentRef.value.$el);
+    pTags?.forEach((pTag) => {
+      pTag.addEventListener('mouseenter', () => handleMouseEnter(pTag));
+      pTag.addEventListener('mouseleave', (e) => handleParentMouseLeave(e, pTag));
+    });
+    // const ele = document.querySelector('.editor_content');
+    // if (ele) {
+    //   ele.addEventListener('mouseenter', (e) => {
+    //     const e_target = e.target;
+    //     console.log(e_ta);
+    //   });
+    // }
+  });
+};
+const handleChange = () => {
+  menus.value = generateTableOfContents();
+  emit('showMenus', menus);
+};
+
+const generateTableOfContents = () => {
+  const headings = document.querySelectorAll('.editor_content h1, .editor_content h2, .editor_content h3');
+  console.log(headings);
+  const toc = [];
+  headings.forEach((heading, index) => {
+    const id = `section-${index + 1}`;
+    const level = heading.tagName === 'H1' ? 1 : heading.tagName === 'H2' ? 2 : 3; // 根据标题等级设置目录项的缩进
+    heading.setAttribute('id', id); // 设置标题的id属性
+    toc.push({ id: id, text: heading.textContent, level: level, index: index }); // 将标题文本、id和等级添加到目录项中
+  });
+  return toc;
+};
+const 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);
+  if (targetElement && editorContent) {
+    console.dir(editorContent, 1111);
+    targetElement.scrollIntoView({
+      behavior: 'smooth',
+      block: 'start'
+    });
+  }
+};
+
+const getAllPTags = (parentNode) => {
+  const pTags = [];
+
+  const traverse = (node) => {
+    if (node.tagName === 'P') {
+      pTags.push(node);
+    }
+    for (let i = 0; i < node.children.length; i++) {
+      traverse(node.children[i]);
+    }
+  };
+  traverse(parentNode);
+  return pTags;
+};
+
+const handleMouseEnter = (pElement) => {
+  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.innerText = 'AI 转写';
+    btn.classList.add('ai-btn');
+    btn.style.position = 'absolute';
+    btn.style.height = '40px';
+    // btn.style.height = '30px';
+    btn.style.top = '-10px';
+    btn.style.left = '-45px';
+    btn.style.padding = '5px 10px';
+    // btn.style.fontSize = '14px';
+    btn.src = RobotIcon;
+    btn.style.color = '#fff';
+    btn.style.border = 'none';
+    btn.style.cursor = 'pointer';
+    btn.style.borderRadius = '4px';
+    btn.addEventListener('click', () => {
+      target_active_pElement.value = pElement;
+      // alert('AI 转写');
+      show_ai_modal.value = true;
+    });
+    //设置p元素为相对定位,确保按钮能相对于P元素定位
+    pElement.style.position = 'relative';
+    pElement.appendChild(btn);
+    btn.addEventListener('mouseleave', () => handleMouseLeave(pElement));
+  }
+};
+const handleMouseLeave = (pElement) => {
+  const existingBtn = pElement.querySelector('.ai-btn');
+  console.log(existingBtn, 9999);
+  if (existingBtn) {
+    pElement.removeChild(existingBtn);
+  }
+};
+
+const handleParentMouseLeave = (event, pTag) => {
+  const pElement = event.target;
+  pElement.style.background = '#fff';
+  console.dir(pElement, 222);
+  handleMouseLeave(pElement);
+};
+
+const closeAIModal = () => {
+  show_ai_modal.value = false;
+  target_active_pElement.value = null;
+};
+const changePElement = (obj) => {
+  if (editorContentRef.value) {
+    const targetP = editorContentRef.value.$el.querySelector(`p#${obj.pid}`);
+    if (targetP) {
+      targetP.innerHTML = obj.content.innerHTML;
+    }
+  }
+  closeAIModal();
+};
+const insertPElement = (obj) => {
+  if (editorContentRef.value) {
+    const targetP = editorContentRef.value.$el.querySelector(`p#${obj.pid}`);
+    if (targetP) {
+      targetP.innerHTML += obj.content.innerHTML;
+    }
+  }
+  closeAIModal();
+};
+
+watch(
+  () => props.target_active_item,
+  (newValue) => {
+    if (Object.keys(newValue).length) {
+      handleItemClick(newValue);
+    }
+  }
+);
+</script>
+<style scoped lang="css">
+.editor {
+  /* flex: 4; */
+  width: calc(100%);
+  height: 100%;
+  overflow: hidden;
+}
+
+.tool_bar {
+  height: 80px;
+  max-height: 80px;
+  width: 100%;
+}
+
+.editor_content {
+  scroll-margin-top: 80px;
+  height: calc(100% - 80px) !important;
+  /* 留出Toolbar高度 */
+  overflow-y: auto;
+  padding: 0px 62px;
+}
+
+:deep(.w-e-text-container .w-e-scroll) {
+  padding: 20px 50px;
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+/* .ai-btn {
+  position: absolute;
+  top: -30px;
+  left: 0;
+  padding: 5px 10px;
+  font-size: 14px;
+  background-image: url('../../assets/robot.svg');
+  width: 30px;
+  height: 30px;
+}
+.active_p {
+  background-color: #007bff;
+} */
+</style>

+ 330 - 103
src/modules/report/Editor.vue

@@ -1,13 +1,33 @@
 <template>
   <div class="editor">
-    <Toolbar class="tool_bar" ref="toolbarRef"
-      style="position: sticky; top: 0; z-index: 1; border-bottom: 1px solid #ccc" :editor="editorRef"
-      :defaultConfig="toolbarConfig" :mode="mode" />
-    <Editor class="editor_content" ref="editorContentRef" v-model="valueHtml" :defaultConfig="editorConfig" :mode="mode"
-      @onCreated="handleCreated" @onChange="handleChange" />
+    <Toolbar
+      class="tool_bar"
+      ref="toolbarRef"
+      style="position: sticky; top: 0; z-index: 1; border-bottom: 1px solid #ccc"
+      :editor="editorRef"
+      :defaultConfig="toolbarConfig"
+      :mode="mode"
+    />
+    <Editor
+      class="editor_content"
+      ref="editorContentRef"
+      v-model="valueHtml"
+      :defaultConfig="editorConfig"
+      :mode="mode"
+      @onCreated="handleCreated"
+      @onChange="handleChange"
+    />
+    <!-- 隐藏的图片上传input -->
+    <input type="file" ref="imageUploadRef" accept="image/*" style="display: none" @change="handleImageUpload" />
   </div>
-  <AIModal v-if="show_ai_modal" :show_modal="show_ai_modal" :target_active_pElement="target_active_pElement"
-    @close="closeAIModal" @changePElement="changePElement" @insertPElement="insertPElement"></AIModal>
+  <AIModal
+    v-if="show_ai_modal"
+    :show_modal="show_ai_modal"
+    :target_active_pElement="target_active_pElement"
+    @close="closeAIModal"
+    @changePElement="changePElement"
+    @insertPElement="insertPElement"
+  ></AIModal>
 </template>
 <script setup>
 import '@wangeditor/editor/dist/css/style.css'; // 引入 css
@@ -27,31 +47,69 @@ const emit = defineEmits(['showMenus']);
 
 // 编辑器实例,必须用 shallowRef
 const editorRef = shallowRef();
-
-const toolbarRef = ref(null)
-
-// 正文内容
+const toolbarRef = ref(null);
 const editorContentRef = ref();
+// 图片上传input的ref
+const imageUploadRef = ref(null);
 
 // 内容 HTML
-const valueHtml = ref('<p>hello</p>');
+const valueHtml = ref('');
 // 目录
 const menus = ref([]);
 
 // 当前要AI转写的P标签
 const target_active_pElement = ref(null);
-
 const show_ai_modal = ref(false);
 
 // 模拟 ajax 异步获取内容
-onMounted(() => {
-});
+onMounted(() => {});
 
-const toolbarConfig = {}
+const toolbarConfig = {
+  // 保留图片上传按钮
+};
 const editorConfig = {
   placeholder: '请输入内容...',
-}
+  MENU_CONF: {
+    editImage: {
+      hideLinkImg: true // 隐藏图片链接按钮
+    },
+    // 配置图片上传
+    uploadImage: {
+      // 自定义上传逻辑
+      customUpload: async (file, insertFn) => {
+        // 验证文件类型
+        if (!file.type.startsWith('image/')) {
+          alert('请选择图片文件');
+          return;
+        }
+
+        // 验证文件大小 (限制10MB)
+        const maxSize = 10 * 1024 * 1024; // 10MB
+        if (file.size > maxSize) {
+          alert('图片大小不能超过10MB');
+          return;
+        }
+
+        // 这里我们将使用base64处理,不发送真实请求
+        const base64Url = await convertImageToBase64(file);
 
+        // 检查base64数据是否有效
+        if (!base64Url.startsWith('data:image/')) {
+          alert('图片转换失败,请重试');
+          return;
+        }
+
+        // 插入图片到编辑器
+        insertFn(base64Url, file.name, file.size);
+
+        // 图片插入后重新绑定事件
+        nextTick(() => {
+          bindPElementEvents();
+        });
+      }
+    }
+  }
+};
 
 // 组件销毁时,也及时销毁编辑器
 onBeforeUnmount(() => {
@@ -62,62 +120,134 @@ onBeforeUnmount(() => {
 
 const handleCreated = (editor) => {
   editorRef.value = editor; // 记录 editor 实例,重要!
+
+  // 修改工具栏配置,保留图片上传按钮
   toolbarConfig.excludeKeys = [
-    "insertImage",
+    'insertImage',
     'fullScreen',
     'enter',
     'emotion',
     'group-video',
-    'todo'
-  ]
-  console.log('工具栏默认配置:', editor.getAllMenuKeys());
-  console.log('工具栏默认配置:', editor.getConfig());
-  console.log('工具栏默认配置:', Toolbar);
+    'todo',
+    'unLink',
+    'viewLink',
+    'insertLink',
+    'insertTable',
+    'editImage',
+    'editLink',
+    'viewImageLink'
+  ];
+  console.log(editor.getAllMenuKeys());
 
-  // console.log(toolbarRef,.getConfig().toolbarKeys)
   valueHtml.value = str_html;
+  // 让所有的h1居中
+  toAlginCenterByH1();
   nextTick(() => {
-    const pTags = getAllPTags(editorContentRef.value.$el);
-    pTags?.forEach((pTag) => {
-      pTag.addEventListener('mouseenter', () => handleMouseEnter(pTag));
-      pTag.addEventListener('mouseleave', (e) => handleParentMouseLeave(e, pTag));
-    });
-    // const ele = document.querySelector('.editor_content');
-    // if (ele) {
-    //   ele.addEventListener('mouseenter', (e) => {
-    //     const e_target = e.target;
-    //     console.log(e_ta);
-    //   });
-    // }
+    bindPElementEvents();
   });
 };
+
 const handleChange = () => {
   menus.value = generateTableOfContents();
   emit('showMenus', menus);
+
+  // 内容变化时重新绑定事件
+  nextTick(() => {
+    bindPElementEvents();
+  });
+};
+
+// 统一绑定P元素事件的函数
+const bindPElementEvents = () => {
+  if (!editorContentRef.value) return;
+
+  // 先移除所有已存在的事件监听和AI按钮
+  const oldPTags = getAllPTags(editorContentRef.value.$el);
+  oldPTags?.forEach((pTag) => {
+    // 移除事件监听
+    pTag.removeEventListener('mouseenter', handleMouseEnter);
+    pTag.removeEventListener('mouseleave', handleParentMouseLeave);
+
+    // 移除AI按钮
+    const existingBtn = pTag.querySelector('.ai-btn');
+    if (existingBtn) {
+      pTag.removeChild(existingBtn);
+    }
+
+    // 重置背景色
+    pTag.style.background = '#fff';
+  });
+
+  // 重新绑定事件
+  const pTags = getAllPTags(editorContentRef.value.$el);
+  pTags?.forEach((pTag) => {
+    // 使用箭头函数包装,确保正确的this绑定
+    pTag.addEventListener('mouseenter', () => handleMouseEnter(pTag));
+    pTag.addEventListener('mouseleave', (e) => handleParentMouseLeave(e, pTag));
+  });
+};
+// 获取到文档中前几个H1进行居中
+const toAlginCenterByH1 = () => {
+  const max_count = 10;
+  let count = 0;
+  let interval = null;
+  interval = setInterval(() => {
+    if (count > max_count) {
+      clearInterval(interval);
+      return;
+    }
+    count += 1;
+    if (editorRef.value.getHtml()) {
+      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);
+          emit('renderTitle', item.innerText);
+        }
+      });
+      editorRef.value.updateView();
+      count = 11;
+    }
+  }, 300);
 };
 
 const generateTableOfContents = () => {
-  const headings = document.querySelectorAll('.editor_content h1, .editor_content h2, .editor_content h3');
-  console.log(headings);
+  const headings = document.querySelectorAll(
+    '.editor_content h1, .editor_content h2, .editor_content h3,.editor_content h4,.editor_content h5'
+  );
   const toc = [];
   headings.forEach((heading, index) => {
     const id = `section-${index + 1}`;
-    const level = heading.tagName === 'H1' ? 1 : heading.tagName === 'H2' ? 2 : 3; // 根据标题等级设置目录项的缩进
-    heading.setAttribute('id', id); // 设置标题的id属性
-    toc.push({ id: id, text: heading.textContent, level: level, index: index }); // 将标题文本、id和等级添加到目录项中
+    const level =
+      heading.tagName === 'H1'
+        ? 1
+        : heading.tagName === 'H2'
+        ? 2
+        : heading.tagName === 'H3'
+        ? 3
+        : heading.tagName === 'H4'
+        ? 4
+        : 5;
+    heading.setAttribute('id', id);
+    toc.push({ id: id, text: heading.textContent, level: level, index: index });
   });
   return toc;
 };
+
 const 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);
   if (targetElement && editorContent) {
-    console.dir(editorContent, 1111);
     targetElement.scrollIntoView({
       behavior: 'smooth',
       block: 'start'
@@ -141,42 +271,53 @@ const getAllPTags = (parentNode) => {
 };
 
 const handleMouseEnter = (pElement) => {
-  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.innerText = 'AI 转写';
-    btn.classList.add('ai-btn');
-    btn.style.position = 'absolute';
-    btn.style.height = '40px';
-    // btn.style.height = '30px';
-    btn.style.top = '-10px';
-    btn.style.left = '-45px';
-    btn.style.padding = '5px 10px';
-    // btn.style.fontSize = '14px';
-    btn.src = RobotIcon;
-    btn.style.color = '#fff';
-    btn.style.border = 'none';
-    btn.style.cursor = 'pointer';
-    btn.style.borderRadius = '4px';
-    btn.addEventListener('click', () => {
-      target_active_pElement.value = pElement;
-      // alert('AI 转写');
-      show_ai_modal.value = true;
-    });
-    //设置p元素为相对定位,确保按钮能相对于P元素定位
-    pElement.style.position = 'relative';
-    pElement.appendChild(btn);
-    btn.addEventListener('mouseleave', () => handleMouseLeave(pElement));
+  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', () => {
+        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);
 };
+
 const handleMouseLeave = (pElement) => {
   const existingBtn = pElement.querySelector('.ai-btn');
-  console.log(existingBtn, 9999);
   if (existingBtn) {
     pElement.removeChild(existingBtn);
   }
@@ -185,32 +326,112 @@ const handleMouseLeave = (pElement) => {
 const handleParentMouseLeave = (event, pTag) => {
   const pElement = event.target;
   pElement.style.background = '#fff';
-  console.dir(pElement, 222);
   handleMouseLeave(pElement);
 };
 
 const closeAIModal = () => {
-  show_ai_modal.value = false
-  target_active_pElement.value = null
-}
+  show_ai_modal.value = false;
+  target_active_pElement.value = null;
+};
+// 需要删除的文本字数
+let offset = 0;
+// 修改替换内容的方法
 const changePElement = (obj) => {
-  if (editorContentRef.value) {
-    const targetP = editorContentRef.value.$el.querySelector(`p#${obj.pid}`)
+  if (editorRef.value) {
+    const editor = editorRef.value;
+    console.log(editor.selection);
+    const targetP = document.querySelector(`p#${obj.pid}`);
     if (targetP) {
-      targetP.innerHTML = obj.content.innerHTML
+      // 创建一个临时Range来选中整个p元素
+      const range = document.createRange();
+      range.selectNodeContents(targetP);
+
+      // 将DOM范围转换为编辑器选区
+      editor.restoreSelection(range);
+
+      offset = Number(editor.selection.focus.offset);
+      // 删除选中内容
+      for (let i = 0; i <= offset; i++) {
+        editor.deleteBackward();
+      }
+      // 插入空格
+      editor.insertBreak();
+      // 替换为新内容
+      editor.dangerouslyInsertHtml(obj.content.innerHTML);
+      // targetP.innerHTML = obj.content.innerHTML;
+      console.log(editor.getHtml());
     }
+    // }
   }
-  closeAIModal()
-}
+  closeAIModal();
+
+  nextTick(() => {
+    bindPElementEvents();
+  });
+};
 const insertPElement = (obj) => {
   if (editorContentRef.value) {
-    const targetP = editorContentRef.value.$el.querySelector(`p#${obj.pid}`)
+    const editor = editorRef.value;
+    const targetP = editorContentRef.value.$el.querySelector(`p#${obj.pid}`);
     if (targetP) {
-      targetP.innerHTML += obj.content.innerHTML
+      // targetP.innerHTML += obj.content.innerHTML;
+      editor.dangerouslyInsertHtml(obj.content.innerHTML);
+      editor.updateView();
+      console.log(editor.getHtml());
     }
   }
-  closeAIModal()
-}
+  closeAIModal();
+
+  // 插入后重新绑定事件
+  nextTick(() => {
+    bindPElementEvents();
+  });
+};
+
+// 将图片转换为base64格式
+const convertImageToBase64 = (file) => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      resolve(e.target.result);
+    };
+    reader.onerror = (error) => {
+      reject(error);
+    };
+    reader.readAsDataURL(file);
+  });
+};
+
+// 处理图片上传
+const handleImageUpload = async (e) => {
+  const file = e.target.files[0];
+  if (!file) return;
+
+  try {
+    const editor = editorRef.value;
+    if (!editor) {
+      console.error('编辑器实例不存在');
+      return;
+    }
+
+    // 转换为base64
+    const base64Url = await convertImageToBase64(file);
+
+    // 插入图片到编辑器
+    editor.insertImage(base64Url, file.name);
+
+    // 图片插入后重新绑定事件
+    nextTick(() => {
+      bindPElementEvents();
+    });
+  } catch (error) {
+    console.error('图片上传失败:', error);
+    alert('图片上传失败,请重试');
+  }
+
+  // 重置input值,以便可以重复上传同一文件
+  e.target.value = '';
+};
 
 watch(
   () => props.target_active_item,
@@ -220,10 +441,18 @@ watch(
     }
   }
 );
+defineExpose({
+  getHtml: () => {
+    if (editorRef.value) {
+      return editorRef.value.getHtml();
+    } else {
+      return '';
+    }
+  }
+});
 </script>
 <style scoped lang="css">
 .editor {
-  /* flex: 4; */
   width: calc(100%);
   height: 100%;
   overflow: hidden;
@@ -238,28 +467,26 @@ watch(
 .editor_content {
   scroll-margin-top: 80px;
   height: calc(100% - 80px) !important;
-  /* 留出Toolbar高度 */
   overflow-y: auto;
   padding: 0px 62px;
 }
 
 :deep(.w-e-text-container .w-e-scroll) {
-  padding: 20px 50px;
+  padding: 20px 40px;
   -ms-overflow-style: none;
-  scrollbar-width: none;
+  /* scrollbar-width: none; */
+}
+
+/* 为图片添加一些样式 */
+:deep(.w-e-image-container img) {
+  max-width: 100%;
+  height: auto;
+  margin: 10px 0;
+  border-radius: 4px;
 }
 
-/* .ai-btn {
-  position: absolute;
-  top: -30px;
-  left: 0;
-  padding: 5px 10px;
-  font-size: 14px;
-  background-image: url('../../assets/robot.svg');
-  width: 30px;
-  height: 30px;
+/* 确保AI按钮不会被编辑器遮挡 */
+:deep(.ai-btn) {
+  z-index: 10;
 }
-.active_p {
-  background-color: #007bff;
-} */
 </style>

+ 3 - 1
src/modules/report/EditorMenu.vue

@@ -4,7 +4,7 @@
       <li
         v-for="(item, index) in editor_menus"
         :key="item.id"
-        :style="{ paddingLeft: item.level * 10 + 'px' }"
+        :style="{ paddingLeft: item.level * 15 + 'px' }"
         class="list-item"
       >
         <a :class="{ active: activeIndex === index }" :href="`#${item.id}`" @click="handleItemClick($event, item)">{{
@@ -48,6 +48,8 @@ watch(
   border: 1px solid #e5e6eb;
   padding: 10px;
   overflow-y: auto;
+  border-top-left-radius: 20px;
+  border-bottom-left-radius: 20px;
 }
 .menu_list {
   width: calc(100%);

+ 164 - 0
src/modules/report/conversation-znws/Answer.vue

@@ -0,0 +1,164 @@
+<template>
+  <div class="dialog_answer">
+    <div class="answer-item">
+      <div class="answer-item-icon">
+        <img :src="target_icon" alt="" class="img-icon" />
+      </div>
+      <div class="answer-item-content" v-if="chat_type === 'znws'">
+        <!-- 智能问数的组件渲染和报告生成的一致,先使用同一个,后续有变动再单独拆分 -->
+        <DialogZnwsContent :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"
+          />
+        </DialogZnwsContent>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { Message } from '@arco-design/web-vue';
+import DialogContent from './DialogContent.vue';
+import DialogZnwsContent from './DialogZnwsContent.vue';
+import DialogActions from '../../chat-item/DialogActions.vue';
+import copyText from '../../../3rd-libs/clipboard';
+import formatAnwserString from '../../conversation-dialog/format-anwser-string';
+import { useThemeStore } from '../../../base/store/use-theme';
+import { THEME_LIGHT, THEME_DARK, THEME_GREEN, THEME_PARTY } from '../../../base/variables';
+import { getIcon } from '../../common/use-icon-map';
+const route = useRoute();
+const router = useRouter();
+
+const props = defineProps({
+  dialog: {
+    type: Object,
+    default: ''
+  },
+  type: {
+    type: String,
+    default: 'znwd'
+  },
+  target_agent_icon: {
+    type: String,
+    default: 'icon_1'
+  }
+});
+
+const themeStore = useThemeStore();
+
+const target_icon = computed(() => {
+  if (themeStore.value == THEME_GREEN) {
+    return getIcon(props.target_agent_icon, 'a');
+  } else if (themeStore.value == THEME_PARTY) {
+    return getIcon(props.target_agent_icon, 'b');
+  } else if (themeStore.value == THEME_LIGHT) {
+    return getIcon(props.target_agent_icon, 'c');
+  } else {
+    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)
+
+  const result_list = props.dialog.result_list;
+  // console.log(result_list)
+  // if (result_list) {
+  //   result_list.forEach(item => {
+  //     if (typeof item === 'string') {
+  //       console.log(111111111111111)
+  //       item = formatAnwserString(item)
+  //     }
+  //   })
+
+  // }
+  return result_list;
+});
+
+const chat_type = computed(() => {
+  return props.type;
+});
+const answer_html = computed(() => {
+  const answer = props.dialog?.answer;
+
+  let answer_string = formatAnwserString(answer);
+
+  // let answer_string = answer ? marked.parse(answer) : '暂无回答';
+  return answer_string;
+});
+
+const answer_status = computed(() => {
+  return props.dialog.client_custom_chat_status;
+});
+/**
+ * 内容是否正在加载中
+ */
+const is_content_loading = computed(() => {
+  // console.log(props.dialog);
+  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('');
+  copyText(copy_text);
+  Message.success('复制成功');
+};
+
+onMounted(() => {});
+</script>
+<style scoped lang="css">
+.dialog_answer {
+  width: calc(100%);
+  height: auto;
+}
+
+.file_list {
+  display: flex;
+  align-items: center;
+  /* justify-content: center; */
+  gap: 15px;
+}
+
+.answer-item {
+  display: flex;
+  /* justify-content: flex-end; */
+  align-items: flex-start;
+}
+
+.answer-item-content {
+  max-width: calc(100% - 62px - 62px + 55px);
+}
+
+.answer-item-icon {
+  position: relative;
+}
+
+.answer-item-icon .edit-icon {
+  position: absolute;
+  right: 0;
+  bottom: 10px;
+  visibility: hidden;
+  cursor: pointer;
+}
+
+.answer-item-icon:hover .edit-icon {
+  visibility: visible;
+}
+
+.answer-item-icon img {
+  margin-right: 22px;
+  /* border-radius: 50%; */
+  width: 40px;
+  /* height: 30px; */
+  flex-shrink: 0;
+  cursor: pointer;
+  margin-top: 15px;
+}
+</style>

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

@@ -0,0 +1,42 @@
+<template>
+  <div class="chat-item">
+    <Question v-if="dialog.client_custom_item_type === 'q'" :dialog="dialog"></Question>
+    <Answer
+      v-if="dialog.client_custom_item_type === 'a'"
+      :dialog="dialog"
+      :type="type"
+      :target_agent_icon="target_agent_icon"
+    ></Answer>
+  </div>
+</template>
+
+<script setup>
+import Answer from './Answer.vue';
+import Question from './Question.vue';
+
+const props = defineProps({
+  dialog: {
+    type: Object,
+    default: () => {
+      return {};
+    }
+  },
+  type: {
+    type: String,
+    default: 'znwd'
+  },
+  target_agent_icon: {
+    type: String,
+    default: 'icon_1'
+  }
+});
+</script>
+<style scoped lang="css">
+.chat-item {
+  /* margin-bottom: 40px; */
+  /* display: flex;
+  flex-direction: column;
+  width: 100%;
+  gap: 20px; */
+}
+</style>

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

@@ -0,0 +1,36 @@
+<template>
+  <div class="chat-list">
+    <ChatItem
+      v-for="(item, index) in chat_list"
+      :key="index"
+      :dialog="item"
+      :type="type"
+      :target_agent_icon="target_agent_icon"
+    ></ChatItem>
+  </div>
+</template>
+
+<script setup>
+import ChatItem from './ChatItem.vue';
+
+const props = defineProps({
+  chat_list: {
+    type: Array,
+    default: () => []
+  },
+  type: {
+    type: String,
+    default: 'znws'
+  },
+  target_agent_icon: {
+    type: String,
+    default: 'icon_1'
+  }
+});
+</script>
+<style scoped lang="css">
+.chat-list {
+  /* margin-top: 80px; */
+  width: 100%;
+}
+</style>

+ 117 - 0
src/modules/report/conversation-znws/DialogContent.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="dialog-chat-answer-content" :style="content_style" :class="[type === 'q' ? 'question_input_bg' : '']">
+    <div v-if="is_loading" class="loading"><a-spin></a-spin> <span>正在思考中...</span></div>
+    <!-- <DialogThinkContent v-if="is_think && deep_answer" :deep_loading="deep_loading" :deep_answer="deep_answer">
+    </DialogThinkContent> -->
+    <div v-if="content.length !== 0 && !is_loading" v-html="content" class="answer-content-text" />
+    <slot></slot>
+  </div>
+</template>
+
+<script setup>
+import { computed, onMounted } from 'vue';
+// import DialogThinkContent from './DialogThinkContent.vue';
+
+// 接收markdown数据,进行一些转换
+const props = defineProps({
+  /**
+   * 内容类型
+   */
+  type: String,
+  /**
+   * 文本内容
+   */
+  content: String,
+  /**
+   * 状态值
+   */
+  is_loading: Boolean,
+  /**
+   * 是否正在思考
+   * */
+  is_think: Boolean,
+  /**
+   * 思考内容
+   * */
+  deep_answer: String,
+  /**
+   * 思考状态
+   * */
+  deep_loading: Boolean
+});
+
+const content_style = computed(() => {
+  if (props.type === 'q') {
+    return {
+      // background: 'rgba(10, 129, 126, 0.1)',
+      color: '#2C2C2C',
+      fontWeight: 500
+    };
+  }
+});
+const content_list = computed(() => {});
+onMounted(() => {});
+</script>
+
+<style lang="css" scoped>
+.dialog-chat-answer-content {
+  position: relative;
+  /* background-color: var(--color-bg-1); */
+  color: var(--color-text-1);
+  border-radius: 12px;
+
+  padding-top: 15px;
+  padding-bottom: 15px;
+  padding-left: 30px;
+  padding-right: 30px;
+}
+
+.dialog-chat-answer-content code {
+  overflow-x: auto;
+}
+
+.answer-content-text {
+  line-height: 24px;
+}
+
+.loading {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+</style>
+<style>
+.answer-content-text table {
+  border-collapse: collapse;
+}
+
+.answer-content-text table,
+.answer-content-text th,
+.answer-content-text td {
+  border: 1px solid rgb(184, 179, 179);
+}
+
+.answer-content-text th,
+.answer-content-text td {
+  padding: 8px;
+  text-align: left;
+}
+
+.answer-content-text .katex-display > .katex {
+  white-space: pre-wrap !important;
+}
+
+.refferences {
+  background: rgb(238, 243, 254);
+  border: 1px solid rgb(255, 255, 255);
+  box-shadow: rgba(29, 48, 91, 0.2) 0px 1px 1px 0px;
+
+  padding: 2px 6px;
+  cursor: pointer;
+}
+
+.refferences:hover {
+  background-color: var(--primary-default);
+  color: #fff;
+}
+</style>

+ 156 - 0
src/modules/report/conversation-znws/DialogZnwsContent.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="dialog-chat-answer-content">
+    <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>
+    </div>
+    <slot></slot>
+    <div class="report-loading" v-if="answer_status === 1">
+      <img src="../../assets/chat-report/report-ing.png" alt="" />
+      <!-- <span>报告生成中...</span> -->
+    </div>
+  </div>
+  <!-- <div v-html="html_1"></div> -->
+</template>
+
+<script setup>
+import { computed, onMounted } from 'vue';
+import HomeChart from '../../chat_echart/HomeChart.vue';
+import marked from '../../../3rd-libs/marked';
+// 接收markdown数据,进行一些转换
+const props = defineProps({
+  /**
+   * 内容类型
+   */
+  type: String,
+  /**
+   * 文本内容数组
+   */
+  result_list: Array,
+  /**
+   * 状态值
+   */
+  is_loading: Boolean,
+  /**
+   * 会话状态
+   * */
+  answer_status: Boolean
+});
+
+const content_style = computed(() => {
+  if (props.type === 'q') {
+    return {
+      background: 'var(--primary-default)',
+      color: '#fff'
+    };
+  }
+});
+// 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;');
+  // renderContent = renderContent
+  //   .replace(/<p>(.*?)<\/p>/gs, (match, content) => {
+  //     return `<p class="indented" style="text-indent: 2em;">${content}</p>`;
+  //     // return `<p>${content}</p>`
+  //   })
+  //   .replace(/<br\s*\/?>([^<]*)/g, (match, content) => {
+  //     if (content.trim() === '') return match;
+  //     return `<br/><span style="display:inline-block;text-indent:2em;">${content}</span>`;
+  //   });
+  // renderContent = renderContent
+  //   .replace(/<pre><code>/gs, '<p style="text-indent: 2em;">')
+  //   .replace(/<\/code><\/pre>/gs, '</p>');
+  return renderContent;
+};
+onMounted(() => {});
+</script>
+
+<style lang="css" scoped>
+.dialog-chat-answer-content {
+  position: relative;
+  background-color: var(--color-bg-1);
+  color: var(--color-text-1);
+  border-radius: 12px;
+
+  /* padding-top: 15px;
+  padding-bottom: 15px;
+  padding-left: 30px;
+  padding-right: 30px; */
+}
+
+.answer-content-report .answer-content * p {
+  text-indent: 2em !important;
+}
+
+.answer-content * p {
+  text-indent: 2em !important;
+}
+
+p {
+  text-indent: 2em !important;
+}
+
+.dialog-chat-answer-content code {
+  overflow-x: auto;
+}
+
+.answer-content-text {
+  line-height: 24px;
+}
+
+.loading {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+</style>
+<style>
+.answer-content-text table {
+  border-collapse: collapse;
+}
+
+.answer-content-text table,
+.answer-content-text th,
+.answer-content-text td {
+  border: 1px solid rgb(184, 179, 179);
+}
+
+.answer-content-text th,
+.answer-content-text td {
+  padding: 8px;
+  text-align: left;
+}
+
+.refferences {
+  background: rgb(238, 243, 254);
+  border: 1px solid rgb(255, 255, 255);
+  box-shadow: rgba(29, 48, 91, 0.2) 0px 1px 1px 0px;
+
+  padding: 2px 6px;
+  cursor: pointer;
+}
+
+.refferences:hover {
+  background-color: var(--primary-default);
+  color: #fff;
+}
+
+.indented br + * {
+  display: inline-block;
+  text-indent: 2em;
+}
+.report-loading {
+  margin-top: 10px;
+}
+.report-loading img {
+  width: 32px;
+}
+</style>

+ 60 - 0
src/modules/report/conversation-znws/Question.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="dialog_question">
+    <div class="question-item">
+      <div class="question-item-content">
+        <!-- {{ dialog.question }} -->
+        <DialogContent :content="dialog.question" :type="dialog.client_custom_item_type"></DialogContent>
+      </div>
+      <div class="question-item-icon">
+        <img src="../../assets/user.png" alt="" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import DialogContent from './DialogContent.vue';
+const props = defineProps({
+  dialog: {
+    type: Object,
+    default: ''
+  }
+});
+</script>
+<style scoped lang="css">
+.dialog_question {
+  width: calc(100%);
+  height: auto;
+}
+
+.question-item {
+  display: flex;
+  justify-content: flex-end;
+  align-items: flex-start;
+}
+
+.question-file-list {
+  display: flex;
+  justify-content: flex-end;
+  align-items: flex-start;
+  gap: 20px;
+  flex-wrap: wrap;
+  margin-top: 20px;
+  margin-right: 60px;
+  /* max-width: calc(100% - 62px - 62px); */
+}
+
+.question-item-content {
+  max-width: calc(100% - 62px - 62px + 55px);
+}
+
+.question-item-icon img {
+  margin-left: 22px;
+  border-radius: 50%;
+  width: 40px;
+  height: 40px;
+  flex-shrink: 0;
+  visibility: hidden;
+  display: none;
+}
+</style>

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
src/modules/report/text.js


+ 1 - 1
src/views/layout/LayoutExperienceReport.vue

@@ -78,7 +78,7 @@ onMounted(() => {
   background-size: cover;
   padding: 16px 16px 16px 0; */
   background-color: var(--bg-body);
-  padding: 20px;
+  /* padding: 10px; */
   box-sizing: border-box;
 }
 </style>

+ 79 - 20
src/views/report/ReportDetail.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="report-detail">
-    <ReportHeader></ReportHeader>
+    <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">
@@ -10,18 +10,27 @@
             <div class="save">已保存{{ save_time }}</div>
           </div>
           <div class="tools-right">
-            <img class="icon" src="../../assets/chat-report-detail/icon_copy.svg" alt="">
-            <img class="icon" src="../../assets/chat-report-detail/icon_download.svg" alt="">
-            <img class="icon" src="../../assets/chat-report-detail/icon_send.svg" alt="">
+            <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 :target_active_item="target_active_item" @showMenus="showMenus"></Editor>
+        <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>
+        <!-- <ConversationChatContainer origin="report_detail" :mainStyle="mainStyle"> </ConversationChatContainer>
+         -->
+        <ConversationZnwd></ConversationZnwd>
       </div>
     </div>
   </div>
@@ -33,17 +42,20 @@ 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 { 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 editor_menus = ref([]);
 const target_active_item = ref({});
 
-const report_title = ref('国网四川省电力公司供电可靠性周报')
-const save_time = ref('10:20')
+const report_title = ref('国网四川省电力公司供电可靠性周报');
+const save_time = ref('10:20');
 
-const mainStyle = computed(() => {
-  return {
-    width: '300px'
-  }
-})
+const my_editor = ref();
 
 const showMenus = (menus) => {
   editor_menus.value = menus.value;
@@ -52,12 +64,56 @@ const showMenus = (menus) => {
 const itemClick = (item) => {
   target_active_item.value = item;
 };
+
+const toCopy = () => {
+  const html = my_editor.value.getHtml();
+  copyText(html);
+  Message.success('复制成功');
+};
+
+const renderTitle = (title) => {
+  report_title.value = title;
+};
+
+const toDownload = (item) => {
+  const html = my_editor.value.getHtml();
+  const fullHtml = html.replace(
+    /<img src="data:image\/([^"]+)"/g,
+    (match) => `${match} width="600" height="auto"  style="width: 12cm; height: 8cm;"`
+  );
+  const htmlString = `<!DOCTYPE html>
+    <html lang="en">
+    <head>
+      <meta charset="UTF-8">
+      <title>Document</title>
+    </head>
+    <body>
+      ${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`);
+  } catch (error) {
+    console.error('导出 docx 失败:', error);
+    alert('导出失败,可能存在过大的图片,请压缩后重试');
+  }
+};
 </script>
 <style scoped lang="css">
 .report-detail {
   /* flex-basis: 1; */
   flex-grow: 1;
-  background: #F5FAFA;
+  background: #f5fafa;
   /* flex-shrink: 1; */
   /* background-color: #111; */
 }
@@ -72,7 +128,7 @@ const itemClick = (item) => {
 .editor-detail-editor {
   display: flex;
   flex-direction: column;
-  width: calc(65% - 300px);
+  width: calc(70% - 300px);
 }
 
 .editor-detail-editor .editor-tools {
@@ -95,14 +151,14 @@ const itemClick = (item) => {
   font-family: SourceHanSansCN, SourceHanSansCN;
   /* font-weight: 500; */
   font-size: 20px;
-  color: #181D1D;
+  color: #181d1d;
 }
 
 .tools-left .save {
   font-family: SourceHanSansCN, SourceHanSansCN;
   font-weight: 400;
   font-size: 16px;
-  color: #B0BEBD;
+  color: #b0bebd;
 }
 
 .tools-right {
@@ -114,8 +170,11 @@ const itemClick = (item) => {
 .tools-right .icon {
   cursor: pointer;
 }
+.tools-right .icon:hover {
+  opacity: 0.6;
+}
 
 .right-znws {
-  width: 35%;
+  width: 30%;
 }
 </style>

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

@@ -2,13 +2,18 @@
   <div class="report-header">
     <div class="left-title">
       <!-- <icon-left class="icon" @click="toBack" /> -->
-      <img src="../../assets/chat-report-detail/icon_back.svg" class="icon" @click="toBack" alt="">
+      <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>
-        <span v-else>{{ title }}</span>
+        <a-input ref="inputRef" :style="{ width: '320px' }" v-model="title" v-if="is_edit_title"></a-input>
+        <span v-else>{{ report_title }}</span>
       </div>
-      <img src="../../assets/chat-report-detail/icon_edit.svg" alt="" v-if="!is_edit_title" class="icon"
-        @click="changeEdit">
+      <img
+        src="../../assets/chat-report-detail/icon_edit.svg"
+        alt=""
+        v-if="!is_edit_title"
+        class="icon"
+        @click="changeEdit"
+      />
       <icon-check v-if="is_edit_title" class="icon" @click="changeCheck" />
       <icon-close v-if="is_edit_title" class="icon" @click="changeCancel" />
     </div>
@@ -27,42 +32,49 @@
 </template>
 
 <script setup>
+import { ref, nextTick } from 'vue';
+import { useRouter } from 'vue-router';
+import Template from '../../modules/report/Template.vue';
 
-import { ref, nextTick } from 'vue'
-import { useRouter } from 'vue-router'
-import Template from '../../modules/report/Template.vue'
+const router = useRouter();
 
-const router = useRouter()
+const props = defineProps({
+  report_title: String
+});
 
-const title_origin = ref('')
-const title = ref('国网四川省电力公司供电可靠性周报')
-const is_edit_title = ref(false)
+const emit = defineEmits(['updateTitle']);
 
-const inputRef = ref(null)
+const title_origin = ref('');
+const title = ref();
+const is_edit_title = ref(false);
+
+const inputRef = ref(null);
 
 const changeEdit = () => {
-  is_edit_title.value = !is_edit_title.value
+  is_edit_title.value = !is_edit_title.value;
   if (is_edit_title.value) {
-    title_origin.value = title.value
+    title_origin.value = props.report_title;
+    title.value = props.report_title;
     nextTick(() => {
-      inputRef.value.focus()
-    })
+      inputRef.value.focus();
+    });
   }
-}
+};
 
 const changeCheck = () => {
-  title_origin.value = title.value
-  is_edit_title.value = false
+  emit('updateTitle', title.value);
+  title_origin.value = title.value;
+  is_edit_title.value = false;
   // TODO 调接口更改
-}
+};
 const changeCancel = () => {
-  title.value = title_origin.value
-  is_edit_title.value = false
-}
+  title.value = title_origin.value;
+  is_edit_title.value = false;
+};
 
 const toBack = () => {
-  router.go(-1)
-}
+  router.go(-1);
+};
 </script>
 <style scoped lang="css">
 .report-header {
@@ -93,12 +105,12 @@ const toBack = () => {
 .tool-item {
   display: flex;
   gap: 5px;
-  color: #3D3D3D;
+  color: #3d3d3d;
   cursor: pointer;
 }
 
 .tool-item .icon {
-  color: #3D3D3D;
+  color: #3d3d3d;
 }
 
 :deep(.arco-input-wrapper .arco-input.arco-input-size-medium) {

+ 1 - 1
src/views/routes.js

@@ -21,7 +21,7 @@ const routes = [
     ]
 
   },
-  { path: '/report', component: () => import('./Report.vue') },
+  { path: '/report/:record_id', component: () => import('./Report.vue') },
 
 
   // 登录与注册

+ 2 - 2
vite.config.mjs

@@ -23,8 +23,8 @@ export default defineConfig({
       '/iaserverapi': {
         // target: 'http://192.168.141.78:5678',
         // target: 'http://172.20.10.3:5678',
-        target: 'http://192.168.0.100:5678',
-        // target: 'http://192.168.31.217:5678',
+        // target: 'http://192.168.0.100:5678',
+        target: 'http://47.117.92.63:5678/',
         // target: 'http://ty.bai.sc.sgcc.com.cn',
         // target: 'http://25.214.216.111:9000',
         changeOrigin: true,

Неке датотеке нису приказане због велике количине промена