case-detail.component.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import { Component, OnInit } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { ActivatedRoute, Router } from '@angular/router';
  4. interface Case {
  5. id: string;
  6. name: string;
  7. coverImage: string;
  8. projectType: '工装' | '家装';
  9. spaceType: '平层' | '复式' | '别墅' | '自建房';
  10. renderingLevel: '高端' | '中端' | '低端';
  11. designer: string;
  12. team: string;
  13. area: number;
  14. styleTags: string[];
  15. customerReview?: string;
  16. viewCount: number;
  17. shareCount: number;
  18. favoriteCount: number;
  19. isFavorite: boolean;
  20. isExcellent: boolean;
  21. createdAt: Date;
  22. }
  23. @Component({
  24. selector: 'app-case-detail',
  25. standalone: true,
  26. imports: [CommonModule],
  27. templateUrl: './case-detail.component.html',
  28. styleUrls: ['./case-detail.component.scss']
  29. })
  30. export class CaseDetailComponent implements OnInit {
  31. case: Case | null = null;
  32. isInternalUser: boolean = true; // 可根据实际用户权限设置
  33. isLoading: boolean = true;
  34. caseId: string = '';
  35. constructor(
  36. private route: ActivatedRoute,
  37. private router: Router
  38. ) {}
  39. ngOnInit() {
  40. // 获取路由参数中的案例ID
  41. this.route.paramMap.subscribe(params => {
  42. this.caseId = params.get('id') || '';
  43. if (this.caseId) {
  44. this.loadCaseDetail(this.caseId);
  45. }
  46. });
  47. }
  48. private loadCaseDetail(caseId: string) {
  49. this.isLoading = true;
  50. // 模拟API调用,实际项目中应该调用真实的API
  51. setTimeout(() => {
  52. this.case = this.generateMockCase(caseId);
  53. this.isLoading = false;
  54. // 增加浏览次数
  55. if (this.case) {
  56. this.case.viewCount++;
  57. this.recordBehavior('case_view', this.case.id, {
  58. caseName: this.case.name,
  59. designer: this.case.designer,
  60. projectType: this.case.projectType,
  61. spaceType: this.case.spaceType
  62. });
  63. }
  64. }, 500);
  65. }
  66. private generateMockCase(caseId: string): Case {
  67. const projectTypes: ('工装' | '家装')[] = ['工装', '家装'];
  68. const spaceTypes: ('平层' | '复式' | '别墅' | '自建房')[] = ['平层', '复式', '别墅', '自建房'];
  69. const renderingLevels: ('高端' | '中端' | '低端')[] = ['高端', '中端', '低端'];
  70. const styles = ['现代', '中式', '欧式', '美式', '日式', '工业风', '极简风', '轻奢风'];
  71. const designers = ['张三', '李四', '王五', '赵六', '钱七'];
  72. const teams = ['设计一组', '设计二组', '设计三组', '设计四组'];
  73. const projectType = projectTypes[Math.floor(Math.random() * projectTypes.length)];
  74. const spaceType = spaceTypes[Math.floor(Math.random() * spaceTypes.length)];
  75. const renderingLevel = renderingLevels[Math.floor(Math.random() * renderingLevels.length)];
  76. const styleCount = Math.floor(Math.random() * 3) + 1;
  77. const styleTags = Array.from({ length: styleCount }, () =>
  78. styles[Math.floor(Math.random() * styles.length)]
  79. );
  80. return {
  81. id: caseId,
  82. name: `${projectType}${spaceType}设计案例 - ${caseId}`,
  83. coverImage: this.generatePlaceholderImage(800, 600, caseId),
  84. projectType,
  85. spaceType,
  86. renderingLevel,
  87. designer: designers[Math.floor(Math.random() * designers.length)],
  88. team: teams[Math.floor(Math.random() * teams.length)],
  89. area: Math.floor(Math.random() * 200) + 50,
  90. styleTags: [...new Set(styleTags)], // 去重
  91. customerReview: Math.random() > 0.3 ? `客户非常满意这次${projectType}设计,${spaceType}空间利用得很合理,设计师的专业水平很高,整体效果超出预期。` : undefined,
  92. viewCount: Math.floor(Math.random() * 1000),
  93. shareCount: Math.floor(Math.random() * 100),
  94. favoriteCount: Math.floor(Math.random() * 50),
  95. isFavorite: Math.random() > 0.7,
  96. isExcellent: Math.random() > 0.5,
  97. createdAt: new Date(Date.now() - Math.floor(Math.random() * 365 * 24 * 60 * 60 * 1000))
  98. };
  99. }
  100. private generatePlaceholderImage(width: number, height: number, seed: string): string {
  101. return `https://picsum.photos/seed/${seed}/${width}/${height}`;
  102. }
  103. goBack() {
  104. this.router.navigate(['/customer-service/case-library']);
  105. }
  106. toggleFavorite() {
  107. if (!this.case) return;
  108. const wasLiked = this.case.isFavorite;
  109. this.case.isFavorite = !this.case.isFavorite;
  110. if (this.case.isFavorite) {
  111. this.case.favoriteCount++;
  112. this.showToast('已收藏该案例', 'success');
  113. this.recordBehavior('case_favorite', this.case.id, {
  114. action: 'add',
  115. caseName: this.case.name,
  116. designer: this.case.designer
  117. });
  118. } else {
  119. this.case.favoriteCount = Math.max(0, this.case.favoriteCount - 1);
  120. this.showToast('已取消收藏', 'info');
  121. this.recordBehavior('case_favorite', this.case.id, {
  122. action: 'remove',
  123. caseName: this.case.name,
  124. designer: this.case.designer
  125. });
  126. }
  127. }
  128. shareCase() {
  129. if (!this.case) return;
  130. this.case.shareCount++;
  131. // 生成分享链接
  132. const shareLink = this.generateShareLink();
  133. // 复制到剪贴板
  134. navigator.clipboard.writeText(shareLink).then(() => {
  135. this.showToast('分享链接已复制到剪贴板', 'success');
  136. }).catch(() => {
  137. this.showToast('复制失败,请手动复制链接', 'error');
  138. });
  139. this.recordBehavior('share', this.case.id, {
  140. caseName: this.case.name,
  141. designer: this.case.designer,
  142. projectType: this.case.projectType
  143. });
  144. }
  145. generateShareLink(): string {
  146. if (!this.case) return '';
  147. return `${window.location.origin}/customer-service/case-detail/${this.case.id}?from=share&designer=${encodeURIComponent(this.case.designer)}`;
  148. }
  149. getFormattedDate(date: Date): string {
  150. return new Intl.DateTimeFormat('zh-CN', {
  151. year: 'numeric',
  152. month: 'long',
  153. day: 'numeric'
  154. }).format(new Date(date));
  155. }
  156. // 与案例库保持一致的封面选择逻辑
  157. coverImageOf(c: any): string {
  158. if (!c) return '/assets/presets/家装图片.jpg';
  159. const images: string[] = c?.images || [];
  160. const uploaded = images.find((img: string) =>
  161. img && img.startsWith('http') &&
  162. !img.includes('placeholder') &&
  163. !img.includes('unsplash.com') &&
  164. !img.endsWith('.svg') &&
  165. !img.includes('/stage/requirements/') &&
  166. (img.includes('file-cloud.fmode.cn') || img.includes('storage'))
  167. );
  168. if (uploaded) return uploaded;
  169. const cover: string = c?.coverImage || '';
  170. const isReal = !!(cover && cover.startsWith('http') && !cover.includes('placeholder') && !cover.includes('unsplash.com') && !cover.endsWith('.svg') && !cover.includes('/stage/requirements/') && (cover.includes('file-cloud.fmode.cn') || cover.includes('storage')));
  171. return isReal ? cover : '/assets/presets/家装图片.jpg';
  172. }
  173. private recordBehavior(action: string, caseId: string, data?: any) {
  174. // 模拟行为记录,实际项目中应该调用真实的API
  175. console.log('行为记录:', {
  176. action,
  177. caseId,
  178. data,
  179. timestamp: new Date().toISOString(),
  180. userAgent: navigator.userAgent
  181. });
  182. }
  183. private showToast(message: string, type: 'success' | 'error' | 'info' = 'info') {
  184. // 简单的toast实现,实际项目中可以使用更完善的toast库
  185. const toast = document.createElement('div');
  186. toast.className = `toast toast-${type}`;
  187. toast.textContent = message;
  188. toast.style.cssText = `
  189. position: fixed;
  190. top: 20px;
  191. right: 20px;
  192. padding: 12px 20px;
  193. border-radius: 8px;
  194. color: white;
  195. font-size: 14px;
  196. z-index: 10000;
  197. animation: slideInRight 0.3s ease-out;
  198. background: ${type === 'success' ? '#34c759' : type === 'error' ? '#ff3b30' : '#007aff'};
  199. `;
  200. document.body.appendChild(toast);
  201. setTimeout(() => {
  202. toast.style.animation = 'slideOutRight 0.3s ease-in';
  203. setTimeout(() => {
  204. document.body.removeChild(toast);
  205. }, 300);
  206. }, 3000);
  207. }
  208. }