customer-review-card.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. import { Component, Input, computed, signal } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { CustomerFeedback } from '../../../models/project.model';
  5. export interface DetailedReviewDimensions {
  6. [key: string]: number; // 添加索引签名
  7. overall: number; // 总体评价 0-5
  8. serviceTimeliness: number; // 服务时效 0-5
  9. suggestionResponse: number; // 建议回应 0-5
  10. smallImageDelivery: number; // 小图交付 0-5
  11. imageRevisionTimeliness: number; // 改图及时性 0-5
  12. renderingQuality: number; // 图纸渲染 0-5
  13. materialQuality: number; // 材质 0-5
  14. lightingQuality: number; // 灯光 0-5
  15. textureQuality: number; // 纹理 0-5
  16. structureQuality: number; // 硬装结构 0-5
  17. dataCompleteness: number; // 资料齐全情况 0-5
  18. requirementUnderstanding: number; // 需求理解 0-5
  19. communicationEffectiveness: number; // 沟通效果 0-5
  20. sceneDesignQuality: number; // 场景设计质量 0-5
  21. }
  22. export interface SceneReview {
  23. sceneType: 'bedroom' | 'dining_room' | 'living_room' | 'other';
  24. sceneName: string;
  25. designerId: string;
  26. designerName: string;
  27. rating: number; // 0-5
  28. feedback: string;
  29. }
  30. export interface DetailedCustomerReview {
  31. id: string;
  32. projectId: string;
  33. customerId: string;
  34. customerName: string;
  35. dimensions: DetailedReviewDimensions;
  36. sceneReviews: SceneReview[];
  37. improvementSuggestions: string; // 下次合作希望优化的点
  38. optimizationSuggestions?: string[]; // 优化建议列表
  39. overallFeedback: string;
  40. submittedAt: Date;
  41. status: 'pending' | 'completed';
  42. }
  43. export interface ReviewStats {
  44. totalCount: number;
  45. averageScore: number;
  46. satisfiedCount: number;
  47. unsatisfiedCount: number;
  48. pendingCount: number;
  49. processedCount: number;
  50. categoryStats: { [key: string]: number };
  51. dimensionAverages: Partial<DetailedReviewDimensions>;
  52. }
  53. @Component({
  54. selector: 'app-customer-review-card',
  55. standalone: true,
  56. imports: [CommonModule, FormsModule],
  57. templateUrl: './customer-review-card.html',
  58. styleUrls: ['./customer-review-card.scss']
  59. })
  60. export class CustomerReviewCardComponent {
  61. @Input() feedbacks: CustomerFeedback[] = [];
  62. @Input() detailedReviews: DetailedCustomerReview[] = [];
  63. // 筛选条件
  64. statusFilter = signal<string>('all');
  65. categoryFilter = signal<string>('all');
  66. scoreFilter = signal<string>('all');
  67. viewMode = signal<'simple' | 'detailed'>('detailed');
  68. // 详细评价维度配置
  69. reviewDimensions = [
  70. { key: 'overall', label: '总体评价', description: '对整体服务的满意度' },
  71. { key: 'serviceTimeliness', label: '服务时效', description: '响应速度和交付及时性' },
  72. { key: 'suggestionResponse', label: '建议回应', description: '对客户建议的响应程度' },
  73. { key: 'smallImageDelivery', label: '小图交付', description: '小图质量和交付效率' },
  74. { key: 'imageRevisionTimeliness', label: '改图及时性', description: '修改图纸的响应速度' },
  75. { key: 'renderingQuality', label: '图纸渲染', description: '渲染效果和技术质量' },
  76. { key: 'materialQuality', label: '材质', description: '材质选择和表现效果' },
  77. { key: 'lightingQuality', label: '灯光', description: '灯光设计和氛围营造' },
  78. { key: 'textureQuality', label: '纹理', description: '纹理细节和真实感' },
  79. { key: 'structureQuality', label: '硬装结构', description: '结构设计的合理性' },
  80. { key: 'dataCompleteness', label: '资料齐全情况', description: '提供资料的完整性' },
  81. { key: 'requirementUnderstanding', label: '需求理解', description: '对客户需求的理解准确度' },
  82. { key: 'communicationEffectiveness', label: '沟通效果', description: '沟通的有效性和清晰度' },
  83. { key: 'sceneDesignQuality', label: '场景设计质量', description: '各场景设计的整体质量' }
  84. ];
  85. // 场景类型配置
  86. sceneTypes = [
  87. { value: 'bedroom', label: '卧室' },
  88. { value: 'dining_room', label: '餐厅' },
  89. { value: 'living_room', label: '客厅' },
  90. { value: 'other', label: '其他' }
  91. ];
  92. // 评价分类
  93. categories = [
  94. { value: 'color', label: '色彩问题' },
  95. { value: 'furniture', label: '家具款式问题' },
  96. { value: 'lighting', label: '光线问题' },
  97. { value: 'layout', label: '布局问题' },
  98. { value: 'material', label: '材质问题' },
  99. { value: 'other', label: '其他问题' }
  100. ];
  101. // 计算统计数据
  102. stats = computed<ReviewStats>(() => {
  103. const feedbacks = this.feedbacks || [];
  104. const detailedReviews = this.detailedReviews || [];
  105. const categoryStats: { [key: string]: number } = {};
  106. this.categories.forEach(cat => {
  107. categoryStats[cat.value] = feedbacks.filter(f => this.getFeedbackCategory(f) === cat.value).length;
  108. });
  109. const scores = feedbacks.filter(f => f.rating !== undefined).map(f => f.rating || 0);
  110. const averageScore = scores.length > 0 ? scores.reduce((sum, score) => sum + score, 0) / scores.length : 0;
  111. // 计算详细评价维度的平均分
  112. const dimensionAverages: Partial<DetailedReviewDimensions> = {};
  113. if (detailedReviews.length > 0) {
  114. this.reviewDimensions.forEach(dimension => {
  115. const key = dimension.key as keyof DetailedReviewDimensions;
  116. const values = detailedReviews.map(review => review.dimensions[key]).filter(v => v !== undefined);
  117. if (values.length > 0) {
  118. dimensionAverages[key] = values.reduce((sum, val) => sum + val, 0) / values.length;
  119. }
  120. });
  121. }
  122. return {
  123. totalCount: feedbacks.length + detailedReviews.length,
  124. averageScore: Math.round(averageScore * 10) / 10,
  125. satisfiedCount: feedbacks.filter(f => f.isSatisfied).length + detailedReviews.filter(r => r.dimensions.overall >= 4).length,
  126. unsatisfiedCount: feedbacks.filter(f => !f.isSatisfied).length + detailedReviews.filter(r => r.dimensions.overall < 3).length,
  127. pendingCount: feedbacks.filter(f => f.status === '待处理').length + detailedReviews.filter(r => r.status === 'pending').length,
  128. processedCount: feedbacks.filter(f => f.status === '已解决').length + detailedReviews.filter(r => r.status === 'completed').length,
  129. categoryStats,
  130. dimensionAverages
  131. };
  132. });
  133. // 筛选后的评价列表
  134. filteredFeedbacks = computed(() => {
  135. let filtered = this.feedbacks || [];
  136. // 状态筛选
  137. const status = this.statusFilter();
  138. if (status !== 'all') {
  139. if (status === 'satisfied') {
  140. filtered = filtered.filter(f => f.isSatisfied);
  141. } else if (status === 'unsatisfied') {
  142. filtered = filtered.filter(f => !f.isSatisfied);
  143. } else {
  144. filtered = filtered.filter(f => f.status === status);
  145. }
  146. }
  147. // 分类筛选
  148. const category = this.categoryFilter();
  149. if (category !== 'all') {
  150. filtered = filtered.filter(f => this.getFeedbackCategory(f) === category);
  151. }
  152. // 评分筛选
  153. const score = this.scoreFilter();
  154. if (score !== 'all') {
  155. if (score === 'high') {
  156. filtered = filtered.filter(f => (f.rating || 0) >= 4);
  157. } else if (score === 'medium') {
  158. filtered = filtered.filter(f => (f.rating || 0) >= 2 && (f.rating || 0) < 4);
  159. } else if (score === 'low') {
  160. filtered = filtered.filter(f => (f.rating || 0) < 2);
  161. }
  162. }
  163. return filtered.sort((a, b) => {
  164. // 按状态排序:待处理 > 已处理,按时间倒序
  165. if (a.status !== b.status) {
  166. if (a.status === '待处理') return -1;
  167. if (b.status === '待处理') return 1;
  168. }
  169. return new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime();
  170. });
  171. });
  172. // 获取反馈分类
  173. getFeedbackCategory(feedback: CustomerFeedback): string {
  174. const content = feedback.content.toLowerCase();
  175. const location = (feedback.problemLocation || '').toLowerCase();
  176. if (content.includes('色彩') || content.includes('颜色') || location.includes('色彩')) {
  177. return 'color';
  178. } else if (content.includes('家具') || content.includes('沙发') || content.includes('桌子') || location.includes('家具')) {
  179. return 'furniture';
  180. } else if (content.includes('光线') || content.includes('照明') || content.includes('灯光')) {
  181. return 'lighting';
  182. } else if (content.includes('布局') || content.includes('空间') || content.includes('摆放')) {
  183. return 'layout';
  184. } else if (content.includes('材质') || content.includes('质感') || content.includes('纹理')) {
  185. return 'material';
  186. }
  187. return 'other';
  188. }
  189. // 获取分类标签
  190. getCategoryLabel(category: string): string {
  191. const cat = this.categories.find(c => c.value === category);
  192. return cat ? cat.label : '其他问题';
  193. }
  194. // 获取评分星级
  195. getStarRating(score: number): string[] {
  196. const stars = [];
  197. for (let i = 1; i <= 5; i++) {
  198. if (i <= score) {
  199. stars.push('★');
  200. } else if (i - 0.5 <= score) {
  201. stars.push('☆');
  202. } else {
  203. stars.push('☆');
  204. }
  205. }
  206. return stars;
  207. }
  208. // 获取状态样式类
  209. getStatusClass(feedback: CustomerFeedback): string {
  210. if (feedback.status === '已解决') return 'processed';
  211. if (feedback.status === '处理中') return 'processing';
  212. return 'pending';
  213. }
  214. // 获取评分样式类
  215. getScoreClass(score: number): string {
  216. if (score >= 4) return 'high';
  217. if (score >= 2) return 'medium';
  218. return 'low';
  219. }
  220. // 更新筛选条件
  221. updateStatusFilter(status: string): void {
  222. this.statusFilter.set(status);
  223. }
  224. updateCategoryFilter(event: any): void {
  225. this.categoryFilter.set(event.target.value);
  226. }
  227. updateScoreFilter(event: any): void {
  228. this.scoreFilter.set(event.target.value);
  229. }
  230. // 重置筛选条件
  231. resetFilters(): void {
  232. this.statusFilter.set('all');
  233. this.categoryFilter.set('all');
  234. this.scoreFilter.set('all');
  235. }
  236. // 处理客户评价的方法
  237. startProcessing(feedbackId: string): void {
  238. console.log('开始处理客户评价:', feedbackId);
  239. // 这里可以调用服务更新状态
  240. const feedback = this.feedbacks.find(f => f.id === feedbackId);
  241. if (feedback) {
  242. feedback.status = '处理中';
  243. feedback.updatedAt = new Date();
  244. }
  245. }
  246. markAsResolved(feedbackId: string): void {
  247. console.log('标记客户评价为已解决:', feedbackId);
  248. // 这里可以调用服务更新状态
  249. const feedback = this.feedbacks.find(f => f.id === feedbackId);
  250. if (feedback) {
  251. feedback.status = '已解决';
  252. feedback.updatedAt = new Date();
  253. }
  254. }
  255. async openReplyModal(feedback: CustomerFeedback): Promise<void> {
  256. console.log('打开回复模态框:', feedback);
  257. // 这里可以打开一个模态框让用户输入回复内容
  258. const reply = await window?.fmode?.input('请输入回复内容:');
  259. if (reply && reply.trim()) {
  260. feedback.response = reply.trim();
  261. feedback.updatedAt = new Date();
  262. if (feedback.status === '待处理') {
  263. feedback.status = '处理中';
  264. }
  265. }
  266. }
  267. viewDetails(feedback: CustomerFeedback): void {
  268. console.log('查看客户评价详情:', feedback);
  269. // 这里可以打开详情页面或模态框
  270. window?.fmode?.alert(`客户评价详情:\n\n客户: ${feedback.customerName}\n评分: ${feedback.rating}/5\n内容: ${feedback.content}\n回复: ${feedback.response || '暂无回复'}`);
  271. }
  272. // 新增方法:获取维度评分的星级显示
  273. getDimensionStars(score: number): string[] {
  274. const stars = [];
  275. const fullStars = Math.floor(score);
  276. const hasHalfStar = score % 1 >= 0.5;
  277. for (let i = 0; i < 5; i++) {
  278. if (i < fullStars) {
  279. stars.push('★');
  280. } else if (i === fullStars && hasHalfStar) {
  281. stars.push('☆');
  282. } else {
  283. stars.push('☆');
  284. }
  285. }
  286. return stars;
  287. }
  288. // 获取维度评分的颜色类
  289. getDimensionScoreClass(score: number): string {
  290. if (score >= 4.5) return 'score-excellent';
  291. if (score >= 3.5) return 'score-good';
  292. if (score >= 2.5) return 'score-average';
  293. return 'score-poor';
  294. }
  295. // 获取场景类型标签
  296. getSceneTypeLabel(sceneType: string): string {
  297. const scene = this.sceneTypes.find(s => s.value === sceneType);
  298. return scene ? scene.label : sceneType;
  299. }
  300. // 切换视图模式
  301. toggleViewMode(): void {
  302. this.viewMode.set(this.viewMode() === 'simple' ? 'detailed' : 'simple');
  303. }
  304. // 创建新的详细评价
  305. createDetailedReview(projectId: string): void {
  306. console.log('创建详细评价:', projectId);
  307. // 这里可以打开评价表单模态框
  308. }
  309. // 查看详细评价
  310. viewDetailedReview(review: DetailedCustomerReview): void {
  311. console.log('查看详细评价:', review);
  312. // 这里可以打开详细评价查看模态框
  313. }
  314. // 详细评价表单状态
  315. showDetailedForm = false;
  316. currentRatings: { [key: string]: number } = {};
  317. currentSceneRatings: { [key: string]: number } = {};
  318. currentSceneFeedback: { [key: string]: string } = {};
  319. selectedOptimizations: { [key: string]: boolean } = {};
  320. customOptimizationSuggestion = '';
  321. // 优化建议类别
  322. optimizationCategories = [
  323. { key: 'service_speed', label: '服务时效', description: '提高响应速度和交付效率' },
  324. { key: 'communication', label: '沟通效果', description: '加强沟通频率和质量' },
  325. { key: 'design_quality', label: '设计质量', description: '提升设计水平和创意' },
  326. { key: 'material_quality', label: '材质表现', description: '优化材质和纹理效果' },
  327. { key: 'lighting_effect', label: '灯光效果', description: '改善灯光设置和氛围' },
  328. { key: 'detail_accuracy', label: '细节准确性', description: '提高细节还原度' },
  329. { key: 'revision_efficiency', label: '修改效率', description: '加快修改响应速度' },
  330. { key: 'requirement_understanding', label: '需求理解', description: '更准确理解客户需求' }
  331. ];
  332. // 设置维度评分
  333. setDimensionRating(dimensionKey: string, rating: number): void {
  334. this.currentRatings[dimensionKey] = rating;
  335. }
  336. // 设置场景评分
  337. setSceneRating(sceneKey: string, rating: number): void {
  338. this.currentSceneRatings[sceneKey] = rating;
  339. }
  340. // 保存详细评价
  341. saveDetailedReview(): void {
  342. const newReview: DetailedCustomerReview = {
  343. id: Date.now().toString(),
  344. customerName: 'Customer Name',
  345. projectId: 'proj_001',
  346. customerId: 'customer-' + Date.now(),
  347. overallFeedback: '',
  348. submittedAt: new Date(),
  349. status: 'completed' as 'pending' | 'completed',
  350. dimensions: { ...this.currentRatings } as unknown as DetailedReviewDimensions,
  351. sceneReviews: this.buildSceneReviews(),
  352. optimizationSuggestions: this.getSelectedOptimizations(),
  353. improvementSuggestions: this.customOptimizationSuggestion
  354. };
  355. // 添加到详细评价列表
  356. if (!this.detailedReviews) {
  357. this.detailedReviews = [];
  358. }
  359. this.detailedReviews.push(newReview);
  360. console.log('保存详细评价:', newReview);
  361. // 重置表单
  362. this.resetDetailedForm();
  363. this.showDetailedForm = false;
  364. }
  365. // 计算总体评分
  366. calculateOverallRating(): number {
  367. const ratings = Object.values(this.currentRatings);
  368. if (ratings.length === 0) return 0;
  369. const sum = ratings.reduce((acc, rating) => acc + rating, 0);
  370. return Math.round(sum / ratings.length);
  371. }
  372. // 构建场景评价
  373. buildSceneReviews(): SceneReview[] {
  374. return this.sceneTypes
  375. .filter(scene => this.currentSceneRatings[scene.value] > 0)
  376. .map(scene => ({
  377. sceneType: scene.value as 'bedroom' | 'dining_room' | 'living_room' | 'other',
  378. sceneName: scene.label,
  379. designerId: 'current-designer', // 这里应该从项目数据中获取
  380. designerName: '当前设计师', // 这里应该从项目数据中获取
  381. rating: this.currentSceneRatings[scene.value],
  382. feedback: this.currentSceneFeedback[scene.value] || ''
  383. }));
  384. }
  385. // 获取选中的优化建议
  386. getSelectedOptimizations(): string[] {
  387. return this.optimizationCategories
  388. .filter(category => this.selectedOptimizations[category.key])
  389. .map(category => category.label);
  390. }
  391. // 重置详细评价表单
  392. resetDetailedForm(): void {
  393. this.currentRatings = {};
  394. this.currentSceneRatings = {};
  395. this.currentSceneFeedback = {};
  396. this.selectedOptimizations = {};
  397. this.customOptimizationSuggestion = '';
  398. }
  399. // 取消详细评价
  400. cancelDetailedReview(): void {
  401. this.resetDetailedForm();
  402. this.showDetailedForm = false;
  403. }
  404. // 编辑详细评价
  405. editDetailedReview(reviewId: string): void {
  406. const review = this.detailedReviews?.find(r => r.id === reviewId);
  407. if (review) {
  408. // 填充维度评分
  409. this.currentRatings = { ...review.dimensions };
  410. // 填充场景评价数据
  411. review.sceneReviews?.forEach(scene => {
  412. this.currentSceneRatings[scene.sceneType] = scene.rating;
  413. this.currentSceneFeedback[scene.sceneType] = scene.feedback || '';
  414. });
  415. // 填充优化建议
  416. this.selectedOptimizations = {};
  417. review.optimizationSuggestions?.forEach((suggestion: string) => {
  418. const category = this.optimizationCategories.find(cat => cat.label === suggestion);
  419. if (category) {
  420. this.selectedOptimizations[category.key] = true;
  421. }
  422. });
  423. this.customOptimizationSuggestion = review.improvementSuggestions || '';
  424. this.showDetailedForm = true;
  425. }
  426. }
  427. // 导出评价报告
  428. exportReviewReport(): void {
  429. console.log('导出评价报告');
  430. // 这里可以实现导出功能
  431. }
  432. }