Henry 1 год назад
Родитель
Сommit
4f7056355d

+ 74 - 35
package-lock.json

@@ -12,11 +12,13 @@
         "@highlightjs/vue-plugin": "^2.0.1",
         "axios": "^0.21.1",
         "core-js": "^3.6.5",
-        "diff2html": "^3.4.16",
+        "diff": "^7.0.0",
+        "diff2html": "^3.4.51",
         "echarts": "^5.1.1",
         "element-plus": "^1.0.2-beta.31",
-        "highlight.js": "^11.5.1",
+        "highlight.js": "^11.11.1",
         "jsonwebtoken": "^8.5.1",
+        "marked": "^4.2.12",
         "vue": "^3.0.5",
         "vue-axios": "^3.2.4",
         "vue-class-component": "^8.0.0-0",
@@ -7534,35 +7536,38 @@
       "dev": true
     },
     "node_modules/diff": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz",
-      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
-      "dev": true,
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+      "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.3.1"
       }
     },
     "node_modules/diff2html": {
-      "version": "3.4.17",
-      "resolved": "https://registry.npmmirror.com/diff2html/-/diff2html-3.4.17.tgz",
-      "integrity": "sha512-B/H+iLRHTsRl2Ffs/7tYJ0Rg4uisXe83inIdNE8trXY83Wn7OCTslJNP7fyaUpSsLbRIzPSNgT7LqFNiIQlDyg==",
+      "version": "3.4.51",
+      "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.51.tgz",
+      "integrity": "sha512-/rVCSDyokkzSCEGaGjkkElXtIRwyNDRzIa3S8VUhR6pjk25p6+AMnb1s2zGmhjl66D5m/HnV3IeZoxnWsvTy+w==",
+      "license": "MIT",
       "dependencies": {
-        "diff": "5.0.0",
+        "diff": "^7.0.0",
         "hogan.js": "3.0.2"
       },
       "engines": {
         "node": ">=12"
       },
       "optionalDependencies": {
-        "highlight.js": "11.5.1"
+        "highlight.js": "11.9.0"
       }
     },
-    "node_modules/diff2html/node_modules/diff": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmmirror.com/diff/-/diff-5.0.0.tgz",
-      "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+    "node_modules/diff2html/node_modules/highlight.js": {
+      "version": "11.9.0",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz",
+      "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==",
+      "license": "BSD-3-Clause",
+      "optional": true,
       "engines": {
-        "node": ">=0.3.1"
+        "node": ">=12.0.0"
       }
     },
     "node_modules/diffie-hellman": {
@@ -9710,9 +9715,10 @@
       "dev": true
     },
     "node_modules/highlight.js": {
-      "version": "11.5.1",
-      "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.5.1.tgz",
-      "integrity": "sha512-LKzHqnxr4CrD2YsNoIf/o5nJ09j4yi/GcH5BnYz9UnVpZdS4ucMgvP61TDty5xJcFGRjnH4DpujkS9bHT3hq0Q==",
+      "version": "11.11.1",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+      "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=12.0.0"
       }
@@ -11911,6 +11917,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/marked": {
+      "version": "4.2.12",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz",
+      "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==",
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
     "node_modules/md5.js": {
       "version": "1.3.5",
       "resolved": "https://registry.npmmirror.com/md5.js/-/md5.js-1.3.5.tgz",
@@ -16509,6 +16527,16 @@
         "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev"
       }
     },
+    "node_modules/tslint/node_modules/diff": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
     "node_modules/tslint/node_modules/tsutils": {
       "version": "2.29.0",
       "resolved": "https://registry.npmmirror.com/tsutils/-/tsutils-2.29.0.tgz",
@@ -24535,25 +24563,25 @@
       "dev": true
     },
     "diff": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz",
-      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
-      "dev": true
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+      "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="
     },
     "diff2html": {
-      "version": "3.4.17",
-      "resolved": "https://registry.npmmirror.com/diff2html/-/diff2html-3.4.17.tgz",
-      "integrity": "sha512-B/H+iLRHTsRl2Ffs/7tYJ0Rg4uisXe83inIdNE8trXY83Wn7OCTslJNP7fyaUpSsLbRIzPSNgT7LqFNiIQlDyg==",
+      "version": "3.4.51",
+      "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.51.tgz",
+      "integrity": "sha512-/rVCSDyokkzSCEGaGjkkElXtIRwyNDRzIa3S8VUhR6pjk25p6+AMnb1s2zGmhjl66D5m/HnV3IeZoxnWsvTy+w==",
       "requires": {
-        "diff": "5.0.0",
-        "highlight.js": "11.5.1",
+        "diff": "^7.0.0",
+        "highlight.js": "11.9.0",
         "hogan.js": "3.0.2"
       },
       "dependencies": {
-        "diff": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmmirror.com/diff/-/diff-5.0.0.tgz",
-          "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="
+        "highlight.js": {
+          "version": "11.9.0",
+          "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz",
+          "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==",
+          "optional": true
         }
       }
     },
@@ -26291,9 +26319,9 @@
       "dev": true
     },
     "highlight.js": {
-      "version": "11.5.1",
-      "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.5.1.tgz",
-      "integrity": "sha512-LKzHqnxr4CrD2YsNoIf/o5nJ09j4yi/GcH5BnYz9UnVpZdS4ucMgvP61TDty5xJcFGRjnH4DpujkS9bHT3hq0Q=="
+      "version": "11.11.1",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+      "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="
     },
     "hmac-drbg": {
       "version": "1.0.1",
@@ -28037,6 +28065,11 @@
         "object-visit": "^1.0.0"
       }
     },
