组件-项目问卷.md 20 KB

项目问卷组件产品需求文档

一、概述

1.1 功能定位

项目问卷是家装效果图服务的初次合作需求调研工具,通过精简的选择式问卷快速了解客户需求、服务偏好和协作习惯,帮助团队更精准地提供服务。

1.2 业务价值

  • 客户视角: 5分钟快速完成,明确表达需求偏好,减少后期沟通成本
  • 服务视角: 提前了解客户侧重点,制定针对性服务方案,提升满意度
  • 数据视角: 积累客户需求数据,优化服务流程和质量管控点

1.3 应用场景

  1. 项目启动前: 客服在项目订单分配阶段,发送问卷给客户填写
  2. 群聊分享: 通过企微群聊直接发送问卷链接,客户点击即可填写
  3. 多客户项目: 支持一个项目多个客户联系人分别填写(如公司项目的多个负责人)
  4. 结果查看: 客服/组员/组长可随时查看客户已填写的问卷结果

二、数据范式

2.1 SurveyLog 问卷结果表

字段名 类型 必填 说明 示例值
objectId String 主键ID "survey001"
contact Pointer 提交联系人 → ContactInfo
project Pointer 关联项目 → Project
profile Pointer 提交员工(内部员工填写时使用) → Profile
company Pointer 所属帐套 → Company
type String 问卷类型 "survey-project"
data Object 问卷结果 {q1: "答案1", ...}
isCompleted Boolean 是否完整填写 true
completedAt Date 完成时间 2024-12-01T10:00:00.000Z
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-12-01T09:00:00.000Z
updatedAt Date 自动 更新时间 2024-12-01T10:00:00.000Z

type 枚举值:

  • survey-project: 项目问卷
  • survey-contact: 联系人问卷(暂未实现)
  • survey-profile: 员工问卷(暂未实现)

data 字段结构示例:

{
  "q1_service_type": "效果图+技术配合",
  "q2_space_count": "3",
  "q2_space_types": "客厅/主卧/儿童房",
  "q3_value_focus": ["细节写实度", "视觉吸引力"],
  "q4_tech_support": "需要",
  "q4_tech_focus": ["材质搭配", "灯光布局"],
  "q5_cooperation_mode": "前期多沟通",
  "q6_attention_points": ["软装色调易偏差"],
  "q7_special_requirements": "业主喜欢暖色调,注意避免冷色",
  "q8_has_reference": "有",
  "contact_name": "李总",
  "contact_phone": "13800138000"
}

三、核心组件设计

3.1 ProjectSurveyComponent 项目问卷组件

3.1.1 路由配置

// 路由: /wxwork/:cid/survey/project/:projectId
{
  path: 'wxwork/:cid',
  children: [
    {
      path: 'survey/project/:projectId',
      loadComponent: () => import('../modules/project/pages/project-survey/project-survey.component'),
      title: '项目需求调查'
    }
  ]
}

3.1.2 组件状态机

组件包含三种状态,通过 currentState 控制:

type SurveyState = 'welcome' | 'questionnaire' | 'result';

currentState: SurveyState = 'welcome';

状态转换流程:

[欢迎页] --点击开始--> [答题页] --提交完成--> [结果页]
    ↑                                              ↓
    └──────────────── 查看结果 ──────────────────┘

3.2 欢迎页 (welcome)

3.2.1 页面布局

┌─────────────────────────────────┐
│         问卷欢迎页                │
├─────────────────────────────────┤
│  [用户头像]                       │
│  您好,李总                        │
│                                  │
│  《家装效果图服务初次合作需求调查表》 │
│                                  │
│  尊敬的伙伴:                      │
│  为让本次效果图服务更贴合您的工作节  │
│  奏与核心需求,我们准备了简短选择式  │
│  问卷,您的偏好将直接帮我们校准服务  │
│  方向,感谢支持!                   │
│                                  │
│  • 预计用时: 3-5分钟               │
│  • 题目数量: 8题                  │
│  • 题型: 选择题为主                │
│                                  │
│         [开始填写]                 │
└─────────────────────────────────┘

3.2.2 功能实现

  1. 用户识别:

    • 通过 WxworkAuth.currentContact() 获取当前外部联系人
    • 显示联系人头像和名称
    • 记录 contact.id 用于后续保存
  2. 数据检查:

    • 组件初始化时查询 SurveyLog 表
    • 条件: project == projectId AND contact == contactId
    • 如果已存在且 isCompleted == true,直接跳转到结果页
  3. 开始按钮:

    • 点击后执行 startSurvey()
    • 切换状态: currentState = 'questionnaire'
    • 初始化题目索引: currentQuestionIndex = 0

3.3 答题页 (questionnaire)

3.3.1 页面布局

