|
@@ -1,33 +1,34 @@
|
|
|
-# 项目管理 - 交付执行阶段 PRD (Product表版本)
|
|
|
+# 项目管理 - 交付执行阶段 PRD (基于Product表和NovaStorage)
|
|
|
|
|
|
## 1. 功能概述
|
|
|
|
|
|
### 1.1 阶段定位
|
|
|
-交付执行阶段是项目管理流程的核心执行环节,包含建模、软装、渲染、后期四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。
|
|
|
+交付执行阶段是项目管理流程的核心执行环节,包含**白模建模、软装设计、渲染输出、后期处理**四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。
|
|
|
|
|
|
### 1.2 核心目标
|
|
|
-- **多产品设计协同管理**:支持单产品设计到多产品设计项目的灵活管理
|
|
|
-- **按产品设计维度组织文件上传和进度管理**
|
|
|
-- **实现四个执行阶段的串行推进**
|
|
|
-- **跨产品设计协调与依赖管理**:处理产品设计间的风格一致性、色彩流线、材质匹配
|
|
|
-- **提供实时进度跟踪和状态可视化**
|
|
|
-- **支持组长审核和质量把控**
|
|
|
-- **确保交付物符合质量标准**
|
|
|
+- **多空间产品协同管理**:基于Product表统一管理各空间设计产品的交付执行
|
|
|
+- **四阶段全流程管控**:白模→软装→渲染→后期的完整执行链路
|
|
|
+- **文件管理与存储集成**:基于NovaStorage的文件上传,prefixKeys=project/:pid/,Attachment与ProjectFile同步保存
|
|
|
+- **ProjectTeam协作管理**:根据团队成员角色和技能,合理分配各阶段任务
|
|
|
+- **交付物分类管理**:按四大核心内容(白模\软装\渲染\后期)分类管理交付物
|
|
|
+- **质量把控与审核**:支持组长审核和质量标准验证
|
|
|
+- **实时进度跟踪**:多维度进度监控和状态可视化
|
|
|
|
|
|
### 1.3 涉及角色
|
|
|
-- **设计师**:负责建模、软装、后期等设计工作
|
|
|
-- **渲染师**:负责渲染阶段的大图输出
|
|
|
-- **组长**:审核各阶段交付物、把控质量
|
|
|
-- **技术**:验收最终交付物、确认质量
|
|
|
+- **设计师**:负责白模建模、软装设计、后期处理等设计工作
|
|
|
+- **渲染师**:专门负责渲染阶段的图片输出和质量把控
|
|
|
+- **组长**:审核各阶段交付物、把控质量标准、团队协调
|
|
|
+- **技术**:验收最终交付物、确认技术规范和质量标准
|
|
|
+- **客服**:沟通客户需求、传递反馈信息、协调交付时间
|
|
|
|
|
|
### 1.4 四大执行子阶段
|
|
|
|
|
|
```mermaid
|
|
|
graph LR
|
|
|
- A[方案确认] --> B[建模]
|
|
|
- B --> C[软装]
|
|
|
- C --> D[渲染]
|
|
|
- D --> E[后期]
|
|
|
+ A[方案确认] --> B[白模建模]
|
|
|
+ B --> C[软装设计]
|
|
|
+ C --> D[渲染输出]
|
|
|
+ D --> E[后期处理]
|
|
|
E --> F[尾款结算]
|
|
|
|
|
|
style B fill:#e3f2fd
|
|
@@ -36,11 +37,226 @@ graph LR
|
|
|
style E fill:#f3e5f5
|
|
|
```
|
|
|
|
|
|
-## 2. 基于Product表的交付管理系统
|
|
|
+### 1.5 四大核心交付内容
|
|
|
+1. **白模建模**:空间结构建模、基础框架搭建
|
|
|
+2. **软装设计**:家具配置、材质选择、色彩搭配
|
|
|
+3. **渲染输出**:高清效果图、全景图、细节特写
|
|
|
+4. **后期处理**:色彩调整、效果优化、最终成品
|
|
|
|
|
|
-### 2.1 产品交付管理架构
|
|
|
+## 2. 基于Product表和ProjectTeam的交付管理系统
|
|
|
|
|
|
-#### 2.1.1 增强的DeliveryProcess接口
|
|
|
+### 2.1 交付执行管理架构
|
|
|
+
|
|
|
+#### 2.1.1 核心数据关系
|
|
|
+```typescript
|
|
|
+// Product产品表 - 空间设计产品核心
|
|
|
+interface Product {
|
|
|
+ objectId: string;
|
|
|
+ project: Pointer<Project>;
|
|
|
+ profile: Pointer<Profile>; // 负责设计师
|
|
|
+ productName: string; // 产品名称:李总主卧设计
|
|
|
+ productType: string; // 空间类型:bedroom
|
|
|
+ stage: string; // 当前阶段:modeling/softDecor/rendering/postProcess
|
|
|
+ status: string; // 状态:not_started/in_progress/completed
|
|
|
+ quotation: Object; // 产品报价信息
|
|
|
+ requirements: Object; // 设计需求
|
|
|
+ space: Object; // 空间信息
|
|
|
+ data: Object; // 扩展数据
|
|
|
+}
|
|
|
+
|
|
|
+// ProjectTeam项目团队表 - 团队成员管理
|
|
|
+interface ProjectTeam {
|
|
|
+ objectId: string;
|
|
|
+ project: Pointer<Project>;
|
|
|
+ profile: Pointer<Profile>;
|
|
|
+ role: string; // designer/renderer/team_leader/technical
|
|
|
+ workload: Number; // 工作量
|
|
|
+ specialties: string[]; // 专业技能
|
|
|
+ assignedProducts: string[]; // 分配的产品ID列表
|
|
|
+}
|
|
|
+
|
|
|
+// ProjectFile项目文件表 - 交付物管理
|
|
|
+interface ProjectFile {
|
|
|
+ objectId: string;
|
|
|
+ project: Pointer<Project>;
|
|
|
+ product: Pointer<Product>; // 关联的空间产品
|
|
|
+ attach: Pointer<Attachment>; // NovaStorage附件
|
|
|
+ category: string; // white_model/soft_decor/rendering/post_process
|
|
|
+ stage: string; // modeling/softDecor/rendering/postProcess
|
|
|
+ uploadedBy: Pointer<Profile>;
|
|
|
+ data: Object; // 文件元数据、审核状态等
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.1.2 基于NovaStorage的文件管理
|
|
|
+```typescript
|
|
|
+// 文件上传服务 - 使用NovaStorage
|
|
|
+class DeliveryFileService {
|
|
|
+ private storage: NovaStorage;
|
|
|
+
|
|
|
+ constructor() {
|
|
|
+ const cid = localStorage.getItem('company')!;
|
|
|
+ this.storage = NovaStorage.withCid(cid);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传交付文件
|
|
|
+ async uploadDeliveryFile(
|
|
|
+ projectId: string,
|
|
|
+ productId: string,
|
|
|
+ category: string,
|
|
|
+ file: File
|
|
|
+ ): Promise<{ attachment: Parse.Object; projectFile: Parse.Object }> {
|
|
|
+
|
|
|
+ // 1. 上传到NovaStorage,使用项目前缀
|
|
|
+ const novaFile: NovaFile = await this.storage.upload(file, {
|
|
|
+ prefixKey: `project/${projectId}/`,
|
|
|
+ onProgress: (progress) => {
|
|
|
+ console.log('Upload progress:', progress.total.percent);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. 创建Attachment记录
|
|
|
+ const attachment = new Parse.Object("Attachment");
|
|
|
+ attachment.set("name", novaFile.name);
|
|
|
+ attachment.set("url", novaFile.url);
|
|
|
+ attachment.set("size", novaFile.size);
|
|
|
+ attachment.set("mime", novaFile.type);
|
|
|
+ attachment.set("md5", novaFile.md5);
|
|
|
+ attachment.set("metadata", novaFile.metadata);
|
|
|
+ attachment.set("company", { __type: "Pointer", className: "Company", objectId: cid });
|
|
|
+ await attachment.save();
|
|
|
+
|
|
|
+ // 3. 创建ProjectFile记录
|
|
|
+ const projectFile = new Parse.Object("ProjectFile");
|
|
|
+ projectFile.set("project", { __type: "Pointer", className: "Project", objectId: projectId });
|
|
|
+ projectFile.set("product", { __type: "Pointer", className: "Product", objectId: productId });
|
|
|
+ projectFile.set("attach", { __type: "Pointer", className: "Attachment", objectId: attachment.id });
|
|
|
+ projectFile.set("category", category); // white_model/soft_decor/rendering/post_process
|
|
|
+ projectFile.set("stage", this.mapCategoryToStage(category));
|
|
|
+ projectFile.set("uploadedBy", { __type: "Pointer", className: "Profile", objectId: currentUser.id });
|
|
|
+ projectFile.set("data", {
|
|
|
+ reviewStatus: "pending",
|
|
|
+ uploadTime: new Date(),
|
|
|
+ fileSize: novaFile.size,
|
|
|
+ fileType: novaFile.type
|
|
|
+ });
|
|
|
+ await projectFile.save();
|
|
|
+
|
|
|
+ return { attachment, projectFile };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 映射category到stage
|
|
|
+ private mapCategoryToStage(category: string): string {
|
|
|
+ const mapping = {
|
|
|
+ 'white_model': 'modeling',
|
|
|
+ 'soft_decor': 'softDecor',
|
|
|
+ 'rendering': 'rendering',
|
|
|
+ 'post_process': 'postProcess'
|
|
|
+ };
|
|
|
+ return mapping[category] || 'modeling';
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 基于ProjectTeam的团队协作管理
|
|
|
+
|
|
|
+#### 2.2.1 团队分配策略服务
|
|
|
+```typescript
|
|
|
+class DeliveryTeamManagementService {
|
|
|
+ // 智能分配团队成员到产品阶段
|
|
|
+ async assignTeamMembers(
|
|
|
+ projectId: string,
|
|
|
+ productId: string,
|
|
|
+ stage: string
|
|
|
+ ): Promise<TeamAssignmentResult> {
|
|
|
+
|
|
|
+ // 1. 获取项目团队成员
|
|
|
+ const teamQuery = new Parse.Query("ProjectTeam");
|
|
|
+ teamQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
|
|
|
+ teamQuery.include("profile");
|
|
|
+ const teamMembers = await teamQuery.find();
|
|
|
+
|
|
|
+ // 2. 根据阶段和专业技能匹配
|
|
|
+ const stageRequirements = this.getStageRequirements(stage);
|
|
|
+ const suitableMembers = teamMembers.filter(member => {
|
|
|
+ const specialties = member.get("data")?.specialties || [];
|
|
|
+ const role = member.get("role");
|
|
|
+
|
|
|
+ return this.isMemberSuitableForStage(role, specialties, stage);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3. 考虑当前工作量
|
|
|
+ const availableMembers = await this.filterByWorkload(suitableMembers);
|
|
|
+
|
|
|
+ // 4. 分配主负责人和协作者
|
|
|
+ const assignment = this.createTeamAssignment(availableMembers, stageRequirements);
|
|
|
+
|
|
|
+ // 5. 更新Product的负责人
|
|
|
+ await this.updateProductAssignee(productId, assignment.primaryAssignee);
|
|
|
+
|
|
|
+ return assignment;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 阶段需求定义
|
|
|
+ private getStageRequirements(stage: string): StageRequirements {
|
|
|
+ const requirements = {
|
|
|
+ modeling: {
|
|
|
+ primaryRole: 'designer',
|
|
|
+ requiredSkills: ['3d_modeling', 'autocad', 'sketchup'],
|
|
|
+ minHours: 8,
|
|
|
+ maxHours: 40,
|
|
|
+ complexity: 'medium'
|
|
|
+ },
|
|
|
+ softDecor: {
|
|
|
+ primaryRole: 'designer',
|
|
|
+ requiredSkills: ['interior_design', 'material_selection', 'color_theory'],
|
|
|
+ minHours: 6,
|
|
|
+ maxHours: 32,
|
|
|
+ complexity: 'high'
|
|
|
+ },
|
|
|
+ rendering: {
|
|
|
+ primaryRole: 'renderer',
|
|
|
+ requiredSkills: ['3ds_max', 'vray', 'corona', 'lumion', 'photoshop'],
|
|
|
+ minHours: 4,
|
|
|
+ maxHours: 24,
|
|
|
+ complexity: 'high'
|
|
|
+ },
|
|
|
+ postProcess: {
|
|
|
+ primaryRole: 'designer',
|
|
|
+ requiredSkills: ['photoshop', 'lightroom', 'color_correction'],
|
|
|
+ minHours: 3,
|
|
|
+ maxHours: 16,
|
|
|
+ complexity: 'low'
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return requirements[stage] || requirements.modeling;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查成员是否适合阶段
|
|
|
+ private isMemberSuitableForStage(
|
|
|
+ role: string,
|
|
|
+ specialties: string[],
|
|
|
+ stage: string
|
|
|
+ ): boolean {
|
|
|
+ const requirements = this.getStageRequirements(stage);
|
|
|
+
|
|
|
+ // 角色匹配
|
|
|
+ if (role !== requirements.primaryRole && role !== 'team_leader') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 技能匹配
|
|
|
+ const hasRequiredSkills = requirements.requiredSkills.some(skill =>
|
|
|
+ specialties.includes(skill)
|
|
|
+ );
|
|
|
+
|
|
|
+ return hasRequiredSkills;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.2.2 增强的DeliveryProcess接口
|
|
|
```typescript
|
|
|
interface ProductDeliveryProcess {
|
|
|
id: string; // 流程ID: 'modeling' | 'softDecor' | 'rendering' | 'postProcess'
|
|
@@ -609,16 +825,831 @@ class BatchOperationService {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-## 3. 交付执行界面设计
|
|
|
+## 3. 完整的交付执行界面设计
|
|
|
+
|
|
|
+### 3.1 交付执行主界面布局
|
|
|
|
|
|
-### 3.1 产品交付管理主界面
|
|
|
+#### 3.1.1 主界面结构
|
|
|
```html
|
|
|
-<!-- 产品交付管理主界面 -->
|
|
|
-<div class="product-delivery-container">
|
|
|
- <!-- 阶段导航 -->
|
|
|
+<!-- 交付执行主界面 -->
|
|
|
+<div class="delivery-execution-container">
|
|
|
+ <!-- 顶部导航栏 -->
|
|
|
+ <div class="delivery-header">
|
|
|
+ <div class="project-info">
|
|
|
+ <h2>{{ project.title }}</h2>
|
|
|
+ <div class="project-meta">
|
|
|
+ <span class="customer">{{ project.customer.name }}</span>
|
|
|
+ <span class="deadline">截止:{{ formatDate(project.deadline) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="header-actions">
|
|
|
+ <button class="btn btn-primary" @click="showTeamManagement = true">
|
|
|
+ <i class="fas fa-users"></i>
|
|
|
+ 团队管理
|
|
|
+ </button>
|
|
|
+ <button class="btn btn-secondary" @click="exportDeliveryReport">
|
|
|
+ <i class="fas fa-download"></i>
|
|
|
+ 导出报告
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 四阶段导航 -->
|
|
|
<div class="stage-navigation">
|
|
|
<div class="stage-tabs">
|
|
|
<div v-for="stage in deliveryStages"
|
|
|
+ :key="stage.id"
|
|
|
+ class="stage-tab"
|
|
|
+ :class="{
|
|
|
+ active: activeStage === stage.id,
|
|
|
+ completed: stage.status === 'completed',
|
|
|
+ current: stage.status === 'in_progress'
|
|
|
+ }"
|
|
|
+ @click="switchStage(stage.id)">
|
|
|
+
|
|
|
+ <!-- 阶段图标和进度 -->
|
|
|
+ <div class="stage-icon">
|
|
|
+ <i :class="getStageIcon(stage.id)"></i>
|
|
|
+ <div class="stage-progress-ring" :style="getProgressStyle(stage.progress)"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 阶段信息 -->
|
|
|
+ <div class="stage-info">
|
|
|
+ <h4>{{ getStageDisplayName(stage.id) }}</h4>
|
|
|
+ <div class="progress-info">
|
|
|
+ <div class="progress-bar">
|
|
|
+ <div class="progress-fill" :style="{ width: stage.progress + '%' }"></div>
|
|
|
+ </div>
|
|
|
+ <span class="progress-text">{{ stage.progress }}%</span>
|
|
|
+ </div>
|
|
|
+ <div class="stage-meta">
|
|
|
+ <span class="product-count">{{ stage.productCount }}个产品</span>
|
|
|
+ <span class="time-remaining">{{ stage.remainingDays }}天</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 团队成员状态 -->
|
|
|
+ <div class="team-status">
|
|
|
+ <div class="team-avatars">
|
|
|
+ <img v-for="member in stage.teamMembers"
|
|
|
+ :key="member.id"
|
|
|
+ :src="member.avatar"
|
|
|
+ :title="member.name"
|
|
|
+ class="team-avatar" />
|
|
|
+ </div>
|
|
|
+ <span class="team-status-text" :class="stage.teamStatus">
|
|
|
+ {{ getTeamStatusText(stage.teamStatus) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 批量操作工具栏 -->
|
|
|
+ <div class="batch-operations-toolbar">
|
|
|
+ <div class="selection-controls">
|
|
|
+ <label class="checkbox-wrapper">
|
|
|
+ <input type="checkbox"
|
|
|
+ v-model="selectAllProducts"
|
|
|
+ @change="toggleSelectAll">
|
|
|
+ <span class="checkmark"></span>
|
|
|
+ <span>全选</span>
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <span v-if="selectedProducts.length > 0" class="selection-count">
|
|
|
+ 已选择 {{ selectedProducts.length }} 个产品
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="batch-actions" v-if="selectedProducts.length > 0">
|
|
|
+ <!-- 文件上传 -->
|
|
|
+ <div class="upload-group">
|
|
|
+ <button class="btn btn-primary"
|
|
|
+ @click="showBatchFileUpload = true">
|
|
|
+ <i class="fas fa-upload"></i>
|
|
|
+ 批量上传文件
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div class="upload-categories">
|
|
|
+ <button v-for="category in fileCategories"
|
|
|
+ :key="category.id"
|
|
|
+ class="btn-category"
|
|
|
+ :class="category.id"
|
|
|
+ @click="batchUploadCategory = category.id"
|
|
|
+ :title="category.description">
|
|
|
+ <i :class="category.icon"></i>
|
|
|
+ {{ category.name }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 团队分配 -->
|
|
|
+ <button class="btn btn-secondary"
|
|
|
+ @click="showTeamAssignment = true">
|
|
|
+ <i class="fas fa-user-plus"></i>
|
|
|
+ 分配团队成员
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 状态更新 -->
|
|
|
+ <button class="btn btn-info"
|
|
|
+ @click="showBatchStatusUpdate = true">
|
|
|
+ <i class="fas fa-edit"></i>
|
|
|
+ 批量更新状态
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 质量检查 -->
|
|
|
+ <button class="btn btn-warning"
|
|
|
+ @click="startBatchQualityCheck">
|
|
|
+ <i class="fas fa-check-circle"></i>
|
|
|
+ 批量质检
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 产品管理区域 -->
|
|
|
+ <div class="products-management-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <h3>{{ getStageDisplayName(activeStage) }} - 产品管理</h3>
|
|
|
+
|
|
|
+ <!-- 视图切换 -->
|
|
|
+ <div class="view-controls">
|
|
|
+ <div class="view-toggle">
|
|
|
+ <button v-for="view in viewOptions"
|
|
|
+ :key="view.id"
|
|
|
+ class="view-btn"
|
|
|
+ :class="{ active: currentView === view.id }"
|
|
|
+ @click="currentView = view.id"
|
|
|
+ :title="view.description">
|
|
|
+ <i :class="view.icon"></i>
|
|
|
+ {{ view.name }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 筛选和排序 -->
|
|
|
+ <div class="filter-controls">
|
|
|
+ <select v-model="filterByStatus" class="filter-select">
|
|
|
+ <option value="all">全部状态</option>
|
|
|
+ <option value="not_started">未开始</option>
|
|
|
+ <option value="in_progress">进行中</option>
|
|
|
+ <option value="awaiting_review">待审核</option>
|
|
|
+ <option value="completed">已完成</option>
|
|
|
+ </select>
|
|
|
+
|
|
|
+ <select v-model="sortBy" class="sort-select">
|
|
|
+ <option value="priority">优先级</option>
|
|
|
+ <option value="deadline">截止时间</option>
|
|
|
+ <option value="progress">进度</option>
|
|
|
+ <option value="assignee">负责人</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 产品网格视图 -->
|
|
|
+ <div v-if="currentView === 'grid'" class="products-grid">
|
|
|
+ <div v-for="product in filteredProducts"
|
|
|
+ :key="product.productId"
|
|
|
+ class="product-card"
|
|
|
+ :class="{
|
|
|
+ selected: selectedProducts.includes(product.productId),
|
|
|
+ expanded: product.isExpanded,
|
|
|
+ 'status-' + product.status
|
|
|
+ }"
|
|
|
+ @click="toggleProductSelection(product.productId, $event)">
|
|
|
+
|
|
|
+ <!-- 产品头部信息 -->
|
|
|
+ <div class="product-header">
|
|
|
+ <div class="product-basic-info">
|
|
|
+ <div class="product-icon">
|
|
|
+ <i :class="getProductTypeIcon(product.productType)"></i>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="product-details">
|
|
|
+ <h4 class="product-name">{{ product.productName }}</h4>
|
|
|
+ <span class="product-type">{{ getProductTypeName(product.productType) }}</span>
|
|
|
+ <div class="space-info">
|
|
|
+ <span>{{ product.space?.area }}m²</span>
|
|
|
+ <span v-if="product.space?.priority === 'high'" class="priority-badge high">
|
|
|
+ 高优先级
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 产品状态和操作 -->
|
|
|
+ <div class="product-status-section">
|
|
|
+ <div class="status-badge" :class="product.status">
|
|
|
+ {{ getStatusText(product.status) }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="product-actions">
|
|
|
+ <button class="action-btn"
|
|
|
+ @click.stop="toggleProductExpansion(product.productId)"
|
|
|
+ :title="product.isExpanded ? '收起' : '展开'">
|
|
|
+ <i :class="product.isExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <label class="checkbox-wrapper">
|
|
|
+ <input type="checkbox"
|
|
|
+ :value="product.productId"
|
|
|
+ v-model="selectedProducts"
|
|
|
+ @click.stop>
|
|
|
+ <span class="checkmark"></span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 进度和团队信息 -->
|
|
|
+ <div class="product-progress-section">
|
|
|
+ <div class="progress-overview">
|
|
|
+ <div class="progress-bar">
|
|
|
+ <div class="progress-fill"
|
|
|
+ :style="{ width: product.stageProgress[activeStage] + '%' }"
|
|
|
+ :class="getProgressClass(product.stageProgress[activeStage])">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <span class="progress-text">{{ product.stageProgress[activeStage] }}%</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="team-info">
|
|
|
+ <div class="assignee-info">
|
|
|
+ <img :src="product.assignee?.avatar"
|
|
|
+ class="assignee-avatar"
|
|
|
+ :title="product.assignee?.name" />
|
|
|
+ <span>{{ product.assignee?.name }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="time-info">
|
|
|
+ <span v-if="product.estimatedHours" class="time-estimated">
|
|
|
+ 预计 {{ product.estimatedHours }}h
|
|
|
+ </span>
|
|
|
+ <span v-if="product.actualHours" class="time-actual">
|
|
|
+ 实际 {{ product.actualHours }}h
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 文件管理预览 -->
|
|
|
+ <div class="files-preview">
|
|
|
+ <div class="file-categories-preview">
|
|
|
+ <div v-for="category in fileCategories"
|
|
|
+ :key="category.id"
|
|
|
+ class="category-preview"
|
|
|
+ :class="category.id">
|
|
|
+ <i :class="category.icon"></i>
|
|
|
+ <span class="file-count">{{ getFileCount(product, category.id) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="recent-files">
|
|
|
+ <div v-for="file in getRecentFiles(product)"
|
|
|
+ :key="file.id"
|
|
|
+ class="recent-file"
|
|
|
+ :title="file.name">
|
|
|
+ <i :class="getFileIcon(file.name)"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 展开的详细内容 -->
|
|
|
+ <div v-if="product.isExpanded" class="product-expanded-content">
|
|
|
+ <!-- 文件管理区域 -->
|
|
|
+ <div class="files-management">
|
|
|
+ <div class="files-header">
|
|
|
+ <h5>交付文件管理</h5>
|
|
|
+ <div class="file-upload-btn">
|
|
|
+ <input type="file"
|
|
|
+ :id="'file-upload-' + product.productId"
|
|
|
+ multiple
|
|
|
+ @change="handleFileUpload($event, product.productId)"
|
|
|
+ style="display: none;">
|
|
|
+ <button class="btn btn-sm btn-primary"
|
|
|
+ @click="$event.stopPropagation(); triggerFileUpload(product.productId)">
|
|
|
+ <i class="fas fa-plus"></i>
|
|
|
+ 上传文件
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分类文件列表 -->
|
|
|
+ <div class="files-by-category">
|
|
|
+ <div v-for="category in fileCategories"
|
|
|
+ :key="category.id"
|
|
|
+ class="category-section">
|
|
|
+ <div class="category-header">
|
|
|
+ <h6>
|
|
|
+ <i :class="category.icon"></i>
|
|
|
+ {{ category.name }}
|
|
|
+ <span class="count">({{ getFileCount(product, category.id) }})</span>
|
|
|
+ </h6>
|
|
|
+ <button class="btn-xs"
|
|
|
+ @click="expandCategory(product.productId, category.id)"
|
|
|
+ v-if="getFileCount(product, category.id) > 0">
|
|
|
+ <i :class="expandedCategories[product.productId + '-' + category.id] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="expandedCategories[product.productId + '-' + category.id] || getFileCount(product, category.id) <= 3"
|
|
|
+ class="files-list">
|
|
|
+ <div v-for="file in getFilesByCategory(product, category.id)"
|
|
|
+ :key="file.id"
|
|
|
+ class="file-item"
|
|
|
+ :class="{
|
|
|
+ 'status-approved': file.reviewStatus === 'approved',
|
|
|
+ 'status-pending': file.reviewStatus === 'pending',
|
|
|
+ 'status-rejected': file.reviewStatus === 'rejected'
|
|
|
+ }">
|
|
|
+ <div class="file-preview">
|
|
|
+ <img v-if="isImageFile(file.name)"
|
|
|
+ :src="file.url"
|
|
|
+ :alt="file.name"
|
|
|
+ @error="handleImageError" />
|
|
|
+ <i v-else :class="getFileIcon(file.name)"></i>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="file-info">
|
|
|
+ <span class="file-name" :title="file.name">{{ file.name }}</span>
|
|
|
+ <span class="file-meta">
|
|
|
+ {{ formatFileSize(file.size) }} • {{ formatDate(file.uploadTime) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="file-actions">
|
|
|
+ <button class="action-btn"
|
|
|
+ @click="previewFile(file)"
|
|
|
+ title="预览">
|
|
|
+ <i class="fas fa-eye"></i>
|
|
|
+ </button>
|
|
|
+ <button class="action-btn"
|
|
|
+ @click="downloadFile(file)"
|
|
|
+ title="下载">
|
|
|
+ <i class="fas fa-download"></i>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 审核操作 -->
|
|
|
+ <div v-if="canReviewFiles" class="review-actions">
|
|
|
+ <button class="action-btn approve"
|
|
|
+ @click="approveFile(file)"
|
|
|
+ title="审核通过"
|
|
|
+ :disabled="file.reviewStatus === 'approved'">
|
|
|
+ <i class="fas fa-check"></i>
|
|
|
+ </button>
|
|
|
+ <button class="action-btn reject"
|
|
|
+ @click="rejectFile(file)"
|
|
|
+ title="驳回"
|
|
|
+ :disabled="file.reviewStatus === 'rejected'">
|
|
|
+ <i class="fas fa-times"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 备注和日志 -->
|
|
|
+ <div class="notes-and-logs">
|
|
|
+ <div class="notes-section">
|
|
|
+ <h6>阶段备注</h6>
|
|
|
+ <textarea class="notes-textarea"
|
|
|
+ v-model="product.stageNotes[activeStage]"
|
|
|
+ @blur="updateStageNotes(product.productId)"
|
|
|
+ placeholder="添加当前阶段的备注信息..."
|
|
|
+ rows="3"></textarea>
|
|
|
+
|
|
|
+ <div class="notes-meta">
|
|
|
+ <span class="last-updated">
|
|
|
+ 最后更新: {{ formatDateTime(product.lastUpdated) }}
|
|
|
+ </span>
|
|
|
+ <span class="updated-by">{{ product.lastUpdatedBy }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="activity-logs">
|
|
|
+ <h6>活动日志</h6>
|
|
|
+ <div class="log-list">
|
|
|
+ <div v-for="log in getActivityLogs(product.productId)"
|
|
|
+ :key="log.id"
|
|
|
+ class="log-item">
|
|
|
+ <div class="log-time">{{ formatTime(log.timestamp) }}</div>
|
|
|
+ <div class="log-content">{{ log.message }}</div>
|
|
|
+ <div class="log-user">{{ log.user }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 产品列表视图(表格) -->
|
|
|
+ <div v-if="currentView === 'list'" class="products-table-container">
|
|
|
+ <table class="products-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th class="checkbox-col">
|
|
|
+ <input type="checkbox" v-model="selectAllProducts" @change="toggleSelectAll">
|
|
|
+ </th>
|
|
|
+ <th>产品名称</th>
|
|
|
+ <th>空间类型</th>
|
|
|
+ <th>负责人</th>
|
|
|
+ <th>进度</th>
|
|
|
+ <th>文件数量</th>
|
|
|
+ <th>状态</th>
|
|
|
+ <th>预计时间</th>
|
|
|
+ <th>操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="product in filteredProducts"
|
|
|
+ :key="product.productId"
|
|
|
+ class="product-row"
|
|
|
+ :class="{ 'status-' + product.status, selected: selectedProducts.includes(product.productId) }">
|
|
|
+ <td class="checkbox-col">
|
|
|
+ <input type="checkbox" :value="product.productId" v-model="selectedProducts">
|
|
|
+ </td>
|
|
|
+ <td>
|
|
|
+ <div class="product-cell">
|
|
|
+ <i :class="getProductTypeIcon(product.productType)"></i>
|
|
|
+ <span>{{ product.productName }}</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td>{{ getProductTypeName(product.productType) }}</td>
|
|
|
+ <td>
|
|
|
+ <div class="assignee-cell">
|
|
|
+ <img :src="product.assignee?.avatar" class="avatar-sm">
|
|
|
+ <span>{{ product.assignee?.name }}</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td>
|
|
|
+ <div class="progress-cell">
|
|
|
+ <div class="progress-bar-sm">
|
|
|
+ <div class="progress-fill" :style="{ width: product.stageProgress[activeStage] + '%' }"></div>
|
|
|
+ </div>
|
|
|
+ <span>{{ product.stageProgress[activeStage] }}%</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td>{{ getTotalFileCount(product) }}</td>
|
|
|
+ <td>
|
|
|
+ <span class="status-badge" :class="product.status">
|
|
|
+ {{ getStatusText(product.status) }}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td>{{ product.estimatedHours }}h</td>
|
|
|
+ <td>
|
|
|
+ <div class="action-buttons">
|
|
|
+ <button class="action-btn" @click="viewProductDetails(product)" title="查看详情">
|
|
|
+ <i class="fas fa-eye"></i>
|
|
|
+ </button>
|
|
|
+ <button class="action-btn" @click="editProduct(product)" title="编辑">
|
|
|
+ <i class="fas fa-edit"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 核心组件设计
|
|
|
+
|
|
|
+#### 3.2.1 文件上传组件
|
|
|
+```typescript
|
|
|
+// 智能文件上传组件
|
|
|
+@Component({
|
|
|
+ selector: 'app-delivery-file-upload',
|
|
|
+ standalone: true,
|
|
|
+ imports: [CommonModule, FormsModule, IonIcon]
|
|
|
+})
|
|
|
+export class DeliveryFileUploadComponent {
|
|
|
+ @Input() projectId: string = '';
|
|
|
+ @Input() productId: string = '';
|
|
|
+ @Input() category: 'white_model' | 'soft_decor' | 'rendering' | 'post_process' = 'white_model';
|
|
|
+ @Output() fileUploaded = new EventEmitter<UploadResult>();
|
|
|
+
|
|
|
+ uploading: boolean = false;
|
|
|
+ uploadProgress: number = 0;
|
|
|
+ dragOver: boolean = false;
|
|
|
+
|
|
|
+ constructor(private fileService: DeliveryFileService) {}
|
|
|
+
|
|
|
+ async onFileSelect(event: Event): Promise<void> {
|
|
|
+ const files = (event.target as HTMLInputElement).files;
|
|
|
+ if (!files?.length) return;
|
|
|
+
|
|
|
+ await this.uploadFiles(Array.from(files));
|
|
|
+ }
|
|
|
+
|
|
|
+ async uploadFiles(files: File[]): Promise<void> {
|
|
|
+ this.uploading = true;
|
|
|
+ this.uploadProgress = 0;
|
|
|
+
|
|
|
+ try {
|
|
|
+ for (const file of files) {
|
|
|
+ const result = await this.fileService.uploadDeliveryFile(
|
|
|
+ this.projectId,
|
|
|
+ this.productId,
|
|
|
+ this.category,
|
|
|
+ file
|
|
|
+ );
|
|
|
+
|
|
|
+ this.fileUploaded.emit({
|
|
|
+ file: result.projectFile,
|
|
|
+ attachment: result.attachment,
|
|
|
+ category: this.category
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('File upload failed:', error);
|
|
|
+ // 显示错误提示
|
|
|
+ } finally {
|
|
|
+ this.uploading = false;
|
|
|
+ this.uploadProgress = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getFileIcon(fileName: string): string {
|
|
|
+ const ext = fileName.split('.').pop()?.toLowerCase();
|
|
|
+ const iconMap = {
|
|
|
+ 'jpg': 'fas fa-image',
|
|
|
+ 'png': 'fas fa-image',
|
|
|
+ 'gif': 'fas fa-image',
|
|
|
+ 'pdf': 'fas fa-file-pdf',
|
|
|
+ 'dwg': 'fas fa-file-cad',
|
|
|
+ 'dxf': 'fas fa-file-cad',
|
|
|
+ 'skp': 'fas fa-file-cad',
|
|
|
+ 'max': 'fas fa-file-cad'
|
|
|
+ };
|
|
|
+ return iconMap[ext] || 'fas fa-file';
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.2.2 团队分配组件
|
|
|
+```typescript
|
|
|
+// 团队成员分配组件
|
|
|
+@Component({
|
|
|
+ selector: 'app-team-assignment',
|
|
|
+ standalone: true,
|
|
|
+ imports: [CommonModule, FormsModule, IonIcon]
|
|
|
+})
|
|
|
+export class TeamAssignmentComponent {
|
|
|
+ @Input() projectId: string = '';
|
|
|
+ @Input() stage: string = '';
|
|
|
+ @Input() selectedProducts: string[] = [];
|
|
|
+
|
|
|
+ availableTeamMembers: TeamMember[] = [];
|
|
|
+ assignmentSuggestions: AssignmentSuggestion[] = [];
|
|
|
+
|
|
|
+ constructor(private teamService: DeliveryTeamManagementService) {}
|
|
|
+
|
|
|
+ async loadTeamMembers(): Promise<void> {
|
|
|
+ const teamQuery = new Parse.Query("ProjectTeam");
|
|
|
+ teamQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: this.projectId });
|
|
|
+ teamQuery.include("profile");
|
|
|
+ const members = await teamQuery.find();
|
|
|
+
|
|
|
+ this.availableTeamMembers = members.map(member => ({
|
|
|
+ id: member.id,
|
|
|
+ name: member.get("profile").get("name"),
|
|
|
+ avatar: member.get("profile").get("data")?.avatar,
|
|
|
+ role: member.get("role"),
|
|
|
+ specialties: member.get("data")?.specialties || [],
|
|
|
+ currentWorkload: member.get("workload") || 0,
|
|
|
+ assignedProducts: member.get("data")?.assignedProducts || []
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ async generateAssignmentSuggestions(): Promise<void> {
|
|
|
+ const requirements = this.getStageRequirements(this.stage);
|
|
|
+
|
|
|
+ this.assignmentSuggestions = this.selectedProducts.map(productId => {
|
|
|
+ const suitableMembers = this.availableTeamMembers.filter(member =>
|
|
|
+ this.isMemberSuitable(member, requirements)
|
|
|
+ );
|
|
|
+
|
|
|
+ return {
|
|
|
+ productId,
|
|
|
+ productName: this.getProductName(productId),
|
|
|
+ primaryAssignee: this.selectBestMember(suitableMembers),
|
|
|
+ collaborators: this.selectCollaborators(suitableMembers, 2),
|
|
|
+ reasoning: this.generateAssignmentReasoning(suitableMembers[0], requirements)
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async applyAssignments(): Promise<void> {
|
|
|
+ for (const suggestion of this.assignmentSuggestions) {
|
|
|
+ await this.teamService.assignTeamMembers(
|
|
|
+ this.projectId,
|
|
|
+ suggestion.productId,
|
|
|
+ this.stage,
|
|
|
+ suggestion.primaryAssignee,
|
|
|
+ suggestion.collaborators
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.2.3 进度跟踪组件
|
|
|
+```typescript
|
|
|
+// 进度跟踪组件
|
|
|
+@Component({
|
|
|
+ selector: 'app-progress-tracker',
|
|
|
+ standalone: true,
|
|
|
+ imports: [CommonModule, FormsModule]
|
|
|
+})
|
|
|
+export class ProgressTrackerComponent {
|
|
|
+ @Input() productId: string = '';
|
|
|
+ @Input() stage: string = '';
|
|
|
+
|
|
|
+ progressData: ProgressData = {
|
|
|
+ current: 0,
|
|
|
+ target: 100,
|
|
|
+ milestones: [],
|
|
|
+ activities: []
|
|
|
+ };
|
|
|
+
|
|
|
+ constructor(private progressService: ProductProgressService) {}
|
|
|
+
|
|
|
+ async loadProgressData(): Promise<void> {
|
|
|
+ this.progressData = await this.progressService.getProductProgress(
|
|
|
+ this.productId,
|
|
|
+ this.stage
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ updateProgress(newProgress: number): void {
|
|
|
+ this.progressService.updateProductProgress(
|
|
|
+ this.productId,
|
|
|
+ this.stage,
|
|
|
+ { progress: newProgress }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ addMilestone(milestone: Milestone): void {
|
|
|
+ this.progressData.milestones.push(milestone);
|
|
|
+ this.progressService.addMilestone(this.productId, this.stage, milestone);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 交互流程设计
|
|
|
+
|
|
|
+#### 3.3.1 文件上传交互
|
|
|
+1. **拖拽上传**:支持拖拽文件到指定区域
|
|
|
+2. **批量选择**:支持多文件同时上传
|
|
|
+3. **进度显示**:实时显示上传进度和状态
|
|
|
+4. **自动分类**:根据文件类型自动选择category
|
|
|
+5. **重试机制**:上传失败时支持重试
|
|
|
+
|
|
|
+#### 3.3.2 团队协作交互
|
|
|
+1. **智能推荐**:根据阶段需求自动推荐合适的团队成员
|
|
|
+2. **工作量平衡**:考虑当前工作量,避免过度分配
|
|
|
+3. **技能匹配**:基于专业技能进行精准匹配
|
|
|
+4. **协作支持**:支持主负责人+协作者模式
|
|
|
+
|
|
|
+#### 3.3.3 审核流程交互
|
|
|
+1. **预览功能**:支持文件预览和快速查看
|
|
|
+2. **审核操作**:通过/驳回/重新审核
|
|
|
+3. **批注功能**:支持在文件上添加批注
|
|
|
+4. **版本管理**:支持文件版本比较和回滚
|
|
|
+
|
|
|
+### 3.4 数据结构定义
|
|
|
+
|
|
|
+#### 3.4.1 文件分类结构
|
|
|
+```typescript
|
|
|
+interface FileCategory {
|
|
|
+ id: 'white_model' | 'soft_decor' | 'rendering' | 'post_process';
|
|
|
+ name: string;
|
|
|
+ icon: string;
|
|
|
+ description: string;
|
|
|
+ allowedTypes: string[];
|
|
|
+ maxSize: number; // MB
|
|
|
+}
|
|
|
+
|
|
|
+export const FILE_CATEGORIES: FileCategory[] = [
|
|
|
+ {
|
|
|
+ id: 'white_model',
|
|
|
+ name: '白模建模',
|
|
|
+ icon: 'fas fa-cube',
|
|
|
+ description: '空间结构建模、基础框架',
|
|
|
+ allowedTypes: ['.skp', '.max', '.dwg', '.dxf'],
|
|
|
+ maxSize: 50
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'soft_decor',
|
|
|
+ name: '软装设计',
|
|
|
+ icon: 'fas fa-couch',
|
|
|
+ description: '家具配置、材质选择、色彩搭配',
|
|
|
+ allowedTypes: ['.jpg', '.png', '.pdf', '.psd'],
|
|
|
+ maxSize: 20
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'rendering',
|
|
|
+ name: '渲染输出',
|
|
|
+ icon: 'fas fa-image',
|
|
|
+ description: '高清效果图、全景图、细节特写',
|
|
|
+ allowedTypes: ['.jpg', '.png', '.tiff', '.hdr'],
|
|
|
+ maxSize: 30
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'post_process',
|
|
|
+ name: '后期处理',
|
|
|
+ icon: 'fas fa-magic',
|
|
|
+ description: '色彩调整、效果优化、最终成品',
|
|
|
+ allowedTypes: ['.jpg', '.png', '.psd', '.tiff'],
|
|
|
+ maxSize: 25
|
|
|
+ }
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.4.2 团队角色定义
|
|
|
+```typescript
|
|
|
+interface TeamRole {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ description: string;
|
|
|
+ requiredSkills: string[];
|
|
|
+ icon: string;
|
|
|
+ color: string;
|
|
|
+}
|
|
|
+
|
|
|
+export const TEAM_ROLES: TeamRole[] = [
|
|
|
+ {
|
|
|
+ id: 'designer',
|
|
|
+ name: '设计师',
|
|
|
+ description: '负责建模、软装、后期设计工作',
|
|
|
+ requiredSkills: ['autocad', 'sketchup', '3ds_max', 'photoshop'],
|
|
|
+ icon: 'fas fa-pencil-ruler',
|
|
|
+ color: '#007bff'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'renderer',
|
|
|
+ name: '渲染师',
|
|
|
+ description: '负责效果图渲染和输出',
|
|
|
+ requiredSkills: ['3ds_max', 'vray', 'corona', 'lumion'],
|
|
|
+ icon: 'fas fa-palette',
|
|
|
+ color: '#28a745'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'team_leader',
|
|
|
+ name: '组长',
|
|
|
+ description: '负责团队协调和质量把控',
|
|
|
+ requiredSkills: ['project_management', 'quality_control'],
|
|
|
+ icon: 'fas fa-users-cog',
|
|
|
+ color: '#ffc107'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'technical',
|
|
|
+ name: '技术',
|
|
|
+ description: '负责技术验收和质量标准',
|
|
|
+ requiredSkills: ['technical_review', 'standards'],
|
|
|
+ icon: 'fas fa-tools',
|
|
|
+ color: '#6c757d'
|
|
|
+ }
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+## 4. 技术实现要点
|
|
|
+
|
|
|
+### 4.1 性能优化策略
|
|
|
+- **虚拟滚动**:处理大量产品时的性能优化
|
|
|
+- **懒加载**:文件和详情按需加载
|
|
|
+- **缓存策略**:产品状态和文件信息缓存
|
|
|
+- **CDN加速**:文件预览和下载使用CDN
|
|
|
+
|
|
|
+### 4.2 用户体验优化
|
|
|
+- **响应式设计**:适配不同屏幕尺寸
|
|
|
+- **离线支持**:基本的离线操作和数据同步
|
|
|
+- **快捷键支持**:提高操作效率
|
|
|
+- **实时通知**:进度更新和状态变更通知
|
|
|
+
|
|
|
+### 4.3 数据一致性保障
|
|
|
+- **事务处理**:确保批量操作的原子性
|
|
|
+- **乐观锁**:防止并发修改冲突
|
|
|
+- **版本控制**:文件版本管理和回滚
|
|
|
+- **数据验证**:严格的前后端数据验证
|
|
|
+
|
|
|
+### 4.4 安全性考虑
|
|
|
+- **权限控制**:基于角色的访问控制
|
|
|
+- **文件安全**:文件上传安全检查和病毒扫描
|
|
|
+- **数据加密**:敏感数据传输和存储加密
|
|
|
+- **审计日志**:操作日志记录和追踪
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**文档版本**: v4.0 (完整交付执行设计)
|
|
|
+**最后更新**: 2025-10-21
|
|
|
+**维护者**: YSS Development Team
|
|
|
:key="stage.id"
|
|
|
class="stage-tab"
|
|
|
:class="{ active: activeStage === stage.id }"
|