增加图片上传按钮
parent
8c303df485
commit
0252c4c34a
|
@ -19,6 +19,7 @@ const API_CONFIG = {
|
||||||
TRANSCRIPT: (meetingId) => `/api/meetings/${meetingId}/transcript`,
|
TRANSCRIPT: (meetingId) => `/api/meetings/${meetingId}/transcript`,
|
||||||
AUDIO: (meetingId) => `/api/meetings/${meetingId}/audio`,
|
AUDIO: (meetingId) => `/api/meetings/${meetingId}/audio`,
|
||||||
UPLOAD_AUDIO: '/api/meetings/upload-audio',
|
UPLOAD_AUDIO: '/api/meetings/upload-audio',
|
||||||
|
UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`,
|
||||||
REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`
|
REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.regenerate-btn {
|
.regenerate-btn {
|
||||||
|
@ -521,11 +521,37 @@
|
||||||
|
|
||||||
.markdown-editor-container .w-md-editor-toolbar button {
|
.markdown-editor-container .w-md-editor-toolbar button {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-editor-container .w-md-editor-toolbar button:hover {
|
.markdown-editor-container .w-md-editor-toolbar button:hover {
|
||||||
background-color: #e2e8f0;
|
background-color: #e2e8f0;
|
||||||
color: #334155;
|
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 */
|
/* Responsive Design */
|
||||||
|
|
|
@ -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 { Link, useNavigate, useParams } from 'react-router-dom';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus } from 'lucide-react';
|
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Image } from 'lucide-react';
|
||||||
import MDEditor from '@uiw/react-md-editor';
|
import MDEditor, * as commands from '@uiw/react-md-editor';
|
||||||
import '@uiw/react-md-editor/markdown-editor.css';
|
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';
|
import './EditMeeting.css';
|
||||||
|
|
||||||
const EditMeeting = ({ user }) => {
|
const EditMeeting = ({ user }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { meeting_id } = useParams();
|
const { meeting_id } = useParams();
|
||||||
|
const imageInputRef = useRef(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: '',
|
title: '',
|
||||||
meeting_time: '',
|
meeting_time: '',
|
||||||
|
@ -23,6 +24,7 @@ const EditMeeting = ({ user }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [isUploadingImage, setIsUploadingImage] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [meeting, setMeeting] = useState(null);
|
const [meeting, setMeeting] = useState(null);
|
||||||
const [showUploadArea, setShowUploadArea] = useState(false);
|
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: <span style={{fontSize:12}}>UploadImage</span>,
|
||||||
|
execute: () => {
|
||||||
|
imageInputRef.current?.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建修改后的图片URL命令
|
||||||
|
const imageUrlCommand = {
|
||||||
|
...commands.image,
|
||||||
|
name: 'image-url',
|
||||||
|
icon: <span style={{fontSize:12}}>AddImageURL</span>,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义工具栏命令配置
|
||||||
|
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: (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
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 => {
|
const filteredUsers = availableUsers.filter(user => {
|
||||||
// Exclude users already selected as attendees
|
// Exclude users already selected as attendees
|
||||||
const isAlreadySelected = formData.attendees.some(attendee => attendee.user_id === user.user_id);
|
const isAlreadySelected = formData.attendees.some(attendee => attendee.user_id === user.user_id);
|
||||||
|
@ -406,11 +542,26 @@ const EditMeeting = ({ user }) => {
|
||||||
preview="edit"
|
preview="edit"
|
||||||
hideToolbar={false}
|
hideToolbar={false}
|
||||||
toolbarBottom={false}
|
toolbarBottom={false}
|
||||||
|
visibleDragBar={false}
|
||||||
|
commands={customCommands}
|
||||||
|
extraCommands={customExtraCommands}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref={imageInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageSelect}
|
||||||
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="markdown-hint">
|
<div className="markdown-hint">
|
||||||
<small>使用Markdown格式编写会议摘要,支持**粗体**、*斜体*、# 标题、- 列表等格式</small>
|
<small>使用Markdown格式编写会议摘要,支持**粗体**、*斜体*、# 标题、- 列表等格式。工具栏中可以上传图片或插入图片URL。</small>
|
||||||
</div>
|
</div>
|
||||||
|
{isUploadingImage && (
|
||||||
|
<div className="uploading-indicator">
|
||||||
|
<span>正在上传图片...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
Loading…
Reference in New Issue