feat: V21 design system alignment for workspace pages and business components
CI/CD Pipeline / Validate Code Quality And Tests (push) Waiting to run
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Waiting to run

- 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:
CI Test
2026-06-26 21:37:28 +08:00
parent 43d5f6e73f
commit 955885d909
10 changed files with 739 additions and 124 deletions
+134 -8
View File
@@ -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
View File
@@ -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: '请选择角色' }]}
>
+22 -16
View File
@@ -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;
}
+62 -23
View File
@@ -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