更新前端语法,系统环境要求等

codex/dev
AlanPaine 2026-03-26 11:51:00 +00:00
parent 498bd97f99
commit c6188809ff
20 changed files with 1731 additions and 749 deletions

View File

@ -82,8 +82,8 @@ iMeeting 是一个基于 AI 技术的智能会议记录与内容管理平台,通
### 环境要求
- Node.js 16+
- Python 3.9+
- Node.js 22.12+
- Python 3.12+
- MySQL 5.7+
- Redis 5.0+
- Docker (可选)
@ -95,7 +95,7 @@ iMeeting 是一个基于 AI 技术的智能会议记录与内容管理平台,通
```bash
cd backend
pip install -r requirements.txt
python main.py
python app/main.py
```
默认运行在 `http://localhost:8000`
@ -110,6 +110,17 @@ npm run dev
默认运行在 `http://localhost:5173`
#### 使用 Conda 一键启动前后端
项目根目录提供了 Conda 启动脚本,会分别创建并使用独立环境启动前后端:
- 后端环境: `imetting_backend` (Python 3.12)
- 前端环境: `imetting_frontend` (Node.js 22)
```bash
./start-conda.sh
```
### 配置说明
详细的配置文档请参考:

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@
"axios": "^1.6.2",
"canvg": "^4.0.3",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.2",
"jspdf": "^4.2.1",
"markmap-common": "^0.18.9",
"markmap-lib": "^0.18.12",
"markmap-view": "^0.18.12",

View File

