feat: vue-mobile => 完成会话聊天页面功能,增加主题切换功能

This commit is contained in:
RockYang 2023-06-26 16:39:00 +08:00
parent b9e9eae93f
commit 6a733de556
11 changed files with 417 additions and 258 deletions

View File

@ -8,6 +8,7 @@ import (
"chatplus/store/model" "chatplus/store/model"
"chatplus/utils" "chatplus/utils"
"chatplus/utils/resp" "chatplus/utils/resp"
"strings"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
@ -88,7 +89,7 @@ func (h *ManagerHandler) Migrate(c *gin.Context) {
continue continue
} }
for k, _ := range m { for k := range m {
roleKeys = append(roleKeys, k) roleKeys = append(roleKeys, k)
} }
u.ChatRoles = utils.JsonEncode(roleKeys) u.ChatRoles = utils.JsonEncode(roleKeys)
@ -101,9 +102,11 @@ func (h *ManagerHandler) Migrate(c *gin.Context) {
var roles []model.ChatRole var roles []model.ChatRole
h.db.Find(&roles) h.db.Find(&roles)
for _, r := range roles { for _, r := range roles {
if !strings.HasPrefix(r.Icon, "/") {
r.Icon = "/" + r.Icon r.Icon = "/" + r.Icon
h.db.Updates(&r) h.db.Updates(&r)
} }
}
break break
case "history": case "history":
// 修改角色图片,改成绝对路径 // 修改角色图片,改成绝对路径
@ -114,6 +117,18 @@ func (h *ManagerHandler) Migrate(c *gin.Context) {
h.db.Updates(&r) h.db.Updates(&r)
} }
break break
case "avatar":
// 更新用户的头像地址
var users []model.User
h.db.Find(&users)
for _, u := range users {
if !strings.HasPrefix(u.Avatar, "/") {
u.Avatar = "/" + u.Avatar
h.db.Updates(&u)
}
}
break
} }
resp.SUCCESS(c, "SUCCESS") resp.SUCCESS(c, "SUCCESS")

View File

@ -52,10 +52,20 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
session := h.App.ChatSession.Get(sessionId) session := h.App.ChatSession.Get(sessionId)
if session.SessionId == "" { if session.SessionId == "" {
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
logger.Info("用户未登录") logger.Info("用户未登录")
c.Abort() c.Abort()
return return
} }
session = types.ChatSession{
SessionId: sessionId,
ClientIP: c.ClientIP(),
Username: user.Username,
UserId: user.Id,
}
h.App.ChatSession.Put(sessionId, session)
}
// use old chat data override the chat model and role ID // use old chat data override the chat model and role ID
var chat model.ChatItem var chat model.ChatItem

View File

