project-detail.component.ts 14 KB

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