فهرست منبع

feat: contact selector

Future 2 روز پیش
والد
کامیت
59809fe390

+ 73 - 0
src/modules/project/components/contact-selector/contact-selector.component.html

@@ -0,0 +1,73 @@
+<div class="contact-selector" [class.disabled]="disabled">
+  <div class="loading" *ngIf="loading">正在加载客户数据...</div>
+
+  <!-- 已有客户卡片 -->
+  <div class="customer-exists" *ngIf="currentCustomer">
+    <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>
+        </div>
+        <div class="info" (click)="viewCustomerDetail()">
+          <div class="name">{{ currentCustomer.get('name') || currentCustomer.get('data')?.external_contact?.name || currentCustomer.get('data')?.name }}</div>
+          <div class="meta">
+            <span class="chip" *ngIf="currentCustomer.get('data')?.external_contact?.type">{{ currentCustomer.get('data')?.type === 1 ? '外部联系人' : '企业成员' }}</span>
+            <span class="chip" *ngIf="canViewSensitiveInfo && currentCustomer.get('mobile')">{{ currentCustomer.get('mobile') }}</span>
+          </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>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 选择客户列表 -->
+  <div class="selecting" *ngIf="!currentCustomer">
+    <div class="toolbar">
+      <input class="search" type="text" [(ngModel)]="searchKeyword" [placeholder]="placeholder" />
+    </div>
+
+    <div class="section">
+      <div class="section-title">已建档的群聊客户</div>
+      <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>
+          </div>
+          <div class="detail">
+            <div class="title">{{ c.get('name') || c.get('data')?.name }}</div>
+            <div class="sub" *ngIf="canViewSensitiveInfo && c.get('mobile')">{{ c.get('mobile') }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="section" *ngIf="showCreateButton">
+      <div class="section-title">未建档的群聊外部联系人</div>
+      <div class="list">
+        <div class="item" *ngFor="let m of unbuiltExternalMembers">
+          <div class="thumb">👤</div>
+          <div class="detail">
+            <div class="title">{{ m.name || '外部客户' }}</div>
+            <div class="sub">{{ m.userid }}</div>
+          </div>
+          <div class="ops">
+            <button class="btn primary" (click)="createFromMember(m.userid)">创建并关联</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 客户详情侧栏弹层 -->
+  <div class="overlay" *ngIf="showCustomerPanel" (click)="closeCustomerDetail()"></div>
+  <div class="customer-panel" *ngIf="showCustomerPanel">
+    <app-contact [customer]="currentCustomer" [currentUser]="currentUser" (close)="closeCustomerDetail()"></app-contact>
+    <button class="close" (click)="closeCustomerDetail()">返回</button>
+  </div>
+</div>

+ 30 - 0
src/modules/project/components/contact-selector/contact-selector.component.scss

@@ -0,0 +1,30 @@
+.contact-selector { padding: 8px 0; }
+.loading { padding: 8px; color: #666; }
+.card { background: #fff; border: 1px solid #eee; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
+.row { display:flex; align-items:center; gap:12px; }
+.avatar { width:48px; height:48px; border-radius:50%; overflow:hidden; background:#f2f2f2; display:flex; align-items:center; justify-content:center; }
+.avatar img { width:100%; height:100%; object-fit:cover; }
+.info { flex:1; min-width:0; }
+.name { font-weight:600; font-size:15px; }
+.meta { margin-top:4px; color:#666; display:flex; gap:6px; flex-wrap:wrap; }
+.chip { background:#f3f6ff; color:#2b4eff; border-radius:10px; padding:2px 8px; font-size:12px; }
+.actions { display:flex; gap:8px; }
+.btn { padding:6px 10px; border:1px solid #ccc; border-radius:6px; cursor:pointer; background:#fff; }
+.btn.primary { background:#2b4eff; color:#fff; border-color:#2b4eff; }
+.btn.outline { background:#fff; }
+.toolbar { margin:8px 0; }
+.search { width:100%; padding:8px; border:1px solid #ddd; border-radius:6px; }
+.section { margin-top:12px; }
+.section-title { font-size:13px; color:#555; margin-bottom:6px; }
+.list { display:flex; flex-direction:column; gap:8px; }
+.item { display:flex; align-items:center; padding:8px; border:1px solid #eee; border-radius:8px; background:#fff; }
+.item:hover { background:#fafafa; }
+.thumb { width:36px; height:36px; border-radius:50%; overflow:hidden; background:#f2f2f2; display:flex; align-items:center; justify-content:center; }
+.thumb img { width:100%; height:100%; object-fit:cover; }
+.detail { flex:1; min-width:0; margin-left:10px; }
+.title { font-size:14px; font-weight:500; }
+.sub { font-size:12px; color:#777; }
+.ops { display:flex; align-items:center; }
+.overlay { position:fixed; inset:0; background:rgba(0,0,0,0.2); }
+.customer-panel { position:fixed; right:20px; top:60px; width:480px; height:80vh; background:#fff; border:1px solid #ddd; border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,0.1); overflow:auto; padding:8px; }
+.customer-panel .close { position:absolute; right:12px; top:10px; padding:6px 10px; }

+ 195 - 0
src/modules/project/components/contact-selector/contact-selector.component.ts

@@ -0,0 +1,195 @@
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { IonicModule } from '@ionic/angular';
+import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
+import { WxworkCorp } from 'fmode-ng/core';
+import { CustomerProfileComponent } from '../../pages/contact/contact.component';
+
+const Parse: any = FmodeParse.with('nova');
+
+@Component({
+  selector: 'app-contact-selector',
+  standalone: true,
+  imports: [CommonModule, FormsModule, IonicModule, CustomerProfileComponent],
+  templateUrl: './contact-selector.component.html',
+  styleUrls: ['./contact-selector.component.scss']
+})
+export class CustomerSelectorComponent implements OnInit {
+  @Input() project: FmodeObject | null = null;
+  @Input() groupChat: FmodeObject | null = null;
+  @Input() currentUser: FmodeObject | null = null;
+  @Input() placeholder: string = '请选择项目客户';
+  @Input() disabled: boolean = false;
+  @Input() showCreateButton: boolean = true;
+  @Output() contactSelected = new EventEmitter<{ contact: FmodeObject; isNewCustomer: boolean; action: 'selected' | 'created' | 'updated' }>();
+
+  loading: boolean = false;
+  searchKeyword: string = '';
+  currentCustomer: FmodeObject | null = null;
+  availableCustomers: FmodeObject[] = [];
+  externalMembers: Array<{ userid: string; name?: string }> = [];
+  unbuiltExternalMembers: Array<{ userid: string; name?: string }> = [];
+  showCustomerPanel: boolean = false;
+
+  get canViewSensitiveInfo(): boolean {
+    const role = this.currentUser?.get?.('roleName') || '';
+    return ['客服', '组长', '管理员'].includes(role);
+  }
+
+  async ngOnInit() {
+    await this.init();
+  }
+
+  private async init() {
+    if (!this.project || !this.groupChat) return;
+    try {
+      this.loading = true;
+      await this.checkProjectCustomer();
+      await this.loadExternalMembers();
+      await this.loadAvailableCustomers();
+      this.computeUnbuiltMembers();
+    } finally {
+      this.loading = false;
+    }
+  }
+
+  private async checkProjectCustomer() {
+    const ptr = this.project!.get('contact');
+    if (!ptr) { this.currentCustomer = null; return; }
+    try {
+      if (ptr.id && (ptr as any).get) {
+        this.currentCustomer = ptr as any;
+      } else if (ptr.id) {
+        const query = new Parse.Query('ContactInfo');
+        this.currentCustomer = await query.get(ptr.id);
+      }
+    } catch {
+      this.currentCustomer = null;
+    }
+  }
+
+  private async loadExternalMembers() {
+    const list = this.groupChat!.get('member_list') || [];
+    const external = Array.isArray(list) ? list.filter((m: any) => m && m.type === 2) : [];
+    this.externalMembers = external.map((m: any) => ({ userid: m.userid, name: m.name }));
+  }
+
+  private async loadAvailableCustomers() {
+    const companyId = this.project!.get('company')?.id || localStorage.getItem('company');
+    if (!companyId) return;
+    const extIds = this.externalMembers.map(m => m.userid);
+    if (extIds.length === 0) { this.availableCustomers = []; return; }
+    const query = new Parse.Query('ContactInfo');
+    query.equalTo('company', companyId);
+    query.containedIn('external_userid', extIds);
+    query.notEqualTo('isDeleted', true);
+    this.availableCustomers = await query.find();
+  }
+
+  private computeUnbuiltMembers() {
+    const builtIds = new Set<string>(
+      this.availableCustomers.map((c: any) => c.get('external_userid')).filter(Boolean)
+    );
+    this.unbuiltExternalMembers = this.externalMembers.filter(m => !builtIds.has(m.userid));
+  }
+
+  private getMemberInfo(userid: string): any {
+    const list = this.groupChat!.get('member_list') || [];
+    return (list || []).find((m: any) => m && m.userid === userid) || null;
+  }
+
+  get filteredCustomers(): FmodeObject[] {
+    const kw = (this.searchKeyword || '').trim().toLowerCase();
+    const base = this.availableCustomers;
+    if (!kw) return base;
+    return base.filter(c => {
+      const name = (c.get('name') || c.get('data')?.name || '').toLowerCase();
+      const mobile = (c.get('mobile') || '').toLowerCase();
+      return name.includes(kw) || mobile.includes(kw);
+    });
+  }
+
+  async selectExistingCustomer(contact: FmodeObject) {
+    if (this.disabled || !this.project) return;
+    const nameMissing = !contact.get('name') && !contact.get('data')?.name && !contact.get('data')?.external_contact?.name;
+    const extid = contact.get('external_userid');
+    if (nameMissing && extid) {
+      await this.refreshContactInfo(contact);
+    }
+    this.project.set('contact', contact.toPointer());
+    await this.project.save();
+    this.currentCustomer = contact;
+    this.contactSelected.emit({ contact, isNewCustomer: false, action: 'selected' });
+  }
+
+  switchToSelecting() {
+    this.currentCustomer = null;
+    this.searchKeyword = '';
+  }
+
+  async createFromMember(memberUserid: string) {
+    if (this.disabled || !this.project) return;
+    const companyId = this.project.get('company')?.id || localStorage.getItem('company');
+    if (!companyId) throw new Error('无法获取企业信息');
+    const query = new Parse.Query('ContactInfo');
+    query.equalTo('external_userid', memberUserid);
+    query.equalTo('company', companyId);
+    let contactInfo = await query.first();
+    if (!contactInfo) {
+      const corp = new WxworkCorp(companyId);
+      const extData = await corp.externalContact.get(memberUserid);
+      const ext = (extData && extData.external_contact) ? extData.external_contact : {};
+      const follow = (extData && extData.follow_user) ? extData.follow_user : [];
+      const ContactInfo = Parse.Object.extend('ContactInfo');
+      contactInfo = new ContactInfo();
+      contactInfo.set('name', ext.name || this.getMemberInfo(memberUserid)?.name || '客户');
+      contactInfo.set('external_userid', memberUserid);
+      const company = new Parse.Object('Company');
+      company.id = companyId;
+      contactInfo.set('company', company.toPointer());
+      const mapped = {
+        external_contact: ext,
+        follow_user: follow,
+        member: this.getMemberInfo(memberUserid),
+        name: ext.name || this.getMemberInfo(memberUserid)?.name,
+        avatar: ext.avatar,
+        gender: ext.gender,
+        type: ext.type
+      } as any;
+      contactInfo.set('data', mapped);
+      contactInfo = await contactInfo.save();
+      await this.loadAvailableCustomers();
+      this.computeUnbuiltMembers();
+    }
+    this.project.set('contact', contactInfo.toPointer());
+    await this.project.save();
+    this.currentCustomer = contactInfo;
+    this.contactSelected.emit({ contact: contactInfo, isNewCustomer: true, action: 'created' });
+  }
+
+  async refreshContactInfo(contact: any) {
+    const externalUserId = contact.get('external_userid');
+    const companyId = this.project?.get('company')?.id || localStorage.getItem('company');
+    if (!externalUserId || !companyId) return;
+    const corp = new WxworkCorp(companyId);
+    const extData = await corp.externalContact.get(externalUserId);
+    const ext = (extData && extData.external_contact) ? extData.external_contact : {};
+    const follow = (extData && extData.follow_user) ? extData.follow_user : [];
+    if (ext.name) contact.set('name', ext.name);
+    const mapped = {
+      external_contact: ext,
+      follow_user: follow,
+      member: this.getMemberInfo(externalUserId),
+      name: ext.name || this.getMemberInfo(externalUserId)?.name,
+      avatar: ext.avatar,
+      gender: ext.gender,
+      type: ext.type
+    } as any;
+    contact.set('data', mapped);
+    await contact.save();
+  }
+
+  viewCustomerDetail() { this.showCustomerPanel = true; }
+  closeCustomerDetail() { this.showCustomerPanel = false; }
+}

+ 3 - 22
src/modules/project/pages/contact/contact.component.ts

@@ -1,4 +1,4 @@
-import { Component, OnInit, Input, Output, EventEmitter, HostBinding } from '@angular/core';
+import { Component, OnInit, Input } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { Router, ActivatedRoute } from '@angular/router';
 import { IonicModule } from '@ionic/angular';
@@ -32,10 +32,6 @@ export class CustomerProfileComponent implements OnInit {
   // 输入参数(支持组件复用)
   @Input() customer: FmodeObject | null = null;
   @Input() currentUser: FmodeObject | null = null;
-  // 新增:嵌入模式与关闭事件
-  @Input() embeddedMode: boolean = false;
-  @Output() close = new EventEmitter<void>();
-  @HostBinding('class.embedded') get embeddedClass() { return this.embeddedMode; }
 
   // 路由参数
   cid: string = '';
@@ -271,6 +267,7 @@ export class CustomerProfileComponent implements OnInit {
     try {
       // 查询包含该客户的群聊
       const query = new Parse.Query('GroupChat');
+      query.include("project");
       query.equalTo('company', this.currentUser!.get('company'));
       query.notEqualTo('isDeleted', true);
 
@@ -288,18 +285,7 @@ export class CustomerProfileComponent implements OnInit {
       // 加载群聊关联的项目
       this.profile.groups = await Promise.all(
         filteredGroups.map(async (groupChat: any) => {
-          const projectPointer = groupChat.get('project');
-          let project = null;
-
-          if (projectPointer) {
-            try {
-              const pQuery = new Parse.Query('Project');
-              project = await pQuery.get(projectPointer.id);
-            } catch (err) {
-              console.error('加载项目失败:', err);
-            }
-          }
-
+          let project =  groupChat.get('project');
           return { groupChat, project };
         })
       );
@@ -390,11 +376,6 @@ export class CustomerProfileComponent implements OnInit {
    * 返回
    */
   goBack() {
-    // 嵌入模式下,触发关闭事件而非跳转
-    if (this.embeddedMode) {
-      this.close.emit();
-      return;
-    }
     this.router.navigate(['/wxwork', this.cid, 'project-loader']);
   }
 

+ 1 - 1
src/modules/project/pages/project-detail/project-detail.component.html

@@ -51,7 +51,7 @@
 
   <!-- 项目详情内容 -->
   @if (!loading && !error && project) {
-    <!-- 客户选择组件(替换原 contact-quick-view) -->
+    <!-- 客户选择组件(剥离为外部组件) -->
     <app-contact-selector
       [project]="project"
       [groupChat]="groupChat"

+ 1 - 228
src/modules/project/pages/project-detail/project-detail.component.ts

@@ -12,234 +12,7 @@ import { ProjectIssuesModalComponent } from '../../components/project-issues-mod
 import { ProjectIssueService } from '../../services/project-issue.service';
 import { CustomerProfileComponent } from '../contact/contact.component';
 import { FormsModule } from '@angular/forms';
-
-// Customer selector component declared before ProjectDetailComponent
-@Component({
-  selector: 'app-contact-selector',
-  standalone: true,
-  imports: [CommonModule, FormsModule, IonicModule, CustomerProfileComponent],
-  template: `
-    <div class="contact-selector" [class.disabled]="disabled">
-      <div class="loading" *ngIf="loading">正在加载客户数据...</div>
-
-      <!-- 已有客户卡片 -->
-      <div class="customer-exists" *ngIf="currentCustomer">
-        <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>
-            </div>
-            <div class="info" (click)="viewCustomerDetail()">
-              <div class="name">{{ currentCustomer.get('name') || currentCustomer.get('data')?.external_contact?.name || currentCustomer.get('data')?.name }}</div>
-              <div class="meta">
-                <span class="chip" *ngIf="currentCustomer.get('data')?.external_contact?.type">{{ currentCustomer.get('data')?.external_contact?.type === 2 ? '外部联系人' : '企业成员' }}</span>
-                <span class="chip" *ngIf="canViewSensitiveInfo && currentCustomer.get('mobile')">{{ currentCustomer.get('mobile') }}</span>
-              </div>
-            </div>
-            <div class="actions">
-              <button class="btn outline" (click)="switchToSelecting()">重新选择</button>
-              <button class="btn" (click)="viewCustomerDetail()">查看详情</button>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 选择客户列表 -->
-      <div class="selecting" *ngIf="!currentCustomer">
-        <div class="toolbar">
-          <input class="search" type="text" [(ngModel)]="searchKeyword" [placeholder]="placeholder" />
-        </div>
-
-        <div class="section">
-          <div class="section-title">已建档的群聊客户</div>
-          <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>
-              </div>
-              <div class="detail">
-                <div class="title">{{ c.get('name') }}</div>
-                <div class="sub" *ngIf="canViewSensitiveInfo && c.get('mobile')">{{ c.get('mobile') }}</div>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <div class="section" *ngIf="showCreateButton">
-          <div class="section-title">未建档的群聊外部联系人</div>
-          <div class="list">
-            <div class="item" *ngFor="let m of externalMembers">
-              <div class="thumb">👤</div>
-              <div class="detail">
-                <div class="title">{{ m.name || '外部客户' }}</div>
-                <div class="sub">{{ m.userid }}</div>
-              </div>
-              <div class="ops">
-                <button class="btn primary" (click)="createFromMember(m.userid)">创建并关联</button>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 客户详情侧栏弹层 -->
-      <div class="overlay" *ngIf="showCustomerPanel" (click)="closeCustomerDetail()"></div>
-      <div class="customer-panel" *ngIf="showCustomerPanel">
-        <app-contact [customer]="currentCustomer" [currentUser]="currentUser" [embeddedMode]="true" (close)="closeCustomerDetail()"></app-contact>
-        <button class="close" (click)="closeCustomerDetail()">返回</button>
-      </div>
-    </div>
-  `,
-  styles: [
-    `
-      .contact-selector { padding: 8px 0; }
-      .loading { padding: 8px; color: #666; }
-      .card { background: #fff; border: 1px solid #eee; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
-      .row { display:flex; align-items:center; gap:12px; }
-      .avatar { width:48px; height:48px; border-radius:50%; overflow:hidden; background:#f2f2f2; display:flex; align-items:center; justify-content:center; }
-      .avatar img { width:100%; height:100%; object-fit:cover; }
-      .info { flex:1; min-width:0; }
-      .name { font-weight:600; font-size:15px; }
-      .meta { margin-top:4px; color:#666; display:flex; gap:6px; flex-wrap:wrap; }
-      .chip { background:#f3f6ff; color:#2b4eff; border-radius:10px; padding:2px 8px; font-size:12px; }
-      .actions { display:flex; gap:8px; }
-      .btn { padding:6px 10px; border:1px solid #ccc; border-radius:6px; cursor:pointer; background:#fff; }
-      .btn.primary { background:#2b4eff; color:#fff; border-color:#2b4eff; }
-      .btn.outline { background:#fff; }
-      .toolbar { margin:8px 0; }
-      .search { width:100%; padding:8px; border:1px solid #ddd; border-radius:6px; }
-      .section { margin-top:12px; }
-      .section-title { font-size:13px; color:#555; margin-bottom:6px; }
-      .list { display:flex; flex-direction:column; gap:8px; }
-      .item { display:flex; align-items:center; padding:8px; border:1px solid #eee; border-radius:8px; background:#fff; }
-      .item:hover { background:#fafafa; }
-      .thumb { width:36px; height:36px; border-radius:50%; overflow:hidden; background:#f2f2f2; display:flex; align-items:center; justify-content:center; }
-      .thumb img { width:100%; height:100%; object-fit:cover; }
-      .detail { flex:1; min-width:0; margin-left:10px; }
-      .title { font-size:14px; font-weight:500; }
-      .sub { font-size:12px; color:#777; }
-      .ops { display:flex; align-items:center; }
-      .overlay { position:fixed; inset:0; background:rgba(0,0,0,0.2); }
-      .customer-panel { position:fixed; right:20px; top:60px; width:480px; height:80vh; background:#fff; border:1px solid #ddd; border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,0.1); overflow:auto; padding:8px; }
-      .customer-panel .close { position:absolute; right:12px; top:10px; padding:6px 10px; }
-    `
-  ]
-})
-export class CustomerSelectorComponent implements OnInit {
-  @Input() project: FmodeObject | null = null;
-  @Input() groupChat: FmodeObject | null = null;
-  @Input() currentUser: FmodeObject | null = null;
-  @Input() placeholder: string = '请选择项目客户';
-  @Input() disabled: boolean = false;
-  @Input() showCreateButton: boolean = true;
-  @Output() contactSelected = new EventEmitter<{ contact: FmodeObject; isNewCustomer: boolean; action: 'selected' | 'created' | 'updated' }>();
-
-  loading: boolean = false;
-  searchKeyword: string = '';
-  currentCustomer: FmodeObject | null = null;
-  availableCustomers: FmodeObject[] = [];
-  externalMembers: Array<{ userid: string; name?: string }> = [];
-  showCustomerPanel: boolean = false;
-
-  get canViewSensitiveInfo(): boolean {
-    const role = this.currentUser?.get?.('roleName') || '';
-    return ['客服', '组长', '管理员'].includes(role);
-  }
-  async ngOnInit() { await this.init(); }
-  private async init() {
-    if (!this.project || !this.groupChat) return;
-    try {
-      this.loading = true;
-      await this.checkProjectCustomer();
-      await this.loadExternalMembers();
-      await this.loadAvailableCustomers();
-    } finally { this.loading = false; }
-  }
-  private async checkProjectCustomer() {
-    const ptr = this.project!.get('contact');
-    if (!ptr) { this.currentCustomer = null; return; }
-    try {
-      if (ptr.id && (ptr as any).get) { this.currentCustomer = ptr as any; }
-      else if (ptr.id) { const query = new (FmodeParse.with('nova') as any).Query('ContactInfo'); this.currentCustomer = await query.get(ptr.id); }
-    } catch { this.currentCustomer = null; }
-  }
-  private async loadExternalMembers() {
-    const list = this.groupChat!.get('member_list') || [];
-    const external = Array.isArray(list) ? list.filter((m: any) => m && m.type === 2) : [];
-    this.externalMembers = external.map((m: any) => ({ userid: m.userid, name: m.name }));
-  }
-  private async loadAvailableCustomers() {
-    const companyId = this.project!.get('company')?.id || localStorage.getItem('company');
-    if (!companyId) return;
-    const extIds = this.externalMembers.map(m => m.userid);
-    if (extIds.length === 0) { this.availableCustomers = []; return; }
-    const query = new (FmodeParse.with('nova') as any).Query('ContactInfo');
-    query.equalTo('company', companyId);
-    query.containedIn('external_userid', extIds);
-    query.notEqualTo('isDeleted', true);
-    this.availableCustomers = await query.find();
-  }
-  get filteredCustomers(): FmodeObject[] {
-    const kw = (this.searchKeyword || '').trim().toLowerCase();
-    if (!kw) return this.availableCustomers;
-    return this.availableCustomers.filter(c => {
-      const name = (c.get('name') || '').toLowerCase();
-      const mobile = (c.get('mobile') || '').toLowerCase();
-      return name.includes(kw) || mobile.includes(kw);
-    });
-  }
-  async selectExistingCustomer(contact: FmodeObject) {
-    if (this.disabled || !this.project) return;
-    this.project.set('contact', contact.toPointer());
-    await this.project.save();
-    this.currentCustomer = contact;
-    this.contactSelected.emit({ contact, isNewCustomer: false, action: 'selected' });
-  }
-  switchToSelecting() { this.currentCustomer = null; this.searchKeyword = ''; }
-  async createFromMember(memberUserid: string) {
-    if (this.disabled || !this.project) return;
-    const companyId = this.project.get('company')?.id || localStorage.getItem('company');
-    if (!companyId) throw new Error('无法获取企业信息');
-    const query = new (FmodeParse.with('nova') as any).Query('ContactInfo');
-    query.equalTo('external_userid', memberUserid);
-    query.equalTo('company', companyId);
-    let contactInfo = await query.first();
-    if (!contactInfo) {
-      const corp = new WxworkCorp(companyId);
-      const extData = await corp.externalContact.get(memberUserid);
-      const ext = (extData && extData.external_contact) ? extData.external_contact : {};
-      const follow = (extData && extData.follow_user) ? extData.follow_user : [];
-      const ContactInfo = (FmodeParse.with('nova') as any).Object.extend('ContactInfo');
-      contactInfo = new ContactInfo();
-      // 顶部字段
-      contactInfo.set('name', ext.name || '客户');
-      contactInfo.set('external_userid', memberUserid);
-      // 公司指针
-      const company = new (FmodeParse.with('nova') as any).Object('Company');
-      company.id = companyId;
-      contactInfo.set('company', company.toPointer());
-      // data 映射:嵌入 external_contact 与 follow_user,并兼容常用扁平字段
-      const mapped = {
-        external_contact: ext,
-        follow_user: follow,
-        name: ext.name,
-        avatar: ext.avatar,
-        gender: ext.gender,
-        type: ext.type
-      } as any;
-      contactInfo.set('data', mapped);
-      contactInfo = await contactInfo.save();
-    }
-    this.project.set('contact', contactInfo.toPointer());
-    await this.project.save();
-    this.currentCustomer = contactInfo;
-    this.contactSelected.emit({ contact: contactInfo, isNewCustomer: true, action: 'created' });
-  }
-  viewCustomerDetail() { this.showCustomerPanel = true; }
-  closeCustomerDetail() { this.showCustomerPanel = false; }
-}
+import { CustomerSelectorComponent } from '../../components/contact-selector/contact-selector.component';
 
 const Parse = FmodeParse.with('nova');