Jelajahi Sumber

feat: 增加团队背景调查步骤

Developer 3 minggu lalu
induk
melakukan
15fc7e46ae

+ 5 - 3
components/CusNotice/index2.vue

@@ -3,8 +3,10 @@
 		<view class="notice-box adffc">
 			<view class="notice-box-title">问卷答题说明</view>
 			<image class="notice-box-close" :src="imgBase+'remind_close.png'" @click="close"></image>
-			<view class="notice-box-content">
-				<div class="notice-box-content-p1" style="margin-top: 36rpx;">创衡国际PERILL团队发展动态评估旨在深入了解支撑高价值团队的关键因素。这些关键因素涵盖六个维度:宗旨和动机、外部流程/系统和结构、人际关系、外部流程/系统和结构、学习、领导力。</div>
+			<view class="notice-box-content">
+				<div class="notice-box-content-p1" style="margin-top: 36rpx;">本次问卷分为两个阶段:先完成5道团队背景调研,再完成36道PERILL团队发展动态评估题。</div>
+				<div class="notice-box-content-p2">This questionnaire has two stages: 5 team background questions followed by the 36-item PERILL assessment.</div>
+				<div class="notice-box-content-p1">创衡国际PERILL团队发展动态评估旨在深入了解支撑高价值团队的关键因素。这些关键因素涵盖六个维度:宗旨和动机、外部流程/系统和结构、人际关系、内部流程/系统和结构、学习、领导力。</div>
 				<div class="notice-box-content-p2">Transcend International PERILL Assessment is designed to provide insight into the key factors that underpin high value-creating team. It consists of six elements, focusing on Purpose & Motivation, External Systems and Processes, Relationships, Internal Systems and Processes, Learning Processes, Leadership Qualities and Behaviours.</div>
 				<div class="notice-box-content-p1">您所填写的所有信息将被保密的。</div>
 				<div class="notice-box-content-p2">All information will be kept confidential.</div>
@@ -153,4 +155,4 @@
 			}
 		}
 	}
-</style>
+</style>

+ 21 - 9
pages.json

@@ -194,14 +194,26 @@
 						"navigationStyle": "custom"
 					}
 				},
