fix: connect API to tracker.db via SQLite repository
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Admin Dashboard 仪表盘
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Row, Col, Statistic, Table } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
CrownOutlined,
|
||||
DollarOutlined,
|
||||
RiseOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
// 模拟数据
|
||||
const stats = [
|
||||
{ title: '总用户数', value: 1234, icon: <UserOutlined />, color: '#1890ff' },
|
||||
{ title: '付费用户', value: 156, icon: <CrownOutlined />, color: '#52c41a' },
|
||||
{ title: '月收入', value: 15444, prefix: '¥', icon: <DollarOutlined />, color: '#faad14' },
|
||||
{ title: '活跃用户', value: 892, icon: <RiseOutlined />, color: '#722ed1' },
|
||||
];
|
||||
|
||||
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' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
{stats.map((stat, index) => (
|
||||
<Col span={6} key={index}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={stat.title}
|
||||
value={stat.value}
|
||||
prefix={stat.prefix}
|
||||
valueStyle={{ color: stat.color }}
|
||||
suffix={stat.icon}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<Card title="最近注册用户">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={recentUsers}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
export const Component = Dashboard;
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Admin 用户管理页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Table, Button, Space, Tag, Input, Card } from 'antd';
|
||||
import { SearchOutlined, LockOutlined, UnlockOutlined } from '@ant-design/icons';
|
||||
|
||||
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 color={verified ? 'success' : 'default'}>
|
||||
{verified ? '已验证' : '未验证'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={status === 'active' ? 'success' : 'error'}>
|
||||
{status === 'active' ? '正常' : '已封禁'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button type="link" size="small">
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger={record.status === 'active'}
|
||||
icon={record.status === 'active' ? <LockOutlined /> : <UnlockOutlined />}
|
||||
>
|
||||
{record.status === 'active' ? '封禁' : '解封'}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>用户管理</h1>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Search
|
||||
placeholder="搜索用户名或邮箱"
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={users} rowKey="id" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManagement;
|
||||
|
||||
export const Component = UserManagement;
|
||||
@@ -0,0 +1,18 @@
|
||||
.forgot-password-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.forgot-password-card {
|
||||
width: 400px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.forgot-password-card .ant-card-head-title {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 忘记密码页面
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Input, Button, Card, message, Result } from 'antd';
|
||||
import { MailOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { requestPasswordReset } from '@/api/auth';
|
||||
import './ForgotPassword.css';
|
||||
|
||||
const ForgotPassword: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [emailSent, setEmailSent] = useState(false);
|
||||
|
||||
const resetMutation = useMutation({
|
||||
mutationFn: (email: string) => requestPasswordReset(email),
|
||||
onSuccess: () => {
|
||||
setEmailSent(true);
|
||||
message.success('重置邮件已发送!');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
message.error(error.response?.data?.message || '发送失败,请重试');
|
||||
},
|
||||
});
|
||||
|
||||
const onFinish = (values: { email: string }) => {
|
||||
resetMutation.mutate(values.email);
|
||||
};
|
||||
|
||||
if (emailSent) {
|
||||
return (
|
||||
<div className="forgot-password-container">
|
||||
<Card className="forgot-password-card">
|
||||
<Result
|
||||
status="success"
|
||||
title="重置邮件已发送"
|
||||
subTitle="请检查您的邮箱,点击邮件中的链接重置密码。"
|
||||
extra={[
|
||||
<Link to="/login" key="login">
|
||||
<Button type="primary">返回登录</Button>
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="forgot-password-container">
|
||||
<Card className="forgot-password-card" title="重置密码">
|
||||
<p style={{ marginBottom: '24px', color: '#666' }}>
|
||||
请输入您的邮箱地址,我们将发送重置密码的链接到您的邮箱。
|
||||
</p>
|
||||
|
||||
<Form form={form} name="forgot-password" onFinish={onFinish} size="large">
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<MailOutlined />} placeholder="邮箱" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={resetMutation.isPending}
|
||||
block
|
||||
>
|
||||
发送重置邮件
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Link to="/login">返回登录</Link>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -0,0 +1,18 @@
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.login-card .ant-card-head-title {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 登录页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Form, Input, Button, Card, message, Checkbox } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLogin } from '@/hooks/useAuth';
|
||||
import './Login.css';
|
||||
|
||||
interface LoginFormValues {
|
||||
email: string;
|
||||
password: string;
|
||||
remember: boolean;
|
||||
}
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const loginMutation = useLogin();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = async (values: LoginFormValues) => {
|
||||
try {
|
||||
await loginMutation.mutateAsync({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
});
|
||||
message.success('登录成功!');
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.message || '登录失败,请检查邮箱和密码');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
<Card className="login-card" title="登录小虾 SaaS">
|
||||
<Form
|
||||
form={form}
|
||||
name="login"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="邮箱"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Form.Item name="remember" valuePropName="checked" noStyle>
|
||||
<Checkbox>记住我</Checkbox>
|
||||
</Form.Item>
|
||||
<Link to="/forgot-password" style={{ float: 'right' }}>
|
||||
忘记密码?
|
||||
</Link>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loginMutation.isPending}
|
||||
block
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
还没有账号? <Link to="/register">立即注册</Link>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -0,0 +1,18 @@
|
||||
.register-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.register-card {
|
||||
width: 400px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.register-card .ant-card-head-title {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 注册页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Form, Input, Button, Card, message } from 'antd';
|
||||
import { UserOutlined, LockOutlined, MailOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useRegister } from '@/hooks/useAuth';
|
||||
import './Register.css';
|
||||
|
||||
interface RegisterFormValues {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const Register: React.FC = () => {
|
||||
const registerMutation = useRegister();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = async (values: RegisterFormValues) => {
|
||||
try {
|
||||
await registerMutation.mutateAsync({
|
||||
email: values.email,
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
});
|
||||
message.success('注册成功!请查收验证邮件。');
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.message || '注册失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="register-container">
|
||||
<Card className="register-card" title="注册小虾 SaaS">
|
||||
<Form
|
||||
form={form}
|
||||
name="register"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined />}
|
||||
placeholder="邮箱"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少 3 个字符' },
|
||||
{ max: 20, message: '用户名最多 20 个字符' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: '用户名只能包含字母、数字和下划线',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="用户名"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 8, message: '密码至少 8 个字符' },
|
||||
{
|
||||
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
|
||||
message: '密码必须包含大小写字母和数字',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
dependencies={['password']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="确认密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={registerMutation.isPending}
|
||||
block
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
已有账号? <Link to="/login">立即登录</Link>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
@@ -0,0 +1,18 @@
|
||||
.reset-password-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.reset-password-card {
|
||||
width: 400px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.reset-password-card .ant-card-head-title {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 重置密码页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Form, Input, Button, Card, message, Result } from 'antd';
|
||||
import { LockOutlined } from '@ant-design/icons';
|
||||
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { resetPassword } from '@/api/auth';
|
||||
import './ResetPassword.css';
|
||||
|
||||
const ResetPassword: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const token = searchParams.get('token');
|
||||
|
||||
const resetMutation = useMutation({
|
||||
mutationFn: (password: string) => resetPassword(token!, password),
|
||||
onSuccess: () => {
|
||||
message.success('密码重置成功!');
|
||||
setTimeout(() => navigate('/login'), 2000);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
message.error(error.response?.data?.message || '重置失败,请重试');
|
||||
},
|
||||
});
|
||||
|
||||
const onFinish = (values: { password: string }) => {
|
||||
resetMutation.mutate(values.password);
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="reset-password-container">
|
||||
<Card className="reset-password-card">
|
||||
<Result
|
||||
status="error"
|
||||
title="无效的重置链接"
|
||||
subTitle="该链接无效或已过期,请重新申请密码重置。"
|
||||
extra={[
|
||||
<Link to="/forgot-password" key="forgot">
|
||||
<Button type="primary">重新申请</Button>
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (resetMutation.isSuccess) {
|
||||
return (
|
||||
<div className="reset-password-container">
|
||||
<Card className="reset-password-card">
|
||||
<Result
|
||||
status="success"
|
||||
title="密码重置成功"
|
||||
subTitle="您的密码已成功重置,即将跳转到登录页面..."
|
||||
extra={[
|
||||
<Link to="/login" key="login">
|
||||
<Button type="primary">立即登录</Button>
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="reset-password-container">
|
||||
<Card className="reset-password-card" title="设置新密码">
|
||||
<Form form={form} name="reset-password" onFinish={onFinish} size="large">
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 8, message: '密码至少 8 个字符' },
|
||||
{
|
||||
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
|
||||
message: '密码必须包含大小写字母和数字',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="新密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
dependencies={['password']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="确认新密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={resetMutation.isPending}
|
||||
block
|
||||
>
|
||||
重置密码
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Link to="/login">返回登录</Link>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPassword;
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 账号安全设置页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Form, Input, Button, message, Divider } from 'antd';
|
||||
import { LockOutlined, MailOutlined } from '@ant-design/icons';
|
||||
|
||||
const AccountSecurity: React.FC = () => {
|
||||
const [passwordForm] = Form.useForm();
|
||||
const [emailForm] = Form.useForm();
|
||||
|
||||
const onPasswordSubmit = (values: any) => {
|
||||
console.log('修改密码:', values);
|
||||
message.success('密码修改成功');
|
||||
passwordForm.resetFields();
|
||||
};
|
||||
|
||||
const onEmailSubmit = (values: any) => {
|
||||
console.log('修改邮箱:', values);
|
||||
message.success('验证邮件已发送到新邮箱');
|
||||
emailForm.resetFields();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>账号安全</h1>
|
||||
|
||||
<Card title="修改密码" style={{ marginBottom: 24 }}>
|
||||
<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">
|
||||
修改密码
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card title="修改邮箱">
|
||||
<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">
|
||||
修改邮箱
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSecurity;
|
||||
|
||||
export const Component = AccountSecurity;
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 通知设置页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Form, Switch, Button, message, Divider } from 'antd';
|
||||
|
||||
const NotificationSettings: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
console.log('保存通知设置:', values);
|
||||
message.success('通知设置已保存');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>通知设置</h1>
|
||||
<Card>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
emailNotifications: true,
|
||||
inviteNotifications: true,
|
||||
projectNotifications: false,
|
||||
marketingEmails: false,
|
||||
}}
|
||||
>
|
||||
<h3>邮件通知</h3>
|
||||
<Form.Item
|
||||
name="emailNotifications"
|
||||
label="启用邮件通知"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
|
||||
<h3>通知类型</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>营销</h3>
|
||||
<Form.Item
|
||||
name="marketingEmails"
|
||||
label="接收产品更新和优惠信息"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存设置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationSettings;
|
||||
|
||||
export const Component = NotificationSettings;
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Session 管理页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Table, Button, Tag, Space, Popconfirm, message } from 'antd';
|
||||
import { LaptopOutlined, MobileOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
device: string;
|
||||
deviceType: 'desktop' | 'mobile';
|
||||
location: string;
|
||||
lastActive: string;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
const SessionManagement: React.FC = () => {
|
||||
// 模拟数据
|
||||
const sessions: Session[] = [
|
||||
{
|
||||
id: '1',
|
||||
device: 'Chrome on Windows',
|
||||
deviceType: 'desktop',
|
||||
location: '北京, 中国',
|
||||
lastActive: '当前会话',
|
||||
isCurrent: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
device: 'Safari on iPhone',
|
||||
deviceType: 'mobile',
|
||||
location: '上海, 中国',
|
||||
lastActive: '2 小时前',
|
||||
isCurrent: false,
|
||||
},
|
||||
];
|
||||
|
||||
const handleLogout = (sessionId: string) => {
|
||||
console.log('登出会话:', sessionId);
|
||||
message.success('会话已结束');
|
||||
};
|
||||
|
||||
const handleLogoutAll = () => {
|
||||
console.log('登出所有其他设备');
|
||||
message.success('所有其他设备已登出');
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '设备',
|
||||
dataIndex: 'device',
|
||||
key: 'device',
|
||||
render: (device: string, record: Session) => (
|
||||
<Space>
|
||||
{record.deviceType === 'desktop' ? <LaptopOutlined /> : <MobileOutlined />}
|
||||
<span>{device}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '位置',
|
||||
dataIndex: 'location',
|
||||
key: 'location',
|
||||
},
|
||||
{
|
||||
title: '最后活动',
|
||||
dataIndex: 'lastActive',
|
||||
key: 'lastActive',
|
||||
render: (text: string, record: Session) => (
|
||||
<>
|
||||
{text}
|
||||
{record.isCurrent && <Tag color="success" style={{ marginLeft: 8 }}>当前</Tag>}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, record: Session) => {
|
||||
if (record.isCurrent) {
|
||||
return <span style={{ color: '#999' }}>-</span>;
|
||||
}
|
||||
return (
|
||||
<Popconfirm
|
||||
title="确定要结束此会话吗?"
|
||||
onConfirm={() => handleLogout(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
结束会话
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>Session 管理</h1>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Popconfirm
|
||||
title="确定要登出所有其他设备吗?"
|
||||
onConfirm={handleLogoutAll}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button danger>登出所有其他设备</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={sessions} rowKey="id" pagination={false} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionManagement;
|
||||
|
||||
export const Component = SessionManagement;
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 个人设置页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Form, Input, Button, message } from 'antd';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
console.log('保存设置:', values);
|
||||
message.success('设置已保存');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<Card title="个人信息">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
username: user?.username,
|
||||
email: user?.email,
|
||||
display_name: user?.display_name,
|
||||
}}
|
||||
>
|
||||
<Form.Item label="用户名" name="username">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="邮箱" name="email">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="显示名称" name="display_name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
||||
export const Component = Settings;
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 账单管理页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Table, Button, Tag, Space } from 'antd';
|
||||
import { DownloadOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
|
||||
interface Invoice {
|
||||
id: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
status: 'paid' | 'pending' | 'overdue';
|
||||
plan: string;
|
||||
}
|
||||
|
||||
const Billing: React.FC = () => {
|
||||
// 模拟数据
|
||||
const invoices: Invoice[] = [
|
||||
{
|
||||
id: 'INV-2024-001',
|
||||
date: '2024-06-01',
|
||||
amount: 99,
|
||||
status: 'paid',
|
||||
plan: 'Pro',
|
||||
},
|
||||
{
|
||||
id: 'INV-2024-002',
|
||||
date: '2024-05-01',
|
||||
amount: 99,
|
||||
status: 'paid',
|
||||
plan: 'Pro',
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const config: Record<string, { color: string; text: string }> = {
|
||||
paid: { color: 'success', text: '已支付' },
|
||||
pending: { color: 'processing', text: '待支付' },
|
||||
overdue: { color: 'error', text: '逾期' },
|
||||
};
|
||||
const item = config[status] || config.pending;
|
||||
return <Tag color={item.color}>{item.text}</Tag>;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '账单编号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: '日期',
|
||||
dataIndex: 'date',
|
||||
key: 'date',
|
||||
},
|
||||
{
|
||||
title: '套餐',
|
||||
dataIndex: 'plan',
|
||||
key: 'plan',
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
render: (amount: number) => `¥${amount}`,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, record: Invoice) => (
|
||||
<Space>
|
||||
<Button type="link" icon={<DownloadOutlined />}>
|
||||
下载
|
||||
</Button>
|
||||
<Button type="link" icon={<FileTextOutlined />}>
|
||||
发票
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>账单管理</h1>
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={invoices}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Billing;
|
||||
|
||||
export const Component = Billing;
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 订阅计划页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Row, Col, Button, List } from 'antd';
|
||||
import { CheckOutlined } from '@ant-design/icons';
|
||||
|
||||
const Plans: React.FC = () => {
|
||||
const plans = [
|
||||
{
|
||||
name: 'Free',
|
||||
price: '¥0',
|
||||
period: '/月',
|
||||
features: ['3 个项目', '10GB 存储', '3 个成员', '邮件支持'],
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
price: '¥99',
|
||||
period: '/月',
|
||||
features: ['无限项目', '100GB 存储', '20 个成员', '优先支持'],
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
price: '¥999',
|
||||
period: '/月',
|
||||
features: ['无限项目', '1TB 存储', '无限成员', '专属支持', '定制开发'],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1 style={{ textAlign: 'center', marginBottom: '48px' }}>选择适合你的套餐</h1>
|
||||
<Row gutter={24} justify="center">
|
||||
{plans.map((plan) => (
|
||||
<Col key={plan.name} xs={24} sm={12} md={8}>
|
||||
<Card
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
border: plan.recommended ? '2px solid #1890ff' : undefined,
|
||||
}}
|
||||
>
|
||||
{plan.recommended && (
|
||||
<div style={{ color: '#1890ff', marginBottom: '16px' }}>推荐</div>
|
||||
)}
|
||||
<h2>{plan.name}</h2>
|
||||
<div style={{ fontSize: '32px', fontWeight: 'bold', margin: '24px 0' }}>
|
||||
{plan.price}
|
||||
<span style={{ fontSize: '16px', fontWeight: 'normal' }}>
|
||||
{plan.period}
|
||||
</span>
|
||||
</div>
|
||||
<List
|
||||
dataSource={plan.features}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ border: 'none', padding: '8px 0' }}>
|
||||
<CheckOutlined style={{ color: '#52c41a', marginRight: '8px' }} />
|
||||
{item}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type={plan.recommended ? 'primary' : 'default'}
|
||||
size="large"
|
||||
block
|
||||
style={{ marginTop: '24px' }}
|
||||
>
|
||||
{plan.name === 'Free' ? '当前计划' : '升级'}
|
||||
</Button>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Plans;
|
||||
|
||||
export const Component = Plans;
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 订阅升级流程页面
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Card, Steps, Button, Result, Descriptions, message } from 'antd';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { upgradeSubscription } from '@/api/subscription';
|
||||
|
||||
const UpgradeSubscription: React.FC = () => {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [selectedPlan, setSelectedPlan] = useState<'pro' | 'enterprise'>('pro');
|
||||
|
||||
const upgradeMutation = useMutation({
|
||||
mutationFn: () => upgradeSubscription(workspaceId!, selectedPlan),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['workspace', workspaceId] });
|
||||
setCurrent(2);
|
||||
message.success('订阅升级成功!');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
message.error(error.response?.data?.message || '升级失败');
|
||||
},
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: '选择套餐',
|
||||
content: (
|
||||
<div>
|
||||
<Card
|
||||
style={{ marginBottom: 16, cursor: 'pointer' }}
|
||||
onClick={() => setSelectedPlan('pro')}
|
||||
className={selectedPlan === 'pro' ? 'selected-plan' : ''}
|
||||
>
|
||||
<h3>Pro 套餐</h3>
|
||||
<p>¥99/月</p>
|
||||
<ul>
|
||||
<li>无限项目</li>
|
||||
<li>100GB 存储</li>
|
||||
<li>20 个成员</li>
|
||||
</ul>
|
||||
</Card>
|
||||
<Card
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => setSelectedPlan('enterprise')}
|
||||
className={selectedPlan === 'enterprise' ? 'selected-plan' : ''}
|
||||
>
|
||||
<h3>Enterprise 套餐</h3>
|
||||
<p>¥999/月</p>
|
||||
<ul>
|
||||
<li>无限项目</li>
|
||||
<li>1TB 存储</li>
|
||||
<li>无限成员</li>
|
||||
<li>专属支持</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '确认订单',
|
||||
content: (
|
||||
<Card>
|
||||
<Descriptions column={1}>
|
||||
<Descriptions.Item label="套餐">
|
||||
{selectedPlan === 'pro' ? 'Pro' : 'Enterprise'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="价格">
|
||||
{selectedPlan === 'pro' ? '¥99' : '¥999'} / 月
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="支付方式">
|
||||
在线支付(待接入)
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '完成',
|
||||
content: (
|
||||
<Result
|
||||
status="success"
|
||||
title="升级成功!"
|
||||
subTitle="您的订阅已升级,新配额已生效。"
|
||||
extra={[
|
||||
<Button
|
||||
type="primary"
|
||||
key="workspace"
|
||||
onClick={() => navigate(`/workspaces/${workspaceId}`)}
|
||||
>
|
||||
返回工作空间
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const next = () => {
|
||||
if (current === 0) {
|
||||
setCurrent(current + 1);
|
||||
} else if (current === 1) {
|
||||
upgradeMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
const prev = () => {
|
||||
setCurrent(current - 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>升级订阅</h1>
|
||||
<Steps current={current} items={steps} style={{ marginBottom: 24 }} />
|
||||
<div>{steps[current].content}</div>
|
||||
<div style={{ marginTop: 24 }}>
|
||||
{current < steps.length - 1 && (
|
||||
<>
|
||||
{current > 0 && (
|
||||
<Button style={{ marginRight: 8 }} onClick={prev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={next}
|
||||
loading={upgradeMutation.isPending}
|
||||
>
|
||||
{current === 1 ? '确认升级' : '下一步'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpgradeSubscription;
|
||||
|
||||
export const Component = UpgradeSubscription;
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 工作空间详情页面
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Tabs, Button, Descriptions, Progress, Card, Row, Col } from 'antd';
|
||||
import { PlusOutlined, TeamOutlined, ProjectOutlined } from '@ant-design/icons';
|
||||
import { useWorkspace } from '@/hooks/useWorkspace';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getQuotaStatus } from '@/api/subscription';
|
||||
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 { data: workspace, isLoading } = useWorkspace(id!);
|
||||
const { data: quota } = useQuery({
|
||||
queryKey: ['quota', id],
|
||||
queryFn: () => getQuotaStatus(id!),
|
||||
enabled: !!id,
|
||||
});
|
||||
const [inviteModalOpen, setInviteModalOpen] = useState(false);
|
||||
|
||||
if (isLoading) return <div>加载中...</div>;
|
||||
if (!workspace) return <div>工作空间不存在</div>;
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
normal: '#52c41a',
|
||||
warning: '#faad14',
|
||||
critical: '#ff4d4f',
|
||||
exceeded: '#ff4d4f',
|
||||
};
|
||||
return colors[status] || colors.normal;
|
||||
};
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'overview',
|
||||
label: '概览',
|
||||
children: (
|
||||
<div>
|
||||
<Card title="工作空间信息" style={{ marginBottom: 16 }}>
|
||||
<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="创建时间">
|
||||
{new Date(workspace.created_at).toLocaleDateString()}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{quota && (
|
||||
<Card title="配额使用情况">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<ProjectOutlined /> 项目数量:{quota.projects.used} /{' '}
|
||||
{quota.projects.limit}
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(
|
||||
(quota.projects.used / quota.projects.limit) * 100
|
||||
)}
|
||||
strokeColor={getStatusColor(quota.projects.status)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
存储空间:{quota.storage.used_gb.toFixed(2)} GB /{' '}
|
||||
{quota.storage.limit_gb} GB
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(
|
||||
(quota.storage.used_gb / quota.storage.limit_gb) * 100
|
||||
)}
|
||||
strokeColor={getStatusColor(quota.storage.status)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
</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} />
|
||||
<InviteMemberModal
|
||||
workspaceId={id!}
|
||||
open={inviteModalOpen}
|
||||
onClose={() => setInviteModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceDetail;
|
||||
|
||||
export const Component = WorkspaceDetail;
|
||||
@@ -0,0 +1,37 @@
|
||||
.workspace-list {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.workspace-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.workspace-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.workspace-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin: 24px 0;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 工作空间列表页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Button, Row, Col, Tag, Space } from 'antd';
|
||||
import { PlusOutlined, TeamOutlined, CrownOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useWorkspaces } from '@/hooks/useWorkspace';
|
||||
import './WorkspaceList.css';
|
||||
|
||||
const WorkspaceList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data: workspaces, isLoading } = useWorkspaces();
|
||||
|
||||
const getPlanTag = (plan: string) => {
|
||||
const planConfig = {
|
||||
free: { color: 'default', text: 'Free' },
|
||||
pro: { color: 'blue', text: 'Pro' },
|
||||
enterprise: { color: 'gold', text: 'Enterprise' },
|
||||
};
|
||||
const config = planConfig[plan as keyof typeof planConfig] || planConfig.free;
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="workspace-list">
|
||||
<div className="workspace-list-header">
|
||||
<h1>我的工作空间</h1>
|
||||
<Button type="primary" icon={<PlusOutlined />} size="large">
|
||||
创建工作空间
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{workspaces?.map((workspace) => (
|
||||
<Col key={workspace.id} xs={24} sm={12} md={8} lg={6}>
|
||||
<Card
|
||||
hoverable
|
||||
onClick={() => navigate(`/workspaces/${workspace.id}`)}
|
||||
className="workspace-card"
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div className="workspace-name">{workspace.name}</div>
|
||||
<div>{getPlanTag(workspace.subscription_plan)}</div>
|
||||
<div style={{ color: '#666', fontSize: '14px' }}>
|
||||
<TeamOutlined /> 成员数量
|
||||
</div>
|
||||
<div style={{ color: '#999', fontSize: '12px' }}>
|
||||
创建于 {new Date(workspace.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{!isLoading && workspaces?.length === 0 && (
|
||||
<div className="empty-state">
|
||||
<CrownOutlined style={{ fontSize: '64px', color: '#ccc' }} />
|
||||
<p>还没有工作空间</p>
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
创建第一个工作空间
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceList;
|
||||
|
||||
export const Component = WorkspaceList;
|
||||
Reference in New Issue
Block a user