c02cd2b9d319030b670c0a7115dedcb351f01be2..e4269277e493dad1c410ad40f8fd4e2c934a9b75
9 天以前 小小儁爺
1.新增ai测试页
e42692 对比 | 目录
9 天以前 小小儁爺
1.递交
cb6086 对比 | 目录
已添加1个文件
已修改7个文件
818 ■■■■■ 文件已修改
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/permission.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/deviceManager/checkRecord.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/deviceManager/checkStandard.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/deviceManager/maintainRecord.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/deviceManager/maintainStandard.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/systemSetting/ai.vue 796 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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",
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
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
  },
  {
src/views/deviceManager/checkRecord.vue
@@ -294,7 +294,7 @@
        {
          minWidth: false,
          width: 130,
          prop: 'org_code',
          prop: 'torg_code',
          label: '生产车间编码',
          id: 4,
          show: false,
@@ -304,7 +304,7 @@
        {
          minWidth: 110,
          width: false,
          prop: 'org_name',
          prop: 'torg_name',
          label: '生产车间',
          id: 5,
          show: true,
src/views/deviceManager/checkStandard.vue
@@ -754,6 +754,9 @@
      if (this.tableDataDialog.length === 0) {
        return this.$message.info('设备关联点检项信息不能为空!')
      }
      if (this.tableDataDialog.filter(i => !i.checkitem_code).length > 0) {
        return this.$message.info('设备关联点检项信息不能为空!')
      }
      this.$refs.dialogForm.validate(valid => {
        if (valid) {
src/views/deviceManager/maintainRecord.vue
@@ -304,7 +304,7 @@
        {
          minWidth: false,
          width: 130,
          prop: 'org_code',
          prop: 'torg_code',
          label: '生产车间编码',
          id: 4,
          show: false,
@@ -314,7 +314,7 @@
        {
          minWidth: 110,
          width: false,
          prop: 'org_name',
          prop: 'torg_name',
          label: '生产车间',
          id: 5,
          show: true,
src/views/deviceManager/maintainStandard.vue
@@ -715,6 +715,10 @@
        return this.$message.info('设备关联保养项信息不能为空!')
      }
      if (this.tableDataDialog.filter(i => !i.repairitem_code).length > 0) {
        return this.$message.info('设备关联保养项信息不能为空!')
      }
      this.$refs.dialogForm.validate(valid => {
        if (valid) {
          this.$store.state.app.buttonIsDisabled = true
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 è§£æžå¤±è´¥:', 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
          }
          // è§£ç æ•°æ®
          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('解析 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>