feat: V21 design system alignment for workspace pages and business components
- Add V21 styles to WorkspaceDetail page (page-head, cards, buttons) - Create ProjectTasks.css and update ProjectTasks.tsx with V21 UI - Create business.css with shared V21 styles (buttons, cards, tables, tags) - Update InviteMemberModal with V21 modal styles - Update MemberList with V21 table and tag styles - Update PermissionMatrix with V21 card and table styles - Update QuotaDisplay with V21 card and progress styles - Update App.tsx and App.css with V21 design system
This commit is contained in:
+134
-8
@@ -1,14 +1,140 @@
|
||||
.App {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
/* V21 App Root Styles */
|
||||
|
||||
.xx-app-root {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.xx-app-container {
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
/* Hero Section */
|
||||
.xx-hero-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
.xx-logo {
|
||||
font-size: 72px;
|
||||
margin-bottom: 24px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.xx-hero-section h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 850;
|
||||
color: #0f172a;
|
||||
margin: 0 0 12px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.xx-hero-section p {
|
||||
font-size: 18px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.xx-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
padding: 8px 16px;
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.xx-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #4f46e5;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.xx-card {
|
||||
background: rgba(255,255,255,0.94);
|
||||
border: 1px solid rgba(226,232,240,0.95);
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 24px 70px rgba(15,23,42,0.09);
|
||||
padding: 28px;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.xx-card:hover {
|
||||
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.xx-card h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin: 0 0 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Quick Links */
|
||||
.xx-link-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.xx-link-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 24px 16px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
text-decoration: none;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.xx-link-item:hover {
|
||||
background: #eef2ff;
|
||||
border-color: #4f46e5;
|
||||
color: #4f46e5;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(79,70,229,0.15);
|
||||
}
|
||||
|
||||
.xx-link-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
.xx-hero-section h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.xx-link-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
+33
-16
@@ -1,23 +1,40 @@
|
||||
import { useState } from 'react'
|
||||
import './App.css'
|
||||
/**
|
||||
* App Root Component - V21 UI
|
||||
* 注意:此组件已被路由系统接管,仅作为默认展示
|
||||
*/
|
||||
import { Link } from 'react-router-dom';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>小虾 SaaS</h1>
|
||||
<p>自动化视频剪辑平台</p>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<div className="xx-app-root">
|
||||
<div className="xx-app-container">
|
||||
<div className="xx-hero-section">
|
||||
<div className="xx-logo">🎬</div>
|
||||
<h1>小虾 SaaS</h1>
|
||||
<p>自动化视频剪辑平台</p>
|
||||
<div className="xx-status-badge">
|
||||
<span className="xx-status-dot"></span>
|
||||
前端开发中
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="xx-card xx-quick-links">
|
||||
<h3>快速链接</h3>
|
||||
<div className="xx-link-grid">
|
||||
<Link to="/workspaces" className="xx-link-item">
|
||||
<span className="xx-link-icon">📁</span>
|
||||
<span>工作空间</span>
|
||||
</Link>
|
||||
<Link to="/projects" className="xx-link-item">
|
||||
<span className="xx-link-icon">📋</span>
|
||||
<span>我的项目</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Phase 6: 前端开发中...
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* 邀请成员弹窗
|
||||
* 邀请成员弹窗 - V21 UI
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Modal, Form, Input, Select, message } from 'antd';
|
||||
import { useInviteMember } from '@/hooks/useWorkspace';
|
||||
import './business.css';
|
||||
|
||||
interface InviteMemberModalProps {
|
||||
workspaceId: string;
|
||||
@@ -45,6 +46,8 @@ const InviteMemberModal: React.FC<InviteMemberModalProps> = ({
|
||||
confirmLoading={inviteMutation.isPending}
|
||||
okText="发送邀请"
|
||||
cancelText="取消"
|
||||
className="xx-modal"
|
||||
width={480}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
@@ -60,7 +63,7 @@ const InviteMemberModal: React.FC<InviteMemberModalProps> = ({
|
||||
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="角色"
|
||||
label="角色权限"
|
||||
initialValue="member"
|
||||
rules={[{ required: true, message: '请选择角色' }]}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 成员列表组件
|
||||
* 成员列表组件 - V21 UI
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Table, Button, Tag, Space, Popconfirm, message, Select } from 'antd';
|
||||
@@ -7,6 +7,7 @@ import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { useWorkspaceMembers } from '@/hooks/useWorkspace';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { removeMember, updateMemberRole } from '@/api/workspace';
|
||||
import './business.css';
|
||||
|
||||
interface MemberListProps {
|
||||
workspaceId: string;
|
||||
@@ -34,14 +35,14 @@ const MemberList: React.FC<MemberListProps> = ({ 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 roleConfig: Record<string, { color: string; text: string; className: string }> = {
|
||||
owner: { color: 'gold', text: '拥有者', className: 'xx-tag-warning' },
|
||||
admin: { color: 'blue', text: '管理员', className: 'xx-tag-indigo' },
|
||||
member: { color: 'green', text: '成员', className: 'xx-tag-success' },
|
||||
viewer: { color: 'default', text: '访客', className: '' },
|
||||
};
|
||||
const config = roleConfig[role] || roleConfig.member;
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
return <Tag color={config.color} className={`xx-tag ${config.className}`}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
@@ -49,11 +50,13 @@ const MemberList: React.FC<MemberListProps> = ({ workspaceId }) => {
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
render: (text: string) => <span style={{ fontWeight: 600, color: '#0f172a' }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
render: (text: string) => <span style={{ color: '#64748b' }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
@@ -66,7 +69,7 @@ const MemberList: React.FC<MemberListProps> = ({ workspaceId }) => {
|
||||
return (
|
||||
<Select
|
||||
value={role}
|
||||
style={{ width: 120 }}
|
||||
style={{ width: 130 }}
|
||||
onChange={(newRole) =>
|
||||
updateRoleMutation.mutate({ memberId: record.id, role: newRole })
|
||||
}
|
||||
@@ -83,11 +86,12 @@ const MemberList: React.FC<MemberListProps> = ({ workspaceId }) => {
|
||||
title: '加入时间',
|
||||
dataIndex: 'joined_at',
|
||||
key: 'joined_at',
|
||||
render: (date: string) => new Date(date).toLocaleDateString(),
|
||||
render: (date: string) => <span style={{ color: '#64748b' }}>{new Date(date).toLocaleDateString()}</span>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
render: (_: any, record: any) => {
|
||||
if (record.role === 'owner') {
|
||||
return <span style={{ color: '#999' }}>-</span>;
|
||||
@@ -116,13 +120,15 @@ const MemberList: React.FC<MemberListProps> = ({ workspaceId }) => {
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={members}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
<div className="xx-table-wrapper">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={members}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10, showSizeChanger: false }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* 权限矩阵展示组件
|
||||
* 权限矩阵展示组件 - 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 = [
|
||||
@@ -24,18 +25,20 @@ const PermissionMatrix: React.FC = () => {
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
fixed: 'left' as const,
|
||||
width: 150,
|
||||
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: '#52c41a', fontSize: '16px' }} />
|
||||
<CheckOutlined style={{ color: '#16a34a', fontSize: '18px' }} />
|
||||
) : (
|
||||
<CloseOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />
|
||||
<CloseOutlined style={{ color: '#dc2626', fontSize: '16px' }} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -43,11 +46,12 @@ const PermissionMatrix: React.FC = () => {
|
||||
dataIndex: 'admin',
|
||||
key: 'admin',
|
||||
align: 'center' as const,
|
||||
width: 100,
|
||||
render: (value: boolean) =>
|
||||
value ? (
|
||||
<CheckOutlined style={{ color: '#52c41a', fontSize: '16px' }} />
|
||||
<CheckOutlined style={{ color: '#16a34a', fontSize: '18px' }} />
|
||||
) : (
|
||||
<CloseOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />
|
||||
<CloseOutlined style={{ color: '#dc2626', fontSize: '16px' }} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -55,11 +59,12 @@ const PermissionMatrix: React.FC = () => {
|
||||
dataIndex: 'member',
|
||||
key: 'member',
|
||||
align: 'center' as const,
|
||||
width: 100,
|
||||
render: (value: boolean) =>
|
||||
value ? (
|
||||
<CheckOutlined style={{ color: '#52c41a', fontSize: '16px' }} />
|
||||
<CheckOutlined style={{ color: '#16a34a', fontSize: '18px' }} />
|
||||
) : (
|
||||
<CloseOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />
|
||||
<CloseOutlined style={{ color: '#dc2626', fontSize: '16px' }} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -67,24 +72,27 @@ const PermissionMatrix: React.FC = () => {
|
||||
dataIndex: 'viewer',
|
||||
key: 'viewer',
|
||||
align: 'center' as const,
|
||||
width: 100,
|
||||
render: (value: boolean) =>
|
||||
value ? (
|
||||
<CheckOutlined style={{ color: '#52c41a', fontSize: '16px' }} />
|
||||
<CheckOutlined style={{ color: '#16a34a', fontSize: '18px' }} />
|
||||
) : (
|
||||
<CloseOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />
|
||||
<CloseOutlined style={{ color: '#dc2626', fontSize: '16px' }} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card title="权限说明">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={permissions}
|
||||
pagination={false}
|
||||
rowKey="action"
|
||||
size="small"
|
||||
/>
|
||||
<Card title="权限说明" className="xx-card">
|
||||
<div className="xx-table-wrapper">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={permissions}
|
||||
pagination={false}
|
||||
rowKey="action"
|
||||
size="middle"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* 配额使用展示组件
|
||||
* 配额使用展示组件 - V21 UI
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Progress, Row, Col, Tag, Space } from 'antd';
|
||||
import { ProjectOutlined, CloudOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import './business.css';
|
||||
|
||||
interface QuotaStatus {
|
||||
projects: {
|
||||
used: number;
|
||||
@@ -23,68 +25,70 @@ interface QuotaDisplayProps {
|
||||
|
||||
const QuotaDisplay: React.FC<QuotaDisplayProps> = ({ quota }) => {
|
||||
const getStatusTag = (status: string) => {
|
||||
const config: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
normal: { color: 'success', text: '正常', icon: null },
|
||||
warning: { color: 'warning', text: '接近上限', icon: <WarningOutlined /> },
|
||||
critical: { color: 'error', text: '即将超限', icon: <WarningOutlined /> },
|
||||
exceeded: { color: 'error', text: '已超限', icon: <WarningOutlined /> },
|
||||
const config: Record<string, { color: string; text: string; className: string }> = {
|
||||
normal: { color: 'success', text: '正常', className: 'xx-tag-success' },
|
||||
warning: { color: 'warning', text: '接近上限', className: 'xx-tag-warning' },
|
||||
critical: { color: 'error', text: '即将超限', className: 'xx-tag-error' },
|
||||
exceeded: { color: 'error', text: '已超限', className: 'xx-tag-error' },
|
||||
};
|
||||
const item = config[status] || config.normal;
|
||||
return (
|
||||
<Tag color={item.color} icon={item.icon}>
|
||||
<Tag color={item.color} icon={<WarningOutlined />} className={`xx-tag ${item.className}`}>
|
||||
{item.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const getProgressStatus = (status: string) => {
|
||||
const statusMap: Record<string, 'success' | 'exception' | 'normal'> = {
|
||||
normal: 'success',
|
||||
warning: 'normal',
|
||||
critical: 'exception',
|
||||
exceeded: 'exception',
|
||||
const getProgressColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
normal: '#16a34a',
|
||||
warning: '#d97706',
|
||||
critical: '#dc2626',
|
||||
exceeded: '#dc2626',
|
||||
};
|
||||
return statusMap[status] || 'normal';
|
||||
return colorMap[status] || '#4f46e5';
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="配额使用情况">
|
||||
<Card title="配额使用情况" className="xx-card">
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
<div>
|
||||
<Row justify="space-between" style={{ marginBottom: 8 }}>
|
||||
<div className="xx-quota-item">
|
||||
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
|
||||
<Col>
|
||||
<Space>
|
||||
<ProjectOutlined />
|
||||
<span>项目数量</span>
|
||||
<ProjectOutlined style={{ fontSize: 18, color: '#4f46e5' }} />
|
||||
<span style={{ fontWeight: 600, color: '#0f172a', fontSize: 15 }}>项目数量</span>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>{getStatusTag(quota.projects.status)}</Col>
|
||||
</Row>
|
||||
<div style={{ marginBottom: 4, color: '#666' }}>
|
||||
<div style={{ marginBottom: 8, color: '#64748b', fontSize: 14 }}>
|
||||
{quota.projects.used} / {quota.projects.limit} 个项目
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round((quota.projects.used / quota.projects.limit) * 100)}
|
||||
status={getProgressStatus(quota.projects.status)}
|
||||
strokeColor={getProgressColor(quota.projects.status)}
|
||||
trailColor="#f1f5f9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Row justify="space-between" style={{ marginBottom: 8 }}>
|
||||
<div className="xx-quota-item">
|
||||
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
|
||||
<Col>
|
||||
<Space>
|
||||
<CloudOutlined />
|
||||
<span>存储空间</span>
|
||||
<CloudOutlined style={{ fontSize: 18, color: '#4f46e5' }} />
|
||||
<span style={{ fontWeight: 600, color: '#0f172a', fontSize: 15 }}>存储空间</span>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>{getStatusTag(quota.storage.status)}</Col>
|
||||
</Row>
|
||||
<div style={{ marginBottom: 4, color: '#666' }}>
|
||||
<div style={{ marginBottom: 8, color: '#64748b', fontSize: 14 }}>
|
||||
{quota.storage.used_gb.toFixed(2)} / {quota.storage.limit_gb} GB
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round((quota.storage.used_gb / quota.storage.limit_gb) * 100)}
|
||||
status={getProgressStatus(quota.storage.status)}
|
||||
strokeColor={getProgressColor(quota.storage.status)}
|
||||
trailColor="#f1f5f9"
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
/* V21 业务组件统一样式 */
|
||||
|
||||
/* ==================== 按钮 ==================== */
|
||||
.xx-primary-btn {
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
border-radius: 14px !important;
|
||||
padding: 10px 20px !important;
|
||||
font-weight: 600 !important;
|
||||
box-shadow: 0 14px 26px rgba(79,70,229,0.22) !important;
|
||||
transition: all 0.2s !important;
|
||||
cursor: pointer;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.xx-primary-btn:hover {
|
||||
box-shadow: 0 18px 34px rgba(79,70,229,0.28) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.xx-ghost-btn {
|
||||
background: transparent !important;
|
||||
color: #4f46e5 !important;
|
||||
border: 2px solid #4f46e5 !important;
|
||||
border-radius: 14px !important;
|
||||
padding: 8px 18px !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.2s !important;
|
||||
cursor: pointer;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.xx-ghost-btn:hover {
|
||||
background: #eef2ff !important;
|
||||
}
|
||||
|
||||
/* ==================== 卡片 ==================== */
|
||||
.xx-card {
|
||||
background: rgba(255,255,255,0.94);
|
||||
border: 1px solid rgba(226,232,240,0.95);
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 24px 70px rgba(15,23,42,0.09);
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.xx-card:hover {
|
||||
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ==================== 页面结构 ==================== */
|
||||
.xx-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.xx-page-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 18px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.xx-page-head h2 {
|
||||
font-size: 26px;
|
||||
font-weight: 850;
|
||||
color: #0f172a;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.xx-page-head p {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==================== 表格样式 ==================== */
|
||||
.xx-table-card {
|
||||
background: rgba(255,255,255,0.94);
|
||||
border: 1px solid rgba(226,232,240,0.95);
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 24px 70px rgba(15,23,42,0.09);
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xx-table-wrapper {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==================== 标签/Tag ==================== */
|
||||
.xx-tag {
|
||||
padding: 4px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.xx-tag-indigo {
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
border: 1px solid #c7d2fe;
|
||||
}
|
||||
|
||||
.xx-tag-success {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.xx-tag-warning {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
.xx-tag-error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
/* ==================== 搜索栏 ==================== */
|
||||
.xx-search-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.xx-search-input {
|
||||
width: 100%;
|
||||
padding: 12px 18px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
transition: all 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xx-search-input:focus {
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 4px rgba(79,70,229,0.1);
|
||||
}
|
||||
|
||||
/* ==================== Modal ==================== */
|
||||
.xx-modal .ant-modal-content {
|
||||
border-radius: 22px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.xx-modal .ant-modal-header {
|
||||
border-radius: 22px 22px 0 0;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.xx-modal .ant-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.xx-modal .ant-modal-footer {
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
/* ==================== 空状态 ==================== */
|
||||
.xx-empty-state {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.xx-empty-state-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ==================== 网格布局 ==================== */
|
||||
.xx-grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.xx-grid-3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.xx-grid-4 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.xx-grid-2, .xx-grid-3, .xx-grid-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 配额展示 ==================== */
|
||||
.xx-quota-item {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.xx-quota-item:hover {
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 8px 24px rgba(79,70,229,0.1);
|
||||
}
|
||||
|
||||
/* ==================== Ant Design 覆盖样式 ==================== */
|
||||
.ant-table-wrapper .ant-table-thead > tr > th {
|
||||
background: #f8fafc !important;
|
||||
font-weight: 700 !important;
|
||||
color: #0f172a !important;
|
||||
border-bottom: 2px solid #e2e8f0 !important;
|
||||
padding: 14px 16px !important;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-tbody > tr > td {
|
||||
padding: 14px 16px !important;
|
||||
border-bottom: 1px solid #f1f5f9 !important;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-tbody > tr:hover > td {
|
||||
background: #fafbfc !important;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border-radius: 22px !important;
|
||||
border: 1px solid rgba(226,232,240,0.95) !important;
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
border-bottom: 1px solid #e2e8f0 !important;
|
||||
min-height: 52px !important;
|
||||
padding: 0 24px !important;
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
font-weight: 700 !important;
|
||||
font-size: 16px !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 20px 24px !important;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 22px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
padding: 20px 24px !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
.ant-modal-title {
|
||||
font-weight: 700 !important;
|
||||
font-size: 18px !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 24px !important;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
padding: 16px 24px !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
|
||||
border: none !important;
|
||||
border-radius: 14px !important;
|
||||
box-shadow: 0 14px 26px rgba(79,70,229,0.22) !important;
|
||||
height: auto !important;
|
||||
padding: 10px 20px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:hover {
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
|
||||
box-shadow: 0 18px 34px rgba(79,70,229,0.28) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
border-radius: 10px !important;
|
||||
padding: 4px 12px !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 14px !important;
|
||||
border-color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.ant-select:not(.ant-select-disabled):hover .ant-select-selector {
|
||||
border-color: #4f46e5 !important;
|
||||
}
|
||||
|
||||
.ant-select-focused .ant-select-selector {
|
||||
border-color: #4f46e5 !important;
|
||||
box-shadow: 0 0 0 3px rgba(79,70,229,0.1) !important;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
border-radius: 14px !important;
|
||||
border-color: #e2e8f0 !important;
|
||||
padding: 10px 14px !important;
|
||||
}
|
||||
|
||||
.ant-input:hover {
|
||||
border-color: #4f46e5 !important;
|
||||
}
|
||||
|
||||
.ant-input:focus {
|
||||
border-color: #4f46e5 !important;
|
||||
box-shadow: 0 0 0 3px rgba(79,70,229,0.1) !important;
|
||||
}
|
||||
|
||||
.ant-progress-inner {
|
||||
background: #f1f5f9 !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.ant-progress-bg {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/* V21 项目任务中心页面 */
|
||||
.xx-project-tasks {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* 任务列表项 */
|
||||
.xx-task-item {
|
||||
padding: 20px 0 !important;
|
||||
border-bottom: 1px solid #f1f5f9 !important;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.xx-task-item:last-child {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.xx-task-item:hover {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
/* 任务标题行 */
|
||||
.xx-task-title {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.xx-task-type {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* 任务描述 */
|
||||
.xx-task-desc {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* V21 标签 */
|
||||
.xx-tag {
|
||||
padding: 4px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.xx-tag-indigo {
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
border: 1px solid #c7d2fe;
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
/**
|
||||
* 项目任务中心 - V21 UI
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Alert, Button, Card, Empty, List, Progress, Space, Tag, Typography } from 'antd';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { getProjectTasks, retryProjectTask } from '@/api/tasks';
|
||||
import '@/components/business/business.css';
|
||||
import './ProjectTasks.css';
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '排队中', color: 'default' },
|
||||
processing: { label: '处理中', color: 'blue' },
|
||||
running: { label: '运行中', color: 'blue' },
|
||||
completed: { label: '已完成', color: 'green' },
|
||||
failed: { label: '失败', color: 'red' },
|
||||
processing: { label: '处理中', color: 'processing' },
|
||||
running: { label: '运行中', color: 'processing' },
|
||||
completed: { label: '已完成', color: 'success' },
|
||||
failed: { label: '失败', color: 'error' },
|
||||
cancelled: { label: '已取消', color: 'default' },
|
||||
};
|
||||
|
||||
@@ -31,49 +36,83 @@ const ProjectTasks: React.FC = () => {
|
||||
});
|
||||
|
||||
const retryMutation = useMutation({
|
||||
mutationFn: ({ taskType, sourceId }: { taskType: string; sourceId: string }) => retryProjectTask(taskType, sourceId),
|
||||
mutationFn: ({ taskType, sourceId }: { taskType: string; sourceId: string }) =>
|
||||
retryProjectTask(taskType, sourceId),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['project-tasks', projectId] }),
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Card title="项目任务中心" extra={<Button onClick={() => tasksQuery.refetch()}>刷新</Button>}>
|
||||
{tasksQuery.isError && <Alert style={{ marginBottom: 16 }} type="error" showIcon message="任务列表加载失败" />}
|
||||
<div className="xx-project-tasks">
|
||||
<div className="xx-page-head">
|
||||
<div>
|
||||
<h2>任务中心</h2>
|
||||
<p>查看和管理项目中的所有任务</p>
|
||||
</div>
|
||||
<Button className="xx-ghost-btn" onClick={() => tasksQuery.refetch()}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="xx-card">
|
||||
{tasksQuery.isError && (
|
||||
<Alert style={{ marginBottom: 16 }} type="error" showIcon message="任务列表加载失败" />
|
||||
)}
|
||||
{tasksQuery.data?.some((task) => task.retryable) && (
|
||||
<Alert
|
||||
style={{ marginBottom: 16 }}
|
||||
type="warning"
|
||||
showIcon
|
||||
message="部分任务可重试"
|
||||
description="失败任务可通过“重试”重新排队;系统会保留原始失败原因方便排查。"
|
||||
description="失败任务可通过"重试"重新排队;系统会保留原始失败原因方便排查。"
|
||||
/>
|
||||
)}
|
||||
<List
|
||||
loading={tasksQuery.isLoading}
|
||||
dataSource={tasksQuery.data || []}
|
||||
locale={{ emptyText: <Empty description="暂无任务,上传素材或发起生成后会出现在这里" /> }}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div className="xx-empty-state">
|
||||
<div className="xx-empty-state-icon">📋</div>
|
||||
<p>暂无任务,上传素材或发起生成后会出现在这里</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
renderItem={(task) => {
|
||||
const status = statusMap[task.status] || { label: task.status, color: 'default' };
|
||||
return (
|
||||
<List.Item>
|
||||
<List.Item className="xx-task-item">
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<Space wrap>
|
||||
<Typography.Text>{taskTypeLabels[task.task_type] || task.task_type}</Typography.Text>
|
||||
<Tag color={status.color}>{status.label}</Tag>
|
||||
<Tag>{task.current_step}</Tag>
|
||||
{task.retryable ? (
|
||||
<Button size="small" loading={retryMutation.isPending} onClick={() => retryMutation.mutate({ taskType: task.task_type, sourceId: task.source_id })}>
|
||||
<Space wrap className="xx-task-title">
|
||||
<span className="xx-task-type">{taskTypeLabels[task.task_type] || task.task_type}</span>
|
||||
<Tag color={status.color} className="xx-tag">{status.label}</Tag>
|
||||
<Tag className="xx-tag xx-tag-indigo">{task.current_step}</Tag>
|
||||
{task.retryable && (
|
||||
<Button
|
||||
size="small"
|
||||
className="xx-ghost-btn"
|
||||
loading={retryMutation.isPending}
|
||||
onClick={() =>
|
||||
retryMutation.mutate({ taskType: task.task_type, sourceId: task.source_id })
|
||||
}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
) : null}
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Progress percent={Math.round(task.progress || 0)} size="small" status={task.status === 'failed' ? 'exception' : undefined} />
|
||||
{task.user_message ? <Typography.Text type="danger">{task.user_message}</Typography.Text> : null}
|
||||
<Typography.Text type="secondary" copyable>
|
||||
<Space direction="vertical" style={{ width: '100%' }} className="xx-task-desc">
|
||||
<Progress
|
||||
percent={Math.round(task.progress || 0)}
|
||||
size="small"
|
||||
status={task.status === 'failed' ? 'exception' : undefined}
|
||||
strokeColor={task.status === 'failed' ? '#ef4444' : '#4f46e5'}
|
||||
/>
|
||||
{task.user_message ? (
|
||||
<Typography.Text type="danger">{task.user_message}</Typography.Text>
|
||||
) : null}
|
||||
<Typography.Text type="secondary" copyable style={{ fontSize: 12 }}>
|
||||
任务 ID:{task.source_id}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
@@ -83,7 +122,7 @@ const ProjectTasks: React.FC = () => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 工作空间详情页面
|
||||
* 工作空间详情页面 - V21 UI
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
@@ -11,6 +11,7 @@ import { createProject, getProjects } from '@/api/projects';
|
||||
import MemberList from '@/components/business/MemberList';
|
||||
import InviteMemberModal from '@/components/business/InviteMemberModal';
|
||||
import PermissionMatrix from '@/components/business/PermissionMatrix';
|
||||
import '@/components/business/business.css';
|
||||
|
||||
const WorkspaceDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -50,20 +51,20 @@ const WorkspaceDetail: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) return <div>加载中...</div>;
|
||||
if (!workspace) return <div>工作空间不存在</div>;
|
||||
if (isLoading) return <div className="xx-page"><div className="xx-card" style={{textAlign: 'center', padding: '40px'}}>加载中...</div></div>;
|
||||
if (!workspace) return <div className="xx-page"><div className="xx-card" style={{textAlign: 'center', padding: '40px'}}>工作空间不存在</div></div>;
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'overview',
|
||||
label: '概览',
|
||||
children: (
|
||||
<div>
|
||||
<div className="xx-workspace-overview">
|
||||
<Card
|
||||
title="工作空间信息"
|
||||
style={{ marginBottom: 16 }}
|
||||
className="xx-card"
|
||||
extra={
|
||||
<Button type="primary" onClick={() => setCreateProjectOpen(true)}>
|
||||
<Button type="primary" className="xx-primary-btn" onClick={() => setCreateProjectOpen(true)}>
|
||||
创建项目
|
||||
</Button>
|
||||
}
|
||||
@@ -80,7 +81,7 @@ const WorkspaceDetail: React.FC = () => {
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card title="项目" style={{ marginBottom: 16 }}>
|
||||
<Card title="项目列表" className="xx-card">
|
||||
{projectsQuery.isError && (
|
||||
<Alert type="error" showIcon message="项目列表加载失败" style={{ marginBottom: 16 }} />
|
||||
)}
|
||||
@@ -131,18 +132,25 @@ const WorkspaceDetail: React.FC = () => {
|
||||
</>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div className="xx-members-section">
|
||||
<div className="xx-page-head" style={{marginBottom: 20}}>
|
||||
<div>
|
||||
<h3 style={{margin: 0, fontSize: 18, fontWeight: 700, color: '#0f172a'}}>工作空间成员</h3>
|
||||
<p style={{margin: '4px 0 0', fontSize: 14, color: '#64748b'}}>管理有权访问此工作空间的成员</p>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
className="xx-primary-btn"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setInviteModalOpen(true)}
|
||||
>
|
||||
邀请成员
|
||||
</Button>
|
||||
</div>
|
||||
<MemberList workspaceId={id!} />
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<div className="xx-card">
|
||||
<MemberList workspaceId={id!} />
|
||||
</div>
|
||||
<div style={{marginTop: 24}}>
|
||||
<PermissionMatrix />
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,13 +159,18 @@ const WorkspaceDetail: React.FC = () => {
|
||||
{
|
||||
key: 'settings',
|
||||
label: '设置',
|
||||
children: <div>工作空间设置(待开发)</div>,
|
||||
children: <div className="xx-card"><p style={{color: '#64748b'}}>工作空间设置(待开发)</p></div>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{workspace.name}</h1>
|
||||
<div className="xx-page">
|
||||
<div className="xx-page-head">
|
||||
<div>
|
||||
<h1 style={{margin: 0, fontSize: 28, fontWeight: 850, color: '#0f172a'}}>{workspace.name}</h1>
|
||||
<p style={{margin: '8px 0 0', fontSize: 14, color: '#64748b'}}>工作空间详情与成员管理</p>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs items={items} />
|
||||
<Modal
|
||||
title="创建项目"
|
||||
@@ -167,6 +180,7 @@ const WorkspaceDetail: React.FC = () => {
|
||||
confirmLoading={createProjectMutation.isPending}
|
||||
onOk={handleCreateProject}
|
||||
onCancel={() => setCreateProjectOpen(false)}
|
||||
className="xx-modal"
|
||||
>
|
||||
<Form form={projectForm} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
|
||||
Reference in New Issue
Block a user