交付执行阶段是项目管理流程的核心执行环节,包含建模、软装、渲染、后期四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。
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
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="product-delivery-container">
<!-- 阶段导航 -->
<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 }"
@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>
<!-- 批量操作工具栏 -->
<div class="batch-operations">
<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>
</div>
<!-- 产品列表 -->
<div class="product-delivery-section">
<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>
</div>
<!-- 协调报告 -->
<div class="coordination-report" v-if="coordinationReport">
<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>
</div>
</div>
<!-- 批量上传文件弹窗 -->
<div v-if="showBatchUpload" class="modal-overlay" @click="showBatchUpload = false">
<div class="modal-content" @click.stop>
<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>
</div>
<!-- 批量状态更新弹窗 -->
<div v-if="showBatchStatusUpdate" class="modal-overlay" @click="showBatchStatusUpdate = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>批量更新状态</h3>
<button class="close-button" @click="showBatchStatusUpdate = false">×</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>
</div>
</div>
文档版本: v3.0 (Product表统一空间管理) 最后更新: 2025-10-20 维护者: YSS Development Team