feat: V21 Workspace+组件样式对齐 (#30)

Resolved merge conflict in business.css by keeping both sides
This commit is contained in:
CI Bot
2026-06-26 22:15:36 +08:00
9 changed files with 376 additions and 110 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>
@@ -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;
}
+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>
);
};