ryanemax 21 цаг өмнө
parent
commit
6e395c6484

+ 7 - 1
docs/task/2025101713-project.md

@@ -42,4 +42,10 @@
     - 仅组员分配,加入群聊部分,静默调用sdk中ww库能力变更添加群成员
 - 开发过程,如果更新了schemas.md内容请同步更新文档
     - 例如:ProjectTeam,识别项目组员,和.data中负责空间信息
-        - .data也需要预留到后续,记录该组员在该项目的整体表现评估复盘的内容
+        - .data也需要预留到后续,记录该组员在该项目的整体表现评估复盘的内容
+
+## FAQ:迭代细节
+第一步:stage-order页面已选择组员相关的scss样式缺失,请您补全美观.第二步请您参考project-loader从
+  groupChat信息,创建新的Project的逻辑,在groupchats.ts组件中,增加点击未关联项目,引导创建新项目设
+  置在GroupChat.project Pointer<project>属性内,添加对应的ProjectGroup同时根据project-management
+  组件加载项目详情页路由点击后展示项目详情页,方便我在后台管理也可以创建群组对应的项目. 

+ 49 - 1
src/app/pages/admin/groupchats/groupchats.html

@@ -42,7 +42,15 @@
         <tr *ngFor="let group of filtered" [class.disabled]="group.isDisabled">
           <td>{{ group.name }}</td>
           <td><code>{{ group.chat_id }}</code></td>
-          <td>{{ group.project?.get("title") || '未关联' }}</td>
+          <td>
+            <span *ngIf="group.project" class="project-link" [routerLink]="['/admin/project-detail', group.project.id]" title="点击查看项目详情">
+              {{ group.project?.get("title") }}
+            </span>
+            <span *ngIf="!group.project" class="no-project">
+              未关联
+              <button class="btn-link" (click)="openCreateProjectModal(group)">创建项目</button>
+            </span>
+          </td>
           <td>{{ group.memberCount }}</td>
           <td>
             <span [class]="'status ' + (group.isDisabled ? 'disabled' : 'active')">
@@ -133,4 +141,44 @@
       </div>
     </div>
   </div>
+
+  <!-- 创建项目模态框 -->
+  <div class="modal-overlay" *ngIf="showCreateProjectModal" (click)="closeCreateProjectModal()">
+    <div class="modal-dialog" (click)="$event.stopPropagation()">
+      <div class="modal-header">
+        <h2>创建项目</h2>
+        <button class="btn-close" (click)="closeCreateProjectModal()">×</button>
+      </div>
+      <div class="modal-body">
+        <div class="modal-info">
+          <p>为群组 <strong>{{ creatingGroupChat?.name }}</strong> 创建关联项目</p>
+        </div>
+        <div class="form-group">
+          <label>项目名称 <span class="required">*</span></label>
+          <input
+            type="text"
+            class="form-control"
+            [(ngModel)]="newProjectName"
+            placeholder="请输入项目名称"
+            [disabled]="creating()"
+          />
+        </div>
+        <div class="form-tip">
+          <svg class="icon-info" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16">
+            <path fill="currentColor" d="M256 56C145.72 56 56 145.72 56 256s89.72 200 200 200 200-89.72 200-200S366.28 56 256 56zm0 82a26 26 0 11-26 26 26 26 0 0126-26zm48 226h-88a16 16 0 010-32h28v-88h-16a16 16 0 010-32h32a16 16 0 0116 16v104h28a16 16 0 010 32z"/>
+          </svg>
+          <span>项目将自动关联到此群组,状态为"待分配"</span>
+        </div>
+      </div>
+      <div class="modal-footer">
+        <button class="btn btn-default" (click)="closeCreateProjectModal()" [disabled]="creating()">
+          取消
+        </button>
+        <button class="btn btn-primary" (click)="createProject()" [disabled]="creating() || !newProjectName.trim()">
+          <span *ngIf="!creating()">创建项目</span>
+          <span *ngIf="creating()">创建中...</span>
+        </button>
+      </div>
+    </div>
+  </div>
 </div>

