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

## 一、概述

### 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 字段结构示例**:
```json
{
  "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 路由配置
```typescript
// 路由: /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` 控制:

```typescript
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 题目数据结构
```typescript
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 题目列表
```typescript
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 (防止中途退出丢失数据)
- 保存方式:
  ```typescript
  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` 的客户联系人卡片区域添加问卷入口:

```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` 中添加:

```typescript
// 问卷状态
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 问卷发送功能

```typescript
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('发送失败,请重试');
  }
}
```

### 4.4 问卷查看功能

```typescript
// 新增模态框状态
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 企微授权集成

```typescript
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();
}
```

### 5.2 数据查询与保存

```typescript
// 查询现有问卷
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 表:

```typescript
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个,空间类型:客厅/主卧/儿童房)


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

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


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


### 四、特殊提醒:提前规避潜在偏差
6. 过往合作中,是否有需要特别注意的点?(可多选)
   □ 软装色调易偏差 □ 建模细节需盯控 □ 其他:______

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

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


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