# 设计师端(组员端)企业微信身份识别与真实数据接入实施方案 ## 文档概述 本文档详细说明如何为**设计师端(Designer/组员端)**优化企业微信身份识别并接入真实数据库,包括任务管理、请假申请、个人数据等功能的完整实现。 --- ## 一、现状分析 ### 1.1 设计师端当前实现情况 #### 企微认证 ✅ **已实现内容**: - ✅ 路由守卫:`WxworkAuthGuard`(`app.routes.ts` 第64行) - ✅ 组件内认证:`WxworkAuth` 实例(`dashboard.ts` 第55-72行) - ✅ 认证流程:`authenticateAndLoadData()` 方法(第79-96行) - ✅ 降级机制:认证失败时使用模拟数据 **代码位置**: ```typescript // src/app/pages/designer/dashboard/dashboard.ts export class Dashboard implements OnInit { private wxAuth: WxworkAuth | null = null; private currentUser: FmodeUser | null = null; constructor(private projectService: ProjectService) { this.initAuth(); } private initAuth(): void { this.wxAuth = new WxworkAuth({ cid: 'cDL6R1hgSi' // 公司帐套ID }); } async ngOnInit(): Promise { await this.authenticateAndLoadData(); } } ``` #### 数据接入现状 ❌ | 功能模块 | 当前状态 | 数据来源 | 问题 | |---------|---------|---------|------| | **任务列表** | ❌ 模拟数据 | `ProjectService.getTasks()` | 返回的是硬编码的模拟数据 | | **待处理反馈** | ❌ 模拟数据 | `loadPendingFeedbacks()` | 使用 mockFeedbacks 数组 | | **代班任务** | ❌ 模拟数据 | `loadShiftTasks()` | 使用 mockShiftTasks | | **工作量计算** | ❌ 模拟数据 | `calculateWorkloadPercentage()` | 基于模拟任务计算 | | **项目时间线** | ❌ 模拟数据 | `loadProjectTimeline()` | 使用 mockTimeline | | **技能标签** | ❌ 模拟数据 | `PersonalBoard.loadSkillTags()` | 从 ProjectService 获取模拟数据 | | **绩效数据** | ❌ 模拟数据 | `PersonalBoard.loadPerformanceData()` | 从 ProjectService 获取模拟数据 | | **请假申请** | ❌ 未实现 | 无 | 设计师无法申请或查看请假 | ### 1.2 核心问题 1. **数据真实性问题**:所有数据都是模拟的,无法支持真实业务 2. **功能缺失问题**:缺少请假申请、个人信息管理等关键功能 3. **角色识别问题**:虽有企微认证,但未验证是否为"组员"角色 4. **数据同步问题**:设计师的个人信息(技能、绩效)无法更新 --- ## 二、技术方案设计 ### 2.1 优化企微身份识别 #### 当前问题 - ❌ cid 硬编码在代码中(`'cDL6R1hgSi'`) - ❌ 未从URL参数获取cid - ❌ 未验证用户的"组员"角色 - ❌ 未获取当前登录的Profile信息 #### 优化方案 **第1步:修改路由配置,支持cid参数** ```typescript // src/app/app.routes.ts { path: ':cid/designer', // 添加 cid 参数 canActivate: [WxworkAuthGuard], children: [ { path: 'dashboard', loadComponent: () => import('./pages/designer/dashboard/dashboard') .then(m => m.Dashboard), title: '设计师工作台' }, // ... 其他子路由 ] } ``` **第2步:优化Dashboard组件的认证流程** ```typescript // src/app/pages/designer/dashboard/dashboard.ts import { ActivatedRoute } from '@angular/router'; import { ProfileService } from '../../../services/profile.service'; export class Dashboard implements OnInit { private wxAuth: WxworkAuth | null = null; private currentUser: FmodeUser | null = null; private currentProfile: FmodeObject | null = null; // 新增:当前Profile private cid: string = ''; constructor( private projectService: ProjectService, private route: ActivatedRoute, // 新增 private router: Router, // 新增 private profileService: ProfileService // 新增 ) {} async ngOnInit(): Promise { // 1. 从URL获取cid this.route.paramMap.subscribe(async params => { this.cid = params.get('cid') || localStorage.getItem('company') || ''; if (!this.cid) { console.error('❌ 未找到公司ID'); alert('缺少公司信息,请联系管理员'); return; } // 2. 初始化企微认证 this.initAuth(); // 3. 执行认证并加载数据 await this.authenticateAndLoadData(); }); } // 初始化企业微信认证(修改版) private initAuth(): void { try { this.wxAuth = new WxworkAuth({ cid: this.cid, // 使用动态获取的cid appId: 'crm' }); console.log('✅ 设计师端企微认证初始化成功'); } catch (error) { console.error('❌ 设计师端企微认证初始化失败:', error); } } // 认证并加载数据(优化版) private async authenticateAndLoadData(): Promise { try { // 执行企业微信认证和登录 const { user, profile } = await this.wxAuth!.authenticateAndLogin(); this.currentUser = user; this.currentProfile = profile; if (!user || !profile) { console.error('❌ 设计师登录失败'); this.loadMockData(); return; } console.log('✅ 设计师登录成功:', user.get('username')); console.log('✅ Profile ID:', profile.id); // 验证角色是否为"组员" if (!await this.validateDesignerRole()) { alert('您不是设计师,无权访问此页面'); this.router.navigate(['/']); return; } // 缓存Profile ID localStorage.setItem('Parse/ProfileId', profile.id); // 加载真实数据 await this.loadDashboardData(); } catch (error) { console.error('❌ 设计师认证过程出错:', error); // 降级到模拟数据 this.loadMockData(); } } /** * 验证设计师(组员)角色 */ private async validateDesignerRole(): Promise { if (!this.currentProfile) { return false; } const roleName = this.currentProfile.get('roleName'); if (roleName !== '组员') { console.warn(`⚠️ 用户角色为"${roleName}",不是"组员"`); return false; } console.log('✅ 角色验证通过:组员(设计师)'); return true; } } ``` ### 2.2 任务数据真实接入 #### 创建DesignerTaskService ```typescript // src/app/services/designer-task.service.ts import { Injectable } from '@angular/core'; import { FmodeParse } from 'fmode-ng/parse'; export interface DesignerTask { id: string; projectId: string; projectName: string; stage: string; deadline: Date; isOverdue: boolean; priority: 'high' | 'medium' | 'low'; customerName: string; space?: string; // 空间名称(如"主卧") productId?: string; // 关联的Product ID } @Injectable({ providedIn: 'root' }) export class DesignerTaskService { private Parse: any = null; private cid: string = ''; constructor() { this.initParse(); } private async initParse(): Promise { try { const { FmodeParse } = await import('fmode-ng/parse'); this.Parse = FmodeParse.with('nova'); this.cid = localStorage.getItem('company') || ''; } catch (error) { console.error('DesignerTaskService: Parse初始化失败:', error); } } /** * 获取当前设计师的任务列表 * @param designerId Profile的objectId */ async getMyTasks(designerId: string): Promise { if (!this.Parse) await this.initParse(); if (!this.Parse || !this.cid) return []; try { // 方案1:从ProjectTeam表查询(设计师实际负责的项目) const teamQuery = new this.Parse.Query('ProjectTeam'); teamQuery.equalTo('profile', this.Parse.Object.extend('Profile').createWithoutData(designerId)); teamQuery.notEqualTo('isDeleted', true); teamQuery.include('project'); teamQuery.include('project.contact'); teamQuery.limit(1000); const teamRecords = await teamQuery.find(); if (teamRecords.length === 0) { console.warn('⚠️ 未找到分配给该设计师的项目'); return []; } const tasks: DesignerTask[] = []; for (const teamRecord of teamRecords) { const project = teamRecord.get('project'); if (!project) continue; const projectId = project.id; const projectName = project.get('title') || '未命名项目'; const currentStage = project.get('currentStage') || '未知'; const deadline = project.get('deadline') || new Date(); const contact = project.get('contact'); const customerName = contact?.get('name') || '未知客户'; // 查询该项目下该设计师负责的Product(空间设计产品) const productQuery = new this.Parse.Query('Product'); productQuery.equalTo('project', project); productQuery.equalTo('profile', this.Parse.Object.extend('Profile').createWithoutData(designerId)); productQuery.notEqualTo('isDeleted', true); productQuery.containedIn('status', ['in_progress', 'awaiting_review']); const products = await productQuery.find(); if (products.length === 0) { // 如果没有具体的Product,创建项目级任务 tasks.push({ id: projectId, projectId, projectName, stage: currentStage, deadline: new Date(deadline), isOverdue: new Date(deadline) < new Date(), priority: this.calculatePriority(deadline, currentStage), customerName }); } else { // 如果有Product,为每个Product创建任务 products.forEach((product: any) => { const productName = product.get('productName') || '未命名空间'; const productStage = product.get('stage') || currentStage; tasks.push({ id: `${projectId}-${product.id}`, projectId, projectName: `${projectName} - ${productName}`, stage: productStage, deadline: new Date(deadline), isOverdue: new Date(deadline) < new Date(), priority: this.calculatePriority(deadline, productStage), customerName, space: productName, productId: product.id }); }); } } // 按截止日期排序 tasks.sort((a, b) => a.deadline.getTime() - b.deadline.getTime()); console.log(`✅ 成功加载 ${tasks.length} 个任务`); return tasks; } catch (error) { console.error('获取设计师任务失败:', error); return []; } } /** * 计算任务优先级 */ private calculatePriority(deadline: Date, stage: string): 'high' | 'medium' | 'low' { const now = new Date(); const daysLeft = Math.ceil((new Date(deadline).getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); // 超期或临期(3天内) if (daysLeft < 0 || daysLeft <= 3) { return 'high'; } // 渲染阶段优先级高 if (stage === 'rendering' || stage === '渲染') { return 'high'; } // 7天内 if (daysLeft <= 7) { return 'medium'; } return 'low'; } /** * 方案2:从Project.assignee查询(如果不使用ProjectTeam表) */ async getMyTasksFromAssignee(designerId: string): Promise { if (!this.Parse) await this.initParse(); if (!this.Parse || !this.cid) return []; try { const query = new this.Parse.Query('Project'); query.equalTo('assignee', this.Parse.Object.extend('Profile').createWithoutData(designerId)); query.equalTo('company', this.cid); query.containedIn('status', ['进行中', '待审核']); query.notEqualTo('isDeleted', true); query.include('contact'); query.ascending('deadline'); query.limit(1000); const projects = await query.find(); return projects.map((project: any) => { const deadline = project.get('deadline') || new Date(); const currentStage = project.get('currentStage') || '未知'; const contact = project.get('contact'); return { id: project.id, projectId: project.id, projectName: project.get('title') || '未命名项目', stage: currentStage, deadline: new Date(deadline), isOverdue: new Date(deadline) < new Date(), priority: this.calculatePriority(deadline, currentStage), customerName: contact?.get('name') || '未知客户' }; }); } catch (error) { console.error('从Project.assignee获取任务失败:', error); return []; } } } ``` #### 修改Dashboard组件使用真实数据 ```typescript // src/app/pages/designer/dashboard/dashboard.ts import { DesignerTaskService } from '../../../services/designer-task.service'; export class Dashboard implements OnInit { constructor( private projectService: ProjectService, private route: ActivatedRoute, private router: Router, private profileService: ProfileService, private taskService: DesignerTaskService // 新增 ) {} // 加载仪表板数据(修改版) private async loadDashboardData(): Promise { try { if (!this.currentProfile) { throw new Error('未找到当前Profile'); } await Promise.all([ this.loadRealTasks(), // 使用真实数据 this.loadShiftTasks(), this.calculateWorkloadPercentage(), this.loadProjectTimeline() ]); console.log('✅ 设计师仪表板数据加载完成'); } catch (error) { console.error('❌ 设计师仪表板数据加载失败:', error); throw error; } } /** * 加载真实任务数据 */ private async loadRealTasks(): Promise { try { const designerTasks = await this.taskService.getMyTasks(this.currentProfile!.id); // 转换为组件所需格式 this.tasks = designerTasks.map(task => ({ id: task.id, projectId: task.projectId, name: task.projectName, stage: task.stage, deadline: task.deadline, isOverdue: task.isOverdue, priority: task.priority, customerName: task.customerName })); // 筛选超期任务 this.overdueTasks = this.tasks.filter(task => task.isOverdue); // 筛选紧急任务 this.urgentTasks = this.tasks.filter(task => { const now = new Date(); const diffHours = (task.deadline.getTime() - now.getTime()) / (1000 * 60 * 60); return diffHours <= 3 && diffHours > 0 && task.stage === '渲染'; }); // 加载待处理反馈 await this.loadRealPendingFeedbacks(); // 启动倒计时 this.startCountdowns(); console.log(`✅ 成功加载 ${this.tasks.length} 个真实任务`); } catch (error) { console.error('❌ 加载真实任务失败:', error); throw error; } } /** * 加载真实待处理反馈 */ private async loadRealPendingFeedbacks(): Promise { try { const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova')); // 查询该设计师相关项目的待处理反馈 const projectIds = this.tasks.map(t => t.projectId); const query = new Parse.Query('ProjectFeedback'); query.containedIn('project', projectIds.map(id => Parse.Object.extend('Project').createWithoutData(id) )); query.equalTo('status', '待处理'); query.notEqualTo('isDeleted', true); query.include('project'); query.include('contact'); query.descending('createdAt'); query.limit(100); const feedbacks = await query.find(); this.pendingFeedbacks = feedbacks.map((feedback: any) => { const project = feedback.get('project'); const task = this.tasks.find(t => t.projectId === project?.id); return { task: task || { id: project?.id || '', projectId: project?.id || '', name: project?.get('title') || '未知项目', stage: '反馈处理', deadline: new Date(), isOverdue: false }, feedback: { id: feedback.id, content: feedback.get('content') || '', rating: feedback.get('rating'), createdAt: feedback.get('createdAt') } }; }); console.log(`✅ 成功加载 ${this.pendingFeedbacks.length} 个待处理反馈`); } catch (error) { console.error('❌ 加载待处理反馈失败:', error); this.pendingFeedbacks = []; } } } ``` ### 2.3 请假申请功能实现 #### 创建LeaveService(设计师端使用) ```typescript // src/app/services/leave.service.ts import { Injectable } from '@angular/core'; import { FmodeParse } from 'fmode-ng/parse'; export interface LeaveApplication { id?: string; startDate: Date; endDate: Date; type: 'annual' | 'sick' | 'personal' | 'other'; reason: string; status: 'pending' | 'approved' | 'rejected'; days: number; createdAt?: Date; } @Injectable({ providedIn: 'root' }) export class LeaveService { private Parse: any = null; private cid: string = ''; constructor() { this.initParse(); } private async initParse(): Promise { try { const { FmodeParse } = await import('fmode-ng/parse'); this.Parse = FmodeParse.with('nova'); this.cid = localStorage.getItem('company') || ''; } catch (error) { console.error('LeaveService: Parse初始化失败:', error); } } /** * 提交请假申请 */ async submitLeaveApplication( designerId: string, application: Omit ): Promise { if (!this.Parse) await this.initParse(); if (!this.Parse || !this.cid) return false; try { // 方案1:添加到Profile.data.leave.records const query = new this.Parse.Query('Profile'); const profile = await query.get(designerId); const data = profile.get('data') || {}; const leaveData = data.leave || { records: [], statistics: {} }; const records = leaveData.records || []; // 生成请假日期列表 const leaveDates: string[] = []; const currentDate = new Date(application.startDate); const endDate = new Date(application.endDate); while (currentDate <= endDate) { leaveDates.push(currentDate.toISOString().split('T')[0]); currentDate.setDate(currentDate.getDate() + 1); } // 添加每一天的请假记录 leaveDates.forEach(date => { records.push({ id: `leave-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, date, type: application.type, status: 'pending', // 待审批 reason: application.reason, createdAt: new Date().toISOString(), days: application.days }); }); leaveData.records = records; data.leave = leaveData; profile.set('data', data); await profile.save(); console.log('✅ 请假申请提交成功'); return true; } catch (error) { console.error('❌ 提交请假申请失败:', error); return false; } } /** * 获取我的请假记录 */ async getMyLeaveRecords(designerId: string): Promise { if (!this.Parse) await this.initParse(); if (!this.Parse) return []; try { const query = new this.Parse.Query('Profile'); const profile = await query.get(designerId); const data = profile.get('data') || {}; const leaveData = data.leave || { records: [] }; const records = leaveData.records || []; // 按日期聚合成请假申请 const applications = new Map(); records.forEach((record: any) => { const key = `${record.type}-${record.reason}-${record.status}`; if (!applications.has(key)) { applications.set(key, { id: record.id, startDate: new Date(record.date), endDate: new Date(record.date), type: record.type, reason: record.reason, status: record.status, days: 1, createdAt: new Date(record.createdAt) }); } else { const app = applications.get(key)!; const recordDate = new Date(record.date); if (recordDate < app.startDate) { app.startDate = recordDate; } if (recordDate > app.endDate) { app.endDate = recordDate; } app.days++; } }); return Array.from(applications.values()) .sort((a, b) => b.createdAt!.getTime() - a.createdAt!.getTime()); } catch (error) { console.error('❌ 获取请假记录失败:', error); return []; } } /** * 计算请假天数(排除周末) */ calculateLeaveDays(startDate: Date, endDate: Date): number { let days = 0; const currentDate = new Date(startDate); while (currentDate <= endDate) { const dayOfWeek = currentDate.getDay(); // 排除周六(6)和周日(0) if (dayOfWeek !== 0 && dayOfWeek !== 6) { days++; } currentDate.setDate(currentDate.getDate() + 1); } return days; } } ``` #### 在Dashboard中添加请假申请功能 ```typescript // src/app/pages/designer/dashboard/dashboard.ts import { LeaveService, LeaveApplication } from '../../../services/leave.service'; export class Dashboard implements OnInit { // 请假相关 showLeaveModal: boolean = false; leaveApplications: LeaveApplication[] = []; // 请假表单 leaveForm = { startDate: '', endDate: '', type: 'personal' as 'annual' | 'sick' | 'personal' | 'other', reason: '' }; constructor( private projectService: ProjectService, private route: ActivatedRoute, private router: Router, private profileService: ProfileService, private taskService: DesignerTaskService, private leaveService: LeaveService // 新增 ) {} /** * 打开请假申请弹窗 */ openLeaveModal(): void { this.showLeaveModal = true; } /** * 关闭请假申请弹窗 */ closeLeaveModal(): void { this.showLeaveModal = false; this.resetLeaveForm(); } /** * 提交请假申请 */ async submitLeaveApplication(): Promise { if (!this.currentProfile) { alert('未找到当前用户信息'); return; } // 验证表单 if (!this.leaveForm.startDate || !this.leaveForm.endDate) { alert('请选择请假日期'); return; } if (!this.leaveForm.reason.trim()) { alert('请输入请假原因'); return; } const startDate = new Date(this.leaveForm.startDate); const endDate = new Date(this.leaveForm.endDate); if (startDate > endDate) { alert('结束日期不能早于开始日期'); return; } // 计算请假天数 const days = this.leaveService.calculateLeaveDays(startDate, endDate); if (days === 0) { alert('请假天数必须大于0(周末不计入)'); return; } try { const success = await this.leaveService.submitLeaveApplication( this.currentProfile.id, { startDate, endDate, type: this.leaveForm.type, reason: this.leaveForm.reason, days } ); if (success) { alert(`请假申请已提交!共${days}天(已排除周末)`); this.closeLeaveModal(); await this.loadMyLeaveRecords(); } else { alert('请假申请提交失败,请重试'); } } catch (error) { console.error('提交请假申请失败:', error); alert('请假申请提交失败,请重试'); } } /** * 加载我的请假记录 */ private async loadMyLeaveRecords(): Promise { if (!this.currentProfile) return; try { this.leaveApplications = await this.leaveService.getMyLeaveRecords( this.currentProfile.id ); console.log(`✅ 成功加载 ${this.leaveApplications.length} 条请假记录`); } catch (error) { console.error('❌ 加载请假记录失败:', error); } } /** * 重置请假表单 */ private resetLeaveForm(): void { this.leaveForm = { startDate: '', endDate: '', type: 'personal', reason: '' }; } } ``` #### 请假申请UI(添加到dashboard.html) ```html @if (showLeaveModal) { }

