员工身份激活与问卷整合方案.md 34 KB

员工身份激活与问卷整合方案

一、需求概述

1.1 业务目标

基于现有企微身份认证功能,增加员工问卷填写问卷查看功能:

  • 员工端:首次登录完成企微认证后,引导填写问卷
  • 管理端:组长/管理员可以查看员工完整信息和问卷答案

注意:企业微信身份激活功能已存在,本方案复用现有认证流程,仅增加问卷引导和查看功能。

1.2 核心场景

场景1:员工首次登录(企微端)

员工打开企微应用
    ↓
[企微自动认证] ← 现有功能
    ├─ WxworkAuth.authenticate()
    ├─ 获取企微用户信息
    └─ 自动登录/注册
    ↓
[检查问卷状态] ← 新增功能
    ↓ 未填写
[引导填写问卷弹窗] ← 新增功能
    ├─ 提示:完善能力画像
    ├─ [稍后填写] [立即填写]
    └─ 点击"立即填写"
    ↓
[员工问卷页面] ← 已完成
    ├─ 填写17道调研题目
    ├─ 逐题自动保存
    └─ 提交完成
    ↓
[问卷完成提示] ← 新增功能
    ├─ 显示能力画像
    └─ 进入工作台

场景2:组长查看设计师详情

组长工作台
    ↓
点击设计师卡片/列表
    ↓
[设计师详情弹窗]
    ├─ Tab1: 基本信息
    ├─ Tab2: 负载概况
    ├─ Tab3: 负载详细日历
    └─ Tab4: 能力问卷 ← 新增
        ├─ 显示问卷填写状态
        ├─ 查看完整问卷答案
        └─ 查看能力画像摘要

二、数据模型设计

2.1 Profile 表扩展(员工表)

需要在现有 Profile 表中增加以下字段:

字段名 类型 说明 示例值
isActivated Boolean 是否已激活 true
activatedAt Date 激活时间 2025-10-30T10:00:00Z
surveyCompleted Boolean 是否完成问卷 true
surveyCompletedAt Date 问卷完成时间 2025-10-30T10:30:00Z
surveyLogId String 关联的SurveyLog ID "survey_xxx"

这些字段用于快速判断员工状态,避免每次都查询 SurveyLog 表

2.2 SurveyLog 表(复用现有)

已实现,用于存储问卷答案:

字段名 类型 说明
profile Pointer → Profile 关联员工
type String 'survey-profile'
data Object 问卷答案
isCompleted Boolean 是否完成
completedAt Date 完成时间

三、核心功能设计

注意:企微身份认证已完成,本方案仅需增加问卷引导和查看功能

3.1 工作台问卷引导(修改现有组件)

组件路径:根据项目架构,可能在以下位置之一

  • src/app/pages/designer/dashboard/dashboard.ts (设计师工作台)
  • src/modules/wxwork/pages/home/home.component.ts (企微端首页)

3.1.1 增加问卷状态检查

在工作台组件中增加

export class Dashboard implements OnInit {
  private wxAuth: WxworkAuth | null = null;
  private currentProfile: FmodeObject | null = null;
  
  // 新增:问卷相关属性
  showSurveyGuide: boolean = false;
  surveyCompleted: boolean = false;

  async ngOnInit() {
    // 1. 企微认证(现有功能)
    await this.authenticateAndLoadData();
    
    // 2. 检查问卷状态(新增功能)
    await this.checkSurveyStatus();
    
    // 3. 显示问卷引导(新增功能)
    if (!this.surveyCompleted) {
      this.showSurveyGuide = true;
    }
  }

  /**
   * 认证并加载数据(现有方法)
   */
  private async authenticateAndLoadData(): Promise<void> {
    if (!this.wxAuth) return;

    try {
      // 执行企微认证
      await this.wxAuth.authenticate();
      
      // 获取当前员工
      this.currentProfile = await this.wxAuth.currentProfile();
      
      console.log('当前员工:', this.currentProfile);
      
      // 加载工作台数据
      await this.loadDashboardData();
    } catch (error) {
      console.error('认证或加载数据失败:', error);
      // 使用模拟数据降级
      this.loadMockData();
    }
  }

