diff --git a/src/layout/components/SystemSettingDialog.vue b/src/layout/components/SystemSettingDialog.vue
index 2ccdf65..fcff424 100644
--- a/src/layout/components/SystemSettingDialog.vue
+++ b/src/layout/components/SystemSettingDialog.vue
@@ -29,8 +29,8 @@
-->
- Ctrl+Enter发送
- Enter发送
+ Ctrl+Enter发送
+ Enter发送
@@ -173,7 +173,7 @@ export default {
cacheAuthorized: false,
basicForm: {
notification: true,
- sendKey: "enter",
+ sendKey: "Ctrl+Enter",
videoPath: "",
cachePath: "",
},
@@ -215,6 +215,7 @@ export default {
const defaultPath = this.getDefaultPath();
if (savedSettings) {
const settings = JSON.parse(savedSettings);
+ let needSave = false;
// 合并保存的设置到表单
if (settings.basicForm) {
this.basicForm = { ...this.basicForm, ...settings.basicForm };
@@ -225,6 +226,11 @@ export default {
if (!this.basicForm.cachePath) {
this.basicForm.cachePath = defaultPath;
}
+ // 如果没有保存发送键设置,使用默认值 Ctrl+Enter
+ if (!this.basicForm.sendKey) {
+ this.basicForm.sendKey = "Ctrl+Enter";
+ needSave = true;
+ }
}
if (settings.audioForm) {
this.audioForm = { ...this.audioForm, ...settings.audioForm };
@@ -232,10 +238,20 @@ export default {
if (settings.otherForm) {
this.otherForm = { ...this.otherForm, ...settings.otherForm };
}
+ // 迁移逻辑:如果设置中没有版本标记,说明是旧版本,更新发送键为新默认值
+ if (!settings.version) {
+ this.basicForm.sendKey = "Ctrl+Enter";
+ needSave = true;
+ }
+ if (needSave) {
+ this.saveSettings();
+ }
} else {
- // 如果没有保存的设置,使用默认路径
+ // 如果没有保存的设置,使用默认值并保存
this.basicForm.videoPath = defaultPath;
this.basicForm.cachePath = defaultPath;
+ this.basicForm.sendKey = "Ctrl+Enter";
+ this.saveSettings();
}
} catch (e) {
console.error("加载系统设置失败:", e);
@@ -245,11 +261,16 @@ export default {
saveSettings() {
try {
const settings = {
+ version: "1.0",
basicForm: { ...this.basicForm },
audioForm: { ...this.audioForm },
otherForm: { ...this.otherForm },
};
localStorage.setItem(SYSTEM_SETTINGS_KEY, JSON.stringify(settings));
+ const event = new CustomEvent('systemSettingsChanged', {
+ detail: { settings }
+ });
+ window.dispatchEvent(event);
} catch (e) {
console.error("保存系统设置失败:", e);
}
@@ -400,7 +421,7 @@ export default {
this.cacheAuthorized = false;
this.basicForm = {
notification: true,
- sendKey: "enter",
+ sendKey: "Ctrl+Enter",
videoPath: defaultPath,
cachePath: defaultPath,
};
diff --git a/src/views/message/components/MessageEditor.vue b/src/views/message/components/MessageEditor.vue
index 55d131d..23a1705 100644
--- a/src/views/message/components/MessageEditor.vue
+++ b/src/views/message/components/MessageEditor.vue
@@ -125,7 +125,8 @@
ref="fileInput"
type="file"
style="display: none"
- @change="(e) => handleFileChange(e, 'file')"
+ accept="image/*,video/*,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar,.txt"
+ @change="(e) => handleFileChange(e)"
/>
@@ -159,7 +160,7 @@ export default {
currentRange: null,
sending: false,
maxFileSize: 100 * 1024 * 1024,
- sendMode: "enter",
+ sendMode: this.getSystemSendMode(),
fontSizeMap: {
10: "small",
11: "small",
@@ -170,6 +171,11 @@ export default {
// 记录被@的用户
selectedAtUsers: [],
meetingModes: meetingModes(),
+ // Quill初始化重试次数
+ quillInitRetryCount: 0,
+ // 存储编辑器中的文件信息
+ editorFiles: [],
+ maxQuillInitRetries: 20,
};
},
@@ -190,40 +196,104 @@ export default {
this.$nextTick(() => {
this.initQuill();
});
- // 挂载全局键盘监听
document.addEventListener("keydown", this.handleGlobalKeydown);
+ window.addEventListener("systemSettingsChanged", this.handleSettingsChanged);
+ this.loadSendModeFromSettings();
},
beforeDestroy() {
if (this.quill) {
this.quill = null;
}
- // 移除全局监听(必加,避免多组件冲突)
document.removeEventListener("keydown", this.handleGlobalKeydown);
+ window.removeEventListener("systemSettingsChanged", this.handleSettingsChanged);
+ },
+
+ watch: {
+ sendMode(newMode) {
+ this.saveSendModeToSettings(newMode);
+ },
},
methods: {
+ getSystemSendMode() {
+ try {
+ const settings = localStorage.getItem("systemSettings");
+ if (settings) {
+ const parsed = JSON.parse(settings);
+ if (parsed.basicForm && parsed.basicForm.sendKey) {
+ const savedKey = parsed.basicForm.sendKey;
+ if (savedKey === "Ctrl+Enter") {
+ return "ctrlEnter";
+ } else if (savedKey === "Enter") {
+ return "enter";
+ }
+ }
+ }
+ } catch (e) {
+ console.error("读取系统设置失败:", e);
+ }
+ return "ctrlEnter";
+ },
+
+ loadSendModeFromSettings() {
+ const mode = this.getSystemSendMode();
+ if (mode !== this.sendMode) {
+ this.sendMode = mode;
+ this.refreshQuillBindings();
+ }
+ },
+
+ saveSendModeToSettings(mode) {
+ try {
+ const settings = localStorage.getItem("systemSettings");
+ const parsed = settings ? JSON.parse(settings) : {};
+ if (!parsed.basicForm) {
+ parsed.basicForm = {};
+ }
+ parsed.basicForm.sendKey = mode === "ctrlEnter" ? "ctrl" : "enter";
+ localStorage.setItem("systemSettings", JSON.stringify(parsed));
+ } catch (e) {
+ console.error("保存发送模式失败:", e);
+ }
+ },
+
+ handleSettingsChanged(e) {
+ if (e.detail && e.detail.settings) {
+ this.loadSendModeFromSettings();
+ }
+ },
+
// 初始化Quill
initQuill() {
const editorDom = this.$refs.quillEditor;
if (!editorDom) {
- console.warn("Quill container not found, retrying...");
- setTimeout(() => {
- this.initQuill();
- }, 100);
+ this.quillInitRetryCount++;
+ if (this.quillInitRetryCount <= this.maxQuillInitRetries) {
+ console.warn(
+ `Quill container not found, retrying... (${this.quillInitRetryCount}/${this.maxQuillInitRetries})`
+ );
+ setTimeout(() => {
+ this.initQuill();
+ }, 100);
+ } else {
+ console.error("Quill container not found after maximum retries");
+ }
return;
}
- // 防止重复初始化
- if (this.quill) {
- return;
- }
+ this.quillInitRetryCount = 0;
+ if (this.quill) return;
+ // 初始化时注入键盘绑定
this.quill = new Quill(editorDom, {
theme: "snow",
placeholder: "请输入消息...",
modules: {
toolbar: false,
+ keyboard: {
+ bindings: this.getQuillKeyBindings(), // 抽离绑定配置,支持动态更新
+ },
},
readOnly: false,
});
@@ -233,49 +303,103 @@ export default {
toolbar.container.style.display = "none";
}
- // 监听光标位置
this.quill.on("selection-change", (range) => {
this.currentRange = range;
});
- // 监听文本变化,检测@符号输入
this.quill.on("text-change", (delta, oldDelta, source) => {
if (source === "user") {
this.handleTextChange(delta);
}
});
- // 粘贴图片
this.quill.root.addEventListener("paste", this.handlePaste, true);
- // 回车发送逻辑
- this.quill.root.addEventListener("keydown", (e) => {
- if (this.sendMode === "enter") {
- // Enter发送模式:Enter发送,Ctrl+Enter换行
- if (e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
- e.preventDefault();
- this.handleSendText();
- }
- } else {
- // Ctrl+Enter发送模式:Ctrl+Enter发送,Enter换行
- if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- this.handleSendText();
- }
- }
- });
+ // ========= 删除原来这里的 keydown 监听,改用 bindings 控制 =========
+ },
+ // 根据当前 sendMode 返回 Quill 按键规则
+ getQuillKeyBindings() {
+ const self = this;
+ if (this.sendMode === "enter") {
+ // Enter发送模式:Enter=发送,Ctrl/Cmd+Enter=换行,Shift+Enter=换行
+ return {
+ enter: {
+ key: "Enter",
+ handler() {
+ self.handleSendText();
+ },
+ },
+ ctrlEnter: {
+ key: "Enter",
+ ctrlKey: true,
+ metaKey: true,
+ handler(range) {
+ const quill = self.quill;
+ quill.insertText(range.index, "\n");
+ quill.setSelection(range.index + 1);
+ },
+ },
+ shiftEnter: {
+ key: "Enter",
+ shiftKey: true,
+ handler(range) {
+ const quill = self.quill;
+ quill.insertText(range.index, "\n");
+ quill.setSelection(range.index + 1);
+ },
+ },
+ };
+ } else {
+ // Ctrl+Enter发送模式:Enter=换行,Shift+Enter=换行,Ctrl/Cmd+Enter=发送
+ return {
+ enter: {
+ key: "Enter",
+ shiftKey: null,
+ handler(range) {
+ const quill = self.quill;
+ quill.insertText(range.index, "\n");
+ quill.setSelection(range.index + 1);
+ },
+ },
+ shiftEnter: {
+ key: "Enter",
+ shiftKey: true,
+ handler(range) {
+ const quill = self.quill;
+ quill.insertText(range.index, "\n");
+ quill.setSelection(range.index + 1);
+ },
+ },
+ ctrlEnter: {
+ key: "Enter",
+ ctrlKey: true,
+ metaKey: true,
+ handler() {
+ self.handleSendText();
+ },
+ },
+ };
+ }
+ },
+
+ // 切换发送模式后,刷新Quill键盘绑定
+ refreshQuillBindings() {
+ if (!this.quill) return;
+ const keyboard = this.quill.getModule("keyboard");
+ // 清空旧绑定,替换新规则
+ keyboard.bindings = this.getQuillKeyBindings();
},
handleGlobalKeydown(e) {
- // 条件:Ctrl+Enter / Command+Enter(Mac)发送
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
- // 排除弹窗、输入框、下拉等场景误触发
const tag = document.activeElement.tagName;
if (["INPUT", "TEXTAREA", "SELECT"].includes(tag)) return;
- // 无内容 / 发送中 不执行
const text = this.quill ? this.quill.getText().trim() : "";
if (!text || this.sending) return;
+ // 仅在 ctrlEnter 模式下全局快捷键生效
+ if (this.sendMode !== "ctrlEnter") return;
+
e.preventDefault();
this.handleSendText();
}
@@ -323,6 +447,7 @@ export default {
if (this.quill) {
this.quill.setText("");
}
+ this.editorFiles = [];
},
// 字号切换 - 调整消息显示区域的文字大小
@@ -333,6 +458,8 @@ export default {
// 发送方式切换
handleSendModeChange(mode) {
this.sendMode = mode;
+ // 切换后刷新键盘规则立即生效
+ this.refreshQuillBindings();
},
// 文件按钮
@@ -345,24 +472,32 @@ export default {
try {
await takeScreenshot(
async (blob) => {
- const fileName = `screenshot_${Date.now()}.png`;
- const objectName = this.getObjectPath(fileName);
-
- await this.ensureMinioInitialized();
- const config = this.$store.getters.config;
- const bucket = config.MINIO_BUCKET_FILE || "remote-file-test";
-
- const result = await uploadFile(
- bucket,
- objectName,
- blob,
- {},
- (percent) => {}
- );
-
- const uploadedPath = bucket + "/" + result.objectName;
- this.insertImageToEditor(uploadedPath);
- this.$message.success("截图已插入到编辑器");
+ try {
+ const fileName = `screenshot_${Date.now()}.png`;
+ const objectName = this.getObjectPath(fileName);
+
+ await this.ensureMinioInitialized();
+ const config = this.$store.getters.config;
+ const bucket = config.MINIO_BUCKET_FILE || "remote-file-test";
+
+ const result = await uploadFile(
+ bucket,
+ objectName,
+ blob,
+ {},
+ (percent) => {}
+ );
+
+ const uploadedPath = bucket + "/" + result.objectName;
+ // 创建文件对象用于 insertFileToEditor
+ const screenshotFile = new File([blob], fileName, {
+ type: "image/png",
+ });
+ await this.insertFileToEditor(screenshotFile, uploadedPath);
+ this.$message.success("截图已插入到编辑器");
+ } catch (e) {
+ this.$message.error("截图上传失败: " + e.message);
+ }
},
() => {
this.$message.info("已取消截图");
@@ -375,8 +510,6 @@ export default {
// ========== 核心:选中@成员 插入富文本 ==========
handleAtSelect(user) {
- console.log(user);
-
if (!this.quill || !user) return;
const index = this.currentRange
? this.currentRange.index
@@ -433,7 +566,14 @@ export default {
const html = this.quill.root.innerHTML;
const textContent = this.quill.getText().trim();
- if (!textContent) return;
+ // 检查是否有内容可以发送(文本或文件)
+ const hasText = textContent.replace(/\s+/g, "").length > 0;
+ const files = this.extractFilesFromEditor();
+
+ if (!hasText && files.length === 0) {
+ this.$message.warning("请输入内容或上传文件");
+ return;
+ }
this.sending = true;
@@ -476,13 +616,13 @@ export default {
}
},
- // 解析编辑器内容,分离文本和图片
+ // 解析编辑器内容,分离文本和文件
parseEditorContent(html, textContent) {
const messages = [];
const atUserIds = this.extractAtUsers(html);
const textOnly = textContent.replace(/\s+/g, "").length > 0;
- const images = this.extractImagesFromEditor();
+ const files = this.extractFilesFromEditor();
if (textOnly) {
messages.push({
@@ -505,67 +645,86 @@ export default {
});
}
- images.forEach((image) => {
+ files.forEach((file) => {
+ const msgType = this.getMessageType(file.type);
+
messages.push({
at_users: [],
message_id: 0,
payload: {
content: "",
file_duration: 0,
- file_ico: "",
- file_name: image.fileName,
- file_path: image.filePath,
- file_size: image.fileSize || 0,
- file_type: "image",
+ file_ico: file.icon,
+ file_name: file.name,
+ file_path: file.path,
+ file_size: file.size || 0,
+ file_type: file.type,
},
scene: this.currentChat.scene,
source_id: this.userInfo.id,
target_id: this.currentChat.id,
timestamp: 0,
- type: "image",
+ type: msgType,
});
});
return messages;
},
- // 从编辑器中提取图片信息
- extractImagesFromEditor() {
- const images = [];
- if (!this.quill) return images;
+ // 从编辑器中提取所有文件信息
+ extractFilesFromEditor() {
+ const files = [];
+ if (!this.quill) return files;
const config = this.$store.getters.config;
const endpoint = config.MINIO_ENDPOINT_HTTPS || "";
- const bucket = config.MINIO_BUCKET_FILE || "remote-file-test";
this.quill.root.querySelectorAll("img").forEach((img) => {
const src = img.src;
if (src.startsWith(endpoint)) {
const filePath = src.replace(endpoint, "");
const fileName = filePath.split("/").pop();
- images.push({
- src: src,
- filePath: filePath,
- fileName: fileName,
+ files.push({
+ name: fileName,
+ path: filePath,
+ type: "image",
+ size: 0,
+ icon: "",
});
}
});
- return images;
+ this.quill.root.querySelectorAll(".file-card").forEach((card) => {
+ const fileIndex = card.getAttribute("data-file-index");
+ if (fileIndex !== null && this.editorFiles[fileIndex]) {
+ files.push(this.editorFiles[fileIndex]);
+ }
+ });
+
+ return files;
+ },
+
+ // 根据文件类型获取消息类型
+ getMessageType(fileType) {
+ switch (fileType) {
+ case "image":
+ return "image";
+ case "video":
+ return "video";
+ case "audio":
+ return "audio";
+ default:
+ return "file";
+ }
},
// 文件相关逻辑
- async handleFileChange(e, type) {
+ async handleFileChange(e) {
const files = e.target.files;
if (!files || !files.length) return;
const file = files[0];
e.target.value = "";
- if (!file.type.startsWith("image/")) {
- this.$message.warning("只能上传图片文件");
- return;
- }
-
if (file.size > this.maxFileSize) {
this.$message.warning("文件大小不能超过100MB");
return;
@@ -593,12 +752,104 @@ export default {
);
const uploadedPath = bucket + "/" + result.objectName;
- this.insertImageToEditor(uploadedPath);
- this.$message.success("图片上传成功");
+
+ // 所有文件都插入到编辑器
+ await this.insertFileToEditor(file, uploadedPath);
+ this.$message.success("文件上传成功");
+ } catch (e) {
+ this.$message.error("文件上传失败: " + e.message);
+ }
+ },
+ // 直接发送文件消息
+ async sendFileMessageDirect(file, filePath) {
+ if (!this.currentChat) return;
+ const messageId = this.generateMessageId();
+
+ const fileType = this.getFileType(file);
+ const messageType = file.type.startsWith("video/") ? "video" : "file";
+
+ const message = {
+ at_users: [],
+ message_id: messageId,
+ payload: {
+ content: "",
+ file_duration: 0,
+ file_ico: this.getFileIcon(fileType),
+ file_name: file.name,
+ file_path: filePath,
+ file_size: file.size,
+ file_type: fileType,
+ },
+ scene: this.currentChat.scene,
+ source_id: this.userInfo.id,
+ target_id: this.currentChat.id,
+ timestamp: Date.now(),
+ type: messageType,
+ is_self: true,
+ sending: true,
+ };
+
+ this.$emit("send-message", message);
+
+ try {
+ const body = {
+ client_id: this.getClientId(),
+ message: {
+ at_users: [],
+ message_id: 0,
+ payload: {
+ content: "",
+ file_duration: 0,
+ file_ico: this.getFileIcon(fileType),
+ file_name: file.name,
+ file_path: filePath,
+ file_size: file.size,
+ file_type: fileType,
+ },
+ scene: this.currentChat.scene,
+ source_id: this.userInfo.id,
+ target_id: this.currentChat.id,
+ timestamp: 0,
+ type: messageType,
+ },
+ target_id: this.currentChat.id,
+ topic: this.getTopic(),
+ };
+
+ if (this.currentChat.scene === ContactsScene.PRIVATE) {
+ await sendPrivateMessage(body);
+ } else if (this.currentChat.scene === ContactsScene.GROUP) {
+ await sendMultiChatMessage(body);
+ }
+
+ this.$emit("update-message-status", messageId, { sending: false });
} catch (e) {
- this.$message.error("图片上传失败: " + e.message);
+ this.$message.error("发送文件失败: " + e.message);
+ this.$emit("update-message-status", messageId, {
+ sending: false,
+ sendFailed: true,
+ });
}
},
+ // 获取文件类型
+ getFileType(file) {
+ const ext = file.name.split(".").pop().toLowerCase();
+ const imageExts = ["jpg", "jpeg", "png", "gif", "bmp", "webp"];
+ const videoExts = ["mp4", "webm", "ogg", "mov", "avi"];
+ const docExts = ["doc", "docx"];
+ const excelExts = ["xls", "xlsx"];
+ const pptExts = ["ppt", "pptx"];
+
+ if (imageExts.includes(ext)) return "image";
+ if (videoExts.includes(ext)) return "video";
+ if (docExts.includes(ext)) return ext;
+ if (excelExts.includes(ext)) return ext;
+ if (pptExts.includes(ext)) return ext;
+ if (ext === "pdf") return "pdf";
+ if (ext === "zip" || ext === "rar") return ext;
+ if (ext === "txt") return "txt";
+ return "file";
+ },
async handlePaste(e) {
const items = e.clipboardData.items;
@@ -728,6 +979,103 @@ export default {
this.quill.setSelection(index + 1);
},
+ insertFileToEditor(file, filePath) {
+ if (!this.quill) return;
+
+ const index = this.currentRange
+ ? this.currentRange.index
+ : this.quill.getLength();
+
+ const fileType = this.getFileType(file);
+ const fileInfo = {
+ name: file.name,
+ path: filePath,
+ type: fileType,
+ size: file.size,
+ icon: this.getFileIcon(fileType),
+ };
+
+ if (!this.editorFiles) {
+ this.editorFiles = [];
+ }
+ this.editorFiles.push(fileInfo);
+
+ if (fileType === "image") {
+ const imageUrl =
+ this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS + filePath;
+ this.quill.insertEmbed(index, "image", imageUrl);
+ } else {
+ const fileSize = this.formatFileSize(file.size);
+ const fileHtml = `
+
+

+
+
+
${file.name}
+
${fileSize}
+
+
+
+
+
`;
+ this.quill.clipboard.dangerouslyPasteHTML(index, fileHtml);
+ }
+ this.quill.setSelection(index + 1);
+ },
+
+ formatFileSize(bytes) {
+ if (bytes === 0) return "0 B";
+ const k = 1024;
+ const sizes = ["B", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
+ },
+
+ getFileType(file) {
+ const fileName = file.name.toLowerCase();
+ const ext = fileName.split(".").pop();
+
+ if (
+ file.type.startsWith("image/") ||
+ ["jpg", "jpeg", "png", "gif", "bmp", "svg"].includes(ext)
+ ) {
+ return "image";
+ }
+ if (
+ file.type.startsWith("video/") ||
+ ["mp4", "webm", "ogg", "mov", "avi"].includes(ext)
+ ) {
+ return "video";
+ }
+ if (
+ file.type.startsWith("audio/") ||
+ ["mp3", "wav", "ogg"].includes(ext)
+ ) {
+ return "audio";
+ }
+ if (["zip", "rar", "7z", "tar", "gz"].includes(ext)) {
+ return "zip";
+ }
+ if (["doc", "docx"].includes(ext)) {
+ return "doc";
+ }
+ if (["xls", "xlsx"].includes(ext)) {
+ return "xls";
+ }
+ if (["ppt", "pptx"].includes(ext)) {
+ return "ppt";
+ }
+ if (ext === "pdf") {
+ return "pdf";
+ }
+ if (ext === "txt") {
+ return "txt";
+ }
+ return "other";
+ },
+
ensureMinioInitialized() {
const config = this.$store.getters.config;
if (!config || !config.MINIO_ENDPOINT) {
@@ -852,6 +1200,14 @@ export default {
::v-deep .ql-editor {
min-height: 100%;
padding: 8px;
+
+ img {
+ max-width: 100%;
+ height: auto;
+ display: block;
+ margin: 4px 0;
+ border-radius: 4px;
+ }
}
::v-deep .ql-toolbar {
@@ -878,4 +1234,89 @@ export default {
}
}
}
+
+.file-card {
+ display: inline-flex;
+ align-items: center;
+ background: #f5f7fa;
+ border: 1px solid #e4e7ed;
+ border-radius: 8px;
+ padding: 8px 12px;
+ margin: 4px 4px 4px 0;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: #eef2f7;
+ border-color: #c0c4cc;
+ }
+
+ .file-icon-wrapper {
+ width: 32px;
+ height: 32px;
+ border-radius: 6px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 10px;
+ flex-shrink: 0;
+
+ .file-icon {
+ width: 18px;
+ height: 18px;
+ object-fit: contain;
+ filter: brightness(0) invert(1);
+ }
+ }
+
+ .file-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+
+ .file-name {
+ font-size: 13px;
+ color: #303133;
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 180px;
+ }
+
+ .file-size {
+ font-size: 11px;
+ color: #909399;
+ }
+ }
+
+ .file-remove {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: #ccc;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: 10px;
+ cursor: pointer;
+ opacity: 0;
+ transition: all 0.2s ease;
+
+ i {
+ font-size: 12px;
+ color: #fff;
+ }
+
+ &:hover {
+ background: #f56c6c;
+ }
+ }
+
+ &:hover .file-remove {
+ opacity: 1;
+ }
+}
\ No newline at end of file