project-list.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. import { Component, OnInit, signal, computed } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { Router, RouterModule } from '@angular/router';
  5. import { ProjectService } from '../../../services/project.service';
  6. import { Project, ProjectStatus, ProjectStage } from '../../../models/project.model';
  7. // 定义项目列表项接口,包含计算后的属性
  8. interface ProjectListItem extends Project {
  9. progress: number;
  10. daysUntilDeadline: number;
  11. isUrgent: boolean;
  12. tagDisplayText: string;
  13. }
  14. @Component({
  15. selector: 'app-project-list',
  16. standalone: true,
  17. imports: [CommonModule, FormsModule, RouterModule],
  18. templateUrl: './project-list.html',
  19. styleUrls: ['./project-list.scss', '../customer-service-styles.scss']
  20. })
  21. export class ProjectList implements OnInit {
  22. // 项目列表数据
  23. projects = signal<ProjectListItem[]>([]);
  24. // 原始项目数据(用于筛选)
  25. allProjects = signal<Project[]>([]);
  26. // 视图模式:卡片 / 列表(默认卡片)
  27. viewMode = signal<'card' | 'list'>('card');
  28. // 看板列配置
  29. columns = [
  30. { id: 'pending', name: '待分配' },
  31. { id: 'req', name: '需求深化' },
  32. { id: 'delivery', name: '交付中' },
  33. { id: 'done', name: '已完成' }
  34. ] as const;
  35. // 创建项目弹窗
  36. createModalVisible = signal(false);
  37. newCustomerName = signal('');
  38. newRequirement = signal('');
  39. // 基础项目集合(服务端返回 + 本地生成),用于二次处理
  40. private baseProjects: Project[] = [];
  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(private projectService: ProjectService, private router: Router) {}
  88. ngOnInit(): void {
  89. // 读取上次的视图记忆
  90. const saved = localStorage.getItem('cs.viewMode');
  91. if (saved === 'card' || saved === 'list') {
  92. this.viewMode.set(saved);
  93. }
  94. this.loadProjects();
  95. }
  96. // 视图切换
  97. toggleView(mode: 'card' | 'list') {
  98. if (this.viewMode() !== mode) {
  99. this.viewMode.set(mode);
  100. localStorage.setItem('cs.viewMode', mode);
  101. }
  102. }
  103. // 加载项目列表
  104. loadProjects(): void {
  105. this.projectService.getProjects().subscribe(projects => {
  106. this.allProjects.set(projects);
  107. // 生成基础列表(服务返回 + 模拟)
  108. this.baseProjects = [...projects, ...this.generateMockProjects()];
  109. this.processProjects(this.baseProjects);
  110. });
  111. }
  112. // 处理项目数据,添加计算属性
  113. processProjects(projects: Project[]): void {
  114. const processedProjects = projects.map(project => {
  115. // 计算项目进度(模拟)
  116. const progress = this.calculateProjectProgress(project);
  117. // 计算距离截止日期的天数
  118. const daysUntilDeadline = this.calculateDaysUntilDeadline(project.deadline);
  119. // 判断是否紧急(截止日期前3天或已逾期)
  120. const isUrgent = daysUntilDeadline <= 3 && project.status === '进行中';
  121. // 生成标签显示文本
  122. const tagDisplayText = this.generateTagDisplayText(project);
  123. return {
  124. ...project,
  125. progress,
  126. daysUntilDeadline,
  127. isUrgent,
  128. tagDisplayText
  129. };
  130. });
  131. this.projects.set(this.applyFiltersAndSorting(processedProjects));
  132. }
  133. // 应用筛选和排序
  134. applyFiltersAndSorting(projects: ProjectListItem[]): ProjectListItem[] {
  135. let filteredProjects = [...projects];
  136. // 搜索筛选
  137. if (this.searchTerm().trim()) {
  138. const searchLower = this.searchTerm().toLowerCase().trim();
  139. filteredProjects = filteredProjects.filter(project =>
  140. project.name.toLowerCase().includes(searchLower) ||
  141. project.customerName.toLowerCase().includes(searchLower)
  142. );
  143. }
  144. // 状态筛选(按看板列映射)
  145. if (this.statusFilter() !== 'all') {
  146. const col = this.statusFilter() as 'pending' | 'req' | 'delivery' | 'done';
  147. filteredProjects = filteredProjects.filter(project =>
  148. this.getColumnIdForProject(project) === col
  149. );
  150. }
  151. // 阶段筛选
  152. if (this.stageFilter() !== 'all') {
  153. filteredProjects = filteredProjects.filter(project =>
  154. project.currentStage === this.stageFilter()
  155. );
  156. }
  157. // 排序
  158. filteredProjects.sort((a, b) => {
  159. switch (this.sortBy()) {
  160. case 'deadline':
  161. return new Date(a.deadline).getTime() - new Date(b.deadline).getTime();
  162. case 'createdAt':
  163. return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
  164. case 'name':
  165. return a.name.localeCompare(b.name);
  166. default:
  167. return 0;
  168. }
  169. });
  170. return filteredProjects;
  171. }
  172. // 生成标签显示文本
  173. generateTagDisplayText(project: Project): string {
  174. if (!project.customerTags || project.customerTags.length === 0) {
  175. return '普通项目';
  176. }
  177. const tag = project.customerTags[0];
  178. return `${tag.preference}${tag.needType}`;
  179. }
  180. // 计算项目进度(模拟)
  181. calculateProjectProgress(project: Project): number {
  182. if (project.status === '已完成') return 100;
  183. if (project.status === '已暂停' || project.status === '已延期') return 0;
  184. // 基于当前阶段计算进度
  185. const stageProgress: Record<ProjectStage, number> = {
  186. '需求沟通': 20,
  187. '建模': 40,
  188. '软装': 60,
  189. '渲染': 80,
  190. '后期': 90,
  191. '投诉处理': 100,
  192. 订单创建: 0,
  193. 方案确认: 0,
  194. 尾款结算: 0,
  195. 客户评价: 0
  196. };
  197. return stageProgress[project.currentStage] || 0;
  198. }
  199. // 计算距离截止日期的天数
  200. calculateDaysUntilDeadline(deadline: Date): number {
  201. const now = new Date();
  202. const deadlineDate = new Date(deadline);
  203. const diffTime = deadlineDate.getTime() - now.getTime();
  204. return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  205. }
  206. // 生成模拟项目数据
  207. generateMockProjects(): Project[] {
  208. const statuses: ProjectStatus[] = ['进行中', '已完成', '已暂停', '已延期'];
  209. const stages: ProjectStage[] = ['需求沟通', '建模', '软装', '渲染', '后期', '投诉处理'];
  210. const preferences: ('现代' | '宋式' | '欧式')[] = ['现代', '宋式', '欧式'];
  211. const needTypes: ('硬装' | '软装')[] = ['硬装', '软装'];
  212. const sources: ('朋友圈' | '信息流')[] = ['朋友圈', '信息流'];
  213. const mockProjects: Project[] = [];
  214. for (let i = 1; i <= 12; i++) {
  215. const baseDate = new Date();
  216. const createdDate = new Date(baseDate.setDate(baseDate.getDate() - Math.floor(Math.random() * 30)));
  217. baseDate.setDate(baseDate.getDate() + Math.floor(Math.random() * 15) + 5);
  218. const deadlineDate = new Date(baseDate);
  219. const preference = preferences[Math.floor(Math.random() * preferences.length)];
  220. const needType = needTypes[Math.floor(Math.random() * needTypes.length)];
  221. const source = sources[Math.floor(Math.random() * sources.length)];
  222. const stage = stages[Math.floor(Math.random() * stages.length)];
  223. const status = stage === '投诉处理' ? '已完成' : statuses[Math.floor(Math.random() * 3)];
  224. mockProjects.push({
  225. id: `mock-${i}`,
  226. name: `${preference}风格${needType === '硬装' ? '全屋' : '客厅'}设计`,
  227. customerName: `客户${String.fromCharCode(64 + i)}`,
  228. customerTags: [
  229. {
  230. source: source,
  231. needType: needType,
  232. preference: preference,
  233. colorAtmosphere: i % 2 === 0 ? '简约明亮' : '温馨舒适'
  234. }
  235. ],
  236. highPriorityNeeds: i % 3 === 0 ? ['需快速交付', '重要客户'] : [],
  237. status: status,
  238. currentStage: stage,
  239. createdAt: createdDate,
  240. deadline: deadlineDate,
  241. assigneeId: i % 4 === 0 ? '' : `designer${i % 3 + 1}`,
  242. assigneeName: i % 4 === 0 ? '' : `设计师${String.fromCharCode(64 + (i % 3 + 1))}`,
  243. skillsRequired: [preference + '风格', needType]
  244. });
  245. }
  246. return mockProjects;
  247. }
  248. // 列表/筛选交互(保留已有实现)
  249. onSearch(): void {
  250. // 搜索后重算
  251. this.processProjects(this.baseProjects);
  252. }
  253. onStatusChange(event: Event): void {
  254. const value = (event.target as HTMLSelectElement).value;
  255. this.statusFilter.set(value);
  256. this.processProjects(this.baseProjects);
  257. }
  258. onStageChange(event: Event): void {
  259. const value = (event.target as HTMLSelectElement).value;
  260. this.stageFilter.set(value);
  261. this.processProjects(this.baseProjects);
  262. }
  263. onSortChange(event: Event): void {
  264. const value = (event.target as HTMLSelectElement).value;
  265. this.sortBy.set(value);
  266. this.processProjects(this.baseProjects);
  267. }
  268. goToPage(page: number): void {
  269. if (page >= 1 && page <= this.totalPages()) {
  270. this.currentPage.set(page);
  271. }
  272. }
  273. prevPage(): void {
  274. if (this.currentPage() > 1) {
  275. this.currentPage.update(v => v - 1);
  276. }
  277. }
  278. nextPage(): void {
  279. if (this.currentPage() < this.totalPages()) {
  280. this.currentPage.update(v => v + 1);
  281. }
  282. }
  283. pageNumbers = computed(() => {
  284. const total = this.totalPages();
  285. const pages: number[] = [];
  286. const maxToShow = Math.min(total, 5);
  287. for (let i = 1; i <= maxToShow; i++) pages.push(i);
  288. return pages;
  289. });
  290. getAbsValue(value: number): number {
  291. return Math.abs(value);
  292. }
  293. formatDate(date: Date): string {
  294. const d = new Date(date);
  295. const y = d.getFullYear();
  296. const m = String(d.getMonth() + 1).padStart(2, '0');
  297. const day = String(d.getDate()).padStart(2, '0');
  298. return `${y}-${m}-${day}`;
  299. }
  300. getStatusClass(status: string): string {
  301. switch (status) {
  302. case '进行中': return 'status-in-progress';
  303. case '已完成': return 'status-completed';
  304. case '已暂停': return 'status-paused';
  305. case '已延期': return 'status-overdue';
  306. default: return '';
  307. }
  308. }
  309. getStageClass(stage: string): string {
  310. switch (stage) {
  311. case '需求沟通': return 'stage-requirement';
  312. case '建模': return 'stage-modeling';
  313. case '软装': return 'stage-soft';
  314. case '渲染': return 'stage-render';
  315. case '后期': return 'stage-post';
  316. case '投诉处理': return 'stage-issue';
  317. default: return '';
  318. }
  319. }
  320. // 看板分组逻辑
  321. private isPendingAssignment(p: Project): boolean {
  322. return !p.assigneeId || p.assigneeId.trim() === '';
  323. }
  324. private isRequirementElaboration(p: Project): boolean {
  325. // 已分配但仍在需求沟通阶段
  326. return !this.isCompleted(p) && !this.isPendingAssignment(p) && p.currentStage === '需求沟通';
  327. }
  328. private isInDelivery(p: Project): boolean {
  329. const deliveryStages: ProjectStage[] = ['建模', '软装', '渲染', '后期'];
  330. return !this.isCompleted(p) && !this.isPendingAssignment(p) && deliveryStages.includes(p.currentStage);
  331. }
  332. private isCompleted(p: Project): boolean {
  333. return p.status === '已完成';
  334. }
  335. getProjectsByColumn(columnId: 'pending' | 'req' | 'delivery' | 'done'): ProjectListItem[] {
  336. const list = this.projects();
  337. switch (columnId) {
  338. case 'pending':
  339. return list.filter(p => this.isPendingAssignment(p));
  340. case 'req':
  341. return list.filter(p => this.isRequirementElaboration(p));
  342. case 'delivery':
  343. return list.filter(p => this.isInDelivery(p));
  344. case 'done':
  345. return list.filter(p => this.isCompleted(p));
  346. }
  347. }
  348. // 新增:根据项目状态与阶段推断所在看板列
  349. getColumnIdForProject(project: ProjectListItem): 'pending' | 'req' | 'delivery' | 'done' {
  350. if (this.isPendingAssignment(project)) return 'pending';
  351. if (this.isRequirementElaboration(project)) return 'req';
  352. if (this.isInDelivery(project)) return 'delivery';
  353. if (this.isCompleted(project)) return 'done';
  354. return 'req';
  355. }
  356. // 详情跳转(附带角色与模块)
  357. navigateToProject(project: ProjectListItem, columnId: 'pending' | 'req' | 'delivery' | 'done') {
  358. const tab = columnId === 'pending' ? 'members' : (columnId === 'req' ? 'requirements' : 'overview');
  359. this.router.navigate(['/customer-service/project-detail', project.id], {
  360. queryParams: { role: 'customer_service', activeTab: tab }
  361. });
  362. }
  363. // 新增:直接进入沟通管理(消息)标签
  364. navigateToMessages(project: ProjectListItem) {
  365. this.router.navigate(['/customer-service/project-detail', project.id], {
  366. queryParams: { role: 'customer_service', activeTab: 'messages' }
  367. });
  368. }
  369. // 打开/关闭创建项目弹窗
  370. openCreateProjectModal() {
  371. this.newCustomerName.set('');
  372. this.newRequirement.set('');
  373. this.createModalVisible.set(true);
  374. }
  375. cancelCreateProject() {
  376. this.createModalVisible.set(false);
  377. }
  378. // 提交创建项目(最小必填:客户名称 + 核心需求)
  379. submitCreateProject() {
  380. const customerName = this.newCustomerName().trim();
  381. const requirementText = this.newRequirement().trim();
  382. if (!customerName || !requirementText) {
  383. alert('请填写客户名称和核心需求');
  384. return;
  385. }
  386. const payload = {
  387. customerId: 'temp-' + Date.now(),
  388. customerName,
  389. requirement: requirementText,
  390. referenceCases: [],
  391. tags: { followUpStatus: '待分配' }
  392. };
  393. this.projectService.createProject(payload).subscribe(res => {
  394. if (res.success) {
  395. // 组装前端项目对象(默认待分配:无assignee)
  396. const now = new Date();
  397. const deadline = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
  398. const newProject: Project = {
  399. id: res.projectId,
  400. name: `${customerName} 项目`,
  401. customerName,
  402. customerTags: [],
  403. highPriorityNeeds: [],
  404. status: '进行中',
  405. currentStage: '需求沟通',
  406. createdAt: now,
  407. deadline: deadline,
  408. assigneeId: '',
  409. assigneeName: '',
  410. skillsRequired: []
  411. };
  412. this.baseProjects = [newProject, ...this.baseProjects];
  413. this.processProjects(this.baseProjects);
  414. this.createModalVisible.set(false);
  415. // 新建后滚动到“待分配”列顶部
  416. setTimeout(() => {
  417. const el = document.querySelector('.kanban-column[data-col="pending"]');
  418. el?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
  419. }, 0);
  420. }
  421. });
  422. }
  423. }