From e4269277e493dad1c410ad40f8fd4e2c934a9b75 Mon Sep 17 00:00:00 2001
From: 小小儁爺 <1694218219@qq.com>
Date: 星期二, 24 三月 2026 13:36:02 +0800
Subject: [PATCH] 1.新增ai测试页

---
 src/permission.js              |    2 
 package.json                   |    1 
 src/router/index.js            |    4 
 src/views/systemSetting/ai.vue |  796 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 802 insertions(+), 1 deletions(-)

diff --git a/package.json b/package.json
index cc3953f..e024ab2 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
     "jsonwebtoken": "^9.0.1",
     "lib-flexible": "^0.3.2",
     "lib-flexible-computer": "^1.0.2",
+    "marked": "^4.3.0",
     "moment": "^2.30.1",
     "nanoid": "^4.0.2",
     "normalize.css": "7.0.0",
diff --git a/src/permission.js b/src/permission.js
index 7fa5299..43b5d32 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -9,7 +9,7 @@
 
 NProgress.configure({ showSpinner: false }) // NProgress Configuration
 
-const whiteList = ['/login', '/zhkb', '/cj', '/ckgl', '/sop', '/gantt'] // no redirect whitelist
+const whiteList = ['/login', '/zhkb', '/cj', '/ckgl', '/sop', '/gantt', '/ai'] // no redirect whitelist
 
 router.beforeEach(async(to, from, next) => {
   // start progress bar
diff --git a/src/router/index.js b/src/router/index.js
index 04e38c6..3bff355 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -56,6 +56,10 @@
     path: '/sop',
     component: () => import('@/views/sopManager/sop'),
     hidden: true
+  }, {
+    path: '/ai',
+    component: () => import('@/views/systemSetting/ai'),
+    hidden: true
   },
 
   {
diff --git a/src/views/systemSetting/ai.vue b/src/views/systemSetting/ai.vue
new file mode 100644
index 0000000..cc8a0a8
--- /dev/null
+++ b/src/views/systemSetting/ai.vue
@@ -0,0 +1,796 @@
+<template>
+  <div class="chat-container">
+    <!-- 宸︿晶鍘嗗彶瀵硅瘽鍒楄〃 -->
+    <div class="sidebar">
+      <div class="sidebar-header">
+        <el-button type="primary" icon="el-icon-plus" @click="createNewChat">
+          鏂板缓瀵硅瘽
+        </el-button>
+      </div>
+
+      <div class="chat-list">
+        <div
+          v-for="(chat, index) in chatList"
+          :key="index"
+          :class="['chat-item', currentChatIndex === index ? 'active' : '']"
+          @click="switchChat(index)"
+        >
+          <div class="chat-item-icon">
+            <i class="el-icon-chat-dot-round" />
+          </div>
+          <div class="chat-item-content">
+            <div class="chat-item-title">{{ chat.title }}</div>
+            <div class="chat-item-time">{{ chat.time }}</div>
+          </div>
+          <div class="chat-item-delete" @click.stop="deleteChat(index)">
+            <i class="el-icon-close" />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 鑱婂ぉ涓诲尯鍩� -->
+    <div class="chat-main">
+      <!-- 娑堟伅鍒楄〃 -->
+      <div ref="messageList" class="message-list">
+        <div
+          v-for="(message, index) in messages"
+          :key="index"
+          :class="['message-item', message.role === 'user' ? 'message-user' : 'message-ai']"
+        >
+          <!-- AI 澶村儚 -->
+          <div v-if="message.role === 'assistant'" class="avatar avatar-ai">
+            <span>AI</span>
+          </div>
+
+          <!-- 娑堟伅鍐呭 -->
+          <div class="message-content">
+            <div class="message-info">
+              <span class="sender-name">{{ message.role === 'user' ? '鎴�' : 'AI 鍔╂墜' }}</span>
+              <span class="message-time">{{ message.time }}</span>
+            </div>
+            <div class="message-text" v-html="formattedContent(message.content)" />
+
+            <!-- 娴佸紡鍔犺浇涓殑鍏夋爣 -->
+            <span v-if="message.streaming" class="cursor-blink">|</span>
+          </div>
+
+          <!-- 鐢ㄦ埛澶村儚 -->
+          <div v-if="message.role === 'user'" class="avatar avatar-user">
+            <span>鎴�</span>
+          </div>
+        </div>
+
+        <!-- 鍔犺浇涓彁绀� -->
+        <div v-if="isLoading" class="loading-indicator">
+          <span class="loading-dot" />
+          <span class="loading-dot" />
+          <span class="loading-dot" />
+        </div>
+      </div>
+
+      <!-- 杈撳叆鍖哄煙 -->
+      <div class="input-area">
+        <div class="input-wrapper">
+          <el-input
+            v-model="userInput"
+            type="textarea"
+            :rows="3"
+            autofocus
+            placeholder="璇疯緭鍏ユ偍鐨勯棶棰�... (鍥炶溅鍙戦��)"
+            resize="none"
+            class="chat-input"
+            @keyup.enter.native="sendMessage"
+            @keydown.enter.native="e => e.preventDefault()"
+          />
+          <!--        @keydown.enter.exact.prevent="sendMessage"-->
+          <el-button
+            type="primary"
+            :disabled="!userInput.trim() || isLoading"
+            class="send-button"
+            @click="sendMessage"
+          >
+            {{ isLoading ? '鎬濊�冧腑...' : '鍙戦��' }}
+          </el-button>
+        </div>
+        <div class="input-tips">
+          <span>AI 鐢熸垚鍐呭浠呬緵鍙傝�冿紝璇疯皑鎱庣攧鍒�</span>
+        </div>
+      </div>
+    </div>
+
+  </div>
+</template>
+
+<script>
+import { marked } from 'marked'
+
+// 閰嶇疆 marked 閫夐」
+marked.setOptions({
+  breaks: true, // 鏀寔 GFM 鎹㈣
+  gfm: true, // GitHub Flavored Markdown
+  sanitize: false // 涓嶈繃婊� HTML锛堝鏋滈渶瑕� XSS 淇濇姢锛屽彲浠ヤ娇鐢� DOMPurify锛�
+})
+
+export default {
+  name: 'ProcessSetting',
+  data() {
+    return {
+      userInput: '',
+      messages: [],
+      isLoading: false,
+      currentStreamingIndex: null,
+      // 闃块噷浜戝崈闂厤缃�
+      API_KEY: 'sk-825b87021a0a4dfb9d3fdf20acb4fcc2', // 璇锋浛鎹负浣犵殑 API Key
+      API_URL: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
+      // 鍘嗗彶瀵硅瘽鐩稿叧
+      chatList: [],
+      currentChatIndex: 0
+    }
+  },
+  watch: {
+    // 鐩戝惉娑堟伅鍙樺寲锛岃嚜鍔ㄦ洿鏂版爣棰�
+    messages: {
+      handler() {
+        this.updateChatTitle()
+        this.saveChatHistory()
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    // 鍒濆鍖栨杩庢秷鎭�
+    this.createNewChat()
+    // 浠庢湰鍦板瓨鍌ㄥ姞杞藉巻鍙插璇�
+    this.loadChatHistory()
+  },
+  methods: {
+    // 浣跨敤 marked 搴撴覆鏌� Markdown
+    formattedContent(content) {
+      if (!content) return ''
+
+      try {
+        return marked.parse(content)
+      } catch (e) {
+        console.error('Markdown 瑙f瀽澶辫触:', e)
+        return content.replace(/\n/g, '<br>')
+      }
+    },
+
+    // 澶勭悊 Enter 閿� - 鐩存帴鍙戦��
+    handleEnterKey(event) {
+      event.preventDefault()
+      this.sendMessage()
+    },
+
+    // 鍙戦�佹秷鎭�
+    async sendMessage() {
+      const text = this.userInput.trim()
+      if (!text || this.isLoading) return
+
+      // 娣诲姞鐢ㄦ埛娑堟伅
+      this.addMessage('user', text)
+      this.userInput = ''
+
+      // 鍒涘缓 AI 鍥炲娑堟伅锛堢┖鍐呭锛�
+      const aiMessageIndex = this.addMessage('assistant', '', true)
+
+      // 璋冪敤闃块噷浜戝崈闂祦寮忔帴鍙�
+      await this.fetchQwenStreamingResponse(aiMessageIndex)
+    },
+
+    // 娣诲姞娑堟伅鍒板垪琛�
+    addMessage(role, content, streaming = false) {
+      const now = new Date()
+      const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
+
+      const message = {
+        role,
+        content,
+        time,
+        streaming
+      }
+
+      this.messages.push(message)
+      this.$nextTick(() => {
+        this.scrollToBottom()
+      })
+
+      return this.messages.length - 1
+    },
+
+    // 闃块噷浜戝崈闂祦寮忓搷搴�
+    async fetchQwenStreamingResponse(messageIndex) {
+      this.isLoading = true
+      this.currentStreamingIndex = messageIndex
+
+      try {
+        // 鏋勫缓鍘嗗彶瀵硅瘽锛堢敤浜庝笂涓嬫枃锛�
+        const conversationHistory = this.messages
+          .filter((msg, idx) => idx < this.messages.length - 1 || msg.role === 'user')
+          .map(msg => ({
+            role: msg.role === 'assistant' ? 'assistant' : 'user',
+            content: msg.content
+          }))
+
+        // 鑾峰彇鏈�鍚庝竴鏉$敤鎴锋秷鎭綔涓哄綋鍓嶉棶棰�
+        const lastUserMessage = this.messages[this.messages.length - 2]
+        if (lastUserMessage) {
+          conversationHistory.push({
+            role: 'user',
+            content: lastUserMessage.content
+          })
+        }
+
+        const response = await fetch(this.API_URL, {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json',
+            'Authorization': `Bearer ${this.API_KEY}`
+          },
+          body: JSON.stringify({
+            model: 'qwen-plus', // 鎴� qwen-turbo, qwen-max 绛�
+            messages: conversationHistory,
+            stream: true, // 鍚敤娴佸紡杈撳嚭
+            temperature: 0.7
+          })
+        })
+
+        if (!response.ok) {
+          throw new Error(`HTTP error! status: ${response.status}`)
+        }
+
+        // 澶勭悊娴佸紡鍝嶅簲
+        const reader = response.body.getReader()
+        const decoder = new TextDecoder('utf-8')
+        let accumulatedContent = ''
+
+        // eslint-disable-next-line no-constant-condition
+        while (true) {
+          const { done, value } = await reader.read()
+
+          if (done) {
+            break
+          }
+
+          // 瑙g爜鏁版嵁
+          const chunk = decoder.decode(value, { stream: true })
+          const lines = chunk.split('\n')
+
+          for (const line of lines) {
+            if (line.startsWith('data: ')) {
+              const data = line.slice(6)
+
+              // [DONE] 鏍囪琛ㄧず娴佸紡缁撴潫
+              if (data.trim() === '[DONE]') {
+                continue
+              }
+
+              try {
+                const parsed = JSON.parse(data)
+                const delta = parsed.choices?.[0]?.delta?.content
+
+                if (delta) {
+                  accumulatedContent += delta
+
+                  // 浣跨敤 $set 纭繚 Vue2 鐨勫搷搴斿紡鏇存柊
+                  this.$set(this.messages[messageIndex], 'content', accumulatedContent)
+
+                  // 婊氬姩鍒板簳閮�
+                  this.$nextTick(() => {
+                    this.scrollToBottom()
+                  })
+                }
+              } catch (e) {
+                console.error('瑙f瀽 SSE 鏁版嵁澶辫触:', e)
+              }
+            }
+          }
+        }
+
+        // 缁撴潫娴佸紡鐘舵��
+        this.$set(this.messages[messageIndex], 'streaming', false)
+      } catch (error) {
+        console.error('璋冪敤鍗冮棶鎺ュ彛澶辫触:', error)
+        this.$message.error('AI 鍝嶅簲澶辫触锛�' + error.message)
+
+        // 璁剧疆閿欒淇℃伅鍒版秷鎭腑
+        this.$set(this.messages[messageIndex], 'content', '鎶辨瓑锛屽搷搴斿け璐ワ細' + error.message)
+        this.$set(this.messages[messageIndex], 'streaming', false)
+      } finally {
+        this.isLoading = false
+        this.currentStreamingIndex = null
+      }
+    },
+
+    // 婊氬姩鍒板簳閮�
+    scrollToBottom() {
+      const container = this.$refs.messageList
+      if (container) {
+        this.$nextTick(() => {
+          container.scrollTop = container.scrollHeight
+        })
+      }
+    },
+
+    // 鍒涘缓鏂板璇�
+    createNewChat() {
+      const now = new Date()
+      const time = `${now.getMonth() + 1}/${now.getDate()} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
+
+      const newChat = {
+        title: '鏂板璇�',
+        time: time,
+        messages: [{
+          role: 'assistant',
+          content: '鎮ㄥソ锛佹垜鏄� AI 鍔╂墜锛岃闂湁浠�涔堝彲浠ュ府鎮紵',
+          time: time,
+          streaming: false
+        }]
+      }
+
+      this.chatList.unshift(newChat)
+      this.currentChatIndex = 0
+      this.messages = newChat.messages
+      this.saveChatHistory()
+    },
+
+    // 鍒囨崲瀵硅瘽
+    switchChat(index) {
+      this.currentChatIndex = index
+      this.messages = this.chatList[index].messages
+      this.$nextTick(() => {
+        this.scrollToBottom()
+      })
+    },
+
+    // 鍒犻櫎瀵硅瘽
+    deleteChat(index) {
+      this.$confirm('纭畾瑕佸垹闄よ繖涓璇濆悧锛�', '鎻愮ず', {
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        type: 'warning'
+      }).then(() => {
+        this.chatList.splice(index, 1)
+
+        if (this.chatList.length === 0) {
+          this.createNewChat()
+        } else if (index === this.currentChatIndex) {
+          this.currentChatIndex = Math.max(0, index - 1)
+          this.switchChat(this.currentChatIndex)
+        } else if (index < this.currentChatIndex) {
+          this.currentChatIndex--
+        }
+
+        this.saveChatHistory()
+        this.$message.success('鍒犻櫎鎴愬姛')
+      }).catch(() => {})
+    },
+
+    // 鏇存柊褰撳墠瀵硅瘽鏍囬
+    updateChatTitle() {
+      if (this.messages.length > 0 && this.chatList[this.currentChatIndex]) {
+        const firstUserMessage = this.messages.find(msg => msg.role === 'user')
+        if (firstUserMessage) {
+          const title = firstUserMessage.content.substring(0, 20) + (firstUserMessage.content.length > 20 ? '...' : '')
+          this.$set(this.chatList[this.currentChatIndex], 'title', title)
+          this.saveChatHistory()
+        }
+      }
+    },
+
+    // 淇濆瓨瀵硅瘽鍘嗗彶鍒版湰鍦板瓨鍌�
+    saveChatHistory() {
+      try {
+        localStorage.setItem('chatHistory', JSON.stringify({
+          chatList: this.chatList,
+          currentChatIndex: this.currentChatIndex
+        }))
+      } catch (e) {
+        console.error('淇濆瓨鑱婂ぉ璁板綍澶辫触:', e)
+      }
+    },
+
+    // 浠庢湰鍦板瓨鍌ㄥ姞杞藉璇濆巻鍙�
+    loadChatHistory() {
+      try {
+        const saved = localStorage.getItem('chatHistory')
+        if (saved) {
+          const data = JSON.parse(saved)
+          this.chatList = data.chatList || []
+          this.currentChatIndex = data.currentChatIndex || 0
+
+          if (this.chatList.length > 0) {
+            this.messages = this.chatList[this.currentChatIndex].messages
+          } else {
+            this.createNewChat()
+          }
+        }
+      } catch (e) {
+        console.error('鍔犺浇鑱婂ぉ璁板綍澶辫触:', e)
+        this.createNewChat()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.chat-container {
+  display: flex;
+  height: 100vh;
+  background: #f5f7fa;
+}
+
+.sidebar {
+  width: 260px;
+  background: #1a1a2e;
+  display: flex;
+  flex-direction: column;
+  //border-radius: 8px 0 0 8px;
+  overflow: hidden;
+
+  .sidebar-header {
+    padding: 16px;
+    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+
+    button {
+      width: 100%;
+      background: rgba(255, 255, 255, 0.1);
+      border-color: transparent;
+      color: white;
+
+      &:hover {
+        background: rgba(255, 255, 255, 0.2);
+      }
+    }
+  }
+
+  .chat-list {
+    flex: 1;
+    overflow-y: auto;
+    padding: 8px;
+
+    &::-webkit-scrollbar {
+      width: 4px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: rgba(255, 255, 255, 0.2);
+      border-radius: 2px;
+    }
+  }
+
+  .chat-item {
+    display: flex;
+    align-items: center;
+    padding: 12px;
+    margin-bottom: 4px;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.3s;
+    position: relative;
+
+    &:hover {
+      background: rgba(255, 255, 255, 0.1);
+
+      .chat-item-delete {
+        opacity: 1;
+      }
+    }
+
+    &.active {
+      background: rgba(64, 158, 255, 0.2);
+      border-left: 3px solid #409eff;
+    }
+
+    .chat-item-icon {
+      width: 32px;
+      height: 32px;
+      border-radius: 8px;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: white;
+      font-size: 16px;
+      margin-right: 12px;
+      flex-shrink: 0;
+    }
+
+    .chat-item-content {
+      flex: 1;
+      overflow: hidden;
+
+      .chat-item-title {
+        color: white;
+        font-size: 14px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        margin-bottom: 4px;
+      }
+
+      .chat-item-time {
+        color: rgba(255, 255, 255, 0.5);
+        font-size: 12px;
+      }
+    }
+
+    .chat-item-delete {
+      opacity: 0;
+      color: rgba(255, 255, 255, 0.6);
+      font-size: 14px;
+      padding: 4px;
+      transition: opacity 0.3s;
+
+      &:hover {
+        color: #ff4d4f;
+      }
+    }
+  }
+}
+
+.chat-main {
+  flex: 1;
+  overflow: hidden;
+  padding: 20px;
+  background: white;
+  border-radius: 0 8px 8px 0;
+}
+
+.message-list {
+  height: 85%;
+  overflow-y: auto;
+  padding: 0 10px;
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #c0c4cc;
+    border-radius: 3px;
+  }
+}
+
+.message-item {
+  display: flex;
+  margin-bottom: 24px;
+  align-items: flex-start;
+
+  &.message-user {
+    flex-direction: row-reverse;
+
+    .message-content {
+      margin-right: 12px;
+
+      .message-info {
+        justify-content: flex-end;
+      }
+
+      .message-text {
+        background: #409eff;
+        color: white;
+        border-radius: 12px 12px 0 12px;
+      }
+    }
+  }
+
+  &.message-ai {
+    .message-content {
+      margin-left: 12px;
+    }
+  }
+}
+
+.avatar {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 14px;
+  font-weight: bold;
+  flex-shrink: 0;
+
+  &.avatar-ai {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+  }
+
+  &.avatar-user {
+    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+    color: white;
+  }
+}
+
+.message-content {
+  max-width: 70%;
+
+  .message-info {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin-bottom: 6px;
+    font-size: 12px;
+    color: #909399;
+  }
+
+  .message-text {
+    padding: 12px 16px;
+    line-height: 1.6;
+    word-break: break-word;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+
+    ::v-deep {
+      pre {
+        background: #282c34;
+        color: #abb2bf;
+        padding: 12px;
+        border-radius: 6px;
+        overflow-x: auto;
+        margin: 8px 0;
+      }
+
+      code {
+        background: rgba(27,31,35,0.05);
+        padding: 2px 4px;
+        border-radius: 3px;
+        font-family: Consolas, monospace;
+      }
+
+      pre code {
+        background: transparent;
+        padding: 0;
+      }
+
+      strong {
+        color: #303133;
+      }
+
+      table {
+        border-collapse: collapse;
+        width: 100%;
+        margin: 12px 0;
+        font-size: 14px;
+
+        th, td {
+          border: 1px solid #dcdfe6;
+          padding: 8px 12px;
+          text-align: left;
+        }
+
+        th {
+          background: #f5f7fa;
+          font-weight: 600;
+          color: #606266;
+        }
+
+        tr:nth-child(even) {
+          background: #fafafa;
+        }
+
+        tr:hover {
+          background: #f5f7fa;
+        }
+      }
+
+      blockquote {
+        border-left: 4px solid #409eff;
+        margin: 12px 0;
+        padding: 8px 16px;
+        background: #ecf5ff;
+        color: #606266;
+      }
+
+      ul, ol {
+        padding-left: 24px;
+        margin: 8px 0;
+
+        li {
+          margin: 4px 0;
+        }
+      }
+
+      h1, h2, h3, h4, h5, h6 {
+        margin: 16px 0 8px;
+        color: #303133;
+        font-weight: 600;
+      }
+
+      h1 { font-size: 24px; }
+      h2 { font-size: 20px; }
+      h3 { font-size: 18px; }
+      h4 { font-size: 16px; }
+    }
+  }
+}
+
+.cursor-blink {
+  display: inline-block;
+  animation: blink 1s infinite;
+  color: #409eff;
+  font-weight: bold;
+}
+
+@keyframes blink {
+  0%, 50% { opacity: 1; }
+  51%, 100% { opacity: 0; }
+}
+
+.loading-indicator {
+  display: flex;
+  justify-content: center;
+  gap: 4px;
+  padding: 16px;
+
+  .loading-dot {
+    width: 8px;
+    height: 8px;
+    background: #409eff;
+    border-radius: 50%;
+    animation: bounce 1.4s infinite ease-in-out both;
+
+    &:nth-child(1) {
+      animation-delay: -0.32s;
+    }
+
+    &:nth-child(2) {
+      animation-delay: -0.16s;
+    }
+  }
+}
+
+@keyframes bounce {
+  0%, 80%, 100% {
+    transform: scale(0);
+  }
+  40% {
+    transform: scale(1);
+  }
+}
+
+.input-area {
+  background: white;
+  padding: 16px 20px ;
+  border-top: 1px solid #e4e7ed;
+
+  .input-wrapper {
+    display: flex;
+    gap: 12px;
+    align-items: flex-end;
+    max-width: 1200px;
+    margin: 0 auto;
+  }
+
+  .chat-input {
+    flex: 1;
+
+    ::v-deep textarea {
+      resize: none;
+      padding: 12px 16px;
+      font-size: 14px;
+      line-height: 1.6;
+
+      &:focus {
+        border-color: #409eff;
+      }
+    }
+  }
+
+  .send-button {
+    min-width: 80px;
+    height: auto;
+    padding: 12px 24px;
+  }
+
+  .input-tips {
+    text-align: center;
+    margin-top: 8px;
+    font-size: 12px;
+    color: #909399;
+  }
+}
+</style>

--
Gitblit v1.9.3