project-loader.component.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import { Component, OnInit } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { Router, ActivatedRoute } from '@angular/router';
  4. import { FormsModule } from '@angular/forms';
  5. import { IonicModule } from '@ionic/angular';
  6. import { WxworkSDK, WxworkCorp, WxworkCurrentChat } from 'fmode-ng/core';
  7. import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
  8. import { wxdebug } from 'fmode-ng';
  9. import { addIcons } from 'ionicons';
  10. import {
  11. rocketOutline,
  12. addCircleOutline,
  13. timeOutline,
  14. personCircleOutline,
  15. alertCircleOutline,
  16. refreshOutline,
  17. chevronForwardOutline
  18. } from 'ionicons/icons';
  19. const Parse = FmodeParse.with('nova');
  20. /**
  21. * 项目预加载页面
  22. *
  23. * 功能:
  24. * 1. 从企微会话获取上下文(群聊或联系人)
  25. * 2. 获取当前登录用户(Profile)
  26. * 3. 根据场景跳转到对应页面
  27. * - 群聊 → 项目详情 或 创建项目引导
  28. * - 联系人 → 客户画像
  29. *
  30. * 路由:/wxwork/:cid/project-loader
  31. *
  32. * 参考实现:nova-admin/projects/nova-crm/src/modules/chat/page-chat-context
  33. */
  34. @Component({
  35. selector: 'app-project-loader',
  36. standalone: true,
  37. imports: [CommonModule, FormsModule, IonicModule],
  38. templateUrl: './project-loader.component.html',
  39. styleUrls: ['./project-loader.component.scss']
  40. })
  41. export class ProjectLoaderComponent implements OnInit {
  42. // 基础数据
  43. cid: string = '';
  44. appId: string = 'crm';
  45. // 加载状态
  46. loading: boolean = true;
  47. loadingMessage: string = '正在加载...';
  48. error: string | null = null;
  49. // 企微SDK
  50. wxwork: WxworkSDK | null = null;
  51. wecorp: WxworkCorp | null = null;
  52. // 上下文数据
  53. currentUser: FmodeObject | null = null; // Profile 或 UserSocial
  54. currentChat: WxworkCurrentChat | null = null;
  55. chatType: 'group' | 'contact' | 'none' = 'none';
  56. groupChat: FmodeObject | null = null; // GroupChat
  57. contact: FmodeObject | null = null; // ContactInfo
  58. project: FmodeObject | null = null; // Project
  59. // 创建项目引导
  60. showCreateGuide: boolean = false;
  61. defaultProjectName: string = '';
  62. projectName: string = '';
  63. creating: boolean = false;
  64. // 历史项目(当前群聊无项目时展示)
  65. historyProjects: FmodeObject[] = [];
  66. constructor(
  67. private router: Router,
  68. private route: ActivatedRoute
  69. ) {
  70. addIcons({
  71. rocketOutline,
  72. addCircleOutline,
  73. timeOutline,
  74. personCircleOutline,
  75. alertCircleOutline,
  76. refreshOutline,
  77. chevronForwardOutline
  78. });
  79. }
  80. async ngOnInit() {
  81. // 获取路由参数
  82. this.route.paramMap.subscribe(async params => {
  83. this.cid = params.get('cid') || '';
  84. this.appId = params.get('appId') || 'crm';
  85. if (!this.cid) {
  86. this.error = '缺少企业ID参数';
  87. this.loading = false;
  88. return;
  89. }
  90. await this.loadData();
  91. });
  92. }
  93. /**
  94. * 加载数据主流程(参考 page-chat-context 实现)
  95. */
  96. async loadData() {
  97. try {
  98. this.loading = true;
  99. this.loadingMessage = '初始化企微SDK...';
  100. // 1️⃣ 初始化 SDK
  101. this.wxwork = new WxworkSDK({ cid: this.cid, appId: this.appId });
  102. this.wecorp = new WxworkCorp(this.cid);
  103. wxdebug('1. SDK初始化完成', { cid: this.cid, appId: this.appId });
  104. // 2️⃣ 加载当前登录员工信息(由 WxworkAuthGuard 自动登录)
  105. this.loadingMessage = '获取用户信息...';
  106. try {
  107. this.currentUser = await this.wxwork.getCurrentUser();
  108. wxdebug('2. 获取当前用户成功', this.currentUser?.toJSON());
  109. } catch (err) {
  110. console.error('获取当前用户失败:', err);
  111. wxdebug('2. 获取当前用户失败', err);
  112. throw new Error('获取用户信息失败,请重试');
  113. }
  114. // 3️⃣ 加载当前聊天上下文
  115. this.loadingMessage = '获取会话信息...';
  116. try {
  117. this.currentChat = await this.wxwork.getCurrentChat();
  118. wxdebug('3. getCurrentChat返回', this.currentChat);
  119. } catch (err) {
  120. console.error('getCurrentChat失败:', err);
  121. wxdebug('3. getCurrentChat失败', err);
  122. }
  123. // 4️⃣ 根据场景同步数据
  124. if (this.currentChat?.type === "chatId" && this.currentChat?.group) {
  125. // 群聊场景
  126. wxdebug('4. 检测到群聊场景', this.currentChat.group);
  127. this.loadingMessage = '同步群聊信息...';
  128. try {
  129. this.chatType = 'group';
  130. this.groupChat = await this.wxwork.syncGroupChat(this.currentChat.group);
  131. wxdebug('5. 群聊同步完成', this.groupChat?.toJSON());
  132. // 处理群聊场景
  133. await this.handleGroupChatScene();
  134. } catch (err) {
  135. console.error('群聊同步失败:', err);
  136. wxdebug('5. 群聊同步失败', err);
  137. throw new Error('群聊信息同步失败');
  138. }
  139. } else if (this.currentChat?.type === "userId" && this.currentChat?.id) {
  140. // 联系人场景
  141. wxdebug('4. 检测到联系人场景', { id: this.currentChat.id });
  142. this.loadingMessage = '同步联系人信息...';
  143. try {
  144. this.chatType = 'contact';
  145. // 获取完整联系人信息
  146. const contactInfo = await this.wecorp!.externalContact.get(this.currentChat.id);
  147. wxdebug('5. 获取完整联系人信息', contactInfo);
  148. this.contact = await this.wxwork.syncContact(contactInfo);
  149. wxdebug('6. 联系人同步完成', this.contact?.toJSON());
  150. // 处理联系人场景
  151. await this.handleContactScene();
  152. } catch (err) {
  153. console.error('联系人同步失败:', err);
  154. wxdebug('联系人同步失败', err);
  155. throw new Error('联系人信息同步失败');
  156. }
  157. } else {
  158. // 未检测到有效场景
  159. wxdebug('4. 未检测到有效场景', {
  160. currentChat: this.currentChat,
  161. type: this.currentChat?.type,
  162. hasGroup: !!this.currentChat?.group,
  163. hasContact: !!this.currentChat?.contact,
  164. hasId: !!this.currentChat?.id
  165. });
  166. throw new Error('无法识别当前会话类型,请在群聊或联系人会话中打开');
  167. }
  168. wxdebug('加载完成', {
  169. chatType: this.chatType,
  170. hasGroupChat: !!this.groupChat,
  171. hasContact: !!this.contact,
  172. hasCurrentUser: !!this.currentUser
  173. });
  174. } catch (err: any) {
  175. console.error('加载失败:', err);
  176. this.error = err.message || '加载失败,请重试';
  177. } finally {
  178. this.loading = false;
  179. }
  180. }
  181. /**
  182. * 处理群聊场景
  183. */
  184. async handleGroupChatScene() {
  185. this.loadingMessage = '查询项目信息...';
  186. // 查询群聊关联的项目
  187. const projectPointer = this.groupChat!.get('project');
  188. if (projectPointer) {
  189. // 有项目,加载项目详情
  190. try {
  191. const query = new Parse.Query('Project');
  192. query.include('customer', 'assignee');
  193. this.project = await query.get(projectPointer.id);
  194. wxdebug('找到项目', this.project.toJSON());
  195. // 跳转项目详情
  196. await this.navigateToProjectDetail();
  197. } catch (err) {
  198. console.error('加载项目失败:', err);
  199. wxdebug('加载项目失败', err);
  200. this.error = '项目已删除或无权访问';
  201. }
  202. } else {
  203. // 无项目,查询历史项目并显示创建引导
  204. await this.loadHistoryProjects();
  205. this.showCreateProjectGuide();
  206. }
  207. }
  208. /**
  209. * 处理联系人场景
  210. */
  211. async handleContactScene() {
  212. wxdebug('联系人场景,跳转客户画像', {
  213. contactId: this.contact!.id,
  214. contactName: this.contact!.get('name')
  215. });
  216. // 跳转客户画像页面
  217. await this.router.navigate(['/wxwork', this.cid, 'contact', this.contact!.id], {
  218. queryParams: {
  219. profileId: this.currentUser!.id
  220. }
  221. });
  222. }
  223. /**
  224. * 加载历史项目(当前群聊相关的其他项目)
  225. */
  226. async loadHistoryProjects() {
  227. try {
  228. // 通过 ProjectGroup 查询该群聊的所有项目
  229. const pgQuery = new Parse.Query('ProjectGroup');
  230. pgQuery.equalTo('groupChat', this.groupChat!.toPointer());
  231. pgQuery.include('project');
  232. pgQuery.descending('createdAt');
  233. const projectGroups = await pgQuery.find();
  234. this.historyProjects = projectGroups
  235. .map(pg => pg.get('project'))
  236. .filter(p => p && !p.get('isDeleted'));
  237. wxdebug('找到历史项目', { count: this.historyProjects.length });
  238. } catch (err) {
  239. console.error('加载历史项目失败:', err);
  240. wxdebug('加载历史项目失败', err);
  241. }
  242. }
  243. /**
  244. * 显示创建项目引导
  245. */
  246. showCreateProjectGuide() {
  247. this.showCreateGuide = true;
  248. this.defaultProjectName = this.groupChat!.get('name') || '新项目';
  249. this.projectName = this.defaultProjectName;
  250. wxdebug('显示创建项目引导', {
  251. groupName: this.groupChat!.get('name'),
  252. historyProjectsCount: this.historyProjects.length
  253. });
  254. }
  255. /**
  256. * 创建项目
  257. */
  258. async createProject() {
  259. if (!this.projectName.trim()) {
  260. alert('请输入项目名称');
  261. return;
  262. }
  263. // 权限检查
  264. const role = this.currentUser!.get('role');
  265. if (!['客服', '组长', '管理员'].includes(role)) {
  266. alert('您没有权限创建项目');
  267. return;
  268. }
  269. try {
  270. this.creating = true;
  271. wxdebug('开始创建项目', {
  272. projectName: this.projectName,
  273. groupChatId: this.groupChat!.id,
  274. currentUserId: this.currentUser!.id,
  275. role: role
  276. });
  277. // 1. 创建项目
  278. const Project = Parse.Object.extend('Project');
  279. const project = new Project();
  280. project.set('title', this.projectName.trim());
  281. project.set('company', this.currentUser!.get('company'));
  282. project.set('status', '待分配');
  283. project.set('currentStage', '订单分配');
  284. project.set('data', {
  285. createdBy: this.currentUser!.id,
  286. createdFrom: 'wxwork_groupchat',
  287. groupChatId: this.groupChat!.id
  288. });
  289. await project.save();
  290. wxdebug('项目创建成功', { projectId: project.id });
  291. // 2. 关联群聊
  292. this.groupChat!.set('project', project.toPointer());
  293. await this.groupChat!.save();
  294. wxdebug('群聊关联项目成功');
  295. // 3. 创建 ProjectGroup 关联(支持多项目多群)
  296. const ProjectGroup = Parse.Object.extend('ProjectGroup');
  297. const pg = new ProjectGroup();
  298. pg.set('project', project.toPointer());
  299. pg.set('groupChat', this.groupChat!.toPointer());
  300. pg.set('isPrimary', true);
  301. pg.set('company', this.currentUser!.get('company'));
  302. await pg.save();
  303. wxdebug('ProjectGroup关联创建成功');
  304. // 4. 跳转项目详情
  305. this.project = project;
  306. await this.navigateToProjectDetail();
  307. } catch (err: any) {
  308. console.error('创建项目失败:', err);
  309. wxdebug('创建项目失败', err);
  310. alert('创建失败: ' + (err.message || '未知错误'));
  311. } finally {
  312. this.creating = false;
  313. }
  314. }
  315. /**
  316. * 选择历史项目
  317. */
  318. async selectHistoryProject(project: FmodeObject) {
  319. try {
  320. wxdebug('选择历史项目', {
  321. projectId: project.id,
  322. projectTitle: project.get('title')
  323. });
  324. // 更新群聊的当前项目
  325. this.groupChat!.set('project', project.toPointer());
  326. await this.groupChat!.save();
  327. // 跳转项目详情
  328. this.project = project;
  329. await this.navigateToProjectDetail();
  330. } catch (err: any) {
  331. console.error('关联项目失败:', err);
  332. alert('关联失败: ' + (err.message || '未知错误'));
  333. }
  334. }
  335. /**
  336. * 跳转项目详情
  337. */
  338. async navigateToProjectDetail() {
  339. wxdebug('跳转项目详情', {
  340. projectId: this.project!.id,
  341. cid: this.cid,
  342. groupChatId: this.groupChat?.id
  343. });
  344. await this.router.navigate(['/wxwork', this.cid, 'project', this.project!.id], {
  345. queryParams: {
  346. groupId: this.groupChat?.id,
  347. profileId: this.currentUser!.id
  348. }
  349. });
  350. }
  351. /**
  352. * 重新加载
  353. */
  354. async reload() {
  355. this.error = null;
  356. this.showCreateGuide = false;
  357. this.historyProjects = [];
  358. this.chatType = 'none';
  359. await this.loadData();
  360. }
  361. /**
  362. * 获取当前员工姓名
  363. */
  364. getCurrentUserName(): string {
  365. if (!this.currentUser) return '未知';
  366. return this.currentUser.get('name') || this.currentUser.get('userid') || '未知';
  367. }
  368. /**
  369. * 获取当前员工角色
  370. */
  371. getCurrentUserRole(): string {
  372. if (!this.currentUser) return '未知';
  373. return this.currentUser.get('role') || '未知';
  374. }
  375. /**
  376. * 获取项目状态的显示样式类
  377. */
  378. getProjectStatusClass(status: string): string {
  379. const classMap: any = {
  380. '待分配': 'status-pending',
  381. '进行中': 'status-active',
  382. '已完成': 'status-completed',
  383. '已暂停': 'status-paused',
  384. '已取消': 'status-cancelled'
  385. };
  386. return classMap[status] || 'status-default';
  387. }
  388. /**
  389. * 格式化日期
  390. */
  391. formatDate(date: Date): string {
  392. if (!date) return '';
  393. const d = new Date(date);
  394. return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
  395. }
  396. }