init
This commit is contained in:
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>
|
||||
266
pages/chat/components/WaveDisplay.vue
Normal file
266
pages/chat/components/WaveDisplay.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<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 AMPLIFY = 1.6; // 振幅放大
|
||||
const center = centerIndex.value;
|
||||
|
||||
// 如果没有传入音频数据,则使用模拟数据(加强振幅)
|
||||
const audioData =
|
||||
props.audioData.length > 0
|
||||
? props.audioData.map((v) => Math.min(v * AMPLIFY, 1))
|
||||
: Array(center + 1)
|
||||
.fill(0)
|
||||
.map(() => Math.random() * 0.7 + 0.3); // 模拟值更明显
|
||||
|
||||
for (let i = 0; i <= center; i++) {
|
||||
const leftIndex = center - i;
|
||||
const rightIndex = center + i;
|
||||
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到42rpx之间)
|
||||
const height = 2 + value * 38;
|
||||
// // 动态颜色
|
||||
// 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>
|
||||
1099
pages/chat/components/ai-paging.vue
Normal file
1099
pages/chat/components/ai-paging.vue
Normal file
File diff suppressed because it is too large
Load Diff
48
pages/chat/components/fileIcon.vue
Normal file
48
pages/chat/components/fileIcon.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<image v-if="type === 'application/pdf'" :src="pdfIcon" class="file-icon" />
|
||||
<image
|
||||
v-else-if="
|
||||
type === 'application/msword' ||
|
||||
type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
"
|
||||
:src="docIcon"
|
||||
class="file-icon"
|
||||
/>
|
||||
<image
|
||||
v-else-if="
|
||||
type === 'application/vnd.ms-powerpoint' ||
|
||||
type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
"
|
||||
:src="pptIcon"
|
||||
class="file-icon"
|
||||
/>
|
||||
<image v-else-if="type === 'text/markdown'" :src="mdIcon" class="file-icon" />
|
||||
<image v-else-if="type === 'text/plain'" :src="txtIcon" class="file-icon" />
|
||||
<image v-else-if="type === 'text/html'" :src="htmlIcon" class="file-icon" />
|
||||
<image
|
||||
v-else-if="
|
||||
type === 'application/vnd.ms-excel' ||
|
||||
type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
"
|
||||
:src="excelIcon"
|
||||
class="file-icon"
|
||||
/>
|
||||
<image v-else :src="otherIcon" class="file-icon" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import pdfIcon from '@/static/file/pdf.png';
|
||||
import docIcon from '@/static/file/doc.png';
|
||||
import pptIcon from '@/static/file/ppt.png';
|
||||
import mdIcon from '@/static/file/md.png';
|
||||
import txtIcon from '@/static/file/txt.png';
|
||||
import htmlIcon from '@/static/file/html.png';
|
||||
import excelIcon from '@/static/file/excel.png';
|
||||
import otherIcon from '@/static/file/other.png';
|
||||
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
});
|
||||
|
||||
const type = props.type;
|
||||
</script>
|
||||
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>
|
||||
197
pages/chat/components/popupbadFeeback.vue
Normal file
197
pages/chat/components/popupbadFeeback.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom" borderRadius="12px 12px 0 0" background-color="#F6F6F6">
|
||||
<view class="feeback">
|
||||
<view class="titile">反馈</view>
|
||||
<view class="pop-h3">针对问题</view>
|
||||
<view class="pop-content">
|
||||
<view
|
||||
class="item"
|
||||
:class="{ active: item.check }"
|
||||
@click="toggleCheck(item.id)"
|
||||
v-for="item in wt"
|
||||
:key="item.id"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="pop-h3">针对回答</view>
|
||||
<view class="pop-content">
|
||||
<view
|
||||
class="item"
|
||||
:class="{ active: item.check }"
|
||||
@click="toggleCheck(item.id)"
|
||||
v-for="item in hd"
|
||||
:key="item.id"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="pop-h3">我要补充</view>
|
||||
<view class="supplement">
|
||||
<textarea v-model="inputText" cols="30" rows="10"></textarea>
|
||||
</view>
|
||||
<view class="btn button-click" @click="send">提交</view>
|
||||
<view class="close-btn" @click="close"></view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, defineEmits } from 'vue';
|
||||
const emit = defineEmits(['onSend']);
|
||||
const { $api } = inject('globalFunction');
|
||||
const popup = ref(null);
|
||||
const inputText = ref('');
|
||||
const wt = ref([
|
||||
{ label: '上下文错误', id: 1, check: false },
|
||||
{ label: '理解错误', id: 2, check: false },
|
||||
{ label: '未识别问题中的错误', id: 3, check: false },
|
||||
]);
|
||||
const hd = ref([
|
||||
{ label: '违法有害', id: 11, check: false },
|
||||
{ label: '内容不专业', id: 12, check: false },
|
||||
{ label: '推理错误', id: 13, check: false },
|
||||
{ label: '计算错误', id: 14, check: false },
|
||||
{ label: '内容不完整', id: 15, check: false },
|
||||
{ label: '事实错误', id: 16, check: false },
|
||||
]);
|
||||
|
||||
function open() {
|
||||
resetCheck();
|
||||
inputText.value = '';
|
||||
popup.value.open();
|
||||
}
|
||||
|
||||
function close() {
|
||||
popup.value.close();
|
||||
}
|
||||
|
||||
function send() {
|
||||
const text = getLabel();
|
||||
if (text) {
|
||||
emit('onSend', text);
|
||||
// close();
|
||||
} else {
|
||||
$api.msg('清输入反馈内容');
|
||||
}
|
||||
}
|
||||
|
||||
function getLabel() {
|
||||
const wtArr = wt.value.filter((item) => item.check).map((item) => item.label);
|
||||
const hdArr = hd.value.filter((item) => item.check).map((item) => item.label);
|
||||
let str = '';
|
||||
wtArr.length ? (str += `问题:${wtArr.join(',')}. `) : '';
|
||||
hdArr.length ? (str += `回答:${hdArr.join(',')}. `) : '';
|
||||
inputText.value ? (str += `描述:${inputText.value}. `) : '';
|
||||
return str;
|
||||
}
|
||||
|
||||
function resetCheck() {
|
||||
wt.value = wt.value.map((item) => {
|
||||
return { ...item, check: false }; // 创建新对象,确保更新响应式
|
||||
});
|
||||
hd.value = hd.value.map((item) => {
|
||||
return { ...item, check: false }; // 创建新对象,确保更新响应式
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCheck(id) {
|
||||
if (id < 10) {
|
||||
const newWt = wt.value.map((item) => {
|
||||
if (item.id === id) {
|
||||
return { ...item, check: !item.check };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
wt.value = newWt;
|
||||
} else {
|
||||
const newHd = hd.value.map((item) => {
|
||||
if (item.id === id) {
|
||||
return { ...item, check: !item.check };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
hd.value = newHd;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.feeback
|
||||
padding: 38rpx 32rpx;
|
||||
.titile
|
||||
font-weight: 500;
|
||||
font-size: 36rpx;
|
||||
color: #333333;
|
||||
line-height: 42rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 20rpx;
|
||||
.pop-h3
|
||||
font-weight: 600;
|
||||
font-size: 32rpx;
|
||||
color: #000000;
|
||||
line-height: 38rpx;
|
||||
text-align: left;
|
||||
padding: 8rpx 0
|
||||
margin-top: 32rpx
|
||||
.pop-content
|
||||
.item
|
||||
width: fit-content;
|
||||
height: 80rpx;
|
||||
background: #E8EAEE;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
text-align: center;
|
||||
line-height: 80rpx;
|
||||
padding: 0 36rpx
|
||||
border: 2rpx solid transparent;
|
||||
display: inline-block
|
||||
margin-right: 28rpx
|
||||
margin-top: 28rpx
|
||||
.active
|
||||
border: 2rpx solid #256BFA;
|
||||
color: #256BFA;
|
||||
.supplement
|
||||
height: 200rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx 20rpx 20rpx 20rpx;
|
||||
margin-top: 28rpx
|
||||
padding: 20rpx 24rpx
|
||||
.btn
|
||||
height: 90rpx;
|
||||
background: #256BFA;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
font-weight: 500;
|
||||
font-size: 32rpx;
|
||||
color: #FFFFFF;
|
||||
line-height: 90rpx;
|
||||
text-align: center;
|
||||
margin-top: 62rpx
|
||||
.close-btn
|
||||
position: absolute;
|
||||
right: 32rpx;
|
||||
top: 32rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
.close-btn::before
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
content: '';
|
||||
width: 4rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 2rpx
|
||||
background: #5A5A68;
|
||||
transform: translate(50%, -50%) rotate(-45deg) ;
|
||||
.close-btn::after
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
content: '';
|
||||
width: 4rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 2rpx
|
||||
background: #5A5A68;
|
||||
transform: translate(50%, -50%) rotate(45deg)
|
||||
</style>
|
||||
Reference in New Issue
Block a user