+    "marked": {
+      "version": "4.2.12",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz",
+      "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw=="
+    },
     "md5.js": {
       "version": "1.3.5",
       "resolved": "https://registry.npmmirror.com/md5.js/-/md5.js-1.3.5.tgz",
@@ -31790,6 +31823,12 @@
         "tsutils": "^2.29.0"
       },
       "dependencies": {
+        "diff": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+          "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+          "dev": true
+        },
         "tsutils": {
           "version": "2.29.0",
           "resolved": "https://registry.npmmirror.com/tsutils/-/tsutils-2.29.0.tgz",

+ 4 - 2
package.json

@@ -15,11 +15,13 @@
     "@highlightjs/vue-plugin": "^2.0.1",
     "axios": "^0.21.1",
     "core-js": "^3.6.5",
-    "diff2html": "^3.4.16",
+    "diff": "^7.0.0",
+    "diff2html": "^3.4.51",
     "echarts": "^5.1.1",
     "element-plus": "^1.0.2-beta.31",
-    "highlight.js": "^11.5.1",
+    "highlight.js": "^11.11.1",
     "jsonwebtoken": "^8.5.1",
+    "marked": "^4.2.12",
     "vue": "^3.0.5",
     "vue-axios": "^3.2.4",
     "vue-class-component": "^8.0.0-0",

+ 1 - 1
src/api/devcloud/project.ts

@@ -14,7 +14,7 @@ export const createProject = (name: string, description: string) => axios.post(`
   description,
 });
 
-export const getProjectListByUserId = () => axios.get(`${PROJECT_PREFIX}/listByUser`);
+export const getProjectListByUserId = () => axios.get(`${PROJECT_PREFIX}/listByUser`).then((res) => { console.log(res); return res; });
 
 export const addProjectMember = (addProjectMemberPayload: AddProjectMemberPayload) => axios.post(`${PROJECT_PREFIX}/members`, null, {
   params: addProjectMemberPayload,

+ 13 - 0
src/components/SideBar.vue

@@ -41,6 +41,7 @@ const sidebar = [
   { name: 'bugList', description: '缺陷列表', hide: false },
   { name: 'codereview', description: '代码评审', hide: false },
   { name: 'invite', description: '项目关联', hide: false },
+  { name: 'configPatch', description: '配置补丁', hide: false },
 ];
 
 export default {
@@ -202,6 +203,18 @@ export default {
     color: #1296db;
   }
 }
+.icon[target=configPatch]{
+  background-image: url("../assets/icon_new/codereview.svg");
+  .el-menu-item:hover & {
+    background-image: url("../assets/icon_new/codereview-hover.svg");
+    color: #2c2c2c;
+  }
+  .el-menu-item.is-active & {
+    background-image: url("../assets/icon_new/codereview-using.svg");
+    color: #1296db;
+  }
+}
+
 .title{
   font-size: 9px;
   margin-top: 12px;

+ 17 - 1
src/router/index.ts

@@ -1,6 +1,8 @@
 import { isUserLoggedIn } from '@/utils/user';
 import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
-import { HOME, LOGIN, PROJECT } from './name';
+import {
+  HOME, LOGIN, PROJECT, CHANGE_DETAIL, CONVERSATION_DETAIL,
+} from './name';
 
 const AuthWrapper = (routeRecord: RouteRecordRaw) => {
   const meta = routeRecord.meta || {};
@@ -86,6 +88,20 @@ const routes: Array<RouteRecordRaw> = [
         path: 'commit-detail',
         component: () => import(/* webpackChunkName: "project" */'@/views/Project/CommitDetails.vue'),
       },
+      {
+        path: 'configpatch',
+        component: () => import(/* webpackChunkName: "project" */'@/views/Project/ConfigPatch.vue'),
+      },
+      {
+        path: 'change-detail/:id',
+        name: CHANGE_DETAIL,
+        component: () => import(/* webpackChunkName: "project" */'@/views/Project/ChangeDetail.vue'),
+      },
+      {
+        path: 'conversation-detail/:id',
+        name: CONVERSATION_DETAIL,
+        component: () => import(/* webpackChunkName: "project" */'@/views/Project/ConversationDetail.vue'),
+      },
     ],
   }),
 ];

+ 2 - 0
src/router/name.ts

@@ -1,3 +1,5 @@
 export const HOME = 'Home';
 export const PROJECT = 'Project';
 export const LOGIN = 'Login';
+export const CHANGE_DETAIL = 'ChangeDetail';
+export const CONVERSATION_DETAIL = 'ConversationDetail';

+ 413 - 0
src/views/Project/ChangeDetail.vue

@@ -0,0 +1,413 @@
+<template>
+  <div>
+    <el-button type="text" size="small" @click="goBack">
+      <i class="el-icon-back">返回上一页</i>
+    </el-button>
+
+    <div class="detail-container">
+      <el-card shadow="never">
+        <template #header>
+          <div class="card-header">
+            <h2>配置更改详情</h2>
+            <div class="header-actions">
+              <el-button type="danger" size="small" @click="rollbackChanges" :loading="isRollbackLoading">
+                <i class="el-icon-refresh-left"></i> 回退此更改
+              </el-button>
+              <div class="change-time">{{ time }}</div>
+            </div>
+          </div>
+        </template>
+
+        <div class="change-info">
+          <h3>更改描述</h3>
+          <div class="description">{{ description }}</div>
+
+          <el-divider></el-divider>
+
+          <h3>更改内容</h3>
+          <div class="content-comparison">
+            <div class="before-change">
+              <div class="code-header">
+                <h4>更改前</h4>
+                <span class="file-path" v-if="filePath">{{ filePath }}</span>
+              </div>
+              <div class="code-editor">
+                <div class="code-content" ref="beforeCodeContent" @scroll="syncScroll($event, 'before')">
+                  <div class="line-numbers">
+                    <div v-for="i in beforeLineCount" :key="'before-'+i" class="line-number">{{ i }}</div>
+                  </div>
+                  <pre class="code-block"><code v-html="highlightedBeforeContent"></code></pre>
+                </div>
+              </div>
+            </div>
+
+            <div class="after-change">
+              <div class="code-header">
+                <h4>更改后</h4>
+                <span class="file-path" v-if="filePath">{{ filePath }}</span>
+              </div>
+              <div class="code-editor">
+                <div class="code-content" ref="afterCodeContent" @scroll="syncScroll($event, 'after')">
+                  <div class="line-numbers">
+                    <div v-for="i in afterLineCount" :key="'after-'+i" class="line-number">{{ i }}</div>
+                  </div>
+                  <pre class="code-block"><code v-html="highlightedAfterContent"></code></pre>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+import hljs from 'highlight.js/lib/core';
+import gradle from 'highlight.js/lib/languages/gradle';
+import 'highlight.js/styles/vs2015.css';
+
+// 注册需要的语言
+hljs.registerLanguage('gradle', gradle);
+
+export default {
+  name: 'ChangeDetail',
+  data() {
+    return {
+      id: null,
+      description: '',
+      time: '',
+      filePath: '',
+      isRollbackLoading: false,
+      beforeContent: '',
+      afterContent: '',
+      fileChangeId: null,
+      highlightedBeforeContent: '',
+      highlightedAfterContent: '',
+      scrollingSide: null, // 用于跟踪哪个面板正在滚动
+    };
+  },
+  computed: {
+    beforeLineCount() {
+      return this.beforeContent ? this.beforeContent.split('\n').length : 0;
+    },
+    afterLineCount() {
+      return this.afterContent ? this.afterContent.split('\n').length : 0;
+    },
+  },
+  watch: {
+    beforeContent() {
+      this.highlightCode();
+    },
+    afterContent() {
+      this.highlightCode();
+    },
+  },
+  created() {
+    // 从路由获取参数
+    this.id = this.$route.params.id;
+    this.description = this.$route.query.description || '未知修改';
+    this.time = this.$route.query.time || '未知时间';
+    this.fileChangeId = this.$route.query.fileChangeId;
+
+    // 加载更改详情数据
+    this.loadChangeDetails();
+  },
+  methods: {
+    goBack() {
+      this.$router.go(-1);
+    },
+    async loadChangeDetails() {
+      try {
+        if (!this.fileChangeId) {
+          this.$message.error('未找到更改ID');
+          return;
+        }
+
+        const response = await axios.get(`http://localhost:8080/config/changes/${this.fileChangeId}`);
+        if (response.data && response.data.length) {
+          const codeFiles = response.data;
+          // 获取文件路径
+          if (codeFiles.length > 0 && codeFiles[0].filePath) {
+            this.filePath = codeFiles[0].filePath;
+          }
+
+          // 显示前后文件内容
+          if (codeFiles.length >= 2) {
+            this.beforeContent = codeFiles[0].content;
+            this.afterContent = codeFiles[1].content;
+          } else if (codeFiles.length === 1) {
+            // 如果只有一个文件,可能是新增文件
+            this.afterContent = codeFiles[0].content;
+            this.beforeContent = '// 无变更前内容';
+          }
+
+          this.highlightCode();
+        } else {
+          this.$message.warning('未找到文件变更内容');
+        }
+      } catch (error) {
+        console.error('获取变更详情失败:', error);
+        this.$message.error('获取变更详情失败');
+      }
+    },
+    highlightCode() {
+      try {
+        // 根据文件类型判断使用的语言
+        const fileExtension = this.filePath ? this.filePath.split('.').pop().toLowerCase() : '';
+        let language = 'gradle'; // 默认为gradle
+
+        // 根据文件扩展名判断语言类型
+        switch (fileExtension) {
+          case 'java':
+            language = 'java';
+            break;
+          case 'js':
+            language = 'javascript';
+            break;
+          case 'py':
+            language = 'python';
+            break;
+          case 'xml':
+            language = 'xml';
+            break;
+          // 可以添加更多类型
+          default:
+            language = 'gradle'; // 默认使用gradle语法高亮
+            break;
+        }
+
+        // 为代码块添加高亮样式
+        if (this.beforeContent) {
+          const highlighted = hljs.highlight(this.beforeContent, { language }).value;
+          this.highlightedBeforeContent = highlighted;
+        }
+
+        if (this.afterContent) {
+          const highlighted = hljs.highlight(this.afterContent, { language }).value;
+          this.highlightedAfterContent = highlighted;
+        }
+      } catch (error) {
+        console.error('代码高亮处理失败:', error);
+        // 如果高亮失败,使用原始内容
+        this.highlightedBeforeContent = this.escapeHtml(this.beforeContent);
+        this.highlightedAfterContent = this.escapeHtml(this.afterContent);
+      }
+    },
+    escapeHtml(unsafe) {
+      return unsafe
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/"/g, '&quot;')
+        .replace(/'/g, '&#039;');
+    },
+    async rollbackChanges() {
+      // 显示确认对话框
+      try {
+        await this.$confirm('确定要回退此更改吗?此操作不可逆。', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        });
+
+        // 用户确认回退
+        this.isRollbackLoading = true;
+
+        try {
+          const response = await axios.post(`http://localhost:8080/config/rollback/${this.fileChangeId}`);
+          this.$message.success(response.data || '回退成功!');
+
+          // 回退成功后返回上一页
+          setTimeout(() => {
+            this.goBack();
+          }, 1000);
+        } catch (error) {
+          console.error('回退操作失败:', error);
+          let errorMsg = '回退失败';
+          if (error.response && error.response.data) {
+            errorMsg = error.response.data;
+          }
+          this.$message.error(errorMsg);
+        } finally {
+          this.isRollbackLoading = false;
+        }
+      } catch (error) {
+        // 用户取消回退
+        this.$message.info('已取消回退');
+      }
+    },
+    syncScroll(event, side) {
+      // 防止无限循环滚动
+      if (this.scrollingSide && this.scrollingSide !== side) {
+        return;
+      }
+
+      this.scrollingSide = side;
+
+      // 延迟清除滚动状态
+      setTimeout(() => {
+        this.scrollingSide = null;
+      }, 50);
+    },
+  },
+};
+</script>
+
+<style scoped>
+.detail-container {
+  margin: 20px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-actions {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+}
+
+.change-time {
+  color: #909399;
+  font-size: 14px;
+}
+
+.change-info {
+  padding: 10px;
+}
+
+.description {
+  color: #606266;
+  padding: 10px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  margin-bottom: 20px;
+}
+
+.content-comparison {
+  display: flex;
+  gap: 30px;
+  margin-top: 20px;
+  max-width: 100%;
+  height: calc(100vh - 300px); /* 使用视口高度限制总高度 */
+  min-height: 400px; /* 最小高度确保足够的显示空间 */
+}
+
+.before-change, .after-change {
+  flex: 1;
+  min-width: 0; /* 确保flex子项目不会超出容器 */
+  display: flex;
+  flex-direction: column;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  overflow: hidden;
+  max-height: 100%; /* 确保不超出父容器 */
+}
+
+.code-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background-color: #2c3e50;
+  color: #fff;
+  padding: 8px 12px;
+  border-bottom: 1px solid #1c2b3a;
+}
+
+.code-header h4 {
+  margin: 0;
+  font-size: 14px;
+}
+
+.file-path {
+  font-size: 12px;
+  opacity: 0.8;
+  max-width: 200px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.code-editor {
+  display: flex;
+  background-color: #1e1e1e;
+  overflow: hidden;
+  height: 100%; /* 使编辑器填充剩余空间 */
+  flex: 1; /* 让编辑器区域占用所有可用空间 */
+}
+
+.code-content {
+  display: flex;
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+  position: relative;
+}
+
+.line-numbers {
+  background-color: #252526;
+  color: #858585;
+  text-align: right;
+  user-select: none;
+  min-width: 40px;
+  border-right: 1px solid #333;
+  flex-shrink: 0;
+  padding: 10px 0;
+  position: sticky;
+  left: 0;
+  z-index: 1;
+}
+
+.line-number {
+  padding: 0 8px;
+  font-family: 'Consolas', 'Monaco', monospace;
+  font-size: 13px;
+  line-height: 1.5;
+}
+
+.code-block {
+  flex: 1;
+  margin: 0;
+  padding: 10px 0 10px 12px;
+  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  background-color: #1e1e1e;
+  color: #d4d4d4;
+  white-space: pre;
+  min-width: 100%;
+}
+
+/* 自定义滚动条样式,应用于code-content */
+.code-content::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+.code-content::-webkit-scrollbar-track {
+  background: #1e1e1e;
+}
+
+.code-content::-webkit-scrollbar-thumb {
+  background: #555;
+  border-radius: 4px;
+}
+
+.code-content::-webkit-scrollbar-thumb:hover {
+  background: #777;
+}
+
+/* 不同的面板边框色 */
+.before-change {
+  border-top-color: #e6a23c;
+}
+
+.after-change {
+  border-top-color: #67c23a;
+}
+</style>

+ 750 - 0
src/views/Project/ConfigPatch.vue

@@ -0,0 +1,750 @@
+<template>
+  <div>
+    <el-button type="text" size="small" @click="goBack">
+      <i class="el-icon-back">返回上一页</i>
+    </el-button>
+    <div class="container">
+      <el-card class="card" shadow="never">
+        <div class="left-content">
+          <div class="action-buttons">
+            <el-button type="primary" @click="openConfigDialog">修改配置文件</el-button>
+          </div>
+          <div class="change-list-container">
+            <el-divider content-position="left">
+              更改历史
+            </el-divider>
+            <div v-if="changeList.length" class="change-list">
+              <div v-for="(item, index) in changeList" :key="index" class="change-item" @click="viewChangeDetail(item, index)">
+                <div class="change-content">
+                  {{ item.description }}
+                </div>
+                <div class="change-time">
+                  {{ item.time }}
+                </div>
+              </div>
+            </div>
+            <div v-else class="no-changes">
+              <el-empty description="当前暂无更改操作">
+                <template #description>
+                  <p v-if="loading">正在加载数据...</p>
+                  <p v-else>当前暂无更改操作</p>
+                </template>
+              </el-empty>
+            </div>
+          </div>
+        </div>
+      </el-card>
+
+      <el-divider direction="vertical" class="divider"></el-divider>
+
+      <el-card class="card" shadow="never">
+        <div class="right-content">
+          <div class="action-buttons">
+            <el-button type="primary" @click="analyzeProject">项目分析</el-button>
+            <el-button type="primary" @click="askQuestion">选中项目文件提问</el-button>
+          </div>
+          <div class="conversation-container">
+            <el-divider content-position="left">
+              对话历史
+            </el-divider>
+            <div v-if="conversations.length" class="conversation-list">
+              <div v-for="(item, index) in conversations"
+                :key="index"
+                class="conversation-item"
+                @click="viewConversationDetail(item, index)">
+                <div class="conversation-header">
+                  <span class="conversation-type">{{ item.type }}</span>
+                  <span class="conversation-time">{{ item.time }}</span>
+                </div>
+                <div class="conversation-content">
+                  {{ item.content }}
+                </div>
+              </div>
+            </div>
+            <div v-else class="no-conversations">
+              <el-empty description="暂无对话历史"></el-empty>
+            </div>
+          </div>
+        </div>
+      </el-card>
+    </div>
+
+    <!-- 配置文件修改对话框 -->
+    <el-dialog
+      title="修改配置文件"
+      v-model="dialogVisible"
+      width="50%">
+      <div class="dialog-content">
+        <el-form :model="configForm" label-width="120px" ref="configFormRef">
+          <el-form-item label="文件路径" prop="filePath" :rules="[{ required: true, message: '请输入文件路径', trigger: 'blur' }]">
+            <el-input v-model="configForm.filePath" placeholder="请输入文件路径"></el-input>
+          </el-form-item>
+          <el-form-item label="修改方式" prop="modifyMethod" :rules="[{ required: true, message: '请选择修改方式', trigger: 'change' }]">
+            <el-select v-model="configForm.modifyMethod" placeholder="请选择修改方式" style="width: 100%">
+              <el-option label="AST方式修改" value="ast"></el-option>
+              <el-option label="大模型(LLM)方式修改" value="llm"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="closeConfigDialog">取消</el-button>
+          <el-button type="primary" :loading="submitting" @click="saveConfig">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 项目分析对话框 -->
+    <el-dialog
+      title="项目分析"
+      v-model="analysisDialogVisible"
+      width="60%"
+      :fullscreen="showAnalysisResult">
+      <div class="dialog-content" v-if="!showAnalysisResult">
+        <el-form :model="analysisForm" label-width="120px" ref="analysisFormRef">
+          <el-form-item label="项目路径" prop="projectPath" :rules="[{ required: true, message: '请输入项目路径', trigger: 'blur' }]">
+            <el-input v-model="analysisForm.projectPath" placeholder="请输入项目路径,例如: E:/codes/MyProject"></el-input>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div v-else class="analysis-result">
+        <div v-if="analysisLoading" class="analysis-loading">
+          <el-spinner></el-spinner>
+          <p>正在分析项目,请稍候...</p>
+        </div>
+        <div v-else>
+          <div v-html="formattedAnalysisResult" class="markdown-content"></div>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="closeAnalysisDialog">{{ showAnalysisResult ? '关闭' : '取消' }}</el-button>
+          <el-button v-if="!showAnalysisResult" type="primary" :loading="analysisLoading" @click="submitProjectAnalysis">开始分析</el-button>
+          <el-button v-else type="primary" @click="saveAnalysis">保存分析结果</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 文件提问对话框 -->
+    <el-dialog
+      title="选中项目文件提问"
+      v-model="questionDialogVisible"
+      width="50%">
+      <div class="dialog-content">
+        <div class="question-form">
+          <el-form label-position="top">
+            <el-form-item label="选择文件路径">
+              <el-input
+                v-model="selectedFile"
+                placeholder="请输入或选择文件的完整路径,例如: E:/project/src/main.js"
+                clearable>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="提问内容">
+              <el-input
+                type="textarea"
+                v-model="questionContent"
+                rows="4"
+                placeholder="请输入您的问题,例如:这个文件的主要功能是什么?"
+                :maxlength="500"
+                show-word-limit>
+              </el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="questionDialogVisible = false">取消</el-button>
+          <el-button type="primary" :loading="submitting" @click="submitQuestion">提交问题</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { CHANGE_DETAIL, CONVERSATION_DETAIL } from '@/router/name';
+import axios from 'axios';
+import marked from 'marked';
+
+export default {
+  name: 'ConfigPatch.vue',
+  data() {
+    return {
+      changeList: [],
+      dialogVisible: false,
+      analysisDialogVisible: false,
+      questionDialogVisible: false,
+      selectedFile: '',
+      questionContent: '',
+      conversations: [],
+      conversationsLoading: false,
+      loading: false,
+      configForm: {
+        filePath: '',
+        modifyMethod: '',
+      },
+      submitting: false,
+      showAnalysisResult: false,
+      analysisLoading: false,
+      analysisForm: {
+        projectPath: '',
+      },
+      formattedAnalysisResult: '',
+    };
+  },
+  created() {
+    this.fetchChangeList();
+    this.fetchConversations();
+  },
+  methods: {
+    goBack() {
+      this.$router.go(-1);
+      this.loadCommitInfo();
+    },
+    fetchChangeList() {
+      this.loading = true;
+      axios.get('http://localhost:8080/config/changes')
+        .then((response) => {
+          if (response.data && response.data.length) {
+            // Transform the API response to match our UI format
+            this.changeList = response.data.map((item) => ({
+              id: item.id,
+              description: `修改配置文件操作ID: ${item.id}`,
+              time: this.formatApiDate(item.createdTime),
+              userId: item.userId,
+              oldFileId: item.oldFileId,
+              newFileId: item.newFileId,
+            }));
+          }
+        })
+        .catch((error) => {
+          console.error('获取更改列表失败:', error);
+          // Set some default data or show error message
+          this.$message.error('获取更改列表失败,请稍后重试');
+        })
+        .finally(() => {
+          this.loading = false;
+        });
+    },
+    formatApiDate(dateString) {
+      if (!dateString) return '';
+      const date = new Date(dateString);
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      const hours = String(date.getHours()).padStart(2, '0');
+      const minutes = String(date.getMinutes()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hours}:${minutes}`;
+    },
+    fetchConversations() {
+      this.conversationsLoading = true;
+      axios.get('http://localhost:8080/api/project/qa-records')
+        .then((response) => {
+          if (response.data && response.data.length) {
+            this.conversations = response.data.map((item) => ({
+              id: item.id,
+              type: item.category || '对话',
+              time: this.formatApiDate(item.createdTime),
+              content: `对话ID: ${item.conversationId}`,
+              conversationId: item.conversationId,
+              userId: item.userId,
+            }));
+          }
+        })
+        .catch((error) => {
+          console.error('获取对话历史失败:', error);
+          this.$message.error('获取对话历史失败,请稍后重试');
+        })
+        .finally(() => {
+          this.conversationsLoading = false;
+        });
+    },
+    openConfigDialog() {
+      // 重置表单
+      this.configForm = {
+        filePath: '',
+        modifyMethod: '',
+      };
+      // 打开对话框
+      this.dialogVisible = true;
+    },
+    saveConfig() {
+      this.$refs.configFormRef.validate(async (valid) => {
+        if (!valid) {
+          return;
+        }
+
+        this.submitting = true;
+        const { filePath, modifyMethod } = this.configForm;
+
+        // 根据选择的修改方式调用不同的接口
+        const url = modifyMethod === 'ast'
+          ? 'http://localhost:8080/config/modify-by-ast'
+          : 'http://localhost:8080/config/modify-by-llm';
+
+        try {
+          const response = await axios.post(url, filePath, {
+            headers: {
+              'Content-Type': 'application/json',
+            },
+          });
+
+          this.$message.success(response.data);
+          this.closeConfigDialog();
+          // 修改成功后重新获取更改列表
+          this.fetchChangeList();
+        } catch (error) {
+          console.error('修改配置文件失败:', error);
+          let errorMsg = '修改配置文件失败';
+          if (error.response && error.response.data) {
+            errorMsg = error.response.data;
+          }
+          this.$message.error(errorMsg);
+        } finally {
+          this.submitting = false;
+        }
+      });
+    },
+    loadCommitInfo() {
+      // 这里可以加载提交信息
+    },
+    analyzeProject() {
+      // 重置表单
+      this.analysisForm = {
+        projectPath: '',
+      };
+      this.showAnalysisResult = false;
+      this.formattedAnalysisResult = '';
+      this.analysisDialogVisible = true;
+    },
+    saveAnalysis() {
+      // 创建分析结果记录并保存
+      const analysisContent = this.analysisForm.projectPath
+        ? `项目【${this.analysisForm.projectPath}】分析结果`
+        : '项目分析结果';
+
+      const newAnalysis = {
+        type: '项目分析',
+        time: this.getCurrentTime(),
+        content: analysisContent,
+        // 如果需要,可以保存完整的分析结果
+        fullResult: this.formattedAnalysisResult,
+      };
+
+      // 将分析结果添加到对话历史
+      this.conversations.unshift(newAnalysis);
+      this.$message.success('分析结果已保存');
+      this.closeAnalysisDialog();
+
+      // 重新获取对话历史
+      this.fetchConversations();
+    },
+    askQuestion() {
+      console.log('文件提问');
+      // 重置表单数据
+      this.selectedFile = '';
+      this.questionContent = '';
+      // 文件提问逻辑
+      this.questionDialogVisible = true;
+    },
+    submitQuestion() {
+      if (!this.selectedFile || !this.questionContent) {
+        this.$message.warning('请选择文件并输入问题');
+        return;
+      }
+
+      // 显示加载状态
+      this.submitting = true;
+
+      // 准备请求数据
+      const requestData = {
+        filePath: this.selectedFile,
+        question: this.questionContent,
+      };
+
+      // 调用后端API
+      axios.post('http://localhost:8080/api/project/answer-code-question', requestData, {
+        headers: {
+          'Content-Type': 'application/json',
+        },
+      })
+        .then((response) => {
+          console.log('提交问题成功', response.data);
+
+          // 创建新的对话记录
+          const newQuestion = {
+            type: '文件提问',
+            time: this.getCurrentTime(),
+            content: `问:${this.questionContent}?答:${response.data.answer || '分析中...'}`,
+            conversationId: response.data.conversation_id,
+          };
+
+          // 添加到对话历史
+          this.conversations.unshift(newQuestion);
+          this.questionDialogVisible = false;
+          this.selectedFile = '';
+          this.questionContent = '';
+
+          // 重新获取对话历史
+          this.fetchConversations();
+
+          this.$message.success('问题提交成功');
+        })
+        .catch((error) => {
+          console.error('提交问题失败', error);
+          this.$message.error('问题提交失败,请稍后重试');
+        })
+        .finally(() => {
+          this.submitting = false;
+        });
+    },
+    getCurrentTime() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, '0');
+      const day = String(now.getDate()).padStart(2, '0');
+      const hours = String(now.getHours()).padStart(2, '0');
+      const minutes = String(now.getMinutes()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hours}:${minutes}`;
+    },
+    viewChangeDetail(item, index) {
+      // 使用路由导航到详情页面
+      this.$router.push({
+        name: CHANGE_DETAIL,
+        params: { id: item.id || index },
+        query: {
+          description: item.description,
+          time: item.time,
+          userId: item.userId,
+          oldFileId: item.oldFileId,
+          newFileId: item.newFileId,
+          fileChangeId: item.id,
+        },
+      });
+    },
+    viewConversationDetail(item, index) {
+      // 实现查看对话详情逻辑
+      console.log('查看对话详情', item, index);
+      this.$router.push({
+        name: CONVERSATION_DETAIL,
+        params: { id: item.id || index },
+        query: {
+          type: item.type,
+          time: item.time,
+          content: item.content,
+          conversationId: item.conversationId,
+        },
+      });
+    },
+    closeConfigDialog() {
+      this.dialogVisible = false;
+      if (this.$refs.configFormRef) {
+        this.$refs.configFormRef.resetFields();
+      }
+    },
+    closeAnalysisDialog() {
+      this.analysisDialogVisible = false;
+      this.showAnalysisResult = false;
+      this.analysisLoading = false;
+      this.formattedAnalysisResult = '';
+    },
+    async submitProjectAnalysis() {
+      this.$refs.analysisFormRef.validate(async (valid) => {
+        if (!valid) {
+          return;
+        }
+
+        try {
+          this.analysisLoading = true;
+          const { projectPath } = this.analysisForm;
+
+          const response = await axios.post('http://localhost:8080/api/project/analyze', projectPath, {
+            headers: {
+              'Content-Type': 'application/json',
+            },
+          });
+
+          if (response.data) {
+            // 使用marked库将Markdown格式转换为HTML
+            this.formattedAnalysisResult = marked.parse(response.data);
+            this.showAnalysisResult = true;
+          } else {
+            this.$message.warning('获取分析结果为空');
+          }
+        } catch (error) {
+          console.error('项目分析失败:', error);
+          let errorMessage = error.message || '未知错误';
+          if (error.response && error.response.data) {
+            errorMessage = error.response.data;
+          }
+          this.$message.error(`项目分析失败: ${errorMessage}`);
+        } finally {
+          this.analysisLoading = false;
+        }
+      });
+    },
+  },
+};
+</script>
+
+<style scoped>
+.container {
+  display: flex;
+  align-items: stretch; /* 改为stretch让子元素撑满高度 */
+  justify-content: center;
+  height: 500px; /* 给容器一个明确的高度 */
+  padding: 20px;
+  border: 1px solid #eee; /* 临时添加,用于调试容器边界 */
+  overflow: hidden; /* 防止容器溢出 */
+}
+
+.card {
+  flex: 1;
+  max-width: 50%;
+  border: 1px solid #ebeef5; /* 确保卡片有可见边框 */
+  overflow: hidden; /* 防止卡片内容溢出 */
+}
+
+.divider {
+  height: 100%;
+  margin: 0 20px;
+  background-color: #dcdfe6; /* 明确设置背景色 */
+}
+
+.left-content, .right-content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden; /* 防止内容溢出 */
+}
+
+.change-list-container, .conversation-container {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden; /* 确保容器本身不会溢出 */
+  padding: 0 5px; /* 给滚动条留出空间 */
+}
+
+.change-list, .conversation-list {
+  flex: 1;
+  overflow-y: scroll; /* 强制显示垂直滚动条 */
+  overflow-x: hidden; /* 防止水平滚动 */
+  padding-right: 5px; /* 给滚动条留点额外空间 */
+  margin-bottom: 10px; /* 底部留出一些空间 */
+  max-height: 350px; /* 设置最大高度确保滚动条出现 */
+}
+
+/* 自定义滚动条样式 */
+.change-list::-webkit-scrollbar,
+.conversation-list::-webkit-scrollbar {
+  width: 6px;
+}
+
+.change-list::-webkit-scrollbar-track,
+.conversation-list::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.change-list::-webkit-scrollbar-thumb,
+.conversation-list::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 3px;
+}
+
+.change-list::-webkit-scrollbar-thumb:hover,
+.conversation-list::-webkit-scrollbar-thumb:hover {
+  background: #909399;
+}
+
+.change-item, .conversation-item {
+  padding: 12px;
+  margin-bottom: 10px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.change-item:hover {
+  background-color: #e6f1fc;
+  transform: translateY(-2px);
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.change-content {
+  color: #606266;
+  margin-bottom: 8px;
+  line-height: 1.5;
+}
+
+.change-time {
+  color: #909399;
+  font-size: 12px;
+  align-self: flex-end;
+}
+
+.no-changes, .no-conversations {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+}
+
+/* 右侧内容样式 */
+.action-buttons {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 15px;
+}
+
+.conversation-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+}
+
+.conversation-type {
+  font-weight: bold;
+  color: #409EFF;
+}
+
+.conversation-time {
+  color: #909399;
+  font-size: 12px;
+}
+
+.conversation-content {
+  color: #606266;
+  line-height: 1.5;
+}
+
+/* 对话框样式 */
+.dialog-content {
+  padding: 10px;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.question-form {
+  padding: 10px;
+}
+
+.conversation-item:hover {
+  background-color: #e6f1fc;
+  transform: translateY(-2px);
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.analysis-result {
+  padding: 10px;
+}
+
+.analysis-loading {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+}
+
+.markdown-content {
+  margin-top: 10px;
+  line-height: 1.6;
+  font-size: 14px;
+  color: #333;
+  padding: 10px;
+  max-height: 70vh;
+  overflow: auto;
+}
+
+.markdown-content :deep(h1) {
+  font-size: 24px;
+  margin-top: 28px;
+  margin-bottom: 18px;
+  border-bottom: 1px solid #eaecef;
+  padding-bottom: 8px;
+}
+
+.markdown-content :deep(h2) {
+  font-size: 20px;
+  margin-top: 24px;
+  margin-bottom: 16px;
+  border-bottom: 1px solid #eaecef;
+  padding-bottom: 6px;
+}
+
+.markdown-content :deep(h3) {
+  font-size: 18px;
+  margin-top: 20px;
+  margin-bottom: 12px;
+}
+
+.markdown-content :deep(p) {
+  margin-bottom: 12px;
+}
+
+.markdown-content :deep(ul), .markdown-content :deep(ol) {
+  padding-left: 20px;
+  margin-bottom: 12px;
+}
+
+.markdown-content :deep(li) {
+  margin-bottom: 6px;
+}
+
+.markdown-content :deep(code) {
+  background-color: #f6f8fa;
+  padding: 2px 4px;
+  border-radius: 3px;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 12px;
+}
+
+.markdown-content :deep(pre) {
+  background-color: #f6f8fa;
+  padding: 12px;
+  border-radius: 4px;
+  overflow: auto;
+  margin-bottom: 16px;
+}
+
+.markdown-content :deep(pre code) {
+  background-color: transparent;
+  padding: 0;
+  font-size: 13px;
+  line-height: 1.5;
+  display: block;
+}
+
+.markdown-content :deep(blockquote) {
+  border-left: 4px solid #dfe2e5;
+  padding: 0 16px;
+  color: #6a737d;
+  margin: 0 0 16px;
+}
+
+.markdown-content :deep(table) {
+  border-collapse: collapse;
+  width: 100%;
+  margin-bottom: 16px;
+}
+
+.markdown-content :deep(th), .markdown-content :deep(td) {
+  border: 1px solid #dfe2e5;
+  padding: 8px 12px;
+  text-align: left;
+}
+
+.markdown-content :deep(th) {
+  background-color: #f6f8fa;
+  font-weight: 600;
+}
+</style>

+ 673 - 0
src/views/Project/ConversationDetail.vue

@@ -0,0 +1,673 @@
+<template>
+  <div class="page-container">
+    <div class="back-button">
+      <el-button type="text" size="small" @click="goBack">
+        <i class="el-icon-back">返回上一页</i>
+      </el-button>
+    </div>
+
+    <el-card shadow="never" class="chat-card">
+      <template #header>
+        <div class="chat-header">
+          <h2>{{ conversationType }} - {{ conversationTime }}</h2>
+          <el-button type="primary" size="small" @click="exportConversation">导出对话</el-button>
+        </div>
+      </template>
+
+      <div v-if="loading" class="loading-container">
+        <el-spinner></el-spinner>
+        <p>正在加载对话历史...</p>
+      </div>
+
+      <div v-else class="chat-messages" ref="chatContent">
+        <div class="message-container" v-for="(message, index) in messages" :key="index"
+            :class="['message-container', message.isUser ? 'user-message' : 'ai-message',
+                     message.content.length > 500 ? 'large-content' : '',
+                     message.isLoading ? 'loading-message' : '',
+                     message.isError ? 'error-message' : '']">
+          <div class="avatar">
+            <el-avatar :size="40" :src="message.isUser ? userAvatar : aiAvatar"></el-avatar>
+          </div>
+          <div class="message-content">
+            <div class="message-bubble" v-html="formatMessage(message.content)"></div>
+            <div class="message-time">{{ message.time }}</div>
+          </div>
+        </div>
+      </div>
+
+      <div class="chat-input-container">
+        <el-input
+          v-model="inputMessage"
+          type="textarea"
+          :rows="3"
+          placeholder="输入新的问题..."
+          @keyup.enter.exact="sendMessage"
+          @keyup.ctrl.enter="addNewLine"
+        ></el-input>
+        <div class="input-actions">
+          <el-button type="primary" @click="sendMessage" :disabled="!inputMessage.trim()">发送</el-button>
+        </div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import marked from 'marked';
+import axios from 'axios';
+
+export default {
+  name: 'ConversationDetail',
+  data() {
+    return {
+      conversationType: '',
+      conversationTime: '',
+      initialContent: '',
+      inputMessage: '',
+      userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
+      aiAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
+      messages: [],
+      loading: false,
+      conversationId: '',
+    };
+  },
+  created() {
+    // 从路由获取参数
+    const {
+      type, time, content, conversationId,
+    } = this.$route.query;
+    this.conversationType = type || '未知类型';
+    this.conversationTime = time || '未知时间';
+    this.initialContent = content || '';
+    this.conversationId = conversationId;
+
+    // 如果有会话ID,则从API获取历史对话
+    if (this.conversationId) {
+      this.fetchConversationHistory();
+    } else {
+      // 没有会话ID时使用初始化方法
+      this.initializeChat();
+    }
+  },
+  mounted() {
+    this.scrollToBottom();
+  },
+  updated() {
+    this.scrollToBottom();
+  },
+  methods: {
+    goBack() {
+      this.$router.go(-1);
+    },
+    fetchConversationHistory() {
+      this.loading = true;
+      axios.get(`http://localhost:8080/api/project/conversation-history?conversationId=${this.conversationId}`)
+        .then((response) => {
+          if (response.data && response.data.length) {
+            // 转换API响应为消息格式
+            this.messages = response.data.map((item) => ({
+              isUser: item.role === 'user',
+              content: item.content,
+              time: this.formatApiDate(item.createdTime),
+              id: item.id,
+            }));
+          } else {
+            // 如果没有数据,显示提示
+            this.messages = [{
+              isUser: false,
+              content: '没有找到相关对话历史',
+              time: this.getCurrentTime(),
+            }];
+          }
+        })
+        .catch((error) => {
+          console.error('获取对话历史失败:', error);
+          // 显示错误消息
+          this.messages = [{
+            isUser: false,
+            content: '获取对话历史失败,请稍后重试',
+            time: this.getCurrentTime(),
+          }];
+          this.$message.error('获取对话历史失败,请稍后重试');
+        })
+        .finally(() => {
+          this.loading = false;
+        });
+    },
+    formatApiDate(dateString) {
+      if (!dateString) return this.getCurrentTime();
+
+      try {
+        const date = new Date(dateString);
+        const year = date.getFullYear();
+        const month = String(date.getMonth() + 1).padStart(2, '0');
+        const day = String(date.getDate()).padStart(2, '0');
+        const hours = String(date.getHours()).padStart(2, '0');
+        const minutes = String(date.getMinutes()).padStart(2, '0');
+        return `${year}-${month}-${day} ${hours}:${minutes}`;
+      } catch (e) {
+        console.error('日期解析错误:', e);
+        return this.getCurrentTime();
+      }
+    },
+    initializeChat() {
+      // 根据初始内容生成对话历史
+      if (this.conversationType === '项目分析') {
+        // 项目分析类型的对话
+        this.messages = [
+          {
+            isUser: false,
+            content: '我已完成对项目的分析,以下是分析结果:',
+            time: this.getRelativeTime(this.conversationTime, -2),
+          },
+          {
+            isUser: false,
+            content: this.initialContent,
+            time: this.conversationTime,
+          },
+        ];
+      } else if (this.conversationType === '文件提问') {
+        // 文件提问类型的对话
+        const parts = this.initialContent.split('问:');
+        if (parts.length > 1) {
+          const questionParts = parts[1].split('答:');
+          if (questionParts.length > 1) {
+            const question = questionParts[0];
+            const answer = questionParts[1];
+
+            this.messages = [
+              {
+                isUser: true,
+                content: question,
+                time: this.getRelativeTime(this.conversationTime, -2),
+              },
+              {
+                isUser: false,
+                content: answer,
+                time: this.conversationTime,
+              },
+            ];
+          }
+        }
+      }
+
+      // 如果没有成功解析,就直接添加一条消息
+      if (this.messages.length === 0) {
+        this.messages = [
+          {
+            isUser: false,
+            content: this.initialContent,
+            time: this.conversationTime,
+          },
+        ];
+      }
+    },
+    sendMessage() {
+      if (!this.inputMessage.trim()) return;
+
+      // 添加用户消息
+      this.messages.push({
+        isUser: true,
+        content: this.inputMessage,
+        time: this.getCurrentTime(),
+      });
+
+      const userQuestion = this.inputMessage;
+      this.inputMessage = '';
+
+      // 如果有会话ID,则调用API继续对话
+      if (this.conversationId) {
+        this.sendMessageToAPI(userQuestion);
+      } else {
+        // 模拟AI响应(仅用于无会话ID的场景)
+        setTimeout(() => {
+          this.messages.push({
+            isUser: false,
+            content: this.generateResponse(userQuestion),
+            time: this.getCurrentTime(),
+          });
+        }, 1000);
+      }
+    },
+    sendMessageToAPI(question) {
+      // 显示加载状态或者临时消息
+      const loadingMsgIndex = this.messages.length;
+      this.messages.push({
+        isUser: false,
+        content: '正在思考中...',
+        time: this.getCurrentTime(),
+        isLoading: true,
+      });
+
+      // 准备请求数据
+      const requestData = {
+        conversationId: this.conversationId,
+        question,
+      };
+
+      // 调用后端API
+      axios.post('http://localhost:8080/api/project/answer-code', requestData, {
+        headers: {
+          'Content-Type': 'application/json',
+        },
+      })
+        .then((response) => {
+          console.log('收到AI回复', response.data);
+
+          // 更新之前的加载消息
+          if (loadingMsgIndex < this.messages.length) {
+          // 创建新的消息对象替换加载消息
+            const newMessage = {
+              isUser: false,
+              content: response.data.answer || '无法获取回复',
+              time: this.getCurrentTime(),
+              id: response.data.message_id,
+            };
+
+            // 使用Vue的splice方法更新数组,确保视图更新
+            this.messages.splice(loadingMsgIndex, 1, newMessage);
+          }
+
+          // 强制滚动到底部
+          this.scrollToBottom();
+        })
+        .catch((error) => {
+          console.error('获取AI回复失败', error);
+
+          // 更新错误消息
+          if (loadingMsgIndex < this.messages.length) {
+          // 创建新的错误消息对象
+            const errorMessage = {
+              isUser: false,
+              content: '获取回复失败,请稍后重试',
+              time: this.getCurrentTime(),
+              isError: true,
+            };
+
+            // 使用Vue的splice方法更新数组,确保视图更新
+            this.messages.splice(loadingMsgIndex, 1, errorMessage);
+          }
+
+          this.$message.error('获取回复失败,请稍后重试');
+        });
+    },
+    generateResponse(question) {
+      // 简单的响应生成逻辑
+      const responses = [
+        `根据项目分析,${question.replace(/\?|?/g, '')}的情况是代码库中有相关实现,但可能需要进一步优化。`,
+        `经过检查,关于"${question.replace(/\?|?/g, '')}"的问题,建议查看项目文档或相关源码。`,
+        `您提到的"${question.replace(/\?|?/g, '')}"问题涉及到项目架构的多个部分,需要综合考虑。`,
+        `对于"${question.replace(/\?|?/g, '')}",推荐参考相似项目的最佳实践或设计模式。`,
+      ];
+
+      return responses[Math.floor(Math.random() * responses.length)];
+    },
+    formatMessage(text) {
+      // 使用 marked 库将 Markdown 转换为 HTML
+      try {
+        return marked.parse(text);
+      } catch (error) {
+        console.error('Markdown 解析错误:', error);
+        // 如果解析失败,就使用基本的处理
+        return text
+          .replace(/\n/g, '<br>')
+          .replace(/`([^`]+)`/g, '<code>$1</code>');
+      }
+    },
+    getCurrentTime() {
+      const now = new Date();
+      const hours = String(now.getHours()).padStart(2, '0');
+      const minutes = String(now.getMinutes()).padStart(2, '0');
+      return `${hours}:${minutes}`;
+    },
+    getRelativeTime(timeString, minuteOffset) {
+      // 从时间字符串计算相对时间
+      const [datePart, timePart] = timeString.split(' ');
+      const [hours, minutes] = timePart.split(':').map(Number);
+
+      const date = new Date();
+      date.setHours(hours);
+      date.setMinutes(minutes + minuteOffset);
+
+      return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
+    },
+    scrollToBottom() {
+      // 滚动到最新消息
+      this.$nextTick(() => {
+        if (this.$refs.chatContent) {
+          this.$refs.chatContent.scrollTop = this.$refs.chatContent.scrollHeight;
+        }
+      });
+    },
+    exportConversation() {
+      // 导出对话内容
+      const content = this.messages.map((msg) => `${msg.isUser ? '用户' : 'AI'} (${msg.time}):\n${msg.content}`).join('\n\n');
+
+      // 创建下载链接
+      const element = document.createElement('a');
+      const file = new Blob([content], { type: 'text/plain' });
+      element.href = URL.createObjectURL(file);
+      element.download = `对话记录_${this.conversationType}_${new Date().toISOString().split('T')[0]}.txt`;
+      document.body.appendChild(element);
+      element.click();
+      document.body.removeChild(element);
+    },
+    addNewLine() {
+      // 添加新行
+      this.inputMessage += '\n';
+    },
+  },
+};
+</script>
+
+<style scoped>
+.page-container {
+  height: calc(100vh - 40px);
+  padding: 20px;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  max-height: 700px;
+  margin: 0 auto;
+}
+
+.back-button {
+  margin-bottom: 10px;
+}
+
+.chat-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.chat-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 200px;
+}
+
+.chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 15px;
+  background-color: #f9f9f9;
+  border-radius: 4px;
+  margin-bottom: 15px;
+  max-height: 300px;
+}
+
+.chat-input-container {
+  padding: 15px;
+  background-color: #fff;
+  border-top: 1px solid #ebeef5;
+}
+
+/* 自定义滚动条样式 */
+.chat-messages::-webkit-scrollbar {
+  width: 8px;
+}
+
+.chat-messages::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 4px;
+}
+
+.chat-messages::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 4px;
+}
+
+.chat-messages::-webkit-scrollbar-thumb:hover {
+  background: #909399;
+}
+
+.message-container {
+  display: flex;
+  margin-bottom: 20px;
+  max-width: 80%;
+}
+
+.large-content {
+  max-width: 95%;
+}
+
+.user-message {
+  flex-direction: row-reverse;
+  margin-left: auto;
+}
+
+.ai-message {
+  margin-right: auto;
+}
+
+.large-content.ai-message {
+  width: 95%;
+  max-width: 95%;
+}
+
+.loading-message .message-bubble {
+  position: relative;
+  overflow: hidden;
+}
+
+.loading-message .message-bubble:after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  height: 2px;
+  width: 50%;
+  background: linear-gradient(to right, transparent, #409EFF, transparent);
+  animation: loading 1.5s infinite;
+}
+
+.error-message .message-bubble {
+  background-color: #fef0f0 !important;
+  border-color: #fde2e2 !important;
+  color: #f56c6c !important;
+}
+
+@keyframes loading {
+  0% {
+    left: -100%;
+  }
+  100% {
+    left: 100%;
+  }
+}
+
+.avatar {
+  margin: 0 10px;
+  flex-shrink: 0;
+}
+
+.message-content {
+  display: flex;
+  flex-direction: column;
+}
+
+.message-bubble {
+  padding: 10px 15px;
+  border-radius: 8px;
+  word-break: break-word;
+  overflow-x: auto;
+}
+
+.message-bubble p {
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+
+.user-message .message-bubble {
+  background-color: #e6f1fc;
+  color: #303133;
+}
+
+.ai-message .message-bubble {
+  background-color: #fff;
+  border: 1px solid #ebeef5;
+  color: #303133;
+}
+
+.message-time {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+  text-align: right;
+}
+
+.user-message .message-time {
+  text-align: right;
+}
+
+.ai-message .message-time {
+  text-align: left;
+}
+
+.input-actions {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 10px;
+}
+
+/* 代码样式 */
+code {
+  background-color: #f0f0f0;
+  padding: 2px 4px;
+  border-radius: 3px;
+  font-family: 'Courier New', monospace;
+}
+
+/* Markdown 样式 */
+.message-bubble :deep(h1),
+.message-bubble :deep(h2),
+.message-bubble :deep(h3),
+.message-bubble :deep(h4),
+.message-bubble :deep(h5),
+.message-bubble :deep(h6) {
+  margin-top: 10px;
+  margin-bottom: 6px;
+  font-weight: 600;
+  line-height: 1.25;
+}
+
+.message-bubble :deep(h1) {
+  font-size: 1.5em;
+  border-bottom: 1px solid #eaecef;
+  padding-bottom: 0.3em;
+}
+
+.message-bubble :deep(h2) {
+  font-size: 1.3em;
+  border-bottom: 1px solid #eaecef;
+  padding-bottom: 0.3em;
+}
+
+.message-bubble :deep(h3) {
+  font-size: 1.2em;
+}
+
+.message-bubble :deep(h4) {
+  font-size: 1.1em;
+}
+
+.message-bubble :deep(p) {
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+
+.message-bubble :deep(ul),
+.message-bubble :deep(ol) {
+  padding-left: 2em;
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+
+.message-bubble :deep(li) {
+  margin-bottom: 4px;
+}
+
+.message-bubble :deep(pre) {
+  background-color: #f6f8fa;
+  border-radius: 3px;
+  padding: 12px;
+  overflow: auto;
+  margin-bottom: 10px;
+}
+
+.message-bubble :deep(code) {
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 85%;
+  background-color: rgba(27, 31, 35, 0.05);
+  padding: 0.2em 0.4em;
+  border-radius: 3px;
+  white-space: pre-wrap;
+}
+
+.message-bubble :deep(pre code) {
+  padding: 0;
+  background-color: transparent;
+  white-space: pre;
+  display: block;
+  overflow-x: auto;
+}
+
+.message-bubble :deep(blockquote) {
+  margin: 0;
+  padding: 0 1em;
+  color: #6a737d;
+  border-left: 0.25em solid #dfe2e5;
+}
+
+.message-bubble :deep(table) {
+  border-collapse: collapse;
+  width: 100%;
+  margin-bottom: 10px;
+  overflow: auto;
+}
+
+.message-bubble :deep(table th),
+.message-bubble :deep(table td) {
+  padding: 6px 13px;
+  border: 1px solid #dfe2e5;
+}
+
+.message-bubble :deep(table tr) {
+  background-color: #fff;
+  border-top: 1px solid #c6cbd1;
+}
+
+.message-bubble :deep(table tr:nth-child(2n)) {
+  background-color: #f6f8fa;
+}
+
+.message-bubble :deep(img) {
+  max-width: 100%;
+  box-sizing: content-box;
+}
+
+/* AI 消息和用户消息的气泡样式调整 */
+.ai-message .message-bubble {
+  background-color: #fff;
+  border: 1px solid #ebeef5;
+  color: #303133;
+  max-width: 100%;
+  padding: 12px 16px;
+}
+
+.user-message .message-bubble {
+  background-color: #e6f1fc;
+  color: #303133;
+  max-width: 100%;
+  padding: 12px 16px;
+}
+</style>