chenrong 1 年間 前
コミット
69a0bac462

+ 2 - 2
public/index.html

@@ -198,12 +198,12 @@
   </head>
   <body>
     <div id="app">
-	    <div id="loader-wrapper">
+	    <!-- <div id="loader-wrapper">
 		    <div id="loader"></div>
 		    <div class="loader-section section-left"></div>
 		    <div class="loader-section section-right"></div>
 		    <div class="load_title">正在加载系统资源,请耐心等待</div>
-        </div>
+      </div> -->
 	</div>
   </body>
 </html>

+ 21 - 6
src/api/chat.js

@@ -1,21 +1,36 @@
-// import request from '@/utils/request'
+import request from '@/utils/request'
 
 // 设置终止信号
 // const controller = new AbortController();
 // setAbortController(controller)
 // const signal = controller.signal;
 
-// ai对话
+// ai对话AXIOS
 export function streamChatWithWebApi(data) {
-  return fetch(`${process.env.VUE_APP_BASE_API}/system/chat/qwenChat`, {
+  return request({
+    url: '/system/test/auth/localAi',
     headers: {
       isToken: false,
       returnData: true,
       'Content-Type': 'application/json'
     },
-    method: 'POST',
-    body: JSON.stringify(data),
-    credentials: 'include',
+    method: 'post',
+    data: data
   })
 }
+// ai对话
+// export function streamChatWithWebApi(data) {
+//   return fetch(`${process.env.VUE_APP_BASE_API}/system/test/auth/localAi`, {
+//     headers: {
+//       isToken: false,
+//       returnData: true,
+//       'Content-Type': 'application/json'
+//     },
+//     method: 'POST',
+//     body: JSON.stringify(data),
+//     // credentials: 'include',
+//   })
+// }
+// /system/chat/qwenChatHistory
+// /system/test/auth/localAi
 // http://localhost:8086/system/chat/streamChatWithWeb?content=牛肉怎么炒好吃

BIN
src/assets/images/default_avatar_user.png


+ 177 - 26
src/views/chat/index.vue

@@ -18,44 +18,44 @@
               <img src="https://fallfor.ai/public/img/uploads/11/img_1715399064396_1 (2).gif">
             </span>
             <div class="info">
-              <div class="name">Juniper</div>
+              <div class="name">{{ info.characterName }}</div>
               <div class="tags">
-                <span class="tag">Female</span>
-                <span class="tag">Dominant</span>
-                <span class="tag">Submissive</span>
+                <span class="tag" v-for="(item, index) in info.labelArr" :key="index">
+                  {{ item }}
+                </span>
               </div>
-              <div class="infoContent">继母会做任何事情让你接受她。</div>
+              <div class="infoContent">{{ info.prologue }}</div>
             </div>
           </div>
         </div>
         <div class="chat-box"> 
-          <div class="messages">
+          <div class="messages" ref="messages">
             <template v-for="(item, index) in returnMessage">
-              <div v-if="item.user == 'ai'" class="text-white mb-4 flex" :key="index">
+              <div v-if="item.role == 'assistant'" class="text-white mb-4 flex" :key="index">
                 <!-- ai返回的信息 -->
                 <div class="pt-2 photo">
                   <img
-                    src="@/assets/images/1.png"
+                    src="https://fallfor.ai/public/img/uploads/11/img_1715399064396_1 (2).gif"
                     class="rounded-full w-14"
                   />
                 </div>
                 <div class="message fex-1 ml-4 mt-2 p-2 rounded-r-md rounded-bl-md text-xs" >
-                  <div v-show="messageLoading" class="loadingMessage" >
-                    <div v-loading="messageLoading" element-loading-background="rgba(0, 0, 0, 0.0)"></div>
+                  <div v-show="!item.content" class="loadingMessage" >
+                    <div v-loading="!item.content" element-loading-background="rgba(0, 0, 0, 0.0)"></div>
                   </div>
-                  <p v-show="!messageLoading" >{{ item.text }}</p>
+                  <p v-show="item.content" v-html="item.content"> </p>
                 </div>
               </div>
               <!-- 用户发送的信息 -->
-              <div v-if="item.user == 'me'" class="text-white mb-4 flex me" :key="index">
+              <div v-if="item.role == 'user'" class="text-white mb-4 flex me" :key="index">
                 <div class="pt-2 photo">
                   <img
-                    src="@/assets/images/1.png"
+                    src="@/assets/images/default_avatar_user.png"
                     class="rounded-full w-14"
                   />
                 </div>
                 <div class="message fex-1 mr-4  mt-2 p-2 rounded-l-md rounded-br-md text-xs">
-                  <p>{{ item.text }}</p>
+                  <p>{{ item.content }}</p>
                 </div>
               </div>
             </template>
@@ -66,6 +66,7 @@
               <span>@Leon S Kennedy - Resident Evil</span>
             </div> -->
             <div class="flex">
+              <el-button class="buttonBg1 mButton" @click="newStart" style="margin-right: 5px;" type="primary" round icon="el-icon-plus"></el-button>
               <input
                 type="text"
                 placeholder="输入消息..."
@@ -77,6 +78,7 @@
               <button @click="getStreamChatWithWeb" class="buttonBg1 text-white px-4 rounded-r-lg text-sm">
                 发送
               </button>
+              
             </div>
           </div>
         </div>
@@ -88,18 +90,56 @@
 
 <script>
 import { streamChatWithWebApi } from "@/api/chat.js"
+import { detailApi } from "@/api/detail.js"
 export default {
   data() {
     return {
-      messageLoading: true,
+      info: {},
+      messageLoading: false,
       returnMessage: [],
       content: ''
     }
   },
+  mounted() {
+    if (this.$route.query.characterId) {
+      this.getDetail(this.$route.query.characterId)
+    }
+  },
+  watch: {
+    returnMessage: {
+      handler(newVal, lodVal) {
+        let messages = this.$refs.messages
+        messages.scrollTop = messages.scrollHeight
+      },
+      deep: true
+    }
+  },
   methods: {
+    getDetail(id) {
+      detailApi(id).then(res => {
+        console.log(res, '角色详情');
+        this.info = res.data
+        let HistoryMessage = JSON.parse(localStorage.getItem(`[${123},${this.info.id}]`));
+        if (HistoryMessage) {
+          this.returnMessage = HistoryMessage
+        } else {
+          this.returnMessage.push({
+            role: 'assistant',
+            content: this.info.firstContent
+          })
+          localStorage.setItem(`[${123},${this.info.id}]`, JSON.stringify(this.returnMessage));
+        }
+      })
+    },
     goBack() {
       this.$router.back()
     },
+    newStart() {
+      // 清空历史数据
+      localStorage.removeItem(`[${123},${this.info.id}]`);
+      this.returnMessage = []
+      this.getDetail(this.info.id)
+    },
     Enterkey(e) {
       if (e.keyCode == 13) {
         this.getStreamChatWithWeb()
@@ -111,19 +151,90 @@ export default {
         return
       }
       this.returnMessage.push({
-        user: 'me',
-        text: this.content
+        role: 'user',
+        content: this.content
       })
+      let HistoryMessage = JSON.parse(localStorage.getItem(`[${123},${this.info.id}]`));
+      // 历史记录取最新的20条传给后端
+      if (HistoryMessage && HistoryMessage.length > 20) {
+        HistoryMessage = HistoryMessage.slice(HistoryMessage.length - 1, 20)
+      }
       let params = {
+        historyMessage: HistoryMessage || [],
+        characterId: this.info.id,
+        prompt: JSON.parse(JSON.stringify(this.content))
+      }
+      // 清空输入框的值
+      this.content = ''
+      // 新增一条ai信息
+      this.returnMessage.push({
+        role: 'assistant',
+        content: ''
+      })
+      let res = await streamChatWithWebApi(params)
+      console.log(res.data.message.content, 'res');
+      let content = res.data.message.content
+      let index = 0
+      let xing = 1
+      let messageInterval = setInterval(() => {
+        if (index > content.length - 1) {
+          console.log('结束');
+          // 消息全部显示后存到历史localStorage
+          let HistoryMessage = []
+          // 历史只存100条,长度超出截取最新的
+          if (this.returnMessage.length > 100) {
+            HistoryMessage = JSON.stringify(this.returnMessage.slice(this.returnMessage.length - 100, 100))
+          } else {
+            HistoryMessage = JSON.stringify(this.returnMessage)
+          }
+          localStorage.setItem(`[${123},${this.info.id}]`, HistoryMessage);
+
+          clearInterval(messageInterval)
+          return
+        }
+        console.log(content[index], 'content[index]');
+        if (content[index] == "*") {
+          if (xing == 1) {
+            xing = 2
+            this.returnMessage[this.returnMessage.length - 1].content += "("
+          } else if (xing == 2) {
+            xing = 1
+            this.returnMessage[this.returnMessage.length - 1].content += ")"
+          }
+        } else {
+          this.returnMessage[this.returnMessage.length - 1].content += content[index]
+        }
+        index += 1
+      }, 50)
+    },
+    async getStreamChatWithWeb_old() {
+      this.messageLoading = true
+      if (!this.content) {
+        return
+      }
+      this.returnMessage.push({
+        role: 'user',
         content: this.content
+      })
+      let HistoryMessage = JSON.parse(localStorage.getItem(`[${123},${this.info.id}]`));
+      // 历史记录取最新的20条传给后端
+      if (HistoryMessage && HistoryMessage.length > 20) {
+        HistoryMessage = HistoryMessage.slice(HistoryMessage.length - 1, 20)
+      }
+      let params = {
+        historyMessage: HistoryMessage || [],
+        characterId: this.info.id,
+        prompt: this.content
       }
+      // 新增一条ai信息
       this.returnMessage.push({
-        user: 'ai',
-        text: ''
+        role: 'assistant',
+        content: ''
       })
       let res = await streamChatWithWebApi(params)
+      console.log(res, 'res');
       this.messageLoading = false
-      // 新增一条ai信息
+      
       if (res.status != 200) {
         this.$message.error(res.statusText)
         this.returnMessage.splice(this.returnMessage.length - 1, 1)
@@ -132,21 +243,58 @@ export default {
       // 清空输入框的值
       this.content = ''
 
-      console.log(res, 'res.body');
+      console.log(res, 'res>>>');
+      return
       const reader = res.body.getReader()
       const decoder=new TextDecoder()
       while(1){
         const {done, value} = await reader.read()
+        // console.log(done, 'done');
         if(done){
+          console.log(value, 'value');
+          // if (typeof(value) == "undefined") {
+          //   this.$message.error('错误')
+          //   this.returnMessage.splice(this.returnMessage.length - 1, 1)
+          // }
+          console.log('结束');
+          // 消息全部显示后存到历史localStorage
+          let HistoryMessage = []
+          // 历史只存100条,长度超出截取最新的
+          if (this.returnMessage.length > 100) {
+            HistoryMessage = JSON.stringify(this.returnMessage.slice(this.returnMessage.length - 100, 100))
+          } else {
+            HistoryMessage = JSON.stringify(this.returnMessage)
+          }
+          localStorage.setItem(`[${123},${this.info.id}]`, HistoryMessage);
           break;
         }
-        const txt = decoder.decode(value)
-        console.log(txt, 'txt'); 
         
-        this.returnMessage[this.returnMessage.length - 1].text += txt
         //txt就是一个一个的字 然后添加到页面上就可以了
+        const txt = decoder.decode(value)
+        // const txt = decoder.decode(value)
+        
+        let data = JSON.parse(txt).message.content
+        console.log(data, 'data');
+        // if (this.isJSON(txt) && JSON.parse(txt).code != 200) {
+        //   let data = JSON.parse(txt)
+        //   this.$message.error(data.msg)
+        //   this.returnMessage.splice(this.returnMessage.length - 1, 1)
+        //   break;
+        // }
+        this.returnMessage[this.returnMessage.length - 1].content += txt
       }
-    }
+    },
+    isJSON(str) {
+      if (typeof str == 'string') {
+          try {
+              JSON.parse(str);
+              return true;
+          } catch(e) {
+              // console.log(e);
+              return false;
+          }
+      }
+    },
   }
 }
 </script>
@@ -282,7 +430,7 @@ export default {
 .message {
   background: #ffffff0f;
   border: 1px solid #5b5b5e;
-  max-width: calc(100% - 56px);
+  max-width: calc(100% - 72px);
   // flex: 1;
   min-width: 80px;
 }
@@ -310,4 +458,7 @@ export default {
     display: flex;
     justify-content: center;
   }
+  .mButton {
+    border: solid 0px;
+  }
 </style>

+ 12 - 5
src/views/detail/index.vue

@@ -4,7 +4,7 @@
     <div class="text-white w-2/4 mx-auto">
       <div class="flex justify-between">
         <div>
-          <img src="@/assets/images/4.png" class="w-40 h-40" />
+          <img src="https://fallfor.ai/public/img/uploads/11/img_1715399022390_00024-654400262.jpg" class="img w-40 h-40" />
           <div class="">
             <div>
               <button class="text-xs bg-gray-700 py-1 px-2 rounded mt-2 flex">
@@ -58,7 +58,7 @@
         </div>
       </div>
 
-      <div class="flex items-center space-x-4 mt-4">
+      <!-- <div class="flex items-center space-x-4 mt-4">
         <div class="flex-1">
           <h1 class="text-2xl mb-2">
             {{ info.characterName }}
@@ -70,12 +70,12 @@
             {{ info.characterPersonalityTags }}
           </p>
         </div>
-      </div>
+      </div> -->
 
       <div class="mt-4">
         <h2 class="text-lg mb-2">描述</h2>
         <p class="text-sm">
-          {{ info.characterIntroduction }}
+          {{ info.prologue }}
         </p>
       </div>
       <div class="flex justify-center mt-8">
@@ -114,7 +114,10 @@ export default {
   methods: {
     toChat() {
       this.$router.push({
-        path: '/chat'
+        path: '/chat',
+        query: {
+          characterId: this.info.id
+        }
       })
     },
     getDetail(id) {
@@ -134,4 +137,8 @@ export default {
 .labels {
   grid-auto-rows: 32px;
 }
+.img {
+  object-fit: cover;
+  object-position: top;
+}
 </style>

+ 22 - 285
src/views/home.vue

@@ -1,310 +1,47 @@
 <template>
-  <div class="home bg-gray-900 comBg">
-    <Header @search="searchChange" :searchName="searchName" />
-    <div class="min-h-screen text-white">
-      <div class="container mx-auto py-3">
-        <div class="flex mb-3">
-          <div class="flex space-x-4">
-            <button class="px-3 py-2 rounded topActive">热门</button>
-            <button class="bg-gray-800 px-3 py-2 rounded">趋势</button>
-            <button class="bg-gray-800 px-3 py-2 rounded">最新</button>
-          </div>
-        </div>
-        <div class="flex flex-grow mb-3 items-center">
-          <input
-            type="text"
-            placeholder="搜索标签"
-            class="searchInput mr-2 px-3 py-2 rounded w-1/4 focus:border-blue-300 focus:outline-none focus:ring"
-          />
-          <div
-            v-for="(item, index) in selectLabelObj"
-            :key="index"
-            class="px-3 py-1 mx-1 rounded flex items-center cursor-pointer buttonBg1"
-            @click="clickLabel(item)"
-          >
-            {{ item.labelName }}
-            <i class="fas el-icon-close text-white"></i>
-          </div>
-          <el-button v-show="selectLabelObj.length > 0" class="textButton" type="text" @click="clearSelectLabel">移除所有</el-button>
-        </div>
-        <div class="flex flex-wrap">
-          <div
-            v-for="(item, index) in labelData.slice(0, labelLength)"
-            :key="index"
-            :class="[
-              selectLabel.indexOf(item.id) != -1 ? 'buttonBg1' : 'labelButton',
-              'labelButton px-3 py-1 mx-1 mb-1 rounded flex items-center cursor-pointer'
-            ]" 
-            @click="clickLabel(item)"
-          >
-            <i class="fas fa-fire text-yellow-500 mr-2"></i>
-            {{ item.labelName }}({{item.num}})
-          </div>
-          <div v-show="labelData.length > 8 && labelLength == 8" @click="labelLength = labelData.length"  class="labelButton px-3 py-1 mx-1 mb-1 rounded cursor-pointer">
-            ....
-          </div>
-          <div v-show="labelData.length > 8 && labelLength == 8" @click="labelLength = labelData.length" class="labelButton px-3 py-1 mx-1 mb-1 rounded cursor-pointer">
-            显示更多
-          </div>
-          <div v-show="labelData.length > 8 && labelLength > 8" @click="labelLength = 8" class="labelButton px-3 py-1 mx-1 mb-1 rounded cursor-pointer">
-            隐藏
-          </div>
-        </div>
-
-        <div class="grid grid-cols-5 gap-4 mt-4">
-          <div
-            v-for="(item, index) in characterList"
-            :key="index"
-            class="box2 rounded-lg overflow-hidden shadow-lg cursor-pointer"
-            @click="toDetail(item)"
-          >
-            <img :src="url[index % 4]" class="w-full" />
-            <div class="p-3">
-              <h3 class="text-lg font-bold">{{ item.characterName }}</h3>
-              <p class="text-sm">
-                {{ item.characterIntroduction }}
-              </p>
-
-              <div class="flex my-2 flex-wrap">
-                <div v-for="(item2, index2) in item.labelArr" :key="index2" class="m-0.5 tag px-2 py-1 rounded flex items-center text-xs">
-                  <!-- <i class="fas fa-smile text-yellow-400 mr-2"></i> -->
-                  {{ item2 }}
-                </div>
-              </div>
-            </div>
-            <div class="flex items-center justify-between mt-3 px-3 pb-3">
-              <div class="flex items-center box1">
-                <v-icon name="heart" scale="1"/>
-                <span class="ml-1">{{ item.likeNum }}</span>
-              </div>
-              <div class="flex items-center box1">
-                <!-- <i class="fas fa-comment mr-1"></i> -->
-                <v-icon name="star" scale="1"/>
-                <span class="ml-1">{{ item.collections }}</span>
-              </div>
-              <!-- <div class="flex items-center">
-                <v-icon name="share" scale="1.5"/>
-                <span>{{ item.likeNum }}</span>
-              </div> -->
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
+  <div>
+    <HomePC v-if="!isMobile" />
+    <HomeH5 v-else />
   </div>
 </template>
 
 <script>
-import Header from "@/views/homeComponents/Header.vue"
-import { labelListApi, characterListApi } from "@/api/home.js"
-// 引入需要的图标
-import 'vue-awesome/icons/heart'
-// import 'vue-awesome/icons/comment'
-import 'vue-awesome/icons/star'
+import HomePC from "@/views/homeComponents/HomePC.vue"
+import HomeH5 from "@/views/homeComponents/HomeH5.vue"
 
 export default {
-  name: "Index",
+  name: "home",
   components: {
-    Header
+    HomePC,
+    HomeH5
   },
   data() {
     return {
-      labelLength: 8,
-      searchName: '',
-      labelData: [],
-      selectLabel: [],
-      selectLabelObj: [],
-      characterList: [],
-      url: [
-        require('@/assets/images/1.png'),
-        require('@/assets/images/2.png'),
-        require('@/assets/images/3.png'),
-        require('@/assets/images/4.png'),
-      ]
+      isMobile: false,
     }
   },
   mounted() {
-    // 获取标签
-    this.getLabelList()
-    // 获取角色列表
-    if (this.$route.params.searchValue) {
-      // 有查询条件,做查询处理
-      this.searchChange(this.$route.params.searchValue)
-    } else {
-      // 没有查询条件则直接调用默认查询
-      this.getCharacterList()
+    let _this = this
+    window.onresize = () => {
+      setTimeout(() => {
+        if (_this.fIsMobile()) {
+          console.log('移动端');
+          _this.isMobile = true
+        } else {
+          console.log('pc端');
+          _this.isMobile = false
+        }
+      }, 100)
     }
-    
   },
   methods: {
-    searchChange(value) {
-      // console.log('查询条件', value);
-      this.searchName = value
-      let query = {
-        characterName: this.searchName,
-        labelId: this.selectLabel
-      }
-      this.getCharacterList(query)
-    },
-    getLabelList() {
-      let params = {
-        pageSize: 20,
-        pageNum: 1
-      }
-      labelListApi(params).then(res => {
-        // console.log(res, '标签列表');
-        this.labelData = res.rows
-      })
-    },
-    getCharacterList(query) {
-      let params = {
-        pageSize: 20,
-        pageNum: 1
-      }
-      if (query) {
-        params = {
-          ...params,
-          ...query
-        }
-      }
-      characterListApi(params).then(res => {
-        // console.log(res, '角色列表');
-        this.characterList = res.rows
-      })
-    },
-    clickLabel(item) {
-      let index = this.selectLabel.indexOf(item.id)
-      if (index == -1) {
-        this.selectLabel.push(item.id)
-        this.selectLabelObj.push(item)
-      } else {
-        this.selectLabel.splice(index, 1)
-        this.selectLabelObj.splice(index, 1)
-      }
-      let query = {
-        characterName: this.searchName,
-        labelId: this.selectLabel
-      }
-      this.getCharacterList(query)
-    },
-    clearSelectLabel() {
-      this.selectLabel = []
-      this.selectLabelObj = []
-      let query = {
-        characterName: this.searchName,
-        labelId: this.selectLabel
-      }
-      this.getCharacterList(query)
-    },
-    toDetail(item) {
-      this.$router.push({
-        path: '/detail',
-        query: {
-          id: item.id
-        }
-      })
+    fIsMobile() {
+      return /Android|iPhone|iPad|iPod|BlackBerry|webOS|Windows Phone|SymbianOS|IEMobile|Opera Mini/i.test(navigator.userAgent);
     }
   },
 };
 </script>
 
 <style scoped lang="scss">
-.home {
-  font-size: 17.5px;
-  blockquote {
-    padding: 10px 20px;
-    margin: 0 0 20px;
-    font-size: 17.5px;
-    border-left: 5px solid #eee;
-  }
-  hr {
-    margin-top: 20px;
-    margin-bottom: 20px;
-    border: 0;
-    border-top: 1px solid #eee;
-  }
-  .col-item {
-    margin-bottom: 20px;
-  }
-
-  ul {
-    padding: 0;
-    margin: 0;
-  }
-
-  font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
-  font-size: 16px;
-  color: #676a6c;
-  overflow-x: hidden;
-
-  ul {
-    list-style-type: none;
-  }
-
-  h4 {
-    margin-top: 0px;
-  }
-
-  h2 {
-    margin-top: 10px;
-    font-size: 26px;
-    font-weight: 100;
-  }
-
-  p {
-    margin-top: 10px;
-
-    b {
-      font-weight: 700;
-    }
-  }
-
-  .update-log {
-    ol {
-      display: block;
-      list-style-type: decimal;
-      margin-block-start: 1em;
-      margin-block-end: 1em;
-      margin-inline-start: 0;
-      margin-inline-end: 0;
-      padding-inline-start: 40px;
-    }
-  }
-}
-.textButton {
-  color: #eee;
-  margin-left: 5px;
-}
-.textButton:hover {
-  color: #9333ea;
-}
-.labelButton {
-  background: #ffffff1a;
-}
-.buttonBg1 {
-  background: linear-gradient(270deg, #5ea1f9, #d287f1);
-}
 
-.topActive {
-  background: var(--bg-color1);
-}
-.tag {
-  background: rgba(245, 174, 67, 0.2);
-}
-.searchInput {
-  background: #ffffff00;
-  border: 1px solid #ffffff4d;
-}
-.box1 {
-  border: 1px solid #ffffff4d;
-  height: 30px;
-  padding: 0 10px;
-  border-radius: 18px;
-  width: 68px;
-  font-size: 14px;
-}
-.box2 {
-  background: #ffffff0f; 
-  border: 1px solid #ffffff2e;
-}
 </style>

+ 316 - 0
src/views/homeComponents/HomeH5.vue

@@ -0,0 +1,316 @@
+<template>
+  <div class="home bg-gray-900 comBg">
+    <Header @search="searchChange" :searchName="searchName" />
+    <div class="min-h-screen text-white">
+      <div class="container mx-auto py-3">
+        <div class="flex mb-3">
+          <div class="flex space-x-4">
+            <button class="px-3 py-2 rounded topActive">这是h5</button>
+            <button class="bg-gray-800 px-3 py-2 rounded">趋势</button>
+            <button class="bg-gray-800 px-3 py-2 rounded">最新</button>
+          </div>
+        </div>
+        <div class="flex flex-grow mb-3 items-center">
+          <input
+            type="text"
+            placeholder="搜索标签"
+            class="searchInput mr-2 px-3 py-2 rounded w-1/4 focus:border-blue-300 focus:outline-none focus:ring"
+          />
+          <div
+            v-for="(item, index) in selectLabelObj"
+            :key="index"
+            class="px-3 py-1 mx-1 rounded flex items-center cursor-pointer buttonBg1"
+            @click="clickLabel(item)"
+          >
+            {{ item.labelName }}
+            <i class="fas el-icon-close text-white"></i>
+          </div>
+          <el-button v-show="selectLabelObj.length > 0" class="textButton" type="text" @click="clearSelectLabel">移除所有</el-button>
+        </div>
+        <div class="flex flex-wrap">
+          <div
+            v-for="(item, index) in labelData.slice(0, labelLength)"
+            :key="index"
+            :class="[
+              selectLabel.indexOf(item.id) != -1 ? 'buttonBg1' : 'labelButton',
+              'labelButton px-3 py-1 mx-1 mb-1 rounded flex items-center cursor-pointer'
+            ]" 
+            @click="clickLabel(item)"
+          >
+            <i class="fas fa-fire text-yellow-500 mr-2"></i>
+            {{ item.labelName }}({{item.num}})
+          </div>
+          <div v-show="labelData.length > 8 && labelLength == 8" @click="labelLength = labelData.length"  class="labelButton px-3 py-1 mx-1 mb-1 rounded cursor-pointer">
+            ....
+          </div>
+          <div v-show="labelData.length > 8 && labelLength == 8" @click="labelLength = labelData.length" class="labelButton px-3 py-1 mx-1 mb-1 rounded cursor-pointer">
+            显示更多
+          </div>
+          <div v-show="labelData.length > 8 && labelLength > 8" @click="labelLength = 8" class="labelButton px-3 py-1 mx-1 mb-1 rounded cursor-pointer">
+            隐藏
+          </div>
+        </div>
+
+        <div class="grid grid-cols-5 gap-4 mt-4">
+          <div
+            v-for="(item, index) in characterList"
+            :key="index"
+            class="box2 rounded-lg overflow-hidden shadow-lg cursor-pointer"
+            @click="toDetail(item)"
+          >
+            <img v-if="item.id != 4" :src="url[index % 4]" class="w-full img" />
+            <img v-if="item.id == 4" src="https://fallfor.ai/public/img/uploads/11/img_1715399022390_00024-654400262.jpg" class="w-full img" />
+            <div class="p-3">
+              <h3 class="text-lg font-bold">{{ item.characterName }}</h3>
+              <p class="text-sm">
+                {{ item.prologue }}
+              </p>
+
+              <div class="flex my-2 flex-wrap">
+                <div v-for="(item2, index2) in item.labelArr" :key="index2" class="m-0.5 tag px-2 py-1 rounded flex items-center text-xs">
+                  <!-- <i class="fas fa-smile text-yellow-400 mr-2"></i> -->
+                  {{ item2 }}
+                </div>
+              </div>
+            </div>
+            <div class="flex items-center justify-between mt-3 px-3 pb-3">
+              <div class="flex items-center box1">
+                <v-icon name="heart" scale="1"/>
+                <span class="ml-1">{{ item.likeNum }}</span>
+              </div>
+              <div class="flex items-center box1">
+                <!-- <i class="fas fa-comment mr-1"></i> -->
+                <v-icon name="star" scale="1"/>
+                <span class="ml-1">{{ item.collections }}</span>
+              </div>
+              <!-- <div class="flex items-center">
+                <v-icon name="share" scale="1.5"/>
+                <span>{{ item.likeNum }}</span>
+              </div> -->
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Header from "@/views/homeComponents/Header.vue"
+import { labelListApi, characterListApi } from "@/api/home.js"
+// 引入需要的图标
+import 'vue-awesome/icons/heart'
+// import 'vue-awesome/icons/comment'
+import 'vue-awesome/icons/star'
+
+export default {
+  name: "HomeH5",
+  components: {
+    Header
+  },
+  data() {
+    return {
+      labelLength: 8,
+      searchName: '',
+      labelData: [],
+      selectLabel: [],
+      selectLabelObj: [],
+      characterList: [],
+      url: [
+        require('@/assets/images/1.png'),
+        require('@/assets/images/2.png'),
+        require('@/assets/images/3.png'),
+        require('@/assets/images/4.png'),
+      ]
+    }
+  },
+  mounted() {
+    // 获取标签
+    this.getLabelList()
+    // 获取角色列表
+    if (this.$route.params.searchValue) {
+      // 有查询条件,做查询处理
+      this.searchChange(this.$route.params.searchValue)
+    } else {
+      // 没有查询条件则直接调用默认查询
+      this.getCharacterList()
+    }
+    
+  },
+  methods: {
+    searchChange(value) {
+      // console.log('查询条件', value);
+      this.searchName = value
+      let query = {
+        characterName: this.searchName,
+        labelId: this.selectLabel
+      }
+      this.getCharacterList(query)
+    },
+    getLabelList() {
+      let params = {
+        pageSize: 20,
+        pageNum: 1
+      }
+      labelListApi(params).then(res => {
+        // console.log(res, '标签列表');
+        this.labelData = res.rows
+      })
+    },
+    getCharacterList(query) {
+      let params = {
+        pageSize: 20,
+        pageNum: 1
+      }
+      if (query) {
+        params = {
+          ...params,
+          ...query
+        }
+      }
+      characterListApi(params).then(res => {
+        // console.log(res, '角色列表');
+        this.characterList = res.rows
+      })
+    },
+    clickLabel(item) {
+      let index = this.selectLabel.indexOf(item.id)
+      if (index == -1) {
+        this.selectLabel.push(item.id)
+        this.selectLabelObj.push(item)
+      } else {
+        this.selectLabel.splice(index, 1)
+        this.selectLabelObj.splice(index, 1)
+      }
+      let query = {
+        characterName: this.searchName,
+        labelId: this.selectLabel
+      }
+      this.getCharacterList(query)
+    },
+    clearSelectLabel() {
+      this.selectLabel = []
+      this.selectLabelObj = []
+      let query = {
+        characterName: this.searchName,
+        labelId: this.selectLabel
+      }
+      this.getCharacterList(query)
+    },
+    toDetail(item) {
+      this.$router.push({
+        path: '/detail',
+        query: {
+          id: item.id
+        }
+      })
+    }
+  },
+};
+</script>
+
+<style scoped lang="scss">
+.home {
+  font-size: 17.5px;
+  blockquote {
+    padding: 10px 20px;
+    margin: 0 0 20px;
+    font-size: 17.5px;
+    border-left: 5px solid #eee;
+  }
+  hr {
+    margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;
+  }
+  .col-item {
+    margin-bottom: 20px;
+  }
+
+  ul {
+    padding: 0;
+    margin: 0;
+  }
+
+  font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 16px;
+  color: #676a6c;
+  overflow-x: hidden;
+
+  ul {
+    list-style-type: none;
+  }
+
+  h4 {
+    margin-top: 0px;
+  }
+
+  h2 {
+    margin-top: 10px;
+    font-size: 26px;
+    font-weight: 100;
+  }
+
+  p {
+    margin-top: 10px;
+
+    b {
+      font-weight: 700;
+    }
+  }
+
+  .update-log {
+    ol {
+      display: block;
+      list-style-type: decimal;
+      margin-block-start: 1em;
+      margin-block-end: 1em;
+      margin-inline-start: 0;
+      margin-inline-end: 0;
+      padding-inline-start: 40px;
+    }
+  }
+}
+.textButton {
+  color: #eee;
+  margin-left: 5px;
+}
+.textButton:hover {
+  color: #9333ea;
+}
+.labelButton {
+  background: #ffffff1a;
+}
+.buttonBg1 {
+  background: linear-gradient(270deg, #5ea1f9, #d287f1);
+}
+
+.topActive {
+  background: var(--bg-color1);
+}
+.tag {
+  background: rgba(245, 174, 67, 0.2);
+}
+.searchInput {
+  background: #ffffff00;
+  border: 1px solid #ffffff4d;
+}
+.box1 {
+  border: 1px solid #ffffff4d;
+  height: 30px;
+  padding: 0 10px;
+  border-radius: 18px;
+  width: 68px;
+  font-size: 14px;
+}
+.box2 {
+  background: #ffffff0f; 
+  border: 1px solid #ffffff2e;
+}
+.img {
+  max-height: 220px;
+  object-fit: cover;
+  object-position: top;
+}
+</style>

+ 316 - 0
src/views/homeComponents/HomePC.vue

@@ -0,0 +1,316 @@
+<template>
+  <div class="home bg-gray-900 comBg">
+    <Header @search="searchChange" :searchName="searchName" />
+    <div class="min-h-screen text-white">
+      <div class="container mx-auto py-3">
+        <div class="flex mb-3">
+          <div class="flex space-x-4">
+            <button class="px-3 py-2 rounded topActive">热门</button>
+            <button class="bg-gray-800 px-3 py-2 rounded">趋势</button>
+            <button class="bg-gray-800 px-3 py-2 rounded">最新</button>
+          </div>
+        </div>
+        <div class="flex flex-grow mb-3 items-center">
+          <input
+            type="text"
+            placeholder="搜索标签"
+            class="searchInput mr-2 px-3 py-2 rounded w-1/4 focus:border-blue-300 focus:outline-none focus:ring"
+          />
+          <div
+            v-for="(item, index) in selectLabelObj"
+            :key="index"
+            class="px-3 py-1 mx-1 rounded flex items-center cursor-pointer buttonBg1"
+            @click="clickLabel(item)"
+          >
+            {{ item.labelName }}
+            <i class="fas el-icon-close text-white"></i>
+          </div>
+          <el-button v-show="selectLabelObj.length > 0" class="textButton" type="text" @click="clearSelectLabel">移除所有</el-button>
+        </div>
+        <div class="flex flex-wrap">
+          <div
+            v-for="(item, index) in labelData.slice(0, labelLength)"
+            :key="index"
+            :class="[
+              selectLabel.indexOf(item.id) != -1 ? 'buttonBg1' : 'labelButton',
+              'labelButton px-3 py-1 mx-1 mb-1 rounded flex items-center cursor-pointer'
+            ]" 
+            @click="clickLabel(item)"
+          >
+            <i class="fas fa-fire text-yellow-500 mr-2"></i>
+            {{ item.labelName }}({{item.num}})
+          </div>
+          <div v-show="labelData.length > 8 && labelLength == 8" @click="labelLength = labelData.length"  class="labelButton px-3 py-1 mx-1 mb-1 rounded cursor-pointer">
+            ....
+          </div>
+          <div v-show="labelData.length > 8 && labelLength == 8" @click="labelLength = labelData.length" class="labelButton px-3 py-1 mx-1 mb-1 rounded cursor-pointer">
+            显示更多
+          </div>
+          <div v-show="labelData.length > 8 && labelLength > 8" @click="labelLength = 8" class="labelButton px-3 py-1 mx-1 mb-1 rounded cursor-pointer">
+            隐藏
+          </div>
+        </div>
+
+        <div class="grid grid-cols-5 gap-4 mt-4">
+          <div
+            v-for="(item, index) in characterList"
+            :key="index"
+            class="box2 rounded-lg overflow-hidden shadow-lg cursor-pointer"
+            @click="toDetail(item)"
+          >
+            <img v-if="item.id != 4" :src="url[index % 4]" class="w-full img" />
+            <img v-if="item.id == 4" src="https://fallfor.ai/public/img/uploads/11/img_1715399022390_00024-654400262.jpg" class="w-full img" />
+            <div class="p-3">
+              <h3 class="text-lg font-bold">{{ item.characterName }}</h3>
+              <p class="text-sm">
+                {{ item.prologue }}
+              </p>
+
+              <div class="flex my-2 flex-wrap">
+                <div v-for="(item2, index2) in item.labelArr" :key="index2" class="m-0.5 tag px-2 py-1 rounded flex items-center text-xs">
+                  <!-- <i class="fas fa-smile text-yellow-400 mr-2"></i> -->
+                  {{ item2 }}
+                </div>
+              </div>
+            </div>
+            <div class="flex items-center justify-between mt-3 px-3 pb-3">
+              <div class="flex items-center box1">
+                <v-icon name="heart" scale="1"/>
+                <span class="ml-1">{{ item.likeNum }}</span>
+              </div>
+              <div class="flex items-center box1">
+                <!-- <i class="fas fa-comment mr-1"></i> -->
+                <v-icon name="star" scale="1"/>
+                <span class="ml-1">{{ item.collections }}</span>
+              </div>
+              <!-- <div class="flex items-center">
+                <v-icon name="share" scale="1.5"/>
+                <span>{{ item.likeNum }}</span>
+              </div> -->
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Header from "@/views/homeComponents/Header.vue"
+import { labelListApi, characterListApi } from "@/api/home.js"
+// 引入需要的图标
+import 'vue-awesome/icons/heart'
+// import 'vue-awesome/icons/comment'
+import 'vue-awesome/icons/star'
+
+export default {
+  name: "HomePC",
+  components: {
+    Header
+  },
+  data() {
+    return {
+      labelLength: 8,
+      searchName: '',
+      labelData: [],
+      selectLabel: [],
+      selectLabelObj: [],
+      characterList: [],
+      url: [
+        require('@/assets/images/1.png'),
+        require('@/assets/images/2.png'),
+        require('@/assets/images/3.png'),
+        require('@/assets/images/4.png'),
+      ]
+    }
+  },
+  mounted() {
+    // 获取标签
+    this.getLabelList()
+    // 获取角色列表
+    if (this.$route.params.searchValue) {
+      // 有查询条件,做查询处理
+      this.searchChange(this.$route.params.searchValue)
+    } else {
+      // 没有查询条件则直接调用默认查询
+      this.getCharacterList()
+    }
+    
+  },
+  methods: {
+    searchChange(value) {
+      // console.log('查询条件', value);
+      this.searchName = value
+      let query = {
+        characterName: this.searchName,
+        labelId: this.selectLabel
+      }
+      this.getCharacterList(query)
+    },
+    getLabelList() {
+      let params = {
+        pageSize: 20,
+        pageNum: 1
+      }
+      labelListApi(params).then(res => {
+        // console.log(res, '标签列表');
+        this.labelData = res.rows
+      })
+    },
+    getCharacterList(query) {
+      let params = {
+        pageSize: 20,
+        pageNum: 1
+      }
+      if (query) {
+        params = {
+          ...params,
+          ...query
+        }
+      }
+      characterListApi(params).then(res => {
+        // console.log(res, '角色列表');
+        this.characterList = res.rows
+      })
+    },
+    clickLabel(item) {
+      let index = this.selectLabel.indexOf(item.id)
+      if (index == -1) {
+        this.selectLabel.push(item.id)
+        this.selectLabelObj.push(item)
+      } else {
+        this.selectLabel.splice(index, 1)
+        this.selectLabelObj.splice(index, 1)
+      }
+      let query = {
+        characterName: this.searchName,
+        labelId: this.selectLabel
+      }
+      this.getCharacterList(query)
+    },
+    clearSelectLabel() {
+      this.selectLabel = []
+      this.selectLabelObj = []
+      let query = {
+        characterName: this.searchName,
+        labelId: this.selectLabel
+      }
+      this.getCharacterList(query)
+    },
+    toDetail(item) {
+      this.$router.push({
+        path: '/detail',
+        query: {
+          id: item.id
+        }
+      })
+    }
+  },
+};
+</script>
+
+<style scoped lang="scss">
+.home {
+  font-size: 17.5px;
+  blockquote {
+    padding: 10px 20px;
+    margin: 0 0 20px;
+    font-size: 17.5px;
+    border-left: 5px solid #eee;
+  }
+  hr {
+    margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;
+  }
+  .col-item {
+    margin-bottom: 20px;
+  }
+
+  ul {
+    padding: 0;
+    margin: 0;
+  }
+
+  font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 16px;
+  color: #676a6c;
+  overflow-x: hidden;
+
+  ul {
+    list-style-type: none;
+  }
+
+  h4 {
+    margin-top: 0px;
+  }
+
+  h2 {
+    margin-top: 10px;
+    font-size: 26px;
+    font-weight: 100;
+  }
+
+  p {
+    margin-top: 10px;
+
+    b {
+      font-weight: 700;
+    }
+  }
+
+  .update-log {
+    ol {
+      display: block;
+      list-style-type: decimal;
+      margin-block-start: 1em;
+      margin-block-end: 1em;
+      margin-inline-start: 0;
+      margin-inline-end: 0;
+      padding-inline-start: 40px;
+    }
+  }
+}
+.textButton {
+  color: #eee;
+  margin-left: 5px;
+}
+.textButton:hover {
+  color: #9333ea;
+}
+.labelButton {
+  background: #ffffff1a;
+}
+.buttonBg1 {
+  background: linear-gradient(270deg, #5ea1f9, #d287f1);
+}
+
+.topActive {
+  background: var(--bg-color1);
+}
+.tag {
+  background: rgba(245, 174, 67, 0.2);
+}
+.searchInput {
+  background: #ffffff00;
+  border: 1px solid #ffffff4d;
+}
+.box1 {
+  border: 1px solid #ffffff4d;
+  height: 30px;
+  padding: 0 10px;
+  border-radius: 18px;
+  width: 68px;
+  font-size: 14px;
+}
+.box2 {
+  background: #ffffff0f; 
+  border: 1px solid #ffffff2e;
+}
+.img {
+  max-height: 220px;
+  object-fit: cover;
+  object-position: top;
+}
+</style>

+ 1 - 1
vue.config.js

@@ -36,7 +36,7 @@ module.exports = {
     open: true,
     proxy: {
       [process.env.VUE_APP_BASE_API]: {
-        target: `http://192.168.0.103:8086`,
+        target: `http://192.168.0.101:8086`,
         // target: `http://127.0.0.1:8090`,
         changeOrigin: true,
         pathRewrite: {