project-detail.html 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. <!-- 只展示修改处,未变更部分用占位注释表示 -->
  2. <div class="project-detail-container designer-page">
  3. <!-- 项目标题栏 -->
  4. <div class="project-header card">
  5. <div class="header-left">
  6. <div class="header-content">
  7. <h1>项目详情</h1>
  8. <div class="project-meta">
  9. <span class="project-id">项目ID: {{ projectId }}</span>
  10. @if (project) { <span class="project-status">{{ project.status }}</span> }
  11. <!-- 紧急与异常徽标(使用控制流指令) -->
  12. <!-- 保持已有@if 徽标逻辑不变 -->
  13. </div>
  14. </div>
  15. </div>
  16. <div class="header-right">
  17. <div class="header-actions">
  18. <!-- 导航条 - 移动到顶部与导出按钮水平对齐 -->
  19. <div class="top-nav-container">
  20. <app-vertical-nav
  21. [activeTab]="activeTab"
  22. (tabChange)="switchTab($event)"
  23. class="top-nav">
  24. </app-vertical-nav>
  25. </div>
  26. <!-- 导出阶段报告 -->
  27. <button (click)="exportProjectReport()" class="action-btn secondary-btn">
  28. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  29. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
  30. <polyline points="14 2 14 8 20 8"></polyline>
  31. <line x1="16" y1="13" x2="8" y2="13"></line>
  32. <line x1="16" y1="17" x2="8" y2="17"></line>
  33. </svg>
  34. 导出报告
  35. </button>
  36. <button (click)="generateReminderMessage()" class="action-btn stagnation-btn">
  37. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  38. <circle cx="12" cy="12" r="10"></circle>
  39. <polyline points="12 6 12 12 16 14"></polyline>
  40. </svg>
  41. 设置停滞
  42. </button>
  43. <!-- 切换项目下拉菜单 -->
  44. <div class="project-switcher">
  45. <button (click)="showDropdown = !showDropdown" class="action-btn switch-btn">
  46. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  47. <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
  48. <polyline points="9 22 9 12 15 12 15 22"></polyline>
  49. </svg>
  50. 切换项目
  51. <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="dropdown-icon">
  52. <polyline points="6 9 12 15 18 9"></polyline>
  53. </svg>
  54. </button>
  55. @if (showDropdown) {
  56. <div class="switch-dropdown" (click)="$event.stopPropagation()">
  57. @for (p of projects; track p.id) {
  58. <div (click)="switchProject(p.id); showDropdown = false"
  59. [class.active]="p.id === projectId"
  60. class="project-item">
  61. <span class="project-name">{{ p.name }}</span>
  62. <span class="project-status-badge"
  63. [class.ongoing]="p.status === '进行中'"
  64. [class.completed]="p.status === '已完成'"
  65. [class.pending]="p.status === '待处理'">
  66. {{ p.status }}
  67. </span>
  68. </div>
  69. }
  70. </div>
  71. }
  72. </div>
  73. <!-- 返回工作台按钮 -->
  74. <button (click)="backToWorkbench()" class="action-btn back-btn primary">
  75. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  76. <path d="M19 12H5"></path>
  77. <polyline points="12 19 5 12 12 5"></polyline>
  78. </svg>
  79. 返回工作台
  80. </button>
  81. </div>
  82. </div>
  83. <!-- 团队分配弹窗 -->
  84. @if (showTeamAssignmentModal) {
  85. <app-team-assignment-modal
  86. [isVisible]="showTeamAssignmentModal"
  87. (closeModal)="closeTeamAssignmentModal()"
  88. (confirmAssignment)="confirmTeamAssignment($event)">
  89. </app-team-assignment-modal>
  90. }
  91. </div>
  92. <!-- 四大板块按钮组 - 位于项目详情标题区域正下方 -->
  93. <div class="sections-toolbar-header">
  94. @for (sec of sections; track sec.key) {
  95. <button class="section-btn"
  96. [class.completed]="getSectionStatus(sec.key) === 'completed'"
  97. [class.active]="getSectionStatus(sec.key) === 'active'"
  98. [class.pending]="getSectionStatus(sec.key) === 'pending'"
  99. [class.current-order]="sec.key === 'order' && isCurrentOrderCreation()"
  100. (click)="toggleSection(sec.key)">
  101. <span class="section-label">{{ sec.label }}</span>
  102. </button>
  103. }
  104. </div>
  105. <!-- 图片预览模态框 -->
  106. @if (showImagePreview) {
  107. <div class="image-preview-modal" (click)="closeImagePreview()">
  108. <div class="modal-backdrop"></div>
  109. <div class="modal-content" (click)="$event.stopPropagation()">
  110. <div class="modal-header">
  111. <h3>{{ previewImageData?.name }}</h3>
  112. <button class="close-btn" (click)="closeImagePreview()">
  113. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  114. <line x1="18" y1="6" x2="6" y2="18"></line>
  115. <line x1="6" y1="6" x2="18" y2="18"></line>
  116. </svg>
  117. </button>
  118. </div>
  119. <div class="modal-body">
  120. <img [src]="previewImageData?.url" [alt]="previewImageData?.name" />
  121. </div>
  122. <div class="modal-footer">
  123. <div class="image-info">
  124. <span>文件大小: {{ previewImageData?.size }}</span>
  125. @if (previewImageData?.reviewStatus) {
  126. <span class="status-badge">{{ getImageReviewStatusText(previewImageData) }}</span>
  127. }
  128. </div>
  129. <div class="modal-actions">
  130. <button class="secondary-btn" (click)="downloadImage(previewImageData)">下载</button>
  131. <button class="danger-btn" (click)="removeImageFromPreview()">删除</button>
  132. </div>
  133. </div>
  134. </div>
  135. </div>
  136. }
  137. <!-- 提醒消息弹窗 -->
  138. @if (reminderMessage) {
  139. <div class="reminder-popup">
  140. {{ reminderMessage }}
  141. </div>
  142. }
  143. <!-- 标准阶段进度(5阶段) -->
  144. <!-- 已采用@for,不变 -->
  145. <!-- 顶部导航标签页 -->
  146. <!-- 原有代码保留 -->
  147. <!-- 水平导航栏 - 已移动到顶部,此处删除 -->
  148. <div class="tab-content">
  149. <!-- 项目进度标签页 -->
  150. @if (isActiveTab('progress')) {
  151. <div class="progress-tab-content">
  152. <div class="main-content-layout">
  153. <!-- 左侧保留 -->
  154. <div class="left-column">
  155. <div class="project-info-card card">
  156. <h2>客户信息</h2>
  157. @if (project) {
  158. <div class="info-grid">
  159. <!-- 显示订单创建时填写的客户信息,如果有的话 -->
  160. @if (orderCreationData?.customerInfo) {
  161. <div class="info-item key-info"><label>客户姓名</label><span>{{ orderCreationData.customerInfo.name }}</span></div>
  162. <div class="info-item key-info"><label>手机号码</label><span>{{ orderCreationData.customerInfo.phone }}</span></div>
  163. @if (orderCreationData.customerInfo.wechat) {
  164. <div class="info-item"><label>微信号</label><span>{{ orderCreationData.customerInfo.wechat }}</span></div>
  165. }
  166. <div class="info-item"><label>客户类型</label><span>{{ orderCreationData.customerInfo.customerType }}</span></div>
  167. } @else {
  168. <!-- 默认显示项目信息 -->
  169. <div class="info-item key-info"><label>客户姓名</label><span>{{ project.customerName }}</span></div>
  170. <div class="info-item key-info"><label>项目负责人</label><span>{{ project.assigneeName }}</span></div>
  171. }
  172. <div class="info-item"><label>项目创建</label><span>{{ formatDate(project.createdAt) }}</span></div>
  173. <div class="info-item"><label>截止日期</label><span>{{ formatDate(project.deadline) }}</span></div>
  174. </div>
  175. <div class="tags-container" style="margin-top: 12px;">
  176. <div class="tag-section">
  177. <h3>客户标签</h3>
  178. <div class="tags">
  179. @for (tag of project.customerTags; track $index) {
  180. <span class="tag">{{ tag.source }} · {{ tag.needType }} · {{ tag.preference }} · {{ tag.colorAtmosphere }}</span>
  181. }
  182. @if (project.customerTags.length === 0) { <span class="desc">暂无标签</span> }
  183. </div>
  184. </div>
  185. <div class="tag-section">
  186. <h3>高优先级需求</h3>
  187. <ul class="need-list">
  188. @for (need of project.highPriorityNeeds; track $index) {
  189. <li>{{ need }}</li>
  190. }
  191. @if (project.highPriorityNeeds.length === 0) { <li class="desc">无</li> }
  192. </ul>
  193. <!-- 新增:需求关键信息同步区域 -->
  194. <div class="requirement-sync-info">
  195. <h4>项目需求信息</h4>
  196. <!-- 显示订单创建时填写的需求信息 -->
  197. @if (orderCreationData?.requirementInfo) {
  198. <div class="order-requirement-info">
  199. <h5>订单创建阶段需求</h5>
  200. <div class="key-info-grid">
  201. @if (orderCreationData.requirementInfo.decorationType) {
  202. <div class="info-item">
  203. <span class="info-label">装修类型</span>
  204. <span class="info-value">{{ orderCreationData.requirementInfo.decorationType }}</span>
  205. </div>
  206. }
  207. @if (orderCreationData.requirementInfo.downPayment) {
  208. <div class="info-item">
  209. <span class="info-label">首付款</span>
  210. <span class="info-value">¥{{ orderCreationData.requirementInfo.downPayment }}</span>
  211. </div>
  212. }
  213. @if (orderCreationData.requirementInfo.firstDraftDate) {
  214. <div class="info-item">
  215. <span class="info-label">首稿时间</span>
  216. <span class="info-value">{{ orderCreationData.requirementInfo.firstDraftDate }}</span>
  217. </div>
  218. }
  219. @if (orderCreationData.requirementInfo.style) {
  220. <div class="info-item">
  221. <span class="info-label">装修风格</span>
  222. <span class="info-value">{{ orderCreationData.requirementInfo.style }}</span>
  223. </div>
  224. }
  225. @if (orderCreationData.requirementInfo.spaceRequirements) {
  226. <div class="info-item">
  227. <span class="info-label">涉及空间</span>
  228. <span class="info-value">{{ orderCreationData.requirementInfo.spaceRequirements }}</span>
  229. </div>
  230. }
  231. </div>
  232. <!-- 显示偏好标签 -->
  233. @if (orderCreationData.preferenceTags && orderCreationData.preferenceTags.length > 0) {
  234. <div class="preference-tags">
  235. <span class="info-label">偏好标签:</span>
  236. <div class="tags">
  237. @for (tag of orderCreationData.preferenceTags; track tag) {
  238. <span class="tag">{{ tag }}</span>
  239. }
  240. </div>
  241. </div>
  242. }
  243. </div>
  244. }
  245. <!-- 确认需求阶段的关键信息 -->
  246. <div class="confirmed-requirement-info">
  247. <h5>确认需求关键信息</h5>
  248. <div class="key-info-grid">
  249. @if (requirementKeyInfo.colorAtmosphere.description) {
  250. <div class="info-item">
  251. <span class="info-label">色彩氛围</span>
  252. <span class="info-value">{{ requirementKeyInfo.colorAtmosphere.description }}</span>
  253. <div class="color-preview" [style.background-color]="requirementKeyInfo.colorAtmosphere.mainColor"></div>
  254. </div>
  255. }
  256. @if (requirementKeyInfo.spaceStructure.aspectRatio > 0) {
  257. <div class="info-item">
  258. <span class="info-label">空间结构</span>
  259. <span class="info-value">比例 {{ requirementKeyInfo.spaceStructure.aspectRatio.toFixed(1) }}</span>
  260. </div>
  261. }
  262. @if (requirementKeyInfo.materialWeights.woodRatio > 0 || requirementKeyInfo.materialWeights.fabricRatio > 0) {
  263. <div class="info-item">
  264. <span class="info-label">材质权重</span>
  265. <div class="material-weights">
  266. @if (requirementKeyInfo.materialWeights.woodRatio > 0) {
  267. <span class="weight-item">木质 {{ requirementKeyInfo.materialWeights.woodRatio }}%</span>
  268. }
  269. @if (requirementKeyInfo.materialWeights.fabricRatio > 0) {
  270. <span class="weight-item">布艺 {{ requirementKeyInfo.materialWeights.fabricRatio }}%</span>
  271. }
  272. @if (requirementKeyInfo.materialWeights.metalRatio > 0) {
  273. <span class="weight-item">金属 {{ requirementKeyInfo.materialWeights.metalRatio }}%</span>
  274. }
  275. </div>
  276. </div>
  277. }
  278. @if (requirementKeyInfo.presetAtmosphere.name) {
  279. <div class="info-item">
  280. <span class="info-label">预设氛围</span>
  281. <span class="info-value">{{ requirementKeyInfo.presetAtmosphere.name }}</span>
  282. <span class="color-temp">{{ requirementKeyInfo.presetAtmosphere.colorTemp }}</span>
  283. </div>
  284. }
  285. </div>
  286. @if (getRequirementSummary().length === 0 && !orderCreationData?.requirementInfo) {
  287. <div class="sync-placeholder">
  288. <span class="placeholder-text">暂无同步的需求信息</span>
  289. <button class="sync-btn" (click)="syncRequirementKeyInfo({})">手动同步</button>
  290. </div>
  291. }
  292. </div>
  293. </div>
  294. </div>
  295. </div>
  296. } @else {
  297. <div class="loading-state">
  298. <div class="loading-spinner"></div>
  299. <div>正在加载客户信息...</div>
  300. </div>
  301. }
  302. </div>
  303. </div>
  304. <!-- 右侧三分之二 - 制作流程进度 -->
  305. <div class="right-column">
  306. <div class="process-card card">
  307. <h2>制作流程进度</h2>
  308. <!-- 串式流程:10个阶段横向排列(保持) -->
  309. <div class="stage-progress-container">
  310. @for (stage of getVisibleStages(); track stage) {
  311. <div class="vertical-stage-block" [attr.id]="stageToAnchor(stage)" [class.active]="getStageStatus(stage) === 'active'">
  312. <div class="vertical-stage-header">
  313. <span class="dot" [class.completed]="getStageStatus(stage) === 'completed'" [class.active]="getStageStatus(stage) === 'active'"></span>
  314. <h3>{{ stage }}</h3>
  315. </div>
  316. <!-- 直接复用原阶段内容卡片:按stage匹配显示 -->
  317. <div class="vertical-stage-body">
  318. @if (stage === '订单创建') {
  319. <app-consultation-order-panel
  320. (orderCreated)="onConsultationOrderSubmit($event)"
  321. (projectCreated)="onProjectCreated($event)"
  322. ></app-consultation-order-panel>
  323. } @else if (stage === '需求沟通') {
  324. <app-requirements-confirm-card
  325. (requirementConfirmed)="syncRequirementKeyInfo($event)"
  326. (progressUpdated)="syncRequirementKeyInfo($event)"
  327. (stageCompleted)="onRequirementsStageCompleted($event)">
  328. </app-requirements-confirm-card>
  329. } @else if (stage === '方案确认') {
  330. <!-- 方案确认阶段 - 需求信息展示 -->
  331. <div class="proposal-confirm-section">
  332. <div class="section-header">
  333. <h4>方案确认</h4>
  334. <div class="progress-indicator">
  335. <span class="progress-text">{{ getRequiredStagesProgress() }}% 完成</span>
  336. </div>
  337. </div>
  338. <!-- 需求信息展示区域 -->
  339. @if (areRequiredStagesCompleted()) {
  340. <div class="requirement-info-display">
  341. <div class="info-header">
  342. <h5>确认的需求信息</h5>
  343. <span class="info-subtitle">来自需求沟通阶段</span>
  344. </div>
  345. <div class="info-grid">
  346. <!-- 色调和材质并排布局 -->
  347. <div class="color-material-row">
  348. <!-- 色调 -->
  349. @if (requirementKeyInfo.colorAtmosphere.description) {
  350. <div class="info-item color-item">
  351. <div class="info-label">
  352. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  353. <circle cx="12" cy="12" r="10"></circle>
  354. <path d="M12 6v6l4 2"></path>
  355. </svg>
  356. 色调
  357. </div>
  358. <div class="info-content">
  359. <span class="info-value">{{ requirementKeyInfo.colorAtmosphere.description }}</span>
  360. @if (requirementKeyInfo.colorAtmosphere.mainColor) {
  361. <div class="color-preview" [style.background-color]="requirementKeyInfo.colorAtmosphere.mainColor"></div>
  362. }
  363. </div>
  364. </div>
  365. }
  366. <!-- 材质 -->
  367. @if (requirementKeyInfo.colorAtmosphere.materials && requirementKeyInfo.colorAtmosphere.materials.length > 0) {
  368. <div class="info-item material-item">
  369. <div class="info-label">
  370. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  371. <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
  372. <rect x="7" y="7" width="10" height="10" rx="1" ry="1"></rect>
  373. </svg>
  374. 材质
  375. </div>
  376. <div class="info-content">
  377. <div class="material-list">
  378. @for (material of requirementKeyInfo.colorAtmosphere.materials; track material) {
  379. <span class="material-tag">{{ material }}</span>
  380. }
  381. </div>
  382. </div>
  383. </div>
  384. }
  385. </div>
  386. <!-- 空间结构和材质权重并排布局 -->
  387. <div class="structure-weight-row">
  388. <!-- 空间结构 -->
  389. @if (requirementKeyInfo.spaceStructure.aspectRatio > 0) {
  390. <div class="info-item structure-item">
  391. <div class="info-label">
  392. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  393. <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
  394. <line x1="9" y1="9" x2="15" y2="15"></line>
  395. </svg>
  396. 空间结构
  397. </div>
  398. <div class="info-content">
  399. <div class="structure-details">
  400. <span class="structure-item">比例: {{ requirementKeyInfo.spaceStructure.aspectRatio.toFixed(1) }}</span>
  401. @if (requirementKeyInfo.spaceStructure.ceilingHeight > 0) {
  402. <span class="structure-item">层高: {{ requirementKeyInfo.spaceStructure.ceilingHeight }}m</span>
  403. }
  404. @if (requirementKeyInfo.spaceStructure.flowWidth > 0) {
  405. <span class="structure-item">流线宽度: {{ requirementKeyInfo.spaceStructure.flowWidth }}m</span>
  406. }
  407. </div>
  408. </div>
  409. </div>
  410. }
  411. <!-- 材质权重 -->
  412. @if (requirementKeyInfo.materialWeights.woodRatio > 0 || requirementKeyInfo.materialWeights.fabricRatio > 0 || requirementKeyInfo.materialWeights.metalRatio > 0) {
  413. <div class="info-item weight-item">
  414. <div class="info-label">
  415. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  416. <path d="M3 3v18h18"></path>
  417. <path d="M7 16l4-4 4 4 6-6"></path>
  418. </svg>
  419. 材质权重
  420. </div>
  421. <div class="info-content">
  422. <div class="weight-list">
  423. @if (requirementKeyInfo.materialWeights.woodRatio > 0) {
  424. <div class="weight-item">
  425. <span class="weight-label">木质</span>
  426. <span class="weight-value">{{ requirementKeyInfo.materialWeights.woodRatio }}%</span>
  427. </div>
  428. }
  429. @if (requirementKeyInfo.materialWeights.fabricRatio > 0) {
  430. <div class="weight-item">
  431. <span class="weight-label">布艺</span>
  432. <span class="weight-value">{{ requirementKeyInfo.materialWeights.fabricRatio }}%</span>
  433. </div>
  434. }
  435. @if (requirementKeyInfo.materialWeights.metalRatio > 0) {
  436. <div class="weight-item">
  437. <span class="weight-label">金属</span>
  438. <span class="weight-value">{{ requirementKeyInfo.materialWeights.metalRatio }}%</span>
  439. </div>
  440. }
  441. </div>
  442. </div>
  443. </div>
  444. }
  445. </div>
  446. <!-- 预设氛围和小图时间并排布局 -->
  447. <div class="atmosphere-time-row">
  448. <!-- 预设氛围 -->
  449. @if (requirementKeyInfo.presetAtmosphere.name) {
  450. <div class="info-item atmosphere-item">
  451. <div class="info-label">
  452. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  453. <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
  454. </svg>
  455. 预设氛围
  456. </div>
  457. <div class="info-content">
  458. <div class="atmosphere-info">
  459. <span class="atmosphere-name">{{ requirementKeyInfo.presetAtmosphere.name }}</span>
  460. @if (requirementKeyInfo.presetAtmosphere.colorTemp) {
  461. <span class="color-temp">{{ requirementKeyInfo.presetAtmosphere.colorTemp }}</span>
  462. }
  463. </div>
  464. </div>
  465. </div>
  466. }
  467. <!-- 小图时间 -->
  468. @if (getEstimatedSmallImageTime()) {
  469. <div class="info-item time-item">
  470. <div class="info-label">
  471. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  472. <circle cx="12" cy="12" r="10"></circle>
  473. <polyline points="12,6 12,12 16,14"></polyline>
  474. </svg>
  475. 小图时间
  476. </div>
  477. <div class="info-content">
  478. <span class="time-estimate">{{ getEstimatedSmallImageTime() }}</span>
  479. </div>
  480. </div>
  481. }
  482. </div>
  483. </div>
  484. </div>
  485. <!-- 确认方案按钮 -->
  486. <div class="proposal-confirm-actions">
  487. <button class="confirm-proposal-btn"
  488. (click)="confirmProposal()"
  489. style="
  490. appearance: none !important;
  491. border: none !important;
  492. background: linear-gradient(135deg, #007aff 0%, #0066ff 50%, #0051d5 100%) !important;
  493. color: white !important;
  494. padding: 16px 40px !important;
  495. border-radius: 16px !important;
  496. font-size: 16px !important;
  497. font-weight: 600 !important;
  498. cursor: pointer !important;
  499. transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important;
  500. display: flex !important;
  501. align-items: center !important;
  502. gap: 10px !important;
  503. box-shadow: 0 8px 24px rgba(0, 122, 255, 0.25), 0 4px 12px rgba(0, 122, 255, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
  504. min-width: 180px !important;
  505. justify-content: center !important;
  506. position: relative !important;
  507. overflow: hidden !important;
  508. text-transform: none !important;
  509. letter-spacing: normal !important;
  510. line-height: normal !important;
  511. ">
  512. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color: white !important;">
  513. <polyline points="20,6 9,17 4,12"></polyline>
  514. </svg>
  515. 确认方案
  516. </button>
  517. </div>
  518. } @else {
  519. <div class="requirement-pending">
  520. <div class="pending-icon">
  521. <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  522. <circle cx="12" cy="12" r="10"></circle>
  523. <polyline points="12,6 12,12 16,14"></polyline>
  524. </svg>
  525. </div>
  526. <h5>等待需求确认完成</h5>
  527. <p>请先完成"流程进度 > 确认需求 > 需求沟通"四个流程,确认需求信息后方可查看。</p>
  528. <div class="progress-status">
  529. <span>当前进度: {{ getRequiredStagesProgress() }}%</span>
  530. </div>
  531. </div>
  532. }
  533. </div>
  534. } @else if (stage === '建模') {
  535. <div class="upload-section">
  536. <div class="upload-header">
  537. <h4>上传白模图片</h4>
  538. <span class="hint">支持:JPG/PNG,不强制4K</span>
  539. </div>
  540. @if (canEditSection('delivery')) {
  541. <div class="upload-dropzone"
  542. (click)="whiteModelImages.length === 0 ? triggerFileInput('whiteModel') : null"
  543. (dragover)="whiteModelImages.length === 0 ? onDragOver($event) : null"
  544. (dragleave)="whiteModelImages.length === 0 ? onDragLeave($event) : null"
  545. (drop)="whiteModelImages.length === 0 ? onFileDrop($event, 'whiteModel') : null"
  546. [class.drag-over]="isDragOver && whiteModelImages.length === 0"
  547. [class.has-images]="whiteModelImages.length > 0">
  548. @if (whiteModelImages.length === 0) {
  549. <div class="upload-icon"></div>
  550. <div class="upload-text">点击此处或拖拽文件到此处上传</div>
  551. <div class="upload-hint">支持 JPG、PNG 格式,单个文件最大 10MB</div>
  552. } @else {
  553. <div class="uploaded-images-grid">
  554. @for (img of whiteModelImages; track img.id) {
  555. <div class="uploaded-image-item" (click)="previewImage(img)">
  556. <img [src]="img.url" [alt]="img.name" />
  557. <div class="image-overlay">
  558. <div class="image-name">{{ img.name }}</div>
  559. <div class="image-actions">
  560. <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
  561. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  562. <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
  563. <circle cx="12" cy="12" r="3"></circle>
  564. </svg>
  565. </button>
  566. <button class="remove-btn" (click)="$event.stopPropagation(); removeWhiteModelImage(img.id)">
  567. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  568. <line x1="18" y1="6" x2="6" y2="18"></line>
  569. <line x1="6" y1="6" x2="18" y2="18"></line>
  570. </svg>
  571. </button>
  572. </div>
  573. </div>
  574. </div>
  575. }
  576. <div class="add-more-btn" (click)="triggerFileInput('whiteModel')">
  577. <div class="add-icon">+</div>
  578. <div class="add-text">添加更多</div>
  579. </div>
  580. </div>
  581. }
  582. <input type="file"
  583. id="whiteModelFileInput"
  584. accept="{{allowedImageTypes}}"
  585. multiple
  586. (change)="onWhiteModelSelected($event)"
  587. style="display: none;" />
  588. </div>
  589. }
  590. <div class="upload-actions">
  591. @if (canEditSection('delivery')) {
  592. <button class="primary-btn" [disabled]="whiteModelImages.length===0" (click)="confirmWhiteModelUpload()">确认上传</button>
  593. }
  594. @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('white')">同步图片信息</button> }
  595. @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
  596. </div>
  597. </div>
  598. <div class="model-check-section">
  599. <h4>模型差异检查清单</h4>
  600. <div class="checklist">
  601. @for (item of modelCheckItems; track item.id) {
  602. <div class="checklist-item">
  603. <label class="checklist-label">
  604. <input type="checkbox" class="custom-checkbox" [checked]="item.isPassed" (change)="updateModelCheckItem(item.id, $any($event.target).checked)" [disabled]="!canEditSection('delivery')">
  605. <span class="checklist-text">{{ item.name }}</span>
  606. </label>
  607. <span class="check-status">{{ item.isPassed ? '已通过' : '待处理' }}</span>
  608. </div>
  609. }
  610. </div>
  611. </div>
  612. } @else if (stage === '软装') {
  613. <div class="softdecor-section">
  614. <h4>软装清单素材</h4>
  615. <div class="upload-section">
  616. <div class="upload-header">
  617. <h4>上传软装小图</h4>
  618. <span class="hint">建议 ≤1MB 的 JPG/PNG 小图</span>
  619. </div>
  620. @if (canEditSection('delivery')) {
  621. <div class="upload-dropzone"
  622. (click)="softDecorImages.length === 0 ? triggerFileInput('softDecor') : null"
  623. (dragover)="softDecorImages.length === 0 ? onDragOver($event) : null"
  624. (dragleave)="softDecorImages.length === 0 ? onDragLeave($event) : null"
  625. (drop)="softDecorImages.length === 0 ? onFileDrop($event, 'softDecor') : null"
  626. [class.drag-over]="isDragOver && softDecorImages.length === 0"
  627. [class.has-images]="softDecorImages.length > 0">
  628. @if (softDecorImages.length === 0) {
  629. <div class="upload-icon"></div>
  630. <div class="upload-text">点击此处或拖拽文件到此处上传</div>
  631. <div class="upload-hint">支持 JPG、PNG 格式,建议文件大小 ≤1MB</div>
  632. } @else {
  633. <div class="uploaded-images-grid">
  634. @for (img of softDecorImages; track img.id) {
  635. <div class="uploaded-image-item" (click)="previewImage(img)">
  636. <img [src]="img.url" [alt]="img.name" />
  637. <div class="image-overlay">
  638. <div class="image-name">{{ img.name }}</div>
  639. <div class="image-actions">
  640. <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
  641. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  642. <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
  643. <circle cx="12" cy="12" r="3"></circle>
  644. </svg>
  645. </button>
  646. <button class="remove-btn" (click)="$event.stopPropagation(); removeSoftDecorImage(img.id)">
  647. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  648. <line x1="18" y1="6" x2="6" y2="18"></line>
  649. <line x1="6" y1="6" x2="18" y2="18"></line>
  650. </svg>
  651. </button>
  652. </div>
  653. </div>
  654. </div>
  655. }
  656. <div class="add-more-btn" (click)="triggerFileInput('softDecor')">
  657. <div class="add-icon">+</div>
  658. <div class="add-text">添加更多</div>
  659. </div>
  660. </div>
  661. }
  662. <input type="file"
  663. id="softDecorFileInput"
  664. accept="{{allowedImageTypes}}"
  665. multiple
  666. (change)="onSoftDecorSmallPicsSelected($event)"
  667. style="display: none;" />
  668. </div>
  669. }
  670. <div class="upload-actions">
  671. @if (canEditSection('delivery')) {
  672. <button class="primary-btn" [disabled]="softDecorImages.length===0" (click)="confirmSoftDecorUpload()">确认上传</button>
  673. }
  674. @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('soft')">同步图片信息</button> }
  675. @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
  676. </div>
  677. </div>
  678. </div>
  679. } @else if (stage === '渲染') {
  680. <div class="render-progress-section">
  681. @if (isLoadingRenderProgress) {
  682. <div class="loading-state">
  683. <div class="loading-spinner"></div>
  684. <div>正在加载渲染进度...</div>
  685. </div>
  686. }
  687. @if (errorLoadingRenderProgress) {
  688. <div class="error-state">
  689. <div>渲染进度加载失败</div>
  690. <button class="secondary-btn" (click)="retryLoadRenderProgress()">重试</button>
  691. </div>
  692. }
  693. @if (!isLoadingRenderProgress && !errorLoadingRenderProgress && renderProgress) {
  694. <div class="progress-info" style="display:flex;gap:16px;align-items:center;margin:12px 0;">
  695. <span>状态:{{ renderProgress.status }}</span>
  696. <span>完成度:{{ renderProgress.completionRate }}%</span>
  697. <span>预计剩余:{{ renderProgress.estimatedTimeRemaining }} 小时</span>
  698. </div>
  699. }
  700. <div class="upload-section">
  701. <div class="upload-header">
  702. <h4>上传渲染大图</h4>
  703. <span class="hint">需满足4K标准(最长边 ≥ 4000px)</span>
  704. </div>
  705. @if (canEditSection('delivery')) {
  706. <div class="upload-dropzone"
  707. (click)="renderLargeImages.length === 0 ? triggerFileInput('render') : null"
  708. (dragover)="renderLargeImages.length === 0 ? onDragOver($event) : null"
  709. (dragleave)="renderLargeImages.length === 0 ? onDragLeave($event) : null"
  710. (drop)="renderLargeImages.length === 0 ? onFileDrop($event, 'render') : null"
  711. [class.drag-over]="isDragOver && renderLargeImages.length === 0"
  712. [class.has-images]="renderLargeImages.length > 0">
  713. @if (renderLargeImages.length === 0) {
  714. <div class="upload-icon"></div>
  715. <div class="upload-text">点击此处或拖拽文件到此处上传</div>
  716. <div class="upload-hint">支持 JPG、PNG 格式,需满足4K标准(最长边 ≥ 4000px)</div>
  717. } @else {
  718. <div class="uploaded-images-grid">
  719. @for (img of renderLargeImages; track img.id) {
  720. <div class="uploaded-image-item" (click)="previewImage(img)">
  721. <img [src]="img.url" [alt]="img.name" />
  722. <div class="image-overlay">
  723. <div class="image-name">{{ img.name }}</div>
  724. <div class="image-actions">
  725. <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
  726. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  727. <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
  728. <circle cx="12" cy="12" r="3"></circle>
  729. </svg>
  730. </button>
  731. <button class="remove-btn" (click)="$event.stopPropagation(); removeRenderLargeImage(img.id)">
  732. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  733. <line x1="18" y1="6" x2="6" y2="18"></line>
  734. <line x1="6" y1="6" x2="18" y2="18"></line>
  735. </svg>
  736. </button>
  737. </div>
  738. </div>
  739. </div>
  740. }
  741. <div class="add-more-btn" (click)="triggerFileInput('render')">
  742. <div class="add-icon">+</div>
  743. <div class="add-text">添加更多</div>
  744. </div>
  745. </div>
  746. }
  747. <input type="file"
  748. id="renderFileInput"
  749. accept="{{allowedImageTypes}}"
  750. multiple
  751. (change)="onRenderLargePicsSelected($event)"
  752. style="display: none;" />
  753. </div>
  754. }
  755. <div class="upload-actions">
  756. @if (canEditSection('delivery')) {
  757. <button class="primary-btn" [disabled]="renderLargeImages.length===0" (click)="confirmRenderUpload()">确认上传</button>
  758. }
  759. @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('render')">同步图片信息</button> }
  760. @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
  761. </div>
  762. </div>
  763. </div>
  764. } @else if (stage === '后期') {
  765. <div class="post-process-section">
  766. <div class="upload-section">
  767. <div class="upload-header">
  768. <h4>上传后期处理图片</h4>
  769. <span class="hint">包含调色、修图、特效等后期处理结果</span>
  770. </div>
  771. @if (canEditSection('delivery')) {
  772. <div class="upload-dropzone"
  773. (click)="postProcessImages.length === 0 ? triggerFileInput('postProcess') : null"
  774. (dragover)="postProcessImages.length === 0 ? onDragOver($event) : null"
  775. (dragleave)="postProcessImages.length === 0 ? onDragLeave($event) : null"
  776. (drop)="postProcessImages.length === 0 ? onFileDrop($event, 'postProcess') : null"
  777. [class.drag-over]="isDragOver && postProcessImages.length === 0"
  778. [class.has-images]="postProcessImages.length > 0">
  779. @if (postProcessImages.length === 0) {
  780. <div class="upload-icon"></div>
  781. <div class="upload-text">点击此处或拖拽文件到此处上传</div>
  782. <div class="upload-hint">支持 JPG、PNG 格式,展示后期处理效果</div>
  783. } @else {
  784. <div class="uploaded-images-grid">
  785. @for (img of postProcessImages; track img.id) {
  786. <div class="uploaded-image-item" (click)="previewImage(img)">
  787. <img [src]="img.url" [alt]="img.name" />
  788. <div class="image-overlay">
  789. <div class="image-name">{{ img.name }}</div>
  790. <div class="image-actions">
  791. <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
  792. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  793. <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
  794. <circle cx="12" cy="12" r="3"></circle>
  795. </svg>
  796. </button>
  797. <button class="remove-btn" (click)="$event.stopPropagation(); removePostProcessImage(img.id)">
  798. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  799. <line x1="18" y1="6" x2="6" y2="18"></line>
  800. <line x1="6" y1="6" x2="18" y2="18"></line>
  801. </svg>
  802. </button>
  803. </div>
  804. </div>
  805. </div>
  806. }
  807. <div class="add-more-btn" (click)="triggerFileInput('postProcess')">
  808. <div class="add-icon">+</div>
  809. <div class="add-text">添加更多</div>
  810. </div>
  811. </div>
  812. }
  813. <input type="file"
  814. id="postProcessFileInput"
  815. accept="{{allowedImageTypes}}"
  816. multiple
  817. (change)="onPostProcessPicsSelected($event)"
  818. style="display: none;" />
  819. </div>
  820. }
  821. <div class="upload-actions">
  822. @if (canEditSection('delivery')) {
  823. <button class="primary-btn" [disabled]="postProcessImages.length===0" (click)="confirmPostProcessUpload()">确认上传</button>
  824. }
  825. @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('postProcess')">同步图片信息</button> }
  826. @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
  827. </div>
  828. </div>
  829. </div>
  830. } @else if (stage === '尾款结算') {
  831. <app-settlement-card [settlements]="settlements"></app-settlement-card>
  832. } @else if (stage === '客户评价') {
  833. <app-customer-review-card [feedbacks]="feedbacks"></app-customer-review-card>
  834. } @else if (stage === '投诉处理') {
  835. <app-complaint-card [complaints]="exceptionHistories"></app-complaint-card>
  836. }
  837. </div>
  838. </div>
  839. }
  840. </div>
  841. </div>
  842. </div>
  843. </div>
  844. </div>
  845. }
  846. <!-- 项目人员标签页 -->
  847. @if (isActiveTab('members')) {
  848. <div class="members-tab-content">
  849. <div class="main-content-layout">
  850. <!-- 项目人员内容 -->
  851. <div class="members-content">
  852. <div class="members-header">
  853. <h2>项目成员</h2>
  854. <p class="members-count">共 {{ projectMembers.length }} 名成员</p>
  855. </div>
  856. <div class="members-grid">
  857. @for (member of projectMembers; track member.id) {
  858. <div class="member-card">
  859. <div class="member-avatar">
  860. <img [src]="member.avatar" [alt]="member.name">
  861. </div>
  862. <div class="member-info">
  863. <h3 class="member-name">{{ member.name }}</h3>
  864. <p class="member-role">{{ member.role }}</p>
  865. <div class="member-stats">
  866. <div class="stat-item">
  867. <span class="stat-label">技能匹配度</span>
  868. <div class="progress-bar">
  869. <div class="progress-fill" [style.width.%]="member.skillMatch"></div>
  870. </div>
  871. <span class="stat-value">{{ member.skillMatch }}%</span>
  872. </div>
  873. <div class="stat-item">
  874. <span class="stat-label">项目进度</span>
  875. <div class="progress-bar">
  876. <div class="progress-fill" [style.width.%]="member.progress"></div>
  877. </div>
  878. <span class="stat-value">{{ member.progress }}%</span>
  879. </div>
  880. <div class="stat-item">
  881. <span class="stat-label">贡献度</span>
  882. <div class="progress-bar">
  883. <div class="progress-fill" [style.width.%]="member.contribution"></div>
  884. </div>
  885. <span class="stat-value">{{ member.contribution }}%</span>
  886. </div>
  887. </div>
  888. </div>
  889. </div>
  890. }
  891. </div>
  892. @if (projectMembers.length === 0) {
  893. <div class="empty-state">
  894. <p>暂无项目成员信息</p>
  895. </div>
  896. }
  897. </div>
  898. </div>
  899. </div>
  900. }
  901. <!-- 项目文件标签页 -->
  902. @if (isActiveTab('files')) {
  903. <div class="files-tab-content">
  904. <div class="main-content-layout">
  905. <!-- 项目文件内容 -->
  906. <div class="files-content">
  907. <div class="files-header">
  908. <h2>项目文件</h2>
  909. <p class="files-count">共 {{ projectFiles.length }} 个文件</p>
  910. </div>
  911. <div class="files-list">
  912. @for (file of projectFiles; track file.id) {
  913. <div class="file-item">
  914. <div class="file-icon">
  915. <span class="file-type">{{ file.type.toUpperCase() }}</span>
  916. </div>
  917. <div class="file-info">
  918. <h3 class="file-name">{{ file.name }}</h3>
  919. <div class="file-meta">
  920. <span class="file-size">{{ file.size }}</span>
  921. <span class="file-date">{{ file.date }}</span>
  922. </div>
  923. </div>
  924. <div class="file-actions">
  925. <button class="btn-download" (click)="downloadFile(file)">下载</button>
  926. <button class="btn-preview" (click)="previewFile(file)">预览</button>
  927. </div>
  928. </div>
  929. }
  930. </div>
  931. @if (projectFiles.length === 0) {
  932. <div class="empty-state">
  933. <p>暂无项目文件</p>
  934. </div>
  935. }
  936. </div>
  937. </div>
  938. </div>
  939. }
  940. </div>