提交 96d615d7 编写于 作者: DCloud_JSON's avatar DCloud_JSON

适配pc端

上级 f732c299
<script> <script>
export default { export default {
onLaunch: function() { onLaunch: function() {
console.log('App Launch') console.log('App Launch')
let version = uni.getSystemInfoSync().uniRuntimeVersion
function toNum(a){
const c = a.toString().split('.');
const num_place = ["", "0", "00", "000", "0000"],
r = num_place.reverse();
for (let i = 0; i < c.length; i++){
const len=c[i].length;
c[i]=r[len]+c[i];
}
return c.join('');
}
if(toNum(version) < toNum("3.8.0")){
uni.showModal({
content: '本示例的HBuilderX版本不得低于3.8.0,请升级',
showCancel: false
});
}
}, },
onShow: function() { onShow: function() {
console.log('App Show') console.log('App Show')
...@@ -12,5 +29,17 @@ ...@@ -12,5 +29,17 @@
} }
</script> </script>
<style> <style lang="scss">
/*每个页面公共css */
/* #ifdef H5 */
@media screen and (min-width:650px){
/* pc宽屏 隐藏会话页面头部 && 全局底部导航 以下兼容了Vue2和3两种模式的样式*/
uni-page[data-page="pages/chat/chat"] uni-page-head,
{
display: none !important;
background-color: red !important;
}
/* #endif */
}
</style> </style>
...@@ -68,10 +68,10 @@ ...@@ -68,10 +68,10 @@
"uniStatistics" : { "uniStatistics" : {
"enable" : false "enable" : false
}, },
"vueVersion" : "2", "vueVersion" : "3",
"h5" : { "h5" : {
"unipush" : { "unipush" : {
"enable" : true "enable" : false
} }
} }
} }
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"path" : "pages/chat/chat", "path" : "pages/chat/chat",
"style" : "style" :
{ {
"navigationBarTitleText": "uni-ai", "navigationBarTitleText": "uni-ai-chat",
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
} }
......
<template> <template>
<view class="page"> <view class="page">
<view class="container"> <view class="container">
<view v-if="isWidescreen" class="header">uni-im-chat</view>
<text class="noData" v-if="msgList.length === 0">没有对话记录</text> <text class="noData" v-if="msgList.length === 0">没有对话记录</text>
<scroll-view :scroll-into-view="scrollIntoView" scroll-y="true" class="msg-list" :enable-flex="true"> <scroll-view :scroll-into-view="scrollIntoView" scroll-y="true" class="msg-list" :enable-flex="true">
<view v-for="(msg,index) in msgList" class="msg-item" :key="index"> <view v-for="(msg,index) in msgList" class="msg-item" :key="index">
<uni-dateformat class="create_time" :date="msg.create_time" format="MM/dd hh:mm:ss"></uni-dateformat> <view class="create_time-box">
<uni-dateformat class="create_time" :date="msg.create_time" format="MM/dd hh:mm:ss"></uni-dateformat>
</view>
<view :class="{reverse:!msg.isAi}"> <view :class="{reverse:!msg.isAi}">
<view class="userInfo"> <view class="userInfo">
<image class="avatar" :src="msg.isAi?'../../static/uni-ai.png':'../../static/avatar.png'" mode="widthFix"></image> <image class="avatar" :src="msg.isAi?'../../static/uni-ai.png':'../../static/avatar.png'" mode="widthFix"></image>
</view> </view>
<view style="flex-direction: column;"> <view class="content">
<view class="content"> <uni-ai-msg :md="msg.content" :show-cursor="index == msgList.length-1 && msg.isAi && sseIndex"></uni-ai-msg>
<uni-ai-msg :md="msg.content" :show-cursor="index == msgList.length-1 && msg.isAi && sseIndex"></uni-ai-msg>
<!-- <text :selectable="true">{{msg.content}}</text> -->
</view>
</view> </view>
<uni-icons v-if="index == msgList.length-1 && !msg.isAi && msg.state != 100 && msgStateIcon(msg)" <uni-icons v-if="index == msgList.length-1 && !msg.isAi && msg.state != 100 && msgStateIcon(msg)"
@click="msg.state == -100 ? retriesSendMsg() : ''" :color="msg.state===0?'#999':'#d22'" @click="msg.state == -100 ? retriesSendMsg() : ''" :color="msg.state===0?'#999':'#d22'"
...@@ -24,23 +24,45 @@ ...@@ -24,23 +24,45 @@
<view id="last-msg-item"></view> <view id="last-msg-item"></view>
</scroll-view> </scroll-view>
<view class="foot-box"> <view class="foot-box">
<view class="trash"> <view class="menu" v-if="isWidescreen">
<uni-icons @click="clear" type="trash" size="24" color="#888"></uni-icons> <view class="trash menu-item">
</view> <image @click="clear" src="@/static/remove.png" mode="heightFix"></image>
<view class="textarea-box"> </view>
<textarea v-model="content" class="textarea" :auto-height="true" :disabled="inputBoxDisabled" <view class="set-stream menu-item">
:placeholder="placeholderText" :maxlength="-1" placeholder-class="input-placeholder"></textarea> <view class="title">
<text>流式响应</text>
<uni-icons @click="toStreamMD" class="help" type="help"></uni-icons>
<text>:</text>
</view>
<switch :checked="stream" @change="changeStream" />
</view>
</view> </view>
<view class="send-btn-box">
<button @click="beforeSendMsg" :disabled="inputBoxDisabled || !content" class="send">发送</button> <view class="foot-box-content">
<view v-if="!isWidescreen" class="trash">
<uni-icons @click="clear" type="trash" size="24" color="#888"></uni-icons>
</view>
<view class="textarea-box">
<textarea v-model="content" class="textarea" :auto-height="!isWidescreen" :disabled="inputBoxDisabled"
:placeholder="placeholderText" :maxlength="-1" placeholder-class="input-placeholder"></textarea>
</view>
<view class="send-btn-box">
<text v-if="isWidescreen" class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text>
<button @click="beforeSendMsg" :disabled="inputBoxDisabled || !content" class="send" type="primary">发送</button>
</view>
</view> </view>
</view>
<view v-if="!isWidescreen" id="set-stream">
<view class="title">
<text>流式响应</text>
<uni-icons @click="toStreamMD" class="help" type="help"></uni-icons>
<text>:</text>
</view>
<switch :checked="stream" @change="changeStream" />
</view> </view>
</view> </view>
<label class="set-stream">
<text style="font-size: 12px;">流式响应:</text>
<switch style="transform: scale(0.7);" :checked="stream" @change="changeStream" />
</label>
</view> </view>
</template> </template>
...@@ -52,7 +74,8 @@ ...@@ -52,7 +74,8 @@
msgList: [], msgList: [],
content: "", content: "",
sseIndex: 0, sseIndex: 0,
stream:false stream:false,
isWidescreen:false
} }
}, },
computed: { computed: {
...@@ -113,7 +136,7 @@ ...@@ -113,7 +136,7 @@
//按下了shift ctrl alt windows键 //按下了shift ctrl alt windows键
adjunctKeydown = true; adjunctKeydown = true;
} }
if (e.keyCode == 13 && adjunctKeydown) { if (e.keyCode == 13 && !adjunctKeydown) {
this.beforeSendMsg(); this.beforeSendMsg();
} }
}; };
...@@ -124,13 +147,59 @@ ...@@ -124,13 +147,59 @@
} }
}; };
} }
// #endif // #endif
// 添加惰性函数,检查是否开通push
this.changeStream.check = async ()=>{
uni.getPushClientId({
fail:()=> {
this.stream = false
uni.showModal({
content: '你暂未开通uni-push。不支持此功能',
showCancel: false,
confirmText:"查看详情",
complete() {
let url = "https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9"
// #ifndef H5
uni.setClipboardData({
data:url,
showToast:false,
success() {
uni.showToast({
title: '已复制文档链接,请到浏览器粘贴浏览',
icon: 'none',
duration:5000
});
}
})
// #endif
// #ifdef H5
window.open(url)
// #endif
}
});
console.log('你暂未开通uni-push。不支持此功能。详情:https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9');
},
success:()=>{
this.changeStream.check = ()=>{}
}
})
}
uni.createMediaQueryObserver(this).observe({
minWidth: 650,
}, matches => {
this.isWidescreen = matches;
})
}, },
methods: { methods: {
// updateLastMsg(){ // updateLastMsg(){
// }, // },
changeStream(e){ changeStream(e){
this.changeStream.check()
// console.log('e',e.detail.value); // console.log('e',e.detail.value);
this.stream = e.detail.value this.stream = e.detail.value
}, },
...@@ -302,6 +371,25 @@ ...@@ -302,6 +371,25 @@
} }
}); });
},
toStreamMD(){
let url = "https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion-stream"
// #ifndef H5
uni.setClipboardData({
data:url,
showToast:false,
success() {
uni.showToast({
title: '已复制文档链接,请到浏览器粘贴浏览',
icon: 'none',
duration:5000
});
}
})
// #endif
// #ifdef H5
window.open(url)
// #endif
} }
} }
} }
...@@ -312,13 +400,10 @@ ...@@ -312,13 +400,10 @@
view, view,
textarea, textarea,
button, button,
.page, .page
/* #ifdef H5 */
.page *
/* #endif */
{ {
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
} }
/* #endif */ /* #endif */
...@@ -329,38 +414,34 @@ ...@@ -329,38 +414,34 @@
.page, .page,
.container { .container {
background-color: #efefef; background-color: #efefef;
width: 750rpx; flex: 1;
flex: 1;
/* #ifdef H5 */ /* #ifdef H5 */
height: calc(100vh - 44px); height: calc(100vh - 44px);
/* #endif */ /* #endif */
/* #ifndef H5 */
height: 100vh;
/* #endif */
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} justify-content: center;
}
/* #ifndef APP-NVUE */ /* #ifndef APP-NVUE */
.container { .container {
background-color: #FAFAFA; background-color: #FAFAFA;
}
.container,
.container * {
max-width: 550px;
} }
/* #endif */ /* #endif */
.foot-box { .foot-box {
width: 750rpx; width: 750rpx;
display: flex; display: flex;
justify-content: space-around; flex-direction: column;
align-items: self-start;
padding: 10px 0px; padding: 10px 0px;
background-color: #FFF; background-color: #FFF;
}
.foot-box-content{
justify-content: space-around;
} }
.textarea-box { .textarea-box {
padding: 8px 10px; padding: 8px 10px;
background-color: #f9f9f9; background-color: #f9f9f9;
border-radius: 5px; border-radius: 5px;
...@@ -368,11 +449,11 @@ ...@@ -368,11 +449,11 @@
.textarea-box .textarea { .textarea-box .textarea {
max-height: 100px; max-height: 100px;
width: 460rpx;
font-size: 14px; font-size: 14px;
/* #ifndef APP-NVUE */ /* #ifndef APP-NVUE */
overflow: auto; overflow: auto;
/* #endif */ /* #endif */
width: 450rpx;
} }
/* #ifndef APP-NVUE */ /* #ifndef APP-NVUE */
...@@ -388,7 +469,6 @@ ...@@ -388,7 +469,6 @@
.trash, .trash,
.send { .send {
/* border: 1px solid #000; */
width: 50px; width: 50px;
height: 30px; height: 30px;
justify-content: center; justify-content: center;
...@@ -397,12 +477,11 @@ ...@@ -397,12 +477,11 @@
} }
.trash { .trash {
width: 30rpx; width: 30rpx;
margin-left: 20rpx; margin-left: 10rpx;
} }
.send { .send {
background-color: #2eaf4d;
color: #FFF; color: #FFF;
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
...@@ -463,7 +542,9 @@ ...@@ -463,7 +542,9 @@
} }
.content { .content {
max-width: 500rpx; /* #ifndef APP-NVUE */
max-width: 550rpx;
/* #endif */
background-color: #FFF; background-color: #FFF;
border-radius: 5px; border-radius: 5px;
padding: 12px 10px; padding: 12px 10px;
...@@ -507,33 +588,134 @@ ...@@ -507,33 +588,134 @@
justify-content: center; justify-content: center;
} }
.set-stream{ #set-stream{
position: fixed;
left: 0px;
top: 0;
z-index: 999; z-index: 999;
padding:0 5px; padding:0 5px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: fixed;
bottom: 50px;
right: 0;
}
#set-stream .title{
font-size: 12px;
position: relative;
left: 10rpx;
} }
#set-stream switch{
transform: scale(0.5);
}
/* #ifdef H5 */ /* #ifdef H5 */
@media screen and (min-width:600px){ @media screen and (min-width:650px){
.textarea-box{ .foot-box{
flex:1 border-top: solid 1px #dde0e2;
}
.page{
width: 100vw;
flex-direction: row;
}
.page * {
max-width: 950px;
}
.container, {
box-shadow: 0 0 5px #e0e1e7;
margin-top: 44px;
border-radius: 10px;
overflow: hidden;
}
.container .header{
height: 44px;
line-height: 44px;
border-bottom: 1px solid #F0F0F0;
width: 100vw;
justify-content: center;
font-weight: 500;
}
.content {
background-color: #f9f9f9;
}
.foot-box,
.foot-box-content,
.msg-list,
.msg-item,
// .create_time,
.noData,
.textarea-box,
.textarea,
textarea-box {
width: 100% !important;
} }
.trash,
.send-btn-box{ .create_time-box {
flex-shrink: 0; margin-top: 15px;
width: 100px;
justify-content: center; justify-content: center;
padding: 0;
margin: 0;
} }
.trash{ .create_time{
width: 60px; display: flex;
}
.textarea-box,
.textarea,
textarea,
textarea-box {
height: 120px;
}
.container,
.foot-box,
.textarea-box {
background-color: #FFF;
}
.foot-box-content{
flex-direction: column;
justify-content: center;
align-items: flex-end;
padding-bottom: 0;
}
.menu {
padding:0 10px;
}
.menu-item{
height:20px;
justify-content: center;
align-items: center;
align-content: center;
display: flex;
margin-right: 10px;
cursor: pointer;
}
.trash {
opacity: 0.8;
}
.trash image{
height: 15px;
}
.set-stream .title {
font-size: 12px;
}
.set-stream switch {
transform: scale(0.6);
position: relative;
left: -5px;
}
.textarea-box,.textarea-box *{
// border: 1px solid #000;
} }
.textarea-box .textarea {
width: 100%; .send-btn-box .send-btn-tip{
color: #919396;
margin-right: 8px;
font-size: 12px;
line-height: 28px;
} }
} }
/* #endif */ /* #endif */
......
## 简介 ## 简介
> 仅支持3.8.0及以上版本的HBuilderX
uni-ai-chat是基于[uni-ai](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html)的云端一体AI项目模板 uni-ai-chat是基于[uni-ai](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html)的云端一体AI项目模板
视频效果: 视频效果:
...@@ -6,6 +9,16 @@ uni-ai-chat是基于[uni-ai](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html) ...@@ -6,6 +9,16 @@ uni-ai-chat是基于[uni-ai](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html)
包含一个前端页面(路径:`/pages/chat/chat.vue`)和一个云对象(路径:`uniCloud/cloudfunctions/uni-ai-chat/index.obj.js`) 包含一个前端页面(路径:`/pages/chat/chat.vue`)和一个云对象(路径:`uniCloud/cloudfunctions/uni-ai-chat/index.obj.js`)
## 响应流程图
<img width="400px" src="https://web-assets.dcloud.net.cn/unidoc/zh/uni-ai-chat/20230424211201.jpg">
- 普通响应的流程较为简单,全流程基于 HTTP 网络请求。其缺陷是,当访问 AI 聊天接口时,如果生成的回复内容过大,响应时间会很长,导致前端用户需要等待很长时间才能收到结果。详情请参考:[stream的优势](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion-stream)
- 流式响应的使用需客户端先通过 `new uniCloud.SSEChannel()` 创建 SSE 通道,并获得 `channel` 值(详情请参考:[https://uniapp.dcloud.net.cn/uniCloud/sse-channel.html#create-sse-channel](https://uniapp.dcloud.net.cn/uniCloud/sse-channel.html#create-sse-channel))。在客户端向 uniCloud 云对象发起请求时,需要将该 `channel` 值作为参数一同携带;然后 uniCloud 云对象与 uni-ai 建立 流式响应[(stream)](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion-stream) 通讯,云对象监听 uni-ai 返回的分片数据,在接收到数据后再通过 sse-channel ([反序列化消息通道](https://uniapp.dcloud.net.cn/uniCloud/sse-channel.html#cloud-deserialize-channel))向客户端推送消息。
### 注意事项 @heed
- 使用`sse-channel`需要先[开通uni-push](https://uniapp.dcloud.net.cn/unipush-v2.html#%E7%AC%AC%E4%B8%80%E6%AD%A5-%E5%BC%80%E9%80%9A)
- 目前uni-push2.0不支持本地调试(后续版本会支持),需要在HBuilderX控制台,更改`连接本地云函数``连接云端云函数`
## 体验步骤 ## 体验步骤
...@@ -14,11 +27,4 @@ uni-ai-chat是基于[uni-ai](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html) ...@@ -14,11 +27,4 @@ uni-ai-chat是基于[uni-ai](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html)
3. 打开`uni-ai-chat`插件下载地址:[https://ext.dcloud.net.cn/plugin?name=uni-ai-chat](https://ext.dcloud.net.cn/plugin?name=uni-ai-chat) 3. 打开`uni-ai-chat`插件下载地址:[https://ext.dcloud.net.cn/plugin?name=uni-ai-chat](https://ext.dcloud.net.cn/plugin?name=uni-ai-chat)
4. 点击`使用HBuilderX导入示例项目` 4. 点击`使用HBuilderX导入示例项目`
5. 对项目根目录uniCloud点右键选择“云服务空间初始化向导”界面按提示部署项目 5. 对项目根目录uniCloud点右键选择“云服务空间初始化向导”界面按提示部署项目
6. 在uni-app项目点右键,关联之前创建的服务空间。 6. 在uni-app项目点右键,关联之前创建的服务空间。
\ No newline at end of file
## 注意事项
uni-ai-chat支持[流式响应](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#%E6%B5%81%E5%BC%8F%E5%93%8D%E5%BA%94-chat-completion-stream)
模式,该模式会接收uni-ai的流式响应的数据,通过sse-channel[(云函数(云对象)请求中的中间状态通知通道)](https://uniapp.dcloud.net.cn/uniCloud/sse-channel.html)向客户端推送消息,
而使用`sse-channel`需要先[开通uni-push](https://uniapp.dcloud.net.cn/unipush-v2.html#%E7%AC%AC%E4%B8%80%E6%AD%A5-%E5%BC%80%E9%80%9A)
,目前uni-push2.0不支持本地调试(后续版本会支持),需要在HBuilderX控制台,更改`连接本地云函数``连接云端云函数`
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册