Browse Source

feat: admin with parse & font-awesome

ryanemax 21 hours ago
parent
commit
b1d5e5492c

+ 3 - 0
angular.json

@@ -36,6 +36,9 @@
             ],
             "styles": [
               "src/styles.scss",
+              {
+                "input":"node_modules/@fortawesome/fontawesome-free/css/all.min.css"
+              },
               {
                 "input": "node_modules/@ionic/angular/css/core.css"
               },

+ 1 - 0
package.json

@@ -58,6 +58,7 @@
     "@codemirror/theme-one-dark": "^6.1.3",
     "@codemirror/view": "^6.38.6",
     "@ctrl/tinycolor": "^4.2.0",
+    "@fortawesome/fontawesome-free": "^7.1.0",
     "@ionic/angular": "^8.7.7",
     "@langchain/core": "^0.3.78",
     "@types/spark-md5": "^3.0.5",

+ 29 - 0
src/app/app.routes.ts

@@ -193,6 +193,35 @@ export const routes: Routes = [
         loadComponent: () => import('./pages/admin/project-management/project-management').then(m => m.ProjectManagement),
         title: '项目管理'
       },
+      // 项目详情页(复用wxwork模块)
+      {
+        path: 'project-detail/:projectId',
+        loadComponent: () => import('../modules/project/pages/project-detail/project-detail.component').then(m => m.ProjectDetailComponent),
+        title: '项目详情',
+        children: [
+          { path: '', redirectTo: 'order', pathMatch: 'full' },
+          {
+            path: 'order',
+            loadComponent: () => import('../modules/project/pages/project-detail/stages/stage-order.component').then(m => m.StageOrderComponent),
+            title: '订单分配'
+          },
+          {
+            path: 'requirements',
+            loadComponent: () => import('../modules/project/pages/project-detail/stages/stage-requirements.component').then(m => m.StageRequirementsComponent),
+            title: '确认需求'
+          },
+          {
+            path: 'delivery',
+            loadComponent: () => import('../modules/project/pages/project-detail/stages/stage-delivery.component').then(m => m.StageDeliveryComponent),
+            title: '交付执行'
+          },
+          {
+            path: 'aftercare',
+            loadComponent: () => import('../modules/project/pages/project-detail/stages/stage-aftercare.component').then(m => m.StageAftercareComponent),
+            title: '售后归档'
+          }
+        ]
+      },
       // 用户与角色管理相关路由
       {
         path: 'user-management',

+ 14 - 1
src/app/pages/admin/customers/customers.html

@@ -33,7 +33,7 @@
       <div>来源</div>
       <div>创建时间</div>
     </div>
-    <div class="table row" *ngFor="let c of filtered">
+    <div class="table row clickable" *ngFor="let c of filtered" (click)="openCustomerDetail(c)">
       <div class="name">
         <div class="title">{{ c.name }}</div>
       </div>
@@ -50,4 +50,17 @@
       }
     </div>
   </div>
+
+  <!-- 客户详情面板 -->
+  <div class="customer-panel-overlay" *ngIf="showCustomerPanel" (click)="closeCustomerPanel()">
+    <div class="customer-panel" (click)="$event.stopPropagation()">
+      <div class="panel-header">
+        <h2>客户详情</h2>
+        <button class="close-btn" (click)="closeCustomerPanel()">×</button>
+      </div>
+      <div class="panel-body">
+        <app-contact *ngIf="selectedCustomer" [customer]="selectedCustomer"></app-contact>
+      </div>
+    </div>
+  </div>
 </div>

+ 106 - 1
src/app/pages/admin/customers/customers.scss

@@ -71,4 +71,109 @@
 
   .btn { padding: 8px 16px; border-radius: 6px; border: 1px solid #e5e6eb; background: #fff; cursor: pointer; }
   .btn.primary { background: #3a7afe; color: #fff; border-color: #3a7afe; }
-}
+}
+/* 可点击行样式 */
+.table.row.clickable {
+  cursor: pointer;
+  transition: background-color 0.2s;
+
+  &:hover {
+    background-color: #f8fafc;
+  }
+}
+
+/* 客户详情面板样式 */
+.customer-panel-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 2000;
+  animation: fadeIn 0.2s ease-out;
+}
+
+.customer-panel {
+  width: 90%;
+  max-width: 900px;
+  max-height: 90vh;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+  display: flex;
+  flex-direction: column;
+  animation: slideUp 0.3s ease-out;
+
+  .panel-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 20px 24px;
+    border-bottom: 1px solid #e5e7eb;
+
+    h2 {
+      margin: 0;
+      font-size: 20px;
+      font-weight: 600;
+      color: #1f2937;
+    }
+
+    .close-btn {
+      background: none;
+      border: none;
+      font-size: 28px;
+      color: #6b7280;
+      cursor: pointer;
+      line-height: 1;
+      padding: 0;
+      width: 32px;
+      height: 32px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 6px;
+      transition: all 0.2s;
+
+      &:hover {
+        background: #f3f4f6;
+        color: #1f2937;
+      }
+    }
+  }
+
+  .panel-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 0;
+  }
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes slideUp {
+  from {
+    transform: translateY(20px);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+@media (max-width: 768px) {
+  .customer-panel {
+    width: 100%;
+    max-width: 100%;
+    max-height: 100vh;
+    border-radius: 0;
+  }
+}

+ 34 - 3
src/app/pages/admin/customers/customers.ts

@@ -2,6 +2,8 @@ import { Component, signal, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { CustomerService } from '../services/customer.service';
+import { FmodeObject } from 'fmode-ng/core';
+import { CustomerProfileComponent } from '../../../../modules/project/pages/contact/contact.component';
 
 interface Customer {
   id: string;
@@ -15,7 +17,7 @@ interface Customer {
 @Component({
   selector: 'app-admin-customers',
   standalone: true,
-  imports: [CommonModule, FormsModule],
+  imports: [CommonModule, FormsModule, CustomerProfileComponent],
   templateUrl: './customers.html',
   styleUrl: './customers.scss'
 })
@@ -26,6 +28,11 @@ export class Customers implements OnInit {
 
   // 数据
   customers = signal<Customer[]>([]);
+  customerObjects: Map<string, FmodeObject> = new Map(); // 存储Parse对象
+
+  // 客户详情面板
+  showCustomerPanel = false;
+  selectedCustomer: FmodeObject | null = null;
 
   constructor(private customerService: CustomerService) {}
 
@@ -37,15 +44,24 @@ export class Customers implements OnInit {
     this.loading.set(true);
     try {
       const custs = await this.customerService.findCustomers();
+
+      // 清空之前的对象映射
+      this.customerObjects.clear();
+
       const custList: Customer[] = custs.map(c => {
         const json = this.customerService.toJSON(c);
+        const customerId = json.objectId;
+
+        // 保存Parse对象以便后续使用
+        this.customerObjects.set(customerId, c);
+
         return {
-          id: json.objectId,
+          id: customerId,
           name: json.name || '未知客户',
           mobile: json.mobile || '',
           external_userid: json.external_userid,
           source: json.source,
-          createdAt: json.createdAt
+          createdAt: json.createdAt?.iso || json.createdAt
         };
       });
 
@@ -58,6 +74,21 @@ export class Customers implements OnInit {
     }
   }
 
+  // 打开客户详情面板
+  openCustomerDetail(customer: Customer) {
+    const customerObj = this.customerObjects.get(customer.id);
+    if (customerObj) {
+      this.selectedCustomer = customerObj;
+      this.showCustomerPanel = true;
+    }
+  }
+
+  // 关闭客户详情面板
+  closeCustomerPanel() {
+    this.showCustomerPanel = false;
+    this.selectedCustomer = null;
+  }
+
   // 筛选
   keyword = signal('');
   status = signal<'all' | 'active' | 'inactive'>('all');

+ 8 - 8
src/app/pages/admin/departments/departments.html

@@ -7,10 +7,10 @@
     </div>
     <div class="header-right">
       <button class="btn btn-secondary" (click)="exportDepartments()">
-        <i class="icon-export"></i> 导出
+        <i class="fa-solid fa-arrow-up-from-bracket"></i> 导出
       </button>
       <button class="btn btn-primary" (click)="addDepartment()">
-        <i class="icon-plus"></i> 新建项目组
+        <i class="fa-solid fa-plus"></i> 新建项目组
       </button>
     </div>
   </div>
@@ -19,7 +19,7 @@
   <div class="stats-cards">
     <div class="stat-card">
       <div class="stat-icon" style="background: #E6F7FF">
-        <i class="icon-team" style="color: #165DFF"></i>
+        <i class="fa-solid fa-users" style="color: #165DFF"></i>
       </div>
       <div class="stat-info">
         <div class="stat-value">{{ total() }}</div>
@@ -65,7 +65,7 @@
       <tbody>
         <tr *ngFor="let dept of filtered">
           <td>{{ dept.name }}</td>
-          <td>{{ dept.leader }}</td>
+          <td>{{ dept.leaderName }}</td>
           <td>{{ dept.memberCount }}</td>
           <td>
             {{
@@ -81,21 +81,21 @@
                 (click)="viewDepartment(dept)"
                 title="查看"
               >
-                <i class="icon-eye"></i>
+                <i class="fa-solid fa-eye"></i>
               </button>
               <button
                 class="btn-icon"
                 (click)="editDepartment(dept)"
                 title="编辑"
               >
-                <i class="icon-edit"></i>
+                <i class="fa-solid fa-edit"></i>
               </button>
               <button
                 class="btn-icon btn-danger"
                 (click)="deleteDepartment(dept)"
                 title="删除"
               >
-                <i class="icon-delete"></i>
+                <i class="fa-solid fa-trash"></i>
               </button>
             </div>
           </td>
@@ -133,7 +133,7 @@
           </div>
           <div class="detail-item">
             <label>组长</label>
-            <div class="detail-value">{{ currentDepartment.leader }}</div>
+            <div class="detail-value">{{ currentDepartment.leader?.get("name") }}</div>
           </div>
           <div class="detail-item">
             <label>成员数</label>

+ 9 - 7
src/app/pages/admin/departments/departments.ts

@@ -3,11 +3,13 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { DepartmentService } from '../services/department.service';
 import { EmployeeService } from '../services/employee.service';
+import { FmodeObject } from 'fmode-ng/core';
 
 interface Department {
   id: string;
   name: string;
-  leader: string;
+  leader: FmodeObject;
+  leaderName: string;
   leaderId?: string;
   type: string;
   memberCount: number;
@@ -66,15 +68,15 @@ export class Departments implements OnInit {
         depts.map(async d => {
           const json = this.departmentService.toJSON(d);
           const memberCount = await this.departmentService.countDepartmentMembers(json.objectId);
-
           return {
             id: json.objectId,
             name: json.name || '未命名项目组',
-            leader: json.leaderName || '未分配',
-            leaderId: json.leaderId,
+            leader: d?.get("leader"),
+            leaderName: d?.get("leader")?.get("name") || '未分配',
+            leaderId: d?.get("leader").id,
             type: json.type || 'project',
             memberCount,
-            createdAt: json.createdAt
+            createdAt: json.createdAt?.iso || json.createdAt
           };
         })
       );
@@ -117,7 +119,7 @@ export class Departments implements OnInit {
     return this.departments().filter(
       d =>
         d.name.toLowerCase().includes(kw) ||
-        d.leader.toLowerCase().includes(kw)
+        d.leaderName.toLowerCase().includes(kw)
     );
   }
 
@@ -237,7 +239,7 @@ export class Departments implements OnInit {
     const header = ['项目组名称', '组长', '成员数', '创建时间'];
     const rows = this.filtered.map(d => [
       d.name,
-      d.leader,
+      d.leaderName,
       String(d.memberCount),
       d.createdAt instanceof Date
         ? d.createdAt.toISOString().slice(0, 10)

+ 2 - 2
src/app/pages/admin/employees/employees.html

@@ -66,7 +66,7 @@
         <tr *ngFor="let emp of filtered" [class.disabled]="emp.isDisabled">
           <td>{{ emp.name }}</td>
           <td>{{ emp.mobile }}</td>
-          <td>{{ emp.userId }}</td>
+          <td>{{ emp.userid }}</td>
           <td><span class="badge">{{ emp.roleName }}</span></td>
           <td>{{ emp.department }}</td>
           <td><span [class]="'status ' + (emp.isDisabled ? 'disabled' : 'active')">{{ emp.isDisabled ? '已禁用' : '正常' }}</span></td>
@@ -97,7 +97,7 @@
         <div *ngIf="panelMode === 'detail'" class="detail-view">
           <div class="detail-item"><label>姓名</label><div>{{ currentEmployee.name }}</div></div>
           <div class="detail-item"><label>手机号</label><div>{{ currentEmployee.mobile }}</div></div>
-          <div class="detail-item"><label>企微ID</label><div>{{ currentEmployee.userId }}</div></div>
+          <div class="detail-item"><label>企微ID</label><div>{{ currentEmployee.userid }}</div></div>
           <div class="detail-item"><label>身份</label><div>{{ currentEmployee.roleName }}</div></div>
           <div class="detail-item"><label>部门</label><div>{{ currentEmployee.department }}</div></div>
           <div class="detail-item"><label>状态</label><div>{{ currentEmployee.isDisabled ? '已禁用' : '正常' }}</div></div>

+ 4 - 4
src/app/pages/admin/employees/employees.ts

@@ -8,7 +8,7 @@ interface Employee {
   id: string;
   name: string;
   mobile: string;
-  userId: string;
+  userid: string;
   roleName: string;
   department: string;
   departmentId?: string;
@@ -78,7 +78,7 @@ export class Employees implements OnInit {
           id: json.objectId,
           name: json.name || '未知',
           mobile: json.mobile || '',
-          userId: json.userId || '',
+          userid: json.userid || '',
           roleName: json.roleName || '未分配',
           department: json.departmentName || '未分配',
           departmentId: json.departmentId,
@@ -135,7 +135,7 @@ export class Employees implements OnInit {
         e =>
           e.name.toLowerCase().includes(kw) ||
           e.mobile.includes(kw) ||
-          e.userId.toLowerCase().includes(kw)
+          e.userid.toLowerCase().includes(kw)
       );
     }
 
@@ -215,7 +215,7 @@ export class Employees implements OnInit {
     const rows = this.filtered.map(e => [
       e.name,
       e.mobile,
-      e.userId,
+      e.userid,
       e.roleName,
       e.department,
       e.isDisabled ? '已禁用' : '正常',

+ 31 - 0
src/app/pages/admin/groupchats/groupchats.html

@@ -74,11 +74,42 @@
       </div>
       <div class="panel-body" *ngIf="currentGroupChat">
         <div *ngIf="panelMode === 'detail'" class="detail-view">
+          <!-- 基础信息 -->
+          <div class="section-title">基础信息</div>
           <div class="detail-item"><label>群名称</label><div>{{ currentGroupChat.name }}</div></div>
           <div class="detail-item"><label>企微群ID</label><div><code>{{ currentGroupChat.chat_id }}</code></div></div>
           <div class="detail-item"><label>关联项目</label><div>{{ currentGroupChat.project || '未关联' }}</div></div>
           <div class="detail-item"><label>成员数</label><div>{{ currentGroupChat.memberCount }}</div></div>
           <div class="detail-item"><label>状态</label><div>{{ currentGroupChat.isDisabled ? '已禁用' : '正常' }}</div></div>
+
+          <!-- 入群二维码 -->
+          <div class="section-title" *ngIf="qrCodeUrl">入群二维码</div>
+          <div class="qr-code-container" *ngIf="qrCodeUrl">
+            <img [src]="qrCodeUrl" alt="入群二维码" class="qr-code-image" />
+            <p class="qr-code-tip">扫描二维码加入群聊</p>
+          </div>
+
+          <!-- 群成员列表 -->
+          <div class="section-title">群成员列表</div>
+          <div class="loading-text" *ngIf="loadingDetail()">加载成员信息中...</div>
+          <div class="members-list" *ngIf="!loadingDetail() && groupMembers.length > 0">
+            <div class="member-item" *ngFor="let member of groupMembers">
+              <div class="member-info">
+                <div class="member-name">
+                  {{ member.name || member.userid || '未知成员' }}
+                  <span class="member-type" [class.external]="member.type === 2">
+                    {{ member.type === 1 ? '内部' : '外部' }}
+                  </span>
+                </div>
+                <div class="member-detail" *ngIf="member.userid">
+                  <code>{{ member.userid }}</code>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="empty-text" *ngIf="!loadingDetail() && groupMembers.length === 0">
+            暂无成员信息
+          </div>
         </div>
         <div *ngIf="panelMode === 'edit'" class="form-view">
           <div class="form-group">

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

@@ -8,3 +8,114 @@ code {
   border-radius: 3px;
   font-size: 12px;
 }
+
+/* 详情面板增强样式 */
+.section-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2937;
+  margin: 24px 0 12px;
+  padding-bottom: 8px;
+  border-bottom: 2px solid #e5e7eb;
+
+  &:first-child {
+    margin-top: 0;
+  }
+}
+
+.qr-code-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20px;
+  background: #f9fafb;
+  border-radius: 8px;
+  margin: 12px 0;
+
+  .qr-code-image {
+    max-width: 200px;
+    max-height: 200px;
+    border: 4px solid white;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+
+  .qr-code-tip {
+    margin-top: 12px;
+    color: #6b7280;
+    font-size: 14px;
+  }
+}
+
+.members-list {
+  max-height: 400px;
+  overflow-y: auto;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  margin: 12px 0;
+
+  .member-item {
+    padding: 12px 16px;
+    border-bottom: 1px solid #f3f4f6;
+    transition: background-color 0.2s;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &:hover {
+      background-color: #f9fafb;
+    }
+
+    .member-info {
+      .member-name {
+        font-weight: 500;
+        color: #1f2937;
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-bottom: 4px;
+
+        .member-type {
+          display: inline-block;
+          padding: 2px 8px;
+          background: #dbeafe;
+          color: #1e40af;
+          font-size: 12px;
+          border-radius: 4px;
+          font-weight: 600;
+
+          &.external {
+            background: #fef3c7;
+            color: #92400e;
+          }
+        }
+      }
+
+      .member-detail {
+        font-size: 12px;
+        color: #6b7280;
+
+        code {
+          background: #f3f4f6;
+          padding: 2px 6px;
+          border-radius: 4px;
+          font-family: 'Courier New', monospace;
+        }
+      }
+    }
+  }
+}
+
+.loading-text, .empty-text {
+  padding: 20px;
+  text-align: center;
+  color: #9ca3af;
+  font-size: 14px;
+}
+
+.empty-text {
+  background: #f9fafb;
+  border-radius: 8px;
+  border: 1px dashed #d1d5db;
+}

+ 59 - 2
src/app/pages/admin/groupchats/groupchats.ts

@@ -3,6 +3,8 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { GroupChatService } from '../services/groupchat.service';
 import { ProjectService } from '../services/project.service';
+import { WxworkCorp } from 'fmode-ng/core';
+import { FmodeObject } from 'fmode-ng/core';
 
 interface GroupChat {
   id: string;
@@ -40,10 +42,23 @@ export class GroupChats implements OnInit {
   currentGroupChat: GroupChat | null = null;
   formModel: Partial<GroupChat> = {};
 
+  // 群组详情数据
+  groupChatDetail: any = null;
+  groupMembers: any[] = [];
+  qrCodeUrl: string = '';
+  loadingDetail = signal(false);
+
+  // 企微Corp(需要配置cid)
+  private wecorp: WxworkCorp | null = null;
+  private readonly COMPANY_ID = 'cDL6R1hgSi'; // 映三色帐套
+
   constructor(
     private groupChatService: GroupChatService,
     private projectService: ProjectService
-  ) {}
+  ) {
+    // 初始化企微Corp
+    this.wecorp = new WxworkCorp(this.COMPANY_ID);
+  }
 
   ngOnInit(): void {
     this.loadGroupChats();
@@ -105,10 +120,52 @@ export class GroupChats implements OnInit {
     this.keyword.set('');
   }
 
-  viewGroupChat(group: GroupChat) {
+  async viewGroupChat(group: GroupChat) {
     this.currentGroupChat = group;
     this.panelMode = 'detail';
     this.showPanel = true;
+
+    // 加载群组详情
+    await this.loadGroupChatDetail(group);
+  }
+
+  /**
+   * 加载群组详情(从企微获取)
+   */
+  async loadGroupChatDetail(group: GroupChat) {
+    if (!this.wecorp || !group.chat_id) {
+      console.error('企微Corp未初始化或群组chat_id为空');
+      return;
+    }
+
+    this.loadingDetail.set(true);
+    try {
+      // 从企微获取群组详情
+      const chatInfo: any = await this.wecorp.externalContact.groupChat.get(
+        group.chat_id
+      );
+
+      console.log('群组详情:', chatInfo);
+
+      if (chatInfo && chatInfo.group_chat) {
+        this.groupChatDetail = chatInfo.group_chat;
+        this.groupMembers = chatInfo.group_chat.member_list || [];
+
+        // 获取入群二维码(如果有)
+        // 注意:入群二维码需要单独调用API获取
+        // 这里简化处理,实际可能需要根据API文档调整
+        if (chatInfo.group_chat.qr_code) {
+          this.qrCodeUrl = chatInfo.group_chat.qr_code;
+        } else {
+          this.qrCodeUrl = '';
+        }
+      }
+    } catch (error) {
+      console.error('加载群组详情失败:', error);
+      alert('加载群组详情失败,可能是企微API调用失败');
+    } finally {
+      this.loadingDetail.set(false);
+    }
   }
 
   editGroupChat(group: GroupChat) {

+ 2 - 17
src/app/pages/admin/project-management/project-management.html

@@ -6,14 +6,7 @@
       <p class="page-description">管理所有项目的创建、分配和状态</p>
     </div>
     <div class="header-right">
-      <button mat-raised-button color="primary" class="create-btn"
-              (click)="openProjectDialog()">
-        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-          <line x1="12" y1="5" x2="12" y2="19"></line>
-          <line x1="5" y1="12" x2="19" y2="12"></line>
-        </svg>
-        创建新项目
-      </button>
+      <!-- 项目从客服端创建,管理端只查看和分配 -->
     </div>
   </div>
 
@@ -202,20 +195,12 @@
             </button>
             <button mat-icon-button class="action-btn" color="accent"
                     title="编辑项目"
-                    (click)="openProjectDialog(project)">
+                    (click)="openEditDialog(project)">
               <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                 <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
                 <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
               </svg>
             </button>
-            <button mat-icon-button class="action-btn" color="warn"
-                    title="删除项目"
-                    (click)="deleteProject(project.id)">
-              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                <polyline points="3,6 5,6 21,6"></polyline>
-                <path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2V6"></path>
-              </svg>
-            </button>
           </div>
         </td>
       </ng-container>

+ 7 - 55
src/app/pages/admin/project-management/project-management.ts

@@ -106,8 +106,8 @@ export class ProjectManagement implements OnInit {
           status: json.status || '待分配',
           assignee: json.assigneeName || '未分配',
           assigneeId: json.assigneeId,
-          createdAt: json.createdAt,
-          updatedAt: json.updatedAt,
+          createdAt: json.createdAt?.iso || json.createdAt,
+          updatedAt: json.updatedAt?.iso || json.updatedAt,
           deadline: json.deadline,
           currentStage: json.currentStage
         };
@@ -175,61 +175,13 @@ export class ProjectManagement implements OnInit {
     this.applyFilters();
   }
 
-  openProjectDialog(project?: Project): void {
-    const dialogRef = this.dialog.open(ProjectDialogComponent, {
-      width: '600px',
-      data: project ? { ...project } : {
-        id: '',
-        name: '',
-        client: '',
-        designer: '',
-        status: 'pending',
-        priority: 'medium',
-        startDate: new Date().toISOString().split('T')[0],
-        endDate: '',
-        budget: 0,
-        progress: 0,
-        description: '',
-        tasks: []
-      }
-    });
-
-    dialogRef.afterClosed().subscribe(result => {
-      if (result) {
-        if (result.id) {
-          // 更新项目
-          this.updateProject(result);
-        } else {
-          // 创建新项目
-          this.createProject(result);
-        }
-      }
-    });
+  // 简化的编辑对话框(只允许修改名字和分配组员)
+  openEditDialog(project: Project): void {
+    // TODO: 实现简单的编辑对话框,使用设计师分配组件
+    // 暂时使用alert提示
+    alert('编辑功能将在设计师分配组件对接完成后实现');
   }
 
-  createProject(projectData: Omit<Project, 'id'>): void {
-    const newProject: Project = {
-      ...projectData,
-      id: (this.projects().length + 1).toString()
-    };
-    
-    this.projects.set([newProject, ...this.projects()]);
-    this.applyFilters();
-  }
-
-  updateProject(updatedProject: Project): void {
-    this.projects.set(this.projects().map(project => 
-      project.id === updatedProject.id ? updatedProject : project
-    ));
-    this.applyFilters();
-  }
-
-  deleteProject(id: string): void {
-    if (confirm('确定要删除这个项目吗?')) {
-      this.projects.set(this.projects().filter(project => project.id !== id));
-      this.applyFilters();
-    }
-  }
 
   formatCurrency(amount: number): string {
     return new Intl.NumberFormat('zh-CN', {

+ 7 - 0
src/app/pages/designer/project-detail/components/designer-assignment/designer-assignment.component.scss

@@ -3,6 +3,13 @@
   background: white;
   border-radius: 8px;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+  // 移动端优化
+  @media (max-width: 768px) {
+    padding: 16px;
+    border-radius: 0;
+    box-shadow: none;
+  }
 }
 
 .section-header {

+ 38 - 3
src/app/pages/designer/project-detail/components/designer-assignment/designer-assignment.component.ts

@@ -66,8 +66,12 @@ export class DesignerAssignmentComponent implements OnInit {
   showDesignerCalendar = false; // 控制设计师日历显示
   selectedDesignerForCalendar: Designer | null = null; // 当前查看日历的设计师
 
-  // 模拟数据 - 实际项目中应该从服务获取
-  projectTeams: ProjectTeam[] = [
+  // 项目组数据(从服务加载)
+  projectTeams: ProjectTeam[] = [];
+  loadingTeams = false;
+
+  // 模拟数据备份(如果服务加载失败则使用)
+  private mockProjectTeams: ProjectTeam[] = [
     {
       id: 'team-1',
       name: '家装设计组',
@@ -509,7 +513,10 @@ export class DesignerAssignmentComponent implements OnInit {
 
   constructor() {}
 
-  ngOnInit() {
+  async ngOnInit() {
+    // 加载项目组数据
+    await this.loadTeamsData();
+
     if (this.initialAssignment) {
       this.assignmentData = { ...this.initialAssignment };
       this.selectedTeamId = this.assignmentData.primaryTeamId;
@@ -519,6 +526,34 @@ export class DesignerAssignmentComponent implements OnInit {
     this.initializeQuotationAssignments();
   }
 
+  /**
+   * 加载项目组数据
+   */
+  async loadTeamsData() {
+    this.loadingTeams = true;
+    try {
+      // 动态导入服务
+      const { DesignerDataService } = await import('../../services/designer-data.service');
+      const service = new DesignerDataService();
+
+      const teams = await service.loadTeamsWithMembers();
+
+      if (teams && teams.length > 0) {
+        this.projectTeams = teams;
+      } else {
+        // 如果没有数据,使用模拟数据
+        console.warn('未加载到项目组数据,使用模拟数据');
+        this.projectTeams = this.mockProjectTeams;
+      }
+    } catch (error) {
+      console.error('加载项目组数据失败:', error);
+      // 加载失败,使用模拟数据
+      this.projectTeams = this.mockProjectTeams;
+    } finally {
+      this.loadingTeams = false;
+    }
+  }
+
   // 初始化报价分配
   initializeQuotationAssignments() {
     if (this.quotationItems.length > 0 && this.assignmentData.quotationAssignments.length === 0) {

+ 277 - 0
src/app/pages/designer/project-detail/services/designer-data.service.ts

@@ -0,0 +1,277 @@
+import { Injectable } from '@angular/core';
+import { FmodeParse, FmodeObject } from 'fmode-ng/core';
+
+const Parse: any = FmodeParse.with('nova');
+
+export interface DesignerData {
+  id: string;
+  name: string;
+  avatar?: string;
+  teamId: string;
+  teamName: string;
+  isTeamLeader: boolean;
+  status: 'idle' | 'busy' | 'reviewing';
+  idleDays: number;
+  workload: number;
+  skills: string[];
+  roleName: string;
+  // 为兼容现有组件增加别名字段
+  groupId: string;
+  groupName: string;
+  isLeader: boolean;
+  currentProjects: number;
+  isInStagnantProject: boolean;
+  availableDates: string[];
+  reviewDates: string[];
+  recentOrders: number;
+  lastOrderDate?: string;
+}
+
+export interface TeamData {
+  id: string;
+  name: string;
+  leaderId: string;
+  leaderName: string;
+  members: DesignerData[];
+}
+
+/**
+ * 设计师数据服务
+ * 从Parse加载实际的项目组和设计师数据
+ */
+@Injectable({
+  providedIn: 'root'
+})
+export class DesignerDataService {
+  private readonly COMPANY_ID = 'cDL6R1hgSi'; // 映三色帐套
+
+  constructor() {}
+
+  /**
+   * 获取公司指针
+   */
+  private getCompanyPointer(): any {
+    return {
+      __type: 'Pointer',
+      className: '_Company',
+      objectId: this.COMPANY_ID
+    };
+  }
+
+  /**
+   * 加载所有项目组及其成员
+   */
+  async loadTeamsWithMembers(): Promise<TeamData[]> {
+    try {
+      // 1. 查询所有项目组
+      const deptQuery = new Parse.Query('Department');
+      deptQuery.equalTo('company', this.getCompanyPointer());
+      deptQuery.equalTo('type', 'project');
+      deptQuery.notEqualTo('isDeleted', true);
+      deptQuery.include(['leader']);
+
+      const departments = await deptQuery.find();
+
+      // 2. 为每个项目组加载成员
+      const teams: TeamData[] = await Promise.all(
+        departments.map(async (dept: any) => {
+          const deptId = dept.id;
+          const deptName = dept.get('name') || '未命名项目组';
+          const leader = dept.get('leader');
+          const leaderId = leader?.id || '';
+          const leaderName = leader?.get('name') || '未分配';
+
+          // 查询项目组成员
+          const memberQuery = new Parse.Query('Profile');
+          memberQuery.equalTo('company', this.getCompanyPointer());
+          memberQuery.equalTo('department', dept.toPointer());
+          memberQuery.notEqualTo('isDeleted', true);
+          memberQuery.equalTo('roleName', '组员'); // 只查询组员角色
+
+          const members = await memberQuery.find();
+
+          // 转换成员数据
+          const memberList: DesignerData[] = members.map((m: any) =>
+            this.transformToDesignerData(m, deptId, deptName)
+          );
+
+          // 如果组长也在Profile中,也加入成员列表
+          if (leader) {
+            const leaderData = this.transformToDesignerData(
+              leader,
+              deptId,
+              deptName
+            );
+            leaderData.isTeamLeader = true;
+            leaderData.isLeader = true;
+            memberList.unshift(leaderData);
+          }
+
+          return {
+            id: deptId,
+            name: deptName,
+            leaderId,
+            leaderName,
+            members: memberList
+          };
+        })
+      );
+
+      return teams;
+    } catch (error) {
+      console.error('加载项目组失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 将Profile转换为DesignerData
+   */
+  private transformToDesignerData(
+    profile: FmodeObject,
+    teamId: string,
+    teamName: string
+  ): DesignerData {
+    const data = profile.get('data') || {};
+    const roleName = profile.get('roleName') || '组员';
+    const isLeader = roleName === '组长';
+
+    // 计算工作负载(简化逻辑,实际应从项目分配中统计)
+    const workload = data.workload || 0;
+
+    // 计算空闲天数(简化逻辑)
+    const lastOrderDate = data.lastOrderDate;
+    let idleDays = 0;
+    if (lastOrderDate) {
+      const last = new Date(lastOrderDate);
+      const now = new Date();
+      idleDays = Math.floor(
+        (now.getTime() - last.getTime()) / (1000 * 60 * 60 * 24)
+      );
+    }
+
+    // 判断状态
+    let status: 'idle' | 'busy' | 'reviewing' = 'idle';
+    if (workload > 70) {
+      status = 'busy';
+    } else if (data.isReviewing) {
+      status = 'reviewing';
+    }
+
+    return {
+      id: profile.id || '',
+      name: profile.get('name') || '未知设计师',
+      avatar: data.avatar || '',
+      teamId,
+      teamName,
+      isTeamLeader: isLeader,
+      status,
+      idleDays,
+      workload,
+      skills: data.skills || [],
+      roleName,
+      // 别名字段
+      groupId: teamId,
+      groupName: teamName,
+      isLeader,
+      currentProjects: data.currentProjects || 0,
+      isInStagnantProject: false,
+      availableDates: data.availableDates || [],
+      reviewDates: data.reviewDates || [],
+      recentOrders: data.recentOrders || 0,
+      lastOrderDate: data.lastOrderDate
+    };
+  }
+
+  /**
+   * 根据设计师ID列表获取设计师信息
+   */
+  async getDesignersByIds(designerIds: string[]): Promise<DesignerData[]> {
+    if (!designerIds || designerIds.length === 0) {
+      return [];
+    }
+
+    try {
+      const query = new Parse.Query('Profile');
+      query.containedIn('objectId', designerIds);
+      query.include(['department']);
+
+      const profiles = await query.find();
+
+      return profiles.map((p: any) => {
+        const dept = p.get('department');
+        const teamId = dept?.id || '';
+        const teamName = dept?.get('name') || '未分配';
+        return this.transformToDesignerData(p, teamId, teamName);
+      });
+    } catch (error) {
+      console.error('获取设计师信息失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 保存项目的设计师分配
+   */
+  async saveProjectAssignment(
+    projectId: string,
+    assignment: {
+      primaryTeamId: string;
+      assignedDesignerIds: string[];
+      crossTeamCollaborators: string[];
+    }
+  ): Promise<boolean> {
+    try {
+      const projectQuery = new Parse.Query('Project');
+      const project = await projectQuery.get(projectId);
+
+      // 主要负责人(取第一个设计师)
+      if (assignment.assignedDesignerIds.length > 0) {
+        const mainDesignerId = assignment.assignedDesignerIds[0];
+        project.set('assignee', {
+          __type: 'Pointer',
+          className: 'Profile',
+          objectId: mainDesignerId
+        });
+      }
+
+      // 保存所有分配的设计师到data字段
+      const projectData = project.get('data') || {};
+      projectData.assignedDesigners = assignment.assignedDesignerIds;
+      projectData.crossTeamCollaborators = assignment.crossTeamCollaborators;
+      projectData.primaryTeamId = assignment.primaryTeamId;
+      project.set('data', projectData);
+
+      await project.save();
+      return true;
+    } catch (error) {
+      console.error('保存设计师分配失败:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 获取项目的设计师分配信息
+   */
+  async getProjectAssignment(projectId: string): Promise<{
+    primaryTeamId: string;
+    assignedDesignerIds: string[];
+    crossTeamCollaborators: string[];
+  } | null> {
+    try {
+      const projectQuery = new Parse.Query('Project');
+      const project = await projectQuery.get(projectId);
+
+      const data = project.get('data') || {};
+
+      return {
+        primaryTeamId: data.primaryTeamId || '',
+        assignedDesignerIds: data.assignedDesigners || [],
+        crossTeamCollaborators: data.crossTeamCollaborators || []
+      };
+    } catch (error) {
+      console.error('获取项目分配失败:', error);
+      return null;
+    }
+  }
+}

+ 1 - 0
src/index.html

@@ -10,6 +10,7 @@
     <!-- Material Icons & Material Symbols 本地/线上字体引入 -->
     <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
+    <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
     <!-- Local import map to resolve bare specifier 'echarts' to local ESM file -->
     <!-- Use local UMD build to provide window.echarts globally (avoid external CDN errors) -->
     <script defer src="assets/echarts/echarts.min.js"></script>

+ 1 - 0
src/styles.scss

@@ -1,4 +1,5 @@
 
+
 // Include theming for Angular Material with `mat.theme()`.
 // This Sass mixin will define CSS variables that are used for styling Angular Material
 // components according to the Material 3 design spec.