project-detail.component.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import { Component, OnInit, Input } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { Router, ActivatedRoute, RouterModule } from '@angular/router';
  4. import { IonicModule } from '@ionic/angular';
  5. import { WxworkSDK, WxworkCorp } from 'fmode-ng/core';
  6. import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
  7. import { ProfileService } from '../../../../app/services/profile.service';
  8. import { ProjectBottomCardComponent } from '../../components/project-bottom-card/project-bottom-card.component';
  9. import { ProjectFilesModalComponent } from '../../components/project-files-modal/project-files-modal.component';
  10. import { ProjectMembersModalComponent } from '../../components/project-members-modal/project-members-modal.component';
  11. const Parse = FmodeParse.with('nova');
  12. /**
  13. * 项目详情核心组件
  14. *
  15. * 功能:
  16. * 1. 展示四阶段导航(订单分配、确认需求、交付执行、售后归档)
  17. * 2. 根据角色控制权限
  18. * 3. 子路由切换阶段内容
  19. * 4. 支持@Input和路由参数两种数据加载方式
  20. *
  21. * 路由:/wxwork/:cid/project/:projectId
  22. */
  23. @Component({
  24. selector: 'app-project-detail',
  25. standalone: true,
  26. imports: [CommonModule, IonicModule, RouterModule, ProjectBottomCardComponent, ProjectFilesModalComponent, ProjectMembersModalComponent],
  27. templateUrl: './project-detail.component.html',
  28. styleUrls: ['./project-detail.component.scss']
  29. })
  30. export class ProjectDetailComponent implements OnInit {
  31. // 输入参数(支持组件复用)
  32. @Input() project: FmodeObject | null = null;
  33. @Input() groupChat: FmodeObject | null = null;
  34. @Input() currentUser: FmodeObject | null = null;
  35. // 路由参数
  36. cid: string = '';
  37. projectId: string = '';
  38. groupId: string = '';
  39. profileId: string = '';
  40. chatId: string = ''; // 从企微进入时的 chat_id
  41. // 企微SDK
  42. wxwork: WxworkSDK | null = null;
  43. wecorp: WxworkCorp | null = null;
  44. wxAuth: any = null; // WxworkAuth 实例
  45. // 加载状态
  46. loading: boolean = true;
  47. error: string | null = null;
  48. // 项目数据
  49. customer: FmodeObject | null = null;
  50. assignee: FmodeObject | null = null;
  51. // 当前阶段
  52. currentStage: string = 'order'; // order | requirements | delivery | aftercare
  53. stages = [
  54. { id: 'order', name: '订单分配', icon: 'document-text-outline', number: 1 },
  55. { id: 'requirements', name: '确认需求', icon: 'checkmark-circle-outline', number: 2 },
  56. { id: 'delivery', name: '交付执行', icon: 'rocket-outline', number: 3 },
  57. { id: 'aftercare', name: '售后归档', icon: 'archive-outline', number: 4 }
  58. ];
  59. // 权限
  60. canEdit: boolean = false;
  61. canViewCustomerPhone: boolean = false;
  62. role: string = '';
  63. // 模态框状态
  64. showFilesModal: boolean = false;
  65. showMembersModal: boolean = false;
  66. constructor(
  67. private router: Router,
  68. private route: ActivatedRoute,
  69. private profileService: ProfileService
  70. ) {}
  71. async ngOnInit() {
  72. // 获取路由参数
  73. this.cid = this.route.snapshot.paramMap.get('cid') || '';
  74. this.projectId = this.route.snapshot.paramMap.get('projectId') || '';
  75. this.groupId = this.route.snapshot.queryParamMap.get('groupId') || '';
  76. this.profileId = this.route.snapshot.queryParamMap.get('profileId') || '';
  77. this.chatId = this.route.snapshot.queryParamMap.get('chatId') || '';
  78. // 监听路由变化
  79. this.route.firstChild?.url.subscribe((segments) => {
  80. if (segments.length > 0) {
  81. this.currentStage = segments[0].path;
  82. }
  83. });
  84. // 初始化企微授权(不阻塞页面加载)
  85. this.initWxworkAuth();
  86. await this.loadData();
  87. }
  88. /**
  89. * 初始化企微授权(不阻塞页面)
  90. */
  91. async initWxworkAuth() {
  92. if (!this.cid) return;
  93. try {
  94. // 动态导入 WxworkAuth 避免导入错误
  95. const { WxworkAuth } = await import('fmode-ng/core');
  96. this.wxAuth = new WxworkAuth({ cid: this.cid, appId: 'crm' });
  97. // 静默授权并同步 Profile,不阻塞页面
  98. const { profile } = await this.wxAuth.authenticateAndLogin();
  99. if (profile) {
  100. this.profileService.setCurrentProfile(profile);
  101. }
  102. } catch (error) {
  103. console.warn('企微授权失败:', error);
  104. // 授权失败不影响页面加载,继续使用其他方式加载数据
  105. }
  106. }
  107. /**
  108. * 加载数据
  109. */
  110. async loadData() {
  111. try {
  112. this.loading = true;
  113. // 1. 初始化SDK(用于企微API调用,不需要等待授权)
  114. if (!this.wxwork && this.cid) {
  115. this.wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
  116. this.wecorp = new WxworkCorp(this.cid);
  117. }
  118. // 2. 获取当前用户(优先从全局服务获取)
  119. if (!this.currentUser) {
  120. // 优先级1: 使用 profileId 参数
  121. if (this.profileId) {
  122. this.currentUser = await this.profileService.getProfileById(this.profileId);
  123. }
  124. // 优先级2: 从全局服务获取当前 Profile
  125. if (!this.currentUser) {
  126. this.currentUser = await this.profileService.getCurrentProfile(this.cid);
  127. }
  128. // 优先级3: 企微环境下尝试从SDK获取
  129. if (!this.currentUser && this.wxwork) {
  130. try {
  131. this.currentUser = await this.wxwork.getCurrentUser();
  132. } catch (err) {
  133. console.warn('无法从企微SDK获取用户:', err);
  134. }
  135. }
  136. }
  137. // 设置权限
  138. this.role = this.currentUser?.get('roleName') || '';
  139. this.canEdit = ['客服', '组员', '组长', '管理员'].includes(this.role);
  140. this.canViewCustomerPhone = ['客服', '组长', '管理员'].includes(this.role);
  141. // 3. 加载项目
  142. if (!this.project) {
  143. if (this.projectId) {
  144. // 通过 projectId 加载(从后台进入)
  145. const query = new Parse.Query('Project');
  146. query.include('customer', 'assignee','department','department.leader');
  147. this.project = await query.get(this.projectId);
  148. } else if (this.chatId) {
  149. // 通过 chat_id 查找项目(从企微群聊进入)
  150. const companyId = this.currentUser?.get('company')?.id;
  151. if (companyId) {
  152. // 先查找 GroupChat
  153. const gcQuery = new Parse.Query('GroupChat');
  154. gcQuery.equalTo('chat_id', this.chatId);
  155. gcQuery.equalTo('company', companyId);
  156. let groupChat = await gcQuery.first();
  157. if(!groupChat?.id){
  158. const gcQuery2 = new Parse.Query('GroupChat');
  159. gcQuery2.equalTo('project', this.projectId);
  160. gcQuery2.equalTo('company', companyId);
  161. groupChat = await gcQuery2.first();
  162. }
  163. if (groupChat) {
  164. this.groupChat = groupChat;
  165. const projectPointer = groupChat.get('project');
  166. if (projectPointer) {
  167. const pQuery = new Parse.Query('Project');
  168. pQuery.include('customer', 'assignee','department','department.leader');
  169. this.project = await pQuery.get(projectPointer.id);
  170. }
  171. }
  172. if (!this.project) {
  173. throw new Error('该群聊尚未关联项目,请先在后台创建项目');
  174. }
  175. }
  176. }
  177. }
  178. if (!this.project) {
  179. throw new Error('无法加载项目信息');
  180. }
  181. this.customer = this.project.get('customer');
  182. this.assignee = this.project.get('assignee');
  183. // 4. 加载群聊(如果没有传入且有groupId)
  184. if (!this.groupChat && this.groupId) {
  185. try {
  186. const gcQuery = new Parse.Query('GroupChat');
  187. this.groupChat = await gcQuery.get(this.groupId);
  188. } catch (err) {
  189. console.warn('加载群聊失败:', err);
  190. }
  191. }
  192. // 5. 根据项目当前阶段设置默认路由
  193. const projectStage = this.project.get('currentStage');
  194. const stageMap: any = {
  195. '订单分配': 'order',
  196. '确认需求': 'requirements',
  197. '方案确认': 'requirements',
  198. '方案深化': 'requirements',
  199. '交付执行': 'delivery',
  200. '建模': 'delivery',
  201. '软装': 'delivery',
  202. '渲染': 'delivery',
  203. '后期': 'delivery',
  204. '尾款结算': 'aftercare',
  205. '客户评价': 'aftercare',
  206. '投诉处理': 'aftercare'
  207. };
  208. const targetStage = stageMap[projectStage] || 'order';
  209. // 如果当前没有子路由,跳转到对应阶段
  210. if (!this.route.firstChild) {
  211. this.router.navigate([targetStage], { relativeTo: this.route, replaceUrl: true });
  212. }
  213. } catch (err: any) {
  214. console.error('加载失败:', err);
  215. this.error = err.message || '加载失败';
  216. } finally {
  217. this.loading = false;
  218. }
  219. }
  220. /**
  221. * 切换阶段
  222. */
  223. switchStage(stageId: string) {
  224. this.currentStage = stageId;
  225. this.router.navigate([stageId], { relativeTo: this.route });
  226. }
  227. /**
  228. * 获取阶段状态
  229. */
  230. getStageStatus(stageId: string): 'completed' | 'active' | 'pending' {
  231. const projectStage = this.project?.get('currentStage') || '';
  232. const stageOrder = ['订单分配', '确认需求', '建模', '软装', '渲染', '后期', '尾款结算', '客户评价'];
  233. const currentIndex = stageOrder.indexOf(projectStage);
  234. const stageIndexMap: any = {
  235. 'order': 0,
  236. 'requirements': 1,
  237. 'delivery': 3,
  238. 'aftercare': 6
  239. };
  240. const targetIndex = stageIndexMap[stageId];
  241. if (currentIndex > targetIndex) {
  242. return 'completed';
  243. } else if (this.currentStage === stageId) {
  244. return 'active';
  245. } else {
  246. return 'pending';
  247. }
  248. }
  249. /**
  250. * 返回
  251. */
  252. goBack() {
  253. let ua = navigator.userAgent.toLowerCase();
  254. let isWeixin = ua.indexOf("micromessenger") != -1;
  255. if(isWeixin){
  256. this.router.navigate(['/wxwork', this.cid, 'project-loader']);
  257. }else{
  258. history.back();
  259. }
  260. }
  261. /**
  262. * 更新项目阶段
  263. */
  264. async updateProjectStage(stage: string) {
  265. if (!this.project || !this.canEdit) return;
  266. try {
  267. this.project.set('currentStage', stage);
  268. await this.project.save();
  269. // 添加阶段历史
  270. const data = this.project.get('data') || {};
  271. const stageHistory = data.stageHistory || [];
  272. stageHistory.push({
  273. stage,
  274. startTime: new Date(),
  275. status: 'current',
  276. operator: {
  277. id: this.currentUser!.id,
  278. name: this.currentUser!.get('name'),
  279. role: this.role
  280. }
  281. });
  282. this.project.set('data', { ...data, stageHistory });
  283. await this.project.save();
  284. } catch (err) {
  285. console.error('更新阶段失败:', err);
  286. alert('更新失败');
  287. }
  288. }
  289. /**
  290. * 发送企微消息
  291. */
  292. async sendWxMessage(message: string) {
  293. if (!this.groupChat || !this.wecorp) return;
  294. try {
  295. const chatId = this.groupChat.get('chat_id');
  296. await this.wecorp.appchat.sendText(chatId, message);
  297. } catch (err) {
  298. console.error('发送消息失败:', err);
  299. }
  300. }
  301. /**
  302. * 选择客户(从群聊成员中选择外部联系人)
  303. */
  304. async selectCustomer() {
  305. if (!this.canEdit || !this.groupChat) return;
  306. try {
  307. const memberList = this.groupChat.get('member_list') || [];
  308. const externalMembers = memberList.filter((m: any) => m.type === 2);
  309. if (externalMembers.length === 0) {
  310. alert('当前群聊中没有外部联系人');
  311. return;
  312. }
  313. // 简单实现:选择第一个外部联系人
  314. // TODO: 实现选择器UI
  315. const selectedMember = externalMembers[0];
  316. await this.setCustomerFromMember(selectedMember);
  317. } catch (err) {
  318. console.error('选择客户失败:', err);
  319. alert('选择客户失败');
  320. }
  321. }
  322. /**
  323. * 从群成员设置客户
  324. */
  325. async setCustomerFromMember(member: any) {
  326. if (!this.wecorp) return;
  327. try {
  328. const companyId = this.currentUser?.get('company')?.id;
  329. if (!companyId) throw new Error('无法获取企业信息');
  330. // 1. 查询是否已存在 ContactInfo
  331. const query = new Parse.Query('ContactInfo');
  332. query.equalTo('external_userid', member.userid);
  333. query.equalTo('company', companyId);
  334. let contactInfo = await query.first();
  335. // 2. 如果不存在,通过企微API获取并创建
  336. if (!contactInfo) {
  337. const externalContactData = await this.wecorp.externalContact.get(member.userid);
  338. const ContactInfo = Parse.Object.extend('ContactInfo');
  339. contactInfo = new ContactInfo();
  340. contactInfo.set('name', externalContactData.name);
  341. contactInfo.set('external_userid', member.userid);
  342. const company = new Parse.Object('Company');
  343. company.id = companyId;
  344. const companyPointer = company.toPointer();
  345. contactInfo.set('company', companyPointer);
  346. contactInfo.set('data', {
  347. avatar: externalContactData.avatar,
  348. type: externalContactData.type,
  349. gender: externalContactData.gender,
  350. follow_user: externalContactData.follow_user
  351. });
  352. await contactInfo.save();
  353. }
  354. // 3. 设置为项目客户
  355. if (this.project) {
  356. this.project.set('customer', contactInfo.toPointer());
  357. await this.project.save();
  358. this.customer = contactInfo;
  359. alert('客户设置成功');
  360. }
  361. } catch (err) {
  362. console.error('设置客户失败:', err);
  363. throw err;
  364. }
  365. }
  366. /**
  367. * 显示文件模态框
  368. */
  369. showFiles() {
  370. this.showFilesModal = true;
  371. }
  372. /**
  373. * 显示成员模态框
  374. */
  375. showMembers() {
  376. this.showMembersModal = true;
  377. }
  378. /**
  379. * 关闭文件模态框
  380. */
  381. closeFilesModal() {
  382. this.showFilesModal = false;
  383. }
  384. /**
  385. * 关闭成员模态框
  386. */
  387. closeMembersModal() {
  388. this.showMembersModal = false;
  389. }
  390. }