  /**
   * 检查问卷状态(新增方法)
   */
  async checkSurveyStatus() {
    if (!this.currentProfile?.id) return;

    try {
      const Parse = (window as any).Parse;
      const query = new Parse.Query('SurveyLog');
      query.equalTo('profile', this.currentProfile.toPointer());
      query.equalTo('type', 'survey-profile');
      query.equalTo('isCompleted', true);
      
      const surveyLog = await query.first();
      this.surveyCompleted = !!surveyLog;
      
      console.log('问卷状态:', this.surveyCompleted ? '已填写' : '未填写');
    } catch (err) {
      console.error('检查问卷状态失败:', err);
    }
  }

  /**
   * 跳转到问卷页面(新增方法)
   */
  goToSurvey() {
    const cid = this.wxAuth?.config.cid || '';
    window.location.href = `/wxwork/${cid}/survey/profile`;
  }

  /**
   * 关闭问卷引导(新增方法)
   */
  closeSurveyGuide() {
    this.showSurveyGuide = false;
    // 可以记录到localStorage,避免每次都弹出
    localStorage.setItem('survey_guide_closed', 'true');
  }
}

3.1.2 问卷引导弹窗(新增HTML)

在工作台HTML中增加

<!-- 问卷引导弹窗 -->
@if (showSurveyGuide) {
  <div class="survey-guide-overlay" (click)="closeSurveyGuide()">
    <div class="survey-guide-modal" (click)="$event.stopPropagation()">
      <button class="close-btn" (click)="closeSurveyGuide()">
        <ion-icon name="close"></ion-icon>
      </button>
      
      <div class="guide-content">
        <div class="guide-icon">
          <ion-icon name="clipboard-outline"></ion-icon>
        </div>
        
        <h2>完善您的能力画像</h2>
        
        <p class="guide-description">
          为了更精准地为您匹配合适的项目,<br/>
          请花8-10分钟完成能力调研问卷。
        </p>
        
        <ul class="guide-benefits">
          <li>
            <ion-icon name="checkmark-circle"></ion-icon>
            <span>智能匹配项目,发挥您的专长</span>
          </li>
          <li>
            <ion-icon name="checkmark-circle"></ion-icon>
            <span>避免不匹配项目导致返工</span>
          </li>
          <li>
            <ion-icon name="checkmark-circle"></ion-icon>
            <span>合理分配工作量,提升效率</span>
          </li>
        </ul>
        
        <div class="guide-actions">
          <button class="btn-secondary" (click)="closeSurveyGuide()">
            稍后填写
          </button>
          <button class="btn-primary" (click)="goToSurvey()">
            <span>立即填写</span>
            <ion-icon name="arrow-forward"></ion-icon>
          </button>
        </div>
        
        <p class="guide-hint">
          <ion-icon name="time-outline"></ion-icon>
          预计用时:8-10分钟
        </p>
      </div>
    </div>
  </div>
}

3.1.3 问卷引导样式(新增SCSS)

// 问卷引导弹窗样式
.survey-guide-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  animation: fadeIn 0.3s ease;
}

