442 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			442 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <script>
 | |
| import ChatContent from "./components/chatContent.vue";
 | |
| import ThinkingBar from "./components/thinkingBar.vue";
 | |
| import {mapState} from "vuex";
 | |
| 
 | |
| export default {
 | |
|   name: "AiCopilot",
 | |
|   props: {
 | |
|     http: Function,
 | |
|     title: {default: "Copilot小助理"}
 | |
|   },
 | |
|   data() {
 | |
|     return {
 | |
|       show: false,
 | |
|       expand: false,
 | |
|       loading: false,
 | |
|       prompt: "",
 | |
|       history: [],
 | |
|       apps: [],
 | |
|       filter: "",
 | |
|       conversations: [],
 | |
|       currentConversation: null,
 | |
|       app: {}
 | |
|     }
 | |
|   },
 | |
|   computed: {
 | |
|     ...mapState(["user"]),
 | |
|     profile: v => v.user.info || {},
 | |
|     expandBtn: v => v.expand ? "收起" : "展开",
 | |
|     btns: () => [
 | |
|       {
 | |
|         label: "文件", icon: "https://cdn.sinoecare.com/i/2024/07/04/668663436e46e.png", click: () => {
 | |
|         }
 | |
|       }
 | |
|     ],
 | |
|     rowBtns: v => [
 | |
|       {icon: "https://cdn.sinoecare.com/i/2024/07/04/66866edc2910a.png", label: "置顶", click: row => 0},
 | |
|       {icon: "https://cdn.sinoecare.com/i/2024/07/04/66866ed734540.png", label: "编辑", click: row => 0},
 | |
|       {icon: "https://cdn.sinoecare.com/i/2024/07/04/66866eda99e4d.png", label: "删除", click: row => v.handleDeleteConversation(row.conversationId)},
 | |
|     ]
 | |
|   },
 | |
|   components: {ThinkingBar, ChatContent},
 | |
|   methods: {
 | |
|     getHistory(params) {
 | |
|       this.http.post("/app/appaicopilotinfo/list", null, {params}).then(res => {
 | |
|         if (res?.data) {
 | |
|           this.history = res.data.records
 | |
|         }
 | |
|       })
 | |
|     },
 | |
|     getApps() {
 | |
|       return this.http.post("/app/appaiconfiginfo/list", null, {
 | |
|         params: {
 | |
|           status: 1, size: 999
 | |
|         }
 | |
|       }).then(res => {
 | |
|         if (res?.data) {
 | |
|           return this.apps = res.data.records
 | |
|         }
 | |
|       })
 | |
|     },
 | |
|     getConversations() {
 | |
|       return this.http.post("/app/appaicopilotinfo/listHistory", null, {
 | |
|         params: {content: this.filter}
 | |
|       }).then(res => {
 | |
|         if (res?.data) {
 | |
|           return this.conversations = res.data.records || []
 | |
|         }
 | |
|       })
 | |
|     },
 | |
|     handleHotkey(evt) {
 | |
|       if (evt.ctrlKey && evt.key == "Enter") {
 | |
|         this.handleSend()
 | |
|       }
 | |
|     },
 | |
|     handleSend() {
 | |
|       if (!this.prompt.trim()) return this.$message.error("无法发送空白信息")
 | |
|       const concatenateStr = (content, i = 0, target = this.history.at(-1)) => {
 | |
|         target.content += content.slice(i, i + 1)
 | |
|         if (++i < content.length) setTimeout(() => concatenateStr(content, i, target), 50)
 | |
|       }
 | |
|       this.$debounce(() => {
 | |
|         const {currentConversation: conversationId, app, prompt: content} = this.$data
 | |
|         const message = {appType: "2", userType: 0, content, conversationId, ...app}
 | |
|         this.history.push(message)
 | |
|         this.loading = true
 | |
|         this.prompt = ""
 | |
|         this.http.post("/app/appaicopilotinfo/add", message).then(res => {
 | |
|           if (res?.data?.length >= 2) {
 | |
|             const last = res.data.at(-1)
 | |
|             this.history.push({...last, content: ""})
 | |
|             concatenateStr(last.content)
 | |
|           }
 | |
|         }).finally(() => {
 | |
|           this.loading = false
 | |
|           this.getConversations()
 | |
|         })
 | |
|       }, 100)
 | |
|     },
 | |
|     handleDeleteConversation(conversationId) {
 | |
|       this.$confirm("是否要删除该会话历史?").then(() => {
 | |
|         this.http.post("/app/appaicopilotinfo/deleteConversation", null, {params: {conversationId}}).then(res => {
 | |
|           if (res?.code == '0') {
 | |
|             this.$message.success("删除成功!")
 | |
|             this.getConversations()
 | |
|           }
 | |
|         })
 | |
|       }).catch(() => 0)
 | |
|     },
 | |
|     handleChangeApp(item) {
 | |
|       const {appId, id: aiConfigId} = item
 | |
|       this.handleChangeConversation({appId, aiConfigId})
 | |
|       this.history = [{userType: 2, content: `当前应用已切换至【${item.appName}】`}]
 | |
|     },
 | |
|     handleChangeConversation({conversationId, appId, aiConfigId} = {}) {
 | |
|       this.currentConversation = conversationId
 | |
|       this.app = {appId, aiConfigId}
 | |
|     },
 | |
|     getIcon(item = {}) {
 | |
|       const icon = item.appIconUrl || "https://cdn.sinoecare.com/i/2024/07/04/66864da1684ad.png"
 | |
|       return {
 | |
|         backgroundImage: `url(${icon})`
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   watch: {
 | |
|     currentConversation(v) {
 | |
|       v && this.getHistory({conversationId: v})
 | |
|     }
 | |
|   },
 | |
|   created() {
 | |
|     Promise.all([this.getApps(), this.getConversations()]).then(() => {
 | |
|       const {appId, id: aiConfigId} = this.apps.at(0)
 | |
|       this.handleChangeConversation(this.conversations.at(0) || {appId, aiConfigId})
 | |
|     })
 | |
|   }
 | |
| 
 | |
| }
 | |
| </script>
 | |
| 
 | |
| <template>
 | |
|   <section class="AiCopilot">
 | |
|     <div class="copilot" v-if="show">
 | |
|       <div class="flex header">
 | |
|         <b class="fill" v-text="title"/>
 | |
|         <div class="expandBtn pointer" v-text="expandBtn" @click="expand=!expand"/>
 | |
|         <div class="minimal pointer" v-text="'最小化'" @click="show=false"/>
 | |
|       </div>
 | |
|       <thinking-bar v-show="loading"/>
 | |
|       <div class="flex content">
 | |
|         <div class="left" :class="{expand}" v-loading="loading" element-loading-text="小助手正在思考中.."
 | |
|              element-loading-spinner="el-icon-loading" element-loading-background="rgba(255,255,255,0.8)">
 | |
|           <div class="profile">
 | |
|             <div v-text="profile.name"/>
 | |
|             <span v-text="profile.girdName"/>
 | |
|           </div>
 | |
|           <div class="apps flex">
 | |
|             <div v-for="(item,i) in apps" :key="i" class="app pointer" :style="getIcon(item)" v-text="item.appName" @click="handleChangeApp(item)"/>
 | |
|           </div>
 | |
|           <div class="conversation">
 | |
|             <el-input class="search" v-model="filter" placeholder="搜索历史对话记录" size="small" suffix-icon="el-icon-search" clearable @change="getConversations"/>
 | |
|             <div class="item pointer" v-for="item in conversations" :key="item.id" @click="currentConversation=item.conversationId" :class="{current:item.conversationId==currentConversation}">
 | |
|               {{ item.content }}
 | |
|               <div class="operation flex">
 | |
|                 <div v-for="(btn,i) in rowBtns" :key="i" class="pointer" :style="{backgroundImage: `url(${btn.icon})`}" @click.stop="btn.click(item)"/>
 | |
|               </div>
 | |
|             </div>
 | |
|           </div>
 | |
|         </div>
 | |
|         <div class="right flex column gap-14">
 | |
|           <chat-content class="fill" :list="history"/>
 | |
|           <div class="sendBox">
 | |
|             <div class="topBar flex">
 | |
|               <div v-for="(btn,i) in btns" :key="i" class="btn pointer" :style="{backgroundImage: `url(${btn.icon})`}" v-text="btn.label" @click="btn.click"/>
 | |
|             </div>
 | |
|             <div class="flex end">
 | |
|               <el-input type="textarea" class="fill input" autosize resize="none" v-model="prompt" placeholder="请输入..." :rows="5"
 | |
|                         @keydown.native="handleHotkey" :disabled="loading" :placeholder="loading?'正在思考中...':'请输入'"/>
 | |
|               <div class="sendBtn" @click="handleSend"/>
 | |
|             </div>
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>
 | |
|     <img class="icon" src="https://cdn.sinoecare.com/i/2024/06/04/665ec6f5ef213.png" @click="show=true"/>
 | |
|   </section>
 | |
| </template>
 | |
| 
 | |
| <style scoped lang="scss">
 | |
| .AiCopilot {
 | |
|   position: fixed;
 | |
|   right: 20px;
 | |
|   bottom: 48px;
 | |
|   display: flex;
 | |
|   align-items: flex-end;
 | |
|   gap: 18px;
 | |
|   z-index: 1888;
 | |
| 
 | |
|   .copilot {
 | |
|     border-radius: 0 0 8px 8px;
 | |
|     height: 600px;
 | |
|     background: #FFFFFF;
 | |
|     box-shadow: 0 0 20px 1px #0a255c1a;
 | |
| 
 | |
|     .header {
 | |
|       height: 48px;
 | |
|       background-image: linear-gradient(90deg, #3577FD 0%, #216AFD 100%);
 | |
|       box-shadow: 0 2px 6px 0 #0a255c14;
 | |
|       border-radius: 8px 8px 0 0;
 | |
|       overflow: hidden;
 | |
|       padding: 0 8px 0 14px;
 | |
|       color: #fff;
 | |
|       font-size: 14px;
 | |
|       gap: 14px;
 | |
| 
 | |
|       & > b {
 | |
|         font-size: 16px;
 | |
|         cursor: default;
 | |
|       }
 | |
| 
 | |
|       .expandBtn, .minimal {
 | |
|         padding-left: 18px;
 | |
|         font-weight: normal;
 | |
|         background-position: left center;
 | |
|         background-repeat: no-repeat;
 | |
|         background-size: 14px 14px;
 | |
|         line-height: 20px;
 | |
|       }
 | |
| 
 | |
|       .minimal {
 | |
|         background-image: url("https://cdn.sinoecare.com/i/2024/06/04/665ed2bd0a79e.png");
 | |
|       }
 | |
| 
 | |
|       .expandBtn {
 | |
|         background-image: url("https://cdn.sinoecare.com/i/2024/06/04/665ed2bd8b021.png");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     .content {
 | |
|       height: calc(100% - 50px);
 | |
| 
 | |
|       .left {
 | |
|         width: 0;
 | |
|         height: 100%;
 | |
|         transition: width 1s;
 | |
|         padding: 14px 0;
 | |
|         overflow: hidden;
 | |
| 
 | |
|         & > * {
 | |
|           margin: 0 14px;
 | |
|         }
 | |
| 
 | |
|         &.expand {
 | |
|           width: 306px;
 | |
| 
 | |
|           & + .right {
 | |
|             border-left-color: #ddd;
 | |
|             width: 660px;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         .profile {
 | |
|           padding: 18px 14px;
 | |
|           height: 88px;
 | |
|           background: url("https://cdn.sinoecare.com/i/2024/06/04/665ed2bc580fa.png") no-repeat;
 | |
|           background-size: 100% 100%;
 | |
|           font-size: 14px;
 | |
|           color: #222222;
 | |
|           letter-spacing: 0;
 | |
|           line-height: 20px;
 | |
| 
 | |
|           & > div {
 | |
|             font-weight: bold;
 | |
|             font-size: 20px;
 | |
|             line-height: 28px;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         .apps {
 | |
|           color: #333;
 | |
|           font-size: 14px;
 | |
|           margin-top: 18px;
 | |
|           padding-top: 29px;
 | |
|           position: relative;
 | |
| 
 | |
|           &:before {
 | |
|             content: "常用功能";
 | |
|             font-weight: bold;
 | |
|             position: absolute;
 | |
|             top: 0;
 | |
|             left: 0;
 | |
|           }
 | |
| 
 | |
|           .app {
 | |
|             font-size: 13px;
 | |
|             line-height: 18px;
 | |
|             text-align: center;
 | |
|             width: 72px;
 | |
|             height: 72px;
 | |
|             padding-top: 47px;
 | |
|             background-repeat: no-repeat;
 | |
|             background-position: center 7px;
 | |
|             background-size: 36px 36px;
 | |
|             border-radius: 4px;
 | |
| 
 | |
| 
 | |
|             &:hover {
 | |
|               background-color: #286ffd14;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         .conversation {
 | |
|           margin-top: 17px;
 | |
| 
 | |
|           :deep(.search) {
 | |
|             margin-bottom: 10px;
 | |
| 
 | |
|             input {
 | |
|               border-radius: 32px;
 | |
|               border: none;
 | |
|               background: #F4F6FA;
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           .item {
 | |
|             height: 32px;
 | |
|             border-radius: 16px;
 | |
|             font-size: 14px;
 | |
|             color: #666666;
 | |
|             line-height: 32px;
 | |
|             padding: 0 14px;
 | |
|             white-space: nowrap;
 | |
|             text-overflow: ellipsis;
 | |
|             position: relative;
 | |
| 
 | |
|             .operation {
 | |
|               position: absolute;
 | |
|               top: 0;
 | |
|               right: 0;
 | |
|               height: 32px;
 | |
|               background-image: linear-gradient(90deg, #e1ebff00 0%, #E2EBFF 14%);
 | |
|               border-radius: 0 16px 16px 0;
 | |
|               gap: 10px;
 | |
|               padding: 0 12px 0 20px;
 | |
|               display: none;
 | |
| 
 | |
|               .pointer {
 | |
|                 width: 14px;
 | |
|                 height: 14px;
 | |
|                 background-repeat: no-repeat;
 | |
|                 background-size: 100% 100%;
 | |
| 
 | |
|                 &:hover {
 | |
|                   opacity: .8;
 | |
|                 }
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             &:hover, &.current {
 | |
|               background: #2b71fd24;
 | |
| 
 | |
|               .operation {
 | |
|                 display: flex;
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       .right {
 | |
|         width: 468px;
 | |
|         height: 100%;
 | |
|         padding: 14px 0 14px 14px;
 | |
|         align-items: stretch;
 | |
|         border-left: 1px solid transparent;
 | |
|         transition: width 1s;
 | |
| 
 | |
|         .sendBtn {
 | |
|           width: 36px;
 | |
|           height: 24px;
 | |
|           border-radius: 24px;
 | |
|           background: url("https://cdn.sinoecare.com/i/2024/07/04/668650935514e.png") no-repeat center;
 | |
|           background-size: 100% 100%;
 | |
|           cursor: pointer;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       .chatPanel {
 | |
|         width: 100%;
 | |
|       }
 | |
| 
 | |
|       :deep(.sendBox) {
 | |
|         width: calc(100% - 14px);
 | |
|         margin-right: 14px;
 | |
|         padding: 8px 14px;
 | |
|         background: #F4F6FA;
 | |
|         align-items: flex-end;
 | |
|         border-radius: 4px;
 | |
| 
 | |
|         .topBar {
 | |
|           width: 100%;
 | |
|           height: 28px;
 | |
|           border-bottom: 1px solid #E0E0E0;
 | |
|           margin-bottom: 10px;
 | |
|           align-items: flex-start;
 | |
| 
 | |
|           .btn {
 | |
|             font-size: 12px;
 | |
|             color: #525F7A;
 | |
|             padding-left: 16px;
 | |
|             background-repeat: no-repeat;
 | |
|             background-position: left center;
 | |
|             background-size: 14px 14px;
 | |
| 
 | |
|             &:hover {
 | |
|               opacity: .8;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         .input > textarea {
 | |
|           width: 100%;
 | |
|           line-height: 20px;
 | |
|           padding: 0 5px 0 0;
 | |
|           border: none;
 | |
|           box-sizing: border-box;
 | |
|           background: transparent;
 | |
|           min-height: 73px !important;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   .icon {
 | |
|     width: 68px;
 | |
|     height: 58px;
 | |
|     cursor: pointer;
 | |
|   }
 | |
| }
 | |
| </style>
 |