@ -23,7 +23,7 @@ const ContentViewer = ({
if (!content) {
return (
<Card bordered={false} style={{ borderRadius: 12 }}>
<Card variant="borderless" style={{ borderRadius: 12 }}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={emptyMessage} />
</Card>
);
@ -59,7 +59,7 @@ const ContentViewer = ({
];
return (
<Card bordered={false} bodyStyle={{ padding: '12px 24px' }} style={{ borderRadius: 12 }}>
<Card variant="borderless" bodyStyle={{ padding: '12px 24px' }} style={{ borderRadius: 12 }}>
<Tabs
className="console-tabs"
activeKey={activeTab}

View File

@ -93,7 +93,7 @@ const MarkdownEditor = ({
<Card
size="small"
bodyStyle={{ padding: '4px 8px', background: '#f5f5f5', borderBottom: '1px solid #d9d9d9', borderRadius: '8px 8px 0 0' }}
bordered={false}
variant="borderless"
>
<Space split={<Divider type="vertical" />} size={4}>
<Space size={2}>

View File

@ -341,7 +341,7 @@ const AdminDashboard = () => {
<Row gutter={[16, 16]} className="admin-overview-grid">
<Col xs={24} sm={12} lg={6}>
<Card className="admin-overview-card" bordered={false}>
<Card className="admin-overview-card" variant="borderless">
<div className="admin-overview-title">用户统计</div>
<div className="admin-overview-main">
<span className="admin-overview-icon users"><UsergroupAddOutlined /></span>
@ -356,7 +356,7 @@ const AdminDashboard = () => {
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card className="admin-overview-card" bordered={false}>
<Card className="admin-overview-card" variant="borderless">
<div className="admin-overview-title">会议统计</div>
<div className="admin-overview-main">
<span className="admin-overview-icon meetings"><VideoCameraAddOutlined /></span>
@ -371,7 +371,7 @@ const AdminDashboard = () => {
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card className="admin-overview-card" bordered={false}>
<Card className="admin-overview-card" variant="borderless">
<div className="admin-overview-title">存储统计</div>
<div className="admin-overview-main">
<span className="admin-overview-icon storage"><DatabaseOutlined /></span>
@ -386,7 +386,7 @@ const AdminDashboard = () => {
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card className="admin-overview-card" bordered={false}>
<Card className="admin-overview-card" variant="borderless">
<div className="admin-overview-title">服务器资源</div>
<div className="admin-overview-main">
<span className="admin-overview-icon resources"><DeploymentUnitOutlined /></span>

View File

@ -9,7 +9,7 @@ const { Title, Paragraph } = Typography;
const ClientDownloadPage = () => {
return (
<div className="download-page-modern" style={{ maxWidth: 1000, margin: '0 auto', padding: '24px 0' }}>
<Card bordered={false} style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
<Card variant="borderless" style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<FireOutlined style={{ fontSize: 48, color: '#1677ff', marginBottom: 24 }} />
<Title level={2}>随时随地开启智能会议</Title>

View File

@ -67,7 +67,7 @@ const CreateMeeting = ({ user }) => {
return (
<div className="create-meeting-modern" style={{ maxWidth: 800, margin: '0 auto', padding: '24px 0' }}>
<Card bordered={false} style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
<Card variant="borderless" style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 32 }}>
<Button icon={<ArrowLeftOutlined />} shape="circle" onClick={() => navigate(-1)} style={{ marginRight: 16 }} />
<Title level={3} style={{ margin: 0 }}>新建会议纪要</Title>

View File

@ -266,7 +266,7 @@ const Dashboard = ({ user }) => {
return (
<div className="dashboard-v3">
<Card bordered={false} className="dashboard-hero-card" style={{ marginBottom: 24 }}>
<Card variant="borderless" className="dashboard-hero-card" style={{ marginBottom: 24 }}>
<Row gutter={[28, 24]} align="middle">
<Col xs={24} xl={8}>
<div className="dashboard-user-block">
@ -327,7 +327,7 @@ const Dashboard = ({ user }) => {
</Row>
</Card>
<Card bordered={false} className="dashboard-main-card" bodyStyle={{ padding: 0 }}>
<Card variant="borderless" className="dashboard-main-card" bodyStyle={{ padding: 0 }}>
<div className="dashboard-toolbar">
<div className="dashboard-search-row">
<Input

View File

@ -51,7 +51,7 @@ const EditKnowledgeBase = ({ user }) => {
return (
<div className="edit-kb-modern" style={{ maxWidth: 1000, margin: '0 auto', padding: '24px 0' }}>
<Card bordered={false} style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
<Card variant="borderless" style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 32 }}>
<Button icon={<ArrowLeftOutlined />} shape="circle" onClick={() => navigate(-1)} style={{ marginRight: 16 }} />
<Title level={3} style={{ margin: 0 }}>编辑知识库条目</Title>

View File

@ -75,7 +75,7 @@ const EditMeeting = ({ user }) => {
return (
<div className="edit-meeting-modern" style={{ maxWidth: 800, margin: '0 auto', padding: '24px 0' }}>
<Card bordered={false} loading={fetching} style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
<Card variant="borderless" loading={fetching} style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 32 }}>
<Button icon={<ArrowLeftOutlined />} shape="circle" onClick={() => navigate(-1)} style={{ marginRight: 16 }} />
<Title level={3} style={{ margin: 0 }}>编辑会议信息</Title>

View File

@ -173,7 +173,7 @@ const MeetingCenterPage = ({ user }) => {
}}
>
<Card
bordered={false}
variant="borderless"
style={{
borderRadius: 20,
marginBottom: 22,
@ -252,7 +252,7 @@ const MeetingCenterPage = ({ user }) => {
return (
<Card
key={meeting.meeting_id}
bordered={false}
variant="borderless"
hoverable
onClick={() => navigate(`/meetings/${meeting.meeting_id}`)}
style={{

View File

@ -374,7 +374,7 @@ const MeetingDetails = ({ user }) => {
<div>
{/* ── 标题 Header ── */}
<Card
bordered={false}
variant="borderless"
className="console-surface"
style={{ marginBottom: 16 }}
styles={{ body: { padding: '16px 24px' } }}
@ -418,7 +418,7 @@ const MeetingDetails = ({ user }) => {
{/* ── 转录进度条 ── */}
{transcriptionStatus && ['pending', 'processing'].includes(transcriptionStatus.status) && (
<Card bordered={false} className="console-surface" style={{ marginBottom: 16 }} styles={{ body: { padding: '12px 24px' } }}>
<Card variant="borderless" className="console-surface" style={{ marginBottom: 16 }} styles={{ body: { padding: '12px 24px' } }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<Text><SyncOutlined spin style={{ marginRight: 8 }} />正在转录中...</Text>
<Text>{transcriptionProgress}%</Text>
@ -442,7 +442,7 @@ const MeetingDetails = ({ user }) => {
{/* 左列: 语音转录 */}
<Col xs={24} lg={10}>
<Card
bordered={false}
variant="borderless"
className="console-surface"
style={{ height: 'calc(100vh - 220px)', display: 'flex', flexDirection: 'column' }}
styles={{ body: { display: 'flex', flexDirection: 'column', flex: 1, overflow: 'hidden', padding: 0 } }}
@ -559,7 +559,7 @@ const MeetingDetails = ({ user }) => {
{/* 右列: AI 总结 / 思维导图 */}
<Col xs={24} lg={14}>
<Card
bordered={false}
variant="borderless"
className="console-surface"
style={{ height: 'calc(100vh - 220px)', display: 'flex', flexDirection: 'column' }}
styles={{ body: { display: 'flex', flexDirection: 'column', flex: 1, overflow: 'hidden', padding: '12px 0 0' } }}
@ -610,7 +610,7 @@ const MeetingDetails = ({ user }) => {
{/* 总结生成中进度条 */}
{summaryLoading && (
<div style={{ padding: '0 16px 16px', flexShrink: 0 }}>
<Card bordered={false} style={{ borderRadius: 10, background: '#f6f8fa' }} styles={{ body: { padding: '12px 16px' } }}>
<Card variant="borderless" style={{ borderRadius: 10, background: '#f6f8fa' }} styles={{ body: { padding: '12px 16px' } }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<Text><SyncOutlined spin style={{ marginRight: 6 }} />{summaryTaskMessage || 'AI 正在思考中...'}</Text>
<Text>{summaryTaskProgress}%</Text>
@ -735,7 +735,7 @@ const MeetingDetails = ({ user }) => {
{/* 生成进度 */}
{summaryLoading && (
<Card bordered={false} style={{ borderRadius: 12, background: '#f6f8fa' }}>
<Card variant="borderless" style={{ borderRadius: 12, background: '#f6f8fa' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<Text><SyncOutlined spin style={{ marginRight: 8 }} />{summaryTaskMessage || 'AI 正在思考中...'}</Text>
<Text>{summaryTaskProgress}%</Text>

View File

@ -98,7 +98,7 @@ const MeetingPreview = () => {
</Header>
<Content style={{ padding: '40px 20px', maxWidth: 900, margin: '0 auto', width: '100%' }}>
<Card bordered={false} style={{ borderRadius: 16 }}>
<Card variant="borderless" style={{ borderRadius: 16 }}>
<Title level={2}>{meeting.title}</Title>
<Space wrap style={{ marginBottom: 20 }}>
{meeting.tags?.map(t => <Tag key={t.id} color="blue">{t.name}</Tag>)}

View File

@ -109,7 +109,7 @@ const PromptConfigPage = ({ user }) => {
return (
<div>
<Card className="console-surface" bordered={false} style={{ marginBottom: 16 }}>
<Card className="console-surface" variant="borderless" style={{ marginBottom: 16 }}>
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
<div>
<Title level={3} style={{ margin: 0 }}>提示词配置</Title>
@ -117,7 +117,7 @@ const PromptConfigPage = ({ user }) => {
</div>
</Space>
</Card>
<Card className="console-surface" bordered={false} bodyStyle={{ padding: 12 }}>
<Card className="console-surface" variant="borderless" bodyStyle={{ padding: 12 }}>
<Tabs
className="console-tabs"
defaultActiveKey="config"
@ -142,7 +142,7 @@ const PromptConfigPage = ({ user }) => {
</div>
<Row gutter={16}>
<Col xs={24} xl={14}>
<Card title="全部可用提示词" loading={loading} bordered={false} className="console-card-panel">
<Card title="全部可用提示词" loading={loading} variant="borderless" className="console-card-panel">
<Space direction="vertical" style={{ width: '100%' }}>
{availablePrompts.length ? availablePrompts.map((item) => {
const isSystem = Number(item.is_system) === 1;
@ -188,7 +188,7 @@ const PromptConfigPage = ({ user }) => {
</Card>
</Col>
<Col xs={24} xl={10}>
<Card title="已启用顺序" loading={loading} bordered={false} className="console-card-panel">
<Card title="已启用顺序" loading={loading} variant="borderless" className="console-card-panel">
<Space direction="vertical" style={{ width: '100%' }}>
{selectedPromptCards.length ? selectedPromptCards.map((item, index) => (
<Card key={item.id} size="small" style={{ borderRadius: 10 }}>

View File

@ -13,6 +13,7 @@ import {
Segmented,
Select,
Space,
Switch,
Tag,
Tooltip,
Typography,
@ -240,7 +241,7 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
return (
<div>
{!embedded ? (
<Card className="console-surface" bordered={false} style={{ marginBottom: 16 }}>
<Card className="console-surface" variant="borderless" style={{ marginBottom: 16 }}>
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
<div>
<Title level={3} style={{ margin: 0 }}>{pageTitle}</Title>
@ -333,7 +334,7 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
)}
</div>
) : (
<Card className="console-surface" bordered={false}>
<Card className="console-surface" variant="borderless">
{prompts.length ? (
<>
<Row gutter={[16, 16]}>

View File

@ -236,7 +236,7 @@ const DictManagement = () => {
新增
</Button>
}
bordered={false}
variant="borderless"
>
<div style={{ marginBottom: 16 }}>
<Select
@ -300,13 +300,14 @@ const DictManagement = () => {
</Space>
)
}
bordered={false}
variant="borderless"
>
{isEditing ? (
<Form
form={form}
layout="vertical"
>
{isEditing ? (
<>
<Row gutter={16}>
<Col span={12}>
<Form.Item
@ -385,13 +386,14 @@ const DictManagement = () => {
</Select>
</Form.Item>
</Space>
</Form>
</>
) : (
<Empty
description="请从左侧树中选择一个节点进行编辑,或点击新增按钮创建新节点"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</Form>
</Card>
</Col>
</Row>

View File

@ -675,7 +675,7 @@ const PermissionManagement = () => {
const roleManagementView = (
<Row gutter={16}>
<Col span={7}>
<Card className="console-surface" bodyStyle={{ padding: 12 }}>
<Card className="console-surface" styles={{ body: { padding: 12 } }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<Title level={5} style={{ margin: 0 }}>角色列表</Title>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={openCreateRoleDrawer}>新增</Button>
@ -730,7 +730,7 @@ const PermissionManagement = () => {
</Col>
<Col span={17}>
<Card className="console-surface" bodyStyle={{ padding: 12 }}>
<Card className="console-surface" styles={{ body: { padding: 12 } }}>
{!selectedRoleId ? (
<Empty description="请选择左侧角色" />
) : (
@ -763,7 +763,7 @@ const PermissionManagement = () => {
</div>
<Row gutter={14}>
<Col span={14}>
<Card size="small" bodyStyle={{ padding: 12, minHeight: 420 }}>
<Card size="small" styles={{ body: { padding: 12, minHeight: 420 } }}>
<Input.Search
allowClear
placeholder="搜索菜单名称或编码"
@ -806,7 +806,7 @@ const PermissionManagement = () => {
<Card
size="small"
title={`已选权限 (${selectedMenuRows.length})`}
bodyStyle={{ padding: 12, minHeight: 420, maxHeight: 420, overflowY: 'auto' }}
styles={{ body: { padding: 12, minHeight: 420, maxHeight: 420, overflowY: 'auto' } }}
>
{selectedMenuRows.length ? selectedMenuRows.map((menu) => (
<div key={menu.menu_id} style={{ padding: '6px 0' }}>
@ -864,7 +864,7 @@ const PermissionManagement = () => {
const menuManagementView = (
<Row gutter={16}>
<Col span={10}>
<Card className="console-surface" bodyStyle={{ padding: 12 }}>
<Card className="console-surface" styles={{ body: { padding: 12 } }}>
<Space style={{ marginBottom: 12, width: '100%', justifyContent: 'space-between' }}>
<Input.Search
allowClear
@ -918,7 +918,7 @@ const PermissionManagement = () => {
<Col span={14}>
<Card
className="console-surface"
bodyStyle={{ padding: 16, minHeight: 640 }}
styles={{ body: { padding: 16, minHeight: 640 } }}
title={
menuPanelMode === 'view' && selectedManageMenu
? `${MENU_PANEL_TITLE_MAP[menuPanelMode]} · ${selectedManageMenu.menu_name}`

File diff suppressed because it is too large Load Diff

139
start-conda.sh 100755
View File

@ -0,0 +1,139 @@
#!/bin/bash
# Exit on error
set -e
# Project Root Directory
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$PROJECT_ROOT/backend"
FRONTEND_DIR="$PROJECT_ROOT/frontend"
# Define environment names
BACKEND_ENV="imetting_backend"
FRONTEND_ENV="imetting_frontend"
PYTHON_VERSION="3.12"
NODE_VERSION="22"
echo "========================================"
echo "Starting iMeeting with Conda"
echo "========================================"
# Try to find conda if not in PATH
if ! command -v conda &> /dev/null; then
echo "Conda not found in PATH, trying common locations..."
# Check common installation paths
CONDA_PATHS=(
"$HOME/miniconda3/bin"
"$HOME/anaconda3/bin"
"/opt/conda/bin"
"$HOME/miniconda/bin"
"$HOME/anaconda/bin"
"/usr/local/miniconda3/bin"
"/usr/local/anaconda3/bin"
)
for bin_path in "${CONDA_PATHS[@]}"; do
if [ -x "$bin_path/conda" ]; then
export PATH="$bin_path:$PATH"
echo "Found conda at: $bin_path/conda"
break
fi
done
fi
if ! command -v conda &> /dev/null; then
echo "Error: Conda is still not found."
echo "Please ensure Miniconda or Anaconda is installed and accessible."
exit 1
fi
# Initialize conda for bash script
eval "$(conda shell.bash hook)"
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main >/dev/null 2>&1 || true
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r >/dev/null 2>&1 || true
version_gte() {
[ "$(printf '%s\n' "$1" "$2" | sort -V | head -n 1)" = "$2" ]
}
node_version_supported_for_vite() {
local version="$1"
version="${version#v}"
if version_gte "$version" "22.12.0"; then
return 0
fi
if version_gte "$version" "20.19.0" && ! version_gte "$version" "21.0.0"; then
return 0
fi
return 1
}
BACKEND_PYTHON_VERSION=""
if conda info --envs | awk '{print $1}' | grep -qx "$BACKEND_ENV"; then
BACKEND_PYTHON_VERSION="$(conda run -n "$BACKEND_ENV" python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null | tail -n 1 | tr -d '[:space:]')"
if [ "$BACKEND_PYTHON_VERSION" != "$PYTHON_VERSION" ]; then
echo "Backend environment '$BACKEND_ENV' is using Python $BACKEND_PYTHON_VERSION, recreating with Python $PYTHON_VERSION..."
conda remove -y -n "$BACKEND_ENV" --all
else
echo "Backend environment '$BACKEND_ENV' already exists with Python $PYTHON_VERSION."
fi
fi
if ! conda info --envs | awk '{print $1}' | grep -qx "$BACKEND_ENV"; then
echo "Creating Conda environment '$BACKEND_ENV' with Python $PYTHON_VERSION..."
conda create -y -n "$BACKEND_ENV" python=$PYTHON_VERSION -c conda-forge --override-channels
fi
FRONTEND_NODE_VERSION=""
if conda info --envs | awk '{print $1}' | grep -qx "$FRONTEND_ENV"; then
FRONTEND_NODE_VERSION="$(conda run -n "$FRONTEND_ENV" node -p 'process.versions.node' 2>/dev/null | tail -n 1 | tr -d '[:space:]')"
if node_version_supported_for_vite "$FRONTEND_NODE_VERSION"; then
echo "Frontend environment '$FRONTEND_ENV' already exists with Node.js $FRONTEND_NODE_VERSION."
else
echo "Frontend environment '$FRONTEND_ENV' is using Node.js $FRONTEND_NODE_VERSION, recreating with Node.js $NODE_VERSION..."
conda remove -y -n "$FRONTEND_ENV" --all
fi
fi
if ! conda info --envs | awk '{print $1}' | grep -qx "$FRONTEND_ENV"; then
echo "Creating Conda environment '$FRONTEND_ENV' with Node.js $NODE_VERSION..."
conda create -y -n "$FRONTEND_ENV" nodejs=$NODE_VERSION -c conda-forge --override-channels
fi
# Start Backend in a subshell
echo "Starting backend..."
(
eval "$(conda shell.bash hook)"
conda activate "$BACKEND_ENV"
cd "$BACKEND_DIR"
pip install -r requirements.txt
python app/main.py
) &
BACKEND_PID=$!
# Start Frontend in a subshell
echo "Starting frontend..."
(
eval "$(conda shell.bash hook)"
conda activate "$FRONTEND_ENV"
cd "$FRONTEND_DIR"
npm install
npm run dev
) &
FRONTEND_PID=$!
echo "========================================"
echo "iMeeting is starting!"
echo "Backend Env: $BACKEND_ENV (PID: $BACKEND_PID)"
echo "Frontend Env: $FRONTEND_ENV (PID: $FRONTEND_PID)"
echo "Backend URL: http://localhost:8000"
echo "Frontend URL: http://localhost:5173"
echo "Press Ctrl+C to stop both services."
echo "========================================"
# Trap Ctrl+C (SIGINT) and kill both processes
trap "echo -e '\nStopping services...'; kill $BACKEND_PID $FRONTEND_PID 2>/dev/null; exit 0" SIGINT SIGTERM
# Wait for background processes to keep the script running
wait $BACKEND_PID $FRONTEND_PID