Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括: - 配置文件中的项目名称 - 代码注释和文档引用 - 环境变量和路径 - 类型定义和接口名称 - 测试用例和模拟数据 同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
616 lines
15 KiB
Markdown
616 lines
15 KiB
Markdown
# ZCLAW UI/UX 全面优化计划
|
|
|
|
## 背景
|
|
|
|
ZCLAW 是基于 ZCLAW (Rust Agent OS) 的 AI Agent 桌面客户端。经过 Phase 1-8 的功能开发,系统已达到 93% API 覆盖率和 100% UI 组件完成度。现在需要对前端设计元素进行全面优化,提升用户操作效率与视觉体验。
|
|
|
|
## 当前状态分析
|
|
|
|
### 代码结构
|
|
- **框架**: React 18 + TypeScript + Tauri
|
|
- **样式**: Tailwind CSS v4.2.1
|
|
- **状态管理**: Zustand (gatewayStore, chatStore, teamStore)
|
|
- **图标**: lucide-react
|
|
- **组件数量**: 37 个组件
|
|
|
|
### 现有设计特点
|
|
- **主色调**: Orange (#f97316) 作为品牌色
|
|
- **布局**: 三栏式 (Sidebar w-64 | Main flex-1 | RightPanel w-80)
|
|
- **暗黑模式**: 部分支持 (部分组件有 `dark:` 变体)
|
|
- **动画**: 基础 CSS 动画 (animate-pulse, animate-spin)
|
|
|
|
---
|
|
|
|
## 发现的问题
|
|
|
|
### 1. 设计系统问题 (CRITICAL)
|
|
|
|
| 问题 | 位置 | 影响 |
|
|
|------|------|------|
|
|
| 无 UI 组件库 | 全局 | 一致性差,重复代码 |
|
|
| 颜色系统不完整 | index.css | 品牌识别度低 |
|
|
| 暗黑模式不完整 | 多个组件 | 用户体验不一致 |
|
|
| **使用 Emoji 作为 UI 图标** | ChatArea, App.tsx | 不专业,违反设计规范 |
|
|
|
|
### 2. 可访问性问题 (CRITICAL)
|
|
|
|
| 问题 | 位置 | 影响 |
|
|
|------|------|------|
|
|
| 缺少 aria-label | 图标按钮 | 屏幕阅读器无法识别 |
|
|
| 焦点状态不明显 | 多个交互元素 | 键盘导航困难 |
|
|
| 颜色对比度不足 | text-gray-400 类 | 可读性差 |
|
|
|
|
### 3. 交互与动画 (HIGH)
|
|
|
|
| 问题 | 位置 | 影响 |
|
|
|------|------|------|
|
|
| 无过渡动画库 | 全局 | 交互生硬 |
|
|
| 悬停状态不一致 | 多个组件 | 用户困惑 |
|
|
| 缺少骨架屏 | 数据加载 | 感知性能差 |
|
|
|
|
### 4. 视觉层次 (MEDIUM)
|
|
|
|
| 问题 | 位置 | 影响 |
|
|
|------|------|------|
|
|
| 卡片阴影过轻 | shadow-sm | 层次不明显 |
|
|
| 间距不一致 | padding/margin | 视觉混乱 |
|
|
| 字体过小 | text-xs | 可读性差 |
|
|
|
|
### 5. 用户体验 (MEDIUM)
|
|
|
|
| 问题 | 位置 | 影响 |
|
|
|------|------|------|
|
|
| 空状态设计简陋 | 多个列表 | 缺乏引导 |
|
|
| 错误提示不明显 | 表单等 | 用户困惑 |
|
|
| 加载反馈不足 | API 调用 | 用户焦虑 |
|
|
|
|
---
|
|
|
|
## 用户确认的优化方向
|
|
|
|
| 决策项 | 选择 |
|
|
|--------|------|
|
|
| 动画库 | ✅ 引入 Framer Motion |
|
|
| 暗黑模式 | ✅ 完整支持所有组件 |
|
|
| 组件库 | ✅ 创建 Button, Card, Input 等基础组件 |
|
|
|
|
---
|
|
|
|
## 优化方案
|
|
|
|
### Phase 1: 设计系统建立 (P0)
|
|
|
|
#### 1.1 CSS 变量系统
|
|
|
|
**文件**: `desktop/src/index.css`
|
|
|
|
```css
|
|
:root {
|
|
/* Brand Colors */
|
|
--color-primary: #f97316;
|
|
--color-primary-hover: #ea580c;
|
|
--color-primary-light: #fff7ed;
|
|
|
|
/* Semantic Colors */
|
|
--color-success: #22c55e;
|
|
--color-warning: #eab308;
|
|
--color-error: #ef4444;
|
|
--color-info: #3b82f6;
|
|
|
|
/* Neutral Colors */
|
|
--color-bg: #ffffff;
|
|
--color-bg-secondary: #f9fafb;
|
|
--color-border: #e5e7eb;
|
|
--color-text: #111827;
|
|
--color-text-secondary: #6b7280;
|
|
--color-text-muted: #9ca3af;
|
|
|
|
/* Spacing Scale */
|
|
--space-xs: 4px;
|
|
--space-sm: 8px;
|
|
--space-md: 16px;
|
|
--space-lg: 24px;
|
|
--space-xl: 32px;
|
|
|
|
/* Border Radius */
|
|
--radius-sm: 6px;
|
|
--radius-md: 8px;
|
|
--radius-lg: 12px;
|
|
--radius-xl: 16px;
|
|
--radius-full: 9999px;
|
|
|
|
/* Shadows */
|
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
|
|
|
/* Transitions */
|
|
--transition-fast: 150ms ease;
|
|
--transition-normal: 200ms ease;
|
|
--transition-slow: 300ms ease;
|
|
}
|
|
|
|
.dark {
|
|
--color-bg: #0f172a;
|
|
--color-bg-secondary: #1e293b;
|
|
--color-border: #334155;
|
|
--color-text: #f1f5f9;
|
|
--color-text-secondary: #94a3b8;
|
|
--color-text-muted: #64748b;
|
|
}
|
|
```
|
|
|
|
#### 1.2 统一 Tailwind 配置
|
|
|
|
**文件**: `desktop/tailwind.config.js` (新建)
|
|
|
|
```javascript
|
|
export default {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
DEFAULT: 'var(--color-primary)',
|
|
hover: 'var(--color-primary-hover)',
|
|
light: 'var(--color-primary-light)',
|
|
},
|
|
},
|
|
boxShadow: {
|
|
'card': 'var(--shadow-md)',
|
|
'hover': 'var(--shadow-lg)',
|
|
},
|
|
transitionDuration: {
|
|
'fast': '150ms',
|
|
'normal': '200ms',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
### Phase 2: 基础组件库 (P0)
|
|
|
|
#### 2.1 Button 组件
|
|
|
|
**文件**: `desktop/src/components/ui/Button.tsx`
|
|
|
|
```tsx
|
|
import { forwardRef } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { cn } from '../../lib/utils';
|
|
|
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
size?: 'sm' | 'md' | 'lg';
|
|
loading?: boolean;
|
|
}
|
|
|
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
({ className, variant = 'primary', size = 'md', loading, children, disabled, ...props }, ref) => {
|
|
return (
|
|
<motion.button
|
|
ref={ref}
|
|
whileTap={{ scale: 0.98 }}
|
|
className={cn(
|
|
'inline-flex items-center justify-center font-medium rounded-lg transition-colors',
|
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
|
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
variants[variant],
|
|
sizes[size],
|
|
className
|
|
)}
|
|
disabled={disabled || loading}
|
|
{...props}
|
|
>
|
|
{loading && <Spinner className="mr-2" />}
|
|
{children}
|
|
</motion.button>
|
|
);
|
|
}
|
|
);
|
|
|
|
const variants = {
|
|
primary: 'bg-primary text-white hover:bg-primary-hover',
|
|
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
|
|
ghost: 'text-gray-600 hover:bg-gray-100',
|
|
danger: 'bg-red-500 text-white hover:bg-red-600',
|
|
};
|
|
|
|
const sizes = {
|
|
sm: 'px-3 py-1.5 text-xs',
|
|
md: 'px-4 py-2 text-sm',
|
|
lg: 'px-6 py-3 text-base',
|
|
};
|
|
```
|
|
|
|
#### 2.2 Card 组件
|
|
|
|
**文件**: `desktop/src/components/ui/Card.tsx`
|
|
|
|
```tsx
|
|
import { motion } from 'framer-motion';
|
|
import { cn } from '../../lib/utils';
|
|
|
|
interface CardProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
hoverable?: boolean;
|
|
onClick?: () => void;
|
|
}
|
|
|
|
export function Card({ children, className, hoverable, onClick }: CardProps) {
|
|
const Component = hoverable ? motion.div : 'div';
|
|
|
|
return (
|
|
<Component
|
|
className={cn(
|
|
'rounded-xl border border-gray-200 bg-white p-4 shadow-sm',
|
|
'dark:border-gray-700 dark:bg-gray-800',
|
|
hoverable && 'cursor-pointer hover:shadow-md transition-shadow duration-200',
|
|
className
|
|
)}
|
|
{...(hoverable && {
|
|
whileHover: { y: -2 },
|
|
onClick,
|
|
})}
|
|
>
|
|
{children}
|
|
</Component>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### 2.3 cn() 工具函数
|
|
|
|
**文件**: `desktop/src/lib/utils.ts`
|
|
|
|
```typescript
|
|
import { clsx, type ClassValue } from 'clsx';
|
|
import { twMerge } from 'tailwind-merge';
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs));
|
|
}
|
|
```
|
|
|
|
**原则**: 使用 SVG 图标 (lucide-react),不使用 emoji 作为 UI 元素
|
|
|
|
| 文件 | 当前 | 修改为 |
|
|
|------|------|--------|
|
|
| `ChatArea.tsx:79` | `🤖` | `<BotIcon className="w-10 h-10 text-gray-400" />` |
|
|
| `ChatArea.tsx:279` | `🦞` (消息气泡) | `<ZClawLogo className="w-6 h-6" />` (自定义图标) |
|
|
| `App.tsx:79` | `🤖` | `<BotIcon />` |
|
|
| `App.tsx:98` | `👥` | `<UsersIcon />` |
|
|
| `Sidebar.tsx:98` | 用户头像 emoji | 首字母 + 渐变背景 |
|
|
|
|
### Phase 3: 可访问性改进 (P0)
|
|
|
|
#### 3.1 添加 aria-label
|
|
|
|
**关键文件**:
|
|
- `Sidebar.tsx` - 设置按钮、tab 切换
|
|
- `ChatArea.tsx` - 发送按钮、附件按钮、模型选择器
|
|
- `RightPanel.tsx` - tab 切换、刷新按钮
|
|
- `SettingsLayout.tsx` - 返回按钮、菜单项
|
|
|
|
```tsx
|
|
// 示例修复
|
|
<button
|
|
onClick={onOpenSettings}
|
|
aria-label="打开设置"
|
|
className="..."
|
|
>
|
|
<Settings className="w-4 h-4" />
|
|
</button>
|
|
```
|
|
|
|
#### 3.2 焦点状态增强
|
|
|
|
```css
|
|
/* 全局焦点样式 */
|
|
:focus-visible {
|
|
outline: 2px solid var(--color-primary);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* 按钮焦点 */
|
|
.btn:focus-visible {
|
|
ring: 2px;
|
|
ring-color: var(--color-primary);
|
|
ring-offset: 2px;
|
|
}
|
|
```
|
|
|
|
#### 3.3 颜色对比度修复
|
|
|
|
| 当前 | 对比度 | 修复 |
|
|
|------|--------|------|
|
|
| `text-gray-400` on white | 3.1:1 (FAIL) | 改为 `text-gray-500` (#6b7280) 4.6:1 |
|
|
| `text-gray-300` on white | 2.9:1 (FAIL) | 改为 `text-gray-600` (#4b5563) 7.0:1 |
|
|
|
|
### Phase 4: 交互与动画增强 (P1)
|
|
|
|
#### 4.1 Framer Motion 动画系统
|
|
|
|
**新文件**: `desktop/src/lib/animations.ts`
|
|
|
|
```typescript
|
|
import { Variants } from 'framer-motion';
|
|
|
|
// 页面切换动画
|
|
export const pageVariants: Variants = {
|
|
initial: { opacity: 0, y: 10 },
|
|
animate: { opacity: 1, y: 0 },
|
|
exit: { opacity: 0, y: -10 },
|
|
};
|
|
|
|
// 列表项交错动画
|
|
export const listItemVariants: Variants = {
|
|
hidden: { opacity: 0, x: -10 },
|
|
visible: { opacity: 1, x: 0 },
|
|
};
|
|
|
|
// 按钮点击动画
|
|
export const buttonTap = { scale: 0.98 };
|
|
|
|
// 卡片悬停动画
|
|
export const cardHover = { y: -2, boxShadow: '0 10px 25px -5px rgb(0 0 0 / 0.1)' };
|
|
|
|
// 默认过渡配置
|
|
export const defaultTransition = {
|
|
duration: 0.2,
|
|
ease: [0.4, 0, 0.2, 1],
|
|
};
|
|
```
|
|
|
|
**使用示例**:
|
|
```tsx
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { pageVariants, defaultTransition } from '../lib/animations';
|
|
|
|
// 页面切换
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={view}
|
|
variants={pageVariants}
|
|
initial="initial"
|
|
animate="animate"
|
|
exit="exit"
|
|
transition={defaultTransition}
|
|
>
|
|
{content}
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
```
|
|
|
|
**新文件**: `desktop/src/components/ui/Skeleton.tsx`
|
|
|
|
```tsx
|
|
export function Skeleton({ className }: { className?: string }) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"animate-pulse bg-gray-200 dark:bg-gray-700 rounded",
|
|
className
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function CardSkeleton() {
|
|
return (
|
|
<div className="rounded-xl border border-gray-200 p-4">
|
|
<Skeleton className="h-4 w-24 mb-3" />
|
|
<Skeleton className="h-3 w-full mb-2" />
|
|
<Skeleton className="h-3 w-3/4" />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### 4.3 Toast 通知系统
|
|
|
|
**新文件**: `desktop/src/components/ui/Toast.tsx`
|
|
|
|
用于替代 `alert()` 和内联错误消息。
|
|
|
|
### Phase 5: 视觉层次优化 (P1)
|
|
|
|
#### 5.1 卡片阴影增强
|
|
|
|
```diff
|
|
- className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm"
|
|
+ className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow duration-200"
|
|
```
|
|
|
|
#### 5.2 间距标准化
|
|
|
|
| 区域 | 当前 | 标准 |
|
|
|------|------|------|
|
|
| 页面内边距 | 不一致 | `p-6` (24px) |
|
|
| 卡片内边距 | `p-3`/`p-4` | `p-4` (16px) |
|
|
| 列表项间距 | `space-y-1`/`space-y-2` | `space-y-2` (8px) |
|
|
| 区域间距 | `space-y-3`/`space-y-4` | `space-y-4` (16px) |
|
|
|
|
#### 5.3 字体大小优化
|
|
|
|
| 类型 | 当前 | 建议 |
|
|
|------|------|------|
|
|
| 正文 | `text-xs` (12px) | `text-sm` (14px) |
|
|
| 小标签 | `text-[10px]` | `text-xs` (12px) |
|
|
| 标题 | `text-lg` | 保持 |
|
|
|
|
### Phase 6: 用户体验改进 (P2)
|
|
|
|
#### 6.1 空状态设计
|
|
|
|
**新组件**: `EmptyState.tsx`
|
|
|
|
```tsx
|
|
interface EmptyStateProps {
|
|
icon: React.ReactNode;
|
|
title: string;
|
|
description: string;
|
|
action?: React.ReactNode;
|
|
}
|
|
|
|
export function EmptyState({ icon, title, description, action }: EmptyStateProps) {
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center p-6">
|
|
<div className="text-center max-w-sm">
|
|
<div className="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-4 text-gray-400">
|
|
{icon}
|
|
</div>
|
|
<h3 className="text-base font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
|
{title}
|
|
</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
|
{description}
|
|
</p>
|
|
{action}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### 6.2 错误状态设计
|
|
|
|
- 使用红色边框和背景
|
|
- 显示错误图标
|
|
- 提供重试按钮
|
|
|
|
#### 6.3 加载状态改进
|
|
|
|
- 按钮加载时禁用并显示 spinner
|
|
- 列表加载时显示骨架屏
|
|
- 全局加载时显示顶部进度条
|
|
|
|
---
|
|
|
|
## 实施顺序
|
|
|
|
### 阶段 1: 基础设施 (2-3 天)
|
|
1. 安装依赖: `framer-motion clsx tailwind-merge`
|
|
2. 创建 CSS 变量系统 (含完整暗黑模式)
|
|
3. 创建 tailwind.config.js
|
|
4. 创建 utils.ts (cn 函数)
|
|
5. 创建 animations.ts (Framer Motion 预设)
|
|
|
|
### 阶段 2: 基础组件库 (2-3 天)
|
|
1. 创建 Button 组件 (含变体: primary, secondary, ghost)
|
|
2. 创建 Card 组件 (含悬停动画)
|
|
3. 创建 Input 组件 (含焦点状态)
|
|
4. 创建 Badge 组件
|
|
5. 创建 Skeleton 组件
|
|
6. 创建 EmptyState 组件
|
|
7. 创建 Toast 组件
|
|
|
|
### 阶段 3: 可访问性修复 (1-2 天)
|
|
1. 添加 aria-label 到所有图标按钮
|
|
2. 修复颜色对比度问题
|
|
3. 增强焦点状态
|
|
4. 添加键盘导航支持
|
|
|
|
### 阶段 4: 视觉优化 (2-3 天)
|
|
1. 移除所有 emoji 图标,替换为 SVG
|
|
2. 使用基础组件库重构现有组件
|
|
3. 标准化间距
|
|
4. 增强卡片阴影和悬停效果
|
|
5. 完善暗黑模式支持
|
|
|
|
### 阶段 5: 交互增强 (2-3 天)
|
|
1. 添加 Framer Motion 页面切换动画
|
|
2. 添加列表项动画 (stagger)
|
|
3. 添加按钮点击反馈
|
|
4. 实现骨架屏加载
|
|
5. 集成 Toast 通知系统
|
|
|
|
### 阶段 6: 组件重构 (2-3 天)
|
|
1. 重构 Sidebar 组件 (使用新组件库 + 动画)
|
|
2. 重构 ChatArea 组件
|
|
3. 重构 RightPanel 组件
|
|
4. 重构 SettingsLayout 组件
|
|
5. 完整暗黑模式测试
|
|
|
|
---
|
|
|
|
## 关键文件清单
|
|
|
|
### 需要修改的文件
|
|
|
|
| 文件 | 修改内容 |
|
|
|------|----------|
|
|
| `desktop/src/index.css` | 添加 CSS 变量系统 |
|
|
| `desktop/src/components/Sidebar.tsx` | aria-label, 动画, emoji 移除 |
|
|
| `desktop/src/components/ChatArea.tsx` | aria-label, emoji 移除, 空状态 |
|
|
| `desktop/src/components/RightPanel.tsx` | aria-label, 间距标准化 |
|
|
| `desktop/src/components/Settings/SettingsLayout.tsx` | aria-label, 间距标准化 |
|
|
| `desktop/src/App.tsx` | emoji 移除, 空状态组件 |
|
|
|
|
### 需要新建的文件
|
|
|
|
| 文件 | 用途 |
|
|
|------|------|
|
|
| `desktop/tailwind.config.js` | Tailwind 自定义配置 |
|
|
| `desktop/src/components/ui/Button.tsx` | 按钮组件 |
|
|
| `desktop/src/components/ui/Card.tsx` | 卡片组件 |
|
|
| `desktop/src/components/ui/Input.tsx` | 输入框组件 |
|
|
| `desktop/src/components/ui/Badge.tsx` | 标签组件 |
|
|
| `desktop/src/components/ui/Skeleton.tsx` | 骨架屏组件 |
|
|
| `desktop/src/components/ui/EmptyState.tsx` | 空状态组件 |
|
|
| `desktop/src/components/ui/Toast.tsx` | Toast 通知组件 |
|
|
| `desktop/src/lib/utils.ts` | cn() 工具函数 |
|
|
| `desktop/src/lib/animations.ts` | Framer Motion 动画预设 |
|
|
|
|
### 需要安装的依赖
|
|
|
|
```bash
|
|
cd desktop
|
|
pnpm add framer-motion clsx tailwind-merge
|
|
```
|
|
|
|
---
|
|
|
|
## 验证方法
|
|
|
|
### 1. 视觉验证
|
|
```bash
|
|
pnpm tauri:dev
|
|
```
|
|
- 检查所有页面在浅色/深色模式下的显示
|
|
- 检查所有交互元素的悬停状态
|
|
- 检查所有空状态的显示
|
|
|
|
### 2. 可访问性验证
|
|
- 使用 Chrome DevTools Lighthouse 进行可访问性审计
|
|
- 使用 Tab 键进行键盘导航测试
|
|
- 使用屏幕阅读器测试 (NVDA/JAWS)
|
|
|
|
### 3. 类型检查
|
|
```bash
|
|
cd desktop && pnpm tsc --noEmit
|
|
```
|
|
|
|
### 4. 构建验证
|
|
```bash
|
|
pnpm build
|
|
```
|
|
|
|
---
|
|
|
|
## 预期成果
|
|
|
|
1. **设计系统**: 完整的 CSS 变量和 Tailwind 配置
|
|
2. **组件库**: 7 个可复用 UI 组件 (Button, Card, Input, Badge, Skeleton, EmptyState, Toast)
|
|
3. **可访问性**: Lighthouse 可访问性分数 > 90
|
|
4. **暗黑模式**: 所有组件完整支持暗黑模式
|
|
5. **动画系统**: Framer Motion 动画预设和页面过渡
|
|
6. **视觉一致性**: 所有组件遵循统一设计规范
|
|
7. **无 Emoji**: 所有 UI 图标使用 SVG
|
|
|
|
---
|
|
|
|
*计划创建: 2026-03-15*
|
|
*预计完成: 11-17 个工作日*
|