| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- import { Component, OnInit, Input, Output, EventEmitter, HostBinding } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { Router, ActivatedRoute } from '@angular/router';
- import { IonicModule } from '@ionic/angular';
- import { WxworkSDK, WxworkCorp } from 'fmode-ng/core';
- import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
- import { WxworkSDKService } from '../../services/wxwork-sdk.service';
- import { ProfileService } from '../../../../app/services/profile.service';
- const Parse = FmodeParse.with('nova');
- /**
- * 客户画像组件
- *
- * 功能:
- * 1. 展示客户基础信息(权限控制:手机号仅客服/组长可见)
- * 2. 客户画像标签(风格偏好、预算、色彩氛围)
- * 3. 所在群聊列表(点击可跳转企微群聊)
- * 4. 历史项目列表
- * 5. 跟进记录时间线
- *
- * 路由:/wxwork/:cid/customer/:contactId
- */
- @Component({
- selector: 'app-contact',
- standalone: true,
- imports: [CommonModule, IonicModule],
- templateUrl: './contact.component.html',
- styleUrls: ['./contact.component.scss']
- })
- export class CustomerProfileComponent implements OnInit {
- // 输入参数(支持组件复用)
- @Input() customer: FmodeObject | null = null;
- @Input() currentUser: FmodeObject | null = null;
- // 新增:嵌入模式与项目过滤
- @Input() embeddedMode: boolean = false;
- @Input() projectIdFilter: string | null = null;
- @Output() close = new EventEmitter<void>();
- @HostBinding('class.embedded') get embeddedClass() { return this.embeddedMode; }
- // 路由参数
- cid: string = '';
- contactId: string = '';
- profileId: string = '';
- externalUserId: string = ''; // 从企微进入时的 external_userid
- // 企微SDK
- wxwork: WxworkSDK | null = null;
- wecorp: WxworkCorp | null = null;
- wxAuth: any = null; // WxworkAuth 实例
- // 加载状态
- loading: boolean = true;
- error: string | null = null;
- refreshing: boolean = false;
- // 客户数据
- contactInfo: FmodeObject | null = null;
- // 客户画像数据
- profile: {
- basic: {
- name: string;
- mobile: string;
- wechat: string;
- avatar: string;
- source: string;
- tags: string[];
- };
- preferences: {
- style: string[];
- budget: { min: number; max: number };
- colorAtmosphere: string;
- demandType: string;
- };
- groups: Array<{
- groupChat: FmodeObject;
- project: FmodeObject | null;
- }>;
- projects: FmodeObject[];
- followUpRecords: Array<{
- time: Date;
- type: string;
- content: string;
- operator: string;
- }>;
- } = {
- basic: {
- name: '',
- mobile: '',
- wechat: '',
- avatar: '',
- source: '',
- tags: []
- },
- preferences: {
- style: [],
- budget: { min: 0, max: 0 },
- colorAtmosphere: '',
- demandType: ''
- },
- groups: [],
- projects: [],
- followUpRecords: []
- };
- // 权限控制
- canViewSensitiveInfo: boolean = false;
- constructor(
- private router: Router,
- private route: ActivatedRoute,
- private wxworkService: WxworkSDKService,
- private profileService: ProfileService
- ) {}
- async ngOnInit() {
- // 获取路由参数
- this.cid = this.route.snapshot.paramMap.get('cid') || '';
- this.contactId = this.route.snapshot.paramMap.get('contactId') || '';
- this.profileId = this.route.snapshot.queryParamMap.get('profileId') || '';
- this.externalUserId = this.route.snapshot.queryParamMap.get('externalUserId') || '';
- // 如果有Input传入,直接使用
- if (this.customer) {
- this.contactInfo = this.customer;
- }
- // 初始化企微授权(不阻塞页面加载)
- this.initWxworkAuth();
- await this.loadData();
- }
- /**
- * 初始化企微授权(不阻塞页面)
- */
- async initWxworkAuth() {
- if (!this.cid) return;
- try {
- // 动态导入 WxworkAuth 避免导入错误
- const { WxworkAuth } = await import('fmode-ng/core');
- this.wxAuth = new WxworkAuth({ cid: this.cid, appId: 'crm' });
- // 静默授权并同步 Profile,不阻塞页面
- const { profile } = await this.wxAuth.authenticateAndLogin();
- if (profile) {
- this.profileService.setCurrentProfile(profile);
- }
- } catch (error) {
- console.warn('企微授权失败:', error);
- // 授权失败不影响页面加载,继续使用其他方式加载数据
- }
- }
- /**
- * 加载数据
- */
- async loadData() {
- try {
- this.loading = true;
- // 1. 初始化SDK(用于企微API调用,不需要等待授权)
- if (!this.wxwork && this.cid) {
- this.wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
- this.wecorp = new WxworkCorp(this.cid);
- }
- // 2. 获取当前用户(优先从全局服务获取)
- if (!this.currentUser) {
- // 优先级1: 使用 profileId 参数
- if (this.profileId) {
- this.currentUser = await this.profileService.getProfileById(this.profileId);
- }
- // 优先级2: 从全局服务获取当前 Profile
- if (!this.currentUser) {
- this.currentUser = await this.profileService.getCurrentProfile(this.cid);
- }
- // 优先级3: 企微环境下尝试从SDK获取
- if (!this.currentUser && this.wxwork) {
- try {
- this.currentUser = await this.wxwork.getCurrentUser();
- } catch (err) {
- console.warn('无法从企微SDK获取用户:', err);
- }
- }
- }
- // 检查权限
- const role = this.currentUser?.get('roleName');
- this.canViewSensitiveInfo = ['客服', '组长', '管理员'].includes(role);
- // 3. 加载客户信息
- if (!this.contactInfo) {
- if (this.contactId) {
- // 通过 contactId 加载(从后台进入)
- const query = new Parse.Query('ContactInfo');
- this.contactInfo = await query.get(this.contactId);
- } else if (this.externalUserId) {
- // 通过 external_userid 查找(从企微进入)
- const companyId = this.currentUser?.get('company')?.id;
- if (companyId) {
- const query = new Parse.Query('ContactInfo');
- query.equalTo('external_userid', this.externalUserId);
- query.equalTo('company', companyId);
- this.contactInfo = await query.first();
- if (!this.contactInfo) {
- throw new Error('未找到客户信息,请先在企微中添加该客户');
- }
- }
- }
- }
- // 4. 构建客户画像
- if (this.contactInfo) {
- await this.buildCustomerProfile();
- } else {
- throw new Error('无法加载客户信息');
- }
- } catch (err: any) {
- console.error('加载失败:', err);
- this.error = err.message || '加载失败';
- } finally {
- this.loading = false;
- }
- }
- /**
- * 构建客户画像
- */
- async buildCustomerProfile() {
- if (!this.contactInfo) return;
- const data = this.contactInfo.get('data') || {};
- // 基础信息
- this.profile.basic = {
- name: this.contactInfo.get('name') || '',
- mobile: this.canViewSensitiveInfo ? (this.contactInfo.get('mobile') || '') : '***',
- wechat: this.canViewSensitiveInfo ? (data.wechat || '') : '***',
- avatar: data.avatar || '',
- source: this.contactInfo.get('source') || '其他',
- tags: data.tags?.preferenceTags || []
- };
- // 客户画像
- this.profile.preferences = {
- style: data.tags?.preference ? [data.tags.preference] : [],
- budget: data.tags?.budget || { min: 0, max: 0 },
- colorAtmosphere: data.tags?.colorAtmosphere || '',
- demandType: data.demandType || ''
- };
- // 加载所在群聊
- await this.loadGroupChats();
- // 加载历史项目
- await this.loadProjects();
- // 加载跟进记录
- await this.loadFollowUpRecords();
- }
- /**
- * 加载所在群聊
- */
- async loadGroupChats() {
- try {
- // 查询包含该客户的群聊
- const query = new Parse.Query('GroupChat');
- query.include("project");
- query.equalTo('company', this.currentUser!.get('company'));
- query.notEqualTo('isDeleted', true);
- const groups = await query.find();
- // 过滤包含该客户的群聊
- const externalUserId = this.contactInfo!.get('external_userid');
- const filteredGroups = groups.filter((g: any) => {
- const memberList = g.get('member_list') || [];
- return memberList.some((m: any) =>
- m.type === 2 && m.userid === externalUserId
- );
- });
- // 加载群聊关联的项目
- this.profile.groups = await Promise.all(
- filteredGroups.map(async (groupChat: any) => {
- let project = groupChat.get('project');
- return { groupChat, project };
- })
- );
- } catch (err) {
- console.error('加载群聊失败:', err);
- }
- }
- /**
- * 加载历史项目
- */
- async loadProjects() {
- try {
- const query = new Parse.Query('Project');
- query.equalTo('customer', this.contactInfo!.toPointer());
- query.notEqualTo('isDeleted', true);
- query.descending('updatedAt');
- query.limit(10);
- this.profile.projects = await query.find();
- } catch (err) {
- console.error('加载项目失败:', err);
- }
- }
- /**
- * 加载跟进记录
- */
- async loadFollowUpRecords() {
- try {
- // 使用 ContactFollow 表,默认按项目过滤
- const query = new Parse.Query('ContactFollow');
- query.equalTo('contact', this.contactInfo!.toPointer());
- query.notEqualTo('isDeleted', true);
- if (this.projectIdFilter) {
- const project = new Parse.Object('Project');
- project.id = this.projectIdFilter;
- query.equalTo('project', project.toPointer());
- }
- query.descending('createdAt');
- query.limit(50);
- const records = await query.find();
- this.profile.followUpRecords = records.map((rec: any) => ({
- time: rec.get('createdAt'),
- type: rec.get('type') || 'message',
- content: rec.get('content') || '',
- operator: rec.get('sender')?.get('name') || '系统'
- }));
- // 若无 ContactFollow 记录,则兼容 data.follow_user
- if (this.profile.followUpRecords.length === 0) {
- const data = this.contactInfo!.get('data') || {};
- const followUsers = data.follow_user || [];
- this.profile.followUpRecords = followUsers.map((fu: any) => {
- // 处理操作员名称,避免显示企微userid
- let operatorName = '企微用户';
- if (fu.remark && fu.remark.length < 50) {
- operatorName = fu.remark;
- } else if (fu.name && fu.name.length < 50 && !fu.name.startsWith('woAs2q')) {
- operatorName = fu.name;
- }
-
- return {
- time: fu.createtime ? new Date(fu.createtime * 1000) : new Date(),
- type: 'follow',
- content: '添加客户',
- operator: operatorName
- };
- });
- }
- } catch (err) {
- console.error('加载跟进记录失败:', err);
- }
- }
- /**
- * 跳转到群聊
- */
- async navigateToGroupChat(chatId: string) {
- try {
- // 使用企微SDK服务跳转到群聊
- await this.wxworkService.openChat(chatId);
- } catch (error: any) {
- console.error('跳转群聊失败:', error);
- window?.fmode?.alert('跳转失败,请在企业微信中操作');
- }
- }
- /**
- * 跳转到项目详情
- */
- navigateToProject(project: FmodeObject) {
- this.router.navigate(['/wxwork', this.cid, 'project', project.id], {
- queryParams: {
- profileId: this.currentUser!.id
- }
- });
- }
- /**
- * 返回
- */
- goBack() {
- // 嵌入模式下不跳转,触发关闭
- if (this.embeddedMode) {
- this.close.emit();
- return;
- }
- this.router.navigate(['/wxwork', this.cid, 'project-loader']);
- }
- /**
- * 获取来源图标
- */
- getSourceIcon(source: string): string {
- const iconMap: any = {
- '朋友圈': 'logo-wechat',
- '信息流': 'newspaper-outline',
- '转介绍': 'people-outline',
- '其他': 'help-circle-outline'
- };
- return iconMap[source] || 'help-circle-outline';
- }
- /**
- * 获取项目状态类
- */
- getProjectStatusClass(status: string): string {
- const classMap: any = {
- '待分配': 'status-pending',
- '进行中': 'status-active',
- '已完成': 'status-completed',
- '已暂停': 'status-paused',
- '已取消': 'status-cancelled'
- };
- return classMap[status] || 'status-default';
- }
- /**
- * 获取跟进类型图标
- */
- getFollowUpIcon(type: string): string {
- const iconMap: any = {
- 'message': 'chatbubble-outline',
- 'call': 'call-outline',
- 'meeting': 'people-outline',
- 'email': 'mail-outline',
- 'follow': 'person-add-outline'
- };
- return iconMap[type] || 'ellipsis-horizontal-outline';
- }
- /**
- * 格式化日期
- */
- formatDate(date: Date): string {
- if (!date) return '';
- const d = new Date(date);
- const now = new Date();
- const diff = now.getTime() - d.getTime();
- const days = Math.floor(diff / (1000 * 60 * 60 * 24));
- if (days === 0) {
- return '今天';
- } else if (days === 1) {
- return '昨天';
- } else if (days < 7) {
- return `${days}天前`;
- } else {
- return `${d.getMonth() + 1}/${d.getDate()}`;
- }
- }
- /**
- * 格式化预算
- */
- formatBudget(budget: { min: number; max: number }): string {
- if (!budget || (!budget.min && !budget.max)) return '未设置';
- if (budget.min === budget.max) return `¥${budget.min}`;
- return `¥${budget.min} - ¥${budget.max}`;
- }
- /** 刷新客户数据(基于 external_userid 拉取企微数据并保存) */
- async refreshContactData() {
- try {
- if (!this.contactInfo) return;
- const externalUserId = this.contactInfo.get('external_userid');
- const companyId = this.currentUser?.get('company')?.id || this.contactInfo.get('company')?.id || localStorage.getItem('company');
- if (!externalUserId || !companyId) {
- window?.fmode?.alert('无法刷新:缺少企业或external_userid');
- return;
- }
- this.refreshing = true;
- 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) this.contactInfo.set('name', ext.name);
- const prev = this.contactInfo.get('data') || {};
- const mapped = {
- ...prev,
- external_contact: ext,
- follow_user: follow,
- name: ext.name,
- avatar: ext.avatar,
- gender: ext.gender,
- type: ext.type
- } as any;
- this.contactInfo.set('data', mapped);
- await this.contactInfo.save();
- await this.buildCustomerProfile();
- } catch (e) {
- console.warn('刷新客户数据失败:', e);
- window?.fmode?.alert('刷新失败,请稍后重试');
- } finally {
- this.refreshing = false;
- }
- }
- }
|