flat:4.7暂存
This commit is contained in:
@@ -37,9 +37,14 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="drawer-user">
|
||||
<image class="drawer-user-img" src="/static/icon/boy.png"></image>
|
||||
<text>用户123</text>
|
||||
<image class="drawer-user-setting" src="/static/icon/setting.png" mode=""></image>
|
||||
<image class="drawer-user-img" v-if="userInfo.age === '0'" src="/static/icon/boy.png"></image>
|
||||
<image class="drawer-user-img" v-else src="/static/icon/girl.png"></image>
|
||||
<text>{{ userInfo.name || '暂无用户名' }}</text>
|
||||
<image
|
||||
class="drawer-user-setting button-click"
|
||||
src="/static/icon/setting.png"
|
||||
@click="updateSetting"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -64,11 +69,13 @@
|
||||
<script setup>
|
||||
import { ref, inject, nextTick, computed } from 'vue';
|
||||
const { $api, navTo, insertSortData } = inject('globalFunction');
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
|
||||
import useChatGroupDBStore from '@/stores/userChatGroupStore';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
import aiPaging from './components/ai-paging.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
const { isTyping, tabeList, chatSessionID } = storeToRefs(useChatGroupDBStore());
|
||||
const { userInfo } = storeToRefs(useUserStore());
|
||||
const isDrawerOpen = ref(false);
|
||||
const scrollIntoView = ref(false);
|
||||
import config from '@/config';
|
||||
@@ -94,12 +101,17 @@ onShow(() => {
|
||||
});
|
||||
});
|
||||
|
||||
onHide(() => {
|
||||
paging.value?.handleTouchCancel();
|
||||
});
|
||||
|
||||
const toggleDrawer = () => {
|
||||
isDrawerOpen.value = !isDrawerOpen.value;
|
||||
};
|
||||
|
||||
const addNewDialogue = () => {
|
||||
$api.msg('新对话');
|
||||
paging.value?.changeQueries();
|
||||
useChatGroupDBStore().addNewDialogue();
|
||||
};
|
||||
|
||||
@@ -113,6 +125,9 @@ const changeDialogue = (item) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
function updateSetting() {
|
||||
$api.msg('该功能正在开发中,敬请期待后续更新!');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@@ -190,13 +205,14 @@ const changeDialogue = (item) => {
|
||||
position: relative
|
||||
margin-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/
|
||||
margin-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/
|
||||
color: #000000
|
||||
.drawer-user-img
|
||||
width: 57.2rpx;
|
||||
height: 57.2rpx
|
||||
margin-right: 20rpx
|
||||
.drawer-user-setting
|
||||
width: 57.2rpx;
|
||||
height: 57.2rpx
|
||||
width: 48rpx
|
||||
height: 48rpx
|
||||
position: absolute
|
||||
top: 50%
|
||||
right: 28rpx
|
||||
@@ -267,7 +283,7 @@ const changeDialogue = (item) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between
|
||||
font-size: 28rpx
|
||||
font-size: 32rpx
|
||||
color: #000000
|
||||
padding: 0 30rpx;
|
||||
font-weight: bold
|
||||
|
273
pages/chat/components/WaveDisplay.vue
Normal file
273
pages/chat/components/WaveDisplay.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<view class="wave-container active" :style="{ background }">
|
||||
<!-- 波形显示区域 -->
|
||||
<view class="wave">
|
||||
<view
|
||||
v-for="(bar, index) in waveBars"
|
||||
:key="index"
|
||||
class="wave-bar"
|
||||
:style="{
|
||||
height: `${bar.height}px`,
|
||||
backgroundColor: bar.color,
|
||||
borderRadius: `${bar.borderRadius}px`,
|
||||
}"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
// 是否激活显示
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 音频数据数组 (0-1之间的值)
|
||||
audioData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 是否显示录音信息
|
||||
showInfo: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 录音时间 (秒)
|
||||
recordingTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 是否显示取消提示
|
||||
showCancelTip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 是否处于取消状态
|
||||
isCanceling: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: 'linear-gradient(to right, #377dff, #9a60ff)',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:audioData']);
|
||||
|
||||
// 波形条数据
|
||||
const waveBars = ref([]);
|
||||
// 中心条索引
|
||||
const centerIndex = ref(0);
|
||||
// 动画帧ID
|
||||
let animationId = null;
|
||||
|
||||
// 格式化显示时间
|
||||
const formattedTime = computed(() => {
|
||||
const mins = Math.floor(props.recordingTime / 60)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
const secs = (props.recordingTime % 60).toString().padStart(2, '0');
|
||||
return `${mins}:${secs}`;
|
||||
});
|
||||
|
||||
// 录音提示文本
|
||||
const recordingTip = computed(() => {
|
||||
return props.isCanceling ? '松开取消' : '松手发送';
|
||||
});
|
||||
|
||||
// 初始化波形条
|
||||
const initWaveBars = () => {
|
||||
waveBars.value = [];
|
||||
// 创建31个波形条(奇数个,确保有真正的中心点)
|
||||
const barCount = 31;
|
||||
centerIndex.value = Math.floor(barCount / 2);
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
// 设置初始状态(中心条最高,向两侧递减)
|
||||
const distanceFromCenter = Math.abs(i - centerIndex.value);
|
||||
const initialHeight = Math.max(2, 20 - distanceFromCenter * 3);
|
||||
|
||||
waveBars.value.push({
|
||||
height: initialHeight,
|
||||
color: '#FFFFFF',
|
||||
borderRadius: 2,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新波形显示
|
||||
const updateWaveform = () => {
|
||||
if (!props.isActive) return;
|
||||
// 如果没有传入音频数据,则使用模拟数据
|
||||
const audioData =
|
||||
props.audioData.length > 0
|
||||
? props.audioData
|
||||
: Array(centerIndex.value + 1)
|
||||
.fill(0)
|
||||
.map(() => Math.random() * 0.5 + 0.2);
|
||||
// 从中间向两侧处理
|
||||
for (let i = 0; i <= centerIndex.value; i++) {
|
||||
// 左侧条索引
|
||||
const leftIndex = centerIndex.value - i;
|
||||
// 右侧条索引
|
||||
const rightIndex = centerIndex.value + i;
|
||||
|
||||
// 获取音频数据值 (归一化到0-1)
|
||||
const value = audioData[i] || 0;
|
||||
|
||||
// 更新左侧条
|
||||
if (leftIndex >= 0) {
|
||||
updateWaveBar(leftIndex, value);
|
||||
}
|
||||
|
||||
// 更新右侧条(避免重复更新中心条)
|
||||
if (rightIndex < waveBars.value.length && rightIndex !== leftIndex) {
|
||||
updateWaveBar(rightIndex, value);
|
||||
}
|
||||
}
|
||||
|
||||
// 继续动画
|
||||
animationId = requestAnimationFrame(updateWaveform);
|
||||
};
|
||||
|
||||
// 更新单个波形条
|
||||
const updateWaveBar = (index, value) => {
|
||||
// 动态高度 (4rpx到200rpx之间)
|
||||
const height = 2 + value * 98;
|
||||
|
||||
// // 动态颜色
|
||||
// let color;
|
||||
// if (props.isCanceling) {
|
||||
// color = '#F44336'; // 取消状态显示红色
|
||||
// } else {
|
||||
// const intensity = Math.min(1, value * 1.5);
|
||||
// const r = Math.floor(7 + intensity * 200);
|
||||
// const g = Math.floor(193 - intensity * 50);
|
||||
// const b = Math.floor(96 - intensity * 30);
|
||||
// color = `rgb(${r}, ${g}, ${b})`;
|
||||
// }
|
||||
let color = '#FFFFFF';
|
||||
// 动态圆角
|
||||
const borderRadius = Math.min(4, height * 0.4);
|
||||
|
||||
waveBars.value[index] = {
|
||||
height,
|
||||
color,
|
||||
borderRadius,
|
||||
};
|
||||
};
|
||||
|
||||
// 开始动画
|
||||
const startAnimation = () => {
|
||||
if (!animationId) {
|
||||
animationId = requestAnimationFrame(updateWaveform);
|
||||
}
|
||||
};
|
||||
|
||||
// 停止动画
|
||||
const stopAnimation = () => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听激活状态变化
|
||||
watch(
|
||||
() => props.isActive,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
startAnimation();
|
||||
} else {
|
||||
stopAnimation();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
initWaveBars();
|
||||
if (props.isActive) {
|
||||
startAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
stopAnimation();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.wave-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
padding: 16rpx;
|
||||
border-radius: 36rpx;
|
||||
height: calc(102rpx - 40rpx);
|
||||
gap: 4rpx;
|
||||
box-shadow: 0rpx 8rpx 40rpx 0rpx rgba(0, 54, 170, 0.15);
|
||||
overflow: hidden;
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.wave {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wave-bar {
|
||||
width: 6rpx;
|
||||
min-height: 4rpx;
|
||||
border-radius: 4rpx;
|
||||
margin: 0 3rpx;
|
||||
transition: height 0.3s ease-out, background-color 0.2s;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
.wave-bar:nth-child(3n) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.recording-info {
|
||||
position: absolute;
|
||||
bottom: -80rpx;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.recording-tip {
|
||||
font-size: 28rpx;
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.recording-time {
|
||||
font-size: 28rpx;
|
||||
color: #07c160;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.cancel-tip {
|
||||
position: absolute;
|
||||
bottom: -120rpx;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #f44336;
|
||||
}
|
||||
</style>
|
@@ -8,8 +8,12 @@
|
||||
我可以根据您的简历和求职需求,帮你精准匹配青岛市互联网招聘信息,对比招聘信息的优缺点,提供面试指导等,请把你的任务交给我吧~
|
||||
</view>
|
||||
<view class="back-rowh3">猜你所想</view>
|
||||
<view class="back-rowmsg">我希望找青岛的IT行业岗位,薪资能否在12000以上?</view>
|
||||
<view class="back-rowmsg">我有三年的工作经验,能否推荐一些适合我的青岛的国企 岗位?</view>
|
||||
<view class="back-rowmsg" v-for="item in queries" @click="sendMessageGuess(item)">
|
||||
{{ item }}
|
||||
</view>
|
||||
<view class="chat-item self" v-if="isRecording">
|
||||
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</FadeView>
|
||||
<scroll-view class="chat-list scrollView" :scroll-top="scrollTop" :scroll-y="true" scroll-with-animation>
|
||||
@@ -25,7 +29,7 @@
|
||||
<view class="message" v-if="msg.self">
|
||||
<view class="msg-filecontent" v-if="msg.files.length">
|
||||
<view
|
||||
class="msg-files"
|
||||
class="msg-files btn-light"
|
||||
v-for="(file, vInex) in msg.files"
|
||||
:key="vInex"
|
||||
@click="jumpUrl(file)"
|
||||
@@ -40,7 +44,39 @@
|
||||
<!-- {{ msg.displayText }} -->
|
||||
<view class="message-markdown">
|
||||
<md-render :content="msg.displayText"></md-render>
|
||||
<view class="message-controll"></view>
|
||||
<view class="message-controll" v-show="showControll(index)">
|
||||
<view class="controll-left">
|
||||
<image
|
||||
class="controll-icon btn-light"
|
||||
src="/static/icon/copy.png"
|
||||
@click="copyMarkdown(msg.displayText)"
|
||||
></image>
|
||||
<image
|
||||
class="controll-icon mar_le10 btn-light"
|
||||
src="/static/icon/feedback.png"
|
||||
@click="userGoodFeedback(msg)"
|
||||
></image>
|
||||
<image
|
||||
v-if="isSpeaking && !isPaused && speechIndex === index"
|
||||
class="controll-icon mar_le10 btn-light"
|
||||
src="/static/icon/stop.png"
|
||||
@click="stopMarkdown(msg.displayText, index)"
|
||||
></image>
|
||||
<image
|
||||
class="controll-icon mar_le10 btn-light"
|
||||
src="/static/icon/broadcast.png"
|
||||
@click="readMarkdown(msg.displayText, index)"
|
||||
v-else
|
||||
></image>
|
||||
</view>
|
||||
<view class="controll-right">
|
||||
<image
|
||||
class="controll-icon mar_ri10 btn-light"
|
||||
src="/static/icon/refresh.png"
|
||||
@click="refreshMarkdown(index)"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- guess -->
|
||||
<view
|
||||
@@ -60,6 +96,9 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="chat-item self" v-if="isRecording">
|
||||
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
|
||||
</view>
|
||||
<view v-if="isTyping" :class="{ self: true }">
|
||||
<text class="message msg-loading">
|
||||
<span class="ai-loading"></span>
|
||||
@@ -70,7 +109,12 @@
|
||||
</scroll-view>
|
||||
<view class="vio_container" :class="status" v-if="status !== 'idle'">
|
||||
<view class="record-tip">{{ statusText }}</view>
|
||||
<AudioWave :background="audiowaveStyle" />
|
||||
<WaveDisplay
|
||||
:background="audiowaveStyle"
|
||||
:isActive="isRecording"
|
||||
:audioData="audioDataForDisplay"
|
||||
:showInfo="isRecording"
|
||||
/>
|
||||
</view>
|
||||
<view class="input-area" v-else>
|
||||
<view class="areatext">
|
||||
@@ -82,7 +126,7 @@
|
||||
:disabled="isTyping"
|
||||
:adjust-position="false"
|
||||
placeholder="请输入您的职位名称、薪资要求、岗位地址"
|
||||
v-if="!isVoice"
|
||||
v-show="!isVoice"
|
||||
/>
|
||||
<view
|
||||
class="input_vio"
|
||||
@@ -90,8 +134,8 @@
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
@touchcancel="handleTouchCancel"
|
||||
v-show="isVoice"
|
||||
type="default"
|
||||
v-else
|
||||
>
|
||||
按住说话
|
||||
</view>
|
||||
@@ -114,8 +158,8 @@
|
||||
<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 class="btn-box purple" v-else>
|
||||
<view class="btn-box-round"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- btn -->
|
||||
@@ -153,15 +197,19 @@
|
||||
:src="file.url"
|
||||
></image>
|
||||
<view class="file-doc" @click="jumpUrl(file)" v-else>
|
||||
<view style="width: 100%">
|
||||
<FileIcon class="doc-icon" :type="file.type"></FileIcon>
|
||||
<view class="doc-con">
|
||||
<view class="filename-text">{{ file.name }}</view>
|
||||
<view class="filename-size">{{ file.size }}</view>
|
||||
<view class="filerow">
|
||||
<FileText :type="file.type"></FileText>
|
||||
<view class="row-x"></view>
|
||||
<view class="filename-size">{{ file.size }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<FileIcon :type="file.type"></FileIcon>
|
||||
<!-- <image class="file-icon" @click="jumpUrl(file)" :src=""></image> -->
|
||||
</view>
|
||||
<view class="file-del" catchtouchmove="true" @click="delfile(file)">
|
||||
<uni-icons type="closeempty" color="#4B4B4B" size="10"></uni-icons>
|
||||
<uni-icons type="closeempty" color="#FFFFFF" size="10"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -176,20 +224,36 @@ import { ref, inject, nextTick, defineProps, defineEmits, onMounted, toRaw, reac
|
||||
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 WaveDisplay from './WaveDisplay.vue';
|
||||
import FileIcon from './fileIcon.vue';
|
||||
import FileText from './fileText.vue';
|
||||
import { useSpeechReader } from '@/hook/useSpeechReader';
|
||||
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
||||
// 全局
|
||||
const { $api, navTo, throttle } = inject('globalFunction');
|
||||
const emit = defineEmits(['onConfirm']);
|
||||
const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGroupDBStore());
|
||||
|
||||
const { isRecording, recognizedText, startRecording, stopRecording, cancelRecording } = useAudioRecorder(
|
||||
config.vioceBaseURl
|
||||
);
|
||||
// hook
|
||||
const {
|
||||
isRecording,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
cancelRecording,
|
||||
audioDataForDisplay,
|
||||
volumeLevel,
|
||||
recognizedText,
|
||||
lastFinalText,
|
||||
} = useAudioRecorder(config.vioceBaseURl);
|
||||
|
||||
const { speak, pause, resume, isSpeaking, isPaused } = useSpeechReader();
|
||||
|
||||
// state
|
||||
const queries = ref([]);
|
||||
const guessList = ref([]);
|
||||
const scrollTop = ref(0);
|
||||
const showGuess = ref(false);
|
||||
@@ -200,18 +264,62 @@ const isVoice = ref(false);
|
||||
const status = ref('idle'); // idle | recording | cancel
|
||||
const startY = ref(0);
|
||||
const cancelThreshold = 100;
|
||||
let recordingTimer = null;
|
||||
const speechIndex = ref(0);
|
||||
const isAudioPermission = ref(false);
|
||||
|
||||
const state = reactive({
|
||||
uploadFileTips: '请根据以上附件,帮我推荐岗位。',
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
scrollToBottom();
|
||||
const statusText = computed(() => {
|
||||
switch (status.value) {
|
||||
case 'recording':
|
||||
return '松手发送,上划取消';
|
||||
case 'cancel':
|
||||
return '松手取消';
|
||||
default:
|
||||
return '按住说话';
|
||||
}
|
||||
});
|
||||
|
||||
const sendMessage = () => {
|
||||
const values = textInput.value;
|
||||
const audiowaveStyle = computed(() => {
|
||||
return status.value === 'cancel'
|
||||
? '#f54545'
|
||||
: status.value === 'recording'
|
||||
? 'linear-gradient(to right, #377dff, #9a60ff)'
|
||||
: '#f1f1f1';
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
changeQueries();
|
||||
scrollToBottom();
|
||||
isAudioPermission.value = await requestMicPermission();
|
||||
});
|
||||
|
||||
const requestMicPermission = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
console.log('✅ 麦克风权限已授权');
|
||||
|
||||
// 立刻停止所有音轨,释放麦克风
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('❌ 用户拒绝麦克风权限或不支持:', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function showControll(index) {
|
||||
if (index === messages.value.length - 1 && isTyping.value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const sendMessage = (text) => {
|
||||
const values = textInput.value || text;
|
||||
showfile.value = false;
|
||||
showGuess.value = false;
|
||||
if (values.trim()) {
|
||||
@@ -222,7 +330,9 @@ const sendMessage = () => {
|
||||
const newMsg = { text: values, self: true, displayText: values, files: normalArr };
|
||||
useChatGroupDBStore().addMessage(newMsg);
|
||||
useChatGroupDBStore()
|
||||
.getStearm(values, normalArr, scrollToBottom)
|
||||
.getStearm(values, normalArr, scrollToBottom, {
|
||||
onComplete: () => console.log('Display complete'),
|
||||
})
|
||||
.then(() => {
|
||||
getGuess();
|
||||
scrollToBottom();
|
||||
@@ -276,19 +386,17 @@ const delfile = (file) => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
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;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
@@ -386,14 +494,29 @@ function getUploadFile(type = 'camera') {
|
||||
});
|
||||
}
|
||||
|
||||
const handleTouchStart = (e) => {
|
||||
const tipsPermisson = () => {
|
||||
uni.showToast({
|
||||
title: '需要授权麦克风权限才能使用语音功能',
|
||||
icon: 'none',
|
||||
});
|
||||
};
|
||||
|
||||
const handleTouchStart = async (e) => {
|
||||
if (!isAudioPermission.value) {
|
||||
return tipsPermisson();
|
||||
}
|
||||
console.log('handleTouchStart');
|
||||
startY.value = e.touches[0].clientY;
|
||||
status.value = 'recording';
|
||||
showfile.value = false;
|
||||
startRecording();
|
||||
$api.sleep(1000).then(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
};
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
console.log('handleTouchMove');
|
||||
const moveY = e.touches[0].clientY;
|
||||
if (startY.value - moveY > cancelThreshold) {
|
||||
status.value = 'cancel';
|
||||
@@ -403,40 +526,32 @@ const handleTouchMove = (e) => {
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
console.log('handleTouchEnd');
|
||||
if (status.value === 'cancel') {
|
||||
console.log('取消发送');
|
||||
cancelRecording();
|
||||
} else {
|
||||
console.log('stopRecording');
|
||||
stopRecording();
|
||||
console.log('发送语音');
|
||||
$api.sleep(1000).then(() => {
|
||||
if (isAudioPermission.value) {
|
||||
if (recognizedText.value) {
|
||||
sendMessage(recognizedText.value);
|
||||
} else {
|
||||
$api.msg('说话时长太短');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
status.value = 'idle';
|
||||
};
|
||||
|
||||
const handleTouchCancel = () => {
|
||||
console.log('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;
|
||||
}
|
||||
@@ -452,7 +567,74 @@ function changeShowFile() {
|
||||
function colseFile() {
|
||||
showfile.value = false;
|
||||
}
|
||||
defineExpose({ scrollToBottom, closeGuess, colseFile });
|
||||
|
||||
function copyMarkdown(value) {
|
||||
$api.copyText(value);
|
||||
}
|
||||
|
||||
function userGoodFeedback(msg) {
|
||||
$api.msg('该功能正在开发中,敬请期待后续更新!');
|
||||
// const params = {
|
||||
// dataId: msg.dataId,
|
||||
// sessionId: msg.parentGroupId,
|
||||
// userGoodFeedback: 'no',
|
||||
// };
|
||||
}
|
||||
|
||||
function readMarkdown(value, index) {
|
||||
speechIndex.value = index;
|
||||
if (speechIndex.value !== index) {
|
||||
speak(value);
|
||||
return;
|
||||
}
|
||||
if (isPaused.value) {
|
||||
resume();
|
||||
} else {
|
||||
speak(value);
|
||||
}
|
||||
}
|
||||
function stopMarkdown(value, index) {
|
||||
pause(value);
|
||||
speechIndex.value = index;
|
||||
}
|
||||
function refreshMarkdown(index) {
|
||||
if (isTyping.value) {
|
||||
$api.msg('正在生成');
|
||||
} else {
|
||||
const text = messages.value[index - 1].text;
|
||||
sendMessageGuess(text);
|
||||
}
|
||||
}
|
||||
|
||||
const jobSearchQueries = [
|
||||
'青岛有哪些薪资 12K 以上的岗位适合我?',
|
||||
'青岛 3 年工作经验能找到哪些 12K 以上的工作?',
|
||||
'青岛哪些公司在招聘,薪资范围在 12K 以上?',
|
||||
'青岛有哪些企业提供 15K 以上的岗位?',
|
||||
'青岛哪些公司在招 3-5 年经验的岗位?',
|
||||
'我有三年的工作经验,能否推荐一些适合我的青岛的国企 岗位?',
|
||||
'青岛国企目前在招聘哪些岗位?',
|
||||
'青岛有哪些适合 3 年经验的国企岗位?',
|
||||
'青岛国企招聘的岗位待遇如何?',
|
||||
'青岛国企岗位的薪资水平是多少?',
|
||||
'青岛哪些国企支持双休 & 五险一金完善?',
|
||||
'青岛有哪些公司支持远程办公?',
|
||||
'青岛有哪些外企的岗位,薪资 12K 以上的多吗?',
|
||||
'青岛哪些企业在招聘 Web3.0 相关岗位?',
|
||||
'青岛哪些岗位支持海外远程?薪资如何?',
|
||||
'青岛招聘 AI/大数据相关岗位的公司有哪些?',
|
||||
];
|
||||
|
||||
function changeQueries(value) {
|
||||
queries.value = getRandomJobQueries(jobSearchQueries);
|
||||
}
|
||||
|
||||
function getRandomJobQueries(queries, count = 2) {
|
||||
const shuffled = queries.sort(() => 0.5 - Math.random()); // 随机打乱数组
|
||||
return shuffled.slice(0, count); // 取前 count 条
|
||||
}
|
||||
|
||||
defineExpose({ scrollToBottom, closeGuess, colseFile, changeQueries, handleTouchCancel });
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@@ -536,11 +718,11 @@ image-margin-top = 40rpx
|
||||
flex-direction: column
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
width: calc(100% - 88rpx)
|
||||
.backlogo
|
||||
width: 313rpx;
|
||||
height: 190rpx;
|
||||
.back-rowTitle
|
||||
width: 100%;
|
||||
height: 56rpx;
|
||||
font-weight: bold;
|
||||
font-size: 40rpx;
|
||||
@@ -550,27 +732,31 @@ image-margin-top = 40rpx
|
||||
.back-rowText
|
||||
margin-top: 28rpx
|
||||
width: 100%;
|
||||
height: 144rpx;
|
||||
height: 148rpx;
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
border-bottom: 2rpx dashed rgba(0, 0, 0, 0.2);
|
||||
line-height: 40rpx
|
||||
// border-bottom: 2rpx dashed rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 2rpx solid transparent;
|
||||
background-image: linear-gradient(to right, rgba(0, 0, 0, 0.2) 50%, transparent 50%);
|
||||
background-size: 10rpx 2rpx; /* 调整虚线宽度和间距 */
|
||||
background-repeat: repeat-x;
|
||||
background-position: 0 148rpx
|
||||
.back-rowh3
|
||||
width: 100%;
|
||||
height: 30rpx;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
font-size: 28rpx;
|
||||
color: #000000;
|
||||
margin-top: 24rpx
|
||||
margin-top: 28rpx
|
||||
.back-rowmsg
|
||||
width: 630rpx
|
||||
margin-top: 20rpx
|
||||
width: calc(100% - 32rpx)
|
||||
margin-top: 24rpx
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
color: rgba(51, 51, 51, 1);
|
||||
line-height: 28rpx;
|
||||
font-size: 24rpx
|
||||
background: #F6F6F6;
|
||||
background: rgba(246, 246, 246, 1);
|
||||
border-radius: 8rpx 8rpx 8rpx 8rpx;
|
||||
padding: 32rpx 18rpx;
|
||||
|
||||
@@ -586,6 +772,7 @@ image-margin-top = 40rpx
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
width: 100%;
|
||||
}
|
||||
.chat-item.self {
|
||||
justify-content: flex-end;
|
||||
@@ -602,6 +789,19 @@ image-margin-top = 40rpx
|
||||
border-radius: 0 20rpx 20rpx 20rpx;
|
||||
padding: 20rpx 20rpx 0 20rpx;
|
||||
background: #F6F6F6;
|
||||
.message-controll
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
border-top: 2rpx solid #EAEAEA
|
||||
padding: 24rpx 0
|
||||
margin-top: -10rpx
|
||||
.controll-left
|
||||
.controll-right
|
||||
.controll-icon
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
|
||||
.messageNull
|
||||
display: none
|
||||
.msg-loading{
|
||||
@@ -703,6 +903,11 @@ image-margin-top = 40rpx
|
||||
transition: transform 0.5s ease;
|
||||
width: 38rpx;
|
||||
height: 38rpx;
|
||||
.btn-box-round
|
||||
width: 22rpx
|
||||
height: 22rpx
|
||||
border-radius: 4rpx
|
||||
background: #FFFFFF
|
||||
.purple
|
||||
background: linear-gradient( 225deg, #9E74FD 0%, #256BFA 100%);
|
||||
.add-file-btn{
|
||||
@@ -725,7 +930,7 @@ image-margin-top = 40rpx
|
||||
text
|
||||
font-size: 24rpx
|
||||
font-weight: 500
|
||||
color: #000000
|
||||
color: rgba(0,0,0,.5)
|
||||
padding-top: 8rpx
|
||||
.card-img
|
||||
height: 56rpx
|
||||
@@ -736,38 +941,45 @@ image-margin-top = 40rpx
|
||||
.area-uploadfiles
|
||||
position: absolute
|
||||
top: -180rpx
|
||||
width: calc(100% - 40rpx)
|
||||
width: calc(100% - 30rpx)
|
||||
background: #FFFFFF
|
||||
left: 0
|
||||
padding: 10rpx 20rpx
|
||||
padding: 10rpx 0 10rpx 30rpx
|
||||
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
|
||||
.uploadfiles-scroll
|
||||
height: 100%
|
||||
.uploadfiles-list
|
||||
height: 100%
|
||||
display: flex
|
||||
margin-right: 28rpx
|
||||
flex-wrap: nowrap
|
||||
.file-doc
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: flex-start
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
justify-content: flex-start
|
||||
padding: 16rpx 20rpx 18rpx 20rpx
|
||||
height: calc(100% - 40rpx)
|
||||
.doc-icon
|
||||
width: 60rpx
|
||||
height: 76rpx
|
||||
margin-right: 20rpx
|
||||
.doc-con
|
||||
flex: 1
|
||||
width: 0
|
||||
.file-uploadsend
|
||||
margin: 10rpx 18rpx 0 10rpx;
|
||||
margin: 10rpx 18rpx 0 0;
|
||||
height: 100%
|
||||
border-radius: 30rpx
|
||||
font-size: 24rpx
|
||||
position: relative
|
||||
width: 360rpx;
|
||||
min-width: 460rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
border: 2rpx solid #E2E2E2;
|
||||
overflow: hidden
|
||||
.file-del
|
||||
position: absolute
|
||||
right: 0
|
||||
top: 0
|
||||
right: 25rpx
|
||||
top: 18rpx
|
||||
z-index: 9
|
||||
border-radius: 50%
|
||||
display: flex
|
||||
@@ -775,10 +987,10 @@ image-margin-top = 40rpx
|
||||
justify-content: center
|
||||
transform: translate(50%, -10rpx)
|
||||
height: 40rpx
|
||||
width: 23rpx;
|
||||
height: 23rpx;
|
||||
background: #FFFFFF;
|
||||
border: 2rpx solid #E2E2E2;
|
||||
width: 37rpx;
|
||||
height: 37rpx;
|
||||
background: #4B4B4B;
|
||||
border: 2rpx solid #FFFFFF;
|
||||
.file-del:active
|
||||
background: #e8e8e8
|
||||
.filename-text
|
||||
@@ -787,7 +999,6 @@ image-margin-top = 40rpx
|
||||
white-space: nowrap
|
||||
color: #333333
|
||||
font-size: 24rpx
|
||||
flex: 1
|
||||
font-weight: 500
|
||||
max-width: 100%
|
||||
.filename-size
|
||||
@@ -795,18 +1006,21 @@ image-margin-top = 40rpx
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
color: #7B7B7B;
|
||||
flex: 1
|
||||
max-width: 100%
|
||||
.file-icon
|
||||
height: 40rpx
|
||||
width: 40rpx
|
||||
.file-iconImg
|
||||
height: 100%
|
||||
width: 100%
|
||||
border-radius: 15rpx
|
||||
.filerow
|
||||
display: flex
|
||||
align-items: center
|
||||
margin-top: 7rpx
|
||||
.row-x
|
||||
margin: 0 18rpx
|
||||
height: 20rpx
|
||||
width: 2rpx
|
||||
background: rgba(226, 226, 226, .9)
|
||||
.file-border
|
||||
width: 160rpx;
|
||||
border: 0
|
||||
width: 160rpx !important;
|
||||
|
||||
@keyframes ai-circle {
|
||||
0% {
|
||||
|
@@ -46,5 +46,3 @@ const props = defineProps({
|
||||
|
||||
const type = props.type;
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
38
pages/chat/components/fileText.vue
Normal file
38
pages/chat/components/fileText.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<view class="file-type-box">
|
||||
{{ fileAbbreviation }}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
});
|
||||
|
||||
const fileAbbreviation = computed(() => {
|
||||
const typeMap = {
|
||||
'application/pdf': 'PDF',
|
||||
'application/msword': 'DOC',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX',
|
||||
'application/vnd.ms-powerpoint': 'PPT',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPTX',
|
||||
'application/vnd.ms-excel': 'XLS',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
|
||||
'text/markdown': 'MD',
|
||||
'text/plain': 'TXT',
|
||||
'text/html': 'HTML',
|
||||
};
|
||||
return typeMap[props.type] || 'OTHER';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-type-box {
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
color: #7b7b7b;
|
||||
line-height: 28rpx;
|
||||
}
|
||||
</style>
|
@@ -328,9 +328,9 @@ function getJobList(type = 'add') {
|
||||
pageState.total = resData.total;
|
||||
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
|
||||
if (rows.length < pageState.pageSize) {
|
||||
loadmoreRef.value.change('noMore');
|
||||
loadmoreRef.value?.change('noMore');
|
||||
} else {
|
||||
loadmoreRef.value.change('more');
|
||||
loadmoreRef.value?.change('more');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -56,16 +56,12 @@
|
||||
|
||||
<script setup>
|
||||
import { reactive, inject, watch, ref, onMounted } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
const { $api, navTo } = inject('globalFunction');
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
const userInfo = ref({});
|
||||
const Completion = ref({});
|
||||
const popup = ref(null);
|
||||
onShow(() => {
|
||||
userInfo.value = useUserStore().userInfo;
|
||||
Completion.value = useUserStore().Completion;
|
||||
});
|
||||
const { userInfo, Completion } = storeToRefs(useUserStore());
|
||||
|
||||
function logOut() {
|
||||
popup.value.open();
|
||||
|
Reference in New Issue
Block a user