project-detail.component.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. import { Component, OnInit, Input, Output, EventEmitter } 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. import { FormsModule } from '@angular/forms';
  14. import { CustomerSelectorComponent } from '../../components/contact-selector/contact-selector.component';
  15. import { OrderApprovalPanelComponent } from '../../../../app/shared/components/order-approval-panel/order-approval-panel.component';
  16. import { GroupChatSummaryComponent } from '../../components/group-chat-summary/group-chat-summary.component';
  17. const Parse = FmodeParse.with('nova');
  18. /**
  19. * 项目详情核心组件
  20. *
  21. * 功能:
  22. * 1. 展示四阶段导航(订单分配、确认需求、交付执行、售后归档)
  23. * 2. 根据角色控制权限
  24. * 3. 子路由切换阶段内容
  25. * 4. 支持@Input和路由参数两种数据加载方式
  26. *
  27. * 路由:/wxwork/:cid/project/:projectId
  28. */
  29. @Component({
  30. selector: 'app-project-detail',
  31. standalone: true,
  32. imports: [
  33. CommonModule,
  34. IonicModule,
  35. RouterModule,
  36. ProjectBottomCardComponent,
  37. ProjectFilesModalComponent,
  38. ProjectMembersModalComponent,
  39. ProjectIssuesModalComponent,
  40. CustomerSelectorComponent,
  41. OrderApprovalPanelComponent,
  42. GroupChatSummaryComponent
  43. ],
  44. templateUrl: './project-detail.component.html',
  45. styleUrls: ['./project-detail.component.scss']
  46. })
  47. export class ProjectDetailComponent implements OnInit {
  48. // 输入参数(支持组件复用)
  49. @Input() project: FmodeObject | null = null;
  50. @Input() groupChat: FmodeObject | null = null;
  51. @Input() currentUser: FmodeObject | null = null;
  52. // 问题统计
  53. issueCount: number = 0;
  54. // 路由参数
  55. cid: string = '';
  56. projectId: string = '';
  57. groupId: string = '';
  58. profileId: string = '';
  59. chatId: string = ''; // 从企微进入时的 chat_id
  60. // 企微SDK
  61. wxwork: WxworkSDK | null = null;
  62. wecorp: WxworkCorp | null = null;
  63. wxAuth: WxworkAuth | null = null; // WxworkAuth 实例
  64. // 加载状态
  65. loading: boolean = true;
  66. error: string | null = null;
  67. // 项目数据
  68. contact: FmodeObject | null = null;
  69. assignee: FmodeObject | null = null;
  70. // 当前阶段
  71. currentStage: string = 'order'; // order | requirements | delivery | aftercare
  72. stages = [
  73. { id: 'order', name: '订单分配', icon: 'document-text-outline', number: 1 },
  74. { id: 'requirements', name: '确认需求', icon: 'checkmark-circle-outline', number: 2 },
  75. { id: 'delivery', name: '交付执行', icon: 'rocket-outline', number: 3 },
  76. { id: 'aftercare', name: '售后归档', icon: 'archive-outline', number: 4 }
  77. ];
  78. // 权限
  79. canEdit: boolean = false;
  80. canViewCustomerPhone: boolean = false;
  81. role: string = '';
  82. // 模态框状态
  83. showFilesModal: boolean = false;
  84. showMembersModal: boolean = false;
  85. showIssuesModal: boolean = false;
  86. // 新增:客户详情侧栏面板状态
  87. showContactPanel: boolean = false;
  88. // 问卷状态
  89. surveyStatus: {
  90. filled: boolean;
  91. text: string;
  92. icon: string;
  93. surveyLog?: FmodeObject;
  94. contact?: FmodeObject;
  95. } = {
  96. filled: false,
  97. text: '发送问卷',
  98. icon: 'document-text-outline'
  99. };
  100. constructor(
  101. private router: Router,
  102. private route: ActivatedRoute,
  103. private profileService: ProfileService,
  104. private issueService: ProjectIssueService
  105. ) {}
  106. async ngOnInit() {
  107. // 获取路由参数
  108. this.cid = this.route.snapshot.paramMap.get('cid') || '';
  109. this.projectId = this.route.snapshot.paramMap.get('projectId') || '';
  110. this.groupId = this.route.snapshot.queryParamMap.get('groupId') || '';
  111. this.profileId = this.route.snapshot.queryParamMap.get('profileId') || '';
  112. this.chatId = this.route.snapshot.queryParamMap.get('chatId') || '';
  113. // 监听路由变化
  114. this.route.firstChild?.url.subscribe((segments) => {
  115. if (segments.length > 0) {
  116. this.currentStage = segments[0].path;
  117. }
  118. });
  119. // 初始化企微授权(不阻塞页面加载)
  120. await this.initWxworkAuth();
  121. await this.loadData();
  122. }
  123. /**
  124. * 初始化企微授权(不阻塞页面)
  125. */
  126. async initWxworkAuth() {
  127. try {
  128. let cid = this.cid || localStorage.getItem("company") || "";
  129. // 如果没有cid,记录警告但不抛出错误
  130. if (!cid) {
  131. console.warn('⚠️ 未找到company ID (cid),企微功能将不可用');
  132. return;
  133. }
  134. this.wxAuth = new WxworkAuth({ cid: cid });
  135. this.wxwork = new WxworkSDK({ cid: cid, appId: 'crm' });
  136. this.wecorp = new WxworkCorp(cid);
  137. console.log('✅ 企微SDK初始化成功,cid:', cid);
  138. } catch (error) {
  139. console.error('❌ 企微SDK初始化失败:', error);
  140. // 不阻塞页面加载
  141. }
  142. }
  143. /**
  144. * 加载数据
  145. */
  146. async loadData() {
  147. try {
  148. this.loading = true;
  149. // 2. 获取当前用户(优先从全局服务获取)
  150. if (!this.currentUser?.id && this.wxAuth) {
  151. try {
  152. this.currentUser = await this.wxAuth.currentProfile();
  153. } catch (error) {
  154. console.warn('⚠️ 获取当前用户Profile失败:', error);
  155. }
  156. }
  157. // 设置权限
  158. this.role = this.currentUser?.get('roleName') || '';
  159. this.canEdit = ['客服', '组员', '组长', '管理员'].includes(this.role);
  160. this.canViewCustomerPhone = ['客服', '组长', '管理员'].includes(this.role);
  161. const companyId = this.currentUser?.get('company')?.id || localStorage?.getItem("company");
  162. // 3. 加载项目
  163. if (!this.project) {
  164. if (this.projectId) {
  165. // 通过 projectId 加载(从后台进入)
  166. const query = new Parse.Query('Project');
  167. query.include('contact', 'assignee','department','department.leader');
  168. this.project = await query.get(this.projectId);
  169. } else if (this.chatId) {
  170. // 通过 chat_id 查找项目(从企微群聊进入)
  171. if (companyId) {
  172. // 先查找 GroupChat
  173. const gcQuery = new Parse.Query('GroupChat');
  174. gcQuery.equalTo('chat_id', this.chatId);
  175. gcQuery.equalTo('company', companyId);
  176. let groupChat = await gcQuery.first();
  177. if (groupChat) {
  178. this.groupChat = groupChat;
  179. const projectPointer = groupChat.get('project');
  180. if (projectPointer) {
  181. const pQuery = new Parse.Query('Project');
  182. pQuery.include('contact', 'assignee','department','department.leader');
  183. this.project = await pQuery.get(projectPointer.id);
  184. }
  185. }
  186. if (!this.project) {
  187. throw new Error('该群聊尚未关联项目,请先在后台创建项目');
  188. }
  189. }
  190. }
  191. }
  192. if(!this.groupChat?.id){
  193. const gcQuery2 = new Parse.Query('GroupChat');
  194. gcQuery2.equalTo('project', this.projectId);
  195. gcQuery2.equalTo('company', companyId);
  196. this.groupChat = await gcQuery2.first();
  197. }
  198. this.wxwork?.syncGroupChat(this.groupChat?.toJSON())
  199. if (!this.project) {
  200. throw new Error('无法加载项目信息');
  201. }
  202. this.contact = this.project.get('contact');
  203. this.assignee = this.project.get('assignee');
  204. // 加载问卷状态
  205. await this.loadSurveyStatus();
  206. // 更新问题计数
  207. try {
  208. if (this.project?.id) {
  209. this.issueService.seed(this.project.id!);
  210. const counts = this.issueService.getCounts(this.project.id!);
  211. this.issueCount = counts.total;
  212. }
  213. } catch (e) {
  214. console.warn('统计问题数量失败:', e);
  215. }
  216. // 4. 加载群聊(如果没有传入且有groupId)
  217. if (!this.groupChat && this.groupId) {
  218. try {
  219. const gcQuery = new Parse.Query('GroupChat');
  220. this.groupChat = await gcQuery.get(this.groupId);
  221. } catch (err) {
  222. console.warn('加载群聊失败:', err);
  223. }
  224. }
  225. // 5. 根据项目当前阶段设置默认路由
  226. const projectStage = this.project.get('currentStage');
  227. const stageMap: any = {
  228. '订单分配': 'order',
  229. '确认需求': 'requirements',
  230. '方案确认': 'requirements',
  231. '方案深化': 'requirements',
  232. '交付执行': 'delivery',
  233. '建模': 'delivery',
  234. '软装': 'delivery',
  235. '渲染': 'delivery',
  236. '后期': 'delivery',
  237. '尾款结算': 'aftercare',
  238. '客户评价': 'aftercare',
  239. '投诉处理': 'aftercare'
  240. };
  241. const targetStage = stageMap[projectStage] || 'order';
  242. // 如果当前没有子路由,跳转到对应阶段
  243. if (!this.route.firstChild) {
  244. this.router.navigate([targetStage], { relativeTo: this.route, replaceUrl: true });
  245. }
  246. } catch (err: any) {
  247. console.error('加载失败:', err);
  248. this.error = err.message || '加载失败';
  249. } finally {
  250. this.loading = false;
  251. }
  252. }
  253. /**
  254. * 切换阶段
  255. */
  256. switchStage(stageId: string) {
  257. this.currentStage = stageId;
  258. this.router.navigate([stageId], { relativeTo: this.route });
  259. }
  260. /**
  261. * 获取阶段状态
  262. */
  263. getStageStatus(stageId: string): 'completed' | 'active' | 'pending' {
  264. const projectStage = this.project?.get('currentStage') || '';
  265. const stageOrder = ['订单分配', '确认需求', '建模', '软装', '渲染', '后期', '尾款结算', '客户评价'];
  266. const currentIndex = stageOrder.indexOf(projectStage);
  267. const stageIndexMap: any = {
  268. 'order': 0,
  269. 'requirements': 1,
  270. 'delivery': 3,
  271. 'aftercare': 6
  272. };
  273. const targetIndex = stageIndexMap[stageId];
  274. if (currentIndex > targetIndex) {
  275. return 'completed';
  276. } else if (this.currentStage === stageId) {
  277. return 'active';
  278. } else {
  279. return 'pending';
  280. }
  281. }
  282. /**
  283. * 返回
  284. */
  285. goBack() {
  286. let ua = navigator.userAgent.toLowerCase();
  287. let isWeixin = ua.indexOf("micromessenger") != -1;
  288. if(isWeixin){
  289. this.router.navigate(['/wxwork', this.cid, 'project-loader']);
  290. }else{
  291. history.back();
  292. }
  293. }
  294. /**
  295. * 更新项目阶段
  296. */
  297. async updateProjectStage(stage: string) {
  298. if (!this.project || !this.canEdit) return;
  299. try {
  300. this.project.set('currentStage', stage);
  301. await this.project.save();
  302. // 添加阶段历史
  303. const data = this.project.get('data') || {};
  304. const stageHistory = data.stageHistory || [];
  305. stageHistory.push({
  306. stage,
  307. startTime: new Date(),
  308. status: 'current',
  309. operator: {
  310. id: this.currentUser!.id,
  311. name: this.currentUser!.get('name'),
  312. role: this.role
  313. }
  314. });
  315. this.project.set('data', { ...data, stageHistory });
  316. await this.project.save();
  317. } catch (err) {
  318. console.error('更新阶段失败:', err);
  319. window?.fmode?.alert('更新失败');
  320. }
  321. }
  322. /**
  323. * 发送企微消息
  324. */
  325. async sendWxMessage(message: string) {
  326. if (!this.groupChat || !this.wecorp) return;
  327. try {
  328. const chatId = this.groupChat.get('chat_id');
  329. await this.wecorp.appchat.sendText(chatId, message);
  330. } catch (err) {
  331. console.error('发送消息失败:', err);
  332. }
  333. }
  334. /**
  335. * 选择客户(从群聊成员中选择外部联系人)
  336. */
  337. async selectCustomer() {
  338. console.log(this.canEdit, this.groupChat)
  339. if (!this.groupChat) return;
  340. try {
  341. const memberList = this.groupChat.get('member_list') || [];
  342. const externalMembers = memberList.filter((m: any) => m.type === 2);
  343. if (externalMembers.length === 0) {
  344. window?.fmode?.alert('当前群聊中没有外部联系人');
  345. return;
  346. }
  347. console.log(externalMembers)
  348. // 简单实现:选择第一个外部联系人
  349. // TODO: 实现选择器UI
  350. const selectedMember = externalMembers[0];
  351. await this.setCustomerFromMember(selectedMember);
  352. } catch (err) {
  353. console.error('选择客户失败:', err);
  354. window?.fmode?.alert('选择客户失败');
  355. }
  356. }
  357. /**
  358. * 从群成员设置客户
  359. */
  360. async setCustomerFromMember(member: any) {
  361. if (!this.wecorp) return;
  362. try {
  363. const companyId = this.currentUser?.get('company')?.id || localStorage.getItem("company");
  364. if (!companyId) throw new Error('无法获取企业信息');
  365. // 1. 查询是否已存在 ContactInfo
  366. const query = new Parse.Query('ContactInfo');
  367. query.equalTo('external_userid', member.userid);
  368. query.equalTo('company', companyId);
  369. let contactInfo = await query.first();
  370. // 2. 如果不存在,通过企微API获取并创建
  371. if (!contactInfo) {
  372. contactInfo = new Parse.Object("ContactInfo");
  373. }
  374. const externalContactData = await this.wecorp.externalContact.get(member.userid);
  375. console.log("externalContactData",externalContactData)
  376. const ContactInfo = Parse.Object.extend('ContactInfo');
  377. contactInfo.set('name', externalContactData.name);
  378. contactInfo.set('external_userid', member.userid);
  379. const company = new Parse.Object('Company');
  380. company.id = companyId;
  381. const companyPointer = company.toPointer();
  382. contactInfo.set('company', companyPointer);
  383. contactInfo.set('data', externalContactData);
  384. await contactInfo.save();
  385. // 3. 设置为项目客户
  386. if (this.project) {
  387. this.project.set('contact', contactInfo.toPointer());
  388. await this.project.save();
  389. this.contact = contactInfo;
  390. window?.fmode?.alert('客户设置成功');
  391. }
  392. } catch (err) {
  393. console.error('设置客户失败:', err);
  394. throw err;
  395. }
  396. }
  397. /**
  398. * 显示文件模态框
  399. */
  400. showFiles() {
  401. this.showFilesModal = true;
  402. }
  403. /**
  404. * 显示成员模态框
  405. */
  406. showMembers() {
  407. this.showMembersModal = true;
  408. }
  409. /** 显示问题模态框 */
  410. showIssues() {
  411. this.showIssuesModal = true;
  412. }
  413. /**
  414. * 关闭文件模态框
  415. */
  416. closeFilesModal() {
  417. this.showFilesModal = false;
  418. }
  419. /**
  420. * 关闭成员模态框
  421. */
  422. closeMembersModal() {
  423. this.showMembersModal = false;
  424. }
  425. /** 显示客户详情面板 */
  426. openContactPanel() {
  427. if (this.contact) {
  428. this.showContactPanel = true;
  429. }
  430. }
  431. /** 关闭客户详情面板 */
  432. closeContactPanel() {
  433. this.showContactPanel = false;
  434. }
  435. /** 关闭问题模态框 */
  436. closeIssuesModal() {
  437. this.showIssuesModal = false;
  438. if (this.project?.id) {
  439. const counts = this.issueService.getCounts(this.project.id!);
  440. this.issueCount = counts.total;
  441. }
  442. }
  443. /** 客户选择事件回调(接收子组件输出) */
  444. onContactSelected(evt: { contact: FmodeObject; isNewCustomer: boolean; action: 'selected' | 'created' | 'updated' }) {
  445. this.contact = evt.contact;
  446. // 重新加载问卷状态
  447. this.loadSurveyStatus();
  448. }
  449. /**
  450. * 加载问卷状态
  451. */
  452. async loadSurveyStatus() {
  453. if (!this.project?.id) return;
  454. try {
  455. const query = new Parse.Query('SurveyLog');
  456. query.equalTo('project', this.project.toPointer());
  457. query.equalTo('type', 'survey-project');
  458. query.equalTo('isCompleted', true);
  459. query.include("contact")
  460. const surveyLog = await query.first();
  461. if (surveyLog) {
  462. this.surveyStatus = {
  463. filled: true,
  464. text: '查看问卷',
  465. icon: 'checkmark-circle',
  466. surveyLog,
  467. contact:surveyLog?.get("contact")
  468. };
  469. console.log('✅ 问卷已填写');
  470. } else {
  471. this.surveyStatus = {
  472. filled: false,
  473. text: '发送问卷',
  474. icon: 'document-text-outline'
  475. };
  476. console.log('✅ 问卷未填写');
  477. }
  478. } catch (err) {
  479. console.error('❌ 查询问卷状态失败:', err);
  480. }
  481. }
  482. /**
  483. * 发送问卷
  484. */
  485. async sendSurvey() {
  486. if (!this.groupChat || !this.wxwork) {
  487. window?.fmode?.alert('无法发送问卷:未找到群聊或企微SDK未初始化');
  488. return;
  489. }
  490. try {
  491. const chatId = this.groupChat.get('chat_id');
  492. const surveyUrl = `${document.baseURI}/wxwork/${this.cid}/survey/project/${this.project?.id}`;
  493. await this.wxwork.ww.openExistedChatWithMsg({
  494. chatId: chatId,
  495. msg: {
  496. msgtype: 'link',
  497. link: {
  498. title: '《家装效果图服务需求调查表》',
  499. desc: '为让本次服务更贴合您的需求,请花3-5分钟填写简短问卷,感谢支持!',
  500. url: surveyUrl,
  501. imgUrl: `${document.baseURI}/assets/logo.jpg`
  502. }
  503. }
  504. });
  505. window?.fmode?.alert('问卷已发送到群聊!');
  506. } catch (err) {
  507. console.error('❌ 发送问卷失败:', err);
  508. window?.fmode?.alert('发送失败,请重试');
  509. }
  510. }
  511. /**
  512. * 查看问卷结果
  513. */
  514. async viewSurvey() {
  515. if (!this.surveyStatus.surveyLog) return;
  516. // 跳转到问卷页面查看结果
  517. this.router.navigate(['/wxwork', this.cid, 'survey', 'project', this.project?.id]);
  518. }
  519. /**
  520. * 处理问卷点击
  521. */
  522. async handleSurveyClick(event: Event) {
  523. event.stopPropagation();
  524. if (this.surveyStatus.filled) {
  525. // 已填写,查看结果
  526. await this.viewSurvey();
  527. } else {
  528. // 未填写,发送问卷
  529. await this.sendSurvey();
  530. }
  531. }
  532. /**
  533. * 是否显示审批面板
  534. * 条件:当前用户是组长 + 项目处于订单分配阶段 + 审批状态为待审批
  535. * ⚠️ 临时放开权限:允许所有角色查看审批面板(测试用)
  536. */
  537. get showApprovalPanel(): boolean {
  538. if (!this.project || !this.currentUser) {
  539. console.log('🔍 审批面板检查: 缺少项目或用户数据');
  540. return false;
  541. }
  542. const userRole = this.currentUser.get('roleName') || '';
  543. // ⚠️ 临时注释角色检查,允许所有角色访问
  544. // const isTeamLeader = userRole === '设计组长' || userRole === 'team-leader';
  545. const isTeamLeader = true; // 临时放开权限
  546. const currentStage = this.project.get('currentStage') || '';
  547. const isOrderStage = currentStage === '订单分配' || currentStage === 'order';
  548. const data = this.project.get('data') || {};
  549. const approvalStatus = data.approvalStatus;
  550. const isPending = approvalStatus === 'pending';
  551. console.log('🔍 审批面板检查 [临时放开权限]:', {
  552. userRole,
  553. isTeamLeader,
  554. currentStage,
  555. isOrderStage,
  556. approvalStatus,
  557. isPending,
  558. result: isTeamLeader && isOrderStage && isPending
  559. });
  560. return isTeamLeader && isOrderStage && isPending;
  561. }
  562. /**
  563. * 处理审批完成事件
  564. */
  565. async onApprovalCompleted(event: { action: 'approved' | 'rejected'; reason?: string; comment?: string }) {
  566. if (!this.project) return;
  567. try {
  568. const data = this.project.get('data') || {};
  569. const approvalHistory = data.approvalHistory || [];
  570. const latestRecord = approvalHistory[approvalHistory.length - 1];
  571. if (latestRecord) {
  572. latestRecord.status = event.action;
  573. latestRecord.approver = {
  574. id: this.currentUser?.id,
  575. name: this.currentUser?.get('name'),
  576. role: this.currentUser?.get('roleName')
  577. };
  578. latestRecord.approvalTime = new Date();
  579. latestRecord.comment = event.comment;
  580. latestRecord.reason = event.reason;
  581. }
  582. if (event.action === 'approved') {
  583. // 通过审批:推进到确认需求阶段
  584. data.approvalStatus = 'approved';
  585. this.project.set('currentStage', '确认需求');
  586. this.project.set('data', data);
  587. await this.project.save();
  588. alert('✅ 审批通过,项目已进入确认需求阶段');
  589. // 刷新页面数据
  590. await this.loadData();
  591. } else {
  592. // 驳回:保持在订单分配阶段,记录驳回原因
  593. data.approvalStatus = 'rejected';
  594. data.lastRejectionReason = event.reason || '未提供原因';
  595. this.project.set('data', data);
  596. await this.project.save();
  597. alert('✅ 已驳回订单,客服将收到通知');
  598. // 刷新页面数据
  599. await this.loadData();
  600. }
  601. } catch (err) {
  602. console.error('处理审批失败:', err);
  603. alert('审批操作失败,请重试');
  604. }
  605. }
  606. }
  607. // duplicate inline CustomerSelectorComponent removed (we keep single declaration above)