| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543 |
- 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<string, number>();
- 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<string, any>();
- 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 = `
- <svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
- <rect width="200" height="200" fill="white"/>
- <rect x="20" y="20" width="160" height="160" fill="black"/>
- <rect x="30" y="30" width="140" height="140" fill="white"/>
- <text x="100" y="105" text-anchor="middle" font-size="12" fill="black">案例二维码</text>
- <text x="100" y="125" text-anchor="middle" font-size="10" fill="gray">${caseItem.name}</text>
- </svg>
- `;
- // 使用 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);
- }
- }
|