2
0

6 Коммитууд 2ef1e70911 ... 0a32b10d2e

Эзэн SHA1 Мессеж Огноо
  xieyunhui 0a32b10d2e updated 2 сар өмнө
  xieyunhui 80d199b30a Merge branch 'master' of http://47.97.198.219:3000/xeeyh/Web_TracerMethodology 8 сар өмнө
  xieyunhui 149d09d686 Merge branch 'v0.7.7' 8 сар өмнө
  xieyunhui d2ba9b3d2a 调整home界面布局 2 жил өмнө
  xieyunhui 0716f93aa2 更改查核项目记分逻辑 2 жил өмнө
  xieyunhui be6ae450f9 只是调整Ip 3 жил өмнө
74 өөрчлөгдсөн 4973 нэмэгдсэн , 1811 устгасан
  1. BIN
      .DS_Store
  2. 417 211
      App.vue
  3. BIN
      components/.DS_Store
  4. 111 0
      components/app-confirm/app-confirm.vue
  5. 8 0
      components/tm-modal/tm-modal.vue
  6. 34 2
      components/tm-tabbar/tm-tabbar.vue
  7. 23 23
      components/tm-upload-img/tm-upload-img.vue
  8. 6 0
      main.js
  9. 32 5
      manifest.json
  10. 98 88
      pages.json
  11. BIN
      pages/.DS_Store
  12. 804 0
      pages/ai-qa/ai-qa.vue
  13. 530 0
      pages/applyAuth/applyAuth.vue
  14. BIN
      pages/applyAuth/images/bgtop.png
  15. BIN
      pages/applyAuth/images/blue_logo.png
  16. BIN
      pages/applyAuth/images/depart.png
  17. BIN
      pages/applyAuth/images/gray_logo.png
  18. BIN
      pages/applyAuth/images/hosp.png
  19. BIN
      pages/applyAuth/images/name.png
  20. BIN
      pages/applyAuth/images/number.png
  21. BIN
      pages/applyAuth/images/remark.png
  22. BIN
      pages/applyAuth/images/time.png
  23. 26 33
      pages/calendar/calendar.vue
  24. 8 2
      pages/checkMainPoints/checkMainPoints.vue
  25. 45 43
      pages/creatingSituations/components/preview.vue
  26. 8 7
      pages/creatingSituations/components/utils.js
  27. 328 383
      pages/creatingSituations/creatingSituations.vue
  28. 11 0
      pages/creatingSituations/server.js
  29. 28 20
      pages/home/home.vue
  30. 219 130
      pages/login/login.vue
  31. 2 1
      pages/login/model.js
  32. 98 88
      pages/mainPointsDetail/mainPointsDetail.vue
  33. 2 3
      pages/mission/mission.vue
  34. 71 73
      pages/planList/planList.vue
  35. 1 1
      pages/situationDetail/situationDetail.vue
  36. 716 670
      pages/situationsCenter/situationsCenter.vue
  37. BIN
      static/.DS_Store
  38. BIN
      static/ai_avatar.png
  39. BIN
      static/ai_banner.png
  40. BIN
      static/ai_bg.png
  41. BIN
      static/ai_copy.png
  42. BIN
      static/aisend_btn_white.png
  43. BIN
      static/aiwenda_tabicon.png
  44. BIN
      static/aiwenda_tabicon_color.png
  45. BIN
      static/collapase_up.png
  46. BIN
      static/images/bgtop.png
  47. BIN
      static/images/blue_logo.png
  48. BIN
      static/images/depart.png
  49. BIN
      static/images/gean.png
  50. BIN
      static/images/gray_logo.png
  51. BIN
      static/images/hosp.png
  52. BIN
      static/images/login_account_icon.png
  53. BIN
      static/images/login_pwd_icon.png
  54. BIN
      static/images/name.png
  55. BIN
      static/images/number.png
  56. BIN
      static/images/quwanshan_btn.png
  57. BIN
      static/images/remark.png
  58. BIN
      static/images/shade.png
  59. BIN
      static/images/shade_old.png
  60. BIN
      static/images/time.png
  61. BIN
      static/images/xitongzhuizong.png
  62. BIN
      static/images/zichaducha.png
  63. BIN
      static/images/zu_text.png
  64. BIN
      uni_modules/.DS_Store
  65. 553 0
      utils/ai-api.js
  66. 273 0
      utils/ai-websocket.js
  67. 229 0
      utils/authorize.js
  68. 54 0
      utils/confirm.js
  69. 95 0
      utils/device.js
  70. 82 0
      utils/markdown.js
  71. 2 3
      utils/request.js
  72. 56 22
      utils/requestUrl.js
  73. 3 3
      utils/uploadImg.js
  74. BIN
      uview-ui/.DS_Store

BIN
.DS_Store


+ 417 - 211
App.vue

@@ -1,226 +1,432 @@
+ 
 <script>
-	import {loginAfterHandle} from './utils/loginHandle.js'
-	export default {
-		onLaunch: function(e) {
-			//当url存在loginData时直接种上数据
-			/* 条件编译,仅在H5平台生效 */
+	import {
+		loginAfterHandle
+	} from './utils/loginHandle.js'
+  import AppConfirm from '@/components/app-confirm/app-confirm.vue'
+  // #ifdef H5
+  import Vue from 'vue'
+  // #endif
+	export default {
+    components:{ AppConfirm },
+		globalData: {
+			isPad: false,
+			lastAuthCheckTime: 0
+		},
+			onLaunch: function(e) {
+			//当url存在loginData时直接种上数据
+			/* 条件编译,仅在H5平台生效 */
 			// #ifdef H5
-			
-			if(e.query.loginData&&e.query.hospSign){
-				try{
-					loginAfterHandle(JSON.parse(decodeURI(e.query.loginData)),e.query.hospSign);
-				}catch(e){
-					console.log({e})
+
+			if (e.query.loginData && e.query.hospSign) {
+				try {
+					loginAfterHandle(JSON.parse(decodeURI(e.query.loginData)), e.query.hospSign);
+				} catch (e) {
+					console.log({
+						e
+					})
 				}
 			}
+
+
+			uni.getSystemInfo({
+				success:(e)=>{
+					/* 窗口宽度大于420px且不在PC页面且不在移动设备时跳转至 PC.html 页面 */
+					console.log(e, window.top.isPC, !/iOS|Android/i.test(e.system));
+					this.globalData.isPad =
+						e.deviceType === 'pad' ||
+						/iPad|Pad/i.test(e.model) ||
+						e.screenWidth >= 768
+						
+					if (e.windowWidth > 420 && !window.top.isPC && !/iOS|Android/i.test(e.system)) {
+
+						const url = decodeURIComponent(`/TracerMethodology/static/html/template.html`);
+						// console.log({hospSign,url});
+						window.location.pathname = url;
+						/* 若项目未设置根目录(默认为 / 时),则使用下方代码 */
+						// window.location.pathname = '/static/html/pc.html';
+					}
+				}
+			})
+			// #endif		
+
+			// 统一:H5 端在关闭页签/刷新时清空 AI 问答本地缓存
+			// #ifdef H5
+			try {
+				if (typeof window !== 'undefined') {
+					window.addEventListener('beforeunload', () => {
+						try {
+							uni.removeStorageSync('aiQaMessagesV1');
+						} catch (err) {}
+					});
+				}
+			} catch (err) {}
+			// #endif
 			
-			
-			uni.getSystemInfo({
-				success(e) {
-					/* 窗口宽度大于420px且不在PC页面且不在移动设备时跳转至 PC.html 页面 */
-					console.log(e, window.top.isPC, !/iOS|Android/i.test(e.system));
-
-					if (e.windowWidth > 420 && !window.top.isPC && !/iOS|Android/i.test(e.system)) {
-
-						const url = decodeURIComponent(`/TracerMethodology/static/html/template.html`);
-						// console.log({hospSign,url});
-						window.location.pathname = url;
-						/* 若项目未设置根目录(默认为 / 时),则使用下方代码 */
-						// window.location.pathname = '/static/html/pc.html';
-					}
-				}
-			})
-			// #endif		
-		},
-		onShow: function() {
-
-		},
-		onHide: function() {
-
-		},
-		methods: {
-			// 监测是否传参数
-			checkArguments() {
-				try {
-					if (uni.getSystemInfoSync().platform === 'android') {
-						// 接收第三方app传递的参数 extra;
-						// #ifdef APP-PLUS 
-						if (plus.runtime.arguments) {
-							// patParams: 院区,病区, 床号
-							const {
-								patParams
-							} = JSON.parse(plus.runtime.arguments);
-							console.log({
-								patParams
-							});
-							uni.setStorageSync('patParams', patParams);
-						}
-						// #endif
-					}
-				} catch (e) {}
+			// 创建全局自定义弹窗服务(无侵入替换 uni.showModal)
+			try {
+				if (!uni.__ORIG_SHOW_MODAL__) {
+					uni.__ORIG_SHOW_MODAL__ = uni.showModal;
+					uni.showModal = (options = {}) => {
+						try {
+							const app = getApp && getApp();
+							const root = app && app.$vm;
+							const confirmRef = root && root.$refs && (root.$refs.AppConfirm || root.$refs.appConfirm);
+							if (confirmRef && typeof confirmRef.open === 'function') {
+								return confirmRef.open({
+									title: options.title || '提示',
+									content: options.content || '',
+									confirmText: options.confirmText || '确定',
+									cancelText: options.cancelText || '取消',
+									showCancel: options.showCancel !== false, // 透传 showCancel,默认 true
+									type: options.type || 'default'
+								}).then((ok) => {
+									options.success && options.success({ confirm: ok, cancel: !ok });
+									options.complete && options.complete({ confirm: ok, cancel: !ok });
+									return { confirm: ok, cancel: !ok };
+								});
+							}
+						} catch (err) {}
+						// 兜底:若全局组件未就绪,回退原生
+						return uni.__ORIG_SHOW_MODAL__.call(uni, options);
+					}
+				}
+			} catch (err) {}
+
+			// H5:程序化挂载(仅当静态挂载未就绪时)
+			// #ifdef H5
+			try {
+				const app = getApp && getApp();
+				const root = app && app.$vm;
+				if (root && !(root.$refs && root.$refs.AppConfirm)) {
+					const Ctor = Vue.extend(AppConfirm);
+					const vm = new Ctor();
+					vm.$mount();
+					document.body.appendChild(vm.$el);
+					root.$refs = root.$refs || {};
+					root.$refs.AppConfirm = vm;
+				}
+			} catch (e) {}
+			// #endif
+
+			// APP-PLUS:不再进行运行时 DOM 插入,统一依赖静态挂载的 <AppConfirm />
+
+			// 应用启动时执行设备授权校验
+			this.checkDeviceAuthorization();
+		},
+		onShow: function() {
+			// 所有平台在应用启动或回到前台时都执行设备授权校验
+			this.checkDeviceAuthorization();
+		},
+		onHide: function() {
+			// 应用进入后台或被关闭时,清空 AI 问答的本地缓存
+			try {
+				uni.removeStorageSync('aiQaMessagesV1');
+			} catch (e) {}
+		},
+		methods: {
+			// 启动设备授权校验:基于 IMEI 获取授权状态,根据不同状态进行相应跳转
+			async checkDeviceAuthorization() {
+				console.log('checkDeviceAuthorization 被调用');
+
+				// H5 平台:不走授权流程
+				// #ifdef H5
+				return;
+				// #endif
+
+				// 移除H5的debug授权页面兜底逻辑
+
+				// APP 平台:等待 plusready 再执行,确保能获取到设备标识
+				// #ifdef APP-PLUS
+				try {
+					if (typeof plus === 'undefined' || !plus.device) {
+						console.log('APP 未就绪,等待 plusready 后再执行授权校验');
+						const once = () => {
+							try { document.removeEventListener('plusready', once); } catch (e) {}
+							this.checkDeviceAuthorization();
+						};
+						document.addEventListener('plusready', once);
+						return;
+					}
+				} catch (e) {}
+				// #endif
+				
+				// 检查当前页面路径
+				const pages = getCurrentPages();
+				const currentPage = pages[pages.length - 1];
+				const currentRoute = currentPage ? currentPage.route : '';
+				console.log('当前页面路径:', currentRoute);
+				
+				// 不再存在需要排除的 debug 调试页面
+				
+				// 节流机制:避免短时间内重复检查(5秒内只检查一次)
+				const now = Date.now();
+				if (now - this.globalData.lastAuthCheckTime < 5000) {
+					console.log('授权检查节流:跳过重复检查', { 
+						now, 
+						last: this.globalData.lastAuthCheckTime, 
+						diff: now - this.globalData.lastAuthCheckTime 
+					});
+					return;
+				}
+				this.globalData.lastAuthCheckTime = now;
+				
+				try {
+					const { getDeviceImeiOrUuid } = await import('@/utils/device.js');
+					const { fetchDeviceAuthorization } = await import('@/utils/authorize.js');
+					const imei = await getDeviceImeiOrUuid();
+					if (!imei) {
+						// H5+Debug才会执行到这里,为避免误扰,仅提示一次
+						uni.showToast({ title: '无法获取设备标识', icon: 'none' });
+						return;
+					}
+					console.log('开始调用授权查询接口, IMEI:', imei);
+					const auth = await fetchDeviceAuthorization(imei);
+					console.log('设备授权状态返回结果:', auth);
+					
+					if (!auth) {
+						uni.showToast({ title: '授权查询失败', icon: 'none' });
+						return;
+					}
+					
+					switch (auth.status) {
+						case 'authorized':
+							// 已授权:保存hospSign并跳转到登录页
+							if (auth.hospSign) {
+								uni.setStorageSync('hospSign', auth.hospSign);
+							}
+							// 保存后端region作为业务接口域名
+							if (auth.region) {
+								try {
+									const { setRuntimeRegionDomain } = await import('@/utils/requestUrl.js');
+									setRuntimeRegionDomain(auth.region);
+								} catch (e) {}
+							}
+							// 已授权:根据是否已登录决定是否跳转登录页
+							try {
+								const hasToken = !!uni.getStorageSync('token');
+								if (hasToken) {
+									console.log('设备已授权,已登录,跳过登录跳转');
+									return;
+								}
+							} catch (e) {}
+							console.log('设备已授权,未登录,跳转登录页');
+							uni.reLaunch({ url: `/pages/login/login?hospSign=${encodeURIComponent(auth.hospSign || '')}` });
+							return;
+							
+						case 'reviewing':
+							// 审核中:跳转到申请页面并显示待审核状态
+							if (auth.applyInfo) {
+								uni.setStorageSync('authApplyInfo', auth.applyInfo);
+							}
+							uni.reLaunch({ url: `/pages/applyAuth/applyAuth?imei=${encodeURIComponent(imei)}` });
+							break;
+							
+						case 'expired':
+							// 授权已到期:跳转到申请页面并显示到期状态
+							if (auth.applyInfo) {
+								uni.setStorageSync('authExpiredInfo', auth.applyInfo);
+							}
+							uni.reLaunch({ url: `/pages/applyAuth/applyAuth?imei=${encodeURIComponent(imei)}&expired=true` });
+							break;
+							
+						case 'unauthorized':
+						default:
+							// 未授权:清理可能残留的本地状态并跳转到申请页面
+							try {
+								uni.removeStorageSync('authApplyInfo');
+								uni.removeStorageSync('authExpiredInfo');
+							} catch (e) {}
+							uni.reLaunch({ url: `/pages/applyAuth/applyAuth?imei=${encodeURIComponent(imei)}&status=unauthorized` });
+							break;
+					}
+				} catch (e) {
+					console.warn('设备授权校验失败', e);
+					uni.showToast({ title: '授权校验失败', icon: 'none' });
+				}
+			},
+			// 监测是否传参数
+			checkArguments() {
+				try {
+					if (uni.getSystemInfoSync().platform === 'android') {
+						// 接收第三方app传递的参数 extra;
+						// #ifdef APP-PLUS 
+						if (plus.runtime.arguments) {
+							// patParams: 院区,病区, 床号
+							const {
+								patParams
+							} = JSON.parse(plus.runtime.arguments);
+							console.log({
+								patParams
+							});
+							uni.setStorageSync('patParams', patParams);
+						}
+						// #endif
+					}
+				} catch (e) {}
 			},
 			//角色和对应的跳转页面
 			rolToTarget(nowPermission) {
-				if(nowPermission != 0){
+				if (nowPermission != 0) {
 					let current = this.rolList.find(item => item.permission == nowPermission);
-					if(current){
+					if (current) {
 						// 页面跳转
 						uni.redirectTo({
-							 url: `/${current.pagePath}`
+							url: `/${current.pagePath}`
 						});
 					}
 				}
-			}
-		}
-	};
-</script>
-
-<style lang="scss">
-	@import "uview-ui/index.scss";
-	/*每个页面公共css */
-
-
-	/* 条件编译,仅在H5平台生效 */
-	// #ifdef H5
-	body::-webkit-scrollbar,
-	html::-webkit-scrollbar {
-		display: none;
-	}
-
-	// #endif
-
-	body,
-	uni-app,
-	uni-page,
-	uni-page-wrapper,
-	uni-page-body {
-		height: 100%;
-		font-size: 20rpx;
-		line-height: 30rpx;
-		color: #292C33;
-		background-color: #F5F6FA;
-	}
-
-	view,
-	label,
-	scroll-view {
-		box-sizing: border-box;
-	}
-
-	// 底部固定的按钮
-	.fixed-buttom-btn {
-		position: fixed;
-		left: 0;
-		bottom: 0;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		margin-top: 12.5rpx;
-		width: 100%;
-		height: 75rpx;
-		background-color: #3377FF;
-
-		.btn-text {
-			flex: 1;
-			font-size: 22.5rpx;
-			color: #fff;
-			text-align: center;
-		}
-
-		.btn-text.cancle {
-			line-height: 76.25rpx;
-			background-color: #FFFFFF;
-			color: #3377FF;
-		}
-	}
-
-	// 新建情境样式start
-	.creatingSituations {
-		.title {
-			padding-bottom: 35rpx;
-			padding-left: 25rpx;
-			font-size: 30rpx;
-			line-height: 45rpx;
-			color: #292C33;
-		}
-	}
-
-	// 新建情境样式end
-	// 查核地图列表样式start
-	.check-map-list {
-		overflow: hidden;
-		padding: 0 25rpx;
-
-		.item {
-			position: relative;
-			overflow: hidden;
-			margin-bottom: 25rpx;
-			border-radius: 5rpx;
-			padding-top: 16.25rpx;
-			padding-bottom: 0;
-			width: 100%;
-			background-color: #fff;
-			box-shadow: 0 3.75rpx 12.5rpx 0 rgba(0, 13, 51, 0.1);
-
-			.title-wrap {
-				display: flex;
-				flex-direction: row;
-				align-items: center;
-				padding: 0 25rpx;
-
-				>text {
-					font-size: 35rpx;
+			}
+		}
+	};
+</script>
+
+<style lang="scss">
+	@import "uview-ui/index.scss";
+	/*每个页面公共css */
+
+
+	/* 条件编译,仅在H5平台生效 */
+	// #ifdef H5
+	body::-webkit-scrollbar,
+	html::-webkit-scrollbar {
+		display: none;
+	}
+
+	// #endif
+
+	body,
+	uni-app,
+	uni-page,
+	uni-page-wrapper,
+	uni-page-body {
+		height: 100%;
+		font-size: 20rpx;
+		line-height: 30rpx;
+		color: #292C33;
+		background-color: #F5F6FA;
+	}
+
+	view,
+	label,
+	scroll-view {
+		box-sizing: border-box;
+	}
+
+	// 底部固定的按钮
+	.fixed-buttom-btn {
+		position: fixed;
+		left: 0;
+		bottom: 0;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		margin-top: 12.5rpx;
+		width: 100%;
+		height: 75rpx;
+		background-color: #3377FF;
+
+		.btn-text {
+			flex: 1;
+			font-size: 22.5rpx;
+			color: #fff;
+			text-align: center;
+		}
+
+		.btn-text.cancle {
+			line-height: 76.25rpx;
+			background-color: #FFFFFF;
+			color: #3377FF;
+		}
+	}
+
+	// 新建情境样式start
+	.creatingSituations {
+		.title {
+			padding-bottom: 35rpx;
+			padding-left: 25rpx;
+			font-size: 30rpx;
+			line-height: 45rpx;
+			color: #292C33;
+		}
+	}
+
+	// 新建情境样式end
+	// 查核地图列表样式start
+	.check-map-list {
+		overflow: hidden;
+		padding: 0 25rpx;
+
+		.item {
+			position: relative;
+			overflow: hidden;
+			margin-bottom: 25rpx;
+			border-radius: 5rpx;
+			padding-top: 16.25rpx;
+			padding-bottom: 0;
+			width: 100%;
+			background-color: #fff;
+			box-shadow: 0 3.75rpx 12.5rpx 0 rgba(0, 13, 51, 0.1);
+
+			.title-wrap {
+				display: flex;
+				flex-direction: row;
+				align-items: center;
+				padding: 0 25rpx;
+
+				>text {
+					font-size: 35rpx;
 					line-height: 52.5rpx;
-					font-weight: normal;
-					color: #292C33;
-				}
-
-				>view {
-					display: flex;
-					flex-direction: row;
-					align-items: center;
-					margin-left: 20rpx;
-					// border-radius: 17.5rpx;
-					height: 35rpx;
-					font-size: 17.5rpx;
-					line-height: 35rpx;
-					color: #8F9BB3;
-					background-color: #EDF2FA;
-
-					image {
-						width: 35rpx;
-						height: 35rpx;
-					}
-
-					text {
-						padding-left: 10rpx;
-						padding-right: 20rpx;
-					}
-				}
-			}
-
-			.content {
-				display: flex;
-				flex-direction: column;
-				padding: 11.25rpx 25rpx 20rpx;
-
-				>text {
-					overflow: hidden;
-					white-space: nowrap;
-					text-overflow: ellipsis;
-					font-size: 20rpx;
-					line-height: 30rpx;
-					color: #666F80;
-
-					&:first-child {
-						margin-bottom: 5rpx;
-						font-weight: bold;
-						color: #292C33;
-					}
-				}
-			}
-		}
-	}
-
-	// 查核地图列表样式end
-</style>
+					font-weight: normal;
+					color: #292C33;
+				}
+
+				>view {
+					display: flex;
+					flex-direction: row;
+					align-items: center;
+					margin-left: 20rpx;
+					// border-radius: 17.5rpx;
+					height: 35rpx;
+					font-size: 17.5rpx;
+					line-height: 35rpx;
+					color: #8F9BB3;
+					background-color: #EDF2FA;
+
+					image {
+						width: 35rpx;
+						height: 35rpx;
+					}
+
+					text {
+						padding-left: 10rpx;
+						padding-right: 20rpx;
+					}
+				}
+			}
+
+			.content {
+				display: flex;
+				flex-direction: column;
+				padding: 11.25rpx 25rpx 20rpx;
+
+				>text {
+					overflow: hidden;
+					white-space: nowrap;
+					text-overflow: ellipsis;
+					font-size: 20rpx;
+					line-height: 30rpx;
+					color: #666F80;
+
+					&:first-child {
+						margin-bottom: 5rpx;
+						font-weight: bold;
+						color: #292C33;
+					}
+				}
+			}
+		}
+	}
+
+	// 查核地图列表样式end
+</style>

BIN
components/.DS_Store


+ 111 - 0
components/app-confirm/app-confirm.vue

@@ -0,0 +1,111 @@
+<template>
+	<!-- 全局确认弹窗,依赖已全局注册的 uni-popup -->
+	<uni-popup ref="popup" type="center" :mask-click="false">
+		<view class="confirm-card" :class="[type]">
+			<view class="title" v-if="title">{{ title }}</view>
+			<view class="content" v-if="content">{{ content }}</view>
+			<view class="actions">
+				<view class="btn btn-cancel" @click="onCancel">{{ cancelText }}</view>
+				<view class="btn btn-confirm" :class="[type]" @click="onConfirm">{{ confirmText }}</view>
+			</view>
+		</view>
+	</uni-popup>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			// 文案配置
+			title: '',
+			content: '',
+			confirmText: '确定',
+			cancelText: '取消',
+			// 类型:default | danger
+			type: 'default',
+			// Promise 的 resolve/reject 引用
+			_resolve: null,
+			_reject: null
+		}
+	},
+	methods: {
+		// 打开弹窗
+		open(options = {}) {
+			// 合并配置
+			this.title = options.title || '提示';
+			this.content = options.content || '';
+			this.confirmText = options.confirmText || '确定';
+			this.cancelText = options.cancelText || '取消';
+			this.type = options.type || 'default';
+			// 打开并返回 Promise
+			this.$refs.popup && this.$refs.popup.open();
+			return new Promise((resolve) => {
+				this._resolve = resolve;
+			});
+		},
+		// 确认
+		onConfirm() {
+			this.$refs.popup && this.$refs.popup.close();
+			this._resolve && this._resolve(true);
+		},
+		// 取消
+		onCancel() {
+			this.$refs.popup && this.$refs.popup.close();
+			this._resolve && this._resolve(false);
+		}
+	}
+}
+</script>
+
+<style lang="less">
+.confirm-card {
+	min-width: 560rpx;
+	max-width: 680rpx;
+	background: #fff;
+	border-radius: 16rpx;
+	padding: 28rpx 28rpx 20rpx;
+	box-sizing: border-box;
+}
+.confirm-card .title {
+	font-size: 32rpx;
+	font-weight: 600;
+	color: #1f2333;
+	margin-bottom: 16rpx;
+}
+.confirm-card .content {
+	font-size: 28rpx;
+	line-height: 42rpx;
+	color: #4b5563;
+	margin-bottom: 24rpx;
+}
+.confirm-card .actions {
+	display: flex;
+	justify-content: flex-end;
+	gap: 16rpx;
+}
+.confirm-card .btn {
+	min-width: 140rpx;
+	height: 64rpx;
+	border-radius: 12rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 28rpx;
+	padding: 0 24rpx;
+	box-sizing: border-box;
+}
+.confirm-card .btn-cancel {
+	background: #f3f4f6;
+	color: #374151;
+}
+.confirm-card .btn-confirm.default {
+	background: #3377FF;
+	color: #fff;
+}
+.confirm-card .btn-confirm.danger {
+	background: #ef4444;
+	color: #fff;
+}
+</style>
+
+

+ 8 - 0
components/tm-modal/tm-modal.vue

@@ -1,3 +1,11 @@
+<!--
+ * @Author: xieyunhui awesomedema@gmail.com
+ * @Date: 2021-04-19 15:46:08
+ * @LastEditors: xieyunhui awesomedema@gmail.com
+ * @LastEditTime: 2025-09-09 16:50:09
+ * @FilePath: /web_TracerMethodology/components/tm-modal/tm-modal.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
 <template>
 	<view class="tm-modal" @click="$emit('click')">
 		<slot></slot>

+ 34 - 2
components/tm-tabbar/tm-tabbar.vue

@@ -7,7 +7,8 @@
 			  :key="item.text"
 			  @click="navigateTo(item.pagePath)" >
 				<image
-				  class="icon"
+				  :class="['icon', { 'icon-large': isLargeIcon(item) }]"
+				  :style="isLargeIcon(item) ? { marginBottom: largeIconGap + 'rpx' } : {}"
 				  :src="item.pagePath === currentPagePath
 					  ? item.selectedIconPath
 						: item.iconPath"
@@ -38,6 +39,10 @@
 			return {
 				// 当前路由地址
 				currentPagePath: '',
+				// 需要放大图标的页面路径集合(可扩展)
+				largeIconPages: ['pages/ai-qa/ai-qa'],
+				// 放大图标与文字间距(仅 largeIconPages 生效)
+				largeIconGap: 3,
 				// tabbar列表(这是最全的)
 				tabBarList: [
 					{
@@ -71,6 +76,12 @@
 						iconPath: '/static/tabbar/tabicon-zhineng.png',
 						selectedIconPath: '/static/tabbar/tabicon-actived-zhineng.png',
 						pagePath: 'pages/situationsCenter/situationsCenter' // 页面路径
+					},
+					{
+						text: 'AI问答',
+						iconPath: '/static/aiwenda_tabicon.png',
+						selectedIconPath: '/static/aiwenda_tabicon_color.png',
+						pagePath: 'pages/ai-qa/ai-qa'
 					}
 				],
 				// 不同角色的拥有 tabbar下标(1、管理员 2、查核组长 3、查核组员 4、单位负责人 5、改善者)
@@ -130,6 +141,17 @@
 					  current.tabBarIndexs.map(index => {
 							tabBars.push(this.tabBarList[index]);
             });
+            // H5 平台显示 AI问答,APP 端隐藏入口
+            // #ifdef H5
+            const aiTab = this.tabBarList.find(item => item.pagePath === 'pages/ai-qa/ai-qa');
+            if (aiTab) {
+            	const alreadyHas = tabBars.some(item => item.pagePath === aiTab.pagePath);
+            	if (!alreadyHas) {
+            		const middleIndex = Math.floor(tabBars.length / 2);
+            		tabBars.splice(middleIndex, 0, aiTab);
+            	}
+            }
+            // #endif
             this.rolToTabBars = tabBars;
 					}else {
 						this.rolToTabBars = [];
@@ -137,6 +159,10 @@
         }
         let routes = getCurrentPages();
         this.currentPagePath = routes[routes.length - 1].route;
+			},
+			// 判断该项是否需要使用大图标
+			isLargeIcon(item){
+				return this.largeIconPages.includes(item.pagePath);
 			}
 		}
 	}
@@ -147,10 +173,10 @@
 		position: fixed;
 		left: 0;
 		bottom: 0;
+		z-index: 999;
 		display: flex;
 		width: 100%;
 		height: 87.5rpx;
-		border-top: 0.62rpx solid #DADEE6;
 		background-color: #fff;
 
 		.tab-item {
@@ -166,6 +192,12 @@
 				height: 28.75rpx;
 			}
 
