修正MD编辑器

main
mula.liu 2025-11-10 19:42:44 +08:00
parent 70763f502d
commit 06f1b959b4
15 changed files with 816 additions and 267 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
dist.zip

Binary file not shown.

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>慧会议</title>
<title>iMeeting</title>
</head>
<body>
<div id="root"></div>

View File

@ -10,6 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.6",
"@uiw/react-codemirror": "^4.25.3",
"@uiw/react-md-editor": "^4.0.8",
"antd": "^5.27.3",
"axios": "^1.6.2",

View File

@ -96,7 +96,7 @@ function App() {
user ? <EditKnowledgeBase user={user} /> : <Navigate to="/" />
} />
<Route path="/downloads" element={<ClientDownloadPage />} />
<Route path="/meetings/:meetingId/preview" element={<MeetingPreview />} />
<Route path="/meetings/preview/:meeting_id" element={<MeetingPreview />} />
</Routes>
</div>
</Router>

View File

@ -0,0 +1,280 @@
/* Markdown Editor Component */
.markdown-editor-wrapper {
margin-top: 0.5rem;
}
.editor-toolbar {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.5rem;
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 8px 8px 0 0;
flex-wrap: wrap;
}
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
padding: 0.25rem 0.5rem;
background: white;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
color: #374155;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.toolbar-btn:hover {
background: #f0f4ff;
border-color: #667eea;
color: #667eea;
}
.toolbar-btn.active {
background: #667eea;
border-color: #667eea;
color: white;
}
.toolbar-btn.active:hover {
background: #5a67d8;
border-color: #5a67d8;
}
.toolbar-btn strong,
.toolbar-btn em {
font-style: normal;
font-weight: 600;
}
.toolbar-divider {
width: 1px;
height: 24px;
background: #d1d5db;
margin: 0 0.25rem;
}
/* 标题下拉菜单 */
.toolbar-dropdown {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 0.25rem;
background: white;
border: 1px solid #d1d5db;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 100;
min-width: 160px;
overflow: hidden;
}
.dropdown-menu button {
display: block;
width: 100%;
padding: 0.75rem 1rem;
border: none;
background: white;
text-align: left;
cursor: pointer;
transition: background 0.2s ease;
font-family: inherit;
color: #374155;
}
.dropdown-menu button:hover {
background: #f0f4ff;
color: #667eea;
}
.dropdown-menu button h1,
.dropdown-menu button h2,
.dropdown-menu button h3,
.dropdown-menu button h4,
.dropdown-menu button h5,
.dropdown-menu button h6 {
font-weight: 600;
color: inherit;
}
/* CodeMirror 样式覆盖 */
.markdown-editor-wrapper .cm-editor {
border-top: none !important;
border-radius: 0 0 8px 8px !important;
}
/* 预览区域 */
.markdown-preview {
border: 2px solid #e2e8f0;
border-top: none;
border-radius: 0 0 8px 8px;
padding: 2rem;
background: white;
min-height: 400px;
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
margin: 1.5rem 0 0.75rem 0;
font-weight: 600;
color: #1e293b;
line-height: 1.3;
}
.markdown-preview h1:first-child,
.markdown-preview h2:first-child,
.markdown-preview h3:first-child,
.markdown-preview h4:first-child,
.markdown-preview h5:first-child,
.markdown-preview h6:first-child {
margin-top: 0;
}
.markdown-preview h1 { font-size: 1.875rem; }
.markdown-preview h2 { font-size: 1.5rem; }
.markdown-preview h3 { font-size: 1.25rem; }
.markdown-preview h4 { font-size: 1.125rem; }
.markdown-preview h5 { font-size: 1rem; }
.markdown-preview h6 { font-size: 0.875rem; }
.markdown-preview p {
margin: 0.75rem 0;
line-height: 1.7;
color: #475569;
}
.markdown-preview ul,
.markdown-preview ol {
margin: 1rem 0;
padding-left: 2rem;
}
.markdown-preview li {
margin: 0.5rem 0;
line-height: 1.6;
color: #475569;
}
.markdown-preview strong {
font-weight: 600;
color: #1e293b;
}
.markdown-preview em {
font-style: italic;
}
.markdown-preview code {
background: #f1f5f9;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 0.875rem;
color: #dc2626;
}
.markdown-preview pre {
background: #f8fafc;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
border: 1px solid #e2e8f0;
margin: 1rem 0;
}
.markdown-preview pre code {
background: none;
padding: 0;
color: #334155;
}
.markdown-preview blockquote {
border-left: 4px solid #667eea;
padding-left: 1rem;
margin: 1rem 0;
font-style: italic;
color: #64748b;
background: #f8fafc;
padding: 1rem;
border-radius: 0 8px 8px 0;
}
.markdown-preview table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
border: 1px solid #e2e8f0;
}
.markdown-preview th,
.markdown-preview td {
border: 1px solid #e2e8f0;
padding: 0.75rem;
text-align: left;
}
.markdown-preview th {
background: #f8fafc;
font-weight: 600;
color: #334155;
}
.markdown-preview hr {
border: none;
height: 1px;
background: #e2e8f0;
margin: 2rem 0;
}
.markdown-preview img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1rem 0;
}
.markdown-preview a {
color: #667eea;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s ease;
}
.markdown-preview a:hover {
border-bottom-color: #667eea;
}
/* 响应式 */
@media (max-width: 768px) {
.editor-toolbar {
gap: 0.125rem;
padding: 0.375rem;
}
.toolbar-btn {
min-width: 28px;
height: 28px;
font-size: 0.75rem;
}
.markdown-preview {
padding: 1rem;
}
}

