Files
im-uniapp/pages/conversation/chating/components/ChatingFooter/index.vue

1140 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<view>
<view class="chat_footer">
<view class="footer_action_area">
<image v-if="inputBox"
@click.prevent="updateInput"
src="@/static/images/chating_footer_recording.png"
alt=""
srcset=""
/>
<image v-if="!inputBox"
@click.prevent="updateInput"
src="@/static/images/chating_footer_audio_recording.png"
alt=""
srcset=""
/>
</view>
<view class="input_content" v-if="inputBox">
<CustomEditor class="custom_editor" ref="customEditor" @ready="editorReady" @focus="editorFocus"
@blur="editorBlur" @input="editorInput"/>
</view>
<view class="input_content"
v-if="!inputBox"
style="position:relative;"
>
<!-- 录音提示区域 -->
<view v-if="isRecording"
style="position:absolute;left:50%;top:-120rpx;transform:translateX(-50%);z-index:10;display:flex;flex-direction:column;align-items:center;">
<view :style="{
background: willCancel ? 'rgba(255,80,80,0.95)' : 'rgba(0,0,0,0.85)',
color: '#fff',
borderRadius: '12rpx',
padding: '18rpx 32rpx',
fontSize: '28rpx',
minWidth: '260rpx',
textAlign: 'center',
marginBottom: '10rpx',
}">
<text v-if="willCancel">松开手指取消发送</text>
<text v-else>正在讲话已录制 {{recordingTime}} 松开发送</text>
</view>
</view>
<!-- 蒙版 -->
<view v-if="isRecording" style="position:absolute;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.08);z-index:5;border-radius:16rpx;"></view>
<button
class="record_btn"
@touchstart="startRecord"
@touchend="stopRecord"
@touchcancel="cancelRecord"
@touchmove="moveRecord"
style="width:100%;height:100%;background:transparent;border:none;outline:none;display:flex;align-items:center;justify-content:center;z-index:20;position:relative;"
>
<text v-if="!isRecording">按住说话</text>
<text v-else :style="willCancel ? 'color:#ff5050;' : 'color:#4a9cfc;'">{{ willCancel ? '松开手指,取消发送' : '正在说话,松开发送' }}</text>
</button>
</view>
<view class="footer_action_area">
<image
class="emoji_action"
v-if="inputBox && !showEmojiPicker"
@click.prevent="toggleEmojiPicker"
src="@/static/images/chating_footer_emoji.png"
alt=""
srcset=""
/>
<image
class="emoji_action"
v-if="inputBox && showEmojiPicker"
@click.prevent="closeEmojiAndFocusInput"
src="@/static/images/chating_footer_audio_recording.png"
alt=""
srcset=""
/>
<image
v-show="!hasContent"
@click.prevent="updateActionBar"
src="@/static/images/chating_footer_add.png"
alt=""
srcset=""
/>
<image v-show="hasContent" @touchend.prevent="sendTextMessage" src="@/static/images/send_btn.png"
alt="" srcset=""/>
</view>
</view>
<chating-action-bar @sendMessage="sendMessage" @prepareMediaMessage="prepareMediaMessage"
v-show="actionBarVisible"/>
<u-action-sheet :safeAreaInsetBottom="true" round="12" :actions="actionSheetMenu" @select="selectClick"
:closeOnClickOverlay="true" :closeOnClickAction="true" :show="showActionSheet"
@close="showActionSheet = false">
</u-action-sheet>
<!-- Emoji 选择面板 -->
<view v-if="showEmojiPicker">
<view class="emoji-mask" @touchstart="showEmojiPicker = false" style="position:fixed;left:0;top:0;width:100vw;height:100vh;z-index:998;"></view>
<view class="emoji-picker" style="position:relative;z-index:999;">
<EmojiPicker @select="onEmojiSelect" />
</view>
</view>
<!-- 文件选择弹窗 -->
<view v-if="showFileSelectModal" class="file-select-mask" @touchstart="showFileSelectModal = false">
<view class="file-select-container" @touchstart.stop>
<view class="file-select-header">
<text class="file-select-title">选择文件</text>
<text class="file-select-close" @click="showFileSelectModal = false">×</text>
</view>
<view class="file-select-tabs">
<view
class="tab-item"
:class="{ active: currentFileTab === 'chat' }"
@click="currentFileTab = 'chat'"
>
聊天文件
</view>
<view
class="tab-item"
:class="{ active: currentFileTab === 'system' }"
@click="currentFileTab = 'system'"
>
系统文件
</view>
</view>
<view class="file-select-content">
<view v-if="currentFileTab === 'chat'" class="chat-files">
<view v-if="chatFiles.length === 0" class="empty-state">
<text class="empty-text">暂无聊天文件</text>
</view>
<view v-else class="file-list">
<view
v-for="file in chatFiles"
:key="file.clientMsgID"
class="file-item"
@click="selectChatFile(file)"
>
<image :src="getFileIcon(file)" class="file-icon" mode="aspectFit" />
<view class="file-info">
<text class="file-name">{{ getFileName(file) }}</text>
<text class="file-size">{{ getFileSize(file) }}</text>
</view>
</view>
</view>
</view>
<view v-else-if="currentFileTab === 'system'" class="system-files">
<view class="file-options">
<view class="option-item" @click="chooseSystemFile('document')">
<image src="/static/images/file_message/file_pdf.png" class="option-icon" mode="aspectFit" />
<text class="option-text">文档</text>
</view>
<view class="option-item" @click="chooseSystemFile('image')">
<image src="/static/images/file_message/file_image.png" class="option-icon" mode="aspectFit" />
<text class="option-text">图片</text>
</view>
<view class="option-item" @click="chooseSystemFile('video')">
<image src="/static/images/file_message/file_unknown.png" class="option-icon" mode="aspectFit" />
<text class="option-text">视频</text>
</view>
<view class="option-item" @click="chooseSystemFile('audio')">
<image src="/static/images/file_message/file_unknown.png" class="option-icon" mode="aspectFit" />
<text class="option-text">音频</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import {
mapGetters,
mapActions
} from "vuex";
import {
getPurePath,
html2Text
} from "@/util/common";
import {
offlinePushInfo
} from "@/util/imCommon";
import {
ChatingFooterActionTypes,
UpdateMessageTypes,
} from "@/constant";
import IMSDK, {
IMMethods,
MessageStatus,
MessageType,
} from "openim-uniapp-polyfill";
import UParse from "@/components/gaoyia-parse/parse.vue";
import CustomEditor from "./CustomEditor.vue";
import ChatingActionBar from "./ChatingActionBar.vue";
import EmojiPicker from '@/components/EmojiPicker/index.vue';
const needClearTypes = [MessageType.TextMessage];
const albumChoose = [{
name: "图片",
type: ChatingFooterActionTypes.Album,
idx: 0,
},
{
name: "拍照",
type: ChatingFooterActionTypes.Camera,
idx: 1,
},
];
export default {
components: {
CustomEditor,
ChatingActionBar,
UParse,
EmojiPicker,
},
props: {
footerOutsideFlag: Number,
},
data() {
return {
inputBox: true,
customEditorCtx: null,
inputHtml: "",
actionBarVisible: false,
isInputFocus: false,
actionSheetMenu: [],
showActionSheet: false,
isRecording: false,
willCancel: false,
recordingTime: 0,
recordingTimer: null,
showEmojiPicker: false,
showFileSelectModal: false,
currentFileTab: 'chat',
chatFiles: [],
recorderManager: null,
recordFilePath: '',
recordFileDuration: 0,
};
},
computed: {
...mapGetters([
"storeCurrentConversation",
"storeCurrentGroup",
"storeBlackList",
]),
hasContent() {
return html2Text(this.inputHtml) !== "";
},
},
watch: {
footerOutsideFlag(newVal) {
this.onClickActionBarOutside();
},
showFileSelectModal(newVal) {
if (newVal) {
// 当弹窗显示时,获取聊天文件列表
this.getChatFiles();
}
},
},
mounted() {
this.setKeyboardListener();
// 只在浏览器环境下添加事件监听
if (typeof document !== 'undefined') {
document.addEventListener('plusready', function() {
// plus 相关代码
});
}
},
beforeDestroy() {
this.disposeKeyboardListener();
},
methods: {
updateInput() {
this.inputBox = !this.inputBox;
this.showEmojiPicker = false;
},
startRecord(e) {
this.isRecording = true;
this.willCancel = false;
this.recordingTime = 0;
if (this.recordingTimer) clearInterval(this.recordingTimer);
this.recordingTimer = setInterval(() => {
this.recordingTime++;
}, 1000);
// 录音开始
if (!this.recorderManager) {
this.recorderManager = uni.getRecorderManager();
this.recorderManager.onStop(this.onRecordStop);
}
this.recordFilePath = '';
this.recordFileDuration = 0;
this.recorderManager.start({ format: 'mp3' });
},
stopRecord(e) {
if (this.recordingTimer) clearInterval(this.recordingTimer);
this.recordingTimer = null;
this.isRecording = false;
if (this.recorderManager) {
this.recorderManager.stop();
}
this._startY = null;
},
cancelRecord(e) {
if (this.recordingTimer) clearInterval(this.recordingTimer);
this.recordingTimer = null;
this.isRecording = false;
this.willCancel = false;
this.recordingTime = 0;
if (this.recorderManager) {
this.recorderManager.stop();
}
this._startY = null;
},
moveRecord(e) {
// 判断手指是否上滑通常通过e.touches[0].clientY与起始Y比较
if (!this.isRecording) return;
const touch = e.touches[0];
if (!this._startY && touch) {
this._startY = touch.clientY;
}
if (touch && this._startY) {
// 上滑距离大于40px则判定为取消
this.willCancel = (this._startY - touch.clientY) > 40;
}
},
...mapActions("message", ["pushNewMessage", "updateOneMessage"]),
async createTextMessage() {
let message = "";
const text = html2Text(this.inputHtml);
message = await IMSDK.asyncApi(
IMMethods.CreateTextMessage,
IMSDK.uuid(),
text
);
return message;
},
async sendTextMessage() {
if (!this.hasContent) return;
const message = await this.createTextMessage();
this.sendMessage(message);
},
sendMessage(message) {
this.pushNewMessage(message);
if (needClearTypes.includes(message.contentType)) {
this.customEditorCtx.clear();
}
this.$emit("scrollToBottom");
IMSDK.asyncApi(IMMethods.SendMessage, IMSDK.uuid(), {
recvID: this.storeCurrentConversation.userID,
groupID: this.storeCurrentConversation.groupID,
message,
offlinePushInfo,
})
.then(({
data
}) => {
this.updateOneMessage({
message: data,
isSuccess: true,
});
})
.catch(({
data,
errCode,
errMsg
}) => {
this.updateOneMessage({
message: data,
type: UpdateMessageTypes.KeyWords,
keyWords: [{
key: "status",
value: MessageStatus.Failed,
},
{
key: "errCode",
value: errCode,
},
],
});
});
},
// action
onClickActionBarOutside() {
if (this.actionBarVisible) {
this.actionBarVisible = false;
}
},
updateActionBar() {
this.actionBarVisible = !this.actionBarVisible;
this.showEmojiPicker = false;
},
editorReady(e) {
this.customEditorCtx = e.context;
this.customEditorCtx.clear();
},
editorFocus() {
this.isInputFocus = true;
this.showEmojiPicker = false;
this.$emit("scrollToBottom");
},
editorBlur() {
this.isInputFocus = false;
},
editorInput(e) {
this.inputHtml = e.detail.html;
},
prepareMediaMessage(type) {
if (type === ChatingFooterActionTypes.Album) {
this.actionSheetMenu = [...albumChoose];
this.showActionSheet = true;
} else if (type === ChatingFooterActionTypes.File) {
this.chooseFileAndSend();
}
},
async chooseFileAndSend() {
// 显示文件选择弹窗,类似微信的文件选择
this.showFileSelectModal = true;
},
// 获取聊天文件列表
getChatFiles() {
// 从store中获取当前会话的消息列表筛选出文件消息
const messages = this.$store.getters.storeHistoryMessageList || [];
this.chatFiles = messages.filter(msg => {
return msg.contentType === 105; // 文件消息类型
}).slice(-20); // 只显示最近20个文件
},
// 选择聊天文件
selectChatFile(file) {
// 直接转发这个文件消息
this.forwardFileMessage(file);
this.showFileSelectModal = false;
},
// 转发文件消息
async forwardFileMessage(file) {
try {
const uuid = IMSDK.uuid();
const fileElem = file.fileElem;
// 创建新的文件消息
const message = await IMSDK.asyncApi('createFileMessageByURL', IMSDK.uuid(), {
filePath: fileElem.filePath || '',
fileName: fileElem.fileName,
uuid: uuid,
sourceUrl: fileElem.sourceUrl,
fileSize: fileElem.fileSize,
fileType: fileElem.fileType,
});
// 发送消息
this.sendMessage(message);
} catch (e) {
console.log('转发文件失败:', e);
uni.showToast({
title: '转发文件失败',
icon: 'none'
});
}
},
// 获取文件图标
getFileIcon(file) {
const fileName = file.fileElem?.fileName?.toLowerCase() || '';
if (fileName.endsWith('.pdf')) {
return '/static/images/file_message/file_pdf.png';
} else if (fileName.endsWith('.doc') || fileName.endsWith('.docx')) {
return '/static/images/file_message/file_word.png';
} else if (fileName.endsWith('.xls') || fileName.endsWith('.xlsx')) {
return '/static/images/file_message/file_excel.png';
} else if (fileName.endsWith('.ppt') || fileName.endsWith('.pptx')) {
return '/static/images/file_message/file_ppt.png';
} else if (fileName.endsWith('.zip') || fileName.endsWith('.rar') || fileName.endsWith('.7z')) {
return '/static/images/file_message/file_zip.png';
} else if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png') || fileName.endsWith('.gif')) {
return '/static/images/file_message/file_image.png';
} else {
return '/static/images/file_message/file_unknown.png';
}
},
// 获取文件名
getFileName(file) {
return file.fileElem?.fileName || '未知文件';
},
// 获取文件大小
getFileSize(file) {
const size = file.fileElem?.fileSize;
if (!size) return '';
if (size < 1024) {
return size + ' B';
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(1) + ' KB';
} else {
return (size / (1024 * 1024)).toFixed(1) + ' MB';
}
},
// 选择系统文件
chooseSystemFile(type) {
switch (type) {
case 'document':
// 选择文档
uni.chooseMessageFile({
count: 1,
type: 'file',
success: async (res) => {
const file = res.tempFiles[0];
await this.processFileAndSend(file);
},
fail: () => {
uni.showToast({
title: '请从聊天记录中选择文档',
icon: 'none'
});
}
});
break;
case 'image':
// 选择图片
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: async (res) => {
const file = {
name: 'image.jpg',
size: 0,
path: res.tempFilePaths[0],
type: 'image'
};
await this.processFileAndSend(file);
}
});
break;
case 'video':
// 选择视频
uni.chooseVideo({
sourceType: ['album'],
success: async (res) => {
const file = {
name: 'video.mp4',
size: res.size || 0,
path: res.tempFilePath,
type: 'video'
};
await this.processFileAndSend(file);
}
});
break;
case 'audio':
// 选择音频
uni.chooseMessageFile({
count: 1,
type: 'file',
success: async (res) => {
const file = res.tempFiles[0];
await this.processFileAndSend(file);
},
fail: () => {
uni.showToast({
title: '请从聊天记录中选择音频文件',
icon: 'none'
});
}
});
break;
}
this.showFileSelectModal = false;
},
async processFileAndSend(file) {
try {
// 处理文件名
let fileName = file.name;
if (!fileName) {
const pathParts = file.path.split('/');
fileName = pathParts[pathParts.length - 1] || 'unknown';
}
// 处理文件类型
let fileType = file.type;
if (!fileType) {
const nameIdx = fileName.lastIndexOf(".") + 1;
fileType = fileName.slice(nameIdx) || 'unknown';
}
const uuid = IMSDK.uuid();
// 如果是图片类型,直接创建图片消息
if (fileType.toLowerCase().includes('image') || fileType.toLowerCase().includes('jpg') || fileType.toLowerCase().includes('jpeg') || fileType.toLowerCase().includes('png') || fileType.toLowerCase().includes('gif')) {
// 创建图片消息
const message = await IMSDK.asyncApi(
IMMethods.CreateImageMessageFromFullPath,
IMSDK.uuid(),
file.path
);
this.sendMessage(message);
return;
}
// 先保存到本地
uni.saveFile({
tempFilePath: file.path,
success: (saveRes) => {
let savedFilePath = saveRes.savedFilePath;
// 用 plus.io 转换为原生绝对路径
if (typeof plus !== 'undefined') {
plus.io.resolveLocalFileSystemURL(savedFilePath, (entry) => {
IMSDK.asyncApi(IMMethods.UploadFile, uuid, {
name: fileName,
contentType: fileType,
uuid: uuid,
filepath: entry.fullPath
})
.then(({ data }) => {
let sourceUrl = data.url;
IMSDK.asyncApi('createFileMessageByURL', IMSDK.uuid(), {
filePath: entry.fullPath,
fileName: fileName,
uuid: uuid,
sourceUrl: sourceUrl,
fileSize: file.size || 0,
fileType: fileType,
})
.then((message) => {
IMSDK.asyncApi('sendMessageNotOss', IMSDK.uuid(), {
recvID: this.storeCurrentConversation.userID,
groupID: this.storeCurrentConversation.groupID,
message: message,
offlinePushInfo: {
title: '你有新消息',
desc: '新消息',
ex: '',
iOSPushSound: '+1',
iOSBadgeCount: true,
}
})
.then(({ data }) => {
// 1. 本地新增消息
this.pushNewMessage(message);
// 2. 更新消息状态
this.updateOneMessage({
message: data,
isSuccess: true,
});
this.$emit("scrollToBottom");
})
.catch(({ errCode, errMsg }) => {
// 发送失败时也要更新消息状态为失败
this.updateOneMessage({
message: message,
type: UpdateMessageTypes.KeyWords,
keyWords: [
{ key: "status", value: MessageStatus.Failed },
{ key: "errCode", value: errCode },
],
});
});
})
.catch(() => {
// 忽略错误提示
});
})
.catch(() => {
// 忽略错误提示
});
}, () => {
// 忽略路径转换失败
});
}
},
fail: () => {
// 忽略保存失败
}
});
} catch (e) {
console.log('处理文件失败:', e);
}
},
// from comp
batchCreateImageMesage(paths) {
paths.forEach(async (path) => {
const message = await IMSDK.asyncApi(
IMMethods.CreateImageMessageFromFullPath,
IMSDK.uuid(),
getPurePath(path)
);
this.sendMessage(message);
});
},
selectClick({
idx
}) {
if (idx === 0) {
this.chooseOrShotImage(["album"]).then((paths) =>
this.batchCreateImageMesage(paths)
);
} else {
this.chooseOrShotImage(["camera"]).then((paths) =>
this.batchCreateImageMesage(paths)
);
}
},
chooseOrShotImage(sourceType) {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: 9,
sizeType: ["compressed"],
sourceType,
success: function ({
tempFilePaths
}) {
resolve(tempFilePaths);
},
fail: function (err) {
reject(err);
},
});
});
},
// keyboard
keyboardChangeHander({
height
}) {
if (height > 0) {
if (this.actionBarVisible) {
this.actionBarVisible = false;
}
}
},
setKeyboardListener() {
uni.onKeyboardHeightChange(this.keyboardChangeHander);
},
disposeKeyboardListener() {
uni.offKeyboardHeightChange(this.keyboardChangeHander);
},
toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker;
if (!this.showEmojiPicker && this.$refs.customEditor && this.$refs.customEditor.editorCtx) {
// 关闭emoji面板时再focus
this.$refs.customEditor.editorCtx.focus();
}
},
closeEmojiAndFocusInput() {
this.showEmojiPicker = false;
// 只在这里主动focus
if (this.$refs.customEditor && this.$refs.customEditor.editorCtx) {
this.$refs.customEditor.editorCtx.focus();
}
},
onEmojiSelect(emoji) {
this.inputHtml += emoji;
if (this.$refs.customEditor && this.$refs.customEditor.editorCtx) {
this.$refs.customEditor.editorCtx.setContents({
html: this.inputHtml
});
}
// 不做任何focus/blur
},
async onRecordStop(res) {
let tempFilePath = res.tempFilePath;
let fileName = tempFilePath.split('/').pop();
let fileType = fileName.split('.').pop();
let uuid = IMSDK.uuid();
let duration = this.recordingTime;
if (!duration || isNaN(duration) || duration < 1) {
this.recordingTime = 0;
return;
}
// 先保存到本地
uni.saveFile({
tempFilePath,
success: (saveRes) => {
let savedFilePath = saveRes.savedFilePath;
// 用 plus.io 转换为原生绝对路径
if (typeof plus !== 'undefined') {
plus.io.resolveLocalFileSystemURL(savedFilePath, (entry) => {
IMSDK.asyncApi('uploadFile', uuid, {
name: fileName,
contentType: fileType,
uuid: uuid,
filepath: entry.fullPath
})
.then(({ data }) => {
let sourceUrl = data.url;
IMSDK.asyncApi('createSoundMessageByURL', IMSDK.uuid(), {
uuid: uuid,
soundPath: entry.fullPath,
sourceUrl: sourceUrl,
dataSize: res.fileSize || 0,
duration: duration,
soundType: fileType
})
.then((message) => {
IMSDK.asyncApi('sendMessageNotOss', IMSDK.uuid(), {
recvID: this.storeCurrentConversation.userID,
groupID: this.storeCurrentConversation.groupID,
message: message,
offlinePushInfo: {
title: '你有新消息',
desc: '新消息',
ex: '',
iOSPushSound: '+1',
iOSBadgeCount: true,
}
})
.then(({ data }) => {
// 1. 本地新增消息
this.pushNewMessage(message);
// 2. 更新消息状态
this.updateOneMessage({
message: data,
isSuccess: true,
});
this.$emit("scrollToBottom");
})
.catch(({ errCode, errMsg }) => {
// 发送失败时也要更新消息状态为失败
this.updateOneMessage({
message: message,
type: UpdateMessageTypes.KeyWords,
keyWords: [
{ key: "status", value: MessageStatus.Failed },
{ key: "errCode", value: errCode },
],
});
});
})
.catch(() => {
// 忽略错误提示
});
})
.catch(() => {
// 忽略错误提示
});
}, () => {
// 忽略路径转换失败
});
}
},
fail: () => {
// 忽略保存失败
}
});
this.recordingTime = 0;
},
},
};
</script>
<style lang="scss" scoped>
.custom_editor {
img {
vertical-align: sub;
}
}
.forbidden_footer {
width: 100%;
height: 112rpx;
color: #8e9ab0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background: #f0f2f6;
}
.chat_footer {
display: flex;
align-items: center;
// background-color: #e9f4ff;
background: #f0f2f6;
// height: 50px;
max-height: 120px;
padding: 24rpx 20rpx;
.input_content {
flex: 1;
min-height: 30px;
max-height: 120px;
margin: 0 24rpx;
border-radius: 16rpx;
position: relative;
.record_btn {
// background-color: #3c9cff;
background: #fff;
color: black;
height: 30px;
font-size: 24rpx;
}
}
.quote_message {
@include vCenterBox();
justify-content: space-between;
margin-top: 12rpx;
padding: 8rpx;
// padding-top: 20rpx;
border-radius: 6rpx;
background-color: #fff;
color: #666;
.content {
/deep/ uni-view {
@include ellipsisWithLine(2);
}
}
}
.footer_action_area {
display: flex;
align-items: center;
.emoji_action {
margin-right: 24rpx;
}
image {
width: 26px;
height: 26px;
}
}
.send_btn {
height: 30px;
line-height: 30px;
background-color: #4a9cfc;
padding: 0 8px;
border-radius: 4px;
color: #fff;
}
}
.emoji-mask {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: transparent;
z-index: 998;
}
.emoji-picker {
position: relative;
z-index: 999;
}
.file-select-mask {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.6);
z-index: 998;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.file-select-container {
background: #fff;
border-radius: 16px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
.file-select-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
}
.file-select-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.file-select-close {
font-size: 24px;
color: #999;
cursor: pointer;
padding: 4px;
}
.file-select-tabs {
display: flex;
border-bottom: 1px solid #f0f0f0;
}
.tab-item {
flex: 1;
text-align: center;
padding: 16px;
font-size: 16px;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
border-bottom: 2px solid transparent;
}
.tab-item.active {
color: #4a9cfc;
border-bottom-color: #4a9cfc;
background: rgba(74, 156, 252, 0.05);
}
.file-select-content {
max-height: 400px;
overflow-y: auto;
}
.chat-files {
padding: 16px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
}
.empty-text {
color: #999;
font-size: 14px;
}
.file-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.file-item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 8px;
background: #f8f9fa;
cursor: pointer;
transition: all 0.3s ease;
}
.file-item:hover {
background: #e9ecef;
}
.file-icon {
width: 40px;
height: 40px;
margin-right: 12px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
}
.file-name {
font-size: 14px;
color: #333;
font-weight: 500;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
font-size: 12px;
color: #999;
}
.system-files {
padding: 24px;
}
.file-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.option-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
border-radius: 12px;
background: #f8f9fa;
cursor: pointer;
transition: all 0.3s ease;
}
.option-item:hover {
background: #e9ecef;
transform: translateY(-2px);
}
.option-icon {
width: 48px;
height: 48px;
margin-bottom: 8px;
}
.option-text {
font-size: 14px;
color: #333;
font-weight: 500;
}
</style>