Jelajahi Sumber

提交页面

luogang 3 bulan lalu
induk
melakukan
3f37bd91ef

+ 3 - 0
plus-ui-ts/.env.development

@@ -33,3 +33,6 @@ VITE_APP_WEBSOCKET = false
 
 # sse 开关
 VITE_APP_SSE = true
+
+# 服务器地址 用于文件
+VITE_APP_BASE_HOST = 'http://jtjai.xt.wenhq.top:8083'

+ 2 - 0
plus-ui-ts/.env.production

@@ -36,3 +36,5 @@ VITE_APP_WEBSOCKET = false
 
 # sse 开关
 VITE_APP_SSE = true
+# 服务器地址 用于文件
+VITE_APP_BASE_HOST = 'http://jtjai.xt.wenhq.top:8083'

+ 6 - 0
plus-ui-ts/src/api/system/event/index.ts

@@ -21,6 +21,12 @@ export const generateReport = (id) => {
     method: 'post'
   });
 };
+// export const gener = (id) => {
+//   return request({
+//     url: `/system/event/generate/${id}`,
+//     method: 'post'
+//   });
+// };
 /**
  * 查询事件详细
  * @param id

+ 135 - 0
plus-ui-ts/src/utils/download.js

@@ -0,0 +1,135 @@
+import axios from 'axios';
+import { getToken } from '@/utils/auth';
+/**
+ * 文件流下载封装 (支持 POST 请求)
+ * @param {string} url - 下载地址
+ * @param {Object} options - 配置选项
+ * @param {'GET'|'POST'} [options.method='GET'] - 请求方法
+ * @param {Object} [options.params] - URL 参数 (GET 使用)
+ * @param {Object} [options.data] - 请求体数据 (POST 使用)
+ * @param {string} [options.filename] - 自定义文件名
+ * @param {Object} [options.headers] - 自定义请求头
+ * @param {number} [options.timeout=30000] - 超时时间(毫秒)
+ * @param {Function} [options.onProgress] - 下载进度回调
+ */
+export const downloadFile = async (url, options = {}) => {
+  const { method = 'GET', params = {}, data = {}, filename = '', headers = {}, timeout = 30000, onProgress } = options;
+
+  try {
+    // 创建取消令牌
+    const cancelToken = axios.CancelToken.source();
+
+    // 请求配置
+    const config = {
+      url,
+      method,
+      responseType: 'blob',
+      cancelToken: cancelToken.token,
+      timeout,
+      headers: {
+        // 默认 headers
+        'Cache-Control': 'no-cache',
+        Authorization: 'Bearer ' + getToken(),
+        clientid: import.meta.env.VITE_APP_CLIENT_ID,
+        Pragma: 'no-cache',
+        ...headers // 合并自定义 headers
+      },
+      onDownloadProgress: (progressEvent) => {
+        if (onProgress && typeof onProgress === 'function') {
+          const percent = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1));
+          onProgress(percent, progressEvent);
+        }
+      }
+    };
+
+    // 根据请求方法添加参数
+    if (method.toUpperCase() === 'GET') {
+      config.params = params;
+    } else {
+      config.data = data;
+    }
+
+    const response = await axios(config);
+
+    // 获取文件名
+    const detectedFilename = getFilenameFromHeaders(response.headers, filename);
+
+    // 创建下载链接
+    const blob = new Blob([response.data]);
+    const downloadUrl = window.URL.createObjectURL(blob);
+    const link = document.createElement('a');
+
+    link.href = downloadUrl;
+    link.download = detectedFilename;
+    link.style.display = 'none';
+    document.body.appendChild(link);
+    link.click();
+
+    // 清理资源
+    window.URL.revokeObjectURL(downloadUrl);
+    document.body.removeChild(link);
+
+    return { success: true, filename: detectedFilename };
+  } catch (error) {
+    // 取消请求不报错
+    if (axios.isCancel(error)) {
+      console.log('下载已取消');
+      return { success: false, error: '下载已取消' };
+    }
+
+    let errorMessage = '文件下载失败';
+
+    // 尝试解析错误响应
+    if (error.response?.data instanceof Blob) {
+      try {
+        const errorText = await error.response.data.text();
+        const errorJson = JSON.parse(errorText);
+        errorMessage = errorJson.message || errorJson.error || errorMessage;
+      } catch {
+        // 无法解析的错误响应
+        errorMessage = `服务器错误: ${error.response.status}`;
+      }
+    } else if (error.message) {
+      errorMessage = error.message;
+    }
+
+    console.error('下载错误:', errorMessage);
+    return { success: false, error: errorMessage };
+  }
+};
+
+/**
+ * 从响应头解析文件名
+ */
+const getFilenameFromHeaders = (headers, defaultName) => {
+  // 优先使用传入的文件名
+  if (defaultName) return defaultName;
+
+  const disposition = headers['content-disposition'] || '';
+
+  // 尝试匹配标准文件名
+  const filenameMatch = disposition.match(/filename="?([^";]+)"?/i);
+  if (filenameMatch && filenameMatch[1]) {
+    return decodeURIComponent(filenameMatch[1]);
+  }
+
+  // 尝试匹配 RFC 5987 编码文件名 (带*)
+  const rfc5987Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
+  if (rfc5987Match && rfc5987Match[1]) {
+    return decodeURIComponent(rfc5987Match[1]);
+  }
+
+  // 默认文件名
+  const ext = headers['content-type']?.split('/')?.[1] || 'bin';
+  return `download_${Date.now()}.${ext}`;
+};
+
+/**
+ * 取消所有进行中的下载 (可选)
+ */
+let activeDownloads = [];
+
+export const cancelDownloads = () => {
+  activeDownloads.forEach((cancel) => cancel());
+  activeDownloads = [];
+};