.survey-guide-modal {
  background: white;
  border-radius: 16px;
  padding: 32px;
  max-width: 480px;
  width: 90%;
  position: relative;
  animation: slideUp 0.3s ease;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);

  .close-btn {
    position: absolute;
    top: 16px;
    right: 16px;
    width: 32px;
    height: 32px;
    border: none;
    background: #f3f4f6;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.2s;

    &:hover {
      background: #e5e7eb;
    }

    ion-icon {
      width: 20px;
      height: 20px;
    }
  }

  .guide-content {
    text-align: center;

    .guide-icon {
      width: 80px;
      height: 80px;
      margin: 0 auto 24px;
      background: linear-gradient(135deg, #3B82F6, #60A5FA);
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;

      ion-icon {
        width: 40px;
        height: 40px;
        color: white;
      }
    }

    h2 {
      margin: 0 0 16px;
      font-size: 24px;
      font-weight: 700;
      color: #1F2937;
    }

    .guide-description {
      margin: 0 0 24px;
      font-size: 15px;
      line-height: 1.6;
      color: #6B7280;
    }

    .guide-benefits {
      list-style: none;
      padding: 0;
      margin: 0 0 32px;
      text-align: left;

      li {
        display: flex;
        align-items: center;
        gap: 12px;
        padding: 12px;
        margin-bottom: 8px;
        background: #EFF6FF;
        border-radius: 8px;

        ion-icon {
          width: 20px;
          height: 20px;
          color: #3B82F6;
          flex-shrink: 0;
        }

        span {
          font-size: 14px;
          color: #1F2937;
        }
      }
    }

    .guide-actions {
      display: flex;
      gap: 12px;
      margin-bottom: 16px;

      button {
        flex: 1;
        padding: 14px 24px;
        border: none;
        border-radius: 8px;
        font-size: 16px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.2s;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 8px;

        ion-icon {
          width: 18px;
          height: 18px;
        }
      }

      .btn-secondary {
        background: #F3F4F6;
        color: #1F2937;

        &:hover {
          background: #E5E7EB;
        }
      }

      .btn-primary {
        background: linear-gradient(135deg, #3B82F6, #60A5FA);
        color: white;
        box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);

        &:hover {
          transform: translateY(-2px);
          box-shadow: 0 6px 16px rgba(59, 130, 246, 0.5);
        }
      }
    }

    .guide-hint {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      font-size: 13px;
      color: #6B7280;
      margin: 0;

      ion-icon {
        width: 16px;
        height: 16px;
      }
    }
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

3.2 员工详情弹窗扩展(修改现有)

组件路径src/app/pages/team-leader/components/designer-detail-modal.component.ts

3.2.1 增加"能力问卷"Tab

HTML 模板修改

<div class="modal-tabs">
  <button [class.active]="currentTab === 'info'" (click)="currentTab = 'info'">
    基本信息
  </button>
  <button [class.active]="currentTab === 'workload'" (click)="currentTab = 'workload'">
    负载概况
  </button>
  <button [class.active]="currentTab === 'calendar'" (click)="currentTab = 'calendar'">
    负载详细日历
  </button>
  <!-- 新增:能力问卷Tab -->
  <button [class.active]="currentTab === 'survey'" (click)="currentTab = 'survey'; loadSurvey()">
    能力问卷
    @if (!designer.surveyCompleted) {
      <span class="badge-warning">未填写</span>
    } @else {
      <span class="badge-success">✓</span>
    }
  </button>
</div>

<div class="modal-body">
  <!-- 现有Tab内容... -->

  <!-- 新增:能力问卷Tab -->
  @if (currentTab === 'survey') {
    <div class="survey-tab">
      @if (!surveyLoading && !designer.surveyCompleted) {
        <!-- 未填写状态 -->
        <div class="empty-state">
          <ion-icon name="document-text-outline" class="empty-icon"></ion-icon>
          <h3>该员工尚未填写能力问卷</h3>
          <p>问卷数据用于智能订单分配</p>
          <button class="btn-primary" (click)="sendSurveyReminder()">
            <ion-icon name="mail-outline"></ion-icon>
            发送填写提醒
          </button>
        </div>
      } @else if (surveyLoading) {
        <!-- 加载中 -->
        <div class="loading-state">
          <ion-spinner></ion-spinner>
          <p>加载问卷数据...</p>
        </div>
      } @else {
        <!-- 已填写:显示问卷内容 -->
        <div class="survey-content">
          <!-- 能力画像摘要卡片 -->
          <div class="capability-card">
            <h3>
              <ion-icon name="analytics-outline"></ion-icon>
              能力画像摘要
            </h3>
            <div class="capability-grid">
              <div class="capability-item">
                <label>擅长风格</label>
                <div class="tags">
                  @for (style of surveyData.q1_expertise_styles; track style) {
                    <span class="tag tag-blue">{{ style }}</span>
                  }
                </div>
              </div>
              <div class="capability-item">
                <label>擅长空间</label>
                <div class="tags">
                  @for (space of surveyData.q2_expertise_spaces; track space) {
                    <span class="tag tag-green">{{ space }}</span>
                  }
                </div>
              </div>
              <div class="capability-item">
                <label>技术优势</label>
                <div class="tags">
                  @for (adv of surveyData.q3_technical_advantages; track adv) {
                    <span class="tag tag-purple">{{ adv }}</span>
                  }
                </div>
              </div>
              <div class="capability-item">
                <label>项目难度</label>
                <span class="badge badge-primary">{{ surveyData.q5_project_difficulty }}</span>
              </div>
              <div class="capability-item">
                <label>周承接量</label>
                <span>{{ surveyData.q7_weekly_capacity }}</span>
              </div>
              <div class="capability-item">
                <label>紧急订单</label>
                <span>
                  {{ surveyData.q8_urgent_willingness }}
                  @if (surveyData.q8_urgent_limit) {
                    <span class="hint">(每月不超过{{ surveyData.q8_urgent_limit }}次)</span>
                  }
                </span>
              </div>
            </div>
          </div>

          <!-- 详细问卷答案(可折叠) -->
          <div class="survey-details">
            <button class="accordion-header" (click)="toggleDetails()">
              <h3>详细问卷答案</h3>
              <ion-icon [name]="showDetails ? 'chevron-up' : 'chevron-down'"></ion-icon>
            </button>
            
            @if (showDetails) {
              <div class="accordion-content">
                <!-- 一、核心技术能力 -->
                <div class="survey-section">
                  <h4>一、核心技术能力</h4>
                  <div class="answer-item">
                    <label>擅长风格:</label>
                    <span>{{ formatArrayAnswer(surveyData.q1_expertise_styles) }}</span>
                  </div>
                  <div class="answer-item">
                    <label>擅长空间:</label>
                    <span>{{ formatArrayAnswer(surveyData.q2_expertise_spaces) }}</span>
                  </div>
                  <div class="answer-item">
                    <label>技术优势:</label>
                    <span>{{ formatArrayAnswer(surveyData.q3_technical_advantages) }}</span>
                  </div>
                </div>

                <!-- 二、项目经验与案例 -->
                <div class="survey-section">
                  <h4>二、项目经验与案例</h4>
                  <div class="case-card">
                    <h5>📋 代表性案例1</h5>
                    <p><strong>项目类型:</strong>{{ surveyData.q4_case_1_type }}</p>
                    <p><strong>核心亮点:</strong>{{ surveyData.q4_case_1_highlight }}</p>
                  </div>
                  @if (surveyData.q4_case_2_type) {
                    <div class="case-card">
                      <h5>📋 代表性案例2</h5>
                      <p><strong>项目类型:</strong>{{ surveyData.q4_case_2_type }}</p>
                      <p><strong>核心亮点:</strong>{{ surveyData.q4_case_2_highlight }}</p>
                    </div>
                  }
                  <div class="answer-item">
                    <label>项目难度:</label>
                    <span>{{ surveyData.q5_project_difficulty }}</span>
                  </div>
                </div>

                <!-- 三、项目承接偏好 -->
                <div class="survey-section">
                  <h4>三、项目承接偏好</h4>
                  <div class="answer-item">
                    <label>偏好项目类型:</label>
                    <span>{{ formatArrayAnswer(surveyData.q6_prefer_project_types) }}</span>
                  </div>
                  <div class="answer-item">
                    <label>周承接量:</label>
                    <span>{{ surveyData.q7_weekly_capacity }}</span>
                  </div>
                  <div class="answer-item">
                    <label>紧急订单意愿:</label>
                    <span>{{ surveyData.q8_urgent_willingness }}</span>
                  </div>
                </div>

                <!-- 四、协作与交付习惯 -->
                <div class="survey-section">
                  <h4>四、协作与交付习惯</h4>
                  <div class="answer-item">
                    <label>进度反馈:</label>
                    <span>{{ surveyData.q9_progress_feedback }}</span>
                  </div>
                  <div class="answer-item">
                    <label>交付确认:</label>
                    <span>{{ formatArrayAnswer(surveyData.q10_delivery_confirmation) }}</span>
                  </div>
                  <div class="answer-item">
                    <label>需求清晰度:</label>
                    <span>{{ surveyData.q11_requirement_clarity }}</span>
                  </div>
                  <div class="answer-item">
                    <label>沟通方式:</label>
                    <span>{{ formatArrayAnswer(surveyData.q12_communication_methods) }}</span>
                  </div>
                </div>

                <!-- 五、问题应对与风险预警 -->
                <div class="survey-section">
                  <h4>五、问题应对与风险预警</h4>
                  <div class="answer-item">
                    <label>敏感词了解:</label>
                    <span>{{ surveyData.q13_sensitive_words_awareness }}</span>
                  </div>
                  <div class="answer-item">
                    <label>问题处理:</label>
                    <span>{{ surveyData.q14_problem_handling }}</span>
                  </div>
                  <div class="answer-item">
                    <label>任务通知:</label>
                    <span>{{ formatArrayAnswer(surveyData.q15_task_notification) }}</span>
                  </div>
                </div>

                <!-- 六、补充说明 -->
                @if (surveyData.q16_cannot_accept || surveyData.q17_additional_notes) {
                  <div class="survey-section">
                    <h4>六、补充说明</h4>
                    @if (surveyData.q16_cannot_accept) {
                      <div class="answer-item">
                        <label>无法承接类型:</label>
                        <span>{{ surveyData.q16_cannot_accept }}</span>
                      </div>
                    }
                    @if (surveyData.q17_additional_notes) {
                      <div class="answer-item">
                        <label>其他补充:</label>
                        <span>{{ surveyData.q17_additional_notes }}</span>
                      </div>
                    }
                  </div>
                }
              </div>
            }
          </div>

          <!-- 问卷填写时间 -->
          <div class="survey-footer">
            <ion-icon name="time-outline"></ion-icon>
            <span>填写时间:{{ surveyCompletedAt | date:'yyyy-MM-dd HH:mm' }}</span>
            <button class="btn-text" (click)="sendSurveyUpdateReminder()">
              通知更新问卷
            </button>
          </div>
        </div>
      }
    </div>
  }
</div>

3.2.2 TypeScript 逻辑

export class DesignerDetailModalComponent implements OnInit {
  // 现有属性...
  
  // 新增:问卷相关属性
  currentTab: string = 'info';
  surveyLoading: boolean = false;
  surveyData: any = null;
  surveyCompletedAt: Date | null = null;
  showDetails: boolean = false;

  async ngOnInit() {
    // 加载基本信息
    await this.loadDesignerInfo();
    
    // 检查问卷状态
    await this.checkSurveyStatus();
  }

  /**
   * 检查员工问卷状态
   */
  async checkSurveyStatus() {
    if (!this.designer?.id) return;

    try {
      const query = new Parse.Query('SurveyLog');
      query.equalTo('profile', this.designer.toPointer());
      query.equalTo('type', 'survey-profile');
      query.equalTo('isCompleted', true);
      query.descending('updatedAt');
      
      const surveyLog = await query.first();
      
      if (surveyLog) {
        this.designer.surveyCompleted = true;
        this.designer.surveyCompletedAt = surveyLog.get('completedAt');
      } else {
        this.designer.surveyCompleted = false;
      }
    } catch (err) {
      console.error('检查问卷状态失败:', err);
    }
  }

  /**
   * 加载问卷数据
   */
  async loadSurvey() {
    if (!this.designer?.surveyCompleted || this.surveyData) return;

    this.surveyLoading = true;
    
    try {
      const query = new Parse.Query('SurveyLog');
      query.equalTo('profile', this.designer.toPointer());
      query.equalTo('type', 'survey-profile');
      query.equalTo('isCompleted', true);
      query.descending('updatedAt');
      
      const surveyLog = await query.first();
      
      if (surveyLog) {
        this.surveyData = surveyLog.get('data') || {};
        this.surveyCompletedAt = surveyLog.get('completedAt');
      }
    } catch (err) {
      console.error('加载问卷数据失败:', err);
      window?.fmode?.alert('加载问卷数据失败,请重试');
    } finally {
      this.surveyLoading = false;
    }
  }

  /**
   * 切换详细答案显示
   */
  toggleDetails() {
    this.showDetails = !this.showDetails;
  }

  /**
   * 格式化数组答案
   */
  formatArrayAnswer(arr: string[] | string): string {
    if (Array.isArray(arr)) {
      return arr.join('、');
    }
    return arr || '未填写';
  }

  /**
   * 发送填写提醒
   */
  async sendSurveyReminder() {
    if (!this.designer?.id) return;

    try {
      // TODO: 通过企微发送消息提醒填写问卷
      const surveyUrl = `${window.location.origin}/wxwork/${this.cid}/profile/activation`;
      
      // 调用企微API发送消息
      // await this.wxwork.sendMessage({
      //   touser: this.designer.get('userid'),
      //   msgtype: 'text',
      //   text: {
      //     content: `请完成员工能力问卷填写:${surveyUrl}`
      //   }
      // });
      
      window?.fmode?.alert('已发送填写提醒');
    } catch (err) {
      console.error('发送提醒失败:', err);
      window?.fmode?.alert('发送提醒失败,请重试');
    }
  }

  /**
   * 发送更新提醒
   */
  async sendSurveyUpdateReminder() {
    if (!this.designer?.id) return;

    try {
      const surveyUrl = `${window.location.origin}/wxwork/${this.cid}/survey/profile`;
      window?.fmode?.alert('已通知员工更新问卷');
    } catch (err) {
      console.error('发送更新提醒失败:', err);
      window?.fmode?.alert('发送更新提醒失败,请重试');
    }
  }
}

四、完整实施流程

简化说明:由于企微身份认证已完成,本方案只需增加问卷引导和查看功能

4.1 Phase 1:工作台增加问卷引导(4小时)

步骤1:修改工作台组件 TypeScript

  • 文件src/app/pages/designer/dashboard/dashboard.ts
  • 修改内容
    1. 增加问卷状态属性
    2. 增加 checkSurveyStatus() 方法
    3. 增加 goToSurvey() 方法
    4. 增加 closeSurveyGuide() 方法
    5. ngOnInit() 中调用问卷检查

步骤2:增加问卷引导弹窗 HTML

  • 文件src/app/pages/designer/dashboard/dashboard.html
  • 修改内容
    1. 添加问卷引导弹窗HTML代码
    2. 添加条件显示逻辑 @if (showSurveyGuide)

步骤3:添加弹窗样式 SCSS

  • 文件src/app/pages/designer/dashboard/dashboard.scss
  • 修改内容
    1. 添加 .survey-guide-overlay 样式
    2. 添加 .survey-guide-modal 样式
    3. 添加动画效果

4.2 Phase 2:员工详情弹窗扩展(6小时)

步骤1:修改组件 HTML

  • 文件:参考图二所示的设计师详情弹窗组件
  • 修改内容
    1. 增加"能力问卷"Tab
    2. 实现问卷数据展示界面
    3. 添加折叠/展开功能
    4. 添加未填写状态展示

步骤2:实现数据加载逻辑

  • 修改内容
    1. 增加 checkSurveyStatus() 方法
    2. 增加 loadSurvey() 方法
    3. 增加 formatArrayAnswer() 方法
    4. 增加 toggleDetails() 方法

步骤3:添加提醒功能

  • 修改内容
    1. 增加 sendSurveyReminder() 方法(未填写时)
    2. 增加 sendSurveyUpdateReminder() 方法(已填写时)
    3. 集成企微消息发送API

4.3 Phase 3:测试和优化(2小时)

测试项:

  1. 首次登录是否弹出问卷引导
  2. 点击"立即填写"跳转正确
  3. 点击"稍后填写"关闭弹窗
  4. 已填写问卷不再弹出引导
  5. 组长查看员工详情正常显示问卷
  6. 未填写问卷显示提示和发送提醒按钮
  7. 问卷数据格式化正确显示

4.4 Phase 4:路由配置(已完成)

员工问卷路由已配置:

// src/app/app.routes.ts
{
  path: 'survey/profile',
  loadComponent: () => import('../modules/profile/pages/profile-survey/profile-survey.component').then(m => m.ProfileSurveyComponent),
  title: '员工技能调研'
}

无需额外配置路由。


五、UI/UX 设计规范

5.1 员工激活页面

主题色

  • 主色:蓝色 #3B82F6
  • 强调色:橙色 #F59E0B
  • 成功色:绿色 #10B981

布局

  • 最大宽度:640px
  • 表单卡片:白色背景,圆角12px,阴影
  • 按钮:渐变背景,高度48px

5.2 员工详情弹窗问卷Tab

布局结构

能力画像摘要卡片(固定显示)
    ↓
详细问卷答案(可折叠)
    ↓
问卷填写时间 + 操作按钮

样式规范

  • 标签(Tag):蓝色系(风格)、绿色系(空间)、紫色系(优势)
  • 徽章(Badge):项目难度使用主色徽章
  • 折叠按钮:右侧显示箭头图标

六、数据流程图

6.1 员工激活流程

sequenceDiagram
    participant E as 员工端
    participant A as 激活组件
    participant P as Profile表
    participant S as SurveyLog表

    E->>A: 打开企微应用
    A->>A: 检查 isActivated
    alt 未激活
        A->>E: 显示身份填写页面
        E->>A: 填写姓名、手机号等
        A->>P: 保存基本信息
        A->>E: 显示问卷页面
        E->>A: 填写17道题目
        A->>S: 保存问卷答案
        A->>P: 更新 isActivated=true, surveyCompleted=true
        A->>E: 显示激活成功页面
    else 已激活
        A->>E: 进入工作台
    end

6.2 组长查看问卷流程

sequenceDiagram
    participant L as 组长端
    participant D as 详情弹窗
    participant S as SurveyLog表

    L->>D: 点击设计师卡片
    D->>D: 加载基本信息
    L->>D: 点击"能力问卷"Tab
    D->>S: 查询 SurveyLog (type='survey-profile')
    alt 已填写
        S->>D: 返回问卷数据
        D->>L: 显示能力画像 + 详细答案
    else 未填写
        D->>L: 显示"未填写"状态
        L->>D: 点击"发送填写提醒"
        D-->>员工: 发送企微消息
    end

七、测试用例

7.1 员工激活测试

用例ID 测试场景 操作步骤 预期结果
ACT-01 首次登录激活 1. 新员工首次登录
2. 自动跳转激活页面
显示身份填写表单
ACT-02 身份信息验证 1. 不填姓名点击下一步
2. 填写错误手机号
显示验证错误提示
ACT-03 头像上传 1. 点击头像上传
2. 选择图片
预览上传的头像
ACT-04 问卷填写 1. 完成身份填写
2. 进入问卷页面
3. 填写17道题
逐题保存,最后提交成功
ACT-05 激活成功 1. 完成所有步骤
2. 查看成功页面
显示欢迎信息和能力画像
ACT-06 重复访问 1. 已激活员工再次登录 直接进入工作台,不显示激活页面

7.2 组长查看问卷测试

用例ID 测试场景 操作步骤 预期结果
SUR-01 查看已填写问卷 1. 组长打开设计师详情
2. 点击"能力问卷"Tab
显示完整问卷数据和能力画像
SUR-02 查看未填写状态 1. 查看未填写问卷的员工 显示"未填写"提示和发送提醒按钮
SUR-03 发送填写提醒 1. 点击"发送填写提醒"按钮 成功发送企微消息给员工
SUR-04 折叠详细答案 1. 点击"详细问卷答案"
2. 再次点击
答案区域展开/折叠
SUR-05 问卷数据格式 1. 查看各类题型答案 多选题用顿号分隔,文本题完整显示

八、实施时间表

简化后的时间表:基于现有功能,只需增加问卷引导和查看

阶段 任务 预计工时 负责人
Phase 1 工作台增加问卷引导弹窗 4h 前端开发
Phase 2 员工详情弹窗增加问卷Tab 6h 前端开发
Phase 3 测试和优化 2h 前端开发
总计 12小时

对比原方案

  • ✅ 节省14小时(无需开发激活组件)
  • ✅ 复用现有企微认证流程
  • ✅ 复用现有员工问卷组件
  • ✅ 仅需增加UI引导和数据展示

九、技术要点

9.1 员工激活状态检查

// 检查员工激活状态
async checkActivationStatus(): Promise<ActivationStatus> {
  const profile = await this.wxAuth.currentProfile();
  
  return {
    isActivated: profile.get('isActivated') || false,
    surveyCompleted: profile.get('surveyCompleted') || false,
    needsActivation: !profile.get('isActivated'),
    needsSurvey: profile.get('isActivated') && !profile.get('surveyCompleted')
  };
}

9.2 问卷数据查询优化

// 批量查询多个员工的问卷状态(用于列表展示)
async batchLoadSurveyStatus(profiles: FmodeObject[]): Promise<Map<string, boolean>> {
  const profileIds = profiles.map(p => p.id);
  
  const query = new Parse.Query('SurveyLog');
  query.containedIn('profile', profileIds.map(id => ({
    __type: 'Pointer',
    className: 'Profile',
    objectId: id
  })));
  query.equalTo('type', 'survey-profile');
  query.equalTo('isCompleted', true);
  query.select('profile');
  
  const results = await query.find();
  
  const statusMap = new Map<string, boolean>();
  results.forEach(log => {
    const profileId = log.get('profile')?.id;
    if (profileId) {
      statusMap.set(profileId, true);
    }
  });
  
  return statusMap;
}

9.3 Profile表更新逻辑

// 激活完成后更新Profile
async completeActivation(profile: FmodeObject, surveyLogId: string) {
  profile.set('isActivated', true);
  profile.set('activatedAt', new Date());
  profile.set('surveyCompleted', true);
  profile.set('surveyCompletedAt', new Date());
  profile.set('surveyLogId', surveyLogId);
  
  await profile.save();
}

十、后续优化方向

10.1 智能订单匹配

基于问卷数据自动匹配最适合的设计师:

  • 风格匹配度算法
  • 负载平衡算法
  • 紧急订单优先级算法

10.2 团队能力分析

在管理后台增加团队能力分析页面:

  • 风格分布饼图
  • 技能矩阵雷达图
  • 承接能力统计

10.3 问卷版本管理

支持问卷题目迭代:

  • 记录问卷版本号
  • 对比不同版本答案变化
  • 提醒员工更新旧版本问卷

10.4 数据导出

支持导出员工能力数据:

  • Excel格式导出
  • PDF格式能力报告
  • 团队能力分析报告

十一、附录

附录A:API接口清单

接口路径 方法 说明
/api/profile/activate POST 激活员工账号
/api/profile/check-status GET 检查激活状态
/api/survey/save POST 保存问卷答案
/api/survey/get GET 获取问卷数据
/api/survey/send-reminder POST 发送填写提醒

附录B:数据库表结构

Profile 表字段(新增)

ALTER TABLE Profile ADD COLUMN isActivated BOOLEAN DEFAULT FALSE;
ALTER TABLE Profile ADD COLUMN activatedAt TIMESTAMP NULL;
ALTER TABLE Profile ADD COLUMN surveyCompleted BOOLEAN DEFAULT FALSE;
ALTER TABLE Profile ADD COLUMN surveyCompletedAt TIMESTAMP NULL;
ALTER TABLE Profile ADD COLUMN surveyLogId VARCHAR(255) NULL;

附录C:企微消息模板

填写提醒消息

{
  "msgtype": "text",
  "text": {
    "content": "【员工能力调研】\n\n您好,请完成员工能力问卷填写,这将帮助我们更好地为您匹配合适的项目。\n\n点击链接填写:[链接]\n\n预计用时:8-10分钟"
  }
}

更新提醒消息

{
  "msgtype": "text",
  "text": {
    "content": "【问卷更新提醒】\n\n您好,为了更准确地匹配项目,建议您更新一下能力问卷信息。\n\n点击链接更新:[链接]"
  }
}

文档版本:v1.0
创建时间:2025-10-31
最后更新:2025-10-31
状态:待评审