┌─────────────────────────────────┐
│  进度: 1/8 ●●○○○○○○            │
├─────────────────────────────────┤
│  一、基础需求                     │
│                                  │
│  1. 本次您需要的核心服务是?        │
│                                  │
│  ○ 纯效果图渲染                   │
│  ● 效果图+技术配合                │
│  ○ 其他补充: [____________]      │
│                                  │
│                                  │
│         [← 上一题]  [下一题 →]   │
└─────────────────────────────────┘

3.3.2 题目数据结构

interface Question {
  id: string;              // 题目ID,如 "q1", "q2"
  section: string;         // 章节,如 "基础需求", "核心侧重"
  title: string;           // 题目文本
  type: 'single' | 'multiple' | 'text' | 'number'; // 题型
  options?: string[];      // 选项列表
  hasOther?: boolean;      // 是否有"其他"选项
  required?: boolean;      // 是否必填
  skipCondition?: (contact: any) => boolean; // 跳过条件
}

3.3.3 题目列表

const questions: Question[] = [
  // 一、基础需求
  {
    id: 'q1',
    section: '基础需求',
    title: '本次您需要的核心服务是?',
    type: 'single',
    options: ['纯效果图渲染', '效果图+技术配合'],
    hasOther: true,
    required: true
  },
  {
    id: 'q2',
    section: '基础需求',
    title: '需覆盖的关键空间数量及类型?',
    type: 'text',
    placeholder: '例: 3个,客厅/主卧/儿童房',
    required: true
  },

  // 二、核心侧重
  {
    id: 'q3',
    section: '核心侧重',
    title: '您更希望本次效果图突出哪些价值?(可多选2-3项)',
    type: 'multiple',
    options: ['细节写实度', '视觉吸引力', '风格适配性'],
    hasOther: true,
    required: true
  },
  {
    id: 'q4',
    section: '核心侧重',
    title: '关于方案建议,是否需要我们技术团队配合?',
    type: 'single',
    options: ['需要', '暂不需要'],
    required: true
  },

  // 三、协作节奏
  {
    id: 'q5',
    section: '协作节奏',
    title: '您偏好的服务协作方式是?',
    type: 'single',
    options: ['前期多沟通', '先出初版再修改', '灵活协调'],
    required: true
  },

  // 四、特殊提醒
  {
    id: 'q6',
    section: '特殊提醒',
    title: '过往合作中,是否有需要特别注意的点?(可多选)',
    type: 'multiple',
    options: ['软装色调易偏差', '建模细节需盯控'],
    hasOther: true
  },
  {
    id: 'q7',
    section: '特殊提醒',
    title: '本次项目是否有特殊要求?(如业主禁忌、重点展示点)',
    type: 'text',
    placeholder: '请输入特殊要求...'
  },
  {
    id: 'q8',
    section: '特殊提醒',
    title: '是否有参考素材?(如风格图、实景图)',
    type: 'single',
    options: ['有(后续群内发送)', '无(需求已清晰)']
  },

  // 联系信息(自动跳过)
  {
    id: 'contact_name',
    section: '联系信息',
    title: '对接人姓名',
    type: 'text',
    required: true,
    skipCondition: (contact) => !!contact?.get('realname')
  },
  {
    id: 'contact_phone',
    section: '联系信息',
    title: '对接人电话',
    type: 'text',
    required: true,
    skipCondition: (contact) => !!contact?.get('mobile')
  }
];

3.3.4 答题交互逻辑

  1. 单选题:

    • 点击选项后自动保存答案到 answers[questionId]
    • 如果不是"其他"选项,自动跳转下一题
    • 如果是"其他"选项,显示输入框,输入完成后需手动点击"下一题"
  2. 多选题:

    • 可选择多个选项
    • 点击"下一题"后保存并跳转
  3. 文本题/数字题:

    • 输入完成后点击"下一题"
  4. 题目跳过:

    • 如果 skipCondition 返回 true,自动跳过该题
    • 例如: ContactInfo 已有手机号,跳过手机号填写
  5. 进度指示:

    • 顶部显示进度条: currentQuestionIndex / totalQuestions
    • 显示当前章节名称
  6. 导航按钮:

    • "上一题": 返回上一题,可修改答案
    • "下一题": 保存当前答案并跳转(最后一题显示"提交")

3.3.5 数据保存策略

自动保存:

  • 每答完一题后自动保存到 Parse (防止中途退出丢失数据)
  • 保存方式:

    surveyLog.set('data', {
    ...surveyLog.get('data'),
    [questionId]: answer
    });
    await surveyLog.save();
    

完成标记:

  • 最后一题提交后设置 isCompleted = true
  • 设置 completedAt = new Date()

3.4 结果页 (result)

3.4.1 页面布局