我的请假记录

@if (leaveApplications.length > 0) {
@for (leave of leaveApplications; track leave.id) {
{{ getLeaveTypeText(leave.type) }} {{ getLeaveStatusText(leave.status) }}

{{ leave.startDate | date:'yyyy-MM-dd' }} 至 {{ leave.endDate | date:'yyyy-MM-dd' }} (共{{ leave.days }}天)

{{ leave.reason }}

}
} @else {

暂无请假记录

}
``` ### 2.4 个人数据(技能标签、绩效)真实接入 #### 从Profile.data读取真实技能标签 ```typescript // src/app/pages/designer/personal-board/personal-board.ts export class PersonalBoard implements OnInit { private currentProfile: FmodeObject | null = null; async ngOnInit(): Promise { // 获取当前Profile const profileId = localStorage.getItem('Parse/ProfileId'); if (profileId) { await this.loadCurrentProfile(profileId); await this.loadRealSkillTags(); await this.loadRealPerformanceData(); } else { // 降级到模拟数据 this.loadSkillTags(); this.loadPerformanceData(); } } /** * 加载当前Profile */ private async loadCurrentProfile(profileId: string): Promise { try { const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova')); const query = new Parse.Query('Profile'); this.currentProfile = await query.get(profileId); console.log('✅ 成功加载Profile'); } catch (error) { console.error('❌ 加载Profile失败:', error); } } /** * 从Profile.data.tags读取真实技能标签 */ private async loadRealSkillTags(): Promise { if (!this.currentProfile) return; try { const data = this.currentProfile.get('data') || {}; const tags = data.tags || {}; const expertise = tags.expertise || {}; // 转换为组件所需格式 this.skillTags = []; // 添加擅长风格 (expertise.styles || []).forEach((style: string) => { this.skillTags.push({ name: style, level: 85, // 可以从数据中读取实际等级 category: '风格' }); }); // 添加专业技能 (expertise.skills || []).forEach((skill: string) => { this.skillTags.push({ name: skill, level: 80, category: '技能' }); }); // 添加擅长空间 (expertise.spaceTypes || []).forEach((space: string) => { this.skillTags.push({ name: space, level: 75, category: '空间' }); }); console.log(`✅ 成功加载 ${this.skillTags.length} 个技能标签`); } catch (error) { console.error('❌ 加载技能标签失败:', error); } } /** * 从Profile.data.tags.history读取真实绩效数据 */ private async loadRealPerformanceData(): Promise { if (!this.currentProfile) return; try { const data = this.currentProfile.get('data') || {}; const tags = data.tags || {}; const history = tags.history || {}; this.performanceData = { totalProjects: history.totalProjects || 0, completionRate: history.completionRate || 0, onTimeRate: history.onTimeRate || 0, excellentRate: history.excellentCount ? (history.excellentCount / history.totalProjects * 100) : 0, avgRating: history.avgRating || 0 }; console.log('✅ 成功加载绩效数据'); } catch (error) { console.error('❌ 加载绩效数据失败:', error); } } } ``` --- ## 三、实施步骤 ### 阶段1:优化企微认证(第1天上午) **任务清单**: - [ ] 修改路由配置,添加 `:cid` 参数 - [ ] 优化Dashboard的 `initAuth()` 方法,支持动态cid - [ ] 实现 `validateDesignerRole()` 角色验证 - [ ] 测试企微授权流程 **验收标准**: - ✅ 访问设计师端自动触发企微授权 - ✅ 非组员角色无法访问 - ✅ 可以正确获取当前Profile信息 ### 阶段2:任务数据真实接入(第1天下午) **任务清单**: - [ ] 创建 `DesignerTaskService` - [ ] 实现 `getMyTasks()` 方法 - [ ] 修改Dashboard的 `loadRealTasks()` 方法 - [ ] 实现 `loadRealPendingFeedbacks()` 方法 - [ ] 测试任务数据加载 **验收标准**: - ✅ 可以看到真实分配的任务 - ✅ 超期任务、紧急任务正确标识 - ✅ 待处理反馈正确显示 ### 阶段3:请假申请功能(第2天上午) **任务清单**: - [ ] 创建 `LeaveService` - [ ] 实现请假申请表单UI - [ ] 实现 `submitLeaveApplication()` 方法 - [ ] 实现请假记录查看功能 - [ ] 测试请假申请流程 **验收标准**: - ✅ 设计师可以提交请假申请 - ✅ 请假记录正确保存 - ✅ 可以查看自己的请假历史 ### 阶段4:个人数据真实接入(第2天下午) **任务清单**: - [ ] 修改PersonalBoard的数据加载逻辑 - [ ] 实现 `loadRealSkillTags()` 方法 - [ ] 实现 `loadRealPerformanceData()` 方法 - [ ] 测试个人看板数据显示 **验收标准**: - ✅ 技能标签从Profile.data读取 - ✅ 绩效数据正确显示 - ✅ 数据更新后立即反映 ### 阶段5:测试与优化(第3天) **任务清单**: - [ ] 全流程测试 - [ ] 性能优化(缓存、懒加载) - [ ] UI/UX优化 - [ ] 错误处理完善 - [ ] 编写使用文档 --- ## 四、数据初始化 ### 为现有设计师初始化数据 ```typescript // scripts/init-designer-data.ts import { FmodeParse } from 'fmode-ng/parse'; async function initDesignerData() { const Parse = FmodeParse.with('nova'); const cid = 'cDL6R1hgSi'; // 公司ID // 查询所有组员 const query = new Parse.Query('Profile'); query.equalTo('company', cid); query.equalTo('roleName', '组员'); query.notEqualTo('isDeleted', true); const designers = await query.find(); for (const designer of designers) { const data = designer.get('data') || {}; // 初始化tags结构(如果不存在) if (!data.tags) { data.tags = { expertise: { styles: ['现代简约', '北欧风格'], skills: ['3D建模', '效果图渲染'], spaceTypes: ['客厅', '卧室'] }, capacity: { weeklyProjects: 3, maxConcurrent: 5, avgDaysPerProject: 10 }, emergency: { willing: false, premium: 0, maxPerWeek: 0 }, history: { totalProjects: 0, completionRate: 0, avgRating: 0, onTimeRate: 0, excellentCount: 0 }, portfolio: [] }; } // 初始化leave结构(如果不存在) if (!data.leave) { data.leave = { records: [], statistics: { annualTotal: 10, annualUsed: 0, sickUsed: 0, personalUsed: 0 } }; } designer.set('data', data); await designer.save(); console.log(`✅ 为 ${designer.get('name')} 初始化数据`); } console.log('✅ 所有设计师数据初始化完成'); } // 执行 initDesignerData().catch(console.error); ``` --- ## 五、关键技术要点 ### 5.1 Parse查询优化 ```typescript // 1. 批量查询减少请求 await Promise.all([ query1.find(), query2.find(), query3.find() ]); // 2. 使用include减少请求次数 query.include('project', 'project.contact', 'profile'); // 3. 限制返回字段 query.select('title', 'deadline', 'status'); // 4. 添加索引字段 query.equalTo('status', 'in_progress'); // status字段需要索引 ``` ### 5.2 错误处理 ```typescript // 统一错误处理 try { const tasks = await this.taskService.getMyTasks(designerId); // 处理成功逻辑 } catch (error) { console.error('加载任务失败:', error); // 降级到模拟数据或提示用户 this.showErrorMessage('加载任务失败,请刷新重试'); } ``` ### 5.3 数据缓存 ```typescript // 缓存Profile信息 if (profile?.id) { localStorage.setItem('Parse/ProfileId', profile.id); // 可以考虑缓存整个profile数据 localStorage.setItem('Parse/ProfileData', JSON.stringify(profile.toJSON())); } // 读取缓存 const cachedProfileData = localStorage.getItem('Parse/ProfileData'); if (cachedProfileData) { this.currentProfile = JSON.parse(cachedProfileData); } ``` --- ## 六、测试方案 ### 6.1 企微认证测试 | 场景 | 操作 | 预期结果 | |------|------|----------| | 首次访问 | 访问设计师端 | 跳转企微授权 | | 授权成功 | 完成企微授权 | 自动登录并进入工作台 | | 角色不匹配 | 用其他角色访问 | 提示"您不是设计师" | | 已登录 | 再次访问 | 直接进入(无需重复授权) | ### 6.2 任务数据测试 | 场景 | 操作 | 预期结果 | |------|------|----------| | 有任务 | 加载工作台 | 显示所有分配的任务 | | 无任务 | 加载工作台 | 显示"暂无任务" | | 超期任务 | 查看任务列表 | 超期任务标红显示 | | 紧急任务 | 查看任务列表 | 紧急任务优先显示 | ### 6.3 请假申请测试 | 场景 | 操作 | 预期结果 | |------|------|----------| | 提交申请 | 填写并提交 | 成功提示,数据保存 | | 查看记录 | 打开请假记录 | 显示所有历史申请 | | 日期验证 | 选择错误日期 | 提示错误并阻止提交 | --- ## 七、FAQ ### Q1: 为什么要优化企微认证? **A**: 虽然设计师端已有企微认证,但: - cid硬编码,无法支持多公司 - 未验证"组员"角色 - 未缓存Profile信息 ### Q2: 任务数据从哪个表查询? **A**: 推荐使用 `ProjectTeam` 表,因为: - ✅ 更准确:反映实际执行人 - ✅ 支持多人协作 - ✅ 可按Product(空间)粒度管理 降级方案:从 `Project.assignee` 查询 ### Q3: 请假数据存在哪里? **A**: 推荐存在 `Profile.data.leave`: - ✅ 实施快速 - ✅ 无需新建表 - ✅ 查询简单 如需复杂审批流程,可创建独立的 `Leave` 表 ### Q4: 如何处理周末请假? **A**: 使用 `calculateLeaveDays()` 方法自动排除周末: ```typescript calculateLeaveDays(startDate, endDate); // 自动排除周六日 ``` --- ## 八、后续优化建议 ### 8.1 短期(1-2周) 1. **工作负载可视化** - 添加个人工作量甘特图 - 显示任务优先级排序 - 支持任务拖拽调整 2. **协作功能** - 支持任务转交 - 支持请求协助 - 团队消息通知 3. **移动端优化** - 响应式布局优化 - 手势操作支持 - 离线数据缓存 ### 8.2 长期(1-3月) 1. **智能助手** - AI推荐最优任务顺序 - 自动预警风险任务 - 智能工作量评估 2. **成长体系** - 技能成长追踪 - 绩效趋势分析 - 个人成就系统 3. **系统集成** - 对接项目管理系统 - 对接设计软件(3DMax、Photoshop) - 对接企业微信群聊 --- ## 九、参考文档 ### 9.1 内部文档 - `rules/wxwork/auth.md` - 企微认证API - `rules/schemas.md` - 数据表结构 - `src/app/services/project.service.ts` - 原有服务参考 ### 9.2 已实现功能参考 - 设计师端认证:`src/app/pages/designer/dashboard/dashboard.ts` - 组长端数据服务:`src/app/pages/team-leader/services/designer.service.ts` --- ## 十、总结 本方案通过以下步骤实现设计师端的完整功能: 1. **优化企微认证**:支持动态cid、角色验证、Profile缓存 2. **任务数据接入**:创建DesignerTaskService,从ProjectTeam/Product表查询真实任务 3. **请假申请功能**:创建LeaveService,支持申请和查看请假记录 4. **个人数据接入**:从Profile.data读取技能标签和绩效数据 **预计工作量**:3个工作日 **实施难度**:中等 **风险等级**:低 实施完成后,设计师端将具备: - ✅ 完整的企微身份识别 - ✅ 真实的任务数据展示 - ✅ 便捷的请假申请流程 - ✅ 准确的个人数据展示 为设计师提供高效、智能的工作管理平台! --- **文档版本**:v1.0 **创建日期**:2024-12-24 **最后更新**:2024-12-24 **维护人**:开发团队