# 项目问卷组件产品需求文档 ## 一、概述 ### 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

{{ contact?.get('realname') || contact?.get('name') }}

{{ canViewCustomerPhone ? contact?.get('mobile') : '***' }}

{{ surveyStatus.text }}
``` ### 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` } } }); window?.fmode?.alert('问卷已发送到群聊!'); } catch (err) { console.error('发送问卷失败:', err); window?.fmode?.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); window?.fmode?.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. 是否有参考素材(如风格图、实景图)需同步? □ 有(后续群内发送) □ 无(需求已清晰) 感谢您的反馈!我们将根据您的选择制定服务方案,对接人:______(姓名),电话:______,有问题可随时联系~