┌─────────────────────────────────┐
│  ✓ 问卷提交成功                   │
├─────────────────────────────────┤
│  感谢您的反馈!                    │
│  我们将根据您的选择制定服务方案    │
│                                  │
│  【您的答卷】                     │
│  ━━━━━━━━━━━━━━━━━━━━━━━       │
│  核心服务: 效果图+技术配合         │
│  空间数量: 3个(客厅/主卧/儿童房)   │
│  价值侧重: 细节写实度、视觉吸引力  │
│  技术配合: 需要(材质搭配、灯光布局) │
│  协作方式: 前期多沟通              │
│  注意事项: 软装色调易偏差          │
│  特殊要求: 业主喜欢暖色调          │
│  参考素材: 有(后续群内发送)        │
│  ━━━━━━━━━━━━━━━━━━━━━━━       │
│  对接人: 李总                     │
│  电话: 138****8000               │
│                                  │
│         [返回项目]                 │
└─────────────────────────────────┘

3.4.2 功能实现

  1. 结果展示:

    • 从 SurveyLog.data 读取答案
    • 格式化显示(选择题显示选项文本,文本题直接显示)
    • 手机号脱敏显示(中间4位显示为 ****)
  2. 权限控制:

    • 客户本人: 可查看完整结果(包括完整手机号)
    • 客服/组员/组长: 可查看完整结果
    • 其他外部联系人: 无权查看
  3. 返回按钮:

    • 返回项目详情页

四、项目详情页集成

4.1 客户卡片问卷状态显示

project-detail.component.html 的客户联系人卡片区域添加问卷入口:

<!-- 客户信息卡片 -->
<div class="contact-card">
  <div class="contact-info" (click)="openContactPanel()">
    <img [src]="contact?.get('data')?.avatar || 'assets/default-avatar.png'" />
    <div>
      <h3>{{ contact?.get('realname') || contact?.get('name') }}</h3>
      <p>{{ canViewCustomerPhone ? contact?.get('mobile') : '***' }}</p>
    </div>
  </div>

  <!-- 问卷状态 -->
  <div class="survey-status" (click)="handleSurveyClick($event)">
    <ion-icon [name]="surveyStatus.icon"></ion-icon>
    <span>{{ surveyStatus.text }}</span>
  </div>
</div>

4.2 问卷状态查询

project-detail.component.ts 中添加:

// 问卷状态
surveyStatus: {
  filled: boolean;
  text: string;
  icon: string;
  surveyLog?: FmodeObject;
} = {
  filled: false,
  text: '发送问卷',
  icon: 'document-text-outline'
};

async loadSurveyStatus() {
  if (!this.project?.id || !this.contact?.id) return;

  try {
    const query = new Parse.Query('SurveyLog');
    query.equalTo('project', this.project.toPointer());
    query.equalTo('contact', this.contact.toPointer());
    query.equalTo('type', 'survey-project');
    query.equalTo('isCompleted', true);
    const surveyLog = await query.first();

    if (surveyLog) {
      this.surveyStatus = {
        filled: true,
        text: '查看问卷',
        icon: 'checkmark-circle',
        surveyLog
      };
    }
  } catch (err) {
    console.error('查询问卷状态失败:', err);
  }
}

4.3 问卷发送功能

async sendSurvey() {
  if (!this.groupChat || !this.wxwork) return;

  try {
    const chatId = this.groupChat.get('chat_id');
    const surveyUrl = `${window.location.origin}/wxwork/${this.cid}/survey/project/${this.project?.id}`;

    await this.wxwork.ww.openExistedChatWithMsg({
      chatId: chatId,
      msg: {
        msgtype: 'link',
        link: {
          title: '《家装效果图服务初次合作需求调查表》',
          desc: '为让本次服务更贴合您的需求,请花3-5分钟填写简短问卷,感谢支持!',
          url: surveyUrl,
          imgUrl: `${window.location.origin}/assets/logo.jpg`
        }
      }
    });

   window?.fmode?.alert('问卷已发送到群聊!');
  } catch (err) {
    console.error('发送问卷失败:', err);
   window?.fmode?.alert('发送失败,请重试');
  }
}

4.4 问卷查看功能

// 新增模态框状态
showSurveyModal: boolean = false;
selectedSurveyLog: FmodeObject | null = null;

async viewSurvey() {
  if (!this.surveyStatus.surveyLog) return;

  this.selectedSurveyLog = this.surveyStatus.surveyLog;
  this.showSurveyModal = true;
}

async handleSurveyClick(event: Event) {
  event.stopPropagation();

  if (this.surveyStatus.filled) {
    // 已填写,查看结果
    await this.viewSurvey();
  } else {
    // 未填写,发送问卷
    await this.sendSurvey();
  }
}

五、技术实现要点

5.1 企微授权集成

import { WxworkAuth } from 'fmode-ng/core';

