Browse Source

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

ryanemax 1 day ago
parent
commit
5ee839dc9a

+ 34 - 0
CHANGELOG.md

@@ -0,0 +1,34 @@
+# 更新日志
+
+## 2025-10-24
+### 项目启动访谈问卷
+
+## 2025-10-23
+
+### 员工管理(后台)
+- 员工列表显示头像与职位,缺失头像自动使用统一占位图,列表更整齐。
+- 员工详情弹窗更丰富:手机号、邮箱、企微ID、身份、部门、入职时间、技能与工作量等一目了然。
+- 弹窗样式与布局优化,对齐与间距更合理,信息更易读。
+
+### 表格与界面一致性
+- 多页面的列宽与对齐优化,整体信息密度与可读性提升,浏览体验更统一。
+
+### 稳定性与兼容性
+- 刷新后列表自动同步最新数据,减少信息不一致的情况。
+- 无跟进记录时自动显示历史跟进信息,保证页面内容完整性。
+## 2025-10-22
+
+### 客户选择与详情体验
+- 项目页支持便捷选择或创建客户,已建档/未建档清晰分区,搜索更高效。
+- 新增“一键刷新客户信息”,可同步企业微信的最新资料,名单与详情保持一致。
+- 客户详情以侧栏弹窗方式展示,点击返回或遮罩即可关闭,不会跳转到错误页面。
+- 跟进记录默认显示当前项目的记录,支持切换查看该客户的全部跟进历史。
+
+### 客户信息显示优化
+- 统一头像占位图为 `/assets/images/default-avatar.svg`,列表与详情一致,缺失头像时显示更友好。
+- “所在群聊”改为纵向列表,信息展示更完整,阅读更舒适。
+
+### 客户管理(后台)
+- 客户列表增加头像、类型、名称等信息显示,点击即可查看详情。
+- 在详情中可直接刷新客户数据,确保资料实时准确。
+

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

@@ -37,7 +37,7 @@
     <div class="table row clickable" *ngFor="let c of filtered" (click)="openCustomerDetail(c)">
       <div class="name">
         <div class="title">
-          <img *ngIf="c.get('data')?.avatar" [src]="c.get('data')?.avatar" alt="" style="width:24px;height:24px;border-radius:50%;"/>
+          <img [src]="(c.get('data')?.avatar || '/assets/images/default-avatar.svg')" alt="" style="width:24px;height:24px;border-radius:50%;"/>
           <span>{{ c.get('name') || c.get('data')?.name }}</span>
         </div>
       </div>

+ 50 - 18
src/app/pages/admin/employees/employees.html

@@ -64,19 +64,27 @@
       </thead>
       <tbody>
         <tr *ngFor="let emp of filtered" [class.disabled]="emp.isDisabled">
-          <td>{{ emp.name }}</td>
+          <td>
+            <div style="display:flex;align-items:center;gap:8px;">
+              <img [src]="emp.avatar || '/assets/images/default-avatar.svg'" alt="" style="width:28px;height:28px;border-radius:50%;"/>
+              <div>
+                <div style="font-weight:600;">{{ emp.name }}</div>
+                <div style="font-size:12px;color:#888;" *ngIf="emp.position">{{ emp.position }}</div>
+              </div>
+            </div>
+          </td>
           <td>{{ emp.mobile }}</td>
           <td>{{ emp.userid }}</td>
           <td><span class="badge">{{ emp.roleName }}</span></td>
           <td>
             @if(emp.roleName=="客服"){
               客服部
-            }@else if(emp.roleName=="管理员"){
+            } @else if(emp.roleName=="管理员") {
               总部
-            }@else{
+            } @else {
               {{ emp.department }}
             }
-           </td>
+          </td>
           <td><span [class]="'status ' + (emp.isDisabled ? 'disabled' : 'active')">{{ emp.isDisabled ? '已禁用' : '正常' }}</span></td>
           <td>
             <button class="btn-icon" (click)="viewEmployee(emp)" title="查看">👁</button>
