Преглед изворни кода

fix(report): refresh generation status

Developer пре 2 дана
родитељ
комит
e8484c0e77

+ 31 - 7
pagesHome/components/createList.vue

@@ -116,6 +116,27 @@ import CusTeamUser from '@/components/CusTeamUser/index.vue'
 import CusTeamInfoFill from '@/components/CusTeamInfoFill/index.vue'
 import backgroundSurveyRules from '@/utils/backgroundSurvey.js'
 const { backgroundIntroUrl } = backgroundSurveyRules
+
+function extractReportId(payload) {
+	if (payload === null || payload === undefined) return undefined
+	if (typeof payload === 'number') return payload
+	if (typeof payload === 'string') {
+		const numeric = Number(payload)
+		return Number.isNaN(numeric) ? undefined : numeric
+	}
+	if (typeof payload !== 'object') return undefined
+	const direct = payload.reportId ?? payload.id
+	if (direct !== undefined && direct !== null && direct !== '') {
+		const numeric = Number(direct)
+		return Number.isNaN(numeric) ? undefined : numeric
+	}
+	for (const key of ['data', 'dto', 'info']) {
+		const nested = extractReportId(payload[key])
+		if (nested !== undefined) return nested
+	}
+	return undefined
+}
+
 export default {
 	components:{ PageEmpty, CusTeamUser, CusTeamInfoFill },
 	props:{
@@ -358,13 +379,16 @@ export default {
 				this.show = false;
 			})
 		},