+			// 放大图标样式(仅配置的页面使用)
+			.icon-large {
+				width: 35rpx;
+				height: 38rpx;
+			}
+
 			>text {
 				font-size: 15rpx;
 				color: #3D424D;

+ 23 - 23
components/tm-upload-img/tm-upload-img.vue

@@ -24,13 +24,13 @@
 	 * 2021.2.3
 	 * props:属性说明看tm-radio-gruop.vue
 	 */
-	import uploadImage from "../../utils/uploadImg.js";
+	import uploadImage from "../../utils/uploadImg.js";
 	// #ifdef APP
 	const pictureModule = uni.requireNativePlugin("Wlake-PictureView")
-	const modal = uni.requireNativePlugin('modal');
-	// #endif
-	
-	import {URL} from '../../utils/requestUrl.js';
+	const modal = uni.requireNativePlugin('modal');
+	// #endif
+	
+	import {getURL} from '../../utils/requestUrl.js';
 
 	export default {
 		props: {
@@ -65,9 +65,9 @@
 
 		},
 		methods: {
-			previewHandle(index) {
-				// #ifdef APP
-				console.log(this.filePaths[0],index);
+			previewHandle(index) {
+				// #ifdef APP
+				console.log(this.filePaths[0],index);
 				const picList = this.filePaths;
 				pictureModule.PictureViewerMain({
 						'listPic': picList,
@@ -78,14 +78,14 @@
 						// 	message: ret,
 						// 	duration: 1.5
 						// });
-					});
+					});
 				// #endif
 			},
 			// 上传图片
 			uploadPicture() {
-				if (this.disabled) return;
-
-				const URLParms = URL.split('/');
+				if (this.disabled) return;
+
+				const URLParms = getURL().split('/');
 				const IP = URLParms[0];
 				uni.chooseImage({
 					count: this.isMultiple ? 9 : 1, // 是否多选
@@ -93,18 +93,18 @@
 					// sourceType: ['camera'], //从相册选择,默认时可以选择从相机和相册中选取
 					success: res1 => {
 
-						uploadImage(res1).then(fileList => {
-							// const _fileList = fileList.map(t=>`http://${IP}${t}`);  //横店特殊处理
-							const _fileList = fileList;
+						uploadImage(res1).then(fileList => {
+							// const _fileList = fileList.map(t=>`http://${IP}${t}`);  //横店特殊处理
+							const _fileList = fileList;
 							
-							if (this.isMultiple) {
-									this.$emit('changeFilePaths',{
-										  files:[...this.filePaths, ..._fileList],
-										  index:this.pickIndex
+							if (this.isMultiple) {
+									this.$emit('changeFilePaths',{
+										  files:[...this.filePaths, ..._fileList],
+										  index:this.pickIndex
 									});
-							} else {
-								this.$emit('changeFilePaths',{
-									  files:_fileList,
+							} else {
+								this.$emit('changeFilePaths',{
+									  files:_fileList,
 									  index:this.pickIndex
 								});
 							}
@@ -206,4 +206,4 @@
 			}
 		}
 	}
-</style>
+</style>

+ 6 - 0
main.js

@@ -7,6 +7,8 @@ import dateTimePick from './components/date-time-pick/date-time-pick.vue';
 import dropdown from './components/dt-dropdown/dt-dropdown.vue'
 import row from "./components/tm-trees/row.vue";
 import uniPopup from "./components/uni-popup/uni-popup.vue"
+import AppConfirm from './components/app-confirm/app-confirm.vue'
+import { confirm as confirmService } from './utils/confirm.js'
 
 import uView from "uview-ui";
 Vue.use(uView);
@@ -18,6 +20,10 @@ Vue.component('com-date-time-pick', dateTimePick);
 Vue.component('tm-trees-row', row);
 Vue.component('dropdown', dropdown);
 Vue.component('uni-popup', uniPopup);
+Vue.component('app-confirm', AppConfirm);
+
+// 全局挂载确认服务
+Vue.prototype.$confirm = confirmService;
 
 App.mpType = 'app'
 

+ 32 - 5
manifest.json

@@ -2,7 +2,7 @@
     "name" : "追踪方法学",
     "appid" : "__UNI__03C4C69",
     "description" : "",
-    "versionName" : "0.8.6",
+    "versionName" : "0.8.10",
     "versionCode" : 100,
     "transformPx" : false,
     "app-plus" : {
@@ -83,13 +83,31 @@
                         "spotlight@3x" : "unpackage/res/icons/120x120.png"
                     }
                 }
+            },
+            "splashscreen" : {
+                "androidStyle" : "default"
             }
         },
         "modules" : {
             "Camera" : {},
             "Barcode" : {}
         },
-        "nativePlugins" : {}
+        "nativePlugins" : {
+            "Wlake-PictureView" : {
+                "__plugin_info__" : {
+                    "name" : "图片查看器",
+                    "description" : "图片查看器,支持多张图片,手势放大缩小,保存图片",
+                    "platforms" : "Android,iOS",
+                    "url" : "https://ext.dcloud.net.cn/plugin?id=3832",
+                    "android_package_name" : "uni.UNIE410614",
+                    "ios_bundle_id" : "",
+                    "isCloud" : true,
+                    "bought" : 1,
+                    "pid" : "3832",
+                    "parameters" : {}
+                }
+            }
+        }
     },
     /* SDK配置 */
     "quickapp" : {},
@@ -116,12 +134,21 @@
             "proxy" : {
                 "/TracerMethodology/api/*" : {
                     "pathRewrite" : {
-                        "^/TracerMethodology/api" : ""
+                        "^/TracerMethodology/api" : "/imed/pfm"
                     },
-                    "target" : "http://192.168.1.45:8088", // 内网
+                    // "target" : "http://120.27.235.181:8802", // 内网 
+                    "target" : "http://47.97.190.5:8806",
                     "changeOrigin" : true,
                     "secure" : false,
                     "logLevel" : "debug"
+                },
+                "/ai-qa/*" : {
+                    "target" : "http://116.62.47.88:5003",
+                    "changeOrigin" : true,
+                    "secure" : false,
+                    "pathRewrite" : {
+                        "^/ai-qa" : ""
+                    }
                 }
             }
         },
@@ -131,7 +158,7 @@
             }
         },
         "networkTimeout" : {
-            "request" : 5000
+            "request" : 30000
         },
         "template" : ""
     },

+ 98 - 88
pages.json

@@ -1,11 +1,22 @@
 {
 	"pages": [{
-			"path": "pages/login/login",
-			"style": {
-				"navigationStyle":"custom",
-				 "h5": {
-				             "titleNView": false
-				  }
+			"path": "pages/login/login",
+			"style": {
+				"navigationStyle":"custom",
+				 "h5": {
+				             "titleNView": false
+				  },
+				  "app-plus":{
+					"softinputMode": "adjustResize"
+				}
+			}
+		},
+		{
+			"path": "pages/ai-qa/ai-qa",
+			"style": {
+				"navigationBarTitleText": "AI问答",
+				"enablePullDownRefresh": false,
+				"navigationStyle":"custom"
 			}
 		},
 		{
@@ -47,7 +58,7 @@
 			"path": "pages/home/home",
 			"style": {
 				"navigationBarTitleText": "个人中心",
-				"enablePullDownRefresh": false,
+				"enablePullDownRefresh": false,
 				"navigationStyle":"custom"
 			}
 		},
@@ -97,7 +108,7 @@
 			"path": "pages/situationsCenter/situationsCenter",
 			"style": {
 				"navigationBarTitleText": "情境中心",
-				"enablePullDownRefresh": false,
+				"enablePullDownRefresh": false,
 				"navigationStyle":"custom"
 			}
 		},
@@ -126,7 +137,7 @@
 			"path": "pages/checkMainPoints/checkMainPoints",
 			"style": {
 				"navigationBarTitleText": "查核要点",
-				"enablePullDownRefresh": false,
+				"enablePullDownRefresh": false,
 				"navigationStyle":"custom"
 			}
 		},
@@ -174,122 +185,121 @@
 		}
 	    ,{
             "path" : "pages/resetInfo/resetInfo",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "重置密码",
-                "enablePullDownRefresh": false
-            }
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "重置密码",
+                "enablePullDownRefresh": false
+            }
             
         }
         ,{
             "path" : "pages/batchDistribution/batchDistribution",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "查核批量分配",
-                "enablePullDownRefresh": false
-            }
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "查核批量分配",
+                "enablePullDownRefresh": false
+            }
             
         }
         ,{
             "path" : "pages/responsibleList/responsibleList",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "选择当事人",
-                "enablePullDownRefresh": false
-            }
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "选择当事人",
+                "enablePullDownRefresh": false
+            }
             
         }
         ,{
             "path" : "pages/searchPage/searchPage",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "",
-                "enablePullDownRefresh": false
-            }
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
             
         }
         ,{
             "path" : "pages/selectVisitPerson/selectVisitPerson",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "选择对象",
-                "enablePullDownRefresh": false,
-				"navigationBarShadow":{
-					"colorType":"grey"
-				},
-				"app-plus":{
-					"softinputMode": "adjustResize"
-				}
-            }
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "选择对象",
+                "enablePullDownRefresh": false,
+				"navigationBarShadow":{
+					"colorType":"grey"
+				},
+				"app-plus":{
+					"softinputMode": "adjustResize"
+				}
+            }
             
         }
         ,{
-            "path" : "pages/changeSize/changeSize",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "设置",
-                "enablePullDownRefresh": false
-            }
+            "path" : "pages/applyAuth/applyAuth",
+            "style" :                                                                                    
+            {
+                "enablePullDownRefresh": false,
+                "navigationStyle":"custom",
+                "h5":{ "titleNView": false },
+				"app-plus":{
+					"softinputMode": "adjustResize"
+				}
+            }
             
         }
+        
         ,{
-            "path" : "pages/reports/reports",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "报表",
-                "enablePullDownRefresh": false
-            }
+            "path" : "pages/changeSize/changeSize",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "设置",
+                "enablePullDownRefresh": false
+            }
             
         }
         ,{
-            "path" : "pages/reportPage/reportPage",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "",
-                "enablePullDownRefresh": false
-            }
+            "path" : "pages/reports/reports",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "报表",
+                "enablePullDownRefresh": false
+            }
             
         }
         ,{
             "path" : "pages/reportPage/reportPage",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "",
-                "enablePullDownRefresh": false
-            }
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
             
         }
         ,{
             "path" : "pages/planDetailList/planDetailList",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "",
-                "enablePullDownRefresh": false
-            }
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
             
         }
         ,{
             "path" : "pages/checkGroup/checkGroup",
-            "style" :                                                                                    
-            {
-                "navigationBarTitleText": "",
-                "enablePullDownRefresh": false
-            }
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
             
         }
-    ],
-	"easycom": {
-			"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+    ],
+	"easycom": {
+			"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
 	},
-	"globalStyle": {
-		 // #ifdef APP-PLUS
-		"navigationStyle": "default",
-		// #endif
-		// #ifdef H5
-		"navigationStyle": "default",  //default app  custom  网页
-		// #endif
+	"globalStyle": {
+			"navigationStyle": "default",
 		"autoBackButton": true,
-		// "homeButton": true,
 		"navigationBarTextStyle": "black",
 		"navigationBarTitleText": "追踪方法学",
 		"navigationBarBackgroundColor": "#F8F8F8",
@@ -297,9 +307,9 @@
 		"app-plus": {
 			"background": "#efeff4"
 		},
-		"rpxCalcMaxDeviceWidth": 960, // rpx 计算所支持的最大设备宽度,单位 px,默认值为 960
-		"rpxCalcBaseDeviceWidth": 375, // rpx 计算使用的基准设备宽度,设备实际宽度超出 rpx 计算所支持的最大设备宽度时将按基准宽度计算,单位 px,默认值为 375
-		"rpxCalcIncludeWidth": 750, // rpx 计算特殊处理的值,始终按实际的设备宽度计算,单位 rpx,默认值为 750
+			"rpxCalcMaxDeviceWidth": 960,
+			"rpxCalcBaseDeviceWidth": 375,
+			"rpxCalcIncludeWidth": 750,
 		"onReachBottomDistance": 184
 	}
-}
+}

BIN
pages/.DS_Store


+ 804 - 0
pages/ai-qa/ai-qa.vue

