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/utils"
"chatplus/utils/resp"
"strings"
"github.com/gin-contrib/sessions"
@ -88,7 +89,7 @@ func (h *ManagerHandler) Migrate(c *gin.Context) {
continue
}
for k, _ := range m {
for k := range m {
roleKeys = append(roleKeys, k)
}
u.ChatRoles = utils.JsonEncode(roleKeys)
@ -101,8 +102,10 @@ func (h *ManagerHandler) Migrate(c *gin.Context) {
var roles []model.ChatRole
h.db.Find(&roles)
for _, r := range roles {
r.Icon = "/" + r.Icon
h.db.Updates(&r)
if !strings.HasPrefix(r.Icon, "/") {
r.Icon = "/" + r.Icon
h.db.Updates(&r)
}
}
break
case "history":
@ -114,6 +117,18 @@ func (h *ManagerHandler) Migrate(c *gin.Context) {
h.db.Updates(&r)
}
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")

View File

@ -52,9 +52,19 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
session := h.App.ChatSession.Get(sessionId)
if session.SessionId == "" {
logger.Info("用户未登录")
c.Abort()
return
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
logger.Info("用户未登录")
c.Abort()
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

View File

@ -220,7 +220,13 @@ func (h *UserHandler) Logout(c *gin.Context) {
func (h *UserHandler) Session(c *gin.Context) {
user, err := utils.GetLoginUser(c, h.db)
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 {
resp.NotAuth(c)
}

View File

@ -1,46 +1,57 @@
<template>
<div class="message-reply">
<div class="mobile-message-prompt">
<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>
<div class="chat-icon">
<img :src="icon" alt="User"/>
<van-image :src="icon"/>
</div>
</div>
</template>
<script>
import {defineComponent} from "vue"
<script setup>
import {onMounted, ref} from "vue";
import Clipboard from "clipboard";
import {showNotify} from "vant";
export default defineComponent({
name: 'ChatPrompt',
props: {
content: {
type: String,
default: '',
},
icon: {
type: String,
default: 'images/user-icon.png',
}
},
data() {
return {}
const props = defineProps({
content: {
type: String,
default: '',
},
icon: {
type: String,
default: '/images/user-icon.png',
}
});
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>
<style lang="stylus">
.message-reply {
justify-content: flex-end;
.mobile-message-prompt {
display flex
justify-content: flex-end
.chat-icon {
margin-left 5px;
margin-left 5px
img {
border-radius 5px;
.van-image {
width 25px
img {
border-radius 5px
}
}
}
@ -64,11 +75,27 @@ export default defineComponent({
word-break break-word;
padding: 6px 10px;
background-color: #98E165;
color var(--content-color);
font-size: var(--content-font-size);
border-radius: 5px;
color #444444
font-size: 16px
border-radius: 5px
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>

View File

@ -1,74 +1,66 @@
<template>
<div class="message-prompt">
<div class="mobile-message-reply">
<div class="chat-icon">
<img :src="icon" alt="ChatGPT">
<van-image :src="icon"/>
</div>
<div class="chat-item">
<div class="triangle"></div>
<div class="content-box">
<div 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 ref="contentRef" :data-clipboard-text="orgContent" class="content" v-html="content"></div>
</div>
</div>
</div>
</template>
<script>
import {defineComponent} from "vue"
import {randString} from "@/utils/libs";
import {DocumentCopy} from "@element-plus/icons-vue";
<script setup>
import {onMounted, ref} from "vue"
export default defineComponent({
name: 'ChatReply',
components: {DocumentCopy},
props: {
content: {
type: String,
default: '',
},
orgContent: {
type: String,
default: '',
},
icon: {
type: String,
default: 'images/gpt-icon.png',
}
},
data() {
return {
id: randString(32),
clipboard: null,
}
},
import Clipboard from "clipboard";
import {showNotify} from "vant";
const props = defineProps({
content: {
type: String,
default: '',
},
orgContent: {
type: String,
default: '',
},
icon: {
type: String,
default: '/images/gpt-icon.png',
}
});
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>
<style lang="stylus">
.message-prompt {
.mobile-message-reply {
display flex
justify-content: flex-start;
.chat-icon {
margin-right 5px;
margin-right 5px
img {
border-radius 5px;
.van-image {
width 25px
img {
border-radius 5px
}
}
}
@ -95,12 +87,15 @@ export default defineComponent({
flex-direction row
.content {
text-align left
width 100%
overflow-x auto
min-height 20px;
word-break break-word;
padding: 6px 10px;
color var(--content-color)
background-color: #fff;
font-size: var(--content-font-size);
color #444444
background-color: #ffffff;
font-size: 16px
border-radius: 5px;
p:last-child {
@ -112,18 +107,10 @@ export default defineComponent({
}
p > code {
color #cc0000
background-color #f1f1f1
}
}
.tool-box {
padding-left 10px;
font-size 16px;
.el-button {
height 20px
padding 5px 2px;
color #2b2b2b
background-color #c1c1c1
padding 2px 5px
border-radius 5px
}
}
}
@ -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>

View File

@ -22,6 +22,7 @@ import {
Picker,
Popup,
Search,
ShareSheet,
Sticky,
SwipeCell,
Tabbar,
@ -54,6 +55,7 @@ app.use(DropdownItem);
app.use(Sticky);
app.use(SwipeCell);
app.use(Dialog);
app.use(ShareSheet);
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 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">
<el-image :src="user['avatar']"/>
<span class="username">{{ user ? user['nickname'] : 'Chat-Plus-User' }}</span>
<el-image :src="loginUser.avatar"/>
<span class="username">{{ loginUser.nickname }}</span>
<el-icon><ArrowDown/></el-icon>
</span>
<template #dropdown>
@ -212,7 +212,7 @@ import 'highlight.js/styles/a11y-dark.css'
import {dateFormat, randString, removeArrayItem, renderInputText, UUID} from "@/utils/libs";
import {ElMessage, ElMessageBox} from "element-plus";
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 {useRouter} from "vue-router";
import Clipboard from "clipboard";
@ -232,7 +232,7 @@ const mainWinHeight = ref(0); // 主窗口高度
const chatBoxHeight = ref(0); //
const leftBoxHeight = ref(0);
const loading = ref(true);
const user = getLoginUser();
const loginUser = ref(null);
const roles = ref([]);
const roleId = ref(0)
const newChatItem = ref(null);
@ -243,7 +243,8 @@ const isLogin = ref(false)
onMounted(() => {
resizeElement();
checkSession().then(() => {
checkSession().then((user) => {
loginUser.value = user
isLogin.value = true
//
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
@ -267,7 +268,7 @@ onMounted(() => {
})
}).catch((e) => {
console.log(e)
//router.push('login')
router.push('login')
});
const clipboard = new Clipboard('.copy-reply');
@ -282,7 +283,7 @@ onMounted(() => {
//
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) {
chatList.value = res.data;
allChats.value = res.data;
@ -570,7 +571,7 @@ const sendMessage = function () {
chatData.value.push({
type: "prompt",
id: randString(32),
icon: user.avatar,
icon: loginUser.value.avatar,
content: renderInputText(prompt.value),
created_at: new Date().getTime(),
});
@ -707,8 +708,8 @@ const searchChat = function () {
}
const updateUser = function (data) {
user.avatar = data.avatar;
user.nickname = data.nickname;
loginUser.value.avatar = data.avatar;
loginUser.value.nickname = data.nickname;
}
</script>

View File

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

View File

@ -1,86 +1,97 @@
<template>
<div class="mobile-chat">
<van-sticky :offset-top="0" position="top" ref="navBarRef">
<van-nav-bar left-text="返回" left-arrow @click-left="router.back()">
<template #title>
<van-dropdown-menu>
<van-dropdown-item :title="title">
<van-cell center title="角色"> {{ role.name }}</van-cell>
<van-cell center title="模型">{{ model }}</van-cell>
</van-dropdown-item>
</van-dropdown-menu>
</template>
<van-config-provider :theme="getMobileTheme()">
<div class="mobile-chat">
<van-sticky ref="navBarRef" :offset-top="0" position="top">
<van-nav-bar left-arrow left-text="返回" @click-left="router.back()">
<template #title>
<van-dropdown-menu>
<van-dropdown-item :title="title">
<van-cell center title="角色"> {{ role.name }}</van-cell>
<van-cell center title="模型">{{ model }}</van-cell>
</van-dropdown-item>
</van-dropdown-menu>
</template>
<template #right>
<van-icon name="delete-o" @click="clearChatHistory"/>
</template>
</van-nav-bar>
</van-sticky>
<template #right>
<van-icon name="share-o" @click="showShare = true"/>
</template>
</van-nav-bar>
</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'}">
<van-list
v-model:loading="loading"
:finished="finished"
v-model:error="error"
error-text="请求失败,点击重新加载"
@load="onLoad"
>
<van-cell v-for="item in chatData" :key="item">
<chat-prompt
v-if="item.type==='prompt'"
:icon="item.icon"
:created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']"
:model="model"
:content="item.content"/>
<chat-reply v-else-if="item.type==='reply'"
:icon="item.icon"
:org-content="item.orgContent"
:created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']"
:content="item.content"/>
</van-cell>
</van-list>
</div>
<van-sticky :offset-bottom="0" position="bottom" ref="bottomBarRef">
<div class="chat-box">
<van-cell-group>
<van-field
v-model="prompt"
center
clearable
placeholder="输入你的问题"
>
<template #button>
<van-button size="small" type="primary" @click="sendMessage">发送</van-button>
</template>
<template #extra>
<div class="icon-box">
<van-icon v-if="showStopGenerate" name="stop-circle-o" @click="stopGenerate"/>
<van-icon v-if="showReGenerate" name="play-circle-o" @click="reGenerate"/>
</div>
</template>
</van-field>
</van-cell-group>
<div id="message-list-box" :style="{height: winHeight+'px'}" class="message-list-box">
<van-list
v-model:error="error"
v-model:loading="loading"
:finished="finished"
error-text="请求失败,点击重新加载"
@load="onLoad"
>
<van-cell v-for="item in chatData" :key="item" :border="false" class="message-line">
<chat-prompt
v-if="item.type==='prompt'"
:content="item.content"
:created-at="dateFormat(item['created_at'])"
:icon="item.icon"
:model="model"
:tokens="item['tokens']"/>
<chat-reply v-else-if="item.type==='reply'"
:content="item.content"
:created-at="dateFormat(item['created_at'])"
:icon="item.icon"
:org-content="item.orgContent"
:tokens="item['tokens']"/>
</van-cell>
</van-list>
</div>
</van-sticky>
</div>
<van-sticky ref="bottomBarRef" :offset-bottom="0" position="bottom">
<div class="chat-box">
<van-cell-group>
<van-field
v-model="prompt"
center
clearable
placeholder="输入你的问题"
>
<template #button>
<van-button size="small" type="primary" @click="sendMessage">发送</van-button>
</template>
<template #extra>
<div class="icon-box">
<van-icon v-if="showStopGenerate" name="stop-circle-o" @click="stopGenerate"/>
<van-icon v-if="showReGenerate" name="play-circle-o" @click="reGenerate"/>
</div>
</template>
</van-field>
</van-cell-group>
</div>
</van-sticky>
</div>
</van-config-provider>
</template>
<script setup>
import {nextTick, onMounted, ref} from "vue";
import {showToast} from "vant";
import {showToast, showDialog} from "vant";
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 {httpGet} from "@/utils/http";
import hl from "highlight.js";
import 'highlight.js/styles/a11y-dark.css'
import ChatPrompt from "@/components/mobile/ChatPrompt.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 navBarRef = ref(null)
@ -92,6 +103,7 @@ const role = chatConfig.role
const model = chatConfig.model
const title = chatConfig.title
const chatId = chatConfig.chatId
const loginUser = ref(null)
onMounted(() => {
winHeight.value = document.body.offsetHeight - navBarRef.value.$el.offsetHeight - bottomBarRef.value.$el.offsetHeight
@ -101,35 +113,44 @@ const chatData = ref([])
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
checkSession().then(user => {
loginUser.value = user
}).catch(() => {
router.push('/login')
})
const onLoad = () => {
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
//
loading.value = false;
finished.value = true;
const data = res.data
if (!data || data.length === 0) {
return
}
if (data && data.length > 0) {
const md = require('markdown-it')();
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
chatData.value.push(data[i]);
continue;
}
const md = require('markdown-it')();
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
data[i].orgContent = data[i].content;
data[i].content = md.render(data[i].content);
chatData.value.push(data[i]);
continue;
}
data[i].orgContent = data[i].content;
data[i].content = md.render(data[i].content);
chatData.value.push(data[i]);
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#message-list-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
scrollListBox()
})
}
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#message-list-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
//
connect(chatId, role.id);
}).catch(() => {
error.value = true
})
@ -157,7 +178,6 @@ const connect = function (chat_id, role_id) {
socket.value.close();
}
const _role = getRoleById(role_id);
// WebSocket
const _sessionId = getSessionId();
let host = process.env.VUE_APP_WS_HOST
@ -168,9 +188,8 @@ const connect = function (chat_id, role_id) {
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', () => {
chatData.value = []; //
previousText.value = '';
canSend.value = true;
activelyClose.value = false;
@ -180,12 +199,10 @@ const connect = function (chat_id, role_id) {
chatData.value.push({
type: "reply",
id: randString(32),
icon: _role['icon'],
content: _role['hello_msg'],
orgContent: _role['hello_msg'],
icon: role.icon,
content: role.helloMsg,
orgContent: role.helloMsg,
})
} else { //
loadChatHistory(chat_id);
}
});
@ -199,7 +216,7 @@ const connect = function (chat_id, role_id) {
chatData.value.push({
type: "reply",
id: randString(32),
icon: _role['icon'],
icon: role.icon,
content: ""
});
} else if (data.type === 'end') { //
@ -208,26 +225,6 @@ const connect = function (chat_id, role_id) {
showStopGenerate.value = false;
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 {
lineBuffer.value += data.content;
let md = require('markdown-it')();
@ -237,16 +234,16 @@ const connect = function (chat_id, role_id) {
nextTick(() => {
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');
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
}
//
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', () => {
console.log(activelyClose.value)
if (activelyClose.value) { //
return;
}
//
canSend.value = true;
socket.value = null;
loading.value = true;
checkSession().then(() => {
connect(chat_id, role_id)
}).catch(() => {
ElMessageBox({
showDialog({
title: '会话提示',
message: "当前会话已经失效,请重新登录",
confirmButtonText: 'OK',
callback: () => router.push('login')
message: '当前会话已经失效,请重新登录!',
}).then(() => {
router.push('/login')
});
});
});
@ -276,27 +273,92 @@ const connect = function (chat_id, role_id) {
socket.value = _socket;
}
const clearChatHistory = () => {
showToast('清空聊记录')
//
const scrollListBox = () => {
document.getElementById('message-list-box').scrollTo(0, document.getElementById('message-list-box').scrollHeight)
}
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 = () => {
showToast("停止生成")
showStopGenerate.value = false;
httpGet("/api/chat/stop?session_id=" + getSessionId()).then(() => {
canSend.value = true;
if (previousText.value !== '') {
showReGenerate.value = true;
}
})
}
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>
<style scoped lang="stylus">
<style lang="stylus" scoped>
.mobile-chat {
.message-list-box {
padding-top 50px
overflow-x auto
background #F5F5F5;
.van-cell {
background none
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
}
.chat-box {
@ -308,6 +370,14 @@ const reGenerate = () => {
}
}
}
.van-theme-dark {
.mobile-chat {
.message-list-box {
background #232425;
}
}
}
</style>
<style lang="stylus">

View File

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