+ 122 - 25
plus-ui-ts/src/views/index.vue

@@ -228,38 +228,80 @@
           <div class="report-editor">
             <div class="report-title">报告内容:</div>
             <div style="border: 1px solid #d9d9d9; margin-top: 10px">
-              <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" mode="default" />
+              <!-- <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" mode="simple" /> -->
               <Editor style="height: 500px; overflow-y: hidden" v-model="valueHtml" :defaultConfig="editorConfig" @onCreated="handleCreated" />
             </div>
           </div>
           <div style="margin-top: 20px">
             <div class="report-title">格式选择:</div>
-            <el-select style="width: 300px; margin-top: 10px" v-model="fileType" clearable placeholder="请选择">
-              <el-option label="pdf" :value="1"></el-option>
-              <el-option label="word" :value="2"></el-option>
+            <el-select style="width: 300px; margin-top: 10px" v-model="reportForm.type" clearable placeholder="请选择">
+              <el-option label="pdf" value="pdf"></el-option>
+              <el-option label="word" value="docx"></el-option>
             </el-select>
           </div>
-          <div>
-            <el-button color="#2AB55C" style="color: #fff; margin-top: 20px; margin-bottom: 30px" @click="htmlToPdfFn">导出报告</el-button>
+          <div style="margin-top: 20px">
+            <el-button color="#2AB55C" style="color: #fff; margin-bottom: 20px" @click="dialogVisible = true">导出报告</el-button>
           </div>
+          <el-dialog v-model="dialogVisible" title="导出信息" width="500">
+            <el-form ref="reportFormRef" :model="reportForm" label-width="90px" label-suffix=":">
+              <el-row :gutter="20">
+                <el-col :span="24">
+                  <el-form-item label="期数" prop="seq" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
+                    <el-input v-model="reportForm.seq" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="24">
+                  <el-form-item label="发送范围" prop="fw" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
+                    <el-input v-model="reportForm.fw" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="24">
+                  <el-form-item label="备案号" prop="ba" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
+                    <el-input v-model="reportForm.ba" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="24">
+                  <el-form-item label="值班员" prop="zby" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
+                    <el-input v-model="reportForm.zby" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="24">
+                  <el-form-item label="审核" prop="sh" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
+                    <el-input v-model="reportForm.sh" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="24">
+                  <el-form-item label="签发" prop="qf" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
+                    <el-input v-model="reportForm.qf" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </el-form>
+            <template #footer>
+              <div class="dialog-footer">
+                <el-button type="primary" :loading="downLoading" @click="htmlToPdfFn">确 认</el-button>
+              </div>
+            </template>
+          </el-dialog>
         </template>
       </div>
     </el-dialog>
   </div>
 </template>
