fix: 后端代码健康修复 (P1-1/P1-2/P2-1/P2-2/P2-3) #68

Merged
xiaoxia merged 8 commits from develop into main 2026-06-28 10:48:47 +08:00
26 changed files with 12 additions and 2119 deletions
+1 -1
View File
@@ -206,7 +206,7 @@ async def forgot_password(
):
success, error = RequestPasswordResetUseCase(
user_repository=user_repository,
base_url="http://localhost:3000",
base_url=settings.APP_BASE_URL,
email_service=email_service,
).execute(PasswordResetUseCaseRequest(email=request.email))
if not success:
+1 -1
View File
@@ -72,7 +72,7 @@ class Settings(BaseSettings):
SMTP_USER: str = ""
SMTP_PASSWORD: str = ""
SMTP_FROM_EMAIL: str = ""
SMTP_FRON_NAME: str = "小虾 SaaS"
SMTP_FROM_NAME: str = "小虾 SaaS"
SMTP_USE_TLS: bool = True
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
-77
View File
@@ -1,77 +0,0 @@
/**
* 视频生成 API
*/
import apiClient from "./client";
import type { EditingMode } from "./editPlans";
export interface GenerationTaskItem {
id: string;
project_id: string;
asset_library_id: string;
strategy_id?: string | null;
voice_library_id?: string | null;
edit_plan_id?: string | null;
editing_mode?: EditingMode;
status: "pending" | "running" | "completed" | "failed" | "cancelled";
progress: number;
result_count: number;
error_message?: string | null;
created_at?: string;
updated_at?: string;
}
export interface GeneratedVideoItem {
id: string;
project_id: string;
generation_task_id: string;
name: string;
file_url: string;
file_size: number;
duration: number;
thumbnail_url?: string | null;
width: number;
height: number;
fps: number;
status?: string;
review_status?: "pending_review" | "approved" | "rejected";
generation_params?: Record<string, unknown>;
editing_mode?: EditingMode;
}
export const createGenerationTask = async (data: {
project_id: string;
asset_library_id: string;
strategy_id?: string;
voice_library_id?: string;
edit_plan_id?: string;
editing_mode?: EditingMode;
created_by_user_id?: string;
}): Promise<GenerationTaskItem> => {
const response = await apiClient.post("/generation/tasks", data);
return response.data;
};
export const getGenerationTask = async (taskId: string): Promise<GenerationTaskItem> => {
const response = await apiClient.get(`/generation/tasks/${taskId}`);
return response.data;
};
export const getGenerationResults = async (taskId: string): Promise<GeneratedVideoItem[]> => {
const response = await apiClient.get(`/generation/tasks/${taskId}/results`);
return response.data.items;
};
export const getGeneratedVideos = async (projectId: string): Promise<GeneratedVideoItem[]> => {
const response = await apiClient.get("/generated-videos", { params: { project_id: projectId } });
return response.data.items;
};
export const updateGeneratedVideoReviewStatus = async (videoId: string, reviewStatus: "pending_review" | "approved" | "rejected"): Promise<GeneratedVideoItem> => {
const response = await apiClient.patch(`/generated-videos/${videoId}/review`, { review_status: reviewStatus });
return response.data;
};
export const getGeneratedVideoDownloadUrl = async (videoId: string): Promise<string> => {
const response = await apiClient.get(`/generated-videos/${videoId}/download-url`);
return response.data.download_url;
};
-28
View File
@@ -1,28 +0,0 @@
import apiClient from './client';
export interface ProjectTitleItem {
id: string;
project_id: string;
text: string;
category: 'default' | 'marketing' | 'tutorial' | 'story' | 'promo';
favorite: boolean;
usage_count: number;
is_active: boolean;
created_at: string;
updated_at: string;
}
export const getProjectTitles = async (projectId: string, activeOnly = false): Promise<ProjectTitleItem[]> => {
const response = await apiClient.get(`/projects/${projectId}/titles`, { params: { active_only: activeOnly } });
return response.data.items;
};
export const createProjectTitle = async (projectId: string, data: { text: string; category?: ProjectTitleItem['category']; favorite?: boolean }): Promise<ProjectTitleItem> => {
const response = await apiClient.post(`/projects/${projectId}/titles`, data);
return response.data;
};
export const updateProjectTitle = async (titleId: string, data: { text?: string; category?: ProjectTitleItem['category']; favorite?: boolean; is_active?: boolean }): Promise<ProjectTitleItem> => {
const response = await apiClient.patch(`/project-titles/${titleId}`, data);
return response.data;
};
-27
View File
@@ -1,27 +0,0 @@
import apiClient from './client';
export interface ProjectItem {
id: string;
name: string;
description: string;
}
export interface CreateProjectRequest {
name: string;
description?: string;
}
export const getProject = async (projectId: string): Promise<ProjectItem> => {
const response = await apiClient.get(`/projects/${projectId}`);
return response.data;
};
export const getProjects = async (): Promise<ProjectItem[]> => {
const response = await apiClient.get('/projects');
return response.data.items;
};
export const createProject = async (data: CreateProjectRequest): Promise<ProjectItem> => {
const response = await apiClient.post('/projects', data);
return response.data;
};
-64
View File
@@ -1,64 +0,0 @@
/**
* 订阅相关 API
*/
import apiClient from './client';
// 类型定义
export interface SubscriptionPlan {
name: 'free' | 'pro' | 'enterprise';
display_name: string;
price: number;
currency: string;
max_projects: number;
max_storage_gb: number;
features: string[];
}
export interface QuotaStatus {
projects: {
used: number;
limit: number;
status: 'normal' | 'warning' | 'critical' | 'exceeded';
};
storage: {
used_gb: number;
limit_gb: number;
status: 'normal' | 'warning' | 'critical' | 'exceeded';
};
}
// 获取所有订阅计划
export const getPlans = async (): Promise<SubscriptionPlan[]> => {
const response = await apiClient.get('/subscriptions/plans');
return response.data;
};
// 获取当前订阅
export const getCurrentSubscription = async (
): Promise<SubscriptionPlan> => {
const response = await apiClient.get('/subscription');
return response.data;
};
// 升级订阅
export const upgradeSubscription = async (
plan: 'pro' | 'enterprise'
): Promise<{ message: string }> => {
const response = await apiClient.post('/subscription/upgrade', {
plan,
});
return response.data;
};
// 取消订阅
export const cancelSubscription = async (
): Promise<{ message: string }> => {
const response = await apiClient.post('/subscription/cancel');
return response.data;
};
// 获取配额状态
export const getQuotaStatus = async (): Promise<QuotaStatus> => {
const response = await apiClient.get('/quota');
return response.data;
};
@@ -1,80 +0,0 @@
/**
* 邀请成员弹窗
*/
import React from 'react';
import { Modal, Form, Input, Select, message } from 'antd';
import { useInviteMember } from '@/hooks/useWorkspace';
interface InviteMemberModalProps {
workspaceId: string;
open: boolean;
onClose: () => void;
}
const InviteMemberModal: React.FC<InviteMemberModalProps> = ({
workspaceId,
open,
onClose,
}) => {
const [form] = Form.useForm();
const inviteMutation = useInviteMember(workspaceId);
const handleOk = async () => {
try {
const values = await form.validateFields();
await inviteMutation.mutateAsync(values);
message.success('邀请已发送!');
form.resetFields();
onClose();
} catch (error) {
// 验证失败或请求失败
}
};
const handleCancel = () => {
form.resetFields();
onClose();
};
return (
<Modal
title="邀请成员"
open={open}
onOk={handleOk}
onCancel={handleCancel}
confirmLoading={inviteMutation.isPending}
okText="发送邀请"
cancelText="取消"
>
<Form form={form} layout="vertical">
<Form.Item
name="email"
label="邮箱地址"
rules={[
{ required: true, message: '请输入邮箱地址' },
{ type: 'email', message: '请输入有效的邮箱地址' },
]}
>
<Input placeholder="member@example.com" />
</Form.Item>
<Form.Item
name="role"
label="角色"
initialValue="member"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select
options={[
{ value: 'admin', label: '管理员 - 可以管理成员和项目' },
{ value: 'member', label: '成员 - 可以创建和管理项目' },
{ value: 'viewer', label: '访客 - 只读权限' },
]}
/>
</Form.Item>
</Form>
</Modal>
);
};
export default InviteMemberModal;
@@ -1,129 +0,0 @@
/**
* 成员列表组件
*/
import React from 'react';
import { Table, Button, Tag, Space, Popconfirm, message, Select } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import { useWorkspaceMembers } from '@/hooks/useWorkspace';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { removeMember, updateMemberRole } from '@/api/workspace';
interface MemberListProps {
workspaceId: string;
}
const MemberList: React.FC<MemberListProps> = ({ workspaceId }) => {
const { data: members, isLoading } = useWorkspaceMembers(workspaceId);
const queryClient = useQueryClient();
const removeMutation = useMutation({
mutationFn: (memberId: string) => removeMember(workspaceId, memberId),
onSuccess: () => {
message.success('成员已移除');
queryClient.invalidateQueries({ queryKey: ['workspaceMembers', workspaceId] });
},
});
const updateRoleMutation = useMutation({
mutationFn: ({ memberId, role }: { memberId: string; role: string }) =>
updateMemberRole(workspaceId, memberId, role as any),
onSuccess: () => {
message.success('角色已更新');
queryClient.invalidateQueries({ queryKey: ['workspaceMembers', workspaceId] });
},
});
const getRoleTag = (role: string) => {
const roleConfig: Record<string, { color: string; text: string }> = {
owner: { color: 'gold', text: '拥有者' },
admin: { color: 'blue', text: '管理员' },
member: { color: 'green', text: '成员' },
viewer: { color: 'default', text: '访客' },
};
const config = roleConfig[role] || roleConfig.member;
return <Tag color={config.color}>{config.text}</Tag>;
};
const columns = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
render: (role: string, record: any) => {
if (role === 'owner') {
return getRoleTag(role);
}
return (
<Select
value={role}
style={{ width: 120 }}
onChange={(newRole) =>
updateRoleMutation.mutate({ memberId: record.id, role: newRole })
}
options={[
{ value: 'admin', label: '管理员' },
{ value: 'member', label: '成员' },
{ value: 'viewer', label: '访客' },
]}
/>
);
},
},
{
title: '加入时间',
dataIndex: 'joined_at',
key: 'joined_at',
render: (date: string) => new Date(date).toLocaleDateString(),
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => {
if (record.role === 'owner') {
return <span style={{ color: '#999' }}>-</span>;
}
return (
<Space>
<Popconfirm
title="确定移除该成员?"
onConfirm={() => removeMutation.mutate(record.id)}
okText="确定"
cancelText="取消"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
loading={removeMutation.isPending}
>
</Button>
</Popconfirm>
</Space>
);
},
},
];
return (
<Table
columns={columns}
dataSource={members}
loading={isLoading}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
);
};
export default MemberList;
@@ -1,100 +0,0 @@
/**
* 权限矩阵展示组件 - V21 UI
*/
import React from 'react';
import { Card, Table } from 'antd';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import './business.css';
const PermissionMatrix: React.FC = () => {
const permissions = [
{ action: '查看工作空间', owner: true, admin: true, member: true, viewer: true },
{ action: '创建项目', owner: true, admin: true, member: true, viewer: false },
{ action: '编辑项目', owner: true, admin: true, member: true, viewer: false },
{ action: '删除项目', owner: true, admin: true, member: false, viewer: false },
{ action: '邀请成员', owner: true, admin: true, member: false, viewer: false },
{ action: '移除成员', owner: true, admin: true, member: false, viewer: false },
{ action: '修改成员角色', owner: true, admin: true, member: false, viewer: false },
{ action: '升级订阅', owner: true, admin: false, member: false, viewer: false },
{ action: '删除工作空间', owner: true, admin: false, member: false, viewer: false },
];
const columns = [
{
title: '操作',
dataIndex: 'action',
key: 'action',
fixed: 'left' as const,
width: 160,
render: (text: string) => <span style={{ fontWeight: 500 }}>{text}</span>,
},
{
title: '拥有者',
dataIndex: 'owner',
key: 'owner',
align: 'center' as const,
width: 100,
render: (value: boolean) =>
value ? (
<CheckOutlined style={{ color: '#16a34a', fontSize: '18px' }} />
) : (
<CloseOutlined style={{ color: '#dc2626', fontSize: '16px' }} />
),
},
{
title: '管理员',
dataIndex: 'admin',
key: 'admin',
align: 'center' as const,
width: 100,
render: (value: boolean) =>
value ? (
<CheckOutlined style={{ color: '#16a34a', fontSize: '18px' }} />
) : (
<CloseOutlined style={{ color: '#dc2626', fontSize: '16px' }} />
),
},
{
title: '成员',
dataIndex: 'member',
key: 'member',
align: 'center' as const,
width: 100,
render: (value: boolean) =>
value ? (
<CheckOutlined style={{ color: '#16a34a', fontSize: '18px' }} />
) : (
<CloseOutlined style={{ color: '#dc2626', fontSize: '16px' }} />
),
},
{
title: '访客',
dataIndex: 'viewer',
key: 'viewer',
align: 'center' as const,
width: 100,
render: (value: boolean) =>
value ? (
<CheckOutlined style={{ color: '#16a34a', fontSize: '18px' }} />
) : (
<CloseOutlined style={{ color: '#dc2626', fontSize: '16px' }} />
),
},
];
return (
<Card title="权限说明" className="xx-card">
<div className="xx-table-wrapper">
<Table
columns={columns}
dataSource={permissions}
pagination={false}
rowKey="action"
size="middle"
/>
</div>
</Card>
);
};
export default PermissionMatrix;
@@ -89,7 +89,6 @@
overflow: hidden;
}
<<<<<<< HEAD
/* 表格包装器 */
.xx-table-wrapper {
border-radius: 16px;
@@ -223,7 +222,6 @@
box-shadow: 0 8px 24px rgba(79,70,229,0.1);
}
<<<<<<< HEAD
/* ==================== 进度条 ==================== */
.xx-progress {
margin-top: 12px;
@@ -231,7 +229,6 @@
/* ==================== Ant Design 覆盖样式 ==================== */
/* Table overrides */
/* ==================== Ant Design 覆盖样式 ==================== */
.ant-table-wrapper .ant-table-thead > tr > th {
background: #f8fafc !important;
font-weight: 700 !important;
@@ -249,7 +246,6 @@
background: #fafbfc !important;
}
<<<<<<< HEAD
/* Card overrides */
.ant-card {
border-radius: 22px !important;
@@ -272,7 +268,6 @@
padding: 20px 24px !important;
}
<<<<<<< HEAD
/* Modal overrides */
.ant-modal-content {
border-radius: 22px !important;
@@ -298,7 +293,6 @@
padding: 16px 24px !important;
}
<<<<<<< HEAD
/* Button overrides */
.ant-btn-primary {
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
@@ -316,7 +310,6 @@
transform: translateY(-1px);
}
<<<<<<< HEAD
/* Tag overrides */
.ant-tag {
border-radius: 10px !important;
@@ -324,7 +317,6 @@
font-weight: 500 !important;
}
<<<<<<< HEAD
/* Select overrides */
.ant-select-selector {
border-radius: 14px !important;
@@ -340,7 +332,6 @@
box-shadow: 0 0 0 3px rgba(79,70,229,0.1) !important;
}
<<<<<<< HEAD
/* Input overrides */
.ant-input {
border-radius: 14px !important;
@@ -357,7 +348,6 @@
box-shadow: 0 0 0 3px rgba(79,70,229,0.1) !important;
}
<<<<<<< HEAD
/* Progress overrides */
.ant-progress-inner {
background: #f1f5f9 !important;
-107
View File
@@ -1,107 +0,0 @@
/**
* 侧边栏导航
*/
import React from 'react';
import { Layout, Menu } from 'antd';
import {
HomeOutlined,
AppstoreOutlined,
TeamOutlined,
CrownOutlined,
UserOutlined,
DashboardOutlined,
} from '@ant-design/icons';
import { useNavigate, useLocation } from 'react-router-dom';
import type { MenuProps } from 'antd';
const { Sider } = Layout;
interface SidebarProps {
collapsed: boolean;
}
const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
const navigate = useNavigate();
const location = useLocation();
const menuItems: MenuProps['items'] = [
{
key: '/',
icon: <HomeOutlined />,
label: '首页',
onClick: () => navigate('/'),
},
{
key: '/projects',
icon: <AppstoreOutlined />,
label: '项目',
disabled: true,
},
{
key: '/members',
icon: <TeamOutlined />,
label: '成员管理',
disabled: true,
},
{
key: '/subscription',
icon: <CrownOutlined />,
label: '订阅管理',
onClick: () => navigate('/subscription'),
},
{
key: '/admin',
icon: <DashboardOutlined />,
label: 'Admin(暂未开放)',
disabled: true,
},
{
key: '/profile',
icon: <UserOutlined />,
label: '个人中心',
onClick: () => navigate('/profile'),
},
];
// 根据当前路径选中菜单项
const selectedKey = '/' + location.pathname.split('/')[1];
return (
<Sider
collapsible
collapsed={collapsed}
trigger={null}
width={200}
style={{
overflow: 'auto',
height: '100vh',
position: 'sticky',
left: 0,
top: 0,
bottom: 0,
}}
>
<div
style={{
height: 64,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '18px',
fontWeight: 'bold',
}}
>
{collapsed ? '🦐' : '小虾 SaaS'}
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[selectedKey]}
items={menuItems}
/>
</Sider>
);
};
export default Sidebar;
-245
View File
@@ -1,245 +0,0 @@
/**
* Admin Analytics 数据分析页面
*/
import React from 'react';
import { Card, Row, Col, Statistic, Table, DatePicker, Space } from 'antd';
import {
LineChart,
Line,
BarChart,
Bar,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import {
ArrowUpOutlined,
UserAddOutlined,
DollarOutlined,
ProjectOutlined,
} from '@ant-design/icons';
import './Admin.css';
const { RangePicker } = DatePicker;
// V21 配色
const V21_COLORS = {
primary: '#4f46e5',
primaryLight: '#6366f1',
success: '#10b981',
successLight: '#34d399',
warning: '#f59e0b',
warningLight: '#fbbf24',
purple: '#8b5cf6',
purpleLight: '#a78bfa',
};
const Analytics: React.FC = () => {
// 模拟用户增长数据
const userGrowthData = [
{ month: '1月', users: 120, paid: 15 },
{ month: '2月', users: 180, paid: 28 },
{ month: '3月', users: 240, paid: 42 },
{ month: '4月', users: 320, paid: 58 },
{ month: '5月', users: 450, paid: 89 },
{ month: '6月', users: 620, paid: 135 },
];
// 模拟营收数据
const revenueData = [
{ month: '1月', revenue: 1480 },
{ month: '2月', revenue: 2770 },
{ month: '3月', revenue: 4160 },
{ month: '4月', revenue: 5740 },
{ month: '5月', revenue: 8810 },
{ month: '6月', revenue: 13365 },
];
// 订阅计划分布
const planDistribution = [
{ name: 'Free', value: 485, color: '#a78bfa' },
{ name: 'Pro', value: 120, color: '#34d399' },
{ name: 'Enterprise', value: 15, color: '#fbbf24' },
];
// 活跃度统计
const activityStats = [
{ metric: '日活跃用户 (DAU)', value: 892, growth: '+12.5%', icon: <UserAddOutlined />, colorClass: 'primary' },
{ metric: '月活跃用户 (MAU)', value: 3456, growth: '+8.3%', icon: <ArrowUpOutlined />, colorClass: 'success' },
{ metric: '本月收入', value: 13365, prefix: '¥', growth: '+51.6%', icon: <DollarOutlined />, colorClass: 'warning' },
{ metric: '活跃项目数', value: 2341, growth: '+18.2%', icon: <ProjectOutlined />, colorClass: 'purple' },
];
// 用户留存表格
const retentionData = [
{ cohort: '2024-01', day1: '100%', day7: '68%', day30: '45%', day90: '28%' },
{ cohort: '2024-02', day1: '100%', day7: '72%', day30: '48%', day90: '31%' },
{ cohort: '2024-03', day1: '100%', day7: '75%', day30: '52%', day90: '-' },
{ cohort: '2024-04', day1: '100%', day7: '78%', day30: '55%', day90: '-' },
{ cohort: '2024-05', day1: '100%', day7: '80%', day30: '-', day90: '-' },
];
const retentionColumns = [
{ title: 'Cohort', dataIndex: 'cohort', key: 'cohort' },
{ title: 'Day 1', dataIndex: 'day1', key: 'day1' },
{ title: 'Day 7', dataIndex: 'day7', key: 'day7' },
{ title: 'Day 30', dataIndex: 'day30', key: 'day30' },
{ title: 'Day 90', dataIndex: 'day90', key: 'day90' },
];
const colorMap: Record<string, string> = {
primary: '#4f46e5',
success: '#10b981',
warning: '#f59e0b',
purple: '#8b5cf6',
};
return (
<div className="analytics-page">
<div className="xx-page-head">
<div className="xx-page-head-content">
<h2></h2>
<p></p>
</div>
<div className="xx-page-head-actions">
<Space>
<RangePicker className="xx-date-picker" />
</Space>
</div>
</div>
{/* 关键指标 */}
<Row gutter={24} className="xx-grid-4">
{activityStats.map((stat, index) => (
<Col span={6} key={index}>
<div className="xx-stat-card">
<div className="xx-stat-card-header">
<div className={`xx-stat-card-icon ${stat.colorClass}`}>
{stat.icon}
</div>
</div>
<div className="xx-stat-card-label">{stat.metric}</div>
<div
className="xx-stat-card-value"
style={{ color: colorMap[stat.colorClass] }}
>
{stat.prefix}{stat.value.toLocaleString()}
</div>
<div className="xx-stat-card-growth">
{stat.growth} vs
</div>
</div>
</Col>
))}
</Row>
{/* 图表区域 */}
<Row gutter={24} style={{ marginBottom: 24 }}>
{/* 用户增长趋势 */}
<Col span={16}>
<div className="xx-chart-container">
<Card title="用户增长趋势">
<ResponsiveContainer width="100%" height={300}>
<LineChart data={userGrowthData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="users"
stroke={V21_COLORS.primary}
name="总用户"
strokeWidth={3}
/>
<Line
type="monotone"
dataKey="paid"
stroke={V21_COLORS.success}
name="付费用户"
strokeWidth={3}
/>
</LineChart>
</ResponsiveContainer>
</Card>
</div>
</Col>
{/* 订阅计划分布 */}
<Col span={8}>
<div className="xx-chart-container">
<Card title="订阅计划分布">
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={planDistribution}
cx="50%"
cy="50%"
labelLine={false}
label={(entry) => `${entry.name}: ${entry.value}`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{planDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</Card>
</div>
</Col>
</Row>
{/* 营收趋势 */}
<Row gutter={24} style={{ marginBottom: 24 }}>
<Col span={24}>
<div className="xx-chart-container">
<Card title="营收趋势">
<ResponsiveContainer width="100%" height={300}>
<BarChart data={revenueData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Bar
dataKey="revenue"
fill={V21_COLORS.warning}
name="收入 (¥)"
radius={[8, 8, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Card>
</div>
</Col>
</Row>
{/* 用户留存表 */}
<div className="xx-table-wrapper">
<Card title="用户留存率 (Cohort Analysis)">
<Table
columns={retentionColumns}
dataSource={retentionData}
rowKey="cohort"
pagination={false}
/>
</Card>
</div>
</div>
);
};
export default Analytics;
export const Component = Analytics;
-86
View File
@@ -1,86 +0,0 @@
/**
* Admin Dashboard 仪表盘
*/
import React from 'react';
import { Card, Row, Col, Statistic, Table } from 'antd';
import {
UserOutlined,
CrownOutlined,
DollarOutlined,
RiseOutlined,
} from '@ant-design/icons';
import './Admin.css';
const Dashboard: React.FC = () => {
// 模拟数据
const stats = [
{ title: '总用户数', value: 1234, icon: <UserOutlined />, colorClass: 'primary' },
{ title: '付费用户', value: 156, icon: <CrownOutlined />, colorClass: 'success' },
{ title: '月收入', value: 15444, prefix: '¥', icon: <DollarOutlined />, colorClass: 'warning' },
{ title: '活跃用户', value: 892, icon: <RiseOutlined />, colorClass: 'purple' },
];
const recentUsers = [
{ id: '1', username: 'user1', email: 'user1@example.com', created_at: '2024-06-17' },
{ id: '2', username: 'user2', email: 'user2@example.com', created_at: '2024-06-16' },
{ id: '3', username: 'user3', email: 'user3@example.com', created_at: '2024-06-15' },
];
const columns = [
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '邮箱', dataIndex: 'email', key: 'email' },
{ title: '注册时间', dataIndex: 'created_at', key: 'created_at' },
];
const colorMap: Record<string, string> = {
primary: '#4f46e5',
success: '#10b981',
warning: '#f59e0b',
purple: '#8b5cf6',
};
return (
<div className="dashboard-page">
<div className="xx-page-head-simple">
<h2>Dashboard</h2>
<p></p>
</div>
<Row gutter={24} className="xx-grid-4">
{stats.map((stat, index) => (
<Col span={6} key={index}>
<div className="xx-stat-card">
<div className="xx-stat-card-header">
<div className={`xx-stat-card-icon ${stat.colorClass}`}>
{stat.icon}
</div>
</div>
<div className="xx-stat-card-label">{stat.title}</div>
<div
className="xx-stat-card-value"
style={{ color: colorMap[stat.colorClass] }}
>
{stat.prefix}{stat.value.toLocaleString()}
</div>
</div>
</Col>
))}
</Row>
<div className="xx-table-wrapper">
<Card title="最近注册用户" className="xx-card">
<Table
columns={columns}
dataSource={recentUsers}
rowKey="id"
pagination={false}
/>
</Card>
</div>
</div>
);
};
export default Dashboard;
export const Component = Dashboard;
-344
View File
@@ -1,344 +0,0 @@
/**
* 日志查看器
*/
import React, { useState } from 'react';
import { Card, Table, Tag, Input, Select, DatePicker, Space, Button, Drawer } from 'antd';
import {
SearchOutlined,
FilterOutlined,
DownloadOutlined,
EyeOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import './Admin.css';
const { Search } = Input;
const { RangePicker } = DatePicker;
const { Option } = Select;
interface LogEntry {
id: string;
timestamp: string;
level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
service: string;
user?: string;
action: string;
details: string;
ip?: string;
request_id?: string;
}
const LogViewer: React.FC = () => {
const [selectedLog, setSelectedLog] = useState<LogEntry | null>(null);
const [drawerVisible, setDrawerVisible] = useState(false);
const [filterLevel, setFilterLevel] = useState<string>('all');
const [filterService, setFilterService] = useState<string>('all');
// 模拟日志数据
const logs: LogEntry[] = [
{
id: '1',
timestamp: '2024-06-17 17:15:32',
level: 'INFO',
service: 'API',
user: 'user@example.com',
action: 'User Login',
details: 'User successfully logged in',
ip: '192.168.1.100',
request_id: 'req_abc123',
},
{
id: '2',
timestamp: '2024-06-17 17:14:28',
level: 'WARN',
service: 'API',
user: 'admin@example.com',
action: 'Failed Login Attempt',
details: 'Invalid password provided (attempt 2/5)',
ip: '192.168.1.101',
request_id: 'req_def456',
},
{
id: '3',
timestamp: '2024-06-17 17:12:45',
level: 'ERROR',
service: 'Database',
action: 'Connection Timeout',
details: 'PostgreSQL connection pool exhausted, timeout after 5s',
request_id: 'req_ghi789',
},
{
id: '4',
timestamp: '2024-06-17 17:10:15',
level: 'INFO',
service: 'Celery',
action: 'Task Completed',
details: 'Video processing task completed successfully',
request_id: 'task_jkl012',
},
{
id: '5',
timestamp: '2024-06-17 17:08:52',
level: 'WARN',
service: 'ObjectStorage',
action: 'Slow Upload',
details: 'File upload took 3.2s (>1s threshold)',
ip: '192.168.1.102',
request_id: 'req_mno345',
},
{
id: '6',
timestamp: '2024-06-17 17:05:33',
level: 'ERROR',
service: 'API',
user: 'user2@example.com',
action: 'Permission Denied',
details: 'User attempted to access admin endpoint without permission',
ip: '192.168.1.103',
request_id: 'req_pqr678',
},
{
id: '7',
timestamp: '2024-06-17 17:03:21',
level: 'INFO',
service: 'API',
user: 'user3@example.com',
action: 'Project Created', details: 'New project "My Project" created',
ip: '192.168.1.104',
request_id: 'req_stu901',
},
{
id: '8',
timestamp: '2024-06-17 17:01:08',
level: 'DEBUG',
service: 'Redis',
action: 'Cache Miss',
details: 'Cache key "project:123" not found, fetching from DB',
request_id: 'req_vwx234',
},
];
const getLevelTagClass = (level: string) => {
const classes: Record<string, string> = {
INFO: 'xx-tag info',
WARN: 'xx-tag warning',
ERROR: 'xx-tag error',
DEBUG: 'xx-tag debug',
};
return classes[level] || 'xx-tag debug';
};
const handleViewDetails = (log: LogEntry) => {
setSelectedLog(log);
setDrawerVisible(true);
};
const columns: ColumnsType<LogEntry> = [
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
width: 180,
sorter: (a, b) => a.timestamp.localeCompare(b.timestamp),
},
{
title: '级别',
dataIndex: 'level',
key: 'level',
width: 100,
render: (level: string) => <Tag className={getLevelTagClass(level)}>{level}</Tag>,
filters: [
{ text: 'INFO', value: 'INFO' },
{ text: 'WARN', value: 'WARN' },
{ text: 'ERROR', value: 'ERROR' },
{ text: 'DEBUG', value: 'DEBUG' },
],
onFilter: (value, record) => record.level === value,
},
{
title: '服务',
dataIndex: 'service',
key: 'service',
width: 120,
filters: [
{ text: 'API', value: 'API' },
{ text: 'Database', value: 'Database' },
{ text: 'Celery', value: 'Celery' },
{ text: 'ObjectStorage', value: 'ObjectStorage' },
{ text: 'Redis', value: 'Redis' },
],
onFilter: (value, record) => record.service === value,
},
{
title: '用户',
dataIndex: 'user',
key: 'user',
width: 180,
render: (user?: string) => user || '-',
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 200,
},
{
title: '详情',
dataIndex: 'details',
key: 'details',
ellipsis: true,
},
{
title: '操作',
key: 'actions',
width: 100,
render: (_, record) => (
<Button
type="link"
icon={<EyeOutlined />}
onClick={() => handleViewDetails(record)}
>
</Button>
),
},
];
return (
<div className="log-viewer-page">
<div className="xx-page-head-simple">
<h2></h2>
<p></p>
</div>
<div className="xx-filter-bar">
<Search
placeholder="搜索日志内容..."
allowClear
enterButton={<SearchOutlined />}
className="xx-search-input"
style={{ width: 280 }}
/>
<Select
placeholder="日志级别"
className="xx-select"
style={{ width: 130 }}
value={filterLevel}
onChange={setFilterLevel}
>
<Option value="all"></Option>
<Option value="INFO">INFO</Option>
<Option value="WARN">WARN</Option>
<Option value="ERROR">ERROR</Option>
<Option value="DEBUG">DEBUG</Option>
</Select>
<Select
placeholder="服务"
className="xx-select"
style={{ width: 140 }}
value={filterService}
onChange={setFilterService}
>
<Option value="all"></Option>
<Option value="API">API</Option>
<Option value="Database">Database</Option>
<Option value="Celery">Celery</Option>
<Option value="ObjectStorage">Object Storage</Option>
<Option value="Redis">Redis</Option>
</Select>
<RangePicker showTime className="xx-date-picker" />
<Button icon={<FilterOutlined />} disabled className="xx-ghost-btn">
</Button>
<Button icon={<DownloadOutlined />} disabled className="xx-ghost-btn">
</Button>
</div>
<div className="xx-table-wrapper">
<Table
columns={columns}
dataSource={logs}
rowKey="id"
pagination={{
total: logs.length,
pageSize: 10,
showSizeChanger: true,
showTotal: (total) => `${total} 条日志`,
}}
scroll={{ x: 1200 }}
/>
</div>
{/* 日志详情抽屉 */}
<Drawer
title="日志详情"
placement="right"
width={600}
open={drawerVisible}
onClose={() => setDrawerVisible(false)}
className="xx-drawer"
>
{selectedLog && (
<div>
<div className="xx-log-detail">
<div className="xx-log-detail-label"></div>
<div className="xx-log-detail-value">{selectedLog.timestamp}</div>
</div>
<div className="xx-log-detail">
<div className="xx-log-detail-label"></div>
<div className="xx-log-detail-value">
<Tag className={getLevelTagClass(selectedLog.level)}>{selectedLog.level}</Tag>
</div>
</div>
<div className="xx-log-detail">
<div className="xx-log-detail-label"></div>
<div className="xx-log-detail-value">{selectedLog.service}</div>
</div>
{selectedLog.user && (
<div className="xx-log-detail">
<div className="xx-log-detail-label"></div>
<div className="xx-log-detail-value">{selectedLog.user}</div>
</div>
)}
<div className="xx-log-detail">
<div className="xx-log-detail-label"></div>
<div className="xx-log-detail-value">{selectedLog.action}</div>
</div>
<div className="xx-log-detail">
<div className="xx-log-detail-label"></div>
<div className="xx-log-detail-code">{selectedLog.details}</div>
</div>
{selectedLog.ip && (
<div className="xx-log-detail">
<div className="xx-log-detail-label">IP </div>
<div className="xx-log-detail-value">{selectedLog.ip}</div>
</div>
)}
{selectedLog.request_id && (
<div className="xx-log-detail">
<div className="xx-log-detail-label">Request ID</div>
<div className="xx-log-detail-code">{selectedLog.request_id}</div>
</div>
)}
</div>
)}
</Drawer>
</div>
);
};
export default LogViewer;
export const Component = LogViewer;
-224
View File
@@ -1,224 +0,0 @@
/**
* 系统监控页面
*/
import React, { useState, useEffect } from 'react';
import { Card, Row, Col, Progress, Badge, Table, Tag, Button, Space } from 'antd';
import {
CheckCircleOutlined,
CloseCircleOutlined,
SyncOutlined,
ReloadOutlined,
DatabaseOutlined,
ApiOutlined,
CloudServerOutlined,
ThunderboltOutlined,
} from '@ant-design/icons';
import './Admin.css';
const SystemMonitor: React.FC = () => {
const [refreshTime, setRefreshTime] = useState(new Date());
// 模拟实时数据
const [systemMetrics, setSystemMetrics] = useState({
cpu: 35,
memory: 62,
disk: 48,
network: 28,
});
// 服务健康状态
const services = [
{ name: 'API 服务', status: 'healthy', uptime: '99.98%', responseTime: '45ms' },
{ name: 'PostgreSQL', status: 'healthy', uptime: '99.99%', responseTime: '8ms' },
{ name: 'Redis', status: 'healthy', uptime: '99.97%', responseTime: '2ms' },
{ name: 'Celery Worker', status: 'healthy', uptime: '99.95%', responseTime: '-' },
{ name: 'Object Storage', status: 'warning', uptime: '99.85%', responseTime: '120ms' },
];
// API 请求统计
const apiStats = [
{ endpoint: 'POST /api/v1/auth/login', requests: 15420, avgTime: '48ms', errors: 12 },
{ endpoint: 'GET /api/v1/projects', requests: 89340, avgTime: '32ms', errors: 5 },
{ endpoint: 'POST /api/v1/projects', requests: 6780, avgTime: '156ms', errors: 8 },
{ endpoint: 'GET /api/v1/assets', requests: 124560, avgTime: '68ms', errors: 23 },
{ endpoint: 'POST /api/v1/subscriptions/subscribe', requests: 234, avgTime: '890ms', errors: 2 },
];
// 错误日志
const recentErrors = [
{ time: '2024-06-17 16:45:32', level: 'ERROR', service: 'API', message: 'Database connection timeout' },
{ time: '2024-06-17 16:42:15', level: 'WARN', service: 'Object Storage', message: 'Slow response detected (>1s)' },
{ time: '2024-06-17 16:38:41', level: 'ERROR', service: 'Celery', message: 'Task retry limit exceeded' },
];
const serviceColumns = [
{
title: '服务名称',
dataIndex: 'name',
key: 'name',
render: (text: string) => <><CloudServerOutlined /> {text}</>,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const config: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
healthy: { color: 'success', icon: <CheckCircleOutlined />, text: '正常' },
warning: { color: 'warning', icon: <SyncOutlined spin />, text: '警告' },
error: { color: 'error', icon: <CloseCircleOutlined />, text: '故障' },
};
const { color, text } = config[status] || config.healthy;
return <Badge status={color as any} text={text} />;
},
},
{ title: '可用性', dataIndex: 'uptime', key: 'uptime' },
{ title: '响应时间', dataIndex: 'responseTime', key: 'responseTime' },
];
const apiColumns = [
{ title: 'API 端点', dataIndex: 'endpoint', key: 'endpoint' },
{ title: '请求数', dataIndex: 'requests', key: 'requests', sorter: (a: any, b: any) => a.requests - b.requests },
{ title: '平均响应时间', dataIndex: 'avgTime', key: 'avgTime' },
{
title: '错误数',
dataIndex: 'errors',
key: 'errors',
render: (errors: number) => (
<Tag className={errors > 10 ? 'xx-tag error' : errors > 5 ? 'xx-tag warning' : 'xx-tag success'}>{errors}</Tag>
),
},
];
const errorColumns = [
{ title: '时间', dataIndex: 'time', key: 'time', width: 180 },
{
title: '级别',
dataIndex: 'level',
key: 'level',
width: 100,
render: (level: string) => (
<Tag className={level === 'ERROR' ? 'xx-tag error' : 'xx-tag warning'}>{level}</Tag>
),
},
{ title: '服务', dataIndex: 'service', key: 'service', width: 120 },
{ title: '错误信息', dataIndex: 'message', key: 'message' },
];
const refreshMetrics = () => {
setSystemMetrics({
cpu: Math.floor(Math.random() * 30) + 30,
memory: Math.floor(Math.random() * 20) + 55,
disk: Math.floor(Math.random() * 10) + 45,
network: Math.floor(Math.random() * 40) + 20,
});
setRefreshTime(new Date());
};
// 模拟实时更新
useEffect(() => {
const interval = setInterval(refreshMetrics, 5000);
return () => clearInterval(interval);
}, []);
const getProgressColor = (value: number) => {
if (value > 80) return '#ef4444';
if (value > 60) return '#f59e0b';
return '#10b981';
};
const getProgressClass = (value: number) => {
if (value > 80) return 'xx-progress-warning';
if (value > 60) return 'xx-progress-warning';
return 'xx-progress-success';
};
const resourceMetrics = [
{ key: 'cpu', label: 'CPU 使用率', value: systemMetrics.cpu, icon: <ThunderboltOutlined />, bg: 'linear-gradient(135deg, #6366f1, #4f46e5)' },
{ key: 'memory', label: '内存使用率', value: systemMetrics.memory, icon: <DatabaseOutlined />, bg: 'linear-gradient(135deg, #34d399, #10b981)' },
{ key: 'disk', label: '磁盘使用率', value: systemMetrics.disk, icon: <CloudServerOutlined />, bg: 'linear-gradient(135deg, #fbbf24, #f59e0b)' },
{ key: 'network', label: '网络使用率', value: systemMetrics.network, icon: <ApiOutlined />, bg: 'linear-gradient(135deg, #a78bfa, #8b5cf6)' },
];
return (
<div className="system-monitor-page">
<div className="xx-page-head">
<div className="xx-page-head-content">
<h2></h2>
<p> API </p>
</div>
<div className="xx-page-head-actions">
<Space>
<span className="xx-refresh-time">: {refreshTime.toLocaleTimeString()}</span>
<Button icon={<ReloadOutlined />} onClick={refreshMetrics} className="xx-primary-btn">
</Button>
</Space>
</div>
</div>
{/* 系统资源监控 */}
<Row gutter={24} className="xx-grid-4">
{resourceMetrics.map((metric) => (
<Col span={6} key={metric.key}>
<div className="xx-resource-card">
<div
className="xx-resource-card-icon"
style={{ background: metric.bg, color: 'white' }}
>
{metric.icon}
</div>
<div className="xx-resource-card-label">{metric.label}</div>
<Progress
type="circle"
percent={metric.value}
strokeColor={getProgressColor(metric.value)}
className={getProgressClass(metric.value)}
/>
</div>
</Col>
))}
</Row>
{/* 服务健康状态 */}
<div className="xx-table-wrapper" style={{ marginBottom: 24 }}>
<Card title="服务健康状态" className="xx-card">
<Table
columns={serviceColumns}
dataSource={services}
rowKey="name"
pagination={false}
/>
</Card>
</div>
{/* API 请求统计 */}
<div className="xx-table-wrapper" style={{ marginBottom: 24 }}>
<Card title="API 请求统计 (最近 1 小时)" className="xx-card">
<Table
columns={apiColumns}
dataSource={apiStats}
rowKey="endpoint"
pagination={false}
/>
</Card>
</div>
{/* 最近错误 */}
<div className="xx-table-wrapper">
<Card title="最近错误日志" className="xx-card">
<Table
columns={errorColumns}
dataSource={recentErrors}
rowKey="time"
pagination={false}
/>
</Card>
</div>
</div>
);
};
export default SystemMonitor;
export const Component = SystemMonitor;
-117
View File
@@ -1,117 +0,0 @@
/**
* Admin 用户管理页面
*/
import React from 'react';
import { Table, Button, Space, Tag, Input, Card } from 'antd';
import { SearchOutlined, LockOutlined, UnlockOutlined } from '@ant-design/icons';
import './Admin.css';
const { Search } = Input;
const UserManagement: React.FC = () => {
// 模拟数据
const users = [
{
id: '1',
username: 'user1',
email: 'user1@example.com',
is_email_verified: true,
status: 'active',
created_at: '2024-01-15',
},
{
id: '2',
username: 'user2',
email: 'user2@example.com',
is_email_verified: false,
status: 'active',
created_at: '2024-02-20',
},
];
const columns = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
},
{
title: '邮箱验证',
dataIndex: 'is_email_verified',
key: 'is_email_verified',
render: (verified: boolean) => (
<Tag className={verified ? 'xx-tag success' : 'xx-tag debug'}>
{verified ? '已验证' : '未验证'}
</Tag>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag className={status === 'active' ? 'xx-tag success' : 'xx-tag error'}>
{status === 'active' ? '正常' : '已封禁'}
</Tag>
),
},
{
title: '注册时间',
dataIndex: 'created_at',
key: 'created_at',
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Space>
<Button type="link" size="small" disabled>
</Button>
<Button
type="link"
size="small"
danger={record.status === 'active'}
icon={record.status === 'active' ? <LockOutlined /> : <UnlockOutlined />}
disabled
>
{record.status === 'active' ? '封禁暂未开放' : '解封暂未开放'}
</Button>
</Space>
),
},
];
return (
<div className="user-management-page">
<div className="xx-page-head-simple">
<h2></h2>
<p></p>
</div>
<div className="xx-table-wrapper">
<Card title="用户列表" className="xx-card">
<div style={{ marginBottom: 16 }}>
<Search
placeholder="搜索用户名或邮箱"
allowClear
enterButton={<SearchOutlined />}
className="xx-search-input"
style={{ width: 320 }}
/>
</div>
<Table columns={columns} dataSource={users} rowKey="id" />
</Card>
</div>
</div>
);
};
export default UserManagement;
export const Component = UserManagement;
@@ -1,111 +0,0 @@
/**
* 账号安全设置页面
*/
import React from 'react';
import { Form, Input, Button, Alert } from 'antd';
import { LockOutlined, MailOutlined } from '@ant-design/icons';
import './ProfileSettings.css';
const AccountSecurity: React.FC = () => {
const [passwordForm] = Form.useForm();
const [emailForm] = Form.useForm();
const onPasswordSubmit = () => undefined;
const onEmailSubmit = () => undefined;
return (
<div className="xx-settings-page">
<div className="xx-settings-card">
<h3></h3>
<Alert
type="info"
showIcon
style={{ marginBottom: 20 }}
message="账号安全设置暂未开放"
description="当前后端尚未提供登录态修改密码/邮箱接口,避免假成功,入口暂时禁用。"
/>
<Form form={passwordForm} layout="vertical" onFinish={onPasswordSubmit}>
<Form.Item
name="currentPassword"
label="当前密码"
rules={[{ required: true, message: '请输入当前密码' }]}
>
<Input.Password prefix={<LockOutlined />} />
</Form.Item>
<Form.Item
name="newPassword"
label="新密码"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 8, message: '密码至少 8 个字符' },
]}
>
<Input.Password prefix={<LockOutlined />} />
</Form.Item>
<Form.Item
name="confirmPassword"
label="确认新密码"
dependencies={['newPassword']}
rules={[
{ required: true, message: '请确认新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input.Password prefix={<LockOutlined />} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled>
</Button>
</Form.Item>
</Form>
</div>
<div className="xx-settings-card">
<h3></h3>
<Form form={emailForm} layout="vertical" onFinish={onEmailSubmit}>
<Form.Item
name="newEmail"
label="新邮箱地址"
rules={[
{ required: true, message: '请输入新邮箱' },
{ type: 'email', message: '请输入有效的邮箱地址' },
]}
>
<Input prefix={<MailOutlined />} />
</Form.Item>
<Form.Item
name="password"
label="确认密码"
rules={[{ required: true, message: '请输入密码以确认修改' }]}
>
<Input.Password prefix={<LockOutlined />} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled>
</Button>
</Form.Item>
</Form>
</div>
</div>
);
};
export default AccountSecurity;
export const Component = AccountSecurity;
@@ -1,96 +0,0 @@
/**
* 通知设置页面
*/
import React from 'react';
import { Form, Switch, Button, Divider, Alert } from 'antd';
import './ProfileSettings.css';
const NotificationSettings: React.FC = () => {
const [form] = Form.useForm();
const onFinish = () => undefined;
return (
<div className="xx-settings-page">
<div className="xx-settings-card">
<Alert
type="info"
showIcon
style={{ marginBottom: 20 }}
message="通知偏好暂未开放"
description="当前后端尚未提供通知偏好保存接口,避免假成功,保存入口暂时禁用。"
/>
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{
emailNotifications: true,
inviteNotifications: true,
projectNotifications: false,
marketingEmails: false,
}}
>
<h3 className="xx-section-title"></h3>
<Form.Item
name="emailNotifications"
label="启用邮件通知"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Divider />
<h3 className="xx-section-title"></h3>
<Form.Item
name="inviteNotifications"
label="工作空间邀请通知"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name="projectNotifications"
label="项目更新通知"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name="subscriptionNotifications"
label="订阅和账单通知"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Divider />
<h3 className="xx-section-title"></h3>
<Form.Item
name="marketingEmails"
label="接收产品更新和优惠信息"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled>
</Button>
</Form.Item>
</Form>
</div>
</div>
);
};
export default NotificationSettings;
export const Component = NotificationSettings;
@@ -1,21 +0,0 @@
import React from 'react';
import { Result } from 'antd';
import './ProfileSettings.css';
const SessionManagement: React.FC = () => {
return (
<div className="xx-settings-page">
<div className="xx-result-page">
<Result
status="info"
title="会话管理暂未开放"
subTitle="当前版本未接入会话列表、设备管理和远程登出后端服务,因此不展示模拟设备,也不会伪造登出操作。"
/>
</div>
</div>
);
};
export default SessionManagement;
export const Component = SessionManagement;
@@ -1,196 +0,0 @@
/**
* 工作空间详情页面
*/
import React, { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Tabs, Button, Descriptions, Card, List, Modal, Form, Input, message, Alert } from 'antd';
import { PlusOutlined, TeamOutlined } from '@ant-design/icons';
import { useWorkspace } from '@/hooks/useWorkspace';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { createProject, getProjects } from '@/api/projects';
import MemberList from '@/components/business/MemberList';
import InviteMemberModal from '@/components/business/InviteMemberModal';
import PermissionMatrix from '@/components/business/PermissionMatrix';
const WorkspaceDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { data: workspace, isLoading } = useWorkspace(id!);
const queryClient = useQueryClient();
const [inviteModalOpen, setInviteModalOpen] = useState(false);
const [createProjectOpen, setCreateProjectOpen] = useState(false);
const [projectForm] = Form.useForm<{ name: string; description?: string }>();
const projectsQuery = useQuery({
queryKey: ['projects', id],
queryFn: () => getProjects(),
enabled: !!id,
});
const createProjectMutation = useMutation({
mutationFn: createProject,
onSuccess: (project) => {
message.success('项目创建成功');
setCreateProjectOpen(false);
projectForm.resetFields();
queryClient.invalidateQueries({ queryKey: ['projects', id] });
navigate(`/projects/${project.id}/assets`, { state: { workspaceId: id } });
},
onError: (error: any) => {
message.error(error.response?.data?.detail || '项目创建失败,请稍后重试');
},
});
const handleCreateProject = async () => {
const values = await projectForm.validateFields();
createProjectMutation.mutate({
name: values.name,
description: values.description || '',
});
};
if (isLoading) return <div>...</div>;
if (!workspace) return <div></div>;
const items = [
{
key: 'overview',
label: '概览',
children: (
<div>
<Card
title="工作空间信息"
style={{ marginBottom: 16 }}
extra={
<Button type="primary" onClick={() => setCreateProjectOpen(true)}>
</Button>
}
>
<Descriptions column={2}>
<Descriptions.Item label="名称">{workspace.name}</Descriptions.Item>
<Descriptions.Item label="订阅计划">{workspace.subscription_plan}</Descriptions.Item>
<Descriptions.Item label="状态">{workspace.subscription_status}</Descriptions.Item>
<Descriptions.Item label="创建时间">
{workspace.created_at
? new Date(workspace.created_at).toLocaleDateString()
: '暂未返回'}
</Descriptions.Item>
</Descriptions>
</Card>
<Card title="项目" style={{ marginBottom: 16 }}>
{projectsQuery.isError && (
<Alert type="error" showIcon message="项目列表加载失败" style={{ marginBottom: 16 }} />
)}
<List
loading={projectsQuery.isLoading}
dataSource={projectsQuery.data || []}
locale={{ emptyText: '还没有项目,请先创建项目再上传素材' }}
renderItem={(project) => (
<List.Item
actions={[
<Button key="assets" type="link" onClick={() => navigate(`/projects/${project.id}/assets`, { state: { workspaceId: id } })}>
</Button>,
<Button key="titles" type="link" onClick={() => navigate(`/projects/${project.id}/titles`, { state: { workspaceId: id } })}>
</Button>,
<Button key="generation" type="link" onClick={() => navigate(`/projects/${project.id}/generation`, { state: { workspaceId: id } })}>
</Button>,
<Button key="tasks" type="link" onClick={() => navigate(`/projects/${project.id}/tasks`, { state: { workspaceId: id } })}>
</Button>,
<Button key="results" type="link" onClick={() => navigate(`/projects/${project.id}/results`, { state: { workspaceId: id } })}>
</Button>,
]}
>
<List.Item.Meta title={project.name} description={project.description || '暂无描述'} />
</List.Item>
)}
/>
</Card>
<Alert
type="info"
showIcon
message="配额与订阅功能暂未开放"
description="当前版本未接入订阅和配额后端服务,因此不展示模拟配额数据,也不会调用不存在的配额接口。"
/>
</div>
),
},
{
key: 'members',
label: (
<>
<TeamOutlined />
</>
),
children: (
<div>
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setInviteModalOpen(true)}
>
</Button>
</div>
<MemberList workspaceId={id!} />
<div style={{ marginTop: 24 }}>
<PermissionMatrix />
</div>
</div>
),
},
{
key: 'settings',
label: '设置',
children: <div></div>,
},
];
return (
<div>
<h1>{workspace.name}</h1>
<Tabs items={items} />
<Modal
title="创建项目"
open={createProjectOpen}
okText="创建"
cancelText="取消"
confirmLoading={createProjectMutation.isPending}
onOk={handleCreateProject}
onCancel={() => setCreateProjectOpen(false)}
>
<Form form={projectForm} layout="vertical" preserve={false}>
<Form.Item
name="name"
label="项目名称"
rules={[
{ required: true, message: '请输入项目名称' },
{ max: 100, message: '项目名称最多 100 个字符' },
]}
>
<Input placeholder="例如:618 宣传片" />
</Form.Item>
<Form.Item name="description" label="项目描述">
<Input.TextArea placeholder="可选" rows={3} maxLength={500} />
</Form.Item>
</Form>
</Modal>
<InviteMemberModal
workspaceId={id!}
open={inviteModalOpen}
onClose={() => setInviteModalOpen(false)}
/>
</div>
);
};
export default WorkspaceDetail;
export const Component = WorkspaceDetail;
-32
View File
@@ -1,32 +0,0 @@
/**
* UI 状态管理
* 管理全局 UI 状态(侧边栏、加载状态等)
*/
import { create } from 'zustand';
interface UIState {
sidebarCollapsed: boolean;
loading: boolean;
// Actions
toggleSidebar: () => void;
setSidebarCollapsed: (collapsed: boolean) => void;
setLoading: (loading: boolean) => void;
}
export const useUIStore = create<UIState>((set) => ({
sidebarCollapsed: false,
loading: false,
toggleSidebar: () => {
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed }));
},
setSidebarCollapsed: (collapsed) => {
set({ sidebarCollapsed: collapsed });
},
setLoading: (loading) => {
set({ loading });
},
}));
+2 -2
View File
@@ -17,8 +17,8 @@
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path alias */
@@ -15,13 +15,8 @@ from typing import Optional
logger = logging.getLogger(__name__)
class EditingMode(StrEnum):
"""剪辑模式枚举"""
ONE_TAKE = "one_take" # 一镜到底:顺序拼接+转场
PIP = "pip" # 画中画:主视频+叠加
VOICE_OVER = "voice_over" # 口播:背景画面+配音
VOICE_PIP = "voice_pip" # 口播+画中画
# 从 domain 层导入 EditingMode,避免重复定义
from packages.domain.edit_plan import EditingMode
class PIPPosition(StrEnum):
+2 -10
View File
@@ -138,17 +138,9 @@ def _download_library_assets(
# 导入模型和会话
try:
from packages.adapters.sqlalchemy_impl.models import AssetModel
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from worker_app.db import SessionLocal
db_url = os.getenv("DATABASE_URL")
if not db_url:
logger.warning("DATABASE_URL not set, cannot fetch assets")
return []
engine = create_engine(db_url)
Session = sessionmaker(bind=engine)
session = Session()
session = SessionLocal()
try:
# 查询素材库中的视频素材
+1 -1
View File
@@ -116,7 +116,7 @@ services:
resources:
limits:
cpus: '2.0'
memory: 4G
memory: 2G
reservations:
cpus: '0.5'
memory: 1G
+3 -3
View File
@@ -25,9 +25,9 @@ class UserModel(Base):
subscription_status = Column(String(20), nullable=False, default="active")
subscription_expires_at = Column(DateTime, nullable=True)
# 配额限制 (移到 User 级别)
max_projects = Column(Float, nullable=False, default=3)
max_storage_gb = Column(Float, nullable=False, default=10)
used_storage_gb = Column(Float, nullable=False, default=0.0)
max_projects = Column(Integer, nullable=False, default=3)
max_storage_gb = Column(Integer, nullable=False, default=10)
used_storage_gb = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))