@@ -103,20 +111,44 @@
       </div>
       <div class="panel-body" *ngIf="currentEmployee">
         <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>身份</label><div>{{ currentEmployee.roleName }}</div></div>
-          <div class="detail-item"><label>部门</label><div>
-          @if(currentEmployee.roleName=="客服"){
-            客服部
-          }@else if(currentEmployee.roleName=="管理员"){
-            总部
-          }@else{
-            {{ currentEmployee.department }}
-          }
-          </div></div>
-          <div class="detail-item"><label>状态</label><div>{{ currentEmployee.isDisabled ? '已禁用' : '正常' }}</div></div>
+          <div class="detail-row">
+            <img [src]="currentEmployee.avatar || '/assets/images/default-avatar.svg'" class="avatar"/>
+            <div class="title-block">
+              <div class="name">{{ currentEmployee.name }}</div>
+              <div class="position" *ngIf="currentEmployee.position">{{ currentEmployee.position }}</div>
+            </div>
+          </div>
+          <div class="grid">
+            <div class="detail-item"><label>手机号</label><div>{{ currentEmployee.mobile || '-' }}</div></div>
+            <div class="detail-item"><label>邮箱</label><div>{{ currentEmployee.email || '-' }}</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>
+              @if(currentEmployee.roleName=="客服") {
+                客服部
+              } @else if(currentEmployee.roleName=="管理员") {
+                总部
+              } @else {
+                {{ currentEmployee.department }}
+              }
+            </div></div>
+            <div class="detail-item"><label>入职</label><div>{{ currentEmployee.joinDate || '-' }}</div></div>
+            <div class="detail-item"><label>状态</label><div>{{ currentEmployee.isDisabled ? '已禁用' : '正常' }}</div></div>
+          </div>
+          <div class="skills" *ngIf="currentEmployee.skills?.length">
+            <label>技能</label>
+            <div class="tags">
+              <span class="tag" *ngFor="let s of currentEmployee.skills">{{ s }}</span>
+            </div>
+          </div>
+          <div class="workload" *ngIf="currentEmployee.workload">
+            <label>工作量</label>
+            <div class="grid">
+              <div class="detail-item"><label>当前项目</label><div>{{ currentEmployee.workload?.currentProjects || 0 }}</div></div>
+              <div class="detail-item"><label>已完成</label><div>{{ currentEmployee.workload?.completedProjects || 0 }}</div></div>
+              <div class="detail-item"><label>平均质量</label><div>{{ currentEmployee.workload?.averageQuality || 0 }}</div></div>
+            </div>
+          </div>
         </div>
         <div *ngIf="panelMode === 'edit'" class="form-view">
           <div class="form-group">

+ 16 - 44
src/app/pages/admin/employees/employees.scss

@@ -233,48 +233,20 @@
     }
   }
 
