交付执行阶段是项目管理流程的核心执行环节,包含白模建模、软装设计、渲染输出、后期处理四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。
graph LR
A[方案确认] --> B[白模建模]
B --> C[软装设计]
C --> D[渲染输出]
D --> E[后期处理]
E --> F[尾款结算]
style B fill:#e3f2fd
style C fill:#fff3e0
style D fill:#fce4ec
style E fill:#f3e5f5
// 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; // 文件元数据、审核状态等
}
// 文件上传服务 - 使用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';
}
}
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;
}
}
interface ProductDeliveryProcess {
id: string; // 流程ID: 'modeling' | 'softDecor' | 'rendering' | 'postProcess'
name: string; // 流程名称:建模/软装/渲染/后期
type: 'modeling' | 'softDecor' | 'rendering' | 'postProcess';
isExpanded: boolean; // 是否展开
// 产品管理(基于Product表)
products: ProductDelivery[];
content: Record<string, ProductContent>;
// 跨产品协调(基于Product表)
crossProductCoordination: {
dependencies: ProductDependency[]; // 产品依赖关系
batchOperations: BatchOperation[]; // 批量操作
qualityStandards: QualityStandard[]; // 质量标准
};
// 整体进度管理
overallProgress: {
total: number; // 总体进度
byProduct: Record<string, number>; // 各产品进度
byStage: Record<string, number>; // 各阶段进度
estimatedCompletion: Date;
};
}
interface ProductDelivery {
productId: string; // 产品ID(与Product.objectId关联)
productName: string; // 产品名称:李总主卧设计
productType: string; // 产品类型:bedroom
isExpanded: boolean; // 是否展开
order: number; // 排序顺序
priority: number; // 优先级
status: ProductStatus; // 产品状态
assigneeId: string; // 负责人ID(Product.profile)
estimatedHours: number; // 预估工时
actualHours: number; // 实际工时
// 产品报价信息
quotation: {
price: number;
breakdown: {
design: number;
modeling: number;
rendering: number;
softDecor: number;
};
status: string;
};
}
interface ProductContent {
// 产品文件(基于ProjectFile分类)
files: Array<{
id: string;
name: string;
url: string;
size?: string;
fileCategory: string; // 'delivery' | 'reference' | 'other'
reviewStatus?: 'pending' | 'approved' | 'rejected';
synced?: boolean; // 是否已同步到客户端
uploadTime: Date; // 上传时间
uploadedBy: string; // 上传人
}>;
progress: number; // 进度 0-100
status: 'pending' | 'in_progress' | 'completed' | 'approved';
notes: string; // 备注信息
lastUpdated: Date; // 最后更新时间
// 产品特定字段
productSpecific: {
// 建模阶段特有
modelingComplexity?: 'simple' | 'medium' | 'complex';
structuralConstraints?: string[];
// 软装阶段特有
furnitureList?: string[];
materialSelection?: string[];
// 渲染阶段特有
renderingQuality?: 'standard' | 'high' | 'ultra';
outputResolution?: string;
// 后期阶段特有
postProcessingTypes?: string[];
finalTouches?: string[];
};
}
class ProductDeliveryService {
// 获取项目的交付管理数据
async getProjectDeliveryData(projectId: string): Promise<ProductDeliveryProcess[]> {
// 1. 获取项目的所有产品
const productQuery = new Parse.Query("Product");
productQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
productQuery.include("profile");
productQuery.ascending("order");
const products = await productQuery.find();
// 2. 构建交付管理数据结构
const deliveryProcesses: ProductDeliveryProcess[] = [];
for (const stage of ['modeling', 'softDecor', 'rendering', 'postProcess']) {
const process: ProductDeliveryProcess = {
id: stage,
name: this.getStageName(stage),
type: stage as any,
isExpanded: false,
products: [],
content: {},
crossProductCoordination: {
dependencies: [],
batchOperations: [],
qualityStandards: []
},
overallProgress: {
total: 0,
byProduct: {},
byStage: {},
estimatedCompletion: new Date()
}
};
// 3. 为每个产品构建交付数据
for (const product of products) {
const productDelivery: ProductDelivery = {
productId: product.id,
productName: product.get("productName"),
productType: product.get("productType"),
isExpanded: false,
order: product.get("order") || 0,
priority: product.get("space")?.priority || 5,
status: product.get("status") || "not_started",
assigneeId: product.get("profile")?.id,
estimatedHours: product.get("estimatedDuration") * 8, // 天数转小时
actualHours: 0,
quotation: product.get("quotation") || {}
};
// 4. 获取产品的交付文件
const productFiles = await this.getProductDeliveryFiles(product.id, stage);
// 5. 构建产品内容
const productContent: ProductContent = {
files: productFiles.map(file => ({
id: file.id,
name: file.get("fileName"),
url: file.get("fileUrl"),
size: this.formatFileSize(file.get("fileSize")),
fileCategory: file.get("fileCategory"),
reviewStatus: file.get("data")?.reviewStatus || "pending",
uploadTime: file.get("createdAt"),
uploadedBy: file.get("uploadedBy")?.get("name")
})),
progress: this.calculateProductProgress(product, stage),
status: this.getProductStatus(product, stage),
notes: product.get("data")?.deliveryNotes || "",
lastUpdated: product.get("updatedAt"),
productSpecific: this.getProductSpecificFields(product, stage)
};
process.products.push(productDelivery);
process.content[product.id] = productContent;
process.overallProgress.byProduct[product.id] = productContent.progress;
}
deliveryProcesses.push(process);
}
return deliveryProcesses;
}
// 获取产品的交付文件
async getProductDeliveryFiles(productId: string, stage: string): Promise<Parse.Object[]> {
const fileQuery = new Parse.Query("ProjectFile");
fileQuery.equalTo("product", { __type: "Pointer", className: "Product", objectId: productId });
fileQuery.equalTo("fileCategory", "delivery");
fileQuery.equalTo("stage", stage);
fileQuery.notEqualTo("isDeleted", true);
fileQuery.descending("createdAt");
return await fileQuery.find();
}
// 计算产品进度
calculateProductProgress(product: Parse.Object, stage: string): number {
const productFiles = product.get("deliveryFiles") || [];
const stageFiles = productFiles.filter((file: any) => file.stage === stage);
if (stageFiles.length === 0) return 0;
const completedFiles = stageFiles.filter((file: any) =>
file.reviewStatus === "approved"
);
return Math.round((completedFiles.length / stageFiles.length) * 100);
}
// 获取产品状态
getProductStatus(product: Parse.Object, stage: string): string {
const currentStage = product.get("stage");
if (currentStage === stage) {
return product.get("status") || "not_started";
} else if (this.isStageCompleted(stage, currentStage)) {
return "completed";
} else {
return "pending";
}
}
// 获取产品特定字段
getProductSpecificFields(product: Parse.Object, stage: string): any {
const space = product.get("space") || {};
const baseFields = {
structuralConstraints: space.constraints || []
};
switch (stage) {
case "modeling":
return {
...baseFields,
modelingComplexity: space.complexity || "medium"
};
case "softDecor":
return {
...baseFields,
furnitureList: product.get("data")?.furnitureList || [],
materialSelection: product.get("data")?.materialSelection || []
};
case "rendering":
return {
...baseFields,
renderingQuality: product.get("data")?.renderingQuality || "standard",
outputResolution: product.get("data")?.outputResolution || "1920x1080"
};
case "postProcess":
return {
...baseFields,
postProcessingTypes: product.get("data")?.postProcessingTypes || [],
finalTouches: product.get("data")?.finalTouches || []
};
default:
return baseFields;
}
}
}
class ProductProgressService {
// 更新产品进度
async updateProductProgress(
productId: string,
stage: string,
progressData: ProgressUpdateData
): Promise<void> {
const productQuery = new Parse.Query("Product");
const product = await productQuery.get(productId);
// 更新产品状态
if (progressData.status) {
product.set("status", progressData.status);
}
if (progressData.stage) {
product.set("stage", progressData.stage);
}
// 更新产品数据
const currentData = product.get("data") || {};
const updatedData = {
...currentData,
[`${stage}Progress`]: progressData.progress,
[`${stage}Notes`]: progressData.notes,
[`${stage}LastUpdated`]: new Date()
};
product.set("data", updatedData);
await product.save();
// 触发进度更新事件
this.emitProgressUpdate(productId, stage, progressData);
}
// 批量更新产品进度
async batchUpdateProgress(
productIds: string[],
stage: string,
progressData: ProgressUpdateData
): Promise<void> {
const productQuery = new Parse.Query("Product");
productQuery.containedIn("objectId", productIds);
const products = await productQuery.find();
for (const product of products) {
await this.updateProductProgress(product.id, stage, progressData);
}
}
// 计算整体项目进度
calculateOverallProjectProgress(
deliveryProcesses: ProductDeliveryProcess[]
): ProjectProgress {
const totalProducts = deliveryProcesses.reduce((sum, process) => sum + process.products.length, 0);
if (totalProducts === 0) {
return { total: 0, byStage: {}, byProduct: {} };
}
const progress: ProjectProgress = {
total: 0,
byStage: {},
byProduct: {}
};
// 计算各阶段进度
deliveryProcesses.forEach(process => {
const stageProgress = process.products.reduce((sum, product) => {
return sum + (process.content[product.productId]?.progress || 0);
}, 0);
progress.byStage[process.id] = Math.round(stageProgress / process.products.length);
});
// 计算各产品进度
deliveryProcesses.forEach(process => {
process.products.forEach(product => {
const productProgress = process.content[product.productId]?.progress || 0;
progress.byProduct[product.productId] = productProgress;
});
});
// 计算总体进度
const stageSum = Object.values(progress.byStage).reduce((sum, val) => sum + val, 0);
progress.total = Math.round(stageSum / Object.keys(progress.byStage).length);
return progress;
}
}
class ProductDependencyManager {
// 分析产品间依赖关系
analyzeProductDependencies(products: Product[]): ProductDependency[] {
const dependencies: ProductDependency[] = [];
// 基于产品类型分析依赖
for (let i = 0; i < products.length; i++) {
for (let j = i + 1; j < products.length; j++) {
const fromProduct = products[i];
const toProduct = products[j];
const dependency = this.analyzeDependency(fromProduct, toProduct);
if (dependency) {
dependencies.push(dependency);
}
}
}
return dependencies;
}
// 分析两个产品间的依赖关系
private analyzeDependency(
fromProduct: Product,
toProduct: Product
): ProductDependency | null {
const fromType = fromProduct.productType;
const toType = toProduct.productType;
// 定义产品类型间的依赖关系
const dependencyRules: Record<string, { dependsOn: string[]; reason: string }> = {
'living_room': {
dependsOn: [],
reason: '客厅通常是风格参考的起点'
},
'dining_room': {
dependsOn: ['living_room'],
reason: '餐厅通常需要与客厅风格保持一致'
},
'kitchen': {
dependsOn: ['dining_room'],
reason: '厨房与餐厅在空间和功能上紧密相关'
},
'bedroom': {
dependsOn: ['living_room', 'corridor'],
reason: '卧室通常需要参考客厅的整体风格'
},
'bathroom': {
dependsOn: ['bedroom'],
reason: '卫生间与卧室在功能上紧密相关'
},
'balcony': {
dependsOn: ['living_room', 'bedroom'],
reason: '阳台通常连接客厅或卧室'
}
};
const rule = dependencyRules[toType];
if (rule && rule.dependsOn.includes(fromType)) {
return {
id: `${fromProduct.productId}-${toProduct.productId}`,
fromProductId: fromProduct.productId,
toProductId: toProduct.productId,
fromProductName: fromProduct.productName,
toProductName: toProduct.productName,
type: 'style_reference',
description: rule.reason,
priority: this.calculateDependencyPriority(fromType, toType),
confidence: 0.8
};
}
return null;
}
// 生成协调建议
generateCoordinationSuggestions(
dependencies: ProductDependency[]
): CoordinationSuggestion[] {
const suggestions: CoordinationSuggestion[] = [];
dependencies.forEach(dep => {
suggestions.push({
id: `coord-${dep.id}`,
dependencyId: dep.id,
type: 'style_consistency',
title: `风格一致性建议:${dep.fromProductName} → ${dep.toProductName}`,
description: `建议${dep.toProductName}在设计时参考${dep.fromProductName}的整体风格,确保空间的协调统一`,
actions: [
`参考${dep.fromProductName}的色彩方案`,
`保持材质选择的一致性`,
`考虑空间功能的连续性`
],
priority: dep.priority
});
});
return suggestions;
}
}
class BatchOperationService {
// 批量上传文件
async batchUploadFiles(
productIds: string[],
files: File[],
fileCategory: string,
stage: string,
uploaderId: string
): Promise<BatchUploadResult> {
const results: BatchUploadResult = {
successful: [],
failed: [],
summary: {
total: files.length,
uploaded: 0,
failed: 0
}
};
for (const file of files) {
try {
// 为每个产品上传文件
for (const productId of productIds) {
const projectFile = new Parse.Object("ProjectFile");
projectFile.set("product", { __type: "Pointer", className: "Product", objectId: productId });
projectFile.set("fileCategory", fileCategory);
projectFile.set("stage", stage);
projectFile.set("uploadedBy", { __type: "Pointer", className: "Profile", objectId: uploaderId });
// 设置文件基本信息
projectFile.set("fileName", file.name);
projectFile.set("fileSize", file.size);
// 上传文件到存储
const fileData = await this.uploadFile(file);
projectFile.set("fileUrl", fileData.url);
projectFile.set("attach", fileData.attachment);
await projectFile.save();
results.successful.push({
productId,
fileName: file.name,
fileId: projectFile.id
});
}
results.summary.uploaded++;
} catch (error) {
results.failed.push({
fileName: file.name,
error: error.message
});
results.summary.failed++;
}
}
return results;
}
// 批量更新产品状态
async batchUpdateProductStatus(
productIds: string[],
status: string,
stage: string
): Promise<void> {
const productQuery = new Parse.Query("Product");
productQuery.containedIn("objectId", productIds);
const products = await productQuery.find();
for (const product of products) {
product.set("status", status);
product.set("stage", stage);
await product.save();
}
}
// 批量发送审核通知
async batchSendReviewNotifications(
productIds: string[],
stage: string,
reviewerIds: string[]
): Promise<void> {
// 获取需要审核的产品
const productQuery = new Parse.Query("Product");
productQuery.containedIn("objectId", productIds);
productQuery.include("profile");
const products = await productQuery.find();
// 发送通知给审核人员
for (const reviewerId of reviewerIds) {
for (const product of products) {
await this.sendReviewNotification({
reviewerId,
productId: product.id,
productName: product.get("productName"),
stage,
designerId: product.get("profile")?.id
});
}
}
}
}
<!-- 交付执行主界面 -->
<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>
// 智能文件上传组件
@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';
}
}
// 团队成员分配组件
@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
);
}
}
}
// 进度跟踪组件
@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);
}
}
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
}
];
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'
}
];
文档版本: v4.0 (完整交付执行设计) 最后更新: 2025-10-21 维护者: YSS Development Team
:key="stage.id"
class="stage-tab"
:class="{ active: activeStage === stage.id }"
@click="switchStage(stage.id)">
<div class="stage-icon">
<i :class="getStageIcon(stage.id)"></i>
</div>
<div class="stage-info">
<h4>{{ stage.name }}</h4>
<div class="progress-bar">
<div class="progress-fill"
:style="{ width: stage.progress + '%' }"></div>
</div>
<span class="progress-text">{{ stage.progress }}%</span>
</div>
</div>
</div>
<div class="operation-group">
<button class="btn btn-primary"
@click="showBatchUpload = true"
:disabled="selectedProducts.length === 0">
<i class="fas fa-upload"></i>
批量上传文件
</button>
<button class="btn btn-secondary"
@click="showBatchStatusUpdate = true"
:disabled="selectedProducts.length === 0">
<i class="fas fa-edit"></i>
批量更新状态
</button>
<button class="btn btn-info"
@click="generateCoordinationReport">
<i class="fas fa-project-diagram"></i>
协调报告
</button>
</div>
<div class="selection-info">
<span v-if="selectedProducts.length > 0">
已选择 {{ selectedProducts.length }} 个产品
</span>
</div>
<h3>产品交付管理 - {{ getStageName(activeStage) }}</h3>
<div class="product-delivery-grid">
<div v-for="product in filteredProducts"
:key="product.productId"
class="product-delivery-card"
:class="{
active: selectedProducts.includes(product.productId),
expanded: product.isExpanded,
'status-' + product.status
}"
@click="toggleProductExpansion(product.productId)">
<!-- 产品基本信息 -->
<div class="product-header">
<div class="product-info">
<h4>{{ product.productName }}</h4>
<span class="product-type">{{ getProductTypeLabel(product.productType) }}</span>
<div class="designer-info">
<img :src="product.designerAvatar" class="designer-avatar" />
<span>{{ product.designerName }}</span>
</div>
</div>
<div class="product-status">
<span class="status-badge" :class="product.status">
{{ getStatusLabel(product.status) }}
</span>
</div>
<div class="product-actions">
<label class="checkbox-wrapper">
<input type="checkbox"
:value="product.productId"
v-model="selectedProducts"
@click.stop>
<span class="checkmark"></span>
</label>
</div>
</div>
<!-- 产品进度信息 -->
<div class="product-progress">
<div class="progress-stats">
<div class="stat-item">
<label>进度</label>
<span class="progress-value">{{ product.content.progress }}%</span>
</div>
<div class="stat-item">
<label>文件</label>
<span class="file-count">{{ product.content.files.length }}</span>
</div>
<div class="stat-item">
<label>工时</label>
<span class="hours">{{ product.actualHours }}/{{ product.estimatedHours }}h</span>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill"
:style="{ width: product.content.progress + '%' }"></div>
</div>
</div>
<!-- 产品报价信息 -->
<div class="product-quotation" v-if="product.quotation">
<div class="quotation-header">
<span class="quotation-price">¥{{ product.quotation.price.toLocaleString() }}</span>
<span class="quotation-status" :class="product.quotation.status">
{{ getQuotationStatusLabel(product.quotation.status) }}
</span>
</div>
<div class="quotation-breakdown">
<div v-for="(item, key) in product.quotation.breakdown"
:key="key"
class="breakdown-item">
<span class="breakdown-type">{{ getBreakdownTypeLabel(key) }}:</span>
<span class="breakdown-amount">¥{{ item.toLocaleString() }}</span>
</div>
</div>
</div>
<!-- 展开的详细内容 -->
<div v-if="product.isExpanded" class="product-details">
<!-- 文件管理 -->
<div class="file-management">
<h5>交付文件</h5>
<div class="file-list">
<div v-for="file in product.content.files"
:key="file.id"
class="file-item"
:class="'status-' + file.reviewStatus">
<div class="file-info">
<i :class="getFileIcon(file.name)"></i>
<div class="file-details">
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ file.size }}</span>
</div>
</div>
<div class="file-actions">
<button class="btn-sm"
@click="previewFile(file)"
title="预览">
<i class="fas fa-eye"></i>
</button>
<button class="btn-sm"
@click="downloadFile(file)"
title="下载">
<i class="fas fa-download"></i>
</button>
<button class="btn-sm btn-success"
v-if="file.reviewStatus === 'pending'"
@click="approveFile(file)"
title="审核通过">
<i class="fas fa-check"></i>
</button>
</div>
</div>
</div>
<!-- 文件上传按钮 -->
<div class="file-upload">
<button class="btn btn-sm btn-outline"
@click="showFileUpload = true">
<i class="fas fa-plus"></i>
上传文件
</button>
</div>
</div>
<!-- 产品特定字段 -->
<div class="product-specific-fields">
<h5>产品特定信息</h5>
<div class="specific-fields-grid">
<div v-for="(value, key) in product.content.productSpecific"
:key="key"
class="field-item">
<label>{{ getFieldLabel(key) }}:</label>
<span>{{ formatFieldValue(key, value) }}</span>
</div>
</div>
</div>
<!-- 备注和日志 -->
<div class="notes-section">
<h5>备注信息</h5>
<textarea class="notes-textarea"
v-model="product.content.notes"
@blur="updateProductNotes(product.productId)"
placeholder="添加备注信息..."></textarea>
<div class="update-log">
<small>最后更新: {{ formatDateTime(product.content.lastUpdated) }}</small>
</div>
</div>
</div>
</div>
</div>
<h3>产品协调报告</h3>
<div class="report-summary">
<div class="summary-item">
<label>依赖关系:</label>
<span>{{ coordinationReport.dependencies.length }}个</span>
</div>
<div class="summary-item">
<label>协调建议:</label>
<span>{{ coordinationReport.suggestions.length }}个</span>
</div>
</div>
<div class="report-details">
<!-- 依赖关系 -->
<div class="dependencies-section">
<h4>产品依赖关系</h4>
<div class="dependency-list">
<div v-for="dep in coordinationReport.dependencies"
:key="dep.id"
class="dependency-item">
<div class="dependency-arrow">
<i class="fas fa-arrow-right"></i>
</div>
<div class="dependency-content">
<span class="from-product">{{ dep.fromProductName }}</span>
<span class="dependency-type">→</span>
<span class="to-product">{{ dep.toProductName }}</span>
<div class="dependency-description">{{ dep.description }}</div>
</div>
</div>
</div>
</div>
<!-- 协调建议 -->
<div class="suggestions-section">
<h4>协调建议</h4>
<div class="suggestion-list">
<div v-for="suggestion in coordinationReport.suggestions"
:key="suggestion.id"
class="suggestion-item"
:class="'priority-' + suggestion.priority">
<div class="suggestion-header">
<i class="fas fa-lightbulb"></i>
<span class="suggestion-title">{{ suggestion.title }}</span>
</div>
<div class="suggestion-description">{{ suggestion.description }}</div>
<div class="suggestion-actions">
<span v-for="action in suggestion.actions" :key="action" class="action-item">
• {{ action }}
</span>
</div>
</div>
</div>
</div>
</div>
### 3.2 批量操作界面
html
<div class="modal-header">
<h3>批量上传文件</h3>
<button class="close-button" @click="showBatchUpload = false">×</button>
</div>
<div class="modal-body">
<div class="upload-area"
@dragover.prevent="onDragOver"
@drop.prevent="onDrop"
:class="{ 'drag-over': isDragOver }">
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<p>拖拽文件到此处或点击选择文件</p>
<input type="file"
multiple
ref="fileInput"
@change="onFileSelect"
style="display: none;">
<button class="btn btn-primary"
@click="$refs.fileInput.click()">
选择文件
</button>
</div>
<div class="file-list" v-if="selectedFiles.length > 0">
<h4>选择的文件 ({{ selectedFiles.length }})</h4>
<div class="file-items">
<div v-for="(file, index) in selectedFiles"
:key="index"
class="file-item">
<div class="file-info">
<i :class="getFileIcon(file.name)"></i>
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatFileSize(file.size) }}</span>
</div>
<button class="btn-sm btn-danger"
@click="removeFile(index)">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<div class="upload-options">
<div class="form-group">
<label>文件分类:</label>
<select v-model="batchUploadOptions.fileCategory">
<option value="delivery">交付物文件</option>
<option value="reference">参考文件</option>
<option value="document">文档资料</option>
</select>
</div>
<div class="form-group">
<label>目标阶段:</label>
<select v-model="batchUploadOptions.stage">
<option value="modeling">建模</option>
<option value="softDecor">软装</option>
<option value="rendering">渲染</option>
<option value="postProcess">后期</option>
</select>
</div>
<div class="form-group">
<label>目标产品:</label>
<select v-model="batchUploadOptions.targetProducts" multiple>
<option v-for="product in products"
:key="product.productId"
:value="product.productId">
{{ product.productName }}
</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary"
@click="showBatchUpload = false">
取消
</button>
<button class="btn btn-primary"
:disabled="selectedFiles.length === 0 || batchUploadOptions.targetProducts.length === 0"
@click="executeBatchUpload">
开始上传
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>更新状态:</label>
<select v-model="batchStatusOptions.status">
<option value="not_started">未开始</option>
<option value="in_progress">进行中</option>
<option value="awaiting_review">待审核</option>
<option value="completed">已完成</option>
</select>
</div>
<div class="form-group">
<label>更新阶段:</label>
<select v-model="batchStatusOptions.stage">
<option value="modeling">建模</option>
<option value="softDecor">软装</option>
<option value="rendering">渲染</option>
<option value="postProcess">后期</option>
</select>
</div>
<div class="form-group">
<label>备注:</label>
<textarea v-model="batchStatusOptions.notes"
placeholder="批量更新备注..."
rows="3"></textarea>
</div>
<div class="affected-products">
<h4>将更新的产品 ({{ selectedProducts.length }})</h4>
<div class="product-list">
<div v-for="product in selectedProducts"
:key="product.productId"
class="affected-product">
<span class="product-name">{{ product.productName }}</span>
<span class="product-type">{{ product.productType }}</span>
<span class="current-status">{{ product.status }}</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary"
@click="showBatchStatusUpdate = false">
取消
</button>
<button class="btn btn-primary"
@click="executeBatchStatusUpdate">
确认更新
</button>
</div>
文档版本: v3.0 (Product表统一空间管理) 最后更新: 2025-10-20 维护者: YSS Development Team