项目问卷是家装效果图服务的初次合作需求调研工具,通过精简的选择式问卷快速了解客户需求、服务偏好和协作习惯,帮助团队更精准地提供服务。
| 字段名 | 类型 | 必填 | 说明 | 示例值 | 
|---|---|---|---|---|
| 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"
}
// 路由: /wxwork/:cid/survey/project/:projectId
{
  path: 'wxwork/:cid',
  children: [
    {
      path: 'survey/project/:projectId',
      loadComponent: () => import('../modules/project/pages/project-survey/project-survey.component'),
      title: '项目需求调查'
    }
  ]
}
组件包含三种状态,通过 currentState 控制:
type SurveyState = 'welcome' | 'questionnaire' | 'result';
currentState: SurveyState = 'welcome';
状态转换流程:
[欢迎页] --点击开始--> [答题页] --提交完成--> [结果页]
    ↑                                              ↓
    └──────────────── 查看结果 ──────────────────┘
┌─────────────────────────────────┐
│         问卷欢迎页                │
├─────────────────────────────────┤
│  [用户头像]                       │
│  您好,李总                        │
│                                  │
│  《家装效果图服务初次合作需求调查表》 │
│                                  │
│  尊敬的伙伴:                      │
│  为让本次效果图服务更贴合您的工作节  │
│  奏与核心需求,我们准备了简短选择式  │
│  问卷,您的偏好将直接帮我们校准服务  │
│  方向,感谢支持!                   │
│                                  │
│  • 预计用时: 3-5分钟               │
│  • 题目数量: 8题                  │
│  • 题型: 选择题为主                │
│                                  │
│         [开始填写]                 │
└─────────────────────────────────┘
用户识别:
WxworkAuth.currentContact() 获取当前外部联系人contact.id 用于后续保存数据检查:
project == projectId AND contact == contactIdisCompleted == true,直接跳转到结果页开始按钮:
startSurvey()currentState = 'questionnaire'currentQuestionIndex = 0┌─────────────────────────────────┐
│  进度: 1/8 ●●○○○○○○            │
├─────────────────────────────────┤
│  一、基础需求                     │
│                                  │
│  1. 本次您需要的核心服务是?        │
│                                  │
│  ○ 纯效果图渲染                   │
│  ● 效果图+技术配合                │
│  ○ 其他补充: [____________]      │
│                                  │
│                                  │
│         [← 上一题]  [下一题 →]   │
└─────────────────────────────────┘
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; // 跳过条件
}
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')
  }
];
单选题:
answers[questionId]多选题:
文本题/数字题:
题目跳过:
skipCondition 返回 true,自动跳过该题进度指示:
currentQuestionIndex / totalQuestions导航按钮:
自动保存:
保存方式:
surveyLog.set('data', {
...surveyLog.get('data'),
[questionId]: answer
});
await surveyLog.save();
完成标记:
isCompleted = truecompletedAt = new Date()┌─────────────────────────────────┐
│  ✓ 问卷提交成功                   │
├─────────────────────────────────┤
│  感谢您的反馈!                    │
│  我们将根据您的选择制定服务方案    │
│                                  │
│  【您的答卷】                     │
│  ━━━━━━━━━━━━━━━━━━━━━━━       │
│  核心服务: 效果图+技术配合         │
│  空间数量: 3个(客厅/主卧/儿童房)   │
│  价值侧重: 细节写实度、视觉吸引力  │
│  技术配合: 需要(材质搭配、灯光布局) │
│  协作方式: 前期多沟通              │
│  注意事项: 软装色调易偏差          │
│  特殊要求: 业主喜欢暖色调          │
│  参考素材: 有(后续群内发送)        │
│  ━━━━━━━━━━━━━━━━━━━━━━━       │
│  对接人: 李总                     │
│  电话: 138****8000               │
│                                  │
│         [返回项目]                 │
└─────────────────────────────────┘
结果展示:
权限控制:
返回按钮:
在 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>
在 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);
  }
}
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`
        }
      }
    });
    alert('问卷已发送到群聊!');
  } catch (err) {
    console.error('发送问卷失败:', err);
    alert('发送失败,请重试');
  }
}
// 新增模态框状态
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();
  }
}
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);
    alert('无法识别您的身份,请通过企微群聊进入');
    return;
  }
  // 3. 检查是否已填写问卷
  await this.checkExistingSurvey();
}
// 查询现有问卷
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';
}
如果问卷中填写了姓名/手机号,需要同步更新 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();
  }
}
为让本次效果图服务更贴合您的工作节奏与核心需求,我们准备了简短选择式问卷,您的偏好将直接帮我们校准服务方向,感谢支持!
本次您需要的核心服务是? □ 纯效果图渲染(仅输出可视化图像) □ 效果图+技术配合(含方案相关建议) □ 其他补充:______
需覆盖的关键空间数量及类型? 数量:______个(例:3个,空间类型:客厅/主卧/儿童房)
您更希望本次效果图突出哪些价值?(可多选,选2-3项) □ 细节写实度(如空间尺寸匹配、材质还原,贴合落地需求) □ 视觉吸引力(如氛围营造、风格亮点,方便对接业主) □ 风格适配性(精准匹配预设调性,减少后期调整) □ 其他重点:______
关于方案建议,是否需要我们技术团队配合? □ 需要(侧重方向:□ 材质搭配 □ 灯光布局 □ 空间优化) □ 暂不需要(已有明确方案,仅需渲染)
过往合作中,是否有需要特别注意的点?(可多选) □ 软装色调易偏差 □ 建模细节需盯控 □ 其他:______
本次项目是否有特殊要求?(如业主禁忌、重点展示点)
是否有参考素材(如风格图、实景图)需同步? □ 有(后续群内发送) □ 无(需求已清晰)
感谢您的反馈!我们将根据您的选择制定服务方案,对接人:___(姓名),电话:___,有问题可随时联系~