flat:AI+
This commit is contained in:
221
pages/chat/chat.vue
Normal file
221
pages/chat/chat.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 抽屉遮罩层 -->
|
||||
<view v-if="isDrawerOpen" class="overlay" @click="toggleDrawer"></view>
|
||||
|
||||
<!-- 抽屉窗口 -->
|
||||
<view class="drawer" :class="{ open: isDrawerOpen }">
|
||||
<view class="drawer-content">
|
||||
<view class="drawer-title">历史对话</view>
|
||||
<scroll-view scroll-y :show-scrollbar="false" class="chat-scroll">
|
||||
<view
|
||||
class="drawer-rows"
|
||||
@click="changeDialogue(item)"
|
||||
v-for="(item, index) in tabeList"
|
||||
:key="item.id"
|
||||
>
|
||||
<view
|
||||
v-if="!item.isTitle"
|
||||
class="drawer-row-list"
|
||||
:class="{ 'drawer-row-active': item.sessionId === chatSessionID }"
|
||||
>
|
||||
{{ item.title }}
|
||||
</view>
|
||||
<view class="drawer-row-title" v-else>
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主要内容,挤压效果 -->
|
||||
<view class="main-content" :class="{ shift: isDrawerOpen }">
|
||||
<!-- header -->
|
||||
<header class="head">
|
||||
<view class="main-header">
|
||||
<image src="/static/icon/Hamburger-button.png" @click="toggleDrawer"></image>
|
||||
<view class="title">青岛市岗位推荐</view>
|
||||
<image src="/static/icon/Comment-one.png" @click="addNewDialogue"></image>
|
||||
</view>
|
||||
</header>
|
||||
<view class="chatmain-warpper">
|
||||
<ai-paging ref="paging"></ai-paging>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, nextTick } from 'vue';
|
||||
const { $api, navTo } = inject('globalFunction');
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import useChatGroupDBStore from '@/stores/userChatGroupStore';
|
||||
import aiPaging from './components/ai-paging.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
const { isTyping, tabeList, chatSessionID } = storeToRefs(useChatGroupDBStore());
|
||||
const isDrawerOpen = ref(false);
|
||||
const scrollIntoView = ref(false);
|
||||
import config from '@/config';
|
||||
|
||||
const paging = ref(null);
|
||||
|
||||
onLoad(() => {
|
||||
// useChatGroupDBStore().getHistory();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
nextTick(() => {
|
||||
paging.value?.colseFile();
|
||||
});
|
||||
});
|
||||
|
||||
const toggleDrawer = () => {
|
||||
isDrawerOpen.value = !isDrawerOpen.value;
|
||||
};
|
||||
|
||||
const addNewDialogue = () => {
|
||||
$api.msg('新对话');
|
||||
useChatGroupDBStore().addNewDialogue();
|
||||
};
|
||||
|
||||
const changeDialogue = (item) => {
|
||||
if (item.sessionId) {
|
||||
paging.value?.closeGuess();
|
||||
useChatGroupDBStore().changeDialogue(item);
|
||||
toggleDrawer();
|
||||
nextTick(() => {
|
||||
paging.value?.scrollToBottom();
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
/* 页面容器 */
|
||||
.container {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 遮罩层 */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 抽屉窗口 */
|
||||
.drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0; /* 如果要右侧弹出改为 right: 0; */
|
||||
width: 500rpx;
|
||||
height: 100vh;
|
||||
background: #e7e7e6;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* 抽屉展开 */
|
||||
.drawer.open {
|
||||
box-shadow: 4rpx 0 20rpx rgba(0, 0, 0, 0.2);
|
||||
transform: translateX(0);
|
||||
|
||||
}
|
||||
|
||||
/* 抽屉内容 */
|
||||
.drawer-content
|
||||
height: 100%
|
||||
.drawer-title
|
||||
height: calc(88rpx + env(safe-area-inset-top));
|
||||
line-height: calc(88rpx + env(safe-area-inset-top));
|
||||
padding: 0 20rpx;
|
||||
background: rgba(71, 120, 236, 1);
|
||||
color: #FFFFFF;
|
||||
font-size: 30rpx
|
||||
.chat-scroll
|
||||
height: calc(100% - 88rpx + env(safe-area-inset-top));
|
||||
.drawer-rows
|
||||
padding: 0 20rpx;
|
||||
// border-bottom: 2rpx dashed #e8e8e8
|
||||
overflow:hidden; //超出的文本隐藏
|
||||
text-overflow:ellipsis; //溢出用省略号显示
|
||||
white-space:nowrap; //溢出不换行
|
||||
.drawer-row-title
|
||||
color: #5d5d5d;
|
||||
font-weight: bold;
|
||||
font-size: 24rpx
|
||||
line-height: 88rpx
|
||||
height: 88rpx
|
||||
margin-top: 16rpx
|
||||
// border-bottom: 2rpx dashed #5d5d5d
|
||||
.drawer-row-list
|
||||
height: 66rpx;
|
||||
line-height: 66rpx
|
||||
color: #000000
|
||||
font-size: 28rpx
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
.drawer-row-active
|
||||
.drawer-row-list:active
|
||||
background: #DCDCDB
|
||||
border-radius: 8rpx
|
||||
padding: 0 10rpx
|
||||
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
// background: #f8f8f8;
|
||||
transition: margin-left 0.3s ease-in-out;
|
||||
position: relative
|
||||
background: #FFFFFF
|
||||
.head
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
height: calc(88rpx + env(safe-area-inset-top));
|
||||
user-select: none;
|
||||
.main-header
|
||||
position: fixed;
|
||||
left: var(--window-left);
|
||||
right: var(--window-right);
|
||||
height: calc(88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(14rpx + env(safe-area-inset-top));
|
||||
border: 2rpx solid #F4F4F4;
|
||||
background: #FFFFFF
|
||||
z-index: 998;
|
||||
transition-property: all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between
|
||||
font-size: 28rpx
|
||||
color: #000000
|
||||
padding: 0 30rpx;
|
||||
font-weight: bold
|
||||
text-align: center
|
||||
image
|
||||
width: 36rpx;
|
||||
height: 37rpx;
|
||||
|
||||
.chatmain-warpper
|
||||
height: calc(100% - 88rpx - env(safe-area-inset-top));
|
||||
position: relative;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
/* 页面被挤压时向右移动 */
|
||||
.main-content.shift {
|
||||
margin-left: 500rpx;
|
||||
}
|
||||
</style>
|
66
pages/chat/components/AudioWave.vue
Normal file
66
pages/chat/components/AudioWave.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<view class="wave-container" :style="{ background }">
|
||||
<view v-for="(bar, index) in bars" :key="index" class="bar" :style="getBarStyle(index)" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
background: {
|
||||
type: String,
|
||||
default: 'linear-gradient(to right, #377dff, #9a60ff)',
|
||||
},
|
||||
});
|
||||
|
||||
// 默认参数(不暴露)
|
||||
const barCount = 20;
|
||||
const barWidth = 4;
|
||||
const barHeight = 40;
|
||||
const barRadius = 2;
|
||||
const duration = 1200;
|
||||
const gap = 4;
|
||||
|
||||
const bars = computed(() => new Array(barCount).fill(0));
|
||||
|
||||
const getBarStyle = (index) => {
|
||||
const delay = (index * (duration / barCount)) % duration;
|
||||
return {
|
||||
width: `${barWidth}rpx`,
|
||||
height: `${barHeight}rpx`,
|
||||
background: '#fff',
|
||||
borderRadius: `${barRadius}rpx`,
|
||||
animation: `waveAnim ${duration}ms ease-in-out ${delay}ms infinite`,
|
||||
transformOrigin: 'bottom center',
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wave-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16rpx;
|
||||
border-radius: 36rpx;
|
||||
height: calc(102rpx - 40rpx);
|
||||
gap: 4rpx;
|
||||
/* background: linear-gradient(90deg, #9e74fd 0%, #256bfa 100%); */
|
||||
box-shadow: 0rpx 8rpx 40rpx 0rpx rgba(0, 54, 170, 0.15);
|
||||
}
|
||||
|
||||
@keyframes waveAnim {
|
||||
0%,
|
||||
100% {
|
||||
transform: scaleY(0.4);
|
||||
}
|
||||
50% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
</style>
|
783
pages/chat/components/ai-paging.vue
Normal file
783
pages/chat/components/ai-paging.vue
Normal file
@@ -0,0 +1,783 @@
|
||||
<template>
|
||||
<view class="chat-container">
|
||||
<FadeView :show="!messages.length" :duration="600">
|
||||
<view class="chat-background">
|
||||
<image class="backlogo" src="/static/icon/backAI.png"></image>
|
||||
<view class="back-rowTitle">嗨!欢迎使用青岛AI智能求职</view>
|
||||
<view class="back-rowText">
|
||||
我可以根据您的简历和求职需求,帮你精准匹配青岛市互联网招聘信息,对比招聘信息的优缺点,提供面试指导等,请把你的任务交给我吧~
|
||||
</view>
|
||||
<view class="back-rowh3">猜你所想</view>
|
||||
<view class="back-rowmsg">我希望找青岛的IT行业岗位,薪资能否在12000以上?</view>
|
||||
<view class="back-rowmsg">我有三年的工作经验,能否推荐一些适合我的青岛的国企 岗位?</view>
|
||||
</view>
|
||||
</FadeView>
|
||||
<scroll-view class="chat-list scrollView" :scroll-top="scrollTop" :scroll-y="true" scroll-with-animation>
|
||||
<FadeView :show="messages.length" :duration="600">
|
||||
<view class="chat-list list-content">
|
||||
<view
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
:id="'msg-' + index"
|
||||
class="chat-item"
|
||||
:class="{ self: msg.self }"
|
||||
>
|
||||
<text class="message" v-if="msg.self">
|
||||
<view class="msg-filecontent" v-if="msg.files.length">
|
||||
<view
|
||||
class="msg-files"
|
||||
v-for="(file, vInex) in msg.files"
|
||||
:key="vInex"
|
||||
@click="jumpUrl(file)"
|
||||
>
|
||||
<image class="msg-file-icon" src="/static/icon/Vector2.png"></image>
|
||||
<text class="msg-file-text">{{ file.name || '附件' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
{{ msg.displayText }}
|
||||
</text>
|
||||
<text class="message" :class="{ messageNull: !msg.displayText }" v-else>
|
||||
<!-- {{ msg.displayText }} -->
|
||||
<md-render :content="msg.displayText"></md-render>
|
||||
<!-- guess -->
|
||||
<view
|
||||
class="guess"
|
||||
v-if="showGuess && !msg.self && messages.length - 1 === index && msg.displayText"
|
||||
>
|
||||
<view class="guess-top">
|
||||
<image class="guess-icon" src="/static/icon/tips2.png" mode=""></image>
|
||||
猜你所想
|
||||
</view>
|
||||
<view class="gulist">
|
||||
<view
|
||||
class="guess-list"
|
||||
@click="sendMessageGuess(item)"
|
||||
v-for="(item, index) in guessList"
|
||||
:key="index"
|
||||
>
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="isTyping" :class="{ self: true }">
|
||||
<text class="message msg-loading">
|
||||
<span class="ai-loading"></span>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</FadeView>
|
||||
</scroll-view>
|
||||
<view class="vio_container" :class="status" v-if="status !== 'idle'">
|
||||
<view class="record-tip">{{ statusText }}</view>
|
||||
<AudioWave :background="audiowaveStyle" />
|
||||
</view>
|
||||
<view class="input-area" v-else>
|
||||
<view class="areatext">
|
||||
<input
|
||||
v-model="textInput"
|
||||
placeholder-class="inputplaceholder"
|
||||
class="input"
|
||||
@confirm="sendMessage"
|
||||
:disabled="isTyping"
|
||||
:adjust-position="false"
|
||||
placeholder="请输入您的职位名称、薪资要求、岗位地址"
|
||||
v-if="!isVoice"
|
||||
/>
|
||||
<view
|
||||
class="input_vio"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
@touchcancel="handleTouchCancel"
|
||||
type="default"
|
||||
v-else
|
||||
>
|
||||
按住说话
|
||||
</view>
|
||||
<!-- upload -->
|
||||
<view class="btn-box" @click="changeShowFile">
|
||||
<image
|
||||
class="send-btn"
|
||||
:class="{ 'add-file-btn': showfile }"
|
||||
src="/static/icon/addGroup.png"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<!-- sendmessgae Button-->
|
||||
<view class="btn-box purple" v-if="textInput && !isTyping" @click="sendMessage">
|
||||
<image class="send-btn" src="/static/icon/send3.png"></image>
|
||||
</view>
|
||||
<view class="btn-box" v-else-if="!isTyping && !isVoice" @click="changeVoice">
|
||||
<image class="send-btn" src="/static/icon/send2x.png"></image>
|
||||
</view>
|
||||
<view class="btn-box" v-else-if="!isTyping" @click="changeVoice">
|
||||
<image class="send-btn" src="/static/icon/send4.png"></image>
|
||||
</view>
|
||||
<view class="btn-box" v-else>
|
||||
<image class="send-btn" src="/static/icon/send2xx.png"></image>
|
||||
</view>
|
||||
</view>
|
||||
<!-- btn -->
|
||||
<CollapseTransition :show="showfile">
|
||||
<view class="area-file">
|
||||
<image class="file-img" @click="uploadCamera" src="/static/icon/carmreupload.png"></image>
|
||||
<image class="file-img" @click="getUploadFile" src="/static/icon/fileupload.png"></image>
|
||||
<image class="file-img" @click="uploadCamera('album')" src="/static/icon/imgupload.png"></image>
|
||||
</view>
|
||||
</CollapseTransition>
|
||||
|
||||
<!-- filelist -->
|
||||
<view class="area-uploadfiles" v-if="filesList.length">
|
||||
<scroll-view class="uploadfiles-scroll" scroll-x="true">
|
||||
<view class="uploadfiles-list">
|
||||
<view class="file-uploadsend" v-for="(file, index) in filesList" :key="index">
|
||||
<image
|
||||
class="file-icon"
|
||||
@click="jumpUrl(file)"
|
||||
v-if="isImage(file.type)"
|
||||
src="/static/icon/image.png"
|
||||
></image>
|
||||
<image
|
||||
class="file-icon"
|
||||
@click="jumpUrl(file)"
|
||||
v-if="isFile(file.type)"
|
||||
src="/static/icon/doc.png"
|
||||
></image>
|
||||
<text class="filename-text">{{ file.name }}</text>
|
||||
<view class="file-del" catchtouchmove="true" @click="delfile(file)">
|
||||
<uni-icons type="closeempty" color="#4B4B4B" size="10"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, nextTick, defineProps, defineEmits, onMounted, toRaw, reactive, computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import config from '@/config.js';
|
||||
import useChatGroupDBStore from '@/stores/userChatGroupStore';
|
||||
const { $api, navTo, throttle } = inject('globalFunction');
|
||||
const emit = defineEmits(['onConfirm']);
|
||||
import MdRender from '@/components/md-render/md-render.vue';
|
||||
const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGroupDBStore());
|
||||
import CollapseTransition from '@/components/CollapseTransition/CollapseTransition.vue';
|
||||
import FadeView from '@/components/FadeView/FadeView.vue';
|
||||
import AudioWave from './AudioWave.vue';
|
||||
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
||||
|
||||
const { isRecording, recognizedText, startRecording, stopRecording, cancelRecording } = useAudioRecorder(
|
||||
config.vioceBaseURl
|
||||
);
|
||||
|
||||
const guessList = ref([]);
|
||||
const scrollTop = ref(0);
|
||||
const showGuess = ref(false);
|
||||
const showfile = ref(false);
|
||||
const filesList = ref([]);
|
||||
const bgText = ref(false);
|
||||
const isVoice = ref(false);
|
||||
const status = ref('idle'); // idle | recording | cancel
|
||||
const startY = ref(0);
|
||||
const cancelThreshold = 100;
|
||||
let recordingTimer = null;
|
||||
|
||||
const state = reactive({
|
||||
uploadFileTips: '请根据以上附件,帮我推荐岗位。',
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
const sendMessage = () => {
|
||||
const values = textInput.value;
|
||||
showfile.value = false;
|
||||
showGuess.value = false;
|
||||
if (values.trim()) {
|
||||
// 判断是否有对话ID // 没有则创建对话列表
|
||||
const callback = () => {
|
||||
const normalArr = toRaw(filesList.value); // 转换为普通数组
|
||||
filesList.value = [];
|
||||
const newMsg = { text: values, self: true, displayText: values, files: normalArr };
|
||||
useChatGroupDBStore().addMessage(newMsg);
|
||||
useChatGroupDBStore()
|
||||
.getStearm(values, normalArr, scrollToBottom)
|
||||
.then(() => {
|
||||
console.log(messages);
|
||||
getGuess();
|
||||
scrollToBottom();
|
||||
});
|
||||
emit('onConfirm', values);
|
||||
textInput.value = '';
|
||||
scrollToBottom();
|
||||
};
|
||||
// 没有对话列表则创建
|
||||
if (!chatSessionID.value) {
|
||||
useChatGroupDBStore()
|
||||
.addTabel(values)
|
||||
.then((res) => {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
if (filesList.value.length) {
|
||||
$api.msg('上传文件请输入想问的问题描述');
|
||||
} else {
|
||||
$api.msg('请输入职位信息或描述');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessageGuess = (item) => {
|
||||
showGuess.value = false;
|
||||
textInput.value = item;
|
||||
sendMessage(item);
|
||||
};
|
||||
|
||||
const delfile = (file) => {
|
||||
uni.showModal({
|
||||
content: '确认删除文件?',
|
||||
success() {
|
||||
filesList.value = filesList.value.filter((item) => item.url !== file.url);
|
||||
if (!filesList.value.length) {
|
||||
if (textInput.value === state.uploadFileTips) {
|
||||
textInput.value = '';
|
||||
}
|
||||
}
|
||||
$api.msg('附件删除成功');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const scrollToBottom = throttle(function () {
|
||||
nextTick(() => {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
const query = uni.createSelectorQuery();
|
||||
query.select('.scrollView').boundingClientRect();
|
||||
query.select('.list-content').boundingClientRect();
|
||||
query.exec((res) => {
|
||||
const scrollViewHeight = res[0].height;
|
||||
const scrollContentHeight = res[1].height;
|
||||
if (scrollContentHeight > scrollViewHeight) {
|
||||
const scrolldistance = scrollContentHeight - scrollViewHeight;
|
||||
scrollTop.value = scrolldistance;
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
|
||||
function getGuess() {
|
||||
$api.chatRequest('/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => {
|
||||
guessList.value = res.data;
|
||||
showGuess.value = true;
|
||||
nextTick(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isImage(fileNmae) {
|
||||
return new RegExp('image').test(fileNmae);
|
||||
}
|
||||
|
||||
function isFile(fileNmae) {
|
||||
return new RegExp('custom-doc').test(fileNmae);
|
||||
}
|
||||
|
||||
function jumpUrl(file) {
|
||||
if (file.url) {
|
||||
window.open(file.url);
|
||||
} else {
|
||||
$api.msg('文件地址丢失');
|
||||
}
|
||||
}
|
||||
|
||||
function VerifyNumberFiles(num) {
|
||||
if (filesList.value.length >= config.allowedFileNumber) {
|
||||
$api.msg(`最大上传文件数量 ${config.allowedFileNumber} 个`);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function uploadCamera(type = 'camera') {
|
||||
if (VerifyNumberFiles()) return;
|
||||
uni.chooseImage({
|
||||
count: 1, //默认9
|
||||
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
|
||||
sourceType: [type], //从相册选择
|
||||
success: function (res) {
|
||||
const tempFilePaths = res.tempFilePaths;
|
||||
const file = res.tempFiles[0];
|
||||
// 继续上传
|
||||
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
|
||||
resData = JSON.parse(resData);
|
||||
if (isImage(file.type)) {
|
||||
filesList.value.push({
|
||||
url: resData.msg,
|
||||
type: file.type,
|
||||
name: file.name,
|
||||
});
|
||||
textInput.value = state.uploadFileTips;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getUploadFile(type = 'camera') {
|
||||
if (VerifyNumberFiles()) return;
|
||||
uni.chooseFile({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
const tempFilePaths = res.tempFilePaths;
|
||||
const file = res.tempFiles[0];
|
||||
const allowedTypes = config.allowedFileTypes || [];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return $api.msg('仅支持 txt md html word pdf ppt csv excel 格式类型');
|
||||
}
|
||||
// 继续上传
|
||||
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
|
||||
resData = JSON.parse(resData);
|
||||
filesList.value.push({
|
||||
url: resData.msg,
|
||||
type: 'custom-doc',
|
||||
name: file.name,
|
||||
});
|
||||
textInput.value = state.uploadFileTips;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const handleTouchStart = (e) => {
|
||||
startY.value = e.touches[0].clientY;
|
||||
status.value = 'recording';
|
||||
showfile.value = false;
|
||||
startRecording();
|
||||
};
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
const moveY = e.touches[0].clientY;
|
||||
if (startY.value - moveY > cancelThreshold) {
|
||||
status.value = 'cancel';
|
||||
} else {
|
||||
status.value = 'recording';
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (status.value === 'cancel') {
|
||||
console.log('取消发送');
|
||||
cancelRecording();
|
||||
} else {
|
||||
stopRecording();
|
||||
console.log('发送语音');
|
||||
}
|
||||
status.value = 'idle';
|
||||
};
|
||||
|
||||
const handleTouchCancel = () => {
|
||||
stopRecording();
|
||||
status.value = 'idle';
|
||||
};
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (status.value) {
|
||||
case 'recording':
|
||||
return '松手发送,上划取消';
|
||||
case 'cancel':
|
||||
return '松手取消';
|
||||
default:
|
||||
return '按住说话';
|
||||
}
|
||||
});
|
||||
|
||||
const audiowaveStyle = computed(() => {
|
||||
return status.value === 'cancel'
|
||||
? '#f54545'
|
||||
: status.value === 'recording'
|
||||
? 'linear-gradient(to right, #377dff, #9a60ff)'
|
||||
: '#f1f1f1';
|
||||
});
|
||||
|
||||
function closeGuess() {
|
||||
showGuess.value = false;
|
||||
}
|
||||
|
||||
function changeVoice() {
|
||||
isVoice.value = !isVoice.value;
|
||||
}
|
||||
|
||||
function changeShowFile() {
|
||||
showfile.value = !showfile.value;
|
||||
}
|
||||
|
||||
function colseFile() {
|
||||
showfile.value = false;
|
||||
}
|
||||
defineExpose({ scrollToBottom, closeGuess, colseFile });
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
/* 过渡样式 */
|
||||
.collapse-enter-active,
|
||||
.collapse-leave-active {
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.collapse-enter-from,
|
||||
.collapse-leave-to {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.collapse-enter-to,
|
||||
.collapse-leave-from {
|
||||
max-height: 400rpx; /* 根据你内容最大高度设定 */
|
||||
opacity: 1;
|
||||
}
|
||||
.msg-filecontent
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
.msg-files
|
||||
overflow: hidden
|
||||
margin-right: 10rpx
|
||||
height: 30rpx
|
||||
max-width: 201rpx
|
||||
background: #FFFFFF
|
||||
border-radius: 10rpx
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: flex-start
|
||||
padding: 10rpx
|
||||
color: #000000
|
||||
margin-bottom: 10rpx
|
||||
.msg-file-icon
|
||||
width: 29rpx
|
||||
height: 26rpx
|
||||
padding-right: 10rpx
|
||||
.msg-file-text
|
||||
flex: 1
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
color: rgba(96, 96, 96, 1)
|
||||
font-size: 24rpx
|
||||
.msg-files:active
|
||||
background: #e9e9e9
|
||||
.guess
|
||||
border-top: 2rpx solid #8c8c8c
|
||||
padding: 20rpx 0 10rpx 0
|
||||
.guess-top
|
||||
padding: 0 0 10rpx 0
|
||||
display: flex
|
||||
align-items: center
|
||||
color: rgba(255, 173, 71, 1)
|
||||
font-size: 28rpx
|
||||
.guess-icon
|
||||
width: 43rpx
|
||||
height: 43rpx
|
||||
.guess-list
|
||||
border: 2rpx solid #8c8c8c
|
||||
padding: 6rpx 12rpx
|
||||
border-radius: 10rpx;
|
||||
width: fit-content
|
||||
margin: 0 10rpx 10rpx 0
|
||||
font-size: 24rpx
|
||||
color: #8c8c8c
|
||||
.gulist
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
position: relative
|
||||
image-margin-top = 40rpx
|
||||
.chat-container
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
|
||||
position: relative
|
||||
z-index: 1
|
||||
background: #FFFFFF
|
||||
|
||||
.chat-background
|
||||
position: absolute
|
||||
padding: 44rpx;
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
.backlogo
|
||||
width: 313rpx;
|
||||
height: 190rpx;
|
||||
.back-rowTitle
|
||||
width: 100%;
|
||||
height: 56rpx;
|
||||
font-weight: bold;
|
||||
font-size: 40rpx;
|
||||
color: #333333;
|
||||
line-height: 47rpx;
|
||||
margin-top: 40rpx
|
||||
.back-rowText
|
||||
margin-top: 28rpx
|
||||
width: 100%;
|
||||
height: 144rpx;
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
border-bottom: 2rpx dashed rgba(0, 0, 0, 0.2);
|
||||
.back-rowh3
|
||||
width: 100%;
|
||||
height: 30rpx;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #000000;
|
||||
margin-top: 24rpx
|
||||
.back-rowmsg
|
||||
width: 630rpx
|
||||
margin-top: 20rpx
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
line-height: 28rpx;
|
||||
font-size: 24rpx
|
||||
background: #F6F6F6;
|
||||
border-radius: 8rpx 8rpx 8rpx 8rpx;
|
||||
padding: 32rpx 18rpx;
|
||||
|
||||
.chat-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.list-content {
|
||||
padding: 0 44rpx 44rpx 44rpx;
|
||||
}
|
||||
.chat-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.chat-item.self {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.message {
|
||||
margin-top: 40rpx
|
||||
padding: 20rpx 20rpx 0 20rpx;
|
||||
border-radius: 0 20rpx 20rpx 20rpx;
|
||||
background: #F6F6F6;
|
||||
// max-width: 80%;
|
||||
word-break: break-word;
|
||||
color: #333333;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
.messageNull
|
||||
background: transparent;
|
||||
.msg-loading{
|
||||
background: transparent;
|
||||
font-size: 24rpx;
|
||||
color: #8f8d8e;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.loaded{
|
||||
padding-left: 20rpx
|
||||
}
|
||||
.chat-item.self .message {
|
||||
background: linear-gradient( 225deg, #DAE2FE 0%, #E9E3FF 100%);
|
||||
border-radius: 20rpx 0 20rpx 20rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
.input-area {
|
||||
padding: 32rpx;
|
||||
position: relative;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
|
||||
transition: height 2s ease-in-out;
|
||||
}
|
||||
.input-area::after
|
||||
position: absolute
|
||||
content: ''
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
z-index: 1
|
||||
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
|
||||
.areatext{
|
||||
display: flex;
|
||||
}
|
||||
.input {
|
||||
flex: 1;
|
||||
border-radius: 5rpx;
|
||||
min-height: 63rpx;
|
||||
line-height: 63rpx;
|
||||
padding: 4rpx 24rpx;
|
||||
position: relative
|
||||
background: #F5F5F5;
|
||||
border-radius: 60rpx 60rpx 60rpx 60rpx;
|
||||
}
|
||||
.input_vio
|
||||
flex: 1;
|
||||
border-radius: 5rpx;
|
||||
min-height: 63rpx;
|
||||
line-height: 63rpx;
|
||||
padding: 4rpx 24rpx;
|
||||
// position: relative
|
||||
border-radius: 60rpx 60rpx 60rpx 60rpx;
|
||||
font-size: 28rpx
|
||||
color: #333333
|
||||
background: #F5F5F5;
|
||||
text-align: center
|
||||
font-size: 28rpx
|
||||
font-weight: 500
|
||||
-webkit-touch-callout:none;
|
||||
-webkit-user-select:none;
|
||||
-khtml-user-select:none;
|
||||
-moz-user-select:none;
|
||||
-ms-user-select:none;
|
||||
user-select:none;
|
||||
.input_vio:active
|
||||
background: #e8e8e8
|
||||
.vio_container
|
||||
background: transparent
|
||||
padding: 28rpx
|
||||
text-align: center
|
||||
.record-tip
|
||||
font-weight: 400;
|
||||
color: #909090;
|
||||
text-align: center;
|
||||
padding-bottom: 16rpx
|
||||
|
||||
|
||||
.inputplaceholder {
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #000000;
|
||||
line-height: 28rpx;
|
||||
opacity: 0.4
|
||||
}
|
||||
.btn-box
|
||||
margin-left: 12rpx;
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 50%
|
||||
background: #F5F5F5;
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
.send-btn,
|
||||
.receive-btn
|
||||
transition: transform 0.5s ease;
|
||||
width: 38rpx;
|
||||
height: 38rpx;
|
||||
.purple
|
||||
background: linear-gradient( 225deg, #9E74FD 0%, #256BFA 100%);
|
||||
.add-file-btn{
|
||||
transform: rotate(45deg)
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
.area-file
|
||||
display: grid
|
||||
width: 100%
|
||||
grid-template-columns: repeat(3, 1fr)
|
||||
grid-gap: 20rpx
|
||||
padding: 20rpx 0 0 0;
|
||||
.file-img
|
||||
height: 179rpx
|
||||
width: 100%
|
||||
.area-uploadfiles
|
||||
position: absolute
|
||||
top: -100rpx
|
||||
width: calc(100% - 40rpx)
|
||||
background: #FFFFFF
|
||||
left: 0
|
||||
padding: 10rpx 20rpx
|
||||
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
|
||||
.uploadfiles-scroll
|
||||
height: 100%
|
||||
.uploadfiles-list
|
||||
height: 100%
|
||||
display: flex
|
||||
flex-wrap: nowrap
|
||||
.file-uploadsend
|
||||
display: flex
|
||||
flex-wrap: nowrap
|
||||
justify-content: center
|
||||
align-items: center
|
||||
margin: 10rpx 18rpx 0 10rpx;
|
||||
height: 100%
|
||||
border-radius: 30rpx
|
||||
font-size: 24rpx
|
||||
position: relative
|
||||
width: 218rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
border: 2rpx solid #E2E2E2;
|
||||
.file-del
|
||||
position: absolute
|
||||
right: 0
|
||||
top: 0
|
||||
z-index: 9
|
||||
border-radius: 50%
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
transform: translate(50%, -10rpx)
|
||||
height: 40rpx
|
||||
width: 23rpx;
|
||||
height: 23rpx;
|
||||
background: #FFFFFF;
|
||||
border: 2rpx solid #E2E2E2;
|
||||
.file-del:active
|
||||
background: #e8e8e8
|
||||
.filename-text
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
color: #333333
|
||||
flex: 1
|
||||
font-weight: 500
|
||||
max-width: 100%
|
||||
.file-icon
|
||||
height: 40rpx
|
||||
width: 40rpx
|
||||
margin: 0 18rpx 0 18rpx
|
||||
|
||||
@keyframes ai-circle {
|
||||
0% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.ai-loading
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
background: 0 0;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid;
|
||||
border-color: #e5e5e5 #e5e5e5 #e5e5e5 #8f8d8e;
|
||||
-webkit-animation: ai-circle 1s linear infinite;
|
||||
animation: ai-circle 1s linear infinite;
|
||||
</style>
|
Reference in New Issue
Block a user