feat: V21 Workspace+组件样式对齐 (#30)
Resolved merge conflict in business.css by keeping both sides
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>
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* 表格包装器 */
|
||||
.xx-table-wrapper {
|
||||
border-radius: 16px;
|
||||
@@ -222,6 +223,7 @@
|
||||
box-shadow: 0 8px 24px rgba(79,70,229,0.1);
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* ==================== 进度条 ==================== */
|
||||
.xx-progress {
|
||||
margin-top: 12px;
|
||||
@@ -229,6 +231,7 @@
|
||||
|
||||
/* ==================== Ant Design 覆盖样式 ==================== */
|
||||
/* Table overrides */
|
||||
/* ==================== Ant Design 覆盖样式 ==================== */
|
||||
.ant-table-wrapper .ant-table-thead > tr > th {
|
||||
background: #f8fafc !important;
|
||||
font-weight: 700 !important;
|
||||
@@ -246,6 +249,7 @@
|
||||
background: #fafbfc !important;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* Card overrides */
|
||||
.ant-card {
|
||||
border-radius: 22px !important;
|
||||
@@ -268,6 +272,7 @@
|
||||
padding: 20px 24px !important;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* Modal overrides */
|
||||
.ant-modal-content {
|
||||
border-radius: 22px !important;
|
||||
@@ -293,6 +298,7 @@
|
||||
padding: 16px 24px !important;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* Button overrides */
|
||||
.ant-btn-primary {
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
|
||||
@@ -310,6 +316,7 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* Tag overrides */
|
||||
.ant-tag {
|
||||
border-radius: 10px !important;
|
||||
@@ -317,6 +324,7 @@
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* Select overrides */
|
||||
.ant-select-selector {
|
||||
border-radius: 14px !important;
|
||||
@@ -332,6 +340,7 @@
|
||||
box-shadow: 0 0 0 3px rgba(79,70,229,0.1) !important;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* Input overrides */
|
||||
.ant-input {
|
||||
border-radius: 14px !important;
|
||||
@@ -348,6 +357,7 @@
|
||||
box-shadow: 0 0 0 3px rgba(79,70,229,0.1) !important;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* Progress overrides */
|
||||
.ant-progress-inner {
|
||||
background: #f1f5f9 !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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user