@@ -0,0 +1,804 @@
+<template>
+	<view class="ai-qa-page">
+		<!-- 页面内静态挂载的确认弹窗,统一跨端样式与交互 -->
+		<AppConfirm ref="AppConfirm" />
+		<!-- 自定义导航栏,透明覆盖在顶部 -->
+		<view class="custom-navbar" :style="{ paddingTop: statusBarPx + 'px' }">
+			<text class="nav-title">AI 智能助手</text>
+			<view v-if="messages.length > 0" class="clear-btn" @click="clearChat">
+				<text class="clear-text">清空</text>
+			</view>
+		</view>
+		<!-- 顶部区域 -->
+		<view class="top-section" v-show="messages.length === 0">
+			<image class="ai-bg" src="/static/ai_bg.png" mode="aspectFill" />
+			<image class="ai-banner" src="/static/ai_banner.png" mode="widthFix" />
+		</view>
+
+		<!-- 聊天消息列表 -->
+		<view class="chat-list" v-show="messages.length > 0" :style="{ paddingTop: `${statusBarPx + 88}rpx`, bottom: chatBottom }">
+			<scroll-view scroll-y class="chat-scroll" :scroll-top="scrollTop" :scroll-into-view="scrollIntoViewId" :scroll-with-animation="scrollWithAnimation" lower-threshold="60" @scrolltolower="onScrollToLower" @scroll="onScroll">
+				<view v-for="(msg, index) in messages" :key="index" :class="['message', msg.role]">
+										<!-- AI回复卡片 -->
+					<view v-if="msg.role === 'assistant'" class="ai-card">
+						<!-- AI头像和深度思考标识头部 -->
+						<view class="ai-card-header">
+							<view class="ai-avatar">
+								<image src="/static/ai_avatar.png" mode="aspectFill" />
+							</view>
+							<view v-if="msg.isThinking" class="ai-loading-spinner" aria-label="loading"></view>
+							<view class="thinking-header" @click="toggleThinking(index)">
+								
+								<text class="thinking-text" v-if="msg.isThinking || msg.hasThinking">{{ msg.isThinking ? '深度思考中…' : '已深度思考' }}</text>
+															<view class="toggle-icon" :class="{ 'expanded': msg.showThinking }">
+								<image class="caret-icon" src="/static/collapase_up.png" mode="widthFix" />
+							</view>
+							</view>
+						</view>
+						<!-- 回复内容 -->
+						<view class="response-content" v-show="msg.showThinking">
+							<!-- 思考内容(单独显示,支持Markdown) -->
+							<view v-if="msg.hasThinking && msg.thinkContent" class="thinking-block"><rich-text :nodes="msg.thinkContentHtml"></rich-text></view>
+							<!-- 正文内容(支持Markdown) -->
+							<view class="answer-block"><rich-text :nodes="msg.contentHtml"></rich-text></view>
+						</view>
+						<!-- 复制按钮 -->
+						<view class="copy-icon" v-show="msg.showThinking && !msg.isLoading" @click="copyContent(msg.content)">
+							<image class="copy-icon-img" src="/static/ai_copy.png" mode="widthFix" />
+						</view>
+					</view>
+					<!-- 用户消息 -->
+					<view v-else class="user-bubble">
+						<view class="user-content">
+							{{msg.content}}
+						</view>
+					</view>
+				</view>
+				<!-- 底部锚点:用于自动滚动到最底部 -->
+				<view :id="'bottom-anchor-' + anchorKey" style="height:1px;"></view>
+			</scroll-view>
+		</view>
+
+		<!-- 底部输入框区域 -->
+		<view class="input-bar">
+			<view class="input-wrap" :class="{ 'chat-mode': messages.length > 0 }">
+				<textarea
+					class="input textarea"
+					v-model="question"
+					placeholder="输入你的问题"
+					placeholder-style="color:#A3ADC2;"
+					auto-height="true"
+					confirm-type="send"
+					show-confirm-bar="true"
+					@confirm="onSend"
+					@keyup.ctrl.enter="onSend"
+					@keyup.meta.enter="onSend"
+					@input="onInput"
+				/>
+				<view class="send-btn" :class="{ 'chat-mode': messages.length > 0, 'disabled': (messages.length > 0 && !question.trim()) || isSending }" @click="onSend">
+					<image class="send-icon" src="/static/aisend_btn_white.png" mode="widthFix" />
+				</view>
+			</view>
+		</view>
+		<tm-tabbar :permission="nowPermission" />
+	</view>
+</template>
+
+<script>
+import { askAIStream, formatAIResponse, checkNetworkStatus } from '@/utils/ai-api.js';
+import { markdownToHtml } from '@/utils/markdown.js';
+import AppConfirm from '@/components/app-confirm/app-confirm.vue'
+
+export default {
+ 	components: { AppConfirm },
+	data() {
+		return {
+			nowPermission: '', // 当前权限,用于底部tab显示
+			statusBarPx: 0, // 顶部状态栏高度
+			question: '', // 输入的问题
+			messages: [], // 聊天消息列表
+			scrollTop: 0, // 滚动位置
+			scrollIntoViewId: '', // 滚动锚点ID
+			anchorKey: 0, // 锚点变更键,强制刷新
+			scrollWithAnimation: false, // 默认先不开启动画,等待页面ready后再开启,避免初次抖动
+			statusBarRpx: 0,
+			screenWidth: 0,
+			chatBottom: '105rpx',
+			isSending: false, // 是否正在发送消息,防止重复发送
+			isAutoScroll: true, // 是否自动滚动到底部
+			lastAnchorUpdateTs: 0, // 上次锚点更新时间戳
+			lastScrollTop: 0, // 上次滚动位置
+			persistTimer: null // 本地持久化节流定时器
+		}
+	},
+	created() {
+		// 从缓存读取当前权限
+		this.nowPermission = uni.getStorageSync('nowPermission');
+		// 读取状态栏高度,保证顶部视觉延伸到状态栏
+		const info = uni.getSystemInfoSync();
+		this.statusBarPx = info.statusBarHeight || 0;
+		this.screenWidth = info.screenWidth;
+		this.statusBarRpx = Math.round(this.statusBarPx * 750 / this.screenWidth);
+	},
+	mounted() {
+		this.updateChatBottom();
+		this.$nextTick(() => { this.scrollWithAnimation = true; });
+		// 页面挂载后尝试恢复本地缓存消息
+		this.restoreMessagesFromStorage();
+	},
+	beforeDestroy() {
+		// 页面销毁时无需特殊处理
+	},
+	onHide() {
+		// 页面隐藏时立即保存一次
+		this.saveMessagesImmediate();
+	},
+	methods: {
+		onInput() {
+			this.updateChatBottom();
+		},
+		updateChatBottom() {
+			this.$nextTick(() => {
+				const query = uni.createSelectorQuery().in(this);
+				query.select('.input-bar').boundingClientRect(rect => {
+					if (rect) {
+						const heightRpx = Math.round(rect.height * 750 / this.screenWidth);
+						const bottomOffset = 85; // 与 .input-bar 的 bottom 保持一致,避开底部tabbar
+						this.chatBottom = (heightRpx + bottomOffset) + 'rpx';
+					}
+				}).exec();
+			});
+		},
+		// 从本地恢复消息
+		restoreMessagesFromStorage() {
+			try {
+				const key = 'aiQaMessagesV1';
+				const cached = uni.getStorageSync(key);
+				if (cached && Array.isArray(cached)) {
+					this.messages = cached;
+					// 恢复后滚动到底部
+					this.$nextTick(() => { this.forceScrollToBottom(); });
+				}
+			} catch (e) {
+				console.warn('恢复聊天缓存失败', e);
+			}
+		},
+		// 立即保存消息
+		saveMessagesImmediate() {
+			try {
+				const key = 'aiQaMessagesV1';
+				uni.setStorageSync(key, this.messages);
+			} catch (e) {
+				console.warn('保存聊天缓存失败', e);
+			}
+		},
+		// 节流保存
+		saveMessagesDebounced() {
+			if (this.persistTimer) { clearTimeout(this.persistTimer); }
+			this.persistTimer = setTimeout(() => {
+				this.saveMessagesImmediate();
+			}, 300);
+		},
+		// 处理流式响应(HTTP chunked 实时追加),支持 <think> 思考区分和 Markdown
+		async handleStreamingResponse(question, aiMessageIndex) {
+			return new Promise((resolve, reject) => {
+				// 流式期间关闭滚动动画,避免频繁触发导致抖动
+				this.scrollWithAnimation = false;
+				let fullText = '';
+				let thinking = false;
+				const onChunk = (chunkText, nowFull, rawPiece) => {
+					// 识别思考标签:一旦检测到 <think> 立即进入思考态并标记 hasThinking
+					if (typeof rawPiece === 'string') {
+						if (rawPiece.indexOf('<think>') > -1) {
+							thinking = true;
+							this.$set(this.messages[aiMessageIndex], 'isThinking', true);
+							if (!this.messages[aiMessageIndex].hasThinking) {
+								this.$set(this.messages[aiMessageIndex], 'hasThinking', true);
+								// 初始化思考内容(避免首块为空时不显示)
+								if (!this.messages[aiMessageIndex].thinkContent) {
+									this.$set(this.messages[aiMessageIndex], 'thinkContent', '');
+									this.$set(this.messages[aiMessageIndex], 'thinkContentHtml', '');
+								}
+							}
+						}
+						if (rawPiece.indexOf('</think>') > -1) {
+							thinking = false;
+							this.$set(this.messages[aiMessageIndex], 'isThinking', false);
+							this.$set(this.messages[aiMessageIndex], 'hasThinking', true);
+						}
+					}
+					// 根据 thinking 累积到不同区域
+					if (thinking) {
+						const prev = this.messages[aiMessageIndex].thinkContent || '';
+						const merged = prev + (chunkText || '');
+						this.$set(this.messages[aiMessageIndex], 'thinkContent', merged);
+						this.$set(this.messages[aiMessageIndex], 'thinkContentHtml', markdownToHtml(merged));
+					} else {
+						// 仅在非思考阶段累计正文
+						fullText = nowFull || (fullText + (chunkText || ''));
+						this.$set(this.messages[aiMessageIndex], 'content', fullText);
+						this.$set(this.messages[aiMessageIndex], 'contentHtml', markdownToHtml(fullText));
+					}
+					// 统一调度自动滚动,内部带节流与 nextTick
+					this.scheduleScrollToBottom();
+					// 节流持久化,保证切换tab后还能恢复
+					this.saveMessagesDebounced();
+				};
+				askAIStream(question, onChunk)
+					.then((res) => {
+						this.$set(this.messages[aiMessageIndex], 'isLoading', false);
+						this.$set(this.messages[aiMessageIndex], 'isThinking', false);
+						if (!res || res.success !== true) {
+							reject(new Error(res && res.error ? res.error : '服务异常'));
+							return;
+						}
+						// 流式结束,恢复滚动动画并强制滚动一次到底部
+						this.scrollWithAnimation = true;
+						this.forceScrollToBottom();
+						this.saveMessagesImmediate();
+						resolve();
+					})
+					.catch((err) => {
+						console.error('AI流式请求失败:', err);
+						this.$set(this.messages[aiMessageIndex], 'content', '网络连接失败,请检查网络后重试。');
+						this.$set(this.messages[aiMessageIndex], 'isLoading', false);
+						this.$set(this.messages[aiMessageIndex], 'isThinking', false);
+						// 出错也恢复滚动动画并强制滚动
+						this.scrollWithAnimation = true;
+						this.forceScrollToBottom();
+						this.saveMessagesImmediate();
+						reject(err);
+					});
+			});
+		},
+		// 清空聊天记录
+		async clearChat() {
+			const ok = await this.$refs.AppConfirm.open({
+				title: '确认清空',
+				content: '确定要清空所有聊天记录吗?',
+				confirmText: '清空',
+				cancelText: '取消',
+				type: 'danger'
+			});
+			if (ok) {
+				this.messages = [];
+				// 重置滚动位置
+				this.scrollTop = 0;
+				// 清除本地缓存
+				try { uni.removeStorageSync('aiQaMessagesV1'); } catch (e) {}
+			}
+		},
+		// 切换深度思考展开/收起
+		toggleThinking(index) {
+			this.$set(this.messages[index], 'showThinking', !this.messages[index].showThinking);
+		},
+		// 复制内容
+		copyContent(content) {
+			uni.setClipboardData({
+				data: content,
+				success: () => {
+					uni.showToast({
+						title: '复制成功',
+						icon: 'success',
+						duration: 1500
+					});
+				},
+				fail: () => {
+					uni.showToast({
+						title: '复制失败',
+						icon: 'none',
+						duration: 1500
+					});
+				}
+			});
+		},
+		// 发送按钮点击
+		async onSend(){
+			// 如果输入为空或正在发送中,直接返回
+			if(!this.question.trim() || this.isSending){
+				return;
+			}
+			
+			// 设置发送状态,防止重复发送
+			this.isSending = true;
+			
+			const userQuestion = this.question.trim();
+			
+			// 添加用户消息
+			this.messages.push({role: 'user', content: userQuestion});
+			this.saveMessagesDebounced();
+			
+			// 清空输入框
+			this.question = '';
+			
+			// 滚动到底部
+			this.scheduleScrollToBottom();
+			
+			// 添加AI回复占位消息
+			const aiMessageIndex = this.messages.length;
+			this.messages.push({
+				role: 'assistant', 
+				content: '',
+				showThinking: true,
+				isLoading: true, // 标记为加载状态
+				isThinking: true
+			});
+			this.saveMessagesDebounced();
+			
+			try {
+				// 检查网络连接
+				const isNetworkAvailable = await checkNetworkStatus();
+				if (!isNetworkAvailable) {
+					this.$set(this.messages[aiMessageIndex], 'content', '网络连接不可用,请检查网络设置后重试。');
+					this.$set(this.messages[aiMessageIndex], 'isLoading', false);
+					this.saveMessagesImmediate();
+					return;
+				}
+
+				// 使用模拟流式响应处理
+				await this.handleStreamingResponse(userQuestion, aiMessageIndex);
+				
+			} catch (error) {
+				// 处理网络错误或其他异常
+				let errorMessage = '网络连接失败,请检查网络后重试。';
+				
+				if (error && error.error) {
+					errorMessage = error.error;
+				} else if (error && error.message) {
+					errorMessage = error.message;
+				}
+				
+				this.$set(this.messages[aiMessageIndex], 'content', errorMessage);
+				console.error('AI问答API调用异常:', error);
+			} finally {
+				// 无论成功还是失败,都要结束加载状态和发送状态
+				this.$set(this.messages[aiMessageIndex], 'isLoading', false);
+				this.isSending = false;
+			}
+			
+			// 滚动到底部显示最新消息(强制一次)
+			this.forceScrollToBottom();
+			this.saveMessagesImmediate();
+			
+			this.updateChatBottom(); // 更新底部间距
+		},
+		scheduleScrollToBottom() {
+			if (!this.isAutoScroll) return;
+			const now = Date.now();
+			if (now - this.lastAnchorUpdateTs < 80) return; // 80ms节流
+			this.lastAnchorUpdateTs = now;
+			this.anchorKey++;
+			this.$nextTick(() => {
+				this.scrollIntoViewId = 'bottom-anchor-' + this.anchorKey;
+			});
+		},
+		// 强制滚动到底部:用于生成完成/异常时兜底
+		forceScrollToBottom() {
+			this.anchorKey++;
+			this.$nextTick(() => {
+				this.scrollIntoViewId = 'bottom-anchor-' + this.anchorKey;
+				// 兼容部分端:给一个较大的 scrollTop 值
+				this.scrollTop = 999999;
+			});
+		},
+		onScroll(e) {
+			const top = (e && e.detail && typeof e.detail.scrollTop === 'number') ? e.detail.scrollTop : 0;
+			if (top + 40 < this.lastScrollTop) {
+				this.isAutoScroll = false;
+			}
+			this.lastScrollTop = top;
+		},
+		onScrollToLower() {
+			this.isAutoScroll = true;
+			this.scheduleScrollToBottom();
+		}
+	}
+}
+</script>
+
+<style lang="less" scoped>
+.ai-qa-page {
+	min-height: 100%;
+	padding-bottom: 100rpx; // 预留底部tabbar高度
+	.custom-navbar {
+		position: fixed;
+		top: 0;
+		left: 0;
+		z-index: 10;
+		width: 100%;
+		height: 88rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		background: transparent; // 透明,让背景延伸到顶部
+		backdrop-filter: blur(10rpx); // 添加毛玻璃效果
+	}
+	.nav-title {
+		font-size: 32rpx;
+		color: #1f2333;
+		font-weight: 600;
+		letter-spacing: 1rpx; // 增加字间距
+	}
+	.clear-btn {
+		position: absolute;
+		right: 30rpx;
+		top: 50%;
+		transform: translateY(-50%);
+		padding: 8rpx 16rpx;
+		border-radius: 10rpx;
+		background: rgba(255, 255, 255, 0.8);
+		backdrop-filter: blur(10rpx);
+		cursor: pointer;
+		transition: all 0.3s ease;
+	}
+	.clear-btn:active {
+		transform: translateY(-50%) scale(0.95);
+	}
+	.clear-text {
+		font-size: 24rpx;
+		color: #666;
+		font-weight: 500;
+	}
+	.top-section {
+		position: relative;
+		width: 100%;
+		padding-top: 88rpx;	
+		min-height: 800rpx; // 假设值,确保填充屏幕,实际根据banner调整
+		overflow: hidden; // 确保背景图片不会溢出
+	}
+	.ai-bg {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		z-index: 1;
+		object-fit: cover; // 确保图片正确填充
+	}
+	.ai-banner {
+		position: relative;
+		z-index: 2;
+		width: 100%;
+		display: block; // 去除行内元素底部空隙
+		object-fit: contain; // 确保图片保持比例
+	}
+
+	// 底部输入框容器
+	.input-bar{
+		position: fixed;
+		left: 0;
+		bottom: 85rpx; // 避开底部tabbar
+		z-index: 5;
+		width: 100%;
+		padding: 0 25rpx 25rpx;
+		box-sizing: border-box;
+	}
+	.input-wrap{
+		position: relative;
+		border-radius: 22rpx; // 微调圆角使条形更贴设计
+		border: 1px solid transparent; // 初始状态:透明边框配合渐变背景
+		background:
+			linear-gradient(#fff, #fff) padding-box,
+			linear-gradient(90deg, rgba(141, 102, 255, 1), rgba(52, 119, 255, 1)) border-box;
+		overflow: hidden; // 裁剪内部使圆角生效
+		padding-right: 96rpx; // 预留发送按钮空间
+		transition: all 0.3s ease; // 添加过渡动画
+	}
+	.input-wrap.chat-mode {
+		border: 3rpx solid #E7EDF2; // 对话状态:实线边框
+		background: #fff; // 白色背景
+	}
+	.input{
+		width: 100%;
+		min-height: 56rpx; // 提高默认高度
+		max-height: 220rpx;
+		padding: 28rpx 28rpx 4rpx; // 按单行时的垂直居中计算(64-40)/2=12,28=16+12
+		font-size: 28rpx;
+		color: #292C33;
+		box-sizing: border-box;
+		background: transparent; // 确保背景透明
+		border: none; // 移除边框
+		outline: none; // 移除轮廓
+	}
+	// 多行输入样式
+	.textarea{
+		position: relative;
+		top:-4rpx;
+		line-height: 40rpx;
+		overflow-y: auto;
+		resize: none; // 禁用调整大小
+	}
+	.send-btn{
+		position: absolute;
+		right: 10rpx;
+		bottom: 18rpx; // 固定在右下角
+		width: 52rpx;
+		height: 52rpx;
+		border-radius: 16rpx;
+		background: linear-gradient(90deg, #8C66FE 0%, #3377FF 100%); // 初始状态:渐变背景
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		cursor: pointer; // 添加鼠标指针样式
+		transition: all 0.3s ease; // 添加过渡动画
+	}
+	.send-btn.chat-mode {
+		background: #3377FF; // 对话状态:纯色背景
+	}
+	.send-btn.disabled {
+		background: #7A8599 !important; // 禁用状态下的灰色背景,优先级最高
+		cursor: not-allowed; // 禁用状态下的鼠标样式
+	}
+	.send-btn:not(.disabled):active {
+		transform: scale(0.95); // 只有在非禁用状态下才有点击缩小效果
+	}
+	.send-icon{
+		width: 45rpx;
+		height: 45rpx;
+		object-fit: contain; // 确保图标正确显示
+	}
+}
+
+// 添加聊天列表样式
+.chat-list {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	padding: 0 30rpx;
+	background: #F5F6FA; // 对话背景色
+	z-index: 3; // 低于input-bar的z5
+	box-sizing: border-box;
+}
+.chat-scroll {
+	height: 100%;
+}
+.message {
+	display: flex;
+	margin-bottom: 25rpx; // 缩小消息间距
+	align-items: flex-start; // 确保头像和消息顶部对齐
+}
+.message:last-child {
+	margin-bottom: 12rpx; // 最后一条消息底部更紧凑
+}
+.message.assistant {
+	flex-direction: row;
+	justify-content: flex-start; // 确保AI消息左对齐
+	width: 100%; // 确保占满宽度
+}
+.message.user {
+	flex-direction: row-reverse;
+	text-align: right;
+	justify-content: flex-end; // 确保用户消息右对齐
+	width: 100%; // 确保占满宽度
+}
+.avatar {
+	width: 40rpx;
+	height: 40rpx;
+	overflow: hidden;
+	flex-shrink: 0; // 防止头像被压缩
+	box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); // 添加头像阴影
+}
+.avatar image {
+	width: 100%;
+	height: 100%;
+	object-fit: cover; // 确保头像图片正确填充
+}
+
+
+// AI卡片样式
+.ai-card {
+	background: #fff; // AI回复卡片背景色为白色
+	border-radius: 16rpx; // 调整圆角以匹配设计稿
+	overflow: hidden; // 确保内部元素不会超出圆角
+	box-shadow: none; // 去除卡片阴影
+	width: 100%; // AI卡片宽度100%
+}
+.ai-card-header {
+	display: flex;
+	align-items: center;
+	padding: 20rpx 30rpx;
+}
+.ai-loading-spinner {
+	width: 28rpx;
+	height: 28rpx;
+	margin-right: 16rpx;
+	border: 4rpx solid rgba(122, 133, 153, 0.15);
+	border-top-color: #7A8599; // 指定颜色
+	border-radius: 50%;
+	animation: ai-rotate 0.9s linear infinite;
+}
+@keyframes ai-rotate {
+	0% { transform: rotate(0deg); }
+	100% { transform: rotate(360deg); }
+}
+.ai-avatar {
+	width: 40rpx;
+	height: 40rpx;
+	border-radius: 50%;
+	overflow: hidden;
+	margin-right: 20rpx;
+	flex-shrink: 0; // 防止头像被压缩
+
+}
+.ai-avatar image {
+	width: 100%;
+	height: 100%;
+	object-fit: cover; // 确保头像图片正确填充
+}
+// 用户消息气泡样式
+.user-bubble {
+	background: #3377FF; // 蓝色背景
+	color: #fff;
+	box-shadow: 0 2rpx 10rpx rgba(51, 119, 255, 0.2);
+	margin-left: auto; // 确保用户消息靠右
+	max-width: 70%; // 用户消息稍微窄一点
+	border-radius: 16rpx; // 添加圆角
+	overflow: hidden; // 确保内容不超出圆角
+}
+
+
+.thinking-header {
+	display: flex;
+	align-items: center;
+	padding: 0; // 移除内边距,因为现在在头部容器内
+	border-bottom: none; // 移除边框,因为头部容器已经有边框
+	cursor: pointer;
+	background: transparent; // 透明背景,因为头部容器已经有背景
+	transition: background-color 0.2s ease; // 添加过渡效果
+	flex: 1; // 占据剩余空间
+}
+.thinking-header:hover {
+	background: rgba(240, 242, 245, 0.5); // 悬停时的半透明背景色
+}
+.thinking-icon {
+	width: 24rpx;
+	height: 24rpx;
+	margin-right: 12rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-shrink: 0; // 防止图标被压缩
+}
+.dot-line {
+	width: 16rpx;
+	height: 2rpx;
+	background: #6B7280;
+	position: relative;
+	border-radius: 1rpx; // 添加圆角
+}
+.dot-line::before,
+.dot-line::after {
+	content: '';
+	position: absolute;
+	width: 4rpx;
+	height: 4rpx;
+	background: #6B7280;
+	border-radius: 50%;
+	top: -1rpx;
+	box-shadow: 0 0 2rpx rgba(107, 114, 128, 0.3); // 添加阴影效果
+}
+.dot-line::before {
+	left: -2rpx;
+}
+.dot-line::after {
+	right: -2rpx;
+}
+.thinking-text {
+	font-size: 24rpx;
+	color: #6B7280;
+	flex: 1;
+	font-weight: 500; // 添加字重
+}
+.toggle-icon {
+	width: 40rpx;
+	height: 40rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	transition: transform 0.3s ease;
+	flex-shrink: 0; // 防止图标被压缩
+	margin-left: auto; // 推到右侧
+}
+.toggle-icon.expanded {
+	transform: rotate(180deg);
+}
+.caret-icon {
+	width: 30rpx;
+	height: 30rpx;
+	object-fit: contain; // 确保图标正确显示
+}
+.response-content {
+	padding: 20rpx 30rpx;
+	padding-top: 0;
+	font-size: 28rpx;
+	line-height: 42rpx;
+	color: #333;
+	word-break: break-all; // 确保长文本正确换行
+	white-space: pre-wrap; // 保持换行格式
+}
+.thinking-block {
+	font-size: 26rpx; // 比正文稍小
+	line-height: 40rpx;
+	color: #7A8599; // 指定灰色
+	padding-bottom: 20rpx;
+	margin-bottom: 20rpx;
+	border-bottom: 2rpx dashed #E2E8F0; // 底部分割虚线
+}
+.answer-block {
+	font-size: 28rpx;
+	line-height: 42rpx;
+	color: #333;
+}
+.copy-icon {
+	padding: 0 30rpx 20rpx;
+	display: flex;
+	justify-content: flex-start; // 左对齐,显示在左下角
+	cursor: pointer; // 添加鼠标指针样式
+}
+.message:last-child .copy-icon {
+	padding-bottom: 8rpx; // 缩小最后一条的底部留白
+}
+.copy-icon-img {
+	width: 32rpx;
+	height: 32rpx;
+	object-fit: contain; // 确保图标正确显示
+	transition: transform 0.2s ease; // 添加过渡效果
+}
+.copy-icon:hover .copy-icon-img {
+	transform: scale(1.1); // 悬停时稍微放大
+}
+
+// 加载状态样式
+.loading-content {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 40rpx 0;
+}
+.loading-dots {
+	display: flex;
+	align-items: center;
+	margin-bottom: 20rpx;
+}
+.dot {
+	width: 12rpx;
+	height: 12rpx;
+	border-radius: 50%;
+	background: #3377FF;
+	margin: 0 6rpx;
+	animation: loading-dot 1.4s infinite ease-in-out;
+}
+.dot:nth-child(1) {
+	animation-delay: -0.32s;
+}
+.dot:nth-child(2) {
+	animation-delay: -0.16s;
+}
+@keyframes loading-dot {
+	0%, 80%, 100% {
+		transform: scale(0.8);
+		opacity: 0.5;
+	}
+	40% {
+		transform: scale(1);
+		opacity: 1;
+	}
+}
+.loading-text {
+	font-size: 26rpx;
+	color: #6B7280;
+	font-weight: 500;
+}
+
+.user-content {
+	padding: 20rpx 30rpx;
+	font-size: 28rpx;
+	line-height: 42rpx;
+	color: #fff;
+	word-break: break-all; // 确保长文本正确换行
+	text-align: right; // 用户消息文本右对齐
+}
+</style>
+
+

+ 530 - 0
pages/applyAuth/applyAuth.vue

@@ -0,0 +1,530 @@
+<template>
+	<view class="apply-auth-page">
+		<!-- 页面内挂载公共弹窗,保证 APP 真机可用 -->
+		<AppConfirm ref="AppConfirm" />
+		<scroll-view class="page-scroll" :scroll-y="true" :scroll-with-animation="true" :scroll-into-view="activeRowId" :style="{ '--kb-gap': kbGap + 'px' }">
+			<!-- 顶部背景图:请将图片放入本页 images 目录,文件名仅使用字母/下划线 -->
+			<view class="top-banner">
+				<image class="top-bg-img" src="/static/images/bgtop.png" mode="widthFix" />
+				<!-- 顶部信息区:logo、标题、说明(覆盖在背景图上) -->
+				<view class="head">
+					<image class="logo" :src="logoSrc" mode="widthFix" @error="onImageError" @load="onImageLoad" />
+					<view class="tit-wrap">
+						<text class="title">{{ pageTitle }}</text>
+						<text class="desc">{{ pageDesc }}</text>
+					</view>
+				</view>
+			</view>
+			<!-- 信息表单卡片 -->
+			<view class="form">
+				<!-- 待审核状态或授权到期状态:只读展示 -->
+				<view v-if="isReviewing || isExpired" class="review-mode">
+					<view class="row">
+						<view class="label-with-icon">
+							<image class="label-icon" src="/static/images/hosp.png" mode="aspectFit" />
+							<text class="label">医院</text>
+						</view>
+						<text class="value">{{ form.hospital }}</text>
+					</view>
+					<view class="row">
+						<view class="label-with-icon">
+							<image class="label-icon" src="/static/images/depart.png" mode="aspectFit" />
+							<text class="label">科室</text>
+						</view>
+						<text class="value">{{ form.department }}</text>
+					</view>
+					<view class="row">
+						<view class="label-with-icon">
+							<image class="label-icon" src="/static/images/name.png" mode="aspectFit" />
+							<text class="label">姓名</text>
+						</view>
+						<text class="value">{{ form.name }}</text>
+					</view>
+					<view class="row">
+						<view class="label-with-icon">
+							<image class="label-icon" src="/static/images/number.png" mode="aspectFit" />
+							<text class="label">电话</text>
+						</view>
+						<text class="value">{{ form.phone }}</text>
+					</view>
+					<view class="row">
+						<view class="label-with-icon">
+							<image class="label-icon" src="/static/images/time.png" mode="aspectFit" />
+							<text class="label">申请时间</text>
+						</view>
+						<text class="value">{{ applyTime }}</text>
+					</view>
+					<view class="row no-border">
+						<view class="label-with-icon">
+							<image class="label-icon" src="/static/images/remark.png" mode="aspectFit" />
+							<text class="label">备注</text>
+						</view>
+						<text class="value">{{ form.remark }}</text>
+					</view>
+				</view>
+				<!-- 申请状态:可输入 -->
+				<view v-else>
+					<view class="row" id="row-hospital"><text class="label">医院</text><input class="ipt" v-model="form.hospital" placeholder="请输入" placeholder-class="ph" @focus="onFocus('row-hospital','hospital')" @blur="onBlur" cursor-spacing="20" confirm-type="next" :confirm-hold="true" @confirm="onConfirmNext('hospital')" :focus="focusField==='hospital'" /></view>
+					<view class="row" id="row-department"><text class="label">科室</text><input class="ipt" v-model="form.department" placeholder="请输入" placeholder-class="ph" @focus="onFocus('row-department','department')" @blur="onBlur" cursor-spacing="20" confirm-type="next" :confirm-hold="true" @confirm="onConfirmNext('department')" :focus="focusField==='department'" /></view>
+					<view class="row" id="row-name"><text class="label">姓名</text><input class="ipt" v-model="form.name" placeholder="请输入" placeholder-class="ph" @focus="onFocus('row-name','name')" @blur="onBlur" cursor-spacing="20" confirm-type="next" :confirm-hold="true" @confirm="onConfirmNext('name')" :focus="focusField==='name'" /></view>
+					<view class="row no-border" id="row-phone"><text class="label">电话</text><input class="ipt" v-model="form.phone" placeholder="请输入" placeholder-class="ph" @focus="onFocus('row-phone','phone')" @blur="onBlur" cursor-spacing="20" confirm-type="next" :confirm-hold="true" @confirm="onConfirmNext('phone')" :focus="focusField==='phone'" /></view>
+				</view>
+			</view>
+			<!-- 备注独立卡片 - 只在申请状态显示 -->
+			<view class="remark-card" v-if="!isReviewing && !isExpired">
+				<view class="row no-border" id="row-remark">
+					<text class="label">备注</text>
+					<input class="ipt" v-model="form.remark" placeholder="请输入(选填)" placeholder-class="ph" @focus="onFocus('row-remark','remark')" @blur="onBlur" cursor-spacing="20" confirm-type="done" @confirm="onConfirmNext('remark')" :focus="focusField==='remark'" />
+				</view>
+			</view>
+			<!-- 在非审核状态或授权到期状态显示提交按钮 -->
+			<view v-if="!isReviewing || isExpired" class="fixed-buttom-btn" :class="{ 'can-submit': canSubmit || isExpired }" @click="onSubmit">
+				<text class="btn-text">{{ isExpired ? '重新申请' : '申请授权' }}</text>
+			</view>
+		</scroll-view>
+	</view>
+</template>
+
+<script>
+import { submitAuthorizationApply } from '@/utils/authorize.js';
+import AppConfirm from '@/components/app-confirm/app-confirm.vue'
+export default {
+	components: { AppConfirm },
+	data() {
+		return {
+			form: {
+				hospital: '',
+				department: '',
+				name: '',
+				phone: '',
+				remark: ''
+			},
+			imei: '',
+			// 当前聚焦的行,用于 scroll-into-view 将其滚入可视区域
+			activeRowId: '',
+			// 键盘占用的高度,H5 通过 visualViewport 获取
+			kbGap: 0,
+			// 是否处于待审核状态
+			isReviewing: false,
+			// 是否授权已到期
+			isExpired: false,
+			// 申请时间
+			applyTime: '',
+			// 当前聚焦字段,用于回车切换焦点
+			focusField: ''
+		}
+	},
+	onLoad(params) {
+		this.imei = params && params.imei ? params.imei : '';
+		// 检查是否授权到期
+		if (params && params.expired === 'true') {
+			this.isExpired = true;
+		}
+		// 若明确携带未授权状态,强制清空本地审核/到期缓存,避免误判
+		if (params && params.status === 'unauthorized') {
+			try {
+				uni.removeStorageSync('authApplyInfo');
+				uni.removeStorageSync('authExpiredInfo');
+			} catch (e) {}
+		}
+		// 检查页面状态(审核中或授权到期)
+		this.checkPageStatus();
+	},
+    mounted(){
+        // 调试信息
+        console.log('页面mounted - 当前状态:', {
+            isReviewing: this.isReviewing,
+            isExpired: this.isExpired,
+            logoSrc: this.logoSrc,
+            pageTitle: this.pageTitle
+        });
+        
+        // 仅 H5:使用 visualViewport 计算键盘占用高度,作为底部填充,避免遮挡
+        // #ifdef H5
+        if (typeof window !== 'undefined' && window.visualViewport){
+            const applyGap = () => {
+                const gap = Math.max(0, window.innerHeight - window.visualViewport.height);
+                this.kbGap = gap;
+                document.documentElement.style.setProperty('--kb-gap', gap + 'px');
+            };
+            window.visualViewport.addEventListener('resize', applyGap);
+            this.$once('hook:beforeDestroy', () => {
+                window.visualViewport && window.visualViewport.removeEventListener('resize', applyGap);
+            });
+            applyGap();
+        }
+        // #endif
+    },
+	computed: {
+		// 判断必填项是否都已填写(备注为选填)
+		canSubmit() {
+			return this.form.hospital && 
+			       this.form.department && 
+			       this.form.name && 
+			       this.form.phone;
+		},
+		// Logo源路径
+		logoSrc() {
+			console.log('计算logoSrc:', { isExpired: this.isExpired, isReviewing: this.isReviewing });
+			if (this.isExpired) {
+				return '/static/images/gray_logo.png';
+			}
+			return '/static/images/blue_logo.png';
+		},
+		// 页面标题
+		pageTitle() {
+			if (this.isExpired) {
+				return '授权已到期';
+			}
+			if (this.isReviewing) {
+				return '申请审核中...';
+			}
+			return '设备未授权';
+		},
+		// 页面描述
+		pageDesc() {
+			if (this.isExpired) {
+				return '如需继续使用设备,请重新申请授权';
+			}
+			if (this.isReviewing) {
+				return '您的申请已提交,我们将在3个工作日内联系您';
+			}
+			return '提交以下信息申请开通,我们将在3个工作日内联系您';
+		}
+	},
+	methods: {
+		// 检查页面状态(审核中或授权到期)
+		checkPageStatus() {
+			try {
+				// 如果是授权到期状态
+				if (this.isExpired) {
+					// 授权到期时,从本地存储获取之前的申请信息用于显示
+					const expiredInfo = uni.getStorageSync('authExpiredInfo');
+					if (expiredInfo && expiredInfo.imei === this.imei) {
+						this.form = {
+							hospital: expiredInfo.hospital || '',
+							department: expiredInfo.department || '',
+							name: expiredInfo.name || '',
+							phone: expiredInfo.phone || '',
+							remark: expiredInfo.remark || ''
+						};
+						this.applyTime = expiredInfo.applyTime || '';
+					}
+					console.log('授权到期状态数据:', { 
+						isExpired: this.isExpired, 
+						form: this.form, 
+						applyTime: this.applyTime 
+					});
+					return;
+				}
+
+				// 检查是否有待审核的申请信息
+				const applyInfo = uni.getStorageSync('authApplyInfo');
+				if (applyInfo && applyInfo.imei === this.imei) {
+					// 如果存在申请信息且IMEI匹配,显示待审核状态
+					this.isReviewing = true;
+					this.form = {
+						hospital: applyInfo.hospital || '',
+						department: applyInfo.department || '',
+						name: applyInfo.name || '',
+						phone: applyInfo.phone || '',
+						remark: applyInfo.remark || ''
+					};
+					this.applyTime = applyInfo.applyTime || '';
+					console.log('待审核状态数据:', { 
+						isReviewing: this.isReviewing, 
+						form: this.form, 
+						applyTime: this.applyTime 
+					});
+					return;
+				}
+
+				// 如果没有找到状态数据,显示默认的申请表单状态
+				console.log('显示申请表单状态:', { 
+					isReviewing: this.isReviewing, 
+					isExpired: this.isExpired,
+					imei: this.imei
+				});
+			} catch (e) {
+				console.error('获取状态信息失败:', e);
+			}
+		},
+		// 格式化申请时间
+		formatApplyTime() {
+			const now = new Date();
+			const year = now.getFullYear();
+			const month = String(now.getMonth() + 1).padStart(2, '0');
+			const day = String(now.getDate()).padStart(2, '0');
+			return `${year}年${month}月${day}日`;
+		},
+		onFocus(id){
+			// 记录当前聚焦行,触发 scroll-into-view
+			this.activeRowId = id;
+			// H5 再次触发一次,等待键盘动画后保证可见
+			// #ifdef H5
+			setTimeout(() => { this.activeRowId = id; }, 250);
+			// #endif
+		},
+		onBlur(){
+			this.activeRowId = '';
+		},
+		// 回车切换到下一个表单项
+		onConfirmNext(field){
+			const order = ['hospital','department','name','phone','remark'];
+			const idx = order.indexOf(field);
+			const next = order[idx + 1];
+			if (next) {
+				this.focusField = next;
+				// 触发展示到视口
+				const idMap = {
+					hospital: 'row-hospital',
+					department: 'row-department',
+					name: 'row-name',
+					phone: 'row-phone',
+					remark: 'row-remark'
+				};
+				this.onFocus(idMap[next]);
+			} else {
+				// 最后一项回车:提交
+				this.focusField = '';
+				this.onSubmit();
+			}
+		},
+		
+		// 图片加载成功
+		onImageLoad() {
+			console.log('Logo加载成功:', this.logoSrc);
+		},
+		
+		// 图片加载失败
+		onImageError(e) {
+			console.error('Logo加载失败:', this.logoSrc, e);
+		},
+		async onSubmit() {
+			// 如果是授权到期状态,切换到申请表单状态
+			if (this.isExpired) {
+				// 清除到期状态数据
+				uni.removeStorageSync('authExpiredInfo');
+				// 重置表单为初始空值(过期态下展示的 data 不能带入填写态)
+				this.form = {
+					hospital: '',
+					department: '',
+					name: '',
+					phone: '',
+					remark: ''
+				};
+				this.applyTime = '';
+				
+				// 切换到申请状态
+				this.isExpired = false;
+				this.isReviewing = false;
+				
+				uni.showToast({ title: '请重新填写申请信息', icon: 'none' });
+				return;
+			}
+
+			// 普通申请状态的校验和提交
+			if (!this.form.hospital || !this.form.department || !this.form.name || !this.form.phone) {
+				uni.showToast({ title: '请完整填写必填信息', icon: 'none' });
+				return;
+			}
+			
+			const payload = { ...this.form, imei: this.imei };
+			try {
+				const ok = await submitAuthorizationApply(payload);
+				if (ok) {
+					// 保存申请信息到本地存储
+					const applyInfo = {
+						...this.form,
+						imei: this.imei,
+						applyTime: this.formatApplyTime()
+					};
+					uni.setStorageSync('authApplyInfo', applyInfo);
+					
+					// 更新为待审核状态
+					this.isReviewing = true;
+					this.applyTime = applyInfo.applyTime;
+					
+					uni.showToast({ title: '提交成功,等待授权', icon: 'none' });
+					// 不再跳转,直接在当前页面显示待审核状态
+				}
+			} catch(e) {
+				// 接口层已统一弹窗提示错误,这里只阻止后续逻辑
+			}
+		}
+	}
+}
+</script>
+
+<style lang="less" scoped>
+.apply-auth-page{
+    background-color: #F5F7FA;
+    /* 让整体随可视视口高度缩放,避免键盘遮挡 */
+    .page-scroll{
+        height: 100vh;
+        height: 100svh;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        padding-bottom: calc(env(safe-area-inset-bottom) + var(--kb-gap, 0px));
+        /* 使 scroll-into-view 目标行在底部仍留出空间 */
+        scroll-padding-bottom: calc(env(safe-area-inset-bottom) + var(--kb-gap, 0px));
+    }
+	.top-banner{
+		position: relative;
+		margin-bottom: 0vh;
+		.top-bg-img{width: 100%; display: block;}
+		.head{
+			position: absolute; left: 24rpx; top: 10vh; right: 24rpx;
+			display:flex;align-items:flex-start;flex-direction:column;
+			padding: 0 30rpx;
+			.logo{
+				width: 120rpx;
+				height: 120rpx;
+				margin-bottom: 60rpx;
+				display: block;
+			}
+			.tit-wrap{display:flex;flex-direction:column;}
+			.title{font-size: 45rpx;color:#292C33;margin-bottom: 20rpx;}
+			.desc{font-size: 25rpx;color:#7A8499;line-height: 40rpx;}
+		}
+	}
+	// 主表单卡片样式
+	.form{
+		background: #fff;
+		border-radius: 18rpx;
+		padding: 0 30rpx;
+		margin:0 50rpx;
+		box-shadow: 0 12rpx 36rpx rgba(0,0,0,0.04);
+		.row{
+			display:flex;align-items:center;min-height: 80rpx;
+			&.no-border{border-bottom: none;}
+			.label{width: 140rpx;color:#525866;font-size: 28rpx;}
+			.ipt{flex:1;height: 100rpx;border: none;padding: 0 4rpx;font-size:28rpx;text-align: right;}
+			.ph{color:#B8BFCC;font-size: 28rpx;}
+			// 待审核状态的只读值样式
+			.value{
+				flex:1;
+				text-align: right;
+				font-size: 28rpx;
+				color: #292C33;
+				padding: 0 4rpx;
+			}
+			// 带图标的标签样式
+			.label-with-icon{
+				width: 180rpx;
+				display: flex;
+				align-items: center;
+				flex-shrink: 0;
+				.label-icon{
+					width: 32rpx;
+					height: 32rpx;
+					margin-right: 16rpx;
+					flex-shrink: 0;
+				}
+				.label{
+					width: auto;
+					flex: 1;
+					white-space: nowrap;
+				}
+			}
+		}
+		// 待审核模式的特殊样式
+		.review-mode{
+			.row{
+				padding: 30rpx 0;
+				min-height: auto;
+			}
+		}
+	}
+	// 备注卡片
+	.remark-card{	
+		margin:0 50rpx;
+		margin-top: 1.5vh;
+		background: #fff;
+		border-radius: 18rpx;
+		padding:5rpx 30rpx;
+		box-shadow: 0 12rpx 36rpx rgba(0,0,0,0.04);
+		.row{
+			display:flex;
+			align-items:center;
+			min-height: 80rpx;
+			// padding: 25rpx 0;
+		}
+		.label{width: 140rpx;color:#525866;font-size: 28rpx;}
+		.ipt{flex:1;height: 100rpx;border: none;padding: 0 4rpx;font-size:28rpx;text-align: right;}
+		.ph{color:#B8C0CC;font-size: 28rpx;}
+		// 待审核状态的只读值样式
+		.value{
+			flex:1;
+			text-align: right;
+			font-size: 28rpx;
+			color: #292C33;
+			padding: 0 4rpx;
+		}
+		// 带图标的标签样式
+		.label-with-icon{
+			width: 180rpx;
+			display: flex;
+			align-items: center;
+			flex-shrink: 0;
+			.label-icon{
+				width: 32rpx;
+				height: 32rpx;
+				margin-right: 16rpx;
+				flex-shrink: 0;
+			}
+			.label{
+				width: auto;
+				flex: 1;
+				white-space: nowrap;
+			}
+		}
+	}
+
+	// 申请授权按钮样式,跟随在内容后面 (使用!important覆盖全局样式)
+	.fixed-buttom-btn{
+		position: static !important;
+		left: auto !important;
+		bottom: auto !important;
+		width: auto !important;
+		margin: 3vh 50rpx 0vh 50rpx !important;
+		height: 100rpx !important; 
+		border-radius: 60rpx !important;
+		// 默认状态:灰色渐变背景
+		background: linear-gradient(135deg, #A3B1CC 0%, #A3B1CC 100%) !important;
+		box-shadow: 0 8rpx 24rpx rgba(139, 157, 195, 0.4) !important;
+		display: flex !important;
+		align-items: center !important;
+		justify-content: center !important; 
+		transition: all 0.3s ease !important;
+		
+		// 可提交状态:蓝色背景
+		&.can-submit {
+			background: #3377FF !important;
+			box-shadow: 0 8rpx 24rpx rgba(51, 119, 255, 0.4) !important;
+		}
+		
+		&:active {
+			transform: scale(0.98);
+		}
+		
+		&:active:not(.can-submit) {
+			box-shadow: 0 4rpx 12rpx rgba(139, 157, 195, 0.3);
+		}
+		
+		&:active.can-submit {
+			box-shadow: 0 4rpx 12rpx rgba(51, 119, 255, 0.3);
+		}
+		
+		.btn-text{
+			flex: none !important;
+			font-size: 34rpx !important; 
+			color: #FFFFFF !important; 
+			font-weight: 600 !important;
+			letter-spacing: 2rpx !important;
+		}
+	}
+}
+</style>
+
+ie a

BIN
pages/applyAuth/images/bgtop.png


BIN
pages/applyAuth/images/blue_logo.png


BIN
pages/applyAuth/images/depart.png


BIN
pages/applyAuth/images/gray_logo.png


BIN
pages/applyAuth/images/hosp.png


BIN
pages/applyAuth/images/name.png


BIN
pages/applyAuth/images/number.png


BIN
pages/applyAuth/images/remark.png


BIN
pages/applyAuth/images/time.png


+ 26 - 33
pages/calendar/calendar.vue

@@ -1,17 +1,12 @@
 <template>
   <view class="calender-page">
     <view class="calender-remind" @click="toMessagePage">
-      <image
-        :src="
-          messageType
-            ? '/static/message-unread.png'
-            : '/static/message-read.png'
-        "
-        mode=""
-      ></image>
+      <image :src="messageType
+          ? '/static/message-unread.png'
+          : '/static/message-read.png'
+        " mode=""></image>
     </view>
-    <view class="cheanged-time" @click="clickTile"
-      >{{ changedYear }}年{{ changedMonth }}月
+    <view class="cheanged-time" @click="clickTile">{{ changedYear }}年{{ changedMonth }}月
       <!-- <text class="weekDay">{{changedDay}}</text> -->
     </view>
     <view>
@@ -20,48 +15,35 @@
     <view>
       <view v-for="item in data1">
         <!-- {{item}} -->
-        <view
-          v-for="(items, index) in item"
-          class="calendarCon"
-          @click="dateClick(items, index)"
-          :style="items.greyFlag ? { color: '#B8BECC' } : { color: '#292C33' }"
-        >
+        <view v-for="(items, index) in item" class="calendarCon" @click="dateClick(items, index)"
+          :style="items.greyFlag ? { color: '#B8BECC' } : { color: '#292C33' }">
           <view :class="items.specificDate == changedDate ? 'date2' : 'date'">
             {{ items.day ? items.day : "" }}
           </view>
-          <view
-            class="yinDate"
-            :style="
-              items.greyFlag ? { color: '#B8BECC' } : { color: '#292C33' }
-            "
-          >
+          <view class="yinDate" :style="items.greyFlag ? { color: '#B8BECC' } : { color: '#292C33' }
+            ">
             {{ items.chineseDay ? items.chineseDay : "" }}
           </view>
           <view :class="items.deptCount != '0' ? 'work' : ''">
             <view class="sum">{{
               items.deptCount != "0" ? items.deptCount : ""
-            }}</view>
+              }}</view>
             <view class="danwei">{{
               items.deptCount != "0" ? "单位" : ""
-            }}</view>
+              }}</view>
           </view>
         </view>
       </view>
     </view>
-    <change-calendar
-      v-if="showCC"
-      :changedYear="changedYear"
-      :changedMonth="changedMonth"
-      @cancelCC="cancelCC"
-      @sureCC="sureCC"
-    ></change-calendar>
+    <change-calendar v-if="showCC" :changedYear="changedYear" :changedMonth="changedMonth" @cancelCC="cancelCC"
+      @sureCC="sureCC"></change-calendar>
     <tm-tabbar />
   </view>
 </template>
 
 <script>
 import websocket from "../../utils/ws.js"; //引入websocket
-import {wsURL} from "../../utils/requestUrl.js";
+import { wsURL } from "../../utils/requestUrl.js";
 import moment from "moment";
 import changeCalendar from "./components/changeCalendar.vue";
 export default {
@@ -220,6 +202,7 @@ export default {
 .calender-page {
   height: 100%;
   position: relative;
+
   .calender-remind {
     width: 62.5rpx;
     height: 62.5rpx;
@@ -229,6 +212,7 @@ export default {
     background: rgba(255, 255, 255, 0.95);
     border-radius: 50%;
     z-index: 2;
+
     image {
       margin-left: 17.5rpx;
       margin-top: 16.87rpx;
@@ -236,15 +220,18 @@ export default {
       height: 28.75rpx;
     }
   }
+
   .cheanged-time {
     padding: 25rpx 0rpx 15rpx 25rpx;
     font-size: 35rpx;
+
     .weekDay {
       padding-left: 15rpx;
       font-size: 20rpx;
       color: #666f80;
     }
   }
+
   .calendarTitle {
     background-color: #fff;
     display: inline-block;
@@ -255,6 +242,7 @@ export default {
     line-height: 55rpx;
     border-bottom: 1.25rpx solid #f5f6fa;
   }
+
   .calendarCon {
     // margin-right: 1.25rpx;
     // margin-top: 1.25rpx;
@@ -267,6 +255,7 @@ export default {
     color: #292c33;
     border-right: 1.25rpx solid #f5f6fa;
     border-bottom: 1.25rpx solid #f5f6fa;
+
     .date {
       margin: 16.25rpx auto 6.25rpx;
       height: 52.5rpx;
@@ -274,6 +263,7 @@ export default {
       line-height: 52.5rpx;
       border-radius: 50%;
     }
+
     .date2 {
       margin: 16.25rpx auto 6.25rpx;
       height: 52.5rpx;
@@ -283,23 +273,27 @@ export default {
       background-color: #3377ff;
       color: #fff;
     }
+
     .yinDate {
       color: #666e80;
       font-size: 17.5rpx;
       line-height: 17.5rpx;
     }
+
     .work {
       width: 87.5rpx;
       height: 50rpx;
       background-color: #ff894d;
       border-radius: 5rpx;
       margin: 16.25rpx auto;
+
       .sum {
         font-size: 20rpx;
         color: #fff;
         font-weight: bold;
         line-height: 30rpx;
       }
+
       .danwei {
         font-weight: 400;
         font-size: 15rpx;
@@ -310,4 +304,3 @@ export default {
   }
 }
 </style>
-

+ 8 - 2
pages/checkMainPoints/checkMainPoints.vue

@@ -89,7 +89,7 @@
 				:class="(detailList.length>0&&active != 2&&finishedStatus != 1)?'scroll-Y':'scroll-Y noBtn'">
 				
 				<view class="list" v-for="(item, index) in detailList" :key="index">
-					<view class="title" v-if="item.responseList.length > 0">查核要点:{{item.checkPointName}}</view>
+					<view class="title" v-if="item.responseList.length > 0">查核要点:{{formatCheckPointTitle(item)}}</view>
 					<view class="item" v-for="(child, n) in item.responseList"
 						@click="childClick(child,item.checkPointId)" :key="n">
 						<view class="top-box">
@@ -354,8 +354,14 @@
 				key: 'isReloadPageData',
 				data: true
 			});
-		},
+		},
 		methods: {
+			// 生成查核要点展示名称,带上 pointSort 前缀
+			formatCheckPointTitle(item) {
+				const { pointSort, checkPointName } = item || {};
+				const sort = (pointSort || pointSort === 0) ? String(pointSort).trim() : '';
+				return sort ? `${sort}.${checkPointName}` : checkPointName;
+			},
 			/**
 			 * @param {number} checkId  查核id 
 			 * @param {number} deptId  部门id 

+ 45 - 43
pages/creatingSituations/components/preview.vue

@@ -14,10 +14,10 @@
 				</div>
 			</div>
 		</div>
-		<div class="endContent">
-			<span>任务名称</span>
-			<input class="inputArea" :value="name" @input="inputHandle" type="text" >
-		</div>
+        <div class="endContent">
+            <span>任务名称</span>
+            <input class="inputArea" v-model="name" type="text" >
+        </div>
 	</view>
 </template>
 
@@ -26,34 +26,36 @@
 		mapState
 	} from "vuex";
 
-	export default {
-		props: {},
-		data() {
-			return {
-                name:''
-			}
-		},
-		computed:{
-			 ...mapState({
-			 	typeList: state => state.creatingSituations.typeList,
-			 	zhinengDepartments:state => state.creatingSituations.zhinengDepartments,
-				situationPreview:state=>state.creatingSituations.situationPreview,
-			 }),
-			 responsibleDepartments:function(){
-				  const a = this.zhinengDepartments.checkedItems.reduce((prev,cur)=>prev+`${cur.name}(${cur.employees})`,'');
-				  return a;
-			 }
-		},
-		methods: {
-             inputHandle(e){
-				 this.name = e.detail.value;
-				 this.$store.commit({
-				 	type: 'creatingSituations/comChangeState', 
-				 	key: 'situationPreview', 
-				 	data:{...this.situationPreview,sitName:e.detail.value}
-				 });
+export default {
+    props: {},
+    data() {
+        return {}
+    },
+    computed:{
+			 ...mapState({
+			 	typeList: state => state.creatingSituations.typeList,
+			 	zhinengDepartments:state => state.creatingSituations.zhinengDepartments,
+				situationPreview:state=>state.creatingSituations.situationPreview,
+			 }),
+         // 双向绑定任务名称到 store
+         name: {
+             get(){
+                 return this.situationPreview && this.situationPreview.sitName ? this.situationPreview.sitName : ''
+             },
+             set(val){
+                 this.$store.commit({
+                     type: 'creatingSituations/comChangeState',
+                     key: 'situationPreview',
+                     data: { ...this.situationPreview, sitName: val }
+                 });
+             }
+         },
+			 responsibleDepartments:function(){
+				  const a = this.zhinengDepartments.checkedItems.reduce((prev,cur)=>prev+`${cur.name}(${cur.employees})`,'');
+				  return a;
 			 }
-		}
+		},
+    methods: {}
 	}
 </script>
 
@@ -125,27 +127,27 @@
 
 		}
 
-		.endContent {
-			display: flex;
-			flex-direction: row;
-			align-items: center;
-			margin-top: 15rpx;
-			padding: 31.25rpx 25rpx;
+		.endContent {
+			display: flex;
+			flex-direction: row;
+			align-items: center;
+			margin-top: 15rpx;
+			padding: 31.25rpx 25rpx;
 			background-color: #fff;
 			span {
 				font-size: 25rpx;
 				font-family: SourceHanSansCN-Normal, SourceHanSansCN;
 				font-weight: 400;
 				color: #525866;
-				&:last-child {
+				&:last-child {
 					display: inline-block;
-					color: rgba(41, 44, 51, 1);
+					color: rgba(41, 44, 51, 1);
 					padding-left: 75rpx;
 				}
-			}
-			.inputArea {
-				padding-left: 31.25rpx;
+			}
+			.inputArea {
+				padding-left: 31.25rpx;
 			}
 		}
 	}
-</style>
+</style>

+ 8 - 7
pages/creatingSituations/components/utils.js

@@ -88,13 +88,14 @@ export const optionsHandle = (arr, conditionIds) => {
  * 构造查核组提交数据
  */
 export const checkGroup = function({list, checkedItem}) {
-	let _list = [...list].map((ntem)=>{
-		return {
-			...ntem,
-			selectFlag: ntem.id === checkedItem.id ? true : false
-		}
-	});
-	return _list;
+    // 放宽校验:若未选择,则默认选中列表第一项以满足后端最小必填
+    const selectedId = (checkedItem && checkedItem.id != null)
+        ? checkedItem.id
+        : (Array.isArray(list) && list.length > 0 ? list[0].id : null);
+    return [...list].map((item) => ({
+        ...item,
+        selectFlag: selectedId != null ? item.id === selectedId : false
+    }));
 }
 /**
  * 构造查核地图提交数据

+ 328 - 383
pages/creatingSituations/creatingSituations.vue

@@ -1,6 +1,9 @@
 <template>
 	<view class="creatingSituations">
 
+		<!-- 页面内静态挂载的确认弹窗,统一跨端样式与交互 -->
+		<AppConfirm ref="AppConfirm" />
+
 		<uni-popup ref="popup" type="bottom" :maskClick="true" @change="popupChangehandle">
 			<tm-radio-group v-if="popupType==1" :list="situationTypeList" :defaultValue='situationPreview.situationType'
 				:setting="{
@@ -12,17 +15,17 @@
 				:setting="{
 			  value: 'id',
 			  name: 'name'
-			}" :openkeys="[0]" @change="templateTypeChanged" />
+			}" :openkeys="[0]" @change="templateTypeChanged" />
 			
 			<tm-radio-group v-if="popupType==3" :list="pointsetTypeList" :defaultValue='situationPreview.pointsetType'
 				:setting="{
 			  value: 'pointsetType',
 			  name: 'pointsetTypeName'
-			}" :openkeys="[0]" @change="pointsetTypeChanged" />
-			<tm-radio-group v-if="popupType==4" :list="checkOrderSetTypeList" :defaultValue='situationPreview.checkOrderSetType'
-				:setting="{
-			  value: 'checkOrderSetType',
-			  name: 'checkOrderSetTypeName'
+			}" :openkeys="[0]" @change="pointsetTypeChanged" />
+			<tm-radio-group v-if="popupType==4" :list="checkOrderSetTypeList" :defaultValue='situationPreview.checkOrderSetType'
+				:setting="{
+			  value: 'checkOrderSetType',
+			  name: 'checkOrderSetTypeName'
 			}" :openkeys="[0]" @change="checkOrderSetTypeChanged" />
 		</uni-popup>
 
@@ -114,14 +117,17 @@
 	import {
 		dateHandle
 	} from "../../utils/dateHandle.js";
+	import AppConfirm from '@/components/app-confirm/app-confirm.vue'
 
 	export default {
 		data() {
 			return {
 				isPlanSet: false, //自查督查负责人操作计划设置
 				saveType: 'POST',
-				editID: '',
+				editID: '',
 				errMsg:'',
+					fromImprove: false, // 是否从“去完善”进入
+					systemSituationType: null, // 系统情境类型:0个案 1普通 2自查督查
 			}
 		},
 		computed: {
@@ -143,26 +149,26 @@
 				zhinengDepartments:state=>state.creatingSituations.zhinengDepartments,
 				typeList:state=>state.creatingSituations.typeList,
 				checkPerson:state=>state.creatingSituations.checkPerson,
-				conditionCard: state => state.creatingSituations.conditionCard,
-				pointsetTypeList:state=>state.creatingSituations.pointsetTypeList   ,//add by yfb 20230417
-				situationCheckOrder:state=>state.creatingSituations.situationCheckOrder, //add by yfb 20230609
+				conditionCard: state => state.creatingSituations.conditionCard,
+				pointsetTypeList:state=>state.creatingSituations.pointsetTypeList   ,//add by yfb 20230417
+				situationCheckOrder:state=>state.creatingSituations.situationCheckOrder, //add by yfb 20230609
 				checkOrderSetTypeList:state=>state.creatingSituations.checkOrderSetTypeList
 			}),
 			situationTypeName() {
 				const temp = this.situationTypeList.filter(item => item.situationType == this.situationPreview
 					.situationType);
 				return temp[0].situationTypeName;
-			},
-			pointsetTypeName(){
-				const temp = this.pointsetTypeList.filter(item => item.pointsetType == this.situationPreview
-					.pointsetType);
-				return temp[0].pointsetTypeName;
-			},	
-			checkOrderSetTypeName(){
-				const temp = this.checkOrderSetTypeList.filter(item => item.checkOrderSetType == this.situationPreview
-					.checkOrderSetType);
-				console.log('2023061502',temp,this.checkOrderSetTypeList);
-				return temp[0].checkOrderSetTypeName;
+			},
+			pointsetTypeName(){
+				const temp = this.pointsetTypeList.filter(item => item.pointsetType == this.situationPreview
+					.pointsetType);
+				return temp[0].pointsetTypeName;
+			},	
+			checkOrderSetTypeName(){
+				const temp = this.checkOrderSetTypeList.filter(item => item.checkOrderSetType == this.situationPreview
+					.checkOrderSetType);
+				console.log('2023061502',temp,this.checkOrderSetTypeList);
+				return temp[0].checkOrderSetTypeName;
 			},
 			botmBtnGroup: function() {
 				if(this.stepActive === 0){
@@ -304,41 +310,45 @@
 				}
 			}
 		},
-		onLoad: function({
+			onLoad: function({
 			id,
 			type,
 			actTarget,
 			situationId,
 			themeName, //情境名
-			systemSituationType, //系统情境类型 0 个案 1 普通 2自查督查
-			//situationCheckOrder, //查核快捷方式类型 1:分类类型;2:全表类型
+			systemSituationType, //系统情境类型 0 个案 1 普通 2自查督查
+			from, // 来源标识:improve 表示从“去完善”进入
+			//situationCheckOrder, //查核快捷方式类型 1:分类类型;2:全表类型
 			
-		}) {
+		}) {
 			this.errMsg='';
-			this.dispatch('getDictionary').then(res => {
-				//获取后处理pointset类型
-				let tmp= res.PointSet.map((item)=>{
-					return {
-						pointsetType: parseInt(item.itemCode),
-						pointsetTypeName: item.itemName,
-					}
-				});
-				this.myCommit('pointsetTypeList', tmp);
-				this.situationPreview.pointsetTypeName=tmp[0].pointsetTypeName;
-	
-				let checkOrdertmp=res.CheckOrder.map((item)=>{
-					return {
-						checkOrderSetType:parseInt(item.itemCode),
-						checkOrderSetTypeName:item.itemName,
-					}
-				});
-				this.myCommit('checkOrderSetTypeList',checkOrdertmp);
-				this.situationPreview.checkOrderSetTypeName=checkOrdertmp[0].checkOrderSetTypeName;
-				
-			});
+			this.dispatch('getDictionary').then(res => {
+				//获取后处理pointset类型
+				let tmp= res.PointSet.map((item)=>{
+					return {
+						pointsetType: parseInt(item.itemCode),
+						pointsetTypeName: item.itemName,
+					}
+				});
+				this.myCommit('pointsetTypeList', tmp);
+				this.situationPreview.pointsetTypeName=tmp[0].pointsetTypeName;
+	
+				let checkOrdertmp=res.CheckOrder.map((item)=>{
+					return {
+						checkOrderSetType:parseInt(item.itemCode),
+						checkOrderSetTypeName:item.itemName,
+					}
+				});
+				this.myCommit('checkOrderSetTypeList',checkOrdertmp);
+				this.situationPreview.checkOrderSetTypeName=checkOrdertmp[0].checkOrderSetTypeName;
+				
+			});
 			
 			this.isPlanSet = actTarget == 'planSet';
 			this.situationId = situationId;
+			this.fromImprove = (from === 'improve');
+			this.systemSituationType = (typeof systemSituationType !== 'undefined' && systemSituationType !== null)
+				? Number(systemSituationType) : this.systemSituationType;
 			
 			
 			this.saveType = type ? type : 'POST';
@@ -347,7 +357,9 @@
 				  
 				  this.myCommit('theme', {
 					  ...this.theme,
-				  	  id: systemSituationType, title:themeName, des: null,
+					  id: systemSituationType, title:themeName, des: null,
+					  // 自查督查非计划设置走 MULTI 流程
+					  type: this.isPlanSet ? this.theme.type : 'MULTI',
 					  situationId:situationId,
 				  }); // 设置为自查督查
 				  
@@ -359,50 +371,89 @@
 			
 			this.dispatch('getSituationTypes').then(res => {
 				this.myCommit('situationTypeList', res);
-			});
-			
+			});
+			
 			
 			if (id) {
 				this.editID = id;
-				this.dispatch('detialConfig', {
-					id
-				}).then((data) => {
-					if (data) {
-						const {
-							topic
-						} = data;
-						let theme = themeList[Number(topic)],
-							condition = editCondition(data),
-							editConfig = {};
-						this.myCommit('theme', theme); // 主题
-						
-						if (type === 'PUT') { // 编辑
-							let checkRent = editCheckRent(data),
-								checkMap = editCheckMap(data),
-								checkPlan = editCheckPlan(data),
-								situationPreview = editSituationPreview(data);
-							this.myCommit('checkPlan', checkPlan); // 查核计划
-							this.myCommit('situationPreview', situationPreview); // 预览
-							editConfig = {
-								theme,
-								condition,
-								checkRent,
-								checkMap,
-								checkPlan,
-								situationPreview
-							};
-
-						} else { // 复制创建
-							this.myCommit('condition', condition); // 条件
-							editConfig = {
-								theme,
-								condition,
-							};
+				const apiKey = (from === 'improve' && Number(systemSituationType) === 2)
+					? 'getFunctionSituationConfig'
+					: 'detialConfig';
+				this.dispatch(apiKey, { id }).then((data) => {
+					if (!data) return;
+					// from=improve 且 systemSituationType==2 时使用的新接口:自查督查专用回显
+					if (apiKey === 'getFunctionSituationConfig') {
+						// 1) 主题:确保为自查督查,并回填情境名与id
+						const themeUpdate = {
+							...this.theme,
+							id: 2,
+							title: data.situationName || this.theme.title,
+							situationId: this.situationId || id
+						};
+						this.myCommit('theme', themeUpdate);
+						// 2) 条件:直接使用后端返回的筛选条件ID集合
+						const conditionIds = Array.isArray(data.filterCodes) ? data.filterCodes : [];
+						this.myCommit('condition', { ...this.condition, conditionIds });
+						// 3) 计划日期:将“YYYY年MM月DD日”转为“YYYY-MM-DD”
+						const normalizeCnDate = (s) => {
+							try { return String(s || '').replace(/年|月/g, '-').replace(/日/g, ''); } catch (e) { return s; }
+						};
+						const start = data.checkPlanStartDate ? normalizeCnDate(data.checkPlanStartDate) : this.checkPlan.dateObj.start;
+						const end = data.checkPlanEndDate ? normalizeCnDate(data.checkPlanEndDate) : this.checkPlan.dateObj.end;
+						this.myCommit('checkPlan', { ...this.checkPlan, dateObj: { ...this.checkPlan.dateObj, start, end } });
+						// 4) 预览名称:可选
+						if (data.situationName) {
+							this.myCommit('situationPreview', { ...this.situationPreview, sitName: data.situationName });
 						}
-						this.myCommit('editConfig', editConfig);
-						// 回到第一步或者第三步
-						this.myCommit('stepActive', type === 'PUT' ? 0 : 2);
+						// 5) MULTI 流程的类型与职能科室回显
+						const filterCodes = Array.isArray(data.filterCodes) ? data.filterCodes : [];
+						const functionCodes = Array.isArray(data.functionCodes) ? data.functionCodes : [];
+						// 类型列表
+						this.dispatch('getTypeLists', { filter: 0 }).then((typeListData) => {
+							if (Array.isArray(typeListData)) {
+								const checkedItems = typeListData.filter(v => filterCodes.includes(v.id));
+								this.$store.commit({ type: 'creatingSituations/comChangeState', key: 'typeList', data: { list: typeListData, checkedItems } });
+							}
+						});
+						// 职能科室列表
+						this.dispatch('getZhinengDepartments').then((deptData) => {
+							if (Array.isArray(deptData)) {
+								const list = deptData;
+								// 预选项需要包含 employees 字段,供预览展示
+								const checkedItems = list
+									.filter(v => functionCodes.includes(v.id))
+									.map(v => ({
+										id: v.id,
+										name: v.name,
+										employees: Array.isArray(v.employeeList) ? v.employeeList.map(e => e.employeeName).join(',') : ''
+									}));
+								this.$store.commit({ type: 'creatingSituations/comChangeState', key: 'zhinengDepartments', data: { list, checkedItems } });
+							}
+						});
+						// 编辑模式回到第一步
+						this.myCommit('stepActive', 0);
+						return;
+					}
+					// 原有接口:出参与保存入参一致,走通用回显
+					const { topic } = data;
+					let theme = themeList[Number(topic)],
+						condition = editCondition(data),
+						editConfig = {};
+					this.myCommit('theme', theme); // 主题
+					if (type === 'PUT') { // 编辑
+						let checkRent = editCheckRent(data),
+							checkMap = editCheckMap(data),
+							checkPlan = editCheckPlan(data),
+							situationPreview = editSituationPreview(data);
+						this.myCommit('checkPlan', checkPlan); // 查核计划
+						this.myCommit('situationPreview', situationPreview); // 预览
+						editConfig = { theme, condition, checkRent, checkMap, checkPlan, situationPreview };
+					} else { // 复制创建
+						this.myCommit('condition', condition); // 条件
+						editConfig = { theme, condition };
 					}
+					this.myCommit('editConfig', editConfig);
+					this.myCommit('stepActive', type === 'PUT' ? 0 : 2);
 				});
 			}
 		},
@@ -427,27 +478,27 @@
 						templateName: item.name
 					}
 				});
-			},
-			pointsetTypeChanged(type, name) {
-				// console.log({type,name});
-				this.$store.commit('creatingSituations/comChangeState', {
-					key: 'situationPreview',
-					data: {
-						...this.situationPreview,
-						pointsetType: type,
-						pointsetTypeName:name.pointsetTypeName
-					}
-				});
-			},
-			checkOrderSetTypeChanged(type, name){
-				this.$store.commit('creatingSituations/comChangeState', {
-					key: 'situationPreview',
-					data: {
-						...this.situationPreview,
-						checkOrderSetType: type,
-						checkOrderSetTypeName:name.checkOrderSetTypeName
-					}
-				});
+			},
+			pointsetTypeChanged(type, name) {
+				// console.log({type,name});
+				this.$store.commit('creatingSituations/comChangeState', {
+					key: 'situationPreview',
+					data: {
+						...this.situationPreview,
+						pointsetType: type,
+						pointsetTypeName:name.pointsetTypeName
+					}
+				});
+			},
+			checkOrderSetTypeChanged(type, name){
+				this.$store.commit('creatingSituations/comChangeState', {
+					key: 'situationPreview',
+					data: {
+						...this.situationPreview,
+						checkOrderSetType: type,
+						checkOrderSetTypeName:name.checkOrderSetTypeName
+					}
+				});
 			},
 			zichaduchaSave: function() {
 				  let data = {
@@ -458,7 +509,18 @@
 					   filterCodes:this.typeList.checkedItems.map(v=>v.id),
 					   permission:1
 				  }
-                  this.dispatch(`addDuchazichaSituation`, data).then((data) => {
+				  // 去完善编辑:统一走 updateSituationData(pfmFunctionSituation)
+				  if (this.fromImprove) {
+				  	const payload = this.buildUpdatePayload({ pfmData: data });
+				  	this.dispatch('updateSituationData', payload).then((ok) => {
+				  		if (ok) {
+				  			this.clearData();
+				  			uni.navigateTo({ url: '/pages/situationsCenter/situationsCenter' });
+				  		}
+				  	});
+				  	return;
+				  }
+				  this.dispatch(`addDuchazichaSituation`, data).then((data) => {
                   	if (data) {
                   		// 保存成功先清空数据
                   		this.clearData();
@@ -474,25 +536,36 @@
 					  name:this.theme.title,
 					  topic: this.theme.id,
 					  id:this.situationId,
-					  num: this.checkPlan.checkList.length,
-					  checkPlanEndDate:`${this.checkPlan.dateObj.end} 23:59`,
-					  checkPlanStartDate:`${this.checkPlan.dateObj.start} 00:00`,
-					  day:this.checkPlan.dateObj.dayNum,
+					  num: this.checkPlan.checkList.length || 1,
+					  checkPlanEndDate:`${(this.checkPlan.dateObj.end || this.checkPlan.dateObj.start)}${(this.checkPlan.dateObj.end || this.checkPlan.dateObj.start) ? ' 23:59' : ''}`,
+					  checkPlanStartDate:`${this.checkPlan.dateObj.start}${this.checkPlan.dateObj.start ? ' 00:00' : ''}`,
+					  day:this.checkPlan.dateObj.dayNum || 1,
 					  employeeList:this.checkPerson.checkedItems,
 					  filterCondition:this.condition.checkedItems,
-					  frequency:this.checkPlan.checkedItem.value,
-					  planList: [...this.checkPlan.checkList].map((date, i) => {
+					  frequency:this.checkPlan.checkedItem && this.checkPlan.checkedItem.value ? this.checkPlan.checkedItem.value : 2,
+					  planList: (this.checkPlan.checkList.length > 0 ? [...this.checkPlan.checkList] : [this.checkPlan.dateObj.start]).map((date, i) => {
 					  	return {
 					  		startDate:`${date} 00:00`,
 					  		endDate: `${dateHandle.getNewData(date, this.checkPlan.checkedItem.model - 1)} 23:59`
 					  	}
 					  }),
-					  filterDepartments:this.checkMap.list.map(item=>item.departmentId),
-					  scoreType:this.situationPreview.pointsetType,   //add by yfb 20230417 
-					  customScore:this.situationPreview.preTotal,
+					  filterDepartments:this.checkMap.list.map(item=>item.departmentId),
+					  scoreType:this.situationPreview.pointsetType,   //add by yfb 20230417 
+					  customScore:this.situationPreview.preTotal,
 					  checkOrder:this.situationPreview.checkOrderSetType
 					  
-				}
+				}
+				// 自查督查从“去完善”进入统一走 updateSituationData
+				if (this.fromImprove) {
+					const payload = this.buildUpdatePayload({ pfmData: param });
+					this.dispatch('updateSituationData', payload).then((ok) => {
+						if (ok) {
+							this.clearData();
+							uni.navigateTo({ url: '/pages/situationsCenter/situationsCenter' });
+						}
+					});
+					return;
+				}
 				this.dispatch(`saveZichaduchaPlan`, param).then((data) => {
 					if (data) {
 						// 保存成功先清空数据
@@ -503,7 +576,7 @@
 					}
 				});
 			},
-			save: function() {
+			save: async function() {
 				const {
 					sitName,
 					preDay,
@@ -513,9 +586,9 @@
 					situationType,
 					showNotApplicable,
 					showCountNum,
-					pageTemplateId,
-					pointsetType,
-					preTotal,
+					pageTemplateId,
+					pointsetType,
+					preTotal,
 					checkOrderSetType
 				} = this.situationPreview;
 				const {
@@ -529,56 +602,7 @@
 					conditionIds
 				} = this.condition;
 
-				if (!sitName || sitName.length < 2) {
-					uni.showModal({
-						title: '温馨提示',
-						content: `情境名称不能为空也不能少于2个字!`,
-						showCancel: false
-					});
-					return;
-				}
-				if (preDay > 31) {
-					uni.showModal({
-						title: '温馨提示',
-						content: `提醒天数不得大于31天!`,
-						showCancel: false
-					});
-					return;
-				}
-				if (preH > 24) {
-					uni.showModal({
-						title: '温馨提示',
-						content: `提醒天数不得大于24小时!`,
-						showCancel: false
-					});
-					return;
-				}
-				if (preDay != null && !/^\d+$/.test(preDay)) {
-					uni.showModal({
-						title: '温馨提示',
-						content: `提醒天数不能是小数!`,
-						showCancel: false
-					});
-					return;
-				}
-				if (preH != null && !/^\d+$/.test(preH)) {
-					uni.showModal({
-						title: '温馨提示',
-						content: `提醒小时不能是小数!`,
-						showCancel: false
-					});
-					return;
-				}
-				if(pointsetType==2){
-					if (preTotal<0) {
-						uni.showModal({
-							title: '温馨提示',
-							content: `设置总分部能为负数!`,
-							showCancel: false
-						});
-						return;
-					}
-				}
+				// 放宽校验:不再阻断保存
 				
 				let data = {
 					description,
@@ -595,31 +619,45 @@
 					situationType: situationType,
 					pageTemplateId: pageTemplateId,
 					filterCondition:optionsHandle(options, conditionIds),
+					// 最小必填兜底:若未选择查核组,默认选中列表首项
 					checkGroup: checkGroup(this.checkRent),
 					checkDep: checkDep(this.checkMap.list),
 					planConfig: {
-						frequency: checkedItem.value,
-						day: checkedItem.model,
-						startDate: dateObj.start,
-						endDate: dateObj.end,
-						num: checkList.length
+						frequency: (checkedItem && checkedItem.value) ? checkedItem.value : this.checkPlan.checkedItem.value,
+						day: (checkedItem && checkedItem.model) ? checkedItem.model : this.checkPlan.checkedItem.model,
+						startDate: dateObj.start || this.checkPlan.dateObj.start,
+						endDate: dateObj.end || (dateObj.start ? dateHandle.getNewData(dateObj.start, (checkedItem && checkedItem.model ? checkedItem.model : this.checkPlan.checkedItem.model) - 1) : ''),
+						num: (checkList && checkList.length) ? checkList.length : 1
 					},
-					planList: [...checkList].map((date, i) => {
+					// 最小必填兜底:若计划为空,给出最小 1 条基于 start 的计划
+					planList: (checkList.length > 0 ? [...checkList] : [dateObj.start || this.checkPlan.dateObj.start]).map((date, i) => {
 						return {
 							startDate: date,
 							endDate: dateHandle.getNewData(date, checkedItem.model - 1)
 						}
-					}),
-					scoreType:pointsetType,
-					customScore:parseInt(preTotal),
-					remingpreTotal:parseInt(preTotal),
-					remindpointsetType:pointsetType,
-					checkOrder:parseInt(checkOrderSetType)
+					}),
+					scoreType:pointsetType,
+					customScore:parseInt(preTotal),
+					remingpreTotal:parseInt(preTotal),
+					remindpointsetType:pointsetType,
+					checkOrder:parseInt(checkOrderSetType || this.situationPreview.checkOrderSetType)
 				};
+				// 编辑保存:从“去完善”进入时统一走 updateSituationData
+				if (this.saveType === 'PUT' && this.fromImprove) {
+					const payload = this.buildUpdatePayload({ normalData: data });
+					this.dispatch('updateSituationData', payload).then((ok) => {
+						if (ok) {
+							this.clearData();
+							uni.navigateTo({ url: '/pages/situationsCenter/situationsCenter' });
+						}
+					});
+					return;
+				}
+				
 				if (this.saveType === 'PUT') {
 					data.id = this.editID;
 				}
-				
+				
 				this.dispatch(`save${this.saveType}`, data).then((data) => {
 					if (data) {
 						// 保存成功先清空数据
@@ -630,7 +668,18 @@
 					}
 				});
 			},
-			changeStep: function(id) {
+			// 统一构造“去完善”编辑保存 payload
+			buildUpdatePayload({ normalData = null, pfmData = null }) {
+				const situationId = this.situationId || this.editID;
+				const situationType = this.systemSituationType != null ? this.systemSituationType : this.theme.id;
+				return {
+					situationId,
+					situationType,
+					situationReq: normalData || null,
+					pfmFunctionSituation: pfmData || null,
+				};
+			},
+			changeStep: async function(id) {
 				// console.log({id,'stepActive':this.stepActive,'this.options':this.options});
 				switch (id) {
 					case 'goback':
@@ -648,20 +697,11 @@
 						break;
 					case 'next': // 下一步
 						if (this.stepActive < this.options.length)
-							this.nextHandle(this.stepActive);
+							await this.nextHandle(this.stepActive);
 						break;
 					case 'checkPlanCreate': // 生成查核计划
-					    
-						if (this.checkPlan.checkList.length === 0 && this.checkPlan.dateObj.dayNum < 1) {
-							uni.showModal({
-								title: '错误提示',
-								content: '查核频次必须大于或等于1!',
-								showCancel: false
-							});
-						} else {
-							console.log('checkPlanCreate');
-							this.myCommit('showCheckPlan1', false);
-						}
+						// 放宽校验:不再阻断,直接进入生成视图
+						this.myCommit('showCheckPlan1', false);
 						break;
 					case 'checkPlanCallback': // 生成查核计划-返回
 						this.myCommit('showCheckPlan1', true);
@@ -684,129 +724,33 @@
 			/**
 			 * 处理【下一步】逻辑
 			 */
-			nextHandle: function(stepActive) {
-				console.log({stepActive,'themeType':this.themeType})
-				let flage = false;
-				if (this.themeType == 'NORMAL') {
-					//非督查+自查
-					switch (stepActive) {
-						case 1:
-						    if(this.theme.id == 0){
-								//创建个案情境
-								flage = this.conditionErrHandler(this.checkCondition(this.condition),1);			
-							}else{
-								flage = this.errorHandle(this.condition.conditionIds.length > 0, 1);
-							}
-							
-							break;
-						case 2:
-							const {
-								points
-							} = this.checkRent.checkedItem;
-							let condition = this.checkRent.checkedItem.id !== null && points;
-							flage = this.errorHandle(condition, 2);
-							break;
-						case 3:
-							flage = this.errorHandle(!this.dataIsNull, 3);
-							break;
-						case 4:
-							const {
-								checkList
-							} = this.checkPlan;
-							flage = this.errorHandle(checkList.length > 0, 4);
-							break;
-						case 5:
-							this.dispatch('getSituationTypes').then(res => {
-								console.log({
-									res
-								});
-							});
-							this.dispatch('getDictionary').then(res => {
-								console.log({
-									res
-								});
-							});
-							break;
-						default:
-							flage = true;
-							break;
-					}
-
-				}
-				
-				
-				
-				if (this.themeType == 'MULTI') {
-					//管理员创建 督查+自查
-					switch (stepActive) {
-						case 1:
-							flage = true;
-							break;
-						case 2:
-							flage = true;
-							break;
-						case 3:
-							flage = true;
-							break;
-						default:
-							flage = true;
-							break;
-					}
-				}
-				
-				if(this.isPlanSet){
-					  //职能科室负责人  督查+自查 计划设置
-					  switch (stepActive) {
-					  	case 1:
-					  		flage = true;
-					  		break;
-					  	case 2:
-					  		flage = true;
-					  		break;
-					  	case 3:
-					  		flage = true;
-					  		break;
-					  	default:
-					  		flage = true;
-					  		break;
-					  }
-				}
-
-				if (flage) {
-					this.myCommit('needReload', true);
-					this.myCommit('stepActive', stepActive + 1);
-				}
+			nextHandle: async function(stepActive) {
+				// 放宽校验:所有步骤直接进入下一步
+				this.myCommit('needReload', true);
+				this.myCommit('stepActive', stepActive + 1);
 			},
 			/**
 			 * 错误处理,满足条件返回true
 			 * @param {Object} condition 条件
 			 * @param {Object} index 当前下标
 			 */
-			errorHandle: function(conditionflag, index) {
+			errorHandle: async function(conditionflag, index) {
 
 				if (conditionflag) {
 					return true;
 				} else {
-					uni.showModal({
-						title: '温馨提示',
-						content: index === 3 ? '查核地图不能为空' : `请先选择${this.options[index].hint}!`,
-						showCancel: false
-					});
+					await this.$refs.AppConfirm.open({ title: '温馨提示', content: index === 3 ? '查核地图不能为空' : `请先选择${this.options[index].hint}!`, showCancel: false });
 					return false;
 				}
-			},
-			conditionErrHandler:function (flag,index){
-				const results=this.checkCondition(this.condition);
-				//add by yfb 20230214 检查子选项选择情况
-				if (!results.funFlag){
-					uni.showModal({
-						title: '温馨提示',
-						content: `请先选择${this.options[1].hint},`+results.errMsg,
-						showCancel: false
-					});
-					return false;
-				}else
-					return true;
+			},
+			conditionErrHandler: async function (flag,index){
+				const results=this.checkCondition(this.condition);
+				//add by yfb 20230214 检查子选项选择情况
+				if (!results.funFlag){
+					await this.$refs.AppConfirm.open({ title: '温馨提示', content: `请先选择${this.options[1].hint},`+results.errMsg, showCancel: false });
+					return false;
+				}else
+					return true;
 			},
 			myCommit: function(key, data) {
 				this.$store.commit({
@@ -839,79 +783,79 @@
 			},
 			checkCondition:function(condition,msg){
 				//console.log('测试',condition);
-
-				let MasterTemp=condition.options.filter(item=>{
-					if (item.required==1){
-						let ichecked=false;
-						//遍历所有子选项,关联主卡片
-						let childCon=condition.childContainer.filter(cc=>{
-							// console.log('childCondition',cc);
-							// console.log('childid',cc.list[0].id);
-							// console.log('item',item.id);
-							//判断主卡片对应子项列表
-							if (item.id==cc.list[0].parentId){
-								let tmp=cc.list[0].child.filter(mi=>{
-									return condition.conditionIds.includes(mi.id);//记录的选项中是否是子项中
-								});
-								//判断子项是否在已选列表内,即已选项中存在当前主卡片选项
-								if (tmp.length>0)
-								{
-									ichecked=true;
-									return true;
-								}   
-							}
-						});
-						//console.log('childCon',childCon);
-						if (childCon.length<=0)
-						return true;
-					}
+
+				let MasterTemp=condition.options.filter(item=>{
+					if (item.required==1){
+						let ichecked=false;
+						//遍历所有子选项,关联主卡片
+						let childCon=condition.childContainer.filter(cc=>{
+							// console.log('childCondition',cc);
+							// console.log('childid',cc.list[0].id);
+							// console.log('item',item.id);
+							//判断主卡片对应子项列表
+							if (item.id==cc.list[0].parentId){
+								let tmp=cc.list[0].child.filter(mi=>{
+									return condition.conditionIds.includes(mi.id);//记录的选项中是否是子项中
+								});
+								//判断子项是否在已选列表内,即已选项中存在当前主卡片选项
+								if (tmp.length>0)
+								{
+									ichecked=true;
+									return true;
+								}   
+							}
+						});
+						//console.log('childCon',childCon);
+						if (childCon.length<=0)
+						return true;
+					}
 				});
-				//判断主卡片
-				if (MasterTemp.length>0){
-					//console.log('123');
-					// this.errMsg=MasterTemp[0].name+'未选择!';
-					// return false;
-					return {
-						errMsg:MasterTemp[0].name+'未选择!',
-						funFlag:false,
-					}
-				}
-				
-				
-				//console.log('test_checkConditionCard',this.condition.CheckConditionCard);
-				//判断子选项,根据存储的卡片信息进行判断,正常情况不过滤,只返回第一个搜索出的未选择项卡片
-				const tmp=this.condition.CheckConditionCard.filter(cards=>{
-					//必填项检查
-					if (cards.required==1){
-						// console.log('cards',cards);
-						//查看该项下选项是否有选项
-						const childlist= cards.child.filter(a=>{
-							// console.log('child',a);
-							return this.condition.conditionIds.includes(a.id);
-						});
-						// console.log('childlist',childlist);
-						//返回未选择选项
-						return childlist.length<=0;
-					}
-				});
-				//console.log('tmp',tmp);
-				//因为返回未选择项,则未选项列表有数据则返回错误
-				if (tmp.length>0)
-				{
-					// this.errMsg=tmp[0].name+'未选择!';
-					// return false;
-					return {
-						errMsg:'['+tmp[0].name+']未选择!',
-						funFlag:false,
-					}
-				}
-				else {
-					// return true;
-					return {
-						errMsg:'',
-						funFlag:true,
-					}
-				}
+				//判断主卡片
+				if (MasterTemp.length>0){
+					//console.log('123');
+					// this.errMsg=MasterTemp[0].name+'未选择!';
+					// return false;
+					return {
+						errMsg:MasterTemp[0].name+'未选择!',
+						funFlag:false,
+					}
+				}
+				
+				
+				//console.log('test_checkConditionCard',this.condition.CheckConditionCard);
+				//判断子选项,根据存储的卡片信息进行判断,正常情况不过滤,只返回第一个搜索出的未选择项卡片
+				const tmp=this.condition.CheckConditionCard.filter(cards=>{
+					//必填项检查
+					if (cards.required==1){
+						// console.log('cards',cards);
+						//查看该项下选项是否有选项
+						const childlist= cards.child.filter(a=>{
+							// console.log('child',a);
+							return this.condition.conditionIds.includes(a.id);
+						});
+						// console.log('childlist',childlist);
+						//返回未选择选项
+						return childlist.length<=0;
+					}
+				});
+				//console.log('tmp',tmp);
+				//因为返回未选择项,则未选项列表有数据则返回错误
+				if (tmp.length>0)
+				{
+					// this.errMsg=tmp[0].name+'未选择!';
+					// return false;
+					return {
+						errMsg:'['+tmp[0].name+']未选择!',
+						funFlag:false,
+					}
+				}
+				else {
+					// return true;
+					return {
+						errMsg:'',
+						funFlag:true,
+					}
+				}
 
 				//return true;
 			}
@@ -932,12 +876,13 @@
 			person,
 			planPreview,
 			taskPreview,
-			conditionCard
+			conditionCard,
+			AppConfirm
 		}
 	}
 </script>
 
-<style lang="less">
+<style lang="less" scoped>
 	.creatingSituations {
 		width: 100%;
 		height: 100%;

+ 11 - 0
pages/creatingSituations/server.js

@@ -45,6 +45,11 @@ const requestList = {
     method: 'GET',
     url: 'situation/config',
   },
+  // 从情境中心“去完善”入口使用的新配置接口(出参与创建保存入参一致)
+  getFunctionSituationConfig: {
+    method: 'GET',
+    url: 'situation/getFunctionSituationConfig',
+  },
   // 获取情境类型列表
   getSituationTypes: {
     method: 'GET',
@@ -90,6 +95,12 @@ const requestList = {
 	  method: 'POST',
 	  url: 'inspector/savePlan',  
   },
+  // 去完善编辑统一保存接口(所有情境类型)
+  updateSituationData: {
+    method: 'POST',
+    url: 'situation/updateSituationData',
+    successMessage: '编辑成功!'
+  },
   //获取字典数据
   getDictionary:{
 	  method:'GET',

+ 28 - 20
pages/home/home.vue

@@ -67,10 +67,10 @@
 		<view class="logout-box" @click="logOut">
 			<text class="logout-text">退出登录</text>
 		</view>
-		<view class="copyright" @click="showVersionInfo">
+		<!-- <view class="copyright" @click="showVersionInfo">
 			<text>康程智医(成都)信息科技有限公司</text>
 			<text>{{version}}</text>
-		</view>
+		</view> -->
 		<tm-tabbar :permission="nowPermission" />
 		<tm-modal v-if="showJournal">
 			<view class="journal">
@@ -398,11 +398,11 @@
 		}
 	}
 
-	.home-page {
-		height: 100%;
-
+	.home-page {
+		 overflow-y: scroll;
+		 padding-bottom: 100rpx;
 		.content-info {
-			height: 700.25rpx;
+			min-height: 900.25rpx;
 			background-color: #FFFFFF;
 
 			.top-box {
@@ -425,20 +425,24 @@
 				}
 			}
 
-			.info-box {
+			.info-box {
+				position: relative;
+				z-index: 99;
 				width: 700rpx;
-				height: 250rpx;
+				height: 250rpx;
+				margin: 0 auto;
+				margin-top: -130rpx;
 				border-radius: 15rpx;
 				background-color: #FFFFFF;
-				position: absolute;
-				left: 25rpx;
-				right: 25rpx;
-				top: 226.25rpx;
+				// position: absolute;
+				// left: 25rpx;
+				// right: 25rpx;
+				// top: 226.25rpx;
 				box-shadow: 0px 10px 30px 0px rgba(0, 13, 51, 0.1);
 
 				.head {
 					margin-left: 37.5rpx;
-					margin-top: 68.75rpx;
+					padding-top: 68.75rpx;
 
 					.name {
 						font-size: 35rpx;
@@ -494,7 +498,7 @@
 				right: 75rpx;
 				top: 187.5rpx;
 				background: #FFFFFF;
-				z-index: 2;
+				z-index: 100;
 				border-radius: 50%
 			}
 
@@ -507,8 +511,8 @@
 			}
 
 			.function-box {
-				position: absolute;
-				top: 496.25rpx;
+				// position: absolute;
+				// top: 496.25rpx;
 				width: 750rpx;
                 background-color: #FFFFFF;
 				.role-switch {
@@ -604,7 +608,7 @@
 			width: 750rpx;
 			height: 100rpx;
 			background-color: #FFFFFF;
-			margin-top:210rpx;
+			margin-top:50rpx;
 
 			.logout-text {
 				font-size: 22.5rpx;
@@ -617,9 +621,13 @@
 		}
 
 		.copyright {
-			position: absolute;
-			bottom: 90rpx;
-			left: 0;
+			// position: absolute;
+			// bottom: 90rpx;
+			// left: 0;
+			position: relative;
+			z-index: 999;
+			margin-top: 100rpx;
+			
 			display: flex;
 			flex-direction: column;
 			justify-content: center;

+ 219 - 130
pages/login/login.vue

@@ -1,110 +1,172 @@
 <template>
 	<view class="login-page">
-		<image class="bgImage" src="/static/images/shade.png"></image>
-		<text class="titleOne row" @click="openModal">你好,</text>
-		<view class="titleTwo row">欢迎使用追踪方法学系统</view>
-		<view class="inputArea">
-	       <input class="inputTag" type="text" placeholder="请输入账号" placeholder-class="inputTagHolderPlace" v-model="username"  />
-		</view>
-		<view class="inputArea pwd">
-		   <input class="inputTag" password="true" type="text" placeholder="请输入密码" placeholder-class="inputTagHolderPlace"  v-model="password" />
-		</view>
-		
-		<button :class="[isInput?'loginBtn on':'loginBtn']" type="default" @click="login" >登录</button>
-		
-		<tm-modal v-if="showInputModal" @click="clickModalhandle">
-			<view class="checkItemResultModal">
-				 <view class="modalContent">
-				 	  <view class="modalBar">{{`开发者模式(v${version})`}}</view>
-					  <input class="keyInutArea" type="text" v-model="appHospSign" @input='setHospSign' placeholder="请输入hospSign" />
-					  <image @click="openScanner" class="scanBtn" src="../../static/scancodeIcon.png" mode=""></image>
-					  <view>{{deviceInfo}}</view>
-					  <button class="commitActBtn" @click="updateHospSign(true)" type="default">确定</button>
-		<!-- 			  <button class="commitActBtn" @click="updateHospSign(false)" type="default">确定</button> -->
-				 </view>
+		<image class="bgImage" src="/static/images/shade.png" mode="widthFix"></image>
+		<view class="form-area" :style="{ paddingBottom: (keyboardHeight > 0 ? (keyboardPadding + 'px') : '100rpx') }">
+			<view class="inputArea">
+				<image class="inputIcon" src="/static/images/login_account_icon.png" mode="aspectFit"></image>
+				<input class="inputTag" type="text" placeholder="请输入账号" placeholder-class="inputTagHolderPlace"
+					v-model="username" />
+			</view>
+			<view class="inputArea pwd">
+				<image class="inputIcon" src="/static/images/login_pwd_icon.png" mode="aspectFit"></image>
+				<input class="inputTag" password="true" type="text" placeholder="请输入密码"
+					placeholder-class="inputTagHolderPlace" v-model="password" />
 			</view>
-		</tm-modal>
+
+			<button id="loginBtn" :class="[isInput?'loginBtn on':'loginBtn']" type="default" @click="login">登录</button>
+
+			<tm-modal v-if="showInputModal" @click="clickModalhandle">
+				<view class="checkItemResultModal">
+					<view class="modalContent">
+						<view class="modalBar">{{`开发者模式(v${version})`}}</view>
+						<input class="keyInutArea" type="text" v-model="appHospSign" @input='setHospSign'
+							placeholder="请输入hospSign" />
+						<image v-if="enableAppScan" @click="openScanner" class="scanBtn" src="../../static/scancodeIcon.png" mode=""></image>
+						<view>{{deviceInfo}}</view>
+						<button class="commitActBtn" @click="updateHospSign(true)" type="default">确定</button>
+						<!-- 			  <button class="commitActBtn" @click="updateHospSign(false)" type="default">确定</button> -->
+					</view>
+				</view>
+			</tm-modal>
+		</view>
 	</view>
 </template>
 
 <script>
 	import encryption from "../../utils/crypto.js";
-	import {loginAfterHandle} from '../../utils/loginHandle.js';
+	import {
+		mapState
+	} from "vuex";
+	import {
+		loginAfterHandle
+	} from '../../utils/loginHandle.js';
 	export default {
 		data() {
 			return {
-				index:0,
-				appHospSign:'gKMa7SLw9hRglxm5',//app端更新hospSign
-				showInputModal:false,
+				index: 0,
+				appHospSign: 'gKMa7SLw9hRglxm5', //app端更新hospSign
+				showInputModal: false,
 				hospSign: '', // 医院标识
 				username: '', // 用户名
 				password: '', // 密码
-				version:'',
-				deviceInfo:{}
+				version: '',
+				deviceInfo: {},
+				enableAppScan: false,
+				keyboardHeight: 0, // 软键盘高度(px)
+				keyboardPadding: 0 // 计算后的底部最小必要留白(px)
 			}
 		},
-		computed:{
-			isInput:function(){
-				return (this.username.length>0&&this.password.length>0)?true:false
+		computed: {
+			isInput: function() {
+				return (this.username.length > 0 && this.password.length > 0) ? true : false
+			},
+			isPad:function(){
+				return getApp().globalData.isPad
 			},
 		},
-		onLoad({ hospSign }){
-			  
-			  this.hospSign = hospSign;
-			  // #ifdef APP-PLUS
-			     const hospSignFromLocal = uni.getStorageSync('hospSign');
-				 if(hospSignFromLocal != ''){
-					 this.appHospSign = hospSignFromLocal;
-				 }
-				 this.deviceInfo = uni.getSystemInfoSync();
-				 //获取版本号
-				 plus.runtime.getProperty(plus.runtime.appid,(inf)=>{
-				 	this.version = inf.version;
-				 });
-				 
-			  // #endif
+		onLoad({
+			hospSign
+		}) {
+			this.hospSign = hospSign;
+			// #ifdef APP-PLUS
+			const hospSignFromLocal = uni.getStorageSync('hospSign');
+			if (hospSignFromLocal != '') {
+				this.appHospSign = hospSignFromLocal;
+			}
+			this.deviceInfo = uni.getSystemInfoSync();
+			//获取版本号
+			plus.runtime.getProperty(plus.runtime.appid, (inf) => {
+				this.version = inf.version;
+			});
+
+			// #endif
 
-			  
-			  
+		},
+		onReady() {
+			// APP端设置键盘模式为adjustPan
+			// #ifdef APP-PLUS
+			try {
+				plus.webview.currentWebview().setStyle({
+					softinputMode: 'adjustPan'
+				});
+			} catch (e) {}
+			// #endif
+			// 监听键盘高度变化,动态计算最小必要留白
+			if (typeof uni.onKeyboardHeightChange === 'function') {
+				uni.onKeyboardHeightChange((res) => {
+					this.keyboardHeight = res && res.height ? res.height : 0;
+					if (this.keyboardHeight <= 0) {
+						this.keyboardPadding = 0;
+						return;
+					}
+					this.$nextTick(() => {
+						// 获取登录按钮的位置,计算与键盘顶部的距离
+						const query = uni.createSelectorQuery();
+						query.select('#loginBtn').boundingClientRect();
+						query.exec((resRects) => {
+							if (!resRects || !resRects[0]) {
+								this.keyboardPadding = this.keyboardHeight;
+								return;
+							}
+							const btnRect = resRects[0];
+							const windowInfo = uni.getSystemInfoSync();
+							const windowHeight = windowInfo.windowHeight; // px
+							const keyboardTop = windowHeight - this.keyboardHeight; // 键盘顶部y
+							const btnBottom = btnRect.bottom; // 按钮底部y
+							const gap = keyboardTop - btnBottom; // 需要留白的空间
+							const safeInset = (windowInfo.safeAreaInsets && windowInfo.safeAreaInsets.bottom) ? windowInfo.safeAreaInsets.bottom : 0;
+							// 仅当gap<安全间距时,补齐差值与安全区,避免过大留白
+							this.keyboardPadding = gap < 8 ? (8 + safeInset) : (safeInset);
+							// 保底不为负
+							if (this.keyboardPadding < 0) this.keyboardPadding = 0;
+							// 边缘场景:若输入框获得焦点且更靠下,允许适度滚动
+							if (gap < 0) {
+								uni.pageScrollTo({ scrollTop: 999999, duration: 0 });
+							}
+						});
+					});
+				});
+			}
 		},
 		methods: {
-			openScanner(){
+			openScanner() {
 				// 只允许通过相机扫码
 				uni.scanCode({
-				    onlyFromCamera: true,
-				    success:(res)=>{
-				        console.log('条码类型:' + res.scanType);
-				        console.log('条码内容:' + res.result);
+					onlyFromCamera: true,
+					success: (res) => {
+						console.log('条码类型:' + res.scanType);
+						console.log('条码内容:' + res.result);
 						this.appHospSign = res.result;
-				    }
+					}
 				});
 			},
-			setHospSign(e){
+			setHospSign(e) {
 				this.appHospSign = e.target.value
 			},
-			openModal(){
+			openModal() {
 				// #ifdef APP-PLUS
 				this.index = this.index + 1;
-				if(this.index==3)this.showInputModal = true;
+				if (this.index == 3) this.showInputModal = true;
 				// #endif
 			},
-			updateHospSign(bool){
-				if(bool){
-					 this.hospSign = this.appHospSign;
+			updateHospSign(bool) {
+				if (bool) {
+					this.hospSign = this.appHospSign;
 				}
-				this.index=0;
+				this.index = 0;
 				this.showInputModal = false;
 			},
 			login() {
-				if(this.isLogin) return;
+				if (this.isLogin) return;
 				const nowPermission = uni.getStorageSync('nowPermission');
+				let hospSign = '';
 				// #ifdef APP-PLUS
-				   const hospSign  = this.appHospSign;
+				hospSign = uni.getStorageSync('hospSign') || this.appHospSign;
 				// #endif
 				// #ifdef H5
-				  const hospSign = this.hospSign;
+				hospSign = this.hospSign;
 				// #endif
-	             
+
 				this.$store.dispatch({
 					type: 'login/commActions',
 					payload: {
@@ -112,13 +174,13 @@
 						data: {
 							username: encryption(this.username),
 							password: encryption(this.password),
-							hospSign:hospSign,
-							nowPermission:nowPermission
+							hospSign: hospSign,
+							nowPermission: nowPermission
 						}
 					}
 				}).then((data) => {
 					if (data) {
-						 loginAfterHandle(data,hospSign);
+						loginAfterHandle(data, hospSign);
 					}
 				});
 			},
@@ -130,18 +192,21 @@
 	.login-page {
 		display: flex;
 		flex-direction: column;
-		justify-content: center;
-		height:100vh;
-		padding: 0 62.5rpx;
+		justify-content: flex-start;
+		min-height: 100vh; // 使用最小高度避免键盘弹出时整体被压缩
+		height: auto; // 允许内容在需要时溢出并滚动
+
 		padding-bottom: 100rpx;
 		background-color: #FFFFFF;
+
 		.bgImage {
-			position: absolute;
-			right: 0;
-			top:0;
-			width: 430rpx;
-			height: 281.25rpx;
+			width: 100%;
+			display: block;
+			flex-shrink: 0; // 禁止在弹出键盘时被压缩
+			// 不设置height,自动按比例撑开
+			// 去掉position、top、right等定位属性
 		}
+
 		.row {
 			position: relative;
 			z-index: 100;
@@ -151,16 +216,33 @@
 			color: #2E2F33;
 			line-height: 67.5rpx;
 		}
+
 		.titleTwo {
 			margin-bottom: 75rpx;
 		}
+
+		.form-area {
+			padding: 0 62.5rpx;
+		}
+
 		.inputArea {
+			display: flex;
+			align-items: center;
 			height: 80rpx;
 			margin-bottom: 37.5rpx;
 			border-bottom: 1.25rpx solid #E6EAF2;
+
 			input {
 				-webkit-box-shadow: 0 0 0 1000px white inset;
 			}
+
+			.inputIcon {
+				width: 36rpx;
+				height: 36rpx;
+				margin-right: 20rpx;
+				flex-shrink: 0;
+			}
+
 			.inputTag {
 				height: 100%;
 				width: 100%;
@@ -168,11 +250,12 @@
 				font-family: SourceHanSansCN-Medium, SourceHanSansCN;
 				font-weight: 500;
 				color: #292C33;
-				
+
 				// &.-internal-autofill-selected {
 				// 	background-color:#FFFFFF;
 				// }
 			}
+
 			// /deep/.inputTag .uni-input-input {
 			// 	font-size: 30rpx;
 			// 	font-family: SourceHanSansCN-Medium, SourceHanSansCN;
@@ -186,6 +269,7 @@
 				font-weight: 400;
 				color: #B8BECC;
 			}
+
 			// /deep/.inputTag .uni-input-placeholder {
 			// 	font-size: 30rpx;
 			// }
@@ -193,6 +277,7 @@
 				margin-bottom: 62.5rpx;
 			}
 		}
+
 		.loginBtn {
 			display: block;
 			width: 100%;
@@ -203,62 +288,66 @@
 			font-family: SourceHanSansCN-Normal, SourceHanSansCN;
 			font-weight: 400;
 			color: #FFFFFF;
-			background: #A3B1CC;
 			border-radius: 37.5rpx;
-			border:none;
-			background-color: #A3B1CC;
+			border: none;
+			background: #A3B1CC; // 默认灰色
+			// background-color: #A3B1CC; // 建议去掉,避免和渐变冲突
 			&.on {
-				background: linear-gradient(90deg, #3377FF 0%, #4D97FF 100%);
+				background: linear-gradient(90deg, #3377FF 0%, #4D97FF 100%) !important; // 渐变,优先级高
 			}
 		}
-		
+
 		.checkItemResultModal {
-			  display: flex;
-			  height: 100%;
-			  justify-content: center;
-			  align-items: center;
-			  overflow: hidden;
-			  .modalContent {
-				   position: relative;
-				   width: 90%;
-				   height:500rpx;
-				   border-radius: 20rpx;
-				   overflow: hidden;
-				   background-color: #FFFFFF;
-				   .modalBar{
-					   display: flex;
-					   justify-content: center;
-					   align-items: center;
-					   height: 80rpx;
-					   color: #4E78FF;
-					   font-size: 22.5rpx;
-					   border-bottom: 0.1rpx solid #E5E5E5;
-				   }
-				   .keyInutArea {
-					   height: 80rpx;
-					   color: #4E78FF;
-					   font-size: 22.5rpx;
-					   padding: 0 20rpx;
-					   border: 0.1rpx solid #E5E5E5;
-				   }
-				   .scanBtn {
-					   position: absolute;
-					   display: block;
-					   width:25rpx;
-					   height: 25rpx;
-					   top:105rpx;
-					   right:30rpx;
-					   z-index: 10;
-				   }
-				   .commitActBtn {
-					   position: absolute;
-					   width: 100%;
-					   bottom: 0;
-					   border-radius: 0;
-				   }
-			  }
-		}
+			display: flex;
+			height: 100%;
+			justify-content: center;
+			align-items: center;
+			overflow: hidden;
 
-  }
+			.modalContent {
+				position: relative;
+				width: 90%;
+				height: 500rpx;
+				border-radius: 20rpx;
+				overflow: hidden;
+				background-color: #FFFFFF;
 
-</style>
+				.modalBar {
+					display: flex;
+					justify-content: center;
+					align-items: center;
+					height: 80rpx;
+					color: #4E78FF;
+					font-size: 22.5rpx;
+					border-bottom: 0.1rpx solid #E5E5E5;
+				}
+
+				.keyInutArea {
+					height: 80rpx;
+					color: #4E78FF;
+					font-size: 22.5rpx;
+					padding: 0 20rpx;
+					border: 0.1rpx solid #E5E5E5;
+				}
+
+				.scanBtn {
+					position: absolute;
+					display: block;
+					width: 25rpx;
+					height: 25rpx;
+					top: 105rpx;
+					right: 30rpx;
+					z-index: 10;
+				}
+
+				.commitActBtn {
+					position: absolute;
+					width: 100%;
+					bottom: 0;
+					border-radius: 0;
+				}
+			}
+		}
+
+	}
+</style>

+ 2 - 1
pages/login/model.js

@@ -3,12 +3,13 @@ import { commServer } from './server.js';
 export default {
   namespaced: true,
   state: {
+
   },
   mutations: {},
   actions: {
 		commActions({ commit, state }, { payload }) {
 			// payload = {key,data} // data是请求数据,key是请求接口id
-      return commServer(payload);
+            return commServer(payload);
 		},
   }
 }

+ 98 - 88
pages/mainPointsDetail/mainPointsDetail.vue

@@ -216,8 +216,8 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 				deductPoint: 0, //本次扣分
 				currentCheckedTab: null, //当前选中的查核tab
 				moreDeduction: false, //缺陷项是否多选
-				ifInputScore: false, //是否手动干预分值
-				iOrder:1, //查核项明细中,下一步顺序是否按要点结束,1:按要点结束;2:按顺序结束
+				ifInputScore: false, //是否手动干预分值
+				iOrder:1, //查核项明细中,下一步顺序是否按要点结束,1:按要点结束;2:按顺序结束
 				situationType:0,
 			};
 		},
@@ -331,7 +331,7 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 			toSelectResponsible() {
 
 				uni.navigateTo({
-					url: `/pages/responsibleList/responsibleList?deptId=${this.isZichaDucha?this.departmentId:this.deptId}&checkId=${this.id}&isZichaDucha=${this.isZichaDucha}`,
+					url: `/pages/responsibleList/responsibleList?deptId=${this.deptId}&checkId=${this.id}`,
 				});
 			},
 			selectResultHandle(item) {
@@ -450,15 +450,15 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 					this.id = id;
 					this.deptId = res.deptId;
 					//再次编辑回显
-
-					this.Index = this.isZichaDucha?res.checkResultName:res.checkResult;
-					
-					this.totalScore = this.isZichaDucha ? res.itemTotalScore : res.totalScore;
-					this.currentScore = this.isZichaDucha ? (res.checkResult ? res.checkScore : res
-						.itemTotalScore) : (res.checkResult ? res.score : res.totalScore);
+					const filePath = res.checkResultTxt ? JSON.parse(res.checkResultTxt) : [];
+					this.filePath = filePath[0] && filePath[0].checkResultUrl != '' ? filePath[0].checkResultUrl
+						.split(',') : [];
+					this.Index = res.checkResult;
+					this.totalScore = res.totalScore;
+					this.currentScore = res.checkResult?res.score:res.totalScore;
 					this.deductPoint = res.deductPoint;
 					this.moreDeduction = res.moreDeduction;
-					//this.iOrder=res.checkOrder;
+					//this.iOrder=res.checkOrder;
 
 					if (res.checkResultRequestList && res.checkResultRequestList.length > 0) {
 						if (res.checkModelName == '访谈') {
@@ -556,8 +556,8 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 							...res,
 							lastResult: res.lastResultName,
 							checkResult: res.checkResultName
-						} : res;
-						
+						} : res;
+						
 						console.log('this.data',this.data);
 
 						if (res && res.lastResult == "不适用") {
@@ -577,16 +577,29 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 					}
 				});
 			},
-			getPeizhiList() {
-				this.$store.dispatch({
-					type: "configure/commActions",
-					payload: {
-						key: "getResultConfig",
-					},
-				}).then((res) => {
-					this.resultConfigList = res ? res : [];
-				});
-			},
+			getPeizhiList() {
+				this.$store.dispatch({
+					type: "configure/commActions",
+					payload: {
+						key: "getResultConfig",
+					},
+				}).then((res) => {
+					if (Array.isArray(res)) {
+						// 接口可能返回同一 code 的多条,这里仅保留每个 code 的第一条(通常是默认配置)
+						const seen = new Set();
+						const unique = [];
+						res.forEach((item) => {
+							if (!seen.has(item.code)) {
+								seen.add(item.code);
+								unique.push(item);
+							}
+						});
+						this.resultConfigList = unique;
+					} else {
+						this.resultConfigList = [];
+					}
+				});
+			},
 			//查核点击回调
 			checkedOne(data) {
 
@@ -757,44 +770,41 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 										},
 									},
 								}).then((res) => {
-									if (res) {
+									if (res) {
 										
 										resolve(true);
 
 										// const resultIndex = this.resultConfigList.findIndex(item => item.name == this.data.checkResult);
 
-										// console.log({resultIndex,'this.resultConfigList':this.resultConfigList});
-										
-										for (let i = 0; i < checkConfiglist.length; i++) {
+										// console.log({resultIndex,'this.resultConfigList':this.resultConfigList});
+										
+										for (let i = 0; i < checkConfiglist.length; i++) {
+
+											// 兼容 code 或 name 两种返回,确保能拿到配置
+											const index = this.resultConfigList.findIndex(item => (item
+												.code == params.checkResult || item.name == params.checkResult));
+											let name = '';
+											if (index != -1) {
+												name = this.resultConfigList[index].name
+											}
+											const resultName = index != -1 ? this.resultConfigList[index].name : params.checkResult;
 
-											const index = this.resultConfigList.findIndex(item => item
-												.code == params.checkResult);
-											let name = '';
-											if (index != -1) {
-												name = this.resultConfigList[index].name
-											}
-
-											
-											// if (
-											// 	checkConfiglist[i].attr == (this.isZichaDucha ? name :
-											// 		params.checkResult) &&
-											// 	(checkConfiglist[i].resultType == 2 ||
-											// 		checkConfiglist[i].resultType == 3)
-											// )
+											
+											// if (
+											// 	checkConfiglist[i].attr == (this.isZichaDucha ? name :
+											// 		params.checkResult) &&
+											// 	(checkConfiglist[i].resultType == 2 ||
+											// 		checkConfiglist[i].resultType == 3)
+											// )
 											let configResultType=false;
-											//if (this.isZichaDucha){
-											if (this.resultConfigList[resultIndex].resultType==2||this.resultConfigList[resultIndex].resultType==3)
+											const currentResultType = index != -1 ? this.resultConfigList[index].resultType : undefined;
+											if (currentResultType==2||currentResultType==3)
 												configResultType=true;
-											// }else{
-											// 	if (checkConfiglist[i].resultType == 2 ||
-											// 		checkConfiglist[i].resultType == 3)
-											// 	configResultType=true;
-											// }
-											
-											if (
-												checkConfiglist[i].attr == (this.isZichaDucha ? this.resultConfigList[index].name:
-													params.checkResult ) &&
-												configResultType
+											
+											if (
+												checkConfiglist[i].attr == (this.isZichaDucha ? resultName :
+													params.checkResult ) &&
+												configResultType
 											) {
 												//当主要缺失和次要缺失时发起改善任务
 												console.log('开始提交改善前准备');
@@ -824,16 +834,16 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 													desicion: 0,
 													taskType: "1",
 													checkId: this.data.functionId,
-													checkResult: 0,
-													// newResultType: Number(resultIndex != -1 ? this
-													// 	.resultConfigList[resultIndex].code : 0
+													checkResult: 0,
+													// newResultType: Number(resultIndex != -1 ? this
+													// 	.resultConfigList[resultIndex].code : 0
 													// 	),
-													newResultType:checkConfiglist[i].resultType,
-													receiveEmpId: this.data.receiveEmpId,
-													receiveEmpName: this.data.receiveEmpName,
-													checkDetailId: this.data.id,
-													improveType: improveType, //0 普通 1督查 2自查 nowpermission 67 督查 48 自查
-
+													newResultType: currentResultType,
+													receiveEmpId: this.data.receiveEmpId,
+													receiveEmpName: this.data.receiveEmpName,
+													checkDetailId: this.data.id,
+													improveType: improveType, //0 普通 1督查 2自查 nowpermission 67 督查 48 自查
+
 												} : {
 													situationId: this.data.situationId,
 													situationName: this.data.situationName,
@@ -848,14 +858,14 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 													appointFlag: false,
 													desicion: 0,
 													taskType: "1",
-													checkId: this.data.checkId,
-													checkResult: 0,
-													newResultType: checkConfiglist[i].resultType,
-													// checkResult: this.resultConfigList[i].id,
-													receiveEmpId: this.data.receiveEmpId,
-													receiveEmpName: this.data.receiveEmpName,
-													checkDetailId: this.data.id,
-												};
+													checkId: this.data.checkId,
+													checkResult: 0,
+													newResultType: currentResultType,
+													// checkResult: this.resultConfigList[i].id,
+													receiveEmpId: this.data.receiveEmpId,
+													receiveEmpName: this.data.receiveEmpName,
+													checkDetailId: this.data.id,
+												};
 												
 												this.$store.dispatch({
 													type: "mission/commActions",
@@ -969,7 +979,7 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 				}
 				this.checkedSelectResultListIds = [];
 				this.checkedSelectResultList = [];
-				this.checkedSelectResultListData = [];
+				this.checkedSelectResultListData = [];
 				this.filePath=[];
 			},
 			goToPrevPage() {
@@ -1012,7 +1022,7 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 					}
 					let needItemIndex = num > 0 ? current[0].index + 1 : current[0].index - 1;
 					let needItemId = this.itemBelongGroup[needItemIndex].id;
-					let needItemCheckId = this.itemBelongGroup[needItemIndex].checkItemId;
+					let needItemCheckId = this.itemBelongGroup[needItemIndex].checkItemId;
 					
 					this.clearForm();
 					this.clearResult();
@@ -1052,8 +1062,8 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 			situationType,
 			isZichaDucha,
 			departmentId,
-			checkGroupName,
-			checkOrder,
+			checkGroupName,
+			checkOrder,
 			systemSituationType,
 		}) {
 			this.iOrder= checkOrder;
@@ -1065,14 +1075,14 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 
 			this.getCheckConfigList(checkItemId);
 			this.getCheckItemResultList(checkItemId);
-			this.isZichaDucha = isZichaDucha? JSON.parse(isZichaDucha) : false; //是否为自查督查
-			
+			this.isZichaDucha = isZichaDucha? JSON.parse(isZichaDucha) : false; //是否为自查督查
+			
 			this.systemSituationType=systemSituationType;
 			//console.log('2023062604 situationType',this.systemSituationType);
 			//接收来自上个页面所传过来的数据
-			const eventChannel = this.getOpenerEventChannel();
+			const eventChannel = this.getOpenerEventChannel();
 			
-			eventChannel.on('acceptDataFromOpenerPage', (data) => {
+			eventChannel.on('acceptDataFromOpenerPage', (data) => {
 				 if (this.iOrder==1){
 					this.itemBelongGroup = data.data[0].responseList.map((item, index) => {
 
@@ -1081,22 +1091,22 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 							id: item['id'],
 							checkItemId: item['checkItemId']
 						})
-					});
-				 }else if (this.iOrder==2){
-					 var Tindex=0;
-					 let itemList=[];
-					 data.data.filter(parentItem=>{
+					});
+				 }else if (this.iOrder==2){
+					 var Tindex=0;
+					 let itemList=[];
+					 data.data.filter(parentItem=>{
 						var tmp =parentItem.responseList.map((item) => {
 
 							return ({
 								index: Tindex++,
 								id: item['id'],
 								checkItemId: item['checkItemId']
-							})
-						});
-						itemList=itemList.concat(tmp);
-					 });
-					 this.itemBelongGroup=itemList;
+							})
+						});
+						itemList=itemList.concat(tmp);
+					 });
+					 this.itemBelongGroup=itemList;
 				 }
 				//重新导航进页面,删除缓存并设置最新数据
 				uni.removeStorageSync('itemBelongGroup');
@@ -1719,4 +1729,4 @@ import situationPreviewVue from '../creatingSituations/components/situationPrevi
 			}
 		}
 	}
-</style>
+</style>

+ 2 - 3
pages/mission/mission.vue

@@ -8,7 +8,7 @@
 			<text :style="checkIndex==2?{background:'#3377ff',color:'#fff'}:{background:'#F5F6FA',color:'#3377ff'}" @click="cheangeRadioIndex(2)">待处理</text></view>
 	 	<scroll-view class="scroll-y" scroll-y="true" >
 			<list-item
-				v-for="(item, i) in improvingTaskList"
+				v-for="item in improvingTaskList"
 				:key="item.id"
 				:task="item"
 			/>
@@ -26,7 +26,7 @@
 				</view>
 				<template v-if="showCloseList">
 					<list-item
-						v-for="(item, i) in completeTaskList"
+						v-for="item in completeTaskList"
 						:key="item.id"
 						:task="item"
 						:isComplete="true"
@@ -109,7 +109,6 @@
 		    websocket.close();
 		    clearInterval(this.refTimer);
 		},
-
 		methods: {
 			toggleBtn() {
 				let flag = !this.showCloseList;

+ 71 - 73
pages/planList/planList.vue

@@ -2,7 +2,7 @@
 	<view class="planList-page">
 		<scroll-view class="list-box" scroll-y="true" :style="{'height':listHeight+'rpx'}">
 			<uni-swipe-action>
-				<uni-swipe-action-item v-for="(item,index) in planList"  :key="index"
+				<uni-swipe-action-item v-for="(item,index) in planList"  :key="index"
 				    :disabled="!item.planFlag"
 					:threshold="20" :right-options="options" @click="delPlanHandle(item)" >
 					<view class="item-box" @click.stop="gotoCheckList(false,item)">
@@ -26,7 +26,7 @@
 		<view class="btn-distribution" @click="gotoCheckList(true, planList[0])" v-if="isShowDistribution">
 			<text class="btn-text">批量分配</text>
 		</view>
-		<modal v-if="showModal" v-on:callback="callback"></modal>
+		<modal v-if="showModal" v-on:callback="callback"></modal>
 		<tm-callback-listpage/>
 	</view>
 </template>
@@ -69,52 +69,50 @@
 					this.getPlanList();
 				}
 			},
-		},
-		onShow() {
-			this.getPlanList(); //刷新数据
+		},
+		onShow() {
+			this.getPlanList(); //刷新数据
 		},
 		onLoad({
 			situationId,
-			checkGroupId,
-			situationType,
+			checkGroupId,
+			situationType,
 			systemSituationType,
 		}) { // situationId:情景id checkGroupId: 查核组id
 			this.situationID = situationId;
-			this.checkGroupId = checkGroupId;
-			this.situationType = situationType;  
-			this.systemSituationType = systemSituationType;
+			this.checkGroupId = checkGroupId;
+			this.situationType = situationType;  
+			this.systemSituationType = systemSituationType;
 			this.getPlanList();
 		},
 	
-		methods: {
-			delPlanHandle(data){
-				 this.$store.dispatch({
-				 	type: 'planList/commActions',
-				 	payload: {
-				 		key: 'delOutofPlan',
-				 		data:{
-							situationId:data.id,
-						}
-				 	}
-				 }).then(()=>{
-					 this.getPlanList();
-				 })
+		methods: {
+			delPlanHandle(data){
+				 this.$store.dispatch({
+				 	type: 'planList/commActions',
+				 	payload: {
+				 		key: 'delOutofPlan',
+				 		data: data.id,
+				 	}
+				 }).then(()=>{
+					 this.getPlanList();
+				 })
 			},
 			callback(flage) {
 				this.showModal = false;
 				if (flage && this.planList.length > 0) {
-					this.gotoCheckList(true, this.planList[0]);
+					this.gotoCheckList(true, this.planList[0]);
 					
 				}
 			},
-			getPlanList() {
+			getPlanList() {
 			
 				this.$store.dispatch({
 					type: 'planList/commActions',
 					payload: {	
 						key: 'planList',
 						data: {
-							situationId: this.situationID,
+							situationId: this.situationID,
 							type:this.systemSituationType == 2 ? 1 : 0 ,  //0 普通的   1自查督查
 						}
 					}
@@ -130,16 +128,16 @@
 								name: item.name,
 								startDate: item.startDate,
 								endDate: item.endDate,
-								status: item.status,
+								status: item.status,
 								planFlag:item.planFlag,
 								situationType: item.situationType,
 								checkNo: item.checkNo,
 								isCompeleted: item.status == 3 ? true : false,
 								isContinued: item.status == 2 ? true : false,
-								toDistribute: item.toDistribute,
-								departmentId:item.departmentId,
-								departmentType:item.departmentType,
-								empList:item.empList,
+								toDistribute: item.toDistribute,
+								departmentId:item.departmentId,
+								departmentType:item.departmentType,
+								empList:item.empList,
 								
 							}
 						});
@@ -168,56 +166,56 @@
 			 * @param {Boolean} multiple 是否批量编辑
 			 * @param {Number} checkId 查核id
 			 */
-			gotoCheckList(multiple, currentObj) {
+			gotoCheckList(multiple, currentObj) {
 		
 				const {
 					id,
 					startDate,
 					endDate,
 					situationType,
-					checkNo,
-					departmentId,
-					departmentType,
+					checkNo,
+					departmentId,
+					departmentType,
 					empList
-				} = currentObj;
+				} = currentObj;
 		
 				let _startDate = startDate ? startDate + ' 00:00' : ''; // 计划开始时间
-				let _endDate = endDate ? endDate + ' 23:59' : ''; // 计划结束时间
-				
-				if(this.systemSituationType != 2){
-					//非自查督查
-					//跳转到查核列表
-					
-					this.$store.commit('planList/comChangeState', {
-						key: 'ifReloadData',
-						data: false
-					});
-					uni.navigateTo({
-						url: `/pages/editCheckList/editCheckList?situationId=${this.situationID}&checkId=${id}&checkGroupId=${this.checkGroupId}&startDate=${_startDate}&endDate=${_endDate}&multiple=${multiple}&situationType=${situationType}&checkNo=${checkNo}`
-					});
-				}
-				if(this.systemSituationType == 2){
-					//自查督查
-				    let ids = multiple?(this.planList.map(item=>Number(item.id))):[Number(id)];
-					
-					const details = {
-						situationId:this.situationId,
-						endDate:_endDate,
-						startDate:_startDate,
-			            situationType:situationType,
-						isZichaducha:true,
-						departmentId:departmentId,
-						departmentType:departmentType,
-						ids:ids,
-						empId:(empList.map(item=>item.empId)).join(','),
-						empName:(empList.map(item=>item.empName)).join(','),
-					}
-									
-					const _details = encodeURIComponent(JSON.stringify(details))
-
-					uni.navigateTo({
-						url: `/pages/batchDistribution/batchDistribution?multiple=${multiple}&details=${_details}`
-					});
+				let _endDate = endDate ? endDate + ' 23:59' : ''; // 计划结束时间
+				
+				if(this.systemSituationType != 2){
+					//非自查督查
+					//跳转到查核列表
+					
+					this.$store.commit('planList/comChangeState', {
+						key: 'ifReloadData',
+						data: false
+					});
+					uni.navigateTo({
+						url: `/pages/editCheckList/editCheckList?situationId=${this.situationID}&checkId=${id}&checkGroupId=${this.checkGroupId}&startDate=${_startDate}&endDate=${_endDate}&multiple=${multiple}&situationType=${situationType}&checkNo=${checkNo}`
+					});
+				}
+				if(this.systemSituationType == 2){
+					//自查督查
+				    let ids = multiple?(this.planList.map(item=>Number(item.id))):[Number(id)];
+					
+					const details = {
+						situationId:this.situationId,
+						endDate:_endDate,
+						startDate:_startDate,
+			            situationType:situationType,
+						isZichaducha:true,
+						departmentId:departmentId,
+						departmentType:departmentType,
+						ids:ids,
+						empId:(empList.map(item=>item.empId)).join(','),
+						empName:(empList.map(item=>item.empName)).join(','),
+					}
+									
+					const _details = encodeURIComponent(JSON.stringify(details))
+
+					uni.navigateTo({
+						url: `/pages/batchDistribution/batchDistribution?multiple=${multiple}&details=${_details}`
+					});
 				}
 				
 			}
@@ -333,4 +331,4 @@
 			}
 		}
 	}
-</style>
+</style>

+ 1 - 1
pages/situationDetail/situationDetail.vue

@@ -154,7 +154,7 @@
 			this.isChecker=this.nowPermission==3||this.nowPermission==1||this.nowPermission==7||this.nowPermission==8?true:false;
 			this.isStartEndTimeShow=this.nowPermission==2||this.nowPermission==3?true:false;
 			this.isCheckImproveShow=this.nowPermission==1||this.nowPermission==4||this.nowPermission==3||this.nowPermission==6||this.nowPermission==7||this.nowPermission==8?true:false;
-			this.ifShowUnplanedBtn = this.nowPermission==3?true:false;
+			this.ifShowUnplanedBtn = this.nowPermission==2?true:false;
 			this.ifShowPlanSetBtn = this.nowPermission==6?true:false;
 		
 			this.$store.dispatch({

+ 716 - 670
pages/situationsCenter/situationsCenter.vue

@@ -1,684 +1,730 @@
-<template>
-	<view class="situationsCenter-page">
-		<tmNavbar @onsearchChange="onsearchChangehandle" :is-back="false" @checkMessage="checkMessageHandle"
-			:titleMod="nowPermission == 6||nowPermission == 4||nowPermission ==7||nowPermission == 8?'Filter':'Normal'"
-			:filterKeys="filterKeys" :isShowFilter="true" :messageStatus="true" :isShowSearch="true"
-			@filterClick="filterClickHandle" :title="pageTitle" title-color="#292C33"></tmNavbar>
-
-		<view class="situation-list">
-
-			<scroll-view class="scroll-box" scroll-y="true" @scrolltolower="toLower" lower-threshold="5">
-				<view class="content" :style="[situationList.length === 0 ?{height:'100%'}:{}]">
-					<view class="situation" v-for="(item, index) in situationList" :key="item.id"
-						@click="gotoDetail(item.situationID?item.situationID:item.id,item.systemSituationType,item)">
-						<!-- <image v-if="item.systemSituationType != 2" class="situation-topic" :src="`/static/${
-                item.topic ? 'situation-case' : 'situation-system'
-              }.png`"></image> -->
-
-						<view v-if="item.systemSituationType == 2&&nowPermission !=1 " :class="getClass(item)">
-							{{situationStatusName(item)}}</view>
-
-						<view :class="situationTypeName(item,0)"
-							v-if="nowPermission !=4&&nowPermission !=7&&nowPermission !=8&&nowPermission !=6 ">
-							{{situationTypeName(item,1)}}</view>
-
-						<view class="title">
-							<text class="title-name">{{ item.name }}</text>
-						</view>
-						<view class="check-group">
-							<text class="group-text">
-								{{
-                  nowPermission == 2
-                    ? "剩余" + (item.toDistributeCount || 0) + "个待分配"
-                    : item.checkGroupName
-                }}</text>
-						</view>
-						<view class="row">
-							<image class="situation-check" src="/static/situation-check.png"></image>
-							<text class="text">{{ item.checkStatus }}</text>
-						</view>
-						<view class="row">
-							<image class="situation-time" src="/static/situation-time.png"></image>
-							<text class="text">{{ item.nextCheckTime }}</text>
-						</view>
-					</view>
-					<tm-no-data v-if="situationList.length === 0" :textArr="['还没有情境', '快点击右下方按钮新建一个吧']" />
-				</view>
-			</scroll-view>
-		</view>
-		<view v-if="nowPermission == 1" class="situaions-add" @click="gotoCreate">
-			<image class="add-pic" src="/static/situation-add.png"></image>
-		</view>
-		<tm-tabbar :permission="nowPermission" />
-	</view>
-</template>
-/**
-* 情境中心
-*/
-<script>
-	import websocket from "../../utils/ws.js"; //引入websocket
-	import tmNavbar from "@/components/tm-navbar/tm-navbar.vue";
-	import {
-		wsURL
-	} from "../../utils/requestUrl.js";
-	// import appUpdate from "@/components/app-update/app-update.vue"
-
-	export default {
-		components: {
-			tmNavbar
-		},
-		data() {
-			return {
-
-				page: 1, //页数
-				inputValue: "",
-				nowPermission: "",
-				isSearchBarShow: false, //搜索栏是否可见
-				isSearchBoxShow: true, //搜索图标是否可见
-				situationList: [], //情境卡片列表
-				copied_situationList: [], //复制情境卡片列表 用于筛选
-				totalCount: "", //返回数据的总条数
-				refTimer: null,
-				isInitWs: null,
-				messageType: null,
-				num: 1,
-				pageTitle: '情境中心',
-				// showUpdatePop:false,
-				timer: null,
-				// ws接口参数
-				initParams: {},
-
-			};
-		},
-		// components:{appUpdate},
-		created: function() {
-			this.nowPermission = uni.getStorageSync("nowPermission");
-			this.init(true);
-			this.refTimer = setInterval(() => {
-				this.isInitWs = websocket.ws ? false : true;
-				const {
-					hiId,
-					user,
-					permission
-				} = this.initParams;
-				this.isInitWs && this.initWebsocket(hiId, user, permission);
-			}, 3 * 60 * 1000);
-
-			// this.messStatus();
-		},
-		beforeDestroy() {
-			// 关闭ws连接
-			websocket.close();
-			clearInterval(this.refTimer);
-			clearTimeout(this.timer);
-		},
-		computed: {
-			statusBarHeight() {
-				const {
-					statusBarHeight
-				} = uni.getSystemInfoSync();
-				return statusBarHeight;
-			},
-			filterKeys() {
-
-				const list = [{
-						text: '全部',
-						checked: true,
-						key: '0'
-					},
-					{
-						text: '已完成',
-						checked: false,
-						key: '6'
-					},
-					{
-						text: '进行中',
-						checked: false,
-						key: '2'
-					},
-					{
-						text: '待分配',
-						checked: false,
-						key: '4'
-					}
+<template>
+	<view class="situationsCenter-page">
+		<tmNavbar @onsearchChange="onsearchChangehandle" :is-back="false" @checkMessage="checkMessageHandle"
+			:titleMod="nowPermission == 6||nowPermission == 4||nowPermission ==7||nowPermission == 8?'Filter':'Normal'"
+			:filterKeys="filterKeys" :isShowFilter="true" :messageStatus="true" :isShowSearch="true"
+			@filterClick="filterClickHandle" :title="pageTitle" title-color="#292C33"></tmNavbar>
+
+		<view class="situation-list">
+
+			<scroll-view class="scroll-box" scroll-y="true" @scrolltolower="toLower" lower-threshold="5">
+				<view class="content" :style="[situationList.length === 0 ?{height:'100%'}:{}]">
+					<view class="situation" v-for="(item) in situationList" :key="item.id"
+						@click="gotoDetail(item.situationID?item.situationID:item.id,item.systemSituationType,item)">
+						<!-- 左上角类型角标图片(系统/个案/自查督查) -->
+						<image class="type-badge" :src="typeBadgeSrc(item.systemSituationType)" />
+
+						<!-- 右上角组徽标(图标 + 组名) -->
+						<view class="group-badge">
+							<image class="group-icon" src="/static/images/zu_text.png" />
+							<text class="group-text">{{ item.checkGroupName }}</text>
+						</view>
+
+						<view class="title">
+							<text class="title-name">{{ item.name }}</text>
+
+							<!-- 状态角标:在标题行右侧,且仅自查督查时显示 -->
+							<view v-if="item.systemSituationType == 2 && nowPermission != 1" :class="['status-badge', statusClass(item)]">{{ situationStatusName(item) }}</view>
+						</view>
+						<view class="check-group">
+							<text class="group-text-a">{{ planText(item) }}</text>
+							<image  class="improve-btn" src="/static/images/quwanshan_btn.png" @click.stop="gotoImprove(item)" />
+						</view>
+					</view>
+					<tm-no-data v-if="situationList.length === 0" :textArr="['还没有情境', '快点击右下方按钮新建一个吧']" />
+				</view>
+			</scroll-view>
+		</view>
+		<view v-if="nowPermission == 1" class="situaions-add" @click="gotoCreate">
+			<image class="add-pic" src="/static/situation-add.png"></image>
+		</view>
+		<tm-tabbar :permission="nowPermission" />
+	</view>
+</template>
+/**
+* 情境中心
+*/
+<script>
+	import websocket from "../../utils/ws.js"; //引入websocket
+	import tmNavbar from "@/components/tm-navbar/tm-navbar.vue";
+	import {
+		wsURL
+	} from "../../utils/requestUrl.js";
+	// import appUpdate from "@/components/app-update/app-update.vue"
+
+	export default {
+		components: {
+			tmNavbar
+		},
+		data() {
+			return {
+
+				page: 1, //页数
+				inputValue: "",
+				nowPermission: "",
+				isSearchBarShow: false, //搜索栏是否可见
+				isSearchBoxShow: true, //搜索图标是否可见
+				situationList: [], //情境卡片列表
+				copied_situationList: [], //复制情境卡片列表 用于筛选
+				totalCount: "", //返回数据的总条数
+				refTimer: null,
+				isInitWs: null,
+				messageType: null,
+				num: 1,
+				pageTitle: '情境中心',
+				// showUpdatePop:false,
+				timer: null,
+				// ws接口参数
+				initParams: {},
+
+			};
+		},
+		// components:{appUpdate},
+		created: function() {
+			this.nowPermission = uni.getStorageSync("nowPermission");
+			this.init(true);
+			this.refTimer = setInterval(() => {
+				this.isInitWs = websocket.ws ? false : true;
+				const {
+					hiId,
+					user,
+					permission
+				} = this.initParams;
+				this.isInitWs && this.initWebsocket(hiId, user, permission);
+			}, 3 * 60 * 1000);
+
+			// this.messStatus();
+		},
+		beforeDestroy() {
+			// 关闭ws连接
+			websocket.close();
+			clearInterval(this.refTimer);
+			clearTimeout(this.timer);
+		},
+		computed: {
+			statusBarHeight() {
+				const {
+					statusBarHeight
+				} = uni.getSystemInfoSync();
+				return statusBarHeight;
+			},
+			filterKeys() {
+
+				const list = [{
+						text: '全部',
+						checked: true,
+						key: '0'
+					},
+					{
+						text: '已完成',
+						checked: false,
+						key: '6'
+					},
+					{
+						text: '进行中',
+						checked: false,
+						key: '2'
+					},
+					{
+						text: '待分配',
+						checked: false,
+						key: '4'
+					}
 				];
 				
-				return list;
-
-				// const modify = (key) => {
-				// 	const _list = list.map(a => {
-				// 		if (a.key == key) {
-				// 			return {
-				// 				...a,
-				// 				checked: true
-				// 			}
-				// 		} else {
-				// 			return {
-				// 				...a,
-				// 				checked: false
-				// 			}
-				// 		}
+				return list;
+
+				// const modify = (key) => {
+				// 	const _list = list.map(a => {
+				// 		if (a.key == key) {
+				// 			return {
+				// 				...a,
+				// 				checked: true
+				// 			}
+				// 		} else {
+				// 			return {
+				// 				...a,
+				// 				checked: false
+				// 			}
+				// 		}
 				// 	});
-				// 	return _list;
-				// }
-
-				// if (this.nowPermission == 6) {
-				// 	//职能科室负责人
-				// 	return modify(1);
+				// 	return _list;
+				// }
+
+				// if (this.nowPermission == 6) {
+				// 	//职能科室负责人
+				// 	return modify(1);
 				// }
 				// if (this.nowPermission == 6) {
 				// 	//职能科室负责人
 				// 	return modify(4);
-				// }
-
-			}
-
-		},
-		mounted() {
-
-		},
-		methods: {
+				// }
+
+			}
+
+		},
+		mounted() {
+
+		},
+		methods: {
+			// 是否需要展示“去完善”按钮:当查核计划或下次查核缺失时
+			needImprove(item) {
+				// 分配权限卡片不展示“去完善”
+				if (this.nowPermission == 2) return false;
+				// 计划值:自查督查用 checkPlanCount/checkStatus,其他用 checkStatus
+				const hasPlan = (item.systemSituationType == 2)
+					? (typeof item.checkPlanCount === 'number' ? item.checkPlanCount > 0 : !!item.checkStatus)
+					: !!item.checkStatus;
+				const hasNext = !!item.nextCheckTime;
+				return !(hasPlan && hasNext);
+			},
+			planText(item) {
+				// 统一生成“查核计划 | 下次查核”的展示文案:
+				// 仅对“查核计划”的值进行“到次为止”的截取,“下次查核”保留展示
+				const clipToCi = (text) => {
+					// 截取到包含“次”的位置(含“次”),若没有“次”则原样返回
+					try {
+						const str = String(text || '');
+						const idx = str.indexOf('次');
+						return idx >= 0 ? str.slice(0, idx + 1) : str;
+					} catch (e) { return text; }
+				}
+				// 1)分配权限优先显示“剩余待分配”
+				if (this.nowPermission == 2) {
+					return `剩余${item.toDistributeCount || 0}个待分配`;
+				}
+				// 2)自查督查类型展示“第N/次”,并展示“下次查核”
+				if (item.systemSituationType == 2) {
+					const times = (typeof item.checkPlanCount === 'number' ? item.checkPlanCount : parseInt(item.checkStatus, 10)) || 0;
+					const planVal = `第${times}/次`;
+					const next = item.nextCheckTime ? `下次查核:${item.nextCheckTime}` : '下次查核:--';
+					return `查核计划:${clipToCi(planVal)}  |  ${next}`;
+				}
+				// 3)其他类型:截取“查核计划”到“次”,并展示“下次查核”
+				const planRaw = item.checkStatus ? String(item.checkStatus) : '暂无计划';
+				const next = item.nextCheckTime ? `下次查核:${item.nextCheckTime}` : '下次查核:--';
+				return `查核计划:${clipToCi(planRaw)}  |  ${next}`;
+			},
+			typeBadgeSrc(type) {
+				// 根据类型返回左上角角标图片路径
+				if (type === 1) return '/static/images/xitongzhuizong.png';
+				if (type === 0) return '/static/images/gean.png';
+				if (type === 2) return '/static/images/zichaducha.png';
+				return '/static/images/xitongzhuizong.png';
+			},
+			statusClass(item) {
+				// 根据情境状态返回样式类名
+				if (item.situationStatus == 2) return 'doing';
+				if (item.situationStatus == 6) return 'complete';
+				if (item.situationStatus == 4) return 'wait';
+				if (item.situationStatus == 5) return 'complete';
+				return '';
+			},
 			filterClickHandle(filterData) {
-	
-				if (filterData.key != 0) {
-					const _situationList = this.copied_situationList.filter(item => item.situationStatus == Number(
-						filterData.key));
-					this.situationList = _situationList;
+	
+				if (filterData.key != 0) {
+					const _situationList = this.copied_situationList.filter(item => item.situationStatus == Number(
+						filterData.key));
+					this.situationList = _situationList;
 				}else{
 					this.situationList = this.copied_situationList
-				}
-
-			},
-			situationStatusName(item) {
-
-				if (item.situationStatus == 2) {
-					return '进行中'
-				}
-				if (item.situationStatus == 6) {
-					return '全部完成'
-				}
-				if (item.situationStatus == 4) {
-					return '待分配'
-				}
-				if (item.situationStatus == 5) {
-					return '已完成'
-				}
-
-				return null
-
-			},
-			situationTypeName(item, type) {
-
-				if (item.systemSituationType == 0) {
-					return type == 1 ? '个案' : 'Mark case'
-				}
-				if (item.systemSituationType == 1) {
-					return type == 1 ? '系统' : 'Mark system'
-				}
-				if (item.systemSituationType == 2) {
-					return type == 1 ? '自查督查' : 'Mark zichaducha'
-				}
-			},
-			getClass(item) {
-				if (item.situationStatus == 2) {
-					return 'topMark doing'
-				}
-				if (item.situationStatus == 6) {
-					return 'topMark complete'
-				}
-				if (item.situationStatus == 4) {
-					return 'topMark wait'
-				}
-				if (item.situationStatus == 5) {
-					return 'topMark complete'
-				}
-
-				return 'topMark'
-			},
-			onsearchChangehandle(keywords) {
-				this.searchByKeywords(keywords);
-			},
-			openSearchBar() {
-				this.isSearchBarShow = true;
-				this.isSearchBoxShow = false;
-			},
-			checkMessageHandle() {
-				//查看消息
-				uni.navigateTo({
-					url: `/pages/messages/messages`,
-				});
-			},
-			closeSearchBar() {
-				this.isSearchBarShow = false;
-				this.isSearchBoxShow = true;
-			},
-			valueEmpty() {
-				this.inputValue = "";
-			},
-			gotoCreate() {
-				uni.navigateTo({
-					url: "/pages/creatingSituations/creatingSituations",
-				});
-			},
+				}
+
+			},
+			situationStatusName(item) {
+
+				if (item.situationStatus == 2) {
+					return '进行中'
+				}
+				if (item.situationStatus == 6) {
+					return '全部完成'
+				}
+				if (item.situationStatus == 4) {
+					return '待分配'
+				}
+				if (item.situationStatus == 5) {
+					return '已完成'
+				}
+
+				return null
+
+			},
+			situationTypeName(item, type) {
+
+				if (item.systemSituationType == 0) {
+					return type == 1 ? '个案' : 'Mark case'
+				}
+				if (item.systemSituationType == 1) {
+					return type == 1 ? '系统' : 'Mark system'
+				}
+				if (item.systemSituationType == 2) {
+					return type == 1 ? '自查督查' : 'Mark zichaducha'
+				}
+			},
+			getClass(item) {
+				if (item.situationStatus == 2) {
+					return 'topMark doing'
+				}
+				if (item.situationStatus == 6) {
+					return 'topMark complete'
+				}
+				if (item.situationStatus == 4) {
+					return 'topMark wait'
+				}
+				if (item.situationStatus == 5) {
+					return 'topMark complete'
+				}
+
+				return 'topMark'
+			},
+			onsearchChangehandle(keywords) {
+				this.searchByKeywords(keywords);
+			},
+			openSearchBar() {
+				this.isSearchBarShow = true;
+				this.isSearchBoxShow = false;
+			},
+			checkMessageHandle() {
+				//查看消息
+				uni.navigateTo({
+					url: `/pages/messages/messages`,
+				});
+			},
+			closeSearchBar() {
+				this.isSearchBarShow = false;
+				this.isSearchBoxShow = true;
+			},
+			valueEmpty() {
+				this.inputValue = "";
+			},
+			gotoCreate() {
+				uni.navigateTo({
+					url: "/pages/creatingSituations/creatingSituations",
+				});
+			},
+			gotoImprove(item) {
+				// 去完善:进入创建页编辑模式,先回显数据,再从第一步重走
+				const id = item.situationID ? item.situationID : item.id;
+				const systemSituationType = item.systemSituationType;
+				const name = item.name || '';
+                let url = `/pages/creatingSituations/creatingSituations?id=${id}&type=PUT&from=improve`;
+                if (systemSituationType == 2) {
+                    // 自查督查:仅走管理员路线(MULTI),不再附带 planSet
+                    url += `&systemSituationType=${systemSituationType}&themeName=${encodeURIComponent(name)}&situationId=${id}`;
+                }
+				uni.navigateTo({ url });
+			},
 			gotoDetail(id, systemSituationType, item) {
 				console.log(this.situationList);
-                console.log({id, systemSituationType, item});
-				this.$store.commit({
-					type: 'situationsCenter/comChangeState',
-					key: 'currentSelectedSituation',
-					data: item
+                console.log({id, systemSituationType, item});
+				this.$store.commit({
+					type: 'situationsCenter/comChangeState',
+					key: 'currentSelectedSituation',
+					data: item
+				});
+
+				uni.navigateTo({
+					url: `/pages/situationDetail/situationDetail?situationId=${id}&systemSituationType=${systemSituationType}`,
+				});
+			},
+			getSituationList(data, callback) {
+				this.$store
+					.dispatch({
+						type: "situationsCenter/commActions",
+						payload: {
+							data,
+							key: "situationList",
+						},
+					})
+					.then((data) => {
+						if (data) callback(data);
+					});
+			},
+			searchByKeywords(keywords) {
+				let data = {
+					pageNum: 1,
+					pageSize: 300,
+					keyword: keywords,
+				};
+				this.getSituationList(data, (data) => {
+					this.situationList = [];
+					this.createSituationList(data.list);
+				});
+			},
+			createSituationList(list = []) {
+				list.map((item, index) => {
+					this.situationList.push({
+						name: item.name,
+						checkStatus: item.checkStatus,
+						nextCheckTime: item.nextCheckTime,
+						systemSituationType: item.systemSituationType,
+						//systemSituationType == 0 普通 1 自查督查
+						checkGroupName: item.systemSituationType != 2 ? item.checkGroupName : (item
+							.department.map(v => v.deptClassName)).join(','),
+						topic: item.topic == 0 ? true : false,
+						situationStatus: item.situationStatus,
+						situationID: item.id,
+						toDistributeCount: item.toDistributeCount,
+					});
+				});
+			},
+			toLower() {
+
+				uni.showToast({
+					title: "加载中....",
+					icon: "loading",
+					duration: 2000,
+				});
+				let count = this.situationList.length;
+				if (this.totalCount != count) {
+					this.page++;
+					let data = {
+						pageNum: this.page,
+						pageSize: 300,
+					};
+					// this.getSituationList(data, (data) => {
+					// 	this.createSituationList(data.list);
+					// 	let hiId = uni.getStorageSync("hiId");
+					// 	let user = uni.getStorageSync("id");
+					// 	let permission = uni.getStorageSync("nowPermission");
+					// 	this.initParams = {
+					// 		hiId,
+					// 		user,
+					// 		permission
+					// 	};
+					// 	this.isInitWs && this.initWebsocket(hiId, user, permission);
+					// });
+				} else {
+					uni.showToast({
+						title: "没有更多数据了",
+						icon: "none",
+						duration: 1000,
+					});
+				}
+			},
+			// toMessagePage() {
+			// 	// this.messageType = false;
+			// 	uni.navigateTo({
+			// 		url: `/pages/messages/messages`,
+			// 	});
+			// },
+			init(isInitWs) {
+				this.isInitWs = isInitWs;
+				this.initSituationList();
+			},
+			initWebsocket(hiId, user, permission) {
+				websocket.url = wsURL(hiId, user, permission);
+				websocket.createWebSocket(this.resolverWsData.bind(this));
+			},
+			// 解析websocket返回数据
+			resolverWsData(type) {
+				let types = JSON.parse(type);
+				switch (types.type) {
+					case "TO_READ":
+						this.messageType = true;
+						break;
+					default:
+						this.messageType = false;
+						break;
+				}
+			},
+			initSituationList() {
+				let data = {
+					pageNum: 1,
+					pageSize: 300,
+				};
+				this.getSituationList(data, (data) => {
+					this.totalCount = data.totalCount;
+					this.createSituationList(data.list);
+					this.copied_situationList = data.list;
+					let hiId = uni.getStorageSync("hiId");
+					let user = uni.getStorageSync("id");
+					let permission = uni.getStorageSync("nowPermission");
+					this.initParams = {
+						hiId,
+						user,
+						permission
+					};
+					this.isInitWs && this.initWebsocket(hiId, user, permission);
 				});
-
-				uni.navigateTo({
-					url: `/pages/situationDetail/situationDetail?situationId=${id}&systemSituationType=${systemSituationType}`,
-				});
-			},
-			getSituationList(data, callback) {
-				this.$store
-					.dispatch({
-						type: "situationsCenter/commActions",
-						payload: {
-							data,
-							key: "situationList",
-						},
-					})
-					.then((data) => {
-						if (data) callback(data);
-					});
-			},
-			searchByKeywords(keywords) {
-				let data = {
-					pageNum: 1,
-					pageSize: 300,
-					keyword: keywords,
-				};
-				this.getSituationList(data, (data) => {
-					this.situationList = [];
-					this.createSituationList(data.list);
-				});
-			},
-			createSituationList(list = []) {
-				list.map((item, index) => {
-					this.situationList.push({
-						name: item.name,
-						checkStatus: item.checkStatus,
-						nextCheckTime: item.nextCheckTime,
-						systemSituationType: item.systemSituationType,
-						//systemSituationType == 0 普通 1 自查督查
-						checkGroupName: item.systemSituationType != 2 ? item.checkGroupName : (item
-							.department.map(v => v.deptClassName)).join(','),
-						topic: item.topic == 0 ? true : false,
-						situationStatus: item.situationStatus,
-						situationID: item.id,
-						toDistributeCount: item.toDistributeCount,
-					});
-				});
-			},
-			toLower() {
-
-				uni.showToast({
-					title: "加载中....",
-					icon: "loading",
-					duration: 2000,
-				});
-				let count = this.situationList.length;
-				if (this.totalCount != count) {
-					this.page++;
-					let data = {
-						pageNum: this.page,
-						pageSize: 300,
-					};
-					// this.getSituationList(data, (data) => {
-					// 	this.createSituationList(data.list);
-					// 	let hiId = uni.getStorageSync("hiId");
-					// 	let user = uni.getStorageSync("id");
-					// 	let permission = uni.getStorageSync("nowPermission");
-					// 	this.initParams = {
-					// 		hiId,
-					// 		user,
-					// 		permission
-					// 	};
-					// 	this.isInitWs && this.initWebsocket(hiId, user, permission);
-					// });
-				} else {
-					uni.showToast({
-						title: "没有更多数据了",
-						icon: "none",
-						duration: 1000,
-					});
-				}
-			},
-			// toMessagePage() {
-			// 	// this.messageType = false;
-			// 	uni.navigateTo({
-			// 		url: `/pages/messages/messages`,
-			// 	});
-			// },
-			init(isInitWs) {
-				this.isInitWs = isInitWs;
-				this.initSituationList();
-			},
-			initWebsocket(hiId, user, permission) {
-				websocket.url = wsURL(hiId, user, permission);
-				websocket.createWebSocket(this.resolverWsData.bind(this));
-			},
-			// 解析websocket返回数据
-			resolverWsData(type) {
-				let types = JSON.parse(type);
-				switch (types.type) {
-					case "TO_READ":
-						this.messageType = true;
-						break;
-					default:
-						this.messageType = false;
-						break;
-				}
-			},
-			initSituationList() {
-				let data = {
-					pageNum: 1,
-					pageSize: 300,
-				};
-				this.getSituationList(data, (data) => {
-					this.totalCount = data.totalCount;
-					this.createSituationList(data.list);
-					this.copied_situationList = data.list;
-					let hiId = uni.getStorageSync("hiId");
-					let user = uni.getStorageSync("id");
-					let permission = uni.getStorageSync("nowPermission");
-					this.initParams = {
-						hiId,
-						user,
-						permission
-					};
-					this.isInitWs && this.initWebsocket(hiId, user, permission);
-				});
-			},
-			// messStatus(){
-			// 	let num = 1;
-			// 	let timer = setInterval(()=>{
-			// 	  this.$store.dispatch({
-			// 		  type: "calendar/commActions",
-			// 		  payload: {
-			// 			key: "messagesList",
-			// 			data: {
-			// 			  pageNum: num,
-			// 			  pageSize: 100,
-			// 			},
-			// 		  },
-			// 		}).then((res)=>{
-			// 			if(res && res.list.length == 0){
-			// 				clearInterval(timer)
-			// 			}else if(res && res.list.length != 0){
-			// 				res.list.map((item)=>{
-			// 					if(!item.readStatus){
-			// 						this.messageType = true;
-			// 						return;
-			// 					}
-			// 				})
-			// 			}
-			// 		})
-			// 	  num++;
-			// 	},200)
-			// }
-		},
-	};
-</script>
-
-<style lang="less">
-	.situationsCenter-page {
-		height: 100%;
-
-		.navbar {
-			background-color: #ffffff;
-		}
-
-		.calender-remind {
-			width: 62.5rpx;
-			height: 62.5rpx;
-			position: fixed;
-			top: 20rpx;
-			right: 25rpx;
-			background: rgba(255, 255, 255, 0.95);
-			border-radius: 50%;
-			z-index: 2;
-
-			image {
-				margin-left: 17.5rpx;
-				margin-top: 16.87rpx;
-				width: 27.5rpx;
-				height: 28.75rpx;
-			}
-		}
-
-		.search-box {
-			position: fixed;
-			top: 20rpx;
-			left: 25rpx;
-			z-index: 2;
-
-			.search-model {
-				height: 62.5rpx;
-				width: 62.5rpx;
-				background-color: #ffffff;
-				text-align: center;
-				border-radius: 50%;
-				box-shadow: 0px 10px 10px 0px rgba(217, 221, 228, 0.5);
-				border: 1px solid #e6eaf2;
-				opacity: 0.85;
-
-				.search-pic {
-					width: 27.5rpx;
-					height: 27.5rpx;
-					margin-top: 17.5rpx;
-				}
-			}
-
-			.search-bar {
-				background-color: #ffffff;
-				width: 700rpx;
-				height: 62.5rpx;
-				top: 0rpx;
-				position: absolute;
-				z-index: 2;
-
-				.search-item {
-					background-color: #ffffff;
-					width: 593.75rpx;
-					height: 62.5rpx;
-					float: left;
-					border-radius: 6.25rpx;
-					border-right: 1.25rpx solid #f0f2f7;
-					;
-
-					.search-pic {
-						width: 21.87rpx;
-						height: 21.87rpx;
-						margin-left: 12.5rpx;
-						margin-top: 20.62rpx;
-						float: left;
-					}
-
-					.searh-input {
-						background-color: #ffffff;
-						width: 525rpx;
-						height: 55rpx;
-						font-size: 22.5rpx;
-						float: right;
-						margin-top: 3.75rpx;
-						margin-left: 3.12rpx;
-					}
-
-					.text-clear {
-						width: 21.87rpx;
-						height: 21.87rpx;
-						float: right;
-						margin-top: 20.62rpx;
-						margin-right: 6.25rpx;
-					}
-				}
-
-				.cancel-text {
-					font-size: 22.5rpx;
-					line-height: 62.5rpx;
-					color: #a1a7b3;
-					margin-right: 31.25rpx;
-					float: right;
-				}
-			}
-		}
-
-		.situation-list {
-			position: relative;
-			height: 100%;
-
-			.scroll-box {
-				width: 100%;
-				height: calc(100% - 87.5rpx);
-
-				.content {
-					display: flex;
-					flex-flow: row wrap;
-					padding-bottom: 100rpx;
-
-					.situation {
-						position: relative;
-						height: 187.5rpx;
-						width: 337.5rpx;
-						background: #ffffff;
-						box-shadow: 0px 6px 20px 0px rgba(0, 13, 51, 0.1);
-						border-radius: 8px;
-						margin-left: 25rpx;
-						margin-top: 25rpx;
-						// overflow: hidden;
-
-						.Mark {
-							position: absolute;
-							top: 0;
-							right: 0;
-							font-size: 15rpx;
-
-							padding-left: 20rpx;
-							padding-right: 10rpx;
-							color: #ffffff;
-							border-radius: 0px 8rpx 0px 25rpx;
-							background: rgba(255, 51, 85, 1);
-
-							&.case {
-								background: rgba(0, 204, 170, 1);
-							}
-
-							&.system {
-								background: rgba(61, 121, 242, 1);
-							}
-						}
-
-						.topMark {
-							position: absolute;
-							top: 0;
-							right: 0;
-							padding: 3.75rpx 12.5rpx;
-							font-size: 13.75rpx;
-							font-family: SourceHanSansCN-Medium, SourceHanSansCN;
-							font-weight: 500;
-							color: #FFFFFF;
-							background: #FF6680;
-							border-radius: 0px 5rpx 0px 20rpx;
-
-							&.doing {
-								background-color: rgba(255, 204, 102, 1);
-							}
-
-							&.wait {
-								background-color: rgba(102, 179, 255, 1);
-							}
-
-							&.complete {
-								background-color: rgba(173, 184, 204, 1);
-							}
-						}
-
-						.situation-topic {
-							width: 62.5rpx;
-							height: 25rpx;
-							float: right;
-						}
-
-						.title {
-							height: 22.5rpx;
-							margin-left: 20rpx;
-							margin-top: 25rpx;
-							display: flex;
-							align-items: center;
-
-							.title-name {
-								width: 300rpx;
-								font-size: 22.5rpx;
-								font-family: SourceHanSansCN-Bold, SourceHanSansCN;
-								font-weight: bold;
-								color: #292c33;
-								overflow: hidden; //超出的文本隐藏
-								text-overflow: ellipsis; //溢出用省略号显示
-								white-space: nowrap; //溢出不换行
-
-							}
-						}
-
-						.check-group {
-							margin-left: 20rpx;
-							margin-top: 15rpx;
-							margin-bottom: 25rpx;
-							height: 17.5rpx;
-							display: flex;
-							align-items: center;
-
-							.group-text {
-								font-size: 17.5rpx;
-								font-family: SourceHanSansCN-Normal, SourceHanSansCN;
-								font-weight: 400;
-								color: #666e80;
-							}
-						}
-
-						.row {
-							margin-left: 20rpx;
-							margin-bottom: 17.5rpx;
-							display: flex;
-							align-items: center;
-							height: 20rpx;
-
-							.situation-check {
-								width: 20rpx;
-								height: 20rpx;
-							}
-
-							.situation-time {
-								width: 20rpx;
-								height: 20rpx;
-							}
-
-							.text {
-								font-size: 20rpx;
-								font-family: SourceHanSansCN-Normal, SourceHanSansCN;
-								font-weight: 400;
-								color: #292c33;
-								margin-left: 11.25rpx;
-							}
-						}
-					}
-				}
-			}
-		}
-
-		.situaions-add {
-			position: fixed;
-			right: 25rpx;
-			bottom: 130rpx;
-
-			.add-pic {
-				width: 75rpx;
-				height: 75rpx;
-			}
-		}
-	}
+			},
+			// messStatus(){
+			// 	let num = 1;
+			// 	let timer = setInterval(()=>{
+			// 	  this.$store.dispatch({
+			// 		  type: "calendar/commActions",
+			// 		  payload: {
+			// 			key: "messagesList",
+			// 		  },
+			// 		}).then((res)=>{
+			// 			if(res && res.list.length == 0){
+			// 				clearInterval(timer)
+			// 			}else if(res && res.list.length != 0){
+			// 				res.list.map((item)=>{
+			// 					if(!item.readStatus){
+			// 						this.messageType = true;
+			// 						return;
+			// 					}
+			// 				})
+			// 			}
+			// 		})
+			// 	  num++;
+			// 	},200)
+
+
+			// }
+			
+		},
+	};
+</script>
+
+<style lang="less" scoped>
+	.situationsCenter-page {
+		height: 100%;
+
+		.navbar {
+			background-color: #ffffff;
+		}
+
+		.calender-remind {
+			width: 62.5rpx;
+			height: 62.5rpx;
+			position: fixed;
+			top: 20rpx;
+			right: 25rpx;
+			background: rgba(255, 255, 255, 0.95);
+			border-radius: 50%;
+			z-index: 2;
+
+			image {
+				margin-left: 17.5rpx;
+				margin-top: 16.87rpx;
+				width: 27.5rpx;
+				height: 28.75rpx;
+			}
+		}
+
+		.search-box {
+			position: fixed;
+			top: 20rpx;
+			left: 25rpx;
+			z-index: 2;
+
+			.search-model {
+				height: 62.5rpx;
+				width: 62.5rpx;
+				background-color: #ffffff;
+				text-align: center;
+				border-radius: 50%;
+				box-shadow: 0px 10px 10px 0px rgba(217, 221, 228, 0.5);
+				border: 1px solid #e6eaf2;
+				opacity: 0.85;
+
+				.search-pic {
+					width: 27.5rpx;
+					height: 27.5rpx;
+					margin-top: 17.5rpx;
+				}
+			}
+
+			.search-bar {
+				background-color: #ffffff;
+				width: 700rpx;
+				height: 62.5rpx;
+				top: 0rpx;
+				position: absolute;
+				z-index: 2;
+
+				.search-item {
+					background-color: #ffffff;
+					width: 593.75rpx;
+					height: 62.5rpx;
+					float: left;
+					border-radius: 6.25rpx;
+					border-right: 1.25rpx solid #f0f2f7;
+					;
+
+					.search-pic {
+						width: 21.87rpx;
+						height: 21.87rpx;
+						margin-left: 12.5rpx;
+						margin-top: 20.62rpx;
+						float: left;
+					}
+
+					.searh-input {
+						background-color: #ffffff;
+						width: 525rpx;
+						height: 55rpx;
+						font-size: 22.5rpx;
+						float: right;
+						margin-top: 3.75rpx;
+						margin-left: 3.12rpx;
+					}
+
+					.text-clear {
+						width: 21.87rpx;
+						height: 21.87rpx;
+						float: right;
+						margin-top: 20.62rpx;
+						margin-right: 6.25rpx;
+					}
+				}
+
+				.cancel-text {
+					font-size: 22.5rpx;
+					line-height: 62.5rpx;
+					color: #a1a7b3;
+					margin-right: 31.25rpx;
+					float: right;
+				}
+			}
+		}
+
+		.situation-list {
+			position: relative;
+			height: 100%;
+
+			.scroll-box {
+				width: 100%;
+				height: calc(100% - 87.5rpx);
+
+				.content {
+					display: flex;
+					flex-flow: column nowrap;
+					padding-bottom: 100rpx;
+
+					.situation {
+						position: relative;
+						height: 187.5rpx;
+						width: 700rpx;
+						background: #ffffff;
+						box-shadow: 0px 6px 20px 0px rgba(0, 13, 51, 0.1);
+						border-radius: 8px;
+						margin-left: 25rpx;
+						margin-right: 25rpx;
+						margin-top: 25rpx;
+						// overflow: hidden;
+
+						/* 左上角类型角标图片样式 */
+						.type-badge {
+							position: absolute;
+							top: 0rpx;
+							left: 0rpx;
+							width: 480rpx;
+							height: 40rpx;
+							z-index: 1;
+						}
+
+						/* 右上角组徽标样式(图标 + 文字) */
+						.group-badge {
+							position: absolute;
+							top: 8rpx;
+							right: 8rpx;
+							display: flex;
+							align-items: center;
+							padding: 4rpx 10rpx;
+							background: #F2F5FA;
+							border-radius: 10rpx;
+							max-width: 220rpx;
+						}
+
+						.group-icon {
+							width: 22rpx;
+							height: 22rpx;
+							margin-right: 6rpx;
+						}
+
+						.group-text {
+							font-size: 18rpx;
+							color: #7A8599;
+							max-width: 190rpx;
+							overflow: hidden;
+							text-overflow: ellipsis;
+							white-space: nowrap;
+						}
+
+						/* 标题行右侧的状态角标 */
+						.status-badge {
+							margin-left: auto;
+							padding: 3rpx 10rpx;
+							font-size: 13.75rpx;
+							color: #FFFFFF;
+							border-radius: 10rpx;
+						}
+						.status-badge.doing { background-color: rgba(255, 204, 102, 1); }
+						.status-badge.wait { background-color: rgba(102, 179, 255, 1); }
+						.status-badge.complete { background-color: rgba(173, 184, 204, 1); }
+
+						.situation-topic {
+							width: 62.5rpx;
+							height: 25rpx;
+							float: right;
+						}
+
+						.title {
+							margin-left: 20rpx;
+							margin-top: 70rpx;
+							display: flex;
+							align-items: center;
+                            height: 50rpx !important;
+							.title-name {
+								font-size: 40rpx !important;
+								&>span {
+									display: inline-block;
+									width: 300rpx;
+									
+									color: #292c33;
+									overflow: hidden; //超出的文本隐藏
+									text-overflow: ellipsis; //溢出用省略号显示
+									white-space: nowrap; //溢出不换行
+								}
+								
+
+							}
+						}
+
+						.check-group {
+							margin-left: 20rpx;
+							margin-top: 15rpx;
+							margin-bottom: 25rpx;
+							height: 17.5rpx;
+							display: flex;
+							align-items: center;
+
+							.group-text-a {
+								font-size: 22rpx;
+								font-family: SourceHanSansCN-Normal, SourceHanSansCN;
+								font-weight: 400;
+								color: #666e80;
+								overflow: hidden;
+								text-overflow: ellipsis;
+								white-space: nowrap;
+							}
+
+							/* 去完善按钮样式:与文案同一行右侧显示 */
+							.improve-btn {
+								margin-left: auto;
+								width: 120rpx;
+								height: 45rpx;
+								margin-right: 20rpx;
+							}
+						}
+
+					}
+				}
+			}
+		}
+
+		.situaions-add {
+			position: fixed;
+			right: 25rpx;
+			bottom: 130rpx;
+
+			.add-pic {
+				width: 75rpx;
+				height: 75rpx;
+			}
+		}
+	}
 </style>

BIN
static/.DS_Store


BIN
static/ai_avatar.png


BIN
static/ai_banner.png


BIN
static/ai_bg.png


BIN
static/ai_copy.png


BIN
static/aisend_btn_white.png


BIN
static/aiwenda_tabicon.png


BIN
static/aiwenda_tabicon_color.png


BIN
static/collapase_up.png


BIN
static/images/bgtop.png


BIN
static/images/blue_logo.png


BIN
static/images/depart.png


BIN
static/images/gean.png


BIN
static/images/gray_logo.png


BIN
static/images/hosp.png


BIN
static/images/login_account_icon.png


BIN
static/images/login_pwd_icon.png


BIN
static/images/name.png


BIN
static/images/number.png


BIN
static/images/quwanshan_btn.png


BIN
static/images/remark.png


BIN
static/images/shade.png


BIN
static/images/shade_old.png


BIN
static/images/time.png


BIN
static/images/xitongzhuizong.png


BIN
static/images/zichaducha.png


BIN
static/images/zu_text.png


BIN
uni_modules/.DS_Store


+ 553 - 0
utils/ai-api.js

@@ -0,0 +1,553 @@
+/**
+ * AI问答API工具函数
+ */
+
+// AI问答API基础配置
+const AI_API_CONFIG = {
+	baseURL: 'http://116.62.47.88:5003',
+	timeout: 30000, // 30秒超时
+	headers: {
+		'Content-Type': 'application/json'
+	}
+};
+
+/**
+ * 发送AI问答请求(真正的流式处理)
+ * @param {string} question - 用户问题
+ * @param {Function} onChunk - 接收流式数据的回调函数
+ * @returns {Promise} 返回Promise对象
+ */
+export const askAI = (question, onChunk = null) => {
+	return new Promise((resolve, reject) => {
+		// 参数验证
+		if (!question || typeof question !== 'string' || !question.trim()) {
+			reject(new Error('问题不能为空'));
+			return;
+		}
+
+		// 构建请求参数
+		const requestData = {
+			question: question.trim()
+		};
+
+		// 发送请求
+		uni.request({
+			url: `${AI_API_CONFIG.baseURL}/ask`,
+			method: 'POST',
+			header: {
+				...AI_API_CONFIG.headers,
+				'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
+			},
+			data: requestData,
+			timeout: AI_API_CONFIG.timeout,
+			success: (response) => {
+				// 检查响应状态
+				if (response.statusCode === 200) {
+					// 记录响应数据用于调试
+					console.log('AI问答API响应数据:', response.data);
+					console.log('响应数据类型:', typeof response.data);
+					
+					// 处理响应数据
+					let responseText = '';
+					
+					// 如果是字符串,直接使用
+					if (typeof response.data === 'string') {
+						responseText = response.data;
+						console.log('响应是字符串,长度:', responseText.length);
+					} else if (response.data && typeof response.data === 'object') {
+						// 如果是对象,尝试提取文本内容
+						responseText = extractTextFromResponse(response.data);
+						console.log('响应是对象,提取的文本:', responseText);
+					}
+					
+					// 如果有回调函数,实现真正的流式效果
+					if (onChunk && typeof onChunk === 'function') {
+						console.log('开始真正的流式解析');
+						// 立即开始解析,每解析到一个JSON对象就立即显示
+						const fullText = parseAndStreamRealTime(responseText, onChunk);
+						
+						// 返回完整响应
+						resolve({
+							success: true,
+							data: fullText,
+							statusCode: response.statusCode
+						});
+					} else {
+						// 直接解析完整文本
+						const parsedText = parseConcatenatedJson(responseText);
+						console.log('解析后的文本:', parsedText);
+						
+						resolve({
+							success: true,
+							data: parsedText,
+							statusCode: response.statusCode
+						});
+					}
+				} else {
+					// 服务器错误
+					reject({
+						success: false,
+						error: `服务器响应错误 (${response.statusCode})`,
+						statusCode: response.statusCode,
+						data: response.data
+					});
+				}
+			},
+			fail: (error) => {
+				// 网络错误或其他错误
+				console.error('AI问答API请求失败:', error);
+				
+				let errorMessage = '网络连接失败';
+				if (error.errMsg) {
+					if (error.errMsg.includes('timeout')) {
+						errorMessage = '请求超时,请稍后重试';
+					} else if (error.errMsg.includes('fail')) {
+						errorMessage = '网络连接失败,请检查网络';
+					}
+				}
+				
+				reject({
+					success: false,
+					error: errorMessage,
+					originalError: error
+				});
+			}
+		});
+	});
+};
+
+/**
+ * 通过HTTP chunked实现真正的流式读取(H5下)
+ * - 首选 fetch + ReadableStream
+ * - 回退到 XMLHttpRequest + onprogress
+ * @param {string} question 用户问题
+ * @param {(chunkText:string, fullText:string)=>void} onChunk 块回调
+ * @returns {Promise<{success:boolean,data?:string,error?:string}>}
+ */
+export const askAIStream = (question, onChunk) => {
+    return new Promise(async (resolve, reject) => {
+        if (!question || typeof question !== 'string' || !question.trim()) {
+            reject(new Error('问题不能为空'));
+            return;
+        }
+
+        const url = `${AI_API_CONFIG.baseURL}/ask`;
+        const headers = {
+            ...AI_API_CONFIG.headers,
+            'Accept': 'text/event-stream, text/plain, text/html, application/json;q=0.9, */*;q=0.8',
+            'Cache-Control': 'no-cache'
+        };
+        const body = JSON.stringify({ question: question.trim() });
+
+        // 块处理:后端按 "\n\n" 分隔的 JSON 块,逐块解析并输出 chatText
+        const emitByDoubleNewline = (state, emit) => {
+            // 兼容 \r\n\r\n
+            const delimiterRe = /\r?\n\r?\n/;
+            const parts = state.buffer.split(delimiterRe);
+            // 最后一段可能是不完整块,先不处理
+            for (let i = 0; i < parts.length - 1; i++) {
+                const part = (parts[i] || '').trim();
+                if (!part) continue;
+                try {
+                    const obj = JSON.parse(part);
+                    let piece = '';
+                    if (obj && obj.response && typeof obj.response.chatText === 'string') {
+                        piece = obj.response.chatText;
+                    }
+                    // 去掉思考标签,前端只显示可见内容
+                    if (piece) {
+                        const cleaned = piece.replace(/<think>/g, '').replace(/<\/think>/g, '');
+                        emit(cleaned);
+                        state.fullText += cleaned;
+                    }
+                } catch (e) {
+                    // 块级解析失败,忽略该块或原样输出
+                    // 为安全起见这里选择忽略,避免把JSON原文渲染到UI
+                }
+            }
+            // 缓存尾段(不完整)
+            state.buffer = parts[parts.length - 1] || '';
+        };
+
+        // H5 fetch 流式
+        try {
+            if (typeof window !== 'undefined' && window.fetch && typeof ReadableStream !== 'undefined') {
+                const res = await fetch(url, { method: 'POST', headers, body });
+                if (!res.ok) {
+                    reject({ success: false, error: `服务器响应错误 (${res.status})` });
+                    return;
+                }
+                const reader = res.body && res.body.getReader ? res.body.getReader() : null;
+                if (!reader) {
+                    // 无 reader 时,退化为一次性读取
+                    const text = await res.text();
+                    if (onChunk) onChunk(text, text);
+                    resolve({ success: true, data: text });
+                    return;
+                }
+                const decoder = new TextDecoder('utf-8');
+                let done = false;
+                const state = { buffer: '', fullText: '', thinking: false };
+                while (!done) {
+                    const { value, done: readerDone } = await reader.read();
+                    done = readerDone;
+                    if (value) {
+                        const chunkText = decoder.decode(value, { stream: !done });
+                        state.buffer += chunkText;
+                        // 直接在此处解析并调用回调,保证 nowFull 不包含思考段的可见文本
+                        const delimiterRe = /\r?\n\r?\n/;
+                        const parts = state.buffer.split(delimiterRe);
+                        for (let i = 0; i < parts.length - 1; i++) {
+                            const part = (parts[i] || '').trim();
+                            if (!part) continue;
+                            try {
+                                const obj = JSON.parse(part);
+                                let piece = '';
+                                if (obj && obj.response && typeof obj.response.chatText === 'string') {
+                                    piece = obj.response.chatText;
+                                }
+                                const hasOpen = typeof piece === 'string' && piece.indexOf('<think>') > -1;
+                                const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
+                                const cleaned = (piece || '').replace(/<think>/g, '').replace(/<\/think>/g, '');
+                                const isThinkingPart = state.thinking || hasOpen; // 本块属于思考阶段
+                                const addToFull = isThinkingPart ? '' : cleaned;
+                                const computedNowFull = state.fullText + addToFull;
+                                if (onChunk) onChunk(cleaned, computedNowFull, piece || '');
+                                if (addToFull) state.fullText += addToFull;
+                                // 更新思考状态:如果遇到关闭标签则退出思考
+                                if (hasOpen) state.thinking = true;
+                                if (hasClose) state.thinking = false;
+                            } catch (e) {}
+                        }
+                        state.buffer = parts[parts.length - 1] || '';
+                    }
+                }
+                // 读完后,尝试解析剩余缓冲
+                if (state.buffer.trim()) {
+                    try {
+                        const obj = JSON.parse(state.buffer.trim());
+                        let piece = '';
+                        if (obj && obj.response && typeof obj.response.chatText === 'string') piece = obj.response.chatText;
+                        if (piece) {
+                            const hasOpen = typeof piece === 'string' && piece.indexOf('<think>') > -1;
+                            const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
+                            const cleaned = piece.replace(/<think>/g, '').replace(/<\/think>/g, '');
+                            const isThinkingPart = state.thinking || hasOpen;
+                            const addToFull = isThinkingPart ? '' : cleaned;
+                            onChunk && onChunk(cleaned, state.fullText + addToFull, piece);
+                            if (addToFull) state.fullText += addToFull;
+                            if (hasOpen) state.thinking = true;
+                            if (hasClose) state.thinking = false;
+                        }
+                    } catch (_) {}
+                }
+                resolve({ success: true, data: state.fullText });
+                return;
+            }
+        } catch (err) {
+            console.warn('fetch流式失败,尝试XHR回退:', err);
+        }
+
+        // XHR 回退:利用 onprogress 增量读取
+        try {
+            const xhr = new XMLHttpRequest();
+            xhr.open('POST', url, true);
+            Object.keys(headers).forEach((k) => xhr.setRequestHeader(k, headers[k]));
+            let lastLen = 0;
+            const state = { buffer: '', fullText: '', thinking: false };
+            xhr.onreadystatechange = () => {
+                if (xhr.readyState === 4) {
+                    if (xhr.status >= 200 && xhr.status < 300) {
+                        // 完成时解析尾段
+                        if (state.buffer.trim()) {
+                            try {
+                                const obj = JSON.parse(state.buffer.trim());
+                                let piece = '';
+                                if (obj && obj.response && typeof obj.response.chatText === 'string') piece = obj.response.chatText;
+                                if (piece) {
+                                    const hasOpen = typeof piece === 'string' && piece.indexOf('<think>') > -1;
+                                    const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
+                                    const cleaned = piece.replace(/<think>/g, '').replace(/<\/think>/g, '');
+                                    const isThinkingPart = state.thinking || hasOpen;
+                                    const addToFull = isThinkingPart ? '' : cleaned;
+                                    onChunk && onChunk(cleaned, state.fullText + addToFull, piece);
+                                    if (addToFull) state.fullText += addToFull;
+                                    if (hasOpen) state.thinking = true;
+                                    if (hasClose) state.thinking = false;
+                                }
+                            } catch (_) {}
+                        }
+                        resolve({ success: true, data: state.fullText });
+                    } else {
+                        reject({ success: false, error: `服务器响应错误 (${xhr.status})` });
+                    }
+                }
+            };
+            xhr.onprogress = () => {
+                const resp = xhr.responseText || '';
+                if (resp.length > lastLen) {
+                    const delta = resp.substring(lastLen);
+                    lastLen = resp.length;
+                    state.buffer += delta;
+                    // 解析增量并过滤思考段对全文累积的影响
+                    const delimiterRe = /\r?\n\r?\n/;
+                    const parts = state.buffer.split(delimiterRe);
+                    for (let i = 0; i < parts.length - 1; i++) {
+                        const part = (parts[i] || '').trim();
+                        if (!part) continue;
+                        try {
+                            const obj = JSON.parse(part);
+                            let piece = '';
+                            if (obj && obj.response && typeof obj.response.chatText === 'string') {
+                                piece = obj.response.chatText;
+                            }
+                            const hasOpen = typeof piece === 'string' && piece.indexOf('<think>') > -1;
+                            const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
+                            const cleaned = (piece || '').replace(/<think>/g, '').replace(/<\/think>/g, '');
+                            const isThinkingPart = state.thinking || hasOpen;
+                            const addToFull = isThinkingPart ? '' : cleaned;
+                            const computedNowFull = state.fullText + addToFull;
+                            if (onChunk) onChunk(cleaned, computedNowFull, piece || '');
+                            if (addToFull) state.fullText += addToFull;
+                            if (hasOpen) state.thinking = true;
+                            if (hasClose) state.thinking = false;
+                        } catch (e) {}
+                    }
+                    state.buffer = parts[parts.length - 1] || '';
+                }
+            };
+            xhr.onerror = () => {
+                reject({ success: false, error: '网络连接失败' });
+            };
+            xhr.send(body);
+        } catch (err) {
+            reject({ success: false, error: '请求发送失败' });
+        }
+    });
+};
+
+/**
+ * 从响应对象中提取文本内容
+ * @param {any} responseData - 响应数据
+ * @returns {string} 提取的文本内容
+ */
+function extractTextFromResponse(responseData) {
+	if (typeof responseData === 'string') {
+		return responseData;
+	}
+	
+	if (responseData && typeof responseData === 'object') {
+		// 尝试提取各种可能的字段
+		if (responseData.response && responseData.response.chatText) {
+			return responseData.response.chatText;
+		}
+		if (responseData.text) {
+			return responseData.text;
+		}
+		if (responseData.content) {
+			return responseData.content;
+		}
+		if (responseData.message) {
+			return responseData.message;
+		}
+		// 如果是数组,尝试处理第一个元素
+		if (Array.isArray(responseData) && responseData.length > 0) {
+			return extractTextFromResponse(responseData[0]);
+		}
+	}
+	
+	return '';
+}
+
+/**
+ * 解析拼接的JSON对象字符串
+ * @param {string} jsonString - 包含多个JSON对象的字符串
+ * @returns {string} 提取的完整文本
+ */
+function parseConcatenatedJson(jsonString) {
+	if (!jsonString || typeof jsonString !== 'string') {
+		return '';
+	}
+	
+	try {
+		// 尝试直接解析为JSON
+		const parsed = JSON.parse(jsonString);
+		if (parsed && parsed.response && parsed.response.chatText) {
+			return parsed.response.chatText;
+		}
+	} catch (e) {
+		// 如果不是单个JSON对象,尝试解析拼接的JSON
+		console.log('尝试解析拼接的JSON对象');
+	}
+	
+	// 处理拼接的JSON对象
+	let fullText = '';
+	let currentIndex = 0;
+	
+	while (currentIndex < jsonString.length) {
+		// 查找下一个JSON对象的开始位置
+		const startBrace = jsonString.indexOf('{', currentIndex);
+		if (startBrace === -1) break;
+		
+		// 查找对应的结束位置
+		let braceCount = 0;
+		let endBrace = -1;
+		
+		for (let i = startBrace; i < jsonString.length; i++) {
+			if (jsonString[i] === '{') {
+				braceCount++;
+			} else if (jsonString[i] === '}') {
+				braceCount--;
+				if (braceCount === 0) {
+					endBrace = i;
+					break;
+				}
+			}
+		}
+		
+		if (endBrace === -1) break;
+		
+		// 提取单个JSON对象
+		const jsonObj = jsonString.substring(startBrace, endBrace + 1);
+		
+		try {
+			const parsed = JSON.parse(jsonObj);
+			if (parsed && parsed.response && parsed.response.chatText) {
+				fullText += parsed.response.chatText;
+			}
+		} catch (e) {
+			console.error('解析JSON对象失败:', e, jsonObj);
+		}
+		
+		// 移动到下一个位置
+		currentIndex = endBrace + 1;
+	}
+	
+	return fullText;
+}
+
+/**
+ * 实时解析并流式显示
+ * @param {string} jsonString - 包含多个JSON对象的字符串
+ * @param {Function} onChunk - 每解析到一个JSON对象就调用此回调
+ * @returns {string} 提取的完整文本
+ */
+function parseAndStreamRealTime(jsonString, onChunk) {
+	if (!jsonString || typeof jsonString !== 'string') {
+		return '';
+	}
+	
+	let fullText = '';
+	let currentIndex = 0;
+	
+	while (currentIndex < jsonString.length) {
+		// 查找下一个JSON对象的开始位置
+		const startBrace = jsonString.indexOf('{', currentIndex);
+		if (startBrace === -1) break;
+		
+		// 查找对应的结束位置
+		let braceCount = 0;
+		let endBrace = -1;
+		
+		for (let i = startBrace; i < jsonString.length; i++) {
+			if (jsonString[i] === '{') {
+				braceCount++;
+			} else if (jsonString[i] === '}') {
+				braceCount--;
+				if (braceCount === 0) {
+					endBrace = i;
+					break;
+				}
+			}
+		}
+		
+		if (endBrace === -1) break;
+		
+		// 提取单个JSON对象
+		const jsonObj = jsonString.substring(startBrace, endBrace + 1);
+		
+		try {
+			const parsed = JSON.parse(jsonObj);
+			if (parsed && parsed.response && parsed.response.chatText) {
+				const chunkText = parsed.response.chatText;
+				fullText += chunkText;
+				
+				// 立即调用回调函数,实现实时显示
+				console.log('实时解析到文本块:', chunkText, '当前完整文本:', fullText);
+				onChunk(chunkText, fullText);
+			}
+		} catch (e) {
+			console.error('解析JSON对象失败:', e, jsonObj);
+		}
+		
+		// 移动到下一个位置
+		currentIndex = endBrace + 1;
+	}
+	
+	return fullText;
+}
+
+/**
+ * 格式化AI回复内容
+ * @param {any} content - AI回复的原始内容
+ * @returns {string} 格式化后的内容
+ */
+export const formatAIResponse = (content) => {
+	if (!content) {
+		return '抱歉,我暂时无法回答您的问题。';
+	}
+	
+	// 如果是字符串,直接返回
+	if (typeof content === 'string') {
+		return content.trim();
+	}
+	
+	// 如果是对象,尝试提取文本内容
+	if (typeof content === 'object') {
+		// 处理response对象 - 这是API返回的主要格式
+		if (content.response && content.response.chatText) {
+			return content.response.chatText.trim();
+		}
+		// 处理其他可能的字段
+		if (content.text) {
+			return content.text.trim();
+		}
+		if (content.content) {
+			return content.content.trim();
+		}
+		if (content.message) {
+			return content.message.trim();
+		}
+		// 如果是数组,尝试处理第一个元素
+		if (Array.isArray(content) && content.length > 0) {
+			return formatAIResponse(content[0]);
+		}
+	}
+	
+	return '抱歉,我暂时无法回答您的问题。';
+};
+
+/**
+ * 检查网络连接状态
+ * @returns {Promise<boolean>} 返回网络是否可用
+ */
+export const checkNetworkStatus = () => {
+	return new Promise((resolve) => {
+		uni.getNetworkType({
+			success: (res) => {
+				const isConnected = res.networkType !== 'none';
+				resolve(isConnected);
+			},
+			fail: () => {
+				resolve(false);
+			}
+		});
+	});
+};
+
+export default {
+	askAI,
+	formatAIResponse,
+	checkNetworkStatus,
+	askAIStream
+};

+ 273 - 0
utils/ai-websocket.js

@@ -0,0 +1,273 @@
+/**
+ * AI问答WebSocket工具函数
+ */
+
+// AI WebSocket配置
+const AI_WS_CONFIG = {
+	baseURL: 'ws://116.62.47.88:5003',
+	timeout: 30000, // 30秒超时
+	reconnectInterval: 3000, // 重连间隔
+	maxReconnectAttempts: 3 // 最大重连次数
+};
+
+class AIWebSocket {
+	constructor() {
+		this.ws = null;
+		this.url = '';
+		this.connected = false;
+		this.connecting = false;
+		this.reconnectCount = 0;
+		this.lockReconnect = false;
+		this.messageCallback = null;
+		this.errorCallback = null;
+		this.closeCallback = null;
+		this.reconnectTimer = null;
+		this.heartbeatTimer = null;
+		// 最近一次连接的参数,用于必要时重连
+		this.lastQuestion = '';
+		this.lastOnMessage = null;
+		this.lastOnError = null;
+		// 是否允许自动重连(默认不重连,避免重复消息)
+		this.shouldReconnect = false;
+	}
+
+	/**
+	 * 连接WebSocket
+	 * @param {string} question - 用户问题
+	 * @param {Function} onMessage - 消息回调函数
+	 * @param {Function} onError - 错误回调函数
+	 */
+	connect(question, onMessage, onError, onClose) {
+		if (this.connecting || this.connected) {
+			console.log('WebSocket已连接或正在连接中');
+			return;
+		}
+
+		// 记录本次连接参数
+		this.messageCallback = onMessage;
+		this.errorCallback = onError;
+		this.lastQuestion = question;
+		this.lastOnMessage = onMessage;
+		this.lastOnError = onError;
+		this.closeCallback = onClose;
+		// 默认不自动重连,除非调用方显式开启
+		this.shouldReconnect = false;
+		this.url = `${AI_WS_CONFIG.baseURL}/ws`;
+
+		try {
+			this.connecting = true;
+			
+			// 创建WebSocket连接
+			this.ws = uni.connectSocket({
+				url: this.url,
+				success: (data) => {
+					console.log('AI WebSocket连接创建成功');
+				},
+				fail: (error) => {
+					console.error('AI WebSocket连接创建失败:', error);
+					this.handleError('连接创建失败');
+				}
+			});
+
+			// 监听连接打开
+			this.ws.onOpen((res) => {
+				console.log('AI WebSocket连接已打开');
+				this.connecting = false;
+				this.connected = true;
+				this.reconnectCount = 0;
+				
+				// 发送问题
+				this.sendQuestion(question);
+				
+				// 开始心跳检测
+				this.startHeartbeat();
+			});
+
+			// 监听消息
+			this.ws.onMessage((res) => {
+				console.log('AI WebSocket收到消息:', res.data);
+				
+				try {
+					const data = JSON.parse(res.data);
+					if (this.messageCallback) {
+						this.messageCallback(data);
+					}
+				} catch (error) {
+					console.error('解析WebSocket消息失败:', error);
+				}
+			});
+
+			// 监听错误
+			this.ws.onError((res) => {
+				console.error('AI WebSocket连接错误:', res);
+				this.handleError('连接错误');
+			});
+
+			// 监听连接关闭
+			this.ws.onClose((res) => {
+				console.log('AI WebSocket连接已关闭');
+				this.connected = false;
+				this.connecting = false;
+				this.ws = null;
+				
+				// 停止心跳
+				this.stopHeartbeat();
+				
+				// 通知外部关闭事件
+				if (this.closeCallback) {
+					try { this.closeCallback(res); } catch (e) { console.error('关闭回调执行失败', e); }
+				}
+				
+				// 仅在允许时才尝试重连
+				if (this.shouldReconnect) {
+					this.reconnect();
+				}
+			});
+
+		} catch (error) {
+			console.error('创建AI WebSocket失败:', error);
+			this.handleError('创建连接失败');
+		}
+	}
+
+	/**
+	 * 发送问题
+	 * @param {string} question - 用户问题
+	 */
+	sendQuestion(question) {
+		if (!this.connected || !this.ws) {
+			console.error('WebSocket未连接,无法发送问题');
+			return;
+		}
+
+		const message = {
+			question: question
+		};
+
+		this.ws.send({
+			data: JSON.stringify(message),
+			success: () => {
+				console.log('问题发送成功');
+			},
+			fail: (error) => {
+				console.error('问题发送失败:', error);
+				this.handleError('发送问题失败');
+			}
+		});
+	}
+
+	/**
+	 * 开始心跳检测
+	 */
+	startHeartbeat() {
+		this.heartbeatTimer = setInterval(() => {
+			if (this.connected && this.ws) {
+				this.ws.send({
+					data: JSON.stringify({ type: 'ping' }),
+					fail: (error) => {
+						console.error('心跳发送失败:', error);
+						this.reconnect();
+					}
+				});
+			}
+		}, 10000); // 每10秒发送一次心跳
+	}
+
+	/**
+	 * 停止心跳检测
+	 */
+	stopHeartbeat() {
+		if (this.heartbeatTimer) {
+			clearInterval(this.heartbeatTimer);
+			this.heartbeatTimer = null;
+		}
+	}
+
+	/**
+	 * 重连
+	 */
+	reconnect() {
+		if (this.lockReconnect || this.reconnectCount >= AI_WS_CONFIG.maxReconnectAttempts) {
+			console.log('达到最大重连次数或重连被锁定');
+			return;
+		}
+
+		this.lockReconnect = true;
+		this.reconnectCount++;
+
+		console.log(`尝试重连 (${this.reconnectCount}/${AI_WS_CONFIG.maxReconnectAttempts})`);
+
+		this.reconnectTimer = setTimeout(() => {
+			this.lockReconnect = false;
+			if (!this.connected) {
+				// 使用最近一次的参数重连
+				this.connect(this.lastQuestion, this.lastOnMessage, this.lastOnError);
+			}
+		}, AI_WS_CONFIG.reconnectInterval);
+	}
+
+	/**
+	 * 处理错误
+	 * @param {string} error - 错误信息
+	 */
+	handleError(error) {
+		console.error('AI WebSocket错误:', error);
+		if (this.errorCallback) {
+			this.errorCallback(error);
+		}
+	}
+
+	/**
+	 * 关闭连接
+	 */
+	close() {
+		console.log('关闭AI WebSocket连接');
+		
+		// 停止重连
+		if (this.reconnectTimer) {
+			clearTimeout(this.reconnectTimer);
+			this.reconnectTimer = null;
+		}
+		
+		// 主动关闭时不再重连
+		this.shouldReconnect = false;
+		
+		// 停止心跳
+		this.stopHeartbeat();
+		
+		// 关闭WebSocket
+		if (this.ws && this.connected) {
+			this.ws.close();
+		}
+		
+		// 重置状态
+		this.connected = false;
+		this.connecting = false;
+		this.ws = null;
+		this.lockReconnect = false;
+		this.reconnectCount = 0;
+		this.messageCallback = null;
+		this.errorCallback = null;
+	}
+
+	/**
+	 * 启用或禁用自动重连
+	 * @param {boolean} enable 是否启用
+	 */
+	setAutoReconnect(enable) {
+		this.shouldReconnect = !!enable;
+	}
+
+	/**
+	 * 检查连接状态
+	 * @returns {boolean} 是否已连接
+	 */
+	isConnected() {
+		return this.connected;
+	}
+}
+
+// 创建单例实例
+const aiWebSocket = new AIWebSocket();
+
+export default aiWebSocket;

+ 229 - 0
utils/authorize.js

@@ -0,0 +1,229 @@
+// 设备授权相关接口封装
+import { confirm as showAppConfirm } from '@/utils/confirm.js'
+// 获取设备授权信息:入参 imei,返回 { authorized: boolean, hospSign?: string, hospitalName?, deptName?, contactPhone? }
+
+// 移除调试模块
+
+// 统一的错误提示方法(H5不支持error图标,这里统一用none)
+function showErrorToast(message) {
+  try {
+    const text = String(message || '请求失败');
+    uni.showToast({ title: text, icon: 'none' });
+  } catch (e) {
+    // 忽略异常
+  }
+}
+
+// 统一的错误弹窗提示
+function showErrorModal(message) {
+  try {
+    const text = String(message || '请求失败');
+    // 使用公共弹窗组件 app-confirm
+    showAppConfirm({
+      title: '提示',
+      content: text,
+      confirmText: '我知道了',
+      cancelText: '取消',
+      type: 'default'
+    }).then(() => {});
+  } catch (e) {
+    // 忽略异常
+  }
+}
+
+export function fetchDeviceAuthorization(imei) {
+  console.log('fetchDeviceAuthorization 调用:', { imei });
+
+  // 实际接口 - 直接使用uni.request调用外部接口
+  return new Promise((resolve, reject) => {
+    const queryUrl = `http://demo.kcim.cn/region/register/queryEnrollInfo?deviceCode=${encodeURIComponent(imei)}`;
+    console.log('发送真实接口请求(POST, deviceCode在URL):', queryUrl);
+    uni.request({
+      url: queryUrl,
+      method: 'POST',
+      data: {},
+      header: {
+        'Content-Type': 'application/json'
+      },
+        success: (res) => {
+        console.log('授权查询接口返回:', res);
+        if (res.statusCode === 200) {
+          // 根据接口返回的数据结构来解析状态
+          const data = res.data;
+          const result = parseAuthResponse(data, imei);
+          resolve(result);
+        } else {
+          const errorMsg = `接口请求失败: ${res.statusCode}`;
+          console.error(errorMsg, res);
+          showErrorToast(errorMsg);
+          reject(new Error(errorMsg));
+        }
+      },
+      fail: (error) => {
+        console.error('授权查询接口失败:', error);
+        const errorMsg = error.errMsg || error.message || '网络请求失败,请检查网络连接';
+        showErrorToast(errorMsg);
+        reject(new Error(errorMsg));
+      }
+    });
+  });
+}
+
+// 移除 Debug 覆盖逻辑
+
+// 解析授权查询接口的响应数据
+function parseAuthResponse(data, imei) {
+  console.log('解析授权响应数据:', data);
+  
+  if (!data) {
+    return {
+      status: 'unauthorized',
+      authorized: false,
+      message: '查询失败'
+    };
+  }
+  
+  switch (data.code) {
+    case 200:
+      // 已授权
+      const authData = data.data || {};
+      return {
+        status: 'authorized',
+        authorized: true,
+        hospSign: authData.hospSign,
+        region: authData.region || authData.host || authData.domain || '',
+        message: data.msg || '设备已授权'
+      };
+      
+    case 201:
+      // 已提交申请待审核
+      const reviewData = data.data || {};
+      return {
+        status: 'reviewing',
+        authorized: false,
+        applyInfo: {
+          hospital: reviewData.hospInfo || '',
+          department: reviewData.enrollDepartment || '',
+          name: reviewData.enrollName || '',
+          phone: reviewData.enrollPhone || '',
+          remark: reviewData.comment || '',
+          imei: imei,
+          applyTime: formatDate(reviewData.enrollTime)
+        },
+        message: data.msg || '申请审核中'
+      };
+      
+    case 203:
+      // 设备已过期
+      const expData = data.data || {};
+      return {
+        status: 'expired',
+        authorized: false,
+        applyInfo: {
+          hospital: expData.hospInfo || '',
+          department: expData.enrollDepartment || '',
+          name: expData.enrollName || '',
+          phone: expData.enrollPhone || '',
+          remark: expData.comment || '',
+          imei: imei,
+          applyTime: formatDate(expData.enrollTime)
+        },
+        message: data.msg || '授权已到期'
+      };
+      
+    case 204:
+    default:
+      // 未找到申请信息,需要提交申请
+      return {
+        status: 'unauthorized',
+        authorized: false,
+        message: data.msg || '设备未授权'
+      };
+  }
+}
+
+// 格式化日期
+function formatDate(dateStr) {
+  if (!dateStr) return '';
+  
+  try {
+    const date = new Date(dateStr);
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    return `${year}年${month}月${day}日`;
+  } catch (e) {
+    return dateStr;
+  }
+}
+
+// 提交授权申请:入参申请表
+export function submitAuthorizationApply(payload) {
+  console.log('submitAuthorizationApply 调用:', payload);
+
+  // 转换参数格式为接口要求的格式
+  const requestData = {
+    deviceCode: payload.imei,
+    hospInfo: payload.hospital,
+    enrollName: payload.name,
+    enrollPhone: payload.phone,
+    enrollDepartment: payload.department,
+    comment: payload.remark || ''
+  };
+  
+  // 不再支持 Debug 覆盖,统一真实接口
+  
+  // 真实接口提交
+  return new Promise((resolve, reject) => {
+    console.log('发送申请提交请求:', requestData);
+    uni.request({
+      url: 'http://demo.kcim.cn/region/register/submitEnrollInfo',
+      method: 'POST',
+      data: requestData,
+      header: {
+        'Content-Type': 'application/json'
+      },
+      success: (res) => {
+        console.log('申请提交接口返回:', res);
+        if (res.statusCode === 200) {
+          // 兼容多种返回结构:
+          // 1) { code: 200, msg, data }
+          // 2) { success: true/false, errorMessage, showType }
+          // 3) { data: { success: false, errorMessage, ... } }
+          const body = res.data || {};
+          const inner = body && body.data && typeof body.data === 'object' ? body.data : body;
+
+          const isSuccess = (body && body.code === 200) || (body && body.success === true) || (inner && inner.success === true);
+          if (isSuccess) {
+            const successMsg = body.msg || body.message || inner.msg || '提交成功';
+            uni.showToast({ title: successMsg, icon: 'success' });
+            resolve(true);
+          } else {
+            const msgFromBody = body && (body.msg || body.message || body.errorMessage);
+            const msgFromInner = inner && (inner.msg || inner.message || inner.errorMessage);
+            const errorMsg = msgFromBody || msgFromInner || '提交失败';
+            const showType = (body && body.showType) || (inner && inner.showType);
+            // showType=4 一般表示需要弹窗提示,这里统一弹窗
+            if (showType === 4) {
+              showErrorModal(errorMsg);
+            } else {
+              showErrorModal(errorMsg);
+            }
+            reject(new Error(errorMsg));
+          }
+        } else {
+          const errorMsg = `接口请求失败: ${res.statusCode}`;
+          showErrorModal(errorMsg);
+          reject(new Error(errorMsg));
+        }
+      },
+      fail: (error) => {
+        console.error('申请提交接口失败:', error);
+        showErrorModal('网络请求失败');
+        reject(error);
+      }
+    });
+  });
+}
+
+

+ 54 - 0
utils/confirm.js

@@ -0,0 +1,54 @@
+// 全局确认弹窗服务:以编程式方式调用 app-confirm 组件
+import Vue from 'vue'
+import AppConfirm from '@/components/app-confirm/app-confirm.vue'
+
+let singletonVm = null
+
+function getInstance() {
+  // 优先:尝试从根实例的 $refs 获取(所有平台,特别是 APP-PLUS)
+  try {
+    const app = getApp && getApp();
+    const root = app && app.$vm;
+    const viaRef = root && root.$refs && (root.$refs.AppConfirm || root.$refs.appConfirm);
+    if (viaRef) return viaRef;
+  } catch (e) {}
+
+  // 其次:尝试从当前页面实例上获取(页面内也可能挂载了 AppConfirm)
+  try {
+    const pages = getCurrentPages && getCurrentPages();
+    const current = pages && pages[pages.length - 1];
+    const vm = current && current.$vm;
+    const viaPage = vm && vm.$refs && (vm.$refs.AppConfirm || vm.$refs.appConfirm);
+    if (viaPage) return viaPage;
+  } catch (e) {}
+
+  // H5 兜底:程序化创建并挂到 body
+  if (singletonVm) return singletonVm
+  const ConfirmCtor = Vue.extend(AppConfirm)
+  const vm = new ConfirmCtor()
+  vm.$mount()
+  if (typeof document !== 'undefined' && document.body) {
+    document.body.appendChild(vm.$el)
+  }
+  singletonVm = vm
+  return vm
+}
+
+/**
+ * 显示确认弹窗
+ * @param {Object} options - 配置
+ * @param {string} options.title - 标题
+ * @param {string} options.content - 内容
+ * @param {string} options.confirmText - 确认按钮文案
+ * @param {string} options.cancelText - 取消按钮文案
+ * @param {('default'|'danger')} options.type - 风格
+ * @returns {Promise<boolean>} - 用户是否确认
+ */
+export function confirm(options = {}) {
+  const vm = getInstance()
+  return vm.open(options)
+}
+
+export default { confirm }
+
+

+ 95 - 0
utils/device.js

@@ -0,0 +1,95 @@
+// 获取设备唯一标识(优先 IMEI,退化到 uuid)
+// 仅在 APP-PLUS 生效;其他平台返回 null
+export function getDeviceImeiOrUuid() {
+  return new Promise((resolve) => {
+    try {
+      // 规范化设备标识:处理逗号/分隔符/重复等,返回首个有效数字串
+      function normalizeDeviceCode(raw) {
+        try {
+          if (!raw) return null;
+          const text = String(raw).trim();
+          // 拆分出所有数字片段(过滤空串)
+          const parts = text.split(/[^0-9]+/).filter(Boolean);
+          if (parts.length === 0) return null;
+          // 去重并按长度排序,优先选择更长的数字串
+          const uniq = [];
+          for (const p of parts) {
+            if (!uniq.includes(p)) uniq.push(p);
+          }
+          uniq.sort((a, b) => b.length - a.length);
+          // 选择第一个长度足够的候选(常见 IMEI 为15位,这里放宽到>=10)
+          const candidate = uniq.find((v) => v.length >= 10) || uniq[0];
+          return candidate || null;
+        } catch (e) {
+          return raw ? String(raw) : null;
+        }
+      }
+      // H5 平台:从 URL 参数读取 imei(用于调试或外部传入)
+      // 不再依赖任何 debug 模块或本地开关
+      // #ifdef H5
+      try {
+        if (typeof window !== 'undefined') {
+          const params = new URLSearchParams(window.location.search || '');
+          const urlImei = params.get('imei');
+          if (urlImei) {
+            const normalized = normalizeDeviceCode(urlImei);
+            console.log('Device.js H5 从URL获取IMEI:', urlImei, '=>', normalized);
+            resolve(normalized ? String(normalized) : null);
+            return;
+          }
+        }
+      } catch (e) {}
+      // #endif
+      // #ifdef APP-PLUS
+      // 优先尝试异步获取 IMEI
+      if (typeof plus !== 'undefined' && plus.device && typeof plus.device.getInfo === 'function') {
+        plus.device.getInfo({
+          success: (info) => {
+            // info 结构按 5+ 文档约定,可能包含 imei/uuid 等
+            if (info && info.imei) {
+              const normalized = normalizeDeviceCode(info.imei);
+              resolve(normalized ? String(normalized) : null);
+              return;
+            }
+            // 退化到 uuid
+            if (plus.device && plus.device.uuid) {
+              resolve(String(plus.device.uuid));
+              return;
+            }
+            resolve(null);
+          },
+          fail: () => {
+            // 失败后退化到同步获取
+            try {
+              if (plus.device && plus.device.imei) {
+                const normalized = normalizeDeviceCode(plus.device.imei);
+                resolve(normalized ? String(normalized) : null);
+                return;
+              }
+              if (plus.device && plus.device.uuid) {
+                resolve(String(plus.device.uuid));
+                return;
+              }
+            } catch (e) {}
+            resolve(null);
+          }
+        });
+        return;
+      }
+      // 没有 getInfo,尝试直接读取
+      if (plus.device && plus.device.imei) {
+        resolve(String(plus.device.imei));
+        return;
+      }
+      if (plus.device && plus.device.uuid) {
+        resolve(String(plus.device.uuid));
+        return;
+      }
+      // #endif
+    } catch (e) {}
+    console.log('Device.js H5环境返回null (非APP-PLUS且非debug模式)');
+    resolve(null);
+  });
+}
+
+

+ 82 - 0
utils/markdown.js

@@ -0,0 +1,82 @@
+/**
+ * Markdown 转 HTML(优先使用 marked,缺省时使用简化解析)
+ * 所有代码注释使用中文
+ */
+
+// 简单的 HTML 转义
+function escapeHtml(input) {
+    if (!input) return '';
+    return input
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/"/g, '&quot;')
+        .replace(/'/g, '&#39;');
+}
+
+// 简化版 Markdown 解析(支持标题、加粗、斜体、行内代码、代码块、换行)
+function simpleMarkdown(input) {
+    if (!input) return '';
+
+    // 提前提取 ```code``` 代码块,避免被后续规则影响
+    const codeBlocks = [];
+    let text = input.replace(/```([\s\S]*?)```/g, (m, g1) => {
+        const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`;
+        codeBlocks.push(`<pre><code>${escapeHtml(g1)}</code></pre>`);
+        return placeholder;
+    });
+
+    // 转义剩余文本
+    text = escapeHtml(text);
+
+    // 标题(支持 # 到 ######)
+    text = text.replace(/^######\s+(.*)$/gm, '<h6>$1</h6>')
+               .replace(/^#####\s+(.*)$/gm, '<h5>$1</h5>')
+               .replace(/^####\s+(.*)$/gm, '<h4>$1</h4>')
+               .replace(/^###\s+(.*)$/gm, '<h3>$1</h3>')
+               .replace(/^##\s+(.*)$/gm, '<h2>$1</h2>')
+               .replace(/^#\s+(.*)$/gm, '<h1>$1</h1>');
+
+    // 加粗与斜体
+    text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
+               .replace(/\*(.+?)\*/g, '<em>$1</em>');
+
+    // 行内代码
+    text = text.replace(/`([^`]+?)`/g, (m, g1) => `<code>${g1}</code>`);
+
+    // 换行
+    text = text.replace(/\n/g, '<br/>');
+
+    // 还原代码块占位符
+    codeBlocks.forEach((html, idx) => {
+        const placeholder = new RegExp(`__CODE_BLOCK_${idx}__`, 'g');
+        text = text.replace(placeholder, html);
+    });
+
+    return text;
+}
+
+// 尝试使用 marked
+export function markdownToHtml(input) {
+    if (!input) return '';
+    try {
+        // 某些运行环境可能不支持 require,这里用可选方案
+        let markedLib = null;
+        if (typeof require !== 'undefined') {
+            try { markedLib = require('marked'); } catch (e) { markedLib = null; }
+        }
+        if (markedLib && markedLib.parse) {
+            return markedLib.parse(input);
+        }
+    } catch (e) {
+        // 忽略,使用简化解析
+    }
+    return simpleMarkdown(input);
+}
+
+export default {
+    markdownToHtml
+};
+
+
+

+ 2 - 3
utils/request.js

@@ -5,10 +5,9 @@
  * 2、默认loading,不需要的传 additional.needLoading = false。
  */
 
-import { URL } from "./requestUrl";
+import { getURL } from "./requestUrl";
 import { stringify } from 'qs';
 
-const BASE_URL = `http://${URL}`;
 let AllRequestNum = 0; // 存放请求数,以保证有请求未返回就持续loading
 
 /**
@@ -50,7 +49,7 @@ function request(url, options, additional) {
 
   const token = uni.getStorageSync('token');
   return uni.request({
-    url: `${BASE_URL}${url}`,
+    url: `${getURL()}${url}`,
     ...options,
     header: {
       token: token ? token : null,

+ 56 - 22
utils/requestUrl.js

@@ -1,8 +1,8 @@
 /*
  * @Author: your name
  * @Date: 2021-04-19 15:46:08
- * @LastEditTime: 2021-04-19 15:49:28
- * @LastEditors: Please set LastEditors
+ * @LastEditTime: 2025-12-02 17:00:17
+ * @LastEditors: xieyunhui awesomedema@gmail.com
  * @Description: In User Settings Edit
  * @FilePath: /web_TracerMethodology/utils/requestUrl.js
  */
@@ -10,26 +10,60 @@
 
 export const networkType = 1; //网络类型:内网=0,外网=1
 
-// export const URL = '192.168.3.28:8801/imed/pfm/'; // 本地
-// export const URL = '112.124.59.133:8802/imed/pfm/'; //线上测试
-// export const URL = '118.31.245.65:8086/imed/pfm/'; //65测试环境
-// export const URL = '118.31.245.65:8802/imed/pfm/'; //线上
-// export const URL = '112.12.21.134:8111/imed/pfm/'; //横店
-  // export const URL = '47.97.190.5:8086/imed/pfm/'; 
-  
- export const URL = '47.96.149.190:8802/imed/pfm/'; //演示环境
-//export const URL = '120.27.235.181:8802/imed/pfm/'; //开发环境
-//export const URL = '47.97.190.5:8802/imed/pfm/'; //正式环境
-//export const URL = '47.97.190.5:8086/imed/pfm/'; //正式环境dev
-
-// export const URL = '192.168.1.253:8111/imed/pfm/';
-// export const URL = '192.168.1.45:8088/imed/pfm/'; //内网
-// export const URL = 's1.nsloop.com:5137/imed/pfm/';  // 外网
-// export const URL = '121.43.139.179:8801/imed/pfm/';  // 云端服务1
-// export const URL = '172.18.116.20:8801/imed/pfm/';  // 云端服务2
-
-// export const URL = '172.18.116.20:9002/imed/pfm/';  // 云端服务2
+// 运行时可更新的网关域名,由授权查询接口返回的 region 动态设置
+export function setRuntimeRegionDomain(region) {
+  try {
+    if (region && typeof region === 'string') {
+      uni.setStorageSync('runtimeRegion', region);
+    }
+  } catch (e) {}
+}
+
+export function getRuntimeRegionDomain() {
+  try {
+    const r = uni.getStorageSync('runtimeRegion');
+    return r || '';
+  } catch (e) {
+    return '';
+  }
+}
+
+// 计算当前基础URL(无兜底,必须授权设置后才可用)
+export function getBaseURL() {
+  // #ifdef H5
+  try {
+    if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') {
+      // H5 生产:直连真实网关
+      return 'http://47.97.190.5:8806/imed/pfm/';
+	  // return 'http://120.27.235.181:8806/imed/pfm/';
+    // return 'http://120.27.235.181:8802/imed/pfm/';
+    }
+  } catch (e) {}
+  // H5 开发:走本地代理前缀(由 devServer 重写到 /imed/pfm)
+  return '/TracerMethodology/api/';
+  // #endif 
+
+  // 非 H5(例如 APP)走授权下发的网关域名
+  const region = getRuntimeRegionDomain();
+  if (!region) {
+    try { uni.showToast({ title: '接口域名缺失,请重新授权', icon: 'none' }); } catch (e) {}
+    throw new Error('Missing runtime region domain');
+  }
+  const hasProtocol = /^https?:\/\//i.test(region);
+  // 默认拼接 /imed/pfm/ 路径,可按需调整
+  const origin = hasProtocol ? region : `http://${region}`;
+  const normalizedOrigin = origin.replace(/\/$/, '');
+  return `${normalizedOrigin}/imed/pfm/`;
+}
+
+// 兼容旧导出:提供一个按需计算的 URL 常量访问器
+export function getURL() { return getBaseURL(); } 
+
+// WebSocket地址生成:基于当前HTTP基础地址替换协议
 export const wsURL = (hiId, user, permission) => {
-	return `ws://${URL}websocket/${hiId}/${user}/${permission}`;
+  const httpBase = getBaseURL();
+  const wsBase = httpBase.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:');
+  return `${wsBase}websocket/${hiId}/${user}/${permission}`;
 }
 
+		

+ 3 - 3
utils/uploadImg.js

@@ -1,4 +1,4 @@
-import { URL } from "./requestUrl.js";
+import { getURL } from "./requestUrl.js";
 
 // 上传图片方法(单张/多张上传)
 
@@ -10,7 +10,7 @@ const uploadImage = (params) => {
 			uploads[i] = new Promise((resolve) => {
 				// console.log({URL});
 				uni.uploadFile({
-				  url: `http://${URL}/file/uploadImg`,
+				  url: `${getURL()}file/uploadImg`,
 				  fileType: 'image',
 					filePath: item,
 					name: 'file',
@@ -19,7 +19,7 @@ const uploadImage = (params) => {
 					},
 					header:{
 						token: uni.getStorageSync('token')
-          },
+		      },
 					success(res) {
 						// console.log('1122',JSON.parse(res.data));
 						let data = JSON.parse(res.data);

BIN
uview-ui/.DS_Store