-				{
-					"path": "fillTeamInfo",
-					"style": {
-						"navigationStyle": "custom"
-					}
-				},
-				{
-					"path": "questionnaireFill",
+				{
+					"path": "fillTeamInfo",
+					"style": {
+						"navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "backgroundSurveyIntro",
+					"style": {
+						"navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "backgroundSurvey",
+					"style": {
+						"navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "questionnaireFill",
 					"style": {
 						"navigationStyle": "custom"
 					}
@@ -330,4 +342,4 @@
 	"easycom": {
 		"^u-(.*)": "@/uni_modules/uview-ui/components/u-$1/u-$1.vue"
 	}
-}
+}

+ 14 - 7
pages/questionnaire.vue

@@ -42,8 +42,10 @@
 </template>
 
 <script>
-	import pageEmpty from '@/components/pageEmpty/index.vue'
-	export default {
+	import pageEmpty from '@/components/pageEmpty/index.vue'
+	import backgroundSurveyRules from '@/utils/backgroundSurvey.js'
+	const { backgroundIntroUrl } = backgroundSurveyRules
+	export default {
 		components:{ pageEmpty },
 		data(){
 			return {
@@ -67,10 +69,15 @@
 			},
 			toFill(item){
 				if(Date.parse(new Date())-Date.parse(item.endTime)>0) return this.$showToast('已过截止时间,无法作答!')
-				if(item.type==2){
-					uni.navigateTo({
-						url:`/pages/questionnaireFill2?teamQuestionnaireId=${item.teamQuestionnaireId}&title=${item.title}`
-					})
+				if(item.type==2){
+					uni.navigateTo({
+						url:backgroundIntroUrl({
+							teamQuestionnaireId:item.teamQuestionnaireId,
+							teamId:item.teamId,
+							type:item.type,
+							title:item.title
+						})
+					})
 				}else{
 					uni.navigateTo({
 						url:`/pages/questionnaireFill?teamQuestionnaireId=${item.teamQuestionnaireId}&title=${item.title}`
@@ -197,4 +204,4 @@
 			}
 		}
 	}
-</style>
+</style>

+ 21 - 4
pagesHome/components/createList.vue

@@ -114,6 +114,8 @@
 import PageEmpty from '@/components/pageEmpty/index.vue'
 import CusTeamUser from '@/components/CusTeamUser/index.vue'
 import CusTeamInfoFill from '@/components/CusTeamInfoFill/index.vue'
+import backgroundSurveyRules from '@/utils/backgroundSurvey.js'
+const { backgroundIntroUrl } = backgroundSurveyRules
 export default {
 	components:{ PageEmpty, CusTeamUser, CusTeamInfoFill },
 	props:{
@@ -132,7 +134,11 @@ export default {
 			menuList:[
 				{
 					img:this.$imgBase+'questionnaire_edit.png',
-					text:'编辑问卷'
+					text:'编辑问卷(一)'
+				},
+				{
+					img:this.$imgBase+'questionnaire_edit.png',
+					text:'编辑问卷(二)'
 				},
 				{
 					img:this.$imgBase+'questionnaire_share.png',
@@ -194,7 +200,8 @@ export default {
 		},
 		showDialog(item){
 			this.dto = item;
-			if(this.dto.type==1) this.menuListCopy = this.menuList.filter((_, index) => ![0,1,3,4].includes(index));
+			if(this.dto.type==1) this.menuListCopy = this.menuList.filter(item =>
+				['团队信息','生成报告'].includes(item.text));
 			else this.menuListCopy = JSON.parse(JSON.stringify(this.menuList))
 			this.show = true;
 			this.$emit('showDialogFn',item);
@@ -227,7 +234,12 @@ export default {
 			})
 		},
 		async handleMenuClick(item){
-			if(item.text==='编辑问卷'){
+			if(item.text==='编辑问卷(一)'){
+				uni.navigateTo({
+					url:`/pagesPublish/backgroundSurvey?mode=coach&teamQuestionnaireId=${encodeURIComponent(this.dto.teamQuestionnaireId)}`
+				})
+				this.show = false;
+			}else if(item.text==='编辑问卷(二)'){
 				uni.navigateTo({
 					url:`/pagesHome/questionnaireEdit?teamQuestionnaireId=${this.dto.teamQuestionnaireId}&type=${this.dto.type}&questionnaireName=${this.dto.title}&isEdit=true`
 				})
@@ -312,7 +324,12 @@ export default {
 		},
 		handleAnswer(item){
 			uni.navigateTo({
-				url:'/pagesPublish/questionnaireFill?teamQuestionnaireId='+item.teamQuestionnaireId+'&teamId='+item.teamId+'&type='+item.type+'&title='+item.title
+				url:backgroundIntroUrl({
+					teamQuestionnaireId:item.teamQuestionnaireId,
+					teamId:item.teamId,
+					type:item.type,
+					title:item.title
+				})
 			})
 		},
 		createReport(item){

+ 16 - 8
pagesHome/components/receiveList.vue

@@ -62,9 +62,11 @@
 <script>
 	import CusNotice from '@/components/CusNotice/index2.vue'
 	import CusTeamUser from '@/components/CusTeamUser/index.vue'
-	import CusTeamInfo from '@/components/CusTeamInfo/index.vue'
-	import PageEmpty from '@/components/pageEmpty/index.vue'
-	export default {
+	import CusTeamInfo from '@/components/CusTeamInfo/index.vue'
+	import PageEmpty from '@/components/pageEmpty/index.vue'
+	import backgroundSurveyRules from '@/utils/backgroundSurvey.js'
+	const { backgroundIntroUrl } = backgroundSurveyRules
+	export default {
 		components:{ CusNotice, CusTeamUser, CusTeamInfo, PageEmpty },
 		props:{
 			list:{
@@ -153,10 +155,16 @@
 					url:'/pagesHome/report'
 				})
 			},
-			handleKnow(){
-				uni.navigateTo({
-					url:'/pagesPublish/questionnaireFill?teamQuestionnaireId='+this.dto.teamQuestionnaireId+'&teamId='+this.dto.teamId+'&type='+this.dto.type+'&turnType=questionnaire'+'&title='+this.dto.title
-				})
+			handleKnow(){
+				uni.navigateTo({
+					url:backgroundIntroUrl({
+						teamQuestionnaireId:this.dto.teamQuestionnaireId,
+						teamId:this.dto.teamId,
+						type:this.dto.type,
+						turnType:'questionnaire',
+						title:this.dto.title
+					})
+				})
 			},
 			async getTeamScaleData(){
 				return new Promise((resolve,reject)=>{
@@ -425,4 +433,4 @@
 			}
 		}
 	}
-</style>
+</style>

+ 129 - 43
pagesHome/questionnaireEdit.vue

@@ -11,13 +11,23 @@
 				<image :src="imgBase+'my_arrow_right.png'"></image>
 			</view>
 		</view>
-		<view class="box" style="padding: 32rpx 24rpx;" @click="showTeam">
-			<view class="box-title">选择团队</view>
-			<view class="box-team adfacjb" @click="showTeam">
-				<view class="box-tip">{{teamName}}</view>
-				<image :src="imgBase+'my_arrow_right.png'"></image>
-			</view>
-		</view>
+		<view class="box" style="padding: 32rpx 24rpx;" @click="showTeam">
+			<view class="box-title">选择团队</view>
+			<view class="box-team adfacjb" @click="showTeam">
+				<view class="box-tip">{{teamName}}</view>
+				<image :src="imgBase+'my_arrow_right.png'"></image>
+			</view>
+		</view>
+		<view class="box background-survey-row adfacjb" v-if="type==2" @click="openBackgroundSurvey">
+			<view>
+				<view class="box-title">团队背景调研</view>
+				<view class="background-survey-tip">发布团队问卷前必须完成</view>
+			</view>
+			<view class="background-survey-status adfac" :class="{completed:backgroundSurveyStatus==='COMPLETED'}">
+				<text>{{backgroundSurveyStatus==='COMPLETED'?'已完成':'待完成'}}</text>
+				<image :src="imgBase+'my_arrow_right.png'"></image>
+			</view>
+		</view>
 		<view class="box" style="padding: 32rpx 24rpx 0;">
 			<view class="box-title">问卷作答时间设置</view>
 			<view class="box-time adfacjb" style="margin-top: 32rpx;" @click="startShow=true">
@@ -99,8 +109,10 @@
 				teamList:[],
 				minStartTime:new Date().getTime(),
 				minEndTime:new Date().getTime(),
-				isEdit:'',
-			}
+				isEdit:'',
+				backgroundSurveyId:'',
+				backgroundSurveyStatus:'DRAFT',
+			}
 		},
 		onReady() {
 			this.$refs.datetimePicker1.setFormatter(this.formatter)
@@ -144,15 +156,47 @@
 			valChange(e){
 				this.dto.answerSetting = e;
 			},
-			async getDetail(){
-				this.$api.get('/core/team/questionnaire/'+this.teamQuestionnaireId).then(({data:res})=>{
-					if(res.code!==0) return this.$showToast(res.msg)
-					this.dto = {...this.dto,...res.data}
-					this.questionnaireId = this.dto.questionnaireId;
-					this.$refs.cnbRef.value = this.dto.answerSetting;
-					this.teamName = res.data.teamName;
-				})
-			},
+			async getDetail(){
+				this.$api.get('/core/team/questionnaire/'+this.teamQuestionnaireId).then(({data:res})=>{
+					if(res.code!==0) return this.$showToast(res.msg)
+					this.dto = {...this.dto,...res.data}
+					this.questionnaireId = this.dto.questionnaireId;
+					if(this.$refs.cnbRef) this.$refs.cnbRef.value = this.dto.answerSetting;
+					this.teamName = res.data.teamName;
+					this.loadCoachBackgroundSurvey()
+				})
+			},
+			async loadCoachBackgroundSurvey(){
+				if(!this.teamQuestionnaireId) return
+				try {
+					const { data:res } = await this.$api.get(
+						`/core/background/survey/coach/team-questionnaire/${this.teamQuestionnaireId}`)
+					if(res.code!==0) throw new Error(res.msg)
+					this.backgroundSurveyId = res.data.id
+					this.backgroundSurveyStatus = res.data.status
+				} catch(error) {
+					this.backgroundSurveyStatus = 'DRAFT'
+				}
+			},
+			openBackgroundSurvey(){
+				if(!this.dto.teamId) return this.$showToast('请先选择团队')
+				const query = this.isEdit
+					? `mode=coach&teamQuestionnaireId=${encodeURIComponent(this.teamQuestionnaireId)}`
+					: `mode=coach&teamId=${encodeURIComponent(this.dto.teamId)}&surveyId=${encodeURIComponent(this.backgroundSurveyId || '')}`
+				uni.navigateTo({
+					url:`/pagesPublish/backgroundSurvey?${query}`,
+					events:{
+						backgroundSurveyReady:data=>{
+							this.backgroundSurveyId = data.surveyId
+							this.backgroundSurveyStatus = data.status
+						},
+						backgroundSurveyCompleted:data=>{
+							this.backgroundSurveyId = data.surveyId
+							this.backgroundSurveyStatus = data.status
+						}
+					}
+				})
+			},
 			showTeam(){
 				this.teamShow = true;
 				this.$api.get('/core/user/team/page',{
@@ -166,17 +210,23 @@
 					this.teamList.forEach(t=>t.name=t.teamName)
 				})
 			},
-			handlePreview(){
-				uni.navigateTo({
-					url:`/pagesPublish/questionnairePreview?questionnaireName=${this.questionnaireName}&teamQuestionnaireId=${this.teamQuestionnaireId}&type=${this.type}`
-				})
-			},
+			handlePreview(){
+				uni.navigateTo({
+					url:`/pagesPublish/questionnairePreview?questionnaireName=${this.questionnaireName}&teamQuestionnaireId=${this.teamQuestionnaireId || ''}&questionnaireId=${this.questionnaireId || ''}&backgroundSurveyId=${this.backgroundSurveyId || ''}&type=${this.type}`
+				})
+			},
 			addTeam(){
 				uni.navigateTo({
 					url:'/pagesPublish/fillTeamInfo?type=add&qtype=team&questionnaireId='+this.questionnaireId,
 					events:{
-						saveTeamInfo: data =>{
-							this.$api.get('/core/user/team/page',{
+						saveTeamInfo: data =>{
+							if(data && data.teamId){
+								this.dto.teamId = data.teamId
+								this.teamName = data.teamName || '新建团队'
+								this.backgroundSurveyId = ''
+								this.backgroundSurveyStatus = 'DRAFT'
+							}
+							this.$api.get('/core/user/team/page',{
 								page:1,
 								limit:-1,
 								coachId:JSON.parse(uni.getStorageSync('userInfo')).id
@@ -206,11 +256,15 @@
 			deleteTeamUser(item,index){
 				this.dto.memberList.splice(index,1);
 			},
-			confirmTeam(item){
-				if(!item) return this.$showToast('请选择团队')
-				this.teamName = item.teamName;
-				this.dto.teamId = item.id;
-				this.teamShow = false;
+			confirmTeam(item){
+				if(!item) return this.$showToast('请选择团队')
+				if(this.dto.teamId != item.id){
+					this.backgroundSurveyId = ''
+					this.backgroundSurveyStatus = 'DRAFT'
+				}
+				this.teamName = item.teamName;
+				this.dto.teamId = item.id;
+				this.teamShow = false;
 			},
 			startConfirm(e){
 				this.dto.startTime = new Date(e.value).Format('yyyy-MM-dd hh:mm:ss');
@@ -225,19 +279,28 @@
 				let params = JSON.parse(JSON.stringify(this.dto));
 				params.coachId = JSON.parse(uni.getStorageSync('userInfo')).id;
 				params.type = this.type;
-				params.answerSetting = 1;
-				
-				if(!params?.teamId) return this.$showToast('请选择团队')
-				if(!params?.startTime) return this.$showToast('请选择开始时间')
+				params.answerSetting = 1;
+
+				if(!params?.teamId) return this.$showToast('请选择团队')
+				if(this.type==2 && this.backgroundSurveyStatus!=='COMPLETED'){
+					return uni.showModal({
+						title:'团队背景调研未完成',
+						content:'为了帮助教练更好地对团队现状做评估,请先完成团队背景调研',
+						showCancel:false,
+						confirmColor:'#199C9C'
+					})
+				}
+				if(!params?.startTime) return this.$showToast('请选择开始时间')
 				if(!params?.endTime) return this.$showToast('请选择结束时间')
 				if(params?.memberList.length==0&&!this.isEdit) return this.$showToast('请选择团队人员')
 				
 				let url = '/core/team/questionnaire';
 				let method = 'put';
-				if(this.type==2){
-					url = '/core/team/questionnaire/publish';
-					method = 'post'
-					params.questionnaireId = this.questionnaireId
+				if(this.type==2){
+					url = '/core/team/questionnaire/publish';
+					method = 'post'
+					params.questionnaireId = this.questionnaireId
+					params.backgroundSurveyId = this.backgroundSurveyId
 					if(this.isEdit){
 						url = '/core/team/questionnaire';
 						method = 'put';
@@ -403,7 +466,7 @@
 			}
 		}
 		
-		.bottom{
+		.bottom{
 			width: 100%;
 			height: 162rpx;
 			background: #FFFFFF;
@@ -412,7 +475,30 @@
 			position: fixed;
 			left: 0;
 			bottom: 0;
-			z-index: 888;
-		}
-	}
-</style>
+			z-index: 888;
+		}
+
+		.background-survey-row{
+			padding:32rpx 24rpx;
+		}
+		.background-survey-tip{
+			font-size:24rpx;
+			color:#8A9BA8;
+			line-height:34rpx;
+			margin-top:18rpx;
+		}
+		.background-survey-status{
+			font-size:28rpx;
+			color:#FD4F66;
+			flex:none;
+			&.completed{
+				color:#199C9C;
+			}
+			image{
+				width:36rpx;
+				height:36rpx;
+				margin-left:10rpx;
+			}
+		}
+	}
+</style>

+ 345 - 0
pagesPublish/backgroundSurvey.vue

@@ -0,0 +1,345 @@
+<template>
+	<view class="survey-page" :style="{'min-height':h+'px', 'padding-top':mt+'px'}">
+		<cus-header :title="pageTitle"></cus-header>
+		<view v-if="loading" class="survey-loading adfacjc">
+			<u-loading-icon size="42"></u-loading-icon>
+			<text>背景调研加载中...</text>
+		</view>
+		<template v-else-if="currentQuestion">
+			<view class="survey-progress">
+				<view class="survey-progress-row adfacjb">
+					<text>答题进度</text>
+					<text><span>{{currentIndex + 1}}</span>/{{survey.questionCount}}</text>
+				</view>
+				<view class="survey-progress-track">
+					<view class="survey-progress-current" :style="{width:progressPercent+'%'}"></view>
+				</view>
+			</view>
+
+			<scroll-view class="survey-content" scroll-y>
+				<view class="question-card">
+					<view class="question-sequence">第 {{currentIndex + 1}} 题</view>
+					<view class="question-cn">{{currentQuestion.question}}</view>
+					<view class="question-en" v-if="currentQuestion.questionEnglish">{{currentQuestion.questionEnglish}}</view>
+					<view class="option-list">
+						<view
+							v-for="option in currentQuestion.options"
+							:key="option.code"
+							class="option-item adf"
+							:class="{active:selectedCode===option.code}"
+							@click="selectOption(option)"
+						>
+							<view class="option-radio adfacjc">
+								<view v-if="selectedCode===option.code"></view>
+							</view>
+							<text class="option-text">{{option.text}}</text>
+						</view>
+					</view>
+					<textarea
+						v-if="selectedOption && selectedOption.customAllowed"
+						v-model="customText"
+						class="custom-input"
+						maxlength="500"
+						placeholder="请填写您的理解"
+						placeholder-style="color:#B3BFC8"
+					></textarea>
+				</view>
+			</scroll-view>
+
+			<view class="survey-actions adfacjb">
+				<view class="survey-btn secondary" :class="{disabled:currentIndex===0}" @click="previous">上一题</view>
+				<view class="survey-btn primary" :class="{disabled:saving}" @click="next">
+					{{currentIndex === survey.questionCount - 1 ? completeText : '下一题'}}
+				</view>
+			</view>
+		</template>
+		<view v-else class="survey-empty adfacjc">背景调研题库暂无题目</view>
+	</view>
+</template>
+
+<script>
+	import CusHeader from '@/components/CusHeader/index.vue'
+	import backgroundSurveyRules from '@/utils/backgroundSurvey.js'
+	const {
+		selectedOption,
+		validateBackgroundAnswer,
+		createBackgroundAnswerPayload,
+		perillIntroUrl
+	} = backgroundSurveyRules
+
+	export default {
+		components:{ CusHeader },
+		data(){
+			return {
+				mode:'coach',
+				teamId:'',
+				teamQuestionnaireId:'',
+				surveyId:'',
+				title:'',
+				type:'',
+				turnType:'',
+				survey:null,
+				currentIndex:0,
+				selectedCode:'',
+				customText:'',
+				loading:true,
+				saving:false,
+				allowBack:false,
+				completed:false
+			}
+		},
+		computed:{
+			pageTitle(){
+				return this.mode === 'coach' ? '团队背景调研' : '团队背景调研'
+			},
+			completeText(){
+				return this.mode === 'coach' ? '完成调研' : '下一阶段'
+			},
+			currentQuestion(){
+				return this.survey && this.survey.questions
+					? this.survey.questions[this.currentIndex]
+					: null
+			},
+			selectedOption(){
+				return selectedOption(this.currentQuestion, this.selectedCode)
+			},
+			progressPercent(){
+				if(!this.survey || !this.survey.questionCount) return 0
+				return ((this.currentIndex + 1) / this.survey.questionCount) * 100
+			}
+		},
+		onLoad(options){
+			this.mode = options.mode || 'coach'
+			this.teamId = options.teamId || ''
+			this.teamQuestionnaireId = options.teamQuestionnaireId || ''
+			this.surveyId = options.surveyId || ''
+			this.title = options.title || ''
+			this.type = options.type || ''
+			this.turnType = options.turnType || ''
+			this.loadSurvey()
+		},
+		onBackPress(){
+			if(this.allowBack || this.completed || !this.survey || this.survey.status === 'COMPLETED') return false
+			uni.showModal({
+				title:'背景调研尚未完成',
+				content:'当前答案已保存为草稿,仍要退出吗?',
+				confirmText:'仍然退出',
+				confirmColor:'#FD4F66',
+				success: result => {
+					if(result.confirm){
+						this.allowBack = true
+						uni.navigateBack()
+					}
+				}
+			})
+			return true
+		},
+		methods:{
+			async loadSurvey(){
+				this.loading = true
+				try {
+					let response
+					if(this.surveyId){
+						response = await this.$api.get(`/core/background/survey/${this.surveyId}`)
+					}else if(this.mode === 'coach' && this.teamQuestionnaireId){
+						response = await this.$api.get(`/core/background/survey/coach/team-questionnaire/${this.teamQuestionnaireId}`)
+					}else if(this.mode === 'coach'){
+						response = await this.$api.post('/core/background/survey/coach/draft',{teamId:this.teamId})
+					}else{
+						response = await this.$api.get(`/core/background/survey/member/team-questionnaire/${this.teamQuestionnaireId}`)
+					}
+					const res = response.data
+					if(res.code!==0) throw new Error(res.msg || '背景调研加载失败')
+					this.applySurvey(res.data)
+					const eventChannel = this.getOpenerEventChannel()
+					if(eventChannel) eventChannel.emit('backgroundSurveyReady',{
+						surveyId:this.survey.id,
+						status:this.survey.status
+					})
+				} catch(error) {
+					this.$showToast(error && (error.message || error.msg) || '背景调研加载失败')
+				} finally {
+					this.loading = false
+				}
+			},
+			applySurvey(survey){
+				this.survey = survey || { questions:[], questionCount:0 }
+				this.surveyId = this.survey.id || this.surveyId
+				if(this.currentIndex >= this.survey.questionCount) this.currentIndex = 0
+				this.loadCurrentAnswer()
+			},
+			loadCurrentAnswer(){
+				const question = this.currentQuestion
+				this.selectedCode = question && question.optionCode || ''
+				this.customText = question && question.customText || ''
+			},
+			selectOption(option){
+				this.selectedCode = option.code
+				if(!option.customAllowed) this.customText = ''
+			},
+			previous(){
+				if(this.currentIndex===0 || this.saving) return
+				this.currentIndex--
+				this.loadCurrentAnswer()
+			},
+			async saveCurrent(){
+				const error = validateBackgroundAnswer(
+					this.currentQuestion, this.selectedCode, this.customText)
+				if(error) throw new Error(error)
+				const response = await this.$api.put(
+					`/core/background/survey/${this.survey.id}/answer`,
+					createBackgroundAnswerPayload(
+						this.currentQuestion, this.selectedCode, this.customText))
+				const res = response.data
+				if(res.code!==0) throw new Error(res.msg || '答案保存失败')
+				this.applySurvey(res.data)
+			},
+			async next(){
+				if(this.saving) return
+				this.saving = true
+				try {
+					await this.saveCurrent()
+					if(this.currentIndex < this.survey.questionCount - 1){
+						this.currentIndex++
+						this.loadCurrentAnswer()
+						return
+					}
+					const response = await this.$api.post(
+						`/core/background/survey/${this.survey.id}/complete`,{})
+					const res = response.data
+					if(res.code!==0) throw new Error(res.msg || '背景调研提交失败')
+					this.applySurvey(res.data)
+					this.completed = true
+					if(this.mode === 'coach'){
+						const eventChannel = this.getOpenerEventChannel()
+						if(eventChannel) eventChannel.emit('backgroundSurveyCompleted',{
+							surveyId:this.survey.id,
+							status:this.survey.status
+						})
+						this.$showToast('团队背景调研已完成')
+						setTimeout(()=>{
+							this.allowBack = true
+							uni.navigateBack()
+						},500)
+						return
+					}
+					uni.redirectTo({
+						url:perillIntroUrl({
+							teamQuestionnaireId:this.teamQuestionnaireId,
+							teamId:this.teamId,
+							type:this.type,
+							turnType:this.turnType,
+							title:this.title
+						})
+					})
+				} catch(error) {
+					this.$showToast(error && (error.message || error.msg) || '背景调研处理失败')
+				} finally {
+					this.saving = false
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.survey-page{
+		background:#F7F7F7;
+		box-sizing:border-box;
+		padding-bottom:170rpx;
+	}
+	.survey-loading,.survey-empty{
+		height:60vh;
+		color:#667E90;
+		font-size:30rpx;
+		text{margin-left:12rpx;}
+	}
+	.survey-progress{
+		background:#FFFFFF;
+		padding:28rpx 32rpx;
+		&-row{
+			font-size:28rpx;
+			color:#667E90;
+			span{font-size:36rpx;color:#199C9C;}
+		}
+		&-track{
+			height:14rpx;
+			margin-top:20rpx;
+			border-radius:8rpx;
+			background:#F0F2F8;
+			overflow:hidden;
+		}
+		&-current{
+			height:100%;
+			background:#FFD750;
+			border-radius:8rpx;
+			transition:width .2s;
+		}
+	}
+	.survey-content{
+		height:calc(100vh - 360rpx);
+		padding:24rpx;
+		box-sizing:border-box;
+	}
+	.question-card{
+		background:#FFFFFF;
+		border-radius:24rpx;
+		padding:32rpx 24rpx;
+	}
+	.question-sequence{font-size:26rpx;color:#199C9C;margin-bottom:20rpx;}
+	.question-cn{font-size:34rpx;color:#002846;line-height:52rpx;font-weight:500;}
+	.question-en{font-size:26rpx;color:#667E90;line-height:40rpx;margin-top:16rpx;}
+	.option-list{margin-top:30rpx;}
+	.option-item{
+		padding:24rpx 20rpx;
+		background:#F7F8FA;
+		border:2rpx solid transparent;
+		border-radius:18rpx;
+		margin-top:18rpx;
+		align-items:flex-start;
+		&.active{background:#F0FAFA;border-color:#33A7A7;}
+	}
+	.option-radio{
+		width:34rpx;
+		height:34rpx;
+		border:2rpx solid #B3BFC8;
+		border-radius:50%;
+		margin:4rpx 18rpx 0 0;
+		flex:none;
+		view{width:20rpx;height:20rpx;border-radius:50%;background:#199C9C;}
+	}
+	.option-item.active .option-radio{border-color:#199C9C;}
+	.option-text{font-size:28rpx;color:#335368;line-height:42rpx;white-space:pre-line;}
+	.custom-input{
+		width:100%;
+		height:180rpx;
+		background:#F7F8FA;
+		border-radius:18rpx;
+		padding:24rpx;
+		box-sizing:border-box;
+		font-size:28rpx;
+		line-height:42rpx;
+		color:#335368;
+		margin-top:22rpx;
+	}
+	.survey-actions{
+		position:fixed;
+		left:0;
+		right:0;
+		bottom:0;
+		padding:24rpx 32rpx 44rpx;
+		background:#FFFFFF;
+		box-shadow:0 -2rpx 10rpx rgba(0,0,0,.06);
+		z-index:10;
+	}
+	.survey-btn{
+		width:calc(50% - 12rpx);
+		padding:26rpx 0;
+		border-radius:48rpx;
+		text-align:center;
+		font-size:30rpx;
+		&.secondary{background:#F0F2F8;color:#667E90;}
+		&.primary{background:linear-gradient(90deg,#33A7A7,#64BBBB);color:#FFFFFF;}
+		&.disabled{opacity:.5;}
+	}
+</style>

+ 110 - 0
pagesPublish/backgroundSurveyIntro.vue

@@ -0,0 +1,110 @@
+<template>
+	<view class="intro-page" :style="{'min-height':h+'px', 'padding-top':mt+'px'}">
+		<cus-header title="填写问卷"></cus-header>
+		<view class="intro-card adffcac">
+			<view class="intro-stage">阶段 {{stage === 'background' ? '一' : '二'}}</view>
+			<view class="intro-title">{{stageTitle}}</view>
+			<view class="intro-line"></view>
+			<view class="intro-description">{{description}}</view>
+			<view class="intro-count">{{questionCount}}</view>
+			<view class="zt_btn intro-button" @click="start">{{buttonText}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import CusHeader from '@/components/CusHeader/index.vue'
+	import backgroundSurveyRules from '@/utils/backgroundSurvey.js'
+	const { questionnaireFillUrl } = backgroundSurveyRules
+
+	export default {
+		components:{ CusHeader },
+		data(){
+			return {
+				stage:'background',
+				teamQuestionnaireId:'',
+				teamId:'',
+				type:'',
+				turnType:'',
+				title:''
+			}
+		},
+		computed:{
+			stageTitle(){
+				return this.stage === 'background'
+					? '团队背景调研'
+					: 'PERILL团队发展动态评估'
+			},
+			description(){
+				return this.stage === 'background'
+					? '请根据您对团队当前状态的真实认知完成选择。如果没有符合的选项,可在“其他”中填写自己的理解。'
+					: '请继续完成PERILL专业评估。所有信息将被严格保密,并仅用于团队评估分析。'
+			},
+			questionCount(){
+				return this.stage === 'background' ? '共5道单选题' : '共36道专业评估题'
+			},
+			buttonText(){
+				return this.stage === 'background' ? '开始背景调研' : '开始PERILL评估'
+			}
+		},
+		onLoad(options){
+			this.stage = options.stage || 'background'
+			this.teamQuestionnaireId = options.teamQuestionnaireId || ''
+			this.teamId = options.teamId || ''
+			this.type = options.type || ''
+			this.turnType = options.turnType || ''
+			this.title = options.title || ''
+		},
+		methods:{
+			start(){
+				const params = {
+					teamQuestionnaireId:this.teamQuestionnaireId,
+					teamId:this.teamId,
+					type:this.type,
+					turnType:this.turnType,
+					title:this.title
+				}
+				if(this.stage === 'background'){
+					uni.navigateTo({
+						url:`/pagesPublish/backgroundSurvey?mode=member&teamQuestionnaireId=${encodeURIComponent(this.teamQuestionnaireId)}&teamId=${encodeURIComponent(this.teamId)}&type=${encodeURIComponent(this.type)}&turnType=${encodeURIComponent(this.turnType)}&title=${encodeURIComponent(this.title)}`
+					})
+					return
+				}
+				uni.navigateTo({ url:questionnaireFillUrl(params) })
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.intro-page{
+		background:#F7F7F7;
+		box-sizing:border-box;
+		padding:40rpx 28rpx;
+	}
+	.intro-card{
+		background:#FFFFFF;
+		border-radius:28rpx;
+		margin-top:60rpx;
+		padding:70rpx 42rpx 50rpx;
+		text-align:center;
+	}
+	.intro-stage{
+		font-size:26rpx;
+		color:#199C9C;
+		background:#EAF8F8;
+		border-radius:28rpx;
+		padding:12rpx 28rpx;
+	}
+	.intro-title{
+		font-size:40rpx;
+		color:#002846;
+		font-weight:bold;
+		line-height:56rpx;
+		margin-top:34rpx;
+	}
+	.intro-line{width:80rpx;height:6rpx;background:#FFD750;border-radius:4rpx;margin-top:28rpx;}
+	.intro-description{font-size:28rpx;color:#667E90;line-height:48rpx;margin-top:36rpx;text-align:left;}
+	.intro-count{font-size:30rpx;color:#335368;margin-top:36rpx;}
+	.intro-button{width:100%;margin-top:70rpx;}
+</style>

+ 68 - 4
pagesPublish/fillTeamInfo.vue

@@ -8,6 +8,7 @@
 <script>
 	import CusTeamInfoFill from '@/components/CusTeamInfoFill/index.vue'
 	import teamBackgroundRules from '@/utils/teamBackgroundFile.js'
+	import backgroundSurveyRules from '@/utils/backgroundSurvey.js'
 	const {
 		beginTeamSubmission,
 		createTeamCreationState,
@@ -17,6 +18,7 @@
 		rememberCreatedTeam,
 		setTeamSubmissionStage
 	} = teamBackgroundRules
+	const { backgroundIntroUrl } = backgroundSurveyRules
 	export default {
 		components:{ CusTeamInfoFill },
 		data(){
@@ -30,7 +32,8 @@
 				creationState:createTeamCreationState(),
 				navigationTimer:null,
 				navigationReject:null,
-				pageUnloaded:false
+				pageUnloaded:false,
+				resumeAfterSurveyPending:false
 			}
 		},
 		onLoad(options) {
@@ -43,6 +46,11 @@
 			this.next = options.next;
 			if(this.next) this.confirmText = '下一步';
 		},
+		onShow() {
+			if(!this.resumeAfterSurveyPending || this.pageUnloaded) return
+			this.resumeAfterSurveyPending = false
+			this.resumeAfterCoachSurvey()
+		},
 		onUnload() {
 			this.pageUnloaded = true
 			this.cancelPendingNavigation(new Error('页面已离开'))
@@ -107,6 +115,11 @@
 						setTeamSubmissionStage(this.creationState, 'pcSession')
 						await this.$refs.teamRef.waitForDeferredPcUpload(teamId)
 					}
+					if(this.next && this.creationState.backgroundSurveyStatus !== 'COMPLETED'){
+						setTeamSubmissionStage(this.creationState, 'coachSurvey')
+						this.openCoachBackgroundSurvey(teamId)
+						return
+					}
 					if(this.next && !this.creationState.teamQuestionnaireId) {
 						setTeamSubmissionStage(this.creationState, 'publish')
 						const publishResult = await this.publishQuestionnaire(teamId)
@@ -129,12 +142,52 @@
 					update:'团队更新失败',
 					upload:'背景资料处理失败',
 					pcSession:'电脑上传流程失败',
+					coachSurvey:'团队背景调研失败',
 					publish:'问卷发布失败',
 					navigate:'页面跳转失败'
 				}[this.creationState.stage] || '操作失败'
 				const message = error && (error.message || error.msg)
 				this.$showToast(message ? `${prefix}:${message}` : `${prefix},请重试`)
 			},
+			openCoachBackgroundSurvey(teamId) {
+				const surveyId = this.creationState.backgroundSurveyId || ''
+				uni.navigateTo({
+					url:`/pagesPublish/backgroundSurvey?mode=coach&teamId=${encodeURIComponent(teamId)}&surveyId=${encodeURIComponent(surveyId)}`,
+					events:{
+						backgroundSurveyReady:data=>{
+							this.creationState.backgroundSurveyId = data.surveyId
+							this.creationState.backgroundSurveyStatus = data.status
+						},
+						backgroundSurveyCompleted:data=>{
+							this.creationState.backgroundSurveyId = data.surveyId
+							this.creationState.backgroundSurveyStatus = data.status
+							this.resumeAfterSurveyPending = data.status === 'COMPLETED'
+						}
+					},
+					fail:error=>{
+						this.showStageError(new Error(error && error.errMsg || '背景调研页面打开失败'))
+					}
+				})
+			},
+			async resumeAfterCoachSurvey() {
+				if(this.creationState.backgroundSurveyStatus !== 'COMPLETED'
+					|| this.creationState.teamQuestionnaireId
+					|| !beginTeamSubmission(this.creationState)) return
+				const teamId = this.creationState.createdTeamId
+				try {
+					setTeamSubmissionStage(this.creationState, 'publish')
+					const publishResult = await this.publishQuestionnaire(teamId)
+					this.creationState.publishResult = publishResult
+					this.creationState.teamQuestionnaireId = publishResult.data
+					setTeamSubmissionStage(this.creationState, 'navigate')
+					await this.continueAfterTeamCreated(teamId)
+					finishTeamSubmission(this.creationState, true)
+				} catch(error) {
+					this.showStageError(error)
+				} finally {
+					finishTeamSubmission(this.creationState)
+				}
+			},
 			async publishQuestionnaire(teamId) {
 				const response = await this.$api.post('/core/team/questionnaire/publish',{
 						answerSetting:1,
@@ -142,6 +195,7 @@
 						startTime:new Date().Format('yyyy-MM-dd hh:mm:ss'),
 						endTime:new Date(new Date().setDate(new Date().getDate()+30)).Format('yyyy-MM-dd hh:mm:ss'),
 						questionnaireId:this.questionnaireId,
+						backgroundSurveyId:this.creationState.backgroundSurveyId,
 						teamId,
 						type:1
 					})
@@ -154,7 +208,12 @@
 					this.$showToast('保存成功,即将填写问卷')
 					await this.runDelayedNavigation(({ success, fail }) => {
 						uni.navigateTo({
-							url:`/pagesPublish/questionnaireFill?teamQuestionnaireId=${this.creationState.teamQuestionnaireId}&teamId=${teamId}&type=${this.type}&title=${this.title}`,
+							url:backgroundIntroUrl({
+								teamQuestionnaireId:this.creationState.teamQuestionnaireId,
+								teamId,
+								type:this.type,
+								title:this.title
+							}),
 							success,
 							fail
 						})
@@ -163,11 +222,16 @@
 					return
 				}
 				this.$showToast('团队新增成功')
+				const eventChannel = this.getOpenerEventChannel()
+				if(eventChannel) eventChannel.emit('saveTeamInfo',{
+					teamId,
+					teamName:this.$refs.teamRef && this.$refs.teamRef.teamInfo
+						? this.$refs.teamRef.teamInfo.teamName
+						: ''
+				})
 				await this.runDelayedNavigation(({ success, fail }) => {
 					uni.navigateBack({ success, fail })
 				})
-				const eventChannel = this.getOpenerEventChannel()
-				if(eventChannel) eventChannel.emit('saveTeamInfo')
 			},
 			runDelayedNavigation(invokeNavigation) {
 				return new Promise((resolve, reject) => {

+ 83 - 24
pagesPublish/questionnairePreview.vue

@@ -11,15 +11,27 @@
 				<view class="top-progress-num"><span>{{0}}</span>/{{total}}</view>
 			</view>
 		</view>
-		<view class="list">
+		<view class="list">
 			<div v-if="isLoading" class="loading-container adfacjc">
 				<div class="adfac">
 					<u-loading-icon size="42"></u-loading-icon>
 					<text style="margin-left: 10rpx; font-size: 34rpx; color: #666666">问卷加载中...</text>
 				</div>
 			</div>
-			<template v-else>
-				<block v-if="list.length">
+			<template v-else>
+				<view class="preview-section" v-if="type==2">
+					<view class="preview-section-title">阶段一:团队背景调研</view>
+					<view class="preview-tip" v-if="!backgroundQuestions.length">选择团队并开始背景调研后可预览题目</view>
+					<view class="background-question" v-for="(question,index) in backgroundQuestions" :key="'background-'+question.detailId">
+						<view class="background-question-title">{{index+1}}. {{question.question}}</view>
+						<view class="background-question-en">{{question.questionEnglish}}</view>
+						<view class="background-option" v-for="option in question.options" :key="option.code">
+							{{option.text}}
+						</view>
+					</view>
+					<view class="preview-section-title perill-title">阶段二:PERILL团队发展动态评估</view>
+				</view>
+				<block v-if="list.length">
 					<question-item v-for="(item,index) in list" :arrayKey="arrayKey" :index="index" :key="index" :item="item"></question-item>
 				</block>
 				<block v-else>
@@ -46,16 +58,17 @@
 				type:'',
 				questionnaireName:'',
 				total:0,
-				list:[],
+				list:[],
+				backgroundQuestions:[],
 				arrayKey:'userAnswer',
 				isLoading: true,
 			}
 		},
 		onLoad(options) {
-			this.type = options.type;
-			this.questionnaireName = options.questionnaireName;
-			if(this.type==2) this.getData2(options.teamQuestionnaireId)
-			else this.getData(options.teamQuestionnaireId)
+			this.type = options.type;
+			this.questionnaireName = options.questionnaireName;
+			if(this.type==2) this.getData2(options.questionnaireId, options.backgroundSurveyId)
+			else this.getData(options.teamQuestionnaireId)
 		},
 		methods:{
 			getData(id){
@@ -67,18 +80,33 @@
 					this.isLoading = false;
 				})
 			},
-			getData2(id){
-				this.isLoading = true;
-				this.$api.get(`/core/questionnaire/${id}`).then(({data:res})=>{
-					if(res.code!==0) return this.$showToast(res.msg)
-					this.arrayKey = 'questionOption';
-					if(res.data){
-						this.list = res.data.detailList||[];
-						this.total = res.data.detailList.length;
-					}
-					this.isLoading = false;
-				})
-			},
+			async getData2(id, backgroundSurveyId){
+				this.isLoading = true;
+				try {
+					const requests = [this.$api.get(`/core/questionnaire/${id}`)]
+					if(backgroundSurveyId){
+						requests.push(this.$api.get(`/core/background/survey/${backgroundSurveyId}`))
+					}
+					const responses = await Promise.all(requests)
+					const res = responses[0].data
+					if(res.code!==0) return this.$showToast(res.msg)
+					this.arrayKey = 'questionOption';
+					if(res.data){
+						this.list = res.data.detailList||[];
+						this.total = this.list.length;
+					}
+					if(responses[1]){
+						const backgroundRes = responses[1].data
+						if(backgroundRes.code!==0) return this.$showToast(backgroundRes.msg)
+						this.backgroundQuestions = backgroundRes.data && backgroundRes.data.questions || []
+						this.total += this.backgroundQuestions.length
+					}
+				} catch(error) {
+					this.$showToast(error && (error.message || error.msg) || '问卷预览加载失败')
+				} finally {
+					this.isLoading = false;
+				}
+			},
 			handleBack(){
 				uni.navigateBack();
 			}
@@ -146,11 +174,42 @@
 			}
 		}
 			
-		.list{
+		.list{
 			flex: 1;
 			padding: 0 24rpx;
-			overflow-y: auto;
-		}
+			overflow-y: auto;
+		}
+		.preview-section-title{
+			font-size:32rpx;
+			font-weight:bold;
+			color:#002846;
+			line-height:44rpx;
+			margin:8rpx 0 24rpx;
+		}
+		.perill-title{margin-top:42rpx;}
+		.preview-tip{
+			background:#F7F8FA;
+			border-radius:16rpx;
+			padding:24rpx;
+			font-size:26rpx;
+			color:#8A9BA8;
+			margin-bottom:30rpx;
+		}
+		.background-question{
+			background:#F7F8FA;
+			border-radius:18rpx;
+			padding:26rpx 22rpx;
+			margin-bottom:20rpx;
+			&-title{font-size:29rpx;color:#002846;line-height:44rpx;}
+			&-en{font-size:24rpx;color:#667E90;line-height:36rpx;margin-top:10rpx;}
+		}
+		.background-option{
+			font-size:25rpx;
+			color:#335368;
+			line-height:38rpx;
+			margin-top:14rpx;
+			white-space:pre-line;
+		}
 		
 		.bottom{
 			width: 100%;
@@ -178,4 +237,4 @@
 			}
 		}
 	}
-</style>
+</style>

+ 106 - 2
tests/teamBackgroundFile.test.js

@@ -4,6 +4,30 @@ const path = require('node:path')
 const { pathToFileURL } = require('node:url')
 const vm = require('node:vm')
 const rules = require('../utils/teamBackgroundFile')
+const backgroundSurveyRules = require('../utils/backgroundSurvey')
+
+const surveyQuestion = {
+	detailId: 101,
+	options: [
+		{ code:'A', text:'固定选项', customAllowed:false },
+		{ code:'F', text:'其他', customAllowed:true }
+	]
+}
+assert.equal(backgroundSurveyRules.validateBackgroundAnswer(surveyQuestion, '', ''), '请选择当前题目的答案')
+assert.equal(backgroundSurveyRules.validateBackgroundAnswer(surveyQuestion, 'F', ''), '请填写其他选项的具体内容')
+assert.equal(backgroundSurveyRules.validateBackgroundAnswer(surveyQuestion, 'A', '忽略内容'), '')
+assert.deepEqual(
+	backgroundSurveyRules.createBackgroundAnswerPayload(surveyQuestion, 'A', '忽略内容'),
+	{ detailId:101, optionCode:'A', customText:'' }
+)
+assert.deepEqual(
+	backgroundSurveyRules.createBackgroundAnswerPayload(surveyQuestion, 'F', ' 自定义答案 '),
+	{ detailId:101, optionCode:'F', customText:'自定义答案' }
+)
+assert.equal(
+	backgroundSurveyRules.backgroundIntroUrl({ teamQuestionnaireId:12, title:'团队 A' }),
+	'/pagesPublish/backgroundSurveyIntro?stage=background&teamQuestionnaireId=12&title=%E5%9B%A2%E9%98%9F%20A'
+)
 
 assert.equal(rules.validateEnterpriseWebsite(' 无 '), '')
 assert.equal(rules.validateEnterpriseWebsite('https://example.com'), '')
@@ -99,6 +123,8 @@ assert.deepEqual(creationState, {
 	stage: 'idle',
 	completed: false,
 	teamPayloadSnapshot: '',
+	backgroundSurveyId: '',
+	backgroundSurveyStatus: 'DRAFT',
 	teamQuestionnaireId: '',
 	publishResult: null,
 	resumeStage: ''
@@ -617,7 +643,8 @@ function readSfcScript(filePath) {
 }
 
 async function loadVueComponent(filePath, {
-	http = {}, uni = {}, wx = {}, apiRules = rules, timers = {}
+	http = {}, uni = {}, wx = {}, apiRules = rules,
+	backgroundRules = backgroundSurveyRules, timers = {}
 } = {}) {
 	const context = vm.createContext({
 		uni,
@@ -640,6 +667,9 @@ async function loadVueComponent(filePath, {
 	const rulesStub = new vm.SyntheticModule(['default'], function () {
 		this.setExport('default', apiRules)
 	}, { context, identifier: 'test:team-background-rules' })
+	const backgroundRulesStub = new vm.SyntheticModule(['default'], function () {
+		this.setExport('default', backgroundRules)
+	}, { context, identifier: 'test:background-survey-rules' })
 	const httpNames = [
 		'createTeamBackgroundSession',
 		'disableTeamBackgroundFile',
@@ -659,6 +689,7 @@ async function loadVueComponent(filePath, {
 	await componentModule.link(specifier => {
 		if (specifier === '@/http/teamBackgroundFile.js') return httpStub
 		if (specifier === '@/utils/teamBackgroundFile.js') return rulesStub
+		if (specifier === '@/utils/backgroundSurvey.js') return backgroundRulesStub
 		if (specifier.endsWith('.vue')) return vueStub
 		throw new Error(`unexpected component import: ${specifier}`)
 	})
@@ -725,6 +756,17 @@ const createTeamPagePath = path.join(__dirname, '../pagesPublish/fillTeamInfo.vu
 const createListComponentPath = path.join(__dirname, '../pagesHome/components/createList.vue')
 const teamEditPagePath = path.join(__dirname, '../pagesMy/teamEdit.vue')
 const headerComponentPath = path.join(__dirname, '../components/CusHeader/index.vue')
+const backgroundSurveyPagePath = path.join(__dirname, '../pagesPublish/backgroundSurvey.vue')
+const backgroundSurveyIntroPagePath = path.join(__dirname, '../pagesPublish/backgroundSurveyIntro.vue')
+
+async function testBackgroundSurveyPagesLoad() {
+	const survey = await loadVueComponent(backgroundSurveyPagePath)
+	const intro = await loadVueComponent(backgroundSurveyIntroPagePath)
+	assert.equal(typeof survey.methods.loadSurvey, 'function')
+	assert.equal(typeof survey.methods.saveCurrent, 'function')
+	assert.equal(typeof survey.methods.next, 'function')
+	assert.equal(typeof intro.methods.start, 'function')
+}
 
 async function testDeferredPcSelectionSurvivesSessionFailure() {
 	let createCalls = 0
@@ -1307,9 +1349,10 @@ async function testCreatedTeamNavigationIsAwaitableAndResumable() {
 					}
 					assert.equal(url, '/core/team/questionnaire/publish')
 					assert.deepEqual(Object.keys(payload).sort(), [
-						'answerSetting', 'coachId', 'endTime', 'questionnaireId',
+						'answerSetting', 'backgroundSurveyId', 'coachId', 'endTime', 'questionnaireId',
 						'startTime', 'teamId', 'type'
 					].sort())
+					assert.equal(payload.backgroundSurveyId, 456)
 					publishCalls += 1
 					return Promise.resolve({ data: { code: 0, data: 321 } })
 				},
@@ -1334,6 +1377,8 @@ async function testCreatedTeamNavigationIsAwaitableAndResumable() {
 				}
 			}
 		})
+		instance.creationState.backgroundSurveyId = 456
+		instance.creationState.backgroundSurveyStatus = 'COMPLETED'
 
 		const first = instance.handleConfirm({
 			teamName: '团队', enterpriseWebsite: 'https://old.example'
@@ -1394,6 +1439,63 @@ async function testCreatedTeamNavigationIsAwaitableAndResumable() {
 	}
 }
 
+async function testCreatedTeamRequiresCoachSurveyBeforePublish() {
+	let navigationOptions
+	let publishCalls = 0
+	const uni = {
+		getStorageSync: () => JSON.stringify({ id: 7 }),
+		navigateTo(options) {
+			navigationOptions = options
+		}
+	}
+	const component = await loadVueComponent(createTeamPagePath, { uni })
+	const instance = instantiateComponent(component, {
+		next:'1',
+		type:'1',
+		title:'PERILL',
+		questionnaireId:66,
+		$api:{
+			post(url) {
+				assert.equal(url, '/core/user/team')
+				return Promise.resolve({ data:{ code:0, data:{ teamId:88 } } })
+			}
+		},
+		$showToast() {},
+		$showModal: () => Promise.resolve(),
+		$refs:{
+			teamRef:{
+				flushBackgroundFiles: () => Promise.resolve({ success:0, failed:0 }),
+				hasUnresolvedBackgroundFiles: () => false,
+				hasDeferredPcUpload: () => false
+			}
+		}
+	})
+	let continuedTeamId
+	instance.continueAfterTeamCreated = async teamId => {
+		continuedTeamId = teamId
+	}
+	instance.publishQuestionnaire = async teamId => {
+		assert.equal(teamId, 88)
+		assert.equal(instance.creationState.backgroundSurveyId, 456)
+		publishCalls += 1
+		return { code:0, data:321 }
+	}
+
+	await instance.handleConfirm({ teamName:'团队', enterpriseWebsite:'无' })
+	assert.equal(publishCalls, 0, 'publishing must wait for the coach background survey')
+	assert.match(navigationOptions.url, /backgroundSurvey\?mode=coach/)
+	assert.equal(instance.creationState.stage, 'coachSurvey')
+	navigationOptions.events.backgroundSurveyReady({ surveyId:456, status:'DRAFT' })
+	assert.equal(instance.creationState.backgroundSurveyId, 456)
+	navigationOptions.events.backgroundSurveyCompleted({ surveyId:456, status:'COMPLETED' })
+	component.onShow.call(instance)
+	await flushPromises()
+	assert.equal(publishCalls, 1)
+	assert.equal(instance.creationState.teamQuestionnaireId, 321)
+	assert.equal(continuedTeamId, 88)
+	assert.equal(instance.creationState.completed, true)
+}
+
 async function testCreatedTeamUnloadCancelsLateNavigation() {
 	const timers = createFakeTimers()
 	let navigateBackCalls = 0
@@ -1600,6 +1702,7 @@ async function main() {
 	await testUploadTransport()
 	await testApiTransport()
 	await testDownloadTransport()
+	await testBackgroundSurveyPagesLoad()
 	await testDeferredPcSelectionSurvivesSessionFailure()
 	await testSessionRefreshFailureKeepsOldTimer()
 	await testPcSessionCreationIsSingleFlightPerTeam()
@@ -1616,6 +1719,7 @@ async function main() {
 	await testTeamFillFlushesBeforeEmitAndLocks()
 	await testCreatedTeamResumeAndSubmitLock()
 	await testCreatedTeamNavigationIsAwaitableAndResumable()
+	await testCreatedTeamRequiresCoachSurveyBeforePublish()
 	await testCreatedTeamUnloadCancelsLateNavigation()
 	await testTeamEditRedirectIsLockedAndResumable()
 	await testTeamEditUnloadCancelsLateRedirect()

+ 49 - 0
utils/backgroundSurvey.js

@@ -0,0 +1,49 @@
+function selectedOption(question, optionCode) {
+	const options = question && Array.isArray(question.options) ? question.options : []
+	return options.find(option => String(option.code) === String(optionCode)) || null
+}
+
+function validateBackgroundAnswer(question, optionCode, customText) {
+	const option = selectedOption(question, optionCode)
+	if (!option) return '请选择当前题目的答案'
+	if (option.customAllowed && !String(customText || '').trim()) return '请填写其他选项的具体内容'
+	return ''
+}
+
+function createBackgroundAnswerPayload(question, optionCode, customText) {
+	const option = selectedOption(question, optionCode)
+	return {
+		detailId: question && question.detailId,
+		optionCode: optionCode || '',
+		customText: option && option.customAllowed ? String(customText || '').trim() : ''
+	}
+}
+
+function encodeQuery(params) {
+	return Object.keys(params || {})
+		.filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '')
+		.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
+		.join('&')
+}
+
+function backgroundIntroUrl(params) {
+	return `/pagesPublish/backgroundSurveyIntro?${encodeQuery(Object.assign({ stage: 'background' }, params))}`
+}
+
+function perillIntroUrl(params) {
+	return `/pagesPublish/backgroundSurveyIntro?${encodeQuery(Object.assign({ stage: 'perill' }, params))}`
+}
+
+function questionnaireFillUrl(params) {
+	return `/pagesPublish/questionnaireFill?${encodeQuery(params)}`
+}
+
+module.exports = {
+	selectedOption,
+	validateBackgroundAnswer,
+	createBackgroundAnswerPayload,
+	encodeQuery,
+	backgroundIntroUrl,
+	perillIntroUrl,
+	questionnaireFillUrl
+}

+ 2 - 0
utils/teamBackgroundFile.js

@@ -128,6 +128,8 @@ function createTeamCreationState() {
 		stage: 'idle',
 		completed: false,
 		teamPayloadSnapshot: '',
+		backgroundSurveyId: '',
+		backgroundSurveyStatus: 'DRAFT',
 		teamQuestionnaireId: '',
 		publishResult: null,
 		resumeStage: ''