-		createReportFn(teamQuestionnaireId){
-			this.$api.get(`/core/team/questionnaire/genReport/${teamQuestionnaireId}`).then(({data:res})=>{
-				this.show = false;
-				uni.navigateTo({
-					url:'/pagesHome/reportResult?result='+res.code+'&info='+encodeURIComponent(JSON.stringify(this.dto))
-				})
-			})
+		createReportFn(teamQuestionnaireId){
+			this.$api.get(`/core/team/questionnaire/genReport/${teamQuestionnaireId}`).then(({data:res})=>{
+				this.show = false;
+				if(res.code!==0) return this.$showToast(res.msg)
+				const reportId = extractReportId(res.data)
+				if(!reportId) return this.$showToast('未获取到报告编号')
+				uni.navigateTo({
+					url:'/pagesHome/reportResult?reportId='+reportId+'&info='+encodeURIComponent(JSON.stringify(this.dto))
+				})
+			})
 		},
 		sendReport(item){
 

+ 113 - 61
pagesHome/report.vue

@@ -32,12 +32,40 @@
 	</view>
 </template>
 
-<script>
-	import ReceiveList from './components/report/receiveList.vue'
-	import GenerateList from './components/report/generateList.vue'
-	import SendList from './components/report/sendList.vue'
-	import PageEmpty from '@/components/pageEmpty/index.vue'
-	export default {
+<script>
+	import ReceiveList from './components/report/receiveList.vue'
+	import GenerateList from './components/report/generateList.vue'
+	import SendList from './components/report/sendList.vue'
+	import PageEmpty from '@/components/pageEmpty/index.vue'
+
+	function replaceFirstPage(currentList, nextList) {
+		void currentList
+		return Array.isArray(nextList) ? nextList.slice() : []
+	}
+
+	function beginListRefresh(vm) {
+		vm.refreshGeneration++
+		vm.inFlightPageRequests = {}
+		return vm.refreshGeneration
+	}
+
+	function shouldApplyListResponse(vm, generation) {
+		return vm.refreshGeneration === generation
+	}
+
+	function beginListPageRequest(vm, generation, page) {
+		const key = `${generation}:${page}`
+		if(vm.inFlightPageRequests[key]) return null
+		vm.inFlightPageRequests[key] = true
+		return { generation, page, key }
+	}
+
+	function finishListPageRequest(vm, meta) {
+		if(!meta || !meta.key) return
+		delete vm.inFlightPageRequests[meta.key]
+	}
+
+	export default {
 		components:{
 			ReceiveList,
 			GenerateList,
@@ -47,52 +75,69 @@
 		data(){
 			return {
 				tindex:0,
-				queryParams:{
-					page:1,
-					limit:10,
-					teamName:''
-				},
-				list:[],
-				isOver:false,
-				categoryData:[]
+				queryParams:{
+					page:1,
+					limit:10,
+					teamName:''
+				},
+				refreshGeneration:0,
+				inFlightPageRequests:{},
+				list:[],
+				isOver:false,
+				categoryData:[]
 			}
 		},
-		onShow() {
-			this.getList();
-		},
+		onShow() {
+			this.initList();
+		},
 		methods:{
 			changeTab(index){
 				this.tindex = index;
 				this.initList();
 			},
-			initList(){
-				this.queryParams.page = 1;
-				this.isOver = false;
-				this.list = [];
-				this.getList();
-			},
-			getList(){
-				if(this.tindex===0) this.getReceiveList()
-				else if(this.tindex===1) this.getGenerateList()
-				else if(this.tindex===2) this.getSendList()
-			},
-			getReceiveList(){
-				this.$api.get('/core/report/receivedReportList',this.queryParams).then(({data:res})=>{
-					if(res.code!==0) return this.$showToast(res.msg)
-					this.list = [...this.list,...res.data.list];
-					this.queryParams.page++;
-					if(res.data.list.length===0) this.isOver = true;
-					if(this.list.length===0) this.$showToast('暂无数据')
-				})
-			},
-			getGenerateList(){
-				this.$api.get('/core/report/generatedReportList',this.queryParams).then(({data:res})=>{
-					if(res.code!==0) return this.$showToast(res.msg)
-					this.list = [...this.list,...res.data.list];
-					this.queryParams.page++;
-					if(res.data.list.length===0) this.isOver = true;
-					if(this.list.length===0) this.$showToast('暂无数据')
-				})
+			initList(){
+				const generation = beginListRefresh(this)
+				this.queryParams.page = 1;
+				this.isOver = false;
+				this.list = [];
+				this.getList(generation);
+			},
+			getList(generation = this.refreshGeneration){
+				if(this.tindex===0) this.getReceiveList(generation)
+				else if(this.tindex===1) this.getGenerateList(generation)
+				else if(this.tindex===2) this.getSendList(generation)
+			},
+			getReceiveList(generation = this.refreshGeneration){
+				const requestPage = this.queryParams.page
+				const requestMeta = beginListPageRequest(this, generation, requestPage)
+				if(!requestMeta) return
+				const query = { ...this.queryParams }
+				this.$api.get('/core/report/receivedReportList',query).then(({data:res})=>{
+					if(!shouldApplyListResponse(this, generation)) return
+					if(res.code!==0) return this.$showToast(res.msg)
+					this.list = requestPage===1 ? replaceFirstPage(this.list, res.data.list) : [...this.list,...res.data.list];
+					if(this.queryParams.page===requestPage) this.queryParams.page = requestPage + 1;
+					if(res.data.list.length===0) this.isOver = true;
+					if(this.list.length===0) this.$showToast('暂无数据')
+				}).finally(() => {
+					finishListPageRequest(this, requestMeta)
+				})
+			},
+			getGenerateList(generation = this.refreshGeneration){
+				const requestPage = this.queryParams.page
+				const requestMeta = beginListPageRequest(this, generation, requestPage)
+				if(!requestMeta) return
+				const query = { ...this.queryParams }
+				this.$api.get('/core/report/generatedReportList',query).then(({data:res})=>{
+					if(!shouldApplyListResponse(this, generation)) return
+					if(res.code!==0) return this.$showToast(res.msg)
+					this.list = requestPage===1 ? replaceFirstPage(this.list, res.data.list) : [...this.list,...res.data.list];
+					if(this.queryParams.page===requestPage) this.queryParams.page = requestPage + 1;
+					if(res.data.list.length===0) this.isOver = true;
+					if(this.list.length===0) this.$showToast('暂无数据')
+				}).finally(() => {
+					finishListPageRequest(this, requestMeta)
+				})
 			},
 			async getUserCategoryData(){
 				return new Promise((resolve,reject)=>{
@@ -103,21 +148,28 @@
 					})
 				})
 			},
-			async getSendList(){
-				await this.getUserCategoryData()
-				let query = JSON.parse(JSON.stringify(this.queryParams));
-				query.coachId = uni.getStorageSync('userInfo')&&JSON.parse(uni.getStorageSync('userInfo')).id||'';
-				this.$api.get('/core/report/receivedReportList',query).then(({data:res})=>{
-					if(res.code!==0) return this.$showToast(res.msg)
-					this.list = [...this.list,...res.data.list];
-					this.list.forEach(l=>{
-						l.categoryName = this.categoryData.find(c=>c.id===l.category).name||'';
-					})
-					this.queryParams.page++;
-					if(res.data.list.length===0) this.isOver = true;
-					if(this.list.length===0) this.$showToast('暂无数据')
-				})
-			},
+			async getSendList(generation = this.refreshGeneration){
+				await this.getUserCategoryData()
+				if(!shouldApplyListResponse(this, generation)) return
+				const requestPage = this.queryParams.page
+				const requestMeta = beginListPageRequest(this, generation, requestPage)
+				if(!requestMeta) return
+				let query = JSON.parse(JSON.stringify(this.queryParams));
+				query.coachId = uni.getStorageSync('userInfo')&&JSON.parse(uni.getStorageSync('userInfo')).id||'';
+				this.$api.get('/core/report/receivedReportList',query).then(({data:res})=>{
+					if(!shouldApplyListResponse(this, generation)) return
+					if(res.code!==0) return this.$showToast(res.msg)
+					this.list = requestPage===1 ? replaceFirstPage(this.list, res.data.list) : [...this.list,...res.data.list];
+					this.list.forEach(l=>{
+						l.categoryName = this.categoryData.find(c=>c.id===l.category).name||'';
+					})
+					if(this.queryParams.page===requestPage) this.queryParams.page = requestPage + 1;
+					if(res.data.list.length===0) this.isOver = true;
+					if(this.list.length===0) this.$showToast('暂无数据')
+				}).finally(() => {
+					finishListPageRequest(this, requestMeta)
+				})
+			},
 			reSendReport(){
 				this.initList();
 				this.$showToast('重新生成成功')
@@ -211,4 +263,4 @@
 			}
 		}
 	}
-</style>
+</style>

+ 338 - 139
pagesHome/reportResult.vue

@@ -1,139 +1,338 @@
-<template>
-	<view class="default_page" :style="{'height':h+'px', 'padding-top':mt+'px'}">
-		<cus-header title='生成报告' bgColor="transparent"></cus-header>
-		<div class="box adffcac">
-			<image class="box-loading" src="https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/e6d5bea9491a14aafd3f955d332e62d08e521229/report_success.png" v-if="result==0"></image>
-			<image class="box-loading" src="https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/e6d5bea9491a14aafd3f955d332e62d08e521229/report_fail.png" v-else></image>
-			<div class="box-p1">{{result==0?'报告正在生成中~':'报告生成失败'}}</div>
-			<div class="box-p2" v-if="result==0">预计所需时间1小时左右</div>
-			<div class="box-p3">{{result==0?'我们会在报告生成完成后提示你,你可以去我的PREILL报告查阅报告结果。':'失败原因:网络延迟'}}</div>
-		</div>
-		<div class="form">
-			<div class="form-item adfacjb">
-				<div class="form-item-left">问卷名称</div>
-				<div class="form-item-right">{{info.title||''}}</div>
-			</div>
-			<div class="form-item adfacjb">
-				<div class="form-item-left">团队名称</div>
-				<div class="form-item-right">{{info.teamName||''}}</div>
-			</div>
-			<div class="form-item adfacjb">
-				<div class="form-item-left">创建时间</div>
-				<div class="form-item-right">{{info.startTime||info.createDate||''}}</div>
-			</div>
-		</div>
-		<div class="btn">
-			<div class="zt_btn" @click="handleBack" v-if="result==0">返回</div>
-			<div class="zt_btn" @click="handleReCreate" v-else>重新生成</div>
-		</div>
-	</view>
-</template>
-
-<script>
-	export default {
-		data(){
-			return {
-				result:'',
-				info:null
-			}
-		},
-		onLoad(options) {
-			this.result = options.result;
-			this.info = options.info&&JSON.parse(decodeURIComponent(options.info));
-		},
-		methods:{
-			handleBack(){
-				uni.navigateBack()
-			},
-			handleReCreate(){
-				this.$api.get(`/core/team/questionnaire/genReport/${this.info.teamQuestionnaireId}`).then(({data:res})=>{
-					if(res.code!==0) return this.$showToast(res.msg)
-					this.$showToast('重新生成成功')
-					setTimeout(()=>{
-						uni.redirectTo({
-							url:'pagesHome/questionnaire?type=create'
-						})
-					},1500)
-				})
-			}
-		}
-	}
-</script>
-
-<style scoped lang="scss">
-	.default_page{
-		background: #F7F7F7;
-		.box{
-			width: 100%;
-			background: #FFFFFF;
-			border-radius: 36rpx 36rpx 0rpx 0rpx;
-			padding: 54rpx 0 64rpx;
-			&-loading{
-				width: 340rpx;
-				height: 340rpx;
-			}
-			&-p1{
-				font-family: PingFang-SC, PingFang-SC;
-				font-weight: bold;
-				font-size: 32rpx;
-				color: #002846;
-				line-height: 40rpx;
-				text-align: center;
-				margin-top: 41rpx;
-			}
-			&-p2{
-				font-family: PingFang-SC, PingFang-SC;
-				font-weight: bold;
-				font-size: 28rpx;
-				color: #002846;
-				line-height: 28rpx;
-				text-align: center;
-				margin-top: 23rpx;
-			}
-			&-p3{
-				padding: 0 100rpx;
-				font-family: PingFangSC, PingFang SC;
-				font-weight: 400;
-				font-size: 28rpx;
-				color: #667E90;
-				line-height: 42rpx;
-				text-align: center;
-				margin-top: 24rpx;
-			}
-		}
-		
-		.form{
-			width: 100%;
-			margin-top: 20rpx;
-			&-item{
-				padding: 28rpx 24rpx;
-				background: #FFFFFF;
-				box-shadow: inset 0rpx -1rpx 0rpx 0rpx #EFEFEF;
-				&-left{
-					width: 140rpx;
-					font-family: PingFangSC, PingFang SC;
-					font-weight: 400;
-					font-size: 30rpx;
-					color: #002846;
-					line-height: 42rpx;
-				}
-				&-right{
-					width: calc(100% - 140rpx);
-					padding-left: 20rpx;
-					box-sizing: border-box;
-					font-family: PingFangSC, PingFang SC;
-					font-weight: 400;
-					font-size: 30rpx;
-					color: #667E90;
-					line-height: 32rpx;
-					text-align: right;
-				}
-			}
-		}
-		
-		.btn{
-			width: calc(100% -100rpx);
-			margin: 120rpx 50rpx 0;
-		}
-	}
-</style>
+<template>
+	<view class="default_page" :style="{'height':h+'px', 'padding-top':mt+'px'}">
+		<cus-header title='生成报告' bgColor="transparent"></cus-header>
+		<div class="box adffcac">
+			<image class="box-loading" src="https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/e6d5bea9491a14aafd3f955d332e62d08e521229/report_success.png" v-if="generation.imageKey==='success'"></image>
+			<image class="box-loading" src="https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/e6d5bea9491a14aafd3f955d332e62d08e521229/report_fail.png" v-else></image>
+			<div class="box-p1">{{generation.title}}</div>
+			<div class="box-p2" v-if="generation.subtitle">{{generation.subtitle}}</div>
+			<div class="box-p3">{{generation.state===-1 ? '失败原因:'+generation.errorMessage : generation.description}}</div>
+		</div>
+		<div class="form">
+			<div class="form-item adfacjb">
+				<div class="form-item-left">问卷名称</div>
+				<div class="form-item-right">{{info.title||''}}</div>
+			</div>
+			<div class="form-item adfacjb">
+				<div class="form-item-left">团队名称</div>
+				<div class="form-item-right">{{info.teamName||''}}</div>
+			</div>
+			<div class="form-item adfacjb">
+				<div class="form-item-left">创建时间</div>
+				<div class="form-item-right">{{info.startTime||info.createDate||''}}</div>
+			</div>
+		</div>
+		<div class="btn">
+			<div class="zt_btn" @click="handleAction">{{generation.actionText}}</div>
+		</div>
+	</view>
+</template>
+
+<script>
+	function toNumber(value) {
+		if (value === '' || value === null || value === undefined) return undefined
+		const numeric = Number(value)
+		return Number.isNaN(numeric) ? undefined : numeric
+	}
+
+	function extractReportId(payload) {
+		if (payload === null || payload === undefined) return undefined
+		if (typeof payload === 'number') return payload
+		if (typeof payload === 'string') return toNumber(payload)
+		if (typeof payload !== 'object') return undefined
+		const direct = toNumber(payload.reportId ?? payload.id)
+		if (direct !== undefined) return direct
+		for (const key of ['data', 'dto', 'info']) {
+			const nested = extractReportId(payload[key])
+			if (nested !== undefined) return nested
+		}
+		return undefined
+	}
+
+	function normalizeGenerationStatus(payload = {}) {
+		const source = payload && typeof payload === 'object' ? payload : {}
+		const state = toNumber(source.state)
+		return {
+			reportId: extractReportId(source) || '',
+			state: state === undefined ? 0 : state,
+			status: source.status || '',
+			errorMessage: source.errorMessage || ''
+		}
+	}
+
+	function applyGenerationStatus(payload = {}) {
+		const generation = normalizeGenerationStatus(payload)
+		if (generation.state === -1) {
+			return {
+				...generation,
+				title: '报告生成失败',
+				subtitle: '',
+				description: generation.errorMessage || '失败原因:网络延迟',
+				errorMessage: generation.errorMessage || '网络延迟',
+				polling: false,
+				actionText: '重新生成',
+				imageKey: 'fail'
+			}
+		}
+		if (generation.state === 1) {
+			return {
+				...generation,
+				title: '报告生成成功',
+				subtitle: '',
+				description: '报告已生成完成,你可以去我的PREILL报告查阅报告结果。',
+				errorMessage: '',
+				polling: false,
+				actionText: '返回',
+				imageKey: 'success'
+			}
+		}
+		return {
+			...generation,
+			title: '报告正在生成中~',
+			subtitle: '预计所需时间1小时左右',
+			description: '我们会在报告生成完成后提示你,你可以去我的PREILL报告查阅报告结果。',
+			errorMessage: '',
+			polling: true,
+			actionText: '返回',
+			imageKey: 'success'
+		}
+	}
+
+	function shouldResumePolling(reportId, generation = {}) {
+		return Boolean(reportId && normalizeGenerationStatus(generation).state === 0)
+	}
+
+	function setStatusPageVisible(vm, visible) {
+		if(vm.isPageVisible === visible) return vm.statusVisibilityToken
+		vm.isPageVisible = visible
+		if(!visible){
+			vm.statusVisibilityToken++
+			vm.statusActiveRequestId = 0
+			vm.statusRequestInFlight = false
+		}
+		return vm.statusVisibilityToken
+	}
+
+	function beginStatusRequest(vm) {
+		if(!vm.isPageVisible || vm.statusRequestInFlight) return null
+		vm.statusRequestSeq++
+		vm.statusActiveRequestId = vm.statusRequestSeq
+		vm.statusRequestInFlight = true
+		return {
+			requestId: vm.statusActiveRequestId,
+			visibilityToken: vm.statusVisibilityToken
+		}
+	}
+
+	function shouldApplyStatusResponse(vm, meta) {
+		return Boolean(
+			meta
+			&& vm.isPageVisible
+			&& vm.statusRequestInFlight
+			&& vm.statusActiveRequestId === meta.requestId
+			&& vm.statusVisibilityToken === meta.visibilityToken
+		)
+	}
+
+	function finishStatusRequest(vm, meta) {
+		if(!meta) return
+		if(vm.statusActiveRequestId === meta.requestId){
+			vm.statusActiveRequestId = 0
+			vm.statusRequestInFlight = false
+		}
+	}
+
+	export default {
+		data(){
+			return {
+				reportId:'',
+				info:{},
+				generation:applyGenerationStatus(),
+				pollTimer:null,
+				hasStatusErrorToastShown:false,
+				isPageVisible:false,
+				statusVisibilityToken:0,
+				statusRequestSeq:0,
+				statusActiveRequestId:0,
+				statusRequestInFlight:false
+			}
+		},
+		onLoad(options) {
+			this.info = options.info&&JSON.parse(decodeURIComponent(options.info)) || {}
+			this.reportId = extractReportId({ reportId:options.reportId, dto:options, info:this.info }) || ''
+			if(!this.reportId){
+				this.stopPolling('报告编号缺失')
+				return
+			}
+		},
+		onShow() {
+			setStatusPageVisible(this, true)
+			if(shouldResumePolling(this.reportId, this.generation)){
+				this.startPolling()
+				this.fetchGenerationStatus(false)
+			}
+		},
+		onHide() {
+			setStatusPageVisible(this, false)
+			this.clearPolling()
+		},
+		onUnload() {
+			setStatusPageVisible(this, false)
+			this.clearPolling()
+		},
+		methods:{
+			startPolling(){
+				if(!this.isPageVisible || !shouldResumePolling(this.reportId, this.generation) || this.pollTimer) return
+				this.pollTimer = setInterval(() => {
+					this.fetchGenerationStatus(false)
+				}, 5000)
+			},
+			clearPolling(){
+				if(this.pollTimer){
+					clearInterval(this.pollTimer)
+					this.pollTimer = null
+				}
+			},
+			reconcileGeneration(status){
+				this.generation = applyGenerationStatus(status)
+				this.hasStatusErrorToastShown = false
+				if(this.generation.polling && this.reportId) return this.startPolling()
+				this.clearPolling()
+			},
+			stopPolling(errorMessage){
+				this.clearPolling()
+				this.generation = applyGenerationStatus({
+					reportId:this.reportId,
+					state:-1,
+					errorMessage:errorMessage || this.generation.errorMessage || '网络延迟'
+				})
+			},
+			fetchGenerationStatus(showErrorToast = true){
+				if(!this.reportId){
+					this.stopPolling('报告编号缺失')
+					return Promise.resolve()
+				}
+				const requestMeta = beginStatusRequest(this)
+				if(!requestMeta) return Promise.resolve()
+				return this.$api.get(`/core/report/generationStatus/${this.reportId}`, {}, false).then(({data:res})=>{
+					if(!shouldApplyStatusResponse(this, requestMeta)) return
+					if(res.code!==0){
+						if(showErrorToast && !this.hasStatusErrorToastShown){
+							this.$showToast(res.msg || '报告状态获取失败')
+							this.hasStatusErrorToastShown = true
+						}
+						this.startPolling()
+						return
+					}
+					this.reconcileGeneration({ ...(res.data || {}), reportId:this.reportId })
+				}).catch(error => {
+					if(!shouldApplyStatusResponse(this, requestMeta)) return
+					const message = error && (error.message || error.msg) || '报告状态获取失败'
+					if(showErrorToast && !this.hasStatusErrorToastShown){
+						this.$showToast(message)
+						this.hasStatusErrorToastShown = true
+					}
+					this.startPolling()
+				}).finally(() => {
+					finishStatusRequest(this, requestMeta)
+				})
+			},
+			handleAction(){
+				if(this.generation.state===-1){
+					this.handleReCreate()
+					return
+				}
+				uni.navigateBack()
+			},
+			handleReCreate(){
+				this.$api.get(`/core/team/questionnaire/genReport/${this.info.teamQuestionnaireId}`).then(({data:res})=>{
+					if(res.code!==0) return this.$showToast(res.msg)
+					const nextReportId = extractReportId(res.data)
+					if(!nextReportId) return this.$showToast('未获取到报告编号')
+					this.$showToast('重新生成成功')
+					uni.redirectTo({
+						url:'/pagesHome/reportResult?reportId='+nextReportId+'&info='+encodeURIComponent(JSON.stringify(this.info))
+					})
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.default_page{
+		background: #F7F7F7;
+		.box{
+			width: 100%;
+			background: #FFFFFF;
+			border-radius: 36rpx 36rpx 0rpx 0rpx;
+			padding: 54rpx 0 64rpx;
+			&-loading{
+				width: 340rpx;
+				height: 340rpx;
+			}
+			&-p1{
+				font-family: PingFang-SC, PingFang-SC;
+				font-weight: bold;
+				font-size: 32rpx;
+				color: #002846;
+				line-height: 40rpx;
+				text-align: center;
+				margin-top: 41rpx;
+			}
+			&-p2{
+				font-family: PingFang-SC, PingFang-SC;
+				font-weight: bold;
+				font-size: 28rpx;
+				color: #002846;
+				line-height: 28rpx;
+				text-align: center;
+				margin-top: 23rpx;
+			}
+			&-p3{
+				padding: 0 100rpx;
+				font-family: PingFangSC, PingFang SC;
+				font-weight: 400;
+				font-size: 28rpx;
+				color: #667E90;
+				line-height: 42rpx;
+				text-align: center;
+				margin-top: 24rpx;
+			}
+		}
+		
+		.form{
+			width: 100%;
+			margin-top: 20rpx;
+			&-item{
+				padding: 28rpx 24rpx;
+				background: #FFFFFF;
+				box-shadow: inset 0rpx -1rpx 0rpx 0rpx #EFEFEF;
+				&-left{
+					width: 140rpx;
+					font-family: PingFangSC, PingFang SC;
+					font-weight: 400;
+					font-size: 30rpx;
+					color: #002846;
+					line-height: 42rpx;
+				}
+				&-right{
+					width: calc(100% - 140rpx);
+					padding-left: 20rpx;
+					box-sizing: border-box;
+					font-family: PingFangSC, PingFang SC;
+					font-weight: 400;
+					font-size: 30rpx;
+					color: #667E90;
+					line-height: 32rpx;
+					text-align: right;
+				}
+			}
+		}
+		
+		.btn{
+			width: calc(100% -100rpx);
+			margin: 120rpx 50rpx 0;
+		}
+	}
+</style>

+ 77 - 0
tests/reportGenerationStatus.test.js

@@ -0,0 +1,77 @@
+const assert = require('node:assert/strict')
+const {
+	applyGenerationStatus,
+	replaceFirstPage,
+	extractReportId,
+	shouldResumePolling,
+	reconcileStatusQueryError,
+	createStatusRequestState,
+	setStatusPageVisible,
+	beginStatusRequest,
+	shouldApplyStatusResponse,
+	finishStatusRequest,
+	createListRefreshState,
+	beginListRefresh,
+	createListRequestMeta,
+	shouldApplyListResponse,
+	beginListPageRequest,
+	finishListPageRequest
+} = require('../utils/reportGenerationStatus')
+
+const failedState = applyGenerationStatus({ state:-1, errorMessage:'Step 4 failed' })
+assert.equal(failedState.title, '报告生成失败')
+assert.equal(failedState.errorMessage, 'Step 4 failed')
+assert.equal(failedState.polling, false)
+
+assert.deepEqual(
+	replaceFirstPage([{ reportId:9, state:0 }], [{ reportId:9, state:-1 }]),
+	[{ reportId:9, state:-1 }]
+)
+const nextFirstPage = [{ reportId:10, state:1 }]
+const replacedFirstPage = replaceFirstPage([{ reportId:9, state:0 }], nextFirstPage)
+assert.deepEqual(replacedFirstPage, nextFirstPage)
+assert.notEqual(replacedFirstPage, nextFirstPage)
+assert.equal(shouldResumePolling(9, { state:0 }), true)
+assert.equal(shouldResumePolling(9, { state:1 }), false)
+assert.equal(shouldResumePolling('', { state:0 }), false)
+
+const retryAfterTransientError = reconcileStatusQueryError({ reportId:9, state:0, status:'RUNNING' })
+assert.equal(retryAfterTransientError.state, 0)
+assert.equal(retryAfterTransientError.polling, true)
+assert.equal(retryAfterTransientError.title, '报告正在生成中~')
+assert.equal(retryAfterTransientError.imageKey, 'success')
+
+const statusState = createStatusRequestState()
+assert.equal(beginStatusRequest(statusState), null)
+setStatusPageVisible(statusState, true)
+const firstStatusRequest = beginStatusRequest(statusState)
+assert.deepEqual(firstStatusRequest, { requestId:1, visibilityToken:0 })
+assert.equal(beginStatusRequest(statusState), null)
+assert.equal(shouldApplyStatusResponse(statusState, firstStatusRequest), true)
+setStatusPageVisible(statusState, false)
+assert.equal(shouldApplyStatusResponse(statusState, firstStatusRequest), false)
+setStatusPageVisible(statusState, true)
+const resumedStatusRequest = beginStatusRequest(statusState)
+assert.deepEqual(resumedStatusRequest, { requestId:2, visibilityToken:1 })
+finishStatusRequest(statusState, resumedStatusRequest)
+assert.equal(statusState.statusRequestInFlight, false)
+
+const listRefreshState = createListRefreshState()
+const firstGeneration = beginListRefresh(listRefreshState)
+const secondGeneration = beginListRefresh(listRefreshState)
+assert.equal(firstGeneration, 1)
+assert.equal(secondGeneration, 2)
+assert.equal(shouldApplyListResponse(listRefreshState.refreshGeneration, createListRequestMeta(firstGeneration, 1)), false)
+assert.equal(shouldApplyListResponse(listRefreshState.refreshGeneration, createListRequestMeta(secondGeneration, 1)), true)
+const pageRequest = beginListPageRequest(listRefreshState, secondGeneration, 2)
+assert.deepEqual(pageRequest, { generation:2, page:2, key:'2:2' })
+assert.equal(beginListPageRequest(listRefreshState, secondGeneration, 2), null)
+finishListPageRequest(listRefreshState, pageRequest)
+assert.deepEqual(beginListPageRequest(listRefreshState, secondGeneration, 2), { generation:2, page:2, key:'2:2' })
+
+assert.equal(extractReportId(12), 12)
+assert.equal(extractReportId({ reportId:13 }), 13)
+assert.equal(extractReportId({ dto:{ reportId:14 } }), 14)
+assert.equal(extractReportId({ info:{ id:15 } }), 15)
+
+console.log('report generation status rules: PASS')

+ 201 - 0
utils/reportGenerationStatus.js

@@ -0,0 +1,201 @@
+const RUNNING_STATE = 0
+const SUCCESS_STATE = 1
+const FAILED_STATE = -1
+
+function toNumber(value) {
+	if (value === '' || value === null || value === undefined) return undefined
+	const numeric = Number(value)
+	return Number.isNaN(numeric) ? undefined : numeric
+}
+
+function pickReportId(source) {
+	if (source === null || source === undefined) return undefined
+	if (typeof source === 'number') return source
+	if (typeof source === 'string') {
+		const numeric = toNumber(source)
+		return numeric === undefined ? undefined : numeric
+	}
+	if (typeof source !== 'object') return undefined
+	const direct = toNumber(source.reportId ?? source.id)
+	if (direct !== undefined) return direct
+	for (const key of ['data', 'dto', 'info']) {
+		const nested = pickReportId(source[key])
+		if (nested !== undefined) return nested
+	}
+	return undefined
+}
+
+function extractReportId(payload) {
+	return pickReportId(payload)
+}
+
+function normalizeGenerationStatus(payload = {}) {
+	const source = payload && typeof payload === 'object' ? payload : {}
+	const state = toNumber(source.state)
+	return {
+		reportId: extractReportId(source) ?? '',
+		state: state === undefined ? RUNNING_STATE : state,
+		status: source.status || '',
+		errorMessage: source.errorMessage || ''
+	}
+}
+
+function applyGenerationStatus(payload = {}) {
+	const generation = normalizeGenerationStatus(payload)
+	if (generation.state === FAILED_STATE) {
+		return {
+			...generation,
+			title: '报告生成失败',
+			subtitle: '',
+			description: generation.errorMessage || '失败原因:网络延迟',
+			errorMessage: generation.errorMessage || '网络延迟',
+			polling: false,
+			actionText: '重新生成',
+			imageKey: 'fail'
+		}
+	}
+	if (generation.state === SUCCESS_STATE) {
+		return {
+			...generation,
+			title: '报告生成成功',
+			subtitle: '',
+			description: '报告已生成完成,你可以去我的PREILL报告查阅报告结果。',
+			errorMessage: '',
+			polling: false,
+			actionText: '返回',
+			imageKey: 'success'
+		}
+	}
+	return {
+		...generation,
+		title: '报告正在生成中~',
+		subtitle: '预计所需时间1小时左右',
+		description: '我们会在报告生成完成后提示你,你可以去我的PREILL报告查阅报告结果。',
+		errorMessage: '',
+		polling: true,
+		actionText: '返回',
+		imageKey: 'success'
+	}
+}
+
+function replaceFirstPage(currentList, nextList) {
+	void currentList
+	return Array.isArray(nextList) ? nextList.slice() : []
+}
+
+function shouldResumePolling(reportId, generation = {}) {
+	return Boolean(reportId && normalizeGenerationStatus(generation).state === RUNNING_STATE)
+}
+
+function reconcileStatusQueryError(currentGeneration = {}) {
+	return applyGenerationStatus(normalizeGenerationStatus(currentGeneration))
+}
+
+function createStatusRequestState() {
+	return {
+		isPageVisible: false,
+		statusVisibilityToken: 0,
+		statusRequestSeq: 0,
+		statusActiveRequestId: 0,
+		statusRequestInFlight: false
+	}
+}
+
+function setStatusPageVisible(state, visible) {
+	if (!state || state.isPageVisible === visible) return state ? state.statusVisibilityToken : 0
+	state.isPageVisible = visible
+	if (!visible) {
+		state.statusVisibilityToken += 1
+		state.statusActiveRequestId = 0
+		state.statusRequestInFlight = false
+	}
+	return state.statusVisibilityToken
+}
+
+function beginStatusRequest(state) {
+	if (!state || !state.isPageVisible || state.statusRequestInFlight) return null
+	state.statusRequestSeq += 1
+	state.statusActiveRequestId = state.statusRequestSeq
+	state.statusRequestInFlight = true
+	return {
+		requestId: state.statusActiveRequestId,
+		visibilityToken: state.statusVisibilityToken
+	}
+}
+
+function shouldApplyStatusResponse(state, meta) {
+	return Boolean(
+		state
+		&& meta
+		&& state.isPageVisible
+		&& state.statusRequestInFlight
+		&& state.statusActiveRequestId === meta.requestId
+		&& state.statusVisibilityToken === meta.visibilityToken
+	)
+}
+
+function finishStatusRequest(state, meta) {
+	if (!state || !meta) return
+	if (state.statusActiveRequestId === meta.requestId) {
+		state.statusActiveRequestId = 0
+		state.statusRequestInFlight = false
+	}
+}
+
+function createListRefreshState() {
+	return {
+		refreshGeneration: 0,
+		inFlightPageRequests: {}
+	}
+}
+
+function beginListRefresh(state) {
+	if (!state) return 0
+	state.refreshGeneration += 1
+	return state.refreshGeneration
+}
+
+function createListRequestMeta(generation, page) {
+	return { generation, page }
+}
+
+function shouldApplyListResponse(currentGeneration, meta) {
+	return Boolean(meta && currentGeneration === meta.generation)
+}
+
+function beginListPageRequest(state, generation, page) {
+	if (!state) return null
+	const key = `${generation}:${page}`
+	if (state.inFlightPageRequests[key]) return null
+	const meta = { generation, page, key }
+	state.inFlightPageRequests[key] = true
+	return meta
+}
+
+function finishListPageRequest(state, meta) {
+	if (!state || !meta || !meta.key) return
+	delete state.inFlightPageRequests[meta.key]
+}
+
+module.exports = {
+	RUNNING_STATE,
+	SUCCESS_STATE,
+	FAILED_STATE,
+	extractReportId,
+	normalizeGenerationStatus,
+	applyGenerationStatus,
+	replaceFirstPage,
+	shouldResumePolling,
+	reconcileStatusQueryError,
+	createStatusRequestState,
+	setStatusPageVisible,
+	beginStatusRequest,
+	shouldApplyStatusResponse,
+	finishStatusRequest,
+	createListRefreshState,
+	beginListRefresh,
+	createListRequestMeta,
+	shouldApplyListResponse,
+	beginListPageRequest,
+	finishListPageRequest
+}