View File

@ -0,0 +1,233 @@
import React, { useState, useRef, useMemo } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { EditorView } from '@codemirror/view';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import './MarkdownEditor.css';
const MarkdownEditor = ({
value,
onChange,
onImageUpload,
placeholder = '在这里编写内容...',
height = 400,
showImageUpload = true
}) => {
const editorRef = useRef(null);
const imageInputRef = useRef(null);
const [showPreview, setShowPreview] = useState(false);
const [showHeadingMenu, setShowHeadingMenu] = useState(false);
// CodeMirror extensions
const editorExtensions = useMemo(() => [
markdown({ base: markdownLanguage }),
EditorView.lineWrapping,
EditorView.theme({
"&": {
fontSize: "14px",
border: "2px solid #e2e8f0",
borderRadius: "0 0 8px 8px",
borderTop: "none",
},
".cm-content": {
fontFamily: "'Monaco', 'Menlo', 'Consolas', monospace",
padding: "1rem",
minHeight: `${height}px`,
},
".cm-scroller": {
fontFamily: "'Monaco', 'Menlo', 'Consolas', monospace",
},
"&.cm-focused": {
outline: "none",
borderColor: "#667eea",
boxShadow: "0 0 0 3px rgba(102, 126, 234, 0.1)",
}
})
], [height]);
// Markdown
const insertMarkdown = (before, after = '', placeholder = '') => {
if (!editorRef.current?.view) return;
const view = editorRef.current.view;
const selection = view.state.selection.main;
const selectedText = view.state.doc.sliceString(selection.from, selection.to);
const text = selectedText || placeholder;
const newText = `${before}${text}${after}`;
view.dispatch({
changes: { from: selection.from, to: selection.to, insert: newText },
selection: { anchor: selection.from + before.length, head: selection.from + before.length + text.length }
});
view.focus();
};
//
const toolbarActions = {
bold: () => insertMarkdown('**', '**', '粗体文字'),
italic: () => insertMarkdown('*', '*', '斜体文字'),
heading: (level) => {
setShowHeadingMenu(false);
insertMarkdown('#'.repeat(level) + ' ', '', '标题');
},
quote: () => insertMarkdown('> ', '', '引用内容'),
code: () => insertMarkdown('`', '`', '代码'),
codeBlock: () => insertMarkdown('```\n', '\n```', '代码块'),
link: () => insertMarkdown('[', '](url)', '链接文字'),
unorderedList: () => insertMarkdown('- ', '', '列表项'),
orderedList: () => insertMarkdown('1. ', '', '列表项'),
table: () => {
const tableTemplate = '\n| 列1 | 列2 | 列3 |\n| --- | --- | --- |\n| 单元格 | 单元格 | 单元格 |\n| 单元格 | 单元格 | 单元格 |\n';
insertMarkdown(tableTemplate, '', '');
},
hr: () => insertMarkdown('\n---\n', '', ''),
image: () => imageInputRef.current?.click(),
};
//
const handleImageSelect = async (event) => {
const file = event.target.files[0];
if (file && onImageUpload) {
const imageUrl = await onImageUpload(file);
if (imageUrl) {
insertMarkdown(`![${file.name}](${imageUrl})`, '', '');
}
}
// Reset file input
if (imageInputRef.current) {
imageInputRef.current.value = '';
}
};
return (
<div className="markdown-editor-wrapper">
<div className="editor-toolbar">
<button type="button" className="toolbar-btn" onClick={toolbarActions.bold} title="粗体 (Ctrl+B)">
<strong>B</strong>
</button>
<button type="button" className="toolbar-btn" onClick={toolbarActions.italic} title="斜体 (Ctrl+I)">
<em>I</em>
</button>
{/* 多级标题下拉菜单 */}
<div className="toolbar-dropdown">
<button
type="button"
className="toolbar-btn"
onClick={() => setShowHeadingMenu(!showHeadingMenu)}
title="标题"
>
H
</button>
{showHeadingMenu && (
<div className="dropdown-menu">
<button type="button" onClick={() => toolbarActions.heading(1)}>
<h1 style={{ fontSize: '1.5rem', margin: 0 }}>标题 1</h1>
</button>
<button type="button" onClick={() => toolbarActions.heading(2)}>
<h2 style={{ fontSize: '1.3rem', margin: 0 }}>标题 2</h2>
</button>
<button type="button" onClick={() => toolbarActions.heading(3)}>
<h3 style={{ fontSize: '1.1rem', margin: 0 }}>标题 3</h3>
</button>
<button type="button" onClick={() => toolbarActions.heading(4)}>
<h4 style={{ fontSize: '1rem', margin: 0 }}>标题 4</h4>
</button>
<button type="button" onClick={() => toolbarActions.heading(5)}>
<h5 style={{ fontSize: '0.9rem', margin: 0 }}>标题 5</h5>
</button>
<button type="button" onClick={() => toolbarActions.heading(6)}>
<h6 style={{ fontSize: '0.85rem', margin: 0 }}>标题 6</h6>
</button>
</div>
)}
</div>
<span className="toolbar-divider"></span>
<button type="button" className="toolbar-btn" onClick={toolbarActions.quote} title="引用">
"
</button>
<button type="button" className="toolbar-btn" onClick={toolbarActions.code} title="代码">
{'<>'}
</button>
<button type="button" className="toolbar-btn" onClick={toolbarActions.link} title="链接">
🔗
</button>
<button type="button" className="toolbar-btn" onClick={toolbarActions.table} title="表格">
</button>
{showImageUpload && (
<button type="button" className="toolbar-btn" onClick={toolbarActions.image} title="上传图片">
</button>
)}
<span className="toolbar-divider"></span>
<button type="button" className="toolbar-btn" onClick={toolbarActions.unorderedList} title="无序列表">
</button>
<button type="button" className="toolbar-btn" onClick={toolbarActions.orderedList} title="有序列表">
1.
</button>
<button type="button" className="toolbar-btn" onClick={toolbarActions.hr} title="分隔线">
</button>
<span className="toolbar-divider"></span>
{/* 预览按钮 */}
<button
type="button"
className={`toolbar-btn ${showPreview ? 'active' : ''}`}
onClick={() => setShowPreview(!showPreview)}
title={showPreview ? "编辑" : "预览"}
>
{showPreview ? '编辑' : '预览'}
</button>
</div>
{showPreview ? (
<div className="markdown-preview">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
>
{value || '*暂无内容*'}
</ReactMarkdown>
</div>
) : (
<CodeMirror
ref={editorRef}
value={value}
onChange={onChange}
extensions={editorExtensions}
placeholder={placeholder}
basicSetup={{
lineNumbers: false,
foldGutter: false,
highlightActiveLineGutter: false,
highlightActiveLine: false,
}}
/>
)}
{showImageUpload && (
<input
ref={imageInputRef}
type="file"
accept="image/*"
onChange={handleImageSelect}
style={{ display: 'none' }}
/>
)}
</div>
);
};
export default MarkdownEditor;