+ 248 - 0
src/app/pages/admin/groupchats/groupchats.scss

@@ -119,3 +119,251 @@ code {
   border-radius: 8px;
   border: 1px dashed #d1d5db;
 }
+
+// 项目链接和创建按钮
+.project-link {
+  color: var(--primary-color, #3880ff);
+  text-decoration: underline;
+  cursor: pointer;
+  transition: color 0.2s;
+
+  &:hover {
+    color: var(--primary-shade, #2f6ce5);
+    text-decoration: none;
+  }
+}
+
+.no-project {
+  color: var(--medium-color, #92949c);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.btn-link {
+  background: none;
+  border: none;
+  color: var(--primary-color, #3880ff);
+  text-decoration: underline;
+  cursor: pointer;
+  padding: 0;
+  font-size: inherit;
+  transition: color 0.2s;
+
+  &:hover {
+    color: var(--primary-shade, #2f6ce5);
+  }
+}
+
+// 模态框覆盖层
+.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: white;
+  border-radius: 8px;
+  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+  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(--border-color, #e0e0e0);
+
+  h2 {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: var(--dark-color, #222428);
+  }
+
+  .btn-close {
+    background: none;
+    border: none;
+    font-size: 28px;
+    line-height: 1;
+    color: var(--medium-color, #92949c);
+    cursor: pointer;
+    padding: 0;
+    width: 32px;
+    height: 32px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 4px;
+    transition: all 0.2s;
+
+    &:hover {
+      background: var(--light-color, #f4f5f8);
+      color: var(--dark-color, #222428);
+    }
+  }
+}
+
+.modal-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 24px;
+
+  .modal-info {
+    margin-bottom: 20px;
+    padding: 12px 16px;
+    background: var(--light-color, #f4f5f8);
+    border-radius: 6px;
+    border-left: 4px solid var(--primary-color, #3880ff);
+
+    p {
+      margin: 0;
+      font-size: 14px;
+      color: var(--dark-color, #222428);
+
+      strong {
+        color: var(--primary-color, #3880ff);
+        font-weight: 600;
+      }
+    }
+  }
+
+  .form-group {
+    margin-bottom: 16px;
+
+    label {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 14px;
+      font-weight: 500;
+      color: var(--dark-color, #222428);
+
+      .required {
+        color: var(--danger-color, #eb445a);
+        margin-left: 4px;
+      }
+    }
+
+    .form-control {
+      width: 100%;
+      padding: 10px 12px;
+      border: 1px solid var(--border-color, #e0e0e0);
+      border-radius: 6px;
+      font-size: 14px;
+      transition: all 0.2s;
+
+      &:focus {
+        outline: none;
+        border-color: var(--primary-color, #3880ff);
+        box-shadow: 0 0 0 3px rgba(56, 128, 255, 0.1);
+      }
+
+      &:disabled {
+        background: var(--light-color, #f4f5f8);
+        color: var(--medium-color, #92949c);
+        cursor: not-allowed;
+      }
+    }
+  }
+
+  .form-tip {
+    display: flex;
+    align-items: flex-start;
+    gap: 8px;
+    padding: 12px;
+    background: #f0f7ff;
+    border-radius: 6px;
+    margin-top: 16px;
+
+    .icon-info {
+      flex-shrink: 0;
+      color: var(--primary-color, #3880ff);
+    }
+
+    span {
+      font-size: 13px;
+      color: var(--dark-color, #222428);
+      line-height: 1.5;
+    }
+  }
+}
+
+.modal-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 16px 24px;
+  border-top: 1px solid var(--border-color, #e0e0e0);
+
+  .btn {
+    padding: 10px 20px;
+    border-radius: 6px;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s;
+    border: none;
+
+    &.btn-default {
+      background: white;
+      color: var(--dark-color, #222428);
+      border: 1px solid var(--border-color, #e0e0e0);
+
+      &:hover:not(:disabled) {
+        background: var(--light-color, #f4f5f8);
+      }
+    }
+
+    &.btn-primary {
+      background: var(--primary-color, #3880ff);
+      color: white;
+
+      &:hover:not(:disabled) {
+        background: var(--primary-shade, #2f6ce5);
+      }
+    }
+
+    &:disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+    }
+  }
+}

+ 132 - 3
src/app/pages/admin/groupchats/groupchats.ts

@@ -1,10 +1,13 @@
 import { Component, OnInit, signal } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
+import { Router, RouterModule } from '@angular/router';
 import { GroupChatService } from '../services/groupchat.service';
 import { ProjectService } from '../services/project.service';
 import { WxworkCorp } from 'fmode-ng/core';
-import { FmodeObject } from 'fmode-ng/core';
+import { FmodeObject, FmodeParse } from 'fmode-ng/core';
+
+const Parse = FmodeParse.with('nova');
 
 interface GroupChat {
   id: string;
@@ -25,7 +28,7 @@ interface Project {
 @Component({
   selector: 'app-groupchats',
   standalone: true,
-  imports: [CommonModule, FormsModule],
+  imports: [CommonModule, FormsModule, RouterModule],
   templateUrl: './groupchats.html',
   styleUrls: ['./groupchats.scss']
 })
@@ -52,9 +55,16 @@ export class GroupChats implements OnInit {
   private wecorp: WxworkCorp | null = null;
   private readonly COMPANY_ID = 'cDL6R1hgSi'; // 映三色帐套
 
+  // 创建项目相关
+  showCreateProjectModal = false;
+  creatingGroupChat: GroupChat | null = null;
+  newProjectName = '';
+  creating = signal(false);
+
   constructor(
     private groupChatService: GroupChatService,
-    private projectService: ProjectService
+    private projectService: ProjectService,
+    private router: Router
   ) {
     // 初始化企微Corp
     this.wecorp = new WxworkCorp(this.COMPANY_ID);
@@ -245,4 +255,123 @@ export class GroupChats implements OnInit {
     a.click();
     URL.revokeObjectURL(url);
   }
+
+  /**
+   * 打开创建项目模态框
+   */
+  openCreateProjectModal(group: GroupChat) {
+    this.creatingGroupChat = group;
+    this.newProjectName = group.name || '新项目';
+    this.showCreateProjectModal = true;
+  }
+
+  /**
+   * 关闭创建项目模态框
+   */
+  closeCreateProjectModal() {
+    this.showCreateProjectModal = false;
+    this.creatingGroupChat = null;
+    this.newProjectName = '';
+  }
+
+  /**
+   * 创建项目并关联到GroupChat
+   */
+  async createProject() {
+    if (!this.newProjectName.trim()) {
+      alert('请输入项目名称');
+      return;
+    }
+
+    if (!this.creatingGroupChat) {
+      return;
+    }
+
+    this.creating.set(true);
+    try {
+      console.log('开始创建项目', {
+        projectName: this.newProjectName,
+        groupChatId: this.creatingGroupChat.id,
+        groupChatName: this.creatingGroupChat.name
+      });
+
+      // 1. 创建项目
+      const Project = Parse.Object.extend('Project');
+      const project = new Project();
+
+      // 获取公司对象
+      const company = new Parse.Object('Company');
+      company.id = this.COMPANY_ID;
+
+      project.set('title', this.newProjectName.trim());
+      project.set('company', company.toPointer());
+      project.set('status', '待分配');
+      project.set('currentStage', '订单分配');
+      project.set('data', {
+        createdBy: 'admin',
+        createdFrom: 'admin_groupchat',
+        groupChatId: this.creatingGroupChat.id
+      });
+
+      await project.save();
+      console.log('项目创建成功', { projectId: project.id });
+
+      // 2. 关联GroupChat.project
+      const groupChatQuery = new Parse.Query('GroupChat');
+      const groupChatObject = await groupChatQuery.get(this.creatingGroupChat.id);
+      groupChatObject.set('project', project.toPointer());
+      await groupChatObject.save();
+      console.log('群聊关联项目成功');
+
+      // 3. 创建 ProjectGroup 关联(支持多项目多群)
+      const ProjectGroup = Parse.Object.extend('ProjectGroup');
+      const pg = new ProjectGroup();
+      pg.set('project', project.toPointer());
+      pg.set('groupChat', groupChatObject.toPointer());
+      pg.set('isPrimary', true);
+      pg.set('company', company.toPointer());
+      await pg.save();
+      console.log('ProjectGroup关联创建成功');
+
+      alert('项目创建成功!');
+
+      // 4. 刷新群组列表
+      await this.loadGroupChats();
+
+      // 5. 关闭模态框
+      this.closeCreateProjectModal();
+
+    } catch (err: any) {
+      console.error('创建项目失败:', err);
+      alert('创建失败: ' + (err.message || '未知错误'));
+    } finally {
+      this.creating.set(false);
+    }
+  }
+
+  /**
+   * 导航到项目详情页
+   */
+  async navigateToProjectDetail(group: GroupChat) {
+    if (!group.project) {
+      alert('该群组未关联项目');
+      return;
+    }
+
+    const projectId = group.project.id;
+    const cid = this.COMPANY_ID;
+
+    console.log('导航到项目详情', {
+      projectId,
+      cid,
+      groupId: group.id
+    });
+
+    // 导航到项目详情页(企微路由)
+    await this.router.navigate(['/wxwork', cid, 'project', projectId], {
+      queryParams: {
+        groupId: group.id
+      }
+    });
+  }
 }

+ 89 - 0
src/modules/project/pages/project-detail/stages/stage-order.component.scss

@@ -1071,6 +1071,95 @@
       }
     }
 
+    // 已分配组员展示
+    .assigned-teams-section {
+      margin-bottom: 32px;
+      padding-bottom: 24px;
+      border-bottom: 2px solid var(--light-shade);
+
+      .team-list {
+        display: grid;
+        grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+        gap: 16px;
+        margin-top: 16px;
+      }
+
+      .team-item {
+        background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.05) 0%, rgba(var(--primary-rgb), 0.02) 100%);
+        border: 2px solid var(--primary-color);
+        border-radius: 10px;
+        padding: 16px;
+        transition: all 0.3s ease;
+
+        &:hover {
+          transform: translateY(-2px);
+          box-shadow: 0 6px 16px rgba(var(--primary-rgb), 0.15);
+        }
+
+        .team-member {
+          display: flex;
+          align-items: center;
+          gap: 14px;
+
+          .member-avatar {
+            width: 56px;
+            height: 56px;
+            border-radius: 50%;
+            overflow: hidden;
+            background: white;
+            border: 3px solid var(--primary-color);
+            flex-shrink: 0;
+            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: 32px;
+              height: 32px;
+              color: var(--primary-color);
+            }
+          }
+
+          .member-info {
+            flex: 1;
+            min-width: 0;
+
+            h5 {
+              margin: 0 0 6px;
+              font-size: 16px;
+              font-weight: 700;
+              color: var(--dark-color);
+              white-space: nowrap;
+              overflow: hidden;
+              text-overflow: ellipsis;
+            }
+
+            .member-spaces {
+              margin: 0;
+              font-size: 13px;
+              color: var(--primary-color);
+              font-weight: 500;
+              display: flex;
+              align-items: center;
+              gap: 4px;
+
+              &::before {
+                content: '📦';
+                font-size: 14px;
+              }
+            }
+          }
+        }
+      }
+    }
+
     // 组员选择
     .designer-section {
       .designer-grid {