|
|
@@ -0,0 +1,1413 @@
|
|
|
+# 设计师端(组员端)企业微信身份识别与真实数据接入实施方案
|
|
|
+
|
|
|
+## 文档概述
|
|
|
+
|
|
|
+本文档详细说明如何为**设计师端(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<void> {
|
|
|
+ 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<void> {
|
|
|
+ // 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<void> {
|
|
|
+ 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<boolean> {
|
|
|
+ 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<void> {
|
|
|
+ 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<DesignerTask[]> {
|
|
|
+ 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<DesignerTask[]> {
|
|
|
+ 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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<LeaveApplication, 'id' | 'status' | 'createdAt'>
|
|
|
+ ): Promise<boolean> {
|
|
|
+ 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<LeaveApplication[]> {
|
|
|
+ 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<string, LeaveApplication>();
|
|
|
+
|
|
|
+ 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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
|
|
|
+<!-- 请假申请按钮 -->
|
|
|
+<button class="leave-btn" (click)="openLeaveModal()">
|
|
|
+ <svg viewBox="0 0 24 24">
|
|
|
+ <path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11z"/>
|
|
|
+ </svg>
|
|
|
+ 申请请假
|
|
|
+</button>
|
|
|
+
|
|
|
+<!-- 请假申请弹窗 -->
|
|
|
+@if (showLeaveModal) {
|
|
|
+ <div class="modal-overlay" (click)="closeLeaveModal()">
|
|
|
+ <div class="modal-content leave-modal" (click)="$event.stopPropagation()">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h3>申请请假</h3>
|
|
|
+ <button class="close-btn" (click)="closeLeaveModal()">×</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="modal-body">
|
|
|
+ <div class="form-group">
|
|
|
+ <label>请假类型</label>
|
|
|
+ <select [(ngModel)]="leaveForm.type" class="form-control">
|
|
|
+ <option value="annual">年假</option>
|
|
|
+ <option value="sick">病假</option>
|
|
|
+ <option value="personal">事假</option>
|
|
|
+ <option value="other">其他</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label>开始日期</label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ [(ngModel)]="leaveForm.startDate"
|
|
|
+ class="form-control"
|
|
|
+ [min]="today"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label>结束日期</label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ [(ngModel)]="leaveForm.endDate"
|
|
|
+ class="form-control"
|
|
|
+ [min]="leaveForm.startDate || today"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label>请假原因</label>
|
|
|
+ <textarea
|
|
|
+ [(ngModel)]="leaveForm.reason"
|
|
|
+ class="form-control"
|
|
|
+ rows="4"
|
|
|
+ placeholder="请输入请假原因..."
|
|
|
+ ></textarea>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="modal-footer">
|
|
|
+ <button class="btn btn-cancel" (click)="closeLeaveModal()">取消</button>
|
|
|
+ <button class="btn btn-submit" (click)="submitLeaveApplication()">提交申请</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+}
|
|
|
+
|
|
|
+<!-- 我的请假记录 -->
|
|
|
+<div class="leave-records">
|
|
|
+ <h4>我的请假记录</h4>
|
|
|
+ @if (leaveApplications.length > 0) {
|
|
|
+ <div class="records-list">
|
|
|
+ @for (leave of leaveApplications; track leave.id) {
|
|
|
+ <div class="record-item" [class]="leave.status">
|
|
|
+ <div class="record-header">
|
|
|
+ <span class="record-type">{{ getLeaveTypeText(leave.type) }}</span>
|
|
|
+ <span class="record-status" [class]="leave.status">
|
|
|
+ {{ getLeaveStatusText(leave.status) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="record-body">
|
|
|
+ <p class="record-date">
|
|
|
+ {{ leave.startDate | date:'yyyy-MM-dd' }} 至 {{ leave.endDate | date:'yyyy-MM-dd' }}
|
|
|
+ (共{{ leave.days }}天)
|
|
|
+ </p>
|
|
|
+ <p class="record-reason">{{ leave.reason }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="record-footer">
|
|
|
+ <span class="record-time">{{ leave.createdAt | date:'yyyy-MM-dd HH:mm' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ </div>
|
|
|
+ } @else {
|
|
|
+ <p class="no-records">暂无请假记录</p>
|
|
|
+ }
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+### 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<void> {
|
|
|
+ // 获取当前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<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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
|
|
|
+**维护人**:开发团队
|
|
|
+
|