wzx hace 11 meses
padre
commit
9284a20ff8
Se han modificado 70 ficheros con 10525 adiciones y 1850 borrados
  1. 16 2
      card/common/api.js
  2. 138 3
      card/common/cardfunc.js
  3. 21 5
      card/common/define.js
  4. 549 537
      card/common/tools.js
  5. 18 3
      card/components/my-pathList/my-pathList.vue
  6. 59 30
      card/components/my-popup/my-popup.vue
  7. 4 2
      card/components/my-ranklist/my-ranklist.vue
  8. 26 16
      card/main.js
  9. 2 2
      card/manifest.json
  10. 34 0
      card/pages.json
  11. 7 2
      card/pages/achievement/index2.vue
  12. 252 0
      card/pages/app/info/personalInfo.vue
  13. 148 0
      card/pages/app/info/thirdPartyInfo.vue
  14. 1 1
      card/pages/exchange/style1/goodsDetail.vue
  15. 50 0
      card/pages/game/grid/cardconfig/test.js
  16. 560 0
      card/pages/game/grid/grid.vue
  17. 277 0
      card/pages/game/grid/index.vue
  18. 50 0
      card/pages/index/index.vue
  19. 34 0
      card/pages/mytz/cardconfig/test.js
  20. 2 4
      card/pages/mytz/index.vue
  21. 443 0
      card/pages/mytz/rankList.vue
  22. 127 0
      card/pages/tpl/style1/cardconfig/pattern1.js
  23. 90 0
      card/pages/tpl/style1/cardconfig/test_user.js
  24. 69 33
      card/pages/tpl/style1/index.vue
  25. 704 665
      card/pages/tpl/style1/rankList.vue
  26. 573 539
      card/pages/tpl/style1/rankOverview.vue
  27. 45 6
      card/pages/tpl/style1/signup.vue
  28. 1 0
      card/pages/tpl/style3/index.vue
  29. BIN
      card/static/backgroud/grid4_mask.png
  30. BIN
      card/static/backgroud/grid9_mask.png
  31. BIN
      card/static/backgroud/grid9_mask2.png
  32. BIN
      card/static/backgroud/grid_bg.jpg
  33. BIN
      card/static/banner/banner1.png
  34. BIN
      card/static/common/jbbs3.png
  35. BIN
      card/static/ecert/ecert_tpl.jpg
  36. BIN
      card/static/logo/building2.png
  37. BIN
      card/static/logo/park.png
  38. 168 0
      card/uni_modules/uni-datetime-picker/changelog.md
  39. 177 0
      card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue
  40. 947 0
      card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue
  41. 22 0
      card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json
  42. 8 0
      card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js
  43. 22 0
      card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json
  44. 22 0
      card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json
  45. 940 0
      card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue
  46. 1064 0
      card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue
  47. 421 0
      card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js
  48. 88 0
      card/uni_modules/uni-datetime-picker/package.json
  49. 21 0
      card/uni_modules/uni-datetime-picker/readme.md
  50. 17 0
      card/uni_modules/uni-link/changelog.md
  51. 128 0
      card/uni_modules/uni-link/components/uni-link/uni-link.vue
  52. 87 0
      card/uni_modules/uni-link/package.json
  53. 11 0
      card/uni_modules/uni-link/readme.md
  54. 33 0
      card/uni_modules/uni-table/changelog.md
  55. 460 0
      card/uni_modules/uni-table/components/uni-table/uni-table.vue
  56. 34 0
      card/uni_modules/uni-table/components/uni-tbody/uni-tbody.vue
  57. 95 0
      card/uni_modules/uni-table/components/uni-td/uni-td.vue
  58. 511 0
      card/uni_modules/uni-table/components/uni-th/filter-dropdown.vue
  59. 295 0
      card/uni_modules/uni-table/components/uni-th/uni-th.vue
  60. 137 0
      card/uni_modules/uni-table/components/uni-thead/uni-thead.vue
  61. 179 0
      card/uni_modules/uni-table/components/uni-tr/table-checkbox.vue
  62. 184 0
      card/uni_modules/uni-table/components/uni-tr/uni-tr.vue
  63. 9 0
      card/uni_modules/uni-table/i18n/en.json
  64. 9 0
      card/uni_modules/uni-table/i18n/es.json
  65. 9 0
      card/uni_modules/uni-table/i18n/fr.json
  66. 12 0
      card/uni_modules/uni-table/i18n/index.js
  67. 9 0
      card/uni_modules/uni-table/i18n/zh-Hans.json
  68. 9 0
      card/uni_modules/uni-table/i18n/zh-Hant.json
  69. 84 0
      card/uni_modules/uni-table/package.json
  70. 13 0
      card/uni_modules/uni-table/readme.md

+ 16 - 2
card/common/api.js

@@ -3,8 +3,10 @@ export const apiServer = process.env.API_BASE_URL;
 // console.log("ossUrl", ossUrl);
 // console.log("apiServer", apiServer);
 
-export const token = '';
-// export const token = '96ba3c924394934f7d30fa869a94ce0d';
+// export const token = '';
+export const token = '96ba3c924394934f7d30fa869a94ce0d';
+// export const token = '9db42d9fe9c9635c85e6fc04f08e898f';
+// export const token = '39de263745caccbb183703987b1c21eb';
 // export const token = 'd4dd6b57a15b4abaccf6cb6adcd4fd44';
 
 // 卡片基本信息查询
@@ -43,9 +45,15 @@ export const apiCurrentMonthlyChallengeQuery = apiServer + 'CurrentMonthlyChalle
 // 卡片配置信息查询
 export const apiCardConfigQuery = apiServer + 'CardConfigQuery';
 
+// 用户自定义配置信息查询
+export const apiUserConfigQuery = apiServer + 'UserConfigQuery';
+
 // 玩家所有月挑战记录查询
 export const apiMonthlyChallengeQuery = apiServer + 'MonthlyChallengeQuery';
 
+// 月挑战排名查询
+export const apiMonthRankDetailQuery = apiServer + 'MonthRankDetailQuery';
+
 // 玩家活动成就查询
 export const apiAchievementQuery = apiServer + 'AchievementQuery';
 
@@ -91,6 +99,12 @@ export const apiCanExchangeGoodsDetail = apiServer + 'CanExchangeGoodsDetail';
 // 积分兑换商品
 export const apiScoreExchangeGoods = apiServer + 'ScoreExchangeGoods';
 
+// 用户基本信息查询
+export const apiUserBasicInformationQuery = apiServer + 'UserBasicInformationQuery';
+
+// 网格卡片信息查询
+export const apiGridsQuery = apiServer + 'GridsQuery';
+
 
 
 import tools from '/common/tools';

+ 138 - 3
card/common/cardfunc.js

@@ -1,6 +1,7 @@
 import tools from '/common/tools';
 import {
 	apiCardConfigQuery,
+	apiUserConfigQuery,
 	apiWarnMessageQuery,
 	apiUnReadMessageQuery,
 	apiIsNewUserInCardComp,
@@ -9,7 +10,8 @@ import {
 
 import {
 	defaultPopUpDataList,
-	defaultPopUpDataList2
+	defaultPopUpDataList2,
+	defaultPopUpDataList3
 } from '/common/define';
 
 var cardfunc = {
@@ -37,6 +39,10 @@ var cardfunc = {
 		popupWarnList: [], // 警告弹窗数据
 	},
 	
+	userConfigData: {
+		
+	},
+	
 	init(caller, token, ecId) {
 		this.caller = caller;
 		this.token = token;
@@ -48,6 +54,7 @@ var cardfunc = {
 	removeCss() {
 		tools.removeCssCode("css-common");
 		tools.removeCssCode("css-custom");
+		tools.removeCssCode("css-user");
 	},
 
 	getCardConfig(loadConfig, testconfig) {
@@ -62,6 +69,18 @@ var cardfunc = {
 		}
 	},
 
+	getUserConfig(loadConfig, testconfig) {
+		const cardconfigType = getApp().$cardconfigType;
+		// console.log("[getConfig] cardconfigType:", cardconfigType);
+
+		if (cardconfigType == "local") {
+			loadConfig(testconfig);
+			// this.testCardConfig(testconfig, loadConfig);
+		} else {
+			this.userConfigQuery(loadConfig);
+		}
+	},
+	
 	parseCardConfig(cardconfig) {
 		// console.log("[parseCardConfig] cardconfig:", cardconfig);
 		if (cardconfig == undefined || cardconfig == "") {
@@ -134,6 +153,10 @@ var cardfunc = {
 					for (var j = 0; j < defaultPopUpDataList2.length; j++) {
 						this.cardConfigData.popupRuleList.push(defaultPopUpDataList2[j]);
 					}
+				} else if (popupRuleList[i] == 'default3') {
+					for (var j = 0; j < defaultPopUpDataList3.length; j++) {
+						this.cardConfigData.popupRuleList.push(defaultPopUpDataList3[j]);
+					}
 				} else {
 					this.cardConfigData.popupRuleList.push(popupRuleList[i]);
 				}
@@ -164,6 +187,92 @@ var cardfunc = {
 		}
 		// console.log("[loadCardCommonConfig] cardConfigData:", this.cardConfigData);
 	},
+	
+	// 加载用户的弹窗数据
+	loadUserPopupRule(config) {
+		const tplInfo = config.tplInfo;
+		const matchInfo = config.matchInfo;
+		if (matchInfo) {
+			let hint = "<span style='color:#FF5E00;'>参赛要求</span><br>";
+			hint += "① 赛事以自身安全为最高要求,请正确评估自身健康,切勿超负荷运动,适时参赛<br>② 参赛人群建议:6-60岁健康居民<br>";
+			hint += "<br><span style='color:#FF5E00;'>安全提醒</span><br>";
+			hint += "① 请着运动服及运动鞋<br>② 避免聚集、分散参与<br>③ 及时增减衣物,预防感冒<br>④ 注意交通安全与自身安全";
+			const contact = `联系人:${matchInfo.contactName} &nbsp;&nbsp; 电话:<a href='tel:${matchInfo.phone}' style='color: #ff5500;'>${matchInfo.phone}</a>`;
+			const content = `${hint}<br><br>${contact}`;
+			// const content = `${matchInfo.description}<br><br>${contact}`;
+			const popupRule = [{
+				"type": 1,
+				"data": {
+					"title": matchInfo.compName,
+					"logo": {
+						"src": tplInfo.matchLogo,
+						"width": "260px",
+						 "height": "90px"
+					},
+					"content": content
+				}
+			}];
+			this.cardConfigData.popupRuleList.unshift(...popupRule);
+		}
+	},
+	
+	// 获取用户的比赛路线数据
+	getUserPathList(config) {
+		const mapInfo = config.mapInfo;
+		const mapNum = mapInfo.length;
+		let pathList = {};
+		if (mapNum > 0) {
+			let activityList = [];
+			// 将多地图的路线信息数组合并成一个数组
+			for (var m = 0; m < mapNum; m++) {
+				activityList.push(...mapInfo[m].activityList);
+			}
+			console.log("[getUserPathList] activityList:", activityList);
+			const activityNum = activityList.length;
+			let type = 4;
+			let navImg = "/static/common/nav3.png";
+			if (activityNum > 1) {
+				type = 3;
+				navImg = "/static/common/nav.png";
+			}
+			if (activityNum > 0) {
+				const rowSize = 2; // 每行显示的路线数量
+				const rowNum = Math.ceil(activityNum / rowSize);
+				for (var i = 0; i < rowNum; i++) {
+					let row = [];
+					for (var j = 0; j < rowSize; j++) {
+						// console.log(`[getUserPathList] i: ${i} j: ${j}`);
+						const activity = activityList[i * rowSize + j];
+						if (!activity) {
+							break;
+						}
+						const path = {
+							"type": type,
+							"pathName": activity.showName,
+							"pathImg": activity.pathImg,
+							"path": {
+								"ocaId": activity.ocaId,
+								"mcType": activity.matchType
+							},
+							"navImg": navImg,
+							"point": {
+								"longitude": activity.point.longitude,
+								"latitude": activity.point.latitude,
+								"name": activity.point.name
+							}
+						};
+						// console.log(`[getUserPathList] i: ${i} j: ${j} path: ${JSON.stringify(path)}`);
+						row.push(path);
+					}
+					pathList["row" + (i + 1)] = row;
+					// console.log("[getUserPathList] row:", row);
+				}
+			}
+		} else {
+			console.warn("[getUserPathList] mapInfo err:", mapInfo);
+		}
+		return pathList;
+	},
 
 	// 卡片配置信息查询
 	cardConfigQuery(callback) {
@@ -179,18 +288,44 @@ var cardfunc = {
 				pageName: "all"
 			},
 			success: (res) => {
-				// console.log("[cardConfigQuery]", res);
+				console.log("[cardConfigQuery]", res);
 				const data = res.data.data;
 				const config = data.configJson;
 				// console.log("[cardConfigQuery] config", config);
 				callback(config);
 			},
 			fail: (err) => {
-				console.log("[cardConfigQuery] err", err)
+				console.log("[cardConfigQuery] err", err);
 			},
 		});
 	},
 
+	// 用户自定义配置信息查询
+	userConfigQuery(callback) {
+		uni.request({
+			url: apiUserConfigQuery,
+			header: {
+				"Content-Type": "application/x-www-form-urlencoded",
+				"token": this.token,
+			},
+			method: "POST",
+			data: {
+				ecId: this.ecId,
+				pageName: "all"
+			},
+			success: (res) => {
+				// console.log("[userConfigQuery]", res);
+				const data = res.data.data;
+				const config = data.configJson;
+				// console.log("[userConfigQuery] config", config);
+				callback(config);
+			},
+			fail: (err) => {
+				console.log("[userConfigQuery] err", err);
+			},
+		});
+	},
+	
 	// 警告列表查询
 	warnMessageQuery(callback=null) {
 		uni.request({

+ 21 - 5
card/common/define.js

@@ -1,3 +1,8 @@
+export const tplStyleList = [];
+tplStyleList[0] = 'blue';
+tplStyleList[1] = 'orange';
+tplStyleList[2] = 'green';
+
 export const teamName = [];
 
 teamName[0] = [];
@@ -12,14 +17,13 @@ teamName[1][0] = '不组队';
 teamName[1][1] = '学生队';
 teamName[1][2] = '家长队';
 
-export const defaultPopUpDataList = [
-	{
+export const defaultPopUpDataList = [{
 		type: 2,
 		data: {
 			title: "活动流程",
 			img: "/static/common/hdlc.png",
 		}
-	}, 
+	},
 	{
 		type: 2,
 		data: {
@@ -29,8 +33,7 @@ export const defaultPopUpDataList = [
 	}
 ];
 
-export const defaultPopUpDataList2 = [
-	{
+export const defaultPopUpDataList2 = [{
 		"type": 7,
 		"data": {
 			"title": "基本标识",
@@ -55,3 +58,16 @@ export const defaultPopUpDataList2 = [
 		}
 	}
 ];
+
+export const defaultPopUpDataList3 = [{
+		"type": 7,
+		"data": {
+			"title": "基本标识及图例",
+			"logo": {
+				"src": "/static/common/jbbs3.png",
+				"width": "300px",
+				"height": "380px"
+			}
+		}
+	}
+];

+ 549 - 537
card/common/tools.js

@@ -1,538 +1,550 @@
-var tools = {
-	
-	// 对url追加项目版本号,用于页面更新后用户端的强制刷新
-	// 每次页面变更必须更新项目版本号
-	urlAddVer(url) {
-		let newUrl = url;
-		
-		// 获取当前app的版本
-		const systemInfo = uni.getSystemInfoSync();
-		
-		// #ifdef H5
-		const version_number = systemInfo.appVersion;
-		// console.log('版本号:', version_number);
-		
-		if (newUrl.indexOf('_v=') !== -1) {
-			return newUrl;
-		}
-		
-		if (newUrl.indexOf('?') !== -1) {
-			newUrl += "&_v=" + version_number;
-		} else {
-			newUrl += "?_v=" + version_number;
-		}
-		// #endif
-		
-		console.log("[urlAddVer] newUrl", newUrl);
-		return newUrl;
-	},
-	
-	// 导航到彩图奔跑APP内的某个页面或执行APP内部的某些功能
-	appAction(url, actType="") {
-		console.log("appAction", url);
-		// console.log("getApp", getApp());
-		// getApp().$audio.destroy();
-		// getApp().$audio.pause();
-		
-		if (url.indexOf('http') !== -1) {	// http 或 https 开头的网址
-			window.location.href = this.urlAddVer(url);
-		} else if (url == "reload") {
-			window.location.reload();
-		} else if (actType == "uni.navigateTo") {
-			uni.navigateTo({
-				url: this.urlAddVer(url)
-			});
-		} else {
-			window.location.href = url;
-		}
-	},
-	
-	// 格式化赛事时间 09-09 16:48
-	fmtMcTime(timestamp) {
		var date = new Date(timestamp * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
-		// var Y = date.getFullYear() + '-';
-		var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
-		var D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' ';
-		var h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':';
-		var m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes());
-		// var s = (date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds());
-
-		const timeStr = M + D + h + m;
-		// console.log("timeStr", timeStr);
-		return timeStr;
-	},
-	
-	// 获取活动时间 09-09 16:48 至 09-30 16:48
-	getActtime(beginSecond, endSecond) {
-		const acttime = this.fmtMcTime(beginSecond) + " 至 " + this.fmtMcTime(endSecond);
-		// console.log("acttime:", acttime);
-		return acttime;
-	},
-	
-	// 格式化赛事时间 2024.9.9-30 2024.9.9-10.30 2024.9.9-2025.10.30
-	fmtMcTime2(timestamp1, timestamp2) {
-		const date1 = new Date(timestamp1 * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
-		const date2 = new Date(timestamp2 * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
-	
-		const Y1 = date1.getFullYear();
-		const Y2 = date2.getFullYear();
-		const M1 = date1.getMonth() + 1;
-		const M2 = date2.getMonth() + 1;
-		const D1 = date1.getDate();
-		const D2 = date2.getDate();
-	
-		var timeStr1 = Y1 + '.' + M1 + '.' + D1;
-		var timeStr2 = '';
-	
-		if (Y2 != Y1) {
-			timeStr2 += Y2 + '.' + M2 + '.' + D2;
-		} else if (M2 != M1) {
-			timeStr2 += M2 + '.' + D2;
-		} else if (D2 != D1) {
-			timeStr2 += D2;
-		}
-	
-		var timeStr = timeStr1;
-		if (timeStr2.length > 0) {
-			timeStr += '-' + timeStr2;
-		}
-		// console.log("timeStr", timeStr);
-		return timeStr;
-	},
-	
-	// 格式化赛事时间 2024年9月9日 至 9月12日 2024年12月9日 至 2025年1月6日
-	fmtMcTime3(timestamp1, timestamp2) {
-		const date1 = new Date(timestamp1 * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
-		const date2 = new Date(timestamp2 * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
-	
-		const Y1 = date1.getFullYear();
-		const Y2 = date2.getFullYear();
-		const M1 = date1.getMonth() + 1;
-		const M2 = date2.getMonth() + 1;
-		const D1 = date1.getDate();
-		const D2 = date2.getDate();
-	
-		var timeStr1 = Y1 + '年' + M1 + '月' + D1 + '日';
-		var timeStr2 = '';
-	
-		if (Y2 != Y1) {
-			timeStr2 += Y2 + '年' + M2 + '月' + D2 + '日';
-		} else {
-			timeStr2 += M2 + '月' + D2 + '日';
-		}
-	
-		var timeStr = timeStr1;
-		if (timeStr2.length > 0) {
-			timeStr += ' 至 ' + timeStr2;
-		}
-		// console.log("timeStr", timeStr);
-		return timeStr;
-	},
-	
-	// 判断赛事/活动状态 0: 未开始  1: 进行中  2: 已结束
-	checkMcState(beginSecond, endSecond) {
-		let mcState = 0;	// 未开始
-		if (beginSecond > 0 && endSecond > 0) {
-			const now = Date.now() / 1000;
-			const dif1 = beginSecond - now;
-			const dif2 = endSecond - now;
-			// const dif = 3600*24 - 60;
-			if (dif1 > 0) {
-				console.log("活动未开始");
-				mcState = 0;	// 未开始
-			} else if (dif2 > 0) {
-				console.log("活动进行中");
-				mcState = 1;	// 进行中
-			} else {
-				console.log("活动已结束");
-				mcState = 2;	// 已结束
-			}
-		}
-		return mcState;
-	},
-
-	// 动态创建<style>标签,将CSS代码插入到文档中
-	loadCssCode(cssCode, styleId="css-custom") {
-		this.removeCssCode(styleId);
-		
-		// const styleId = "css-custom";
-		var style = window.document.createElement("style");
-		style.type = "text/css";
-		style.id = styleId;
-		if (style.styleSheet) {
-			// This is required for IE8 and below.
-			style.styleSheet.cssText = cssCode;
-		} else {
-			style.appendChild(document.createTextNode(cssCode));
-		}
-		document.getElementsByTagName("head")[0].appendChild(style);
-		// console.log("head:", document.getElementsByTagName("head")[0]);
-		// console.log("head:", document.getElementById(styleId));
-		// console.log("style:", style);
-	},
-	
-	// 删除之前动态创建的<style>标签
-	removeCssCode(styleId="css-custom") {
-		// const styleId = "css-custom";
-		var oldCss = document.getElementById(styleId);
-		// console.log("oldCss:", oldCss);
-		if (oldCss != null) {
-			document.getElementsByTagName("head")[0].removeChild(oldCss);
-			console.log(styleId + " 已移除");
-		}
-	},
-
-	// uni-data-select 组件,根据选中的值获取对应的文本
-	getSelectedText(obj, value) {
-		const selectedOption = obj.find(option => option.value === value);
-		return selectedOption ? selectedOption.text : '';
-	},
-
-	objectToQueryString(obj) {
-		return Object.keys(obj).map(k => k + '=' + obj[k]).join('&');
-	},
-
-	// 秒数转换成 XX天XX小时
-	convertSecondsToDHM(seconds) {
-		var days = Math.floor(seconds / (3600 * 24));
-		var hours = Math.floor((seconds % (3600 * 24)) / 3600);
-		var minutes = Math.floor((seconds % (3600 * 24)) % 3600 / 60);
-		if (days > 0)
-			// return `${days.toString().padStart(2, '0')}天${hours.toString().padStart(2, '0')}小时`;
-			return `${days}天${hours.toString().padStart(2, '0')}小时`;
-		else
-			return `${hours.toString().padStart(2, '0')}小时${minutes.toString().padStart(2, '0')}分钟`;
-	},
-
-	// 秒数转换成时分秒
-	// style:  0 [01:02:03]  1 [1h:02'3"]
-	convertSecondsToHMS(seconds, style = 0) {
-		if (!(seconds > 0)) {
-			return '--';
-		}
-		var hours = Math.floor(seconds / 3600);
-		var minutes = Math.floor((seconds % 3600) / 60);
-		var remainingSeconds = Math.floor(seconds % 60);
-		// return hours + ":" + minutes + ":" + remainingSeconds;
-		if (style == 0)
-			return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
-		else if (style == 1) {
-			if (hours > 0)
-				return `${hours}h${minutes}′${remainingSeconds.toString().padStart(2, '0')}″`;
-			else
-				return `${minutes}′${remainingSeconds.toString().padStart(2, '0')}″`;
-		}
-		else if (style == 2) {
-			if (hours > 0)
-				return `${hours*60+minutes}′${remainingSeconds.toString().padStart(2, '0')}″`;
-			else
-				return `${minutes}′${remainingSeconds.toString().padStart(2, '0')}″`;
-		}
-	},
-
-	// 计算(中英文混合)字符串长度
-	calStrLen(str) {
-		var length = 0;
-		for (var i = 0; i < str.length; i++) {
-			// 将字符转换为 Unicode 编码
-			var charCode = str.charCodeAt(i);
-			if (charCode >= 0 && charCode <= 128) {
-				length++;
-			} else {
-				length += 2;
-			}
-		}
-
-		return length;
-	},
-
-	// 集合对象去重
-	unique(arr, field) {
-		var map = {};
-		var res = [];
-		for (var i = 0; i < arr.length; i++) {
-			if (!map[arr[i][field]]) {
-				map[arr[i][field]] = 1;
-				res.push(arr[i]);
-			}
-		}
-		return res;
-	},
-
-	// 正则取出html标签
-	repalceHtml(str) {
-		var dd = str.replace(/<\/?.+?>/g, "");
-		var dds = dd.replace(/ /g, ""); //dds为得到后的内容
-		return dds;
-	},
-
-	// 判断身份证号    
-	isSfz(idcard) {
-		var id =
-			/^[1-9][0-9]{5}(19|20)[0-9]{2}((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|31)|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|[1-2][0-9]))[0-9]{3}([0-9]|x|X)$/
-		if (idcard === '') {
-			uni.showToast({
-				title: '请输入身份证号',
-				icon: 'none'
-			})
-		} else if (!id.test(idcard)) {
-			uni.showToast({
-				title: '身份证号格式不正确!',
-				icon: 'none'
-			})
-			return false
-		} else {
-			return false
-		}
-	},
-
-	// 判断是否是手机号   
-	isPhone(val) {
-		var patrn = /^(((1[3456789][0-9]{1})|(15[0-9]{1}))+\d{8})$/
-		if (!patrn.test(val) || val === '') {
-			uni.showToast({
-				title: '手机号格式不正确',
-				icon: 'none'
-			})
-			return false
-		} else {
-			return true
-		}
-	},
-
-	// 判断邮箱
-	isEmail(email) {
-		if (email.search(/^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/) != -1)
-			return true;
-		else
-			return false;
-	},
-
-	//获取随机数
-	getSuiji() {
-		var Range = Max - Min;
-		var Rand = Math.random();
-		return (Min + Math.round(Rand * Range));
-	},
-
-	//计算多长时间前
-	getDateDiff(dateTimeStamp) {
-		var minute = 1000 * 60;
-		var hour = minute * 60;
-		var day = hour * 24;
-		var halfamonth = day * 15;
-		var month = day * 30;
-		var year = day * 365;
-		var now = new Date().getTime();
-		var diffValue = now - dateTimeStamp;
-		if (diffValue < 0) {
-			return;
-		}
-		var yearC = diffValue / year;
-		var monthC = diffValue / month;
-		var weekC = diffValue / (7 * day);
-		var dayC = diffValue / day;
-		var hourC = diffValue / hour;
-		var minC = diffValue / minute;
-		if (yearC >= 1) {
-			result = "" + parseInt(yearC) + "年前";
-		}
-		if (monthC >= 1) {
-			result = "" + parseInt(monthC) + "月前";
-		} else if (weekC >= 1) {
-			result = "" + parseInt(weekC) + "周前";
-		} else if (dayC >= 1) {
-			result = "" + parseInt(dayC) + "天前";
-		} else if (hourC >= 1) {
-			result = "" + parseInt(hourC) + "小时前";
-		} else if (minC >= 1) {
-			result = "" + parseInt(minC) + "分钟前";
-		} else
-			result = "刚刚";
-		return result;
-	},
-
-	// 时间戳转时间
-	timestampToTime(timestamp, i) {
-		var date = null;
-		if (timestamp > 0) {
-			date = new Date(timestamp); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
-		} else {
-			date = new Date();
-		}
-		// console.log(date, timestamp)
-		
-		var Y = date.getFullYear();
-		var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1);
-		var D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate());
-		var h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours());
-		var m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes());
-		var s = (date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds());
-		if (i == 1) {
-			return Y + '-' + M + '-' + D;
-		} else if (i == 2) {
-			return Y + '年' + M + '月' + D + '日';
-		} else if (i == 3) {
-			return Y + '-' + M + '-' + D + ' ' + h + ':' + m;
-		}
-		return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s;
-	},
-
-	// 是否是汉字
-	isHanzi(str) {
-		let reg = /\p{Unified_Ideograph}/ug;
-		return reg.test(str);
-	},
-
-	// 是否是字母数字
-	isStringAndNumber(str) {
-		let regNumber = new RegExp(/^[0-9A-Za-z]+$/);
-		return regNumber.test(str)
-	},
-
-	// var arr3 = [30,10,111,35,1899,50,45];
-	// 集合排序  元素数字
-	listSort(list) {
-		arr3.sort(function(a, b) {
-			return a - b;
-		})
-	},
-
-	// var arr5 = [{id:10},{id:5},{id:6},{id:9},{id:2},{id:3}];
-	// 元素  对象
-	listObjectSort(arr) {
-		arr.sort(function(a, b) {
-			return a.id - b.id
-		})
-		return arr;
-	},
-
-	/*
-	 * 忽略大小写判断字符串是否相同
-	 * @param str1
-	 * @param str2
-	 * @returns {Boolean}
-	 */
-	isEqualsIgnorecase: function(str1, str2) {
-		if (str1.toUpperCase() == str2.toUpperCase()) {
-			return true;
-		} else {
-			return false;
-		}
-	},
-
-	/**
-	 * 判断是否是数字
-	 * @param value
-	 * @returns {Boolean}
-	 */
-	isNum: function(value) {
-		if (value != null && value.length > 0 && isNaN(value) == false) {
-			return true;
-		} else {
-			return false;
-		}
-	},
-
-	/**
-	 * 判断是否是中文
-	 * @param str
-	 * @returns {Boolean}
-	 */
-	isChine: function(str) {
-		var reg = /^([u4E00-u9FA5]|[uFE30-uFFA0])*$/;
-		if (reg.test(str)) {
-			return false;
-		}
-		return true;
-	},
-
-	/*验证是否为图片*/
-	tmCheckImage: function(fileName) {
-		return /(gif|jpg|jpeg|png|GIF|JPG|PNG)$/ig.test(fileName);
-	},
-
-	/*验证是否为视频*/
-	tmCheckVideo: function(fileName) {
-		return /(mp4|mp3|flv|wav)$/ig.test(fileName);
-	},
-
-	/**
-	 * 去除字符串两边的空格
-	 * @param str
-	 * @returns {number|Number}
-	 * 调用方法:var str = utils.trim("abcd")
-	 */
-	trim: function(str) {
-		String.prototype.trim = function() {
-			return str.replace(/(^\s*)|(\s*$)/g, "");
-		}
-	},
-
-	// 判断密码是否符合 至少6位,包括大小写字母、数字、特殊字符
-	isPassword(val) {
-		var reg = /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)])+$)^.{8,16}$/;
-		if (val === '') {
-			uni.showToast({
-				title: '请输入密码',
-				icon: 'none'
-			})
-		} else if (!reg.test(val)) {
-			uni.showToast({
-				title: '至少6位,包括大小写字母、数字、特殊字符',
-				icon: 'none'
-			})
-			return false
-		} else {
-			return true
-		}
-	},
-
-	// 电话匿名
-	noPassByMobile(str) {
-		if (null != str && str != undefined) {
-			var pat = /(\d{3})\d*(\d{4})/;
-			return str.replace(pat, '$1****$2');
-		} else {
-			return "";
-		}
-	},
-
-	// 获取两点间的距离
-	//进行经纬度转换为距离的计算
-	Rad(d) {
-		return d * Math.PI / 180.0; //经纬度转换成三角函数中度分表形式。
-	},
-
-	/*
-	 计算距离,参数分别为第一点的纬度,经度;第二点的纬度,经度
-	 默认单位km
-	*/
-	getMapDistance(lat1, lat2, lng1, lng2) {
-		lat1 = lat1 || 0;
-		lng1 = lng1 || 0;
-		lat2 = lat2 || 0;
-		lng2 = lng2 || 0;
-
-		var rad1 = lat1 * Math.PI / 180.0;
-		var rad2 = lat2 * Math.PI / 180.0;
-		var a = rad1 - rad2;
-		var b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
-		var r = 6378137;
-		var distance = r * 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(rad1) * Math.cos(rad2) *
-			Math.pow(Math.sin(b / 2), 2)));
-		// console.log(lat1, lng1, lat2, lng2);
-		// console.log(distance);
-		return Math.round(distance) / 1000;
-	},
-
-	// 预览图片
-	yulanImg(item) {
-		let arr = [item]
-		uni.previewImage({
-			urls: arr,
-		});
-	},
-
-}
-
+var tools = {
+	
+	// 判断对象数组中指定属性是否有某个值的数据
+	objArrHasValue(objArr, key, value) {
+		// console.log("objArrHasValue", objArr, key, value);
+		return objArr.find(obj => obj[key] === value) !== undefined;
+	},
+
+	// 获取对象数组中指定属性为指定值的对象
+	objArrGetObjByValue(objArr, key, value) {
+		// console.log("objArrGetObjByValue", objArr, key, value);
+		return objArr.find(obj => obj[key] === value);
+	},
+	
+	// 对url追加项目版本号,用于页面更新后用户端的强制刷新
+	// 每次页面变更必须更新项目版本号
+	urlAddVer(url) {
+		let newUrl = url;
+
+		// 获取当前app的版本
+		const systemInfo = uni.getSystemInfoSync();
+
+		// #ifdef H5
+		const version_number = systemInfo.appVersion;
+		// console.log('版本号:', version_number);
+
+		if (newUrl.indexOf('_v=') !== -1) {
+			return newUrl;
+		}
+
+		if (newUrl.indexOf('?') !== -1) {
+			newUrl += "&_v=" + version_number;
+		} else {
+			newUrl += "?_v=" + version_number;
+		}
+		// #endif
+
+		console.log("[urlAddVer] newUrl", newUrl);
+		return newUrl;
+	},
+
+	// 导航到彩图奔跑APP内的某个页面或执行APP内部的某些功能
+	appAction(url, actType = "") {
+		console.log("appAction", url);
+		// console.log("getApp", getApp());
+		// getApp().$audio.destroy();
+		// getApp().$audio.pause();
+
+		if (url.indexOf('http') !== -1) { // http 或 https 开头的网址
+			window.location.href = this.urlAddVer(url);
+		} else if (url == "reload") {
+			window.location.reload();
+		} else if (actType == "uni.navigateTo") {
+			uni.navigateTo({
+				url: this.urlAddVer(url)
+			});
+		} else {
+			window.location.href = url;
+		}
+	},
+
+	// 格式化赛事时间 09-09 16:48
+	fmtMcTime(timestamp) {
+		var date = new Date(timestamp * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
+		// var Y = date.getFullYear() + '-';
+		var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
+		var D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' ';
+		var h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':';
+		var m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes());
+		// var s = (date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds());
+
+		const timeStr = M + D + h + m;
+		// console.log("timeStr", timeStr);
+		return timeStr;
+	},
+
+	// 获取活动时间 09-09 16:48 至 09-30 16:48
+	getActtime(beginSecond, endSecond) {
+		const acttime = this.fmtMcTime(beginSecond) + " 至 " + this.fmtMcTime(endSecond);
+		// console.log("acttime:", acttime);
+		return acttime;
+	},
+
+	// 格式化赛事时间 2024.9.9-30 2024.9.9-10.30 2024.9.9-2025.10.30
+	fmtMcTime2(timestamp1, timestamp2) {
+		const date1 = new Date(timestamp1 * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
+		const date2 = new Date(timestamp2 * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
+
+		const Y1 = date1.getFullYear();
+		const Y2 = date2.getFullYear();
+		const M1 = date1.getMonth() + 1;
+		const M2 = date2.getMonth() + 1;
+		const D1 = date1.getDate();
+		const D2 = date2.getDate();
+
+		var timeStr1 = Y1 + '.' + M1 + '.' + D1;
+		var timeStr2 = '';
+
+		if (Y2 != Y1) {
+			timeStr2 += Y2 + '.' + M2 + '.' + D2;
+		} else if (M2 != M1) {
+			timeStr2 += M2 + '.' + D2;
+		} else if (D2 != D1) {
+			timeStr2 += D2;
+		}
+
+		var timeStr = timeStr1;
+		if (timeStr2.length > 0) {
+			timeStr += '-' + timeStr2;
+		}
+		// console.log("timeStr", timeStr);
+		return timeStr;
+	},
+
+	// 格式化赛事时间 2024年9月9日 至 9月12日 2024年12月9日 至 2025年1月6日
+	fmtMcTime3(timestamp1, timestamp2) {
+		const date1 = new Date(timestamp1 * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
+		const date2 = new Date(timestamp2 * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
+
+		const Y1 = date1.getFullYear();
+		const Y2 = date2.getFullYear();
+		const M1 = date1.getMonth() + 1;
+		const M2 = date2.getMonth() + 1;
+		const D1 = date1.getDate();
+		const D2 = date2.getDate();
+
+		var timeStr1 = Y1 + '年' + M1 + '月' + D1 + '日';
+		var timeStr2 = '';
+
+		if (Y2 != Y1) {
+			timeStr2 += Y2 + '年' + M2 + '月' + D2 + '日';
+		} else {
+			timeStr2 += M2 + '月' + D2 + '日';
+		}
+
+		var timeStr = timeStr1;
+		if (timeStr2.length > 0) {
+			timeStr += ' 至 ' + timeStr2;
+		}
+		// console.log("timeStr", timeStr);
+		return timeStr;
+	},
+
+	// 判断赛事/活动状态 0: 未开始  1: 进行中  2: 已结束
+	checkMcState(beginSecond, endSecond) {
+		let mcState = 0; // 未开始
+		if (beginSecond > 0 && endSecond > 0) {
+			const now = Date.now() / 1000;
+			const dif1 = beginSecond - now;
+			const dif2 = endSecond - now;
+			// const dif = 3600*24 - 60;
+			if (dif1 > 0) {
+				console.log("活动未开始");
+				mcState = 0; // 未开始
+			} else if (dif2 > 0) {
+				console.log("活动进行中");
+				mcState = 1; // 进行中
+			} else {
+				console.log("活动已结束");
+				mcState = 2; // 已结束
+			}
+		}
+		return mcState;
+	},
+
+	// 动态创建<style>标签,将CSS代码插入到文档中
+	loadCssCode(cssCode, styleId = "css-custom") {
+		this.removeCssCode(styleId);
+
+		// const styleId = "css-custom";
+		var style = window.document.createElement("style");
+		style.type = "text/css";
+		style.id = styleId;
+		if (style.styleSheet) {
+			// This is required for IE8 and below.
+			style.styleSheet.cssText = cssCode;
+		} else {
+			style.appendChild(document.createTextNode(cssCode));
+		}
+		document.getElementsByTagName("head")[0].appendChild(style);
+		// console.log("head:", document.getElementsByTagName("head")[0]);
+		// console.log("head:", document.getElementById(styleId));
+		// console.log("style:", style);
+	},
+
+	// 删除之前动态创建的<style>标签
+	removeCssCode(styleId = "css-custom") {
+		// const styleId = "css-custom";
+		var oldCss = document.getElementById(styleId);
+		// console.log("oldCss:", oldCss);
+		if (oldCss != null) {
+			document.getElementsByTagName("head")[0].removeChild(oldCss);
+			console.log(styleId + " 已移除");
+		}
+	},
+
+	// uni-data-select 组件,根据选中的值获取对应的文本
+	getSelectedText(obj, value) {
+		const selectedOption = obj.find(option => option.value === value);
+		return selectedOption ? selectedOption.text : '';
+	},
+
+	objectToQueryString(obj) {
+		return Object.keys(obj).map(k => k + '=' + obj[k]).join('&');
+	},
+
+	// 秒数转换成 XX天XX小时
+	convertSecondsToDHM(seconds) {
+		var days = Math.floor(seconds / (3600 * 24));
+		var hours = Math.floor((seconds % (3600 * 24)) / 3600);
+		var minutes = Math.floor((seconds % (3600 * 24)) % 3600 / 60);
+		if (days > 0)
+			// return `${days.toString().padStart(2, '0')}天${hours.toString().padStart(2, '0')}小时`;
+			return `${days}天${hours.toString().padStart(2, '0')}小时`;
+		else
+			return `${hours.toString().padStart(2, '0')}小时${minutes.toString().padStart(2, '0')}分钟`;
+	},
+
+	// 秒数转换成时分秒
+	// style:  0 [01:02:03]  1 [1h:02'3"]
+	convertSecondsToHMS(seconds, style = 0) {
+		if (!(seconds > 0)) {
+			return '--';
+		}
+		var hours = Math.floor(seconds / 3600);
+		var minutes = Math.floor((seconds % 3600) / 60);
+		var remainingSeconds = Math.floor(seconds % 60);
+		// return hours + ":" + minutes + ":" + remainingSeconds;
+		if (style == 0)
+			return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
+		else if (style == 1) {
+			if (hours > 0)
+				return `${hours}h${minutes}′${remainingSeconds.toString().padStart(2, '0')}″`;
+			else
+				return `${minutes}′${remainingSeconds.toString().padStart(2, '0')}″`;
+		} else if (style == 2) {
+			if (hours > 0)
+				return `${hours*60+minutes}′${remainingSeconds.toString().padStart(2, '0')}″`;
+			else
+				return `${minutes}′${remainingSeconds.toString().padStart(2, '0')}″`;
+		}
+	},
+
+	// 计算(中英文混合)字符串长度
+	calStrLen(str) {
+		var length = 0;
+		for (var i = 0; i < str.length; i++) {
+			// 将字符转换为 Unicode 编码
+			var charCode = str.charCodeAt(i);
+			if (charCode >= 0 && charCode <= 128) {
+				length++;
+			} else {
+				length += 2;
+			}
+		}
+
+		return length;
+	},
+
+	// 集合对象去重
+	unique(arr, field) {
+		var map = {};
+		var res = [];
+		for (var i = 0; i < arr.length; i++) {
+			if (!map[arr[i][field]]) {
+				map[arr[i][field]] = 1;
+				res.push(arr[i]);
+			}
+		}
+		return res;
+	},
+
+	// 正则取出html标签
+	repalceHtml(str) {
+		var dd = str.replace(/<\/?.+?>/g, "");
+		var dds = dd.replace(/ /g, ""); //dds为得到后的内容
+		return dds;
+	},
+
+	// 判断身份证号    
+	isSfz(idcard) {
+		var id =
+			/^[1-9][0-9]{5}(19|20)[0-9]{2}((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|31)|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|[1-2][0-9]))[0-9]{3}([0-9]|x|X)$/
+		if (idcard === '') {
+			uni.showToast({
+				title: '请输入身份证号',
+				icon: 'none'
+			})
+		} else if (!id.test(idcard)) {
+			uni.showToast({
+				title: '身份证号格式不正确!',
+				icon: 'none'
+			})
+			return false
+		} else {
+			return false
+		}
+	},
+
+	// 判断是否是手机号   
+	isPhone(val) {
+		var patrn = /^(((1[3456789][0-9]{1})|(15[0-9]{1}))+\d{8})$/
+		if (!patrn.test(val) || val === '') {
+			uni.showToast({
+				title: '手机号格式不正确',
+				icon: 'none'
+			})
+			return false
+		} else {
+			return true
+		}
+	},
+
+	// 判断邮箱
+	isEmail(email) {
+		if (email.search(/^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/) != -1)
+			return true;
+		else
+			return false;
+	},
+
+	//获取随机数
+	getSuiji() {
+		var Range = Max - Min;
+		var Rand = Math.random();
+		return (Min + Math.round(Rand * Range));
+	},
+
+	//计算多长时间前
+	getDateDiff(dateTimeStamp) {
+		var minute = 1000 * 60;
+		var hour = minute * 60;
+		var day = hour * 24;
+		var halfamonth = day * 15;
+		var month = day * 30;
+		var year = day * 365;
+		var now = new Date().getTime();
+		var diffValue = now - dateTimeStamp;
+		if (diffValue < 0) {
+			return;
+		}
+		var yearC = diffValue / year;
+		var monthC = diffValue / month;
+		var weekC = diffValue / (7 * day);
+		var dayC = diffValue / day;
+		var hourC = diffValue / hour;
+		var minC = diffValue / minute;
+		if (yearC >= 1) {
+			result = "" + parseInt(yearC) + "年前";
+		}
+		if (monthC >= 1) {
+			result = "" + parseInt(monthC) + "月前";
+		} else if (weekC >= 1) {
+			result = "" + parseInt(weekC) + "周前";
+		} else if (dayC >= 1) {
+			result = "" + parseInt(dayC) + "天前";
+		} else if (hourC >= 1) {
+			result = "" + parseInt(hourC) + "小时前";
+		} else if (minC >= 1) {
+			result = "" + parseInt(minC) + "分钟前";
+		} else
+			result = "刚刚";
+		return result;
+	},
+
+	// 时间戳转时间
+	timestampToTime(timestamp, i) {
+		var date = null;
+		if (timestamp > 0) {
+			date = new Date(timestamp); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
+		} else {
+			date = new Date();
+		}
+		// console.log(date, timestamp)
+
+		var Y = date.getFullYear();
+		var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1);
+		var D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate());
+		var h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours());
+		var m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes());
+		var s = (date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds());
+		if (i == 1) {
+			return Y + '-' + M + '-' + D;
+		} else if (i == 2) {
+			return Y + '年' + M + '月' + D + '日';
+		} else if (i == 3) {
+			return Y + '-' + M + '-' + D + ' ' + h + ':' + m;
+		}
+		return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s;
+	},
+
+	// 是否是汉字
+	isHanzi(str) {
+		let reg = /\p{Unified_Ideograph}/ug;
+		return reg.test(str);
+	},
+
+	// 是否是字母数字
+	isStringAndNumber(str) {
+		let regNumber = new RegExp(/^[0-9A-Za-z]+$/);
+		return regNumber.test(str)
+	},
+
+	// var arr3 = [30,10,111,35,1899,50,45];
+	// 集合排序  元素数字
+	listSort(list) {
+		arr3.sort(function(a, b) {
+			return a - b;
+		})
+	},
+
+	// var arr5 = [{id:10},{id:5},{id:6},{id:9},{id:2},{id:3}];
+	// 元素  对象
+	listObjectSort(arr) {
+		arr.sort(function(a, b) {
+			return a.id - b.id
+		})
+		return arr;
+	},
+
+	/*
+	 * 忽略大小写判断字符串是否相同
+	 * @param str1
+	 * @param str2
+	 * @returns {Boolean}
+	 */
+	isEqualsIgnorecase: function(str1, str2) {
+		if (str1.toUpperCase() == str2.toUpperCase()) {
+			return true;
+		} else {
+			return false;
+		}
+	},
+
+	/**
+	 * 判断是否是数字
+	 * @param value
+	 * @returns {Boolean}
+	 */
+	isNum: function(value) {
+		if (value != null && value.length > 0 && isNaN(value) == false) {
+			return true;
+		} else {
+			return false;
+		}
+	},
+
+	/**
+	 * 判断是否是中文
+	 * @param str
+	 * @returns {Boolean}
+	 */
+	isChine: function(str) {
+		var reg = /^([u4E00-u9FA5]|[uFE30-uFFA0])*$/;
+		if (reg.test(str)) {
+			return false;
+		}
+		return true;
+	},
+
+	/*验证是否为图片*/
+	tmCheckImage: function(fileName) {
+		return /(gif|jpg|jpeg|png|GIF|JPG|PNG)$/ig.test(fileName);
+	},
+
+	/*验证是否为视频*/
+	tmCheckVideo: function(fileName) {
+		return /(mp4|mp3|flv|wav)$/ig.test(fileName);
+	},
+
+	/**
+	 * 去除字符串两边的空格
+	 * @param str
+	 * @returns {number|Number}
+	 * 调用方法:var str = utils.trim("abcd")
+	 */
+	trim: function(str) {
+		String.prototype.trim = function() {
+			return str.replace(/(^\s*)|(\s*$)/g, "");
+		}
+	},
+
+	// 判断密码是否符合 至少6位,包括大小写字母、数字、特殊字符
+	isPassword(val) {
+		var reg = /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)])+$)^.{8,16}$/;
+		if (val === '') {
+			uni.showToast({
+				title: '请输入密码',
+				icon: 'none'
+			})
+		} else if (!reg.test(val)) {
+			uni.showToast({
+				title: '至少6位,包括大小写字母、数字、特殊字符',
+				icon: 'none'
+			})
+			return false
+		} else {
+			return true
+		}
+	},
+
+	// 电话匿名
+	noPassByMobile(str) {
+		if (null != str && str != undefined) {
+			var pat = /(\d{3})\d*(\d{4})/;
+			return str.replace(pat, '$1****$2');
+		} else {
+			return "";
+		}
+	},
+
+	// 获取两点间的距离
+	//进行经纬度转换为距离的计算
+	Rad(d) {
+		return d * Math.PI / 180.0; //经纬度转换成三角函数中度分表形式。
+	},
+
+	/*
+	 计算距离,参数分别为第一点的纬度,经度;第二点的纬度,经度
+	 默认单位km
+	*/
+	getMapDistance(lat1, lat2, lng1, lng2) {
+		lat1 = lat1 || 0;
+		lng1 = lng1 || 0;
+		lat2 = lat2 || 0;
+		lng2 = lng2 || 0;
+
+		var rad1 = lat1 * Math.PI / 180.0;
+		var rad2 = lat2 * Math.PI / 180.0;
+		var a = rad1 - rad2;
+		var b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
+		var r = 6378137;
+		var distance = r * 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(rad1) * Math.cos(rad2) *
+			Math.pow(Math.sin(b / 2), 2)));
+		// console.log(lat1, lng1, lat2, lng2);
+		// console.log(distance);
+		return Math.round(distance) / 1000;
+	},
+
+	// 预览图片
+	yulanImg(item) {
+		let arr = [item]
+		uni.previewImage({
+			urls: arr,
+		});
+	},
+
+}
+
 export default tools;

+ 18 - 3
card/components/my-pathList/my-pathList.vue

@@ -7,8 +7,11 @@
 				</image>
 			</view>
 			<view v-if="item.type == 3" class="path-nav uni-row uni-aie">
-				<image mode="aspectFit" class="pathimg2" :src="item.pathImg" @click="onPathImgClick(item, 'path')">
-				</image>
+				<view class="uni-column">
+					<image mode="aspectFit" class="pathimg2" :src="item.pathImg" @click="onPathImgClick(item, 'path')">
+					</image>
+					<text v-if="item.pathName != ''" class="pathName2">{{item.pathName}}</text>
+				</view>
 				<view class="uni-column">
 					<image mode="aspectFit" class="navimg" :src="item.navImg" @click="onPathImgClick(item, 'nav')">
 					</image>
@@ -154,6 +157,7 @@
 	}
 	
 	.pathName {
+		text-align: center;
 		font-weight: 500;
 		color: #383838;
 		font-size: 16px;
@@ -161,6 +165,17 @@
 		font-family: Source Han Sans CN;
 	}
 	
+	.pathName2 {
+		width: 100px;
+		text-align: center;
+		font-weight: 500;
+		color: #CE0202;
+		font-size: 10px;
+		font-family: Source Han Sans CN;
+		/* white-space: nowrap; */
+		/* overflow: hidden; */
+	}
+	
 	.navimg {
 		width: 30px;
 		height: 30px;
@@ -180,7 +195,7 @@
 		font-family: Source Han Sans CN;
 		white-space: nowrap;
 	}
-
+	
 	.main-path {
 		width: 90%;
 		margin-top: 10px;

+ 59 - 30
card/components/my-popup/my-popup.vue

@@ -50,7 +50,7 @@
 					<text class="swiper-item-title" v-html="item.data.title"></text>
 
 					<view class="swiper-item-main uni-column uni-jcse">
-						<image mode="aspectFit" style="height: 140px; margin-top: 25px;" :src="item.data.img"></image>
+						<image mode="aspectFit" style="height: 140px; margin-top: 25px;" :src="item.data.img" @click="previewImage(item.data.img)"></image>
 						<text class="swiper-item-content2" v-html="item.data.content"></text>
 					</view>
 
@@ -69,7 +69,7 @@
 							<text class="swiper-item-content3" v-html="item.data.content"></text>
 							<text class="swiper-item-content4">【{{item.data.sourceName}}】</text>
 						</view>
-						<image class="swiper-item-image2" mode="aspectFit" :src="item.data.img"></image>
+						<image class="swiper-item-image2" mode="aspectFit" :src="item.data.img" @click="previewImage(item.data.img)"></image>
 						<template v-if="[1,3].includes(item.data.exState)">
 							<!-- verfType: 兑换券展示类型  1 本系统核销  2 赞助商提供码核销 3 赞助商不提供码核销 -->
 							<uv-qrcode v-if="item.data.verfType==1" ref="qrcode" size="100px" :value="item.data.qrCode">
@@ -247,6 +247,30 @@
 					<button v-else class="swiper-item-button" @click="popupClose">确 定</button>
 				</view>
 				
+				<!-- 【赛事活动导航 - 单点】标题 + 赛事图片 + 导航图片(点击打开地图APP进行地点定位) + 文字介绍 -->
+				<view v-if="item.type == 11" class="swiper-item-view uni-column">
+					<text class="swiper-item-title" v-html="item.data.title"></text>
+				
+					<view class="swiper-item-main uni-column">
+						<image mode="aspectFit" style="height: 100px; margin-top: 15px; margin-bottom: 25px;"
+							:src="item.data.img">
+						</image>
+						<view class="uni-column" style="margin-bottom: 20px;" @click="dealNavClick(item.data.point)">
+							<image mode="aspectFit" style="width: 19px; height: 26px;" src="/static/common/nav3.png"></image>
+							<text class="" style="color: #E60012; font-size: 14px;" >导航前往起点</text>
+						</view>
+						<view class="swiper-item-content5">
+							<view v-if="item.data.content" v-html="item.data.content"></view>
+						</view>
+					</view>
+				
+					<button v-if="index < dataList.length - 1" class="swiper-item-button" @click="swiperNext">继
+						续</button>
+					<button v-else class="swiper-item-button" @click="popupStart">开始挑战</button>
+					<view class="swiper-item-txtClose" @click="popupClose">关 闭</view>
+				</view>
+				
+				
 			</swiper-item>
 		</swiper>
 	</uni-popup>
@@ -274,7 +298,7 @@
 				})
 			}
 		},
-		emits: ['popup-close', 'noMoreRemindersClick'],
+		emits: ['popup-close', 'noMoreRemindersClick', 'popup-start'],
 		data() {
 			return {
 				swiperCurrent: 0, // swiper当前所在滑块的 index
@@ -301,7 +325,8 @@
 				this.swiperCurrent++;
 			},
 			popupOpen() {
-				if (this.dataList.length == 0) {
+				// console.log("[popupOpen] dataList", this.dataList);
+				if (this.dataList == undefined || this.dataList.length == 0) {
 					console.log("[popupOpen] dataList为空,禁止弹窗");
 					return;
 				}
@@ -315,6 +340,11 @@
 				this.isOpen = false;
 				this.$emit('popup-close');
 			},
+			popupStart() {
+				this.$refs.popup.close();
+				this.isOpen = false;
+				this.$emit('popup-start');
+			},
 			fmtTime(timestamp, type = 2) {
 				return tools.timestampToTime(timestamp * 1000, type);
 			},
@@ -388,6 +418,12 @@
 					duration: 3000
 				});
 			},
+			previewImage(imgurl) {
+				uni.previewImage({
+					showmenu: true,
+					urls: [imgurl] // 需要预览的图片 HTTP 链接列表
+				});
+			},
 			// getTeamName(teamType, teamIndex) {
 			// 	return teamName[teamType][teamIndex];
 			// },
@@ -399,7 +435,7 @@
 	}
 </script>
 
-<style lang="scss" scoped>
+<style scoped>
 
 	.swiper {
 		width: 90vw;
@@ -413,12 +449,10 @@
 	}
 
 	.swiper-item-view {
-		// min-height: 95%;
 		min-height: 94.3%;
 		padding-top: 25px;
 		overflow: auto;
 		flex-grow: 1;
-		// justify-content: space-between;
 	}
 
 	.swiper-item-view-bg {
@@ -430,11 +464,11 @@
 	}
 
 	.swiper-item-view-bg2 {
-		// background-image: url("/static/backgroud/oval.png");
-		// background-repeat: no-repeat;
-		// background-position-x: center;
-		// background-position-y: 198px;
-		// background-size: 66%;
+		/* background-image: url("/static/backgroud/oval.png");
+		background-repeat: no-repeat;
+		background-position-x: center;
+		background-position-y: 198px;
+		background-size: 66%; */
 	}
 
 	.swiper-item-topLogo {
@@ -479,7 +513,6 @@
 
 	.swiper-item-image2 {
 		height: 110px;
-		// margin-top: 5px;
 	}
 
 	.swiper-item-time {
@@ -497,7 +530,6 @@
 	.swiper-item-content {
 		width: 80%;
 		margin-top: 15px;
-		// margin-bottom: 30px;
 		justify-content: start;
 		flex-grow: 1;
 		color: #333333;
@@ -508,7 +540,6 @@
 		width: 80%;
 		margin-top: 60px;
 		margin-bottom: 10px;
-		// justify-content: center;
 		text-align: center;
 		color: #333333;
 		font-size: 13px;
@@ -517,15 +548,12 @@
 	}
 
 	.swiper-item-content3 {
-		// width: 80%;
-		// margin-top: 5px;
 		color: #333333;
 		font-weight: 400;
 		text-align: center;
 	}
 
 	.swiper-item-content4 {
-		// width: 80%;
 		text-align: center;
 		color: #333333;
 		font-size: 12px;
@@ -574,7 +602,6 @@
 		width: 80%;
 		margin-top: 10px;
 		margin-bottom: 10px;
-		// justify-content: center;
 		text-align: left;
 		color: #333333;
 		font-size: 13px;
@@ -587,13 +614,11 @@
 		width: 80%;
 		margin-top: 10px;
 		margin-bottom: 10px;
-		// justify-content: center;
 		text-align: left;
 		color: #333333;
 		font-size: 13px;
 		font-weight: 400;
 		line-height: 20px;
-		// flex-grow: 1;
 	}
 
 	.swiper-item-button {
@@ -601,7 +626,6 @@
 		height: 38px;
 		margin-bottom: 25px;
 		color: #ffffff;
-		/* font-weight: bold; */
 		line-height: 38px;
 		background-color: #2e85ec;
 		border-radius: 27px;
@@ -612,12 +636,20 @@
 		height: 38px;
 		margin-bottom: 25px;
 		color: #ffffff;
-		/* font-weight: bold; */
 		line-height: 38px;
 		background-color: #2e85ec;
 		border-radius: 27px;
 	}
 	
+	.swiper-item-txtClose {
+		width: 50%;
+		margin-top: -10px;
+		margin-bottom: 25px;
+		text-align: center;
+		color: #383838;
+		font-size: 16px;
+	}
+	
 	.swiper-item-noMoreReminders {
 		position: absolute;
 		right: 10px;
@@ -645,7 +677,6 @@
 		font-size: 13px;
 		font-weight: 400;
 		line-height: 20px;
-		// flex-grow: 1;
 	}
 
 	.nowrap {
@@ -657,7 +688,6 @@
 	.sponsorsLogo {
 		position: absolute;
 		width: 120px;
-		// width: 75px;
 		height: 75px;
 		left: 18px;
 		top: 8px;
@@ -665,14 +695,13 @@
 		background-position-y: center;
 		background-repeat: no-repeat;
 		background-size: contain;
-		// background-size: 100% auto;
 	}
 		
 	::v-deep .uni-swiper-dots-horizontal {
 		bottom: 75px;
 	}
 
-	// ::v-deep .uni-swiper-dot-active {
-	// 	background: #ff870e !important;
-	// }
-</style>
+	/* ::v-deep .uni-swiper-dot-active {
+		background: #ff870e !important;
+	} */
+</style>

+ 4 - 2
card/components/my-ranklist/my-ranklist.vue

@@ -25,6 +25,8 @@
 					<text class="item-totalTime" v-else-if="rankType == 'totalCp' || rankType == 'totalSysPoint'">{{item.inRankNum}} 个</text>
 					<text class="item-totalTime" v-else-if="rankType == 'totalScore'">{{item.inRankNum}}</text>
 					<text class="item-totalTime" v-else-if="rankType == 'speed'">{{fmtTime(item.inRankNum)}}</text>
+					<text class="item-totalTime" v-else-if="rankType == 'grad'">{{item.inRankNum}}</text>
+					<text class="item-totalTime" v-else-if="rankType == 'mapNum'">{{item.inRankNum}}</text>
 					<text class="item-totalTime" v-else>{{fmtTime(item.totalTime)}}</text>
 				</view>
 			</template>
@@ -46,7 +48,7 @@
 				type: Number,
 				default: -1
 			},
-			rankType: {	// totalScore:总积分 totalDistance:总里程 totalCp:打点数 totalSysPoint:百味豆 fastPace:配速 rightAnswerPer:答题正确率 speed:速度
+			rankType: {	// totalScore:总积分 totalDistance:总里程 totalCp:打点数 totalSysPoint:百味豆 fastPace:配速 rightAnswerPer:答题正确率 speed:速度 grad:积分 mapNum:地图解锁数
 				type: String,
 				default: ""
 			},
@@ -71,7 +73,7 @@
 			this.refList = this.$refs.list;
 			this.refListItems = this.refList.$el.children;
 			// console.log("refListItems", this.refListItems);
-			console.log("rankRs", this.rankType);
+			// console.log("rankRs", this.rankType);
 		},
 		methods: {
 			getListItemClass(item, index) {

+ 26 - 16
card/main.js

@@ -1,6 +1,6 @@
 import App from './App'
 // import audio from "./common/audio.js";
-	
+
 // #ifndef VUE3
 import Vue from 'vue'
 import './uni.promisify.adaptor'
@@ -8,33 +8,43 @@ import './uni.promisify.adaptor'
 Vue.config.productionTip = false
 
 // 卡片配置来源 server:服务器获取 local:本地获取
-// Vue.prototype.$cardconfigType = "server";
-Vue.prototype.$cardconfigType = "local";
+if (process.env.NODE_ENV === 'development') { // 开发版
+	Vue.prototype.$cardconfigType = "server";
+	// Vue.prototype.$cardconfigType = "local";
+} else {
+	Vue.prototype.$cardconfigType = "server";
+}
 
 // Vue.prototype.$audio = audio;
 
 App.mpType = 'app'
 const app = new Vue({
-  ...App
+	...App
 })
 app.$mount()
 // #endif
 
 
 // #ifdef VUE3
-import { createSSRApp } from 'vue'
+import {
+	createSSRApp
+} from 'vue'
 
 export function createApp() {
-  const app = createSSRApp(App)
-  
-  // 卡片配置来源 server:服务器获取 local:本地获取
-  app.config.globalProperties.$cardconfigType = "server";
-  // app.config.globalProperties.$cardconfigType = "local";
-  
-  // app.config.globalProperties.$audio = audio;
-  
-  return {
-    app
-  }
+	const app = createSSRApp(App)
+
+	// 卡片配置来源 server:服务器获取 local:本地获取
+	if (process.env.NODE_ENV === 'development') { // 开发版
+		app.config.globalProperties.$cardconfigType = "server";
+		// app.config.globalProperties.$cardconfigType = "local";
+	} else {
+		app.config.globalProperties.$cardconfigType = "server";
+	}
+
+	// app.config.globalProperties.$audio = audio;
+
+	return {
+		app
+	}
 }
 // #endif

+ 2 - 2
card/manifest.json

@@ -2,8 +2,8 @@
     "name" : "card",
     "appid" : "__UNI__A61F96B",
     "description" : "",
-    "versionName" : "2.0.4",
-    "versionCode" : 204,
+    "versionName" : "2.2.0",
+    "versionCode" : 220,
     "transformPx" : false,
     /* 5+App特有相关 */
     "app-plus" : {

+ 34 - 0
card/pages.json

@@ -49,6 +49,12 @@
 				"navigationBarTitleText": "每月挑战 - 详情"
 			}
 		},
+		{
+			"path": "pages/mytz/rankList",
+			"style": {
+				"navigationBarTitleText": "每月挑战 - 月排名列表"
+			}
+		},
 		{
 			"path": "pages/jbs/index",
 			"style": {
@@ -222,6 +228,34 @@
 			"style": {
 				"navigationBarTitleText": "[模板] 样式3 - 排名总览"
 			}
+		},
+		{
+			"path" : "pages/app/info/personalInfo",
+			"style" : 
+			{
+				"navigationBarTitleText" : "个人信息收集清单"
+			}
+		},
+		{
+			"path" : "pages/app/info/thirdPartyInfo",
+			"style" : 
+			{
+				"navigationBarTitleText" : "第三方信息共享清单"
+			}
+		},
+		{
+			"path" : "pages/game/grid/index",
+			"style" : 
+			{
+				"navigationBarTitleText" : "[游戏] 网格赛事"
+			}
+		},
+		{
+			"path" : "pages/game/grid/grid",
+			"style" : 
+			{
+				"navigationBarTitleText" : "[游戏] 网格赛事 - 网格拼图"
+			}
 		}
 	],
 	"globalStyle": {

+ 7 - 2
card/pages/achievement/index2.vue

@@ -304,7 +304,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/achievement/index2
 						oarId: oarId
 					},
 					success: (res) => {
-						console.log("exchangeDetailQuery", res);
+						// console.log("exchangeDetailQuery", res);
 						if (checkResCode(res)) {
 							const data = res.data.data;
 
@@ -429,7 +429,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/achievement/index2
 				});
 			},
 			showAchDetail(data) {
-				console.log("showAchDetail", data);
+				// console.log("showAchDetail", data);
 				this.popupAchdet.length = 0;
 
 				if (this.tabCurrent == 1) { // 奖牌
@@ -701,4 +701,9 @@ https://oss-mbh5.colormaprun.com/card/#/pages/achievement/index2
 		background-size: 30%;
 		border-radius: 5px;
 	}
+	
+	::v-deep #u-a-p > div {
+		top: 30px !important;
+	}
+	
 </style>

+ 252 - 0
card/pages/app/info/personalInfo.vue

@@ -0,0 +1,252 @@
+<!-- 
+个人信息收集清单
+http://localhost:5173/card/#/pages/app/info/personalInfo
+https://oss-mbh5.colormaprun.com/card/#/pages/app/info/personalInfo
+ -->
+<template>
+	<view class="main">
+		<view class="title">彩图奔跑个人信息收集清单</view>
+		<uni-table ref="table" class="table" border emptyText="暂无更多数据">
+			<uni-tr class="tbHeader">
+				<uni-th class="th" width="80" align="center">信息名称</uni-th>
+				<uni-th class="th" width="80" align="center">使用目的</uni-th>
+				<uni-th class="th" width="80" align="center">使用场景</uni-th>
+				<uni-th class="th" width="80" align="center">收集情况</uni-th>
+				<uni-th class="th" width="80" align="center">信息内容</uni-th>
+			</uni-tr>
+			<uni-tr v-for="(item, index) in tableData" :key="index">
+				<uni-td class="tbText" align="center">
+					<view>{{item.name}}</view>
+				</uni-td>
+				<uni-td class="tbText" align="center">
+					<view>{{item.purpose}}</view>
+				</uni-td>
+				<uni-td class="tbText" align="center">
+					<view>{{item.scene}}</view>
+				</uni-td>
+				<uni-td class="tbText" align="center">
+					<view>{{item.status}}</view>
+				</uni-td>
+				<uni-td class="tbText" align="center">
+					<image v-if="item.contentType == 'img'" class="headimg" mode="aspectFit" :src="item.content"></image>
+					<view v-else>{{item.content}}</view>
+				</uni-td>
+			</uni-tr>
+		</uni-table>
+	</view>
+</template>
+
+<script>
+	import tools from '../../../common/tools';
+	import {
+		token,
+		apiUserBasicInformationQuery,
+		checkResCode
+	} from '../../../common/api';
+
+	export default {
+		data() {
+			return {
+				queryObj: {},
+				queryString: "",
+				token: "",
+				
+				userInfo: {},
+				// userInfo: {
+				// 	"oId": 8,
+				// 	"phone": "15168870729",
+				// 	"nickName": "虚室生白",
+				// 	"headUrl": "https://orienteering.beswell.com/orienteering_head_1713776302_8_%E8%99%9A%E5%AE%A4%E7%94%9F%E7%99%BD.jpeg",
+				// 	"height": 1890,
+				// 	"weight": 112500,
+				// 	"sex": 1,
+				// 	"staticHr": 60,
+				// 	"age": 39
+				// },
+				tableData: [{
+						name: "用户ID",
+						purpose: "展示用户ID",
+						scene: "用户注册账号、用户报名参赛、赛事成绩展示",
+						status: "未收集",
+						content: ""
+					},
+					{
+						name: "昵称",
+						purpose: "完善网络身份标识、展示昵称",
+						scene: "注册账号、报名参赛、赛事成绩展示",
+						status: "未收集",
+						content: ""
+					},
+					{
+						name: "头像",
+						purpose: "完善网络身份标识、展示头像",
+						scene: "报名参赛、赛事成绩展示",
+						status: "未收集",
+						content: "",
+						contentType: "img"
+					},
+					{
+						name: "手机号",
+						purpose: "账号登录、赛事安全服务",
+						scene: "身份验证、报名参赛凭据、用户赛事安全服务",
+						status: "未收集",
+						content: ""
+					},
+					{
+						name: "身高",
+						purpose: "计算卡路里",
+						scene: "比赛过程中计算用户的卡路里消耗情况",
+						status: "未收集",
+						content: ""
+					},
+					{
+						name: "体重",
+						purpose: "计算卡路里",
+						scene: "比赛过程中计算用户的卡路里消耗情况",
+						status: "未收集",
+						content: ""
+					},
+					{
+						name: "年龄",
+						purpose: "计算卡路里",
+						scene: "比赛过程中计算用户的卡路里消耗情况",
+						status: "未收集",
+						content: ""
+					},
+					{
+						name: "静息心率",
+						purpose: "计算卡路里",
+						scene: "比赛过程中计算用户的卡路里消耗情况",
+						status: "未收集",
+						content: ""
+					},
+					{
+						name: "位置信息",
+						purpose: "计算配速里程",
+						scene: "比赛过程中计算用户的配速、里程信息",
+						status: "未收集",
+						content: ""
+					},
+					/* {
+						name: "位置信息",
+						purpose: "XXX",
+						scene: "XXX",
+						status: "未收集",
+						content: ""
+					}, */
+				]
+			}
+		},
+		onLoad(query) { // 类型非必填,可自动推导
+			// console.log(query);
+			this.queryObj = query;
+			this.queryString = tools.objectToQueryString(this.queryObj);
+			// console.log(queryString);
+			this.token = query["token"] ?? token;
+
+			this.userBasicInformationQuery();
+			// this.dealUserInfo();
+		},
+		methods: {
+			// 用户基本信息查询
+			userBasicInformationQuery() {
+				uni.request({
+					url: apiUserBasicInformationQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"Authorization": "Bearer " + this.token,
+						// "token": this.token
+					},
+					method: "GET",
+					data: {},
+					success: (res) => {
+						// console.log("userBasicInformationQuery", res);
+						if (checkResCode(res)) {
+							const userinfo = res.data.data.list;
+							this.userInfo = userinfo;
+							this.dealUserInfo();
+						}
+					},
+					fail: (err) => {
+						console.log("userBasicInformationQuery err", err)
+					},
+				});
+			},
+			dealUserInfo() {
+				// console.log("dealUserInfo length", Object.keys(this.userInfo).length);
+				if (Object.keys(this.userInfo).length > 0) {
+					if (this.userInfo.oId > 0) {
+						this.tableData[0].status = "已收集";
+						this.tableData[0].content = this.userInfo.oId;
+					}
+					if (this.userInfo.nickName.length > 0) {
+						this.tableData[1].status = "已收集";
+						this.tableData[1].content = this.userInfo.nickName;
+					}
+					if (this.userInfo.headUrl.length > 0) {
+						this.tableData[2].status = "已收集";
+						this.tableData[2].content = this.userInfo.headUrl;
+					}
+					if (this.userInfo.phone.length > 0) {
+						this.tableData[3].status = "已收集";
+						this.tableData[3].content = this.userInfo.phone;
+					}
+					if (this.userInfo.height > 0) {
+						this.tableData[4].status = "已收集";
+						this.tableData[4].content = this.userInfo.height;
+					}
+					if (this.userInfo.weight > 0) {
+						this.tableData[5].status = "已收集";
+						this.tableData[5].content = this.userInfo.weight;
+					}
+					if (this.userInfo.age > 0) {
+						this.tableData[6].status = "已收集";
+						this.tableData[6].content = this.userInfo.age;
+					}
+					if (this.userInfo.staticHr > 0) {
+						this.tableData[7].status = "已收集";
+						this.tableData[7].content = this.userInfo.staticHr;
+					}
+					
+					// this.tableData[8].content = this.userInfo.sex;
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.main {
+		width: 100vw;
+		height: 100vh;
+		background-color: #f5f5f5;
+	}
+
+	.title {
+		padding-top: 40px;
+		padding-bottom: 20px;
+		text-align: center;
+		font-size: 18px;
+		font-weight: bold;
+	}
+
+	.table {}
+
+	.tbHeader {
+		background-color: #fcfcfc;
+	}
+
+	.th {
+		color: #000;
+		font-size: 14px;
+	}
+
+	.tbText {
+		font-size: 14px;
+	}
+	
+	.headimg {
+		width: 60px;
+		height: 60px;
+	}
+</style>

+ 148 - 0
card/pages/app/info/thirdPartyInfo.vue

@@ -0,0 +1,148 @@
+<!-- 
+第三方信息共享清单
+http://localhost:5173/card/#/pages/app/info/thirdPartyInfo
+https://oss-mbh5.colormaprun.com/card/#/pages/app/info/thirdPartyInfo
+ -->
+<template>
+	<view class="main">
+		<view class="title">彩图奔跑第三方信息共享清单</view>
+		<view v-if="tableData.length > 0">
+			<uni-table ref="table" class="table" border>
+				<uni-tr class="tbHeader">
+					<uni-th class="th" width="50" align="center">SDK服务</uni-th>
+					<uni-th class="th" width="50" align="center">公司名称</uni-th>
+					<uni-th class="th" width="50" align="center">使用场景</uni-th>
+					<uni-th class="th" width="50" align="center">客户端</uni-th>
+					<uni-th class="th" width="50" align="center">频次</uni-th>
+					<uni-th class="th" width="50" align="center">处理方式</uni-th>
+					<uni-th class="th" width="50" align="center">获取的权限/信息</uni-th>
+					<uni-th class="th" width="20" align="center">隐私权政策</uni-th>
+				</uni-tr>
+				<uni-tr v-for="(item, index) in tableData" :key="index">
+					<uni-td class="tbText" align="center">
+						<view>{{item.sdk}}</view>
+					</uni-td>
+					<uni-td class="tbText" align="center">
+						<view>{{item.comp}}</view>
+					</uni-td>
+					<uni-td class="tbText" align="center">
+						<view>{{item.scene}}</view>
+					</uni-td>
+					<uni-td class="tbText" align="center">
+						<view>{{item.client}}</view>
+					</uni-td>
+					<uni-td class="tbText" align="center">
+						<view>{{item.frequency}}</view>
+					</uni-td>
+					<uni-td class="tbText" align="center">
+						<view>{{item.deal}}</view>
+					</uni-td>
+					<uni-td class="tbText" align="center">
+						<view>{{item.info}}</view>
+					</uni-td>
+					<uni-td class="tbText" align="center">
+						<uni-link :href="item.privacy" fontSize="12" text="点击查看第三方隐私政策"></uni-link>
+					</uni-td>
+				</uni-tr>
+			</uni-table>
+		</view>
+		<view v-else class="noinfo">
+			本APP暂未对第三方进行信息共享
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				info: {
+					userid: 0,
+				},
+				tableData: [{
+						sdk: "七牛云SDK",
+						comp: "上海七牛信息技术有限公司",
+						scene: "上传用户头像",
+						client: "安卓端 / iOS端",
+						frequency: "用户主动触发,在用户每次使用“修改头像”功能时",
+						deal: "通过去标识化、加密传输和处理的安全处理方式",
+						info: "个人信息:用户头像",
+						privacy: "https://www.qiniu.com/agreements/privacy-right"
+					},
+					{
+						sdk: "阿里云存储服务SDK",
+						comp: "阿里云计算有限公司",
+						scene: "下载比赛地图",
+						client: "安卓端 / iOS端",
+						frequency: "用户主动触发,在用户每次开始比赛时",
+						deal: "通过去标识化、加密传输和处理的安全处理方式",
+						info: "个人信息:用户ID",
+						privacy: "https://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201802111644_32057.html?spm=a2c4g.11186623.0.0.7a0973fajRdNKX"
+					},
+					{
+						sdk: "微信分享SDK",
+						comp: "深圳市腾讯计算机系统有限公司",
+						scene: "分享",
+						client: "安卓端 / iOS端",
+						frequency: "用户主动触发,在用户每次使用“分享至微信”功能时",
+						deal: "通过去标识化、加密传输和处理的安全处理方式",
+						info: "个人信息:用户openid、彩图奔跑APP的Token",
+						privacy: "https://weixin.qq.com/cgi-bin/readtemplate?lang=zh_CN&t=weixin_agreement&s=privacy#1"
+					},
+					/* {
+						sdk: "XXX",
+						comp: "XXX",
+						scene: "XXX",
+						client: "安卓端 / iOS端",
+						frequency: "XXX",
+						deal: "XXX",
+						info: "XXX",
+						privacy: "XXX"
+					}, */
+				]
+			}
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style scoped>
+	.main {
+		width: 100vw;
+		height: 100vh;
+		background-color: #f5f5f5;
+	}
+	
+	.title {
+		padding-top: 40px;
+		padding-bottom: 20px;
+		text-align: center;
+		font-size: 18px;
+		font-weight: bold;
+	}
+
+	.table {
+	}
+
+	.tbHeader {
+		background-color: #fcfcfc;
+	}
+
+	.th {
+		color: #000;
+		font-size: 12px;
+	}
+
+	.tbText {
+		font-size: 12px;
+		line-height: 16px;
+	}
+
+	.noinfo {
+		padding: 50px 10px;
+		text-align: center;
+		font-size: 14px;
+	}
+</style>

+ 1 - 1
card/pages/exchange/style1/goodsDetail.vue

@@ -290,7 +290,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/exchange/style1/goodsDetail
 						} else if (res.cancel) {
 						}
 					}
-				})
+				});
 			},
 			btnBack() {
 				const url = "/pages/exchange/style1/goodsList?" + this.queryString;

+ 50 - 0
card/pages/game/grid/cardconfig/test.js

@@ -0,0 +1,50 @@
+export const localCardConfig = `{
+	"common": {
+		"css": "
+			.swiper-item-button {
+				height: 50px !important;
+				line-height: 50px !important;
+				color: #A65600 !important;
+				background: linear-gradient(117.53deg, #FFCD29 0%, #FFE694 42.36%, #FFC508 100%) !important;
+			}
+		"
+	},
+	"index": {
+		"css": "
+			.content-bg{
+				background: linear-gradient(180deg, rgb(255, 216, 77) 0%, rgb(255, 110, 0) 100%) !important;
+			}
+			.logo{
+				width: 40vw !important;
+				height: 40vw !important;
+				background: url('static/logo/building2.png') no-repeat center !important;
+				background-size: contain !important;
+			}
+			.mod-text{
+				color: #FFFFFF !important;
+			}
+			.mod-button{
+				color: #EF6223 !important;
+				background-color: #FFFFFF !important;
+			}
+		"
+	},
+	"grid": {
+		"css": "
+			.cell-name-uncomplete {
+				color: #CF6B00 !important;
+			}
+			.cell-name-complete {
+				color: #ffffff !important;
+			}
+		",
+		"introduce": {
+			"title": "赛事九宫格:",
+			"content": "选择其中一个格子,挑战成功即可解锁。九个格子全部解锁,获取一张奖励卡和对应奖品!"
+		},
+		"activityRules": {
+			"title": "获取积分规则:",
+			"content": "1. 每成功解锁一个新地图获得100积分<br>2. 挑战过程中每成功打点获得1个积分<br>3. 挑战过程中每成功答对一题获得1个积分"
+		}
+	}
+}`;

+ 560 - 0
card/pages/game/grid/grid.vue

@@ -0,0 +1,560 @@
+<!-- 
+[游戏] 网格赛事 - 网格拼图
+http://localhost:5173/card/#/pages/game/grid/grid
+https://oss-mbh5.colormaprun.com/card/#/pages/game/grid/grid
+ -->
+<template>
+	<view class="body">
+		<view v-if="pageReady" class="content uni-column">
+			<view class="uni-column page-top">
+				<my-topbar :mcName="compName" class="topbar-color" :showRule="false"
+					@btnBackClick="btnBack"></my-topbar>
+			</view>
+			<view class="main uni-column">
+				<view v-if="grid.state <= 1" class="mt-content">开始你的挑战吧</view>
+				<view v-if="grid.state == 2" class="mt-content">很棒!继续你的挑战吧</view>
+				<view v-if="grid.state >= 3" class="mt-content" style="color: #FF5733;">挑战成功!</view>
+				<view class="grid uni-column uni-jcse" :style="getGridStyle()">
+					<view class="grid-row uni-row uni-jcse" :style="getGridRowStyle()" v-for="rowId in grid.heightNum"
+						:key="rowId">
+						<view class="grid-cell" :style="getGridCellStyle(rowId, colId)" v-for="colId in grid.widthNum"
+							:key="colId" @click="onCellClick(rowId, colId)">
+							<template v-for="(item, index) in grid.detailRs" :key="index">
+								<view class="cell-name" :class="item.isComplete ? 'cell-name-complete' : 'cell-name-uncomplete'" v-if="item.orderNum == getCellOrderNum(rowId, colId)">
+									{{item.showName}}
+								</view>
+							</template>
+						</view>
+					</view>
+				</view>
+				<view class="introduce uni-column">
+					<text class="introduce-title">{{introduce.title}}</text>
+					<text class="introduce-content" v-html="introduce.content"></text>
+				</view>
+
+				<view v-if="activityRules.content.length > 0" class="activityRules uni-column">
+					<text class="activityRules-title">{{activityRules.title}}</text>
+					<text class="activityRules-content" v-html="activityRules.content"></text>
+				</view>
+				
+			</view>
+
+			<my-popup ref="mypopup" :config="popupDataConfig" :dataList="popupDataList"
+				@popup-start="startGame"></my-popup>
+		</view>
+	</view>
+</template>
+
+<script>
+	import tools from '../../../common/tools';
+	import cardfunc from '../../../common/cardfunc';
+	import {
+		localCardConfig
+	} from "./cardconfig/test.js";
+	import {
+		token,
+		apiGridsQuery,
+		apiUserJoinCardQuery,
+		apiOnlineMcSignUp,
+		checkResCode,
+		checkToken
+	} from '../../../common/api';
+
+	export default {
+		data() {
+			return {
+				cardConfigData: cardfunc.cardConfigData,
+				pageReady: false,
+				pageName: "grid",
+				queryObj: {},
+				queryString: "",
+				token: "",
+				ecId: 0, // 卡片id
+				compId: 0, // 赛事id
+				compName: "", // 赛事名称
+				mcState: 0, // 赛事/活动状态 0: 未开始  1: 进行中  2: 已结束
+				isJoin: false, // 是否报名
+				// acttime: "", // 活动时间
+				// beginSecond: null, // 活动或赛事开始时间戳,单位秒
+				// endSecond: null, // 活动或赛事结束时间戳,单位秒
+
+				sltCellOrderNum: 0, // 用户选中网格的序号
+				sltCellRs: {}, // 用户选中网格对应的活动记录
+				popupDataConfig: {
+					"height": "530px"
+				},
+				popupDataList: [],
+
+				grid: { // 网格数据
+					widthNum: 0, // 横向网格数量
+					heightNum: 0, // 竖向网格数量
+					maskImgPic: "", // 遮罩图url
+					actualImgPic: "", // 真实图url
+					state: 0, // 网格赛事状态 1 未开始 2 进行中 3 已完成
+					detailRs: [{
+						orderNum: 0, //序号
+						ocaId: 0, //活动id    
+						mcType: 0, //活动类型 1普通 2线下 3线上 
+						isComplete: 0, //是否完赛
+						showName: "", //显示名称
+						description: "", //描述
+						longitude: 0, //导航起点经度
+						latitude: 0, //导航起点纬度
+						popupImg: "" // 弹窗图片
+					}]
+				},
+
+				grid0: { // 网格数据
+					widthNum: 1, // 横向网格数量
+					heightNum: 1, // 竖向网格数量
+					maskImgPic: "static/backgroud/grid4_mask.png", // 遮罩图url
+					actualImgPic: "static/backgroud/grid_bg.jpg", // 真实图url
+					state: 0, // 网格赛事状态 1 未开始 2 进行中 3 已完成
+				},
+				grid1: { // 网格数据
+					widthNum: 2, // 横向网格数量
+					heightNum: 2, // 竖向网格数量
+					maskImgPic: "static/backgroud/grid4_mask.png", // 遮罩图url
+					actualImgPic: "static/backgroud/grid_bg.jpg", // 真实图url
+					state: 0, // 网格赛事状态 1 未开始 2 进行中 3 已完成
+					detailRs: [{
+						orderNum: 1, //序号
+						ocaId: 0, //活动id    
+						mcType: 0, //活动类型 1普通 2线下 3线上 
+						isComplete: 1, //是否完赛
+						showName: "", //显示名称
+						description: "", //描述
+						longitude: 0, //导航起点经度
+						latitude: 0 //导航起点纬度
+					}]
+				},
+				grid2: { // 网格数据
+					widthNum: 3, // 横向网格数量
+					heightNum: 3, // 竖向网格数量
+					maskImgPic: "static/backgroud/grid9_mask.png", // 遮罩图url
+					actualImgPic: "static/backgroud/grid_bg.jpg", // 真实图url
+					state: 2, // 网格赛事状态 1 未开始 2 进行中 3 已完成
+					detailRs: [{
+						orderNum: 1, //序号
+						ocaId: 0, //活动id    
+						mcType: 0, //活动类型 1普通 2线下 3线上 
+						isComplete: 1, //是否完赛
+						showName: "asdf", //显示名称
+						description: "", //描述
+						longitude: 0, //导航起点经度
+						latitude: 0 //导航起点纬度
+					}]
+				},
+				grid3: { // 网格数据
+					widthNum: 4, // 横向网格数量
+					heightNum: 4, // 竖向网格数量
+					maskImgPic: "static/backgroud/grid4_mask.png", // 遮罩图url
+					actualImgPic: "static/backgroud/grid_bg.jpg", // 真实图url
+					state: 0, // 网格赛事状态 1 未开始 2 进行中 3 已完成
+					detailRs: [{
+						orderNum: 1, //序号
+						ocaId: 0, //活动id    
+						mcType: 0, //活动类型 1普通 2线下 3线上 
+						isComplete: 0, //是否完赛
+						showName: "", //显示名称
+						description: "", //描述
+						longitude: 0, //导航起点经度
+						latitude: 0, //导航起点纬度
+					}]
+				},
+
+				introduce: {
+					title: "",
+					content: ""
+				},
+				activityRules: {
+					title: "",
+					content: ""
+				}
+			}
+		},
+		computed: {},
+		onLoad(query) { // 类型非必填,可自动推导
+			// console.log(query);
+			this.queryObj = query;
+			this.queryString = tools.objectToQueryString(this.queryObj);
+			// console.log(queryString);
+			this.token = query["token"] ?? token;
+			this.ecId = query["id"] ?? 0;
+
+			cardfunc.init(this, this.token, this.ecId);
+			cardfunc.getCardConfig(this.cardConfigQueryCallback, localCardConfig);
+		},
+		// 页面初次渲染完成,此时组件已挂载完成,DOM 树($el)已可用
+		onReady() {},
+		onUnload() {},
+		methods: {
+			// 获取网格序号 1-9
+			getCellOrderNum(rowId, colId) {
+				return (rowId - 1) * this.grid.widthNum + colId;
+			},
+			getGridStyle() {
+				let style = "";
+				// 网格赛事状态 1 未开始 2 进行中 3 已完成
+				// if (this.grid.state >= 3) {
+				// 	style = `background-image: url("${this.grid.actualImgPic}");`;
+				// } else {
+					style = `background-image: url("${this.grid.maskImgPic}");`;
+				// }
+				// console.log("getGridStyle", style);
+				return style;
+			},
+			getGridRowStyle() {
+				let style = "";
+				let height = "";
+				if (this.grid.heightNum == 1) {
+					height = "98%";
+				} else if (this.grid.heightNum == 2) {
+					height = "49%";
+				} else if (this.grid.heightNum == 3) {
+					height = "32.5%";
+				} else if (this.grid.heightNum == 4) {
+					height = "24.2%";
+				}
+				style = `height: ${height};`;
+				// console.log("getGridRowStyle", style);
+				return style;
+			},
+			getGridCellStyle(rowId, colId) {
+				let style = "";
+				let width = "";
+				let pos = "";
+				const orderNum = this.getCellOrderNum(rowId, colId);
+				const rs = tools.objArrGetObjByValue(this.grid.detailRs, "orderNum", orderNum);
+				// console.log("[getGridCellStyle] rs ", rs);
+
+				if (this.grid.widthNum == 1) {
+					width = "98%";
+					pos = "100%";
+				} else if (this.grid.widthNum == 2) {
+					width = "49%";
+					pos = "100%";
+				} else if (this.grid.widthNum == 3) {
+					width = "32.5%";
+					pos = "50%";
+				} else if (this.grid.widthNum == 4) {
+					width = "24.2%";
+					pos = "33.33%";
+				}
+				style = `width: ${width};`;
+
+				// if (this.grid.state < 3 && rs && rs.isComplete) {
+				if (rs && rs.isComplete) {
+					style += `background-image: url("${this.grid.actualImgPic}");`;
+					style += `background-position: calc(${pos} * ${colId-1}) calc(${pos} * ${rowId-1});`;
+				}
+				// console.log("getGridCellStyle", style);
+
+				return style;
+			},
+			cardConfigQueryCallback(cardconfig) {
+				this.loadConfig(cardconfig);
+				this.gridsQuery();
+			},
+			loadConfig(cardconfig) {
+				cardconfig = cardfunc.parseCardConfig(cardconfig);
+				// console.log("[loadCardConfig] cardconfig:", cardconfig);
+
+				// 加载卡片通用配置
+				if (cardconfig.common != undefined) {
+					cardfunc.loadCardCommonConfig(cardconfig.common);
+				}
+
+				// -------- 加载当前页面的配置 --------
+
+				const config = cardfunc.parseCardConfig(cardconfig[this.pageName]);
+				// console.log("[loadConfig] config_page:", config);
+				if (config == undefined || config == null) {
+					this.pageReady = true;
+					return;
+				}
+
+				// 加载CSS样式
+				const css = config.css;
+				if (css != undefined && css.length > 0) {
+					tools.loadCssCode(css);
+				}
+
+				// 加载介绍内容
+				const introduce = config.introduce;
+				if (introduce != undefined) {
+					if (introduce.title != undefined) {
+						this.introduce.title = introduce.title;
+					}
+					if (introduce.content != undefined) {
+						this.introduce.content = introduce.content;
+					}
+				}
+
+				// 加载活动规则
+				const activityRules = config.activityRules;
+				if (activityRules != undefined) {
+					if (activityRules.title != undefined) {
+						this.activityRules.title = activityRules.title;
+					}
+					if (activityRules.content != undefined) {
+						this.activityRules.content = activityRules.content;
+					}
+				}
+				
+				this.pageReady = true;
+			},
+			// 网格卡片信息查询
+			gridsQuery() {
+				uni.request({
+					url: apiGridsQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token
+					},
+					method: "POST",
+					data: {
+						ecId: this.ecId
+					},
+					success: (res) => {
+						// console.log("gridsQuery", res);
+						const data = res.data.data;
+						this.compId = data.compId,
+							this.compName = data.compName;
+						this.grid.widthNum = data.widthNum;
+						this.grid.heightNum = data.heightNum;
+						this.grid.maskImgPic = data.maskImgPic;
+						this.grid.actualImgPic = data.actualImgPic;
+						this.grid.state = data.state;
+						this.grid.detailRs = data.detailRs;
+
+						this.getUserJoinCardQuery();
+					},
+					fail: (err) => {
+						console.log("gridsQuery err", err)
+					},
+				});
+			},
+			// 用户是否已经报名卡片对应赛事查询
+			getUserJoinCardQuery() {
+				uni.request({
+					url: apiUserJoinCardQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token
+					},
+					method: "POST",
+					data: {
+						ecId: this.ecId
+					},
+					success: (res) => {
+						// console.log("getUserJoinCardQuery", res)
+						if (checkResCode(res)) {
+							const data = res.data.data;
+							this.isJoin = data.isJoin;
+							if (!this.isJoin) { // 未报名,则自动给用户报名
+								this.onlineMcSignUp();
+							}
+						}
+					},
+					fail: (err) => {
+						console.log("getUserJoinCardQuery err", err)
+					},
+				});
+			},
+			// 线上赛报名
+			onlineMcSignUp() {
+				uni.request({
+					url: apiOnlineMcSignUp,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {
+						mcId: this.compId,
+						// coiId: 0,
+						// selectTeam: 0,
+						// nickName: ''
+					},
+					success: (res) => {
+						// console.log("onlineMcSignUp", res);
+
+						if (checkResCode(res)) {
+							// uni.showToast({
+							// 	title: '比赛报名成功!',
+							// 	icon: 'none',
+							// 	duration: 3000
+							// });
+						}
+					},
+					fail: (err) => {
+						console.log("onlineMcSignUp err", err);
+						uni.showToast({
+							title: '出错了,报名失败',
+							icon: 'none',
+							duration: 3000
+						});
+					},
+				});
+			},
+			onCellClick(rowId, colId) {
+				this.sltCellOrderNum = this.getCellOrderNum(rowId, colId);
+				this.sltCellRs = tools.objArrGetObjByValue(this.grid.detailRs, "orderNum", this.sltCellOrderNum);
+				if (this.sltCellRs && this.sltCellRs.ocaId > 0) {
+					this.popupDataList = [{
+						"type": 11,
+						"data": {
+							"title": this.sltCellRs.showName,
+							"img": this.sltCellRs.popupImg,
+							"content": this.sltCellRs.description,
+							"point": {
+								"longitude": this.sltCellRs.longitude,
+								"latitude": this.sltCellRs.latitude,
+								"name": this.sltCellRs.showName
+							}
+						}
+					}];
+					this.$nextTick(() => {
+						this.$refs.mypopup.popupOpen();
+					});
+				} else {
+					uni.showToast({
+						icon: "none",
+						title: "该格子未绑定赛事活动"
+					})
+				}
+			},
+			startGame() {
+				if (this.sltCellRs) {
+					const url = `action://to_detail/?id=${this.sltCellRs.ocaId}&matchType=${this.sltCellRs.mcType}`;
+					tools.appAction(url);
+				}
+			},
+			btnBack() {
+				const url = `action://to_home/`;
+				tools.appAction(url);
+			},
+			btnInfo() {
+				this.$refs.mypopup.popupOpen();
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.content {
+		width: 100vw;
+		min-height: 100vh;
+	}
+
+	.page-top {
+		width: 100%;
+		height: 60px;
+		padding-top: 36px;
+		justify-content: space-between;
+		background: linear-gradient(135deg, #FFEF9E 0%, #FFFCF2 45.14%, #FFF4D4 100%);
+	}
+
+	.topbar-color {
+		color: #000000;
+	}
+
+	.main {
+		width: 88%;
+		min-height: 300px;
+		justify-content: space-around;
+	}
+
+	.mt-content {
+		margin: 20px 0px;
+		font-size: 20px;
+		font-weight: 400;
+		color: #333333;
+	}
+
+	.grid {
+		width: 90vw;
+		height: 90vw;
+		border-radius: 10px;
+		border: #FFC300 solid 10px;
+		background-repeat: no-repeat;
+		background-position: 2px 2px;
+		background-size: calc(90vw - 4px) calc(90vw - 4px);
+	}
+
+	.grid-row {
+		width: 100%;
+	}
+
+	.grid-cell {
+		position: relative;
+		height: 100%;
+		background-repeat: no-repeat;
+		background-size: calc(90vw - 4px) calc(90vw - 4px);
+	}
+
+	.cell-name {
+		position: absolute;
+		left: 5px;
+		bottom: 2px;
+		font-size: 12px;
+		font-weight: 400;
+	}
+
+	.cell-name-uncomplete {
+		color: #CF6B00;
+	}
+
+	.cell-name-complete {
+		color: #CF6B00;
+	}
+		
+	.introduce {
+		width: 96%;
+		margin-top: 10px;
+		margin-bottom: 10px;
+		align-items: flex-start;
+		justify-content: space-around;
+	}
+
+	.introduce-title {
+		color: #333333;
+		font-size: 20px;
+		line-height: 50px;
+		font-family: Source Han Sans CN;
+	}
+
+	.introduce-content {
+		color: #333333;
+		font-size: 16px;
+		line-height: 23px;
+		font-family: Source Han Sans CN;
+	}
+	
+	.activityRules {
+		width: 96%;
+		margin-top: 5px;
+		margin-bottom: 10px;
+		padding: 10px 15px;
+		align-items: flex-start;
+		justify-content: space-around;
+		border-radius: 9px;
+		background: #FAF5E6;
+	}
+	
+	.activityRules-title {
+		color: #333333;
+		font-size: 16px;
+		line-height: 25px;
+		font-weight: 500;
+		font-family: Source Han Sans CN;
+	}
+	
+	.activityRules-content {
+		color: #333333;
+		font-size: 14px;
+		line-height: 23px;
+		font-family: Source Han Sans CN;
+	}
+</style>

+ 277 - 0
card/pages/game/grid/index.vue

@@ -0,0 +1,277 @@
+<!-- 
+[游戏] 网格赛事
+http://localhost:5173/card/#/pages/game/grid/index
+https://oss-mbh5.colormaprun.com/card/#/pages/game/grid/index
+ -->
+<template>
+	<view class="body body-radius">
+		<view v-if="pageReady" class="content content-bg" @click="btnClick">
+			<view class="card-top uni-row">
+				<view class="top-right uni-row">
+					<image mode="aspectFit" class="clock" src="/static/default/clock.png"></image>
+					<view class="countdown">{{countdown}}</view>
+				</view>
+			</view>
+			<view class="card-main uni-column">
+				<view class="logo"></view>
+				<view class="uni-row" style="position: relative;">
+					<image v-if="notice" mode="aspectFit" src="/static/common/notice.png" class="notice"></image>
+					<text class="type mod-text">{{type}}</text>
+				</view>
+				<view class="name mod-text">{{ecName}}</view>
+				<button class="button mod-button">{{btnText}}</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import tools from '../../../common/tools';
+	import cardfunc from '../../../common/cardfunc';
+	import { localCardConfig } from "./cardconfig/test.js";
+	import {
+		token,
+		ossUrl,
+		apiCardBaseQuery
+	} from '../../../common/api';
+	
+	export default {
+		data() {
+			return {
+				cardConfigData: cardfunc.cardConfigData,
+				pageReady: false,
+				pageName: "index",
+				queryObj: {},
+				queryString: "",
+				token: "",
+				
+				ecId: 0, // 卡片id
+				ecName: '', // 卡片名称
+				ecDesc: '', // 卡片简介
+				beginSecond: null, // 卡片开始时间戳,单位秒
+				endSecond: null, // 卡片结束时间戳,单位秒
+				secondCardName: '', // 跳转页面名称
+				
+				isFinished: false, // 赛事是否结束
+				countdown: "", // 倒计时
+				interval: null,
+				
+				type: "",
+				btnText: "",
+				notice: false,	// 是否显示(小红点)通知图标
+			}
+		},
+		computed: {},
+		onLoad(query) { // 类型非必填,可自动推导
+			// console.log(query);
+			this.queryObj = query;
+			this.queryString = tools.objectToQueryString(this.queryObj);
+			// console.log(queryString);
+			this.token = query["token"] ?? token;
+			this.ecId = query["id"] ?? 0;
+			
+			this.type = query["type"] ?? "挑战赛";
+			this.btnText = query["btnText"] ?? "进入活动";
+			
+			cardfunc.init(this, this.token, this.ecId);
+			cardfunc.getCardConfig(this.cardConfigQueryCallback, localCardConfig);
+			
+			this.getCardBaseQuery();
+		},
+		onShow() {
+		},
+		onUnload() {
+			this.clear();
+		},
+		methods: {
+			clear() {
+				if (this.interval != null) {
+					clearInterval(this.interval);
+					this.interval = null;
+				}
+			},
+			cardConfigQueryCallback(cardconfig) {
+				this.loadConfig(cardconfig);
+			},
+			loadConfig(cardconfig) {
+				cardconfig = cardfunc.parseCardConfig(cardconfig);
+				// console.log("[loadCardConfig] cardconfig:", cardconfig);
+				
+				// 加载卡片通用配置
+				if (cardconfig.common != undefined) {
+					cardfunc.loadCardCommonConfig(cardconfig.common);
+				}
+				
+				// -------- 加载当前页面的配置 --------
+				
+				const config = cardfunc.parseCardConfig(cardconfig[this.pageName]);
+				// console.log("[loadConfig] config_page:", config);
+				if (config == undefined || config == null) {
+					this.pageReady = true;
+					return;
+				}
+				
+				// 加载CSS样式
+				const css = config.css;
+				if (css != undefined && css.length > 0) {
+					tools.loadCssCode(css);
+				}
+				
+				this.pageReady = true;
+			},
+			// 获取倒计时
+			getCountdown() {
+				// console.log(this.endSecond)
+				if (this.endSecond > 0) {
+					const now = Date.now() / 1000;
+					const dif = this.endSecond - now;
+					// const dif = 3600*24 - 60;
+					if (dif > 0) {
+						this.countdown = "距结束 " + tools.convertSecondsToDHM(dif);
+					} else {
+						this.countdown = "已结束";
+						this.isFinished = true;
+					}
+					// this.countdown = tools.convertSecondsToHMS(dif);
+				} else {
+					this.countdown = "距结束 --天--小时";
+				}
+			},
+			// 卡片基本信息查询
+			getCardBaseQuery() {
+				uni.request({
+					url: apiCardBaseQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {
+						ecId: this.ecId,
+						pageName: this.pageName
+					},
+					success: (res) => {
+						// console.log("getCardBaseQuery", res)
+						const data = res.data.data;
+						
+						this.ecName = data.ecName;
+						this.ecDesc = data.ecDesc;
+						this.beginSecond = data.beginSecond;
+						this.endSecond = data.endSecond;
+						this.secondCardName = data.secondCardName;
+						
+						this.getCountdown();
+						
+						this.clear();
+						this.interval = setInterval(this.getCountdown, 60000);
+					},
+					fail: (err) => {
+						console.log("getCardBaseQuery err", err)
+					},
+				});
+			},
+			btnClick() {
+				const url = `${ossUrl}#/pages/game/grid/grid?${this.queryString}&full=true`;
+				tools.appAction(url);
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.content {
+		width: 100vw;
+		height: 100vh;
+	}
+
+	.content-bg {
+		background: linear-gradient(180deg, #7aedff 0%, #047200 100%);
+		/* background: linear-gradient(180deg, #178bff 0%, #004d9b 100%); */
+		/* background: linear-gradient(180deg, #7aedff 0%, #8d2219 100%); */
+	}
+	
+	.card-top {
+		width: 100%;
+		justify-content: flex-end;
+	}
+
+	.top-right {
+		min-width: 180rpx;
+		height: 80rpx;
+		margin-top: 30rpx;
+		margin-right: 30rpx;
+		padding-left: 16rpx;
+		padding-right: 16rpx;
+		background-color: rgba(0, 0, 0, 0.3);
+		border-radius: 12px;
+	}
+
+	.clock {
+		width: 50rpx;
+		height: 50rpx;
+		margin-right: 12rpx;
+	}
+
+	.countdown {
+		min-width: 120rpx;
+		text-align: center;
+		font-family: Roboto;
+		color: #ffffff;
+		font-size: 46rpx;
+		/* letter-spacing: 2rpx; */
+	}
+
+	.card-main {
+		width: 100%;
+		/* height: 700rpx; */
+		height: 660rpx;
+		margin-top: 20rpx;
+		justify-content: space-evenly;
+	}
+
+	.logo {
+		width: 50vw;
+		height: 50vw;
+		background-image: url('/static/logo/jbs.png');
+		background-repeat: no-repeat;
+		background-position-x: center;
+		background-position-y: center;
+		background-size: contain;
+	}
+
+	.notice {
+		width: 30rpx;
+		height: 30rpx;
+		/* margin-right: 30rpx; */
+		position: absolute;
+		left: -60rpx;
+	}
+	
+	.type {
+		opacity: 60%;
+		/* line-height: 25px; */
+		font-family: Roboto;
+		color: #ffffff;
+		font-size: 40rpx;
+		text-align: center;
+	}
+
+	.name {
+		font-family: Roboto;
+		color: #ffffff;
+		font-size: 50rpx;
+		text-align: center;
+	}
+
+	.button {
+		width: 320rpx;
+		height: 86rpx;
+		margin-top: 30rpx;
+		color: #000000;
+		background: #ffffff;
+		border-radius: 56rpx;
+		font-size: 46rpx;
+		line-height: 80rpx;
+	}
+	
+</style>

+ 50 - 0
card/pages/index/index.vue

@@ -3,14 +3,64 @@
 </template>
 
 <script>
+	import tools from '../../common/tools';
+	import cardfunc from '../../common/cardfunc';
+	import {
+		token,
+		ossUrl
+	} from '../../common/api';
+
 	export default {
 		data() {
 			return {
+				queryObj: {},
+				queryString: "",
+				token: "",
+				ecId: 0, // 卡片id
 			}
 		},
 		onLoad(query) { // 类型非必填,可自动推导
+			console.log("onLoad");
+			console.log(query);
+			this.queryObj = query;
+			this.queryString = tools.objectToQueryString(this.queryObj);
+			// console.log(this.queryString);
+			this.token = query["token"] ?? token;
+			this.ecId = query["id"] ?? 0;
+
+			cardfunc.init(this, this.token, this.ecId);
+			cardfunc.userConfigQuery(this.userConfigQueryCallback);
 		},
 		methods: {
+			userConfigQueryCallback(userconfig) {
+				// console.log("[userConfigQueryCallback] userconfig:", userconfig);
+				userconfig = cardfunc.parseCardConfig(userconfig);
+				const tplTypeId = userconfig.tplInfo.tplTypeId;	// 模板类型ID
+				const ssctId = userconfig.tplInfo.ssctId;	// 模板ID
+				// const styleId = userconfig.tplInfo.styleId;	// 模板样式ID
+				
+				let tplType = "";	// 模板类型
+				let tpl = "";		// 模板
+				if (tplTypeId == 1) {
+					tplType = "tpl";
+					if (ssctId > 0) {
+						tpl = "style" + ssctId;
+					}
+				}
+				
+				if (tplType != "" && tpl != "") {
+					const url = `/pages/${tplType}/${tpl}/index?${this.queryString}`;
+					uni.reLaunch({
+						url: url
+					});
+				} else {
+					uni.showToast({
+						title: `模板参数错误`,
+						icon: 'none',
+						duration: 3000
+					});
+				}
+			},
 		}
 	}
 </script>

+ 34 - 0
card/pages/mytz/cardconfig/test.js

@@ -0,0 +1,34 @@
+export const localCardConfig = `{
+	"common": {
+		"css": "
+			.page-top {
+			}
+		",
+		"popupRuleConfig": {
+			"height": "430px"
+		},
+		"popupRuleList": [{
+				"type": 7,
+				"data": {
+					"title": "挑战赛规则",
+					"content": "<br>① 选任意定向地图,完成4次挑战,你就胜利啦!<br><br>② 挑战成功,会点亮你的电子奖杯!<br><br>③ 定向里程累计5千米,就有一枚奖章等你哦!",
+					"imageList": [{
+						"width": "90%",
+						"height": "100px",
+						"src": "/static/mytz/main_pic.png"
+					}]
+				}
+			}
+		]
+	},
+	"index": {
+	},
+	"rankList": {
+		"rankParam": {
+			"dispArrStr": "grad,mapNum",
+			"tabItems": ["积分排名", "通关场地排名"],
+			"rankTypeList": ["grad", "mapNum"],
+			"rankRsList": ["gradRs", "mapNumRs"]
+		}
+	}
+}`;

+ 2 - 4
card/pages/mytz/index.vue

@@ -173,10 +173,8 @@ https://oss-mbh5.colormaprun.com/card/#/pages/mytz/index
 				});
 			},
 			btnClick() {
-				// uni.reLaunch({
-				// 	url: '/pages/mytz/detail?full=true&' + this.queryString
-				// });
-				const url = `${ossUrl}#/pages/mytz/detail?${this.queryString}&full=true`;
+				// const url = `${ossUrl}#/pages/mytz/detail?${this.queryString}&full=true`;
+				const url = `${ossUrl}#/pages/mytz/rankList?${this.queryString}&full=true`;
 				tools.appAction(url);
 			}
 		}

+ 443 - 0
card/pages/mytz/rankList.vue

@@ -0,0 +1,443 @@
+<!-- 
+每月挑战 - 月排名列表
+http://localhost:5173/card/#/pages/mytz/rankList
+https://oss-mbh5.colormaprun.com/card/#/pages/mytz/rankList
+ -->
+<template>
+	<view class="body">
+		<view v-if="pageReady" class="content uni-column">
+			<view class="page-top uni-column">
+				<my-topbar :mcName="ecName" class="topbar-color" @btnBackClick="btnBack"
+					@btnInfoClick="btnInfo"></my-topbar>
+
+				<view class="topContent uni-row uni-jcse">
+					<view class="tc-month">{{curMonth}}</view>
+					<view class="tc-count uni-column">
+						<text>挑战次数</text>
+						<text>{{realNum}}/{{targetNum}}</text>
+					</view>
+					<view class="tc-cup" :style="getCupStyle('cup')">
+						<view class="cup-gray" :style="getCupStyle('cup-gray')"></view>
+					</view>
+				</view>
+			</view>
+			<view class="main uni-column">
+				<my-tab ref="tab" :tabItems="tabItems" :tabItemsMark="tabItemsMark" :type="0"
+					:initActIndex="configParam.tabInitActIndex" @onTabClick="onTabClick" :fontSize="12"></my-tab>
+
+				<view class="tab-view uni-column">
+					<template v-for="(item, index) in rankRsList" :key="index">
+						<my-ranklist v-show="tabCurrent === index" :rankRs="rankList[item]"
+							:rank-type="rankTypeList[index]"></my-ranklist>
+					</template>
+				</view>
+			</view>
+
+			<my-popup ref="mypopup" :config="cardConfigData.popupRuleConfig"
+				:dataList="cardConfigData.popupRuleList"></my-popup>
+
+		</view>
+	</view>
+</template>
+
+<script>
+	import tools from '/common/tools';
+	import cardfunc from '/common/cardfunc';
+	import {
+		localCardConfig
+	} from "./cardconfig/test.js";
+	import {
+		token,
+		apiCardBaseQuery,
+		apiCurrentMonthlyChallengeQuery,
+		apiMonthRankDetailQuery,
+		checkResCode,
+		checkToken
+	} from '/common/api';
+
+	export default {
+		data() {
+			return {
+				cardConfigData: cardfunc.cardConfigData,
+				pageReady: false,
+				pageName: "rankList",
+				firstEnterKey: 'firstEnter-mytz',
+				rankKey: "rank-mytz",
+				queryObj: {},
+				queryString: "",
+				token: "",
+				tokenValid: false,
+				ecId: 0, // 卡片id
+				ecName: '', // 卡片名称
+				ecDesc: '', // 卡片简介
+
+				month: '', // 月名称
+				realNum: 0, // 实际完赛次数
+				targetNum: 0, // 要求完赛次数
+
+				cupProgress: 100, // 奖杯进度 (100 - 实际进度,初始值为100)
+
+				dispArrStr: "grad,mapNum", // 要显示的集合范围
+				tabItems: ["积分排名", "通关场地排名"],
+				rankTypeList: ["grad", "mapNum"],
+				tabCurrent: 0,
+				tabItemsMark: [],
+				rankRsList: ["gradRs", "mapNumRs"],
+				rankList: [], // 排名列表
+				configParam: {
+					tabInitActIndex: 0
+				}
+			}
+		},
+		computed: {
+			curMonth() {
+				var currentDate = new Date();
+				var currentMonth = currentDate.getMonth() + 1; // 月份从0开始,所以要加1
+				return `${currentMonth}月`;
+			}
+		},
+		onLoad(query) { // 类型非必填,可自动推导
+			// console.log(query);
+			this.queryObj = query;
+			this.queryString = tools.objectToQueryString(this.queryObj);
+			// console.log(queryString);
+			this.token = query["token"] ?? token;
+			this.ecId = query["id"] ?? 0;
+
+			this.firstEnterKey += "-" + this.ecId;
+			console.log("firstEnterKey:", this.firstEnterKey);
+
+			this.rankKey += "-" + this.ecId;
+			console.log("rankKey:", this.rankKey);
+
+			cardfunc.init(this, this.token, this.ecId);
+			cardfunc.getCardConfig(this.cardConfigQueryCallback, localCardConfig);
+		},
+		// 页面初次渲染完成,此时组件已挂载完成,DOM 树($el)已可用
+		onReady() {},
+		onUnload() {},
+		methods: {
+			cardConfigQueryCallback(cardconfig) {
+				this.loadConfig(cardconfig);
+				this.getCardBaseQuery();
+				this.getCurrentMonthlyChallengeQuery();
+				this.monthRankDetailQuery();
+				setTimeout(this.dealFirstEnter, 500);
+			},
+			loadConfig(cardconfig) {
+				cardconfig = cardfunc.parseCardConfig(cardconfig);
+				// console.log("[loadCardConfig] cardconfig:", cardconfig);
+
+				// 加载卡片通用配置
+				if (cardconfig.common != undefined) {
+					cardfunc.loadCardCommonConfig(cardconfig.common);
+				}
+
+				// -------- 加载当前页面的配置 --------
+
+				const config = cardfunc.parseCardConfig(cardconfig[this.pageName]);
+				// console.log("[loadConfig] config_page:", config);
+				if (config == undefined || config == null) {
+					this.pageReady = true;
+					return;
+				}
+
+				// 加载CSS样式
+				const css = config.css;
+				if (css != undefined && css.length > 0) {
+					tools.loadCssCode(css);
+				}
+
+				// 加载成绩参数
+				const rankParam = config.rankParam;
+				if (rankParam != undefined) {
+					if (rankParam.tabItemsMark != undefined) {
+						this.tabItemsMark = rankParam.tabItemsMark;
+					}
+					if (rankParam.dispArrStr != undefined && rankParam.dispArrStr.length > 0) {
+						this.dispArrStr = rankParam.dispArrStr;
+						// console.log("[loadConfig] dispArrStr:", rankParam.dispArrStr);
+					}
+					if (rankParam.tabItems != undefined && rankParam.tabItems.length > 0) {
+						this.tabItems = rankParam.tabItems;
+						// console.log("[loadConfig] tabItems:", rankParam.tabItems);
+					}
+					if (rankParam.rankTypeList != undefined && rankParam.rankTypeList.length > 0) {
+						this.rankTypeList = rankParam.rankTypeList;
+					}
+					if (rankParam.rankRsList != undefined && rankParam.rankRsList.length > 0) {
+						this.rankRsList = rankParam.rankRsList;
+					}
+				}
+				// console.log("[loadConfig] rankParam:", rankParam);
+
+				// 加载页面参数
+				const param = config.param;
+				if (param != undefined) {
+					if (param.tabInitActIndex != undefined && param.tabInitActIndex >= 0) {
+						this.configParam.tabInitActIndex = param.tabInitActIndex;
+						this.tabCurrent = param.tabInitActIndex;
+					}
+				}
+
+				this.pageReady = true;
+			},
+			dealNotice(rank) {
+				// console.log('[dealFirstEnter]');
+				let that = this;
+				uni.getStorage({
+					key: that.rankKey,
+					success: (res) => {
+						console.log('[getStorage]', that.rankKey, res.data);
+						const oldRank = res.data;
+						if (oldRank != rank) {
+							// that.notice = true;
+							that.setRankValue(rank);
+						}
+					},
+					fail: (e) => {
+						console.log('[getStorage] fail', that.rankKey, e);
+						// that.notice = false;
+						that.setRankValue(rank);
+					},
+				})
+			},
+			setRankValue(data) {
+				let that = this;
+				uni.setStorage({
+					key: that.rankKey,
+					data: data,
+					success: () => {
+						console.log('[setStorage] success', that.rankKey, data);
+					},
+					fail: (e) => {
+						console.log('[setStorage] fail', that.rankKey, e);
+					},
+				})
+			},
+			dealFirstEnter() {
+				// console.log('[dealFirstEnter]');
+				let that = this;
+				uni.getStorage({
+					key: that.firstEnterKey,
+					success: (res) => {
+						console.log('[getStorage]', that.firstEnterKey, res.data);
+					},
+					fail: (e) => {
+						console.log('[getStorage] fail', that.firstEnterKey, e);
+						that.btnInfo();
+						that.setFirstEnterValue(true);
+					},
+				})
+			},
+			setFirstEnterValue(data) {
+				let that = this;
+				uni.setStorage({
+					key: that.firstEnterKey,
+					data: data,
+					success: () => {
+						console.log('[setStorage] success', that.firstEnterKey, data);
+					},
+					fail: (e) => {
+						console.log('[setStorage] fail', that.firstEnterKey, e);
+					},
+				})
+			},
+			getCupProgress() {
+				if (this.targetNum > 0 && this.realNum > 0) {
+					if (this.realNum < this.targetNum) {
+						const progress = this.realNum / this.targetNum * 100;
+						this.cupProgress = 100 - progress;
+					} else {
+						this.cupProgress = 0;
+					}
+				} else {
+					this.cupProgress = 100;
+				}
+				// console.log("cupProgress:", this.cupProgress);
+			},
+			getCupStyle(type) {
+				if (!(this.month > 0)) {
+					return '';
+				}
+
+				let group = 1;
+				if (type == 'cup') {
+					return `background-image: url("static/cup/${group}/${this.month}.png")`;
+				} else if (type == 'cup-gray') {
+					return `background-image: url("static/cup/${group}/${this.month}h.png"); height:${this.cupProgress}% ;`;
+				}
+			},
+			// 卡片基本信息查询
+			getCardBaseQuery() {
+				uni.request({
+					url: apiCardBaseQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {
+						ecId: this.ecId
+					},
+					success: (res) => {
+						// console.log("getCardBaseQuery", res);
+						const data = res.data.data;
+						this.ecName = data.ecName;
+						this.ecDesc = data.ecDesc;
+					},
+					fail: (err) => {
+						console.log("getCardBaseQuery err", err);
+					},
+				});
+			},
+			// 玩家当前月挑战记录查询
+			getCurrentMonthlyChallengeQuery() {
+				uni.request({
+					url: apiCurrentMonthlyChallengeQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {},
+					success: (res) => {
+						// console.log("getCurrentMonthlyChallengeQuery", res);
+						if (checkResCode(res)) {
+							if (res.statusCode == 401) { // 未登录
+								this.tokenValid = false;
+							} else {
+								this.tokenValid = true;
+							}
+
+							const data = res.data.data;
+							this.month = data.month;
+							this.realNum = data.realNum;
+							// this.realNum = 2;
+							this.targetNum = data.targetNum;
+
+							this.dealNotice(this.realNum);
+							this.getCupProgress();
+						}
+					},
+					fail: (err) => {
+						console.log("getCurrentMonthlyChallengeQuery err", err);
+					},
+				});
+			},
+			// 月挑战排名查询
+			monthRankDetailQuery() {
+				uni.request({
+					url: apiMonthRankDetailQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {
+						dispArrStr: this.dispArrStr
+					},
+					success: (res) => {
+						// console.log("monthRankDetailQuery", res);
+						const rankdata = res.data.data;
+						this.rankList = rankdata;
+					},
+					fail: (err) => {
+						console.log("monthRankDetailQuery err", err);
+					},
+				});
+			},
+			btnBack() {
+				// window.history.back();
+				const url = `action://to_home/`;
+				tools.appAction(url);
+			},
+			btnInfo() {
+				this.$refs.mypopup.popupOpen();
+			},
+			onTabClick(val) {
+				// console.log("onTabClick: ", val);
+				this.tabCurrent = val;
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.content {
+		width: 100vw;
+		height: 100vh;
+	}
+
+	.page-top {
+		width: 100%;
+		height: 150px;
+		padding-top: 36px;
+		justify-content: space-between;
+		background-image: url("/static/backgroud/top_run.png"), linear-gradient(180deg, #178bff 0%, #004d9b 100%);
+		background-repeat: no-repeat;
+		background-position: 25px 36px, 0px 0px;
+		background-size: auto 150px, contain;
+	}
+
+	.topbar-color {
+		color: #ffffff;
+	}
+
+	.topContent {
+		width: 90%;
+		height: 130px;
+	}
+
+	.tc-month {
+		width: 80px;
+		height: 80px;
+		background: url("/static/mytz/month_bg.png") no-repeat center;
+		background-size: contain;
+		color: #ff870d;
+		font-size: 22px;
+		font-weight: 700;
+		text-align: center;
+		line-height: 100px;
+	}
+
+	.tc-count {
+		font-size: 14px;
+		font-weight: 500;
+		color: #FFFFFF;
+	}
+
+	.tc-cup {
+		width: 70px;
+		height: 70px;
+		border-radius: 3px;
+		border: solid #FFFFFF 1px;
+		background-position-x: center;
+		background-repeat: no-repeat;
+		background-size: 70px auto;
+	}
+
+	.cup-gray {
+		width: 70px;
+		background-position-x: center;
+		background-repeat: no-repeat;
+		background-size: 70px auto;
+		overflow: hidden;
+	}
+
+	.main {
+		width: 100%;
+		flex-grow: 1;
+		justify-content: space-around;
+	}
+
+	.main-tab {
+		width: 90%;
+		margin-top: 20rpx;
+	}
+
+	.tab-view {
+		width: 100%;
+		flex-grow: 1;
+	}
+</style>

+ 127 - 0
card/pages/tpl/style1/cardconfig/pattern1.js

@@ -0,0 +1,127 @@
+export const localCardConfig = `{
+	"common": {
+		"css": "
+			.color-main {
+				color: #ff870e !important;
+			}
+			.bgcolor-main {
+				background-color: #ff870e !important;
+			}
+			.tab-active{
+				background-color: #ff870e !important;
+			}
+			.swiper-item-button {
+				background-color: #ff870e !important;
+			}
+			.uni-swiper-dot-active {
+				background: #ff870e !important;
+			}
+			.topbar-color {
+				color: #000000 !important;
+			}
+			.topbar-rule {
+				color: #FFFFFF;
+				border-radius: 4px;
+				background: #FF870D;
+			}
+			.page-top {
+				height: 150px !important;
+				background-image: url('https://oss-mbh5.colormaprun.com/static/banner/banner1.png') !important;
+			}
+		",
+		"tabActiveColor": "#FF870D",
+		"popupRuleConfig": {
+			"height": "550px"
+		},
+		"popupRuleList": [
+			"default3",
+			{
+				"type": 10,
+				"data": {
+					"title": "视频教程",
+					"video": {
+						"src": "https://oss-mbh5.colormaprun.com/video/定向讲解.mp4",
+						"poster": "static/common/jbbs2.png",
+						"width": "100%",
+						"height": "280px"
+					},
+					"content": "<br>定向赛怎么玩?点击上面的视频就知道啦~"
+				}
+			}
+		],
+		"popupExchgConfig": {
+			"height": "460px"
+		},
+		"popupExchgList": [
+		],
+		"popupMessageConfig": {
+			"height": "500px"
+		},
+		"popupWarnConfig": {
+			"height": "550px"
+		},
+		"popupHelpConfig": {
+			"height": "539px"
+		},
+		"popupHelpList": [
+		]
+	},
+	"index": {
+		"css": ""
+	},
+	"signup": {
+		"css": ""
+	},
+	"rankList": {
+		"css": "
+			.topbtm-name{
+				display: none;
+			}
+			.topbtm-egg{
+				display: none;
+			}
+			.main-bar{
+				background-color: #FFEDDB !important;
+				color: #ff870d !important;
+			}
+		",		
+		"rankParam": {
+			"tabItemsMark": [],
+			"dispArrStr": "totalSysPoint,totalDistance,rightAnswerPer,totalCp,fastSpeed",
+			"tabItems": [
+				"总积分",
+				"总里程",
+				"正确答题",
+				"打点数",
+				"单圈用时"
+			],
+			"rankTypeList": [
+				"totalScore",
+				"totalDistance",
+				"rightAnswerPer",
+				"totalCp",
+				"speed"
+			],
+			"rankRsList": [
+				"totalSysPointRs",
+				"totalDistanceRs",
+				"rightAnswerPerRs",
+				"totalCpRs",
+				"fastSpeedRs"
+			]
+		}
+	},
+	"rankOverview": {
+		"css": "
+			.page-top {
+			}
+			.mid {
+				margin-top: -50px !important;
+			}
+		",
+		"pathListStyle" : {
+			"showLine" : true,
+			"style": "justify-content: center;"
+		}
+	}
+}`;

+ 90 - 0
card/pages/tpl/style1/cardconfig/test_user.js

@@ -0,0 +1,90 @@
+export const localUserConfig = `{
+	"tplInfo": {
+		"tplTypeId": 1,
+		"ssctId": 1,
+		"styleId": 1,
+		"matchLogo": "https://oss-mbh5.colormaprun.com/static/logo/default.png",
+		"matchBanner": ""
+	},
+	"matchInfo": {
+		"compName": "自助开赛测试定向赛",
+		"description": " · 小飞龙定向赛再次来袭!这次有五个场地哟~<br> · 神秘“蛋叔”闪亮登场~<br> · 蛋叔放大招,百味豆换鸡蛋!<br> · 时不可待!冲鸭!<br><br> · 能不能把蛋叔整郁闷,就看你的啦~<br> · 还等啥?火速报名吧!",
+		"rules": "<li>随时参赛、不限完赛次数、起点任选、实时排名 <li>起点 -各途经点 -结束点完整打卡为一次有效完赛",
+		"maxNum": 20,
+		"contactName": "王老师",
+		"phone": "13335116666",
+		"regBeginSecond": "2024-12-28",
+		"regEndSecond": "2024-12-31",
+		"compBeginSecond": "2025-01-01",
+		"compEndSecond": "2025-01-08"
+	},
+	"mapInfo": [
+		{
+			"mapId": 1,
+			"planId": 1,
+			"activityList": [
+				{
+					"ocaId": 10,
+					"matchType": 3,
+					"showName": "泉城广场12点定向赛asdfasdf",
+					"pathImg": "/static/common/baihuagongyuan.png",
+					"point": {
+						"longitude": 117.022194,
+						"latitude": 36.661612,
+						"name": "泉城广场起始点"
+					}
+				},
+				{
+					"ocaId": 11,
+					"matchType": 3,
+					"showName": "泉城广场16点定向赛",
+					"pathImg": "/static/common/aotizhongxin.png",
+					"point": {
+						"longitude": 117.022194,
+						"latitude": 36.661612,
+						"name": "泉城广场起始点"
+					}
+				},
+				{
+					"ocaId": 11,
+					"matchType": 3,
+					"showName": "泉城广场18点定向赛",
+					"pathImg": "/static/common/quanchenggongyuan.png",
+					"point": {
+						"longitude": 117.022194,
+						"latitude": 36.661612,
+						"name": "泉城公园起始点"
+					}
+				}
+			]
+		},
+		{
+			"mapId": 2,
+			"planId": 2,
+			"activityList": [
+				{
+					"ocaId": 10,
+					"matchType": 3,
+					"showName": "泉城广场12点定向赛",
+					"pathImg": "/static/common/baihuagongyuan.png",
+					"point": {
+						"longitude": 117.022194,
+						"latitude": 36.661612,
+						"name": "泉城广场起始点"
+					}
+				},
+				{
+					"ocaId": 11,
+					"matchType": 3,
+					"showName": "泉城广场16点定向赛",
+					"pathImg": "/static/common/aotizhongxin.png",
+					"point": {
+						"longitude": 117.022194,
+						"latitude": 36.661612,
+						"name": "泉城广场起始点"
+					}
+				}
+			]
+		}
+	]
+}`;

+ 69 - 33
card/pages/tpl/style1/index.vue

@@ -28,7 +28,13 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 <script>
 	import tools from '../../../common/tools';
 	import cardfunc from '../../../common/cardfunc';
-	import { localCardConfig } from "./cardconfig/test.js";
+	// import { localCardConfig } from "./cardconfig/test.js";
+	import {
+		localCardConfig
+	} from "./cardconfig/pattern1.js";
+	import {
+		localUserConfig
+	} from "./cardconfig/test_user.js";
 	import {
 		token,
 		ossUrl,
@@ -36,7 +42,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 		apiUserJoinCardQuery,
 		apiMatchRsDetailQuery
 	} from '../../../common/api';
-	
+
 	export default {
 		data() {
 			return {
@@ -47,27 +53,29 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 				queryObj: {},
 				queryString: "",
 				token: "",
-				
+				cardconfig: {}, // 卡片配置
+				userconfig: {}, // 用户配置
+
 				ecId: 0, // 卡片id
 				ecName: '', // 卡片名称
 				ecDesc: '', // 卡片简介
 				beginSecond: null, // 卡片开始时间戳,单位秒
 				endSecond: null, // 卡片结束时间戳,单位秒
 				secondCardName: '', // 跳转页面名称
-				
+
 				isJoin: null, // 是否报名
 				isFinished: false, // 赛事是否结束
-				
+
 				// mcId: 0, // 赛事id
 				// mcType: 0, // 赛事类型 1 普通活动 2 线下赛 3 线上赛
 				// mcName: "", // 赛事名称
-				
+
 				countdown: "", // 倒计时
 				interval: null,
-				
+
 				type: "",
 				btnText: "",
-				notice: false,	// 是否显示(小红点)通知图标
+				notice: false, // 是否显示(小红点)通知图标
 			}
 		},
 		computed: {},
@@ -78,16 +86,16 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 			// console.log(queryString);
 			this.token = query["token"] ?? token;
 			this.ecId = query["id"] ?? 0;
-			
+
 			this.type = query["type"] ?? "锦标赛";
 			this.btnText = query["btnText"] ?? "开始比赛";
-			
+
 			this.rankKey += "-" + this.ecId;
 			console.log("rankKey:", this.rankKey);
-			
+
 			cardfunc.init(this, this.token, this.ecId);
 			cardfunc.getCardConfig(this.cardConfigQueryCallback, localCardConfig);
-			
+
 			this.getCardBaseQuery();
 			this.matchRsDetailQuery();
 		},
@@ -138,33 +146,64 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 				}
 			},
 			cardConfigQueryCallback(cardconfig) {
-				this.loadConfig(cardconfig);
+				this.cardconfig = cardconfig;
+				cardfunc.getUserConfig(this.userConfigQueryCallback, localUserConfig);
 			},
-			loadConfig(cardconfig) {
+			userConfigQueryCallback(userconfig) {
+				this.userconfig = userconfig;
+				this.loadConfig();
+			},
+			loadConfig() {
+				this.loadCardConfig(this.cardconfig);
+				this.loadUserConfig(this.userconfig);
+				this.pageReady = true;
+			},
+			loadCardConfig(cardconfig) {
 				cardconfig = cardfunc.parseCardConfig(cardconfig);
 				// console.log("[loadCardConfig] cardconfig:", cardconfig);
-				
+
 				// 加载卡片通用配置
 				if (cardconfig.common != undefined) {
 					cardfunc.loadCardCommonConfig(cardconfig.common);
 				}
-				
+
 				// -------- 加载当前页面的配置 --------
-				
+
 				const config = cardfunc.parseCardConfig(cardconfig[this.pageName]);
 				// console.log("[loadConfig] config_page:", config);
 				if (config == undefined || config == null) {
-					this.pageReady = true;
 					return;
 				}
-				
+
 				// 加载CSS样式
 				const css = config.css;
 				if (css != undefined && css.length > 0) {
 					tools.loadCssCode(css);
 				}
+			},
+			loadUserConfig(userconfig) {
+				if (!userconfig) {
+					console.log("[loadUserConfig] userconfig 为空");
+					return;
+				}
 				
-				this.pageReady = true;
+				const config = cardfunc.parseCardConfig(userconfig);
+				console.log("[loadUserConfig] userconfig:", config);
+
+				// 加载CSS样式
+				let css = "";
+				const tplInfo = config.tplInfo;
+				if (tplInfo.matchLogo != '') {
+					css = `.logo{
+						width: 80vw !important;
+						height: 40vw !important;
+						background: url('${tplInfo.matchLogo}') no-repeat center !important;
+						background-size: contain !important;
+					}`;
+				}
+				if (css.length > 0) {
+					tools.loadCssCode(css, "css-user");
+				}
 			},
 			// 获取倒计时
 			getCountdown() {
@@ -200,15 +239,15 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 					success: (res) => {
 						// console.log("getCardBaseQuery", res)
 						const data = res.data.data;
-						
+
 						this.ecName = data.ecName;
 						this.ecDesc = data.ecDesc;
 						this.beginSecond = data.beginSecond;
 						this.endSecond = data.endSecond;
 						this.secondCardName = data.secondCardName;
-						
+
 						this.getCountdown();
-						
+
 						this.clear();
 						this.interval = setInterval(this.getCountdown, 60000);
 					},
@@ -273,12 +312,11 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 				});
 			},
 			btnClick() {
-				if (this.isJoin) {	// 已报名
+				if (this.isJoin) { // 已报名
 					const url = `${ossUrl}#/pages/tpl/style1/rankList?${this.queryString}&full=true`;
 					tools.appAction(url);
-				}
-				else {	// 未报名
-					if (!this.isFinished) {	// 赛事未结束
+				} else { // 未报名
+					if (!this.isFinished) { // 赛事未结束
 						if (this.secondCardName == 'rankList') {
 							const url = `${ossUrl}#/pages/tpl/style1/rankList?${this.queryString}&full=true`;
 							tools.appAction(url);
@@ -286,8 +324,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 							const url = `${ossUrl}#/pages/tpl/style1/signup?${this.queryString}&full=true`;
 							tools.appAction(url);
 						}
-					}
-					else {	// 赛事已结束
+					} else { // 赛事已结束
 						const url = `${ossUrl}#/pages/tpl/style1/rankList?${this.queryString}&full=true`;
 						tools.appAction(url);
 					}
@@ -308,7 +345,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 		/* background: linear-gradient(180deg, #178bff 0%, #004d9b 100%); */
 		/* background: linear-gradient(180deg, #7aedff 0%, #8d2219 100%); */
 	}
-	
+
 	.card-top {
 		width: 100%;
 		justify-content: flex-end;
@@ -351,7 +388,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 	.logo {
 		width: 50vw;
 		height: 50vw;
-		background-image: url('/static/logo/jbs.png');
+		background-image: url('https://oss-mbh5.colormaprun.com/static/logo/default.png');
 		background-repeat: no-repeat;
 		background-position-x: center;
 		background-position-y: center;
@@ -365,7 +402,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 		position: absolute;
 		left: -60rpx;
 	}
-	
+
 	.type {
 		opacity: 60%;
 		/* line-height: 25px; */
@@ -392,5 +429,4 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/index
 		font-size: 46rpx;
 		line-height: 80rpx;
 	}
-	
 </style>

+ 704 - 665
card/pages/tpl/style1/rankList.vue

@@ -1,666 +1,705 @@
-<!-- 
-[模板] 样式1 - 排名列表
-http://localhost:5173/card/#/pages/tpl/style1/rankList
-https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/rankList
- -->
-<template>
-	<view class="body">
-		<view v-if="pageReady" class="content uni-column">
-			<view class="uni-column page-top">
-				<my-topbar :mcName="mcName" class="topbar-color" :showMessage="cardConfigData.popupMessageList.length > 0" 
-					@btnBackClick="btnBack" @btnInfoClick="btnInfo" @btnMessageClick="btnMessage"></my-topbar>
-				
-				<view class="topbtm uni-row">
-					<!-- <view class="topbtm-egg bgcolor-main" @click="btnMyEgg"></view> -->
-					<view class="topbtm-egg" @click="btnMyEgg">{{configParam.labelTicketName}}</view>
-					<text class="topbtm-name bgcolor-main">{{nickName}}</text>
-					<!-- <view class="topbtm-egg bgcolor-main" @click="btnExchg">兑换地址</view> -->
-					<view class="topbtm-egg" v-if="configParam.labelGoodsList && configParam.labelGoodsList.length > 0" @click="btnGoodsList">{{configParam.labelGoodsList}}</view>
-					<view class="topbtm-egg" v-else @click="btnExchg">{{configParam.labelAwardAddress}}</view>
-				</view>
-			</view>
-			<view class="main uni-column">
-				<view class="main-bar uni-row uni-jcse">
-					<text>题目输出:{{all_totalAnswerNum}}</text>
-					<text>总里程:{{fmtDistanct(all_totalDistance)}}km</text>
-					<text>总打点:{{all_totalCp}}</text>
-					<text>总百味豆:{{all_totalSysPoint}}</text>
-				</view>
-				<!-- <view>>> {{rankList}}</view> -->
-				<my-tab ref="tab" :tabItems="tabItems" :tabItemsMark="tabItemsMark" :type="0"
-					:initActIndex=configParam.tabInitActIndex @onTabClick="onTabClick" :fontSize="12"></my-tab>
-				
-				<view class="tab-view uni-column">
-					<template v-for="(item, index) in rankRsList" :key="index">
-						<my-ranklist v-show="tabCurrent === index" :rankRs="rankList[item]"
-							:rank-type="rankTypeList[index]"></my-ranklist>
-					</template>
-				</view>
-
-				<button class="btnBack bgcolor-main" @click="btnStartGame">{{btnStartGameText}}</button>
-			</view>
-			
-			<my-popup ref="mypopup" :config="cardConfigData.popupRuleConfig" :dataList="cardConfigData.popupRuleList" :acttime="acttime"></my-popup>
-			<my-popup ref="mypopupExchg" :config="cardConfigData.popupExchgConfig" :dataList="cardConfigData.popupExchgList"></my-popup>
-			<my-popup ref="mypopupMessage" :config="cardConfigData.popupMessageConfig" :dataList="cardConfigData.popupMessageList" @noMoreRemindersClick="onNoMoreRemindersClick"></my-popup>
-			
-		</view>
-	</view>
-</template>
-
-<script>
-	import tools from '/common/tools';
-	import cardfunc from '../../../common/cardfunc';
-	import { localCardConfig } from "./cardconfig/test.js";
-	import { teamName } from '/common/define';
-	import {
-		token,
-		apiMatchRsDetailQuery,
-		apiCardRankDetailQuery,
-		apiCompStatisticQuery,
-		apiIsAllowMcSignUp,
-		apiUserJoinCardQuery,
-		checkResCode
-	} from '/common/api';
-	
-	export default {
-		data() {
-			return {
-				cardConfigData: cardfunc.cardConfigData,
-				pageReady: false,
-				pageName: "rankList",
-				firstEnterKey: 'firstEnter-tpl-style1',
-				rankKey: "rank-tpl-style1",
-				messageKey: "message-tpl-style1",
-				queryObj: {},
-				queryString: "",
-				token: "",
-				ovtype: "",
-
-				ecId: 0, // 卡片id
-				mcId: 0, // 赛事id
-				mcType: 0, // 赛事类型 1 普通活动 2 线下赛 3 线上赛
-				mcName: "", // 赛事名称
-				acttime: "", // 活动时间
-				beginSecond: null, // 活动或赛事开始时间戳,单位秒
-				endSecond: null, // 活动或赛事结束时间戳,单位秒
-				ocaId: 0,	// 关联id,带入到App活动详情页面
-				nickName: "", // 昵称
-				totalNum: null, // 总场次
-				totalDistanct: null, // 总距离,单位米
-				totalDistanctRankNum: null, // 总距离排名
-				totalCp: null, // 总打点数
-				totalCpRankNum: null, // 总打点数排名
-				totalSysPoint: null, // 总百味豆
-				totalSysPointRankNum: null, // 总百味豆排名
-				fastPace: null, // 个人最快配速
-				fastPaceRankNum: null, // 个人最快配速排名
-				// ocaRs: [], // 卡片对应活动集合
-				
-				isJoin: null, // 是否报名
-				btnStartGameText: "",
-				
-				all_totalDistance: 0, // 赛事所有人累计里程,单位米
-				all_totalRightAnswerNum: 0, // 赛事所有人正确答题数(校园文化输出)
-				all_totalAnswerNum: 0, // 赛事所有人答题数(校园文化输出)
-				all_totalCp: 0, // 赛事中所有人打点数
-				all_totalSysPoint: 0, // 赛事中所有人百味豆
-				
-				mcState: 0 ,	// 赛事/活动状态 0: 未开始  1: 进行中  2: 已结束
-				allowMcSignUp: false,	// 是否允许重新分组
-				countdown: "", // 倒计时
-				rankList: {}, // 排名列表
-				interval: null,
-				
-				teamType: 0, // 队伍类型
-				dispArrStr: "totalDistance,totalCp,totalSysPoint,fastPace", // 要显示的集合范围
-				tabItems: ["总里程", "打点数", "百味豆", "配速"],
-				rankTypeList: ["totalDistance", "totalCp", "totalSysPoint", "fastPace"],
-				tabCurrent: 0,
-				tabItemsMark: [{
-					textColor: "#ff6203",
-					icon: "static/common/award.png"
-				}],
-				rankRsList: ["totalDistanceRs", "totalCpRs", "totalSysPointRs", "fastPaceRs"],
-				
-				
-				configParam: {
-					labelTicketName: "我的奖券",
-					labelAwardAddress: "兑奖地址",
-					// labelGoodsList: "兑换商品",
-					tabInitActIndex: 0
-				}
-			}
-		},
-		computed: {},
-		onLoad(query) { // 类型非必填,可自动推导
-			// console.log(query);
-			this.queryObj = query;
-			this.queryString = tools.objectToQueryString(this.queryObj);
-			// console.log(queryString);
-			this.token = query["token"] ?? token;
-			this.ecId = query["id"] ?? 0;
-			this.ovtype = query["ovtype"] ?? "";
-			
-			this.firstEnterKey += "-" + this.ecId;
-			console.log("firstEnterKey:", this.firstEnterKey);
-
-			this.rankKey += "-" + this.ecId;
-			console.log("rankKey:", this.rankKey);
-			
-			this.messageKey += "-" + this.ecId;
-			console.log("messageKey:", this.messageKey);
-			
-			cardfunc.init(this, this.token, this.ecId);
-			cardfunc.getCardConfig(this.cardConfigQueryCallback, localCardConfig);
-			
-			this.dealOvtype();
-		},
-		// 页面初次渲染完成,此时组件已挂载完成,DOM 树($el)已可用
-		onReady() {
-			// this.dealFirstEnter();
-			// this.$refs.mypopupMessage.popupOpen();
-		},
-		onShow() {
-			this.getUserJoinCardQuery();
-		},
-		onUnload() {
-			this.clear();
-		},
-		methods: {
-			dealOvtype() {
-				if (this.ovtype == "totalDistance") {
-					this.tabCurrent = 0;
-				} else if (this.ovtype == "totalCp") {
-					this.tabCurrent = 1;
-				} else if (this.ovtype == "totalSysPoint") {
-					this.tabCurrent = 2;
-				} else if (this.ovtype == "fastPace") {
-					this.tabCurrent = 3;
-				}
-				console.log(`dealOvtype: ${this.ovtype} tabCurrent: ${this.tabCurrent}`);
-			},
-			dealNotice(rank) {
-				// console.log('[dealFirstEnter]');
-				let that = this;
-				uni.getStorage({
-					key: that.rankKey,
-					success: (res) => {
-						// console.log('[getStorage]', that.rankKey, res.data);
-						const oldRank = res.data;
-						if (oldRank != rank) {
-							// that.notice = true;
-							that.setRankValue(rank);
-						}
-					},
-					fail: (e) => {
-						console.log('[getStorage] fail', that.rankKey, e);
-						// that.notice = false;
-						that.setRankValue(rank);
-					},
-				})
-			},
-			setRankValue(data) {
-				let that = this;
-				uni.setStorage({
-					key: that.rankKey,
-					data: data,
-					success: () => {
-						console.log('[setStorage] success', that.rankKey, data);
-					},
-					fail: (e) => {
-						console.log('[setStorage] fail', that.rankKey, e);
-					},
-				})
-			},
-			dealFirstEnter() {
-				// console.log('[dealFirstEnter]');
-				let that = this;
-				uni.getStorage({
-					key: that.firstEnterKey,
-					success: (res) => {
-						console.log('[getStorage]', that.firstEnterKey, res.data);
-					},
-					fail: (e) => {
-						console.log('[getStorage] fail', that.firstEnterKey, e);
-						that.btnInfo();
-						that.setFirstEnterValue(true);
-					},
-				})
-			},
-			setFirstEnterValue(data) {
-				let that = this;
-				uni.setStorage({
-					key: that.firstEnterKey,
-					data: data,
-					success: () => {
-						console.log('[setStorage] success', that.firstEnterKey, data);
-					},
-					fail: (e) => {
-						console.log('[setStorage] fail', that.firstEnterKey, e);
-					},
-				})
-			},
-			clear() {
-				if (this.interval != null) {
-					clearInterval(this.interval);
-					this.interval = null;
-				}
-			},
-			cardConfigQueryCallback(cardconfig) {
-				this.loadConfig(cardconfig);
-				cardfunc.unReadMessageQuery(this.unReadMessageQueryCallback);
-				this.matchRsDetailQuery();
-				setTimeout(this.dealFirstEnter, 500);
-			},
-			unReadMessageQueryCallback(unReadMessage, mqIdListStr) {
-				// console.log("[unReadMessageQueryCallback] unReadMessage", unReadMessage);
-				if (unReadMessage.length > 0) {
-					this.messageKey += mqIdListStr;
-					// console.log("[unReadMessageQueryCallback] messageKey:", this.messageKey);
-					const messageValue = uni.getStorageSync(this.messageKey);
-					// console.log("[unReadMessageQueryCallback] messageValue:", messageValue);
-					if (!messageValue) {
-						this.$refs.mypopupMessage.popupOpen();
-						// uni.setStorageSync(this.messageKey, true);
-					}
-				}
-			},
-			loadConfig(cardconfig) {
-				cardconfig = cardfunc.parseCardConfig(cardconfig);
-				// console.log("[loadCardConfig] cardconfig:", cardconfig);
-				
-				// 加载卡片通用配置
-				if (cardconfig.common != undefined) {
-					cardfunc.loadCardCommonConfig(cardconfig.common);
-				}
-				
-				// -------- 加载当前页面的配置 --------
-				
-				const config = cardfunc.parseCardConfig(cardconfig[this.pageName]);
-				// console.log("[loadConfig] config_page:", config);
				if (config == undefined || config == null) {
-					this.pageReady = true;
					return;
				}
-				
-				// 加载CSS样式
-				const css = config.css;
-				if (css != undefined && css.length > 0) {
-					tools.loadCssCode(css);
-				}
-				
-				// 加载成绩参数
-				const rankParam = config.rankParam;
-				if (rankParam != undefined) {
-					if (rankParam.tabItemsMark != undefined) {
-						this.tabItemsMark = rankParam.tabItemsMark;
-					}
-					if (rankParam.dispArrStr != undefined && rankParam.dispArrStr.length > 0) {
-						this.dispArrStr = rankParam.dispArrStr;
-						// console.log("[loadConfig] dispArrStr:", rankParam.dispArrStr);
-					}
-					if (rankParam.tabItems != undefined && rankParam.tabItems.length > 0) {
-						this.tabItems = rankParam.tabItems;
-						// console.log("[loadConfig] tabItems:", rankParam.tabItems);
-					}
-					if (rankParam.rankTypeList != undefined && rankParam.rankTypeList.length > 0) {
-						this.rankTypeList = rankParam.rankTypeList;
-					}
-					if (rankParam.rankRsList != undefined && rankParam.rankRsList.length > 0) {
-						this.rankRsList = rankParam.rankRsList;
-					}
-				}
-				// console.log("[loadConfig] rankParam:", rankParam);
-				
-				// 加载页面参数
-				const param = config.param;
-				if (param != undefined) {
-					if (param.labelTicketName != undefined && param.labelTicketName.length > 0) {
-						this.configParam.labelTicketName = param.labelTicketName;
-					}
-					if (param.labelAwardAddress != undefined && param.labelAwardAddress.length > 0) {
-						this.configParam.labelAwardAddress = param.labelAwardAddress;
-					}
-					if (param.labelGoodsList != undefined && param.labelGoodsList.length > 0) {
-						this.configParam.labelGoodsList = param.labelGoodsList;
-					}
-				}
-				
-				this.pageReady = true;
-			},
-			// 获取倒计时
-			getCountdown() {
-				// console.log(this.endSecond)
-				if (this.endSecond > 0) {
-					const now = Date.now() / 1000;
-					const dif = this.endSecond - now;
-					// const dif = 3600*24 - 60;
-					if (dif > 0) {
-						this.countdown = '距结束 ' + tools.convertSecondsToDHM(dif);
-					} else {
-						this.countdown = "活动已结束";
-					}
-					// this.countdown = tools.convertSecondsToHMS(dif);
-				} else {
-					this.countdown = "距结束 --天--小时";
-				}
-			},
-			// 格式化 距离
-			fmtDistanct(val) {
-				return Math.round(val * 100 / 1000) / 100;
-				/* if (val < 10000)
-					return Math.round(val * 10 / 1000) / 10;
-				else
-					return Math.round(val / 1000); */
-			},
-			fmtMcTime(timestamp) {
-				return tools.fmtMcTime(timestamp);
-			},
-			// 获取活动时间
-			getActtime() {
-				this.acttime = tools.getActtime(this.beginSecond, this.endSecond);
-				// console.log("acttime", this.acttime);
-			},
-			getTeamName(teamType, teamIndex) {
-				return teamName[teamType][teamIndex];
-			},
-			// 卡片对应线上赛多个活动查询
-			matchRsDetailQuery() {
-				uni.request({
-					url: apiMatchRsDetailQuery,
-					header: {
-						"Content-Type": "application/x-www-form-urlencoded",
-						"token": this.token,
-					},
-					method: "POST",
-					data: {
-						ecId: this.ecId
-					},
-					success: (res) => {
-						// console.log("matchRsDetailQuery", res);
-						if (checkResCode(res)) {
-							const data = res.data.data;
-							this.mcType = data.mcType;
-							this.mcId = data.mcId;
-							this.mcName = data.mcName;
-							this.beginSecond = data.beginSecond;
-							this.endSecond = data.endSecond;
-							this.nickName = data.nickName;
-							this.totalNum = data.totalNum;
-							this.totalDistanct = data.totalDistanct;
-							this.totalDistanctRankNum = data.totalDistanctRankNum;
-							this.totalCp = data.totalCp;
-							this.totalCpRankNum = data.totalCpRankNum;
-							this.totalSysPoint = data.totalSysPoint;
-							this.totalSysPointRankNum = data.totalSysPointRankNum;
-							this.fastPace = data.fastPace;
-							this.fastPaceRankNum = data.fastPaceRankNum;
-							// this.ocaRs = data.ocaRs;
-				
-							this.mcState = tools.checkMcState(this.beginSecond, this.endSecond);
-				
-							this.getCountdown();
-							this.getActtime();
-							this.compStatisticQuery();
-							this.getCardRankDetailQuery();
-				
-							this.clear();
-							this.interval = setInterval(this.getCountdown, 60000);
-						}
-					},
-					fail: (err) => {
-						console.log("matchRsDetailQuery err", err)
-					},
-				});
-			},
-			// 排名查询
-			getCardRankDetailQuery() {
-				uni.request({
-					url: apiCardRankDetailQuery,
-					header: {
-						"Content-Type": "application/x-www-form-urlencoded",
-						"token": this.token,
-					},
-					method: "POST",
-					data: {
-						mcIdListStr: this.mcId,
-						mcType: this.mcType,
-						dispArrStr: this.dispArrStr
-					},
-					success: (res) => {
-						// console.log("getCardRankDetailQuery", res);
-						const rankdata = res.data.data;
-						this.rankList = rankdata;
-					},
-					fail: (err) => {
-						console.log("getCardRankDetailQuery err", err);
-					},
-				});
-			},
-			// 赛事总成绩统计查询
-			compStatisticQuery() {
-				uni.request({
-					url: apiCompStatisticQuery,
-					header: {
-						"Content-Type": "application/x-www-form-urlencoded",
-						"token": this.token,
-					},
-					method: "POST",
-					data: {
-						mcId: this.mcId
-					},
-					success: (res) => {
-						// console.log("compStatisticQuery", res);
-						if (res.data.code == 0) {
-							const data = res.data.data;
-							this.all_totalDistance = data.totalDistance;
-							this.all_totalRightAnswerNum = data.totalRightAnswerNum;
-							this.all_totalAnswerNum = data.totalAnswerNum;
-							this.all_totalCp = data.totalCp;
-							this.all_totalSysPoint = data.totalSysPoint;
-						}
-					},
-					fail: (err) => {
-						console.log("compStatisticQuery err", err);
-					},
-				});
-			},
-			// 是否允许重新分组(报名)
-			isAllowMcSignUp() {
-				uni.request({
-					url: apiIsAllowMcSignUp,
-					header: {
-						"Content-Type": "application/x-www-form-urlencoded",
-						"token": this.token,
-					},
-					method: "POST",
-					data: {
-						ecId: this.ecId
-					},
-					success: (res) => {
-						// console.log("isAllowMcSignUp", res)
-						if (res.data.code == 0) {
-							const data = res.data.data;
-							this.allowMcSignUp = data.allowSignUp;
-						}
-					},
-					fail: (err) => {
-						console.log("isAllowMcSignUp err", err)
-					},
-				});
-			},
-			// 用户是否已经报名卡片对应赛事查询
-			getUserJoinCardQuery() {
-				uni.request({
-					url: apiUserJoinCardQuery,
-					header: {
-						"Content-Type": "application/x-www-form-urlencoded",
-						"token": this.token
-					},
-					method: "POST",
-					data: {
-						ecId: this.ecId
-					},
-					success: (res) => {
-						// console.log("getUserJoinCardQuery", res)
-						const code = res.data.code;
-						const data = res.data.data;
-						if (code == 0) {
-							this.isJoin = data.isJoin;
-							if (this.isJoin) { // 已报名
-								// this.btnStartGameText = "我要比赛";
-								this.btnStartGameText = "选择场地";
-							} else {	// 未报名
-								this.btnStartGameText = "我要报名";
-							}
-						}
-					},
-					fail: (err) => {
-						console.log("getUserJoinCardQuery err", err)
-					},
-				});
-			},
-			onNoMoreRemindersClick() {
-				this.$refs.mypopupMessage.popupClose();
-				uni.setStorageSync(this.messageKey, true);
-			},
-			btnBack() {
-				const url = `action://to_home/`;
-				tools.appAction(url);
-			},
-			btnStartGame() {
-				if (this.isJoin) {	// 已报名
-					const url = "/pages/tpl/style1/rankOverview?" + this.queryString;
-					tools.appAction(url, "uni.navigateTo");
-				} else {	// 未报名
-					const url = "/pages/tpl/style1/signup?" + this.queryString;
-					tools.appAction(url, "uni.navigateTo");
-				}
-			},
-			btnInfo() {
-				// console.log(this.$refs.mypopup);
-				this.$refs.mypopup.popupOpen();
-			},
-			btnMessage() {
-				// console.log(this.$refs.mypopup);
-				this.$refs.mypopupMessage.popupOpen();
-			},
-			btnMyEgg() {
-				const url = "/pages/achievement/index2?tabCurrent=2&" + this.queryString;
-				tools.appAction(url, "uni.navigateTo");
-			},
-			btnExchg() {
-				this.$refs.mypopupExchg.popupOpen();
-			},
-			btnGoodsList() {
-				this.queryObj.from = "/pages/tpl/style1/rankList";
-				this.queryString = tools.objectToQueryString(this.queryObj);
-				const url = "/pages/exchange/style1/goodsList?" + this.queryString;
-				tools.appAction(url, "uni.navigateTo");
-			},
-			onTabClick(val) {
-				// console.log("onTabClick: ", val);
-				this.tabCurrent = val;
-			}
-		}
-	}
-</script>
-
-<style scoped>
-	.content {
-		width: 100vw;
-		height: 100vh;
-	}
-
-	.page-top {
-		width: 100%;
-		height: 170px;
-		padding-top: 36px;
-		justify-content: space-between;
-		background-image: url("/static/backgroud/top_bg2.png");
-		background-repeat: no-repeat;
-		background-position: center;
-		background-size: cover;
-	}
-
-	.topbar-color {
-		color: #5b9100;
-	}
-	
-	.topbtm {
-		width: 100%;
-		margin-bottom: 5px;
-		justify-content: space-around;
-	}
-	
-	.topbtm-name {
-		max-width: 300rpx;
-		padding: 3px 12px;
-		background-color: #9fda39;
-		border-radius: 5px;
-		text-align: center;
-		font-weight: 500;
-		color: #497400;
-		font-size: 14px;
-		white-space: nowrap;
-		overflow: hidden;
-		text-overflow: ellipsis;
-	}
-	
-	.topbtm-egg {
-		width: 60px;
-		padding: 3px 12px;
-		background-color: #9fda39;
-		border-radius: 50px;
-		text-align: center;
-		color: #497400;
-		font-size: 14px;
-	}
-	
-	.topbtm-null {
-		width: 60px;
-		padding: 3px 12px;
-	}
-	
-	.cal {
-		width: 46rpx;
-		height: 46rpx;
-		margin-right: 20rpx;
-	}
-	
-	.main {
-		width: 100%;
-		flex-grow: 1;
-		justify-content: space-around;
-	}
-	
-	.main-bar {
-		width: 100%;
-		height: 21px;
-		background-color: #d8e8c6;
-		
-		font-size: 10px;
-		font-weight: 500;
-		color: #3d6706;
-	}
-
-	.main-tab {
-		width: 90%;
-		margin-top: 20rpx;
-	}
-
-	.tab-view {
-		width: 100%;
-		flex-grow: 1;
-	}
-
-	.btnBack {
-		width: 70%;
-		height: 80rpx;
-		margin-bottom: 20rpx;
-		color: white;
-		font-size: 32rpx;
-		line-height: 80rpx;
-		border-radius: 27px;
-		background-color: #81cd00;
-	}
-	
+<!-- 
+[模板] 样式1 - 排名列表
+http://localhost:5173/card/#/pages/tpl/style1/rankList
+https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/rankList
+ -->
+<template>
+	<view class="body">
+		<view v-if="pageReady" class="content uni-column">
+			<view class="uni-column page-top">
+				<my-topbar :mcName="mcName" class="topbar-color"
+					:showMessage="cardConfigData.popupMessageList.length > 0" @btnBackClick="btnBack"
+					@btnInfoClick="btnInfo" @btnMessageClick="btnMessage"></my-topbar>
+
+				<view class="topbtm uni-row">
+					<!-- <view class="topbtm-egg bgcolor-main" @click="btnMyEgg"></view> -->
+					<view class="topbtm-egg" @click="btnMyEgg">{{configParam.labelTicketName}}</view>
+					<text class="topbtm-name bgcolor-main">{{nickName}}</text>
+					<!-- <view class="topbtm-egg bgcolor-main" @click="btnExchg">兑换地址</view> -->
+					<view class="topbtm-egg" v-if="configParam.labelGoodsList && configParam.labelGoodsList.length > 0"
+						@click="btnGoodsList">{{configParam.labelGoodsList}}</view>
+					<view class="topbtm-egg" v-else @click="btnExchg">{{configParam.labelAwardAddress}}</view>
+				</view>
+			</view>
+			<view class="main uni-column">
+				<view class="main-bar uni-row uni-jcse">
+					<text>题目输出:{{all_totalAnswerNum}}</text>
+					<text>总里程:{{fmtDistanct(all_totalDistance)}}km</text>
+					<text>总打点:{{all_totalCp}}</text>
+					<text>总百味豆:{{all_totalSysPoint}}</text>
+				</view>
+				<!-- <view>>> {{rankList}}</view> -->
+				<my-tab ref="tab" :tabItems="tabItems" :tabItemsMark="tabItemsMark" :type="0"
+					:initActIndex=configParam.tabInitActIndex @onTabClick="onTabClick" :fontSize="12"></my-tab>
+
+				<view class="tab-view uni-column">
+					<template v-for="(item, index) in rankRsList" :key="index">
+						<my-ranklist v-show="tabCurrent === index" :rankRs="rankList[item]"
+							:rank-type="rankTypeList[index]"></my-ranklist>
+					</template>
+				</view>
+
+				<button class="btnBack bgcolor-main" @click="btnStartGame">{{btnStartGameText}}</button>
+			</view>
+
+			<my-popup ref="mypopup" :config="cardConfigData.popupRuleConfig" :dataList="cardConfigData.popupRuleList"
+				:acttime="acttime"></my-popup>
+			<my-popup ref="mypopupExchg" :config="cardConfigData.popupExchgConfig"
+				:dataList="cardConfigData.popupExchgList"></my-popup>
+			<my-popup ref="mypopupMessage" :config="cardConfigData.popupMessageConfig"
+				:dataList="cardConfigData.popupMessageList" @noMoreRemindersClick="onNoMoreRemindersClick"></my-popup>
+
+		</view>
+	</view>
+</template>
+
+<script>
+	import tools from '/common/tools';
+	import cardfunc from '/common/cardfunc';
+	// import { localCardConfig } from "./cardconfig/test.js";
+	import {
+		localCardConfig
+	} from "./cardconfig/pattern1.js";
+	import {
+		localUserConfig
+	} from "./cardconfig/test_user.js";
+	import {
+		teamName
+	} from '/common/define';
+	import {
+		token,
+		apiMatchRsDetailQuery,
+		apiCardRankDetailQuery,
+		apiCompStatisticQuery,
+		apiIsAllowMcSignUp,
+		apiUserJoinCardQuery,
+		checkResCode
+	} from '/common/api';
+
+	export default {
+		data() {
+			return {
+				cardConfigData: cardfunc.cardConfigData,
+				pageReady: false,
+				pageName: "rankList",
+				firstEnterKey: 'firstEnter-tpl-style1',
+				rankKey: "rank-tpl-style1",
+				messageKey: "message-tpl-style1",
+				queryObj: {},
+				queryString: "",
+				token: "",
+				ovtype: "",
+				cardconfig: {}, // 卡片配置
+				userconfig: {}, // 用户配置
+
+				ecId: 0, // 卡片id
+				mcId: 0, // 赛事id
+				mcType: 0, // 赛事类型 1 普通活动 2 线下赛 3 线上赛
+				mcName: "", // 赛事名称
+				acttime: "", // 活动时间
+				beginSecond: null, // 活动或赛事开始时间戳,单位秒
+				endSecond: null, // 活动或赛事结束时间戳,单位秒
+				ocaId: 0, // 关联id,带入到App活动详情页面
+				nickName: "", // 昵称
+				totalNum: null, // 总场次
+				totalDistanct: null, // 总距离,单位米
+				totalDistanctRankNum: null, // 总距离排名
+				totalCp: null, // 总打点数
+				totalCpRankNum: null, // 总打点数排名
+				totalSysPoint: null, // 总百味豆
+				totalSysPointRankNum: null, // 总百味豆排名
+				fastPace: null, // 个人最快配速
+				fastPaceRankNum: null, // 个人最快配速排名
+				// ocaRs: [], // 卡片对应活动集合
+
+				isJoin: null, // 是否报名
+				btnStartGameText: "",
+
+				all_totalDistance: 0, // 赛事所有人累计里程,单位米
+				all_totalRightAnswerNum: 0, // 赛事所有人正确答题数(校园文化输出)
+				all_totalAnswerNum: 0, // 赛事所有人答题数(校园文化输出)
+				all_totalCp: 0, // 赛事中所有人打点数
+				all_totalSysPoint: 0, // 赛事中所有人百味豆
+
+				mcState: 0, // 赛事/活动状态 0: 未开始  1: 进行中  2: 已结束
+				allowMcSignUp: false, // 是否允许重新分组
+				countdown: "", // 倒计时
+				rankList: {}, // 排名列表
+				interval: null,
+
+				teamType: 0, // 队伍类型
+				dispArrStr: "totalDistance,totalCp,totalSysPoint,fastPace", // 要显示的集合范围
+				tabItems: ["总里程", "打点数", "百味豆", "配速"],
+				rankTypeList: ["totalDistance", "totalCp", "totalSysPoint", "fastPace"],
+				tabCurrent: 0,
+				tabItemsMark: [{
+					textColor: "#ff6203",
+					icon: "static/common/award.png"
+				}],
+				rankRsList: ["totalDistanceRs", "totalCpRs", "totalSysPointRs", "fastPaceRs"],
+
+				configParam: {
+					labelTicketName: "我的奖券",
+					labelAwardAddress: "兑奖地址",
+					// labelGoodsList: "兑换商品",
+					tabInitActIndex: 0
+				}
+			}
+		},
+		computed: {},
+		onLoad(query) { // 类型非必填,可自动推导
+			// console.log(query);
+			this.queryObj = query;
+			this.queryString = tools.objectToQueryString(this.queryObj);
+			// console.log(queryString);
+			this.token = query["token"] ?? token;
+			this.ecId = query["id"] ?? 0;
+			this.ovtype = query["ovtype"] ?? "";
+
+			this.firstEnterKey += "-" + this.ecId;
+			console.log("firstEnterKey:", this.firstEnterKey);
+
+			this.rankKey += "-" + this.ecId;
+			console.log("rankKey:", this.rankKey);
+
+			this.messageKey += "-" + this.ecId;
+			console.log("messageKey:", this.messageKey);
+
+			cardfunc.init(this, this.token, this.ecId);
+			cardfunc.getCardConfig(this.cardConfigQueryCallback, localCardConfig);
+
+			this.dealOvtype();
+		},
+		// 页面初次渲染完成,此时组件已挂载完成,DOM 树($el)已可用
+		onReady() {
+			// this.dealFirstEnter();
+			// this.$refs.mypopupMessage.popupOpen();
+		},
+		onShow() {
+			this.getUserJoinCardQuery();
+		},
+		onUnload() {
+			this.clear();
+		},
+		methods: {
+			dealOvtype() {
+				if (this.ovtype == "totalDistance") {
+					this.tabCurrent = 0;
+				} else if (this.ovtype == "totalCp") {
+					this.tabCurrent = 1;
+				} else if (this.ovtype == "totalSysPoint") {
+					this.tabCurrent = 2;
+				} else if (this.ovtype == "fastPace") {
+					this.tabCurrent = 3;
+				}
+				console.log(`dealOvtype: ${this.ovtype} tabCurrent: ${this.tabCurrent}`);
+			},
+			dealNotice(rank) {
+				// console.log('[dealFirstEnter]');
+				let that = this;
+				uni.getStorage({
+					key: that.rankKey,
+					success: (res) => {
+						// console.log('[getStorage]', that.rankKey, res.data);
+						const oldRank = res.data;
+						if (oldRank != rank) {
+							// that.notice = true;
+							that.setRankValue(rank);
+						}
+					},
+					fail: (e) => {
+						console.log('[getStorage] fail', that.rankKey, e);
+						// that.notice = false;
+						that.setRankValue(rank);
+					},
+				})
+			},
+			setRankValue(data) {
+				let that = this;
+				uni.setStorage({
+					key: that.rankKey,
+					data: data,
+					success: () => {
+						console.log('[setStorage] success', that.rankKey, data);
+					},
+					fail: (e) => {
+						console.log('[setStorage] fail', that.rankKey, e);
+					},
+				})
+			},
+			dealFirstEnter() {
+				// console.log('[dealFirstEnter]');
+				let that = this;
+				uni.getStorage({
+					key: that.firstEnterKey,
+					success: (res) => {
+						console.log('[getStorage]', that.firstEnterKey, res.data);
+					},
+					fail: (e) => {
+						console.log('[getStorage] fail', that.firstEnterKey, e);
+						that.btnInfo();
+						that.setFirstEnterValue(true);
+					},
+				})
+			},
+			setFirstEnterValue(data) {
+				let that = this;
+				uni.setStorage({
+					key: that.firstEnterKey,
+					data: data,
+					success: () => {
+						console.log('[setStorage] success', that.firstEnterKey, data);
+					},
+					fail: (e) => {
+						console.log('[setStorage] fail', that.firstEnterKey, e);
+					},
+				})
+			},
+			clear() {
+				if (this.interval != null) {
+					clearInterval(this.interval);
+					this.interval = null;
+				}
+			},
+			cardConfigQueryCallback(cardconfig) {
+				this.cardconfig = cardconfig;
+				cardfunc.getUserConfig(this.userConfigQueryCallback, localUserConfig);
+			},
+			userConfigQueryCallback(userconfig) {
+				this.userconfig = userconfig;
+				this.loadConfig();
+				cardfunc.unReadMessageQuery(this.unReadMessageQueryCallback);
+				this.matchRsDetailQuery();
+				setTimeout(this.dealFirstEnter, 500);
+			},
+			unReadMessageQueryCallback(unReadMessage, mqIdListStr) {
+				// console.log("[unReadMessageQueryCallback] unReadMessage", unReadMessage);
+				if (unReadMessage.length > 0) {
+					this.messageKey += mqIdListStr;
+					// console.log("[unReadMessageQueryCallback] messageKey:", this.messageKey);
+					const messageValue = uni.getStorageSync(this.messageKey);
+					// console.log("[unReadMessageQueryCallback] messageValue:", messageValue);
+					if (!messageValue) {
+						this.$refs.mypopupMessage.popupOpen();
+						// uni.setStorageSync(this.messageKey, true);
+					}
+				}
+			},
+			loadConfig() {
+				this.loadCardConfig(this.cardconfig);
+				this.loadUserConfig(this.userconfig);
+				this.pageReady = true;
+			},
+			loadCardConfig(cardconfig) {
+				cardconfig = cardfunc.parseCardConfig(cardconfig);
+				// console.log("[loadCardConfig] cardconfig:", cardconfig);
+
+				// 加载卡片通用配置
+				if (cardconfig.common != undefined) {
+					cardfunc.loadCardCommonConfig(cardconfig.common);
+				}
+
+				// -------- 加载当前页面的配置 --------
+
+				const config = cardfunc.parseCardConfig(cardconfig[this.pageName]);
+				// console.log("[loadConfig] config_page:", config);
+				if (config == undefined || config == null) {
+					return;
+				}
+
+				// 加载CSS样式
+				const css = config.css;
+				if (css != undefined && css.length > 0) {
+					tools.loadCssCode(css);
+				}
+
+				// 加载成绩参数
+				const rankParam = config.rankParam;
+				if (rankParam != undefined) {
+					if (rankParam.tabItemsMark != undefined) {
+						this.tabItemsMark = rankParam.tabItemsMark;
+					}
+					if (rankParam.dispArrStr != undefined && rankParam.dispArrStr.length > 0) {
+						this.dispArrStr = rankParam.dispArrStr;
+						// console.log("[loadConfig] dispArrStr:", rankParam.dispArrStr);
+					}
+					if (rankParam.tabItems != undefined && rankParam.tabItems.length > 0) {
+						this.tabItems = rankParam.tabItems;
+						// console.log("[loadConfig] tabItems:", rankParam.tabItems);
+					}
+					if (rankParam.rankTypeList != undefined && rankParam.rankTypeList.length > 0) {
+						this.rankTypeList = rankParam.rankTypeList;
+					}
+					if (rankParam.rankRsList != undefined && rankParam.rankRsList.length > 0) {
+						this.rankRsList = rankParam.rankRsList;
+					}
+				}
+				// console.log("[loadConfig] rankParam:", rankParam);
+
+				// 加载页面参数
+				const param = config.param;
+				if (param != undefined) {
+					if (param.labelTicketName != undefined && param.labelTicketName.length > 0) {
+						this.configParam.labelTicketName = param.labelTicketName;
+					}
+					if (param.labelAwardAddress != undefined && param.labelAwardAddress.length > 0) {
+						this.configParam.labelAwardAddress = param.labelAwardAddress;
+					}
+					if (param.labelGoodsList != undefined && param.labelGoodsList.length > 0) {
+						this.configParam.labelGoodsList = param.labelGoodsList;
+					}
+					if (param.tabInitActIndex != undefined && param.tabInitActIndex >= 0) {
+						this.configParam.tabInitActIndex = param.tabInitActIndex;
+						this.tabCurrent = param.tabInitActIndex;
+					}
+				}
+			},
+			loadUserConfig(userconfig) {
+				if (!userconfig) {
+					console.log("[loadUserConfig] userconfig 为空");
+					return;
+				}
+				
+				const config = cardfunc.parseCardConfig(userconfig);
+				console.log("[loadUserConfig] userconfig:", config);
+				
+				// 加载用户的弹窗数据
+				cardfunc.loadUserPopupRule(config);
+			},
+			// 获取倒计时
+			getCountdown() {
+				// console.log(this.endSecond)
+				if (this.endSecond > 0) {
+					const now = Date.now() / 1000;
+					const dif = this.endSecond - now;
+					// const dif = 3600*24 - 60;
+					if (dif > 0) {
+						this.countdown = '距结束 ' + tools.convertSecondsToDHM(dif);
+					} else {
+						this.countdown = "活动已结束";
+					}
+					// this.countdown = tools.convertSecondsToHMS(dif);
+				} else {
+					this.countdown = "距结束 --天--小时";
+				}
+			},
+			// 格式化 距离
+			fmtDistanct(val) {
+				return Math.round(val * 100 / 1000) / 100;
+				/* if (val < 10000)
+					return Math.round(val * 10 / 1000) / 10;
+				else
+					return Math.round(val / 1000); */
+			},
+			fmtMcTime(timestamp) {
+				return tools.fmtMcTime(timestamp);
+			},
+			// 获取活动时间
+			getActtime() {
+				this.acttime = tools.getActtime(this.beginSecond, this.endSecond);
+				// console.log("acttime", this.acttime);
+			},
+			getTeamName(teamType, teamIndex) {
+				return teamName[teamType][teamIndex];
+			},
+			// 卡片对应线上赛多个活动查询
+			matchRsDetailQuery() {
+				uni.request({
+					url: apiMatchRsDetailQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {
+						ecId: this.ecId
+					},
+					success: (res) => {
+						// console.log("matchRsDetailQuery", res);
+						if (checkResCode(res)) {
+							const data = res.data.data;
+							this.mcType = data.mcType;
+							this.mcId = data.mcId;
+							this.mcName = data.mcName;
+							this.beginSecond = data.beginSecond;
+							this.endSecond = data.endSecond;
+							this.nickName = data.nickName;
+							this.totalNum = data.totalNum;
+							this.totalDistanct = data.totalDistanct;
+							this.totalDistanctRankNum = data.totalDistanctRankNum;
+							this.totalCp = data.totalCp;
+							this.totalCpRankNum = data.totalCpRankNum;
+							this.totalSysPoint = data.totalSysPoint;
+							this.totalSysPointRankNum = data.totalSysPointRankNum;
+							this.fastPace = data.fastPace;
+							this.fastPaceRankNum = data.fastPaceRankNum;
+							// this.ocaRs = data.ocaRs;
+
+							this.mcState = tools.checkMcState(this.beginSecond, this.endSecond);
+
+							this.getCountdown();
+							this.getActtime();
+							this.compStatisticQuery();
+							this.getCardRankDetailQuery();
+
+							this.clear();
+							this.interval = setInterval(this.getCountdown, 60000);
+						}
+					},
+					fail: (err) => {
+						console.log("matchRsDetailQuery err", err)
+					},
+				});
+			},
+			// 排名查询
+			getCardRankDetailQuery() {
+				uni.request({
+					url: apiCardRankDetailQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {
+						mcIdListStr: this.mcId,
+						mcType: this.mcType,
+						dispArrStr: this.dispArrStr
+					},
+					success: (res) => {
+						// console.log("getCardRankDetailQuery", res);
+						const rankdata = res.data.data;
+						this.rankList = rankdata;
+					},
+					fail: (err) => {
+						console.log("getCardRankDetailQuery err", err);
+					},
+				});
+			},
+			// 赛事总成绩统计查询
+			compStatisticQuery() {
+				uni.request({
+					url: apiCompStatisticQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {
+						mcId: this.mcId
+					},
+					success: (res) => {
+						// console.log("compStatisticQuery", res);
+						if (res.data.code == 0) {
+							const data = res.data.data;
+							this.all_totalDistance = data.totalDistance;
+							this.all_totalRightAnswerNum = data.totalRightAnswerNum;
+							this.all_totalAnswerNum = data.totalAnswerNum;
+							this.all_totalCp = data.totalCp;
+							this.all_totalSysPoint = data.totalSysPoint;
+						}
+					},
+					fail: (err) => {
+						console.log("compStatisticQuery err", err);
+					},
+				});
+			},
+			// 是否允许重新分组(报名)
+			isAllowMcSignUp() {
+				uni.request({
+					url: apiIsAllowMcSignUp,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {
+						ecId: this.ecId
+					},
+					success: (res) => {
+						// console.log("isAllowMcSignUp", res)
+						if (res.data.code == 0) {
+							const data = res.data.data;
+							this.allowMcSignUp = data.allowSignUp;
+						}
+					},
+					fail: (err) => {
+						console.log("isAllowMcSignUp err", err)
+					},
+				});
+			},
+			// 用户是否已经报名卡片对应赛事查询
+			getUserJoinCardQuery() {
+				uni.request({
+					url: apiUserJoinCardQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token
+					},
+					method: "POST",
+					data: {
+						ecId: this.ecId
+					},
+					success: (res) => {
+						// console.log("getUserJoinCardQuery", res)
+						const code = res.data.code;
+						const data = res.data.data;
+						if (code == 0) {
+							this.isJoin = data.isJoin;
+							if (this.isJoin) { // 已报名
+								// this.btnStartGameText = "我要比赛";
+								this.btnStartGameText = "选择场地";
+							} else { // 未报名
+								this.btnStartGameText = "我要报名";
+							}
+						}
+					},
+					fail: (err) => {
+						console.log("getUserJoinCardQuery err", err)
+					},
+				});
+			},
+			onNoMoreRemindersClick() {
+				this.$refs.mypopupMessage.popupClose();
+				uni.setStorageSync(this.messageKey, true);
+			},
+			btnBack() {
+				const url = `action://to_home/`;
+				tools.appAction(url);
+			},
+			btnStartGame() {
+				if (this.isJoin) { // 已报名
+					const url = "/pages/tpl/style1/rankOverview?" + this.queryString;
+					tools.appAction(url, "uni.navigateTo");
+				} else { // 未报名
+					const url = "/pages/tpl/style1/signup?" + this.queryString;
+					tools.appAction(url, "uni.navigateTo");
+				}
+			},
+			btnInfo() {
+				// console.log(this.$refs.mypopup);
+				this.$refs.mypopup.popupOpen();
+			},
+			btnMessage() {
+				// console.log(this.$refs.mypopup);
+				this.$refs.mypopupMessage.popupOpen();
+			},
+			btnMyEgg() {
+				const url = "/pages/achievement/index2?tabCurrent=2&" + this.queryString;
+				tools.appAction(url, "uni.navigateTo");
+			},
+			btnExchg() {
+				this.$refs.mypopupExchg.popupOpen();
+			},
+			btnGoodsList() {
+				this.queryObj.from = "/pages/tpl/style1/rankList";
+				this.queryString = tools.objectToQueryString(this.queryObj);
+				const url = "/pages/exchange/style1/goodsList?" + this.queryString;
+				tools.appAction(url, "uni.navigateTo");
+			},
+			onTabClick(val) {
+				// console.log("onTabClick: ", val);
+				this.tabCurrent = val;
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.content {
+		width: 100vw;
+		height: 100vh;
+	}
+
+	.page-top {
+		width: 100%;
+		height: 170px;
+		padding-top: 36px;
+		justify-content: space-between;
+		background-image: url("/static/backgroud/top_bg2.png");
+		background-repeat: no-repeat;
+		background-position: center;
+		background-size: cover;
+	}
+
+	.topbar-color {
+		color: #5b9100;
+	}
+
+	.topbtm {
+		width: 100%;
+		margin-bottom: 5px;
+		justify-content: space-around;
+	}
+
+	.topbtm-name {
+		max-width: 300rpx;
+		padding: 3px 12px;
+		background-color: #9fda39;
+		border-radius: 5px;
+		text-align: center;
+		font-weight: 500;
+		color: #497400;
+		font-size: 14px;
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	.topbtm-egg {
+		width: 60px;
+		padding: 3px 12px;
+		background-color: #9fda39;
+		border-radius: 50px;
+		text-align: center;
+		color: #497400;
+		font-size: 14px;
+	}
+
+	.topbtm-null {
+		width: 60px;
+		padding: 3px 12px;
+	}
+
+	.cal {
+		width: 46rpx;
+		height: 46rpx;
+		margin-right: 20rpx;
+	}
+
+	.main {
+		width: 100%;
+		flex-grow: 1;
+		justify-content: space-around;
+	}
+
+	.main-bar {
+		width: 100%;
+		height: 21px;
+		background-color: #d8e8c6;
+
+		font-size: 10px;
+		font-weight: 500;
+		color: #3d6706;
+	}
+
+	.main-tab {
+		width: 90%;
+		margin-top: 20rpx;
+	}
+
+	.tab-view {
+		width: 100%;
+		flex-grow: 1;
+	}
+
+	.btnBack {
+		width: 70%;
+		height: 80rpx;
+		margin-bottom: 20rpx;
+		color: white;
+		font-size: 32rpx;
+		line-height: 80rpx;
+		border-radius: 27px;
+		background-color: #81cd00;
+	}
 </style>

+ 573 - 539
card/pages/tpl/style1/rankOverview.vue

@@ -1,540 +1,574 @@
-<!-- 
-[模板] 样式1 - 排名总览
-http://localhost:5173/card/#/pages/tpl/style1/rankOverview
-https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/rankOverview
- -->
-<template>
-	<view class="body">
-		<view v-if="pageReady" class="content uni-column">
-			<view class="uni-column page-top">
-				<my-topbar :mcName="mcName" class="topbar-color"
-					@btnBackClick="btnBack" @btnInfoClick="btnInfo"></my-topbar>
-			</view>
-			
-			<view class="mid uni-column uni-jcc">
-				<view class="mid-1 uni-row uni-jcsb">
-					<text>排名:<text style="color: #ff0045;">{{totalSysPointRankNum}}</text></text>
-					<text class="mid-1-name">{{nickName}}</text>
-					<text>场次:<text style="color: #ff0045;">{{totalNum}}</text></text>
-				</view>
-				<view class="mid-2 uni-row uni-jcsa">
-					<view class="uni-column">
-						<text class="mid-2-value">{{totalSysPoint}}</text>
-						<text class="mid-2-text">百味豆</text>
-					</view>
-
-					<view class="mid-line"></view>
-
-					<view class="uni-column">
-						<text class="mid-2-value">{{fmtDistanct(totalDistanct)}}</text>
-						<text class="mid-2-text">里程 km</text>
-					</view>
-
-					<view class="mid-line"></view>
-					<view class="uni-column">
-						<text class="mid-2-value">{{totalCp}}</text>
-						<text class="mid-2-text">打点数</text>
-					</view>
-
-					<view class="mid-line"></view>
-
-					<view class="uni-column">
-						<text class="mid-2-value">{{fmtPace(fastPace)}}</text>
-						<text class="mid-2-text">最快配速</text>
-					</view>
-				</view>
-			</view>
-
-			<view class="main uni-column">
-				<text class="main-title">选择比赛路线</text>
-				<my-pathList ref="myPathList" :style="pathListStyle.style" :pathList="pathList" :mcState="mcState" 
-					:showLine="pathListStyle.showLine" :isNewUser="isNewUser" @onNewUserPathClick="onNewUserPathClick"></my-pathList>
-				<!-- <button class="btnGuide" @click="btnGuide">新手引导</button> -->
-			</view>
-
-			<my-popup ref="mypopup" :config="cardConfigData.popupRuleConfig" :dataList="cardConfigData.popupRuleList" :acttime="acttime"></my-popup>
-			<my-guide ref="myGuide" @popup-close="onGuideClose"></my-guide>
-			<!-- <my-popup-map ref="mypopupmap" :point="navPoint"></my-popup-map> -->
-		</view>
-	</view>
-</template>
-
-<script>
-	import tools from '/common/tools';
-	import cardfunc from '../../../common/cardfunc';
-	import { localCardConfig } from "./cardconfig/test.js";
-	import {
-		token,
-		apiMatchRsDetailQuery,
-		checkResCode
-	} from '/common/api';
-
-	export default {
-		data() {
-			return {
-				cardConfigData: cardfunc.cardConfigData,
-				pageReady: false,
-				// audioSrc: "/static/audio/2.mp3",
-				// audioSrc: "https://oss-mbh5.colormaprun.com/card/static/audio/2.mp3",
-				// audioSrc: "http://t-oss-mbh5.colormaprun.com/card/static/audio/2.mp3",
-				pageName: "rankOverview",
-				firstEnterKey: 'firstEnter-tpl-style1',
-				rankKey: "rank-tpl-style1",
-				queryObj: {},
-				queryString: "",
-				token: "",
-				isNewUser: false,
-
-				ecId: 0, // 卡片id
-				mcId: 0, // 赛事id
-				mcType: 0, // 赛事类型 1 普通活动 2 线下赛 3 线上赛
-				mcName: "", // 赛事名称
-				acttime: "", // 活动时间
-				beginSecond: null, // 活动或赛事开始时间戳,单位秒
-				endSecond: null, // 活动或赛事结束时间戳,单位秒
-				nickName: "", // 昵称
-				totalNum: null, // 总场次
-				totalDistanct: null, // 总距离,单位米
-				totalDistanctRankNum: null, // 总距离排名
-				totalCp: null, // 总打点数
-				totalCpRankNum: null, // 总打点数排名
-				totalSysPoint: null, // 总百味豆
-				totalSysPointRankNum: null, // 总百味豆排名
-				fastPace: null, // 个人最快配速
-				fastPaceRankNum: null, // 个人最快配速排名
-				ocaRs: [], // 卡片对应活动集合
-
-				interval: null,
-				mcState: 0, // 赛事/活动状态 0: 未开始  1: 进行中  2: 已结束
-				pathList: {},
-				pathListStyle: {},
-				selectedPath: null,
-				navPoint: {},
-			}
-		},
-		computed: {
-			pathListLen() {
-				return Object.keys(this.pathList).length;
-			}
-		},
-		onLoad(query) { // 类型非必填,可自动推导
-			// console.log("query:", query);
-			this.queryObj = query;
-			this.queryString = tools.objectToQueryString(this.queryObj);
-			// console.log(queryString);
-			this.token = query["token"] ?? token;
-			this.ecId = query["id"] ?? 0;
-
-			this.firstEnterKey += "-" + this.ecId;
-			console.log("firstEnterKey:", this.firstEnterKey);
-
-			this.rankKey += "-" + this.ecId;
-			console.log("rankKey:", this.rankKey);
-
-			cardfunc.init(this, this.token, this.ecId);
-			cardfunc.getCardConfig(this.cardConfigQueryCallback, localCardConfig);
-			cardfunc.isNewUserQuery(this.isNewUserQueryCallback);
-		},
-		// 页面初次渲染完成,此时组件已挂载完成,DOM 树($el)已可用
-		onReady() {
-			// this.dealFirstEnter();
-		},
-		onShow() {
-		},
-		onUnload() {
-			this.clear();
-		},
-		methods: {
-			dealNotice(rank) {
-				// console.log('[dealFirstEnter]');
-				let that = this;
-				uni.getStorage({
-					key: that.rankKey,
-					success: (res) => {
-						// console.log('[getStorage]', that.rankKey, res.data);
-						const oldRank = res.data;
-						if (oldRank != rank) {
-							// that.notice = true;
-							that.setRankValue(rank);
-						}
-					},
-					fail: (e) => {
-						console.log('[getStorage] fail', that.rankKey, e);
-						// that.notice = false;
-						that.setRankValue(rank);
-					},
-				})
-			},
-			setRankValue(data) {
-				let that = this;
-				uni.setStorage({
-					key: that.rankKey,
-					data: data,
-					success: () => {
-						console.log('[setStorage] success', that.rankKey, data);
-					},
-					fail: (e) => {
-						console.log('[setStorage] fail', that.rankKey, e);
-					},
-				})
-			},
-			dealFirstEnter() {
-				// console.log('[dealFirstEnter]');
-				let that = this;
-				uni.getStorage({
-					key: that.firstEnterKey,
-					success: (res) => {
-						console.log('[getStorage]', that.firstEnterKey, res.data);
-					},
-					fail: (e) => {
-						console.log('[getStorage] fail', that.firstEnterKey, e);
-						that.btnInfo();
-						that.setFirstEnterValue(true);
-					},
-				})
-			},
-			setFirstEnterValue(data) {
-				let that = this;
-				uni.setStorage({
-					key: that.firstEnterKey,
-					data: data,
-					success: () => {
-						console.log('[setStorage] success', that.firstEnterKey, data);
-					},
-					fail: (e) => {
-						console.log('[setStorage] fail', that.firstEnterKey, e);
-					},
-				})
-			},
-			clear() {
-				if (this.interval != null) {
-					clearInterval(this.interval);
-					this.interval = null;
-				}
-			},
-			cardConfigQueryCallback(cardconfig) {
-				this.loadConfig(cardconfig);
-				this.matchRsDetailQuery();
-				setTimeout(this.dealFirstEnter, 500);
-			},
-			isNewUserQueryCallback(isNewUser) {
-				this.isNewUser = isNewUser;
-			},
-			loadConfig(cardconfig) {
-				cardconfig = cardfunc.parseCardConfig(cardconfig);
-				// console.log("[loadCardConfig] cardconfig:", cardconfig);
-				
-				// 加载卡片通用配置
-				if (cardconfig.common != undefined) {
-					cardfunc.loadCardCommonConfig(cardconfig.common);
-				}
-				
-				// -------- 加载当前页面的配置 --------
-				
-				const config = cardfunc.parseCardConfig(cardconfig[this.pageName]);
-				// console.log("[loadConfig] config_page:", config);
				if (config == undefined || config == null) {
-					this.pageReady = true;
					return;
				}
-				
-				// 加载CSS样式
-				const css = config.css;
-				if (css != undefined && css.length > 0) {
-					tools.loadCssCode(css);
-				}
-
-				// 加载比赛路线数据
-				const pathList = config.pathList;
-				// console.log("[loadConfig] pathList:", pathList);
-				if (pathList != undefined) {
-					this.pathList = pathList;
-				}
-				
-				// 加载比赛路线样式
-				const pathListStyle = config.pathListStyle;
-				// console.log("[loadConfig] pathList:", pathList);
-				if (pathListStyle != undefined) {
-					this.pathListStyle = pathListStyle;
-				}
-
-				this.pageReady = true;
-			},
-			// 获取倒计时
-			getCountdown() {
-				// console.log(this.endSecond)
-				if (this.endSecond > 0) {
-					const now = Date.now() / 1000;
-					const dif = this.endSecond - now;
-					// const dif = 3600*24 - 60;
-					if (dif > 0) {
-						this.countdown = '距结束 ' + tools.convertSecondsToDHM(dif);
-					} else {
-						this.countdown = "活动已结束";
-					}
-					// this.countdown = tools.convertSecondsToHMS(dif);
-				} else {
-					this.countdown = "距结束 --天--小时";
-				}
-			},
-			// 格式化 距离
-			fmtDistanct(val) {
-				if (val < 1000)
-					return Math.round(val * 10 / 1000) / 10;
-				else
-					return Math.round(val / 1000);
-			},
-			// 格式化 配速
-			fmtPace(val) {
-				return tools.convertSecondsToHMS(val, 2);
-			},
-			fmtMcTime(timestamp) {
-				return tools.fmtMcTime(timestamp);
-			},
-			// 获取活动时间
-			getActtime() {
-				this.acttime = tools.getActtime(this.beginSecond, this.endSecond);
-			},
-			// 卡片对应线上赛多个活动查询
-			matchRsDetailQuery() {
-				uni.request({
-					url: apiMatchRsDetailQuery,
-					header: {
-						"Content-Type": "application/x-www-form-urlencoded",
-						"token": this.token,
-					},
-					method: "POST",
-					data: {
-						ecId: this.ecId
-					},
-					success: (res) => {
-						// console.log("matchRsDetailQuery", res);
-						if (checkResCode(res)) {
-							const data = res.data.data;
-							this.mcType = data.mcType;
-							this.mcId = data.mcId;
-							this.mcName = data.mcName;
-							this.beginSecond = data.beginSecond;
-							this.endSecond = data.endSecond;
-							this.nickName = data.nickName;
-							this.totalNum = data.totalNum;
-							this.totalDistanct = data.totalDistanct;
-							this.totalDistanctRankNum = data.totalDistanctRankNum;
-							this.totalCp = data.totalCp;
-							this.totalCpRankNum = data.totalCpRankNum;
-							this.totalSysPoint = data.totalSysPoint;
-							this.totalSysPointRankNum = data.totalSysPointRankNum;
-							this.fastPace = data.fastPace;
-							this.fastPaceRankNum = data.fastPaceRankNum;
-							this.ocaRs = data.ocaRs;
-
-							this.mcState = tools.checkMcState(this.beginSecond, this.endSecond);
-
-							const rank = JSON.stringify(data);
-							this.dealNotice(rank);
-
-							this.getCountdown();
-							this.getActtime();
-
-							this.clear();
-							this.interval = setInterval(this.getCountdown, 60000);
-						}
-					},
-					fail: (err) => {
-						console.log("matchRsDetailQuery err", err)
-					},
-				});
-			},
-			btnBack() {
-				const url = "/pages/tpl/style1/rankList?" + this.queryString;
-				tools.appAction(url, "uni.navigateTo");
-			},
-			btnInfo() {
-				// console.log(this.$refs.mypopup);
-				this.$refs.mypopup.popupOpen();
-			},
-			// btnGuide() {
-			// 	this.$refs.myGuide.popupOpen();
-			// },
-			onOverviewClick(ovtype) {
-				this.queryObj.ovtype = ovtype;
-				this.queryString = tools.objectToQueryString(this.queryObj);
-				const url = "/pages/tpl/style1/rankList?" + this.queryString;
-				tools.appAction(url, "uni.navigateTo");
-			},
-			onNewUserPathClick(data) {
-				// console.log("onNewUserPathClick:", data);
-				this.selectedPath = data;
-				this.$refs.myGuide.popupOpen();
-			},
-			onGuideClose() {
-				if (this.isNewUser && this.selectedPath != null) {
-					this.$refs.myPathList.to_detail(this.selectedPath);
-					this.selectedPath = null;
-				}
-			}
-		}
-	}
-</script>
-
-<style scoped>
-	.content {
-		width: 100vw;
-		/* height: 100vh; */
-		overflow-x: scroll;
-	}
-
-	.page-top {
-		position: relative;
-		z-index: 10;
-		width: 100%;
-		height: 270px;
-		padding-top: 36px;
-		justify-content: space-between;
-		background-image: url("/static/backgroud/top_bg_egg2.png");
-		background-repeat: no-repeat;
-		background-position-x: center;
-		background-position-y: center;
-		/* background-position-y: -8px; */
-		/* background-size: 100% 100%; */
-		background-size: cover;
-	}
-
-	.topbar-color {
-		color: #333333;
-	}
-
-	.topbtm {
-		width: 100%;
-		margin-bottom: 40px;
-		justify-content: space-evenly;
-	}
-
-	.topbtm-name {
-		padding: 5px 12px;
-		background-color: #9fda39;
-		border-radius: 5px;
-		/* backdrop-filter: blur(30px); */
-		text-align: center;
-		font-weight: 500;
-		color: #497400;
-		font-size: 14px;
-	}
-
-	.mid {
-		width: 90%;
-		height: 120px;
-		position: relative;
-		z-index: 20;
-		margin-top: -100px;
-		background: #ffffff;
-		border-radius: 9px;
-		box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.13);
-		font-family: Source Han Sans CN;
-	}
-
-	.mid-1 {
-		width: 90%;
-		/* margin: 12px; */
-		margin-bottom: 12px;
-		font-weight: 500;
-		color: #8e8e8e;
-		font-size: 14px;
-	}
-	
-	.mid-1-name {
-		max-width: 310rpx;
-		white-space: nowrap;
-		overflow: hidden;
-		text-overflow: ellipsis;
-	}
-
-	.mid-2 {
-		width: 92%;
-		/* margin: 0 10px; */
-	}
-
-	.mid-2-value {
-		font-weight: 900;
-		font-size: 22px;
-	}
-
-	.mid-2-text {
-		color: #989898;
-		font-size: 12px;
-	}
-
-	.mid-line {
-		width: 0px;
-		height: 45.04px;
-		border: 1px solid;
-		border-color: #e6e6e6;
-	}
-
-	.overview-1 {
-		width: 111px;
-		height: 54px;
-		background: #ffb40b;
-		border-radius: 50%;
-		box-shadow: 3px 3px 0px rgba(140, 140, 140, 1);
-		pointer-events: auto;
-	}
-
-	.overview-2 {
-		margin-top: -43px;
-		color: #ffffff;
-		font-size: 18px;
-		pointer-events: auto;
-	}
-
-	.overview-3 {
-		width: 111px;
-		height: 54px;
-		background: #f39509;
-		border-radius: 50%;
-		box-shadow: 3px 3px 0px rgba(140, 140, 140, 1);
-		pointer-events: auto;
-	}
-
-	.overview-4 {
-		width: 111px;
-		height: 54px;
-		background: #81cd00;
-		border-radius: 50%;
-		box-shadow: 3px 3px 0px rgba(140, 140, 140, 1);
-		pointer-events: auto;
-	}
-
-	.overview-5 {
-		width: 111px;
-		height: 54px;
-		background: #64cbb0;
-		border-radius: 50%;
-		box-shadow: 3px 3px 0px rgba(140, 140, 140, 1);
-		pointer-events: auto;
-	}
-
-	.ovline1 {
-		margin-top: 9px;
-		color: #ffffff;
-		font-size: 12px;
-	}
-
-	.ovline2 {
-		color: #ffffff;
-		font-size: 16px;
-	}
-
-	.main {
-		width: 100%;
-		margin-top: 20px;
-		margin-bottom: 10px;
-		/* height: 70vh; */
-		justify-content: space-around;
-		/* justify-content: space-between; */
-	}
-
-	.main-title {
-		margin-bottom: 10px;
-		font-weight: 550;
-		color: #333333;
-		font-size: 16px;
-	}
+<!-- 
+[模板] 样式1 - 排名总览
+http://localhost:5173/card/#/pages/tpl/style1/rankOverview
+https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/rankOverview
+ -->
+<template>
+	<view class="body">
+		<view v-if="pageReady" class="content uni-column">
+			<view class="uni-column page-top">
+				<my-topbar :mcName="mcName" class="topbar-color" @btnBackClick="btnBack"
+					@btnInfoClick="btnInfo"></my-topbar>
+			</view>
+
+			<view class="mid uni-column uni-jcc">
+				<view class="mid-1 uni-row uni-jcsb">
+					<text>排名:<text style="color: #ff0045;">{{totalSysPointRankNum}}</text></text>
+					<text class="mid-1-name">{{nickName}}</text>
+					<text>场次:<text style="color: #ff0045;">{{totalNum}}</text></text>
+				</view>
+				<view class="mid-2 uni-row uni-jcsa">
+					<view class="uni-column">
+						<text class="mid-2-value">{{totalSysPoint}}</text>
+						<text class="mid-2-text">百味豆</text>
+					</view>
+
+					<view class="mid-line"></view>
+
+					<view class="uni-column">
+						<text class="mid-2-value">{{fmtDistanct(totalDistanct)}}</text>
+						<text class="mid-2-text">里程 km</text>
+					</view>
+
+					<view class="mid-line"></view>
+					<view class="uni-column">
+						<text class="mid-2-value">{{totalCp}}</text>
+						<text class="mid-2-text">打点数</text>
+					</view>
+
+					<view class="mid-line"></view>
+
+					<view class="uni-column">
+						<text class="mid-2-value">{{fmtPace(fastPace)}}</text>
+						<text class="mid-2-text">最快配速</text>
+					</view>
+				</view>
+			</view>
+
+			<view class="main uni-column">
+				<text class="main-title">选择比赛路线</text>
+				<my-pathList ref="myPathList" :style="pathListStyle.style" :pathList="pathList" :mcState="mcState"
+					:showLine="pathListStyle.showLine" :isNewUser="isNewUser"
+					@onNewUserPathClick="onNewUserPathClick"></my-pathList>
+				<!-- <button class="btnGuide" @click="btnGuide">新手引导</button> -->
+			</view>
+
+			<my-popup ref="mypopup" :config="cardConfigData.popupRuleConfig" :dataList="cardConfigData.popupRuleList"
+				:acttime="acttime"></my-popup>
+			<my-guide ref="myGuide" @popup-close="onGuideClose"></my-guide>
+			<!-- <my-popup-map ref="mypopupmap" :point="navPoint"></my-popup-map> -->
+		</view>
+	</view>
+</template>
+
+<script>
+	import tools from '/common/tools';
+	import cardfunc from '../../../common/cardfunc';
+	// import { localCardConfig } from "./cardconfig/test.js";
+	import {
+		localCardConfig
+	} from "./cardconfig/pattern1.js";
+	import {
+		localUserConfig
+	} from "./cardconfig/test_user.js";
+	import {
+		token,
+		apiMatchRsDetailQuery,
+		checkResCode
+	} from '/common/api';
+
+	export default {
+		data() {
+			return {
+				cardConfigData: cardfunc.cardConfigData,
+				pageReady: false,
+				// audioSrc: "/static/audio/2.mp3",
+				// audioSrc: "https://oss-mbh5.colormaprun.com/card/static/audio/2.mp3",
+				// audioSrc: "http://t-oss-mbh5.colormaprun.com/card/static/audio/2.mp3",
+				pageName: "rankOverview",
+				firstEnterKey: 'firstEnter-tpl-style1',
+				rankKey: "rank-tpl-style1",
+				queryObj: {},
+				queryString: "",
+				token: "",
+				isNewUser: false,
+				cardconfig: {}, // 卡片配置
+				userconfig: {}, // 用户配置
+
+				ecId: 0, // 卡片id
+				mcId: 0, // 赛事id
+				mcType: 0, // 赛事类型 1 普通活动 2 线下赛 3 线上赛
+				mcName: "", // 赛事名称
+				acttime: "", // 活动时间
+				beginSecond: null, // 活动或赛事开始时间戳,单位秒
+				endSecond: null, // 活动或赛事结束时间戳,单位秒
+				nickName: "", // 昵称
+				totalNum: null, // 总场次
+				totalDistanct: null, // 总距离,单位米
+				totalDistanctRankNum: null, // 总距离排名
+				totalCp: null, // 总打点数
+				totalCpRankNum: null, // 总打点数排名
+				totalSysPoint: null, // 总百味豆
+				totalSysPointRankNum: null, // 总百味豆排名
+				fastPace: null, // 个人最快配速
+				fastPaceRankNum: null, // 个人最快配速排名
+				ocaRs: [], // 卡片对应活动集合
+
+				interval: null,
+				mcState: 0, // 赛事/活动状态 0: 未开始  1: 进行中  2: 已结束
+				pathList: {},
+				pathListStyle: {},
+				selectedPath: null,
+				navPoint: {},
+			}
+		},
+		computed: {
+			pathListLen() {
+				return Object.keys(this.pathList).length;
+			}
+		},
+		onLoad(query) { // 类型非必填,可自动推导
+			// console.log("query:", query);
+			this.queryObj = query;
+			this.queryString = tools.objectToQueryString(this.queryObj);
+			// console.log(queryString);
+			this.token = query["token"] ?? token;
+			this.ecId = query["id"] ?? 0;
+
+			this.firstEnterKey += "-" + this.ecId;
+			console.log("firstEnterKey:", this.firstEnterKey);
+
+			this.rankKey += "-" + this.ecId;
+			console.log("rankKey:", this.rankKey);
+
+			cardfunc.init(this, this.token, this.ecId);
+			cardfunc.getCardConfig(this.cardConfigQueryCallback, localCardConfig);
+			cardfunc.isNewUserQuery(this.isNewUserQueryCallback);
+		},
+		// 页面初次渲染完成,此时组件已挂载完成,DOM 树($el)已可用
+		onReady() {
+			// this.dealFirstEnter();
+		},
+		onShow() {},
+		onUnload() {
+			this.clear();
+		},
+		methods: {
+			dealNotice(rank) {
+				// console.log('[dealFirstEnter]');
+				let that = this;
+				uni.getStorage({
+					key: that.rankKey,
+					success: (res) => {
+						// console.log('[getStorage]', that.rankKey, res.data);
+						const oldRank = res.data;
+						if (oldRank != rank) {
+							// that.notice = true;
+							that.setRankValue(rank);
+						}
+					},
+					fail: (e) => {
+						console.log('[getStorage] fail', that.rankKey, e);
+						// that.notice = false;
+						that.setRankValue(rank);
+					},
+				})
+			},
+			setRankValue(data) {
+				let that = this;
+				uni.setStorage({
+					key: that.rankKey,
+					data: data,
+					success: () => {
+						console.log('[setStorage] success', that.rankKey, data);
+					},
+					fail: (e) => {
+						console.log('[setStorage] fail', that.rankKey, e);
+					},
+				})
+			},
+			dealFirstEnter() {
+				// console.log('[dealFirstEnter]');
+				let that = this;
+				uni.getStorage({
+					key: that.firstEnterKey,
+					success: (res) => {
+						console.log('[getStorage]', that.firstEnterKey, res.data);
+					},
+					fail: (e) => {
+						console.log('[getStorage] fail', that.firstEnterKey, e);
+						that.btnInfo();
+						that.setFirstEnterValue(true);
+					},
+				})
+			},
+			setFirstEnterValue(data) {
+				let that = this;
+				uni.setStorage({
+					key: that.firstEnterKey,
+					data: data,
+					success: () => {
+						console.log('[setStorage] success', that.firstEnterKey, data);
+					},
+					fail: (e) => {
+						console.log('[setStorage] fail', that.firstEnterKey, e);
+					},
+				})
+			},
+			clear() {
+				if (this.interval != null) {
+					clearInterval(this.interval);
+					this.interval = null;
+				}
+			},
+			cardConfigQueryCallback(cardconfig) {
+				this.cardconfig = cardconfig;
+				cardfunc.getUserConfig(this.userConfigQueryCallback, localUserConfig);
+			},
+			userConfigQueryCallback(userconfig) {
+				this.userconfig = userconfig;
+				this.loadConfig();
+				this.matchRsDetailQuery();
+				setTimeout(this.dealFirstEnter, 500);
+			},
+			loadConfig() {
+				this.loadCardConfig(this.cardconfig);
+				this.loadUserConfig(this.userconfig);
+				this.pageReady = true;
+			},
+			loadCardConfig(cardconfig) {
+				cardconfig = cardfunc.parseCardConfig(cardconfig);
+				// console.log("[loadCardConfig] cardconfig:", cardconfig);
+
+				// 加载卡片通用配置
+				if (cardconfig.common != undefined) {
+					cardfunc.loadCardCommonConfig(cardconfig.common);
+				}
+
+				// -------- 加载当前页面的配置 --------
+
+				const config = cardfunc.parseCardConfig(cardconfig[this.pageName]);
+				// console.log("[loadConfig] config_page:", config);
+				if (config == undefined || config == null) {
+					return;
+				}
+
+				// 加载CSS样式
+				const css = config.css;
+				if (css != undefined && css.length > 0) {
+					tools.loadCssCode(css);
+				}
+
+				// 加载比赛路线数据
+				const pathList = config.pathList;
+				// console.log("[loadConfig] pathList:", pathList);
+				if (pathList != undefined) {
+					this.pathList = pathList;
+				}
+
+				// 加载比赛路线样式
+				const pathListStyle = config.pathListStyle;
+				// console.log("[loadConfig] pathList:", pathList);
+				if (pathListStyle != undefined) {
+					this.pathListStyle = pathListStyle;
+				}
+			},
+			loadUserConfig(userconfig) {
+				if (!userconfig) {
+					console.log("[loadUserConfig] userconfig 为空");
+					return;
+				}
+				
+				const config = cardfunc.parseCardConfig(userconfig);
+				console.log("[loadUserConfig] userconfig:", config);
+				
+				// 加载用户的弹窗数据
+				cardfunc.loadUserPopupRule(config);
+
+				// 加载比赛路线数据
+				this.pathList = cardfunc.getUserPathList(config);
+			},
+			isNewUserQueryCallback(isNewUser) {
+				this.isNewUser = isNewUser;
+			},
+			// 获取倒计时
+			getCountdown() {
+				// console.log(this.endSecond)
+				if (this.endSecond > 0) {
+					const now = Date.now() / 1000;
+					const dif = this.endSecond - now;
+					// const dif = 3600*24 - 60;
+					if (dif > 0) {
+						this.countdown = '距结束 ' + tools.convertSecondsToDHM(dif);
+					} else {
+						this.countdown = "活动已结束";
+					}
+					// this.countdown = tools.convertSecondsToHMS(dif);
+				} else {
+					this.countdown = "距结束 --天--小时";
+				}
+			},
+			// 格式化 距离
+			fmtDistanct(val) {
+				if (val < 1000)
+					return Math.round(val * 10 / 1000) / 10;
+				else
+					return Math.round(val / 1000);
+			},
+			// 格式化 配速
+			fmtPace(val) {
+				return tools.convertSecondsToHMS(val, 2);
+			},
+			fmtMcTime(timestamp) {
+				return tools.fmtMcTime(timestamp);
+			},
+			// 获取活动时间
+			getActtime() {
+				this.acttime = tools.getActtime(this.beginSecond, this.endSecond);
+			},
+			// 卡片对应线上赛多个活动查询
+			matchRsDetailQuery() {
+				uni.request({
+					url: apiMatchRsDetailQuery,
+					header: {
+						"Content-Type": "application/x-www-form-urlencoded",
+						"token": this.token,
+					},
+					method: "POST",
+					data: {
+						ecId: this.ecId
+					},
+					success: (res) => {
+						// console.log("matchRsDetailQuery", res);
+						if (checkResCode(res)) {
+							const data = res.data.data;
+							this.mcType = data.mcType;
+							this.mcId = data.mcId;
+							this.mcName = data.mcName;
+							this.beginSecond = data.beginSecond;
+							this.endSecond = data.endSecond;
+							this.nickName = data.nickName;
+							this.totalNum = data.totalNum;
+							this.totalDistanct = data.totalDistanct;
+							this.totalDistanctRankNum = data.totalDistanctRankNum;
+							this.totalCp = data.totalCp;
+							this.totalCpRankNum = data.totalCpRankNum;
+							this.totalSysPoint = data.totalSysPoint;
+							this.totalSysPointRankNum = data.totalSysPointRankNum;
+							this.fastPace = data.fastPace;
+							this.fastPaceRankNum = data.fastPaceRankNum;
+							this.ocaRs = data.ocaRs;
+
+							this.mcState = tools.checkMcState(this.beginSecond, this.endSecond);
+
+							const rank = JSON.stringify(data);
+							this.dealNotice(rank);
+
+							this.getCountdown();
+							this.getActtime();
+
+							this.clear();
+							this.interval = setInterval(this.getCountdown, 60000);
+						}
+					},
+					fail: (err) => {
+						console.log("matchRsDetailQuery err", err)
+					},
+				});
+			},
+			btnBack() {
+				const url = "/pages/tpl/style1/rankList?" + this.queryString;
+				tools.appAction(url, "uni.navigateTo");
+			},
+			btnInfo() {
+				// console.log(this.$refs.mypopup);
+				this.$refs.mypopup.popupOpen();
+			},
+			// btnGuide() {
+			// 	this.$refs.myGuide.popupOpen();
+			// },
+			onOverviewClick(ovtype) {
+				this.queryObj.ovtype = ovtype;
+				this.queryString = tools.objectToQueryString(this.queryObj);
+				const url = "/pages/tpl/style1/rankList?" + this.queryString;
+				tools.appAction(url, "uni.navigateTo");
+			},
+			onNewUserPathClick(data) {
+				// console.log("onNewUserPathClick:", data);
+				this.selectedPath = data;
+				this.$refs.myGuide.popupOpen();
+			},
+			onGuideClose() {
+				if (this.isNewUser && this.selectedPath != null) {
+					this.$refs.myPathList.to_detail(this.selectedPath);
+					this.selectedPath = null;
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.content {
+		width: 100vw;
+		/* height: 100vh; */
+		overflow-x: scroll;
+	}
+
+	.page-top {
+		position: relative;
+		z-index: 10;
+		width: 100%;
+		height: 270px;
+		padding-top: 36px;
+		justify-content: space-between;
+		background-image: url("/static/backgroud/top_bg_egg2.png");
+		background-repeat: no-repeat;
+		background-position-x: center;
+		background-position-y: center;
+		/* background-position-y: -8px; */
+		/* background-size: 100% 100%; */
+		background-size: cover;
+	}
+
+	.topbar-color {
+		color: #333333;
+	}
+
+	.topbtm {
+		width: 100%;
+		margin-bottom: 40px;
+		justify-content: space-evenly;
+	}
+
+	.topbtm-name {
+		padding: 5px 12px;
+		background-color: #9fda39;
+		border-radius: 5px;
+		/* backdrop-filter: blur(30px); */
+		text-align: center;
+		font-weight: 500;
+		color: #497400;
+		font-size: 14px;
+	}
+
+	.mid {
+		width: 90%;
+		height: 120px;
+		position: relative;
+		z-index: 20;
+		margin-top: -100px;
+		background: #ffffff;
+		border-radius: 9px;
+		box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.13);
+		font-family: Source Han Sans CN;
+	}
+
+	.mid-1 {
+		width: 90%;
+		/* margin: 12px; */
+		margin-bottom: 12px;
+		font-weight: 500;
+		color: #8e8e8e;
+		font-size: 14px;
+	}
+
+	.mid-1-name {
+		max-width: 310rpx;
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	.mid-2 {
+		width: 92%;
+		/* margin: 0 10px; */
+	}
+
+	.mid-2-value {
+		font-weight: 900;
+		font-size: 22px;
+	}
+
+	.mid-2-text {
+		color: #989898;
+		font-size: 12px;
+	}
+
+	.mid-line {
+		width: 0px;
+		height: 45.04px;
+		border: 1px solid;
+		border-color: #e6e6e6;
+	}
+
+	.overview-1 {
+		width: 111px;
+		height: 54px;
+		background: #ffb40b;
+		border-radius: 50%;
+		box-shadow: 3px 3px 0px rgba(140, 140, 140, 1);
+		pointer-events: auto;
+	}
+
+	.overview-2 {
+		margin-top: -43px;
+		color: #ffffff;
+		font-size: 18px;
+		pointer-events: auto;
+	}
+
+	.overview-3 {
+		width: 111px;
+		height: 54px;
+		background: #f39509;
+		border-radius: 50%;
+		box-shadow: 3px 3px 0px rgba(140, 140, 140, 1);
+		pointer-events: auto;
+	}
+
+	.overview-4 {
+		width: 111px;
+		height: 54px;
+		background: #81cd00;
+		border-radius: 50%;
+		box-shadow: 3px 3px 0px rgba(140, 140, 140, 1);
+		pointer-events: auto;
+	}
+
+	.overview-5 {
+		width: 111px;
+		height: 54px;
+		background: #64cbb0;
+		border-radius: 50%;
+		box-shadow: 3px 3px 0px rgba(140, 140, 140, 1);
+		pointer-events: auto;
+	}
+
+	.ovline1 {
+		margin-top: 9px;
+		color: #ffffff;
+		font-size: 12px;
+	}
+
+	.ovline2 {
+		color: #ffffff;
+		font-size: 16px;
+	}
+
+	.main {
+		width: 100%;
+		margin-top: 20px;
+		margin-bottom: 10px;
+		/* height: 70vh; */
+		justify-content: space-around;
+		/* justify-content: space-between; */
+	}
+
+	.main-title {
+		margin-bottom: 10px;
+		font-weight: 550;
+		color: #333333;
+		font-size: 16px;
+	}
 </style>

+ 45 - 6
card/pages/tpl/style1/signup.vue

@@ -49,7 +49,9 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/signup
 <script>
 	import tools from '../../../common/tools';
 	import cardfunc from '../../../common/cardfunc';
-	import { localCardConfig } from "./cardconfig/test.js";
+	// import { localCardConfig } from "./cardconfig/test.js";
+	import { localCardConfig } from "./cardconfig/pattern1.js";
+	import { localUserConfig } from "./cardconfig/test_user.js";
 	import {
 		token,
 		apiCardDetailQuery,
@@ -71,6 +73,9 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/signup
 				queryString: "",
 				from: "", // 来源页面
 				token: "",
+				cardconfig: {}, // 卡片配置
+				userconfig: {}, // 用户配置
+				
 				ecId: 0, // 卡片id
 				mcId: 0, // 赛事id
 				mcType: 0, // 赛事类型 1 普通活动 2 线下赛 3 线上赛
@@ -197,12 +202,22 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/signup
 				}
 			},
 			cardConfigQueryCallback(cardconfig) {
-				this.loadConfig(cardconfig);
+				this.cardconfig = cardconfig;
+				cardfunc.getUserConfig(this.userConfigQueryCallback, localUserConfig);
+			},
+			userConfigQueryCallback(userconfig) {
+				this.userconfig = userconfig;
+				this.loadConfig();
 				setTimeout(this.dealFirstEnter, 500);
 			},
-			loadConfig(cardconfig) {
+			loadConfig() {
+				this.loadCardConfig(this.cardconfig);
+				this.loadUserConfig(this.userconfig);
+				this.pageReady = true;
+			},
+			loadCardConfig(cardconfig) {
 				cardconfig = cardfunc.parseCardConfig(cardconfig);
-				// console.log("[loadCardConfig] cardconfig:", cardconfig);
+				console.log("[loadCardConfig] cardconfig:", cardconfig);
 				
 				// 加载卡片通用配置
 				if (cardconfig.common != undefined) {
@@ -213,7 +228,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/signup
 				
 				const config = cardfunc.parseCardConfig(cardconfig[this.pageName]);
 				// console.log("[loadConfig] config_page:", config);
				if (config == undefined || config == null) {
-					this.pageReady = true;
					return;
				}
+					return;
				}
 
 				// 加载CSS样式
 				const css = config.css;
@@ -247,8 +262,32 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style1/signup
 						this.activityRules.content = activityRules.content;
 					}
 				}
+			},
+			loadUserConfig(userconfig) {
+				if (!userconfig) {
+					console.log("[loadUserConfig] userconfig 为空");
+					return;
+				}
 				
-				this.pageReady = true;
+				const config = cardfunc.parseCardConfig(userconfig);
+				console.log("[loadUserConfig] userconfig:", config);
+				
+				// 加载用户的弹窗数据
+				cardfunc.loadUserPopupRule(config);
+				
+				// 加载介绍内容
+				const introduce = config.matchInfo.description;
+				if (introduce != undefined) {
+					this.introduce.title = "介绍:";
+					this.introduce.content = introduce;
+				}
+				
+				// 加载活动规则
+				const activityRules = config.matchInfo.rules;
+				if (activityRules != undefined) {
+					this.activityRules.title = "活动规则:";
+					this.activityRules.content = activityRules;
+				}
 			},
 			fmtMcTime(timestamp) {
 				return tools.fmtMcTime(timestamp);

+ 1 - 0
card/pages/tpl/style3/index.vue

@@ -41,6 +41,7 @@ https://oss-mbh5.colormaprun.com/card/#/pages/tpl/style3/index
 		data() {
 			return {
 				cardConfigData: cardfunc.cardConfigData,
+				userConfigData: cardfunc.userConfigData,
 				pageReady: false,
 				pageName: "index",
 				rankKey: "rank-tpl-style3",

BIN
card/static/backgroud/grid4_mask.png


BIN
card/static/backgroud/grid9_mask.png


BIN
card/static/backgroud/grid9_mask2.png


BIN
card/static/backgroud/grid_bg.jpg


BIN
card/static/banner/banner1.png


BIN
card/static/common/jbbs3.png


BIN
card/static/ecert/ecert_tpl.jpg


BIN
card/static/logo/building2.png


BIN
card/static/logo/park.png


+ 168 - 0
card/uni_modules/uni-datetime-picker/changelog.md

@@ -0,0 +1,168 @@
+## 2.2.38(2024-10-15)
+- 修复 微信小程序中的getSystemInfo警告
+## 2.2.37(2024-10-12)
+- 修复 微信小程序中的getSystemInfo警告
+## 2.2.36(2024-10-12)
+- 修复 微信小程序中的getSystemInfo警告
+## 2.2.35(2024-09-21)
+- 修复 没有选中日期时点击确定直接报错的Bug [详情](https://ask.dcloud.net.cn/question/198168)
+## 2.2.34(2024-04-24)
+- 新增 日期点击事件,在点击日期时会触发该事件。
+## 2.2.33(2024-04-15)
+- 修复 抖音小程序事件传递失效bug
+## 2.2.32(2024-02-20)
+- 修复 日历的close事件触发异常的bug [详情](https://github.com/dcloudio/uni-ui/issues/844)
+## 2.2.31(2024-02-20)
+- 修复 h5平台 右边日历的月份默认+1的bug [详情](https://github.com/dcloudio/uni-ui/issues/841)
+## 2.2.30(2024-01-31)
+- 修复 隐藏“秒”时,在IOS15及以下版本时出现 结束时间在开始时间之前 的bug [详情](https://github.com/dcloudio/uni-ui/issues/788)
+## 2.2.29(2024-01-20)
+- 新增 show事件,弹窗弹出时触发该事件 [详情](https://github.com/dcloudio/uni-app/issues/4694)
+## 2.2.28(2024-01-18)
+- 去除 noChange事件,当进行日期范围选择时,若只选了一天,则开始结束日期都为同一天 [详情](https://github.com/dcloudio/uni-ui/issues/815)
+## 2.2.27(2024-01-10)
+- 优化 增加noChange事件,当进行日期范围选择时,若有空值,则触发该事件 [详情](https://github.com/dcloudio/uni-ui/issues/815)
+## 2.2.26(2024-01-08)
+- 修复 字节小程序时间选择范围器失效问题 [详情](https://github.com/dcloudio/uni-ui/issues/834)
+## 2.2.25(2023-10-18)
+- 修复 PC端初次修改时间,开始时间未更新的Bug [详情](https://github.com/dcloudio/uni-ui/issues/737)
+## 2.2.24(2023-06-02)
+- 修复 部分情况修改时间,开始、结束时间显示异常的Bug [详情](https://ask.dcloud.net.cn/question/171146)
+- 优化 当前月可以选择上月、下月的日期的Bug
+## 2.2.23(2023-05-02)
+- 修复 部分情况修改时间,开始时间未更新的Bug [详情](https://github.com/dcloudio/uni-ui/issues/737)
+- 修复 部分平台及设备第一次点击无法显示弹框的Bug
+- 修复 ios 日期格式未补零显示及使用异常的Bug [详情](https://ask.dcloud.net.cn/question/162979)
+## 2.2.22(2023-03-30)
+- 修复 日历 picker 修改年月后,自动选中当月1日的Bug [详情](https://ask.dcloud.net.cn/question/165937)
+- 修复 小程序端 低版本 ios NaN的Bug [详情](https://ask.dcloud.net.cn/question/162979)
+## 2.2.21(2023-02-20)
+- 修复 firefox 浏览器显示区域点击无法拉起日历弹框的Bug [详情](https://ask.dcloud.net.cn/question/163362)
+## 2.2.20(2023-02-17)
+- 优化 值为空依然选中当天问题
+- 优化 提供 default-value 属性支持配置选择器打开时默认显示的时间
+- 优化 非范围选择未选择日期时间,点击确认按钮选中当前日期时间
+- 优化 字节小程序日期时间范围选择,底部日期换行的Bug
+## 2.2.19(2023-02-09)
+- 修复 2.2.18 引起范围选择配置 end 选择无效的Bug [详情](https://github.com/dcloudio/uni-ui/issues/686)
+## 2.2.18(2023-02-08)
+- 修复 移动端范围选择change事件触发异常的Bug [详情](https://github.com/dcloudio/uni-ui/issues/684)
+- 优化 PC端输入日期格式错误时返回当前日期时间
+- 优化 PC端输入日期时间超出 start、end 限制的Bug
+- 优化 移动端日期时间范围用法时间展示不完整问题
+## 2.2.17(2023-02-04)
+- 修复 小程序端绑定 Date 类型报错的Bug [详情](https://github.com/dcloudio/uni-ui/issues/679)
+- 修复 vue3 time-picker 无法显示绑定时分秒的Bug
+## 2.2.16(2023-02-02)
+- 修复 字节小程序报错的Bug
+## 2.2.15(2023-02-02)
+- 修复 某些情况切换月份错误的Bug
+## 2.2.14(2023-01-30)
+- 修复 某些情况切换月份错误的Bug [详情](https://ask.dcloud.net.cn/question/162033)
+## 2.2.13(2023-01-10)
+- 修复 多次加载组件造成内存占用的Bug
+## 2.2.12(2022-12-01)
+- 修复 vue3 下 i18n 国际化初始值不正确的Bug
+## 2.2.11(2022-09-19)
+- 修复 支付宝小程序样式错乱的Bug [详情](https://github.com/dcloudio/uni-app/issues/3861)
+## 2.2.10(2022-09-19)
+- 修复 反向选择日期范围,日期显示异常的Bug [详情](https://ask.dcloud.net.cn/question/153401?item_id=212892&rf=false)
+## 2.2.9(2022-09-16)
+- 可以使用 uni-scss 控制主题色
+## 2.2.8(2022-09-08)
+- 修复 close事件无效的Bug
+## 2.2.7(2022-09-05)
+- 修复 移动端 maskClick 无效的Bug [详情](https://ask.dcloud.net.cn/question/140824)
+## 2.2.6(2022-06-30)
+- 优化 组件样式,调整了组件图标大小、高度、颜色等,与uni-ui风格保持一致
+## 2.2.5(2022-06-24)
+- 修复 日历顶部年月及底部确认未国际化的Bug
+## 2.2.4(2022-03-31)
+- 修复 Vue3 下动态赋值,单选类型未响应的Bug
+## 2.2.3(2022-03-28)
+- 修复 Vue3 下动态赋值未响应的Bug
+## 2.2.2(2021-12-10)
+- 修复 clear-icon 属性在小程序平台不生效的Bug
+## 2.2.1(2021-12-10)
+- 修复 日期范围选在小程序平台,必须多点击一次才能取消选中状态的Bug
+## 2.2.0(2021-11-19)
+- 优化 组件UI,并提供设计资源 [详情](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移 [https://uniapp.dcloud.io/component/uniui/uni-datetime-picker](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker)
+## 2.1.5(2021-11-09)
+- 新增 提供组件设计资源,组件样式调整
+## 2.1.4(2021-09-10)
+- 修复 hide-second 在移动端的Bug
+- 修复 单选赋默认值时,赋值日期未高亮的Bug
+- 修复 赋默认值时,移动端未正确显示时间的Bug
+## 2.1.3(2021-09-09)
+- 新增 hide-second 属性,支持只使用时分,隐藏秒
+## 2.1.2(2021-09-03)
+- 优化 取消选中时(范围选)直接开始下一次选择, 避免多点一次
+- 优化 移动端支持清除按钮,同时支持通过 ref 调用组件的 clear 方法
+- 优化 调整字号大小,美化日历界面
+- 修复 因国际化导致的 placeholder 失效的Bug
+## 2.1.1(2021-08-24)
+- 新增 支持国际化
+- 优化 范围选择器在 pc 端过宽的问题
+## 2.1.0(2021-08-09)
+- 新增 适配 vue3
+## 2.0.19(2021-08-09)
+- 新增 支持作为 uni-forms 子组件相关功能
+- 修复 在 uni-forms 中使用时,选择时间报 NAN 错误的Bug
+## 2.0.18(2021-08-05)
+- 修复 type 属性动态赋值无效的Bug
+- 修复 ‘确认’按钮被 tabbar 遮盖 bug
+- 修复 组件未赋值时范围选左、右日历相同的Bug
+## 2.0.17(2021-08-04)
+- 修复 范围选未正确显示当前值的Bug
+- 修复 h5 平台(移动端)报错 'cale' of undefined 的Bug
+## 2.0.16(2021-07-21)
+- 新增 return-type 属性支持返回 date 日期对象
+## 2.0.15(2021-07-14)
+- 修复 单选日期类型,初始赋值后不在当前日历的Bug
+- 新增 clearIcon 属性,显示框的清空按钮可配置显示隐藏(仅 pc 有效)
+- 优化 移动端移除显示框的清空按钮,无实际用途
+## 2.0.14(2021-07-14)
+- 修复 组件赋值为空,界面未更新的Bug
+- 修复 start 和 end 不能动态赋值的Bug
+- 修复 范围选类型,用户选择后再次选择右侧日历(结束日期)显示不正确的Bug
+## 2.0.13(2021-07-08)
+- 修复 范围选择不能动态赋值的Bug
+## 2.0.12(2021-07-08)
+- 修复 范围选择的初始时间在一个月内时,造成无法选择的bug
+## 2.0.11(2021-07-08)
+- 优化 弹出层在超出视窗边缘定位不准确的问题
+## 2.0.10(2021-07-08)
+- 修复 范围起始点样式的背景色与今日样式的字体前景色融合,导致日期字体看不清的Bug
+- 优化 弹出层在超出视窗边缘被遮盖的问题
+## 2.0.9(2021-07-07)
+- 新增 maskClick 事件
+- 修复 特殊情况日历 rpx 布局错误的Bug,rpx -> px
+- 修复 范围选择时清空返回值不合理的bug,['', ''] -> []
+## 2.0.8(2021-07-07)
+- 新增 日期时间显示框支持插槽
+## 2.0.7(2021-07-01)
+- 优化 添加 uni-icons 依赖
+## 2.0.6(2021-05-22)
+- 修复 图标在小程序上不显示的Bug
+- 优化 重命名引用组件,避免潜在组件命名冲突
+## 2.0.5(2021-05-20)
+- 优化 代码目录扁平化
+## 2.0.4(2021-05-12)
+- 新增 组件示例地址
+## 2.0.3(2021-05-10)
+- 修复 ios 下不识别 '-' 日期格式的Bug
+- 优化 pc 下弹出层添加边框和阴影
+## 2.0.2(2021-05-08)
+- 修复 在 admin 中获取弹出层定位错误的bug
+## 2.0.1(2021-05-08)
+- 修复 type 属性向下兼容,默认值从 date 变更为 datetime
+## 2.0.0(2021-04-30)
+- 支持日历形式的日期+时间的范围选择
+ > 注意:此版本不向后兼容,不再支持单独时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker)
+## 1.0.6(2021-03-18)
+- 新增 hide-second 属性,时间支持仅选择时、分
+- 修复 选择跟显示的日期不一样的Bug
+- 修复 chang事件触发2次的Bug
+- 修复 分、秒 end 范围错误的Bug
+- 优化 更好的 nvue 适配

+ 177 - 0
card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue

@@ -0,0 +1,177 @@
+<template>
+	<view class="uni-calendar-item__weeks-box" :class="{
+		'uni-calendar-item--disable':weeks.disable,
+		'uni-calendar-item--before-checked-x':weeks.beforeMultiple,
+		'uni-calendar-item--multiple': weeks.multiple,
+		'uni-calendar-item--after-checked-x':weeks.afterMultiple,
+		}" @click="choiceDate(weeks)" @mouseenter="handleMousemove(weeks)">
+		<view class="uni-calendar-item__weeks-box-item" :class="{
+				'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && (calendar.userChecked || !checkHover),
+				'uni-calendar-item--checked-range-text': checkHover,
+				'uni-calendar-item--before-checked':weeks.beforeMultiple,
+				'uni-calendar-item--multiple': weeks.multiple,
+				'uni-calendar-item--after-checked':weeks.afterMultiple,
+				'uni-calendar-item--disable':weeks.disable,
+				}">
+			<text v-if="selected && weeks.extraInfo" class="uni-calendar-item__weeks-box-circle"></text>
+			<text class="uni-calendar-item__weeks-box-text uni-calendar-item__weeks-box-text-disable uni-calendar-item--checked-text">{{weeks.date}}</text>
+		</view>
+		<view :class="{'uni-calendar-item--today': weeks.isToday}"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			weeks: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			calendar: {
+				type: Object,
+				default: () => {
+					return {}
+				}
+			},
+			selected: {
+				type: Array,
+				default: () => {
+					return []
+				}
+			},
+			checkHover: {
+				type: Boolean,
+				default: false
+			}
+		},
+		methods: {
+			choiceDate(weeks) {
+				this.$emit('change', weeks)
+			},
+			handleMousemove(weeks) {
+				this.$emit('handleMouse', weeks)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" >
+	$uni-primary: #007aff !default;
+
+	.uni-calendar-item__weeks-box {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		margin: 1px 0;
+		position: relative;
+	}
+
+	.uni-calendar-item__weeks-box-text {
+		font-size: 14px;
+		// font-family: Lato-Bold, Lato;
+		font-weight: bold;
+		color: darken($color: $uni-primary, $amount: 40%);
+	}
+
+	.uni-calendar-item__weeks-box-item {
+		position: relative;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		width: 40px;
+		height: 40px;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+
+	.uni-calendar-item__weeks-box-circle {
+		position: absolute;
+		top: 5px;
+		right: 5px;
+		width: 8px;
+		height: 8px;
+		border-radius: 8px;
+		background-color: #dd524d;
+
+	}
+
+	.uni-calendar-item__weeks-box .uni-calendar-item--disable {
+		cursor: default;
+	}
+
+	.uni-calendar-item--disable .uni-calendar-item__weeks-box-text-disable {
+		color: #D1D1D1;
+	}
+
+	.uni-calendar-item--today {
+		position: absolute;
+		top: 10px;
+		right: 17%;
+		background-color: #dd524d;
+		width:6px;
+		height: 6px;
+		border-radius: 50%;
+	}
+
+	.uni-calendar-item--extra {
+		color: #dd524d;
+		opacity: 0.8;
+	}
+
+	.uni-calendar-item__weeks-box .uni-calendar-item--checked {
+		background-color: $uni-primary;
+		border-radius: 50%;
+		box-sizing: border-box;
+		border: 3px solid #fff;
+	}
+
+	.uni-calendar-item--checked .uni-calendar-item--checked-text {
+		color: #fff;
+	}
+
+	.uni-calendar-item--multiple .uni-calendar-item--checked-range-text {
+		color: #333;
+	}
+
+	.uni-calendar-item--multiple {
+		background-color:  #F6F7FC;
+		// color: #fff;
+	}
+
+	.uni-calendar-item--multiple .uni-calendar-item--before-checked,
+	.uni-calendar-item--multiple .uni-calendar-item--after-checked {
+		background-color: $uni-primary;
+		border-radius: 50%;
+		box-sizing: border-box;
+		border: 3px solid #F6F7FC;
+	}
+
+	.uni-calendar-item--before-checked .uni-calendar-item--checked-text,
+	.uni-calendar-item--after-checked .uni-calendar-item--checked-text {
+		color: #fff;
+	}
+
+	.uni-calendar-item--before-checked-x {
+		border-top-left-radius: 50px;
+		border-bottom-left-radius: 50px;
+		box-sizing: border-box;
+		background-color: #F6F7FC;
+	}
+
+	.uni-calendar-item--after-checked-x {
+		border-top-right-radius: 50px;
+		border-bottom-right-radius: 50px;
+		background-color: #F6F7FC;
+	}
+</style>

+ 947 - 0
card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue

@@ -0,0 +1,947 @@
+<template>
+	<view class="uni-calendar" @mouseleave="leaveCale">
+
+		<view v-if="!insert && show" class="uni-calendar__mask" :class="{'uni-calendar--mask-show':aniMaskShow}"
+			@click="maskClick"></view>
+
+		<view v-if="insert || show" class="uni-calendar__content"
+			:class="{'uni-calendar--fixed':!insert,'uni-calendar--ani-show':aniMaskShow, 'uni-calendar__content-mobile': aniMaskShow}">
+			<view class="uni-calendar__header" :class="{'uni-calendar__header-mobile' :!insert}">
+
+				<view class="uni-calendar__header-btn-box" @click.stop="changeMonth('pre')">
+					<view class="uni-calendar__header-btn uni-calendar--left"></view>
+				</view>
+
+				<picker mode="date" :value="date" fields="month" @change="bindDateChange">
+					<text
+						class="uni-calendar__header-text">{{ (nowDate.year||'') + yearText + ( nowDate.month||'') + monthText}}</text>
+				</picker>
+
+				<view class="uni-calendar__header-btn-box" @click.stop="changeMonth('next')">
+					<view class="uni-calendar__header-btn uni-calendar--right"></view>
+				</view>
+
+				<view v-if="!insert" class="dialog-close" @click="maskClick">
+					<view class="dialog-close-plus" data-id="close"></view>
+					<view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
+				</view>
+			</view>
+			<view class="uni-calendar__box">
+
+				<view v-if="showMonth" class="uni-calendar__box-bg">
+					<text class="uni-calendar__box-bg-text">{{nowDate.month}}</text>
+				</view>
+
+				<view class="uni-calendar__weeks" style="padding-bottom: 7px;">
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{SUNText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{MONText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{TUEText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{WEDText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{THUText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{FRIText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{SATText}}</text>
+					</view>
+				</view>
+
+				<view class="uni-calendar__weeks" v-for="(item,weekIndex) in weeks" :key="weekIndex">
+					<view class="uni-calendar__weeks-item" v-for="(weeks,weeksIndex) in item" :key="weeksIndex">
+						<calendar-item class="uni-calendar-item--hook" :weeks="weeks" :calendar="calendar" :selected="selected"
+							:checkHover="range" @change="choiceDate" @handleMouse="handleMouse">
+						</calendar-item>
+					</view>
+				</view>
+			</view>
+
+			<view v-if="!insert && !range && hasTime" class="uni-date-changed uni-calendar--fixed-top"
+				style="padding: 0 80px;">
+				<view class="uni-date-changed--time-date">{{tempSingleDate ? tempSingleDate : selectDateText}}</view>
+				<time-picker type="time" :start="timepickerStartTime" :end="timepickerEndTime" v-model="time"
+					:disabled="!tempSingleDate" :border="false" :hide-second="hideSecond" class="time-picker-style">
+				</time-picker>
+			</view>
+
+			<view v-if="!insert && range && hasTime" class="uni-date-changed uni-calendar--fixed-top">
+				<view class="uni-date-changed--time-start">
+					<view class="uni-date-changed--time-date">{{tempRange.before ? tempRange.before : startDateText}}
+					</view>
+					<time-picker type="time" :start="timepickerStartTime" v-model="timeRange.startTime" :border="false"
+						:hide-second="hideSecond" :disabled="!tempRange.before" class="time-picker-style">
+					</time-picker>
+				</view>
+				<view style="line-height: 50px;">
+					<uni-icons type="arrowthinright" color="#999"></uni-icons>
+				</view>
+				<view class="uni-date-changed--time-end">
+					<view class="uni-date-changed--time-date">{{tempRange.after ? tempRange.after : endDateText}}</view>
+					<time-picker type="time" :end="timepickerEndTime" v-model="timeRange.endTime" :border="false"
+						:hide-second="hideSecond" :disabled="!tempRange.after" class="time-picker-style">
+					</time-picker>
+				</view>
+			</view>
+
+			<view v-if="!insert" class="uni-date-changed uni-date-btn--ok">
+				<view class="uni-datetime-picker--btn" @click="confirm">{{confirmText}}</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		Calendar,
+		getDate,
+		getTime
+	} from './util.js';
+	import calendarItem from './calendar-item.vue'
+	import timePicker from './time-picker.vue'
+
+	import {
+		initVueI18n
+	} from '@dcloudio/uni-i18n'
+	import i18nMessages from './i18n/index.js'
+	const {
+		t
+	} = initVueI18n(i18nMessages)
+
+	/**
+	 * Calendar 日历
+	 * @description 日历组件可以查看日期,选择任意范围内的日期,打点操作。常用场景如:酒店日期预订、火车机票选择购买日期、上下班打卡等
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=56
+	 * @property {String} date 自定义当前时间,默认为今天
+	 * @property {String} startDate 日期选择范围-开始日期
+	 * @property {String} endDate 日期选择范围-结束日期
+	 * @property {Boolean} range 范围选择
+	 * @property {Boolean} insert = [true|false] 插入模式,默认为false
+	 * 	@value true 弹窗模式
+	 * 	@value false 插入模式
+	 * @property {Boolean} clearDate = [true|false] 弹窗模式是否清空上次选择内容
+	 * @property {Array} selected 打点,期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}]
+	 * @property {Boolean} showMonth 是否选择月份为背景
+	 * @property {[String} defaultValue 选择器打开时默认显示的时间
+	 * @event {Function} change 日期改变,`insert :ture` 时生效
+	 * @event {Function} confirm 确认选择`insert :false` 时生效
+	 * @event {Function} monthSwitch 切换月份时触发
+	 * @example <uni-calendar :insert="true" :start-date="'2019-3-2'":end-date="'2019-5-20'"@change="change" />
+	 */
+	export default {
+		components: {
+			calendarItem,
+			timePicker
+		},
+
+		options: {
+			// #ifdef MP-TOUTIAO
+			virtualHost: false,
+			// #endif
+			// #ifndef MP-TOUTIAO
+			virtualHost: true
+			// #endif
+		},
+		props: {
+			date: {
+				type: String,
+				default: ''
+			},
+			defTime: {
+				type: [String, Object],
+				default: ''
+			},
+			selectableTimes: {
+				type: [Object],
+				default () {
+					return {}
+				}
+			},
+			selected: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			startDate: {
+				type: String,
+				default: ''
+			},
+			endDate: {
+				type: String,
+				default: ''
+			},
+			startPlaceholder: {
+				type: String,
+				default: ''
+			},
+			endPlaceholder: {
+				type: String,
+				default: ''
+			},
+			range: {
+				type: Boolean,
+				default: false
+			},
+			hasTime: {
+				type: Boolean,
+				default: false
+			},
+			insert: {
+				type: Boolean,
+				default: true
+			},
+			showMonth: {
+				type: Boolean,
+				default: true
+			},
+			clearDate: {
+				type: Boolean,
+				default: true
+			},
+			checkHover: {
+				type: Boolean,
+				default: true
+			},
+			hideSecond: {
+				type: [Boolean],
+				default: false
+			},
+			pleStatus: {
+				type: Object,
+				default () {
+					return {
+						before: '',
+						after: '',
+						data: [],
+						fulldate: ''
+					}
+				}
+			},
+			defaultValue: {
+				type: [String, Object, Array],
+				default: ''
+			}
+		},
+		data() {
+			return {
+				show: false,
+				weeks: [],
+				calendar: {},
+				nowDate: {},
+				aniMaskShow: false,
+				firstEnter: true,
+				time: '',
+				timeRange: {
+					startTime: '',
+					endTime: ''
+				},
+				tempSingleDate: '',
+				tempRange: {
+					before: '',
+					after: ''
+				}
+			}
+		},
+		watch: {
+			date: {
+				immediate: true,
+				handler(newVal) {
+					if (!this.range) {
+						this.tempSingleDate = newVal
+						setTimeout(() => {
+							this.init(newVal)
+						}, 100)
+					}
+				}
+			},
+			defTime: {
+				immediate: true,
+				handler(newVal) {
+					if (!this.range) {
+						this.time = newVal
+					} else {
+						this.timeRange.startTime = newVal.start
+						this.timeRange.endTime = newVal.end
+					}
+				}
+			},
+			startDate(val) {
+				// 字节小程序 watch 早于 created
+				if (!this.cale) {
+					return
+				}
+				this.cale.setStartDate(val)
+				this.cale.setDate(this.nowDate.fullDate)
+				this.weeks = this.cale.weeks
+			},
+			endDate(val) {
+				// 字节小程序 watch 早于 created
+				if (!this.cale) {
+					return
+				}
+				this.cale.setEndDate(val)
+				this.cale.setDate(this.nowDate.fullDate)
+				this.weeks = this.cale.weeks
+			},
+			selected(newVal) {
+				// 字节小程序 watch 早于 created
+				if (!this.cale) {
+					return
+				}
+				this.cale.setSelectInfo(this.nowDate.fullDate, newVal)
+				this.weeks = this.cale.weeks
+			},
+			pleStatus: {
+				immediate: true,
+				handler(newVal) {
+					const {
+						before,
+						after,
+						fulldate,
+						which
+					} = newVal
+					this.tempRange.before = before
+					this.tempRange.after = after
+					setTimeout(() => {
+						if (fulldate) {
+							this.cale.setHoverMultiple(fulldate)
+							if (before && after) {
+								this.cale.lastHover = true
+								if (this.rangeWithinMonth(after, before)) return
+								this.setDate(before)
+							} else {
+								this.cale.setMultiple(fulldate)
+								this.setDate(this.nowDate.fullDate)
+								this.calendar.fullDate = ''
+								this.cale.lastHover = false
+							}
+						} else {
+							// 字节小程序 watch 早于 created
+							if (!this.cale) {
+								return
+							}
+
+							this.cale.setDefaultMultiple(before, after)
+							if (which === 'left' && before) {
+								this.setDate(before)
+								this.weeks = this.cale.weeks
+							} else if (after) {
+								this.setDate(after)
+								this.weeks = this.cale.weeks
+							}
+							this.cale.lastHover = true
+						}
+					}, 16)
+				}
+			}
+		},
+		computed: {
+			timepickerStartTime() {
+				const activeDate = this.range ? this.tempRange.before : this.calendar.fullDate
+				return activeDate === this.startDate ? this.selectableTimes.start : ''
+			},
+			timepickerEndTime() {
+				const activeDate = this.range ? this.tempRange.after : this.calendar.fullDate
+				return activeDate === this.endDate ? this.selectableTimes.end : ''
+			},
+			/**
+			 * for i18n
+			 */
+			selectDateText() {
+				return t("uni-datetime-picker.selectDate")
+			},
+			startDateText() {
+				return this.startPlaceholder || t("uni-datetime-picker.startDate")
+			},
+			endDateText() {
+				return this.endPlaceholder || t("uni-datetime-picker.endDate")
+			},
+			okText() {
+				return t("uni-datetime-picker.ok")
+			},
+			yearText() {
+				return t("uni-datetime-picker.year")
+			},
+			monthText() {
+				return t("uni-datetime-picker.month")
+			},
+			MONText() {
+				return t("uni-calender.MON")
+			},
+			TUEText() {
+				return t("uni-calender.TUE")
+			},
+			WEDText() {
+				return t("uni-calender.WED")
+			},
+			THUText() {
+				return t("uni-calender.THU")
+			},
+			FRIText() {
+				return t("uni-calender.FRI")
+			},
+			SATText() {
+				return t("uni-calender.SAT")
+			},
+			SUNText() {
+				return t("uni-calender.SUN")
+			},
+			confirmText() {
+				return t("uni-calender.confirm")
+			},
+		},
+		created() {
+			// 获取日历方法实例
+			this.cale = new Calendar({
+				selected: this.selected,
+				startDate: this.startDate,
+				endDate: this.endDate,
+				range: this.range,
+			})
+			// 选中某一天
+			this.init(this.date)
+		},
+		methods: {
+			leaveCale() {
+				this.firstEnter = true
+			},
+			handleMouse(weeks) {
+				if (weeks.disable) return
+				if (this.cale.lastHover) return
+				let {
+					before,
+					after
+				} = this.cale.multipleStatus
+				if (!before) return
+				this.calendar = weeks
+				// 设置范围选
+				this.cale.setHoverMultiple(this.calendar.fullDate)
+				this.weeks = this.cale.weeks
+				// hover时,进入一个日历,更新另一个
+				if (this.firstEnter) {
+					this.$emit('firstEnterCale', this.cale.multipleStatus)
+					this.firstEnter = false
+				}
+			},
+			rangeWithinMonth(A, B) {
+				const [yearA, monthA] = A.split('-')
+				const [yearB, monthB] = B.split('-')
+				return yearA === yearB && monthA === monthB
+			},
+			// 蒙版点击事件
+			maskClick() {
+				this.close()
+				this.$emit('maskClose')
+			},
+
+			clearCalender() {
+				if (this.range) {
+					this.timeRange.startTime = ''
+					this.timeRange.endTime = ''
+					this.tempRange.before = ''
+					this.tempRange.after = ''
+					this.cale.multipleStatus.before = ''
+					this.cale.multipleStatus.after = ''
+					this.cale.multipleStatus.data = []
+					this.cale.lastHover = false
+				} else {
+					this.time = ''
+					this.tempSingleDate = ''
+				}
+				this.calendar.fullDate = ''
+				this.setDate(new Date())
+			},
+
+			bindDateChange(e) {
+				const value = e.detail.value + '-1'
+				this.setDate(value)
+			},
+			/**
+			 * 初始化日期显示
+			 * @param {Object} date
+			 */
+			init(date) {
+				// 字节小程序 watch 早于 created
+				if (!this.cale) {
+					return
+				}
+				this.cale.setDate(date || new Date())
+				this.weeks = this.cale.weeks
+				this.nowDate = this.cale.getInfo(date)
+				this.calendar = {
+					...this.nowDate
+				}
+				if (!date) {
+					// 优化date为空默认不选中今天
+					this.calendar.fullDate = ''
+					if (this.defaultValue && !this.range) {
+						// 暂时只支持移动端非范围选择
+						const defaultDate = new Date(this.defaultValue)
+						const fullDate = getDate(defaultDate)
+						const year = defaultDate.getFullYear()
+						const month = defaultDate.getMonth() + 1
+						const date = defaultDate.getDate()
+						const day = defaultDate.getDay()
+						this.calendar = {
+								fullDate,
+								year,
+								month,
+								date,
+								day
+							},
+							this.tempSingleDate = fullDate
+						this.time = getTime(defaultDate, this.hideSecond)
+					}
+				}
+			},
+			/**
+			 * 打开日历弹窗
+			 */
+			open() {
+				// 弹窗模式并且清理数据
+				if (this.clearDate && !this.insert) {
+					this.cale.cleanMultipleStatus()
+					this.init(this.date)
+				}
+				this.show = true
+				this.$nextTick(() => {
+					setTimeout(() => {
+						this.aniMaskShow = true
+					}, 50)
+				})
+			},
+			/**
+			 * 关闭日历弹窗
+			 */
+			close() {
+				this.aniMaskShow = false
+				this.$nextTick(() => {
+					setTimeout(() => {
+						this.show = false
+						this.$emit('close')
+					}, 300)
+				})
+			},
+			/**
+			 * 确认按钮
+			 */
+			confirm() {
+				this.setEmit('confirm')
+				this.close()
+			},
+			/**
+			 * 变化触发
+			 */
+			change(isSingleChange) {
+				if (!this.insert && !isSingleChange) return
+				this.setEmit('change')
+			},
+			/**
+			 * 选择月份触发
+			 */
+			monthSwitch() {
+				let {
+					year,
+					month
+				} = this.nowDate
+				this.$emit('monthSwitch', {
+					year,
+					month: Number(month)
+				})
+			},
+			/**
+			 * 派发事件
+			 * @param {Object} name
+			 */
+			setEmit(name) {
+				if (!this.range) {
+					if (!this.calendar.fullDate) {
+						this.calendar = this.cale.getInfo(new Date())
+						this.tempSingleDate = this.calendar.fullDate
+					}
+					if (this.hasTime && !this.time) {
+						this.time = getTime(new Date(), this.hideSecond)
+					}
+				}
+				let {
+					year,
+					month,
+					date,
+					fullDate,
+					extraInfo
+				} = this.calendar
+				this.$emit(name, {
+					range: this.cale.multipleStatus,
+					year,
+					month,
+					date,
+					time: this.time,
+					timeRange: this.timeRange,
+					fulldate: fullDate,
+					extraInfo: extraInfo || {}
+				})
+			},
+			/**
+			 * 选择天触发
+			 * @param {Object} weeks
+			 */
+			choiceDate(weeks) {
+				if (weeks.disable) return
+				this.calendar = weeks
+				this.calendar.userChecked = true
+				// 设置多选
+				this.cale.setMultiple(this.calendar.fullDate, true)
+				this.weeks = this.cale.weeks
+				this.tempSingleDate = this.calendar.fullDate
+				const beforeDate = new Date(this.cale.multipleStatus.before).getTime()
+				const afterDate = new Date(this.cale.multipleStatus.after).getTime()
+				if (beforeDate > afterDate && afterDate) {
+					this.tempRange.before = this.cale.multipleStatus.after
+					this.tempRange.after = this.cale.multipleStatus.before
+				} else {
+					this.tempRange.before = this.cale.multipleStatus.before
+					this.tempRange.after = this.cale.multipleStatus.after
+				}
+				this.change(true)
+			},
+			changeMonth(type) {
+				let newDate
+				if (type === 'pre') {
+					newDate = this.cale.getPreMonthObj(this.nowDate.fullDate).fullDate
+				} else if (type === 'next') {
+					newDate = this.cale.getNextMonthObj(this.nowDate.fullDate).fullDate
+				}
+
+				this.setDate(newDate)
+				this.monthSwitch()
+			},
+			/**
+			 * 设置日期
+			 * @param {Object} date
+			 */
+			setDate(date) {
+				this.cale.setDate(date)
+				this.weeks = this.cale.weeks
+				this.nowDate = this.cale.getInfo(date)
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	$uni-primary: #007aff !default;
+
+	.uni-calendar {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+	}
+
+	.uni-calendar__mask {
+		position: fixed;
+		bottom: 0;
+		top: 0;
+		left: 0;
+		right: 0;
+		background-color: rgba(0, 0, 0, 0.4);
+		transition-property: opacity;
+		transition-duration: 0.3s;
+		opacity: 0;
+		/* #ifndef APP-NVUE */
+		z-index: 99;
+		/* #endif */
+	}
+
+	.uni-calendar--mask-show {
+		opacity: 1
+	}
+
+	.uni-calendar--fixed {
+		position: fixed;
+		bottom: calc(var(--window-bottom));
+		left: 0;
+		right: 0;
+		transition-property: transform;
+		transition-duration: 0.3s;
+		transform: translateY(460px);
+		/* #ifndef APP-NVUE */
+		z-index: 99;
+		/* #endif */
+	}
+
+	.uni-calendar--ani-show {
+		transform: translateY(0);
+	}
+
+	.uni-calendar__content {
+		background-color: #fff;
+	}
+
+	.uni-calendar__content-mobile {
+		border-top-left-radius: 10px;
+		border-top-right-radius: 10px;
+		box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.1);
+	}
+
+	.uni-calendar__header {
+		position: relative;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+		height: 50px;
+	}
+
+	.uni-calendar__header-mobile {
+		padding: 10px;
+		padding-bottom: 0;
+	}
+
+	.uni-calendar--fixed-top {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: space-between;
+		border-top-color: rgba(0, 0, 0, 0.4);
+		border-top-style: solid;
+		border-top-width: 1px;
+	}
+
+	.uni-calendar--fixed-width {
+		width: 50px;
+	}
+
+	.uni-calendar__backtoday {
+		position: absolute;
+		right: 0;
+		top: 25rpx;
+		padding: 0 5px;
+		padding-left: 10px;
+		height: 25px;
+		line-height: 25px;
+		font-size: 12px;
+		border-top-left-radius: 25px;
+		border-bottom-left-radius: 25px;
+		color: #fff;
+		background-color: #f1f1f1;
+	}
+
+	.uni-calendar__header-text {
+		text-align: center;
+		width: 100px;
+		font-size: 15px;
+		color: #666;
+	}
+
+	.uni-calendar__button-text {
+		text-align: center;
+		width: 100px;
+		font-size: 14px;
+		color: $uni-primary;
+		/* #ifndef APP-NVUE */
+		letter-spacing: 3px;
+		/* #endif */
+	}
+
+	.uni-calendar__header-btn-box {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		width: 50px;
+		height: 50px;
+	}
+
+	.uni-calendar__header-btn {
+		width: 9px;
+		height: 9px;
+		border-left-color: #808080;
+		border-left-style: solid;
+		border-left-width: 1px;
+		border-top-color: #555555;
+		border-top-style: solid;
+		border-top-width: 1px;
+	}
+
+	.uni-calendar--left {
+		transform: rotate(-45deg);
+	}
+
+	.uni-calendar--right {
+		transform: rotate(135deg);
+	}
+
+
+	.uni-calendar__weeks {
+		position: relative;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+	}
+
+	.uni-calendar__weeks-item {
+		flex: 1;
+	}
+
+	.uni-calendar__weeks-day {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		height: 40px;
+		border-bottom-color: #F5F5F5;
+		border-bottom-style: solid;
+		border-bottom-width: 1px;
+	}
+
+	.uni-calendar__weeks-day-text {
+		font-size: 12px;
+		color: #B2B2B2;
+	}
+
+	.uni-calendar__box {
+		position: relative;
+		// padding: 0 10px;
+		padding-bottom: 7px;
+	}
+
+	.uni-calendar__box-bg {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		justify-content: center;
+		align-items: center;
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+	}
+
+	.uni-calendar__box-bg-text {
+		font-size: 200px;
+		font-weight: bold;
+		color: #999;
+		opacity: 0.1;
+		text-align: center;
+		/* #ifndef APP-NVUE */
+		line-height: 1;
+		/* #endif */
+	}
+
+	.uni-date-changed {
+		padding: 0 10px;
+		// line-height: 50px;
+		text-align: center;
+		color: #333;
+		border-top-color: #DCDCDC;
+		;
+		border-top-style: solid;
+		border-top-width: 1px;
+		flex: 1;
+	}
+
+	.uni-date-btn--ok {
+		padding: 20px 15px;
+	}
+
+	.uni-date-changed--time-start {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		align-items: center;
+	}
+
+	.uni-date-changed--time-end {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		align-items: center;
+	}
+
+	.uni-date-changed--time-date {
+		color: #999;
+		line-height: 50px;
+		/* #ifdef MP-TOUTIAO */
+		font-size: 16px;
+		/* #endif */
+		margin-right: 5px;
+		// opacity: 0.6;
+	}
+
+	.time-picker-style {
+		// width: 62px;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		justify-content: center;
+		align-items: center
+	}
+
+	.mr-10 {
+		margin-right: 10px;
+	}
+
+	.dialog-close {
+		position: absolute;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		padding: 0 25px;
+		margin-top: 10px;
+	}
+
+	.dialog-close-plus {
+		width: 16px;
+		height: 2px;
+		background-color: #737987;
+		border-radius: 2px;
+		transform: rotate(45deg);
+	}
+
+	.dialog-close-rotate {
+		position: absolute;
+		transform: rotate(-45deg);
+	}
+
+	.uni-datetime-picker--btn {
+		border-radius: 100px;
+		height: 40px;
+		line-height: 40px;
+		background-color: $uni-primary;
+		color: #fff;
+		font-size: 16px;
+		letter-spacing: 2px;
+	}
+
+	/* #ifndef APP-NVUE */
+	.uni-datetime-picker--btn:active {
+		opacity: 0.7;
+	}
+
+	/* #endif */
+</style>

+ 22 - 0
card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json

@@ -0,0 +1,22 @@
+{
+	"uni-datetime-picker.selectDate": "select date",
+	"uni-datetime-picker.selectTime": "select time",
+	"uni-datetime-picker.selectDateTime": "select date and time",
+	"uni-datetime-picker.startDate": "start date",
+	"uni-datetime-picker.endDate": "end date",
+	"uni-datetime-picker.startTime": "start time",
+	"uni-datetime-picker.endTime": "end time",
+	"uni-datetime-picker.ok": "ok",
+	"uni-datetime-picker.clear": "clear",
+	"uni-datetime-picker.cancel": "cancel",
+	"uni-datetime-picker.year": "-",
+	"uni-datetime-picker.month": "",
+	"uni-calender.MON": "MON",
+	"uni-calender.TUE": "TUE",
+	"uni-calender.WED": "WED",
+	"uni-calender.THU": "THU",
+	"uni-calender.FRI": "FRI",
+	"uni-calender.SAT": "SAT",
+	"uni-calender.SUN": "SUN",
+	"uni-calender.confirm": "confirm"
+}

+ 8 - 0
card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js

@@ -0,0 +1,8 @@
+import en from './en.json'
+import zhHans from './zh-Hans.json'
+import zhHant from './zh-Hant.json'
+export default {
+	en,
+	'zh-Hans': zhHans,
+	'zh-Hant': zhHant
+}

+ 22 - 0
card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json

@@ -0,0 +1,22 @@
+{
+	"uni-datetime-picker.selectDate": "选择日期",
+	"uni-datetime-picker.selectTime": "选择时间",
+	"uni-datetime-picker.selectDateTime": "选择日期时间",
+	"uni-datetime-picker.startDate": "开始日期",
+	"uni-datetime-picker.endDate": "结束日期",
+	"uni-datetime-picker.startTime": "开始时间",
+	"uni-datetime-picker.endTime": "结束时间",
+	"uni-datetime-picker.ok": "确定",
+	"uni-datetime-picker.clear": "清除",
+	"uni-datetime-picker.cancel": "取消",
+	"uni-datetime-picker.year": "年",
+	"uni-datetime-picker.month": "月",
+	"uni-calender.SUN": "日",
+	"uni-calender.MON": "一",
+	"uni-calender.TUE": "二",
+	"uni-calender.WED": "三",
+	"uni-calender.THU": "四",
+	"uni-calender.FRI": "五",
+	"uni-calender.SAT": "六",
+	"uni-calender.confirm": "确认"
+}

+ 22 - 0
card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json

@@ -0,0 +1,22 @@
+{
+  "uni-datetime-picker.selectDate": "選擇日期",
+  "uni-datetime-picker.selectTime": "選擇時間",
+  "uni-datetime-picker.selectDateTime": "選擇日期時間",
+  "uni-datetime-picker.startDate": "開始日期",
+  "uni-datetime-picker.endDate": "結束日期",
+  "uni-datetime-picker.startTime": "開始时间",
+  "uni-datetime-picker.endTime": "結束时间",
+  "uni-datetime-picker.ok": "確定",
+  "uni-datetime-picker.clear": "清除",
+  "uni-datetime-picker.cancel": "取消",
+  "uni-datetime-picker.year": "年",
+  "uni-datetime-picker.month": "月",
+  "uni-calender.SUN": "日",
+  "uni-calender.MON": "一",
+  "uni-calender.TUE": "二",
+  "uni-calender.WED": "三",
+  "uni-calender.THU": "四",
+  "uni-calender.FRI": "五",
+  "uni-calender.SAT": "六",
+  "uni-calender.confirm": "確認"
+}

+ 940 - 0
card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue

@@ -0,0 +1,940 @@
+<template>
+	<view class="uni-datetime-picker">
+		<view @click="initTimePicker">
+			<slot>
+				<view class="uni-datetime-picker-timebox-pointer"
+					:class="{'uni-datetime-picker-disabled': disabled, 'uni-datetime-picker-timebox': border}">
+					<text class="uni-datetime-picker-text">{{time}}</text>
+					<view v-if="!time" class="uni-datetime-picker-time">
+						<text class="uni-datetime-picker-text">{{selectTimeText}}</text>
+					</view>
+				</view>
+			</slot>
+		</view>
+		<view v-if="visible" id="mask" class="uni-datetime-picker-mask" @click="tiggerTimePicker"></view>
+		<view v-if="visible" class="uni-datetime-picker-popup" :class="[dateShow && timeShow ? '' : 'fix-nvue-height']"
+			:style="fixNvueBug">
+			<view class="uni-title">
+				<text class="uni-datetime-picker-text">{{selectTimeText}}</text>
+			</view>
+			<view v-if="dateShow" class="uni-datetime-picker__container-box">
+				<picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="ymd"
+					@change="bindDateChange">
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in years" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in months" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in days" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+				</picker-view>
+				<!-- 兼容 nvue 不支持伪类 -->
+				<text class="uni-datetime-picker-sign sign-left">-</text>
+				<text class="uni-datetime-picker-sign sign-right">-</text>
+			</view>
+			<view v-if="timeShow" class="uni-datetime-picker__container-box">
+				<picker-view class="uni-datetime-picker-view" :class="[hideSecond ? 'time-hide-second' : '']"
+					:indicator-style="indicatorStyle" :value="hms" @change="bindTimeChange">
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in hours" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in minutes" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!hideSecond">
+						<view class="uni-datetime-picker-item" v-for="(item,index) in seconds" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+				</picker-view>
+				<!-- 兼容 nvue 不支持伪类 -->
+				<text class="uni-datetime-picker-sign" :class="[hideSecond ? 'sign-center' : 'sign-left']">:</text>
+				<text v-if="!hideSecond" class="uni-datetime-picker-sign sign-right">:</text>
+			</view>
+			<view class="uni-datetime-picker-btn">
+				<view @click="clearTime">
+					<text class="uni-datetime-picker-btn-text">{{clearText}}</text>
+				</view>
+				<view class="uni-datetime-picker-btn-group">
+					<view class="uni-datetime-picker-cancel" @click="tiggerTimePicker">
+						<text class="uni-datetime-picker-btn-text">{{cancelText}}</text>
+					</view>
+					<view @click="setTime">
+						<text class="uni-datetime-picker-btn-text">{{okText}}</text>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		initVueI18n
+	} from '@dcloudio/uni-i18n'
+	import i18nMessages from './i18n/index.js'
+	const {
+		t
+	} = initVueI18n(i18nMessages)
+	import {
+		fixIosDateFormat
+	} from './util'
+
+	/**
+	 * DatetimePicker 时间选择器
+	 * @description 可以同时选择日期和时间的选择器
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=xxx
+	 * @property {String} type = [datetime | date | time] 显示模式
+	 * @property {Boolean} multiple = [true|false] 是否多选
+	 * @property {String|Number} value 默认值
+	 * @property {String|Number} start 起始日期或时间
+	 * @property {String|Number} end 起始日期或时间
+	 * @property {String} return-type = [timestamp | string]
+	 * @event {Function} change  选中发生变化触发
+	 */
+
+	export default {
+		name: 'UniDatetimePicker',
+		data() {
+			return {
+				indicatorStyle: `height: 50px;`,
+				visible: false,
+				fixNvueBug: {},
+				dateShow: true,
+				timeShow: true,
+				title: '日期和时间',
+				// 输入框当前时间
+				time: '',
+				// 当前的年月日时分秒
+				year: 1920,
+				month: 0,
+				day: 0,
+				hour: 0,
+				minute: 0,
+				second: 0,
+				// 起始时间
+				startYear: 1920,
+				startMonth: 1,
+				startDay: 1,
+				startHour: 0,
+				startMinute: 0,
+				startSecond: 0,
+				// 结束时间
+				endYear: 2120,
+				endMonth: 12,
+				endDay: 31,
+				endHour: 23,
+				endMinute: 59,
+				endSecond: 59,
+			}
+		},
+		options: {
+			// #ifdef MP-TOUTIAO
+			virtualHost: false,
+			// #endif
+			// #ifndef MP-TOUTIAO
+			virtualHost: true
+			// #endif
+		},
+		props: {
+			type: {
+				type: String,
+				default: 'datetime'
+			},
+			value: {
+				type: [String, Number],
+				default: ''
+			},
+			modelValue: {
+				type: [String, Number],
+				default: ''
+			},
+			start: {
+				type: [Number, String],
+				default: ''
+			},
+			end: {
+				type: [Number, String],
+				default: ''
+			},
+			returnType: {
+				type: String,
+				default: 'string'
+			},
+			disabled: {
+				type: [Boolean, String],
+				default: false
+			},
+			border: {
+				type: [Boolean, String],
+				default: true
+			},
+			hideSecond: {
+				type: [Boolean, String],
+				default: false
+			}
+		},
+		watch: {
+			// #ifndef VUE3
+			value: {
+				handler(newVal) {
+					if (newVal) {
+						this.parseValue(fixIosDateFormat(newVal))
+						this.initTime(false)
+					} else {
+						this.time = ''
+						this.parseValue(Date.now())
+					}
+				},
+				immediate: true
+			},
+			// #endif
+			// #ifdef VUE3
+			modelValue: {
+				handler(newVal) {
+					if (newVal) {
+						this.parseValue(fixIosDateFormat(newVal))
+						this.initTime(false)
+					} else {
+						this.time = ''
+						this.parseValue(Date.now())
+					}
+				},
+				immediate: true
+			},
+			// #endif
+			type: {
+				handler(newValue) {
+					if (newValue === 'date') {
+						this.dateShow = true
+						this.timeShow = false
+						this.title = '日期'
+					} else if (newValue === 'time') {
+						this.dateShow = false
+						this.timeShow = true
+						this.title = '时间'
+					} else {
+						this.dateShow = true
+						this.timeShow = true
+						this.title = '日期和时间'
+					}
+				},
+				immediate: true
+			},
+			start: {
+				handler(newVal) {
+					this.parseDatetimeRange(fixIosDateFormat(newVal), 'start')
+				},
+				immediate: true
+			},
+			end: {
+				handler(newVal) {
+					this.parseDatetimeRange(fixIosDateFormat(newVal), 'end')
+				},
+				immediate: true
+			},
+
+			// 月、日、时、分、秒可选范围变化后,检查当前值是否在范围内,不在则当前值重置为可选范围第一项
+			months(newVal) {
+				this.checkValue('month', this.month, newVal)
+			},
+			days(newVal) {
+				this.checkValue('day', this.day, newVal)
+			},
+			hours(newVal) {
+				this.checkValue('hour', this.hour, newVal)
+			},
+			minutes(newVal) {
+				this.checkValue('minute', this.minute, newVal)
+			},
+			seconds(newVal) {
+				this.checkValue('second', this.second, newVal)
+			}
+		},
+		computed: {
+			// 当前年、月、日、时、分、秒选择范围
+			years() {
+				return this.getCurrentRange('year')
+			},
+
+			months() {
+				return this.getCurrentRange('month')
+			},
+
+			days() {
+				return this.getCurrentRange('day')
+			},
+
+			hours() {
+				return this.getCurrentRange('hour')
+			},
+
+			minutes() {
+				return this.getCurrentRange('minute')
+			},
+
+			seconds() {
+				return this.getCurrentRange('second')
+			},
+
+			// picker 当前值数组
+			ymd() {
+				return [this.year - this.minYear, this.month - this.minMonth, this.day - this.minDay]
+			},
+			hms() {
+				return [this.hour - this.minHour, this.minute - this.minMinute, this.second - this.minSecond]
+			},
+
+			// 当前 date 是 start
+			currentDateIsStart() {
+				return this.year === this.startYear && this.month === this.startMonth && this.day === this.startDay
+			},
+
+			// 当前 date 是 end
+			currentDateIsEnd() {
+				return this.year === this.endYear && this.month === this.endMonth && this.day === this.endDay
+			},
+
+			// 当前年、月、日、时、分、秒的最小值和最大值
+			minYear() {
+				return this.startYear
+			},
+			maxYear() {
+				return this.endYear
+			},
+			minMonth() {
+				if (this.year === this.startYear) {
+					return this.startMonth
+				} else {
+					return 1
+				}
+			},
+			maxMonth() {
+				if (this.year === this.endYear) {
+					return this.endMonth
+				} else {
+					return 12
+				}
+			},
+			minDay() {
+				if (this.year === this.startYear && this.month === this.startMonth) {
+					return this.startDay
+				} else {
+					return 1
+				}
+			},
+			maxDay() {
+				if (this.year === this.endYear && this.month === this.endMonth) {
+					return this.endDay
+				} else {
+					return this.daysInMonth(this.year, this.month)
+				}
+			},
+			minHour() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsStart) {
+						return this.startHour
+					} else {
+						return 0
+					}
+				}
+				if (this.type === 'time') {
+					return this.startHour
+				}
+			},
+			maxHour() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsEnd) {
+						return this.endHour
+					} else {
+						return 23
+					}
+				}
+				if (this.type === 'time') {
+					return this.endHour
+				}
+			},
+			minMinute() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsStart && this.hour === this.startHour) {
+						return this.startMinute
+					} else {
+						return 0
+					}
+				}
+				if (this.type === 'time') {
+					if (this.hour === this.startHour) {
+						return this.startMinute
+					} else {
+						return 0
+					}
+				}
+			},
+			maxMinute() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsEnd && this.hour === this.endHour) {
+						return this.endMinute
+					} else {
+						return 59
+					}
+				}
+				if (this.type === 'time') {
+					if (this.hour === this.endHour) {
+						return this.endMinute
+					} else {
+						return 59
+					}
+				}
+			},
+			minSecond() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsStart && this.hour === this.startHour && this.minute === this.startMinute) {
+						return this.startSecond
+					} else {
+						return 0
+					}
+				}
+				if (this.type === 'time') {
+					if (this.hour === this.startHour && this.minute === this.startMinute) {
+						return this.startSecond
+					} else {
+						return 0
+					}
+				}
+			},
+			maxSecond() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsEnd && this.hour === this.endHour && this.minute === this.endMinute) {
+						return this.endSecond
+					} else {
+						return 59
+					}
+				}
+				if (this.type === 'time') {
+					if (this.hour === this.endHour && this.minute === this.endMinute) {
+						return this.endSecond
+					} else {
+						return 59
+					}
+				}
+			},
+
+			/**
+			 * for i18n
+			 */
+			selectTimeText() {
+				return t("uni-datetime-picker.selectTime")
+			},
+			okText() {
+				return t("uni-datetime-picker.ok")
+			},
+			clearText() {
+				return t("uni-datetime-picker.clear")
+			},
+			cancelText() {
+				return t("uni-datetime-picker.cancel")
+			}
+		},
+
+		mounted() {
+			// #ifdef APP-NVUE
+			const res = uni.getSystemInfoSync();
+			this.fixNvueBug = {
+				top: res.windowHeight / 2,
+				left: res.windowWidth / 2
+			}
+			// #endif
+		},
+
+		methods: {
+			/**
+			 * @param {Object} item
+			 * 小于 10 在前面加个 0
+			 */
+
+			lessThanTen(item) {
+				return item < 10 ? '0' + item : item
+			},
+
+			/**
+			 * 解析时分秒字符串,例如:00:00:00
+			 * @param {String} timeString
+			 */
+			parseTimeType(timeString) {
+				if (timeString) {
+					let timeArr = timeString.split(':')
+					this.hour = Number(timeArr[0])
+					this.minute = Number(timeArr[1])
+					this.second = Number(timeArr[2])
+				}
+			},
+
+			/**
+			 * 解析选择器初始值,类型可以是字符串、时间戳,例如:2000-10-02、'08:30:00'、 1610695109000
+			 * @param {String | Number} datetime
+			 */
+			initPickerValue(datetime) {
+				let defaultValue = null
+				if (datetime) {
+					defaultValue = this.compareValueWithStartAndEnd(datetime, this.start, this.end)
+				} else {
+					defaultValue = Date.now()
+					defaultValue = this.compareValueWithStartAndEnd(defaultValue, this.start, this.end)
+				}
+				this.parseValue(defaultValue)
+			},
+
+			/**
+			 * 初始值规则:
+			 * - 用户设置初始值 value
+			 * 	- 设置了起始时间 start、终止时间 end,并 start < value < end,初始值为 value, 否则初始值为 start
+			 * 	- 只设置了起始时间 start,并 start < value,初始值为 value,否则初始值为 start
+			 * 	- 只设置了终止时间 end,并 value < end,初始值为 value,否则初始值为 end
+			 * 	- 无起始终止时间,则初始值为 value
+			 * - 无初始值 value,则初始值为当前本地时间 Date.now()
+			 * @param {Object} value
+			 * @param {Object} dateBase
+			 */
+			compareValueWithStartAndEnd(value, start, end) {
+				let winner = null
+				value = this.superTimeStamp(value)
+				start = this.superTimeStamp(start)
+				end = this.superTimeStamp(end)
+
+				if (start && end) {
+					if (value < start) {
+						winner = new Date(start)
+					} else if (value > end) {
+						winner = new Date(end)
+					} else {
+						winner = new Date(value)
+					}
+				} else if (start && !end) {
+					winner = start <= value ? new Date(value) : new Date(start)
+				} else if (!start && end) {
+					winner = value <= end ? new Date(value) : new Date(end)
+				} else {
+					winner = new Date(value)
+				}
+
+				return winner
+			},
+
+			/**
+			 * 转换为可比较的时间戳,接受日期、时分秒、时间戳
+			 * @param {Object} value
+			 */
+			superTimeStamp(value) {
+				let dateBase = ''
+				if (this.type === 'time' && value && typeof value === 'string') {
+					const now = new Date()
+					const year = now.getFullYear()
+					const month = now.getMonth() + 1
+					const day = now.getDate()
+					dateBase = year + '/' + month + '/' + day + ' '
+				}
+				if (Number(value)) {
+					value = parseInt(value)
+					dateBase = 0
+				}
+				return this.createTimeStamp(dateBase + value)
+			},
+
+			/**
+			 * 解析默认值 value,字符串、时间戳
+			 * @param {Object} defaultTime
+			 */
+			parseValue(value) {
+				if (!value) {
+					return
+				}
+				if (this.type === 'time' && typeof value === "string") {
+					this.parseTimeType(value)
+				} else {
+					let defaultDate = null
+					defaultDate = new Date(value)
+					if (this.type !== 'time') {
+						this.year = defaultDate.getFullYear()
+						this.month = defaultDate.getMonth() + 1
+						this.day = defaultDate.getDate()
+					}
+					if (this.type !== 'date') {
+						this.hour = defaultDate.getHours()
+						this.minute = defaultDate.getMinutes()
+						this.second = defaultDate.getSeconds()
+					}
+				}
+				if (this.hideSecond) {
+					this.second = 0
+				}
+			},
+
+			/**
+			 * 解析可选择时间范围 start、end,年月日字符串、时间戳
+			 * @param {Object} defaultTime
+			 */
+			parseDatetimeRange(point, pointType) {
+				// 时间为空,则重置为初始值
+				if (!point) {
+					if (pointType === 'start') {
+						this.startYear = 1920
+						this.startMonth = 1
+						this.startDay = 1
+						this.startHour = 0
+						this.startMinute = 0
+						this.startSecond = 0
+					}
+					if (pointType === 'end') {
+						this.endYear = 2120
+						this.endMonth = 12
+						this.endDay = 31
+						this.endHour = 23
+						this.endMinute = 59
+						this.endSecond = 59
+					}
+					return
+				}
+				if (this.type === 'time') {
+					const pointArr = point.split(':')
+					this[pointType + 'Hour'] = Number(pointArr[0])
+					this[pointType + 'Minute'] = Number(pointArr[1])
+					this[pointType + 'Second'] = Number(pointArr[2])
+				} else {
+					if (!point) {
+						pointType === 'start' ? this.startYear = this.year - 60 : this.endYear = this.year + 60
+						return
+					}
+					if (Number(point)) {
+						point = parseInt(point)
+					}
+					// datetime 的 end 没有时分秒, 则不限制
+					const hasTime = /[0-9]:[0-9]/
+					if (this.type === 'datetime' && pointType === 'end' && typeof point === 'string' && !hasTime.test(
+							point)) {
+						point = point + ' 23:59:59'
+					}
+					const pointDate = new Date(point)
+					this[pointType + 'Year'] = pointDate.getFullYear()
+					this[pointType + 'Month'] = pointDate.getMonth() + 1
+					this[pointType + 'Day'] = pointDate.getDate()
+					if (this.type === 'datetime') {
+						this[pointType + 'Hour'] = pointDate.getHours()
+						this[pointType + 'Minute'] = pointDate.getMinutes()
+						this[pointType + 'Second'] = pointDate.getSeconds()
+					}
+				}
+			},
+
+			// 获取 年、月、日、时、分、秒 当前可选范围
+			getCurrentRange(value) {
+				const range = []
+				for (let i = this['min' + this.capitalize(value)]; i <= this['max' + this.capitalize(value)]; i++) {
+					range.push(i)
+				}
+				return range
+			},
+
+			// 字符串首字母大写
+			capitalize(str) {
+				return str.charAt(0).toUpperCase() + str.slice(1)
+			},
+
+			// 检查当前值是否在范围内,不在则当前值重置为可选范围第一项
+			checkValue(name, value, values) {
+				if (values.indexOf(value) === -1) {
+					this[name] = values[0]
+				}
+			},
+
+			// 每个月的实际天数
+			daysInMonth(year, month) { // Use 1 for January, 2 for February, etc.
+				return new Date(year, month, 0).getDate();
+			},
+
+			/**
+			 * 生成时间戳
+			 * @param {Object} time
+			 */
+			createTimeStamp(time) {
+				if (!time) return
+				if (typeof time === "number") {
+					return time
+				} else {
+					time = time.replace(/-/g, '/')
+					if (this.type === 'date') {
+						time = time + ' ' + '00:00:00'
+					}
+					return Date.parse(time)
+				}
+			},
+
+			/**
+			 * 生成日期或时间的字符串
+			 */
+			createDomSting() {
+				const yymmdd = this.year +
+					'-' +
+					this.lessThanTen(this.month) +
+					'-' +
+					this.lessThanTen(this.day)
+
+				let hhmmss = this.lessThanTen(this.hour) +
+					':' +
+					this.lessThanTen(this.minute)
+
+				if (!this.hideSecond) {
+					hhmmss = hhmmss + ':' + this.lessThanTen(this.second)
+				}
+
+				if (this.type === 'date') {
+					return yymmdd
+				} else if (this.type === 'time') {
+					return hhmmss
+				} else {
+					return yymmdd + ' ' + hhmmss
+				}
+			},
+
+			/**
+			 * 初始化返回值,并抛出 change 事件
+			 */
+			initTime(emit = true) {
+				this.time = this.createDomSting()
+				if (!emit) return
+				if (this.returnType === 'timestamp' && this.type !== 'time') {
+					this.$emit('change', this.createTimeStamp(this.time))
+					this.$emit('input', this.createTimeStamp(this.time))
+					this.$emit('update:modelValue', this.createTimeStamp(this.time))
+				} else {
+					this.$emit('change', this.time)
+					this.$emit('input', this.time)
+					this.$emit('update:modelValue', this.time)
+				}
+			},
+
+			/**
+			 * 用户选择日期或时间更新 data
+			 * @param {Object} e
+			 */
+			bindDateChange(e) {
+				const val = e.detail.value
+				this.year = this.years[val[0]]
+				this.month = this.months[val[1]]
+				this.day = this.days[val[2]]
+			},
+			bindTimeChange(e) {
+				const val = e.detail.value
+				this.hour = this.hours[val[0]]
+				this.minute = this.minutes[val[1]]
+				this.second = this.seconds[val[2]]
+			},
+
+			/**
+			 * 初始化弹出层
+			 */
+			initTimePicker() {
+				if (this.disabled) return
+				const value = fixIosDateFormat(this.time)
+				this.initPickerValue(value)
+				this.visible = !this.visible
+			},
+
+			/**
+			 * 触发或关闭弹框
+			 */
+			tiggerTimePicker(e) {
+				this.visible = !this.visible
+			},
+
+			/**
+			 * 用户点击“清空”按钮,清空当前值
+			 */
+			clearTime() {
+				this.time = ''
+				this.$emit('change', this.time)
+				this.$emit('input', this.time)
+				this.$emit('update:modelValue', this.time)
+				this.tiggerTimePicker()
+			},
+
+			/**
+			 * 用户点击“确定”按钮
+			 */
+			setTime() {
+				this.initTime()
+				this.tiggerTimePicker()
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	$uni-primary: #007aff !default;
+
+	.uni-datetime-picker {
+		/* #ifndef APP-NVUE */
+		/* width: 100%; */
+		/* #endif */
+	}
+
+	.uni-datetime-picker-view {
+		height: 130px;
+		width: 270px;
+		/* #ifndef APP-NVUE */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.uni-datetime-picker-item {
+		height: 50px;
+		line-height: 50px;
+		text-align: center;
+		font-size: 14px;
+	}
+
+	.uni-datetime-picker-btn {
+		margin-top: 60px;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		cursor: pointer;
+		/* #endif */
+		flex-direction: row;
+		justify-content: space-between;
+	}
+
+	.uni-datetime-picker-btn-text {
+		font-size: 14px;
+		color: $uni-primary;
+	}
+
+	.uni-datetime-picker-btn-group {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+	}
+
+	.uni-datetime-picker-cancel {
+		margin-right: 30px;
+	}
+
+	.uni-datetime-picker-mask {
+		position: fixed;
+		bottom: 0px;
+		top: 0px;
+		left: 0px;
+		right: 0px;
+		background-color: rgba(0, 0, 0, 0.4);
+		transition-duration: 0.3s;
+		z-index: 998;
+	}
+
+	.uni-datetime-picker-popup {
+		border-radius: 8px;
+		padding: 30px;
+		width: 270px;
+		/* #ifdef APP-NVUE */
+		height: 500px;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		width: 330px;
+		/* #endif */
+		background-color: #fff;
+		position: fixed;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
+		transition-duration: 0.3s;
+		z-index: 999;
+	}
+
+	.fix-nvue-height {
+		/* #ifdef APP-NVUE */
+		height: 330px;
+		/* #endif */
+	}
+
+	.uni-datetime-picker-time {
+		color: grey;
+	}
+
+	.uni-datetime-picker-column {
+		height: 50px;
+	}
+
+	.uni-datetime-picker-timebox {
+
+		border: 1px solid #E5E5E5;
+		border-radius: 5px;
+		padding: 7px 10px;
+		/* #ifndef APP-NVUE */
+		box-sizing: border-box;
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.uni-datetime-picker-timebox-pointer {
+		/* #ifndef APP-NVUE */
+		cursor: pointer;
+		/* #endif */
+	}
+
+
+	.uni-datetime-picker-disabled {
+		opacity: 0.4;
+		/* #ifdef H5 */
+		cursor: not-allowed !important;
+		/* #endif */
+	}
+
+	.uni-datetime-picker-text {
+		font-size: 14px;
+		line-height: 50px
+	}
+
+	.uni-datetime-picker-sign {
+		position: absolute;
+		top: 53px;
+		/* 减掉 10px 的元素高度,兼容nvue */
+		color: #999;
+		/* #ifdef APP-NVUE */
+		font-size: 16px;
+		/* #endif */
+	}
+
+	.sign-left {
+		left: 86px;
+	}
+
+	.sign-right {
+		right: 86px;
+	}
+
+	.sign-center {
+		left: 135px;
+	}
+
+	.uni-datetime-picker__container-box {
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		margin-top: 40px;
+	}
+
+	.time-hide-second {
+		width: 180px;
+	}
+</style>

+ 1064 - 0
card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue

@@ -0,0 +1,1064 @@
+<template>
+	<view class="uni-date">
+		<view class="uni-date-editor" @click="show">
+			<slot>
+				<view class="uni-date-editor--x"
+					:class="{'uni-date-editor--x__disabled': disabled,'uni-date-x--border': border}">
+					<view v-if="!isRange" class="uni-date-x uni-date-single">
+						<uni-icons class="icon-calendar" type="calendar" color="#c0c4cc" size="22"></uni-icons>
+						<view class="uni-date__x-input">{{ displayValue || singlePlaceholderText }}</view>
+					</view>
+
+					<view v-else class="uni-date-x uni-date-range">
+						<uni-icons class="icon-calendar" type="calendar" color="#c0c4cc" size="22"></uni-icons>
+						<view class="uni-date__x-input text-center">{{ displayRangeValue.startDate || startPlaceholderText }}</view>
+
+						<view class="range-separator">{{rangeSeparator}}</view>
+
+						<view class="uni-date__x-input text-center">{{ displayRangeValue.endDate || endPlaceholderText }}</view>
+					</view>
+
+					<view v-if="showClearIcon" class="uni-date__icon-clear" @click.stop="clear">
+						<uni-icons type="clear" color="#c0c4cc" size="22"></uni-icons>
+					</view>
+				</view>
+			</slot>
+		</view>
+
+		<view v-show="pickerVisible" class="uni-date-mask--pc" @click="close"></view>
+
+		<view v-if="!isPhone" v-show="pickerVisible" ref="datePicker" class="uni-date-picker__container">
+			<view v-if="!isRange" class="uni-date-single--x" :style="pickerPositionStyle">
+				<view class="uni-popper__arrow"></view>
+
+				<view v-if="hasTime" class="uni-date-changed popup-x-header">
+					<input class="uni-date__input text-center" type="text" v-model="inputDate" :placeholder="selectDateText" />
+
+					<time-picker type="time" v-model="pickerTime" :border="false" :disabled="!inputDate"
+						:start="timepickerStartTime" :end="timepickerEndTime" :hideSecond="hideSecond" style="width: 100%;">
+						<input class="uni-date__input text-center" type="text" v-model="pickerTime" :placeholder="selectTimeText"
+							:disabled="!inputDate" />
+					</time-picker>
+				</view>
+
+				<Calendar ref="pcSingle" :showMonth="false" :start-date="calendarRange.startDate"
+					:end-date="calendarRange.endDate" :date="calendarDate" @change="singleChange" :default-value="defaultValue"
+					style="padding: 0 8px;" />
+
+				<view v-if="hasTime" class="popup-x-footer">
+					<text class="confirm-text" @click="confirmSingleChange">{{okText}}</text>
+				</view>
+			</view>
+
+			<view v-else class="uni-date-range--x" :style="pickerPositionStyle">
+				<view class="uni-popper__arrow"></view>
+				<view v-if="hasTime" class="popup-x-header uni-date-changed">
+					<view class="popup-x-header--datetime">
+						<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.startDate"
+							:placeholder="startDateText" />
+
+						<time-picker type="time" v-model="tempRange.startTime" :start="timepickerStartTime" :border="false"
+							:disabled="!tempRange.startDate" :hideSecond="hideSecond">
+							<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.startTime"
+								:placeholder="startTimeText" :disabled="!tempRange.startDate" />
+						</time-picker>
+					</view>
+
+					<uni-icons type="arrowthinright" color="#999" style="line-height: 40px;"></uni-icons>
+
+					<view class="popup-x-header--datetime">
+						<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.endDate"
+							:placeholder="endDateText" />
+
+						<time-picker type="time" v-model="tempRange.endTime" :end="timepickerEndTime" :border="false"
+							:disabled="!tempRange.endDate" :hideSecond="hideSecond">
+							<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.endTime"
+								:placeholder="endTimeText" :disabled="!tempRange.endDate" />
+						</time-picker>
+					</view>
+				</view>
+
+				<view class="popup-x-body">
+					<Calendar ref="left" :showMonth="false" :start-date="calendarRange.startDate"
+						:end-date="calendarRange.endDate" :range="true" :pleStatus="endMultipleStatus" @change="leftChange"
+						@firstEnterCale="updateRightCale" style="padding: 0 8px;"/>
+					<Calendar ref="right" :showMonth="false" :start-date="calendarRange.startDate"
+						:end-date="calendarRange.endDate" :range="true" @change="rightChange" :pleStatus="startMultipleStatus"
+						@firstEnterCale="updateLeftCale" style="padding: 0 8px;border-left: 1px solid #F1F1F1;" />
+				</view>
+
+				<view v-if="hasTime" class="popup-x-footer">
+					<text @click="clear">{{clearText}}</text>
+					<text class="confirm-text" @click="confirmRangeChange">{{okText}}</text>
+				</view>
+			</view>
+		</view>
+
+		<Calendar v-if="isPhone" ref="mobile" :clearDate="false" :date="calendarDate" :defTime="mobileCalendarTime"
+			:start-date="calendarRange.startDate" :end-date="calendarRange.endDate" :selectableTimes="mobSelectableTime"
+			:startPlaceholder="startPlaceholder" :endPlaceholder="endPlaceholder" :default-value="defaultValue"
+			:pleStatus="endMultipleStatus" :showMonth="false" :range="isRange" :hasTime="hasTime" :insert="false"
+			:hideSecond="hideSecond" @confirm="mobileChange" @maskClose="close" @change="calendarClick"/>
+	</view>
+</template>
+<script>
+	/**
+	 * DatetimePicker 时间选择器
+	 * @description 同时支持 PC 和移动端使用日历选择日期和日期范围
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=3962
+	 * @property {String} type 选择器类型
+	 * @property {String|Number|Array|Date} value 绑定值
+	 * @property {String} placeholder 单选择时的占位内容
+	 * @property {String} start 起始时间
+	 * @property {String} end 终止时间
+	 * @property {String} start-placeholder 范围选择时开始日期的占位内容
+	 * @property {String} end-placeholder 范围选择时结束日期的占位内容
+	 * @property {String} range-separator 选择范围时的分隔符
+	 * @property {Boolean} border = [true|false] 是否有边框
+	 * @property {Boolean} disabled = [true|false] 是否禁用
+	 * @property {Boolean} clearIcon = [true|false] 是否显示清除按钮(仅PC端适用)
+	 * @property {[String} defaultValue 选择器打开时默认显示的时间
+	 * @event {Function} change 确定日期时触发的事件
+	 * @event {Function} maskClick 点击遮罩层触发的事件
+	 * @event {Function} show 打开弹出层
+	 * @event {Function} close 关闭弹出层
+	 * @event {Function} clear 清除上次选中的状态和值
+	 **/
+	import Calendar from './calendar.vue'
+	import TimePicker from './time-picker.vue'
+	import {
+		initVueI18n
+	} from '@dcloudio/uni-i18n'
+	import i18nMessages from './i18n/index.js'
+	import {
+		getDateTime,
+		getDate,
+		getTime,
+		getDefaultSecond,
+		dateCompare,
+		checkDate,
+		fixIosDateFormat
+	} from './util'
+
+	export default {
+		name: 'UniDatetimePicker',
+
+		options: {
+			// #ifdef MP-TOUTIAO
+			virtualHost: false,
+			// #endif
+			// #ifndef MP-TOUTIAO
+			virtualHost: true
+			// #endif
+		},
+		components: {
+			Calendar,
+			TimePicker
+		},
+		data() {
+			return {
+				isRange: false,
+				hasTime: false,
+				displayValue: '',
+				inputDate: '',
+				calendarDate: '',
+				pickerTime: '',
+				calendarRange: {
+					startDate: '',
+					startTime: '',
+					endDate: '',
+					endTime: ''
+				},
+				displayRangeValue: {
+					startDate: '',
+					endDate: '',
+				},
+				tempRange: {
+					startDate: '',
+					startTime: '',
+					endDate: '',
+					endTime: ''
+				},
+				// 左右日历同步数据
+				startMultipleStatus: {
+					before: '',
+					after: '',
+					data: [],
+					fulldate: ''
+				},
+				endMultipleStatus: {
+					before: '',
+					after: '',
+					data: [],
+					fulldate: ''
+				},
+				pickerVisible: false,
+				pickerPositionStyle: null,
+				isEmitValue: false,
+				isPhone: false,
+				isFirstShow: true,
+				i18nT: () => {}
+			}
+		},
+		props: {
+			type: {
+				type: String,
+				default: 'datetime'
+			},
+			value: {
+				type: [String, Number, Array, Date],
+				default: ''
+			},
+			modelValue: {
+				type: [String, Number, Array, Date],
+				default: ''
+			},
+			start: {
+				type: [Number, String],
+				default: ''
+			},
+			end: {
+				type: [Number, String],
+				default: ''
+			},
+			returnType: {
+				type: String,
+				default: 'string'
+			},
+			placeholder: {
+				type: String,
+				default: ''
+			},
+			startPlaceholder: {
+				type: String,
+				default: ''
+			},
+			endPlaceholder: {
+				type: String,
+				default: ''
+			},
+			rangeSeparator: {
+				type: String,
+				default: '-'
+			},
+			border: {
+				type: [Boolean],
+				default: true
+			},
+			disabled: {
+				type: [Boolean],
+				default: false
+			},
+			clearIcon: {
+				type: [Boolean],
+				default: true
+			},
+			hideSecond: {
+				type: [Boolean],
+				default: false
+			},
+			defaultValue: {
+				type: [String, Object, Array],
+				default: ''
+			}
+		},
+		watch: {
+			type: {
+				immediate: true,
+				handler(newVal) {
+					this.hasTime = newVal.indexOf('time') !== -1
+					this.isRange = newVal.indexOf('range') !== -1
+				}
+			},
+			// #ifndef VUE3
+			value: {
+				immediate: true,
+				handler(newVal) {
+					if (this.isEmitValue) {
+						this.isEmitValue = false
+						return
+					}
+					this.initPicker(newVal)
+				}
+			},
+			// #endif
+			// #ifdef VUE3
+			modelValue: {
+				immediate: true,
+				handler(newVal) {
+					if (this.isEmitValue) {
+						this.isEmitValue = false
+						return
+					}
+					this.initPicker(newVal)
+				}
+			},
+			// #endif
+			start: {
+				immediate: true,
+				handler(newVal) {
+					if (!newVal) return
+					this.calendarRange.startDate = getDate(newVal)
+					if (this.hasTime) {
+						this.calendarRange.startTime = getTime(newVal)
+					}
+				}
+			},
+			end: {
+				immediate: true,
+				handler(newVal) {
+					if (!newVal) return
+					this.calendarRange.endDate = getDate(newVal)
+					if (this.hasTime) {
+						this.calendarRange.endTime = getTime(newVal, this.hideSecond)
+					}
+				}
+			},
+		},
+		computed: {
+			timepickerStartTime() {
+				const activeDate = this.isRange ? this.tempRange.startDate : this.inputDate
+				return activeDate === this.calendarRange.startDate ? this.calendarRange.startTime : ''
+			},
+			timepickerEndTime() {
+				const activeDate = this.isRange ? this.tempRange.endDate : this.inputDate
+				return activeDate === this.calendarRange.endDate ? this.calendarRange.endTime : ''
+			},
+			mobileCalendarTime() {
+				const timeRange = {
+					start: this.tempRange.startTime,
+					end: this.tempRange.endTime
+				}
+				return this.isRange ? timeRange : this.pickerTime
+			},
+			mobSelectableTime() {
+				return {
+					start: this.calendarRange.startTime,
+					end: this.calendarRange.endTime
+				}
+			},
+			datePopupWidth() {
+				// todo
+				return this.isRange ? 653 : 301
+			},
+
+			/**
+			 * for i18n
+			 */
+			singlePlaceholderText() {
+				return this.placeholder || (this.type === 'date' ? this.selectDateText : this.selectDateTimeText)
+			},
+			startPlaceholderText() {
+				return this.startPlaceholder || this.startDateText
+			},
+			endPlaceholderText() {
+				return this.endPlaceholder || this.endDateText
+			},
+			selectDateText() {
+				return this.i18nT("uni-datetime-picker.selectDate")
+			},
+			selectDateTimeText() {
+				return this.i18nT("uni-datetime-picker.selectDateTime")
+			},
+			selectTimeText() {
+				return this.i18nT("uni-datetime-picker.selectTime")
+			},
+			startDateText() {
+				return this.startPlaceholder || this.i18nT("uni-datetime-picker.startDate")
+			},
+			startTimeText() {
+				return this.i18nT("uni-datetime-picker.startTime")
+			},
+			endDateText() {
+				return this.endPlaceholder || this.i18nT("uni-datetime-picker.endDate")
+			},
+			endTimeText() {
+				return this.i18nT("uni-datetime-picker.endTime")
+			},
+			okText() {
+				return this.i18nT("uni-datetime-picker.ok")
+			},
+			clearText() {
+				return this.i18nT("uni-datetime-picker.clear")
+			},
+			showClearIcon() {
+				return this.clearIcon && !this.disabled && (this.displayValue || (this.displayRangeValue.startDate && this
+					.displayRangeValue.endDate))
+			}
+		},
+		created() {
+			this.initI18nT()
+			this.platform()
+		},
+		methods: {
+			initI18nT() {
+				const vueI18n = initVueI18n(i18nMessages)
+				this.i18nT = vueI18n.t
+			},
+			initPicker(newVal) {
+				if ((!newVal && !this.defaultValue) || Array.isArray(newVal) && !newVal.length) {
+					this.$nextTick(() => {
+						this.clear(false)
+					})
+					return
+				}
+
+				if (!Array.isArray(newVal) && !this.isRange) {
+					if (newVal) {
+						this.displayValue = this.inputDate = this.calendarDate = getDate(newVal)
+						if (this.hasTime) {
+							this.pickerTime = getTime(newVal, this.hideSecond)
+							this.displayValue = `${this.displayValue} ${this.pickerTime}`
+						}
+					} else if (this.defaultValue) {
+						this.inputDate = this.calendarDate = getDate(this.defaultValue)
+						if (this.hasTime) {
+							this.pickerTime = getTime(this.defaultValue, this.hideSecond)
+						}
+					}
+				} else {
+					const [before, after] = newVal
+					if (!before && !after) return
+					const beforeDate = getDate(before)
+					const beforeTime = getTime(before, this.hideSecond)
+
+					const afterDate = getDate(after)
+					const afterTime = getTime(after, this.hideSecond)
+					const startDate = beforeDate
+					const endDate = afterDate
+					this.displayRangeValue.startDate = this.tempRange.startDate = startDate
+					this.displayRangeValue.endDate = this.tempRange.endDate = endDate
+
+					if (this.hasTime) {
+						this.displayRangeValue.startDate = `${beforeDate} ${beforeTime}`
+						this.displayRangeValue.endDate = `${afterDate} ${afterTime}`
+						this.tempRange.startTime = beforeTime
+						this.tempRange.endTime = afterTime
+					}
+					const defaultRange = {
+						before: beforeDate,
+						after: afterDate
+					}
+					this.startMultipleStatus = Object.assign({}, this.startMultipleStatus, defaultRange, {
+						which: 'right'
+					})
+					this.endMultipleStatus = Object.assign({}, this.endMultipleStatus, defaultRange, {
+						which: 'left'
+					})
+				}
+			},
+			updateLeftCale(e) {
+				const left = this.$refs.left
+				// 设置范围选
+				left.cale.setHoverMultiple(e.after)
+				left.setDate(this.$refs.left.nowDate.fullDate)
+			},
+			updateRightCale(e) {
+				const right = this.$refs.right
+				// 设置范围选
+				right.cale.setHoverMultiple(e.after)
+				right.setDate(this.$refs.right.nowDate.fullDate)
+			},
+			platform() {
+				if (typeof navigator !== "undefined") {
+					this.isPhone = navigator.userAgent.toLowerCase().indexOf('mobile') !== -1
+					return
+				}
+				// #ifdef MP-WEIXIN
+				const {
+					windowWidth
+				} = uni.getWindowInfo()
+				// #endif
+				// #ifndef MP-WEIXIN
+				const {
+					windowWidth
+				} = uni.getSystemInfoSync()
+				// #endif
+				this.isPhone = windowWidth <= 500
+				this.windowWidth = windowWidth
+			},
+			show() {
+				this.$emit("show")
+				if (this.disabled) {
+					return
+				}
+				this.platform()
+				if (this.isPhone) {
+					setTimeout(() => {
+						this.$refs.mobile.open()
+					}, 0);
+					return
+				}
+				this.pickerPositionStyle = {
+					top: '10px'
+				}
+				const dateEditor = uni.createSelectorQuery().in(this).select(".uni-date-editor")
+				dateEditor.boundingClientRect(rect => {
+					if (this.windowWidth - rect.left < this.datePopupWidth) {
+						this.pickerPositionStyle.right = 0
+					}
+				}).exec()
+				setTimeout(() => {
+					this.pickerVisible = !this.pickerVisible
+					if (!this.isPhone && this.isRange && this.isFirstShow) {
+						this.isFirstShow = false
+						const {
+							startDate,
+							endDate
+						} = this.calendarRange
+						if (startDate && endDate) {
+							if (this.diffDate(startDate, endDate) < 30) {
+								this.$refs.right.changeMonth('pre')
+							}
+						} else {
+							// this.$refs.right.changeMonth('next')
+							if (this.isPhone) {
+								this.$refs.right.cale.lastHover = false;
+							}
+						}
+					}
+
+				}, 50)
+			},
+			close() {
+				setTimeout(() => {
+					this.pickerVisible = false
+					this.$emit('maskClick', this.value)
+					this.$refs.mobile && this.$refs.mobile.close()
+				}, 20)
+			},
+			setEmit(value) {
+				if (this.returnType === "timestamp" || this.returnType === "date") {
+					if (!Array.isArray(value)) {
+						if (!this.hasTime) {
+							value = value + ' ' + '00:00:00'
+						}
+						value = this.createTimestamp(value)
+						if (this.returnType === "date") {
+							value = new Date(value)
+						}
+					} else {
+						if (!this.hasTime) {
+							value[0] = value[0] + ' ' + '00:00:00'
+							value[1] = value[1] + ' ' + '00:00:00'
+						}
+						value[0] = this.createTimestamp(value[0])
+						value[1] = this.createTimestamp(value[1])
+						if (this.returnType === "date") {
+							value[0] = new Date(value[0])
+							value[1] = new Date(value[1])
+						}
+					}
+				}
+
+				this.$emit('update:modelValue', value)
+				this.$emit('input', value)
+				this.$emit('change', value)
+				this.isEmitValue = true
+			},
+			createTimestamp(date) {
+				date = fixIosDateFormat(date)
+				return Date.parse(new Date(date))
+			},
+			singleChange(e) {
+				this.calendarDate = this.inputDate = e.fulldate
+				if (this.hasTime) return
+				this.confirmSingleChange()
+			},
+			confirmSingleChange() {
+				if (!checkDate(this.inputDate)) {
+					const now = new Date()
+					this.calendarDate = this.inputDate = getDate(now)
+					this.pickerTime = getTime(now, this.hideSecond)
+				}
+
+				let startLaterInputDate = false
+				let startDate, startTime
+				if (this.start) {
+					let startString = this.start
+					if (typeof this.start === 'number') {
+						startString = getDateTime(this.start, this.hideSecond)
+					}
+					[startDate, startTime] = startString.split(' ')
+					if (this.start && !dateCompare(startDate, this.inputDate)) {
+						startLaterInputDate = true
+						this.inputDate = startDate
+					}
+				}
+
+				let endEarlierInputDate = false
+				let endDate, endTime
+				if (this.end) {
+					let endString = this.end
+					if (typeof this.end === 'number') {
+						endString = getDateTime(this.end, this.hideSecond)
+					}
+					[endDate, endTime] = endString.split(' ')
+					if (this.end && !dateCompare(this.inputDate, endDate)) {
+						endEarlierInputDate = true
+						this.inputDate = endDate
+					}
+				}
+				if (this.hasTime) {
+					if (startLaterInputDate) {
+						this.pickerTime = startTime || getDefaultSecond(this.hideSecond)
+					}
+					if (endEarlierInputDate) {
+						this.pickerTime = endTime || getDefaultSecond(this.hideSecond)
+					}
+					if (!this.pickerTime) {
+						this.pickerTime = getTime(Date.now(), this.hideSecond)
+					}
+					this.displayValue = `${this.inputDate} ${this.pickerTime}`
+				} else {
+					this.displayValue = this.inputDate
+				}
+				this.setEmit(this.displayValue)
+				this.pickerVisible = false
+			},
+			leftChange(e) {
+				const {
+					before,
+					after
+				} = e.range
+				this.rangeChange(before, after)
+				const obj = {
+					before: e.range.before,
+					after: e.range.after,
+					data: e.range.data,
+					fulldate: e.fulldate
+				}
+				this.startMultipleStatus = Object.assign({}, this.startMultipleStatus, obj)
+				this.$emit('calendarClick', e)
+			},
+			rightChange(e) {
+				const {
+					before,
+					after
+				} = e.range
+				this.rangeChange(before, after)
+				const obj = {
+					before: e.range.before,
+					after: e.range.after,
+					data: e.range.data,
+					fulldate: e.fulldate
+				}
+				this.endMultipleStatus = Object.assign({}, this.endMultipleStatus, obj)
+				this.$emit('calendarClick', e)
+			},
+			mobileChange(e) {
+				if (this.isRange) {
+					const {
+						before,
+						after
+					} = e.range
+					if (!before) {
+						return;
+					}
+
+					this.handleStartAndEnd(before, after, true)
+					if (this.hasTime) {
+						const {
+							startTime,
+							endTime
+						} = e.timeRange
+						this.tempRange.startTime = startTime
+						this.tempRange.endTime = endTime
+					}
+					this.confirmRangeChange()
+				} else {
+					if (this.hasTime) {
+						this.displayValue = e.fulldate + ' ' + e.time
+					} else {
+						this.displayValue = e.fulldate
+					}
+					this.setEmit(this.displayValue)
+				}
+				this.$refs.mobile.close()
+			},
+			rangeChange(before, after) {
+				if (!(before && after)) return
+				this.handleStartAndEnd(before, after, true)
+				if (this.hasTime) return
+				this.confirmRangeChange()
+			},
+			confirmRangeChange() {
+				if (!this.tempRange.startDate || !this.tempRange.endDate) {
+					this.pickerVisible = false
+					return
+				}
+				if (!checkDate(this.tempRange.startDate)) {
+					this.tempRange.startDate = getDate(Date.now())
+				}
+				if (!checkDate(this.tempRange.endDate)) {
+					this.tempRange.endDate = getDate(Date.now())
+				}
+
+				let start, end
+
+				let startDateLaterRangeStartDate = false
+				let startDateLaterRangeEndDate = false
+				let startDate, startTime
+				if (this.start) {
+					let startString = this.start
+					if (typeof this.start === 'number') {
+						startString = getDateTime(this.start, this.hideSecond)
+					}
+					[startDate, startTime] = startString.split(' ')
+					if (this.start && !dateCompare(this.start, `${this.tempRange.startDate} ${this.tempRange.startTime}`)) {
+						startDateLaterRangeStartDate = true
+						this.tempRange.startDate = startDate
+					}
+					if (this.start && !dateCompare(this.start, `${this.tempRange.endDate} ${this.tempRange.endTime}`)) {
+						startDateLaterRangeEndDate = true
+						this.tempRange.endDate = startDate
+					}
+				}
+				let endDateEarlierRangeStartDate = false
+				let endDateEarlierRangeEndDate = false
+				let endDate, endTime
+				if (this.end) {
+					let endString = this.end
+					if (typeof this.end === 'number') {
+						endString = getDateTime(this.end, this.hideSecond)
+					}
+					[endDate, endTime] = endString.split(' ')
+
+					if (this.end && !dateCompare(`${this.tempRange.startDate} ${this.tempRange.startTime}`, this.end)) {
+						endDateEarlierRangeStartDate = true
+						this.tempRange.startDate = endDate
+					}
+					if (this.end && !dateCompare(`${this.tempRange.endDate} ${this.tempRange.endTime}`, this.end)) {
+						endDateEarlierRangeEndDate = true
+						this.tempRange.endDate = endDate
+					}
+				}
+				if (!this.hasTime) {
+					start = this.displayRangeValue.startDate = this.tempRange.startDate
+					end = this.displayRangeValue.endDate = this.tempRange.endDate
+				} else {
+					if (startDateLaterRangeStartDate) {
+						this.tempRange.startTime = startTime || getDefaultSecond(this.hideSecond)
+					} else if (endDateEarlierRangeStartDate) {
+						this.tempRange.startTime = endTime || getDefaultSecond(this.hideSecond)
+					}
+					if (!this.tempRange.startTime) {
+						this.tempRange.startTime = getTime(Date.now(), this.hideSecond)
+					}
+
+					if (startDateLaterRangeEndDate) {
+						this.tempRange.endTime = startTime || getDefaultSecond(this.hideSecond)
+					} else if (endDateEarlierRangeEndDate) {
+						this.tempRange.endTime = endTime || getDefaultSecond(this.hideSecond)
+					}
+					if (!this.tempRange.endTime) {
+						this.tempRange.endTime = getTime(Date.now(), this.hideSecond)
+					}
+					start = this.displayRangeValue.startDate = `${this.tempRange.startDate} ${this.tempRange.startTime}`
+					end = this.displayRangeValue.endDate = `${this.tempRange.endDate} ${this.tempRange.endTime}`
+				}
+				if (!dateCompare(start, end)) {
+					[start, end] = [end, start]
+				}
+				this.displayRangeValue.startDate = start
+				this.displayRangeValue.endDate = end
+				const displayRange = [start, end]
+				this.setEmit(displayRange)
+				this.pickerVisible = false
+			},
+			handleStartAndEnd(before, after, temp = false) {
+				if (!before) return
+				if (!after) after = before;
+				const type = temp ? 'tempRange' : 'range'
+				const isStartEarlierEnd = dateCompare(before, after)
+				this[type].startDate = isStartEarlierEnd ? before : after
+				this[type].endDate = isStartEarlierEnd ? after : before
+			},
+			/**
+			 * 比较时间大小
+			 */
+			dateCompare(startDate, endDate) {
+				// 计算截止时间
+				startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
+				// 计算详细项的截止时间
+				endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
+				return startDate <= endDate
+			},
+
+			/**
+			 * 比较时间差
+			 */
+			diffDate(startDate, endDate) {
+				// 计算截止时间
+				startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
+				// 计算详细项的截止时间
+				endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
+				const diff = (endDate - startDate) / (24 * 60 * 60 * 1000)
+				return Math.abs(diff)
+			},
+
+			clear(needEmit = true) {
+				if (!this.isRange) {
+					this.displayValue = ''
+					this.inputDate = ''
+					this.pickerTime = ''
+					if (this.isPhone) {
+						this.$refs.mobile && this.$refs.mobile.clearCalender()
+					} else {
+						this.$refs.pcSingle && this.$refs.pcSingle.clearCalender()
+					}
+					if (needEmit) {
+						this.$emit('change', '')
+						this.$emit('input', '')
+						this.$emit('update:modelValue', '')
+					}
+				} else {
+					this.displayRangeValue.startDate = ''
+					this.displayRangeValue.endDate = ''
+					this.tempRange.startDate = ''
+					this.tempRange.startTime = ''
+					this.tempRange.endDate = ''
+					this.tempRange.endTime = ''
+					if (this.isPhone) {
+						this.$refs.mobile && this.$refs.mobile.clearCalender()
+					} else {
+						this.$refs.left && this.$refs.left.clearCalender()
+						this.$refs.right && this.$refs.right.clearCalender()
+						this.$refs.right && this.$refs.right.changeMonth('next')
+					}
+					if (needEmit) {
+						this.$emit('change', [])
+						this.$emit('input', [])
+						this.$emit('update:modelValue', [])
+					}
+				}
+			},
+
+			calendarClick(e) {
+				this.$emit('calendarClick', e)
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	$uni-primary: #007aff !default;
+
+	.uni-date {
+		width: 100%;
+		flex: 1;
+	}
+
+	.uni-date-x {
+		display: flex;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		border-radius: 4px;
+		background-color: #fff;
+		color: #666;
+		font-size: 14px;
+		flex: 1;
+
+		.icon-calendar {
+			padding-left: 3px;
+		}
+
+		.range-separator {
+			height: 35px;
+			/* #ifndef MP */
+			padding: 0 2px;
+			/* #endif */
+			line-height: 35px;
+		}
+	}
+
+	.uni-date-x--border {
+		box-sizing: border-box;
+		border-radius: 4px;
+		border: 1px solid #e5e5e5;
+	}
+
+	.uni-date-editor--x {
+		display: flex;
+		align-items: center;
+		position: relative;
+	}
+
+	.uni-date-editor--x .uni-date__icon-clear {
+		padding-right: 3px;
+		display: flex;
+		align-items: center;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.uni-date__x-input {
+		width: auto;
+		height: 35px;
+		/* #ifndef MP */
+		padding-left: 5px;
+		/* #endif */
+		position: relative;
+		flex: 1;
+		line-height: 35px;
+		font-size: 14px;
+		overflow: hidden;
+	}
+
+	.text-center {
+		text-align: center;
+	}
+
+	.uni-date__input {
+		height: 40px;
+		width: 100%;
+		line-height: 40px;
+		font-size: 14px;
+	}
+
+	.uni-date-range__input {
+		text-align: center;
+		max-width: 142px;
+	}
+
+	.uni-date-picker__container {
+		position: relative;
+	}
+
+	.uni-date-mask--pc {
+		position: fixed;
+		bottom: 0px;
+		top: 0px;
+		left: 0px;
+		right: 0px;
+		background-color: rgba(0, 0, 0, 0);
+		transition-duration: 0.3s;
+		z-index: 996;
+	}
+
+	.uni-date-single--x {
+		background-color: #fff;
+		position: absolute;
+		top: 0;
+		z-index: 999;
+		border: 1px solid #EBEEF5;
+		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+		border-radius: 4px;
+	}
+
+	.uni-date-range--x {
+		background-color: #fff;
+		position: absolute;
+		top: 0;
+		z-index: 999;
+		border: 1px solid #EBEEF5;
+		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+		border-radius: 4px;
+	}
+
+	.uni-date-editor--x__disabled {
+		opacity: 0.4;
+		cursor: default;
+	}
+
+	.uni-date-editor--logo {
+		width: 16px;
+		height: 16px;
+		vertical-align: middle;
+	}
+
+	/* 添加时间 */
+	.popup-x-header {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+	}
+
+	.popup-x-header--datetime {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		flex: 1;
+	}
+
+	.popup-x-body {
+		display: flex;
+	}
+
+	.popup-x-footer {
+		padding: 0 15px;
+		border-top-color: #F1F1F1;
+		border-top-style: solid;
+		border-top-width: 1px;
+		line-height: 40px;
+		text-align: right;
+		color: #666;
+	}
+
+	.popup-x-footer text:hover {
+		color: $uni-primary;
+		cursor: pointer;
+		opacity: 0.8;
+	}
+
+	.popup-x-footer .confirm-text {
+		margin-left: 20px;
+		color: $uni-primary;
+	}
+
+	.uni-date-changed {
+		text-align: center;
+		color: #333;
+		border-bottom-color: #F1F1F1;
+		border-bottom-style: solid;
+		border-bottom-width: 1px;
+	}
+
+	.uni-date-changed--time text {
+		height: 50px;
+		line-height: 50px;
+	}
+
+	.uni-date-changed .uni-date-changed--time {
+		flex: 1;
+	}
+
+	.uni-date-changed--time-date {
+		color: #333;
+		opacity: 0.6;
+	}
+
+	.mr-50 {
+		margin-right: 50px;
+	}
+
+	/* picker 弹出层通用的指示小三角, todo:扩展至上下左右方向定位 */
+	.uni-popper__arrow,
+	.uni-popper__arrow::after {
+		position: absolute;
+		display: block;
+		width: 0;
+		height: 0;
+		border: 6px solid transparent;
+		border-top-width: 0;
+	}
+
+	.uni-popper__arrow {
+		filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
+		top: -6px;
+		left: 10%;
+		margin-right: 3px;
+		border-bottom-color: #EBEEF5;
+	}
+
+	.uni-popper__arrow::after {
+		content: " ";
+		top: 1px;
+		margin-left: -6px;
+		border-bottom-color: #fff;
+	}
+</style>

+ 421 - 0
card/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js

@@ -0,0 +1,421 @@
+class Calendar {
+	constructor({
+		selected,
+		startDate,
+		endDate,
+		range,
+	} = {}) {
+		// 当前日期
+		this.date = this.getDateObj(new Date()) // 当前初入日期
+		// 打点信息
+		this.selected = selected || [];
+		// 起始时间
+		this.startDate = startDate
+		// 终止时间
+		this.endDate = endDate
+		// 是否范围选择
+		this.range = range
+		// 多选状态
+		this.cleanMultipleStatus()
+		// 每周日期
+		this.weeks = {}
+		this.lastHover = false
+	}
+	/**
+	 * 设置日期
+	 * @param {Object} date
+	 */
+	setDate(date) {
+		const selectDate = this.getDateObj(date)
+		this.getWeeks(selectDate.fullDate)
+	}
+
+	/**
+	 * 清理多选状态
+	 */
+	cleanMultipleStatus() {
+		this.multipleStatus = {
+			before: '',
+			after: '',
+			data: []
+		}
+	}
+
+	setStartDate(startDate) {
+		this.startDate = startDate
+	}
+
+	setEndDate(endDate) {
+		this.endDate = endDate
+	}
+
+	getPreMonthObj(date) {
+		date = fixIosDateFormat(date)
+		date = new Date(date)
+
+		const oldMonth = date.getMonth()
+		date.setMonth(oldMonth - 1)
+		const newMonth = date.getMonth()
+		if (oldMonth !== 0 && newMonth - oldMonth === 0) {
+			date.setMonth(newMonth - 1)
+		}
+		return this.getDateObj(date)
+	}
+	getNextMonthObj(date) {
+		date = fixIosDateFormat(date)
+		date = new Date(date)
+
+		const oldMonth = date.getMonth()
+		date.setMonth(oldMonth + 1)
+		const newMonth = date.getMonth()
+		if (newMonth - oldMonth > 1) {
+			date.setMonth(newMonth - 1)
+		}
+		return this.getDateObj(date)
+	}
+
+	/**
+	 * 获取指定格式Date对象
+	 */
+	getDateObj(date) {
+		date = fixIosDateFormat(date)
+		date = new Date(date)
+
+		return {
+			fullDate: getDate(date),
+			year: date.getFullYear(),
+			month: addZero(date.getMonth() + 1),
+			date: addZero(date.getDate()),
+			day: date.getDay()
+		}
+	}
+
+	/**
+	 * 获取上一个月日期集合
+	 */
+	getPreMonthDays(amount, dateObj) {
+		const result = []
+		for (let i = amount - 1; i >= 0; i--) {
+			const month = dateObj.month - 1
+			result.push({
+				date: new Date(dateObj.year, month, -i).getDate(),
+				month,
+				disable: true
+			})
+		}
+		return result
+	}
+	/**
+	 * 获取本月日期集合
+	 */
+	getCurrentMonthDays(amount, dateObj) {
+		const result = []
+		const fullDate = this.date.fullDate
+		for (let i = 1; i <= amount; i++) {
+			const currentDate = `${dateObj.year}-${dateObj.month}-${addZero(i)}`
+			const isToday = fullDate === currentDate
+			// 获取打点信息
+			const info = this.selected && this.selected.find((item) => {
+				if (this.dateEqual(currentDate, item.date)) {
+					return item
+				}
+			})
+
+			// 日期禁用
+			let disableBefore = true
+			let disableAfter = true
+			if (this.startDate) {
+				disableBefore = dateCompare(this.startDate, currentDate)
+			}
+
+			if (this.endDate) {
+				disableAfter = dateCompare(currentDate, this.endDate)
+			}
+
+			let multiples = this.multipleStatus.data
+			let multiplesStatus = -1
+			if (this.range && multiples) {
+				multiplesStatus = multiples.findIndex((item) => {
+					return this.dateEqual(item, currentDate)
+				})
+			}
+			const checked = multiplesStatus !== -1
+
+			result.push({
+				fullDate: currentDate,
+				year: dateObj.year,
+				date: i,
+				multiple: this.range ? checked : false,
+				beforeMultiple: this.isLogicBefore(currentDate, this.multipleStatus.before, this.multipleStatus.after),
+				afterMultiple: this.isLogicAfter(currentDate, this.multipleStatus.before, this.multipleStatus.after),
+				month: dateObj.month,
+				disable: (this.startDate && !dateCompare(this.startDate, currentDate)) || (this.endDate && !dateCompare(
+					currentDate, this.endDate)),
+				isToday,
+				userChecked: false,
+				extraInfo: info
+			})
+		}
+		return result
+	}
+	/**
+	 * 获取下一个月日期集合
+	 */
+	_getNextMonthDays(amount, dateObj) {
+		const result = []
+		const month = dateObj.month + 1
+		for (let i = 1; i <= amount; i++) {
+			result.push({
+				date: i,
+				month,
+				disable: true
+			})
+		}
+		return result
+	}
+
+	/**
+	 * 获取当前日期详情
+	 * @param {Object} date
+	 */
+	getInfo(date) {
+		if (!date) {
+			date = new Date()
+		}
+		const res = this.calendar.find(item => item.fullDate === this.getDateObj(date).fullDate)
+		return res ? res : this.getDateObj(date)
+	}
+
+	/**
+	 * 比较时间是否相等
+	 */
+	dateEqual(before, after) {
+		before = new Date(fixIosDateFormat(before))
+		after = new Date(fixIosDateFormat(after))
+		return before.valueOf() === after.valueOf()
+	}
+
+	/**
+	 *  比较真实起始日期
+	 */
+
+	isLogicBefore(currentDate, before, after) {
+		let logicBefore = before
+		if (before && after) {
+			logicBefore = dateCompare(before, after) ? before : after
+		}
+		return this.dateEqual(logicBefore, currentDate)
+	}
+
+	isLogicAfter(currentDate, before, after) {
+		let logicAfter = after
+		if (before && after) {
+			logicAfter = dateCompare(before, after) ? after : before
+		}
+		return this.dateEqual(logicAfter, currentDate)
+	}
+
+	/**
+	 * 获取日期范围内所有日期
+	 * @param {Object} begin
+	 * @param {Object} end
+	 */
+	geDateAll(begin, end) {
+		var arr = []
+		var ab = begin.split('-')
+		var ae = end.split('-')
+		var db = new Date()
+		db.setFullYear(ab[0], ab[1] - 1, ab[2])
+		var de = new Date()
+		de.setFullYear(ae[0], ae[1] - 1, ae[2])
+		var unixDb = db.getTime() - 24 * 60 * 60 * 1000
+		var unixDe = de.getTime() - 24 * 60 * 60 * 1000
+		for (var k = unixDb; k <= unixDe;) {
+			k = k + 24 * 60 * 60 * 1000
+			arr.push(this.getDateObj(new Date(parseInt(k))).fullDate)
+		}
+		return arr
+	}
+
+	/**
+	 *  获取多选状态
+	 */
+	setMultiple(fullDate) {
+		if (!this.range) return
+
+		let {
+			before,
+			after
+		} = this.multipleStatus
+		if (before && after) {
+			if (!this.lastHover) {
+				this.lastHover = true
+				return
+			}
+			this.multipleStatus.before = fullDate
+			this.multipleStatus.after = ''
+			this.multipleStatus.data = []
+			this.multipleStatus.fulldate = ''
+			this.lastHover = false
+		} else {
+			if (!before) {
+				this.multipleStatus.before = fullDate
+				this.multipleStatus.after = undefined;
+				this.lastHover = false
+			} else {
+				this.multipleStatus.after = fullDate
+				if (dateCompare(this.multipleStatus.before, this.multipleStatus.after)) {
+					this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus
+						.after);
+				} else {
+					this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus
+						.before);
+				}
+				this.lastHover = true
+			}
+		}
+		this.getWeeks(fullDate)
+	}
+
+	/**
+	 *  鼠标 hover 更新多选状态
+	 */
+	setHoverMultiple(fullDate) {
+		//抖音小程序点击会触发hover事件,需要避免一下
+		// #ifndef MP-TOUTIAO
+		if (!this.range || this.lastHover) return
+		const {
+			before
+		} = this.multipleStatus
+
+		if (!before) {
+			this.multipleStatus.before = fullDate
+		} else {
+			this.multipleStatus.after = fullDate
+			if (dateCompare(this.multipleStatus.before, this.multipleStatus.after)) {
+				this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after);
+			} else {
+				this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before);
+			}
+		}
+		this.getWeeks(fullDate)
+		// #endif
+
+	}
+
+	/**
+	 * 更新默认值多选状态
+	 */
+	setDefaultMultiple(before, after) {
+		this.multipleStatus.before = before
+		this.multipleStatus.after = after
+		if (before && after) {
+			if (dateCompare(before, after)) {
+				this.multipleStatus.data = this.geDateAll(before, after);
+				this.getWeeks(after)
+			} else {
+				this.multipleStatus.data = this.geDateAll(after, before);
+				this.getWeeks(before)
+			}
+		}
+	}
+
+	/**
+	 * 获取每周数据
+	 * @param {Object} dateData
+	 */
+	getWeeks(dateData) {
+		const {
+			year,
+			month,
+		} = this.getDateObj(dateData)
+
+		const preMonthDayAmount = new Date(year, month - 1, 1).getDay()
+		const preMonthDays = this.getPreMonthDays(preMonthDayAmount, this.getDateObj(dateData))
+
+		const currentMonthDayAmount = new Date(year, month, 0).getDate()
+		const currentMonthDays = this.getCurrentMonthDays(currentMonthDayAmount, this.getDateObj(dateData))
+
+		const nextMonthDayAmount = 42 - preMonthDayAmount - currentMonthDayAmount
+		const nextMonthDays = this._getNextMonthDays(nextMonthDayAmount, this.getDateObj(dateData))
+
+		const calendarDays = [...preMonthDays, ...currentMonthDays, ...nextMonthDays]
+
+		const weeks = new Array(6)
+		for (let i = 0; i < calendarDays.length; i++) {
+			const index = Math.floor(i / 7)
+			if (!weeks[index]) {
+				weeks[index] = new Array(7)
+			}
+			weeks[index][i % 7] = calendarDays[i]
+		}
+
+		this.calendar = calendarDays
+		this.weeks = weeks
+	}
+}
+
+function getDateTime(date, hideSecond) {
+	return `${getDate(date)} ${getTime(date, hideSecond)}`
+}
+
+function getDate(date) {
+	date = fixIosDateFormat(date)
+	date = new Date(date)
+	const year = date.getFullYear()
+	const month = date.getMonth() + 1
+	const day = date.getDate()
+	return `${year}-${addZero(month)}-${addZero(day)}`
+}
+
+function getTime(date, hideSecond) {
+	date = fixIosDateFormat(date)
+	date = new Date(date)
+	const hour = date.getHours()
+	const minute = date.getMinutes()
+	const second = date.getSeconds()
+	return hideSecond ? `${addZero(hour)}:${addZero(minute)}` : `${addZero(hour)}:${addZero(minute)}:${addZero(second)}`
+}
+
+function addZero(num) {
+	if (num < 10) {
+		num = `0${num}`
+	}
+	return num
+}
+
+function getDefaultSecond(hideSecond) {
+	return hideSecond ? '00:00' : '00:00:00'
+}
+
+function dateCompare(startDate, endDate) {
+	startDate = new Date(fixIosDateFormat(startDate))
+	endDate = new Date(fixIosDateFormat(endDate))
+	return startDate <= endDate
+}
+
+function checkDate(date) {
+	const dateReg = /((19|20)\d{2})(-|\/)\d{1,2}(-|\/)\d{1,2}/g
+	return date.match(dateReg)
+}
+//ios低版本15及以下,无法匹配 没有 ’秒‘ 时的情况,所以需要在末尾 秒 加上 问号
+const dateTimeReg = /^\d{4}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])( [0-5]?[0-9]:[0-5]?[0-9](:[0-5]?[0-9])?)?$/;
+
+function fixIosDateFormat(value) {
+	if (typeof value === 'string' && dateTimeReg.test(value)) {
+		value = value.replace(/-/g, '/')
+	}
+	return value
+}
+
+export {
+	Calendar,
+	getDateTime,
+	getDate,
+	getTime,
+	addZero,
+	getDefaultSecond,
+	dateCompare,
+	checkDate,
+	fixIosDateFormat
+}

+ 88 - 0
card/uni_modules/uni-datetime-picker/package.json

@@ -0,0 +1,88 @@
+{
+  "id": "uni-datetime-picker",
+  "displayName": "uni-datetime-picker 日期选择器",
+  "version": "2.2.38",
+  "description": "uni-datetime-picker 日期时间选择器,支持日历,支持范围选择",
+  "keywords": [
+    "uni-datetime-picker",
+    "uni-ui",
+    "uniui",
+    "日期时间选择器",
+    "日期时间"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": ""
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": [
+			"uni-scss",
+			"uni-icons"
+		],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "n"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "n"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 21 - 0
card/uni_modules/uni-datetime-picker/readme.md

@@ -0,0 +1,21 @@
+
+
+> `重要通知:组件升级更新 2.0.0 后,支持日期+时间范围选择,组件 ui 将使用日历选择日期,ui 变化较大,同时支持 PC 和 移动端。此版本不向后兼容,不再支持单独的时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker)。若仍需使用旧版本,可在插件市场下载*非uni_modules版本*,旧版本将不再维护`
+
+## DatetimePicker 时间选择器
+
+> **组件名:uni-datetime-picker**
+> 代码块: `uDatetimePicker`
+
+
+该组件的优势是,支持**时间戳**输入和输出(起始时间、终止时间也支持时间戳),可**同时选择**日期和时间。
+
+若只是需要单独选择日期和时间,不需要时间戳输入和输出,可使用原生的 picker 组件。
+
+**_点击 picker 默认值规则:_**
+
+- 若设置初始值 value, 会显示在 picker 显示框中
+- 若无初始值 value,则初始值 value 为当前本地时间 Date.now(), 但不会显示在 picker 显示框中
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 

+ 17 - 0
card/uni_modules/uni-link/changelog.md

@@ -0,0 +1,17 @@
+## 1.0.0(2021-11-19)
+- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-link](https://uniapp.dcloud.io/component/uniui/uni-link)
+## 1.1.7(2021-11-08)
+## 0.0.7(2021-09-03)
+- 修复 在 nvue 下不显示的 bug
+## 0.0.6(2021-07-30)
+- 新增 支持自定义插槽
+## 0.0.5(2021-06-21)
+- 新增 download 属性,H5平台下载文件名
+## 0.0.4(2021-05-12)
+- 新增 组件示例地址
+## 0.0.3(2021-03-09)
+- 新增 href 属性支持 tel:|mailto:
+
+## 0.0.2(2021-02-05)
+- 调整为uni_modules目录规范

+ 128 - 0
card/uni_modules/uni-link/components/uni-link/uni-link.vue

@@ -0,0 +1,128 @@
+<template>
+	<a v-if="isShowA" class="uni-link" :href="href"
+		:class="{'uni-link--withline':showUnderLine===true||showUnderLine==='true'}"
+		:style="{color,fontSize:fontSize+'px'}" :download="download">
+		<slot>{{text}}</slot>
+	</a>
+	<!-- #ifndef APP-NVUE -->
+	<text v-else class="uni-link" :class="{'uni-link--withline':showUnderLine===true||showUnderLine==='true'}"
+		:style="{color,fontSize:fontSize+'px'}" @click="openURL">
+		<slot>{{text}}</slot>
+	</text>
+	<!-- #endif -->
+	<!-- #ifdef APP-NVUE -->
+	<text v-else class="uni-link" :class="{'uni-link--withline':showUnderLine===true||showUnderLine==='true'}"
+		:style="{color,fontSize:fontSize+'px'}" @click="openURL">
+		{{text}}
+	</text>
+	<!-- #endif -->
+</template>
+
+<script>
+	/**
+	 * Link 外部网页超链接组件
+	 * @description uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打开新网页
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=1182
+	 * @property {String} href 点击后打开的外部网页url
+	 * @property {String} text 显示的文字
+	 * @property {String} downlaod H5平台下载文件名
+	 * @property {Boolean} showUnderLine 是否显示下划线
+	 * @property {String} copyTips 在小程序端复制链接时显示的提示语
+	 * @property {String} color 链接文字颜色
+	 * @property {String} fontSize 链接文字大小
+	 * @example * <uni-link href="https://ext.dcloud.net.cn" text="https://ext.dcloud.net.cn"></uni-link>
+	 */
+	export default {
+		name: 'uniLink',
+		props: {
+			href: {
+				type: String,
+				default: ''
+			},
+			text: {
+				type: String,
+				default: ''
+			},
+			download: {
+				type: String,
+				default: ''
+			},
+			showUnderLine: {
+				type: [Boolean, String],
+				default: true
+			},
+			copyTips: {
+				type: String,
+				default: '已自动复制网址,请在手机浏览器里粘贴该网址'
+			},
+			color: {
+				type: String,
+				default: '#999999'
+			},
+			fontSize: {
+				type: [Number, String],
+				default: 14
+			}
+		},
+		computed: {
+			isShowA() {
+				// #ifdef H5
+				this._isH5 = true;
+				// #endif
+				if ((this.isMail() || this.isTel()) && this._isH5 === true) {
+					return true;
+				}
+				return false;
+			}
+		},
+		created() {
+			this._isH5 = null;
+		},
+		methods: {
+			isMail() {
+				return this.href.startsWith('mailto:');
+			},
+			isTel() {
+				return this.href.startsWith('tel:');
+			},
+			openURL() {
+				// #ifdef APP-PLUS
+				if (this.isTel()) {
+					this.makePhoneCall(this.href.replace('tel:', ''));
+				} else {
+					plus.runtime.openURL(this.href);
+				}
+				// #endif
+				// #ifdef H5
+				window.open(this.href)
+				// #endif
+				// #ifdef MP
+				uni.setClipboardData({
+					data: this.href
+				});
+				uni.showModal({
+					content: this.copyTips,
+					showCancel: false
+				});
+				// #endif
+			},
+			makePhoneCall(phoneNumber) {
+				uni.makePhoneCall({
+					phoneNumber
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+	/* #ifndef APP-NVUE */
+	.uni-link {
+		cursor: pointer;
+	}
+
+	/* #endif */
+	.uni-link--withline {
+		text-decoration: underline;
+	}
+</style>

+ 87 - 0
card/uni_modules/uni-link/package.json

@@ -0,0 +1,87 @@
+{
+  "id": "uni-link",
+  "displayName": "uni-link 超链接",
+  "version": "1.0.0",
+  "description": "uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打",
+  "keywords": [
+    "uni-ui",
+    "uniui",
+    "link",
+    "超链接",
+    ""
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": ""
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+  "dcloudext": {
+    "category": [
+      "前端组件",
+      "通用组件"
+    ],
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
+  },
+  "uni_modules": {
+    "dependencies": ["uni-scss"],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "y",
+          "联盟": "y"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 11 - 0
card/uni_modules/uni-link/readme.md

@@ -0,0 +1,11 @@
+
+
+## Link 链接
+> **组件名:uni-link**
+> 代码块: `uLink`
+
+
+uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打开新网页。
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-link)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 

+ 33 - 0
card/uni_modules/uni-table/changelog.md

@@ -0,0 +1,33 @@
+## 1.2.8(2024-10-15)
+- 修复 运行到抖音小程序上出现的问题
+## 1.2.7(2024-10-15)
+- 修复 微信小程序中的getSystemInfo警告
+## 1.2.4(2023-12-19)
+- 修复 uni-tr只有一列时minWidth计算错误,列变化实时计算更新
+## 1.2.3(2023-03-28)
+- 修复 在vue3模式下可能会出现错误的问题
+## 1.2.2(2022-11-29)
+- 优化 主题样式
+## 1.2.1(2022-06-06)
+- 修复 微信小程序存在无使用组件的问题
+## 1.2.0(2021-11-19)
+- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-table](https://uniapp.dcloud.io/component/uniui/uni-table)
+## 1.1.0(2021-07-30)
+- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+## 1.0.7(2021-07-08)
+- 新增 uni-th 支持 date 日期筛选范围
+## 1.0.6(2021-07-05)
+- 新增 uni-th 支持 range 筛选范围
+## 1.0.5(2021-06-28)
+- 新增 uni-th 筛选功能
+## 1.0.4(2021-05-12)
+- 新增 示例地址
+- 修复 示例项目缺少组件的Bug
+## 1.0.3(2021-04-16)
+- 新增 sortable 属性,是否开启单列排序
+- 优化 表格多选逻辑
+## 1.0.2(2021-03-22)
+- uni-tr 添加 disabled 属性,用于 type=selection 时,设置某行是否可由全选按钮控制
+## 1.0.1(2021-02-05)
+- 调整为uni_modules目录规范

+ 460 - 0
card/uni_modules/uni-table/components/uni-table/uni-table.vue

@@ -0,0 +1,460 @@
+<template>
+	<view class="uni-table-scroll" :class="{ 'table--border': border, 'border-none': !noData }">
+		<!-- #ifdef H5 -->
+		<table class="uni-table" border="0" cellpadding="0" cellspacing="0" :class="{ 'table--stripe': stripe }" :style="{ 'min-width': minWidth + 'px' }">
+			<slot></slot>
+			<tr v-if="noData" class="uni-table-loading">
+				<td class="uni-table-text" :class="{ 'empty-border': border }">{{ emptyText }}</td>
+			</tr>
+			<view v-if="loading" class="uni-table-mask" :class="{ 'empty-border': border }"><div class="uni-table--loader"></div></view>
+		</table>
+		<!-- #endif -->
+		<!-- #ifndef H5 -->
+		<view class="uni-table" :style="{ 'min-width': minWidth + 'px' }" :class="{ 'table--stripe': stripe }">
+			<slot></slot>
+			<view v-if="noData" class="uni-table-loading">
+				<view class="uni-table-text" :class="{ 'empty-border': border }">{{ emptyText }}</view>
+			</view>
+			<view v-if="loading" class="uni-table-mask" :class="{ 'empty-border': border }"><div class="uni-table--loader"></div></view>
+		</view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+/**
+ * Table 表格
+ * @description 用于展示多条结构类似的数据
+ * @tutorial https://ext.dcloud.net.cn/plugin?id=3270
+ * @property {Boolean} 	border 				是否带有纵向边框
+ * @property {Boolean} 	stripe 				是否显示斑马线
+ * @property {Boolean} 	type 					是否开启多选
+ * @property {String} 	emptyText 			空数据时显示的文本内容
+ * @property {Boolean} 	loading 			显示加载中
+ * @event {Function} 	selection-change 	开启多选时,当选择项发生变化时会触发该事件
+ */
+export default {
+	name: 'uniTable',
+	options: {
+		// #ifdef MP-TOUTIAO
+		virtualHost: false,
+		// #endif
+		// #ifndef MP-TOUTIAO
+		virtualHost: true
+		// #endif
+	},
+	emits:['selection-change'],
+	props: {
+		data: {
+			type: Array,
+			default() {
+				return []
+			}
+		},
+		// 是否有竖线
+		border: {
+			type: Boolean,
+			default: false
+		},
+		// 是否显示斑马线
+		stripe: {
+			type: Boolean,
+			default: false
+		},
+		// 多选
+		type: {
+			type: String,
+			default: ''
+		},
+		// 没有更多数据
+		emptyText: {
+			type: String,
+			default: '没有更多数据'
+		},
+		loading: {
+			type: Boolean,
+			default: false
+		},
+		rowKey: {
+			type: String,
+			default: ''
+		}
+	},
+	data() {
+		return {
+			noData: true,
+			minWidth: 0,
+			multiTableHeads: []
+		}
+	},
+	watch: {
+		loading(val) {},
+		data(newVal) {
+			let theadChildren = this.theadChildren
+			let rowspan = 1
+			if (this.theadChildren) {
+				rowspan = this.theadChildren.rowspan
+			}
+
+			// this.trChildren.length - rowspan
+			this.noData = false
+			// this.noData = newVal.length === 0
+		}
+	},
+	created() {
+		// 定义tr的实例数组
+		this.trChildren = []
+		this.thChildren = []
+		this.theadChildren = null
+		this.backData = []
+		this.backIndexData = []
+	},
+
+	methods: {
+		isNodata() {
+			let theadChildren = this.theadChildren
+			let rowspan = 1
+			if (this.theadChildren) {
+				rowspan = this.theadChildren.rowspan
+			}
+			this.noData = this.trChildren.length - rowspan <= 0
+		},
+		/**
+		 * 选中所有
+		 */
+		selectionAll() {
+			let startIndex = 1
+			let theadChildren = this.theadChildren
+			if (!this.theadChildren) {
+				theadChildren = this.trChildren[0]
+			} else {
+				startIndex = theadChildren.rowspan - 1
+			}
+			let isHaveData = this.data && this.data.length > 0
+			theadChildren.checked = true
+			theadChildren.indeterminate = false
+			this.trChildren.forEach((item, index) => {
+				if (!item.disabled) {
+					item.checked = true
+					if (isHaveData && item.keyValue) {
+						const row = this.data.find(v => v[this.rowKey] === item.keyValue)
+						if (!this.backData.find(v => v[this.rowKey] === row[this.rowKey])) {
+							this.backData.push(row)
+						}
+					}
+					if (index > (startIndex - 1) && this.backIndexData.indexOf(index - startIndex) === -1) {
+						this.backIndexData.push(index - startIndex)
+					}
+				}
+			})
+			// this.backData = JSON.parse(JSON.stringify(this.data))
+			this.$emit('selection-change', {
+				detail: {
+					value: this.backData,
+					index: this.backIndexData
+				}
+			})
+		},
+		/**
+		 * 用于多选表格,切换某一行的选中状态,如果使用了第二个参数,则是设置这一行选中与否(selected 为 true 则选中)
+		 */
+		toggleRowSelection(row, selected) {
+			// if (!this.theadChildren) return
+			row = [].concat(row)
+
+			this.trChildren.forEach((item, index) => {
+				// if (item.keyValue) {
+
+				const select = row.findIndex(v => {
+					//
+					if (typeof v === 'number') {
+						return v === index - 1
+					} else {
+						return v[this.rowKey] === item.keyValue
+					}
+				})
+				let ischeck = item.checked
+				if (select !== -1) {
+					if (typeof selected === 'boolean') {
+						item.checked = selected
+					} else {
+						item.checked = !item.checked
+					}
+					if (ischeck !== item.checked) {
+						this.check(item.rowData||item, item.checked, item.rowData?item.keyValue:null, true)
+					}
+				}
+				// }
+			})
+			this.$emit('selection-change', {
+				detail: {
+					value: this.backData,
+					index:this.backIndexData
+				}
+			})
+		},
+
+		/**
+		 * 用于多选表格,清空用户的选择
+		 */
+		clearSelection() {
+			let theadChildren = this.theadChildren
+			if (!this.theadChildren) {
+				theadChildren = this.trChildren[0]
+			}
+			// if (!this.theadChildren) return
+			theadChildren.checked = false
+			theadChildren.indeterminate = false
+			this.trChildren.forEach(item => {
+				// if (item.keyValue) {
+					item.checked = false
+				// }
+			})
+			this.backData = []
+			this.backIndexData = []
+			this.$emit('selection-change', {
+				detail: {
+					value: [],
+					index: []
+				}
+			})
+		},
+		/**
+		 * 用于多选表格,切换所有行的选中状态
+		 */
+		toggleAllSelection() {
+			let list = []
+			let startIndex = 1
+			let theadChildren = this.theadChildren
+			if (!this.theadChildren) {
+				theadChildren = this.trChildren[0]
+			} else {
+				startIndex = theadChildren.rowspan - 1
+			}
+			this.trChildren.forEach((item, index) => {
+				if (!item.disabled) {
+					if (index > (startIndex - 1) ) {
+						list.push(index-startIndex)
+					}
+				}
+			})
+			this.toggleRowSelection(list)
+		},
+
+		/**
+		 * 选中\取消选中
+		 * @param {Object} child
+		 * @param {Object} check
+		 * @param {Object} rowValue
+		 */
+		check(child, check, keyValue, emit) {
+			let theadChildren = this.theadChildren
+			if (!this.theadChildren) {
+				theadChildren = this.trChildren[0]
+			}
+
+
+
+			let childDomIndex = this.trChildren.findIndex((item, index) => child === item)
+			if(childDomIndex < 0){
+				childDomIndex = this.data.findIndex(v=>v[this.rowKey] === keyValue) + 1
+			}
+			const dataLen = this.trChildren.filter(v => !v.disabled && v.keyValue).length
+			if (childDomIndex === 0) {
+				check ? this.selectionAll() : this.clearSelection()
+				return
+			}
+
+			if (check) {
+				if (keyValue) {
+					this.backData.push(child)
+				}
+				this.backIndexData.push(childDomIndex - 1)
+			} else {
+				const index = this.backData.findIndex(v => v[this.rowKey] === keyValue)
+				const idx = this.backIndexData.findIndex(item => item === childDomIndex - 1)
+				if (keyValue) {
+					this.backData.splice(index, 1)
+				}
+				this.backIndexData.splice(idx, 1)
+			}
+
+			const domCheckAll = this.trChildren.find((item, index) => index > 0 && !item.checked && !item.disabled)
+			if (!domCheckAll) {
+				theadChildren.indeterminate = false
+				theadChildren.checked = true
+			} else {
+				theadChildren.indeterminate = true
+				theadChildren.checked = false
+			}
+
+			if (this.backIndexData.length === 0) {
+				theadChildren.indeterminate = false
+			}
+
+			if (!emit) {
+				this.$emit('selection-change', {
+					detail: {
+						value: this.backData,
+						index: this.backIndexData
+					}
+				})
+			}
+		}
+	}
+}
+</script>
+
+<style lang="scss">
+$border-color: #ebeef5;
+
+.uni-table-scroll {
+	width: 100%;
+	/* #ifndef APP-NVUE */
+	overflow-x: auto;
+	/* #endif */
+}
+
+.uni-table {
+	position: relative;
+	width: 100%;
+	border-radius: 5px;
+	// box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.1);
+	background-color: #fff;
+	/* #ifndef APP-NVUE */
+	box-sizing: border-box;
+	display: table;
+	overflow-x: auto;
+	::v-deep .uni-table-tr:nth-child(n + 2) {
+		&:hover {
+			background-color: #f5f7fa;
+		}
+	}
+	::v-deep .uni-table-thead {
+		.uni-table-tr {
+			// background-color: #f5f7fa;
+			&:hover {
+				background-color:#fafafa;
+			}
+		}
+	}
+	/* #endif */
+}
+
+.table--border {
+	border: 1px $border-color solid;
+	border-right: none;
+}
+
+.border-none {
+	/* #ifndef APP-NVUE */
+	border-bottom: none;
+	/* #endif */
+}
+
+.table--stripe {
+	/* #ifndef APP-NVUE */
+	::v-deep .uni-table-tr:nth-child(2n + 3) {
+		background-color: #fafafa;
+	}
+	/* #endif */
+}
+
+/* 表格加载、无数据样式 */
+.uni-table-loading {
+	position: relative;
+	/* #ifndef APP-NVUE */
+	display: table-row;
+	/* #endif */
+	height: 50px;
+	line-height: 50px;
+	overflow: hidden;
+	box-sizing: border-box;
+}
+.empty-border {
+	border-right: 1px $border-color solid;
+}
+.uni-table-text {
+	position: absolute;
+	right: 0;
+	left: 0;
+	text-align: center;
+	font-size: 14px;
+	color: #999;
+}
+
+.uni-table-mask {
+	position: absolute;
+	top: 0;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	background-color: rgba(255, 255, 255, 0.8);
+	z-index: 99;
+	/* #ifndef APP-NVUE */
+	display: flex;
+	margin: auto;
+	transition: all 0.5s;
+	/* #endif */
+	justify-content: center;
+	align-items: center;
+}
+
+.uni-table--loader {
+	width: 30px;
+	height: 30px;
+	border: 2px solid #aaa;
+	// border-bottom-color: transparent;
+	border-radius: 50%;
+	/* #ifndef APP-NVUE */
+	animation: 2s uni-table--loader linear infinite;
+	/* #endif */
+	position: relative;
+}
+
+@keyframes uni-table--loader {
+	0% {
+		transform: rotate(360deg);
+	}
+
+	10% {
+		border-left-color: transparent;
+	}
+
+	20% {
+		border-bottom-color: transparent;
+	}
+
+	30% {
+		border-right-color: transparent;
+	}
+
+	40% {
+		border-top-color: transparent;
+	}
+
+	50% {
+		transform: rotate(0deg);
+	}
+
+	60% {
+		border-top-color: transparent;
+	}
+
+	70% {
+		border-left-color: transparent;
+	}
+
+	80% {
+		border-bottom-color: transparent;
+	}
+
+	90% {
+		border-right-color: transparent;
+	}
+
+	100% {
+		transform: rotate(-360deg);
+	}
+}
+</style>

+ 34 - 0
card/uni_modules/uni-table/components/uni-tbody/uni-tbody.vue

@@ -0,0 +1,34 @@
+<template>
+	<!-- #ifdef H5 -->
+	<tbody>
+		<slot></slot>
+	</tbody>
+	<!-- #endif -->
+	<!-- #ifndef H5 -->
+	<view><slot></slot></view>
+	<!-- #endif -->
+</template>
+
+<script>
+export default {
+	name: 'uniBody',
+	options: {
+		// #ifdef MP-TOUTIAO
+		virtualHost: false,
+		// #endif
+		// #ifndef MP-TOUTIAO
+		virtualHost: true
+		// #endif
+	},
+	data() {
+		return {
+
+		}
+	},
+	created() {},
+	methods: {}
+}
+</script>
+
+<style>
+</style>

+ 95 - 0
card/uni_modules/uni-table/components/uni-td/uni-td.vue

@@ -0,0 +1,95 @@
+<template>
+	<!-- #ifdef H5 -->
+	<td class="uni-table-td" :rowspan="rowspan" :colspan="colspan" :class="{'table--border':border}" :style="{width:width + 'px','text-align':align}">
+		<slot></slot>
+	</td>
+	<!-- #endif -->
+	<!-- #ifndef H5 -->
+	<!-- :class="{'table--border':border}"  -->
+	<view class="uni-table-td" :class="{'table--border':border}" :style="{width:width + 'px','text-align':align}">
+		<slot></slot>
+	</view>
+	<!-- #endif -->
+
+</template>
+
+<script>
+	/**
+	 * Td 单元格
+	 * @description 表格中的标准单元格组件
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=3270
+	 * @property {Number} 	align = [left|center|right]	单元格对齐方式
+	 */
+	export default {
+		name: 'uniTd',
+		options: {
+			// #ifdef MP-TOUTIAO
+			virtualHost: false,
+			// #endif
+			// #ifndef MP-TOUTIAO
+			virtualHost: true
+			// #endif
+		},
+		props: {
+			width: {
+				type: [String, Number],
+				default: ''
+			},
+			align: {
+				type: String,
+				default: 'left'
+			},
+			rowspan: {
+				type: [Number,String],
+				default: 1
+			},
+			colspan: {
+					type: [Number,String],
+				default: 1
+			}
+		},
+		data() {
+			return {
+				border: false
+			};
+		},
+		created() {
+			this.root = this.getTable()
+			this.border = this.root.border
+		},
+		methods: {
+			/**
+			 * 获取父元素实例
+			 */
+			getTable() {
+				let parent = this.$parent;
+				let parentName = parent.$options.name;
+				while (parentName !== 'uniTable') {
+					parent = parent.$parent;
+					if (!parent) return false;
+					parentName = parent.$options.name;
+				}
+				return parent;
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	$border-color:#EBEEF5;
+
+	.uni-table-td {
+		display: table-cell;
+		padding: 8px 10px;
+		font-size: 14px;
+		border-bottom: 1px $border-color solid;
+		font-weight: 400;
+		color: #606266;
+		line-height: 23px;
+		box-sizing: border-box;
+	}
+
+	.table--border {
+		border-right: 1px $border-color solid;
+	}
+</style>

+ 511 - 0
card/uni_modules/uni-table/components/uni-th/filter-dropdown.vue

@@ -0,0 +1,511 @@
+<template>
+	<view class="uni-filter-dropdown">
+		<view class="dropdown-btn" @click="onDropdown">
+			<view class="icon-select" :class="{active: canReset}" v-if="isSelect || isRange"></view>
+			<view class="icon-search" :class="{active: canReset}" v-if="isSearch">
+				<view class="icon-search-0"></view>
+				<view class="icon-search-1"></view>
+			</view>
+			<view class="icon-calendar" :class="{active: canReset}" v-if="isDate">
+				<view class="icon-calendar-0"></view>
+				<view class="icon-calendar-1"></view>
+			</view>
+		</view>
+		<view class="uni-dropdown-cover" v-if="isOpened" @click="handleClose"></view>
+		<view class="dropdown-popup dropdown-popup-right" v-if="isOpened" @click.stop>
+			<!-- select-->
+			<view v-if="isSelect" class="list">
+				<label class="flex-r a-i-c list-item" v-for="(item,index) in dataList" :key="index"
+					@click="onItemClick($event, index)">
+					<check-box class="check" :checked="item.checked" />
+					<view class="checklist-content">
+						<text class="checklist-text" :style="item.styleIconText">{{item[map.text]}}</text>
+					</view>
+				</label>
+			</view>
+			<view v-if="isSelect" class="flex-r opera-area">
+				<view class="flex-f btn btn-default" :class="{disable: !canReset}" @click="handleSelectReset">
+					{{resource.reset}}</view>
+				<view class="flex-f btn btn-submit" @click="handleSelectSubmit">{{resource.submit}}</view>
+			</view>
+			<!-- search -->
+			<view v-if="isSearch" class="search-area">
+				<input class="search-input" v-model="filterValue" />
+			</view>
+			<view v-if="isSearch" class="flex-r opera-area">
+				<view class="flex-f btn btn-submit" @click="handleSearchSubmit">{{resource.search}}</view>
+				<view class="flex-f btn btn-default" :class="{disable: !canReset}" @click="handleSearchReset">
+					{{resource.reset}}</view>
+			</view>
+			<!-- range -->
+			<view v-if="isRange">
+				<view class="input-label">{{resource.gt}}</view>
+				<input class="input" v-model="gtValue" />
+				<view class="input-label">{{resource.lt}}</view>
+				<input class="input" v-model="ltValue" />
+			</view>
+			<view v-if="isRange" class="flex-r opera-area">
+				<view class="flex-f btn btn-default" :class="{disable: !canReset}" @click="handleRangeReset">
+					{{resource.reset}}</view>
+				<view class="flex-f btn btn-submit" @click="handleRangeSubmit">{{resource.submit}}</view>
+			</view>
+			<!-- date -->
+			<view v-if="isDate">
+				<uni-datetime-picker ref="datetimepicker" :value="dateRange" type="datetimerange" return-type="timestamp" @change="datetimechange" @maskClick="timepickerclose">
+					<view></view>
+				</uni-datetime-picker>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import checkBox from '../uni-tr/table-checkbox.vue'
+
+	const resource = {
+		"reset": "重置",
+		"search": "搜索",
+		"submit": "确定",
+		"filter": "筛选",
+		"gt": "大于等于",
+		"lt": "小于等于",
+		"date": "日期范围"
+	}
+
+	const DropdownType = {
+		Select: "select",
+		Search: "search",
+		Range: "range",
+		Date: "date",
+		Timestamp: "timestamp"
+	}
+
+	export default {
+		name: 'FilterDropdown',
+		emits:['change'],
+		components: {
+			checkBox
+		},
+		options: {
+			virtualHost: true
+		},
+		props: {
+			filterType: {
+				type: String,
+				default: DropdownType.Select
+			},
+			filterData: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			mode: {
+				type: String,
+				default: 'default'
+			},
+			map: {
+				type: Object,
+				default () {
+					return {
+						text: 'text',
+						value: 'value'
+					}
+				}
+			},
+			filterDefaultValue: {
+				type: [Array,String],
+				default () {
+					return ""
+				}
+			}
+		},
+		computed: {
+			canReset() {
+				if (this.isSearch) {
+					return this.filterValue.length > 0
+				}
+				if (this.isSelect) {
+					return this.checkedValues.length > 0
+				}
+				if (this.isRange) {
+					return (this.gtValue.length > 0 && this.ltValue.length > 0)
+				}
+				if (this.isDate) {
+					return this.dateSelect.length > 0
+				}
+				return false
+			},
+			isSelect() {
+				return this.filterType === DropdownType.Select
+			},
+			isSearch() {
+				return this.filterType === DropdownType.Search
+			},
+			isRange() {
+				return this.filterType === DropdownType.Range
+			},
+			isDate() {
+				return (this.filterType === DropdownType.Date || this.filterType === DropdownType.Timestamp)
+			}
+		},
+		watch: {
+			filterData(newVal) {
+				this._copyFilters()
+			},
+			indeterminate(newVal) {
+				this.isIndeterminate = newVal
+			}
+		},
+		data() {
+			return {
+				resource,
+				enabled: true,
+				isOpened: false,
+				dataList: [],
+				filterValue: this.filterDefaultValue,
+				checkedValues: [],
+				gtValue: '',
+				ltValue: '',
+				dateRange: [],
+				dateSelect: []
+			};
+		},
+		created() {
+			this._copyFilters()
+		},
+		methods: {
+			_copyFilters() {
+				let dl = JSON.parse(JSON.stringify(this.filterData))
+				for (let i = 0; i < dl.length; i++) {
+					if (dl[i].checked === undefined) {
+						dl[i].checked = false
+					}
+				}
+				this.dataList = dl
+			},
+			openPopup() {
+				this.isOpened = true
+				if (this.isDate) {
+					this.$nextTick(() => {
+						if (!this.dateRange.length) {
+							this.resetDate()
+						}
+						this.$refs.datetimepicker.show()
+					})
+				}
+			},
+			closePopup() {
+				this.isOpened = false
+			},
+			handleClose(e) {
+				this.closePopup()
+			},
+			resetDate() {
+				let date = new Date()
+				let dateText = date.toISOString().split('T')[0]
+				this.dateRange = [dateText + ' 0:00:00', dateText + ' 23:59:59']
+			},
+			onDropdown(e) {
+				this.openPopup()
+			},
+			onItemClick(e, index) {
+				let items = this.dataList
+				let listItem = items[index]
+				if (listItem.checked === undefined) {
+					items[index].checked = true
+				} else {
+					items[index].checked = !listItem.checked
+				}
+
+				let checkvalues = []
+				for (let i = 0; i < items.length; i++) {
+					const item = items[i]
+					if (item.checked) {
+						checkvalues.push(item.value)
+					}
+				}
+				this.checkedValues = checkvalues
+			},
+			datetimechange(e) {
+				this.closePopup()
+				this.dateRange = e
+				this.dateSelect = e
+				this.$emit('change', {
+					filterType: this.filterType,
+					filter: e
+				})
+			},
+			timepickerclose(e) {
+				this.closePopup()
+			},
+			handleSelectSubmit() {
+				this.closePopup()
+				this.$emit('change', {
+					filterType: this.filterType,
+					filter: this.checkedValues
+				})
+			},
+			handleSelectReset() {
+				if (!this.canReset) {
+					return;
+				}
+				var items = this.dataList
+				for (let i = 0; i < items.length; i++) {
+					let item = items[i]
+					this.$set(item, 'checked', false)
+				}
+				this.checkedValues = []
+				this.handleSelectSubmit()
+			},
+			handleSearchSubmit() {
+				this.closePopup()
+				this.$emit('change', {
+					filterType: this.filterType,
+					filter: this.filterValue
+				})
+			},
+			handleSearchReset() {
+				if (!this.canReset) {
+					return;
+				}
+				this.filterValue = ''
+				this.handleSearchSubmit()
+			},
+			handleRangeSubmit(isReset) {
+				this.closePopup()
+				this.$emit('change', {
+					filterType: this.filterType,
+					filter: isReset === true ? [] : [parseInt(this.gtValue), parseInt(this.ltValue)]
+				})
+			},
+			handleRangeReset() {
+				if (!this.canReset) {
+					return;
+				}
+				this.gtValue = ''
+				this.ltValue = ''
+				this.handleRangeSubmit(true)
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	$uni-primary: #1890ff !default;
+	
+	.flex-r {
+		display: flex;
+		flex-direction: row;
+	}
+
+	.flex-f {
+		flex: 1;
+	}
+
+	.a-i-c {
+		align-items: center;
+	}
+
+	.j-c-c {
+		justify-content: center;
+	}
+
+	.icon-select {
+		width: 14px;
+		height: 16px;
+		border: solid 6px transparent;
+		border-top: solid 6px #ddd;
+		border-bottom: none;
+		background-color: #ddd;
+		background-clip: content-box;
+		box-sizing: border-box;
+	}
+
+	.icon-select.active {
+		background-color: $uni-primary;
+		border-top-color: $uni-primary;
+	}
+
+	.icon-search {
+		width: 12px;
+		height: 16px;
+		position: relative;
+	}
+
+	.icon-search-0 {
+		border: 2px solid #ddd;
+		border-radius: 8px;
+		width: 7px;
+		height: 7px;
+	}
+
+	.icon-search-1 {
+		position: absolute;
+		top: 8px;
+		right: 0;
+		width: 1px;
+		height: 7px;
+		background-color: #ddd;
+		transform: rotate(-45deg);
+	}
+
+	.icon-search.active .icon-search-0 {
+		border-color: $uni-primary;
+	}
+
+	.icon-search.active .icon-search-1 {
+		background-color: $uni-primary;
+	}
+
+	.icon-calendar {
+		color: #ddd;
+		width: 14px;
+		height: 16px;
+	}
+
+	.icon-calendar-0 {
+		height: 4px;
+		margin-top: 3px;
+		margin-bottom: 1px;
+		background-color: #ddd;
+		border-radius: 2px 2px 1px 1px;
+		position: relative;
+	}
+	.icon-calendar-0:before, .icon-calendar-0:after {
+		content: '';
+		position: absolute;
+		top: -3px;
+		width: 4px;
+		height: 3px;
+		border-radius: 1px;
+		background-color: #ddd;
+	}
+	.icon-calendar-0:before {
+		left: 2px;
+	}
+	.icon-calendar-0:after {
+		right: 2px;
+	}
+
+	.icon-calendar-1 {
+		height: 9px;
+		background-color: #ddd;
+		border-radius: 1px 1px 2px 2px;
+	}
+
+	.icon-calendar.active {
+		color: $uni-primary;
+	}
+
+	.icon-calendar.active .icon-calendar-0,
+	.icon-calendar.active .icon-calendar-1,
+	.icon-calendar.active .icon-calendar-0:before,
+	.icon-calendar.active .icon-calendar-0:after {
+		background-color: $uni-primary;
+	}
+
+	.uni-filter-dropdown {
+		position: relative;
+		font-weight: normal;
+	}
+
+	.dropdown-popup {
+		position: absolute;
+		top: 100%;
+		background-color: #fff;
+		box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d;
+		min-width: 150px;
+		z-index: 1000;
+	}
+
+	.dropdown-popup-left {
+		left: 0;
+	}
+
+	.dropdown-popup-right {
+		right: 0;
+	}
+
+	.uni-dropdown-cover {
+		position: fixed;
+		left: 0;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		background-color: transparent;
+		z-index: 100;
+	}
+
+	.list {
+		margin-top: 5px;
+		margin-bottom: 5px;
+	}
+
+	.list-item {
+		padding: 5px 10px;
+		text-align: left;
+	}
+
+	.list-item:hover {
+		background-color: #f0f0f0;
+	}
+
+	.check {
+		margin-right: 5px;
+	}
+
+	.search-area {
+		padding: 10px;
+	}
+
+	.search-input {
+		font-size: 12px;
+		border: 1px solid #f0f0f0;
+		border-radius: 3px;
+		padding: 2px 5px;
+		min-width: 150px;
+		text-align: left;
+	}
+
+	.input-label {
+		margin: 10px 10px 5px 10px;
+		text-align: left;
+	}
+
+	.input {
+		font-size: 12px;
+		border: 1px solid #f0f0f0;
+		border-radius: 3px;
+		margin: 10px;
+		padding: 2px 5px;
+		min-width: 150px;
+		text-align: left;
+	}
+
+	.opera-area {
+		cursor: default;
+		border-top: 1px solid #ddd;
+		padding: 5px;
+	}
+
+	.opera-area .btn {
+		font-size: 12px;
+		border-radius: 3px;
+		margin: 5px;
+		padding: 4px 4px;
+	}
+
+	.btn-default {
+		border: 1px solid #ddd;
+	}
+
+	.btn-default.disable {
+		border-color: transparent;
+	}
+
+	.btn-submit {
+		background-color: $uni-primary;
+		color: #ffffff;
+	}
+</style>

+ 295 - 0
card/uni_modules/uni-table/components/uni-th/uni-th.vue

@@ -0,0 +1,295 @@
+<template>
+	<!-- #ifdef H5 -->
+	<th :rowspan="rowspan" :colspan="colspan" class="uni-table-th" :class="{ 'table--border': border }" :style="{ width: customWidth + 'px', 'text-align': align }">
+		<view class="uni-table-th-row">
+			<view class="uni-table-th-content" :style="{ 'justify-content': contentAlign }" @click="sort">
+				<slot></slot>
+				<view v-if="sortable" class="arrow-box">
+					<text class="arrow up" :class="{ active: ascending }" @click.stop="ascendingFn"></text>
+					<text class="arrow down" :class="{ active: descending }" @click.stop="descendingFn"></text>
+				</view>
+			</view>
+			<dropdown v-if="filterType || filterData.length" :filterDefaultValue="filterDefaultValue" :filterData="filterData" :filterType="filterType" @change="ondropdown"></dropdown>
+		</view>
+	</th>
+	<!-- #endif -->
+	<!-- #ifndef H5 -->
+	<view class="uni-table-th" :class="{ 'table--border': border }" :style="{ width: customWidth + 'px', 'text-align': align }"><slot></slot></view>
+	<!-- #endif -->
+</template>
+
+<script>
+	// #ifdef H5
+	import dropdown from './filter-dropdown.vue'
+	// #endif
+/**
+ * Th 表头
+ * @description 表格内的表头单元格组件
+ * @tutorial https://ext.dcloud.net.cn/plugin?id=3270
+ * @property {Number | String} 	width 	单元格宽度(支持纯数字、携带单位px或rpx)
+ * @property {Boolean} 	sortable 					是否启用排序
+ * @property {Number} 	align = [left|center|right]	单元格对齐方式
+ * @value left   	单元格文字左侧对齐
+ * @value center	单元格文字居中
+ * @value right		单元格文字右侧对齐
+ * @property {Array}	filterData 筛选数据
+ * @property {String}	filterType	[search|select] 筛选类型
+ * @value search	关键字搜素
+ * @value select	条件选择
+ * @event {Function} sort-change 排序触发事件
+ */
+export default {
+	name: 'uniTh',
+	options: {
+		// #ifdef MP-TOUTIAO
+		virtualHost: false,
+		// #endif
+		// #ifndef MP-TOUTIAO
+		virtualHost: true
+		// #endif
+	},
+	components: {
+		// #ifdef H5
+		dropdown
+		// #endif
+	},
+	emits:['sort-change','filter-change'],
+	props: {
+		width: {
+			type: [String, Number],
+			default: ''
+		},
+		align: {
+			type: String,
+			default: 'left'
+		},
+		rowspan: {
+			type: [Number, String],
+			default: 1
+		},
+		colspan: {
+			type: [Number, String],
+			default: 1
+		},
+		sortable: {
+			type: Boolean,
+			default: false
+		},
+		filterType: {
+			type: String,
+			default: ""
+		},
+		filterData: {
+			type: Array,
+			default () {
+				return []
+			}
+		},
+		filterDefaultValue: {
+			type: [Array,String],
+			default () {
+				return ""
+			}
+		}
+	},
+	data() {
+		return {
+			border: false,
+			ascending: false,
+			descending: false
+		}
+	},
+	computed: {
+		// 根据props中的width属性 自动匹配当前th的宽度(px)
+		customWidth(){
+			if(typeof this.width === 'number'){
+				return this.width
+			} else if(typeof this.width === 'string') {
+				let regexHaveUnitPx = new RegExp(/^[1-9][0-9]*px$/g)
+				let regexHaveUnitRpx = new RegExp(/^[1-9][0-9]*rpx$/g)
+				let regexHaveNotUnit = new RegExp(/^[1-9][0-9]*$/g)
+				if (this.width.match(regexHaveUnitPx) !== null) { // 携带了 px
+					return this.width.replace('px', '')
+				} else if (this.width.match(regexHaveUnitRpx) !== null) { // 携带了 rpx
+					let numberRpx = Number(this.width.replace('rpx', ''))
+					// #ifdef MP-WEIXIN
+					let widthCoe = uni.getWindowInfo().screenWidth / 750
+					// #endif
+					// #ifndef MP-WEIXIN
+					let widthCoe = uni.getSystemInfoSync().screenWidth / 750
+					// #endif
+					return Math.round(numberRpx * widthCoe)
+				} else if (this.width.match(regexHaveNotUnit) !== null) { // 未携带 rpx或px 的纯数字 String
+					return this.width
+				} else { // 不符合格式
+					return ''
+				}
+			} else {
+				return ''
+			}
+		},
+		contentAlign() {
+			let align = 'left'
+			switch (this.align) {
+				case 'left':
+					align = 'flex-start'
+					break
+				case 'center':
+					align = 'center'
+					break
+				case 'right':
+					align = 'flex-end'
+					break
+			}
+			return align
+		}
+	},
+	created() {
+		this.root = this.getTable('uniTable')
+		this.rootTr = this.getTable('uniTr')
+		this.rootTr.minWidthUpdate(this.customWidth ? this.customWidth : 140)
+		this.border = this.root.border
+		this.root.thChildren.push(this)
+	},
+	methods: {
+		sort() {
+			if (!this.sortable) return
+			this.clearOther()
+			if (!this.ascending && !this.descending) {
+				this.ascending = true
+				this.$emit('sort-change', { order: 'ascending' })
+				return
+			}
+			if (this.ascending && !this.descending) {
+				this.ascending = false
+				this.descending = true
+				this.$emit('sort-change', { order: 'descending' })
+				return
+			}
+
+			if (!this.ascending && this.descending) {
+				this.ascending = false
+				this.descending = false
+				this.$emit('sort-change', { order: null })
+			}
+		},
+		ascendingFn() {
+			this.clearOther()
+			this.ascending = !this.ascending
+			this.descending = false
+			this.$emit('sort-change', { order: this.ascending ? 'ascending' : null })
+		},
+		descendingFn() {
+			this.clearOther()
+			this.descending = !this.descending
+			this.ascending = false
+			this.$emit('sort-change', { order: this.descending ? 'descending' : null })
+		},
+		clearOther() {
+			this.root.thChildren.map(item => {
+				if (item !== this) {
+					item.ascending = false
+					item.descending = false
+				}
+				return item
+			})
+		},
+		ondropdown(e) {
+			this.$emit("filter-change", e)
+		},
+		/**
+		 * 获取父元素实例
+		 */
+		getTable(name) {
+			let parent = this.$parent
+			let parentName = parent.$options.name
+			while (parentName !== name) {
+				parent = parent.$parent
+				if (!parent) return false
+				parentName = parent.$options.name
+			}
+			return parent
+		}
+	}
+}
+</script>
+
+<style lang="scss">
+$border-color: #ebeef5;
+$uni-primary: #007aff !default;
+
+.uni-table-th {
+	padding: 12px 10px;
+	/* #ifndef APP-NVUE */
+	display: table-cell;
+	box-sizing: border-box;
+	/* #endif */
+	font-size: 14px;
+	font-weight: bold;
+	color: #909399;
+	border-bottom: 1px $border-color solid;
+}
+
+.uni-table-th-row {
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: row;
+}
+
+.table--border {
+	border-right: 1px $border-color solid;
+}
+.uni-table-th-content {
+	display: flex;
+	align-items: center;
+	flex: 1;
+}
+.arrow-box {
+}
+.arrow {
+	display: block;
+	position: relative;
+	width: 10px;
+	height: 8px;
+	// border: 1px red solid;
+	left: 5px;
+	overflow: hidden;
+	cursor: pointer;
+}
+.down {
+	top: 3px;
+	::after {
+		content: '';
+		width: 8px;
+		height: 8px;
+		position: absolute;
+		left: 2px;
+		top: -5px;
+		transform: rotate(45deg);
+		background-color: #ccc;
+	}
+	&.active {
+		::after {
+			background-color: $uni-primary;
+		}
+	}
+}
+.up {
+	::after {
+		content: '';
+		width: 8px;
+		height: 8px;
+		position: absolute;
+		left: 2px;
+		top: 5px;
+		transform: rotate(45deg);
+		background-color: #ccc;
+	}
+	&.active {
+		::after {
+			background-color: $uni-primary;
+		}
+	}
+}
+</style>

+ 137 - 0
card/uni_modules/uni-table/components/uni-thead/uni-thead.vue

@@ -0,0 +1,137 @@
+<template>
+	<!-- #ifdef H5 -->
+	<thead class="uni-table-thead">
+		<tr class="uni-table-tr">
+			<th :rowspan="rowspan" colspan="1" class="checkbox" :class="{ 'tr-table--border': border }">
+				<table-checkbox :indeterminate="indeterminate" :checked="checked"
+					@checkboxSelected="checkboxSelected"></table-checkbox>
+			</th>
+		</tr>
+		<slot></slot>
+	</thead>
+	<!-- #endif -->
+	<!-- #ifndef H5 -->
+	<view class="uni-table-thead">
+		<slot></slot>
+	</view>
+	<!-- #endif -->
+</template>
+
+<script>
+	import tableCheckbox from '../uni-tr/table-checkbox.vue'
+	export default {
+		name: 'uniThead',
+		components: {
+			tableCheckbox
+		},
+		options: {
+			// #ifdef MP-TOUTIAO
+			virtualHost: false,
+			// #endif
+			// #ifndef MP-TOUTIAO
+			virtualHost: true
+			// #endif
+		},
+		data() {
+			return {
+				border: false,
+				selection: false,
+				rowspan: 1,
+				indeterminate: false,
+				checked: false
+			}
+		},
+		created() {
+			this.root = this.getTable()
+			// #ifdef H5
+			this.root.theadChildren = this
+			// #endif
+			this.border = this.root.border
+			this.selection = this.root.type
+		},
+		methods: {
+			init(self) {
+				this.rowspan++
+			},
+			checkboxSelected(e) {
+				this.indeterminate = false
+				const backIndexData = this.root.backIndexData
+				const data = this.root.trChildren.filter(v => !v.disabled && v.keyValue)
+				if (backIndexData.length === data.length) {
+					this.checked = false
+					this.root.clearSelection()
+				} else {
+					this.checked = true
+					this.root.selectionAll()
+				}
+			},
+			/**
+			 * 获取父元素实例
+			 */
+			getTable(name = 'uniTable') {
+				let parent = this.$parent
+				let parentName = parent.$options.name
+				while (parentName !== name) {
+					parent = parent.$parent
+					if (!parent) return false
+					parentName = parent.$options.name
+				}
+				return parent
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	$border-color: #ebeef5;
+
+	.uni-table-thead {
+		display: table-header-group;
+	}
+
+	.uni-table-tr {
+		/* #ifndef APP-NVUE */
+		display: table-row;
+		transition: all 0.3s;
+		box-sizing: border-box;
+		/* #endif */
+		border: 1px red solid;
+		background-color: #fafafa;
+	}
+
+	.checkbox {
+		padding: 0 8px;
+		width: 26px;
+		padding-left: 12px;
+		/* #ifndef APP-NVUE */
+		display: table-cell;
+		vertical-align: middle;
+		/* #endif */
+		color: #333;
+		font-weight: 500;
+		border-bottom: 1px $border-color solid;
+		font-size: 14px;
+		// text-align: center;
+	}
+
+	.tr-table--border {
+		border-right: 1px $border-color solid;
+	}
+
+	/* #ifndef APP-NVUE */
+	.uni-table-tr {
+		::v-deep .uni-table-th {
+			&.table--border:last-child {
+				// border-right: none;
+			}
+		}
+
+		::v-deep .uni-table-td {
+			&.table--border:last-child {
+				// border-right: none;
+			}
+		}
+	}
+
+	/* #endif */
+</style>

+ 179 - 0
card/uni_modules/uni-table/components/uni-tr/table-checkbox.vue

@@ -0,0 +1,179 @@
+<template>
+	<view class="uni-table-checkbox" @click="selected">
+		<view v-if="!indeterminate" class="checkbox__inner" :class="{'is-checked':isChecked,'is-disable':isDisabled}">
+			<view class="checkbox__inner-icon"></view>
+		</view>
+		<view v-else class="checkbox__inner checkbox--indeterminate">
+			<view class="checkbox__inner-icon"></view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'TableCheckbox',
+		emits:['checkboxSelected'],
+		props: {
+			indeterminate: {
+				type: Boolean,
+				default: false
+			},
+			checked: {
+				type: [Boolean,String],
+				default: false
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			index: {
+				type: Number,
+				default: -1
+			},
+			cellData: {
+				type: Object,
+				default () {
+					return {}
+				}
+			}
+		},
+		watch:{
+			checked(newVal){
+				if(typeof this.checked === 'boolean'){
+					this.isChecked = newVal
+				}else{
+					this.isChecked = true
+				}
+			},
+			indeterminate(newVal){
+				this.isIndeterminate = newVal
+			}
+		},
+		data() {
+			return {
+				isChecked: false,
+				isDisabled: false,
+				isIndeterminate:false
+			}
+		},
+		created() {
+			if(typeof this.checked === 'boolean'){
+				this.isChecked = this.checked
+			}
+			this.isDisabled = this.disabled
+		},
+		methods: {
+			selected() {
+				if (this.isDisabled) return
+				this.isIndeterminate = false
+				this.isChecked = !this.isChecked
+				this.$emit('checkboxSelected', {
+					checked: this.isChecked,
+					data: this.cellData
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	$uni-primary: #007aff !default;
+	$border-color: #DCDFE6;
+	$disable:0.4;
+
+	.uni-table-checkbox {
+		display: flex;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		position: relative;
+		margin: 5px 0;
+		cursor: pointer;
+
+		// 多选样式
+		.checkbox__inner {
+			/* #ifndef APP-NVUE */
+			flex-shrink: 0;
+			box-sizing: border-box;
+			/* #endif */
+			position: relative;
+			width: 16px;
+			height: 16px;
+			border: 1px solid $border-color;
+			border-radius: 2px;
+			background-color: #fff;
+			z-index: 1;
+
+			.checkbox__inner-icon {
+				position: absolute;
+				/* #ifdef APP-NVUE */
+				top: 2px;
+				/* #endif */
+				/* #ifndef APP-NVUE */
+				top: 2px;
+				/* #endif */
+				left: 5px;
+				height: 7px;
+				width: 3px;
+				border: 1px solid #fff;
+				border-left: 0;
+				border-top: 0;
+				opacity: 0;
+				transform-origin: center;
+				transform: rotate(45deg);
+				box-sizing: content-box;
+			}
+
+			&.checkbox--indeterminate {
+				border-color: $uni-primary;
+				background-color: $uni-primary;
+
+				.checkbox__inner-icon {
+					position: absolute;
+					opacity: 1;
+					transform: rotate(0deg);
+					height: 2px;
+					top: 0;
+					bottom: 0;
+					margin: auto;
+					left: 0px;
+					right: 0px;
+					bottom: 0;
+					width: auto;
+					border: none;
+					border-radius: 2px;
+					transform: scale(0.5);
+					background-color: #fff;
+				}
+			}
+			&:hover{
+				border-color: $uni-primary;
+			}
+			// 禁用
+			&.is-disable {
+				/* #ifdef H5 */
+				cursor: not-allowed;
+				/* #endif */
+				background-color: #F2F6FC;
+				border-color: $border-color;
+			}
+
+			// 选中
+			&.is-checked {
+				border-color: $uni-primary;
+				background-color: $uni-primary;
+
+				.checkbox__inner-icon {
+					opacity: 1;
+					transform: rotate(45deg);
+				}
+
+				// 选中禁用
+				&.is-disable {
+					opacity: $disable;
+				}
+			}
+			
+		}
+	}
+</style>

+ 184 - 0
card/uni_modules/uni-table/components/uni-tr/uni-tr.vue

@@ -0,0 +1,184 @@
+<template>
+	<!-- #ifdef H5 -->
+	<tr class="uni-table-tr">
+		<th v-if="selection === 'selection' && ishead" class="checkbox" :class="{ 'tr-table--border': border }">
+			<table-checkbox :checked="checked" :indeterminate="indeterminate" :disabled="disabled"
+				@checkboxSelected="checkboxSelected"></table-checkbox>
+		</th>
+		<slot></slot>
+		<!-- <uni-th class="th-fixed">123</uni-th> -->
+	</tr>
+	<!-- #endif -->
+	<!-- #ifndef H5 -->
+	<view class="uni-table-tr">
+		<view v-if="selection === 'selection' " class="checkbox" :class="{ 'tr-table--border': border }">
+			<table-checkbox :checked="checked" :indeterminate="indeterminate" :disabled="disabled"
+				@checkboxSelected="checkboxSelected"></table-checkbox>
+		</view>
+		<slot></slot>
+	</view>
+	<!-- #endif -->
+</template>
+
+<script>
+	import tableCheckbox from './table-checkbox.vue'
+	/**
+	 * Tr 表格行组件
+	 * @description 表格行组件 仅包含 th,td 组件
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=
+	 */
+	export default {
+		name: 'uniTr',
+		components: {
+			tableCheckbox
+		},
+		props: {
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			keyValue: {
+				type: [String, Number],
+				default: ''
+			}
+		},
+		options: {
+			// #ifdef MP-TOUTIAO
+			virtualHost: false,
+			// #endif
+			// #ifndef MP-TOUTIAO
+			virtualHost: true
+			// #endif
+		},
+		data() {
+			return {
+				value: false,
+				border: false,
+				selection: false,
+				widthThArr: [],
+				ishead: true,
+				checked: false,
+				indeterminate: false
+			}
+		},
+		created() {
+			this.root = this.getTable()
+			this.head = this.getTable('uniThead')
+			if (this.head) {
+				this.ishead = false
+				this.head.init(this)
+			}
+			this.border = this.root.border
+			this.selection = this.root.type
+			this.root.trChildren.push(this)
+			const rowData = this.root.data.find(v => v[this.root.rowKey] === this.keyValue)
+			if (rowData) {
+				this.rowData = rowData
+			}
+			this.root.isNodata()
+		},
+		mounted() {
+			if (this.widthThArr.length > 0) {
+				const selectionWidth = this.selection === 'selection' ? 50 : 0
+				this.root.minWidth = Number(this.widthThArr.reduce((a, b) => Number(a) + Number(b))) + selectionWidth;
+			}
+		},
+		// #ifndef VUE3
+		destroyed() {
+			const index = this.root.trChildren.findIndex(i => i === this)
+			this.root.trChildren.splice(index, 1)
+			this.root.isNodata()
+		},
+		// #endif
+		// #ifdef VUE3
+		unmounted() {
+			const index = this.root.trChildren.findIndex(i => i === this)
+			this.root.trChildren.splice(index, 1)
+			this.root.isNodata()
+		},
+		// #endif
+		methods: {
+			minWidthUpdate(width) {
+				this.widthThArr.push(width)
+				if (this.widthThArr.length > 0) {
+					const selectionWidth = this.selection === 'selection' ? 50 : 0;
+					this.root.minWidth = Number(this.widthThArr.reduce((a, b) => Number(a) + Number(b))) + selectionWidth;
+				}
+			},
+			// 选中
+			checkboxSelected(e) {
+				let rootData = this.root.data.find(v => v[this.root.rowKey] === this.keyValue)
+				this.checked = e.checked
+				this.root.check(rootData || this, e.checked, rootData ? this.keyValue : null)
+			},
+			change(e) {
+				this.root.trChildren.forEach(item => {
+					if (item === this) {
+						this.root.check(this, e.detail.value.length > 0 ? true : false)
+					}
+				})
+			},
+			/**
+			 * 获取父元素实例
+			 */
+			getTable(name = 'uniTable') {
+				let parent = this.$parent
+				let parentName = parent.$options.name
+				while (parentName !== name) {
+					parent = parent.$parent
+					if (!parent) return false
+					parentName = parent.$options.name
+				}
+				return parent
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	$border-color: #ebeef5;
+
+	.uni-table-tr {
+		/* #ifndef APP-NVUE */
+		display: table-row;
+		transition: all 0.3s;
+		box-sizing: border-box;
+		/* #endif */
+	}
+
+	.checkbox {
+		padding: 0 8px;
+		width: 26px;
+		padding-left: 12px;
+		/* #ifndef APP-NVUE */
+		display: table-cell;
+		vertical-align: middle;
+		/* #endif */
+		color: #333;
+		font-weight: 500;
+		border-bottom: 1px $border-color solid;
+		font-size: 14px;
+		// text-align: center;
+	}
+
+	.tr-table--border {
+		border-right: 1px $border-color solid;
+	}
+
+	/* #ifndef APP-NVUE */
+	.uni-table-tr {
+		::v-deep .uni-table-th {
+			&.table--border:last-child {
+				// border-right: none;
+			}
+		}
+
+		::v-deep .uni-table-td {
+			&.table--border:last-child {
+				// border-right: none;
+			}
+		}
+	}
+
+	/* #endif */
+</style>

+ 9 - 0
card/uni_modules/uni-table/i18n/en.json

@@ -0,0 +1,9 @@
+{
+	"filter-dropdown.reset": "Reset",
+	"filter-dropdown.search": "Search",
+	"filter-dropdown.submit": "Submit",
+	"filter-dropdown.filter": "Filter",
+	"filter-dropdown.gt": "Greater or equal to",
+	"filter-dropdown.lt": "Less than or equal to",
+	"filter-dropdown.date": "Date"
+}

+ 9 - 0
card/uni_modules/uni-table/i18n/es.json

@@ -0,0 +1,9 @@
+{
+	"filter-dropdown.reset": "Reiniciar",
+	"filter-dropdown.search": "Búsqueda",
+	"filter-dropdown.submit": "Entregar",
+	"filter-dropdown.filter": "Filtrar",
+	"filter-dropdown.gt": "Mayor o igual a",
+	"filter-dropdown.lt": "Menos que o igual a",
+	"filter-dropdown.date": "Fecha"
+}

+ 9 - 0
card/uni_modules/uni-table/i18n/fr.json

@@ -0,0 +1,9 @@
+{
+	"filter-dropdown.reset": "Réinitialiser",
+	"filter-dropdown.search": "Chercher",
+	"filter-dropdown.submit": "Soumettre",
+	"filter-dropdown.filter": "Filtre",
+	"filter-dropdown.gt": "Supérieur ou égal à",
+	"filter-dropdown.lt": "Inférieur ou égal à",
+	"filter-dropdown.date": "Date"
+}

+ 12 - 0
card/uni_modules/uni-table/i18n/index.js

@@ -0,0 +1,12 @@
+import en from './en.json'
+import es from './es.json'
+import fr from './fr.json'
+import zhHans from './zh-Hans.json'
+import zhHant from './zh-Hant.json'
+export default {
+	en,
+	es,
+	fr,
+	'zh-Hans': zhHans,
+	'zh-Hant': zhHant
+}

+ 9 - 0
card/uni_modules/uni-table/i18n/zh-Hans.json

@@ -0,0 +1,9 @@
+{
+	"filter-dropdown.reset": "重置",
+	"filter-dropdown.search": "搜索",
+	"filter-dropdown.submit": "确定",
+	"filter-dropdown.filter": "筛选",
+	"filter-dropdown.gt": "大于等于",
+	"filter-dropdown.lt": "小于等于",
+	"filter-dropdown.date": "日期范围"
+}

+ 9 - 0
card/uni_modules/uni-table/i18n/zh-Hant.json

@@ -0,0 +1,9 @@
+{
+	"filter-dropdown.reset": "重置",
+	"filter-dropdown.search": "搜索",
+	"filter-dropdown.submit": "確定",
+	"filter-dropdown.filter": "篩選",
+	"filter-dropdown.gt": "大於等於",
+	"filter-dropdown.lt": "小於等於",
+	"filter-dropdown.date": "日期範圍"
+}

+ 84 - 0
card/uni_modules/uni-table/package.json

@@ -0,0 +1,84 @@
+{
+  "id": "uni-table",
+  "displayName": "uni-table 表格",
+  "version": "1.2.8",
+  "description": "表格组件,多用于展示多条结构类似的数据,如",
+  "keywords": [
+    "uni-ui",
+    "uniui",
+    "table",
+    "表格"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": ""
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": ["uni-scss","uni-datetime-picker"],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "n"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "n"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "n",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "n",
+          "联盟": "n"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 13 - 0
card/uni_modules/uni-table/readme.md

@@ -0,0 +1,13 @@
+
+
+## Table 表单
+> 组件名:``uni-table``,代码块: `uTable`。
+
+用于展示多条结构类似的数据
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-table)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 
+
+
+
+