async ngOnInit() {
  // 1. 初始化企微授权
  const cid = this.route.snapshot.paramMap.get('cid') || '';
  this.wxAuth = new WxworkAuth({ cid, appId: 'crm' });

  // 2. 获取当前外部联系人
  try {
    this.currentContact = await this.wxAuth.currentContact();
    console.log('当前联系人:', this.currentContact);
  } catch (error) {
    console.error('获取联系人失败:', error);
   window?.fmode?.alert('无法识别您的身份,请通过企微群聊进入');
    return;
  }

  // 3. 检查是否已填写问卷
  await this.checkExistingSurvey();
}

5.2 数据查询与保存

// 查询现有问卷
async checkExistingSurvey() {
  const query = new Parse.Query('SurveyLog');
  query.equalTo('project', this.projectId);
  query.equalTo('contact', this.currentContact.toPointer());
  query.equalTo('type', 'survey-project');

  this.surveyLog = await query.first();

  if (this.surveyLog?.get('isCompleted')) {
    // 已完成,直接显示结果
    this.currentState = 'result';
  } else if (this.surveyLog) {
    // 未完成,恢复进度
    this.answers = this.surveyLog.get('data') || {};
    this.currentState = 'questionnaire';
  }
}

// 保存答案
async saveAnswer(questionId: string, answer: any) {
  if (!this.surveyLog) {
    // 首次保存,创建记录
    const SurveyLog = Parse.Object.extend('SurveyLog');
    this.surveyLog = new SurveyLog();

    const company = new Parse.Object('Company');
    company.id = localStorage.getItem('company') || '';

    const project = new Parse.Object('Project');
    project.id = this.projectId;

    this.surveyLog.set('company', company.toPointer());
    this.surveyLog.set('project', project.toPointer());
    this.surveyLog.set('contact', this.currentContact.toPointer());
    this.surveyLog.set('type', 'survey-project');
  }

  // 更新答案
  const data = this.surveyLog.get('data') || {};
  data[questionId] = answer;
  this.surveyLog.set('data', data);

  await this.surveyLog.save();
}

// 完成问卷
async completeSurvey() {
  if (!this.surveyLog) return;

  this.surveyLog.set('isCompleted', true);
  this.surveyLog.set('completedAt', new Date());
  await this.surveyLog.save();

  // 切换到结果页
  this.currentState = 'result';
}

5.3 联系人信息补全

如果问卷中填写了姓名/手机号,需要同步更新 ContactInfo 表:

async updateContactInfo() {
  const data = this.surveyLog.get('data');

  if (data.contact_name || data.contact_phone) {
    if (data.contact_name && !this.currentContact.get('realname')) {
      this.currentContact.set('realname', data.contact_name);
    }

    if (data.contact_phone && !this.currentContact.get('mobile')) {
      this.currentContact.set('mobile', data.contact_phone);
    }

    await this.currentContact.save();
  }
}

六、《家装效果图服务初次合作需求调查表》

尊敬的伙伴:

为让本次效果图服务更贴合您的工作节奏与核心需求,我们准备了简短选择式问卷,您的偏好将直接帮我们校准服务方向,感谢支持!

一、基础需求:快速明确服务范围

  1. 本次您需要的核心服务是? □ 纯效果图渲染(仅输出可视化图像) □ 效果图+技术配合(含方案相关建议) □ 其他补充:______

  2. 需覆盖的关键空间数量及类型? 数量:______个(例:3个,空间类型:客厅/主卧/儿童房)

二、核心侧重:帮我们锁定服务重点

  1. 您更希望本次效果图突出哪些价值?(可多选,选2-3项) □ 细节写实度(如空间尺寸匹配、材质还原,贴合落地需求) □ 视觉吸引力(如氛围营造、风格亮点,方便对接业主) □ 风格适配性(精准匹配预设调性,减少后期调整) □ 其他重点:______

  2. 关于方案建议,是否需要我们技术团队配合? □ 需要(侧重方向:□ 材质搭配 □ 灯光布局 □ 空间优化) □ 暂不需要(已有明确方案,仅需渲染)

三、协作节奏:匹配您的沟通习惯

  1. 您偏好的服务协作方式是? □ 前期多沟通(确认方向、细节后再推进,减少返工) □ 先出初版再修改(快速看到成果,针对性调整) □ 灵活协调(根据进度随时沟通)

四、特殊提醒:提前规避潜在偏差

  1. 过往合作中,是否有需要特别注意的点?(可多选) □ 软装色调易偏差 □ 建模细节需盯控 □ 其他:______

  2. 本次项目是否有特殊要求?(如业主禁忌、重点展示点)


  3. 是否有参考素材(如风格图、实景图)需同步? □ 有(后续群内发送) □ 无(需求已清晰)

感谢您的反馈!我们将根据您的选择制定服务方案,对接人:___(姓名),电话:___,有问题可随时联系~