@ -220,7 +220,13 @@ func (h *UserHandler) Logout(c *gin.Context) {
func (h *UserHandler) Session(c *gin.Context) { func (h *UserHandler) Session(c *gin.Context) {
user, err := utils.GetLoginUser(c, h.db) user, err := utils.GetLoginUser(c, h.db)
if err == nil { if err == nil {
resp.SUCCESS(c, user) var userVo vo.User
err := utils.CopyObject(user, &userVo)
if err != nil {
resp.ERROR(c)
}
userVo.Id = user.Id
resp.SUCCESS(c, userVo)
} else { } else {
resp.NotAuth(c) resp.NotAuth(c)
} }

View File

@ -1,46 +1,57 @@
<template> <template>
<div class="message-reply"> <div class="mobile-message-prompt">
<div class="chat-item"> <div class="chat-item">
<div class="content" v-html="content"></div> <div ref="contentRef" :data-clipboard-text="content" class="content" v-html="content"></div>
<div class="triangle"></div> <div class="triangle"></div>
</div> </div>
<div class="chat-icon"> <div class="chat-icon">
<img :src="icon" alt="User"/> <van-image :src="icon"/>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup>
import {defineComponent} from "vue" import {onMounted, ref} from "vue";
import Clipboard from "clipboard";
import {showNotify} from "vant";
export default defineComponent({ const props = defineProps({
name: 'ChatPrompt',
props: {
content: { content: {
type: String, type: String,
default: '', default: '',
}, },
icon: { icon: {
type: String, type: String,
default: 'images/user-icon.png', default: '/images/user-icon.png',
} }
}, });
data() { const contentRef = ref(null)
return {} onMounted(() => {
}, const clipboard = new Clipboard(contentRef.value);
clipboard.on('success', () => {
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.on('error', () => {
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
}) })
</script> </script>
<style lang="stylus"> <style lang="stylus">
.message-reply { .mobile-message-prompt {
justify-content: flex-end; display flex
justify-content: flex-end
.chat-icon { .chat-icon {
margin-left 5px; margin-left 5px
.van-image {
width 25px
img { img {
border-radius 5px; border-radius 5px
}
} }
} }
@ -64,11 +75,27 @@ export default defineComponent({
word-break break-word; word-break break-word;
padding: 6px 10px; padding: 6px 10px;
background-color: #98E165; background-color: #98E165;
color var(--content-color); color #444444
font-size: var(--content-font-size); font-size: 16px
border-radius: 5px; border-radius: 5px
line-height 1.5 line-height 1.5
} }
} }
} }
.van-theme-dark {
.mobile-message-prompt {
.chat-item {
.triangle {
border-left: 5px solid #223A34
}
.content {
background-color: #223A34
color #c1c1c1
}
}
}
}
</style> </style>

View File

@ -1,42 +1,26 @@
<template> <template>
<div class="message-prompt"> <div class="mobile-message-reply">
<div class="chat-icon"> <div class="chat-icon">
<img :src="icon" alt="ChatGPT"> <van-image :src="icon"/>
</div> </div>
<div class="chat-item"> <div class="chat-item">
<div class="triangle"></div> <div class="triangle"></div>
<div class="content-box"> <div class="content-box">
<div class="content" v-html="content"></div> <div ref="contentRef" :data-clipboard-text="orgContent" class="content" v-html="content"></div>
<div class="tool-box">
<el-tooltip
class="box-item"
effect="dark"
content="复制回答"
placement="bottom"
>
<el-button type="info" class="copy-reply" :data-clipboard-text="orgContent" plain>
<el-icon>
<DocumentCopy/>
</el-icon>
</el-button>
</el-tooltip>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup>
import {defineComponent} from "vue" import {onMounted, ref} from "vue"
import {randString} from "@/utils/libs";
import {DocumentCopy} from "@element-plus/icons-vue";
export default defineComponent({ import Clipboard from "clipboard";
name: 'ChatReply', import {showNotify} from "vant";
components: {DocumentCopy},
props: { const props = defineProps({
content: { content: {
type: String, type: String,
default: '', default: '',
@ -47,28 +31,36 @@ export default defineComponent({
}, },
icon: { icon: {
type: String, type: String,
default: 'images/gpt-icon.png', default: '/images/gpt-icon.png',
} }
}, });
data() {
return {
id: randString(32),
clipboard: null,
}
},
const contentRef = ref(null)
onMounted(() => {
const clipboard = new Clipboard(contentRef.value);
clipboard.on('success', () => {
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.on('error', () => {
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
}) })
</script> </script>
<style lang="stylus"> <style lang="stylus">
.message-prompt { .mobile-message-reply {
display flex
justify-content: flex-start; justify-content: flex-start;
.chat-icon { .chat-icon {
margin-right 5px; margin-right 5px
.van-image {
width 25px
img { img {
border-radius 5px; border-radius 5px
}
} }
} }
@ -95,12 +87,15 @@ export default defineComponent({
flex-direction row flex-direction row
.content { .content {
text-align left
width 100%
overflow-x auto
min-height 20px; min-height 20px;
word-break break-word; word-break break-word;
padding: 6px 10px; padding: 6px 10px;
color var(--content-color) color #444444
background-color: #fff; background-color: #ffffff;
font-size: var(--content-font-size); font-size: 16px
border-radius: 5px; border-radius: 5px;
p:last-child { p:last-child {
@ -112,18 +107,10 @@ export default defineComponent({
} }
p > code { p > code {
color #cc0000 color #2b2b2b
background-color #f1f1f1 background-color #c1c1c1
} padding 2px 5px
} border-radius 5px
.tool-box {
padding-left 10px;
font-size 16px;
.el-button {
height 20px
padding 5px 2px;
} }
} }
} }
@ -131,4 +118,28 @@ export default defineComponent({
} }
} }
.van-theme-dark {
.mobile-message-reply {
.chat-item {
.triangle {
border-right: 5px solid #404042;
}
.content-box {
.content {
color #c1c1c1
background-color: #404042;
p > code {
color #c1c1c1
background-color #2b2b2b
}
}
}
}
}
}
</style> </style>

View File

@ -22,6 +22,7 @@ import {
Picker, Picker,
Popup, Popup,
Search, Search,
ShareSheet,
Sticky, Sticky,
SwipeCell, SwipeCell,
Tabbar, Tabbar,
@ -54,6 +55,7 @@ app.use(DropdownItem);
app.use(Sticky); app.use(Sticky);
app.use(SwipeCell); app.use(SwipeCell);
app.use(Dialog); app.use(Dialog);
app.use(ShareSheet);
app.use(router).use(ElementPlus).mount('#app') app.use(router).use(ElementPlus).mount('#app')

11
web/src/store/system.js Normal file
View File

@ -0,0 +1,11 @@
import Storage from "good-storage";
const MOBILE_THEME = "MOBILE_THEME"
export function getMobileTheme() {
return Storage.get(MOBILE_THEME) ? Storage.get(MOBILE_THEME) : 'light'
}
export function setMobileTheme(theme) {
Storage.set(MOBILE_THEME, theme)
}

View File

@ -40,10 +40,10 @@
</div> </div>
<div class="tool-box"> <div class="tool-box">
<el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="user"> <el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="isLogin">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<el-image :src="user['avatar']"/> <el-image :src="loginUser.avatar"/>
<span class="username">{{ user ? user['nickname'] : 'Chat-Plus-User' }}</span> <span class="username">{{ loginUser.nickname }}</span>
<el-icon><ArrowDown/></el-icon> <el-icon><ArrowDown/></el-icon>
</span> </span>
<template #dropdown> <template #dropdown>
@ -212,7 +212,7 @@ import 'highlight.js/styles/a11y-dark.css'
import {dateFormat, randString, removeArrayItem, renderInputText, UUID} from "@/utils/libs"; import {dateFormat, randString, removeArrayItem, renderInputText, UUID} from "@/utils/libs";
import {ElMessage, ElMessageBox} from "element-plus"; import {ElMessage, ElMessageBox} from "element-plus";
import hl from "highlight.js"; import hl from "highlight.js";
import {getLoginUser, getSessionId, removeLoginUser} from "@/store/session"; import {getSessionId, removeLoginUser} from "@/store/session";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
@ -232,7 +232,7 @@ const mainWinHeight = ref(0); // 主窗口高度
const chatBoxHeight = ref(0); // const chatBoxHeight = ref(0); //
const leftBoxHeight = ref(0); const leftBoxHeight = ref(0);
const loading = ref(true); const loading = ref(true);
const user = getLoginUser(); const loginUser = ref(null);
const roles = ref([]); const roles = ref([]);
const roleId = ref(0) const roleId = ref(0)
const newChatItem = ref(null); const newChatItem = ref(null);
@ -243,7 +243,8 @@ const isLogin = ref(false)
onMounted(() => { onMounted(() => {
resizeElement(); resizeElement();
checkSession().then(() => { checkSession().then((user) => {
loginUser.value = user
isLogin.value = true isLogin.value = true
// //
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => { httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
@ -267,7 +268,7 @@ onMounted(() => {
}) })
}).catch((e) => { }).catch((e) => {
console.log(e) console.log(e)
//router.push('login') router.push('login')
}); });
const clipboard = new Clipboard('.copy-reply'); const clipboard = new Clipboard('.copy-reply');
@ -282,7 +283,7 @@ onMounted(() => {
// //
const loadChats = function () { const loadChats = function () {
httpGet("/api/chat/list?user_id=" + user.id).then((res) => { httpGet("/api/chat/list?user_id=" + loginUser.value.id).then((res) => {
if (res.data) { if (res.data) {
chatList.value = res.data; chatList.value = res.data;
allChats.value = res.data; allChats.value = res.data;
@ -570,7 +571,7 @@ const sendMessage = function () {
chatData.value.push({ chatData.value.push({
type: "prompt", type: "prompt",
id: randString(32), id: randString(32),
icon: user.avatar, icon: loginUser.value.avatar,
content: renderInputText(prompt.value), content: renderInputText(prompt.value),
created_at: new Date().getTime(), created_at: new Date().getTime(),
}); });
@ -707,8 +708,8 @@ const searchChat = function () {
} }
const updateUser = function (data) { const updateUser = function (data) {
user.avatar = data.avatar; loginUser.value.avatar = data.avatar;
user.nickname = data.nickname; loginUser.value.nickname = data.nickname;
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="container mobile-chat-list" v-if="isLogin"> <div v-if="isLogin" class="container mobile-chat-list">
<van-nav-bar <van-nav-bar
:title="title" :title="title"
left-text="新建会话" left-text="新建会话"
@ -12,16 +12,16 @@
<div class="content"> <div class="content">
<van-search <van-search
v-model="chatName" v-model="chatName"
placeholder="请输入会话标题"
input-align="center" input-align="center"
placeholder="请输入会话标题"
@input="search" @input="search"
/> />
<van-list <van-list
v-model:loading="loading"
v-model:error="error" v-model:error="error"
error-text="请求失败,点击重新加载" v-model:loading="loading"
:finished="finished" :finished="finished"
error-text="请求失败,点击重新加载"
finished-text="没有更多了" finished-text="没有更多了"
@load="onLoad" @load="onLoad"
> >
@ -29,15 +29,15 @@
<van-cell @click="changeChat(item)"> <van-cell @click="changeChat(item)">
<div class="chat-list-item"> <div class="chat-list-item">
<van-image <van-image
round
:src="item.icon" :src="item.icon"
round
/> />
<div class="van-ellipsis">{{ item.title }}</div> <div class="van-ellipsis">{{ item.title }}</div>
</div> </div>
</van-cell> </van-cell>
<template #right> <template #right>
<van-button square type="primary" text="修改" @click="editChat(item)"/> <van-button square text="修改" type="primary" @click="editChat(item)"/>
<van-button square type="danger" text="删除" @click="removeChat(item)"/> <van-button square text="删除" type="danger" @click="removeChat(item)"/>
</template> </template>
</van-swipe-cell> </van-swipe-cell>
</van-list> </van-list>
@ -45,18 +45,18 @@
<van-popup v-model:show="showPicker" position="bottom"> <van-popup v-model:show="showPicker" position="bottom">
<van-picker <van-picker
title="选择模型和角色"
:columns="columns" :columns="columns"
title="选择模型和角色"
@cancel="showPicker = false" @cancel="showPicker = false"
@confirm="newChat" @confirm="newChat"
> >
<template #option="item"> <template #option="item">
<div class="picker-option"> <div class="picker-option">
<van-image <van-image
fit="cover"
:src="item.icon"
round
v-if="item.icon" v-if="item.icon"
:src="item.icon"
fit="cover"
round
/> />
<span>{{ item.text }}</span> <span>{{ item.text }}</span>
</div> </div>
@ -69,13 +69,11 @@
<script setup> <script setup>
import {ref} from "vue"; import {ref} from "vue";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import {getLoginUser} from "@/store/session";
import {showConfirmDialog, showFailToast, showSuccessToast, showToast} from "vant"; import {showConfirmDialog, showFailToast, showSuccessToast, showToast} from "vant";
import {checkSession} from "@/action/session"; import {checkSession} from "@/action/session";
import router from "@/router"; import router from "@/router";
import {setChatConfig} from "@/store/chat"; import {setChatConfig} from "@/store/chat";
import {removeArrayItem, UUID} from "@/utils/libs"; import {removeArrayItem} from "@/utils/libs";
import {ElMessage} from "element-plus";
const title = ref("会话列表") const title = ref("会话列表")
const chatName = ref("") const chatName = ref("")
@ -84,14 +82,15 @@ const allChats = ref([])
const loading = ref(false) const loading = ref(false)
const finished = ref(false) const finished = ref(false)
const error = ref(false) const error = ref(false)
const user = getLoginUser() const loginUser = ref(null)
const isLogin = ref(false) const isLogin = ref(false)
const roles = ref([]) const roles = ref([])
const models = ref([]) const models = ref([])
const showPicker = ref(false) const showPicker = ref(false)
const columns = ref([roles.value, models.value]) const columns = ref([roles.value, models.value])
checkSession().then(() => { checkSession().then((user) => {
loginUser.value = user
isLogin.value = true isLogin.value = true
// //
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => { httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
@ -99,7 +98,12 @@ checkSession().then(() => {
const items = res.data const items = res.data
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
// console.log(items[i]) // console.log(items[i])
roles.value.push({text: items[i].name, value: items[i].id, icon: items[i].icon}) roles.value.push({
text: items[i].name,
value: items[i].id,
icon: items[i].icon,
helloMsg: items[i].hello_msg
})
} }
} }
}).catch(() => { }).catch(() => {
@ -122,7 +126,7 @@ checkSession().then(() => {
}) })
const onLoad = () => { const onLoad = () => {
httpGet("/api/chat/list?user_id=" + user.id).then((res) => { httpGet("/api/chat/list?user_id=" + loginUser.value.id).then((res) => {
if (res.data) { if (res.data) {
chats.value = res.data; chats.value = res.data;
allChats.value = res.data; allChats.value = res.data;
@ -172,11 +176,12 @@ const newChat = (item) => {
role: { role: {
id: options[0].value, id: options[0].value,
name: options[0].text, name: options[0].text,
icon: options[0].icon icon: options[0].icon,
helloMsg: options[0].helloMsg
}, },
model: options[1].value, model: options[1].value,
title: '新建会话', title: '新建会话',
chatId: UUID() chatId: 0
}) })
router.push('/mobile/chat/session') router.push('/mobile/chat/session')
} }
@ -197,7 +202,8 @@ const changeChat = (chat) => {
}, },
model: chat.model, model: chat.model,
title: chat.title, title: chat.title,
chatId: chat.chat_id chatId: chat.chat_id,
helloMsg: chat.hello_msg,
}) })
router.push('/mobile/chat/session') router.push('/mobile/chat/session')
} }
@ -220,7 +226,7 @@ const removeChat = (item) => {
</script> </script>
<style scoped lang="stylus"> <style lang="stylus" scoped>
.mobile-chat-list { .mobile-chat-list {
.content { .content {

View File

@ -1,7 +1,8 @@
<template> <template>
<van-config-provider :theme="getMobileTheme()">
<div class="mobile-chat"> <div class="mobile-chat">
<van-sticky :offset-top="0" position="top" ref="navBarRef"> <van-sticky ref="navBarRef" :offset-top="0" position="top">
<van-nav-bar left-text="返回" left-arrow @click-left="router.back()"> <van-nav-bar left-arrow left-text="返回" @click-left="router.back()">
<template #title> <template #title>
<van-dropdown-menu> <van-dropdown-menu>
<van-dropdown-item :title="title"> <van-dropdown-item :title="title">
@ -12,39 +13,45 @@
</template> </template>
<template #right> <template #right>
<van-icon name="delete-o" @click="clearChatHistory"/> <van-icon name="share-o" @click="showShare = true"/>
</template> </template>
</van-nav-bar> </van-nav-bar>
</van-sticky> </van-sticky>
<van-share-sheet
v-model:show="showShare"
title="立即分享给好友"
:options="shareOptions"
@select="shareChat"
/>
<div class="message-list-box" id="message-list-box" :style="{height: winHeight+'px'}"> <div id="message-list-box" :style="{height: winHeight+'px'}" class="message-list-box">
<van-list <van-list
v-model:error="error"
v-model:loading="loading" v-model:loading="loading"
:finished="finished" :finished="finished"
v-model:error="error"
error-text="请求失败,点击重新加载" error-text="请求失败,点击重新加载"
@load="onLoad" @load="onLoad"
> >
<van-cell v-for="item in chatData" :key="item"> <van-cell v-for="item in chatData" :key="item" :border="false" class="message-line">
<chat-prompt <chat-prompt
v-if="item.type==='prompt'" v-if="item.type==='prompt'"
:icon="item.icon" :content="item.content"
:created-at="dateFormat(item['created_at'])" :created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']" :icon="item.icon"
:model="model" :model="model"
:content="item.content"/> :tokens="item['tokens']"/>
<chat-reply v-else-if="item.type==='reply'" <chat-reply v-else-if="item.type==='reply'"
:content="item.content"
:created-at="dateFormat(item['created_at'])"
:icon="item.icon" :icon="item.icon"
:org-content="item.orgContent" :org-content="item.orgContent"
:created-at="dateFormat(item['created_at'])" :tokens="item['tokens']"/>
:tokens="item['tokens']"
:content="item.content"/>
</van-cell> </van-cell>
</van-list> </van-list>
</div> </div>
<van-sticky :offset-bottom="0" position="bottom" ref="bottomBarRef"> <van-sticky ref="bottomBarRef" :offset-bottom="0" position="bottom">
<div class="chat-box"> <div class="chat-box">
<van-cell-group> <van-cell-group>
<van-field <van-field
@ -67,20 +74,24 @@
</div> </div>
</van-sticky> </van-sticky>
</div> </div>
</van-config-provider>
</template> </template>
<script setup> <script setup>
import {nextTick, onMounted, ref} from "vue"; import {nextTick, onMounted, ref} from "vue";
import {showToast} from "vant"; import {showToast, showDialog} from "vant";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {dateFormat, UUID} from "@/utils/libs"; import {dateFormat, randString, renderInputText, UUID} from "@/utils/libs";
import {getChatConfig} from "@/store/chat"; import {getChatConfig} from "@/store/chat";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import hl from "highlight.js"; import hl from "highlight.js";
import 'highlight.js/styles/a11y-dark.css' import 'highlight.js/styles/a11y-dark.css'
import ChatPrompt from "@/components/mobile/ChatPrompt.vue"; import ChatPrompt from "@/components/mobile/ChatPrompt.vue";
import ChatReply from "@/components/mobile/ChatReply.vue"; import ChatReply from "@/components/mobile/ChatReply.vue";
import {getSessionId} from "@/store/session";
import {checkSession} from "@/action/session";
import {getMobileTheme} from "@/store/system";
const winHeight = ref(0) const winHeight = ref(0)
const navBarRef = ref(null) const navBarRef = ref(null)
@ -92,6 +103,7 @@ const role = chatConfig.role
const model = chatConfig.model const model = chatConfig.model
const title = chatConfig.title const title = chatConfig.title
const chatId = chatConfig.chatId const chatId = chatConfig.chatId
const loginUser = ref(null)
onMounted(() => { onMounted(() => {
winHeight.value = document.body.offsetHeight - navBarRef.value.$el.offsetHeight - bottomBarRef.value.$el.offsetHeight winHeight.value = document.body.offsetHeight - navBarRef.value.$el.offsetHeight - bottomBarRef.value.$el.offsetHeight
@ -101,16 +113,20 @@ const chatData = ref([])
const loading = ref(false) const loading = ref(false)
const finished = ref(false) const finished = ref(false)
const error = ref(false) const error = ref(false)
checkSession().then(user => {
loginUser.value = user
}).catch(() => {
router.push('/login')
})
const onLoad = () => { const onLoad = () => {
httpGet('/api/chat/history?chat_id=' + chatId).then(res => { httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
// //
loading.value = false; loading.value = false;
finished.value = true; finished.value = true;
const data = res.data const data = res.data
if (!data || data.length === 0) { if (data && data.length > 0) {
return
}
const md = require('markdown-it')(); const md = require('markdown-it')();
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") { if (data[i].type === "prompt") {
@ -129,7 +145,12 @@ const onLoad = () => {
blocks.forEach((block) => { blocks.forEach((block) => {
hl.highlightElement(block) hl.highlightElement(block)
}) })
scrollListBox()
}) })
}
//
connect(chatId, role.id);
}).catch(() => { }).catch(() => {
error.value = true error.value = true
}) })
@ -157,7 +178,6 @@ const connect = function (chat_id, role_id) {
socket.value.close(); socket.value.close();
} }
const _role = getRoleById(role_id);
// WebSocket // WebSocket
const _sessionId = getSessionId(); const _sessionId = getSessionId();
let host = process.env.VUE_APP_WS_HOST let host = process.env.VUE_APP_WS_HOST
@ -168,9 +188,8 @@ const connect = function (chat_id, role_id) {
host = 'ws://' + location.host; host = 'ws://' + location.host;
} }
} }
const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model=${model.value}`); const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model=${model}`);
_socket.addEventListener('open', () => { _socket.addEventListener('open', () => {
chatData.value = []; //
previousText.value = ''; previousText.value = '';
canSend.value = true; canSend.value = true;
activelyClose.value = false; activelyClose.value = false;
@ -180,12 +199,10 @@ const connect = function (chat_id, role_id) {
chatData.value.push({ chatData.value.push({
type: "reply", type: "reply",
id: randString(32), id: randString(32),
icon: _role['icon'], icon: role.icon,
content: _role['hello_msg'], content: role.helloMsg,
orgContent: _role['hello_msg'], orgContent: role.helloMsg,
}) })
} else { //
loadChatHistory(chat_id);
} }
}); });
@ -199,7 +216,7 @@ const connect = function (chat_id, role_id) {
chatData.value.push({ chatData.value.push({
type: "reply", type: "reply",
id: randString(32), id: randString(32),
icon: _role['icon'], icon: role.icon,
content: "" content: ""
}); });
} else if (data.type === 'end') { // } else if (data.type === 'end') { //
@ -208,26 +225,6 @@ const connect = function (chat_id, role_id) {
showStopGenerate.value = false; showStopGenerate.value = false;
lineBuffer.value = ''; // lineBuffer.value = ''; //
//
if (isNewChat && newChatItem.value !== null) {
newChatItem.value['title'] = previousText.value;
newChatItem.value['chat_id'] = chat_id;
chatList.value.unshift(newChatItem.value);
activeChat.value = newChatItem.value;
newChatItem.value = null; //
}
// token
const reply = chatData.value[chatData.value.length - 1]
httpGet(`/api/chat/tokens?text=${reply.orgContent}&model=${model.value}`).then(res => {
reply['created_at'] = new Date().getTime();
reply['tokens'] = res.data;
//
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
})
})
} else { } else {
lineBuffer.value += data.content; lineBuffer.value += data.content;
let md = require('markdown-it')(); let md = require('markdown-it')();
@ -237,16 +234,16 @@ const connect = function (chat_id, role_id) {
nextTick(() => { nextTick(() => {
hl.configure({ignoreUnescapedHTML: true}) hl.configure({ignoreUnescapedHTML: true})
const lines = document.querySelectorAll('.chat-line'); const lines = document.querySelectorAll('.message-line');
const blocks = lines[lines.length - 1].querySelectorAll('pre code'); const blocks = lines[lines.length - 1].querySelectorAll('pre code');
blocks.forEach((block) => { blocks.forEach((block) => {
hl.highlightElement(block) hl.highlightElement(block)
}) })
}) })
} }
//
nextTick(() => { nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight) scrollListBox()
}) })
}; };
} }
@ -254,21 +251,21 @@ const connect = function (chat_id, role_id) {
}); });
_socket.addEventListener('close', () => { _socket.addEventListener('close', () => {
console.log(activelyClose.value)
if (activelyClose.value) { // if (activelyClose.value) { //
return; return;
} }
// //
canSend.value = true; canSend.value = true;
socket.value = null; socket.value = null;
loading.value = true;
checkSession().then(() => { checkSession().then(() => {
connect(chat_id, role_id) connect(chat_id, role_id)
}).catch(() => { }).catch(() => {
ElMessageBox({ showDialog({
title: '会话提示', title: '会话提示',
message: "当前会话已经失效,请重新登录", message: '当前会话已经失效,请重新登录!',
confirmButtonText: 'OK', }).then(() => {
callback: () => router.push('login') router.push('/login')
}); });
}); });
}); });
@ -276,27 +273,92 @@ const connect = function (chat_id, role_id) {
socket.value = _socket; socket.value = _socket;
} }
const clearChatHistory = () => { //
showToast('清空聊记录') const scrollListBox = () => {
document.getElementById('message-list-box').scrollTo(0, document.getElementById('message-list-box').scrollHeight)
} }
const sendMessage = () => { const sendMessage = () => {
showToast("发送成功") if (canSend.value === false) {
showToast("AI 正在作答中,请稍后...");
return
}
if (prompt.value.trim().length === 0 || canSend.value === false) {
return false;
}
//
chatData.value.push({
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: renderInputText(prompt.value),
created_at: new Date().getTime(),
});
nextTick(() => {
scrollListBox()
})
canSend.value = false;
showStopGenerate.value = true;
showReGenerate.value = false;
socket.value.send(prompt.value);
previousText.value = prompt.value;
prompt.value = '';
return true;
} }
const stopGenerate = () => { const stopGenerate = () => {
showToast("停止生成") showStopGenerate.value = false;
httpGet("/api/chat/stop?session_id=" + getSessionId()).then(() => {
canSend.value = true;
if (previousText.value !== '') {
showReGenerate.value = true;
}
})
} }
const reGenerate = () => { const reGenerate = () => {
showToast('重新生成') canSend.value = false;
showStopGenerate.value = true;
showReGenerate.value = false;
const text = '重新生成上述问题的答案:' + previousText.value;
//
chatData.value.push({
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: renderInputText(text)
});
socket.value.send(text);
}
const showShare = ref(false)
const shareOptions = [
{name: '微信', icon: 'wechat'},
{name: '微博', icon: 'weibo'},
{name: '复制链接', icon: 'link'},
{name: '分享海报', icon: 'poster'},
]
const shareChat = () => {
showShare.value = false
showToast('功能待开发')
} }
</script> </script>
<style scoped lang="stylus"> <style lang="stylus" scoped>
.mobile-chat { .mobile-chat {
.message-list-box { .message-list-box {
padding-top 50px
overflow-x auto overflow-x auto
background #F5F5F5;
.van-cell {
background none
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
} }
.chat-box { .chat-box {
@ -308,6 +370,14 @@ const reGenerate = () => {
} }
} }
} }
.van-theme-dark {
.mobile-chat {
.message-list-box {
background #232425;
}
}
}
</style> </style>
<style lang="stylus"> <style lang="stylus">

View File

@ -1,5 +1,5 @@
<template> <template>
<van-config-provider :theme="theme"> <van-config-provider :theme="getMobileTheme()">
<div class="mobile-home"> <div class="mobile-home">
<router-view/> <router-view/>
@ -16,9 +16,9 @@
<script setup> <script setup>
import {ref} from "vue"; import {ref} from "vue";
import {getMobileTheme} from "@/store/system";
const active = ref('home') const active = ref('home')
const theme = ref("light")
const onChange = (index) => { const onChange = (index) => {
console.log(index) console.log(index)