case-library.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. import { Component, OnInit } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule, ReactiveFormsModule } from '@angular/forms';
  4. import { FormControl } from '@angular/forms';
  5. import { Router } from '@angular/router';
  6. import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
  7. import { CaseDetailPanelComponent } from './case-detail-panel.component';
  8. interface Case {
  9. id: string;
  10. name: string;
  11. coverImage: string;
  12. projectType: '工装' | '家装';
  13. spaceType: '平层' | '复式' | '别墅' | '自建房';
  14. renderingLevel: '高端' | '中端' | '低端';
  15. designer: string;
  16. team: string;
  17. area: number;
  18. styleTags: string[];
  19. customerReview?: string;
  20. viewCount: number;
  21. shareCount: number;
  22. favoriteCount: number;
  23. isFavorite: boolean;
  24. isExcellent: boolean;
  25. createdAt: Date;
  26. }
  27. interface StatItem {
  28. id: string;
  29. name: string;
  30. shareCount: number;
  31. }
  32. interface StyleStat {
  33. style: string;
  34. count: number;
  35. }
  36. interface DesignerStat {
  37. designer: string;
  38. rate: number;
  39. }
  40. @Component({
  41. selector: 'app-case-library',
  42. standalone: true,
  43. imports: [CommonModule, FormsModule, ReactiveFormsModule, CaseDetailPanelComponent],
  44. templateUrl: './case-library.html',
  45. styleUrls: ['./case-library.scss']
  46. })
  47. export class CaseLibrary implements OnInit {
  48. // 表单控件
  49. searchControl = new FormControl('');
  50. projectTypeControl = new FormControl('');
  51. spaceTypeControl = new FormControl('');
  52. renderingLevelControl = new FormControl('');
  53. styleControl = new FormControl('');
  54. areaRangeControl = new FormControl('');
  55. // 数据
  56. cases: Case[] = [];
  57. filteredCases: Case[] = [];
  58. // 统计数据
  59. topSharedCases: StatItem[] = [];
  60. favoriteStyles: StyleStat[] = [];
  61. designerRecommendations: DesignerStat[] = [];
  62. // 状态
  63. showStatsPanel = false;
  64. selectedCase: Case | null = null; // 用于详情面板
  65. selectedCaseForShare: Case | null = null; // 用于分享模态框
  66. currentPage = 1;
  67. itemsPerPage = 10; // 每页显示10个案例
  68. totalPages = 1;
  69. // 用户类型(模拟)
  70. isInternalUser = true; // 可根据实际用户权限设置
  71. // 行为追踪
  72. private pageStartTime = Date.now();
  73. private caseViewStartTimes = new Map<string, number>();
  74. constructor(private router: Router) {}
  75. ngOnInit() {
  76. this.initializeData();
  77. this.setupFilterListeners();
  78. this.setupBehaviorTracking();
  79. }
  80. private setupBehaviorTracking() {
  81. // 记录页面访问
  82. this.recordBehavior('page_view', 'case-library', {
  83. timestamp: new Date().toISOString()
  84. });
  85. // 页面卸载时记录停留时长
  86. window.addEventListener('beforeunload', () => {
  87. const stayDuration = Date.now() - this.pageStartTime;
  88. this.recordBehavior('page_stay', 'case-library', {
  89. duration: stayDuration,
  90. durationMinutes: Math.round(stayDuration / 60000 * 100) / 100
  91. });
  92. });
  93. }
  94. private initializeData() {
  95. // 模拟案例数据
  96. this.cases = this.generateMockCases();
  97. this.filteredCases = [...this.cases];
  98. this.updatePagination();
  99. // 初始化统计数据
  100. this.initializeStats();
  101. }
  102. private setupFilterListeners() {
  103. // 搜索框防抖
  104. this.searchControl.valueChanges.pipe(
  105. debounceTime(300),
  106. distinctUntilChanged()
  107. ).subscribe(() => this.applyFilters());
  108. // 其他筛选条件变化
  109. [
  110. this.projectTypeControl,
  111. this.spaceTypeControl,
  112. this.renderingLevelControl,
  113. this.styleControl,
  114. this.areaRangeControl
  115. ].forEach(control => {
  116. control.valueChanges.subscribe(() => this.applyFilters());
  117. });
  118. }
  119. private generateMockCases(): Case[] {
  120. const mockCases: Case[] = [];
  121. const projectTypes: ('工装' | '家装')[] = ['工装', '家装'];
  122. const spaceTypes: ('平层' | '复式' | '别墅' | '自建房')[] = ['平层', '复式', '别墅', '自建房'];
  123. const renderingLevels: ('高端' | '中端' | '低端')[] = ['高端', '中端', '低端'];
  124. const styles = ['现代', '中式', '欧式', '美式', '日式', '工业风', '极简风', '轻奢风'];
  125. const designers = ['张三', '李四', '王五', '赵六', '钱七'];
  126. const teams = ['设计一组', '设计二组', '设计三组', '设计四组'];
  127. for (let i = 1; i <= 50; i++) {
  128. const projectType = projectTypes[Math.floor(Math.random() * projectTypes.length)];
  129. const spaceType = spaceTypes[Math.floor(Math.random() * spaceTypes.length)];
  130. const renderingLevel = renderingLevels[Math.floor(Math.random() * renderingLevels.length)];
  131. const styleCount = Math.floor(Math.random() * 3) + 1;
  132. const styleTags = Array.from({ length: styleCount }, () =>
  133. styles[Math.floor(Math.random() * styles.length)]
  134. );
  135. mockCases.push({
  136. id: `case-${i}`,
  137. name: `${projectType}${spaceType}设计案例 ${i}`,
  138. coverImage: this.generatePlaceholderImage(400, 300, i),
  139. projectType,
  140. spaceType,
  141. renderingLevel,
  142. designer: designers[Math.floor(Math.random() * designers.length)],
  143. team: teams[Math.floor(Math.random() * teams.length)],
  144. area: Math.floor(Math.random() * 200) + 50,
  145. styleTags: [...new Set(styleTags)], // 去重
  146. customerReview: Math.random() > 0.3 ? `客户非常满意这次${projectType}设计,${spaceType}空间利用得很合理` : undefined,
  147. viewCount: Math.floor(Math.random() * 1000),
  148. shareCount: Math.floor(Math.random() * 100),
  149. favoriteCount: Math.floor(Math.random() * 50),
  150. isFavorite: Math.random() > 0.7,
  151. isExcellent: Math.random() > 0.5,
  152. createdAt: new Date(Date.now() - Math.floor(Math.random() * 365 * 24 * 60 * 60 * 1000))
  153. });
  154. }
  155. return mockCases;
  156. }
  157. private generatePlaceholderImage(width: number, height: number, seed: number): string {
  158. return `https://picsum.photos/seed/${seed}/${width}/${height}`;
  159. }
  160. private initializeStats() {
  161. // Top5 分享案例
  162. this.topSharedCases = this.cases
  163. .sort((a, b) => b.shareCount - a.shareCount)
  164. .slice(0, 5)
  165. .map(caseItem => ({
  166. id: caseItem.id,
  167. name: caseItem.name,
  168. shareCount: caseItem.shareCount
  169. }));
  170. // 客户最喜欢案例风格
  171. const styleCounts = new Map<string, number>();
  172. this.cases.forEach(caseItem => {
  173. caseItem.styleTags.forEach(style => {
  174. styleCounts.set(style, (styleCounts.get(style) || 0) + caseItem.favoriteCount);
  175. });
  176. });
  177. this.favoriteStyles = Array.from(styleCounts.entries())
  178. .sort(([, a], [, b]) => b - a)
  179. .slice(0, 5)
  180. .map(([style, count]) => ({ style, count }));
  181. // 设计师作品推荐率
  182. const designerStats = new Map<string, { total: number; excellent: number }>();
  183. this.cases.forEach(caseItem => {
  184. const stats = designerStats.get(caseItem.designer) || { total: 0, excellent: 0 };
  185. stats.total++;
  186. if (caseItem.isExcellent) stats.excellent++;
  187. designerStats.set(caseItem.designer, stats);
  188. });
  189. this.designerRecommendations = Array.from(designerStats.entries())
  190. .map(([designer, stats]) => ({
  191. designer,
  192. rate: Math.round((stats.excellent / stats.total) * 100)
  193. }))
  194. .sort((a, b) => b.rate - a.rate)
  195. .slice(0, 5);
  196. }
  197. applyFilters() {
  198. const searchTerm = this.searchControl.value?.toLowerCase() || '';
  199. const projectType = this.projectTypeControl.value;
  200. const spaceType = this.spaceTypeControl.value;
  201. const renderingLevel = this.renderingLevelControl.value;
  202. const style = this.styleControl.value;
  203. const areaRange = this.areaRangeControl.value;
  204. this.filteredCases = this.cases.filter(caseItem => {
  205. // 搜索条件
  206. if (searchTerm && !(
  207. caseItem.name.toLowerCase().includes(searchTerm) ||
  208. caseItem.designer.toLowerCase().includes(searchTerm) ||
  209. caseItem.styleTags.some(tag => tag.toLowerCase().includes(searchTerm))
  210. )) {
  211. return false;
  212. }
  213. // 项目类型筛选
  214. if (projectType && caseItem.projectType !== projectType) {
  215. return false;
  216. }
  217. // 空间类型筛选
  218. if (spaceType && caseItem.spaceType !== spaceType) {
  219. return false;
  220. }
  221. // 渲染水平筛选
  222. if (renderingLevel && caseItem.renderingLevel !== renderingLevel) {
  223. return false;
  224. }
  225. // 风格筛选
  226. if (style && !caseItem.styleTags.includes(style)) {
  227. return false;
  228. }
  229. // 面积范围筛选
  230. if (areaRange) {
  231. const [min, max] = areaRange.split('-').map(Number);
  232. if (max === undefined) {
  233. if (caseItem.area < min) return false;
  234. } else if (caseItem.area < min || caseItem.area > max) {
  235. return false;
  236. }
  237. }
  238. return true;
  239. });
  240. this.currentPage = 1;
  241. this.updatePagination();
  242. }
  243. resetFilters() {
  244. this.searchControl.setValue('');
  245. this.projectTypeControl.setValue('');
  246. this.spaceTypeControl.setValue('');
  247. this.renderingLevelControl.setValue('');
  248. this.styleControl.setValue('');
  249. this.areaRangeControl.setValue('');
  250. this.filteredCases = [...this.cases];
  251. this.currentPage = 1;
  252. this.updatePagination();
  253. }
  254. updatePagination() {
  255. this.totalPages = Math.ceil(this.filteredCases.length / this.itemsPerPage);
  256. if (this.currentPage > this.totalPages) {
  257. this.currentPage = this.totalPages || 1;
  258. }
  259. }
  260. get paginatedCases(): Case[] {
  261. const startIndex = (this.currentPage - 1) * this.itemsPerPage;
  262. return this.filteredCases.slice(startIndex, startIndex + this.itemsPerPage);
  263. }
  264. nextPage() {
  265. if (this.currentPage < this.totalPages) {
  266. this.currentPage++;
  267. }
  268. }
  269. previousPage() {
  270. if (this.currentPage > 1) {
  271. this.currentPage--;
  272. }
  273. }
  274. showStatistics() {
  275. this.showStatsPanel = !this.showStatsPanel;
  276. }
  277. viewCaseDetail(caseItem: Case) {
  278. // 记录案例查看开始时间
  279. this.caseViewStartTimes.set(caseItem.id, Date.now());
  280. // 增加浏览次数
  281. caseItem.viewCount++;
  282. // 记录浏览行为
  283. this.recordBehavior('case_view', caseItem.id, {
  284. caseName: caseItem.name,
  285. designer: caseItem.designer,
  286. projectType: caseItem.projectType,
  287. spaceType: caseItem.spaceType
  288. });
  289. // 设置当前选中的案例以显示详情面板
  290. this.selectedCase = caseItem;
  291. }
  292. // 跳转到独立的案例详情页面
  293. navigateToCaseDetail(caseItem: Case) {
  294. // 记录案例查看开始时间
  295. this.caseViewStartTimes.set(caseItem.id, Date.now());
  296. // 增加浏览次数
  297. caseItem.viewCount++;
  298. // 记录浏览行为
  299. this.recordBehavior('case_view', caseItem.id, {
  300. caseName: caseItem.name,
  301. designer: caseItem.designer,
  302. projectType: caseItem.projectType,
  303. spaceType: caseItem.spaceType
  304. });
  305. // 跳转到独立的案例详情页面
  306. this.router.navigate(['/customer-service/case-detail', caseItem.id]);
  307. }
  308. closeCaseDetail() {
  309. // 记录案例查看时长
  310. if (this.selectedCase) {
  311. const viewStartTime = this.caseViewStartTimes.get(this.selectedCase.id);
  312. if (viewStartTime) {
  313. const viewDuration = Date.now() - viewStartTime;
  314. this.recordBehavior('case_view_duration', this.selectedCase.id, {
  315. duration: viewDuration,
  316. durationSeconds: Math.round(viewDuration / 1000)
  317. });
  318. this.caseViewStartTimes.delete(this.selectedCase.id);
  319. }
  320. }
  321. this.selectedCase = null;
  322. }
  323. toggleFavorite(caseItem: Case) {
  324. const wasLiked = caseItem.isFavorite;
  325. caseItem.isFavorite = !caseItem.isFavorite;
  326. if (caseItem.isFavorite) {
  327. caseItem.favoriteCount++;
  328. this.showToast('已收藏该案例', 'success');
  329. // 记录收藏行为
  330. this.recordBehavior('case_favorite', caseItem.id, {
  331. action: 'add',
  332. caseName: caseItem.name,
  333. designer: caseItem.designer
  334. });
  335. } else {
  336. caseItem.favoriteCount = Math.max(0, caseItem.favoriteCount - 1);
  337. this.showToast('已取消收藏', 'info');
  338. // 记录取消收藏行为
  339. this.recordBehavior('case_favorite', caseItem.id, {
  340. action: 'remove',
  341. caseName: caseItem.name,
  342. designer: caseItem.designer
  343. });
  344. }
  345. }
  346. shareCase(caseItem: Case) {
  347. this.selectedCaseForShare = caseItem;
  348. caseItem.shareCount++;
  349. // 记录分享行为数据
  350. this.recordBehavior('share', caseItem.id, {
  351. caseName: caseItem.name,
  352. designer: caseItem.designer,
  353. projectType: caseItem.projectType
  354. });
  355. }
  356. closeShareModal() {
  357. this.selectedCaseForShare = null;
  358. }
  359. generateQRCode(caseItem: Case): string {
  360. // 实际项目中应使用二维码生成库,如 qrcode.js
  361. const qrData = this.generateShareLink(caseItem);
  362. // 这里返回一个模拟的二维码图片
  363. const svgContent = `
  364. <svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  365. <rect width="200" height="200" fill="white"/>
  366. <rect x="20" y="20" width="160" height="160" fill="black"/>
  367. <rect x="30" y="30" width="140" height="140" fill="white"/>
  368. <text x="100" y="105" text-anchor="middle" font-size="12" fill="black">案例二维码</text>
  369. <text x="100" y="125" text-anchor="middle" font-size="10" fill="gray">${caseItem.name}</text>
  370. </svg>
  371. `;
  372. // 使用 encodeURIComponent 来正确处理SVG内容
  373. const encodedSVG = encodeURIComponent(svgContent);
  374. return `data:image/svg+xml;charset=utf-8,${encodedSVG}`;
  375. }
  376. generateShareLink(caseItem: Case): string {
  377. return `${window.location.origin}/customer-service/case-detail/${caseItem.id}?from=share&designer=${encodeURIComponent(caseItem.designer)}`;
  378. }
  379. copyShareLink() {
  380. if (this.selectedCase) {
  381. const link = this.generateShareLink(this.selectedCase);
  382. navigator.clipboard.writeText(link).then(() => {
  383. this.showToast('链接已复制到剪贴板,可直接分享给客户!', 'success');
  384. // 记录复制行为
  385. this.recordBehavior('copy_link', this.selectedCase!.id, {
  386. link: link
  387. });
  388. }).catch(err => {
  389. this.showToast('复制失败,请手动复制链接', 'error');
  390. console.error('复制链接失败:', err);
  391. });
  392. }
  393. }
  394. shareToWeCom() {
  395. if (this.selectedCase) {
  396. // 实际项目中应集成企业微信分享SDK
  397. const shareData = {
  398. title: `${this.selectedCase.name} - ${this.selectedCase.designer}设计作品`,
  399. description: `${this.selectedCase.projectType} | ${this.selectedCase.spaceType} | ${this.selectedCase.area}㎡`,
  400. link: this.generateShareLink(this.selectedCase),
  401. imgUrl: this.selectedCase.coverImage
  402. };
  403. // 模拟企业微信分享
  404. console.log('分享到企业微信:', shareData);
  405. this.showToast('已调用企业微信分享', 'success');
  406. // 记录企业微信分享行为
  407. this.recordBehavior('share_wecom', this.selectedCase.id, shareData);
  408. this.closeShareModal();
  409. }
  410. }
  411. // 新增:行为数据记录方法
  412. private recordBehavior(action: string, caseId: string, data?: any) {
  413. const behaviorData = {
  414. action,
  415. caseId,
  416. timestamp: new Date().toISOString(),
  417. userAgent: navigator.userAgent,
  418. data: data || {}
  419. };
  420. // 实际项目中应发送到后端API
  421. console.log('记录用户行为:', behaviorData);
  422. // 模拟存储到本地(实际应发送到服务器)
  423. const behaviors = JSON.parse(localStorage.getItem('caseBehaviors') || '[]');
  424. behaviors.push(behaviorData);
  425. localStorage.setItem('caseBehaviors', JSON.stringify(behaviors));
  426. }
  427. // 新增:显示提示消息
  428. private showToast(message: string, type: 'success' | 'error' | 'info' = 'info') {
  429. // 实际项目中应使用专业的Toast组件
  430. const toast = document.createElement('div');
  431. toast.className = `toast toast-${type}`;
  432. toast.textContent = message;
  433. toast.style.cssText = `
  434. position: fixed;
  435. top: 20px;
  436. right: 20px;
  437. padding: 12px 20px;
  438. border-radius: 8px;
  439. color: white;
  440. font-weight: 500;
  441. z-index: 10000;
  442. animation: slideIn 0.3s ease;
  443. background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
  444. `;
  445. document.body.appendChild(toast);
  446. setTimeout(() => {
  447. toast.style.animation = 'slideOut 0.3s ease';
  448. setTimeout(() => document.body.removeChild(toast), 300);
  449. }, 3000);
  450. }
  451. }