fix: 修复按钮问题

This commit is contained in:
韩庆伟 2025-05-21 15:55:44 +08:00
parent 9f24fec9d6
commit 316e8101c3
6 changed files with 272 additions and 65 deletions

Binary file not shown.

View File

@ -1,6 +1,8 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
const { statusBarHeight } = useStatus()
import { useStatus } from '@/store/status'
onLaunch(() => {
console.log('App Launch')
@ -18,7 +20,18 @@ onHide(() => {
button::after {
border: none;
}
/*解决阅览图片关闭按钮会显示在状态栏区域的问题*/
#u-a-p > div > div {
margin-top: var(--statusBarHeight);
}
/*不显示滚动条的类*/
.no-scroll {
-ms-overflow-style: none; /* IE 和 Edge */
scrollbar-width: none; /* Firefox */
}
.no-scroll::-webkit-scrollbar {
display: none; /* Webkit 浏览器 */
}
swiper,
scroll-view {
flex: 1;

View File

@ -8,7 +8,7 @@
</route>
<template>
<div class="flex flex-col h-screen bg-#ffffff">
<div class="flex flex-col h-screen bg-#ffffff tops">
<!-- Navigation Bar -->
<div
class="flex-none flex items-center justify-between px-5 py-3 bg-white shadow-md h-20 pt-10 z-999 fixed top-0 w-full box-border"
@ -76,6 +76,7 @@
msg.role === 'assistant'
? 'bg-[#f9f8fd] text-black shadow '
: 'bg-[#45299e] text-white ',
msg.type === 'text' ? 'pr-0' : 'pr-3',
]"
>
<wd-loading
@ -140,6 +141,12 @@
:key="fileIdx"
style="flex: 0 0 6rem"
class="relative text-xs h-12 px-2 py-2 rounded-md overflow-hidden mr-1 bg-white c-black"
:class="{
'w-25 h-15': msg.content.length == 1,
'w-50 h-15': msg.content.length == 2,
'w-30 h-15 ': msg.content.length == 3,
'h-15 flex-grow-0 flex-shrink-0 basis-10 mr-1': msg.content.length >= 4,
}"
@click="previewFile(file.url)"
>
<view>{{ file.name }}</view>
@ -193,11 +200,25 @@
<div
:class="[
'fixed bottom-0 left-0 right-0 bg-white z-[80] overflow-hidden transition-all duration-300',
showActions ? (uploadList.length ? 'h-60' : 'h-40') : 'h-20',
showActions ? (uploadList.length ? 'h-40' : 'h-40') : 'h-20',
]"
>
<!-- 上传列表 -->
<div v-if="uploadList.length" class="flex px-4 py-2 overflow-x-auto space-x-3 bg-transparent">
<div
v-if="uploadList.length"
:class="[
'flex px-4 py-2 overflow-x-auto space-x-3 bg-transparent',
showActions
? uploadList.length
? ' fixed bottom-40'
: 'fixed bottom-19'
: 'fixed bottom-19',
]"
:style="{
transform: `rotate(0deg)`,
transition: 'transform 0.3s ease',
}"
>
<div
v-for="item in uploadList"
:key="item.id"
@ -250,8 +271,8 @@
</div>
</div>
</div>
<!-- 知识库-->
<!-- 知识库-->
<div
@click="toggleKnowledge"
v-if="!uploadList.length"
@ -280,6 +301,7 @@
<view class="flex items-center px-4 py-2.5 border-t border-t-solid border-[#E7E7E7]">
<input
v-model="inputText"
@focus="onFocus"
@keyup.enter="sendText('')"
placeholder="想对我说点什么~"
class="flex-1 h-10 px-3 border border-gray-100 bg-[#f9f9f9] rounded-1 focus:outline-none"
@ -308,8 +330,8 @@
<view
v-show="showActions"
:class="[
'flex justify-around items-center h-20 bg-white border-t border-t-solid border-[#E7E7E7]',
showActions ? (uploadList.length ? 'pt-1' : 'pt-1') : 'pt-0',
'flex justify-around items-center h-10 bg-white border-t border-t-solid border-[#E7E7E7]',
showActions ? (uploadList.length ? 'pt-10' : 'pt-10') : 'pt-0',
]"
>
<view class="flex flex-col items-center">
@ -401,6 +423,7 @@ interface IMessage {
timestamp: Date
}
const userAvatar = ref('')
const chatMode = ref('tongyi-app')
const baseUrl = getEnvBaseUrl()
const messages = reactive<IMessage[]>([])
@ -474,7 +497,7 @@ async function goChat(listUuid: string) {
await fetchHistoryDiets(listUuid)
showPopup.value = false
}
// qwen-vl-plus
/** 1. 创建聊天会话 */
const listUuid = ref('')
@ -484,18 +507,15 @@ async function createChatSession() {
url: `${baseUrl}/chat/create`,
method: 'POST',
data: {
gptModel: 'gpt-4-vision-preview',
gptModel: chatMode.value,
},
header: {
Authorization: token.value,
},
})
// listUuid
console.log('createChatSession →', createResp)
listUuid.value = createResp.data.data.listUuid
} catch (err) {
console.error('createChatSession error:', err)
}
} catch (err) {}
}
// ------------------
// API
@ -532,21 +552,145 @@ async function fetchHistoryDiets(value) {
method: 'POST',
data: {
listUuid: value,
gptModel: 'gpt-4-vision-preview',
gptModel: chatMode.value,
},
header: {
Authorization: token.value,
},
})
if (resp.data) {
rawList.value = resp.data
console.log('fetchHistoryLisssst →', rawList.value)
console.log(resp, '/**************resp*********************/')
if (resp && resp.data) {
const rawList = resp.data.data //
const newMessages = parseBackendMessages(rawList)
//
messages.splice(0, messages.length, ...newMessages)
}
} catch (err) {
console.error('fetchHistoryList error:', err)
}
}
const token = ref<string>('')
function parseBackendMessages(rawList: any[]): IMessage[] {
const messageList: IMessage[] = []
rawList.forEach((item) => {
const rawContent: string = item.text ?? item.content ?? item.message ?? ''
// map[...]
let parts: ParsedPart[] | null = tryParseMapFormat(rawContent)
// map[...] JSON
if (!parts && rawContent.startsWith('[') && rawContent.endsWith(']')) {
try {
const arr = JSON.parse(rawContent) as Array<{ text: string; type: string }>
if (Array.isArray(arr)) {
parts = arr.map((el) => {
if (el.type === 'text') {
return { type: 'text', content: el.text }
}
if (el.type === 'image_url' || el.type === 'image') {
return {
type: 'image',
content: [{ url: el.text, uploadFileType: 'image' }],
}
}
//
return { type: el.type, content: el.text }
})
}
} catch {
// fallback
}
}
if (parts) {
//
parts.forEach((part) => {
messageList.push({
role: item.role,
type: part.type as any, // 'text' 'image'
content: part.content,
timestamp: (item.CreatedAt ?? 0) * 1000,
})
})
} else {
// fallback contenttype 'text'
messageList.push({
role: item.role,
type: 'text',
content: rawContent,
timestamp: (item.CreatedAt ?? 0) * 1000,
})
}
})
return messageList
}
interface ParsedPart {
type: string
content: string | UploadFile[]
}
function tryParseMapFormat(str: string): ParsedPart[] | null {
if (!str || !str.startsWith('[') || !str.endsWith(']') || !str.includes('map[')) {
return null // [map[...] ...]
}
const result: ParsedPart[] = []
// map[...]
const pattern = /map\[([^\]]+)\]/g
let match: RegExpExecArray | null
while ((match = pattern.exec(str)) !== null) {
const contentBlock = match[1] // "text: type:text"
const typeIndex = contentBlock.indexOf(' type:')
if (typeIndex === -1) {
continue // ' type:'
}
//
const contentPart = contentBlock.substring(0, typeIndex).trim() // "text:"
const typeValue = contentBlock.substring(typeIndex + 6).trim() // " type:" "text" "image_url"
//
const colonIndex = contentPart.indexOf(':')
if (colonIndex === -1) {
continue
}
const key = contentPart.substring(0, colonIndex).trim() // "text" "image_url"
let value = contentPart.substring(colonIndex + 1).trim() // 值,比如 "这种" 或 "https://cdn.xx/xxx.jpg"
//
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.substring(1, value.length - 1)
}
// ParsedPart
if (typeValue === 'text' && key === 'text') {
result.push({
type: 'text',
content: value,
})
} else if (typeValue === 'image_url' && key === 'image_url') {
// URL UploadFile
result.push({
type: 'image', // 'image'
content: [{ url: value, uploadFileType: 'image' }],
})
} else {
//
result.push({
type: typeValue,
content: value,
})
}
}
// null
return result.length > 0 ? result : null
}
const token = ref<string>(
'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941ca1430937103230a1e32a1715f569f3efdbe6f8cb8b7b8642bd679668081b9b08f693d1b5be6002d936ec51e1e3e0c4927de9e32ac99a109b326e5d2bda27ec87624bb416ec70d2a95a2e190feeba9f0d6bae8571b3dfe89c824712344759a8f2bff9d70747c52525cf6a5614f9c770bca461a9b9c247b6dca97bcf83bbaf99bb726752c4fe1e9a4aa7de5c4cf3e88a3e480801280d45cdc124f9d8221105d852945dc6ce10bc1647e4f09dff4d52ffdfc878a18b3738809e20de39acfd5430450f2bc3741057dd4ce71ccf64ea02f6a91fd001fa7bde90187008f19e848c70a002c37df28be05b4790e962f001a1361e90f1423dfc5b018ca9fac85ad2fafaef6',
)
const userInfo = ref<any>({})
const refreshToken = ref<string>('')
const statusBarHeight = ref<number>(0)
@ -558,7 +702,7 @@ onMounted(() => {
const init = async () => {
const wv = plus.webview.currentWebview()
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) || {}
refreshToken.value = wv.refreshToken || uni.getStorageSync('refreshToken')
statusBarHeight.value = wv.statusBarHeight || uni.getSystemInfoSync().statusBarHeight
@ -568,23 +712,6 @@ onMounted(() => {
await fetchHistoryList()
}
init()
// // 2. Plus plusready
// if (window.plus && plus.webview) {
// document.addEventListener('plusready', init, false)
// // plusready
// if (plus.webview.currentWebview()) {
// init()
// }
// }
// // 3. H5 storage/SystemInfo
// else {
// token.value = uni.getStorageSync('token') || import.meta.env.VITE_DEV_TOKEN
// userInfo.value = uni.getStorageSync('userInfo')
// refreshToken.value = uni.getStorageSync('refreshToken')
// statusBarHeight.value = uni.getSystemInfoSync().statusBarHeight
// createChatSession().then(fetchHistoryList)
// }
})
function scrollToBottom() {
@ -651,6 +778,7 @@ async function newChat() {
}
const rotation = ref(0)
function toggleActions() {
closeKeyboard()
showActions.value = !showActions.value
rotation.value += 45
scrollToBottom()
@ -661,6 +789,7 @@ const uploadFileTypeEm = {
image: 'image',
video: 'video',
file: 'file',
text: 'text',
}
//
@ -914,7 +1043,11 @@ const msgLoading = ref(true)
async function sendText(msgData) {
msgLoading.value = true
const text = inputText.value.trim()
if (!text) {
const dataBlo = toRaw(msgData)
console.log(dataBlo)
if (!text && dataBlo == '') {
msgLoading.value = false
uni.showToast({ title: '请输入信息', icon: 'error' })
return
@ -935,12 +1068,14 @@ async function sendText(msgData) {
}
//
addMessage({
role: 'user',
type: 'text',
content: text,
timestamp: new Date(),
})
addMessage(
msgData || {
role: 'user',
type: 'text',
content: text,
timestamp: new Date(),
},
)
//
if (tempUploadList.length > 0) {
@ -969,11 +1104,12 @@ async function sendText(msgData) {
showImageMask.value = showMoreImgMask
} else {
//
historyUserMsgs.push({
role: 'user',
content: text,
timestamp: new Date(),
})
!msgData &&
historyUserMsgs.push({
role: 'user',
content: text,
timestamp: new Date(),
})
}
inputText.value = ''
@ -987,19 +1123,19 @@ async function sendText(msgData) {
timestamp: new Date(),
}
addMessage(aiMsg)
//
uploadList.splice(0, uploadList.length)
// resds.value = historyUserMsgs
const body: IGptRequestBody = {
model: 'gpt-4-vision-preview',
model: chatMode.value,
max_tokens: 1000,
temperature: 1,
listUuid: listUuid.value,
top_p: 1,
presence_penalty: 0,
frequency_penalty: 0,
messages: msgData || historyUserMsgs,
messages: msgData ? [msgData] : historyUserMsgs,
stream: true,
}
@ -1011,6 +1147,7 @@ async function sendText(msgData) {
headers: { 'Content-Type': 'application/json', Authorization: token.value },
body: JSON.stringify(body),
})
const reader = resp.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
@ -1021,14 +1158,17 @@ async function sendText(msgData) {
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', aiMsg.content)
const chunk = part.trim()
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 {
@ -1045,13 +1185,16 @@ async function sendText(msgData) {
}
} catch {}
}
//
done && historyUserMsgs.push(aiMsg)
}
}
//
historyUserMsgs.push(aiMsg)
scrollToBottom()
} catch (err) {
aiMsg.content = '请重新发送'
//messages
messages[messages.length - 1] = { ...aiMsg }
console.error(err)
} finally {
loading.value = false
@ -1075,18 +1218,35 @@ function copyText(msg: IMessage) {
}
function refreshText() {
const lastUserMsg = historyUserMsgs[historyUserMsgs.length - 2]
// resds.value = lastUserMsg
const lastUserMsg = historyUserMsgs[historyUserMsgs.length - 1]
lastUserMsg.timestamp = new Date()
sendText(lastUserMsg)
}
const knowledgeOpen = ref(false)
function toggleKnowledge() {
console.error('44444', chatMode.value)
if (chatMode.value == 'tongyi-app') {
chatMode.value = 'qwen-vl-plus'
} else {
chatMode.value = 'tongyi-app'
}
knowledgeOpen.value = !knowledgeOpen.value
showActions.value = false
rotation.value = 0
}
//
const closeKeyboard = () => {
uni.hideKeyboard()
rotation.value = 0
}
//
const onFocus = () => {
showActions.value = false
rotation.value = 0
}
</script>
<style lang="scss" scoped>
@ -1191,4 +1351,8 @@ function toggleKnowledge() {
font-size: 28rpx;
color: #333;
}
.tops {
padding-top: var(--status-bar-height);
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<view class="flex flex-col h-screen bg-#ffffff">
<view class="flex flex-col h-screen bg-#ffffff tops">
<view
class="flex-none flex items-center justify-between px-5 py-3 bg-white shadow-md h-20 pt-10 z-999 fixed top-0 w-full box-border"
>
@ -64,4 +64,8 @@ onMounted(() => {
})
</script>
<style scoped></style>
<style scoped>
.tops {
padding-top: var(--status-bar-height);
}
</style>

15
src/store/status/index.js Normal file
View File

@ -0,0 +1,15 @@
import {ref} from 'vue'
import {createGlobalState, useStorage} from "@vueuse/core";
import {uniStorage} from "@/utils/uniStorage";
export const useStatus =createGlobalState(()=>{
const currentNavbar=ref({title:'',url:''})
const applyTabbarIndex=ref(0)
const statusBarHeight = ref(window?.plus?.navigator?.getStatusbarHeight() ?? 0)
const tabBarIndex = useStorage('tabBarIndex', 0, uniStorage)
return {
statusBarHeight,
applyTabbarIndex,
currentNavbar,
tabBarIndex
}
})

11
src/utils/uniStorage.js Normal file
View File

@ -0,0 +1,11 @@
export const uniStorage = {
getItem(key) {
return uni.getStorageSync(key) || null
},
setItem(key, value) {
return uni.setStorageSync(key, value)
},
removeItem(key) {
return uni.removeStorageSync(key)
},
}