-  .panel-body {
-    flex: 1;
-    overflow-y: auto;
-    padding: 20px;
-  }
-
-  .panel-footer {
-    padding: 16px 20px;
-    border-top: 1px solid #f0f0f0;
-    display: flex;
-    justify-content: flex-end;
-    gap: 12px;
-  }
-}
-
-.form-group {
-  margin-bottom: 20px;
-
-  label {
-    display: block;
-    margin-bottom: 8px;
-    font-weight: 500;
-  }
-
-  .form-control {
-    width: 100%;
-    padding: 8px 12px;
-    border: 1px solid #ddd;
-    border-radius: 4px;
-  }
-}
-
-.detail-item {
-  margin-bottom: 20px;
-
-  label {
-    display: block;
-    color: #999;
-    margin-bottom: 8px;
-  }
-
-  div {
-    font-size: 16px;
-  }
+  .panel-body{padding:20px;overflow:auto}
+  .detail-view{display:flex;flex-direction:column;gap:16px}
+  .detail-row{display:flex;align-items:center;gap:12px;margin-bottom:8px}
+  .avatar{width:64px;height:64px;border-radius:50%;object-fit:cover;border:1px solid #eee}
+  .title-block .name{font-size:18px;font-weight:600}
+  .title-block .position{font-size:12px;color:#888}
+  .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
+  .detail-item{background:#fafafa;border:1px solid #f0f0f0;border-radius:8px;padding:10px}
+  .detail-item label{display:block;color:#666;font-size:12px;margin-bottom:6px}
+  .detail-item div{font-size:14px;color:#333}
+  .skills{margin-top:6px}
+  .tags{display:flex;gap:6px;flex-wrap:wrap}
+  .tag{padding:4px 8px;border-radius:12px;background:#eef2ff;color:#4f46e5;font-size:12px}
+  .workload label{display:block;color:#666;font-size:12px;margin-bottom:6px}
+  .panel-header h2{display:flex;align-items:center;gap:8px}
+  .panel-header h2::before{content:"👤"}
 }

+ 33 - 7
src/app/pages/admin/employees/employees.ts

@@ -14,6 +14,15 @@ interface Employee {
   departmentId?: string;
   isDisabled?: boolean;
   createdAt?: Date;
+  // 新增展示字段
+  avatar?: string;
+  email?: string;
+  position?: string;
+  gender?: string;
+  level?: string;
+  skills?: string[];
+  joinDate?: Date | string;
+  workload?: { currentProjects?: number; completedProjects?: number; averageQuality?: number };
 }
 
 interface Department {
@@ -74,16 +83,31 @@ export class Employees implements OnInit {
 
       const empList: Employee[] = emps.map(e => {
         const json = this.employeeService.toJSON(e);
+        const data = (e as any).get ? ((e as any).get('data') || {}) : {};
+        const workload = data.workload || {};
+        const wxwork = data.wxworkInfo || {};
         return {
           id: json.objectId,
-          name: json.name || '未知',
-          mobile: json.mobile || '',
-          userid: json.userid || '',
+          name: json.name || data.name || '未知',
+          mobile: json.mobile || wxwork.mobile || '',
+          userid: json.userid || wxwork.userid || '',
           roleName: json.roleName || '未分配',
           department: e.get("department")?.get("name") || '未分配',
           departmentId: e.get("department")?.id,
           isDisabled: json.isDisabled || false,
-          createdAt: json.createdAt
+          createdAt: json.createdAt,
+          avatar: data.avatar || wxwork.avatar || '',
+          email: data.email || '',
+          position: wxwork.position || '',
+          gender: data.gender || '',
+          level: data.level || '',
+          skills: Array.isArray(data.skills) ? data.skills : [],
+          joinDate: data.joinDate || '',
+          workload: {
+            currentProjects: workload.currentProjects || 0,
+            completedProjects: workload.completedProjects || 0,
+            averageQuality: workload.averageQuality || 0
+          }
         };
       });
 
@@ -133,9 +157,11 @@ export class Employees implements OnInit {
     if (kw) {
       list = list.filter(
         e =>
-          e.name.toLowerCase().includes(kw) ||
-          e.mobile.includes(kw) ||
-          e.userid.toLowerCase().includes(kw)
+          (e.name || '').toLowerCase().includes(kw) ||
+          (e.mobile || '').includes(kw) ||
+          (e.userid || '').toLowerCase().includes(kw) ||
+          (e.email || '').toLowerCase().includes(kw) ||
+          (e.position || '').toLowerCase().includes(kw)
       );
     }
 

+ 6 - 8
src/modules/project/components/contact-selector/contact-selector.component.html

@@ -6,8 +6,7 @@
     <div class="card">
       <div class="row">
         <div class="avatar" (click)="viewCustomerDetail()">
-          <img *ngIf="currentCustomer.get('data')?.avatar" [src]="currentCustomer.get('data')?.avatar" alt="" />
-          <div class="placeholder" *ngIf="!currentCustomer.get('data')?.avatar">👤</div>
+          <img [src]="currentCustomer.get('data')?.avatar || '/assets/images/default-avatar.svg'" alt="" />
         </div>
         <div class="info" (click)="viewCustomerDetail()">
           <div class="name">{{ currentCustomer.get('name') || currentCustomer.get('data')?.external_contact?.name || currentCustomer.get('data')?.name }}</div>
@@ -17,9 +16,9 @@
           </div>
         </div>
         <div class="actions">
-          <button class="btn outline" (click)="switchToSelecting()">重</button>
-          <button class="btn" (click)="viewCustomerDetail()">查看详情</button>
-          <button class="btn" (click)="refreshContactInfo(currentCustomer)">刷新客户信息</button>
+          <button class="btn outline" (click)="switchToSelecting()">重选</button>
+          <button class="btn" (click)="viewCustomerDetail()">详情</button>
+          <button class="btn" (click)="refreshContactInfo(currentCustomer)">刷新</button>
         </div>
       </div>
     </div>
@@ -36,8 +35,7 @@
       <div class="list">
         <div class="item" *ngFor="let c of filteredCustomers" (click)="selectExistingCustomer(c)">
           <div class="thumb">
-            <img *ngIf="c.get('data')?.avatar" [src]="c.get('data')?.avatar" alt="" />
-            <div class="placeholder" *ngIf="!c.get('data')?.avatar">👤</div>
+            <img [src]="c.get('data')?.avatar || '/assets/images/default-avatar.svg'" alt="" />
           </div>
           <div class="detail">
             <div class="title">{{ c.get('name') || c.get('data')?.name }}</div>
@@ -51,7 +49,7 @@
       <div class="section-title">未建档的群聊外部联系人</div>
       <div class="list">
         <div class="item" *ngFor="let m of unbuiltExternalMembers">
-          <div class="thumb">👤</div>
+          <div class="thumb"><img src="/assets/images/default-avatar.svg" alt="" /></div>
           <div class="detail">
             <div class="title">{{ m.name || '外部客户' }}</div>
             <div class="sub">{{ m.userid }}</div>

+ 4 - 2
src/modules/project/pages/contact/contact.component.html

@@ -36,7 +36,7 @@
               @if (profile.basic.avatar) {
                 <img [src]="profile.basic.avatar" alt="头像" />
               } @else {
-                <svg class="icon-avatar" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M258.9 48C141.92 46.42 46.42 141.92 48 258.9c1.56 112.19 92.91 203.54 205.1 205.1 117 1.6 212.48-93.9 210.88-210.88C462.44 140.91 371.09 49.56 258.9 48zM385.32 375.25a4 4 0 01-6.14-.32 124.27 124.27 0 00-32.35-29.59C321.37 329 289.11 320 256 320s-65.37 9-90.83 25.34a124.24 124.24 0 00-32.35 29.58 4 4 0 01-6.14.32A175.32 175.32 0 0180 259c-1.63-97.31 78.22-178.76 175.57-179S432 158.81 432 256a175.32 175.32 0 01-46.68 119.25z"/><path d="M256 144c-19.72 0-37.55 7.39-50.22 20.82s-19 32-17.57 51.93C191.11 256 221.52 288 256 288s64.83-32 67.79-71.24c1.48-19.74-4.8-38.14-17.68-51.82C293.39 151.44 275.59 144 256 144z"/></svg>
+                <img src="/assets/images/default-avatar.svg" alt="头像" />
               }
             </div>
             <div class="customer-info">
@@ -248,7 +248,9 @@
                 @for (record of profile.followUpRecords; track $index) {
                   <div class="timeline-item">
                     <div class="timeline-dot">
-                      <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm-50.22 116.82C218.45 151.39 236.28 144 256 144s37.39 7.44 50.11 20.94c12.89 13.68 19.16 32.06 17.68 51.82C320.83 256 290.43 288 256 288s-64.89-32-67.79-71.25c-1.47-19.92 4.79-38.36 17.57-51.93zM256 432a175.49 175.49 0 01-126-53.22 122.91 122.91 0 0135.14-33.44C190.63 329 222.89 320 256 320s65.37 9 90.83 25.34A122.87 122.87 0 01382 378.78 175.45 175.45 0 01256 432z"/></svg>
+                      <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M256 176v160m80-80H176"/></svg>
+                    <span>{{ record.operator }}</span>
+                    <p>{{ record.content }}</p>
                     </div>
                     <div class="timeline-content">
                       <div class="timeline-time">{{ formatDate(record.time) }}</div>