Files
ks-app-employment-service/pages/chat/components/ai-paging.vue

1100 lines
35 KiB
Vue
Raw Normal View History

2025-03-28 15:19:42 +08:00
<template>
<view class="chat-container">
2025-05-13 11:42:17 +08:00
<!-- #ifdef MP-WEIXIN -->
<view class="chat-background">
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
2025-04-16 14:24:06 +08:00
<view class="chat-background" v-fade:600="!messages.length">
2025-05-13 11:42:17 +08:00
<!-- #endif -->
2025-04-16 14:24:06 +08:00
<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>
2025-05-13 11:10:38 +08:00
<view
class="back-rowmsg button-click"
v-for="(item, index) in queries"
:key="index"
@click="sendMessageGuess(item)"
>
2025-04-16 14:24:06 +08:00
{{ item }}
2025-03-28 15:19:42 +08:00
</view>
2025-04-16 14:24:06 +08:00
<view class="chat-item self" v-if="isRecording">
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
</view>
</view>
2025-03-28 15:19:42 +08:00
<scroll-view class="chat-list scrollView" :scroll-top="scrollTop" :scroll-y="true" scroll-with-animation>
2025-05-13 11:42:17 +08:00
<!-- #ifdef MP-WEIXIN -->
<view class="chat-list list-content">
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
2025-04-16 14:24:06 +08:00
<view class="chat-list list-content" v-fade:600="messages.length >= 1">
2025-05-13 11:42:17 +08:00
<!-- #endif -->
2025-04-16 14:24:06 +08:00
<view
v-for="(msg, index) in messages"
:key="index"
:id="'msg-' + index"
class="chat-item"
:class="{ self: msg.self }"
>
<view class="message" v-if="msg.self">
<view class="msg-filecontent" v-if="msg.files.length">
<view
class="msg-files btn-light"
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>
2025-03-28 15:19:42 +08:00
</view>
2025-03-29 11:51:48 +08:00
</view>
2025-04-16 14:24:06 +08:00
{{ msg.displayText }}
</view>
<view class="message" :class="{ messageNull: !msg.displayText }" v-else>
<!-- {{ msg.displayText }} -->
<view class="message-markdown">
<md-render
:content="msg.displayText"
:typing="isTyping && messages.length - 1 === index"
></md-render>
<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"
v-if="!msg.userBadFeedback"
@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>
2025-04-07 09:10:55 +08:00
</view>
2025-03-29 20:46:23 +08:00
</view>
2025-04-16 14:24:06 +08:00
</view>
<!-- guess -->
<view
class="guess"
v-if="showGuess && !msg.self && messages.length - 1 === index && msg.displayText"
>
<view class="gulist">
<view
class="guess-list"
@click="sendMessageGuess(item)"
v-for="(item, index) in guessList"
:key="index"
>
{{ item }}
2025-03-28 15:19:42 +08:00
</view>
</view>
2025-03-29 11:51:48 +08:00
</view>
2025-03-28 15:19:42 +08:00
</view>
</view>
2025-04-16 14:24:06 +08:00
<view class="chat-item self" v-if="isRecording">
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
</view>
<view v-if="isTyping" class="self">
<text class="message msg-loading">
<span class="ai-loading"></span>
</text>
</view>
</view>
2025-03-28 15:19:42 +08:00
</scroll-view>
2025-04-16 14:24:06 +08:00
<!-- 使用 v-show 保证不销毁事件 -->
<view class="vio_container" :class="status" v-show="status !== 'idle'">
2025-03-28 15:19:42 +08:00
<view class="record-tip">{{ statusText }}</view>
2025-04-07 09:10:55 +08:00
<WaveDisplay
:background="audiowaveStyle"
:isActive="isRecording"
:audioData="audioDataForDisplay"
:showInfo="isRecording"
/>
2025-03-28 15:19:42 +08:00
</view>
2025-04-16 14:24:06 +08:00
<view class="input-area" v-show="status === 'idle'">
2025-03-28 15:19:42 +08:00
<view class="areatext">
<input
v-model="textInput"
placeholder-class="inputplaceholder"
class="input"
@confirm="sendMessage"
:disabled="isTyping"
:adjust-position="false"
placeholder="请输入您的职位名称、薪资要求、岗位地址"
2025-04-07 09:10:55 +08:00
v-show="!isVoice"
2025-03-28 15:19:42 +08:00
/>
<view
class="input_vio"
2025-04-16 14:24:06 +08:00
@touchstart.prevent="handleTouchStart"
2025-03-28 15:19:42 +08:00
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
2025-04-10 10:59:25 +08:00
:catchtouchstart="true"
:catchtouchmove="true"
:catchtouchend="true"
2025-04-07 09:10:55 +08:00
v-show="isVoice"
2025-03-28 15:19:42 +08:00
type="default"
>
按住说话
</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>
2025-04-07 09:10:55 +08:00
<view class="btn-box purple" v-else>
<view class="btn-box-round"></view>
2025-03-28 15:19:42 +08:00
</view>
</view>
<!-- btn -->
<CollapseTransition :show="showfile">
2025-05-15 14:17:51 +08:00
<view class="area-tips">
<uni-icons type="info-filled" color="#ADADAD" size="15"></uni-icons>
上传后自动解析简历内容
</view>
2025-03-28 15:19:42 +08:00
<view class="area-file">
2025-03-28 18:19:05 +08:00
<view class="file-card" @click="uploadCamera">
<image class="card-img" src="/static/icon/file1.png"></image>
<text>拍照上传</text>
</view>
<view class="file-card" @click="uploadCamera('album')">
<image class="card-img" src="/static/icon/file2.png"></image>
<text>相册上传</text>
</view>
<view class="file-card" @click="getUploadFile">
<image class="card-img" src="/static/icon/file3.png"></image>
<text>文件上传</text>
</view>
2025-03-28 15:19:42 +08:00
</view>
</CollapseTransition>
<!-- filelist -->
<view class="area-uploadfiles" v-if="filesList.length">
<scroll-view class="uploadfiles-scroll" scroll-x="true">
<view class="uploadfiles-list">
2025-03-28 18:19:05 +08:00
<view
class="file-uploadsend"
:class="{ 'file-border': isImage(file.type) }"
v-for="(file, index) in filesList"
:key="index"
>
2025-03-28 15:19:42 +08:00
<image
2025-03-28 18:19:05 +08:00
class="file-iconImg"
2025-03-28 15:19:42 +08:00
@click="jumpUrl(file)"
v-if="isImage(file.type)"
2025-03-28 18:19:05 +08:00
:src="file.url"
2025-03-28 15:19:42 +08:00
></image>
2025-03-28 18:19:05 +08:00
<view class="file-doc" @click="jumpUrl(file)" v-else>
2025-04-07 09:10:55 +08:00
<FileIcon class="doc-icon" :type="file.type"></FileIcon>
<view class="doc-con">
2025-03-28 18:19:05 +08:00
<view class="filename-text">{{ file.name }}</view>
2025-04-07 09:10:55 +08:00
<view class="filerow">
<FileText :type="file.type"></FileText>
<view class="row-x"></view>
<view class="filename-size">{{ file.size }}</view>
</view>
2025-03-28 18:19:05 +08:00
</view>
<!-- <image class="file-icon" @click="jumpUrl(file)" :src=""></image> -->
</view>
2025-03-28 15:19:42 +08:00
<view class="file-del" catchtouchmove="true" @click="delfile(file)">
2025-04-07 09:10:55 +08:00
<uni-icons type="closeempty" color="#FFFFFF" size="10"></uni-icons>
2025-03-28 15:19:42 +08:00
</view>
</view>
</view>
</scroll-view>
</view>
</view>
2025-04-16 14:24:06 +08:00
<PopupFeeBack ref="feeback" @onSend="confirmFeeBack"></PopupFeeBack>
<MsgTips ref="feeBackTips" content="已收到反馈,感谢您的关注" title="反馈成功" :icon="successIcon"></MsgTips>
2025-03-28 15:19:42 +08:00
</view>
</template>
<script setup>
2025-04-16 14:24:06 +08:00
import {
ref,
inject,
nextTick,
defineProps,
defineEmits,
onMounted,
onUnmounted,
toRaw,
reactive,
computed,
watch,
} from 'vue';
2025-03-28 15:19:42 +08:00
import { storeToRefs } from 'pinia';
import config from '@/config.js';
import useChatGroupDBStore from '@/stores/userChatGroupStore';
import MdRender from '@/components/md-render/md-render.vue';
import CollapseTransition from '@/components/CollapseTransition/CollapseTransition.vue';
2025-04-16 14:24:06 +08:00
import PopupFeeBack from './popupbadFeeback.vue';
2025-03-28 15:19:42 +08:00
import AudioWave from './AudioWave.vue';
2025-04-07 09:10:55 +08:00
import WaveDisplay from './WaveDisplay.vue';
2025-03-28 18:19:05 +08:00
import FileIcon from './fileIcon.vue';
2025-04-07 09:10:55 +08:00
import FileText from './fileText.vue';
2025-03-28 15:19:42 +08:00
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
2025-04-16 14:24:06 +08:00
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
2025-04-07 09:10:55 +08:00
// 全局
const { $api, navTo, throttle } = inject('globalFunction');
const emit = defineEmits(['onConfirm']);
const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGroupDBStore());
2025-04-16 14:24:06 +08:00
import successIcon from '@/static/icon/success.png';
2025-04-07 09:10:55 +08:00
// hook
const {
isRecording,
startRecording,
stopRecording,
cancelRecording,
audioDataForDisplay,
volumeLevel,
recognizedText,
lastFinalText,
2025-07-22 15:20:21 +08:00
} = useAudioRecorder();
2025-03-28 15:19:42 +08:00
2025-07-21 14:49:45 +08:00
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useTTSPlayer(config.speechSynthesis);
2025-03-28 15:19:42 +08:00
2025-04-07 09:10:55 +08:00
// state
const queries = ref([]);
2025-03-28 15:19:42 +08:00
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;
2025-04-07 09:10:55 +08:00
const speechIndex = ref(0);
const isAudioPermission = ref(false);
2025-04-16 14:24:06 +08:00
const feebackData = ref(null);
// ref for DOM element
const voiceBtn = ref(null);
const feeback = ref(null);
const feeBackTips = ref(null);
2025-03-28 15:19:42 +08:00
const state = reactive({
uploadFileTips: '请根据以上附件,帮我推荐岗位。',
});
2025-04-07 09:10:55 +08:00
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';
});
onMounted(async () => {
changeQueries();
2025-03-28 15:19:42 +08:00
scrollToBottom();
2025-04-07 09:10:55 +08:00
isAudioPermission.value = await requestMicPermission();
2025-03-28 15:19:42 +08:00
});
2025-04-07 09:10:55 +08:00
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;
2025-03-28 15:19:42 +08:00
showfile.value = false;
showGuess.value = false;
if (values.trim()) {
// 判断是否有对话ID // 没有则创建对话列表
const callback = () => {
const normalArr = toRaw(filesList.value); // 转换为普通数组
filesList.value = [];
useChatGroupDBStore()
2025-04-07 09:10:55 +08:00
.getStearm(values, normalArr, scrollToBottom, {
onComplete: () => console.log('Display complete'),
})
2025-03-28 15:19:42 +08:00
.then(() => {
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: '确认删除文件?',
2025-03-28 18:19:05 +08:00
success(res) {
if (res.confirm) {
filesList.value = filesList.value.filter((item) => item.url !== file.url);
if (!filesList.value.length) {
if (textInput.value === state.uploadFileTips) {
textInput.value = '';
}
2025-03-28 15:19:42 +08:00
}
2025-03-28 18:19:05 +08:00
$api.msg('附件删除成功');
2025-03-28 15:19:42 +08:00
}
},
});
};
const scrollToBottom = throttle(function () {
nextTick(() => {
try {
2025-04-07 09:10:55 +08:00
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;
}
});
2025-03-28 15:19:42 +08:00
} 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();
});
});
}
2025-03-28 18:19:05 +08:00
function isImage(type) {
return new RegExp('image').test(type);
2025-03-28 15:19:42 +08:00
}
2025-03-28 18:19:05 +08:00
function isFile(type) {
const allowedTypes = config.allowedFileTypes || [];
if (!allowedTypes.includes(type)) {
return false;
}
return true;
2025-03-28 15:19:42 +08:00
}
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 || [];
2025-03-28 18:19:05 +08:00
const size = $api.formatFileSize(file.size);
2025-03-28 15:19:42 +08:00
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,
2025-03-28 18:19:05 +08:00
type: file.type,
2025-03-28 15:19:42 +08:00
name: file.name,
2025-03-28 18:19:05 +08:00
size: size,
2025-03-28 15:19:42 +08:00
});
textInput.value = state.uploadFileTips;
});
},
});
}
2025-04-07 09:10:55 +08:00
const tipsPermisson = () => {
uni.showToast({
title: '需要授权麦克风权限才能使用语音功能',
icon: 'none',
});
};
const handleTouchStart = async (e) => {
if (!isAudioPermission.value) {
return tipsPermisson();
}
2025-04-10 10:59:25 +08:00
cancelAudio();
2025-03-28 15:19:42 +08:00
startY.value = e.touches[0].clientY;
status.value = 'recording';
showfile.value = false;
startRecording();
2025-04-07 09:10:55 +08:00
$api.sleep(1000).then(() => {
scrollToBottom();
});
2025-03-28 15:19:42 +08:00
};
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();
2025-04-16 14:24:06 +08:00
if (isAudioPermission.value) {
if (recognizedText.value) {
sendMessage(recognizedText.value);
} else {
$api.msg('说话时长太短');
2025-04-07 09:10:55 +08:00
}
2025-04-16 14:24:06 +08:00
}
2025-03-28 15:19:42 +08:00
}
status.value = 'idle';
};
const handleTouchCancel = () => {
stopRecording();
status.value = 'idle';
};
function closeGuess() {
showGuess.value = false;
}
function changeVoice() {
isVoice.value = !isVoice.value;
}
function changeShowFile() {
showfile.value = !showfile.value;
}
function colseFile() {
showfile.value = false;
}
2025-04-07 09:10:55 +08:00
function copyMarkdown(value) {
$api.copyText(value);
}
function userGoodFeedback(msg) {
2025-04-16 14:24:06 +08:00
// $api.msg('该功能正在开发中,敬请期待后续更新!');
feeback.value?.open();
feebackData.value = msg;
}
function confirmFeeBack(value) {
useChatGroupDBStore()
.badFeedback(feebackData.value, value)
.then(() => {
feeback.value?.close();
feeBackTips.value?.open();
});
2025-04-07 09:10:55 +08:00
}
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 });
2025-03-28 15:19:42 +08:00
</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
2025-03-29 20:46:23 +08:00
padding: 5rpx 0 10rpx 0
2025-03-28 15:19:42 +08:00
.guess-list
2025-03-29 20:46:23 +08:00
padding: 16rpx 24rpx
margin-top: 28rpx
2025-03-28 15:19:42 +08:00
font-size: 24rpx
color: #8c8c8c
2025-03-29 20:46:23 +08:00
width: 100%;
border-radius: 20rpx 20rpx 20rpx 20rpx;
border: 2rpx solid #E5E5E5;
font-size: 28rpx;
color: #333333;
line-height: 33rpx;
2025-03-28 15:19:42 +08:00
.gulist
display: flex
flex-wrap: wrap
position: relative
image-margin-top = 40rpx
.chat-container
display: flex;
flex-direction: column;
2025-04-10 10:59:25 +08:00
height: 100%;
2025-03-28 15:19:42 +08:00
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
2025-04-07 09:10:55 +08:00
width: calc(100% - 88rpx)
2025-05-13 11:10:38 +08:00
position: relative
z-index: 1
2025-03-28 15:19:42 +08:00
.backlogo
width: 313rpx;
height: 190rpx;
.back-rowTitle
height: 56rpx;
font-weight: bold;
font-size: 40rpx;
color: #333333;
line-height: 47rpx;
margin-top: 40rpx
.back-rowText
margin-top: 28rpx
width: 100%;
2025-04-07 09:10:55 +08:00
height: 148rpx;
2025-03-28 15:19:42 +08:00
font-weight: 400;
font-size: 28rpx;
color: #333333;
2025-04-07 09:10:55 +08:00
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
2025-03-28 15:19:42 +08:00
.back-rowh3
width: 100%;
font-weight: 500;
2025-04-07 09:10:55 +08:00
font-size: 28rpx;
2025-03-28 15:19:42 +08:00
color: #000000;
2025-04-07 09:10:55 +08:00
margin-top: 28rpx
2025-03-28 15:19:42 +08:00
.back-rowmsg
2025-04-07 09:10:55 +08:00
width: calc(100% - 32rpx)
margin-top: 24rpx
2025-03-28 15:19:42 +08:00
font-weight: 500;
2025-04-07 09:10:55 +08:00
color: rgba(51, 51, 51, 1);
2025-03-28 15:19:42 +08:00
line-height: 28rpx;
font-size: 24rpx
2025-04-07 09:10:55 +08:00
background: rgba(246, 246, 246, 1);
2025-03-28 15:19:42 +08:00
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;
2025-04-07 09:10:55 +08:00
width: 100%;
2025-03-28 15:19:42 +08:00
}
.chat-item.self {
justify-content: flex-end;
}
2025-03-29 20:46:23 +08:00
.message
2025-03-28 15:19:42 +08:00
margin-top: 40rpx
// max-width: 80%;
2025-03-29 20:46:23 +08:00
width: 100%;
2025-03-28 15:19:42 +08:00
word-break: break-word;
color: #333333;
user-select: text;
-webkit-user-select: text;
2025-03-29 20:46:23 +08:00
.message-markdown
border-radius: 0 20rpx 20rpx 20rpx;
2025-04-16 14:24:06 +08:00
padding: 20rpx 20rpx 20rpx 20rpx;
2025-03-29 20:46:23 +08:00
background: #F6F6F6;
2025-04-07 09:10:55 +08:00
.message-controll
display: flex
justify-content: space-between
align-items: center
border-top: 2rpx solid #EAEAEA
2025-04-16 14:24:06 +08:00
padding: 24rpx 0 4rpx 0
2025-04-10 10:59:25 +08:00
margin-top: 10rpx
2025-04-07 09:10:55 +08:00
.controll-left
.controll-right
.controll-icon
width: 60rpx;
height: 60rpx;
2025-03-28 15:19:42 +08:00
.messageNull
2025-03-29 20:46:23 +08:00
display: none
2025-03-28 15:19:42 +08:00
.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;
2025-03-29 20:46:23 +08:00
width: fit-content;
2025-03-28 15:19:42 +08:00
}
.input-area {
2025-03-28 18:19:05 +08:00
padding: 32rpx 28rpx 24rpx 28rpx;
2025-03-28 15:19:42 +08:00
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
2025-04-10 10:59:25 +08:00
user-select:none;
2025-03-28 15:19:42 +08:00
-webkit-touch-callout:none;
-webkit-user-select:none;
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
2025-04-16 14:24:06 +08:00
touch-action: none; /* 禁用默认滚动 */
2025-03-28 15:19:42 +08:00
.input_vio:active
background: #e8e8e8
.vio_container
background: transparent
padding: 28rpx
text-align: center
2025-04-16 14:24:06 +08:00
-webkit-touch-callout:none;
-webkit-user-select:none;
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
touch-action: none; /* 禁用默认滚动 */
2025-03-28 15:19:42 +08:00
.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;
2025-04-07 09:10:55 +08:00
.btn-box-round
width: 22rpx
height: 22rpx
border-radius: 4rpx
background: #FFFFFF
2025-03-28 15:19:42 +08:00
.purple
background: linear-gradient( 225deg, #9E74FD 0%, #256BFA 100%);
.add-file-btn{
transform: rotate(45deg)
transition: transform 0.5s ease;
}
2025-05-15 14:17:51 +08:00
.area-tips{
font-weight: 400;
font-size: 26rpx;
color: #434343;
margin-top: 18rpx
}
2025-03-28 15:19:42 +08:00
.area-file
display: grid
width: 100%
grid-template-columns: repeat(3, 1fr)
grid-gap: 20rpx
2025-05-15 14:17:51 +08:00
padding: 20rpx 0 0 0;
2025-03-28 18:19:05 +08:00
.file-card
display: flex
flex-direction: column
align-items: center
padding: 24rpx 0
background: #F5F5F5;
border-radius: 20rpx 20rpx 20rpx 20rpx;
text
font-size: 24rpx
font-weight: 500
2025-04-07 09:10:55 +08:00
color: rgba(0,0,0,.5)
2025-03-28 18:19:05 +08:00
padding-top: 8rpx
.card-img
height: 56rpx
width: 56rpx
.file-card:active
background: #e8e8e8
2025-03-28 15:19:42 +08:00
.area-uploadfiles
position: absolute
2025-03-28 18:19:05 +08:00
top: -180rpx
2025-04-07 09:10:55 +08:00
width: calc(100% - 30rpx)
2025-03-28 15:19:42 +08:00
background: #FFFFFF
left: 0
2025-04-07 09:10:55 +08:00
padding: 10rpx 0 10rpx 30rpx
2025-03-28 15:19:42 +08:00
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
.uploadfiles-scroll
height: 100%
.uploadfiles-list
height: 100%
display: flex
2025-04-07 09:10:55 +08:00
margin-right: 28rpx
2025-03-28 15:19:42 +08:00
flex-wrap: nowrap
2025-03-28 18:19:05 +08:00
.file-doc
2025-03-28 15:19:42 +08:00
display: flex
2025-04-07 09:10:55 +08:00
align-items: center
justify-content: flex-start
2025-03-28 18:19:05 +08:00
padding: 16rpx 20rpx 18rpx 20rpx
height: calc(100% - 40rpx)
2025-04-07 09:10:55 +08:00
.doc-icon
width: 60rpx
height: 76rpx
margin-right: 20rpx
.doc-con
flex: 1
width: 0
2025-03-28 18:19:05 +08:00
.file-uploadsend
2025-04-07 09:10:55 +08:00
margin: 10rpx 18rpx 0 0;
2025-03-28 15:19:42 +08:00
height: 100%
font-size: 24rpx
position: relative
2025-04-07 09:10:55 +08:00
min-width: 460rpx;
2025-03-28 18:19:05 +08:00
height: 160rpx;
2025-03-28 15:19:42 +08:00
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #E2E2E2;
2025-04-07 09:10:55 +08:00
overflow: hidden
2025-03-28 15:19:42 +08:00
.file-del
position: absolute
2025-04-07 09:10:55 +08:00
right: 25rpx
top: 18rpx
2025-03-28 15:19:42 +08:00
z-index: 9
border-radius: 50%
display: flex
align-items: center
justify-content: center
transform: translate(50%, -10rpx)
height: 40rpx
2025-04-07 09:10:55 +08:00
width: 37rpx;
height: 37rpx;
background: #4B4B4B;
border: 2rpx solid #FFFFFF;
2025-03-28 15:19:42 +08:00
.file-del:active
background: #e8e8e8
.filename-text
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
color: #333333
2025-03-28 18:19:05 +08:00
font-size: 24rpx
2025-03-28 15:19:42 +08:00
font-weight: 500
max-width: 100%
2025-03-28 18:19:05 +08:00
.filename-size
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
color: #7B7B7B;
max-width: 100%
.file-iconImg
height: 100%
width: 100%
2025-04-07 09:10:55 +08:00
.filerow
display: flex
align-items: center
margin-top: 7rpx
.row-x
margin: 0 18rpx
height: 20rpx
width: 2rpx
background: rgba(226, 226, 226, .9)
2025-03-28 18:19:05 +08:00
.file-border
2025-04-07 09:10:55 +08:00
width: 160rpx !important;
2025-03-28 15:19:42 +08:00
@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>