diff --git a/src/config/api.js b/src/config/api.js index a7034c8..46460cc 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -19,6 +19,7 @@ const API_CONFIG = { TRANSCRIPT: (meetingId) => `/api/meetings/${meetingId}/transcript`, AUDIO: (meetingId) => `/api/meetings/${meetingId}/audio`, UPLOAD_AUDIO: '/api/meetings/upload-audio', + UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`, REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary` } } diff --git a/src/pages/EditMeeting.css b/src/pages/EditMeeting.css index 1463a7e..72d5e36 100644 --- a/src/pages/EditMeeting.css +++ b/src/pages/EditMeeting.css @@ -96,7 +96,7 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 0.75rem; + margin-bottom: 0.25rem; } .regenerate-btn { @@ -521,11 +521,37 @@ .markdown-editor-container .w-md-editor-toolbar button { color: #64748b; + text-align: left; } .markdown-editor-container .w-md-editor-toolbar button:hover { background-color: #e2e8f0; color: #334155; + text-align: left; +} + +/* Upload indicator */ +.uploading-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 6px; + margin-top: 0.5rem; + color: #0369a1; + font-size: 0.9rem; +} + +.uploading-indicator::before { + content: ''; + width: 16px; + height: 16px; + border: 2px solid #bae6fd; + border-top: 2px solid #0369a1; + border-radius: 50%; + animation: spin 1s linear infinite; } /* Responsive Design */ diff --git a/src/pages/EditMeeting.jsx b/src/pages/EditMeeting.jsx index 6c17c95..4dfdf9e 100644 --- a/src/pages/EditMeeting.jsx +++ b/src/pages/EditMeeting.jsx @@ -1,15 +1,16 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Link, useNavigate, useParams } from 'react-router-dom'; import axios from 'axios'; -import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus } from 'lucide-react'; -import MDEditor from '@uiw/react-md-editor'; +import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Image } from 'lucide-react'; +import MDEditor, * as commands from '@uiw/react-md-editor'; import '@uiw/react-md-editor/markdown-editor.css'; -import { buildApiUrl, API_ENDPOINTS } from '../config/api'; +import { buildApiUrl, API_ENDPOINTS, API_BASE_URL } from '../config/api'; import './EditMeeting.css'; const EditMeeting = ({ user }) => { const navigate = useNavigate(); const { meeting_id } = useParams(); + const imageInputRef = useRef(null); const [formData, setFormData] = useState({ title: '', meeting_time: '', @@ -23,6 +24,7 @@ const EditMeeting = ({ user }) => { const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isUploading, setIsUploading] = useState(false); + const [isUploadingImage, setIsUploadingImage] = useState(false); const [error, setError] = useState(''); const [meeting, setMeeting] = useState(null); const [showUploadArea, setShowUploadArea] = useState(false); @@ -185,6 +187,140 @@ const EditMeeting = ({ user }) => { } }; + const handleImageUpload = async (file) => { + if (!file) return null; + + // Validate file type + const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; + if (!allowedTypes.includes(file.type)) { + setError('请上传支持的图片格式 (JPG, PNG, GIF, WebP)'); + return null; + } + + // Validate file size (10MB) + if (file.size > 10 * 1024 * 1024) { + setError('图片大小不能超过10MB'); + return null; + } + + setIsUploadingImage(true); + setError(''); + + try { + const formData = new FormData(); + formData.append('image_file', file); + + const response = await axios.post( + buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_IMAGE(meeting_id)), + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); + + return `${API_BASE_URL}${response.data.url}`; + } catch (err) { + setError('上传图片失败,请重试'); + return null; + } finally { + setIsUploadingImage(false); + } + }; + + const insertImageMarkdown = (imageUrl, altText = '图片') => { + const imageMarkdown = ``; + setFormData(prev => ({ + ...prev, + summary: prev.summary + '\n\n' + imageMarkdown + })); + }; + + const handleImageSelect = async (event) => { + const file = event.target.files[0]; + if (file) { + const imageUrl = await handleImageUpload(file); + if (imageUrl) { + insertImageMarkdown(imageUrl, file.name); + } + } + // Reset file input + if (imageInputRef.current) { + imageInputRef.current.value = ''; + } + }; + + // 创建自定义上传图片命令(只显示文字,不显示图标) + const uploadImageCommand = { + name: 'upload-image', + keyCommand: 'upload-image', + buttonProps: { 'aria-label': '上传本地图片', title: '上传本地图片' }, + icon: UploadImage, + execute: () => { + imageInputRef.current?.click(); + } + }; + + // 创建修改后的图片URL命令 + const imageUrlCommand = { + ...commands.image, + name: 'image-url', + icon: AddImageURL, + }; + + // 自定义工具栏命令配置 + const customCommands = [ + commands.bold, + commands.italic, + commands.strikethrough, + commands.hr, + commands.group([ + commands.title1, + commands.title2, + commands.title3, + commands.title4, + commands.title5, + commands.title6, + ], { + name: 'title', + groupName: 'title', + buttonProps: { 'aria-label': '插入标题', title: '插入标题' } + }), + commands.divider, + commands.link, + commands.quote, + commands.code, + commands.codeBlock, + // 创建图片功能组,使用系统自带的image命令和自定义上传命令 + commands.group([ + imageUrlCommand, + uploadImageCommand + ], { + name: 'image-group', + groupName: 'image', + buttonProps: { 'aria-label': '添加图片', title: '添加图片' }, + icon: ( + + ) + }), + commands.divider, + commands.unorderedListCommand, + commands.orderedListCommand, + commands.checkedListCommand, + ]; + + // 右侧额外命令(预览、全屏等) + const customExtraCommands = [ + commands.codeEdit, + commands.codeLive, + commands.codePreview, + commands.divider, + commands.fullscreen, + ]; + const filteredUsers = availableUsers.filter(user => { // Exclude users already selected as attendees const isAlreadySelected = formData.attendees.some(attendee => attendee.user_id === user.user_id); @@ -406,11 +542,26 @@ const EditMeeting = ({ user }) => { preview="edit" hideToolbar={false} toolbarBottom={false} + visibleDragBar={false} + commands={customCommands} + extraCommands={customExtraCommands} + /> +