feat: 剩余切换模型问题
This commit is contained in:
parent
550a35effc
commit
af586485f0
BIN
AIchat.rar
BIN
AIchat.rar
Binary file not shown.
@ -40,22 +40,6 @@
|
|||||||
"navigationBarTitleText": "关于"
|
"navigationBarTitleText": "关于"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/index/index copy",
|
|
||||||
"type": "page",
|
|
||||||
"layout": "default",
|
|
||||||
"style": {
|
|
||||||
"navigationBarHidden": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/index/index1",
|
|
||||||
"type": "page",
|
|
||||||
"layout": "default",
|
|
||||||
"style": {
|
|
||||||
"navigationBarHidden": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/preview/index",
|
"path": "pages/preview/index",
|
||||||
"type": "page"
|
"type": "page"
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -146,7 +146,7 @@
|
|||||||
@click="previewFile(file.url)"
|
@click="previewFile(file.url)"
|
||||||
>
|
>
|
||||||
<view>{{ file.name }}</view>
|
<view>{{ file.name }}</view>
|
||||||
<view>{{ file.size }}</view>
|
<view>{{ calcFileSize(file.size) }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@ -191,6 +191,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 聊天信息区域 -->
|
||||||
<!-- <view class="" style="height: 72px"></view> -->
|
<!-- <view class="" style="height: 72px"></view> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -225,13 +226,47 @@
|
|||||||
class="relative w-16 h-16 rounded overflow-hidden flex-shrink-0"
|
class="relative w-16 h-16 rounded overflow-hidden flex-shrink-0"
|
||||||
>
|
>
|
||||||
<!-- 预览图,成功后用后端返回的 URL;上传中可以先用本地预览 -->
|
<!-- 预览图,成功后用后端返回的 URL;上传中可以先用本地预览 -->
|
||||||
<img
|
<!-- <img
|
||||||
|
v-if="item.uploadFileType !== uploadFileTypeEm.file"
|
||||||
|
src="/static/aichat/video-icon.png"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
@click="handleImageClick(item.url)"
|
||||||
|
/> {{item.uploadFileType}}-{{ uploadFileTypeEm.file}}-->
|
||||||
|
|
||||||
|
<!-- <img
|
||||||
v-if="item.uploadFileType !== uploadFileTypeEm.file"
|
v-if="item.uploadFileType !== uploadFileTypeEm.file"
|
||||||
:src="item.url || item.tempFilePath"
|
:src="item.url || '/static/aichat/loading.png'"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
@click="handleImageClick(item.url)"
|
||||||
|
/> isFileType-->
|
||||||
|
<!-- 视频类文件 -->
|
||||||
|
<img
|
||||||
|
v-if="videoFileType.includes(isFileType(item.name))"
|
||||||
|
src="/static/aichat/ic_video.png"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
@click="handleImageClick(item.ori_url)"
|
||||||
|
/>
|
||||||
|
<!-- office文档类文件 -->
|
||||||
|
<img
|
||||||
|
v-else-if="officeFileTypeList.includes(isFileType(item.name))"
|
||||||
|
src="/static/aichat/word.png"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
@click="handleImageClick(item.url)"
|
||||||
|
/>
|
||||||
|
<!-- 图片 -->
|
||||||
|
<img
|
||||||
|
v-else-if="picFileType.includes(isFileType(item.name))"
|
||||||
|
:src="item.tempFilePath"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
@click="handleImageClick(item.url)"
|
||||||
|
/>
|
||||||
|
<!-- 其他允许上传的文件 -->
|
||||||
|
<img
|
||||||
|
v-else-if="otherFileType.includes(isFileType(item.name))"
|
||||||
|
src="/static/aichat/txt.png"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
@click="handleImageClick(item.url)"
|
@click="handleImageClick(item.url)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<view v-else class="text-xs text-gray-400 mt-1" @click="previewFile(item.url)">
|
<view v-else class="text-xs text-gray-400 mt-1" @click="previewFile(item.url)">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</view>
|
</view>
|
||||||
@ -265,7 +300,9 @@
|
|||||||
<template v-if="item.status === 'uploading'">
|
<template v-if="item.status === 'uploading'">
|
||||||
<view class="text-white">{{ item.progress }}%</view>
|
<view class="text-white">{{ item.progress }}%</view>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="item.status === 'success'">✔ 成功</template>
|
<template v-else-if="item.status === 'success'">
|
||||||
|
✔ 成功{{ isFileType(item.name) }}
|
||||||
|
</template>
|
||||||
<template v-else>✖ 失败</template>
|
<template v-else>✖ 失败</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -295,6 +332,7 @@
|
|||||||
>
|
>
|
||||||
知识库
|
知识库
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <button @click="reload()">refresh</button> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入 + 切换 -->
|
<!-- 输入 + 切换 -->
|
||||||
@ -302,10 +340,11 @@
|
|||||||
<input
|
<input
|
||||||
v-model="inputText"
|
v-model="inputText"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@keyup.enter="sendText"
|
@confirm="sendText"
|
||||||
placeholder="想对我说点什么~"
|
placeholder="想对我说点什么~"
|
||||||
class="flex-1 h-10 px-3 border border-gray-100 bg-[#f9f9f9] rounded-1 focus:outline-none"
|
class="flex-1 h-10 px-3 border border-gray-100 bg-[#f9f9f9] rounded-1 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
|
<!-- 将keyup替换为confirm -->
|
||||||
<image
|
<image
|
||||||
v-show="!knowledgeOpen"
|
v-show="!knowledgeOpen"
|
||||||
src="/static/aichat/add-circle.png"
|
src="/static/aichat/add-circle.png"
|
||||||
@ -319,7 +358,7 @@
|
|||||||
<image
|
<image
|
||||||
src="/static/aichat/enter.png"
|
src="/static/aichat/enter.png"
|
||||||
class="w-7 h-7"
|
class="w-7 h-7"
|
||||||
@click="sendText('')"
|
@click="sendText()"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
:class="[knowledgeOpen ? 'ml-2' : 'ml-0']"
|
:class="[knowledgeOpen ? 'ml-2' : 'ml-0']"
|
||||||
/>
|
/>
|
||||||
@ -396,13 +435,21 @@
|
|||||||
class="fixed inset-0 bg-black bg-opacity-90 z-999 flex justify-center items-start pt-10"
|
class="fixed inset-0 bg-black bg-opacity-90 z-999 flex justify-center items-start pt-10"
|
||||||
@click="closePreview"
|
@click="closePreview"
|
||||||
>
|
>
|
||||||
<view class="relative w-full max-w-full mt-8 p-5 box-border">
|
<view
|
||||||
|
class="absolute w-full max-w-full h-full mt-8 p-5 box-border flex justify-center items-start"
|
||||||
|
>
|
||||||
<image
|
<image
|
||||||
|
v-if="previewUrl"
|
||||||
class="w-full max-h-[calc(100vh-90px)] object-contain"
|
class="w-full max-h-[calc(100vh-90px)] object-contain"
|
||||||
:src="previewUrl"
|
:src="previewUrl"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
|
<video
|
||||||
|
class="w-full max-h-[calc(100vh-90px)] object-contain"
|
||||||
|
v-else-if="previewVideoUrl"
|
||||||
|
:src="previewVideoUrl"
|
||||||
|
></video>
|
||||||
<view
|
<view
|
||||||
class="absolute top--8 right-5 text-white text-3xl z-1000 w-10 h-10 text-center leading-10"
|
class="absolute top--8 right-5 text-white text-3xl z-1000 w-10 h-10 text-center leading-10"
|
||||||
@click.stop="closePreview"
|
@click.stop="closePreview"
|
||||||
@ -421,7 +468,11 @@ import { useUserStore } from '@/store'
|
|||||||
import { getEnvBaseUrl } from '@/utils'
|
import { getEnvBaseUrl } from '@/utils'
|
||||||
import guid from '@/utils/guid.js'
|
import guid from '@/utils/guid.js'
|
||||||
import type { IGptRequestBody } from '@/service/index/foo'
|
import type { IGptRequestBody } from '@/service/index/foo'
|
||||||
|
import { calcFileSize, formatParams } from './utils/index'
|
||||||
import 'dayjs/locale/zh-cn'
|
import 'dayjs/locale/zh-cn'
|
||||||
|
import { showToastErr, showToastOk } from '@/utils/tools'
|
||||||
|
// import { sendMsg } from "@/utils/api.js"
|
||||||
|
import { TOKEN, AVATAR } from './utils/test'
|
||||||
dayjs.locale('zh-cn')
|
dayjs.locale('zh-cn')
|
||||||
|
|
||||||
interface UploadFile {
|
interface UploadFile {
|
||||||
@ -436,6 +487,7 @@ interface UploadFile {
|
|||||||
url?: string // 本地预览URL或服务器返回的URL
|
url?: string // 本地预览URL或服务器返回的URL
|
||||||
uploadFileType: 'image' | 'video' | 'file'
|
uploadFileType: 'image' | 'video' | 'file'
|
||||||
ori_url?: string // 本地视频
|
ori_url?: string // 本地视频
|
||||||
|
suffix: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMessage {
|
interface IMessage {
|
||||||
@ -444,11 +496,11 @@ interface IMessage {
|
|||||||
content: string | any[]
|
content: string | any[]
|
||||||
timestamp: Date
|
timestamp: Date
|
||||||
}
|
}
|
||||||
const userAvatar = ref('')
|
const userAvatar = ref(AVATAR)
|
||||||
const chatMode = ref('qwen-vl-plus')
|
const chatMode = ref('qwen-vl-plus')
|
||||||
|
|
||||||
const baseUrl = getEnvBaseUrl()
|
const baseUrl = getEnvBaseUrl()
|
||||||
const messages = reactive<IMessage[]>([])
|
const messages = reactive([])
|
||||||
//获取用户上下文
|
//获取用户上下文
|
||||||
const historyUserMsgs = reactive<any[]>([])
|
const historyUserMsgs = reactive<any[]>([])
|
||||||
const inputText = ref('')
|
const inputText = ref('')
|
||||||
@ -464,7 +516,7 @@ const uploadQueue = reactive([])
|
|||||||
// 上传中文件数量
|
// 上传中文件数量
|
||||||
const uploadingCount = ref(0)
|
const uploadingCount = ref(0)
|
||||||
// 最大并行上传数
|
// 最大并行上传数
|
||||||
const MAX_CONCURRENT_UPLOADS = 6
|
const MAX_CONCURRENT_UPLOADS = 5 // 6
|
||||||
|
|
||||||
// ------------------
|
// ------------------
|
||||||
// 控制弹窗显隐 & 全屏/底部模式
|
// 控制弹窗显隐 & 全屏/底部模式
|
||||||
@ -472,6 +524,10 @@ const MAX_CONCURRENT_UPLOADS = 6
|
|||||||
const showPopup = ref(false)
|
const showPopup = ref(false)
|
||||||
const fullscreen = ref(false)
|
const fullscreen = ref(false)
|
||||||
|
|
||||||
|
const officeFileTypeList = ref(['.docx', '.doc', '.excel', '.pdf'])
|
||||||
|
const otherFileType = ref(['.txt'])
|
||||||
|
const videoFileType = ref(['.mp4', '.mov', '.wmv'])
|
||||||
|
const picFileType = ref(['.jpg', '.png', '.jpeg'])
|
||||||
async function openPopup() {
|
async function openPopup() {
|
||||||
await fetchHistoryList()
|
await fetchHistoryList()
|
||||||
showPopup.value = true
|
showPopup.value = true
|
||||||
@ -553,7 +609,7 @@ async function fetchHistoryList() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 9990,
|
pageSize: 10,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
Authorization: token.value,
|
Authorization: token.value,
|
||||||
@ -715,9 +771,7 @@ function tryParseMapFormat(str: string): ParsedPart[] | null {
|
|||||||
return result.length > 0 ? result : null
|
return result.length > 0 ? result : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = ref<string>(
|
const token = ref<string>(TOKEN)
|
||||||
'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941ca1430937103230a1e32a1715f569f3efdbe6f8cb8b7b8642bd679668081b9b08f693d1b5be6002d936ec51e1e3e0c4927de9e32ac99a109b326e5d2bda27ec87624bb416ec70d2a95a2e190feeba9f0d6bae8571b3dfe89c824712344759a8f2bff9d70747c52525cf6a5614f9c770bca461a9b9c247b6dca97bcf83bbaf99bb726752c4fe1e9a4aa7de5c4cf3e88a3e480801280d45cdc124f9d8221105d852945dc6ce10bc1647e4f09dff4d52ffdfc54709466166877e6ec300eecfc80f0c545e4240ab11ec248a32ee67129097b0b7abb383a5e398056f06c98cd65f5944054e10bdad0f7659f6c036b2010f30f74ff2a3fb7bd70a7cd3640a6f393b36a17',
|
|
||||||
)
|
|
||||||
const userInfo = ref<any>({})
|
const userInfo = ref<any>({})
|
||||||
const refreshToken = ref<string>('')
|
const refreshToken = ref<string>('')
|
||||||
const statusBarHeight = ref<number>(0)
|
const statusBarHeight = ref<number>(0)
|
||||||
@ -727,7 +781,7 @@ const mask = ref('')
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 1. 定义一个 init 函数,拿 Extras 并依次调用接口
|
// 1. 定义一个 init 函数,拿 Extras 并依次调用接口
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const wv = plus.webview.currentWebview()
|
const wv = plus.webview.currentWebview() // 获取当前页面所属的 Webview 对象。
|
||||||
// token.value = wv.token || uni.getStorageSync('token') || import.meta.env.VITE_DEV_TOKEN
|
// token.value = wv.token || uni.getStorageSync('token') || import.meta.env.VITE_DEV_TOKEN
|
||||||
userInfo.value = JSON.parse(wv.userInfo) || {}
|
userInfo.value = JSON.parse(wv.userInfo) || {}
|
||||||
refreshToken.value = wv.refreshToken || uni.getStorageSync('refreshToken')
|
refreshToken.value = wv.refreshToken || uni.getStorageSync('refreshToken')
|
||||||
@ -777,7 +831,7 @@ const previewVideo = (files) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMessage(msg: IMessage) {
|
function addMessage(msg) {
|
||||||
messages.push(msg)
|
messages.push(msg)
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
@ -932,12 +986,23 @@ const uploadFile = (file: UploadFile) => {
|
|||||||
file.progress = 100
|
file.progress = 100
|
||||||
file.url = result.data.cover_url || result.data.ori_url || file.tempFilePath
|
file.url = result.data.cover_url || result.data.ori_url || file.tempFilePath
|
||||||
file.ori_url = result.data.ori_url || ''
|
file.ori_url = result.data.ori_url || ''
|
||||||
|
uni.showToast({
|
||||||
|
title: res.msg || '文件上传成功',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
file.status = 'error'
|
file.status = 'error'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
file.status = 'error'
|
file.status = 'error'
|
||||||
|
if (err.errMsg === 'uploadFile:fail timeout') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '上传文件超时',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 3000,
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
// 更新上传文件列表状态
|
// 更新上传文件列表状态
|
||||||
@ -980,10 +1045,11 @@ const uploadFile = (file: UploadFile) => {
|
|||||||
const onPickImage = () => {
|
const onPickImage = () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
uni.chooseImage({
|
uni.chooseImage({
|
||||||
count: 10, // 最多选择9张
|
count: 9, // 最多选择9张
|
||||||
sizeType: ['original', 'compressed'],
|
sizeType: ['original', 'compressed'],
|
||||||
sourceType: ['album'],
|
sourceType: ['album'],
|
||||||
extension: ['.jpeg', '.png', '.jpg'],
|
// extension: ['.rar', '.png', '.jpg'],
|
||||||
|
// extension: ['jpeg', 'png', 'jpg'],
|
||||||
success: (res: any) => {
|
success: (res: any) => {
|
||||||
console.log(res)
|
console.log(res)
|
||||||
|
|
||||||
@ -1005,7 +1071,7 @@ const onPickVideo = () => {
|
|||||||
uni.chooseVideo({
|
uni.chooseVideo({
|
||||||
sourceType: ['album', 'camera'],
|
sourceType: ['album', 'camera'],
|
||||||
compressed: true,
|
compressed: true,
|
||||||
extension: uploadConfig.video.supportType,
|
// extension: uploadConfig.video.supportType,
|
||||||
success: (res: any) => {
|
success: (res: any) => {
|
||||||
console.log(res)
|
console.log(res)
|
||||||
const tempFile = res.tempFile
|
const tempFile = res.tempFile
|
||||||
@ -1022,22 +1088,23 @@ const onPickVideo = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const fileInput = ref(null)
|
||||||
|
// extension: [
|
||||||
|
// 'txt', // 文本
|
||||||
|
// 'pdf', // PDF
|
||||||
|
// 'doc',
|
||||||
|
// 'docx', // Word
|
||||||
|
// 'xls',
|
||||||
|
// 'xlsx', // Excel
|
||||||
|
// 'ppt',
|
||||||
|
// 'pptx', // PowerPoint
|
||||||
|
// ],
|
||||||
// 文件
|
// 文件
|
||||||
const onPickFile = () => {
|
const onPickFile = () => {
|
||||||
uni.chooseFile({
|
uni.chooseFile({
|
||||||
count: 10,
|
count: 9,
|
||||||
type: 'all',
|
type: 'all',
|
||||||
extension: [
|
|
||||||
'.txt', // 文本
|
|
||||||
'.pdf', // PDF
|
|
||||||
'.doc',
|
|
||||||
'.docx', // Word
|
|
||||||
'.xls',
|
|
||||||
'.xlsx', // Excel
|
|
||||||
'.ppt',
|
|
||||||
'.pptx', // PowerPoint
|
|
||||||
],
|
|
||||||
success: (res: any) => {
|
success: (res: any) => {
|
||||||
console.log(res)
|
console.log(res)
|
||||||
// 开始上传
|
// 开始上传
|
||||||
@ -1085,6 +1152,7 @@ const previewFile = (url: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载并打开文件
|
||||||
const downloadAndOpenFile = (downloadUrl: string) => {
|
const downloadAndOpenFile = (downloadUrl: string) => {
|
||||||
uni.showLoading({ title: '加载中...', mask: true })
|
uni.showLoading({ title: '加载中...', mask: true })
|
||||||
|
|
||||||
@ -1119,130 +1187,116 @@ const downloadAndOpenFile = (downloadUrl: string) => {
|
|||||||
dtask.start()
|
dtask.start()
|
||||||
}
|
}
|
||||||
const msgLoading = ref(true)
|
const msgLoading = ref(true)
|
||||||
// 发送消息
|
interface EventTargetSendText {
|
||||||
async function sendText(msgData = '') {
|
detail: { value: string }
|
||||||
console.log(msgData)
|
}
|
||||||
|
type EventTypeTarget = string | EventTargetSendText
|
||||||
msgLoading.value = true
|
async function sendText() {
|
||||||
const text = inputText.value.trim()
|
const msg = inputText.value.trim()
|
||||||
const dataBlo = toRaw(msgData)
|
if (!msg) {
|
||||||
console.log(dataBlo)
|
return showToastErr('不可以发送空消息!')
|
||||||
if (!text && dataBlo == '') {
|
|
||||||
msgLoading.value = false
|
|
||||||
|
|
||||||
uni.showToast({ title: '请输入信息', icon: 'error' })
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (loading.value) return
|
// 开启加载状态
|
||||||
|
loading.value = true
|
||||||
|
// 文本消息
|
||||||
|
|
||||||
const tempUploadList = Object.assign([], uploadList)
|
// 先判断是否上传文件,若有文件在判断文件类型,视频+图片不能与文档类文件同时上传
|
||||||
let fileList: any[] = []
|
addMessage({
|
||||||
if (tempUploadList.length > 0) {
|
role: 'user',
|
||||||
fileList = tempUploadList.map((item: UploadFile) => {
|
type: 'text',
|
||||||
if (item.uploadFileType === uploadFileTypeEm.image) {
|
content: msg,
|
||||||
// 图片格式
|
timestamp: new Date(),
|
||||||
return {
|
})
|
||||||
type: 'image_url',
|
if (uploadList.length > 0) {
|
||||||
image_url: {
|
console.log(': 文件上传', uploadQueue)
|
||||||
url: item.url,
|
console.log(': 文件上传', uploadList)
|
||||||
},
|
let mediaList = [] // 媒体文件 参数中的content
|
||||||
}
|
let fileList = [] // 文档文件
|
||||||
} else if (item.uploadFileType === uploadFileTypeEm.video) {
|
// 格式化请求参数
|
||||||
console.log(item, '====item=====')
|
const { media, file } = formatParams(uploadList)
|
||||||
// 视频格式(改成和图片一样的结构)
|
if (media.length > 0 && file.length > 0) {
|
||||||
return {
|
// 媒体文件与文档同时上传时
|
||||||
type: 'video_url',
|
return showToastErr('视频或图片不能与文件同时上传')
|
||||||
video_url: {
|
} else if (media.length > 0 && file.length <= 0) {
|
||||||
url: item.ori_url,
|
// 仅上传了媒体文件时
|
||||||
},
|
chatMode.value = 'qwen-vl-max'
|
||||||
}
|
let aiMsg = {
|
||||||
} else {
|
text: msg,
|
||||||
// 其他文件类型保持原样
|
type: 'text',
|
||||||
const fileType = `${item.uploadFileType}_url`
|
|
||||||
return {
|
|
||||||
type: fileType,
|
|
||||||
[fileType]: item.url,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
let content = [].concat(media)
|
||||||
}
|
content.push(aiMsg)
|
||||||
|
const lastMsg = {
|
||||||
// 添加用户文本消息
|
role: 'user',
|
||||||
addMessage(
|
content: content,
|
||||||
msgData || {
|
mask: 'new',
|
||||||
role: 'user',
|
|
||||||
type: 'text',
|
|
||||||
content: text,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
//图片、视频、文件分开发送
|
|
||||||
if (tempUploadList.length > 0) {
|
|
||||||
Object.values(uploadFileTypeEm).forEach((item: any) => {
|
|
||||||
if (tempUploadList.find((v: UploadFile) => v.uploadFileType === item)) {
|
|
||||||
addMessage({
|
|
||||||
role: 'user',
|
|
||||||
type: item,
|
|
||||||
content: tempUploadList.filter((v) => v.uploadFileType === item),
|
|
||||||
timestamp: new Date(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
historyUserMsgs.push(lastMsg)
|
||||||
|
let body: IGptRequestBody = {
|
||||||
|
model: chatMode.value, // 模型选择
|
||||||
|
max_tokens: 1000,
|
||||||
|
top_p: 1,
|
||||||
|
presence_penalty: 0,
|
||||||
|
frequency_penalty: 0,
|
||||||
|
messages: historyUserMsgs, // text ? [aiMsg] : historyUserMsgs,
|
||||||
|
stream: true,
|
||||||
|
}
|
||||||
|
// 开始发请求 api
|
||||||
|
} else if (media.length <= 0 && file.length > 0) {
|
||||||
|
// 仅上传了文档类文件时
|
||||||
|
chatMode.value = 'qwen-long'
|
||||||
|
let aiMsg = {
|
||||||
|
role: 'user',
|
||||||
|
content: msg,
|
||||||
|
mask: 'new',
|
||||||
|
}
|
||||||
|
let content = [aiMsg]
|
||||||
|
content.concat(file)
|
||||||
|
let body: IGptRequestBody = {
|
||||||
|
model: chatMode.value, // 模型选择
|
||||||
|
max_tokens: 1000,
|
||||||
|
top_p: 1,
|
||||||
|
presence_penalty: 0,
|
||||||
|
frequency_penalty: 0,
|
||||||
|
messages: historyUserMsgs, // text ? [aiMsg] : historyUserMsgs,
|
||||||
|
stream: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 纯文本时发送
|
||||||
|
chatMode.value = 'tongyi-app'
|
||||||
|
|
||||||
//更新上下文消息
|
//更新上下文消息
|
||||||
historyUserMsgs.push({
|
historyUserMsgs.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: [{ type: 'text', text }, ...fileList],
|
content: msg,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
})
|
})
|
||||||
|
const aiMsg = {
|
||||||
//显示更多图片遮罩
|
role: 'assistant',
|
||||||
const showMoreImgMask =
|
type: 'text',
|
||||||
tempUploadList.filter((v: UploadFile) => v.uploadFileType === uploadFileTypeEm.image).length >
|
content: inputText.value,
|
||||||
4
|
timestamp: new Date(),
|
||||||
showImageMask.value = showMoreImgMask
|
}
|
||||||
} else {
|
addMessage(aiMsg)
|
||||||
//更新上下文消息
|
let body: IGptRequestBody = {
|
||||||
!msgData &&
|
model: chatMode.value, // 模型选择
|
||||||
historyUserMsgs.push({
|
max_tokens: 1000,
|
||||||
role: 'user',
|
top_p: 1,
|
||||||
content: text,
|
presence_penalty: 0,
|
||||||
timestamp: new Date(),
|
frequency_penalty: 0,
|
||||||
})
|
messages: historyUserMsgs, // text ? [aiMsg] : historyUserMsgs,
|
||||||
|
stream: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 第一次发送纯文本消息,第二次发送图片+视频,第三次发送文档,此时因为历史消息都要一起发送给后端,
|
||||||
inputText.value = ''
|
// 所以要想办法在遇到这种情况时,截断历史记录,主动为用户建立一个新的回话,但是不需要清空历史记录
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
// AI消息
|
|
||||||
const aiMsg: IMessage = {
|
|
||||||
role: 'assistant',
|
|
||||||
type: 'text',
|
|
||||||
content: text,
|
|
||||||
timestamp: new Date(),
|
|
||||||
}
|
|
||||||
addMessage(aiMsg)
|
|
||||||
|
|
||||||
//清除上传列表
|
|
||||||
uploadList.splice(0, uploadList.length)
|
uploadList.splice(0, uploadList.length)
|
||||||
|
// 没有上传文件,仅文字消息
|
||||||
const body: IGptRequestBody = {
|
|
||||||
model: chatMode.value,
|
|
||||||
max_tokens: 1000,
|
|
||||||
temperature: 1,
|
|
||||||
listUuid: listUuid.value,
|
|
||||||
top_p: 1,
|
|
||||||
presence_penalty: 0,
|
|
||||||
frequency_penalty: 0,
|
|
||||||
messages: msgData ? [msgData] : historyUserMsgs,
|
|
||||||
stream: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
aiMsg.content = ''
|
// aiMsg.content = ''
|
||||||
|
// 发送问题到后端
|
||||||
|
inputText.value = ''
|
||||||
const resp = await fetch(baseUrl + '/chat/app/completion', {
|
const resp = await fetch(baseUrl + '/chat/app/completion', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: token.value },
|
headers: { 'Content-Type': 'application/json', Authorization: token.value },
|
||||||
@ -1302,7 +1356,6 @@ async function sendText(msgData = '') {
|
|||||||
showActions.value = false
|
showActions.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyText(msg: IMessage) {
|
function copyText(msg: IMessage) {
|
||||||
if (typeof msg.content === 'string') {
|
if (typeof msg.content === 'string') {
|
||||||
uni.setClipboardData({
|
uni.setClipboardData({
|
||||||
@ -1350,7 +1403,7 @@ function refreshText() {
|
|||||||
|
|
||||||
// 4. 自动触发发送(模拟用户点击发送按钮)
|
// 4. 自动触发发送(模拟用户点击发送按钮)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sendText('')
|
sendText()
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1387,15 +1440,39 @@ const onFocus = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const showPreview = ref(false)
|
const showPreview = ref(false)
|
||||||
const previewUrl = ref('')
|
const previewUrl = ref('') // 预览图片
|
||||||
|
const previewVideoUrl = ref('') // 预览视频
|
||||||
const previewTop = ref(30) // 距离顶部30px
|
const previewTop = ref(30) // 距离顶部30px
|
||||||
const handleImageClick = (data) => {
|
const handleImageClick = (src) => {
|
||||||
previewUrl.value = data
|
let fileList = officeFileTypeList.value.concat(otherFileType.value)
|
||||||
showPreview.value = true
|
// officeFileTypeList=ref(['.docx','.doc','.excel','.pdf'])
|
||||||
|
// const otherFileType=ref(['.txt'])
|
||||||
|
// const otherFileType=ref(['.txt'])
|
||||||
|
if (picFileType.value.includes(isFileType(src))) {
|
||||||
|
previewUrl.value = src
|
||||||
|
showPreview.value = true
|
||||||
|
} else if (videoFileType.value.includes(isFileType(src))) {
|
||||||
|
previewVideoUrl.value = src
|
||||||
|
showPreview.value = true
|
||||||
|
} else if (fileList.includes(isFileType(src))) {
|
||||||
|
previewFile(src)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const closePreview = () => {
|
const closePreview = () => {
|
||||||
showPreview.value = false
|
showPreview.value = false
|
||||||
|
previewUrl.value = ''
|
||||||
|
previewVideoUrl.value = ''
|
||||||
|
}
|
||||||
|
const reload = () => {
|
||||||
|
const currentWebview = plus.webview.currentWebview()
|
||||||
|
currentWebview.reload(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFileType = (name: string) => {
|
||||||
|
let reg = /\.[\w\d]+$/
|
||||||
|
let match = name.match(reg)
|
||||||
|
return match ? match[0] : null
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -1404,14 +1481,17 @@ const closePreview = () => {
|
|||||||
.slide-up-leave-active {
|
.slide-up-leave-active {
|
||||||
transition: transform 0.3s ease-out;
|
transition: transform 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up-enter-from,
|
.slide-up-enter-from,
|
||||||
.slide-up-leave-to {
|
.slide-up-leave-to {
|
||||||
transform: translateY(100%);
|
transform: translateY(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up-enter-to,
|
.slide-up-enter-to,
|
||||||
.slide-up-leave-from {
|
.slide-up-leave-from {
|
||||||
transform: translateY(0%);
|
transform: translateY(0%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-pf {
|
.font-pf {
|
||||||
font-family: PingFangSC-Medium, sans-serif;
|
font-family: PingFangSC-Medium, sans-serif;
|
||||||
}
|
}
|
||||||
@ -1431,10 +1511,12 @@ const closePreview = () => {
|
|||||||
padding: 0 24rpx;
|
padding: 0 24rpx;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-title {
|
.nav-title {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-icon {
|
.nav-icon {
|
||||||
width: 40rpx;
|
width: 40rpx;
|
||||||
height: 40rpx;
|
height: 40rpx;
|
||||||
@ -1459,6 +1541,7 @@ const closePreview = () => {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12rpx 12rpx 0 0;
|
border-radius: 12rpx 12rpx 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup.fullscreen {
|
.popup.fullscreen {
|
||||||
height: 90%;
|
height: 90%;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@ -1472,10 +1555,12 @@ const closePreview = () => {
|
|||||||
padding: 0 24rpx;
|
padding: 0 24rpx;
|
||||||
border-bottom: 1rpx solid #f0f0f0;
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-header .icon {
|
.popup-header .icon {
|
||||||
width: 32rpx;
|
width: 32rpx;
|
||||||
height: 32rpx;
|
height: 32rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-header .title {
|
.popup-header .title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
@ -1487,16 +1572,19 @@ const closePreview = () => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 24rpx 0;
|
padding: 24rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-label {
|
.date-label {
|
||||||
padding: 0 32rpx;
|
padding: 0 32rpx;
|
||||||
margin-top: 24rpx;
|
margin-top: 24rpx;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-item {
|
.history-item {
|
||||||
padding: 24rpx 32rpx;
|
padding: 24rpx 32rpx;
|
||||||
border-bottom: 1rpx solid #f0f0f0;
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-text {
|
.history-text {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #333;
|
color: #333;
|
||||||
@ -1505,6 +1593,7 @@ const closePreview = () => {
|
|||||||
.tops {
|
.tops {
|
||||||
padding-top: var(--status-bar-height);
|
padding-top: var(--status-bar-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-i {
|
.flex-i {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
@ -1,553 +0,0 @@
|
|||||||
<route lang="json5" type="page">
|
|
||||||
{
|
|
||||||
layout: 'default',
|
|
||||||
style: {
|
|
||||||
navigationBarHidden: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</route>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-col h-screen bg-gray-50">
|
|
||||||
<!-- Navigation Bar -->
|
|
||||||
<div class="flex-none flex items-center justify-between px-5 py-3 bg-white shadow-md h-10">
|
|
||||||
<image src="/static/aichat/back.png" class="w-2 h-4" @click="goBack" />
|
|
||||||
<div class="text-lg font-medium">小墨</div>
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<image src="/static/aichat/time.png" class="w-5 h-5" @click="viewHistory" />
|
|
||||||
<image src="/static/aichat/new.png" class="w-5 h-5" @click="newChat" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 消息区 -->
|
|
||||||
<div :class="['flex relative', showActions ? 'h-105' : 'h-130']">
|
|
||||||
<!-- 背景层 -->
|
|
||||||
<div class="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
|
|
||||||
<image src="/static/aichat/logo.png" class="w-20 h-24 mb-4" @click="newChat" />
|
|
||||||
<view class="text-xl font-medium mb-1">嗨! 我是小墨</view>
|
|
||||||
<view class="text-gray-400">开启新的聊天吧</view>
|
|
||||||
</div>
|
|
||||||
z
|
|
||||||
<div
|
|
||||||
ref="scrollEl"
|
|
||||||
class="flex-1 overflow-y-auto bg-gray-50"
|
|
||||||
:class="showActions ? 'pb-44' : 'pb-16'"
|
|
||||||
>
|
|
||||||
<div :class="['relative z-10 px-4 py-6', showActions ? 'mb--11 h-105' : 'mb--21 h-135']">
|
|
||||||
<template v-for="(msg, idx) in messages" :key="idx">
|
|
||||||
<view v-if="shouldShowTimestamp(idx)" class="text-center text-xs text-gray-500 my-2">
|
|
||||||
{{ formatDayGroup(msg.timestamp) }}
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="flex items-start"
|
|
||||||
:class="msg.role === 'assistant' ? 'justify-start' : 'justify-end'"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
v-if="msg.role === 'assistant'"
|
|
||||||
:src="assistantAvatar"
|
|
||||||
class="w-8 h-8 rounded-full mr-2 mt-1"
|
|
||||||
/>
|
|
||||||
<view class="relative max-w-[70%] mt-4 mb-3">
|
|
||||||
<view
|
|
||||||
:class="[
|
|
||||||
'absolute -top-4 text-xs text-gray-400',
|
|
||||||
msg.role === 'assistant' ? 'left-0' : 'right-0',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ formatTimeShort(msg.timestamp) }}
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
:class="[
|
|
||||||
'py-2 px-3 rounded-lg break-words mt-1',
|
|
||||||
msg.role === 'assistant'
|
|
||||||
? 'bg-[#f9f8fd] text-black shadow'
|
|
||||||
: 'bg-[#45299e] text-white',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ msg.content }}
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
v-if="msg.role === 'assistant' && msg.type === 'text'"
|
|
||||||
class="absolute bottom-0 flex space-x-3"
|
|
||||||
>
|
|
||||||
<image src="/static/aichat/copy.png" class="w-4 h-4" @click="copyText(msg)" />
|
|
||||||
<image
|
|
||||||
src="/static/aichat/resect.png"
|
|
||||||
class="w-4 h-4"
|
|
||||||
@click="refreshText(msg)"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<image
|
|
||||||
v-if="msg.role === 'user'"
|
|
||||||
:src="userAvatar"
|
|
||||||
class="w-8 h-8 rounded-full ml-2 mt-1"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部上传预览 + 输入区 -->
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'fixed bottom-0 left-0 right-0 bg-white z-[80] overflow-hidden transition-all duration-300',
|
|
||||||
showActions ? 'h-45' : 'h-20',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<!-- 上传列表 -->
|
|
||||||
<div v-if="uploadList.length" class="flex px-4 py-2 overflow-x-auto space-x-3 bg-white">
|
|
||||||
<div
|
|
||||||
v-for="item in uploadList"
|
|
||||||
:key="item.id"
|
|
||||||
class="relative w-16 h-16 rounded overflow-hidden"
|
|
||||||
>
|
|
||||||
<!-- 预览图,成功后用后端返回的 URL;上传中可以先用本地预览 -->
|
|
||||||
<img
|
|
||||||
:src="item.url || item.localPath"
|
|
||||||
class="w-full h-full object-cover"
|
|
||||||
@click="previewImage(item.url || item.localPath)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 关闭按钮 -->
|
|
||||||
<div
|
|
||||||
class="absolute top-1 right-1 w-4 h-4 rounded-full bg-black bg-opacity-50 flex items-center justify-center cursor-pointer text-white text-xs"
|
|
||||||
@click="removeImage(item.id)"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 进度 / 成功 / 失败 -->
|
|
||||||
<div
|
|
||||||
class="absolute bottom-0 left-0 w-full text-xs text-center text-white py-1"
|
|
||||||
:class="{
|
|
||||||
'bg-black bg-opacity-50': item.status === 'uploading',
|
|
||||||
'bg-green-600 bg-opacity-50': item.status === 'success',
|
|
||||||
'bg-red-600 bg-opacity-50': item.status === 'fail',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template v-if="item.status === 'uploading'">{{ item.progress }}%</template>
|
|
||||||
<template v-else-if="item.status === 'success'">✔ 成功</template>
|
|
||||||
<template v-else>
|
|
||||||
✖ 失败
|
|
||||||
<span class="cursor-pointer" @click.stop="retry(item)">↻</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 输入 + 切换 -->
|
|
||||||
<view class="flex items-center px-4 py-2.5 border-t border-solid border-[#E7E7E7]">
|
|
||||||
<input
|
|
||||||
v-model="inputText"
|
|
||||||
@keyup.enter="sendText"
|
|
||||||
placeholder="想对我说点什么~"
|
|
||||||
class="flex-1 h-10 px-3 border border-gray-100 bg-[#f9f9f9] rounded-full focus:outline-none"
|
|
||||||
/>
|
|
||||||
<image src="/static/aichat/add-circle.png" class="w-7 h-7 mx-3" @click="toggleActions" />
|
|
||||||
<image
|
|
||||||
src="/static/aichat/enter.png"
|
|
||||||
class="w-7 h-7"
|
|
||||||
@click="sendText"
|
|
||||||
:disabled="loading"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 操作面板 -->
|
|
||||||
<transition name="slide-up">
|
|
||||||
<view
|
|
||||||
v-show="showActions"
|
|
||||||
class="flex justify-around items-center h-20 border-t border-solid border-[#E7E7E7] bg-white"
|
|
||||||
>
|
|
||||||
<view class="flex flex-col items-center">
|
|
||||||
<image src="/static/aichat/phone-img.png" class="w-13 h-13" @click="onPickImage" />
|
|
||||||
<span class="text-xs mt-1 text-gray-500">照片</span>
|
|
||||||
</view>
|
|
||||||
<view class="flex flex-col items-center">
|
|
||||||
<image src="/static/aichat/photo.png" class="w-13 h-13" @click="onTakePhoto" />
|
|
||||||
<span class="text-xs mt-1 text-gray-500">拍摄</span>
|
|
||||||
</view>
|
|
||||||
<view class="flex flex-col items-center">
|
|
||||||
<image src="/static/aichat/files.png" class="w-13 h-13" @click="onPickFile" />
|
|
||||||
<span class="text-xs mt-1 text-gray-500">文件</span>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
1
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, reactive, nextTick } from 'vue'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { useUserStore } from '@/store'
|
|
||||||
import { getEnvBaseUrl } from '@/utils'
|
|
||||||
import type { IGptRequestBody } from '@/service/index/foo'
|
|
||||||
interface IUpload {
|
|
||||||
id: number
|
|
||||||
url: string
|
|
||||||
filePath: string
|
|
||||||
status: 'uploading' | 'success' | 'fail'
|
|
||||||
progress: number
|
|
||||||
detail: string
|
|
||||||
mask: string
|
|
||||||
}
|
|
||||||
interface IMessage {
|
|
||||||
role: 'user' | 'assistant'
|
|
||||||
type: 'text' | 'images'
|
|
||||||
content: string | string[]
|
|
||||||
timestamp: Date
|
|
||||||
}
|
|
||||||
interface UploadItem {
|
|
||||||
id: string
|
|
||||||
localPath: string // 本地临时路径,用于预览
|
|
||||||
url: string // 后端返回的在线 URL
|
|
||||||
status: 'uploading' | 'success' | 'fail'
|
|
||||||
progress: number // 上传进度 %
|
|
||||||
}
|
|
||||||
const assistantAvatar =
|
|
||||||
'https://dci-file-new.bj.bcebos.com/fonchain-main/test/runtime/image/avatar/40/b8ed6fea-6662-416d-8bb3-1fd8a8197061.jpg'
|
|
||||||
const userAvatar = assistantAvatar
|
|
||||||
const baseUrl = getEnvBaseUrl()
|
|
||||||
const token = useUserStore().userInfo.token || import.meta.env.VITE_DEV_TOKEN || ''
|
|
||||||
const messages = reactive<IMessage[]>([])
|
|
||||||
const inputText = ref('')
|
|
||||||
const loading = ref(false)
|
|
||||||
const showActions = ref(false)
|
|
||||||
const scrollEl = ref<HTMLElement>()
|
|
||||||
const uploadList = reactive<IUpload[]>([])
|
|
||||||
const uploadId = ref(0)
|
|
||||||
|
|
||||||
function scrollToBottom() {
|
|
||||||
const el = scrollEl.value!
|
|
||||||
nextTick(() => {
|
|
||||||
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function addMessage(msg: IMessage) {
|
|
||||||
messages.push(msg)
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldShowTimestamp = (i: number) => {
|
|
||||||
if (i === 0) return true
|
|
||||||
return !dayjs(messages[i].timestamp).isSame(messages[i - 1].timestamp, 'day')
|
|
||||||
}
|
|
||||||
const formatDayGroup = (d: Date) => dayjs(d).format('YYYY/MM/DD HH:mm')
|
|
||||||
const formatTimeShort = (d: Date) => dayjs(d).format('MM/DD HH:mm')
|
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
window.history.back()
|
|
||||||
}
|
|
||||||
function viewHistory() {
|
|
||||||
uni.navigateTo({ url: '/pages/history/history' })
|
|
||||||
}
|
|
||||||
function newChat() {
|
|
||||||
messages.splice(0)
|
|
||||||
inputText.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleActions() {
|
|
||||||
showActions.value = !showActions.value
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 相册
|
|
||||||
function onPickImage() {
|
|
||||||
uni.chooseImage({
|
|
||||||
count: 10,
|
|
||||||
success: (res: any) => {
|
|
||||||
res.tempFilePaths.forEach((path) => {
|
|
||||||
uploadId.value += 1
|
|
||||||
const id = uploadId.value
|
|
||||||
const upload: IUpload = { id, url: path, filePath: path, status: 'uploading', progress: 0 }
|
|
||||||
uploadList.push(upload)
|
|
||||||
uploadFile(path, upload)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 拍照
|
|
||||||
function onTakePhoto() {
|
|
||||||
uni.chooseImage({
|
|
||||||
sourceType: ['camera'],
|
|
||||||
count: 1,
|
|
||||||
success: (res: any) => {
|
|
||||||
const path = res.tempFilePaths[0]
|
|
||||||
uploadId.value += 1
|
|
||||||
const id = uploadId.value
|
|
||||||
const upload: IUpload = { id, url: path, filePath: path, status: 'uploading', progress: 0 }
|
|
||||||
uploadList.push(upload)
|
|
||||||
uploadFile(path, upload)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 文件
|
|
||||||
// 触发文件选择后,或拍照后,或其它入口都调用这个
|
|
||||||
function onPickFile(path: string, detail = '') {
|
|
||||||
const id = Date.now().toString()
|
|
||||||
const item: UploadItem = {
|
|
||||||
id,
|
|
||||||
localPath: path,
|
|
||||||
url: '',
|
|
||||||
status: 'uploading',
|
|
||||||
progress: 0,
|
|
||||||
}
|
|
||||||
uploadList.push(item)
|
|
||||||
startUpload(item, detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UploadFile {
|
|
||||||
uid: string
|
|
||||||
name: string
|
|
||||||
size: number
|
|
||||||
progress: number // 上传进度 0-100
|
|
||||||
status: 'uploading' | 'success' | 'error'
|
|
||||||
file: File
|
|
||||||
url?: string // 本地预览URL或服务器返回的URL
|
|
||||||
}
|
|
||||||
const filesList = ref<UploadFile[]>([])
|
|
||||||
async function startUpload(item: UploadItem, detail: string) {
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
// 标记开始上传
|
|
||||||
item.status = 'uploading'
|
|
||||||
item.progress = 0
|
|
||||||
|
|
||||||
// 发起上传 (不带 success/fail 回调)
|
|
||||||
// @ts-ignore: uni.uploadFile 返回 UploadTask 兼具 Promise 接口
|
|
||||||
const uploadTask: UniApp.UploadTask & Promise<UniApp.UploadFileRes> = uni.uploadFile({
|
|
||||||
// url: 'http://114.218.158.24:9020/upload/multi',
|
|
||||||
url: 'https://ukw0y1.laf.run/upload',
|
|
||||||
filePath: item.localPath,
|
|
||||||
name: 'k1', // 这里的 name 依然是 formData 里的字段名,后端会以此为 key
|
|
||||||
formData: {
|
|
||||||
source: 'chat',
|
|
||||||
mask: '2076',
|
|
||||||
detail,
|
|
||||||
type: 'image',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听进度
|
|
||||||
uploadTask.onProgressUpdate((res: { progress: number }) => {
|
|
||||||
item.progress = res.progress
|
|
||||||
})
|
|
||||||
|
|
||||||
// 3s 超时自动 abort
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
if (item.status === 'uploading') {
|
|
||||||
uploadTask.abort()
|
|
||||||
item.status = 'fail'
|
|
||||||
}
|
|
||||||
}, 3000)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 等待上传完成
|
|
||||||
const res = await uploadTask
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
// 解析后端 JSON
|
|
||||||
const resp = JSON.parse(res.data) as {
|
|
||||||
code: number
|
|
||||||
data: Record<string, string>
|
|
||||||
}
|
|
||||||
console.log(resp, 'resp')
|
|
||||||
if (resp.code === 0 && resp.data) {
|
|
||||||
// 遍历 data 对象,找第一个非空值
|
|
||||||
const urls = Object.values(resp.data).filter((u) => !!u)
|
|
||||||
if (urls.length > 0) {
|
|
||||||
item.url = urls[0]
|
|
||||||
item.status = 'success'
|
|
||||||
} else {
|
|
||||||
console.warn('没有拿到任何上传后的 URL')
|
|
||||||
item.status = 'fail'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('后端返回异常 code=', resp.code)
|
|
||||||
item.status = 'fail'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
console.error('uploadFile 出错:', err)
|
|
||||||
item.status = 'fail'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 点击重试
|
|
||||||
function retry(item: UploadItem) {
|
|
||||||
item.status = 'uploading'
|
|
||||||
item.progress = 0
|
|
||||||
startUpload(item, '') // 如果需要 detail,可缓存后传入
|
|
||||||
}
|
|
||||||
// 删除
|
|
||||||
function removeImage(id: string) {
|
|
||||||
const idx = uploadList.findIndex((i) => i.id === id)
|
|
||||||
if (idx >= 0) uploadList.splice(idx, 1)
|
|
||||||
}
|
|
||||||
// 预览(可自行实现 uni.previewImage 等)
|
|
||||||
function previewImage(url: string) {
|
|
||||||
uni.previewImage({ urls: [url] })
|
|
||||||
}
|
|
||||||
// 上传使用 postUpload
|
|
||||||
async function uploadFile(path: string, upload: IUpload) {
|
|
||||||
// 标记开始
|
|
||||||
upload.status = 'uploading'
|
|
||||||
|
|
||||||
try {
|
|
||||||
// @ts-ignore uni.uploadFile 返回 Promise
|
|
||||||
const res: UniApp.UploadFileRes = await uni.uploadFile({
|
|
||||||
// url: 'http://114.218.158.24:9020/upload/multi',
|
|
||||||
url: 'https://ukw0y1.laf.run/upload',
|
|
||||||
filePath: path,
|
|
||||||
name: 'file', // 这个 name 依然是 formData key,后端会把它用在 data.data 对象里
|
|
||||||
formData: {
|
|
||||||
source: 'chat',
|
|
||||||
mask: '2076',
|
|
||||||
type: 'image',
|
|
||||||
k1: 'xxxx.png',
|
|
||||||
k2: 'xxxx.png',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 解析后端 JSON
|
|
||||||
const resp = JSON.parse(res.data) as {
|
|
||||||
code: number
|
|
||||||
data: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp.code === 0 && resp.data) {
|
|
||||||
// 找到 data 对象里第一个有值的字段并赋给 upload.url
|
|
||||||
let found = false
|
|
||||||
for (const key in resp.data) {
|
|
||||||
const url = resp.data[key]
|
|
||||||
if (url) {
|
|
||||||
upload.url = url
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
upload.status = 'success'
|
|
||||||
} else {
|
|
||||||
console.warn('没有取到任何上传后的 URL')
|
|
||||||
upload.status = 'fail'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('后端返回异常 code=', resp.code)
|
|
||||||
upload.status = 'fail'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('uploadFile 出错:', err)
|
|
||||||
upload.status = 'fail'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function sendText() {
|
|
||||||
const text = inputText.value.trim()
|
|
||||||
if (!text || loading.value) return
|
|
||||||
|
|
||||||
addMessage({ role: 'user', type: 'text', content: text, timestamp: new Date() })
|
|
||||||
inputText.value = ''
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
const aiMsg: IMessage = {
|
|
||||||
role: 'assistant',
|
|
||||||
type: 'text',
|
|
||||||
content: '',
|
|
||||||
timestamp: new Date(),
|
|
||||||
}
|
|
||||||
addMessage(aiMsg)
|
|
||||||
|
|
||||||
const body: IGptRequestBody = {
|
|
||||||
model: 'gpt-4-vision-preview',
|
|
||||||
max_tokens: 1000,
|
|
||||||
temperature: 1,
|
|
||||||
top_p: 1,
|
|
||||||
presence_penalty: 0,
|
|
||||||
frequency_penalty: 0,
|
|
||||||
messages: [{ role: 'user', content: [{ type: 'text', text }] }],
|
|
||||||
stream: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch(baseUrl + '/chat/completion', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: token },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
const reader = resp.body!.getReader()
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
let buffer = ''
|
|
||||||
let done = false
|
|
||||||
|
|
||||||
while (!done) {
|
|
||||||
const { value, done: streamDone } = await reader.read()
|
|
||||||
done = streamDone
|
|
||||||
if (value) {
|
|
||||||
buffer += decoder.decode(value, { stream: true })
|
|
||||||
const parts = buffer.split('data: ')
|
|
||||||
buffer = parts.pop()!
|
|
||||||
for (const part of parts) {
|
|
||||||
scrollToBottom()
|
|
||||||
console.log('1')
|
|
||||||
const chunk = part.trim()
|
|
||||||
if (chunk === '[DONE]') {
|
|
||||||
done = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(chunk)
|
|
||||||
const delta = json.choices?.[0]?.delta?.content
|
|
||||||
if (delta) {
|
|
||||||
aiMsg.content += delta
|
|
||||||
scrollToBottom()
|
|
||||||
console.log('2')
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scrollToBottom()
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
showActions.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyText(msg: IMessage) {
|
|
||||||
if (typeof msg.content === 'string') {
|
|
||||||
navigator.clipboard.writeText(msg.content)
|
|
||||||
alert('已复制')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function refreshText(msg: IMessage) {
|
|
||||||
if (typeof msg.content === 'string') {
|
|
||||||
inputText.value = msg.content
|
|
||||||
const idx = messages.indexOf(msg)
|
|
||||||
messages.splice(idx, 1)
|
|
||||||
sendText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.slide-up-enter-active,
|
|
||||||
.slide-up-leave-active {
|
|
||||||
transition: transform 0.3s ease-out;
|
|
||||||
}
|
|
||||||
.slide-up-enter-from,
|
|
||||||
.slide-up-leave-to {
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
.slide-up-enter-to,
|
|
||||||
.slide-up-leave-from {
|
|
||||||
transform: translateY(0%);
|
|
||||||
}
|
|
||||||
.font-pf {
|
|
||||||
font-family: PingFangSC-Medium, sans-serif;
|
|
||||||
}
|
|
||||||
</style>
|
|
205
src/pages/index/utils/data.js
Normal file
205
src/pages/index/utils/data.js
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
// 发送消息
|
||||||
|
async function sendText1(msgData = '') {
|
||||||
|
// let msgData=''
|
||||||
|
// if(msg===''){
|
||||||
|
// msgData=msg
|
||||||
|
// }else if(msg.detail && msg.detail.value){
|
||||||
|
|
||||||
|
// msgData=msg.detail.value
|
||||||
|
// }
|
||||||
|
// console.log('msgData',msg)
|
||||||
|
// uni.showToast({ title: inputText.value, icon: 'error' })
|
||||||
|
|
||||||
|
msgLoading.value = true
|
||||||
|
const text = inputText.value.trim()
|
||||||
|
const dataBlo = text //toRaw(msgData)
|
||||||
|
console.log('dataBlo', dataBlo)
|
||||||
|
if (!text) {
|
||||||
|
msgLoading.value = false
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '请输入信息',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
inputText.value = ''
|
||||||
|
loading.value = true
|
||||||
|
const tempUploadList = Object.assign([], uploadList)
|
||||||
|
let fileList: any[] = []
|
||||||
|
if (tempUploadList.length > 0) {
|
||||||
|
fileList = tempUploadList.map((item: UploadFile) => {
|
||||||
|
if (item.uploadFileType === uploadFileTypeEm.image) {
|
||||||
|
// 图片格式
|
||||||
|
return {
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: {
|
||||||
|
url: item.url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if (item.uploadFileType === uploadFileTypeEm.video) {
|
||||||
|
console.log(item, '====item=====')
|
||||||
|
// 视频格式(改成和图片一样的结构)
|
||||||
|
return {
|
||||||
|
type: 'video_url',
|
||||||
|
video_url: {
|
||||||
|
url: item.ori_url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他文件类型保持原样
|
||||||
|
const fileType = `${item.uploadFileType}_url`
|
||||||
|
return {
|
||||||
|
type: fileType,
|
||||||
|
[fileType]: item.url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加用户文本消息
|
||||||
|
addMessage({
|
||||||
|
role: 'user',
|
||||||
|
type: 'text',
|
||||||
|
content: text,
|
||||||
|
timestamp: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
//图片、视频、文件分开发送
|
||||||
|
if (tempUploadList.length > 0) {
|
||||||
|
Object.values(uploadFileTypeEm).forEach((item: any) => {
|
||||||
|
if (tempUploadList.find((v: UploadFile) => v.uploadFileType === item)) {
|
||||||
|
addMessage({
|
||||||
|
role: 'user',
|
||||||
|
type: item,
|
||||||
|
content: tempUploadList.filter((v) => v.uploadFileType === item),
|
||||||
|
timestamp: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//更新上下文消息
|
||||||
|
historyUserMsgs.push({
|
||||||
|
role: 'user',
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text
|
||||||
|
}, ...fileList],
|
||||||
|
timestamp: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
//显示更多图片遮罩
|
||||||
|
const showMoreImgMask = tempUploadList.filter((v: UploadFile) => v.uploadFileType === uploadFileTypeEm.image).length > 4
|
||||||
|
showImageMask.value = showMoreImgMask
|
||||||
|
} else {
|
||||||
|
//更新上下文消息
|
||||||
|
historyUserMsgs.push({
|
||||||
|
role: 'user',
|
||||||
|
content: text,
|
||||||
|
timestamp: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI消息
|
||||||
|
// const aiMsg: IMessage = {
|
||||||
|
const aiMsg = {
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: inputText.value,
|
||||||
|
timestamp: new Date(),
|
||||||
|
}
|
||||||
|
addMessage(aiMsg)
|
||||||
|
|
||||||
|
//清除上传列表
|
||||||
|
uploadList.splice(0, uploadList.length)
|
||||||
|
// console.log('[msgData] : historyUserMsgs: ',msgData);
|
||||||
|
// console.log('[msgData] : historyUserMsgs: ', historyUserMsgs);
|
||||||
|
const body: IGptRequestBody = {
|
||||||
|
model: chatMode.value,
|
||||||
|
max_tokens: 1000,
|
||||||
|
temperature: 1,
|
||||||
|
listUuid: listUuid.value,
|
||||||
|
top_p: 1,
|
||||||
|
presence_penalty: 0,
|
||||||
|
frequency_penalty: 0,
|
||||||
|
messages: historyUserMsgs, // text ? [aiMsg] : historyUserMsgs,
|
||||||
|
stream: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// aiMsg.content = ''
|
||||||
|
// 发送问题到后端
|
||||||
|
const resp = await fetch(baseUrl + '/chat/app/completion', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: token.value
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
|
||||||
|
const reader = resp.body!.getReader()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
let buffer = ''
|
||||||
|
let done = false
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
done: streamDone
|
||||||
|
} = await reader.read()
|
||||||
|
done = streamDone
|
||||||
|
if (value) {
|
||||||
|
buffer += decoder.decode(value, {
|
||||||
|
stream: true
|
||||||
|
})
|
||||||
|
const lines = buffer.split(/\r?\n/)
|
||||||
|
buffer = lines.pop() !
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// 只处理以 "data: " 开头的行
|
||||||
|
if (!line.startsWith('data: ')) continue
|
||||||
|
const chunk = line.slice(6).trim()
|
||||||
|
|
||||||
|
if (chunk === '[DONE]') {
|
||||||
|
done = true
|
||||||
|
console.log('sss')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(chunk)
|
||||||
|
const delta = json.choices?.[0]?.delta?.content
|
||||||
|
|
||||||
|
if (delta) {
|
||||||
|
msgLoading.value = false
|
||||||
|
aiMsg.content += delta
|
||||||
|
//每次更新messages消息,实现流式输出
|
||||||
|
messages[messages.length - 1] = {
|
||||||
|
...aiMsg
|
||||||
|
}
|
||||||
|
scrollToBottom()
|
||||||
|
console.log('2')
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
//更新上下文消息
|
||||||
|
done && historyUserMsgs.push(aiMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollToBottom()
|
||||||
|
} catch (err) {
|
||||||
|
aiMsg.content = '请重新发送'
|
||||||
|
//更新messages消息
|
||||||
|
messages[messages.length - 1] = {
|
||||||
|
...aiMsg
|
||||||
|
}
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
showActions.value = false
|
||||||
|
}
|
||||||
|
}
|
52
src/pages/index/utils/index.ts
Normal file
52
src/pages/index/utils/index.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export const formatParams = (uploadList) => {
|
||||||
|
// 上传文件formData类型
|
||||||
|
const uploadFileTypeEm = {
|
||||||
|
image: 'image',
|
||||||
|
video: 'video',
|
||||||
|
file: 'file',
|
||||||
|
text: 'text',
|
||||||
|
}
|
||||||
|
let mediaList = [] // 媒体文件 参数中的content
|
||||||
|
let fileList = [] // 文档文件
|
||||||
|
uploadList.forEach((item) => {
|
||||||
|
if (item.uploadFileType === uploadFileTypeEm.image) {
|
||||||
|
// 图片
|
||||||
|
let media = {
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: {
|
||||||
|
url: item.ori_url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mediaList.push(media)
|
||||||
|
} else if (item.uploadFileType === uploadFileTypeEm.video) {
|
||||||
|
// 视频
|
||||||
|
let media = {
|
||||||
|
type: 'video_url',
|
||||||
|
video_url: {
|
||||||
|
url: item.url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mediaList.push(media)
|
||||||
|
} else if (item.uploadFileType === uploadFileTypeEm.file) {
|
||||||
|
let file = {
|
||||||
|
role: 'system',
|
||||||
|
content: item.url,
|
||||||
|
mask: 'new',
|
||||||
|
}
|
||||||
|
fileList.push(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { media: mediaList, file: fileList }
|
||||||
|
}
|
||||||
|
export const calcFileSize = (size: number) => {
|
||||||
|
const type = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
// for(let i=0;i<type.length && size>1024;i++){
|
||||||
|
// size/=1024
|
||||||
|
// }
|
||||||
|
let unit = 'B'
|
||||||
|
while (size > 1024) {
|
||||||
|
size /= 1024
|
||||||
|
unit = type.shift()
|
||||||
|
}
|
||||||
|
return `${Math.ceil(size)}${unit}`
|
||||||
|
}
|
3
src/pages/index/utils/test.js
Normal file
3
src/pages/index/utils/test.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const TOKEN="79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941ca1430937103230a1e32a1715f569f3efdbe6f8cb8b7b8642bd679668081b9b08f693d1b5be6002d936ec51e1e3e0c4927de9e32ac99a109b326e5d2bda27ec87624bb416ec70d2a95a2e190feeba9f0d6bae8571b3dfe89c824712344759a8f2bff9d70747c52525cf6a5614f9c770bca461a9b9c247b6dca97bcf83bbaf99bb726752c4fe1e9a4aa7de5c4cf3e88a3e480801280d45cdc124f9d8221105d852945dc6ce10bc1647e4f09dff4d52ffdfc7eec89db303f76bb398a9e990517855cc34a9b4b5f8ebb42741e3f2c66d25790e78ad4d101c615554bbe75fdc3c97ddfe1a175322a675f7f0f55870b0222814de6998a4e9f7b24aaf9e07396389c2ec7"
|
||||||
|
|
||||||
|
export const AVATAR="https://ts1.tc.mm.bing.net/th/id/R-C.66d7b796377883a92aad65b283ef1f84?rik=sQ%2fKoYAcr%2bOwsw&riu=http%3a%2f%2fwww.quazero.com%2fuploads%2fallimg%2f140305%2f1-140305131415.jpg&ehk=Hxl%2fQ9pbEiuuybrGWTEPJOhvrFK9C3vyCcWicooXfNE%3d&risl=&pid=ImgRaw&r=0"
|
BIN
src/static/aichat/ic_video.png
Normal file
BIN
src/static/aichat/ic_video.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
src/static/aichat/txt.png
Normal file
BIN
src/static/aichat/txt.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
src/static/aichat/word.png
Normal file
BIN
src/static/aichat/word.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
2
src/types/uni-pages.d.ts
vendored
2
src/types/uni-pages.d.ts
vendored
@ -6,8 +6,6 @@
|
|||||||
interface NavigateToOptions {
|
interface NavigateToOptions {
|
||||||
url: "/pages/index/index" |
|
url: "/pages/index/index" |
|
||||||
"/pages/about/about" |
|
"/pages/about/about" |
|
||||||
"/pages/index/index copy" |
|
|
||||||
"/pages/index/index1" |
|
|
||||||
"/pages/preview/index" |
|
"/pages/preview/index" |
|
||||||
"/pages/webview/index";
|
"/pages/webview/index";
|
||||||
}
|
}
|
||||||
|
8
src/utils/api.js
Normal file
8
src/utils/api.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {httpPost,httpGet} from "./http"
|
||||||
|
import {getEnvBaseUrl} from "./index"
|
||||||
|
const baseUrl=getEnvBaseUrl();
|
||||||
|
|
||||||
|
// 发送文本消息
|
||||||
|
const endMsg=async (params)=>{
|
||||||
|
return await httpGet(baseUrl+url,data)
|
||||||
|
}
|
435
src/utils/tools.js
Normal file
435
src/utils/tools.js
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
import { getEnvBaseUrl } from "./index";
|
||||||
|
export const baseUrl =getEnvBaseUrl();
|
||||||
|
// 获取接口(application/json,带token)
|
||||||
|
export const api = (url = '', params = {}, method = 'post') => {
|
||||||
|
let token = wx.getStorageSync('token');
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.request({
|
||||||
|
url: baseUrl+url,
|
||||||
|
data: params,
|
||||||
|
method,
|
||||||
|
header: {
|
||||||
|
'Content-Type': "application/json",
|
||||||
|
'token': token,
|
||||||
|
},
|
||||||
|
complete: res => {
|
||||||
|
if(res.statusCode == 200) {
|
||||||
|
resolve(res.data)
|
||||||
|
} else {
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 获取接口(application/x-www-form-urlencoded,带token)
|
||||||
|
export const api_form = (url = '', params = {}, method = 'post') => {
|
||||||
|
let token = wx.getStorageSync('token');
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.request({
|
||||||
|
url: baseUrl+url,
|
||||||
|
data: params,
|
||||||
|
method,
|
||||||
|
header: {
|
||||||
|
'Content-Type':"application/x-www-form-urlencoded",
|
||||||
|
'token': token,
|
||||||
|
},
|
||||||
|
complete: res => {
|
||||||
|
if(res.statusCode == 200) {
|
||||||
|
resolve(res.data)
|
||||||
|
} else {
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 获取接口(application/x-www-form-urlencoded,不带token)
|
||||||
|
export const apis = (url = '', params = {}, method = 'post') => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.request({
|
||||||
|
url: baseUrl+url,
|
||||||
|
data: params,
|
||||||
|
method,
|
||||||
|
header: {
|
||||||
|
'Content-Type':"application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
complete: res => {
|
||||||
|
if(res.statusCode == 200) {
|
||||||
|
resolve(res.data)
|
||||||
|
} else {
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 时间戳转日期(年月日)
|
||||||
|
export const time_format = (time) =>{
|
||||||
|
// 判断时间戳是否为13位数,如果不是则*1000,时间戳只有13位数(带毫秒)和10(不带毫秒)位数的
|
||||||
|
if(time.toString().length == 13){
|
||||||
|
var tme = new Date(time);
|
||||||
|
}else{
|
||||||
|
var tme = new Date(time * 1000);
|
||||||
|
}
|
||||||
|
var Y = tme.getFullYear();
|
||||||
|
var M = (tme.getMonth() + 1 < 10 ? '0' + (tme.getMonth() + 1) : tme.getMonth() + 1);
|
||||||
|
var D = tme.getDate();
|
||||||
|
var h = tme.getHours();
|
||||||
|
var m = tme.getMinutes();
|
||||||
|
var s = tme.getSeconds();
|
||||||
|
var tem1 = Y + '-' + M + '-' + D
|
||||||
|
// + h + '时' + m + '分'
|
||||||
|
// + s +'秒'
|
||||||
|
return tem1;
|
||||||
|
}
|
||||||
|
export const time_format3 = (time) =>{
|
||||||
|
// 判断时间戳是否为13位数,如果不是则*1000,时间戳只有13位数(带毫秒)和10(不带毫秒)位数的
|
||||||
|
if(time.toString().length == 13){
|
||||||
|
var tme = new Date(time);
|
||||||
|
}else{
|
||||||
|
var tme = new Date(time * 1000);
|
||||||
|
}
|
||||||
|
var Y = tme.getFullYear();
|
||||||
|
var M = (tme.getMonth() + 1 < 10 ? '0' + (tme.getMonth() + 1) : tme.getMonth() + 1);
|
||||||
|
var D = tme.getDate();
|
||||||
|
var h = tme.getHours();
|
||||||
|
if(h<10){
|
||||||
|
h='0'+h;
|
||||||
|
}
|
||||||
|
var m = tme.getMinutes();
|
||||||
|
if(m<10){
|
||||||
|
m='0'+m;
|
||||||
|
}
|
||||||
|
var s = tme.getSeconds();
|
||||||
|
if(s<10){
|
||||||
|
s='0'+s;
|
||||||
|
}
|
||||||
|
var tem1 = Y + '-' + M + '-' + D +' '+h+':'+m+':'+s
|
||||||
|
// + h + '时' + m + '分'
|
||||||
|
// + s +'秒'
|
||||||
|
return tem1;
|
||||||
|
}
|
||||||
|
// 时间戳转日期(时分)
|
||||||
|
export const time_format1 = (time) =>{
|
||||||
|
if(time.toString().length == 13){
|
||||||
|
var tme = new Date(time);
|
||||||
|
}else{
|
||||||
|
var tme = new Date(time * 1000);
|
||||||
|
}
|
||||||
|
var Y = tme.getFullYear();
|
||||||
|
var M = (tme.getMonth() + 1 < 10 ? '0' + (tme.getMonth() + 1) : tme.getMonth() + 1);
|
||||||
|
var D = tme.getDate();
|
||||||
|
var h = tme.getHours();
|
||||||
|
var m = tme.getMinutes();
|
||||||
|
var s = tme.getSeconds();
|
||||||
|
var tem2 = + h + '时' + m + '分'
|
||||||
|
// + s +'秒'
|
||||||
|
return tem2;
|
||||||
|
}
|
||||||
|
// 时间戳转日期(时分 00:00格式)
|
||||||
|
export const toHHmmss= (data)=> {
|
||||||
|
var time;
|
||||||
|
var hours = parseInt((data % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
var minutes = parseInt((data % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
// var seconds = (data % (1000 * 60)) / 1000;
|
||||||
|
time = (hours < 10 ? ('0' + hours) : hours) + ':' + (minutes < 10 ? ('0' + minutes) : minutes) ;
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
// 错误toast提示
|
||||||
|
export const showToastErr = (title,mask=false) =>{
|
||||||
|
wx.showToast({
|
||||||
|
title: title,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 1500,
|
||||||
|
mask:mask
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function showToastErrMask(title){
|
||||||
|
|
||||||
|
wx.showToast({
|
||||||
|
title:title,
|
||||||
|
icon:"none",
|
||||||
|
duration:1500,
|
||||||
|
mask:true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// export function showToastErrMask(title){
|
||||||
|
// wx.showToast({
|
||||||
|
// title: title,
|
||||||
|
// icon:"none",
|
||||||
|
// duration:1500,
|
||||||
|
// mask:false
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// 成功toast提示
|
||||||
|
export const showToastOk = (title,mask=false) =>{
|
||||||
|
wx.showToast({
|
||||||
|
title: title,
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500,
|
||||||
|
mask:mask
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function showToastOkMask(title){
|
||||||
|
// 用于不需要控制用户点击频率时
|
||||||
|
wx.showToast({
|
||||||
|
title:title,
|
||||||
|
icon:"success",
|
||||||
|
duration:1500,
|
||||||
|
mask:true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 上传图片
|
||||||
|
export const uploadFile = (url,file)=>{
|
||||||
|
let token = wx.getStorageSync('token');
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.uploadFile({
|
||||||
|
url: baseUrl+ url,
|
||||||
|
filePath: file,
|
||||||
|
name: "file",
|
||||||
|
header: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
'token': token,
|
||||||
|
},
|
||||||
|
complete: (res) => {
|
||||||
|
if(res.statusCode == 200) {
|
||||||
|
resolve(res)
|
||||||
|
} else {
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 上传图片(选择图片)
|
||||||
|
export const selectPic = ()=>{
|
||||||
|
return new Promise((resolve,reject) => {
|
||||||
|
wx.chooseImage({
|
||||||
|
count: 4, //默认9
|
||||||
|
sizeType: ["original", "compressed"], //可以指定是原图还是压缩图,默认二者都有
|
||||||
|
sourceType: ["album", "camera"], //从相册选择
|
||||||
|
success: function (res) {
|
||||||
|
resolve(res)
|
||||||
|
},
|
||||||
|
fail:function(){
|
||||||
|
reject("选择文件失败")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 日期字符串添加年月日
|
||||||
|
export const times=(strTime, type)=> {
|
||||||
|
// 字符串转数组
|
||||||
|
var strTimes = strTime.split(type)
|
||||||
|
strTimes[0] = strTimes[0]
|
||||||
|
strTimes[1] = strTimes[1]
|
||||||
|
strTimes[2] = strTimes[2]
|
||||||
|
// 数组转字符串
|
||||||
|
strTimes = strTimes.join(type)
|
||||||
|
return strTimes
|
||||||
|
}
|
||||||
|
export const times1=(strTime, type)=> {
|
||||||
|
// 字符串转数组
|
||||||
|
var strTimes = strTime.split(type)
|
||||||
|
strTimes[0] = strTimes[0] + '年'
|
||||||
|
strTimes[1] = strTimes[1] + '月'
|
||||||
|
strTimes[2] = strTimes[2] + '日'
|
||||||
|
strTimes[2] = strTimes[3] + '时'
|
||||||
|
strTimes[2] = strTimes[4] + '分'
|
||||||
|
strTimes[2] = strTimes[5] + '秒'
|
||||||
|
// 数组转字符串
|
||||||
|
strTimes = strTimes.join(type)
|
||||||
|
return strTimes
|
||||||
|
}
|
||||||
|
// 登录
|
||||||
|
export const login = () => {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
wx.login({
|
||||||
|
success: function (res) {
|
||||||
|
if (res.code) {
|
||||||
|
resolve(res);
|
||||||
|
} else {
|
||||||
|
reject(res);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: function (err) {
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 模态框
|
||||||
|
export const Modal=(title,content)=> {
|
||||||
|
return new Promise((resolve,reject)=>{
|
||||||
|
wx.showModal({
|
||||||
|
title: title,
|
||||||
|
content: content,
|
||||||
|
success: function(res) {
|
||||||
|
if(res.confirm) {
|
||||||
|
resolve(res.confirm)
|
||||||
|
} else if(res.cancel) {
|
||||||
|
reject(res.cancel)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: function(rej){
|
||||||
|
reject(rej)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 获取个人信息
|
||||||
|
export const getUserInfo=()=> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.getUserInfo({
|
||||||
|
desc: '用于完善会员资料',
|
||||||
|
success: function (res) {
|
||||||
|
if (res) {
|
||||||
|
resolve(res);
|
||||||
|
} else {
|
||||||
|
reject(res);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: function (err) {
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
export function navigateTo(url) {
|
||||||
|
wx.navigateTo({
|
||||||
|
url: `/pages/${url}/${url}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export function reLaunch(url) {
|
||||||
|
wx.reLaunch({
|
||||||
|
url: `/pages/${url}/${url}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期转时间戳
|
||||||
|
export const date_stamp=(time)=> new Date(time).getTime()/1000
|
||||||
|
|
||||||
|
export function travelTree(tree,arr) {
|
||||||
|
for (let item of tree) {
|
||||||
|
arr.push(item.label);
|
||||||
|
if (item.children&& item.children.length) travelTree(item.children,arr);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
export function getToday(){
|
||||||
|
let date=new Date();
|
||||||
|
let year=date.getFullYear();
|
||||||
|
let month=date.getMonth();
|
||||||
|
let day=date.getDate();
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重定向到某个页面
|
||||||
|
export function redirectTo(nextUrl){
|
||||||
|
wx.redirectTo({
|
||||||
|
url: nextUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export function showloading(title=""){
|
||||||
|
wx.showLoading({
|
||||||
|
title:title,
|
||||||
|
mask:true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export function hideloading(){
|
||||||
|
wx.hideLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户授权权限
|
||||||
|
export function getLimit(){
|
||||||
|
return new Promise((resolve,reject)=>{
|
||||||
|
wx.getSetting({
|
||||||
|
withSubscriptions:true,
|
||||||
|
success:function(res){
|
||||||
|
if(res){
|
||||||
|
resolve(res)
|
||||||
|
}else{
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail:(rej)=>{
|
||||||
|
reject(rej)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启订阅
|
||||||
|
export function subscribe(id){
|
||||||
|
return new Promise((resolve,reject)=>{
|
||||||
|
wx.requestSubscribeMessage({
|
||||||
|
tmplIds: id,
|
||||||
|
success:function(res){
|
||||||
|
if(res){
|
||||||
|
resolve(res)
|
||||||
|
}else{
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail:function(rej){
|
||||||
|
reject(rej)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chooseMsgFile=()=>{
|
||||||
|
return new Promise((resolve,reject)=>{
|
||||||
|
wx.chooseMessageFile({
|
||||||
|
count: 100,
|
||||||
|
type:"all",
|
||||||
|
success(res){
|
||||||
|
if(res){
|
||||||
|
resolve(res)
|
||||||
|
}else{
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail(rej){
|
||||||
|
reject(rej)
|
||||||
|
},
|
||||||
|
complete(res){
|
||||||
|
if(res){
|
||||||
|
resolve(res)
|
||||||
|
}else{
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拍摄或者从相册上传
|
||||||
|
export const chooseMedia=(count=9)=>{
|
||||||
|
return new Promise((resolve,reject)=>{
|
||||||
|
wx.chooseMedia({
|
||||||
|
count:count,
|
||||||
|
mediaType:["image"],
|
||||||
|
sourceType:['album', 'camera'],
|
||||||
|
camera:"back",
|
||||||
|
success(res){
|
||||||
|
if(res){
|
||||||
|
resolve(res)
|
||||||
|
}else{
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail(rej){
|
||||||
|
reject(rej)
|
||||||
|
},
|
||||||
|
complete(res){
|
||||||
|
if(res){
|
||||||
|
resolve(res)
|
||||||
|
}else{
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
3
src/utils/uploadFile.ts
Normal file
3
src/utils/uploadFile.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const uploadFile = (url: string, options) => {
|
||||||
|
console.log('options: ', options)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user