import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormControl } from '@angular/forms'; import { Router } from '@angular/router'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { CaseDetailPanelComponent, Case } from './case-detail-panel.component'; import { CaseService } from '../../../services/case.service'; import { ProjectAutoCaseService } from '../../admin/services/project-auto-case.service'; interface StatItem { id: string; name: string; shareCount: number; } interface StyleStat { style: string; count: number; } interface DesignerStat { designer: string; rate: number; } @Component({ selector: 'app-case-library', standalone: true, imports: [CommonModule, FormsModule, ReactiveFormsModule, CaseDetailPanelComponent], templateUrl: './case-library.html', styleUrls: ['./case-library.scss'] }) export class CaseLibrary implements OnInit, OnDestroy { // 表单控件 searchControl = new FormControl(''); projectTypeControl = new FormControl(''); spaceTypeControl = new FormControl(''); renderingLevelControl = new FormControl(''); styleControl = new FormControl(''); areaRangeControl = new FormControl(''); // 数据 cases: Case[] = []; filteredCases: Case[] = []; // 统计数据 topSharedCases: StatItem[] = []; favoriteStyles: StyleStat[] = []; designerRecommendations: DesignerStat[] = []; // 状态 showStatsPanel = false; selectedCase: Case | null = null; // 用于详情面板 selectedCaseForShare: Case | null = null; // 用于分享模态框 currentPage = 1; itemsPerPage = 12; // 每页显示12个案例(3列x4行) totalPages = 1; totalCount = 0; loading = false; // 用户类型(模拟) isInternalUser = true; // 可根据实际用户权限设置 // 行为追踪 private pageStartTime = Date.now(); private caseViewStartTimes = new Map(); constructor( private router: Router, private caseService: CaseService, private projectAutoCaseService: ProjectAutoCaseService ) {} /** * 计算封面图:优先使用交付执行上传的照片; * 过滤需求阶段图片(stage/requirements),否则回退到本地家装图片。 */ coverImageOf(caseItem: any): string { // 1. 检查是否有上传的交付执行图片(从 images 数组中查找真实上传的图片) const images = caseItem?.images || []; const uploadedImage = images.find((img: string) => img && img.startsWith('http') && !img.includes('placeholder') && !img.includes('unsplash.com') && // 排除默认的 Unsplash 图片 !img.endsWith('.svg') && // 只接受交付执行或其他实际上传文件,排除需求阶段图片 !img.includes('/stage/requirements/') && (img.includes('file-cloud.fmode.cn') || img.includes('storage')) ); if (uploadedImage) { return uploadedImage; } // 2. 检查 coverImage 是否是真实上传的图片(同样排除需求阶段图片) const coverUrl: string = caseItem?.coverImage || ''; const isRealUpload = !!( coverUrl && coverUrl.startsWith('http') && !coverUrl.includes('placeholder') && !coverUrl.includes('unsplash.com') && !coverUrl.endsWith('.svg') && !coverUrl.includes('/stage/requirements/') && (coverUrl.includes('file-cloud.fmode.cn') || coverUrl.includes('storage')) ); if (isRealUpload) { return coverUrl; } // 3. 使用默认家装图片 return '/assets/presets/家装图片.jpg'; } async ngOnInit() { // 补齐可能遗漏的案例(幂等,不会重复创建) try { const result = await this.projectAutoCaseService.backfillMissingCases(10); if (result.created > 0) { console.log(`✅ 案例库补齐:新增 ${result.created}/${result.scanned}`); } } catch (e) { console.warn('⚠️ 案例库补齐失败(忽略):', e); } this.loadCases(); // loadCases 会自动调用 loadStatistics this.setupFilterListeners(); this.setupBehaviorTracking(); } ngOnDestroy() { // 页面销毁时的清理工作 console.log('案例库页面销毁'); } private setupBehaviorTracking() { // 记录页面访问(可选) console.log('案例库页面已加载'); } /** * 加载案例列表 - 只显示已完成项目的案例 * * 案例库显示条件(由CaseService严格验证): * ✅ 项目必须进入"售后归档"阶段 * ✅ 完成以下任意一项即可进入案例库: * 1. 完成尾款结算(全部支付或部分支付) * 2. 完成客户评价(ProjectFeedback) * 3. 完成项目复盘(project.data.retrospective) */ async loadCases() { this.loading = true; try { const filters = this.getFilters(); const result = await this.caseService.findCases({ ...filters, isPublished: undefined, // 不筛选发布状态,显示所有已完成项目 page: this.currentPage, pageSize: this.itemsPerPage }); console.log(`📊 从Case表查询到 ${result.cases.length} 个有效案例(已在CaseService中验证)`); // 去重:同一项目只展示一个案例(按 projectId 去重,保留最新) const uniqueMap = new Map(); for (const c of result.cases) { if (!c.projectId) { // 无 projectId 的直接保留(极少数异常数据) uniqueMap.set(`__no_project__${c.id}`, c); continue; } if (!uniqueMap.has(c.projectId)) { uniqueMap.set(c.projectId, c); } else { // 保留 publishedAt 较新的 const prev = uniqueMap.get(c.projectId); const prevTime = new Date(prev.publishedAt || prev.createdAt || 0).getTime(); const curTime = new Date(c.publishedAt || c.createdAt || 0).getTime(); if (curTime >= prevTime) { uniqueMap.set(c.projectId, c); } } } const uniqueCases = Array.from(uniqueMap.values()); this.cases = uniqueCases; this.filteredCases = uniqueCases; this.totalCount = uniqueCases.length; this.totalPages = Math.ceil(uniqueCases.length / this.itemsPerPage) || 1; console.log(`✅ 案例库已加载 ${uniqueCases.length} 个有效案例(去重后)`); // 如果没有数据,显示友好提示 if (uniqueCases.length === 0) { console.log('💡 暂无已完成项目案例,请确保有项目已进入"售后归档"阶段并完成以下任意一项:'); console.log(' 1. 完成尾款结算(全部支付或部分支付)'); console.log(' 2. 完成客户评价(ProjectFeedback)'); console.log(' 3. 完成项目复盘(project.data.retrospective)'); } // 加载完案例后更新统计数据 await this.loadStatistics(); } catch (error) { console.error('❌ 加载案例列表失败:', error); // 即使出错也设置为空数组,避免页面崩溃 this.cases = []; this.filteredCases = []; this.totalCount = 0; this.totalPages = 1; this.showToast('加载案例列表失败,请检查数据库连接', 'error'); } finally { this.loading = false; } } /** * 加载统计数据 */ async loadStatistics() { try { // Top 5 分享案例 const sortedByShare = [...this.cases] .filter(c => c.shareCount && c.shareCount > 0) .sort((a, b) => (b.shareCount || 0) - (a.shareCount || 0)) .slice(0, 5); this.topSharedCases = sortedByShare.map(c => ({ id: c.id, name: c.name, shareCount: c.shareCount || 0 })); // 客户最喜欢风格 - 根据收藏数统计 const styleStats: { [key: string]: number } = {}; this.cases.forEach(c => { const tags = c.tag || c.styleTags || []; tags.forEach(tag => { styleStats[tag] = (styleStats[tag] || 0) + (c.favoriteCount || 0); }); }); this.favoriteStyles = Object.entries(styleStats) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([style, count]) => ({ style, count })); // 设计师作品推荐率 - 简化计算 const designerStats: { [key: string]: { total: number; recommended: number } } = {}; this.cases.forEach(c => { const designer = c.designer || '未知设计师'; if (!designerStats[designer]) { designerStats[designer] = { total: 0, recommended: 0 }; } designerStats[designer].total++; if (c.isExcellent) { designerStats[designer].recommended++; } }); this.designerRecommendations = Object.entries(designerStats) .map(([designer, stats]) => ({ designer, rate: stats.total > 0 ? Math.round((stats.recommended / stats.total) * 100) : 0 })) .sort((a, b) => b.rate - a.rate) .slice(0, 5); console.log('✅ 统计数据已加载:', { topSharedCases: this.topSharedCases.length, favoriteStyles: this.favoriteStyles.length, designerRecommendations: this.designerRecommendations.length }); } catch (error) { console.error('❌ 加载统计数据失败:', error); this.topSharedCases = []; this.favoriteStyles = []; this.designerRecommendations = []; } } /** * 获取当前筛选条件 */ private getFilters() { const filters: any = {}; const searchKeyword = this.searchControl.value?.trim(); if (searchKeyword) { filters.searchKeyword = searchKeyword; } const projectType = this.projectTypeControl.value; if (projectType) { filters.projectType = projectType; } const spaceType = this.spaceTypeControl.value; if (spaceType) { filters.spaceType = spaceType; } const renderingLevel = this.renderingLevelControl.value; if (renderingLevel) { filters.renderingLevel = renderingLevel; } const style = this.styleControl.value; if (style) { // 使用tags数组进行筛选 filters.tags = [style]; } const areaRange = this.areaRangeControl.value; if (areaRange) { // 解析面积范围 "0-50" -> { min: 0, max: 50 } const [min, max] = areaRange.split('-').map(Number); filters.areaRange = { min, max: max || undefined }; } return filters; } /** * 获取本月新增案例数 */ get monthlyNewCases(): number { const now = new Date(); const thisMonthStart = new Date(now.getFullYear(), now.getMonth(), 1); return this.cases.filter(c => { const createdAt = new Date(c.createdAt); return createdAt >= thisMonthStart; }).length; } private setupFilterListeners() { // 搜索框防抖 this.searchControl.valueChanges.pipe( debounceTime(300), distinctUntilChanged() ).subscribe(() => this.applyFilters()); // 其他筛选条件变化 [ this.projectTypeControl, this.spaceTypeControl, this.renderingLevelControl, this.styleControl, this.areaRangeControl ].forEach(control => { control.valueChanges.subscribe(() => this.applyFilters()); }); } applyFilters() { this.currentPage = 1; this.loadCases(); } resetFilters() { this.searchControl.setValue(''); this.projectTypeControl.setValue(''); this.spaceTypeControl.setValue(''); this.renderingLevelControl.setValue(''); this.styleControl.setValue(''); this.areaRangeControl.setValue(''); this.currentPage = 1; this.loadCases(); } get paginatedCases(): Case[] { return this.filteredCases; } nextPage() { if (this.currentPage < this.totalPages) { this.currentPage++; this.loadCases(); } } previousPage() { if (this.currentPage > 1) { this.currentPage--; this.loadCases(); } } async viewCaseDetail(caseItem: Case) { // 记录案例查看开始时间 this.caseViewStartTimes.set(caseItem.id, Date.now()); try { // 从数据库获取完整的案例详情 const detail = await this.caseService.getCase(caseItem.id); // 设置当前选中的案例以显示详情面板 this.selectedCase = detail; } catch (error) { console.error('查看案例详情失败:', error); this.showToast('查看案例详情失败', 'error'); } } // 跳转到独立的案例详情页面 async navigateToCaseDetail(caseItem: Case) { // 记录案例查看开始时间 this.caseViewStartTimes.set(caseItem.id, Date.now()); // 跳转到独立的案例详情页面 this.router.navigate(['/customer-service/case-detail', caseItem.id]); } closeCaseDetail() { this.selectedCase = null; } async toggleFavorite(caseItem: Case) { try { // 暂时只切换前端状态,后续可添加收藏功能 caseItem.isFavorite = !caseItem.isFavorite; if (caseItem.isFavorite) { this.showToast('已收藏该案例', 'success'); } else { this.showToast('已取消收藏', 'info'); } } catch (error) { console.error('切换收藏状态失败:', error); this.showToast('操作失败,请重试', 'error'); } } async shareCase(caseItem: Case) { this.selectedCaseForShare = caseItem; } closeShareModal() { this.selectedCaseForShare = null; } generateQRCode(caseItem: Case): string { // 实际项目中应使用二维码生成库,如 qrcode.js const qrData = this.generateShareLink(caseItem); // 这里返回一个模拟的二维码图片 const svgContent = ` 案例二维码 ${caseItem.name} `; // 使用 encodeURIComponent 来正确处理SVG内容 const encodedSVG = encodeURIComponent(svgContent); return `data:image/svg+xml;charset=utf-8,${encodedSVG}`; } generateShareLink(caseItem: Case): string { return `${window.location.origin}/customer-service/case-detail/${caseItem.id}?from=share&designer=${encodeURIComponent(caseItem.designer)}`; } async copyShareLink() { if (this.selectedCaseForShare) { try { const shareUrl = this.generateShareLink(this.selectedCaseForShare); await navigator.clipboard.writeText(shareUrl); this.showToast('链接已复制到剪贴板,可直接分享给客户!', 'success'); } catch (err) { this.showToast('复制失败,请手动复制链接', 'error'); console.error('复制链接失败:', err); } } } async shareToWeCom() { if (this.selectedCaseForShare) { try { const shareUrl = this.generateShareLink(this.selectedCaseForShare); // 实际项目中应集成企业微信分享SDK const shareData = { title: `${this.selectedCaseForShare.name} - ${this.selectedCaseForShare.designer}设计作品`, description: `${this.selectedCaseForShare.projectType} | ${this.selectedCaseForShare.spaceType} | ${this.selectedCaseForShare.area}㎡`, link: shareUrl, imgUrl: this.selectedCaseForShare.coverImage }; // 模拟企业微信分享 console.log('分享到企业微信:', shareData); this.showToast('已调用企业微信分享', 'success'); this.closeShareModal(); } catch (error) { console.error('分享失败:', error); this.showToast('分享失败,请重试', 'error'); } } } showStatistics() { this.showStatsPanel = !this.showStatsPanel; if (this.showStatsPanel) { this.loadStatistics(); } } // ========== 案例已完成项目展示功能(只读) ========== // 移除了手动创建、编辑、删除功能 // 案例由项目自动创建,只提供查看和分享功能 // 显示提示消息 private showToast(message: string, type: 'success' | 'error' | 'info' = 'info') { // 实际项目中应使用专业的Toast组件 const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; toast.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px; color: white; font-weight: 500; z-index: 10000; animation: slideIn 0.3s ease; background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'}; `; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease'; setTimeout(() => document.body.removeChild(toast), 300); }, 3000); } }