-<script lang="ts" setup>
+<script setup lang="ts">
 import { Search } from '@element-plus/icons-vue';
 import BaseChart from '@/components/BaseChart/index.vue';
 import monitorIcon from '@/assets/images/monitor.svg';
 import DPlayer from 'dplayer';
 import '@wangeditor/editor/dist/css/style.css'; // 引入 css
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
-import exportUtils from '@/utils/exportUtils';
+import { downloadFile } from '@/utils/download.js';
 import { listEvent, generateReport, updateEvent } from '@/api/system/event';
 import { dateFormat } from '@/utils/index';
 import { ElLoading } from 'element-plus';
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { proxy } = getCurrentInstance();
 const bdmap = ref(null);
+const downLoading = ref(false);
 const marks = ref([
   {
     longitude: 118.84651017058795,
@@ -283,18 +325,8 @@ const valueHtml = ref('');
 const handleCreated = (editor) => {
   editorRef.value = editor; // 记录 editor 实例,重要!
 };
-const fileType = ref(1);
 const showReport = ref(false);
-const htmlToPdfFn = async () => {
-  const dom = document.querySelector('.w-e-text-container');
-  const type = fileType.value;
-  if (type == 1) {
-    exportUtils.exportPDF(dom, '报告.pdf');
-  } else if (type == 2) {
-    exportUtils.exportWord(valueHtml.value, '报告.docx');
-  }
-};
-
+const reportFormRef = ref(null);
 const queryParams = ref({
   pageNum: 1,
   pageSize: 15,
@@ -414,6 +446,24 @@ const pieOptions = computed(() => {
 });
 const eventList = ref([]);
 const total = ref(0);
+const dialogVisible = ref(false);
+const reportForm = reactive({
+  'year': '',
+  'seq': 1,
+  'title': '',
+  'month': '',
+  'day': '',
+  'content': '',
+  'fw': '市委、市政府总值班室',
+  'qf': '朱劲',
+  'sh': '宗仁',
+  'ps': '',
+  'ba': 'KB608',
+  'zby': '83194295',
+  'tpl': 'sg.docx',
+  'type': 'pdf'
+});
+const reportSeq = ref(0);
 onMounted(() => {
   const map = new BMapGL.Map('map'); // 创建地图实例
   const point = new BMapGL.Point(118.879999, 32.016216); // 创建点坐标
@@ -624,8 +674,8 @@ const showDetails = (row) => {
   let imgList = [];
   let videoList = [];
   if (row.ext1 && row.ext1.length) {
-    videoList = row.ext1.filter((item) => item.includes('mp4')).map((item) => `http://jtjai.xt.wenhq.top:8083/api/oss/local/upload/${item}`);
-    imgList = row.ext1.filter((item) => !item.includes('mp4')).map((item) => `http://jtjai.xt.wenhq.top:8083/api/oss/local/upload/${item}`);
+    videoList = row.ext1.filter((item) => item.includes('mp4')).map((item) => `${import.meta.env.VITE_APP_BASE_HOST}/api/oss/local/upload/${item}`);
+    imgList = row.ext1.filter((item) => !item.includes('mp4')).map((item) => `${import.meta.env.VITE_APP_BASE_HOST}/api/oss/local/upload/${item}`);
   }
   Object.assign(form.value, JSON.parse(JSON.stringify(row)), { imgList, videoList });
   nextTick(() => {
@@ -643,22 +693,67 @@ const showDetails = (row) => {
 const generateClick = () => {
   dialog.loading = ElLoading.service({
     lock: true,
-    text: '正在生成报告,请稍候...',
+    text: '正在生成AI报告...',
     fullscreen: false,
     target: '.dialog-loading-warp',
     background: 'rgba(255, 255, 255, 0.6)'
   });
-  generateReport(form.value.id).then(({ code, msg }) => {
+  generateReport(form.value.id).then(async ({ code, msg }) => {
     dialog.loading.close();
     if (code == 200 && msg) {
       showReport.value = true;
-      valueHtml.value = JSON.parse(msg).data.outputs.report;
+      const { report, time } = JSON.parse(msg).data.outputs;
+      const res = await proxy?.getConfigKey('report_seq');
+      reportSeq.value = Number(res.data);
+      valueHtml.value = report.replace(/```/g, '').replace(/html\n/, '');
+      const [year, month, day] = time.split(' ')[0].split('-');
+      reportForm.year = year;
+      reportForm.month = month;
+      reportForm.day = day;
+      reportForm.seq = reportSeq.value;
       proxy?.$modal.msgSuccess('报告生成成功');
     } else {
       proxy?.$modal.msgError('报告生成失败');
     }
   });
 };
+const extractTitleAndContents = (htmlString) => {
+  // 提取标题(从 <h2><strong> 中获取)
+  const titleMatch = htmlString.match(/<h2[^>]*>.*?<strong>(.*?)<\/strong>.*?<\/h2>/is);
+  const title = titleMatch ? titleMatch[1].trim() : '';
+
+  // 提取所有 <p> 内容(从 <p><span> 中获取)
+  const contentMatches = [...htmlString.matchAll(/<p[^>]*>.*?<span[^>]*>(.*?)<\/span>.*?<\/p>/gis)];
+  const contents = contentMatches.map((match) => match[1].trim());
+
+  return {
+    title,
+    contents
+  };
+};
+const htmlToPdfFn = async () => {
+  reportFormRef.value.validate((valid) => {
+    if (valid) {
+      downLoading.value = true;
+      const { title, contents } = extractTitleAndContents(editorRef.value.getHtml());
+      reportForm.title = title;
+      reportForm.content = contents.join('');
+      downloadFile(`loadFile/generate`, {
+        method: 'POST',
+        data: reportForm,
+        filename: `${reportForm.title}.${reportForm.type}`,
+        onProgress: (percent) => {
+          console.log(percent);
+          if (percent == 100) {
+            downLoading.value = false;
+            proxy?.updateConfigByKey('report_seq', reportSeq.value++);
+            proxy?.$modal.msgSuccess('报告导出成功');
+          }
+        }
+      });
+    }
+  });
+};
 const dialogClose = () => {
   showReport.value = false;
   dialog.loading && dialog.loading.close();
@@ -721,6 +816,7 @@ const formSubmit = async (field) => {
     display: flex;
     flex-direction: column;
   }
+
   .expandOrFold {
     position: absolute;
     top: 50%;
@@ -728,6 +824,7 @@ const formSubmit = async (field) => {
     right: 550px;
     cursor: pointer;
     z-index: 10;
+
     img {
       height: 50px;
     }

+ 8 - 1
plus-ui-ts/vite.config.ts

@@ -25,10 +25,17 @@ export default defineConfig(({ mode, command }) => {
       proxy: {
         [env.VITE_APP_BASE_API]: {
           // target: 'http://localhost:8080',
-          target: 'http://jtjai.xt.wenhq.top:8083//api',
+          target: 'http://jtjai.xt.wenhq.top:8083/api',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
+        },
+        '/loadFile': {
+          // target: 'http://localhost:8080',
+          target: 'http://jtjai.xt.wenhq.top:8083/',
+          changeOrigin: true,
+          ws: true,
+          rewrite: (path) => path.replace(new RegExp('^' + '/loadFile'), '')
         }
       }
     },

File diff ditekan karena terlalu besar
+ 177 - 409
uni-jtjai/pnpm-lock.yaml


+ 12 - 10
uni-jtjai/src/App.vue

@@ -1,16 +1,18 @@
 <script setup>
   const { API_LOGIN } = useRequest()
   onLaunch(() => {
-    // API_LOGIN({
-    //   tenantId: '000000',
-    //   username: 'admin',
-    //   password: 'admin123',
-    //   rememberMe: false,
-    //   uuid: 'e7e7a3cc995c461eb276fc98c6584a9f',
-    //   code: '9',
-    //   clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
-    //   grantType: 'password'
-    // })
+    API_LOGIN({
+      username: 'admin',
+      password: 'admin123',
+      rememberMe: false,
+      clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
+      grantType: 'password'
+    }).then(({ code, data }) => {
+      if (code === 200) {
+        uni.setStorageSync('token', data.access_token)
+        uni.setStorageSync('clientId', data.client_id)
+      }
+    })
   })
 </script>
 

+ 17 - 0
uni-jtjai/src/api/modules/event.js

@@ -28,5 +28,22 @@ export default {
       url: `/system/event/generate/${id}`,
       method: 'post'
     })
+  },
+  // 根据参数键名查询参数值
+  API_CONFIG_GET(configKey) {
+    return request({
+      url: '/system/config/configKey/' + configKey,
+      method: 'get'
+    })
+  },
+  API_CONFIG_UPDATE(key, value) {
+    return request({
+      url: '/system/config/updateByKey',
+      method: 'put',
+      data: {
+        configKey: key,
+        configValue: value
+      }
+    })
   }
 }

+ 2 - 0
uni-jtjai/src/api/request.js

@@ -9,6 +9,8 @@ function createRequest(service) {
       responseType: 'json', // 响应类型
       headers: {
         // 请求头配置...
+        Authorization: 'Bearer ' + uni.getStorageSync('token'),
+        clientid: uni.getStorageSync('clientId')
       }
     }
     const requestConfig = Object.assign(configDefault, config)

+ 10 - 6
uni-jtjai/src/pages/index/index.vue

@@ -198,7 +198,7 @@
   </view>
 </template>
 
-<script setup lang="ts">
+<script setup>
   import monitorIcon from '/static/images/monitor.svg'
   import * as echarts from 'echarts'
   import lEchart from '@/uni_modules/lime-echart/components/l-echart/l-echart.vue'
@@ -597,6 +597,8 @@
   .content {
     height: 100%;
     background: #fff;
+    display: flex;
+    flex-direction: column;
 
     :deep(.anchorBL) {
       display: none;
@@ -606,6 +608,8 @@
   .map-part {
     width: 100%;
     height: 100%;
+    flex: 1;
+    overflow-y: auto;
 
     .map {
       width: 100%;
@@ -642,6 +646,7 @@
     &-header {
       background: #f8f8f8;
       padding: 20rpx;
+
       :deep(.wd-input__placeholder) {
         color: #262626;
       }
@@ -713,9 +718,11 @@
   }
 
   .data-part {
-    height: 100%;
     width: 100%;
-    padding-bottom: 150rpx;
+    flex: 1;
+    padding-bottom: 50rpx;
+    overflow-y: auto;
+
     .data-panel {
       .custom-title {
         display: flex;
@@ -806,8 +813,6 @@
   }
 
   .fixed-footer {
-    position: fixed;
-    bottom: 0;
     height: 120rpx;
     width: 100%;
     display: flex;
@@ -815,7 +820,6 @@
     line-height: 120rpx;
     color: #313131;
     font-size: 34rpx;
-    z-index: 11;
 
     .tab-selected {
       background: #507afc;

+ 171 - 33
uni-jtjai/src/pages/index/info.vue

@@ -1,10 +1,14 @@
 <template>
   <view class="content">
+    <wd-toast />
     <wd-cell-group style="padding: 30rpx 20rpx" title="" border>
       <wd-cell title="事件ID" title-width="100%" :label="form.id" />
       <wd-cell title="事件类型" title-width="100%" :label="form.ext2.lx" />
       <wd-cell title="发生时间" title-width="100%" :label="form.createTimeFormat" />
-      <wd-cell title="事件等级" :label="form.level" />
+      <wd-cell
+        title="事件等级"
+        :label="eventLevel.filter((item) => item.value == form.level)[0]?.label"
+      />
       <wd-cell title="桩号" :label="form.ext2.zh" />
       <wd-cell title="地点描述" title-width="100%" :label="form.addr" />
       <wd-cell title="现场画面" title-width="100%">
@@ -50,7 +54,60 @@
           </view>
         </template>
       </wd-cell>
-      <wd-picker :columns="fileTypeOptions" label="格式选择:" v-model="fileType" align-right />
+      <wd-picker
+        :columns="fileTypeOptions"
+        label="格式选择:"
+        v-model="reportForm.type"
+        align-right
+      />
+      <wd-message-box custom-class="custom-dialog" selector="wd-message-box-slot">
+        <wd-form ref="reportFormRef" :model="reportForm">
+          <wd-cell-group border>
+            <wd-input
+              label="期数"
+              prop="seq"
+              v-model="reportForm.seq"
+              placeholder="请输入"
+              :rules="[{ required: true, message: '请输入' }]"
+            />
+            <wd-input
+              label="发送范围"
+              prop="fw"
+              v-model="reportForm.fw"
+              placeholder="请输入"
+              :rules="[{ required: true, message: '请输入' }]"
+            />
+            <wd-input
+              label="备案号"
+              prop="ba"
+              v-model="reportForm.ba"
+              placeholder="请输入"
+              :rules="[{ required: true, message: '请输入' }]"
+            />
+            <wd-input
+              label="值班员"
+              prop="zby"
+              v-model="reportForm.zby"
+              placeholder="请输入"
+              :rules="[{ required: true, message: '请输入' }]"
+            />
+            <wd-input
+              label="审核"
+              prop="sh"
+              v-model="reportForm.sh"
+              placeholder="请输入"
+              :rules="[{ required: true, message: '请输入' }]"
+            />
+            <wd-input
+              label="签发"
+              prop="qf"
+              v-model="reportForm.qf"
+              placeholder="请输入"
+              :rules="[{ required: true, message: '请输入' }]"
+            />
+          </wd-cell-group>
+        </wd-form>
+      </wd-message-box>
       <wd-button
         type="success"
         :round="false"
@@ -63,24 +120,58 @@
 </template>
 <script setup>
   import DPlayer from 'dplayer'
-  import '@wangeditor/editor/dist/css/style.css' // 引入 css
-  import exportUtils from '@/utils/exportUtils.js'
-  const { API_EVENT_GEN } = useRequest()
+  import { useMessage, useToast } from 'wot-design-uni'
+  import { downloadFile } from '@/utils/download.js'
+  const message = useMessage('wd-message-box-slot')
+  const toast = useToast()
+  const { API_EVENT_GEN, API_CONFIG_GET, API_CONFIG_UPDATE } = useRequest()
   const editorCtx = ref({})
   const valueHtml = ref('')
-  const fileType = ref('1')
+  const reportFormRef = ref(null)
+  const eventLevel = [
+    {
+      label: '内部掌握',
+      value: '1'
+    },
+    {
+      label: '建议上报',
+      value: '2'
+    }
+  ]
   const fileTypeOptions = [
     {
-      value: '1',
+      value: 'pdf',
       label: 'pdf'
     },
     {
-      value: '2',
+      value: 'docx',
       label: 'word'
     }
   ]
   const showReport = ref(false)
-  const form = ref({})
+  const form = ref({
+    ext2: {
+      lx: '',
+      zh: ''
+    }
+  })
+  const reportSeq = ref(1)
+  const reportForm = reactive({
+    year: '',
+    seq: 1,
+    title: '',
+    month: '',
+    day: '',
+    content: '',
+    fw: '市委、市政府总值班室',
+    qf: '朱劲',
+    sh: '宗仁',
+    ps: '',
+    ba: 'KB608',
+    zby: '83194295',
+    tpl: 'sg.docx',
+    type: 'pdf'
+  })
   onLoad(async (data) => {
     form.value = JSON.parse(data.event)
     let imgList = []
@@ -106,41 +197,79 @@
     })
   })
   const generateClick = () => {
-    uni.showLoading({
-      title: '正在生成报告...',
-      mask: true
-    })
-    API_EVENT_GEN(form.value.id).then(({ code, msg }) => {
-      uni.hideLoading()
+    toast.loading('正在生成AI报告...')
+    API_EVENT_GEN(form.value.id).then(async ({ code, msg }) => {
+      toast.close()
       if (code === 200 && msg) {
         showReport.value = true
-        valueHtml.value = JSON.parse(msg).data.outputs.report
+        const { report, time } = JSON.parse(msg).data.outputs
+        const res = await API_CONFIG_GET('report_seq')
+        reportSeq.value = Number(res.data)
+        valueHtml.value = report.replace(/```/g, '').replace(/html\n/, '')
+        const [year, month, day] = time.split(' ')[0].split('-')
+        reportForm.year = year
+        reportForm.month = month
+        reportForm.day = day
+        reportForm.seq = reportSeq.value
         editorCtx.value.setContents({
           html: valueHtml.value
         })
-        uni.showToast({
-          title: '报告生成成功',
-          duration: 2000
-        })
+        toast.success('报告生成成功')
       } else {
-        uni.showToast({
-          title: '报告生成失败',
-          icon: 'fail',
-          duration: 2000
-        })
+        toast.error('报告生成失败')
       }
     })
   }
+  const extractTitleAndContents = (htmlString) => {
+    // 提取标题(从 <h2><strong> 中获取)
+    const titleMatch = htmlString.match(/<h2[^>]*>.*?<strong>(.*?)<\/strong>.*?<\/h2>/is)
+    const title = titleMatch ? titleMatch[1].trim() : ''
+
+    // 提取所有 <p> 内容(从 <p><span> 中获取)
+    const contentMatches = [
+      ...htmlString.matchAll(/<p[^>]*>.*?<span[^>]*>(.*?)<\/span>.*?<\/p>/gis)
+    ]
+    const contents = contentMatches.map((match) => match[1].trim())
+
+    return {
+      title,
+      contents
+    }
+  }
   const htmlToPdfFn = async () => {
-    editorCtx.value.getContents({
-      success: ({ html }) => {
-        if (fileType.value === '1') {
-          exportUtils.exportPDF(html, '报告.pdf')
-        } else if (fileType.value === '2') {
-          exportUtils.exportWord(html, '报告.docx')
+    message
+      .confirm({
+        title: '',
+        beforeConfirm: ({ resolve }) => {
+          reportFormRef.value.validate().then(({ valid, errors }) => {
+            if (valid) {
+              toast.loading('正在导出...')
+              editorCtx.value.getContents({
+                success: ({ html }) => {
+                  const { title, contents } = extractTitleAndContents(html)
+                  reportForm.title = title
+                  reportForm.content = contents.join('')
+                  downloadFile(`loadFile/generate`, {
+                    method: 'POST',
+                    data: reportForm,
+                    filename: `${reportForm.title}.${reportForm.type}`,
+                    onProgress: (percent) => {
+                      console.log(percent)
+                      if (percent === 100) {
+                        toast.close()
+                        resolve(true)
+                        toast.success('报告导出成功')
+                        API_CONFIG_UPDATE('report_seq', reportSeq.value + 1)
+                      }
+                    }
+                  })
+                }
+              })
+            }
+          })
         }
-      }
-    })
+      })
+      .then(() => {})
   }
   const onEditorReady = () => {
     uni
@@ -178,4 +307,13 @@
       height: 100%;
     }
   }
+  .content {
+    min-height: 100vh;
+    :deep(.wd-message-box__body.is-no-title) {
+      padding: 30rpx 0px 0px;
+    }
+    // :deep(.wd-message-box__content) {
+    //   max-height: 700rpx;
+    // }
+  }
 </style>

+ 140 - 0
uni-jtjai/src/utils/download.js

@@ -0,0 +1,140 @@
+import axios from 'axios'
+/**
+ * 文件流下载封装 (支持 POST 请求)
+ * @param {string} url - 下载地址
+ * @param {Object} options - 配置选项
+ * @param {'GET'|'POST'} [options.method='GET'] - 请求方法
+ * @param {Object} [options.params] - URL 参数 (GET 使用)
+ * @param {Object} [options.data] - 请求体数据 (POST 使用)
+ * @param {string} [options.filename] - 自定义文件名
+ * @param {Object} [options.headers] - 自定义请求头
+ * @param {number} [options.timeout=30000] - 超时时间(毫秒)
+ * @param {Function} [options.onProgress] - 下载进度回调
+ */
+export const downloadFile = async (url, options = {}) => {
+  const {
+    method = 'GET',
+    params = {},
+    data = {},
+    filename = '',
+    headers = {},
+    timeout = 30000,
+    onProgress
+  } = options
+
+  try {
+    // 创建取消令牌
+    const cancelToken = axios.CancelToken.source()
+
+    // 请求配置
+    const config = {
+      url,
+      method,
+      responseType: 'blob',
+      cancelToken: cancelToken.token,
+      timeout,
+      headers: {
+        // 默认 headers
+        'Cache-Control': 'no-cache',
+        Pragma: 'no-cache',
+        ...headers // 合并自定义 headers
+      },
+      onDownloadProgress: (progressEvent) => {
+        if (onProgress && typeof onProgress === 'function') {
+          const percent = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1))
+          onProgress(percent, progressEvent)
+        }
+      }
+    }
+
+    // 根据请求方法添加参数
+    if (method.toUpperCase() === 'GET') {
+      config.params = params
+    } else {
+      config.data = data
+    }
+
+    const response = await axios(config)
+
+    // 获取文件名
+    const detectedFilename = getFilenameFromHeaders(response.headers, filename)
+
+    // 创建下载链接
+    const blob = new Blob([response.data])
+    const downloadUrl = window.URL.createObjectURL(blob)
+    const link = document.createElement('a')
+
+    link.href = downloadUrl
+    link.download = detectedFilename
+    link.style.display = 'none'
+    document.body.appendChild(link)
+    link.click()
+
+    // 清理资源
+    window.URL.revokeObjectURL(downloadUrl)
+    document.body.removeChild(link)
+
+    return { success: true, filename: detectedFilename }
+  } catch (error) {
+    // 取消请求不报错
+    if (axios.isCancel(error)) {
+      console.log('下载已取消')
+      return { success: false, error: '下载已取消' }
+    }
+
+    let errorMessage = '文件下载失败'
+
+    // 尝试解析错误响应
+    if (error.response?.data instanceof Blob) {
+      try {
+        const errorText = await error.response.data.text()
+        const errorJson = JSON.parse(errorText)
+        errorMessage = errorJson.message || errorJson.error || errorMessage
+      } catch {
+        // 无法解析的错误响应
+        errorMessage = `服务器错误: ${error.response.status}`
+      }
+    } else if (error.message) {
+      errorMessage = error.message
+    }
+
+    console.error('下载错误:', errorMessage)
+    return { success: false, error: errorMessage }
+  }
+}
+
+/**
+ * 从响应头解析文件名
+ */
+const getFilenameFromHeaders = (headers, defaultName) => {
+  // 优先使用传入的文件名
+  if (defaultName) return defaultName
+
+  const disposition = headers['content-disposition'] || ''
+
+  // 尝试匹配标准文件名
+  const filenameMatch = disposition.match(/filename="?([^";]+)"?/i)
+  if (filenameMatch && filenameMatch[1]) {
+    return decodeURIComponent(filenameMatch[1])
+  }
+
+  // 尝试匹配 RFC 5987 编码文件名 (带*)
+  const rfc5987Match = disposition.match(/filename\*=UTF-8''([^;]+)/i)
+  if (rfc5987Match && rfc5987Match[1]) {
+    return decodeURIComponent(rfc5987Match[1])
+  }
+
+  // 默认文件名
+  const ext = headers['content-type']?.split('/')?.[1] || 'bin'
+  return `download_${Date.now()}.${ext}`
+}
+
+/**
+ * 取消所有进行中的下载 (可选)
+ */
+let activeDownloads = []
+
+export const cancelDownloads = () => {
+  activeDownloads.forEach((cancel) => cancel())
+  activeDownloads = []
+}

+ 6 - 0
uni-jtjai/vite.config.js

@@ -87,6 +87,12 @@ export default ({ command, mode }) => {
               target: VITE_APP_API_BASEURL,
               changeOrigin: true
               //   rewrite: (path) => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), '')
+            },
+            '/loadFile': {
+              target: VITE_APP_API_BASEURL,
+              changeOrigin: true,
+              ws: true,
+              rewrite: (path) => path.replace(new RegExp('^' + '/loadFile'), '')
             }
           }
         : undefined

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini