|
@@ -1,468 +1,499 @@
|
|
|
-import { Component, OnInit, signal, computed } from '@angular/core';
|
|
|
+import { Component, OnInit } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
-import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
|
|
|
-import { RouterModule, ActivatedRoute } from '@angular/router';
|
|
|
-import * as QRCode from 'qrcode';
|
|
|
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
|
+import { FormControl } from '@angular/forms';
|
|
|
+import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
|
|
|
|
|
-// 定义案例接口
|
|
|
-interface CaseItem {
|
|
|
+interface Case {
|
|
|
id: string;
|
|
|
name: string;
|
|
|
- category: string;
|
|
|
- style: string[];
|
|
|
- houseType: string;
|
|
|
- property: string;
|
|
|
- designer: string;
|
|
|
- area: number;
|
|
|
- createdAt: Date;
|
|
|
coverImage: string;
|
|
|
- detailImages: string[];
|
|
|
- isFavorite: boolean;
|
|
|
- tags: string[];
|
|
|
- views: number;
|
|
|
- description: string;
|
|
|
- // 新增字段
|
|
|
projectType: '工装' | '家装';
|
|
|
- subType: '平层' | '复式' | '别墅' | '自建房' | '其他';
|
|
|
- renderingLevel: '高端' | '中端';
|
|
|
+ spaceType: '平层' | '复式' | '别墅' | '自建房';
|
|
|
+ renderingLevel: '高端' | '中端' | '低端';
|
|
|
+ designer: string;
|
|
|
+ team: string;
|
|
|
+ area: number;
|
|
|
+ styleTags: string[];
|
|
|
+ customerReview?: string;
|
|
|
+ viewCount: number;
|
|
|
shareCount: number;
|
|
|
favoriteCount: number;
|
|
|
- likeCount: number;
|
|
|
- conversionRate: number; // 0-100
|
|
|
+ isFavorite: boolean;
|
|
|
+ isExcellent: boolean;
|
|
|
+ createdAt: Date;
|
|
|
+}
|
|
|
+
|
|
|
+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, RouterModule],
|
|
|
+ imports: [CommonModule, FormsModule, ReactiveFormsModule],
|
|
|
templateUrl: './case-library.html',
|
|
|
- styleUrls: ['./case-library.scss', '../customer-service-styles.scss']
|
|
|
+ styleUrls: ['./case-library.scss']
|
|
|
})
|
|
|
export class CaseLibrary implements OnInit {
|
|
|
- // 当前日期
|
|
|
- currentDate = new Date();
|
|
|
+ // 表单控件
|
|
|
+ searchControl = new FormControl('');
|
|
|
+ projectTypeControl = new FormControl('');
|
|
|
+ spaceTypeControl = new FormControl('');
|
|
|
+ renderingLevelControl = new FormControl('');
|
|
|
+ styleControl = new FormControl('');
|
|
|
+ areaRangeControl = new FormControl('');
|
|
|
+
|
|
|
+ // 数据
|
|
|
+ cases: Case[] = [];
|
|
|
+ filteredCases: Case[] = [];
|
|
|
|
|
|
- // 搜索关键词
|
|
|
- searchTerm = signal('');
|
|
|
+ // 统计数据
|
|
|
+ topSharedCases: StatItem[] = [];
|
|
|
+ favoriteStyles: StyleStat[] = [];
|
|
|
+ designerRecommendations: DesignerStat[] = [];
|
|
|
|
|
|
- // 分享弹窗
|
|
|
- showShareModal = signal(false);
|
|
|
- shareLink = signal('');
|
|
|
- qrDataUrl = signal('');
|
|
|
- sharedCaseId = signal<string | null>(null);
|
|
|
-
|
|
|
- // 筛选表单
|
|
|
- filterForm: FormGroup;
|
|
|
+ // 状态
|
|
|
+ showStatsPanel = false;
|
|
|
+ selectedCase: Case | null = null;
|
|
|
+ currentPage = 1;
|
|
|
+ itemsPerPage = 12;
|
|
|
+ totalPages = 1;
|
|
|
|
|
|
- // 案例列表
|
|
|
- cases = signal<CaseItem[]>([]);
|
|
|
+ // 用户类型(模拟)
|
|
|
+ isInternalUser = true; // 可根据实际用户权限设置
|
|
|
|
|
|
- // 筛选后的案例
|
|
|
- filteredCases = computed(() => {
|
|
|
- let result = [...this.cases()];
|
|
|
-
|
|
|
- // 应用搜索筛选
|
|
|
- if (this.searchTerm()) {
|
|
|
- const searchLower = this.searchTerm().toLowerCase();
|
|
|
- result = result.filter(caseItem =>
|
|
|
- caseItem.name.toLowerCase().includes(searchLower) ||
|
|
|
- caseItem.designer.toLowerCase().includes(searchLower) ||
|
|
|
- caseItem.description.toLowerCase().includes(searchLower) ||
|
|
|
- caseItem.tags.some(tag => tag.toLowerCase().includes(searchLower))
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- // 应用表单筛选
|
|
|
- const filters = this.filterForm.value as any;
|
|
|
-
|
|
|
- if (filters.style && filters.style.length > 0) {
|
|
|
- result = result.filter(caseItem =>
|
|
|
- caseItem.style.some((s: string) => filters.style.includes(s))
|
|
|
- );
|
|
|
- }
|
|
|
+ // 行为追踪
|
|
|
+ private pageStartTime = Date.now();
|
|
|
+ private caseViewStartTimes = new Map<string, number>();
|
|
|
+
|
|
|
+ ngOnInit() {
|
|
|
+ this.initializeData();
|
|
|
+ this.setupFilterListeners();
|
|
|
+ this.setupBehaviorTracking();
|
|
|
+ }
|
|
|
+
|
|
|
+ private setupBehaviorTracking() {
|
|
|
+ // 记录页面访问
|
|
|
+ this.recordBehavior('page_view', 'case-library', {
|
|
|
+ timestamp: new Date().toISOString()
|
|
|
+ });
|
|
|
|
|
|
- if (filters.houseType) {
|
|
|
- result = result.filter(caseItem => caseItem.houseType === filters.houseType);
|
|
|
- }
|
|
|
+ // 页面卸载时记录停留时长
|
|
|
+ window.addEventListener('beforeunload', () => {
|
|
|
+ const stayDuration = Date.now() - this.pageStartTime;
|
|
|
+ this.recordBehavior('page_stay', 'case-library', {
|
|
|
+ duration: stayDuration,
|
|
|
+ durationMinutes: Math.round(stayDuration / 60000 * 100) / 100
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private initializeData() {
|
|
|
+ // 模拟案例数据
|
|
|
+ this.cases = this.generateMockCases();
|
|
|
+ this.filteredCases = [...this.cases];
|
|
|
+ this.updatePagination();
|
|
|
|
|
|
+ // 初始化统计数据
|
|
|
+ this.initializeStats();
|
|
|
+ }
|
|
|
|
|
|
+ private setupFilterListeners() {
|
|
|
+ // 搜索框防抖
|
|
|
+ this.searchControl.valueChanges.pipe(
|
|
|
+ debounceTime(300),
|
|
|
+ distinctUntilChanged()
|
|
|
+ ).subscribe(() => this.applyFilters());
|
|
|
|
|
|
- if (filters.projectType) {
|
|
|
- result = result.filter(caseItem => caseItem.projectType === filters.projectType);
|
|
|
- }
|
|
|
+ // 其他筛选条件变化
|
|
|
+ [
|
|
|
+ this.projectTypeControl,
|
|
|
+ this.spaceTypeControl,
|
|
|
+ this.renderingLevelControl,
|
|
|
+ this.styleControl,
|
|
|
+ this.areaRangeControl
|
|
|
+ ].forEach(control => {
|
|
|
+ control.valueChanges.subscribe(() => this.applyFilters());
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- if (filters.subType) {
|
|
|
- result = result.filter(caseItem => caseItem.subType === filters.subType);
|
|
|
- }
|
|
|
+ private generateMockCases(): Case[] {
|
|
|
+ const mockCases: Case[] = [];
|
|
|
+ const projectTypes: ('工装' | '家装')[] = ['工装', '家装'];
|
|
|
+ const spaceTypes: ('平层' | '复式' | '别墅' | '自建房')[] = ['平层', '复式', '别墅', '自建房'];
|
|
|
+ const renderingLevels: ('高端' | '中端' | '低端')[] = ['高端', '中端', '低端'];
|
|
|
+ const styles = ['现代', '中式', '欧式', '美式', '日式', '工业风', '极简风', '轻奢风'];
|
|
|
+ const designers = ['张三', '李四', '王五', '赵六', '钱七'];
|
|
|
+ const teams = ['设计一组', '设计二组', '设计三组', '设计四组'];
|
|
|
|
|
|
- if (filters.renderingLevel) {
|
|
|
- result = result.filter(caseItem => caseItem.renderingLevel === filters.renderingLevel);
|
|
|
- }
|
|
|
+ for (let i = 1; i <= 50; i++) {
|
|
|
+ const projectType = projectTypes[Math.floor(Math.random() * projectTypes.length)];
|
|
|
+ const spaceType = spaceTypes[Math.floor(Math.random() * spaceTypes.length)];
|
|
|
+ const renderingLevel = renderingLevels[Math.floor(Math.random() * renderingLevels.length)];
|
|
|
+ const styleCount = Math.floor(Math.random() * 3) + 1;
|
|
|
+ const styleTags = Array.from({ length: styleCount }, () =>
|
|
|
+ styles[Math.floor(Math.random() * styles.length)]
|
|
|
+ );
|
|
|
|
|
|
- // 价格筛选(基于案例的预估价格范围)
|
|
|
- if (filters.price) {
|
|
|
- // 这里可以根据实际业务逻辑来筛选价格
|
|
|
- // 暂时保留所有案例,实际项目中需要根据案例的价格字段进行筛选
|
|
|
- }
|
|
|
-
|
|
|
- if (filters.favorite) {
|
|
|
- result = result.filter(caseItem => caseItem.isFavorite);
|
|
|
+ mockCases.push({
|
|
|
+ id: `case-${i}`,
|
|
|
+ name: `${projectType}${spaceType}设计案例 ${i}`,
|
|
|
+ coverImage: this.generatePlaceholderImage(400, 300, i),
|
|
|
+ projectType,
|
|
|
+ spaceType,
|
|
|
+ renderingLevel,
|
|
|
+ designer: designers[Math.floor(Math.random() * designers.length)],
|
|
|
+ team: teams[Math.floor(Math.random() * teams.length)],
|
|
|
+ area: Math.floor(Math.random() * 200) + 50,
|
|
|
+ styleTags: [...new Set(styleTags)], // 去重
|
|
|
+ customerReview: Math.random() > 0.3 ? `客户非常满意这次${projectType}设计,${spaceType}空间利用得很合理` : undefined,
|
|
|
+ viewCount: Math.floor(Math.random() * 1000),
|
|
|
+ shareCount: Math.floor(Math.random() * 100),
|
|
|
+ favoriteCount: Math.floor(Math.random() * 50),
|
|
|
+ isFavorite: Math.random() > 0.7,
|
|
|
+ isExcellent: Math.random() > 0.5,
|
|
|
+ createdAt: new Date(Date.now() - Math.floor(Math.random() * 365 * 24 * 60 * 60 * 1000))
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- // 排序
|
|
|
- if (filters.sortBy) {
|
|
|
- switch (filters.sortBy) {
|
|
|
- case 'views':
|
|
|
- result.sort((a, b) => b.views - a.views);
|
|
|
- break;
|
|
|
- case 'shares':
|
|
|
- result.sort((a, b) => b.shareCount - a.shareCount);
|
|
|
- break;
|
|
|
- case 'conversion':
|
|
|
- result.sort((a, b) => b.conversionRate - a.conversionRate);
|
|
|
- break;
|
|
|
- case 'createdAt':
|
|
|
- result.sort((a, b) => +b.createdAt - +a.createdAt);
|
|
|
- break;
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 默认按创建时间倒序
|
|
|
- result.sort((a, b) => +b.createdAt - +a.createdAt);
|
|
|
- }
|
|
|
+ return mockCases;
|
|
|
+ }
|
|
|
+
|
|
|
+ private generatePlaceholderImage(width: number, height: number, seed: number): string {
|
|
|
+ return `https://picsum.photos/seed/${seed}/${width}/${height}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ private initializeStats() {
|
|
|
+ // Top5 分享案例
|
|
|
+ this.topSharedCases = this.cases
|
|
|
+ .sort((a, b) => b.shareCount - a.shareCount)
|
|
|
+ .slice(0, 5)
|
|
|
+ .map(caseItem => ({
|
|
|
+ id: caseItem.id,
|
|
|
+ name: caseItem.name,
|
|
|
+ shareCount: caseItem.shareCount
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 客户最喜欢案例风格
|
|
|
+ const styleCounts = new Map<string, number>();
|
|
|
+ this.cases.forEach(caseItem => {
|
|
|
+ caseItem.styleTags.forEach(style => {
|
|
|
+ styleCounts.set(style, (styleCounts.get(style) || 0) + caseItem.favoriteCount);
|
|
|
+ });
|
|
|
+ });
|
|
|
|
|
|
- return result;
|
|
|
- });
|
|
|
-
|
|
|
- // 显示筛选面板
|
|
|
- showFilterPanel = signal(false);
|
|
|
-
|
|
|
- // 当前查看的案例详情
|
|
|
- selectedCase = signal<CaseItem | null>(null);
|
|
|
-
|
|
|
- // 分页信息
|
|
|
- currentPage = signal(1);
|
|
|
- itemsPerPage = signal(12);
|
|
|
-
|
|
|
- // 分页后的案例
|
|
|
- paginatedCases = computed(() => {
|
|
|
- const startIndex = (this.currentPage() - 1) * this.itemsPerPage();
|
|
|
- return this.filteredCases().slice(startIndex, startIndex + this.itemsPerPage());
|
|
|
- });
|
|
|
-
|
|
|
- // 总页数
|
|
|
- totalPages = computed(() => {
|
|
|
- return Math.ceil(this.filteredCases().length / this.itemsPerPage());
|
|
|
- });
|
|
|
-
|
|
|
- // 筛选选项
|
|
|
- styleOptions = ['现代简约', '北欧风', '工业风', '新中式', '法式轻奢', '日式', '美式', '混搭'];
|
|
|
- houseTypeOptions = ['一室一厅', '两室一厅', '两室两厅', '三室一厅', '三室两厅', '四室两厅', '复式', '别墅', '其他'];
|
|
|
- projectTypeOptions: Array<CaseItem['projectType']> = ['工装', '家装'];
|
|
|
- subTypeOptions: Array<CaseItem['subType']> = ['平层', '复式', '别墅', '自建房', '其他'];
|
|
|
- renderingLevelOptions: Array<CaseItem['renderingLevel']> = ['高端', '中端'];
|
|
|
- priceOptions = ['5万以下', '5-10万', '10-20万', '20-30万', '30-50万', '50万以上'];
|
|
|
- sortOptions = [
|
|
|
- { label: '最新上传', value: 'createdAt' },
|
|
|
- { label: '浏览最多', value: 'views' },
|
|
|
- { label: '分享最多', value: 'shares' },
|
|
|
- { label: '转化率最高', value: 'conversion' }
|
|
|
- ];
|
|
|
-
|
|
|
- constructor(private fb: FormBuilder, private route: ActivatedRoute) {
|
|
|
- // 初始化筛选表单
|
|
|
- this.filterForm = this.fb.group({
|
|
|
- style: [[]],
|
|
|
- houseType: [''],
|
|
|
- projectType: [''],
|
|
|
- subType: [''],
|
|
|
- renderingLevel: [''],
|
|
|
- price: [''],
|
|
|
- favorite: [false],
|
|
|
- sortBy: ['createdAt']
|
|
|
+ this.favoriteStyles = Array.from(styleCounts.entries())
|
|
|
+ .sort(([, a], [, b]) => b - a)
|
|
|
+ .slice(0, 5)
|
|
|
+ .map(([style, count]) => ({ style, count }));
|
|
|
+
|
|
|
+ // 设计师作品推荐率
|
|
|
+ const designerStats = new Map<string, { total: number; excellent: number }>();
|
|
|
+ this.cases.forEach(caseItem => {
|
|
|
+ const stats = designerStats.get(caseItem.designer) || { total: 0, excellent: 0 };
|
|
|
+ stats.total++;
|
|
|
+ if (caseItem.isExcellent) stats.excellent++;
|
|
|
+ designerStats.set(caseItem.designer, stats);
|
|
|
});
|
|
|
+
|
|
|
+ this.designerRecommendations = Array.from(designerStats.entries())
|
|
|
+ .map(([designer, stats]) => ({
|
|
|
+ designer,
|
|
|
+ rate: Math.round((stats.excellent / stats.total) * 100)
|
|
|
+ }))
|
|
|
+ .sort((a, b) => b.rate - a.rate)
|
|
|
+ .slice(0, 5);
|
|
|
}
|
|
|
-
|
|
|
- ngOnInit(): void {
|
|
|
- // 加载模拟案例数据
|
|
|
- this.loadCases();
|
|
|
-
|
|
|
- // 读取分享链接参数并打开对应案例详情
|
|
|
- this.route.queryParamMap.subscribe(params => {
|
|
|
- const caseId = params.get('case');
|
|
|
- if (caseId) {
|
|
|
- const item = this.cases().find(c => c.id === caseId);
|
|
|
- if (item) {
|
|
|
- this.viewCaseDetails(item);
|
|
|
+
|
|
|
+ applyFilters() {
|
|
|
+ const searchTerm = this.searchControl.value?.toLowerCase() || '';
|
|
|
+ const projectType = this.projectTypeControl.value;
|
|
|
+ const spaceType = this.spaceTypeControl.value;
|
|
|
+ const renderingLevel = this.renderingLevelControl.value;
|
|
|
+ const style = this.styleControl.value;
|
|
|
+ const areaRange = this.areaRangeControl.value;
|
|
|
+
|
|
|
+ this.filteredCases = this.cases.filter(caseItem => {
|
|
|
+ // 搜索条件
|
|
|
+ if (searchTerm && !(
|
|
|
+ caseItem.name.toLowerCase().includes(searchTerm) ||
|
|
|
+ caseItem.designer.toLowerCase().includes(searchTerm) ||
|
|
|
+ caseItem.styleTags.some(tag => tag.toLowerCase().includes(searchTerm))
|
|
|
+ )) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 项目类型筛选
|
|
|
+ if (projectType && caseItem.projectType !== projectType) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 空间类型筛选
|
|
|
+ if (spaceType && caseItem.spaceType !== spaceType) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染水平筛选
|
|
|
+ if (renderingLevel && caseItem.renderingLevel !== renderingLevel) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 风格筛选
|
|
|
+ if (style && !caseItem.styleTags.includes(style)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 面积范围筛选
|
|
|
+ if (areaRange) {
|
|
|
+ const [min, max] = areaRange.split('-').map(Number);
|
|
|
+ if (max === undefined) {
|
|
|
+ if (caseItem.area < min) return false;
|
|
|
+ } else if (caseItem.area < min || caseItem.area > max) {
|
|
|
+ return false;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ return true;
|
|
|
});
|
|
|
+
|
|
|
+ this.currentPage = 1;
|
|
|
+ this.updatePagination();
|
|
|
}
|
|
|
-
|
|
|
- // 加载案例数据
|
|
|
- loadCases(): void {
|
|
|
- // 本地占位图集合
|
|
|
- const LOCAL_IMAGES = [
|
|
|
- '/assets/images/portfolio-1.svg',
|
|
|
- '/assets/images/portfolio-2.svg',
|
|
|
- '/assets/images/portfolio-3.svg',
|
|
|
- '/assets/images/portfolio-4.svg'
|
|
|
- ];
|
|
|
-
|
|
|
- const rand = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
|
|
- const pick = <T,>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
|
|
|
-
|
|
|
- // 模拟API请求获取案例数据
|
|
|
- const mockCases: CaseItem[] = Array.from({ length: 24 }, (_, i) => {
|
|
|
- const cover = LOCAL_IMAGES[i % LOCAL_IMAGES.length];
|
|
|
- const details = Array.from({ length: 4 }, (_, j) => LOCAL_IMAGES[(i + j) % LOCAL_IMAGES.length]);
|
|
|
- const projectType = pick(this.projectTypeOptions);
|
|
|
- const subType = pick(this.subTypeOptions);
|
|
|
- const renderingLevel = pick(this.renderingLevelOptions);
|
|
|
- const createdAt = new Date(Date.now() - rand(0, 365) * 24 * 60 * 60 * 1000);
|
|
|
- const views = rand(100, 3000);
|
|
|
- const shareCount = rand(10, 500);
|
|
|
- const favoriteCount = rand(5, 400);
|
|
|
- const likeCount = rand(10, 800);
|
|
|
- const conversionRate = Number((Math.random() * 30 + 5).toFixed(1)); // 5% - 35%
|
|
|
-
|
|
|
- return {
|
|
|
- id: `case-${i + 1}`,
|
|
|
- name: `${pick(this.styleOptions)}风格 ${pick(this.houseTypeOptions)}设计`,
|
|
|
- category: pick(['客厅', '卧室', '厨房', '浴室', '书房', '餐厅']),
|
|
|
- style: [pick(this.styleOptions)],
|
|
|
- houseType: pick(this.houseTypeOptions),
|
|
|
- property: pick(['万科', '绿城', '保利', '龙湖', '融创']),
|
|
|
- designer: pick(['张设计', '李设计', '王设计', '赵设计', '陈设计']),
|
|
|
- area: rand(50, 150),
|
|
|
- createdAt,
|
|
|
- coverImage: cover,
|
|
|
- detailImages: details,
|
|
|
- isFavorite: Math.random() > 0.7,
|
|
|
- tags: ['热门', '精选', '新上传', '高性价比', '业主好评'].filter(() => Math.random() > 0.5),
|
|
|
- views,
|
|
|
- description: '这是一个精美的' + pick(['现代简约', '北欧风', '新中式']) + '风格设计案例,融合了功能性与美学,为客户打造了舒适宜人的居住环境。',
|
|
|
- projectType,
|
|
|
- subType,
|
|
|
- renderingLevel,
|
|
|
- shareCount,
|
|
|
- favoriteCount,
|
|
|
- likeCount,
|
|
|
- conversionRate
|
|
|
- };
|
|
|
- });
|
|
|
+
|
|
|
+ resetFilters() {
|
|
|
+ this.searchControl.setValue('');
|
|
|
+ this.projectTypeControl.setValue('');
|
|
|
+ this.spaceTypeControl.setValue('');
|
|
|
+ this.renderingLevelControl.setValue('');
|
|
|
+ this.styleControl.setValue('');
|
|
|
+ this.areaRangeControl.setValue('');
|
|
|
|
|
|
- this.cases.set(mockCases);
|
|
|
+ this.filteredCases = [...this.cases];
|
|
|
+ this.currentPage = 1;
|
|
|
+ this.updatePagination();
|
|
|
}
|
|
|
-
|
|
|
- // 切换收藏状态(同时更新收藏计数)
|
|
|
- toggleFavorite(caseId: string): void {
|
|
|
- this.cases.set(
|
|
|
- this.cases().map(caseItem => {
|
|
|
- if (caseItem.id === caseId) {
|
|
|
- const isFav = !caseItem.isFavorite;
|
|
|
- const favoriteCount = Math.max(0, caseItem.favoriteCount + (isFav ? 1 : -1));
|
|
|
- return { ...caseItem, isFavorite: isFav, favoriteCount };
|
|
|
- }
|
|
|
- return caseItem;
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- // 查看案例详情(增加浏览量)
|
|
|
- viewCaseDetails(caseItem: CaseItem): void {
|
|
|
- this.selectedCase.set(caseItem);
|
|
|
- // 增加浏览量
|
|
|
- this.cases.set(
|
|
|
- this.cases().map(item =>
|
|
|
- item.id === caseItem.id
|
|
|
- ? { ...item, views: item.views + 1 }
|
|
|
- : item
|
|
|
- )
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- // 关闭案例详情
|
|
|
- closeCaseDetails(): void {
|
|
|
- this.selectedCase.set(null);
|
|
|
- }
|
|
|
-
|
|
|
- // 分享案例:生成链接、复制并展示弹窗,同时更新分享计数
|
|
|
- async shareCase(caseId: string): Promise<void> {
|
|
|
- const link = this.getShareLink(caseId);
|
|
|
- this.shareLink.set(link);
|
|
|
- this.showShareModal.set(true);
|
|
|
- this.sharedCaseId.set(caseId);
|
|
|
-
|
|
|
- // 生成二维码
|
|
|
- await this.generateQrCode(link);
|
|
|
-
|
|
|
- // 分享计数 +1
|
|
|
- this.cases.set(
|
|
|
- this.cases().map(item => item.id === caseId ? { ...item, shareCount: item.shareCount + 1 } : item)
|
|
|
- );
|
|
|
-
|
|
|
- // 尝试自动复制
|
|
|
- try {
|
|
|
- await navigator.clipboard.writeText(link);
|
|
|
- } catch {
|
|
|
- // 忽略复制失败(例如非安全上下文),用户可手动复制
|
|
|
+
|
|
|
+ updatePagination() {
|
|
|
+ this.totalPages = Math.ceil(this.filteredCases.length / this.itemsPerPage);
|
|
|
+ if (this.currentPage > this.totalPages) {
|
|
|
+ this.currentPage = this.totalPages || 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- getShareLink(caseId: string): string {
|
|
|
- const base = window.location.origin;
|
|
|
- return `${base}/customer-service/case-library?case=${encodeURIComponent(caseId)}`;
|
|
|
+ get paginatedCases(): Case[] {
|
|
|
+ const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
|
|
+ return this.filteredCases.slice(startIndex, startIndex + this.itemsPerPage);
|
|
|
}
|
|
|
|
|
|
- async copyShareLink(): Promise<void> {
|
|
|
- const link = this.shareLink();
|
|
|
- try {
|
|
|
- await navigator.clipboard.writeText(link);
|
|
|
- alert('链接已复制到剪贴板');
|
|
|
- } catch {
|
|
|
- alert('复制失败,请手动选择链接复制');
|
|
|
+ nextPage() {
|
|
|
+ if (this.currentPage < this.totalPages) {
|
|
|
+ this.currentPage++;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 生成二维码
|
|
|
- private async generateQrCode(text: string): Promise<void> {
|
|
|
- try {
|
|
|
- const url = await QRCode.toDataURL(text, { width: 160, margin: 1 });
|
|
|
- this.qrDataUrl.set(url);
|
|
|
- } catch (e) {
|
|
|
- console.error('生成二维码失败', e);
|
|
|
- this.qrDataUrl.set('');
|
|
|
+ previousPage() {
|
|
|
+ if (this.currentPage > 1) {
|
|
|
+ this.currentPage--;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- downloadQrCode(): void {
|
|
|
- const dataUrl = this.qrDataUrl();
|
|
|
- if (!dataUrl) { return; }
|
|
|
- const a = document.createElement('a');
|
|
|
- const name = this.sharedCaseId() ? `${this.sharedCaseId()}-qr.png` : 'case-qr.png';
|
|
|
- a.href = dataUrl;
|
|
|
- a.download = name;
|
|
|
- document.body.appendChild(a);
|
|
|
- a.click();
|
|
|
- document.body.removeChild(a);
|
|
|
+ showStatistics() {
|
|
|
+ this.showStatsPanel = !this.showStatsPanel;
|
|
|
}
|
|
|
|
|
|
- openShareLink(): void {
|
|
|
- const link = this.shareLink();
|
|
|
- if (link) {
|
|
|
- window.open(link, '_blank', 'noopener');
|
|
|
- }
|
|
|
+ viewCaseDetail(caseItem: Case) {
|
|
|
+ // 记录案例查看开始时间
|
|
|
+ this.caseViewStartTimes.set(caseItem.id, Date.now());
|
|
|
+
|
|
|
+ // 增加浏览次数
|
|
|
+ caseItem.viewCount++;
|
|
|
+
|
|
|
+ // 记录浏览行为
|
|
|
+ this.recordBehavior('case_view', caseItem.id, {
|
|
|
+ caseName: caseItem.name,
|
|
|
+ designer: caseItem.designer,
|
|
|
+ projectType: caseItem.projectType,
|
|
|
+ spaceType: caseItem.spaceType
|
|
|
+ });
|
|
|
+
|
|
|
+ // 实际项目中可以跳转到详情页或打开模态框
|
|
|
+ console.log('查看案例详情:', caseItem);
|
|
|
+
|
|
|
+ // 模拟停留时长记录(实际应在详情页面实现)
|
|
|
+ setTimeout(() => {
|
|
|
+ const viewStartTime = this.caseViewStartTimes.get(caseItem.id);
|
|
|
+ if (viewStartTime) {
|
|
|
+ const viewDuration = Date.now() - viewStartTime;
|
|
|
+ this.recordBehavior('case_view_duration', caseItem.id, {
|
|
|
+ duration: viewDuration,
|
|
|
+ durationSeconds: Math.round(viewDuration / 1000)
|
|
|
+ });
|
|
|
+ this.caseViewStartTimes.delete(caseItem.id);
|
|
|
+ }
|
|
|
+ }, 5000); // 模拟5秒后记录停留时长
|
|
|
}
|
|
|
|
|
|
- closeShareModal(): void {
|
|
|
- this.showShareModal.set(false);
|
|
|
- this.qrDataUrl.set('');
|
|
|
- this.sharedCaseId.set(null);
|
|
|
+ toggleFavorite(caseItem: Case) {
|
|
|
+ const wasLiked = caseItem.isFavorite;
|
|
|
+ caseItem.isFavorite = !caseItem.isFavorite;
|
|
|
+
|
|
|
+ if (caseItem.isFavorite) {
|
|
|
+ caseItem.favoriteCount++;
|
|
|
+ this.showToast('已收藏该案例', 'success');
|
|
|
+
|
|
|
+ // 记录收藏行为
|
|
|
+ this.recordBehavior('case_favorite', caseItem.id, {
|
|
|
+ action: 'add',
|
|
|
+ caseName: caseItem.name,
|
|
|
+ designer: caseItem.designer
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ caseItem.favoriteCount = Math.max(0, caseItem.favoriteCount - 1);
|
|
|
+ this.showToast('已取消收藏', 'info');
|
|
|
+
|
|
|
+ // 记录取消收藏行为
|
|
|
+ this.recordBehavior('case_favorite', caseItem.id, {
|
|
|
+ action: 'remove',
|
|
|
+ caseName: caseItem.name,
|
|
|
+ designer: caseItem.designer
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- // 重置筛选条件
|
|
|
- resetFilters(): void {
|
|
|
- this.filterForm.reset({
|
|
|
- style: [],
|
|
|
- houseType: '',
|
|
|
- projectType: '',
|
|
|
- subType: '',
|
|
|
- renderingLevel: '',
|
|
|
- price: '',
|
|
|
- favorite: false,
|
|
|
- sortBy: 'createdAt'
|
|
|
+
|
|
|
+ shareCase(caseItem: Case) {
|
|
|
+ this.selectedCase = caseItem;
|
|
|
+ caseItem.shareCount++;
|
|
|
+
|
|
|
+ // 记录分享行为数据
|
|
|
+ this.recordBehavior('share', caseItem.id, {
|
|
|
+ caseName: caseItem.name,
|
|
|
+ designer: caseItem.designer,
|
|
|
+ projectType: caseItem.projectType
|
|
|
});
|
|
|
- this.searchTerm.set('');
|
|
|
- this.currentPage.set(1);
|
|
|
- }
|
|
|
-
|
|
|
- // 切换筛选面板
|
|
|
- toggleFilterPanel(): void {
|
|
|
- this.showFilterPanel.set(!this.showFilterPanel());
|
|
|
- }
|
|
|
-
|
|
|
- // 分页导航
|
|
|
- goToPage(page: number): void {
|
|
|
- if (page >= 1 && page <= this.totalPages()) {
|
|
|
- this.currentPage.set(page);
|
|
|
- }
|
|
|
}
|
|
|
-
|
|
|
- // 上一页
|
|
|
- prevPage(): void {
|
|
|
- this.goToPage(this.currentPage() - 1);
|
|
|
+
|
|
|
+ closeShareModal() {
|
|
|
+ this.selectedCase = null;
|
|
|
}
|
|
|
-
|
|
|
- // 下一页
|
|
|
- nextPage(): void {
|
|
|
- this.goToPage(this.currentPage() + 1);
|
|
|
+
|
|
|
+ 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>
|
|
|
+ `;
|
|
|
+ // 使用 TextEncoder 和 btoa 来正确处理包含中文的字符串
|
|
|
+ const encoder = new TextEncoder();
|
|
|
+ const data = encoder.encode(svgContent);
|
|
|
+ const base64 = btoa(String.fromCharCode(...data));
|
|
|
+ return `data:image/svg+xml;base64,${base64}`;
|
|
|
}
|
|
|
-
|
|
|
- // 格式化日期
|
|
|
- formatDate(date: Date): string {
|
|
|
- return new Date(date).toLocaleDateString('zh-CN', {
|
|
|
- month: '2-digit',
|
|
|
- day: '2-digit',
|
|
|
- year: 'numeric'
|
|
|
- });
|
|
|
+
|
|
|
+ generateShareLink(caseItem: Case): string {
|
|
|
+ return `${window.location.origin}/case/${caseItem.id}?from=share&designer=${encodeURIComponent(caseItem.designer)}`;
|
|
|
}
|
|
|
- // 智能页码生成
|
|
|
- pageNumbers = computed(() => {
|
|
|
- const pages = [] as number[];
|
|
|
- const total = this.totalPages();
|
|
|
- const current = this.currentPage();
|
|
|
-
|
|
|
- // 显示当前页及前后2页,加上第一页和最后一页
|
|
|
- const start = Math.max(1, current - 2);
|
|
|
- const end = Math.min(total, current + 2);
|
|
|
-
|
|
|
- if (start > 1) {
|
|
|
- pages.push(1);
|
|
|
- if (start > 2) {
|
|
|
- pages.push(-1); // 用-1表示省略号
|
|
|
- }
|
|
|
+
|
|
|
+ copyShareLink() {
|
|
|
+ if (this.selectedCase) {
|
|
|
+ const link = this.generateShareLink(this.selectedCase);
|
|
|
+ navigator.clipboard.writeText(link).then(() => {
|
|
|
+ this.showToast('链接已复制到剪贴板,可直接分享给客户!', 'success');
|
|
|
+
|
|
|
+ // 记录复制行为
|
|
|
+ this.recordBehavior('copy_link', this.selectedCase!.id, {
|
|
|
+ link: link
|
|
|
+ });
|
|
|
+ }).catch(err => {
|
|
|
+ this.showToast('复制失败,请手动复制链接', 'error');
|
|
|
+ console.error('复制链接失败:', err);
|
|
|
+ });
|
|
|
}
|
|
|
-
|
|
|
- for (let i = start; i <= end; i++) {
|
|
|
- pages.push(i);
|
|
|
+ }
|
|
|
+
|
|
|
+ shareToWeCom() {
|
|
|
+ if (this.selectedCase) {
|
|
|
+ // 实际项目中应集成企业微信分享SDK
|
|
|
+ const shareData = {
|
|
|
+ title: `${this.selectedCase.name} - ${this.selectedCase.designer}设计作品`,
|
|
|
+ description: `${this.selectedCase.projectType} | ${this.selectedCase.spaceType} | ${this.selectedCase.area}㎡`,
|
|
|
+ link: this.generateShareLink(this.selectedCase),
|
|
|
+ imgUrl: this.selectedCase.coverImage
|
|
|
+ };
|
|
|
+
|
|
|
+ // 模拟企业微信分享
|
|
|
+ console.log('分享到企业微信:', shareData);
|
|
|
+ this.showToast('已调用企业微信分享', 'success');
|
|
|
+
|
|
|
+ // 记录企业微信分享行为
|
|
|
+ this.recordBehavior('share_wecom', this.selectedCase.id, shareData);
|
|
|
+
|
|
|
+ this.closeShareModal();
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ // 新增:行为数据记录方法
|
|
|
+ private recordBehavior(action: string, caseId: string, data?: any) {
|
|
|
+ const behaviorData = {
|
|
|
+ action,
|
|
|
+ caseId,
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
+ userAgent: navigator.userAgent,
|
|
|
+ data: data || {}
|
|
|
+ };
|
|
|
|
|
|
- if (end < total) {
|
|
|
- if (end < total - 1) {
|
|
|
- pages.push(-1); // 用-1表示省略号
|
|
|
- }
|
|
|
- pages.push(total);
|
|
|
- }
|
|
|
+ // 实际项目中应发送到后端API
|
|
|
+ console.log('记录用户行为:', behaviorData);
|
|
|
|
|
|
- return pages;
|
|
|
- });
|
|
|
-
|
|
|
- // 格式化样式显示的辅助方法
|
|
|
- getStyleDisplay(caseItem: CaseItem | null | undefined): string {
|
|
|
- if (!caseItem || !caseItem.style) {
|
|
|
- return '';
|
|
|
- }
|
|
|
- return Array.isArray(caseItem.style) ? caseItem.style.join('、') : String(caseItem.style);
|
|
|
+ // 模拟存储到本地(实际应发送到服务器)
|
|
|
+ const behaviors = JSON.parse(localStorage.getItem('caseBehaviors') || '[]');
|
|
|
+ behaviors.push(behaviorData);
|
|
|
+ localStorage.setItem('caseBehaviors', JSON.stringify(behaviors));
|
|
|
}
|
|
|
|
|
|
- // 获取当前选中案例的样式显示
|
|
|
- getSelectedCaseStyle(): string {
|
|
|
- return this.getStyleDisplay(this.selectedCase());
|
|
|
- }
|
|
|
-
|
|
|
- // 修复 onStyleChange 方法中的类型安全问题
|
|
|
- onStyleChange(style: string, isChecked: boolean): void {
|
|
|
- const currentStyles = (this.filterForm.get('style')?.value || []) as string[];
|
|
|
- let updatedStyles: string[];
|
|
|
+ // 新增:显示提示消息
|
|
|
+ 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'};
|
|
|
+ `;
|
|
|
|
|
|
- if (isChecked) {
|
|
|
- // 如果勾选,则添加风格(避免重复)
|
|
|
- updatedStyles = [...new Set([...currentStyles, style])];
|
|
|
- } else {
|
|
|
- // 如果取消勾选,则移除风格
|
|
|
- updatedStyles = currentStyles.filter(s => s !== style);
|
|
|
- }
|
|
|
+ document.body.appendChild(toast);
|
|
|
|
|
|
- this.filterForm.patchValue({ style: updatedStyles });
|
|
|
+ setTimeout(() => {
|
|
|
+ toast.style.animation = 'slideOut 0.3s ease';
|
|
|
+ setTimeout(() => document.body.removeChild(toast), 300);
|
|
|
+ }, 3000);
|
|
|
}
|
|
|
}
|