project-detail.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. <!-- 项目详情页面内容 -->
  2. <div class="project-detail-container">
  3. <!-- 右侧主要内容 -->
  4. <div class="project-content">
  5. <!-- 项目头部信息 -->
  6. <div class="project-header">
  7. <div class="project-title-section">
  8. <h2 class="project-title">{{ project()?.name || '现代简约风格三居室设计' }}</h2>
  9. <div class="project-meta">
  10. <span class="project-status {{ getProjectStatusClass(project()?.status) }}">
  11. {{ project()?.status || '进行中' }}
  12. </span>
  13. <span class="project-stage">当前阶段:{{ project()?.currentStage || '方案修改与确认' }}</span>
  14. </div>
  15. </div>
  16. <div class="project-actions">
  17. <button class="secondary-btn">
  18. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  19. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
  20. <polyline points="14 2 14 8 20 8"></polyline>
  21. <line x1="16" y1="13" x2="8" y2="13"></line>
  22. <line x1="16" y1="17" x2="8" y2="17"></line>
  23. <polyline points="10 9 9 9 8 9"></polyline>
  24. </svg>
  25. <span>导出报告</span>
  26. </button>
  27. <button class="primary-btn">
  28. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  29. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
  30. <polyline points="22 4 12 14.01 9 11.01"></polyline>
  31. </svg>
  32. <span>联系客户</span>
  33. </button>
  34. </div>
  35. </div>
  36. <!-- 项目进度卡片 -->
  37. <div class="progress-card">
  38. <div class="progress-header">
  39. <h3>项目进度</h3>
  40. <span class="progress-percentage">{{ completionProgress() }}%</span>
  41. </div>
  42. <div class="progress-bar">
  43. <div class="progress-fill" [style.width]="progressFillWidth()"></div>
  44. </div>
  45. <div class="progress-meta">
  46. <span>开始时间:{{ formatDate(project()?.createdAt || '2023-06-01') }}</span>
  47. <span>预计完成:{{ formatDate(project()?.deadline || '2023-07-15') }}</span>
  48. </div>
  49. </div>
  50. <!-- 项目详情标签页 -->
  51. <div class="project-tabs">
  52. <div class="tab-header">
  53. <button class="tab-btn" [class.active]="activeTab() === 'overview'" (click)="switchTab('overview')">
  54. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  55. <path d="M5 12h14M12 5l7 7-7 7"></path>
  56. </svg>
  57. <span>概览</span>
  58. </button>
  59. <button class="tab-btn" [class.active]="activeTab() === 'milestones'" (click)="switchTab('milestones')">
  60. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  61. <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
  62. </svg>
  63. <span>里程碑</span>
  64. </button>
  65. <button class="tab-btn" [class.active]="activeTab() === 'tasks'" (click)="switchTab('tasks')">
  66. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  67. <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
  68. </svg>
  69. <span>任务</span>
  70. </button>
  71. <button class="tab-btn" [class.active]="activeTab() === 'messages'" (click)="switchTab('messages')">
  72. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  73. <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
  74. </svg>
  75. <span>消息</span>
  76. </button>
  77. <button class="tab-btn" [class.active]="activeTab() === 'files'" (click)="switchTab('files')">
  78. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  79. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
  80. <polyline points="14 2 14 8 20 8"></polyline>
  81. <line x1="16" y1="13" x2="8" y2="13"></line>
  82. <line x1="16" y1="17" x2="8" y2="17"></line>
  83. <polyline points="10 9 9 9 8 9"></polyline>
  84. </svg>
  85. <span>文件</span>
  86. </button>
  87. </div>
  88. <!-- 消息标签内容 - 只保留一个消息输入区域 -->
  89. <div *ngIf="activeTab() === 'messages'" class="tab-content">
  90. <div class="messages-container">
  91. <div class="messages-list">
  92. <!-- 消息列表内容保持不变 -->
  93. </div>
  94. <div class="message-input-area">
  95. <!-- 只使用单向绑定 + input事件,移除双向绑定 -->
  96. <textarea
  97. [value]="newMessage()"
  98. (input)="onMessageInput($event)"
  99. placeholder="输入消息内容..."
  100. rows="3"
  101. (keydown.enter.shift)="$event.preventDefault()"
  102. (keydown.enter)="sendMessage()"
  103. ></textarea>
  104. <div class="message-actions">
  105. <button class="secondary-btn">
  106. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  107. <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
  108. <polyline points="14 2 14 8 20 8"></polyline>
  109. </svg>
  110. <span>上传文件</span>
  111. </button>
  112. <button class="primary-btn" (click)="sendMessage()" [disabled]="!newMessage().trim()">
  113. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  114. <line x1="22" y1="2" x2="11" y2="13"></line>
  115. <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
  116. </svg>
  117. <span>发送</span>
  118. </button>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. <!-- 概览标签内容 -->
  124. <div *ngIf="activeTab() === 'overview'" class="tab-content">
  125. <div class="overview-grid">
  126. <!-- 客户信息卡片 -->
  127. <div class="info-card">
  128. <h4 class="card-title">客户信息</h4>
  129. <div class="customer-info">
  130. <div class="info-item">
  131. <label>客户姓名</label>
  132. <span>{{ project()?.customerName || '王先生' }}</span>
  133. </div>
  134. <div class="info-item">
  135. <label>联系方式</label>
  136. <span>138****5678</span>
  137. </div>
  138. <div class="info-item">
  139. <label>标签</label>
  140. <div class="tag-container">
  141. <span class="tag">朋友圈</span>
  142. <span class="tag">软装</span>
  143. <span class="tag">现代风格</span>
  144. </div>
  145. </div>
  146. <div class="info-item">
  147. <label>高优先级需求</label>
  148. <ul class="need-list">
  149. <li *ngFor="let need of project()?.highPriorityNeeds || ['客厅光线充足', '储物空间充足', '环保材料']">
  150. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  151. <polyline points="20 6 9 17 4 12"></polyline>
  152. </svg>
  153. {{ need }}
  154. </li>
  155. </ul>
  156. </div>
  157. </div>
  158. </div>
  159. <!-- 项目团队卡片 -->
  160. <div class="info-card">
  161. <h4 class="card-title">项目团队</h4>
  162. <div class="team-info">
  163. <div class="team-member">
  164. <img src="https://picsum.photos/id/64/48/48" alt="客服小李" class="member-avatar">
  165. <div class="member-details">
  166. <div class="member-name">客服小李</div>
  167. <div class="member-role">客户经理</div>
  168. </div>
  169. <button class="message-btn">
  170. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  171. <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
  172. </svg>
  173. </button>
  174. </div>
  175. <div class="team-member">
  176. <img src="https://picsum.photos/id/91/48/48" alt="张设计师" class="member-avatar">
  177. <div class="member-details">
  178. <div class="member-name">张设计师</div>
  179. <div class="member-role">主设计师</div>
  180. </div>
  181. <button class="message-btn">
  182. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  183. <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
  184. </svg>
  185. </button>
  186. </div>
  187. </div>
  188. </div>
  189. <!-- 最近反馈卡片 -->
  190. <div class="info-card">
  191. <h4 class="card-title">客户反馈</h4>
  192. <div class="feedback-list">
  193. <div *ngFor="let feedback of feedbacks()" class="feedback-item">
  194. <!-- 修改前 -->
  195. <div class="feedback-item">
  196. <div class="feedback-header">
  197. <div class="feedback-author">{{ feedback?.customerName || '未知客户' }}</div>
  198. <div class="feedback-rating">
  199. <span class="rating-stars">★★★★☆</span>
  200. <span class="rating-number">{{ feedback?.rating || 0 }}.0</span>
  201. </div>
  202. </div>
  203. <div class="feedback-content">{{ feedback?.content || '' }}</div>
  204. <div class="feedback-response" *ngIf="feedback?.response">
  205. <div class="response-label">客服回复:</div>
  206. <div class="response-text">{{ feedback.response }}</div>
  207. </div>
  208. <div class="feedback-meta">
  209. <span class="feedback-date">{{ formatDate(feedback?.createdAt) }}</span>
  210. <span class="feedback-status" [class.status-processed]="feedback?.status === '已解决'" [class.status-pending]="feedback?.status === '待处理'">
  211. {{ feedback?.status || '未知状态' }}
  212. </span>
  213. </div>
  214. </div>
  215. <!-- 修改后 -->
  216. <div class="feedback-item">
  217. <div class="feedback-header">
  218. <div class="feedback-author">{{ getFeedbackCustomerName(feedback) }}</div>
  219. <div class="feedback-rating">
  220. <span class="rating-stars">★★★★☆</span>
  221. <span class="rating-number">{{ getFeedbackRating(feedback) }}.0</span>
  222. </div>
  223. </div>
  224. <div class="feedback-content">{{ feedback?.content || '' }}</div>
  225. <div class="feedback-response" *ngIf="feedback?.response">
  226. <div class="response-label">客服回复:</div>
  227. <div class="response-text">{{ feedback.response }}</div>
  228. </div>
  229. <div class="feedback-meta">
  230. <span class="feedback-date">{{ formatDate(feedback?.createdAt) }}</span>
  231. <span class="feedback-status" [class.status-processed]="feedback?.status === '已解决'" [class.status-pending]="feedback?.status === '待处理'" [class.status-processing]="feedback?.status === '处理中'">
  232. {{ feedback?.status || '未知状态' }}
  233. </span>
  234. </div>
  235. </div>
  236. <button class="view-all-btn" *ngIf="feedbacks().length > 0">查看全部反馈</button>
  237. </div>
  238. </div>
  239. </div>
  240. </div>
  241. <!-- 里程碑标签内容 -->
  242. <div *ngIf="activeTab() === 'milestones'" class="tab-content">
  243. <div class="milestones-timeline">
  244. <div *ngFor="let milestone of milestones(); index as i" class="milestone-item">
  245. <div class="milestone-dot" [class.completed]="milestone.isCompleted"></div>
  246. <div class="milestone-line" *ngIf="i < milestones().length - 1" [class.completed]="milestone.isCompleted && milestones()[i+1].isCompleted"></div>
  247. <div class="milestone-content">
  248. <div class="milestone-header">
  249. <h4 class="milestone-title">{{ milestone.title }}</h4>
  250. <span class="milestone-status" [class.status-completed]="milestone.isCompleted" [class.status-pending]="!milestone.isCompleted">
  251. {{ milestone.isCompleted ? '已完成' : '进行中' }}
  252. </span>
  253. </div>
  254. <p class="milestone-description">{{ milestone.description }}</p>
  255. <div class="milestone-dates">
  256. <div class="date-item">
  257. <label>截止日期</label>
  258. <span>{{ formatDate(milestone.dueDate) }}</span>
  259. </div>
  260. <div class="date-item" *ngIf="milestone.completedDate">
  261. <label>完成日期</label>
  262. <span>{{ formatDate(milestone.completedDate) }}</span>
  263. </div>
  264. </div>
  265. <div class="milestone-actions" *ngIf="!milestone.isCompleted">
  266. <button class="primary-btn small" (click)="completeMilestone(milestone.id)">
  267. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  268. <polyline points="20 6 9 17 4 12"></polyline>
  269. </svg>
  270. <span>标记完成</span>
  271. </button>
  272. </div>
  273. </div>
  274. </div>
  275. </div>
  276. </div>
  277. <!-- 任务标签内容 -->
  278. <div *ngIf="activeTab() === 'tasks'" class="tab-content">
  279. <div class="tasks-filter">
  280. <div class="filter-options">
  281. <button class="filter-btn active">全部任务</button>
  282. <button class="filter-btn">进行中</button>
  283. <button class="filter-btn">已完成</button>
  284. <button class="filter-btn">逾期</button>
  285. </div>
  286. <div class="search-box">
  287. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  288. <circle cx="11" cy="11" r="8"></circle>
  289. <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
  290. </svg>
  291. <input type="text" placeholder="搜索任务...">
  292. </div>
  293. </div>
  294. <div class="tasks-list">
  295. <!-- 修复任务列表中的状态显示,确保安全访问 -->
  296. <div *ngFor="let task of tasks()" class="task-item">
  297. <div class="task-checkbox">
  298. <input type="checkbox" [checked]="task.isCompleted" (change)="task.isCompleted ? null : completeTask(task.id)">
  299. </div>
  300. <div class="task-content">
  301. <h4 class="task-title" [class.completed]="task.isCompleted">{{ task.title || '未命名任务' }}</h4>
  302. <p class="task-description">{{ task.description || '' }}</p>
  303. <div class="task-meta">
  304. <span class="task-assignee">
  305. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  306. <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
  307. <circle cx="9" cy="7" r="4"></circle>
  308. <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
  309. <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
  310. </svg>
  311. {{ task.assignee || '未分配' }}
  312. </span>
  313. <span class="task-deadline" [class.overdue]="task.isOverdue">
  314. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  315. <circle cx="12" cy="12" r="10"></circle>
  316. <polyline points="12 6 12 12 16 14"></polyline>
  317. </svg>
  318. {{ formatDate(task.deadline) }}
  319. </span>
  320. <span class="task-priority" [class.priority-high]="task.priority === 'high'" [class.priority-medium]="task.priority === 'medium'" [class.priority-low]="task.priority === 'low'">
  321. {{ task.priority === 'high' ? '高' : task.priority === 'medium' ? '中' : '低' }}
  322. </span>
  323. </div>
  324. </div>
  325. </div>
  326. </div>
  327. </div>
  328. <!-- 消息标签内容 -->
  329. <div *ngIf="activeTab() === 'messages'" class="tab-content">
  330. <div class="messages-container">
  331. <div class="messages-list">
  332. <div *ngFor="let message of messages()" class="message-item">
  333. <div class="message-avatar">
  334. {{ message.sender.charAt(0) }}
  335. </div>
  336. <div class="message-content">
  337. <div class="message-header">
  338. <span class="message-sender">{{ message.sender }}</span>
  339. <span class="message-time">{{ formatDateTime(message.timestamp) }}</span>
  340. </div>
  341. <div class="message-text">{{ message.content }}</div>
  342. </div>
  343. </div>
  344. </div>
  345. <!-- 修复消息输入区域,避免重复定义 -->
  346. <div class="message-input-area">
  347. <!-- 只使用单向绑定 + input事件 -->
  348. <textarea
  349. [value]="newMessage()"
  350. (input)="onMessageInput($event)"
  351. placeholder="输入消息内容..."
  352. rows="3"
  353. (keydown.enter.shift)="$event.preventDefault()"
  354. (keydown.enter)="sendMessage()"
  355. ></textarea>
  356. <div class="message-actions">
  357. <button class="secondary-btn">
  358. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  359. <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
  360. <polyline points="14 2 14 8 20 8"></polyline>
  361. </svg>
  362. <span>上传文件</span>
  363. </button>
  364. <button class="primary-btn" (click)="sendMessage()" [disabled]="!newMessage().trim()">
  365. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  366. <line x1="22" y1="2" x2="11" y2="13"></line>
  367. <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
  368. </svg>
  369. <span>发送</span>
  370. </button>
  371. </div>
  372. </div>
  373. </div>
  374. </div>
  375. <!-- 文件标签内容 -->
  376. <div *ngIf="activeTab() === 'files'" class="tab-content">
  377. <div class="files-filter">
  378. <div class="filter-options">
  379. <button class="filter-btn active">全部文件</button>
  380. <button class="filter-btn">文档</button>
  381. <button class="filter-btn">图片</button>
  382. <button class="filter-btn">表格</button>
  383. </div>
  384. <div class="search-box">
  385. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  386. <circle cx="11" cy="11" r="8"></circle>
  387. <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
  388. </svg>
  389. <input type="text" placeholder="搜索文件...">
  390. </div>
  391. </div>
  392. <div class="files-list">
  393. <div *ngFor="let file of files()" class="file-item">
  394. <div class="file-icon" [class.type-document]="file.type === 'document'" [class.type-image]="file.type === 'image'" [class.type-spreadsheet]="file.type === 'spreadsheet'">
  395. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  396. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
  397. <polyline points="14 2 14 8 20 8"></polyline>
  398. <line x1="16" y1="13" x2="8" y2="13"></line>
  399. <line x1="16" y1="17" x2="8" y2="17"></line>
  400. <polyline points="10 9 9 9 8 9"></polyline>
  401. </svg>
  402. </div>
  403. <div class="file-info">
  404. <h4 class="file-name">{{ file.name }}</h4>
  405. <div class="file-meta">
  406. <span class="file-uploader">上传者:{{ file.uploadedBy }}</span>
  407. <span class="file-date">{{ formatDate(file.uploadedAt) }}</span>
  408. <span class="file-size">{{ file.size }}</span>
  409. </div>
  410. </div>
  411. <div class="file-actions">
  412. <button class="action-btn" (click)="previewFile(file)" title="预览">
  413. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  414. <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
  415. </svg>
  416. </button>
  417. <button class="action-btn" (click)="downloadFile(file)" title="下载">
  418. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  419. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  420. <polyline points="7 10 12 15 17 10"></polyline>
  421. <line x1="12" y1="15" x2="12" y2="3"></line>
  422. </svg>
  423. </button>
  424. </div>
  425. </div>
  426. </div>
  427. </div>
  428. </div>
  429. </div>
  430. </div>