project-list.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. import { Component, OnInit, OnDestroy, signal, computed, Inject } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { Router, RouterModule } from '@angular/router';
  5. import { MatDialog, MatDialogModule } from '@angular/material/dialog';
  6. import { ProjectService } from '../../../services/project.service';
  7. import { ConsultationOrderDialogComponent } from '../consultation-order/consultation-order-dialog.component';
  8. import { Project, ProjectStatus, ProjectStage } from '../../../models/project.model';
  9. // 定义项目列表项接口,包含计算后的属性
  10. interface ProjectListItem extends Project {
  11. progress: number;
  12. daysUntilDeadline: number;
  13. isUrgent: boolean;
  14. tagDisplayText: string;
  15. }
  16. @Component({
  17. selector: 'app-project-list',
  18. standalone: true,
  19. imports: [CommonModule, FormsModule, RouterModule, MatDialogModule],
  20. templateUrl: './project-list.html',
  21. styleUrls: ['./project-list.scss', '../customer-service-styles.scss']
  22. })
  23. export class ProjectList implements OnInit, OnDestroy {
  24. // 项目列表数据
  25. projects = signal<ProjectListItem[]>([]);
  26. // 原始项目数据(用于筛选)
  27. allProjects = signal<Project[]>([]);
  28. // 视图模式:卡片 / 列表 / 监控大盘(默认卡片)
  29. viewMode = signal<'card' | 'list' | 'dashboard'>('card');
  30. // 看板列配置
  31. columns = [
  32. { id: 'pending', name: '待分配' },
  33. { id: 'req', name: '需求深化' },
  34. { id: 'delivery', name: '交付中' },
  35. { id: 'done', name: '已完成' }
  36. ] as const;
  37. // 基础项目集合(服务端返回 + 本地生成),用于二次处理
  38. private baseProjects: Project[] = [];
  39. // 消息监听器
  40. private messageListener?: (event: MessageEvent) => void;
  41. // 添加toggleSidebar方法
  42. toggleSidebar(): void {
  43. // 侧边栏切换逻辑
  44. console.log('Toggle sidebar');
  45. }
  46. // 筛选和排序状态
  47. searchTerm = signal('');
  48. statusFilter = signal<string>('all');
  49. stageFilter = signal<string>('all');
  50. sortBy = signal<string>('deadline');
  51. // 当前页码
  52. currentPage = signal(1);
  53. // 每页显示数量
  54. pageSize = 8;
  55. // 分页后的项目列表(列表模式下可用)
  56. paginatedProjects = computed(() => {
  57. const filteredProjects = this.projects();
  58. const startIndex = (this.currentPage() - 1) * this.pageSize;
  59. return filteredProjects.slice(startIndex, startIndex + this.pageSize);
  60. });
  61. // 总页数
  62. totalPages = computed(() => {
  63. return Math.ceil(this.projects().length / this.pageSize);
  64. });
  65. // 筛选和排序选项
  66. statusOptions = [
  67. { value: 'all', label: '全部' },
  68. { value: 'pending', label: '待分配' },
  69. { value: 'req', label: '需求深化' },
  70. { value: 'delivery', label: '交付中' },
  71. { value: 'done', label: '已完成' }
  72. ];
  73. stageOptions = [
  74. { value: 'all', label: '全部阶段' },
  75. { value: '需求沟通', label: '需求沟通' },
  76. { value: '建模', label: '建模' },
  77. { value: '软装', label: '软装' },
  78. { value: '渲染', label: '渲染' },
  79. { value: '后期', label: '后期' },
  80. { value: '投诉处理', label: '投诉处理' }
  81. ];
  82. sortOptions = [
  83. { value: 'deadline', label: '截止日期' },
  84. { value: 'createdAt', label: '创建时间' },
  85. { value: 'name', label: '项目名称' }
  86. ];
  87. constructor(
  88. private projectService: ProjectService,
  89. private router: Router,
  90. private dialog: MatDialog
  91. ) {}
  92. ngOnInit(): void {
  93. // 读取上次的视图记忆
  94. const saved = localStorage.getItem('cs.viewMode');
  95. if (saved === 'card' || saved === 'list' || saved === 'dashboard') {
  96. this.viewMode.set(saved as 'card' | 'list' | 'dashboard');
  97. }
  98. this.loadProjects();
  99. // 添加消息监听器,处理来自iframe的导航请求
  100. this.messageListener = (event: MessageEvent) => {
  101. // 验证消息来源(可以根据需要添加更严格的验证)
  102. if (event.data && event.data.type === 'navigate' && event.data.route) {
  103. this.router.navigate([event.data.route]);
  104. }
  105. };
  106. window.addEventListener('message', this.messageListener);
  107. }
  108. ngOnDestroy(): void {
  109. // 清理消息监听器
  110. if (this.messageListener) {
  111. window.removeEventListener('message', this.messageListener);
  112. }
  113. }
  114. // 视图切换
  115. toggleView(mode: 'card' | 'list' | 'dashboard') {
  116. if (this.viewMode() !== mode) {
  117. this.viewMode.set(mode);
  118. localStorage.setItem('cs.viewMode', mode);
  119. }
  120. }
  121. // 加载项目列表
  122. loadProjects(): void {
  123. this.projectService.getProjects().subscribe(projects => {
  124. this.allProjects.set(projects);
  125. // 生成基础列表(服务返回 + 模拟)
  126. this.baseProjects = [...projects, ...this.generateMockProjects()];
  127. this.processProjects(this.baseProjects);
  128. });
  129. }
  130. // 处理项目数据,添加计算属性
  131. processProjects(projects: Project[]): void {
  132. const processedProjects = projects.map(project => {
  133. // 计算项目进度(模拟)
  134. const progress = this.calculateProjectProgress(project);
  135. // 计算距离截止日期的天数
  136. const daysUntilDeadline = this.calculateDaysUntilDeadline(project.deadline);
  137. // 判断是否紧急(截止日期前3天或已逾期)
  138. const isUrgent = daysUntilDeadline <= 3 && project.status === '进行中';
  139. // 生成标签显示文本
  140. const tagDisplayText = this.generateTagDisplayText(project);
  141. return {
  142. ...project,
  143. progress,
  144. daysUntilDeadline,
  145. isUrgent,
  146. tagDisplayText
  147. };
  148. });
  149. this.projects.set(this.applyFiltersAndSorting(processedProjects));
  150. }
  151. // 应用筛选和排序
  152. applyFiltersAndSorting(projects: ProjectListItem[]): ProjectListItem[] {
  153. let filteredProjects = [...projects];
  154. // 搜索筛选
  155. if (this.searchTerm().trim()) {
  156. const searchLower = this.searchTerm().toLowerCase().trim();
  157. filteredProjects = filteredProjects.filter(project =>
  158. project.name.toLowerCase().includes(searchLower) ||
  159. project.customerName.toLowerCase().includes(searchLower)
  160. );
  161. }
  162. // 状态筛选(按看板列映射)
  163. if (this.statusFilter() !== 'all') {
  164. const col = this.statusFilter() as 'pending' | 'req' | 'delivery' | 'done';
  165. filteredProjects = filteredProjects.filter(project =>
  166. this.getColumnIdForProject(project) === col
  167. );
  168. }
  169. // 阶段筛选
  170. if (this.stageFilter() !== 'all') {
  171. filteredProjects = filteredProjects.filter(project =>
  172. project.currentStage === this.stageFilter()
  173. );
  174. }
  175. // 排序
  176. filteredProjects.sort((a, b) => {
  177. switch (this.sortBy()) {
  178. case 'deadline':
  179. return new Date(a.deadline).getTime() - new Date(b.deadline).getTime();
  180. case 'createdAt':
  181. return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
  182. case 'name':
  183. return a.name.localeCompare(b.name);
  184. default:
  185. return 0;
  186. }
  187. });
  188. return filteredProjects;
  189. }
  190. // 生成标签显示文本
  191. generateTagDisplayText(project: Project): string {
  192. if (!project.customerTags || project.customerTags.length === 0) {
  193. return '普通项目';
  194. }
  195. const tag = project.customerTags[0];
  196. return `${tag.preference}${tag.needType}`;
  197. }
  198. // 计算项目进度(模拟)
  199. calculateProjectProgress(project: Project): number {
  200. if (project.status === '已完成') return 100;
  201. if (project.status === '已暂停' || project.status === '已延期') return 0;
  202. // 基于当前阶段计算进度
  203. const stageProgress: Record<ProjectStage, number> = {
  204. '需求沟通': 20,
  205. '建模': 40,
  206. '软装': 60,
  207. '渲染': 80,
  208. '后期': 90,
  209. '投诉处理': 100,
  210. 订单创建: 0,
  211. 方案确认: 0,
  212. 尾款结算: 0,
  213. 客户评价: 0
  214. };
  215. return stageProgress[project.currentStage] || 0;
  216. }
  217. // 计算距离截止日期的天数
  218. calculateDaysUntilDeadline(deadline: Date): number {
  219. const now = new Date();
  220. const deadlineDate = new Date(deadline);
  221. const diffTime = deadlineDate.getTime() - now.getTime();
  222. return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  223. }
  224. // 生成模拟项目数据
  225. generateMockProjects(): Project[] {
  226. const statuses: ProjectStatus[] = ['进行中', '已完成', '已暂停', '已延期'];
  227. const stages: ProjectStage[] = ['需求沟通', '建模', '软装', '渲染', '后期', '投诉处理'];
  228. const preferences: ('现代' | '宋式' | '欧式')[] = ['现代', '宋式', '欧式'];
  229. const needTypes: ('硬装' | '软装')[] = ['硬装', '软装'];
  230. const sources: ('朋友圈' | '信息流')[] = ['朋友圈', '信息流'];
  231. const mockProjects: Project[] = [];
  232. for (let i = 1; i <= 12; i++) {
  233. const baseDate = new Date();
  234. const createdDate = new Date(baseDate.setDate(baseDate.getDate() - Math.floor(Math.random() * 30)));
  235. baseDate.setDate(baseDate.getDate() + Math.floor(Math.random() * 15) + 5);
  236. const deadlineDate = new Date(baseDate);
  237. const preference = preferences[Math.floor(Math.random() * preferences.length)];
  238. const needType = needTypes[Math.floor(Math.random() * needTypes.length)];
  239. const source = sources[Math.floor(Math.random() * sources.length)];
  240. const stage = stages[Math.floor(Math.random() * stages.length)];
  241. const status = stage === '投诉处理' ? '已完成' : statuses[Math.floor(Math.random() * 3)];
  242. mockProjects.push({
  243. id: `mock-${i}`,
  244. name: `${preference}风格${needType === '硬装' ? '全屋' : '客厅'}设计`,
  245. customerName: `客户${String.fromCharCode(64 + i)}`,
  246. customerTags: [
  247. {
  248. source: source,
  249. needType: needType,
  250. preference: preference,
  251. colorAtmosphere: i % 2 === 0 ? '简约明亮' : '温馨舒适'
  252. }
  253. ],
  254. highPriorityNeeds: i % 3 === 0 ? ['需快速交付', '重要客户'] : [],
  255. status: status,
  256. currentStage: stage,
  257. stage: stage,
  258. createdAt: createdDate,
  259. deadline: deadlineDate,
  260. assigneeId: i % 4 === 0 ? '' : `designer${i % 3 + 1}`,
  261. assigneeName: i % 4 === 0 ? '' : `设计师${String.fromCharCode(64 + (i % 3 + 1))}`,
  262. skillsRequired: [preference + '风格', needType]
  263. });
  264. }
  265. return mockProjects;
  266. }
  267. // 列表/筛选交互(保留已有实现)
  268. onSearch(): void {
  269. // 搜索后重算
  270. this.processProjects(this.baseProjects);
  271. }
  272. onStatusChange(event: Event): void {
  273. const value = (event.target as HTMLSelectElement).value;
  274. this.statusFilter.set(value);
  275. this.processProjects(this.baseProjects);
  276. }
  277. onStageChange(event: Event): void {
  278. const value = (event.target as HTMLSelectElement).value;
  279. this.stageFilter.set(value);
  280. this.processProjects(this.baseProjects);
  281. }
  282. onSortChange(event: Event): void {
  283. const value = (event.target as HTMLSelectElement).value;
  284. this.sortBy.set(value);
  285. this.processProjects(this.baseProjects);
  286. }
  287. goToPage(page: number): void {
  288. if (page >= 1 && page <= this.totalPages()) {
  289. this.currentPage.set(page);
  290. }
  291. }
  292. prevPage(): void {
  293. if (this.currentPage() > 1) {
  294. this.currentPage.update(v => v - 1);
  295. }
  296. }
  297. nextPage(): void {
  298. if (this.currentPage() < this.totalPages()) {
  299. this.currentPage.update(v => v + 1);
  300. }
  301. }
  302. pageNumbers = computed(() => {
  303. const total = this.totalPages();
  304. const pages: number[] = [];
  305. const maxToShow = Math.min(total, 5);
  306. for (let i = 1; i <= maxToShow; i++) pages.push(i);
  307. return pages;
  308. });
  309. getAbsValue(value: number): number {
  310. return Math.abs(value);
  311. }
  312. formatDate(date: Date): string {
  313. const d = new Date(date);
  314. const y = d.getFullYear();
  315. const m = String(d.getMonth() + 1).padStart(2, '0');
  316. const day = String(d.getDate()).padStart(2, '0');
  317. return `${y}-${m}-${day}`;
  318. }
  319. getStatusClass(status: string): string {
  320. switch (status) {
  321. case '进行中': return 'status-in-progress';
  322. case '已完成': return 'status-completed';
  323. case '已暂停': return 'status-paused';
  324. case '已延期': return 'status-overdue';
  325. default: return '';
  326. }
  327. }
  328. getStageClass(stage: string): string {
  329. switch (stage) {
  330. case '需求沟通': return 'stage-communication';
  331. case '建模': return 'stage-modeling';
  332. case '软装': return 'stage-decoration';
  333. case '渲染': return 'stage-rendering';
  334. case '后期': return 'stage-postproduction';
  335. case '投诉处理': return 'stage-completed';
  336. case '订单创建': return 'stage-active';
  337. case '方案确认': return 'stage-active';
  338. case '尾款结算': return 'stage-completed';
  339. case '客户评价': return 'stage-completed';
  340. default: return '';
  341. }
  342. }
  343. // 看板分组逻辑
  344. private isPendingAssignment(p: Project): boolean {
  345. return !p.assigneeId || p.assigneeId.trim() === '';
  346. }
  347. private isRequirementElaboration(p: Project): boolean {
  348. // 已分配但仍在需求沟通阶段
  349. return !this.isCompleted(p) && !this.isPendingAssignment(p) && p.currentStage === '需求沟通';
  350. }
  351. private isInDelivery(p: Project): boolean {
  352. const deliveryStages: ProjectStage[] = ['建模', '软装', '渲染', '后期'];
  353. return !this.isCompleted(p) && !this.isPendingAssignment(p) && deliveryStages.includes(p.currentStage);
  354. }
  355. private isCompleted(p: Project): boolean {
  356. return p.status === '已完成';
  357. }
  358. getProjectsByColumn(columnId: 'pending' | 'req' | 'delivery' | 'done'): ProjectListItem[] {
  359. const list = this.projects();
  360. switch (columnId) {
  361. case 'pending':
  362. return list.filter(p => this.isPendingAssignment(p));
  363. case 'req':
  364. return list.filter(p => this.isRequirementElaboration(p));
  365. case 'delivery':
  366. return list.filter(p => this.isInDelivery(p));
  367. case 'done':
  368. return list.filter(p => this.isCompleted(p));
  369. }
  370. }
  371. // 新增:根据项目状态与阶段推断所在看板列
  372. getColumnIdForProject(project: ProjectListItem): 'pending' | 'req' | 'delivery' | 'done' {
  373. if (this.isPendingAssignment(project)) return 'pending';
  374. if (this.isRequirementElaboration(project)) return 'req';
  375. if (this.isInDelivery(project)) return 'delivery';
  376. if (this.isCompleted(project)) return 'done';
  377. return 'req';
  378. }
  379. // 详情跳转到设计师项目详情页面,传递客服角色标识和当前阶段信息
  380. navigateToProject(project: ProjectListItem, columnId: 'pending' | 'req' | 'delivery' | 'done') {
  381. // 根据columnId映射到对应的阶段
  382. const stageMapping = {
  383. 'pending': '订单创建',
  384. 'req': project.currentStage || '需求沟通', // 使用项目实际阶段或默认阶段
  385. 'delivery': project.currentStage || '建模', // 使用项目实际阶段或默认阶段
  386. 'done': '客户评价'
  387. };
  388. this.router.navigate(['/designer/project-detail', project.id], {
  389. queryParams: {
  390. role: 'customer-service',
  391. activeTab: 'progress',
  392. currentStage: stageMapping[columnId]
  393. }
  394. });
  395. }
  396. // 新增:直接进入沟通管理(消息)标签
  397. navigateToMessages(project: ProjectListItem) {
  398. this.router.navigate(['/customer-service/messages'], { queryParams: { projectId: project.id } });
  399. }
  400. // 导航到创建订单页面
  401. navigateToCreateOrder() {
  402. // 打开咨询订单弹窗
  403. const dialogRef = this.dialog.open(ConsultationOrderDialogComponent, {
  404. width: '900px',
  405. maxWidth: '95vw',
  406. maxHeight: '90vh',
  407. panelClass: 'consultation-order-dialog'
  408. });
  409. // 监听订单创建成功事件
  410. dialogRef.componentInstance.orderCreated.subscribe((orderData: any) => {
  411. // 关闭弹窗
  412. dialogRef.close();
  413. // 跳转到新创建的项目详情页面
  414. this.router.navigate([
  415. '/designer/project-detail',
  416. orderData.orderId
  417. ], {
  418. queryParams: {
  419. role: 'customer-service',
  420. activeTab: 'overview'
  421. }
  422. });
  423. });
  424. }
  425. }