project-loader.component.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  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 { WxworkSDK, WxworkCorp } from 'fmode-ng/core';
  6. import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
  7. import { ActivityLogService } from '../../../../app/services/activity-log.service';
  8. // WxworkCurrentChat 类型定义
  9. interface WxworkCurrentChat {
  10. type?: string;
  11. chatId?: string;
  12. group?: any;
  13. contact?: any;
  14. id?: string;
  15. [key: string]: any;
  16. }
  17. // 个人技能评分
  18. interface SkillRating {
  19. name: string;
  20. currentScore: number;
  21. targetScore: number;
  22. category: '设计能力' | '沟通能力' | '技术能力' | '项目管理';
  23. }
  24. // 案例作品
  25. interface CaseWork {
  26. id: string;
  27. projectId: string;
  28. projectTitle: string;
  29. coverImage: string;
  30. description: string;
  31. tags: string[];
  32. completionDate: Date;
  33. customerName: string;
  34. status: string;
  35. totalPrice?: number;
  36. roomType?: string;
  37. }
  38. // 月度统计
  39. interface MonthlyStats {
  40. month: string;
  41. totalProjects: number;
  42. completedProjects: number;
  43. revenue: number;
  44. avgScore: number;
  45. }
  46. // 自我评价
  47. interface SelfEvaluation {
  48. strengths: string[]; // 优势
  49. improvements: string[]; // 待提升
  50. personalStatement: string; // 个人陈述
  51. lastUpdated: Date;
  52. }
  53. function wxdebug(...params:any[]){
  54. console.log(params)
  55. }
  56. const Parse = FmodeParse.with('nova');
  57. /**
  58. * 个人看板页面(重构自项目预加载页面)
  59. *
  60. * 功能:
  61. * 1. 展示个人信息和自我评价
  62. * 2. 技能评分和发展目标
  63. * 3. 案例作品集(从完成项目选择)
  64. * 4. 月度接单量统计
  65. * 5. 支持编辑个人资料和案例
  66. *
  67. * 路由:/wxwork/:cid/project-loader
  68. */
  69. @Component({
  70. selector: 'app-project-loader',
  71. standalone: true,
  72. imports: [CommonModule, FormsModule],
  73. templateUrl: './project-loader.component.html',
  74. styleUrls: ['./project-loader.component.scss']
  75. })
  76. export class ProjectLoaderComponent implements OnInit {
  77. // 基础数据
  78. cid: string = '';
  79. appId: string = 'crm';
  80. // 加载状态
  81. loading: boolean = true;
  82. loadingMessage: string = '正在加载...';
  83. error: string | null = null;
  84. // 企微SDK
  85. wxwork: WxworkSDK | null = null;
  86. wecorp: WxworkCorp | null = null;
  87. // 上下文数据
  88. currentUser: FmodeObject | null = null; // Profile
  89. currentChat: WxworkCurrentChat | null = null;
  90. chatType: 'group' | 'contact' | 'personal' = 'personal';
  91. groupChat: FmodeObject | null = null; // GroupChat
  92. contact: FmodeObject | null = null; // ContactInfo
  93. project: FmodeObject | null = null; // Project
  94. // 个人看板数据
  95. skillRatings: SkillRating[] = [];
  96. caseWorks: CaseWork[] = [];
  97. monthlyStats: MonthlyStats[] = [];
  98. selfEvaluation: SelfEvaluation = {
  99. strengths: [],
  100. improvements: [],
  101. personalStatement: '',
  102. lastUpdated: new Date()
  103. };
  104. // UI状态
  105. activeTab: 'overview' | 'cases' | 'stats' | 'skills' = 'overview';
  106. showEditEvaluation: boolean = false;
  107. showCaseSelector: boolean = false;
  108. showSkillEditor: boolean = false;
  109. // 编辑状态
  110. editingEvaluation: SelfEvaluation | null = null;
  111. availableProjects: FmodeObject[] = [];
  112. selectedProjectIds: string[] = [];
  113. // 统计数据
  114. totalProjects: number = 0;
  115. completedProjects: number = 0;
  116. currentMonthProjects: number = 0;
  117. avgCustomerRating: number = 0;
  118. // 创建项目引导(保留原有功能)
  119. showCreateGuide: boolean = false;
  120. defaultProjectName: string = '';
  121. projectName: string = '';
  122. creating: boolean = false;
  123. historyProjects: FmodeObject[] = [];
  124. constructor(
  125. private router: Router,
  126. private route: ActivatedRoute,
  127. private activityLogService: ActivityLogService
  128. ) {}
  129. async ngOnInit() {
  130. // 获取路由参数
  131. this.route.paramMap.subscribe(async params => {
  132. this.cid = params.get('cid') || localStorage.getItem("company") || '';
  133. this.appId = params.get('appId') || 'crm';
  134. if (!this.cid) {
  135. this.error = '缺少企业ID参数';
  136. this.loading = false;
  137. return;
  138. }
  139. await this.loadData();
  140. });
  141. }
  142. /**
  143. * 加载数据主流程
  144. */
  145. async loadData() {
  146. try {
  147. this.loading = true;
  148. this.loadingMessage = '初始化企微SDK...';
  149. // 1️⃣ 初始化 SDK
  150. // @ts-ignore - fmode-ng type issue
  151. this.wxwork = new WxworkSDK({ cid: this.cid, appId: this.appId });
  152. // @ts-ignore - fmode-ng type issue
  153. this.wecorp = new WxworkCorp(this.cid);
  154. wxdebug('1. SDK初始化完成', { cid: this.cid, appId: this.appId });
  155. // 2️⃣ 加载当前登录员工信息
  156. this.loadingMessage = '获取用户信息...';
  157. try {
  158. this.currentUser = await this.wxwork.getCurrentUser();
  159. wxdebug('2. 获取当前用户成功', this.currentUser?.toJSON());
  160. } catch (err) {
  161. console.error('获取当前用户失败:', err);
  162. throw new Error('获取用户信息失败,请重试');
  163. }
  164. // 3️⃣ 加载当前聊天上下文
  165. this.loadingMessage = '获取会话信息...';
  166. try {
  167. this.currentChat = await this.wxwork.getCurrentChat();
  168. wxdebug('3. getCurrentChat返回', this.currentChat);
  169. } catch (err) {
  170. console.error('getCurrentChat失败:', err);
  171. wxdebug('3. getCurrentChat失败', err);
  172. }
  173. // 4️⃣ 根据场景处理
  174. if (this.currentChat?.type === "chatId" && this.currentChat?.group) {
  175. // 群聊场景 - 保留原有逻辑
  176. this.chatType = 'group';
  177. this.groupChat = await this.wxwork.syncGroupChat(this.currentChat.group);
  178. await this.handleGroupChatScene();
  179. } else if (this.currentChat?.type === "userId" && this.currentChat?.id) {
  180. // 联系人场景 - 保留原有逻辑
  181. this.chatType = 'contact';
  182. const contactInfo = await this.wecorp!.externalContact.get(this.currentChat.id);
  183. this.contact = await this.wxwork.syncContact(contactInfo);
  184. await this.handleContactScene();
  185. } else {
  186. // 个人看板场景(默认)
  187. this.chatType = 'personal';
  188. await this.loadPersonalBoard();
  189. }
  190. wxdebug('加载完成', {
  191. chatType: this.chatType,
  192. hasCurrentUser: !!this.currentUser
  193. });
  194. } catch (err: any) {
  195. console.error('加载失败:', err);
  196. this.error = err.message || '加载失败,请重试';
  197. } finally {
  198. this.loading = false;
  199. }
  200. }
  201. /**
  202. * 加载个人看板数据
  203. */
  204. async loadPersonalBoard() {
  205. if (!this.currentUser) {
  206. throw new Error('用户信息不存在');
  207. }
  208. this.loadingMessage = '加载个人信息...';
  209. try {
  210. // 并行加载所有数据
  211. await Promise.all([
  212. this.loadProfileData(),
  213. this.loadSkillRatings(),
  214. this.loadCaseWorks(),
  215. this.loadMonthlyStats(),
  216. this.loadSelfEvaluation()
  217. ]);
  218. console.log('✅ 个人看板数据加载完成');
  219. } catch (err) {
  220. console.error('加载个人看板数据失败:', err);
  221. throw err;
  222. }
  223. }
  224. /**
  225. * 加载个人资料数据
  226. */
  227. async loadProfileData() {
  228. try {
  229. // 从Profile表获取最新数据
  230. const query = new Parse.Query('Profile');
  231. const profile = await query.get(this.currentUser!.id);
  232. this.currentUser = profile;
  233. const data = profile.get('data') || {};
  234. // 计算统计数据
  235. await this.calculateStatistics();
  236. } catch (err) {
  237. console.error('加载个人资料失败:', err);
  238. }
  239. }
  240. /**
  241. * 加载技能评分
  242. */
  243. async loadSkillRatings() {
  244. try {
  245. const data = this.currentUser!.get('data') || {};
  246. const skills = data.skillRatings || [];
  247. // 如果没有技能评分,创建默认值
  248. if (skills.length === 0) {
  249. this.skillRatings = this.getDefaultSkillRatings();
  250. } else {
  251. this.skillRatings = skills;
  252. }
  253. } catch (err) {
  254. console.error('加载技能评分失败:', err);
  255. this.skillRatings = this.getDefaultSkillRatings();
  256. }
  257. }
  258. /**
  259. * 加载案例作品
  260. */
  261. async loadCaseWorks() {
  262. try {
  263. const data = this.currentUser!.get('data') || {};
  264. const caseProjectIds = data.caseWorks || [];
  265. if (caseProjectIds.length === 0) {
  266. this.caseWorks = [];
  267. return;
  268. }
  269. // 查询案例对应的项目
  270. const query = new Parse.Query('Project');
  271. query.containedIn('objectId', caseProjectIds);
  272. query.equalTo('currentStage', '售后归档');
  273. query.notEqualTo('isDeleted', true);
  274. query.include('contact');
  275. query.descending('updatedAt');
  276. query.limit(20);
  277. const projects = await query.find();
  278. this.caseWorks = projects.map(p => this.transformProjectToCase(p));
  279. console.log(`✅ 加载了 ${this.caseWorks.length} 个案例作品`);
  280. } catch (err) {
  281. console.error('加载案例作品失败:', err);
  282. this.caseWorks = [];
  283. }
  284. }
  285. /**
  286. * 加载月度统计
  287. */
  288. async loadMonthlyStats() {
  289. try {
  290. // 查询最近6个月的项目
  291. const sixMonthsAgo = new Date();
  292. sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
  293. const query = new Parse.Query('Project');
  294. query.equalTo('assignee', this.currentUser!.toPointer());
  295. query.greaterThanOrEqualTo('createdAt', sixMonthsAgo);
  296. query.notEqualTo('isDeleted', true);
  297. query.limit(1000);
  298. const projects = await query.find();
  299. // 按月分组统计
  300. const monthlyMap = new Map<string, MonthlyStats>();
  301. projects.forEach(p => {
  302. const date = p.get('createdAt');
  303. const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
  304. if (!monthlyMap.has(monthKey)) {
  305. monthlyMap.set(monthKey, {
  306. month: monthKey,
  307. totalProjects: 0,
  308. completedProjects: 0,
  309. revenue: 0,
  310. avgScore: 0
  311. });
  312. }
  313. const stats = monthlyMap.get(monthKey)!;
  314. stats.totalProjects++;
  315. if (p.get('currentStage') === '售后归档' || p.get('status') === '已完成') {
  316. stats.completedProjects++;
  317. // 计算收入
  318. const pricing = p.get('data')?.pricing || {};
  319. const totalPrice = pricing.totalAmount || pricing.total || pricing.finalPrice || 0;
  320. stats.revenue += totalPrice;
  321. }
  322. });
  323. // 转换为数组并排序
  324. this.monthlyStats = Array.from(monthlyMap.values())
  325. .sort((a, b) => b.month.localeCompare(a.month))
  326. .slice(0, 6);
  327. console.log(`✅ 加载了 ${this.monthlyStats.length} 个月的统计数据`);
  328. } catch (err) {
  329. console.error('加载月度统计失败:', err);
  330. this.monthlyStats = [];
  331. }
  332. }
  333. /**
  334. * 加载自我评价
  335. */
  336. async loadSelfEvaluation() {
  337. try {
  338. const data = this.currentUser!.get('data') || {};
  339. const evaluation = data.selfEvaluation;
  340. if (evaluation) {
  341. this.selfEvaluation = {
  342. strengths: evaluation.strengths || [],
  343. improvements: evaluation.improvements || [],
  344. personalStatement: evaluation.personalStatement || '',
  345. lastUpdated: evaluation.lastUpdated ? new Date(evaluation.lastUpdated) : new Date()
  346. };
  347. } else {
  348. // 默认值
  349. this.selfEvaluation = {
  350. strengths: ['专业扎实', '责任心强'],
  351. improvements: ['沟通效率', '时间管理'],
  352. personalStatement: '我是一名热爱设计的专业人士,致力于为客户提供优质的服务。',
  353. lastUpdated: new Date()
  354. };
  355. }
  356. } catch (err) {
  357. console.error('加载自我评价失败:', err);
  358. }
  359. }
  360. /**
  361. * 计算统计数据
  362. */
  363. async calculateStatistics() {
  364. try {
  365. const profilePointer = this.currentUser!.toPointer();
  366. // 查询总项目数
  367. const totalQuery = new Parse.Query('Project');
  368. totalQuery.equalTo('assignee', profilePointer);
  369. totalQuery.notEqualTo('isDeleted', true);
  370. this.totalProjects = await totalQuery.count();
  371. // 查询已完成项目数
  372. const completedQuery = new Parse.Query('Project');
  373. completedQuery.equalTo('assignee', profilePointer);
  374. completedQuery.equalTo('currentStage', '售后归档');
  375. completedQuery.notEqualTo('isDeleted', true);
  376. this.completedProjects = await completedQuery.count();
  377. // 查询本月项目数
  378. const currentMonth = new Date();
  379. currentMonth.setDate(1);
  380. currentMonth.setHours(0, 0, 0, 0);
  381. const monthQuery = new Parse.Query('Project');
  382. monthQuery.equalTo('assignee', profilePointer);
  383. monthQuery.greaterThanOrEqualTo('createdAt', currentMonth);
  384. monthQuery.notEqualTo('isDeleted', true);
  385. this.currentMonthProjects = await monthQuery.count();
  386. console.log(`✅ 统计数据:总项目=${this.totalProjects}, 已完成=${this.completedProjects}, 本月=${this.currentMonthProjects}`);
  387. } catch (err) {
  388. console.error('计算统计数据失败:', err);
  389. }
  390. }
  391. /**
  392. * 将项目转换为案例
  393. */
  394. transformProjectToCase(project: FmodeObject): CaseWork {
  395. const data = project.get('data') || {};
  396. const pricing = data.pricing || {};
  397. const contact = project.get('contact');
  398. // 获取封面图片
  399. let coverImage = '/assets/images/default-project.jpg';
  400. if (data.referenceImages && data.referenceImages.length > 0) {
  401. coverImage = data.referenceImages[0];
  402. } else if (data.deliverables && data.deliverables.length > 0) {
  403. const firstDeliverable = data.deliverables[0];
  404. if (firstDeliverable.files && firstDeliverable.files.length > 0) {
  405. coverImage = firstDeliverable.files[0];
  406. }
  407. }
  408. return {
  409. id: project.id,
  410. projectId: project.id,
  411. projectTitle: project.get('title') || '未命名项目',
  412. coverImage: coverImage,
  413. description: data.description || project.get('title') || '',
  414. tags: data.tags || data.stylePreferences || [],
  415. completionDate: project.get('updatedAt') || new Date(),
  416. customerName: contact?.get('name') || '客户',
  417. status: project.get('status') || '已完成',
  418. totalPrice: pricing.totalAmount || pricing.total || pricing.finalPrice,
  419. roomType: data.roomType || data.spaceType
  420. };
  421. }
  422. /**
  423. * 获取默认技能评分
  424. */
  425. getDefaultSkillRatings(): SkillRating[] {
  426. const role = this.currentUser?.get('roleName') || '组员';
  427. if (role === '组员' || role === '设计师') {
  428. return [
  429. { name: '空间设计', currentScore: 70, targetScore: 90, category: '设计能力' },
  430. { name: '色彩搭配', currentScore: 65, targetScore: 85, category: '设计能力' },
  431. { name: '软装搭配', currentScore: 75, targetScore: 90, category: '设计能力' },
  432. { name: '客户沟通', currentScore: 60, targetScore: 80, category: '沟通能力' },
  433. { name: '需求分析', currentScore: 65, targetScore: 85, category: '沟通能力' },
  434. { name: '3D建模', currentScore: 70, targetScore: 85, category: '技术能力' },
  435. { name: '效果图渲染', currentScore: 75, targetScore: 90, category: '技术能力' },
  436. { name: '项目管理', currentScore: 60, targetScore: 80, category: '项目管理' }
  437. ];
  438. } else if (role === '客服') {
  439. return [
  440. { name: '客户接待', currentScore: 80, targetScore: 95, category: '沟通能力' },
  441. { name: '需求挖掘', currentScore: 75, targetScore: 90, category: '沟通能力' },
  442. { name: '订单管理', currentScore: 70, targetScore: 85, category: '项目管理' },
  443. { name: '售后服务', currentScore: 75, targetScore: 90, category: '沟通能力' },
  444. { name: '问题解决', currentScore: 65, targetScore: 85, category: '项目管理' }
  445. ];
  446. }
  447. return [];
  448. }
  449. // ==================== 编辑功能 ====================
  450. /**
  451. * 打开编辑自我评价
  452. */
  453. openEditEvaluation() {
  454. this.editingEvaluation = JSON.parse(JSON.stringify(this.selfEvaluation));
  455. this.showEditEvaluation = true;
  456. }
  457. /**
  458. * 保存自我评价
  459. */
  460. async saveEvaluation() {
  461. if (!this.editingEvaluation) return;
  462. try {
  463. this.editingEvaluation.lastUpdated = new Date();
  464. const data = this.currentUser!.get('data') || {};
  465. data.selfEvaluation = this.editingEvaluation;
  466. this.currentUser!.set('data', data);
  467. await this.currentUser!.save();
  468. this.selfEvaluation = this.editingEvaluation;
  469. this.showEditEvaluation = false;
  470. this.editingEvaluation = null;
  471. window?.fmode?.alert('保存成功!');
  472. } catch (err: any) {
  473. console.error('保存自我评价失败:', err);
  474. window?.fmode?.alert('保存失败: ' + (err.message || '未知错误'));
  475. }
  476. }
  477. /**
  478. * 打开案例选择器
  479. */
  480. async openCaseSelector() {
  481. try {
  482. this.loadingMessage = '加载可选项目...';
  483. this.loading = true;
  484. // 查询已完成的项目
  485. const query = new Parse.Query('Project');
  486. query.equalTo('assignee', this.currentUser!.toPointer());
  487. query.equalTo('currentStage', '售后归档');
  488. query.notEqualTo('isDeleted', true);
  489. query.include('contact');
  490. query.descending('updatedAt');
  491. query.limit(100);
  492. this.availableProjects = await query.find();
  493. this.selectedProjectIds = this.caseWorks.map(c => c.projectId);
  494. this.showCaseSelector = true;
  495. console.log(`✅ 找到 ${this.availableProjects.length} 个可选项目`);
  496. } catch (err) {
  497. console.error('加载可选项目失败:', err);
  498. window?.fmode?.alert('加载失败,请重试');
  499. } finally {
  500. this.loading = false;
  501. }
  502. }
  503. /**
  504. * 切换项目选择
  505. */
  506. toggleProjectSelection(projectId: string) {
  507. const index = this.selectedProjectIds.indexOf(projectId);
  508. if (index > -1) {
  509. this.selectedProjectIds.splice(index, 1);
  510. } else {
  511. if (this.selectedProjectIds.length >= 12) {
  512. window?.fmode?.alert('最多选择12个案例');
  513. return;
  514. }
  515. this.selectedProjectIds.push(projectId);
  516. }
  517. }
  518. /**
  519. * 保存案例选择
  520. */
  521. async saveCaseSelection() {
  522. try {
  523. const data = this.currentUser!.get('data') || {};
  524. data.caseWorks = this.selectedProjectIds;
  525. this.currentUser!.set('data', data);
  526. await this.currentUser!.save();
  527. // 重新加载案例
  528. await this.loadCaseWorks();
  529. this.showCaseSelector = false;
  530. window?.fmode?.alert('保存成功!');
  531. } catch (err: any) {
  532. console.error('保存案例选择失败:', err);
  533. window?.fmode?.alert('保存失败: ' + (err.message || '未知错误'));
  534. }
  535. }
  536. /**
  537. * 保存技能评分
  538. */
  539. async saveSkillRatings() {
  540. try {
  541. const data = this.currentUser!.get('data') || {};
  542. data.skillRatings = this.skillRatings;
  543. this.currentUser!.set('data', data);
  544. await this.currentUser!.save();
  545. this.showSkillEditor = false;
  546. window?.fmode?.alert('保存成功!');
  547. } catch (err: any) {
  548. console.error('保存技能评分失败:', err);
  549. window?.fmode?.alert('保存失败: ' + (err.message || '未知错误'));
  550. }
  551. }
  552. // ==================== 原有群聊/联系人场景功能(保留) ====================
  553. async handleGroupChatScene() {
  554. this.loadingMessage = '查询项目信息...';
  555. const projectPointer = this.groupChat!.get('project');
  556. if (projectPointer) {
  557. let pid = projectPointer.id || projectPointer.objectId
  558. try {
  559. const query = new Parse.Query('Project');
  560. query.include('contact', 'assignee');
  561. this.project = await query.get(pid);
  562. await this.navigateToProjectDetail();
  563. } catch (err) {
  564. console.error('加载项目失败:', err);
  565. this.error = '项目已删除或无权访问';
  566. }
  567. } else {
  568. await this.loadHistoryProjects();
  569. this.showCreateProjectGuide();
  570. }
  571. }
  572. async handleContactScene() {
  573. await this.router.navigate(['/wxwork', this.cid, 'contact', this.contact!.id], {
  574. queryParams: { profileId: this.currentUser!.id }
  575. });
  576. }
  577. async loadHistoryProjects() {
  578. try {
  579. const pgQuery = new Parse.Query('ProjectGroup');
  580. pgQuery.equalTo('groupChat', this.groupChat!.toPointer());
  581. pgQuery.include('project');
  582. pgQuery.descending('createdAt');
  583. const projectGroups = await pgQuery.find();
  584. this.historyProjects = projectGroups
  585. .map((pg: any) => pg.get('project'))
  586. .filter((p: any) => p && !p.get('isDeleted'));
  587. } catch (err) {
  588. console.error('加载历史项目失败:', err);
  589. }
  590. }
  591. showCreateProjectGuide() {
  592. this.showCreateGuide = true;
  593. this.defaultProjectName = this.groupChat!.get('name') || '新项目';
  594. this.projectName = this.defaultProjectName;
  595. }
  596. async createProject() {
  597. if (!this.projectName.trim()) {
  598. window?.fmode?.alert('请输入项目名称');
  599. return;
  600. }
  601. const role = this.currentUser!.get('roleName');
  602. if (!['客服', '组长', '管理员'].includes(role)) {
  603. window?.fmode?.alert('您没有权限创建项目');
  604. return;
  605. }
  606. try {
  607. this.creating = true;
  608. const Project = Parse.Object.extend('Project');
  609. const project = new Project();
  610. project.set('title', this.projectName.trim());
  611. project.set('company', this.currentUser!.get('company'));
  612. project.set('status', '待分配');
  613. project.set('currentStage', '订单分配');
  614. project.set('data', {
  615. createdBy: this.currentUser!.id,
  616. createdFrom: 'wxwork_groupchat',
  617. groupChatId: this.groupChat!.id
  618. });
  619. await project.save();
  620. this.groupChat!.set('project', project.toPointer());
  621. await this.groupChat!.save();
  622. const ProjectGroup = Parse.Object.extend('ProjectGroup');
  623. const pg = new ProjectGroup();
  624. pg.set('project', project.toPointer());
  625. pg.set('groupChat', this.groupChat!.toPointer());
  626. pg.set('isPrimary', true);
  627. pg.set('company', this.currentUser!.get('company'));
  628. await pg.save();
  629. await this.activityLogService.logActivity({
  630. actorId: this.currentUser!.id,
  631. actorName: this.currentUser!.get('name') || '系统',
  632. actorRole: role,
  633. actionType: 'create',
  634. module: 'project',
  635. entityType: 'Project',
  636. entityId: project.id,
  637. entityName: this.projectName.trim(),
  638. description: '创建了新项目',
  639. metadata: {
  640. createdFrom: 'wxwork_groupchat',
  641. groupChatId: this.groupChat!.id,
  642. status: '待分配',
  643. stage: '订单分配'
  644. }
  645. });
  646. this.project = project;
  647. await this.navigateToProjectDetail();
  648. } catch (err: any) {
  649. console.error('创建项目失败:', err);
  650. window?.fmode?.alert('创建失败: ' + (err.message || '未知错误'));
  651. } finally {
  652. this.creating = false;
  653. }
  654. }
  655. async selectHistoryProject(project: FmodeObject) {
  656. try {
  657. this.groupChat!.set('project', project.toPointer());
  658. await this.groupChat!.save();
  659. this.project = project;
  660. await this.navigateToProjectDetail();
  661. } catch (err: any) {
  662. console.error('关联项目失败:', err);
  663. window?.fmode?.alert('关联失败: ' + (err.message || '未知错误'));
  664. }
  665. }
  666. async navigateToProjectDetail() {
  667. await this.router.navigate(['/wxwork', this.cid, 'project', this.project!.id], {
  668. queryParams: {
  669. groupId: this.groupChat?.id,
  670. profileId: this.currentUser!.id
  671. }
  672. });
  673. }
  674. // ==================== 工具方法 ====================
  675. async reload() {
  676. this.error = null;
  677. this.showCreateGuide = false;
  678. this.historyProjects = [];
  679. this.chatType = 'personal';
  680. await this.loadData();
  681. }
  682. getCurrentUserName(): string {
  683. if (!this.currentUser) return '未知';
  684. return this.currentUser.get('name') || this.currentUser.get('userid') || '未知';
  685. }
  686. getCurrentUserRole(): string {
  687. if (!this.currentUser) return '未知';
  688. return this.currentUser.get('roleName') || '未知';
  689. }
  690. getProjectStatusClass(status: string): string {
  691. const classMap: any = {
  692. '待分配': 'status-pending',
  693. '进行中': 'status-active',
  694. '已完成': 'status-completed',
  695. '已暂停': 'status-paused',
  696. '已取消': 'status-cancelled'
  697. };
  698. return classMap[status] || 'status-default';
  699. }
  700. formatDate(date: Date | string): string {
  701. if (!date) return '';
  702. const d = new Date(date);
  703. return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
  704. }
  705. formatMonth(monthStr: string): string {
  706. const [year, month] = monthStr.split('-');
  707. return `${year}年${month}月`;
  708. }
  709. formatCurrency(amount: number): string {
  710. if (!amount) return '¥0';
  711. return `¥${amount.toLocaleString()}`;
  712. }
  713. getScoreColor(score: number): string {
  714. if (score >= 80) return 'score-high';
  715. if (score >= 60) return 'score-medium';
  716. return 'score-low';
  717. }
  718. getScoreProgress(current: number, target: number): number {
  719. if (target === 0) return 0;
  720. return Math.min((current / target) * 100, 100);
  721. }
  722. filterSkillsByCategory(category: string): SkillRating[] {
  723. return this.skillRatings.filter(s => s.category === category);
  724. }
  725. getMaxMonthlyProjects(): number {
  726. if (this.monthlyStats.length === 0) return 1;
  727. return Math.max(...this.monthlyStats.map(m => m.totalProjects));
  728. }
  729. updateStrengths(value: string) {
  730. if (!this.editingEvaluation) return;
  731. this.editingEvaluation.strengths = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
  732. }
  733. updateImprovements(value: string) {
  734. if (!this.editingEvaluation) return;
  735. this.editingEvaluation.improvements = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
  736. }
  737. }