View File

@ -1,11 +1,10 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import React, { useState, useEffect } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import apiClient from '../utils/apiClient';
import { ArrowLeft, FileText, Tag, Save } 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 TagEditor from '../components/TagEditor';
import MarkdownEditor from '../components/MarkdownEditor';
import './EditKnowledgeBase.css';
const EditKnowledgeBase = ({ user }) => {
@ -21,9 +20,6 @@ const EditKnowledgeBase = ({ user }) => {
const [error, setError] = useState('');
const [kb, setKb] = useState(null);
const handleContentChange = useCallback((value) => {
setFormData(prev => ({ ...prev, content: value || '' }));
}, []);
useEffect(() => {
fetchKbData();
@ -88,44 +84,6 @@ const EditKnowledgeBase = ({ user }) => {
}
};
//
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,
commands.image,
commands.divider,
commands.unorderedListCommand,
commands.orderedListCommand,
commands.checkedListCommand,
];
//
const customExtraCommands = [
commands.codeEdit,
commands.codeLive,
commands.codePreview,
commands.divider,
commands.fullscreen,
];
if (isLoading) {
return (
@ -192,35 +150,15 @@ const EditKnowledgeBase = ({ user }) => {
内容总结
</label>
</div>
<div className="markdown-editor-container">
<MDEditor
key="content-editor"
value={formData.content}
onChange={handleContentChange}
data-color-mode="light"
height={500}
preview="edit"
hideToolbar={false}
toolbarBottom={false}
commands={customCommands}
extraCommands={customExtraCommands}
autoFocus={false}
textareaProps={{
placeholder: '在这里编写知识库内容摘要...',
style: {
fontSize: '14px',
lineHeight: '1.5',
fontFamily: 'inherit'
},
spellCheck: false,
autoComplete: 'off',
autoCapitalize: 'off',
autoCorrect: 'off'
}}
/>
</div>
<MarkdownEditor
value={formData.content}
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
placeholder="在这里编写知识库内容摘要..."
height={500}
showImageUpload={false}
/>
<div className="markdown-hint">
<small>使用Markdown格式编写知识库内容支持**粗体***斜体*# 标题- 列表等格式</small>
<small>使用Markdown格式编写知识库内容支持**粗体***斜体*# 标题- 列表表格等格式</small>
</div>
</div>

View File

@ -498,50 +498,6 @@
transform: none;
}
/* Markdown Editor */
.markdown-editor-container {
margin-top: 0.5rem;
}
.markdown-editor-container .w-md-editor {
background-color: white;
}
.markdown-editor-container .w-md-editor-text-input,
.markdown-editor-container .w-md-editor-text-textarea,
.markdown-editor-container .w-md-editor-text {
font-size: 0.9rem !important;
line-height: 1.6 !important;
caret-color: #667eea !important;
}
.markdown-editor-container .w-md-editor-text-input {
resize: none !important;
}
.markdown-editor-container .w-md-editor-text-textarea {
resize: none !important;
outline: none !important;
border: none !important;
box-shadow: none !important;
}
.markdown-editor-container .w-md-editor-toolbar {
background-color: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.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;

View File

@ -1,19 +1,17 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import apiClient from '../utils/apiClient';
import configService from '../utils/configService';
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Image, Tag } from 'lucide-react';
import MDEditor, * as commands from '@uiw/react-md-editor';
import '@uiw/react-md-editor/markdown-editor.css';
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Tag } from 'lucide-react';
import { buildApiUrl, API_ENDPOINTS, API_BASE_URL } from '../config/api';
import DateTimePicker from '../components/DateTimePicker';
import TagEditor from '../components/TagEditor';
import MarkdownEditor from '../components/MarkdownEditor';
import './EditMeeting.css';
const EditMeeting = ({ user }) => {
const navigate = useNavigate();
const { meeting_id } = useParams();
const imageInputRef = useRef(null);
const [formData, setFormData] = useState({
title: '',
meeting_time: '',
@ -36,10 +34,6 @@ const EditMeeting = ({ user }) => {
const [maxFileSize, setMaxFileSize] = useState(100 * 1024 * 1024); // 100MB
const [maxImageSize, setMaxImageSize] = useState(10 * 1024 * 1024); // 10MB
const handleSummaryChange = useCallback((value) => {
setFormData(prev => ({ ...prev, summary: value || '' }));
}, []);
useEffect(() => {
fetchMeetingData();
fetchUsers();
@ -232,7 +226,7 @@ const EditMeeting = ({ user }) => {
formData.append('image_file', file);
const response = await apiClient.post(
buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_IMAGE(meeting_id)),
buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_IMAGE(meeting_id)),
formData,
{
headers: {
@ -250,98 +244,6 @@ const EditMeeting = ({ user }) => {
}
};
const insertImageMarkdown = (imageUrl, altText = '图片') => {
const imageMarkdown = `![${altText}](${imageUrl})`;
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 => {
// Exclude users already selected as attendees
const isAlreadySelected = formData.attendees.some(attendee => attendee.user_id === user.user_id);
@ -591,42 +493,16 @@ const EditMeeting = ({ user }) => {
会议摘要
</label>
</div>
<div className="markdown-editor-container">
<MDEditor
key="summary-editor"
value={formData.summary}
onChange={handleSummaryChange}
data-color-mode="light"
height={400}
preview="edit"
hideToolbar={false}
toolbarBottom={false}
commands={customCommands}
extraCommands={customExtraCommands}
autoFocus={false}
textareaProps={{
placeholder: '在这里编写会议摘要...',
style: {
fontSize: '14px',
lineHeight: '1.5',
fontFamily: 'inherit'
},
spellCheck: false,
autoComplete: 'off',
autoCapitalize: 'off',
autoCorrect: 'off'
}}
/>
<input
ref={imageInputRef}
type="file"
accept="image/*"
onChange={handleImageSelect}
style={{ display: 'none' }}
/>
</div>
<MarkdownEditor
value={formData.summary}
onChange={(value) => setFormData(prev => ({ ...prev, summary: value || '' }))}
onImageUpload={handleImageUpload}
placeholder="在这里编写会议摘要..."
height={400}
showImageUpload={true}
/>
<div className="markdown-hint">
<small>使用Markdown格式编写会议摘要支持**粗体***斜体*# 标题- 列表等格式工具栏中可以上传图片或插入图片URL</small>
<small>使用Markdown格式编写会议摘要支持**粗体***斜体*# 标题- 列表表格等格式</small>
</div>
{isUploadingImage && (
<div className="uploading-indicator">

View File

@ -90,7 +90,7 @@ const HomePage = ({ onLogin }) => {
{/* Hero Section */}
<section className="hero">
<div className="hero-content">
<h1 className="hero-title">慧会议 让会议更智能</h1>
<h1 className="hero-title">iMeeting 让会议更智能</h1>
<p className="hero-subtitle">
通过AI将会议音频转录并自动总结对齐团队目标构建企业知识库
</p>

View File

@ -893,6 +893,19 @@
padding: 4px 10px;
border-radius: 12px;
border: 1px solid #e2e8f0;
display: flex;
align-items: center;
gap: 4px;
}
.speaker-index {
font-weight: 500;
font-size: 0.75rem;
color: #64748b;
background: #e2e8f0;
padding: 2px 6px;
border-radius: 8px;
margin-left: 4px;
}
.timestamp {

View File

@ -1266,19 +1266,26 @@ const MeetingDetails = ({ user }) => {
</div>
<div className="transcript-content">
{transcript.map((item, index) => (
<div
key={item.segment_id}
{transcript.map((item, index) => {
//
const speakerSegments = transcript.filter(seg => seg.speaker_id === item.speaker_id);
const currentSpeakerIndex = speakerSegments.findIndex(seg => seg.segment_id === item.segment_id) + 1;
const totalSpeakerSegments = speakerSegments.length;
return (
<div
key={item.segment_id}
ref={(el) => transcriptRefs.current[index] = el}
className={`transcript-item ${currentHighlightIndex === index ? 'active' : ''}`}
>
<div className="transcript-header-item">
<span
<span
className="speaker-name clickable"
onClick={() => jumpToTime(item.start_time_ms / 1000)}
title="跳转到此时间点播放"
>
{item.speaker_tag}
<span className="speaker-index"> {currentSpeakerIndex}/{totalSpeakerSegments}</span>
</span>
<div className="transcript-item-actions">
<span
@ -1299,7 +1306,7 @@ const MeetingDetails = ({ user }) => {
)}
</div>
</div>
<div
<div
className="transcript-text clickable"
onClick={() => jumpToTime(item.start_time_ms / 1000)}
title="跳转到此时间点播放"
@ -1307,7 +1314,8 @@ const MeetingDetails = ({ user }) => {
{item.text_content}
</div>
</div>
))}
);
})}
</div>
</div>
</div>
@ -1574,7 +1582,7 @@ const MeetingDetails = ({ user }) => {
<QRCodeModal
isOpen={showQRModal}
onClose={() => setShowQRModal(false)}
url={`${window.location.origin}/meetings/${meeting_id}/preview`}
url={`${window.location.origin}/meetings/preview/${meeting_id}`}
title={meeting.title}
description="扫描二维码查看会议总结"
/>

View File

@ -7,7 +7,7 @@ import rehypeSanitize from 'rehype-sanitize';
import './MeetingPreview.css';
const MeetingPreview = () => {
const { meetingId } = useParams();
const { meeting_id } = useParams();
const [meetingData, setMeetingData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@ -15,13 +15,13 @@ const MeetingPreview = () => {
useEffect(() => {
fetchMeetingPreviewData();
}, [meetingId]);
}, [meeting_id]);
const fetchMeetingPreviewData = async () => {
try {
setLoading(true);
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
const response = await fetch(`${apiUrl}/api/meetings/${meetingId}/preview-data`);
const response = await fetch(`${apiUrl}/api/meetings/${meeting_id}/preview-data`);
const result = await response.json();
if (result.code === "200") {

243
yarn.lock
View File

@ -201,7 +201,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.27.1"
"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.21.5", "@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0":
"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.21.5", "@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0":
version "7.28.4"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
@ -246,6 +246,135 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.7.1":
version "6.19.1"
resolved "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz#355e49c9fd275b42a6e16e9ea0cf4361f67a3ec4"
integrity sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0":
version "6.10.0"
resolved "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.10.0.tgz#b3206984fec8443c4d910565eb2e9d591c7d80b2"
integrity sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.4.0"
"@codemirror/view" "^6.27.0"
"@lezer/common" "^1.1.0"
"@codemirror/lang-css@^6.0.0":
version "6.3.1"
resolved "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz#763ca41aee81bb2431be55e3cfcc7cc8e91421a3"
integrity sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@lezer/common" "^1.0.2"
"@lezer/css" "^1.1.7"
"@codemirror/lang-html@^6.0.0":
version "6.4.11"
resolved "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.11.tgz#c46ba46ae642fd567cf05c4129005d2913ac248d"
integrity sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/lang-css" "^6.0.0"
"@codemirror/lang-javascript" "^6.0.0"
"@codemirror/language" "^6.4.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@lezer/css" "^1.1.0"
"@lezer/html" "^1.3.12"
"@codemirror/lang-javascript@^6.0.0":
version "6.2.4"
resolved "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz#eef2227d1892aae762f3a0f212f72bec868a02c5"
integrity sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.6.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@lezer/javascript" "^1.0.0"
"@codemirror/lang-markdown@^6.5.0":
version "6.5.0"
resolved "https://registry.npmmirror.com/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz#29df87310a555b007beba8e12893363956a26e8e"
integrity sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==
dependencies:
"@codemirror/autocomplete" "^6.7.1"
"@codemirror/lang-html" "^6.0.0"
"@codemirror/language" "^6.3.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@lezer/common" "^1.2.1"
"@lezer/markdown" "^1.0.0"
"@codemirror/language@^6.0.0", "@codemirror/language@^6.3.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0":
version "6.11.3"
resolved "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.3.tgz#8e6632df566a7ed13a1bd307f9837765bb1abfdd"
integrity sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.23.0"
"@lezer/common" "^1.1.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
style-mod "^4.0.0"
"@codemirror/lint@^6.0.0":
version "6.9.2"
resolved "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.2.tgz#09ed0aedec13381c9e36e1ac5d126027740c3ef4"
integrity sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.35.0"
crelt "^1.0.5"
"@codemirror/search@^6.0.0":
version "6.5.11"
resolved "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.11.tgz#a324ffee36e032b7f67aa31c4fb9f3e6f9f3ed63"
integrity sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0", "@codemirror/state@^6.5.2":
version "6.5.2"
resolved "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz#8eca3a64212a83367dc85475b7d78d5c9b7076c6"
integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==
dependencies:
"@marijn/find-cluster-break" "^1.0.0"
"@codemirror/theme-one-dark@^6.0.0":
version "6.1.3"
resolved "https://registry.npmmirror.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz#1dbb73f6e73c53c12ad2aed9f48c263c4e63ea37"
integrity sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@lezer/highlight" "^1.0.0"
"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0", "@codemirror/view@^6.38.6":
version "6.38.6"
resolved "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.6.tgz#25d9df071393801196c311025d2caa7a5523c26c"
integrity sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==
dependencies:
"@codemirror/state" "^6.5.0"
crelt "^1.0.6"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@emotion/hash@^0.8.0":
version "0.8.0"
resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz"
@ -513,6 +642,65 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@lezer/common@^1.0.0", "@lezer/common@^1.0.2", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0", "@lezer/common@^1.2.1", "@lezer/common@^1.3.0":
version "1.3.0"
resolved "https://registry.npmmirror.com/@lezer/common/-/common-1.3.0.tgz#123427ec4c53c2c8367415b4441e555b4f85c696"
integrity sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==
"@lezer/css@^1.1.0", "@lezer/css@^1.1.7":
version "1.3.0"
resolved "https://registry.npmmirror.com/@lezer/css/-/css-1.3.0.tgz#296f298814782c2fad42a936f3510042cdcd2034"
integrity sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.3.0"
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
version "1.2.3"
resolved "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz#a20f324b71148a2ea9ba6ff42e58bbfaec702857"
integrity sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==
dependencies:
"@lezer/common" "^1.3.0"
"@lezer/html@^1.3.12":
version "1.3.12"
resolved "https://registry.npmmirror.com/@lezer/html/-/html-1.3.12.tgz#a438e2d04f4c863d49cad27efe714cde8cf3ff1b"
integrity sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/javascript@^1.0.0":
version "1.5.4"
resolved "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.4.tgz#11746955f957d33c0933f17d7594db54a8b4beea"
integrity sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.1.3"
"@lezer/lr" "^1.3.0"
"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0":
version "1.4.3"
resolved "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.3.tgz#51b252ff8ff9fea863819de7f4b6501ccf69d403"
integrity sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==
dependencies:
"@lezer/common" "^1.0.0"
"@lezer/markdown@^1.0.0":
version "1.6.0"
resolved "https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.6.0.tgz#9db3356d52955391b021cc3ead9c79f87186840f"
integrity sha512-AXb98u3M6BEzTnreBnGtQaF7xFTiMA92Dsy5tqEjpacbjRxDSFdN4bKJo9uvU4cEEOS7D2B9MT7kvDgOEIzJSw==
dependencies:
"@lezer/common" "^1.0.0"
"@lezer/highlight" "^1.0.0"
"@marijn/find-cluster-break@^1.0.0":
version "1.0.2"
resolved "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@rc-component/async-validator@^5.0.3":
version "5.0.4"
resolved "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz"
@ -825,11 +1013,36 @@
resolved "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz"
integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==
"@uiw/codemirror-extensions-basic-setup@4.25.3":
version "4.25.3"
resolved "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.3.tgz#6fb28745e7012bfcad0dc5103119487e40a744bc"
integrity sha512-F1doRyD50CWScwGHG2bBUtUpwnOv/zqSnzkZqJcX5YAHQx6Z1CuX8jdnFMH6qktRrPU1tfpNYftTWu3QIoHiMA==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/commands" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/search" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@uiw/copy-to-clipboard@~1.0.12":
version "1.0.17"
resolved "https://registry.npmmirror.com/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.17.tgz"
integrity sha512-O2GUHV90Iw2VrSLVLK0OmNIMdZ5fgEg4NhvtwINsX+eZ/Wf6DWD0TdsK9xwV7dNRnK/UI2mQtl0a2/kRgm1m1A==
"@uiw/react-codemirror@^4.25.3":
version "4.25.3"
resolved "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.25.3.tgz#dd61549051d4398068f087858b39f3fc988c7537"
integrity sha512-1wtBZTXPIp8u6F/xjHvsUAYlEeF5Dic4xZBnqJyLzv7o7GjGYEUfSz9Z7bo9aK9GAx2uojG/AuBMfhA4uhvIVQ==
dependencies:
"@babel/runtime" "^7.18.6"
"@codemirror/commands" "^6.1.0"
"@codemirror/state" "^6.1.1"
"@codemirror/theme-one-dark" "^6.0.0"
"@uiw/codemirror-extensions-basic-setup" "4.25.3"
codemirror "^6.0.0"
"@uiw/react-markdown-preview@^5.0.6":
version "5.1.5"
resolved "https://registry.npmmirror.com/@uiw/react-markdown-preview/-/react-markdown-preview-5.1.5.tgz"
@ -1137,6 +1350,19 @@ classnames@2.x, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classna
resolved "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
codemirror@^6.0.0:
version "6.0.2"
resolved "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.2.tgz#4d3fea1ad60b6753f97ca835f2f48c6936a8946e"
integrity sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/commands" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/search" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz"
@ -1203,6 +1429,11 @@ core-js@^3.6.0, core-js@^3.8.3:
resolved "https://registry.npmmirror.com/core-js/-/core-js-3.45.1.tgz"
integrity sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==
crelt@^1.0.5, crelt@^1.0.6:
version "1.0.6"
resolved "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz"
@ -3879,6 +4110,11 @@ strip-json-comments@^3.1.1:
resolved "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-mod@^4.0.0, style-mod@^4.1.0:
version "4.1.3"
resolved "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz#6e9012255bb799bdac37e288f7671b5d71bf9f73"
integrity sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==
style-to-js@^1.0.0:
version "1.1.17"
resolved "https://registry.npmmirror.com/style-to-js/-/style-to-js-1.1.17.tgz"
@ -4082,6 +4318,11 @@ vite@^7.0.4:
optionalDependencies:
fsevents "~2.3.3"
w3c-keyname@^2.2.4:
version "2.2.8"
resolved "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
web-namespaces@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz"