Переглянути джерело

fix: order with select project team

ryanemax 1 день тому
батько
коміт
3aedd67328

+ 4 - 1
src/app/pages/admin/groupchats/groupchats.ts

@@ -180,7 +180,10 @@ export class GroupChats implements OnInit {
 
   editGroupChat(group: GroupChat) {
     this.currentGroupChat = group;
-    this.formModel = { ...group };
+    this.formModel = {
+      ...group,
+      projectId: group.project?.id || undefined
+    };
     this.panelMode = 'edit';
     this.showPanel = true;
   }

+ 11 - 3
src/modules/project/pages/project-detail/stages/stage-order.component.html

@@ -425,7 +425,10 @@
             <h4 class="section-title">已分配组员</h4>
             <div class="team-list">
               @for (team of projectTeams; track team.id) {
-                <div class="team-item">
+                <div
+                  class="team-item"
+                  [class.clickable]="canEdit"
+                  (click)="canEdit ? editAssignedDesigner(team) : null">
                   <div class="team-member">
                     <div class="member-avatar">
                       @if (team.get('profile')?.get('data')?.avatar) {
@@ -441,6 +444,11 @@
                       <p class="member-spaces">负责空间: {{ getMemberSpaces(team) }}</p>
                     </div>
                   </div>
+                  @if (canEdit) {
+                    <svg class="icon edit-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                      <path fill="currentColor" d="M384 224v184a40 40 0 01-40 40H104a40 40 0 01-40-40V168a40 40 0 0140-40h167.48M459.94 53.25a16.06 16.06 0 00-23.22-.56L424.35 65a8 8 0 000 11.31l11.34 11.32a8 8 0 0011.34 0l12.06-12c6.1-6.09 6.67-16.01.85-22.38zM399.34 90L218.82 270.2a9 9 0 00-2.31 3.93L208.16 299a3.91 3.91 0 004.86 4.86l24.85-8.35a9 9 0 003.93-2.31L422 112.66a9 9 0 000-12.66l-9.95-10a9 9 0 00-12.71 0z"/>
+                    </svg>
+                  }
                 </div>
               }
             </div>
@@ -551,7 +559,7 @@
     <div class="modal-overlay" (click)="cancelAssignDialog()">
       <div class="modal-dialog" (click)="$event.stopPropagation()">
         <div class="modal-header">
-          <h3 class="modal-title">分配设计师</h3>
+          <h3 class="modal-title">{{ editingTeam ? '编辑分配' : '分配设计师' }}</h3>
           <button class="modal-close" (click)="cancelAssignDialog()">
             <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
               <path fill="currentColor" d="M289.94 256l95-95A24 24 0 00351 127l-95 95-95-95a24 24 0 00-34 34l95 95-95 95a24 24 0 1034 34l95-95 95 95a24 24 0 0034-34z"/>
@@ -597,7 +605,7 @@
             class="btn btn-primary"
             (click)="confirmAssignDesigner()"
             [disabled]="saving || selectedSpaces.length === 0">
-            确认分配
+            {{ editingTeam ? '确认更新' : '确认分配' }}
           </button>
         </div>
       </div>

+ 324 - 1
src/modules/project/pages/project-detail/stages/stage-order.component.scss

@@ -1090,12 +1090,42 @@
         border-radius: 10px;
         padding: 16px;
         transition: all 0.3s ease;
+        position: relative;
 
-        &:hover {
+        &.clickable {
+          cursor: pointer;
+
+          &:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 6px 16px rgba(var(--primary-rgb), 0.15);
+            background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.08) 0%, rgba(var(--primary-rgb), 0.04) 100%);
+
+            .edit-icon {
+              opacity: 1;
+            }
+          }
+
+          &:active {
+            transform: translateY(0);
+          }
+        }
+
+        &:not(.clickable):hover {
           transform: translateY(-2px);
           box-shadow: 0 6px 16px rgba(var(--primary-rgb), 0.15);
         }
 
+        .edit-icon {
+          position: absolute;
+          top: 12px;
+          right: 12px;
+          width: 20px;
+          height: 20px;
+          color: var(--primary-color);
+          opacity: 0.6;
+          transition: opacity 0.3s;
+        }
+
         .team-member {
           display: flex;
           align-items: center;
@@ -1359,6 +1389,299 @@
   }
 }
 
