网页客户端
commit
5bbe57d56b
|
|
@ -0,0 +1,22 @@
|
||||||
|
# API 后端地址配置示例
|
||||||
|
# 开发环境可以使用代理(vite.config.js中配置),或直接指定后端地址
|
||||||
|
|
||||||
|
# 后端 API 地址(不需要 /api 后缀,会自动添加)
|
||||||
|
VITE_API_BASE_URL=http://localhost:8000/api
|
||||||
|
|
||||||
|
# 录音配置
|
||||||
|
# 分片上传间隔(毫秒),默认10000(10秒)
|
||||||
|
VITE_AUDIO_CHUNK_INTERVAL=10000
|
||||||
|
|
||||||
|
# 其他示例:
|
||||||
|
# 生产环境
|
||||||
|
# VITE_API_BASE_URL=https://api.yourserver.com/api
|
||||||
|
|
||||||
|
# 其他服务器
|
||||||
|
# VITE_API_BASE_URL=http://192.168.1.100:8000/api
|
||||||
|
|
||||||
|
# 更频繁的上传(5秒)
|
||||||
|
# VITE_AUDIO_CHUNK_INTERVAL=5000
|
||||||
|
|
||||||
|
# 更长的上传间隔(30秒)
|
||||||
|
# VITE_AUDIO_CHUNK_INTERVAL=30000
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# 生产环境配置
|
||||||
|
# 后端 API 地址(部署时请修改为实际的后端地址)
|
||||||
|
VITE_API_BASE_URL=http://api.imeeting.unisspace.com/api
|
||||||
|
|
||||||
|
# 录音配置
|
||||||
|
# 分片上传间隔(毫秒),默认10000(10秒)
|
||||||
|
VITE_AUDIO_CHUNK_INTERVAL=10000
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
# iMeeting Client 部署指南
|
||||||
|
|
||||||
|
## 前后端分离部署说明
|
||||||
|
|
||||||
|
iMeeting Client 是一个独立的前端应用,可以部署到任何静态文件服务器,与后端服务器完全分离。
|
||||||
|
|
||||||
|
## 配置后端地址
|
||||||
|
|
||||||
|
### 1. 环境变量配置
|
||||||
|
|
||||||
|
前端通过环境变量 `VITE_API_BASE_URL` 配置后端API地址。
|
||||||
|
|
||||||
|
**开发环境** (`.env`):
|
||||||
|
```bash
|
||||||
|
VITE_API_BASE_URL=http://localhost:8000/api
|
||||||
|
```
|
||||||
|
|
||||||
|
**生产环境** (`.env.production`):
|
||||||
|
```bash
|
||||||
|
VITE_API_BASE_URL=https://your-backend-server.com/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置文件说明
|
||||||
|
|
||||||
|
- `.env` - 本地开发环境配置(不提交到git)
|
||||||
|
- `.env.production` - 生产环境配置(需要修改为实际后端地址)
|
||||||
|
- `.env.example` - 配置示例文件(提交到git供参考)
|
||||||
|
|
||||||
|
## 部署方案
|
||||||
|
|
||||||
|
### 方案一:同一服务器不同端口
|
||||||
|
|
||||||
|
**后端**: `http://your-server.com:8000`
|
||||||
|
**前端**: `http://your-server.com:3002`
|
||||||
|
|
||||||
|
修改 `.env.production`:
|
||||||
|
```bash
|
||||||
|
VITE_API_BASE_URL=http://your-server.com:8000/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案二:不同服务器
|
||||||
|
|
||||||
|
**后端**: `http://backend-server.com:8000`
|
||||||
|
**前端**: `http://frontend-server.com`
|
||||||
|
|
||||||
|
修改 `.env.production`:
|
||||||
|
```bash
|
||||||
|
VITE_API_BASE_URL=http://backend-server.com:8000/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案三:使用域名(推荐)
|
||||||
|
|
||||||
|
**后端**: `https://api.yourdomain.com`
|
||||||
|
**前端**: `https://meeting.yourdomain.com`
|
||||||
|
|
||||||
|
修改 `.env.production`:
|
||||||
|
```bash
|
||||||
|
VITE_API_BASE_URL=https://api.yourdomain.com/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建和部署步骤
|
||||||
|
|
||||||
|
### 1. 修改生产环境配置
|
||||||
|
|
||||||
|
编辑 `.env.production` 文件,设置正确的后端地址:
|
||||||
|
```bash
|
||||||
|
VITE_API_BASE_URL=https://your-actual-backend.com/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# 或
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
# 或
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建完成后,会在 `dist` 目录生成静态文件。
|
||||||
|
|
||||||
|
### 4. 部署静态文件
|
||||||
|
|
||||||
|
将 `dist` 目录部署到任何静态文件服务器:
|
||||||
|
|
||||||
|
#### 使用 Nginx
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-frontend-domain.com;
|
||||||
|
|
||||||
|
root /path/to/imeeting/client/dist;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 如果需要CORS(跨域),添加以下配置
|
||||||
|
# location /api {
|
||||||
|
# proxy_pass http://your-backend-server:8000;
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用 Apache
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName your-frontend-domain.com
|
||||||
|
DocumentRoot /path/to/imeeting/client/dist
|
||||||
|
|
||||||
|
<Directory /path/to/imeeting/client/dist>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
# 支持SPA路由
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteBase /
|
||||||
|
RewriteRule ^index\.html$ - [L]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule . /index.html [L]
|
||||||
|
</Directory>
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用 Node.js (serve)
|
||||||
|
```bash
|
||||||
|
npm install -g serve
|
||||||
|
serve -s dist -p 3002
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用 Docker
|
||||||
|
创建 `Dockerfile`:
|
||||||
|
```dockerfile
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# 复制构建产物
|
||||||
|
COPY dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制nginx配置(支持SPA路由)
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
```
|
||||||
|
|
||||||
|
创建 `nginx.conf`:
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
构建和运行:
|
||||||
|
```bash
|
||||||
|
docker build -t imeeting-client .
|
||||||
|
docker run -d -p 3002:80 imeeting-client
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS 跨域配置
|
||||||
|
|
||||||
|
如果前后端部署在不同域名,需要在**后端**配置CORS:
|
||||||
|
|
||||||
|
**FastAPI 后端** (`main.py`):
|
||||||
|
```python
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=[
|
||||||
|
"http://your-frontend-domain.com",
|
||||||
|
"https://your-frontend-domain.com"
|
||||||
|
],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量优先级
|
||||||
|
|
||||||
|
1. `.env.production` - 生产环境(`npm run build` 时使用)
|
||||||
|
2. `.env` - 本地开发环境(`npm run dev` 时使用)
|
||||||
|
3. 默认值 - `/api`(如果未配置环境变量,则使用相对路径)
|
||||||
|
|
||||||
|
## 验证部署
|
||||||
|
|
||||||
|
1. 访问前端地址,检查页面是否正常加载
|
||||||
|
2. 打开浏览器开发者工具 - Network
|
||||||
|
3. 尝试登录,查看API请求是否指向正确的后端地址
|
||||||
|
4. 检查是否有CORS错误
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 1. API请求404
|
||||||
|
检查 `.env.production` 中的地址是否正确,包括端口号和路径。
|
||||||
|
|
||||||
|
### 2. CORS错误
|
||||||
|
在后端添加CORS中间件,允许前端域名。
|
||||||
|
|
||||||
|
### 3. 刷新页面404
|
||||||
|
配置服务器(Nginx/Apache)支持SPA路由,将所有请求重定向到 `index.html`。
|
||||||
|
|
||||||
|
### 4. 环境变量未生效
|
||||||
|
- 重新构建:删除 `dist` 目录后重新 `npm run build`
|
||||||
|
- 确认环境变量名以 `VITE_` 开头
|
||||||
|
- 重启开发服务器(开发环境)
|
||||||
|
|
||||||
|
## 生产环境检查清单
|
||||||
|
|
||||||
|
- [ ] 修改 `.env.production` 为实际后端地址
|
||||||
|
- [ ] 运行 `npm run build` 构建生产版本
|
||||||
|
- [ ] 后端配置CORS允许前端域名
|
||||||
|
- [ ] 配置Web服务器支持SPA路由
|
||||||
|
- [ ] 测试登录功能
|
||||||
|
- [ ] 测试录音功能
|
||||||
|
- [ ] 测试会议列表
|
||||||
|
- [ ] 检查浏览器控制台无错误
|
||||||
|
|
||||||
|
## 更新部署
|
||||||
|
|
||||||
|
每次更新前端代码后:
|
||||||
|
```bash
|
||||||
|
# 1. 拉取最新代码
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# 2. 重新构建
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 3. 更新静态文件
|
||||||
|
# 将 dist 目录内容复制到Web服务器
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<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>iMeeting - 微客户端</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "imeeting-client",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.20.0",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"qrcode.react": "^3.1.0",
|
||||||
|
"lucide-react": "^0.292.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import Home from './pages/Home/Home';
|
||||||
|
import Record from './pages/Record/Record';
|
||||||
|
import Meetings from './pages/Meetings/Meetings';
|
||||||
|
import { authService } from './services/auth';
|
||||||
|
import './styles/global.css';
|
||||||
|
|
||||||
|
// 私有路由保护
|
||||||
|
function PrivateRoute({ children }) {
|
||||||
|
return authService.isAuthenticated() ? children : <Navigate to="/" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route
|
||||||
|
path="/record"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Record />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/meetings"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Meetings />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
.app-header {
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
color: var(--primary-orange, #FF8C42);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-actions {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu-trigger:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 160px;
|
||||||
|
padding: 8px 0;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: slideDown 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item svg {
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu-trigger {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { ChevronDown, LogOut, MessageSquare } from 'lucide-react';
|
||||||
|
import { authService } from '../services/auth';
|
||||||
|
import './Header.css';
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const user = authService.getLocalUser();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
authService.logout();
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="app-header">
|
||||||
|
<div className="header-content">
|
||||||
|
<div className="logo">
|
||||||
|
<MessageSquare className="logo-icon" />
|
||||||
|
<span className="logo-text">iMeeting</span>
|
||||||
|
</div>
|
||||||
|
<div className="user-actions">
|
||||||
|
<div
|
||||||
|
className="user-menu-trigger"
|
||||||
|
onClick={() => setShowDropdown(!showDropdown)}
|
||||||
|
>
|
||||||
|
<span className="welcome-text">欢迎,{user?.caption || user?.username}</span>
|
||||||
|
<ChevronDown size={20} />
|
||||||
|
</div>
|
||||||
|
{showDropdown && (
|
||||||
|
<>
|
||||||
|
<div className="dropdown-overlay" onClick={() => setShowDropdown(false)} />
|
||||||
|
<div className="user-menu-dropdown">
|
||||||
|
<div className="dropdown-item" onClick={handleLogout}>
|
||||||
|
<LogOut size={16} />
|
||||||
|
<span>退出登录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 24px;
|
||||||
|
right: 24px;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 480px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
z-index: 3000;
|
||||||
|
animation: slideInRight 0.3s ease-out;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:hover {
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 不同类型的样式 */
|
||||||
|
.toast-success {
|
||||||
|
border-left: 4px solid #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-success .toast-icon {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
border-left: 4px solid #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error .toast-icon {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-warning {
|
||||||
|
border-left: 4px solid #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-warning .toast-icon {
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-info {
|
||||||
|
border-left: 4px solid #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-info .toast-icon {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.toast {
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
left: 12px;
|
||||||
|
min-width: auto;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { CheckCircle, XCircle, AlertCircle, Info } from 'lucide-react';
|
||||||
|
import './Toast.css';
|
||||||
|
|
||||||
|
const Toast = ({ message, type = 'info', duration = 3000, onClose }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (duration > 0) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
onClose();
|
||||||
|
}, duration);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [duration, onClose]);
|
||||||
|
|
||||||
|
const getIcon = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'success':
|
||||||
|
return <CheckCircle size={20} />;
|
||||||
|
case 'error':
|
||||||
|
return <XCircle size={20} />;
|
||||||
|
case 'warning':
|
||||||
|
return <AlertCircle size={20} />;
|
||||||
|
case 'info':
|
||||||
|
default:
|
||||||
|
return <Info size={20} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`toast toast-${type}`}>
|
||||||
|
<div className="toast-icon">{getIcon()}</div>
|
||||||
|
<div className="toast-message">{message}</div>
|
||||||
|
<button className="toast-close" onClick={onClose}>×</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toast;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,473 @@
|
||||||
|
.home-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 576px; /* 72%: 800 * 0.72 = 576 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 14px; /* 72%: 20 * 0.72 ≈ 14 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
background: white;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px; /* 72%: 16 * 0.72 ≈ 12 */
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.08),
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.04),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.04);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card.active:hover {
|
||||||
|
transform: translateY(-6px) scale(1.02);
|
||||||
|
box-shadow:
|
||||||
|
0 12px 32px rgba(0, 0, 0, 0.12),
|
||||||
|
0 6px 12px rgba(0, 0, 0, 0.08),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card.active:active {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card.inactive {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card svg {
|
||||||
|
width: 46px; /* 72%: 64 * 0.72 ≈ 46 */
|
||||||
|
height: 46px;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card.active:hover svg {
|
||||||
|
transform: scale(1.1);
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 橙色主题 - 增强渐变和质感 */
|
||||||
|
.feature-card.orange.active {
|
||||||
|
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 16px rgba(255, 107, 53, 0.3),
|
||||||
|
0 2px 8px rgba(255, 107, 53, 0.2),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card.orange.active:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 12px 40px rgba(255, 107, 53, 0.4),
|
||||||
|
0 6px 16px rgba(255, 107, 53, 0.25),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.3),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 灰色主题 - 增强质感 */
|
||||||
|
.feature-card.gray {
|
||||||
|
background: linear-gradient(135deg, #E8E8E8 0%, #D5D5D5 100%);
|
||||||
|
color: #999;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.06),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 蓝色主题 - 用户卡片,增强渐变和质感 */
|
||||||
|
.feature-card.blue.active {
|
||||||
|
color: #667eea;
|
||||||
|
background: linear-gradient(135deg, #f0f4ff 0%, #e0e7ff 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 16px rgba(102, 126, 234, 0.2),
|
||||||
|
0 2px 8px rgba(102, 126, 234, 0.1),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.5),
|
||||||
|
inset 0 -1px 0 rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card.blue.active:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 12px 40px rgba(102, 126, 234, 0.25),
|
||||||
|
0 6px 16px rgba(102, 126, 234, 0.15),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.6),
|
||||||
|
inset 0 -1px 0 rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 19px; /* 72%: 20 * 0.8 * 1.2 ≈ 19 */
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户统计信息 - 增强质感 */
|
||||||
|
.user-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px; /* 72%: 16 * 0.72 ≈ 12 */
|
||||||
|
font-size: 13px; /* 72%: 14 * 0.85 * 1.1 ≈ 13 */
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 6px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats span {
|
||||||
|
padding: 4px 10px; /* 增大尺寸 */
|
||||||
|
background: rgba(102, 126, 234, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card.blue.active:hover .user-stats span {
|
||||||
|
background: rgba(102, 126, 234, 0.18);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 3px 6px rgba(102, 126, 234, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态框覆盖层 */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录模态框 */
|
||||||
|
.login-modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-modal h3 {
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form input {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form input:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form input:disabled {
|
||||||
|
background: #f1f5f9;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover:not(:disabled) {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出确认模态框 */
|
||||||
|
.logout-modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-modal h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-hint {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 - 高度不足时横向排列 */
|
||||||
|
@media (max-height: 600px) {
|
||||||
|
.home-container {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 - 小屏幕 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.home-container {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
font-size: 11px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 - 横屏手机或矮屏幕 */
|
||||||
|
@media (max-width: 768px) and (max-height: 500px) {
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 - 超小屏幕 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.home-page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-container {
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
border-radius: 16px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-modal,
|
||||||
|
.logout-modal {
|
||||||
|
padding: 30px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-modal h3,
|
||||||
|
.logout-modal h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Mic, Brain, FileText, User, LogOut } from 'lucide-react';
|
||||||
|
import { authService } from '../../services/auth';
|
||||||
|
import Toast from '../../components/Toast';
|
||||||
|
import './Home.css';
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [userStats, setUserStats] = useState(null);
|
||||||
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||||
|
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||||
|
const [loginForm, setLoginForm] = useState({ username: '', password: '' });
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
|
const [toast, setToast] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuthStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkAuthStatus = () => {
|
||||||
|
const currentUser = authService.getLocalUser();
|
||||||
|
setUser(currentUser);
|
||||||
|
if (currentUser) {
|
||||||
|
fetchUserStats();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUserStats = async () => {
|
||||||
|
// 从会议统计接口获取统计信息
|
||||||
|
try {
|
||||||
|
const currentUser = authService.getLocalUser();
|
||||||
|
if (!currentUser?.user_id) return;
|
||||||
|
|
||||||
|
const response = await authService.getUserStats(currentUser.user_id);
|
||||||
|
setUserStats(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch user stats:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!loginForm.username || !loginForm.password) {
|
||||||
|
setToast({ message: '请输入用户名和密码', type: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoggingIn(true);
|
||||||
|
try {
|
||||||
|
await authService.login(loginForm.username, loginForm.password);
|
||||||
|
setToast({ message: '登录成功!', type: 'success' });
|
||||||
|
setShowLoginModal(false);
|
||||||
|
setLoginForm({ username: '', password: '' });
|
||||||
|
checkAuthStatus();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login error:', err);
|
||||||
|
setToast({
|
||||||
|
message: err.response?.data?.message || '登录失败,请检查用户名和密码',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
authService.logout();
|
||||||
|
setUser(null);
|
||||||
|
setUserStats(null);
|
||||||
|
setShowLogoutModal(false);
|
||||||
|
setToast({ message: '已退出登录', type: 'success' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFeatureClick = (feature) => {
|
||||||
|
if (!user && feature.requireAuth) {
|
||||||
|
setToast({ message: '请先登录', type: 'error' });
|
||||||
|
setShowLoginModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (feature.onClick) {
|
||||||
|
feature.onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
title: user ? user.caption : '请点击登录',
|
||||||
|
icon: User,
|
||||||
|
active: true,
|
||||||
|
color: 'blue',
|
||||||
|
isUserCard: true,
|
||||||
|
requireAuth: false,
|
||||||
|
onClick: () => {
|
||||||
|
if (user) {
|
||||||
|
setShowLogoutModal(true);
|
||||||
|
} else {
|
||||||
|
setShowLoginModal(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'record',
|
||||||
|
title: '会议录音',
|
||||||
|
icon: Mic,
|
||||||
|
active: true,
|
||||||
|
color: 'orange',
|
||||||
|
requireAuth: true,
|
||||||
|
onClick: () => navigate('/record')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'records',
|
||||||
|
title: '会议记录',
|
||||||
|
icon: FileText,
|
||||||
|
active: true,
|
||||||
|
color: 'orange',
|
||||||
|
requireAuth: true,
|
||||||
|
onClick: () => navigate('/meetings')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai',
|
||||||
|
title: 'AI',
|
||||||
|
icon: Brain,
|
||||||
|
active: false,
|
||||||
|
color: 'gray',
|
||||||
|
requireAuth: false,
|
||||||
|
onClick: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="home-page">
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
duration={3000}
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="home-container">
|
||||||
|
<div className="features-grid">
|
||||||
|
{features.map((feature) => {
|
||||||
|
const Icon = feature.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={feature.id}
|
||||||
|
className={`feature-card ${feature.active ? 'active' : 'inactive'} ${feature.color}`}
|
||||||
|
onClick={() => handleFeatureClick(feature)}
|
||||||
|
disabled={!feature.active}
|
||||||
|
>
|
||||||
|
<Icon size={46} strokeWidth={1.5} />
|
||||||
|
<span className="feature-title">{feature.title}</span>
|
||||||
|
{feature.isUserCard && user && userStats && (
|
||||||
|
<div className="user-stats">
|
||||||
|
<span>创建: {userStats.created_meetings || 0}</span>
|
||||||
|
<span>参加: {userStats.attended_meetings || 0}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 登录模态框 */}
|
||||||
|
{showLoginModal && (
|
||||||
|
<div className="modal-overlay" onClick={() => setShowLoginModal(false)}>
|
||||||
|
<div className="login-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h3>登录 iMeeting</h3>
|
||||||
|
<form onSubmit={handleLogin} className="login-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="用户名"
|
||||||
|
value={loginForm.username}
|
||||||
|
onChange={(e) => setLoginForm({ ...loginForm, username: e.target.value })}
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="密码"
|
||||||
|
value={loginForm.password}
|
||||||
|
onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })}
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
/>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="cancel-btn"
|
||||||
|
onClick={() => setShowLoginModal(false)}
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="submit-btn"
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
>
|
||||||
|
{isLoggingIn ? '登录中...' : '登录'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 退出确认模态框 */}
|
||||||
|
{showLogoutModal && (
|
||||||
|
<div className="modal-overlay" onClick={() => setShowLogoutModal(false)}>
|
||||||
|
<div className="logout-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="logout-icon">
|
||||||
|
<LogOut size={48} />
|
||||||
|
</div>
|
||||||
|
<h3>确认退出登录?</h3>
|
||||||
|
<p className="logout-hint">退出后需要重新登录才能使用录音和记录功能</p>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button
|
||||||
|
className="cancel-btn"
|
||||||
|
onClick={() => setShowLogoutModal(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="logout-btn"
|
||||||
|
onClick={handleLogout}
|
||||||
|
>
|
||||||
|
退出登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px 30px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-orange);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-gray);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 2px solid #E0E0E0;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:disabled {
|
||||||
|
background: #F5F5F5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-me {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: -8px;
|
||||||
|
margin-bottom: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-me label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-me input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
accent-color: var(--primary-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #E53E3E;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #FFF5F5;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #FEB2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
height: 48px;
|
||||||
|
background: linear-gradient(135deg, var(--primary-orange) 0%, var(--primary-orange-dark) 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(255, 140, 66, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-card {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { authService } from '../../services/auth';
|
||||||
|
import Toast from '../../components/Toast';
|
||||||
|
import './Login.css';
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
const [toast, setToast] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 组件挂载时,从localStorage读取保存的用户名
|
||||||
|
useEffect(() => {
|
||||||
|
const savedUsername = localStorage.getItem('rememberedUsername');
|
||||||
|
const isRemembered = localStorage.getItem('rememberMe') === 'true';
|
||||||
|
|
||||||
|
if (savedUsername && isRemembered) {
|
||||||
|
setUsername(savedUsername);
|
||||||
|
setRememberMe(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
const errorMsg = '请输入用户名和密码';
|
||||||
|
setToast({ message: errorMsg, type: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authService.login(username, password);
|
||||||
|
|
||||||
|
// 处理记住用户名
|
||||||
|
if (rememberMe) {
|
||||||
|
localStorage.setItem('rememberedUsername', username);
|
||||||
|
localStorage.setItem('rememberMe', 'true');
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('rememberedUsername');
|
||||||
|
localStorage.removeItem('rememberMe');
|
||||||
|
}
|
||||||
|
|
||||||
|
setToast({ message: '登录成功!', type: 'success' });
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/');
|
||||||
|
}, 500);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login error:', err);
|
||||||
|
const errorMsg = err.response?.data?.message || '登录失败,请检查用户名和密码';
|
||||||
|
setToast({ message: errorMsg, type: 'error' });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-page">
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
duration={3000}
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="login-container">
|
||||||
|
<div className="login-card">
|
||||||
|
<h1 className="login-title">iMeeting</h1>
|
||||||
|
<p className="login-subtitle">微客户端</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="login-form">
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="用户名"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="密码"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="remember-me">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={rememberMe}
|
||||||
|
onChange={(e) => setRememberMe(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>记住用户名</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="login-button" disabled={loading}>
|
||||||
|
{loading ? '登录中...' : '登录'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
|
|
@ -0,0 +1,628 @@
|
||||||
|
.meetings-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面头部 - 返回按钮 + 标题 */
|
||||||
|
.page-header {
|
||||||
|
padding: 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateX(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:active {
|
||||||
|
transform: translateX(-1px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meetings-container {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meetings-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meetings-content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-item {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border-left: 4px solid;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自己创建的 - 橙色边框 */
|
||||||
|
.meeting-item.created-by-me {
|
||||||
|
border-left-color: #FF8C42;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 别人创建的 - 绿色边框 */
|
||||||
|
.meeting-item.attended-by-me {
|
||||||
|
border-left-color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-main {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 会议操作按钮组 */
|
||||||
|
.meeting-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-btn,
|
||||||
|
.delete-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-btn {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-btn:hover {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 竖直分页器 */
|
||||||
|
.vertical-pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
position: sticky;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-arrow-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-arrow-btn:hover:not(:disabled) {
|
||||||
|
transform: scale(1.15);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-arrow-btn:active:not(:disabled) {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-arrow-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #94a3b8;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-page-info {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
padding: 4px 0;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QR码模态框 */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
text-align: center;
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-modal h3 {
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-container img {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-url {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
word-break: break-all;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-modal .close-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-modal .close-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除确认模态框 */
|
||||||
|
.delete-modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 450px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
text-align: center;
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-warning {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #475569;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-warning strong {
|
||||||
|
color: #ef4444;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-hint {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover:not(:disabled) {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-delete-btn {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-delete-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.meetings-page-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meetings-page-header .logo-text {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meetings-page-header h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meetings-container {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meetings-content-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-pagination {
|
||||||
|
flex-direction: row;
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-arrow-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-item {
|
||||||
|
padding: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-actions {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-meta {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行内音频播放器 */
|
||||||
|
.meeting-item.playing {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-item.playing .meeting-main {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player .play-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player .play-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player .play-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-current,
|
||||||
|
.time-duration {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar:hover {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
box-shadow: 0 0 8px rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-player-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-player-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-player-btn:active {
|
||||||
|
transform: rotate(90deg) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 - 播放器 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.audio-player {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player .play-btn {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-current,
|
||||||
|
.time-duration {
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-player-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,410 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { ArrowLeft, Clock, User, QrCode, ChevronUp, ChevronDown, Trash2, Play, Pause, X } from 'lucide-react';
|
||||||
|
import { meetingService } from '../../services/meeting';
|
||||||
|
import { authService } from '../../services/auth';
|
||||||
|
import Toast from '../../components/Toast';
|
||||||
|
import './Meetings.css';
|
||||||
|
|
||||||
|
function Meetings() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [meetings, setMeetings] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
total: 0,
|
||||||
|
total_pages: 0
|
||||||
|
});
|
||||||
|
const [toast, setToast] = useState(null);
|
||||||
|
const [showQRModal, setShowQRModal] = useState(false);
|
||||||
|
const [selectedMeetingUrl, setSelectedMeetingUrl] = useState('');
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [deletingMeeting, setDeletingMeeting] = useState(null);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const currentUser = authService.getLocalUser();
|
||||||
|
|
||||||
|
// 音频播放相关状态
|
||||||
|
const [playingMeetingId, setPlayingMeetingId] = useState(null);
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [duration, setDuration] = useState(0);
|
||||||
|
const audioRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMeetings(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchMeetings = async (page) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await meetingService.getMeetings({
|
||||||
|
page,
|
||||||
|
page_size: pagination.page_size
|
||||||
|
});
|
||||||
|
|
||||||
|
// API响应格式: { code, message, data: { meetings, page, page_size, total, total_pages } }
|
||||||
|
const data = response.data;
|
||||||
|
setMeetings(data.meetings || []);
|
||||||
|
setPagination({
|
||||||
|
page: data.page,
|
||||||
|
page_size: data.page_size,
|
||||||
|
total: data.total,
|
||||||
|
total_pages: data.total_pages
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch meetings:', error);
|
||||||
|
setToast({ message: '加载会议列表失败', type: 'error' });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (newPage) => {
|
||||||
|
if (newPage >= 1 && newPage <= pagination.total_pages) {
|
||||||
|
fetchMeetings(newPage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowQR = (meetingId) => {
|
||||||
|
const url = `${window.location.origin}/meetings/preview/${meetingId}`;
|
||||||
|
setSelectedMeetingUrl(url);
|
||||||
|
setShowQRModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (meeting, e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeletingMeeting(meeting);
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = async () => {
|
||||||
|
if (!deletingMeeting) return;
|
||||||
|
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await meetingService.deleteMeeting(deletingMeeting.meeting_id);
|
||||||
|
setToast({ message: '会议删除成功', type: 'success' });
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setDeletingMeeting(null);
|
||||||
|
|
||||||
|
// 重新加载当前页
|
||||||
|
// 如果当前页只有一条记录且不是第一页,则返回上一页
|
||||||
|
const isLastItemOnPage = meetings.length === 1 && pagination.page > 1;
|
||||||
|
const targetPage = isLastItemOnPage ? pagination.page - 1 : pagination.page;
|
||||||
|
fetchMeetings(targetPage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete meeting:', error);
|
||||||
|
setToast({
|
||||||
|
message: error.response?.data?.message || '删除会议失败',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelDelete = () => {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setDeletingMeeting(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 音频播放相关函数
|
||||||
|
const handlePlayAudio = (meetingId, e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// 如果点击的是同一个会议,且正在播放,则暂停
|
||||||
|
if (playingMeetingId === meetingId && isPlaying) {
|
||||||
|
audioRef.current?.pause();
|
||||||
|
setIsPlaying(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果点击的是不同的会议,或者同一个会议但暂停状态
|
||||||
|
if (playingMeetingId !== meetingId) {
|
||||||
|
// 停止之前的播放
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
audioRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的音频元素
|
||||||
|
const audio = new Audio(`/api/meetings/${meetingId}/audio/stream`);
|
||||||
|
audioRef.current = audio;
|
||||||
|
|
||||||
|
// 监听音频事件
|
||||||
|
audio.addEventListener('loadedmetadata', () => {
|
||||||
|
setDuration(audio.duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('timeupdate', () => {
|
||||||
|
setCurrentTime(audio.currentTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('ended', () => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
setCurrentTime(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('error', (e) => {
|
||||||
|
console.error('Audio error:', e);
|
||||||
|
setToast({ message: '音频加载失败', type: 'error' });
|
||||||
|
setPlayingMeetingId(null);
|
||||||
|
setIsPlaying(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
setPlayingMeetingId(meetingId);
|
||||||
|
setCurrentTime(0);
|
||||||
|
audio.play();
|
||||||
|
setIsPlaying(true);
|
||||||
|
} else {
|
||||||
|
// 同一个会议,从暂停恢复播放
|
||||||
|
audioRef.current?.play();
|
||||||
|
setIsPlaying(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlayPause = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isPlaying) {
|
||||||
|
audioRef.current?.pause();
|
||||||
|
setIsPlaying(false);
|
||||||
|
} else {
|
||||||
|
audioRef.current?.play();
|
||||||
|
setIsPlaying(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSeek = (e) => {
|
||||||
|
const progressBar = e.currentTarget;
|
||||||
|
const rect = progressBar.getBoundingClientRect();
|
||||||
|
const percent = (e.clientX - rect.left) / rect.width;
|
||||||
|
const newTime = percent * duration;
|
||||||
|
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.currentTime = newTime;
|
||||||
|
setCurrentTime(newTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClosePlayer = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
audioRef.current = null;
|
||||||
|
}
|
||||||
|
setPlayingMeetingId(null);
|
||||||
|
setIsPlaying(false);
|
||||||
|
setCurrentTime(0);
|
||||||
|
setDuration(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds) => {
|
||||||
|
if (isNaN(seconds)) return '0:00';
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="meetings-page">
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
duration={3000}
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="page-header">
|
||||||
|
<button className="back-button" onClick={() => navigate('/')} title="返回首页">
|
||||||
|
<ArrowLeft size={24} />
|
||||||
|
</button>
|
||||||
|
<h1 className="page-title">会议记录</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="meetings-container">
|
||||||
|
{loading ? (
|
||||||
|
<div className="loading-state">
|
||||||
|
<p>加载中...</p>
|
||||||
|
</div>
|
||||||
|
) : meetings.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<p>暂无会议记录</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="meetings-content-wrapper">
|
||||||
|
<div className="meetings-list">
|
||||||
|
{meetings.map((meeting) => {
|
||||||
|
const isCreator = String(meeting.creator_id) === String(currentUser?.user_id);
|
||||||
|
const cardClass = isCreator ? 'created-by-me' : 'attended-by-me';
|
||||||
|
const isPlaying = playingMeetingId === meeting.meeting_id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={meeting.meeting_id} className={`meeting-item ${cardClass} ${isPlaying ? 'playing' : ''}`}>
|
||||||
|
<div
|
||||||
|
className="meeting-main"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!isPlaying) {
|
||||||
|
handlePlayAudio(meeting.meeting_id, e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
/* 播放器模式 */
|
||||||
|
<div className="audio-player">
|
||||||
|
<button className="play-btn" onClick={handlePlayPause} title={isPlaying ? '暂停' : '播放'}>
|
||||||
|
{isPlaying ? <Pause size={24} /> : <Play size={24} />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="player-info">
|
||||||
|
<div className="player-title">{meeting.title}</div>
|
||||||
|
<div className="progress-container">
|
||||||
|
<span className="time-current">{formatTime(currentTime)}</span>
|
||||||
|
<div className="progress-bar" onClick={handleSeek}>
|
||||||
|
<div
|
||||||
|
className="progress-fill"
|
||||||
|
style={{ width: `${duration ? (currentTime / duration) * 100 : 0}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span className="time-duration">{formatTime(duration)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="close-player-btn" onClick={handleClosePlayer} title="关闭播放器">
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 正常信息显示 */
|
||||||
|
<div className="meeting-info">
|
||||||
|
<h3 className="meeting-title">{meeting.title}</h3>
|
||||||
|
<div className="meeting-meta">
|
||||||
|
<div className="meta-item">
|
||||||
|
<Clock size={14} />
|
||||||
|
<span>{formatDateTime(meeting.meeting_time)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="meta-item">
|
||||||
|
<User size={14} />
|
||||||
|
<span>创建人: {meeting.creator_username}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isPlaying && (
|
||||||
|
<div className="meeting-actions">
|
||||||
|
<button
|
||||||
|
className="qr-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleShowQR(meeting.meeting_id);
|
||||||
|
}}
|
||||||
|
title="查看二维码"
|
||||||
|
>
|
||||||
|
<QrCode size={18} />
|
||||||
|
</button>
|
||||||
|
{isCreator && (
|
||||||
|
<button
|
||||||
|
className="delete-btn"
|
||||||
|
onClick={(e) => handleDeleteClick(meeting, e)}
|
||||||
|
title="删除会议"
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 竖直分页器 */}
|
||||||
|
{pagination.total_pages > 1 && (
|
||||||
|
<div className="vertical-pagination">
|
||||||
|
<button
|
||||||
|
className="pagination-arrow-btn"
|
||||||
|
onClick={() => handlePageChange(pagination.page - 1)}
|
||||||
|
disabled={pagination.page === 1}
|
||||||
|
title="上一页"
|
||||||
|
>
|
||||||
|
<ChevronUp size={20} />
|
||||||
|
</button>
|
||||||
|
<div className="pagination-page-info">
|
||||||
|
{pagination.page}/{pagination.total_pages}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="pagination-arrow-btn"
|
||||||
|
onClick={() => handlePageChange(pagination.page + 1)}
|
||||||
|
disabled={pagination.page === pagination.total_pages}
|
||||||
|
title="下一页"
|
||||||
|
>
|
||||||
|
<ChevronDown size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* QR码模态框 */}
|
||||||
|
{showQRModal && (
|
||||||
|
<div className="modal-overlay" onClick={() => setShowQRModal(false)}>
|
||||||
|
<div className="qr-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h3>扫码查看会议</h3>
|
||||||
|
<div className="qr-code-container">
|
||||||
|
<img
|
||||||
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(selectedMeetingUrl)}`}
|
||||||
|
alt="QR Code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="qr-url">{selectedMeetingUrl}</p>
|
||||||
|
<button className="close-btn" onClick={() => setShowQRModal(false)}>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 删除确认模态框 */}
|
||||||
|
{showDeleteModal && (
|
||||||
|
<div className="modal-overlay" onClick={handleCancelDelete}>
|
||||||
|
<div className="delete-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="delete-icon">
|
||||||
|
<Trash2 size={48} />
|
||||||
|
</div>
|
||||||
|
<h3>确认删除会议?</h3>
|
||||||
|
<p className="delete-warning">
|
||||||
|
确定要删除会议 "<strong>{deletingMeeting?.title}</strong>" 吗?
|
||||||
|
</p>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button className="cancel-btn" onClick={handleCancelDelete} disabled={isDeleting}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button className="confirm-delete-btn" onClick={handleConfirmDelete} disabled={isDeleting}>
|
||||||
|
{isDeleting ? '删除中...' : '确认删除'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Meetings;
|
||||||
|
|
@ -0,0 +1,507 @@
|
||||||
|
.record-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面头部 - 返回按钮 + 标题 */
|
||||||
|
.page-header {
|
||||||
|
padding: 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateX(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:active {
|
||||||
|
transform: translateX(-1px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主容器 */
|
||||||
|
.record-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 40px 40px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部提示文字区域 */
|
||||||
|
.status-text-area {
|
||||||
|
text-align: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.success {
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.recording-pulse {
|
||||||
|
animation: colorPulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes colorPulse {
|
||||||
|
0%, 100% {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
color: #E53E3E;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-hint {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中心按钮区域 */
|
||||||
|
.center-button-area {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 音频源选择按钮组 */
|
||||||
|
.audio-source-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button {
|
||||||
|
width: 160px;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button.microphone {
|
||||||
|
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button.microphone:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 40px rgba(255, 107, 53, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button.system {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button.system:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 40px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button:active {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button span {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中心麦克风按钮 */
|
||||||
|
.center-mic-button {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-mic-button:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 15px 50px rgba(255, 107, 53, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-mic-button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 录音状态 - 红色停止按钮 */
|
||||||
|
.center-mic-button.recording {
|
||||||
|
background: linear-gradient(135deg, #E53E3E 0%, #C53030 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-mic-button.recording:hover {
|
||||||
|
box-shadow: 0 15px 50px rgba(197, 48, 48, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 - 灰色 */
|
||||||
|
.center-mic-button.loading {
|
||||||
|
background: linear-gradient(135deg, #E0E0E0 0%, #BDBDBD 100%);
|
||||||
|
color: #757575;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-mic-button.loading:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 完成状态 - 绿色 */
|
||||||
|
.center-mic-button.completed {
|
||||||
|
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
|
||||||
|
animation: successPop 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes successPop {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 录音容器 - 包含波纹 */
|
||||||
|
.recording-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 240px;
|
||||||
|
height: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 波纹动画 */
|
||||||
|
.ripple-wave {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #FF8C42;
|
||||||
|
opacity: 0;
|
||||||
|
animation: ripple 2s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple-wave.wave-1 {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple-wave.wave-2 {
|
||||||
|
animation-delay: 0.7s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple-wave.wave-3 {
|
||||||
|
animation-delay: 1.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
0% {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 旋转动画 */
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时长显示 */
|
||||||
|
.duration-display {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 56px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用淡入动画 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.record-page-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-page-header .logo-text {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-page-header h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-card {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text-area {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-button-area {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-source-buttons {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button {
|
||||||
|
width: 130px;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button svg {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button span {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-mic-button {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-mic-button svg {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recording-container {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple-wave {
|
||||||
|
animation: ripple-mobile 2s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple-mobile {
|
||||||
|
0% {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 320px;
|
||||||
|
height: 320px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-display {
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.record-page-header .header-content {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-page-header .logo-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-page-header .logo-text {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-page-header h1 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-card {
|
||||||
|
padding: 25px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text-area {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-hint {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-button-area {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-source-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button {
|
||||||
|
width: 200px;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
border-radius: 15px;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button span {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-mic-button {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-mic-button svg {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recording-container {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple-small {
|
||||||
|
0% {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple-wave {
|
||||||
|
animation: ripple-small 2s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-display {
|
||||||
|
font-size: 40px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,376 @@
|
||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Mic, Square, Loader, ArrowLeft, Monitor } from 'lucide-react';
|
||||||
|
import { meetingService } from '../../services/meeting';
|
||||||
|
import { audioService } from '../../services/audio';
|
||||||
|
import { authService } from '../../services/auth';
|
||||||
|
import Toast from '../../components/Toast';
|
||||||
|
import './Record.css';
|
||||||
|
|
||||||
|
function Record() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
|
const [duration, setDuration] = useState(0);
|
||||||
|
const [status, setStatus] = useState('idle'); // idle, requesting-permission, recording, uploading, completed
|
||||||
|
const [audioSource, setAudioSource] = useState(null); // 'microphone' or 'system'
|
||||||
|
const [toast, setToast] = useState(null);
|
||||||
|
|
||||||
|
const mediaRecorderRef = useRef(null);
|
||||||
|
const streamRef = useRef(null);
|
||||||
|
const chunksRef = useRef([]);
|
||||||
|
const durationIntervalRef = useRef(null);
|
||||||
|
const sessionIdRef = useRef(null);
|
||||||
|
const meetingIdRef = useRef(null);
|
||||||
|
const chunkIndexRef = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// 清理资源
|
||||||
|
if (streamRef.current) {
|
||||||
|
streamRef.current.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
if (durationIntervalRef.current) {
|
||||||
|
clearInterval(durationIntervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getSupportedMimeType = () => {
|
||||||
|
const types = [
|
||||||
|
'audio/mp4', // 优先使用 mp4 格式
|
||||||
|
'audio/webm;codecs=opus',
|
||||||
|
'audio/webm',
|
||||||
|
'audio/ogg;codecs=opus'
|
||||||
|
];
|
||||||
|
for (const type of types) {
|
||||||
|
if (MediaRecorder.isTypeSupported(type)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDuration = (seconds) => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartRecording = async (source = 'microphone') => {
|
||||||
|
try {
|
||||||
|
setStatus('requesting-permission');
|
||||||
|
setAudioSource(source);
|
||||||
|
|
||||||
|
let stream;
|
||||||
|
|
||||||
|
// 1. 根据音频源类型请求权限
|
||||||
|
if (source === 'system') {
|
||||||
|
// 录制系统音频(需要屏幕共享)
|
||||||
|
try {
|
||||||
|
// getDisplayMedia 必须包含 video,即使我们只需要音频
|
||||||
|
const displayStream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true, // 必须为 true,否则会报错
|
||||||
|
audio: {
|
||||||
|
echoCancellation: false, // 系统音频不需要回声消除
|
||||||
|
noiseSuppression: false,
|
||||||
|
autoGainControl: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否成功获取音频轨道
|
||||||
|
const audioTracks = displayStream.getAudioTracks();
|
||||||
|
if (audioTracks.length === 0) {
|
||||||
|
// 用户没有勾选"共享音频"
|
||||||
|
displayStream.getTracks().forEach(track => track.stop());
|
||||||
|
setToast({
|
||||||
|
message: '请在屏幕共享时勾选"共享音频"选项',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
setStatus('idle');
|
||||||
|
setAudioSource(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止视频轨道,只保留音频
|
||||||
|
displayStream.getVideoTracks().forEach(track => track.stop());
|
||||||
|
|
||||||
|
// 创建只包含音频的新 stream
|
||||||
|
stream = new MediaStream(audioTracks);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
// 如果用户取消屏幕共享
|
||||||
|
if (err.name === 'NotAllowedError' || err.name === 'AbortError') {
|
||||||
|
setToast({
|
||||||
|
message: '已取消屏幕共享',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
setStatus('idle');
|
||||||
|
setAudioSource(null);
|
||||||
|
return;
|
||||||
|
} else if (err.name === 'NotSupportedError') {
|
||||||
|
setToast({
|
||||||
|
message: '您的浏览器不支持系统音频录制,请使用 Chrome 或 Edge 浏览器',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
setStatus('idle');
|
||||||
|
setAudioSource(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 录制麦克风音频
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: {
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true,
|
||||||
|
sampleRate: 44100
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
streamRef.current = stream;
|
||||||
|
setStatus('recording');
|
||||||
|
|
||||||
|
// 2. 创建会议
|
||||||
|
const user = authService.getLocalUser();
|
||||||
|
const meetingResponse = await meetingService.createMeeting({
|
||||||
|
title: `录音会议-${new Date().toLocaleString('zh-CN')}`,
|
||||||
|
meeting_time: new Date().toISOString(),
|
||||||
|
user_id: user.user_id,
|
||||||
|
attendee_ids: [user.user_id],
|
||||||
|
tags: '客户端快速会议'
|
||||||
|
});
|
||||||
|
|
||||||
|
meetingIdRef.current = meetingResponse.data.meeting_id;
|
||||||
|
console.log('Meeting created:', meetingIdRef.current);
|
||||||
|
|
||||||
|
// 3. 创建MediaRecorder
|
||||||
|
const mimeType = getSupportedMimeType();
|
||||||
|
if (!mimeType) {
|
||||||
|
throw new Error('浏览器不支持录音功能');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Using MIME type:', mimeType);
|
||||||
|
|
||||||
|
// 4. 初始化上传会话
|
||||||
|
const sessionResponse = await audioService.initUploadSession(
|
||||||
|
meetingIdRef.current,
|
||||||
|
mimeType
|
||||||
|
);
|
||||||
|
|
||||||
|
sessionIdRef.current = sessionResponse.data.session_id;
|
||||||
|
console.log('Upload session initialized:', sessionIdRef.current);
|
||||||
|
|
||||||
|
// 5. 创建并启动MediaRecorder
|
||||||
|
const mediaRecorder = new MediaRecorder(stream, {
|
||||||
|
mimeType,
|
||||||
|
audioBitsPerSecond: 128000
|
||||||
|
});
|
||||||
|
|
||||||
|
mediaRecorder.ondataavailable = async (event) => {
|
||||||
|
if (event.data.size > 0) {
|
||||||
|
chunksRef.current.push(event.data);
|
||||||
|
|
||||||
|
// 实时上传分片
|
||||||
|
try {
|
||||||
|
await audioService.uploadChunk(
|
||||||
|
sessionIdRef.current,
|
||||||
|
chunkIndexRef.current,
|
||||||
|
event.data
|
||||||
|
);
|
||||||
|
console.log(`Chunk ${chunkIndexRef.current} uploaded`);
|
||||||
|
chunkIndexRef.current++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error uploading chunk:', err);
|
||||||
|
// 继续录音,不中断
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.start(
|
||||||
|
Number(import.meta.env.VITE_AUDIO_CHUNK_INTERVAL) || 10000
|
||||||
|
); // 从配置文件读取分片上传间隔,默认10秒
|
||||||
|
mediaRecorderRef.current = mediaRecorder;
|
||||||
|
setIsRecording(true);
|
||||||
|
|
||||||
|
// 6. 启动计时器
|
||||||
|
durationIntervalRef.current = setInterval(() => {
|
||||||
|
setDuration(prev => prev + 1);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Start recording error:', err);
|
||||||
|
let errorMessage = '启动录音失败';
|
||||||
|
let showPermissionHelp = false;
|
||||||
|
|
||||||
|
if (err.name === 'NotAllowedError') {
|
||||||
|
errorMessage = '麦克风权限被拒绝,请在浏览器设置中允许使用麦克风';
|
||||||
|
showPermissionHelp = true;
|
||||||
|
} else if (err.name === 'NotFoundError') {
|
||||||
|
errorMessage = '未检测到麦克风设备';
|
||||||
|
} else if (err.name === 'NotReadableError') {
|
||||||
|
errorMessage = '麦克风被其他应用占用';
|
||||||
|
} else if (err.message) {
|
||||||
|
errorMessage = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
setToast({ message: errorMessage, type: 'error' });
|
||||||
|
setStatus('idle');
|
||||||
|
setAudioSource(null);
|
||||||
|
|
||||||
|
// 清理资源
|
||||||
|
if (streamRef.current) {
|
||||||
|
streamRef.current.getTracks().forEach(track => track.stop());
|
||||||
|
streamRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStopRecording = async () => {
|
||||||
|
if (!mediaRecorderRef.current || !isRecording) return;
|
||||||
|
|
||||||
|
setIsRecording(false);
|
||||||
|
setStatus('uploading');
|
||||||
|
|
||||||
|
// 停止计时
|
||||||
|
if (durationIntervalRef.current) {
|
||||||
|
clearInterval(durationIntervalRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止录音
|
||||||
|
mediaRecorderRef.current.stop();
|
||||||
|
|
||||||
|
// 停止麦克风
|
||||||
|
if (streamRef.current) {
|
||||||
|
streamRef.current.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待最后的数据
|
||||||
|
mediaRecorderRef.current.onstop = async () => {
|
||||||
|
try {
|
||||||
|
// 完成上传
|
||||||
|
const mimeType = getSupportedMimeType();
|
||||||
|
await audioService.completeUpload(
|
||||||
|
sessionIdRef.current,
|
||||||
|
meetingIdRef.current,
|
||||||
|
chunkIndexRef.current,
|
||||||
|
mimeType
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Upload completed successfully');
|
||||||
|
setStatus('completed');
|
||||||
|
setToast({ message: '录音完成!', type: 'success' });
|
||||||
|
|
||||||
|
// 跳转到会议详情页
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/meetings');
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Complete upload error:', err);
|
||||||
|
setToast({
|
||||||
|
message: '上传完成失败: ' + (err.response?.data?.message || err.message),
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
setStatus('idle');
|
||||||
|
setAudioSource(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="record-page">
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
duration={3000}
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="page-header">
|
||||||
|
<button className="back-button" onClick={() => navigate('/')} title="返回首页">
|
||||||
|
<ArrowLeft size={24} />
|
||||||
|
</button>
|
||||||
|
<h1 className="page-title">会议录音</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="record-container">
|
||||||
|
<div className="record-card">
|
||||||
|
{/* 提示文字 */}
|
||||||
|
<div className="status-text-area">
|
||||||
|
{status === 'idle' && <p className="status-text">选择录音方式</p>}
|
||||||
|
{status === 'requesting-permission' && (
|
||||||
|
<>
|
||||||
|
<p className="status-text">正在请求{audioSource === 'system' ? '屏幕共享' : '麦克风'}权限</p>
|
||||||
|
<p className="status-hint">请在浏览器弹窗中允许{audioSource === 'system' ? '共享屏幕音频' : '使用麦克风'}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{status === 'recording' && <p className="status-text recording-pulse">正在录音中...</p>}
|
||||||
|
{status === 'uploading' && <p className="status-text">正在上传处理...</p>}
|
||||||
|
{status === 'completed' && <p className="status-text success">录音完成!</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 中心按钮区域 */}
|
||||||
|
<div className="center-button-area">
|
||||||
|
{/* 空闲状态 - 两个音频源选择按钮 */}
|
||||||
|
{status === 'idle' && (
|
||||||
|
<div className="audio-source-buttons">
|
||||||
|
<button onClick={() => handleStartRecording('microphone')} className="source-button microphone">
|
||||||
|
<Mic size={60} strokeWidth={1.5} />
|
||||||
|
<span>麦克风</span>
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handleStartRecording('system')} className="source-button system">
|
||||||
|
<Monitor size={60} strokeWidth={1.5} />
|
||||||
|
<span>系统音频</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 请求权限 - 加载动画 */}
|
||||||
|
{status === 'requesting-permission' && (
|
||||||
|
<div className="center-mic-button loading">
|
||||||
|
<Loader size={60} className="spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 录音中 - 停止按钮 + 波纹 */}
|
||||||
|
{status === 'recording' && (
|
||||||
|
<div className="recording-container">
|
||||||
|
<div className="ripple-wave wave-1"></div>
|
||||||
|
<div className="ripple-wave wave-2"></div>
|
||||||
|
<div className="ripple-wave wave-3"></div>
|
||||||
|
<button onClick={handleStopRecording} className="center-mic-button recording">
|
||||||
|
<Square size={60} fill="currentColor" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 上传中 - 加载动画 */}
|
||||||
|
{status === 'uploading' && (
|
||||||
|
<div className="center-mic-button loading">
|
||||||
|
<Loader size={60} className="spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 完成 - 成功图标 */}
|
||||||
|
{status === 'completed' && (
|
||||||
|
<div className="center-mic-button completed">
|
||||||
|
<Mic size={60} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 时长显示 - 始终占位,保持高度一致 */}
|
||||||
|
<div className="duration-display">
|
||||||
|
{(status === 'recording' || status === 'uploading') ? formatDuration(duration) : '\u00A0'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Record;
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 请求拦截器:添加token
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应拦截器:处理统一格式
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// 后端返回格式: { code, message, data }
|
||||||
|
// 统一提取 data 字段
|
||||||
|
if (response.data && response.data.data !== undefined) {
|
||||||
|
// 如果是标准格式,检查 code
|
||||||
|
const code = response.data.code;
|
||||||
|
if (code !== "200") {
|
||||||
|
// 如果 code 不是 200,抛出错误
|
||||||
|
return Promise.reject({
|
||||||
|
response: {
|
||||||
|
data: {
|
||||||
|
message: response.data.message || '请求失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 返回完整的 response.data,保留 code, message, data
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
// 如果不是标准格式,直接返回
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default api;
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import api from './api';
|
||||||
|
|
||||||
|
export const audioService = {
|
||||||
|
// 初始化上传会话
|
||||||
|
initUploadSession: async (meetingId, mimeType) => {
|
||||||
|
return await api.post('/audio/stream/init', {
|
||||||
|
meeting_id: meetingId,
|
||||||
|
mime_type: mimeType
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 上传音频分片
|
||||||
|
uploadChunk: async (sessionId, chunkIndex, chunk) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('session_id', sessionId);
|
||||||
|
formData.append('chunk_index', chunkIndex);
|
||||||
|
formData.append('chunk', chunk);
|
||||||
|
|
||||||
|
return await api.post('/audio/stream/chunk', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 完成上传
|
||||||
|
completeUpload: async (sessionId, meetingId, totalChunks, mimeType) => {
|
||||||
|
return await api.post('/audio/stream/complete', {
|
||||||
|
session_id: sessionId,
|
||||||
|
meeting_id: meetingId,
|
||||||
|
total_chunks: totalChunks,
|
||||||
|
mime_type: mimeType,
|
||||||
|
auto_transcribe: true,
|
||||||
|
auto_summarize: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 取消上传
|
||||||
|
cancelUpload: async (sessionId) => {
|
||||||
|
return await api.delete('/audio/stream/cancel', {
|
||||||
|
data: { session_id: sessionId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import api from './api';
|
||||||
|
|
||||||
|
export const authService = {
|
||||||
|
// 登录
|
||||||
|
login: async (username, password) => {
|
||||||
|
const response = await api.post('/auth/login', { username, password });
|
||||||
|
// 响应格式: { code, message, data: { user_id, username, caption, email, token, role_id } }
|
||||||
|
if (response.data?.token) {
|
||||||
|
localStorage.setItem('token', response.data.token);
|
||||||
|
// 保存用户信息,不包括 token
|
||||||
|
const { token, ...userInfo } = response.data;
|
||||||
|
localStorage.setItem('user', JSON.stringify(userInfo));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取当前用户
|
||||||
|
getCurrentUser: async () => {
|
||||||
|
return await api.get('/auth/me');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查是否已登录
|
||||||
|
isAuthenticated: () => {
|
||||||
|
return !!localStorage.getItem('token');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取本地用户信息
|
||||||
|
getLocalUser: () => {
|
||||||
|
const userStr = localStorage.getItem('user');
|
||||||
|
// 防止解析 "undefined" 或 "null" 字符串
|
||||||
|
if (!userStr || userStr === 'undefined' || userStr === 'null') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(userStr);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse user data:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取用户统计信息
|
||||||
|
getUserStats: async (userId) => {
|
||||||
|
return await api.get('/meetings/stats', {
|
||||||
|
params: { user_id: userId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import api from './api';
|
||||||
|
|
||||||
|
export const meetingService = {
|
||||||
|
// 创建会议
|
||||||
|
createMeeting: async (data) => {
|
||||||
|
return await api.post('/meetings', {
|
||||||
|
user_id: data.user_id,
|
||||||
|
title: data.title || `临时会议-${new Date().getTime()}`,
|
||||||
|
meeting_time: data.meeting_time || new Date().toISOString(),
|
||||||
|
attendee_ids: data.attendee_ids || [],
|
||||||
|
tags: data.tags || ''
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取会议列表
|
||||||
|
getMeetings: async (params) => {
|
||||||
|
return await api.get('/meetings', { params });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取会议详情
|
||||||
|
getMeetingDetail: async (meetingId) => {
|
||||||
|
return await api.get(`/meetings/${meetingId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除会议
|
||||||
|
deleteMeeting: async (meetingId) => {
|
||||||
|
return await api.delete(`/meetings/${meetingId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-orange: #FF8C42;
|
||||||
|
--primary-orange-dark: #FF6B35;
|
||||||
|
--inactive-gray: #E0E0E0;
|
||||||
|
--text-dark: #333;
|
||||||
|
--text-gray: #666;
|
||||||
|
--text-light: #999;
|
||||||
|
--white: #FFFFFF;
|
||||||
|
--shadow: rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-hover: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3002,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,921 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@babel/code-frame@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
|
||||||
|
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-validator-identifier" "^7.27.1"
|
||||||
|
js-tokens "^4.0.0"
|
||||||
|
picocolors "^1.1.1"
|
||||||
|
|
||||||
|
"@babel/compat-data@^7.27.2":
|
||||||
|
version "7.28.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f"
|
||||||
|
integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==
|
||||||
|
|
||||||
|
"@babel/core@^7.28.0":
|
||||||
|
version "7.28.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e"
|
||||||
|
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.27.1"
|
||||||
|
"@babel/generator" "^7.28.5"
|
||||||
|
"@babel/helper-compilation-targets" "^7.27.2"
|
||||||
|
"@babel/helper-module-transforms" "^7.28.3"
|
||||||
|
"@babel/helpers" "^7.28.4"
|
||||||
|
"@babel/parser" "^7.28.5"
|
||||||
|
"@babel/template" "^7.27.2"
|
||||||
|
"@babel/traverse" "^7.28.5"
|
||||||
|
"@babel/types" "^7.28.5"
|
||||||
|
"@jridgewell/remapping" "^2.3.5"
|
||||||
|
convert-source-map "^2.0.0"
|
||||||
|
debug "^4.1.0"
|
||||||
|
gensync "^1.0.0-beta.2"
|
||||||
|
json5 "^2.2.3"
|
||||||
|
semver "^6.3.1"
|
||||||
|
|
||||||
|
"@babel/generator@^7.28.5":
|
||||||
|
version "7.28.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298"
|
||||||
|
integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.28.5"
|
||||||
|
"@babel/types" "^7.28.5"
|
||||||
|
"@jridgewell/gen-mapping" "^0.3.12"
|
||||||
|
"@jridgewell/trace-mapping" "^0.3.28"
|
||||||
|
jsesc "^3.0.2"
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets@^7.27.2":
|
||||||
|
version "7.27.2"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d"
|
||||||
|
integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/compat-data" "^7.27.2"
|
||||||
|
"@babel/helper-validator-option" "^7.27.1"
|
||||||
|
browserslist "^4.24.0"
|
||||||
|
lru-cache "^5.1.1"
|
||||||
|
semver "^6.3.1"
|
||||||
|
|
||||||
|
"@babel/helper-globals@^7.28.0":
|
||||||
|
version "7.28.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674"
|
||||||
|
integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
|
||||||
|
|
||||||
|
"@babel/helper-module-imports@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204"
|
||||||
|
integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/traverse" "^7.27.1"
|
||||||
|
"@babel/types" "^7.27.1"
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms@^7.28.3":
|
||||||
|
version "7.28.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6"
|
||||||
|
integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-module-imports" "^7.27.1"
|
||||||
|
"@babel/helper-validator-identifier" "^7.27.1"
|
||||||
|
"@babel/traverse" "^7.28.3"
|
||||||
|
|
||||||
|
"@babel/helper-plugin-utils@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c"
|
||||||
|
integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==
|
||||||
|
|
||||||
|
"@babel/helper-string-parser@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
||||||
|
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5":
|
||||||
|
version "7.28.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
|
||||||
|
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
|
||||||
|
|
||||||
|
"@babel/helper-validator-option@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f"
|
||||||
|
integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
|
||||||
|
|
||||||
|
"@babel/helpers@^7.28.4":
|
||||||
|
version "7.28.4"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827"
|
||||||
|
integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/template" "^7.27.2"
|
||||||
|
"@babel/types" "^7.28.4"
|
||||||
|
|
||||||
|
"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.27.2", "@babel/parser@^7.28.5":
|
||||||
|
version "7.28.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08"
|
||||||
|
integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.28.5"
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-self@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92"
|
||||||
|
integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-plugin-utils" "^7.27.1"
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-source@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0"
|
||||||
|
integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-plugin-utils" "^7.27.1"
|
||||||
|
|
||||||
|
"@babel/template@^7.27.2":
|
||||||
|
version "7.27.2"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
|
||||||
|
integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.27.1"
|
||||||
|
"@babel/parser" "^7.27.2"
|
||||||
|
"@babel/types" "^7.27.1"
|
||||||
|
|
||||||
|
"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5":
|
||||||
|
version "7.28.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b"
|
||||||
|
integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.27.1"
|
||||||
|
"@babel/generator" "^7.28.5"
|
||||||
|
"@babel/helper-globals" "^7.28.0"
|
||||||
|
"@babel/parser" "^7.28.5"
|
||||||
|
"@babel/template" "^7.27.2"
|
||||||
|
"@babel/types" "^7.28.5"
|
||||||
|
debug "^4.3.1"
|
||||||
|
|
||||||
|
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5":
|
||||||
|
version "7.28.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b"
|
||||||
|
integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-string-parser" "^7.27.1"
|
||||||
|
"@babel/helper-validator-identifier" "^7.28.5"
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
|
||||||
|
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
|
||||||
|
|
||||||
|
"@esbuild/android-arm64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
|
||||||
|
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
|
||||||
|
|
||||||
|
"@esbuild/android-arm@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
|
||||||
|
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
|
||||||
|
|
||||||
|
"@esbuild/android-x64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
|
||||||
|
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
|
||||||
|
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
|
||||||
|
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
|
||||||
|
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
|
||||||
|
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
|
||||||
|
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
|
||||||
|
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
|
||||||
|
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
|
||||||
|
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
|
||||||
|
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
|
||||||
|
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
|
||||||
|
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
|
||||||
|
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
|
||||||
|
|
||||||
|
"@esbuild/linux-x64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
|
||||||
|
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
|
||||||
|
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
|
||||||
|
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
|
||||||
|
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
|
||||||
|
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
|
||||||
|
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
|
||||||
|
|
||||||
|
"@esbuild/win32-x64@0.21.5":
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
|
||||||
|
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5":
|
||||||
|
version "0.3.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
|
||||||
|
integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||||
|
"@jridgewell/trace-mapping" "^0.3.24"
|
||||||
|
|
||||||
|
"@jridgewell/remapping@^2.3.5":
|
||||||
|
version "2.3.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1"
|
||||||
|
integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/gen-mapping" "^0.3.5"
|
||||||
|
"@jridgewell/trace-mapping" "^0.3.24"
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri@^3.1.0":
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
|
||||||
|
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
|
||||||
|
version "1.5.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
|
||||||
|
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28":
|
||||||
|
version "0.3.31"
|
||||||
|
resolved "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
|
||||||
|
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
|
"@remix-run/router@1.23.1":
|
||||||
|
version "1.23.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@remix-run/router/-/router-1.23.1.tgz#0ce8857b024e24fc427585316383ad9d295b3a7f"
|
||||||
|
integrity sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==
|
||||||
|
|
||||||
|
"@rolldown/pluginutils@1.0.0-beta.27":
|
||||||
|
version "1.0.0-beta.27"
|
||||||
|
resolved "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz#47d2bf4cef6d470b22f5831b420f8964e0bf755f"
|
||||||
|
integrity sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb"
|
||||||
|
integrity sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz#2b025510c53a5e3962d3edade91fba9368c9d71c"
|
||||||
|
integrity sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz#3577c38af68ccf34c03e84f476bfd526abca10a0"
|
||||||
|
integrity sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz#2bf5f2520a1f3b551723d274b9669ba5b75ed69c"
|
||||||
|
integrity sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz#4bb9cc80252564c158efc0710153c71633f1927c"
|
||||||
|
integrity sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz#2301289094d49415a380cf942219ae9d8b127440"
|
||||||
|
integrity sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz#1d03d776f2065e09fc141df7d143476e94acca88"
|
||||||
|
integrity sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz#8623de0e040b2fd52a541c602688228f51f96701"
|
||||||
|
integrity sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz#ce2d1999bc166277935dde0301cde3dd0417fb6e"
|
||||||
|
integrity sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz#88c2523778444da952651a2219026416564a4899"
|
||||||
|
integrity sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz#578ca2220a200ac4226c536c10c8cc6e4f276714"
|
||||||
|
integrity sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz#aa338d3effd4168a20a5023834a74ba2c3081293"
|
||||||
|
integrity sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz#16ba582f9f6cff58119aa242782209b1557a1508"
|
||||||
|
integrity sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz#e404a77ebd6378483888b8064c703adb011340ab"
|
||||||
|
integrity sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz#92ad52d306227c56bec43d96ad2164495437ffe6"
|
||||||
|
integrity sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz#fd0dea3bb9aa07e7083579f25e1c2285a46cb9fa"
|
||||||
|
integrity sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz#37a3efb09f18d555f8afc490e1f0444885de8951"
|
||||||
|
integrity sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz#c489bec9f4f8320d42c9b324cca220c90091c1f7"
|
||||||
|
integrity sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz#152832b5f79dc22d1606fac3db946283601b7080"
|
||||||
|
integrity sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz#54d91b2bb3bf3e9f30d32b72065a4e52b3a172a5"
|
||||||
|
integrity sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz#df9df03e61a003873efec8decd2034e7f135c71e"
|
||||||
|
integrity sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc@4.53.3":
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe"
|
||||||
|
integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==
|
||||||
|
|
||||||
|
"@types/babel__core@^7.20.5":
|
||||||
|
version "7.20.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
|
||||||
|
integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.20.7"
|
||||||
|
"@babel/types" "^7.20.7"
|
||||||
|
"@types/babel__generator" "*"
|
||||||
|
"@types/babel__template" "*"
|
||||||
|
"@types/babel__traverse" "*"
|
||||||
|
|
||||||
|
"@types/babel__generator@*":
|
||||||
|
version "7.27.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9"
|
||||||
|
integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.0.0"
|
||||||
|
|
||||||
|
"@types/babel__template@*":
|
||||||
|
version "7.4.4"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f"
|
||||||
|
integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.1.0"
|
||||||
|
"@babel/types" "^7.0.0"
|
||||||
|
|
||||||
|
"@types/babel__traverse@*":
|
||||||
|
version "7.28.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74"
|
||||||
|
integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.28.2"
|
||||||
|
|
||||||
|
"@types/estree@1.0.8":
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
|
||||||
|
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||||
|
|
||||||
|
"@types/prop-types@*":
|
||||||
|
version "15.7.15"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
|
||||||
|
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
|
||||||
|
|
||||||
|
"@types/react-dom@^18.2.17":
|
||||||
|
version "18.3.7"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f"
|
||||||
|
integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==
|
||||||
|
|
||||||
|
"@types/react@^18.2.43":
|
||||||
|
version "18.3.27"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/react/-/react-18.3.27.tgz#74a3b590ea183983dc65a474dc17553ae1415c34"
|
||||||
|
integrity sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
csstype "^3.2.2"
|
||||||
|
|
||||||
|
"@vitejs/plugin-react@^4.2.1":
|
||||||
|
version "4.7.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz#647af4e7bb75ad3add578e762ad984b90f4a24b9"
|
||||||
|
integrity sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/core" "^7.28.0"
|
||||||
|
"@babel/plugin-transform-react-jsx-self" "^7.27.1"
|
||||||
|
"@babel/plugin-transform-react-jsx-source" "^7.27.1"
|
||||||
|
"@rolldown/pluginutils" "1.0.0-beta.27"
|
||||||
|
"@types/babel__core" "^7.20.5"
|
||||||
|
react-refresh "^0.17.0"
|
||||||
|
|
||||||
|
asynckit@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
|
axios@^1.6.0:
|
||||||
|
version "1.13.2"
|
||||||
|
resolved "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687"
|
||||||
|
integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.15.6"
|
||||||
|
form-data "^4.0.4"
|
||||||
|
proxy-from-env "^1.1.0"
|
||||||
|
|
||||||
|
baseline-browser-mapping@^2.8.25:
|
||||||
|
version "2.8.31"
|
||||||
|
resolved "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz#16c0f1814638257932e0486dbfdbb3348d0a5710"
|
||||||
|
integrity sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==
|
||||||
|
|
||||||
|
browserslist@^4.24.0:
|
||||||
|
version "4.28.0"
|
||||||
|
resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929"
|
||||||
|
integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==
|
||||||
|
dependencies:
|
||||||
|
baseline-browser-mapping "^2.8.25"
|
||||||
|
caniuse-lite "^1.0.30001754"
|
||||||
|
electron-to-chromium "^1.5.249"
|
||||||
|
node-releases "^2.0.27"
|
||||||
|
update-browserslist-db "^1.1.4"
|
||||||
|
|
||||||
|
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||||
|
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
caniuse-lite@^1.0.30001754:
|
||||||
|
version "1.0.30001757"
|
||||||
|
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz#a46ff91449c69522a462996c6aac4ef95d7ccc5e"
|
||||||
|
integrity sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==
|
||||||
|
|
||||||
|
combined-stream@^1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||||
|
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||||
|
dependencies:
|
||||||
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
|
convert-source-map@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||||
|
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
||||||
|
|
||||||
|
csstype@^3.2.2:
|
||||||
|
version "3.2.3"
|
||||||
|
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
|
||||||
|
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
|
||||||
|
|
||||||
|
debug@^4.1.0, debug@^4.3.1:
|
||||||
|
version "4.4.3"
|
||||||
|
resolved "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||||
|
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
||||||
|
dependencies:
|
||||||
|
ms "^2.1.3"
|
||||||
|
|
||||||
|
delayed-stream@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
|
dunder-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||||
|
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
|
||||||
|
electron-to-chromium@^1.5.249:
|
||||||
|
version "1.5.260"
|
||||||
|
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz#73f555d3e9b9fd16ff48fc406bbad84efa9b86c7"
|
||||||
|
integrity sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==
|
||||||
|
|
||||||
|
es-define-property@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||||
|
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||||
|
|
||||||
|
es-errors@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||||
|
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||||
|
|
||||||
|
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||||
|
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
|
||||||
|
es-set-tostringtag@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
|
||||||
|
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
get-intrinsic "^1.2.6"
|
||||||
|
has-tostringtag "^1.0.2"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
|
||||||
|
esbuild@^0.21.3:
|
||||||
|
version "0.21.5"
|
||||||
|
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
|
||||||
|
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
|
||||||
|
optionalDependencies:
|
||||||
|
"@esbuild/aix-ppc64" "0.21.5"
|
||||||
|
"@esbuild/android-arm" "0.21.5"
|
||||||
|
"@esbuild/android-arm64" "0.21.5"
|
||||||
|
"@esbuild/android-x64" "0.21.5"
|
||||||
|
"@esbuild/darwin-arm64" "0.21.5"
|
||||||
|
"@esbuild/darwin-x64" "0.21.5"
|
||||||
|
"@esbuild/freebsd-arm64" "0.21.5"
|
||||||
|
"@esbuild/freebsd-x64" "0.21.5"
|
||||||
|
"@esbuild/linux-arm" "0.21.5"
|
||||||
|
"@esbuild/linux-arm64" "0.21.5"
|
||||||
|
"@esbuild/linux-ia32" "0.21.5"
|
||||||
|
"@esbuild/linux-loong64" "0.21.5"
|
||||||
|
"@esbuild/linux-mips64el" "0.21.5"
|
||||||
|
"@esbuild/linux-ppc64" "0.21.5"
|
||||||
|
"@esbuild/linux-riscv64" "0.21.5"
|
||||||
|
"@esbuild/linux-s390x" "0.21.5"
|
||||||
|
"@esbuild/linux-x64" "0.21.5"
|
||||||
|
"@esbuild/netbsd-x64" "0.21.5"
|
||||||
|
"@esbuild/openbsd-x64" "0.21.5"
|
||||||
|
"@esbuild/sunos-x64" "0.21.5"
|
||||||
|
"@esbuild/win32-arm64" "0.21.5"
|
||||||
|
"@esbuild/win32-ia32" "0.21.5"
|
||||||
|
"@esbuild/win32-x64" "0.21.5"
|
||||||
|
|
||||||
|
escalade@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
|
||||||
|
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
|
||||||
|
|
||||||
|
follow-redirects@^1.15.6:
|
||||||
|
version "1.15.11"
|
||||||
|
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
|
||||||
|
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
|
||||||
|
|
||||||
|
form-data@^4.0.4:
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
|
||||||
|
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
|
||||||
|
dependencies:
|
||||||
|
asynckit "^0.4.0"
|
||||||
|
combined-stream "^1.0.8"
|
||||||
|
es-set-tostringtag "^2.1.0"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||||
|
version "2.3.3"
|
||||||
|
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||||
|
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||||
|
|
||||||
|
function-bind@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||||
|
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||||
|
|
||||||
|
gensync@^1.0.0-beta.2:
|
||||||
|
version "1.0.0-beta.2"
|
||||||
|
resolved "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||||
|
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||||
|
|
||||||
|
get-intrinsic@^1.2.6:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||||
|
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.2"
|
||||||
|
es-define-property "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
es-object-atoms "^1.1.1"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
get-proto "^1.0.1"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
has-symbols "^1.1.0"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
math-intrinsics "^1.1.0"
|
||||||
|
|
||||||
|
get-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||||
|
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||||
|
dependencies:
|
||||||
|
dunder-proto "^1.0.1"
|
||||||
|
es-object-atoms "^1.0.0"
|
||||||
|
|
||||||
|
gopd@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||||
|
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||||
|
|
||||||
|
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||||
|
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||||
|
|
||||||
|
has-tostringtag@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||||
|
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||||
|
dependencies:
|
||||||
|
has-symbols "^1.0.3"
|
||||||
|
|
||||||
|
hasown@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||||
|
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||||
|
dependencies:
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
|
jsesc@^3.0.2:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
|
||||||
|
integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
|
||||||
|
|
||||||
|
json5@^2.2.3:
|
||||||
|
version "2.2.3"
|
||||||
|
resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||||
|
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||||
|
|
||||||
|
loose-envify@^1.1.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
|
dependencies:
|
||||||
|
js-tokens "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
|
lru-cache@^5.1.1:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||||
|
integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
|
||||||
|
dependencies:
|
||||||
|
yallist "^3.0.2"
|
||||||
|
|
||||||
|
lucide-react@^0.292.0:
|
||||||
|
version "0.292.0"
|
||||||
|
resolved "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.292.0.tgz#c8a06b2ccd8a348a88669def3c0291c035de2884"
|
||||||
|
integrity sha512-rRgUkpEHWpa5VCT66YscInCQmQuPCB1RFRzkkxMxg4b+jaL0V12E3riWWR2Sh5OIiUhCwGW/ZExuEO4Az32E6Q==
|
||||||
|
|
||||||
|
math-intrinsics@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||||
|
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
version "1.52.0"
|
||||||
|
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||||
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
|
mime-types@^2.1.12:
|
||||||
|
version "2.1.35"
|
||||||
|
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||||
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
|
dependencies:
|
||||||
|
mime-db "1.52.0"
|
||||||
|
|
||||||
|
ms@^2.1.3:
|
||||||
|
version "2.1.3"
|
||||||
|
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
||||||
|
nanoid@^3.3.11:
|
||||||
|
version "3.3.11"
|
||||||
|
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||||
|
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||||
|
|
||||||
|
node-releases@^2.0.27:
|
||||||
|
version "2.0.27"
|
||||||
|
resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
|
||||||
|
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
||||||
|
|
||||||
|
picocolors@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||||
|
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||||
|
|
||||||
|
postcss@^8.4.43:
|
||||||
|
version "8.5.6"
|
||||||
|
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
|
||||||
|
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.11"
|
||||||
|
picocolors "^1.1.1"
|
||||||
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
|
proxy-from-env@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||||
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
|
qrcode.react@^3.1.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/qrcode.react/-/qrcode.react-3.2.0.tgz#97daabd4ff641a3f3c678f87be106ebc55f9cd07"
|
||||||
|
integrity sha512-YietHHltOHA4+l5na1srdaMx4sVSOjV9tamHs+mwiLWAMr6QVACRUw1Neax5CptFILcNoITctJY0Ipyn5enQ8g==
|
||||||
|
|
||||||
|
react-dom@^18.2.0:
|
||||||
|
version "18.3.1"
|
||||||
|
resolved "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
|
||||||
|
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
scheduler "^0.23.2"
|
||||||
|
|
||||||
|
react-refresh@^0.17.0:
|
||||||
|
version "0.17.0"
|
||||||
|
resolved "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz#b7e579c3657f23d04eccbe4ad2e58a8ed51e7e53"
|
||||||
|
integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==
|
||||||
|
|
||||||
|
react-router-dom@^6.20.0:
|
||||||
|
version "6.30.2"
|
||||||
|
resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.30.2.tgz#ee8c161bce4890d34484b552f8510f9af0e22b01"
|
||||||
|
integrity sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==
|
||||||
|
dependencies:
|
||||||
|
"@remix-run/router" "1.23.1"
|
||||||
|
react-router "6.30.2"
|
||||||
|
|
||||||
|
react-router@6.30.2:
|
||||||
|
version "6.30.2"
|
||||||
|
resolved "https://registry.npmmirror.com/react-router/-/react-router-6.30.2.tgz#c78a3b40f7011f49a373b1df89492e7d4ec12359"
|
||||||
|
integrity sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==
|
||||||
|
dependencies:
|
||||||
|
"@remix-run/router" "1.23.1"
|
||||||
|
|
||||||
|
react@^18.2.0:
|
||||||
|
version "18.3.1"
|
||||||
|
resolved "https://registry.npmmirror.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
||||||
|
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
rollup@^4.20.0:
|
||||||
|
version "4.53.3"
|
||||||
|
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.53.3.tgz#dbc8cd8743b38710019fb8297e8d7a76e3faa406"
|
||||||
|
integrity sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==
|
||||||
|
dependencies:
|
||||||
|
"@types/estree" "1.0.8"
|
||||||
|
optionalDependencies:
|
||||||
|
"@rollup/rollup-android-arm-eabi" "4.53.3"
|
||||||
|
"@rollup/rollup-android-arm64" "4.53.3"
|
||||||
|
"@rollup/rollup-darwin-arm64" "4.53.3"
|
||||||
|
"@rollup/rollup-darwin-x64" "4.53.3"
|
||||||
|
"@rollup/rollup-freebsd-arm64" "4.53.3"
|
||||||
|
"@rollup/rollup-freebsd-x64" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-arm64-gnu" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-arm64-musl" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-loong64-gnu" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-riscv64-musl" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-s390x-gnu" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-x64-gnu" "4.53.3"
|
||||||
|
"@rollup/rollup-linux-x64-musl" "4.53.3"
|
||||||
|
"@rollup/rollup-openharmony-arm64" "4.53.3"
|
||||||
|
"@rollup/rollup-win32-arm64-msvc" "4.53.3"
|
||||||
|
"@rollup/rollup-win32-ia32-msvc" "4.53.3"
|
||||||
|
"@rollup/rollup-win32-x64-gnu" "4.53.3"
|
||||||
|
"@rollup/rollup-win32-x64-msvc" "4.53.3"
|
||||||
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
scheduler@^0.23.2:
|
||||||
|
version "0.23.2"
|
||||||
|
resolved "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
|
||||||
|
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
semver@^6.3.1:
|
||||||
|
version "6.3.1"
|
||||||
|
resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
|
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||||
|
|
||||||
|
source-map-js@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||||
|
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||||
|
|
||||||
|
update-browserslist-db@^1.1.4:
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz#7802aa2ae91477f255b86e0e46dbc787a206ad4a"
|
||||||
|
integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==
|
||||||
|
dependencies:
|
||||||
|
escalade "^3.2.0"
|
||||||
|
picocolors "^1.1.1"
|
||||||
|
|
||||||
|
vite@^5.0.8:
|
||||||
|
version "5.4.21"
|
||||||
|
resolved "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027"
|
||||||
|
integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==
|
||||||
|
dependencies:
|
||||||
|
esbuild "^0.21.3"
|
||||||
|
postcss "^8.4.43"
|
||||||
|
rollup "^4.20.0"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "~2.3.3"
|
||||||
|
|
||||||
|
yallist@^3.0.2:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||||
|
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||||
Loading…
Reference in New Issue