+// 模态框样式
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10000;
+  animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+.modal-dialog {
+  background: var(--white);
+  border-radius: 12px;
+  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
+  width: 90%;
+  max-width: 500px;
+  max-height: 90vh;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  animation: slideUp 0.3s ease-out;
+}
+
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 24px;
+  border-bottom: 1px solid var(--light-shade);
+
+  .modal-title {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: var(--dark-color);
+  }
+
+  .modal-close {
+    background: none;
+    border: none;
+    cursor: pointer;
+    padding: 4px;
+    width: 32px;
+    height: 32px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 4px;
+    transition: all 0.2s;
+
+    .icon {
+      width: 20px;
+      height: 20px;
+      color: var(--medium-color);
+    }
+
+    &:hover {
+      background: var(--light-color);
+
+      .icon {
+        color: var(--dark-color);
+      }
+    }
+  }
+}
+
+.modal-content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 24px;
+
+  .designer-preview {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 12px;
+    padding: 20px;
+    background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.05) 0%, rgba(var(--primary-rgb), 0.02) 100%);
+    border-radius: 10px;
+    margin-bottom: 24px;
+
+    .designer-avatar {
+      width: 64px;
+      height: 64px;
+      border-radius: 50%;
+      overflow: hidden;
+      background: var(--white);
+      border: 3px solid var(--primary-color);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      box-shadow: 0 2px 8px rgba(var(--primary-rgb), 0.2);
+
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+
+      .avatar-icon {
+        width: 36px;
+        height: 36px;
+        color: var(--primary-color);
+      }
+    }
+
+    .designer-name {
+      font-size: 18px;
+      font-weight: 600;
+      color: var(--dark-color);
+    }
+  }
+
+  .space-selection-section {
+    .form-label {
+      display: block;
+      font-weight: 500;
+      color: var(--dark-color);
+      margin-bottom: 8px;
+      font-size: 14px;
+
+      .required {
+        color: var(--danger-color);
+        margin-left: 4px;
+      }
+    }
+
+    .form-help {
+      margin: 0 0 12px;
+      font-size: 13px;
+      color: var(--medium-color);
+    }
+
+    .space-checkbox-list {
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+
+      .space-checkbox-item {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        padding: 12px 16px;
+        background: var(--light-color);
+        border: 2px solid transparent;
+        border-radius: 8px;
+        cursor: pointer;
+        transition: all 0.3s;
+        user-select: none;
+
+        &:hover {
+          background: var(--light-shade);
+          border-color: rgba(var(--primary-rgb), 0.3);
+        }
+
+        input[type="checkbox"] {
+          position: absolute;
+          opacity: 0;
+          cursor: pointer;
+          width: 0;
+          height: 0;
+
+          &:checked + .checkbox-custom {
+            background-color: var(--primary-color);
+            border-color: var(--primary-color);
+
+            &::after {
+              display: block;
+            }
+          }
+
+          &:checked ~ .space-name {
+            color: var(--primary-color);
+            font-weight: 600;
+          }
+        }
+
+        .checkbox-custom {
+          position: relative;
+          height: 20px;
+          width: 20px;
+          background-color: var(--white);
+          border: 2px solid var(--medium-color);
+          border-radius: 4px;
+          transition: all 0.3s;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          flex-shrink: 0;
+
+          &::after {
+            content: '';
+            display: none;
+            width: 5px;
+            height: 10px;
+            border: solid white;
+            border-width: 0 2px 2px 0;
+            transform: rotate(45deg);
+          }
+        }
+
+        .space-name {
+          flex: 1;
+          font-size: 14px;
+          color: var(--dark-color);
+          transition: all 0.3s;
+        }
+      }
+    }
+  }
+}
+
+.modal-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 16px 24px;
+  border-top: 1px solid var(--light-shade);
+
+  .btn {
+    padding: 10px 20px;
+    border-radius: 8px;
+    font-size: 14px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.3s;
+    border: none;
+    outline: none;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+
+    &.btn-outline {
+      background: white;
+      color: var(--dark-color);
+      border: 2px solid var(--light-shade);
+
+      &:hover:not(:disabled) {
+        background: var(--light-color);
+        border-color: var(--medium-color);
+      }
+    }
+
+    &.btn-primary {
+      background: var(--primary-color);
+      color: white;
+
+      &:hover:not(:disabled) {
+        background: #2f6ce5;
+        transform: translateY(-2px);
+        box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.3);
+      }
+
+      &:active:not(:disabled) {
+        transform: translateY(0);
+      }
+    }
+
+    &:disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+      pointer-events: none;
+    }
+  }
+}
+
 // 移动端优化
 @media (max-width: 480px) {
   .stage-order-container {

+ 55 - 18
src/modules/project/pages/project-detail/stages/stage-order.component.ts

@@ -131,6 +131,7 @@ export class StageOrderComponent implements OnInit {
   showAssignDialog: boolean = false;
   assigningDesigner: FmodeObject | null = null;
   selectedSpaces: string[] = [];
+  editingTeam: FmodeObject | null = null; // 当前正在编辑的团队对象
 
   // 加载状态
   loading: boolean = true;
@@ -511,6 +512,26 @@ export class StageOrderComponent implements OnInit {
     this.showAssignDialog = true;
   }
 
+  /**
+   * 编辑已分配的设计师 - 重用分配对话框
+   */
+  editAssignedDesigner(team: FmodeObject) {
+    if (!this.canEdit) return;
+
+    const designer = team.get('profile');
+    if (!designer) return;
+
+    // 设置当前编辑的设计师和团队对象
+    this.assigningDesigner = designer;
+    this.editingTeam = team;
+
+    // 预选当前已分配的空间
+    const currentSpaces = team.get('data')?.spaces || [];
+    this.selectedSpaces = [...currentSpaces];
+
+    this.showAssignDialog = true;
+  }
+
   /**
    * 切换空间选择
    */
@@ -524,7 +545,7 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 确认分配设计师
+   * 确认分配设计师(支持创建和更新)
    */
   async confirmAssignDesigner() {
     if (!this.assigningDesigner || !this.project) return;
@@ -537,22 +558,37 @@ export class StageOrderComponent implements OnInit {
     try {
       this.saving = true;
 
-      // 创建 ProjectTeam
-      const ProjectTeam = Parse.Object.extend('ProjectTeam');
-      const team = new ProjectTeam();
-      team.set('project', this.project.toPointer());
-      team.set('profile', this.assigningDesigner.toPointer());
-      team.set('role', '组员');
-      team.set('data', {
-        spaces: this.selectedSpaces,
-        assignedAt: new Date(),
-        assignedBy: this.currentUser?.id
-      });
+      if (this.editingTeam) {
+        // 更新现有团队成员的空间分配
+        const data = this.editingTeam.get('data') || {};
+        data.spaces = this.selectedSpaces;
+        data.updatedAt = new Date();
+        data.updatedBy = this.currentUser?.id;
+        this.editingTeam.set('data', data);
+
+        await this.editingTeam.save();
+
+        alert('更新成功');
+      } else {
+        // 创建新的 ProjectTeam
+        const ProjectTeam = Parse.Object.extend('ProjectTeam');
+        const team = new ProjectTeam();
+        team.set('project', this.project.toPointer());
+        team.set('profile', this.assigningDesigner.toPointer());
+        team.set('role', '组员');
+        team.set('data', {
+          spaces: this.selectedSpaces,
+          assignedAt: new Date(),
+          assignedBy: this.currentUser?.id
+        });
+
+        await team.save();
 
-      await team.save();
+        // 加入群聊(静默执行)
+        await this.addMemberToGroupChat(this.assigningDesigner.get('userId'));
 
-      // 加入群聊(静默执行)
-      await this.addMemberToGroupChat(this.assigningDesigner.get('userId'));
+        alert('分配成功');
+      }
 
       // 重新加载团队列表
       await this.loadProjectTeams();
@@ -560,11 +596,11 @@ export class StageOrderComponent implements OnInit {
       this.showAssignDialog = false;
       this.assigningDesigner = null;
       this.selectedSpaces = [];
+      this.editingTeam = null;
 
-      alert('分配成功');
     } catch (err) {
-      console.error('分配设计师失败:', err);
-      alert('分配失败');
+      console.error(this.editingTeam ? '更新失败:' : '分配设计师失败:', err);
+      alert(this.editingTeam ? '更新失败' : '分配失败');
     } finally {
       this.saving = false;
     }
@@ -577,6 +613,7 @@ export class StageOrderComponent implements OnInit {
     this.showAssignDialog = false;
     this.assigningDesigner = null;
     this.selectedSpaces = [];
+    this.editingTeam = null;
   }
 
   /**