Browse Source

S3 Card Test

Rockz-Home 1 tháng trước cách đây
mục cha
commit
f7e7f9130c
39 tập tin đã thay đổi với 2413 bổ sung4083 xóa
  1. 178 0
      card/newCards/S3/index.html
  2. 58 32
      card/newCards/S3/js/api.js
  3. 0 0
      card/newCards/S3/js/bridge.js
  4. 136 0
      card/newCards/S3/js/utils.js
  5. 362 0
      card/newCards/S3/rankOverview.html
  6. 529 0
      card/newCards/S3/ranklist - Copy.html
  7. 531 0
      card/newCards/S3/ranklist.html
  8. 379 0
      card/newCards/S3/signup.html
  9. BIN
      card/newCards/S3/static/backgroud/top_bg_egg2.png
  10. BIN
      card/newCards/S3/static/backgroud/top_bg_sddx.png
  11. BIN
      card/newCards/S3/static/common/notice.png
  12. BIN
      card/newCards/S3/static/logo/jbs.png
  13. BIN
      card/newCards/S3/static/logo/sddx.png
  14. 36 1
      card/newCards/nanning/api.js
  15. 204 169
      card/newCards/nanning/ranklist.html
  16. 0 29
      card/pages/tpl/style3/new/AGENTS.md
  17. 0 148
      card/pages/tpl/style3/new/API.md
  18. 0 556
      card/pages/tpl/style3/new/API_SERVER.md
  19. 0 378
      card/pages/tpl/style3/new/HTML_MIGRATION_STRATEGY.md
  20. 0 93
      card/pages/tpl/style3/new/PROJECT_INSIGHTS.md
  21. BIN
      card/pages/tpl/style3/new/bd.png
  22. 0 5
      card/pages/tpl/style3/new/css/all.min.css
  23. BIN
      card/pages/tpl/style3/new/gd.png
  24. 0 77
      card/pages/tpl/style3/new/index.html
  25. 0 68
      card/pages/tpl/style3/new/js/api.js
  26. 0 401
      card/pages/tpl/style3/new/js/cardfunc.js
  27. 0 75
      card/pages/tpl/style3/new/js/define.js
  28. 0 207
      card/pages/tpl/style3/new/js/logic-index.js
  29. 0 463
      card/pages/tpl/style3/new/js/logic-ranklist.js
  30. 0 264
      card/pages/tpl/style3/new/js/logic-signup.js
  31. 0 0
      card/pages/tpl/style3/new/js/multiavatar.min.js
  32. 0 205
      card/pages/tpl/style3/new/js/tools.js
  33. 0 187
      card/pages/tpl/style3/new/js/uni-compat.js
  34. 0 146
      card/pages/tpl/style3/new/mock_flutter.js
  35. 0 275
      card/pages/tpl/style3/new/ranklist.html
  36. 0 304
      card/pages/tpl/style3/new/signup.html
  37. BIN
      card/pages/tpl/style3/new/webfonts/fa-brands-400.woff2
  38. BIN
      card/pages/tpl/style3/new/webfonts/fa-regular-400.woff2
  39. BIN
      card/pages/tpl/style3/new/webfonts/fa-solid-900.woff2

+ 178 - 0
card/newCards/S3/index.html

@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>S3 活动首页</title>
+	<script src="https://cdn.tailwindcss.com"></script>
+	<style>
+		@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
+		body { font-family: 'Roboto', sans-serif; }
+		.content-bg {
+			background: linear-gradient(180deg, #7aedff 0%, #047200 100%);
+			min-height: 100vh;
+		}
+		.logo-bg {
+			background-image: url('./static/logo/jbs.png');
+			background-repeat: no-repeat;
+			background-position: center;
+			background-size: contain;
+		}
+	</style>
+</head>
+<body class="bg-gray-100">
+
+	<div class="content-bg w-full flex flex-col relative">
+		<!-- Top Bar -->
+		<div class="w-full flex justify-end p-4 pt-8">
+			<div class="flex items-center bg-black/30 rounded-xl px-3 py-1 gap-2 min-w-[90px] h-[40px]">
+				<img src="https://img.icons8.com/ios-filled/50/ffffff/clock.png" class="w-5 h-5 object-contain">
+				<div id="countdown" class="text-white text-lg font-bold text-center min-w-[60px]">--:--</div>
+			</div>
+		</div>
+
+		<!-- Main Content -->
+		<div class="flex-1 flex flex-col items-center justify-center gap-6 -mt-20">
+			<!-- Logo -->
+			<div class="w-[50vw] h-[50vw] logo-bg"></div>
+
+			<!-- Type & Notice -->
+			<div class="relative flex items-center justify-center w-full">
+				<img id="notice-icon" src="./static/common/notice.png" class="absolute left-10 w-4 h-4 hidden" alt="notice">
+				<span id="activity-type" class="text-white/60 text-xl font-bold">锦标赛</span>
+			</div>
+
+			<!-- Name -->
+			<div id="activity-name" class="text-white text-2xl font-bold text-center px-4">正在加载...</div>
+
+			<!-- Button -->
+			<button id="action-btn" onclick="handleBtnClick()" class="bg-white text-black text-xl font-bold py-3 px-12 rounded-full shadow-lg active:scale-95 transition-transform mt-4">
+				开始比赛
+			</button>
+		</div>
+	</div>
+
+	<!-- Scripts -->
+	<script src="./js/utils.js"></script>
+	<script src="./js/bridge.js"></script>
+	<script src="./js/api.js"></script>
+	<script>
+		const state = {
+			ecId: 0,
+			isJoin: false,
+			isFinished: false,
+			secondCardName: '',
+			beginSecond: 0,
+			endSecond: 0,
+			timer: null,
+			rankKey: ''
+		};
+
+		window.onload = function() {
+			const token = Tools.getQueryParam('token');
+			state.ecId = Tools.getQueryParam('id') || 0;
+			state.ecId = 112;
+			state.rankKey = `rank-tpl-style3-${state.ecId}`;
+
+			if (window.API) {
+				API.setToken(token);
+				if (Tools.getQueryParam('env') === 'mock') {
+					API.init({ useMock: true });
+					if (!state.ecId) state.ecId = 'mock_id';
+				}
+			}
+
+			const typeParam = Tools.getQueryParam('type');
+			if (typeParam) document.getElementById('activity-type').innerText = decodeURIComponent(typeParam);
+
+			const btnTextParam = Tools.getQueryParam('btnText');
+			if (btnTextParam) document.getElementById('action-btn').innerText = decodeURIComponent(btnTextParam);
+
+			loadCardBase();
+			checkUserJoin();
+			loadMatchDetail();
+		};
+
+		window.onunload = function() {
+			if (state.timer) {
+				clearInterval(state.timer);
+			}
+		};
+
+		function loadCardBase() {
+			if (!window.API) return;
+			API.getCardBase(state.ecId).then(res => {
+				if (res) {
+					document.getElementById('activity-name').innerText = res.ecName || '未命名活动';
+					state.beginSecond = res.beginSecond;
+					state.endSecond = res.endSecond;
+					state.secondCardName = res.secondCardName;
+					startCountdown();
+				}
+			});
+		}
+
+		function checkUserJoin() {
+			if (!window.API) return;
+			API.getUserJoinStatus(state.ecId).then(res => {
+				if (res && res.isJoin) {
+					state.isJoin = true;
+				}
+			});
+		}
+
+		function loadMatchDetail() {
+			if (!window.API) return;
+			API.getMatchRsDetail(state.ecId,0).then(res => {
+				if (!res) return;
+				const rankStr = JSON.stringify(res);
+				const cache = localStorage.getItem(state.rankKey);
+				if (cache !== rankStr) {
+					document.getElementById('notice-icon').classList.remove('hidden');
+					localStorage.setItem(state.rankKey, rankStr);
+				}
+			});
+		}
+
+		function startCountdown() {
+			const countdownEl = document.getElementById('countdown');
+			const update = () => {
+				if (state.endSecond > 0) {
+					const diff = state.endSecond - Date.now() / 1000;
+					if (diff > 0) {
+						countdownEl.innerText = '距结束' + Tools.convertSecondsToDHM(diff);
+					} else {
+						countdownEl.innerText = '已结束';
+						state.isFinished = true;
+						clearInterval(state.timer);
+					}
+				}
+			};
+			update();
+			state.timer = setInterval(update, 60000);
+		}
+
+		function handleBtnClick() {
+			//const queryStr = window.location.search;
+			const queryStr='?token='+ Tools.getQueryParam('token') +'&id=112';
+			if (state.isJoin) {
+				const target = `./ranklist.html${queryStr}&full=true`;
+				Bridge.appAction(target);
+			} else {
+				if (!state.isFinished) {
+					if (state.secondCardName === 'rankList') {
+						const target = `./ranklist.html${queryStr}&full=true`;
+						Bridge.appAction(target);
+					} else {
+						const target = `./signup.html${queryStr}&full=true`;
+						Bridge.appAction(target);
+					}
+				} else {
+					const target = `./ranklist.html${queryStr}&full=true`;
+					Bridge.appAction(target);
+				}
+			}
+		}
+	</script>
+</body>
+</html>

+ 58 - 32
card/pages/tpl/style3/new/api.js → card/newCards/S3/js/api.js

@@ -1,3 +1,9 @@
+/**
+ * 请任何自动化工具每次更新本文档,要再下面的版本号自动加1,并更新日期
+ * 
+ * 版本号:1
+ * 日期:2025-12-4 16:14:00
+*/
 (function (window) {
     'use strict';
 
@@ -60,6 +66,7 @@
         'CardDetailQuery': {
             mcId: 101,
             mcName: '[Mock]线上马拉松',
+            nickName: '[Mock]用户昵称', // Added as per user request
             mcType: 1,
             beginSecond: Date.now() / 1000 - 86400 * 2,
             endSecond: Date.now() / 1000 + 86400 * 5,
@@ -68,21 +75,40 @@
             coiName: '个人组',
             ocaId: 201 // Mocked ocaId
         },
-        'MatchRsDetailQuery': [
-            { id: 1, name: '[Mock]活动1', status: 1 },
-            { id: 2, name: '[Mock]活动2', status: 0 }
-        ],
+        'MatchRsDetailQuery': { 
+            id: 1, 
+            name: '[Mock]活动1', 
+            status: 1, 
+            totalSysPoint: 100,
+            mcType: 1,
+            mcId: 101,
+            mcName: '[Mock]线上马拉松',
+            beginSecond: Date.now() / 1000 - 86400 * 2,
+            endSecond: Date.now() / 1000 + 86400 * 5,
+            nickName: '[Mock]用户昵称',
+            totalNum: 10,
+            totalDistanct: 5000,
+            totalDistanctRankNum: 5,
+            totalCp: 20,
+            totalCpRankNum: 3,
+            totalSysPointRankNum: 10,
+            fastPace: 300,
+            fastPaceRankNum: 8
+        },
         'CardRankDetailQuery': {
             totalRankRs: [
-                { nickName: 'Mock张三', score: 10000, headUrl: 'https://picsum.photos/40/40?random=1', rankNum: 1 },
-                { nickName: 'Mock李四', score: 9500, headUrl: 'https://picsum.photos/40/40?random=2', rankNum: 2 },
-                { nickName: 'Mock王五', score: 8800, headUrl: 'https://picsum.photos/40/40?random=3', rankNum: 3 },
-                { nickName: 'Mock赵六', score: 7200, headUrl: 'https://picsum.photos/40/40?random=4', rankNum: 4 },
-                { nickName: 'Mock小明', score: 6500, headUrl: 'https://picsum.photos/40/40?random=5', rankNum: 5 }
+                { userName: 'Mock张三', score: 10000, headUrl: 'https://picsum.photos/40/40?random=1', rankNum: 1, isSelf: 0, inRankNum: 1, isInGame: 0, additionalName: '计算机学院', isDispAdditionalName: 1, inGameUserNum: 0, isDispInGameUserNum: 0, isTodayFinishGame: 0 },
+                { userName: 'Mock李四', score: 9500, headUrl: 'https://picsum.photos/40/40?random=2', rankNum: 2, isSelf: 0, inRankNum: 2, isInGame: 0, additionalName: '土木建筑', isDispAdditionalName: 1, inGameUserNum: 0, isDispInGameUserNum: 0, isTodayFinishGame: 0 },
+                { userName: 'Mock王五', score: 8800, headUrl: 'https://picsum.photos/40/40?random=3', rankNum: 3, isSelf: 0, inRankNum: 3, isInGame: 0, additionalName: '艺术设计', isDispAdditionalName: 1, inGameUserNum: 0, isDispInGameUserNum: 0, isTodayFinishGame: 0 },
+                { userName: 'Mock赵六', score: 7200, headUrl: 'https://picsum.photos/40/40?random=4', rankNum: 4, isSelf: 0, inRankNum: 4, isInGame: 0, additionalName: '交通运输', isDispAdditionalName: 1, inGameUserNum: 0, isDispInGameUserNum: 0, isTodayFinishGame: 0 },
+                { userName: 'Mock小明', score: 6500, headUrl: 'https://picsum.photos/40/40?random=5', rankNum: 5, isSelf: 1, inRankNum: 5, isInGame: 0, additionalName: '信息工程', isDispAdditionalName: 1, inGameUserNum: 0, isDispInGameUserNum: 0, isTodayFinishGame: 0 }
             ],
             teamRankRs: [],
             inTeamRs: [],
-            otherRs: []
+            otherRs: [],
+            regionPaceRs: null,
+            regionTodayPaceRs: null,
+            regionSpeedRs: null
         },
         'UserCurrentRankNumQuery': { rankNum: 5 },
         'UserJoinCardQuery': { isJoin: false }, // 默认未报名
@@ -159,7 +185,7 @@
         'MapListQuery': [
             { mapId: 1, mapName: '[Mock]公园地图', latitude: 36.6, longitude: 117.0, activityList: [] }
         ],
-        'CompStatisticQuery': { totalDistance: 123.45, totalPeople: 1000 },
+        'CompStatisticQuery': { totalDistance: 5024, totalPeople: 1000, totalAnswerNum: 1500, totalCp: 8900 },
         'WarnMessageQuery': [
             { warnType: 1, warnTitle: '[Mock]黄牌警告', warnMessage: '您的成绩异常', iconUrl: 'https://picsum.photos/50/50?random=10' }
         ],
@@ -192,9 +218,9 @@
                 { orderNum: 2, isComplete: 0, showName: '未完2', relationType: 1, ocaId: 11, longitude: 117.2, latitude: 36.7, popupImg: 'https://picsum.photos/100/100?random=20' }
             ]
         },
-        'CardUriQuery': {}, // 暂无 Mock 结构
-        'MatchFinishInfoQuery': {}, // 暂无 Mock 结构
-        'RedisRebuild': {} // 暂无 Mock 结构
+        'CardUrlQuery': { url: 'https://mock-uri.colormaprun.com/card/nanning1/index.html'}, // 暂无 Mock 结构  
+        'MatchFininshInfoQuery': {}, // 暂无 Mock 结构
+        'RedisRebulid': {} // 暂无 Mock 结构
     };
 
     var API = {
@@ -263,16 +289,16 @@
         // ==============================
 
         // 1. 卡片基本信息查询
-        getCardBase: function(ecId, pageName) { return this.request('CardBaseQuery', { ecId: ecId, pageName: pageName }); },
+        getCardBase: function(ecId) { return this.request('CardBaseQuery', { ecId: ecId }); },
 
         // 2. 卡片对应活动或赛事详情查询
         getCardDetail: function(ecId) { return this.request('CardDetailQuery', { ecId: ecId }); },
 
         // 3. 卡片对应线上赛多个活动查询
-        getMatchRsDetail: function(ecId) { return this.request('MatchRsDetailQuery', { ecId: ecId }); },
+        getMatchRsDetail: function(ecId, ocaId) { return this.request('MatchRsDetailQuery', { ecId: ecId, ocaId: ocaId }); },
 
         // 4. 排名查询
-        getRankDetail: function(mcIdListStr, mcType, dispArrStr) { return this.request('CardRankDetailQuery', { mcIdListStr: mcIdListStr, mcType: mcType, dispArrStr: dispArrStr }); },
+        getRankDetail: function(mcIdListStr, mcType, ocaId, dispArrStr) { return this.request('CardRankDetailQuery', { mcIdListStr: mcIdListStr, mcType: mcType, ocaId: ocaId, dispArrStr: dispArrStr }); },
 
         // 5. 卡片用户当前排名查询
         getUserCurrentRank: function(ecId) { return this.request('UserCurrentRankNumQuery', { ecId: ecId }); },
@@ -284,7 +310,7 @@
         isNewUserInCardComp: function(ecId) { return this.request('IsNewUserInCardComp', { ecId: ecId }); },
 
         // 8. 线上赛报名页面信息详情
-        getOnlineMcSignUpDetail: function(ecId) { return this.request('OnlineMcSignUpDetail', { ecId: ecId }); },
+        getOnlineMcSignUpDetail: function(ecId, mcId) { return this.request('OnlineMcSignUpDetail', { ecId: ecId, mcId: mcId }); },
 
         // 9. 线上赛报名(重新分组)
         signUpOnline: function(mcId, coiId, selectTeam, nickName) { 
@@ -295,7 +321,7 @@
         isAllowMcSignUp: function(ecId) { return this.request('IsAllowMcSignUp', { ecId: ecId }); },
 
         // 11. 玩家当前月挑战记录查询
-        getCurrentMonthlyChallenge: function() { return this.request('CurrentMonthlyChallengeQuery', {}); },
+        getCurrentMonthlyChallenge: function(year, month) { return this.request('CurrentMonthlyChallengeQuery', {year: year, month: month}); },
 
         // 12. 卡片配置信息查询
         getCardConfig: function(ecId, pageName) { return this.request('CardConfigQuery', { ecId: ecId, pageName: pageName }); },
@@ -307,16 +333,16 @@
         getMonthlyChallenge: function() { return this.request('MonthlyChallengeQuery', {}); },
 
         // 15. 月挑战排名查询
-        getMonthRankDetail: function() { return this.request('MonthRankDetailQuery', {}); },
+        getMonthRankDetail: function(year, month, dispArrStr) { return this.request('MonthRankDetailQuery', {year: year, month: month, dispArrStr: dispArrStr}); },
 
         // 16. 玩家活动成就查询
         getAchievement: function() { return this.request('AchievementQuery', {}); },
 
         // 17. 玩家兑换记录查询
-        getExchangeList: function(ecId) { return this.request('ExchangeListQuery', { ecId: ecId }); },
+        getExchangeList: function() { return this.request('ExchangeListQuery', {}); },
 
         // 18. 玩家兑换详情查询
-        getExchangeDetail: function(ecId, exchangeId) { return this.request('ExchangeDetailQuery', { ecId: ecId, exchangeId: exchangeId }); },
+        getExchangeDetail: function(oarId) { return this.request('ExchangeDetailQuery', { oarId: oarId}); },
 
         // 19. 未读消息列表查询
         getUnReadMessages: function(relationId, relationType) { return this.request('UnReadMessageQuery', { relationId: relationId, relationType: relationType || 2 }); },
@@ -325,22 +351,22 @@
         readMessage: function(mqIdListStr) { return this.request('ReadMessage', { mqIdListStr: mqIdListStr }); },
 
         // 21. 卡片对应地图列表详情查询
-        getMapList: function(ecId) { return this.request('MapListQuery', { ecId: ecId }); },
+        getMapList: function(mcId) { return this.request('MapListQuery', { mcId: mcId }); },
 
         // 22. 赛事总成绩统计查询
-        getCompStatistic: function(ecId) { return this.request('CompStatisticQuery', { ecId: ecId }); },
+        getCompStatistic: function(mcId) { return this.request('CompStatisticQuery', { mcId: mcId }); },
 
         // 23. 警告列表查询
         getWarnMessage: function(ecId) { return this.request('WarnMessageQuery', { ecId: ecId }); },
 
         // 24. 查询电子证书样式
-        getCertStyle: function(ecId) { return this.request('CertStyleQuery', { ecId: ecId }); },
+        getCertStyle: function(certStyleType) { return this.request('CertStyleQuery', { certStyleType: certStyleType }); },
 
         // 25. 查询电子证书成就对应用户基本信息
-        getUserBaseInCertificate: function(ecId) { return this.request('UserBaseQueryInCertificate', { ecId: ecId }); },
+        getUserBaseInCertificate: function(oarId) { return this.request('UserBaseQueryInCertificate', { oarId: oarId }); },
 
         // 26. 根据成就信息确认生成电子证书
-        createCertificate: function(data) { return this.request('CertificateCreateByUserAi', data); },
+        createCertificate: function(nickName,oarId) { return this.request('CertificateCreateByUserAi', {nickName: nickName, oarId: oarId}); },
 
         // 27. 卡片内可用积分查询
         getScore: function(ecId) { return this.request('OnlineScoreQuery', { ecId: ecId }); },
@@ -349,10 +375,10 @@
         getGoodsList: function(ecId) { return this.request('CanExchangeGoodsList', { ecId: ecId }); },
 
         // 29. 积分可兑换商品详情
-        getGoodsDetail: function(goodsId) { return this.request('CanExchangeGoodsDetail', { goodsId: goodsId }); },
+        getGoodsDetail: function(ecId, goodsId) { return this.request('CanExchangeGoodsDetail', {ecid: ecId, goodsId: goodsId }); },
 
         // 30. 积分兑换商品
-        exchangeGoods: function(ecId, goodsId) { return this.request('ScoreExchangeGoods', { ecId: ecId, goodsId: goodsId }); },
+        exchangeGoods: function(ecId, goodsId, exchNum) { return this.request('ScoreExchangeGoods', { ecId: ecId, goodsId: goodsId, exchNum: exchNum }); },
 
         // 31. 用户基本信息查询
         getUserInfo: function() { return this.request('UserBasicInformationQuery', {}); },
@@ -361,13 +387,13 @@
         getGrids: function(ecId) { return this.request('GridsQuery', { ecId: ecId }); },
 
         // 33. 卡片URI查询
-        getCardUri: function(ecId) { return this.request('CardUriQuery', { ecId: ecId }); },
+        getCardUrl: function(actId, matchType) { return this.request('CardUrlQuery', { actId: actId, matchType: matchType }); },
 
         // 34. 赛事完赛信息查询
-        getMatchFinishInfo: function(ecId) { return this.request('MatchFinishInfoQuery', { ecId: ecId }); },
+        getMatchFinishInfo: function(actId, matchType) { return this.request('MatchFininshInfoQuery', { actId, matchType }); },
 
         // 35. Redis 重建 (管理接口)
-        redisRebuild: function(ecId) { return this.request('RedisRebuild', { ecId: ecId }); }
+        redisRebuild: function(compId) { return this.request('RedisRebulid', { compId: compId }); }
     };
 
     window.API = API;

+ 0 - 0
card/pages/tpl/style3/new/bridge.js → card/newCards/S3/js/bridge.js


+ 136 - 0
card/newCards/S3/js/utils.js

@@ -0,0 +1,136 @@
+/**
+ * 通用工具库(原生 JS 版),替代 common/tools.js 的常用能力
+ */
+const Tools = {
+	getQueryParam(name) {
+		const params = new URLSearchParams(window.location.search);
+		let val = params.get(name);
+		if (!val && window.location.hash) {
+			const hash = window.location.hash;
+			const idx = hash.indexOf('?');
+			if (idx >= 0) {
+				const hashParams = new URLSearchParams(hash.slice(idx + 1));
+				val = hashParams.get(name);
+			}
+		}
+		return val;
+	},
+
+	objectToQueryString(obj) {
+		return Object.keys(obj)
+			.filter(k => obj[k] !== undefined && obj[k] !== null)
+			.map(k => k + '=' + encodeURIComponent(obj[k]))
+			.join('&');
+	},
+
+	appAction(url, actType = '') {
+		if (window.Bridge && window.Bridge.appAction) {
+			window.Bridge.appAction(url, actType);
+			return;
+		}
+		if (url.indexOf('http') !== -1 || url.startsWith('action://')) {
+			window.location.href = url;
+		} else if (url === 'reload') {
+			window.location.reload();
+		} else {
+			window.location.href = url;
+		}
+	},
+
+	fmtMcTime(timestamp) {
+		if (!timestamp) return '--';
+		const date = new Date(timestamp * 1000);
+		const M = (date.getMonth() + 1).toString().padStart(2, '0');
+		const D = date.getDate().toString().padStart(2, '0');
+		const h = date.getHours().toString().padStart(2, '0');
+		const m = date.getMinutes().toString().padStart(2, '0');
+		return `${M}-${D} ${h}:${m}`;
+	},
+
+	getActtime(beginSecond, endSecond) {
+		if (!beginSecond || !endSecond) return '-- 至 --';
+		return `${this.fmtMcTime(beginSecond)} 至 ${this.fmtMcTime(endSecond)}`;
+	},
+
+	fmtMcTime2(timestamp1, timestamp2) {
+		if (!timestamp1 || !timestamp2) return '';
+		const date1 = new Date(timestamp1 * 1000);
+		const date2 = new Date(timestamp2 * 1000);
+		const parts1 = [date1.getFullYear(), date1.getMonth() + 1, date1.getDate()];
+		const parts2 = [date2.getFullYear(), date2.getMonth() + 1, date2.getDate()];
+
+		let suffix = '';
+		if (parts2[0] !== parts1[0]) suffix = `${parts2[0]}.${parts2[1]}.${parts2[2]}`;
+		else if (parts2[1] !== parts1[1]) suffix = `${parts2[1]}.${parts2[2]}`;
+		else if (parts2[2] !== parts1[2]) suffix = `${parts2[2]}`;
+
+		return suffix ? `${parts1[0]}.${parts1[1]}.${parts1[2]}-${suffix}` : `${parts1[0]}.${parts1[1]}.${parts1[2]}`;
+	},
+
+	timestampToTime(timestamp, type = 1) {
+		if (!timestamp) return '--';
+		const date = new Date(timestamp * 1000);
+		const Y = date.getFullYear();
+		const M = (date.getMonth() + 1).toString().padStart(2, '0');
+		const D = date.getDate().toString().padStart(2, '0');
+		const h = date.getHours().toString().padStart(2, '0');
+		const m = date.getMinutes().toString().padStart(2, '0');
+		const s = date.getSeconds().toString().padStart(2, '0');
+		return type === 2 ? `${Y}.${M}.${D}` : `${Y}-${M}-${D} ${h}:${m}:${s}`;
+	},
+
+	convertSecondsToDHM(seconds) {
+		if (seconds <= 0) return '00小时00分钟';
+		const days = Math.floor(seconds / (3600 * 24));
+		const hours = Math.floor((seconds % (3600 * 24)) / 3600);
+		const minutes = Math.floor((seconds % (3600 * 24)) % 3600 / 60);
+		return days > 0 ? `${days}天${hours}小时` : `${hours}小时${minutes}分钟`;
+	},
+
+	convertSecondsToHMS(seconds, type = 1) {
+		if (seconds === undefined || seconds === null || Number.isNaN(seconds)) return '--';
+		const s = Math.max(0, Math.floor(seconds));
+		const h = Math.floor(s / 3600);
+		const m = Math.floor((s % 3600) / 60);
+		const sec = s % 60;
+		if (type === 2) return `${m.toString().padStart(2, '0')}'${sec.toString().padStart(2, '0')}"`;
+		return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
+	},
+
+	fmtDistance(val) {
+		if (!val || val < 0) return '0';
+		if (val < 10000) return Math.round(val * 100 / 1000) / 100;
+		return Math.round(val / 1000);
+	},
+
+	checkMcState(beginSecond, endSecond) {
+		let mcState = 0;
+		if (beginSecond > 0 && endSecond > 0) {
+			const now = Date.now() / 1000;
+			const dif1 = beginSecond - now;
+			const dif2 = endSecond - now;
+			if (dif1 > 0) mcState = 0;
+			else if (dif2 > 0) mcState = 1;
+			else mcState = 2;
+		}
+		return mcState;
+	},
+
+	showToast(title, icon = 'none', duration = 2000) {
+		if (window.Bridge && window.Bridge.showToast) {
+			window.Bridge.showToast(title, icon, duration);
+			return;
+		}
+		const div = document.createElement('div');
+		div.style.cssText = `
+            position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
+            background: rgba(0,0,0,0.7); color: white; padding: 10px 20px;
+            border-radius: 5px; font-size: 14px; z-index: 9999; text-align: center;
+        `;
+		div.innerText = title;
+		document.body.appendChild(div);
+		setTimeout(() => document.body.removeChild(div), duration);
+	}
+};
+
+window.Tools = Tools;

+ 362 - 0
card/newCards/S3/rankOverview.html

@@ -0,0 +1,362 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>S3 赛事总览</title>
+	<script src="https://cdn.tailwindcss.com"></script>
+	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
+	<style>
+		@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;900&display=swap');
+		body { font-family: 'Noto Sans SC', sans-serif; }
+		.page-top {
+			background-image: url('./static/backgroud/top_bg_egg2.png');
+			background-repeat: no-repeat;
+			background-position: center;
+			background-size: cover;
+			min-height: 270px;
+		}
+		.logo-bg {
+			background-image: url('./static/logo/sddx.png');
+			background-repeat: no-repeat;
+			background-position: center;
+			background-size: contain;
+			width: 80px; height: 80px; margin-top: 10px;
+		}
+		.mid-card {
+			width: 90%;
+			background: #ffffff;
+			border-radius: 9px;
+			box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.13);
+			position: relative;
+			z-index: 20;
+			margin-left: auto;
+			margin-right: auto;
+		}
+		.mid-line { width: 1px; height: 40px; background-color: #e6e6e6; }
+		.path-item { display: flex; align-items: center; justify-content: space-between; background: white; padding: 15px; margin-bottom: 10px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); cursor: pointer; border: 2px solid transparent; }
+		.path-item.selected { border-color: #81cd00; background-color: #f9fff0; }
+	</style>
+</head>
+<body class="bg-gray-50 min-h-screen flex flex-col relative pb-20">
+
+	<!-- Top Section -->
+	<div class="page-top w-full flex flex-col items-center pt-8 pb-12">
+		<!-- Navbar -->
+		<div class="w-full flex justify-between items-center px-4">
+			<button onclick="handleBack()" class="bg-black/20 backdrop-blur-sm p-2 rounded-full w-9 h-9 flex items-center justify-center text-white active:scale-90 transition">
+				<i class="fas fa-chevron-left"></i>
+			</button>
+			<h1 id="mc-name" class="text-gray-800 text-lg font-bold">赛事名称</h1>
+			<button onclick="handleInfo()" class="bg-black/20 backdrop-blur-sm px-3 py-1.5 rounded-full text-xs font-semibold flex items-center gap-1 text-white active:scale-90 transition">
+				<i class="fas fa-question-circle"></i> 说明
+			</button>
+		</div>
+
+		<div class="flex flex-col items-center gap-2 mt-4">
+			<div class="logo-bg"></div>
+			<p id="sub-title" class="text-yellow-400 text-xl font-bold drop-shadow-md">活动时间</p>
+		</div>
+	</div>
+
+	<!-- Mid Section -->
+	<div id="mid-type-0" class="mid-card -mt-10 flex flex-col p-4">
+		<div class="flex justify-center items-center mb-3 relative">
+			<select id="map-select-0" onchange="handleMapChange(this.value)" class="bg-transparent text-gray-500 font-medium text-sm outline-none cursor-pointer">
+			</select>
+			<button onclick="handleHelp()" class="absolute right-0 text-red-800 text-xs font-medium">帮助</button>
+		</div>
+		<div class="flex justify-around items-center mb-4 text-xs font-medium text-red-900">
+			<div class="flex items-center gap-1"><span id="nick-name-0">昵称</span></div>
+			<span id="coi-name-0">组织</span>
+			<span id="regroup-btn-0" class="text-gray-400 cursor-pointer hidden" onclick="handleRegroup()">修改</span>
+		</div>
+		<div class="flex justify-around items-center text-center">
+			<div><div class="text-xl font-black" id="stat-num-0">--</div><div class="text-gray-400 text-xs">场次</div></div>
+			<div class="mid-line"></div>
+			<div><div class="text-xl font-black" id="stat-cp-0">--</div><div class="text-gray-400 text-xs">打点数</div></div>
+			<div class="mid-line"></div>
+			<div><div class="text-xl font-black" id="stat-rank-0">--</div><div class="text-gray-400 text-xs">个人排名</div></div>
+		</div>
+	</div>
+
+	<div id="mid-type-1" class="mid-card -mt-16 flex flex-col p-4 hidden">
+		<div class="flex justify-center items-center mb-3 relative">
+			<select id="map-select-1" onchange="handleMapChange(this.value)" class="bg-transparent text-gray-500 font-medium text-sm outline-none cursor-pointer"></select>
+			<div class="absolute right-0 flex gap-3 text-xs font-medium text-red-800">
+				<button id="regroup-btn-1" class="hidden" onclick="handleRegroup()">修改</button>
+				<button onclick="handleHelp()">帮助</button>
+			</div>
+		</div>
+		<div class="flex justify-between items-center mb-4 text-sm font-medium text-gray-500 px-2">
+			<div class="flex items-center gap-1"><span id="nick-name-1" class="text-red-900">昵称</span></div>
+			<span id="coi-name-1" class="text-red-900 truncate max-w-[100px]">组织</span>
+			<span>场次:<span id="stat-num-1">--</span></span>
+		</div>
+		<div class="flex justify-around items-center text-center">
+			<div><div class="text-xl font-black text-pink-600" id="stat-point-1">--</div><div class="text-gray-400 text-xs">百味豆</div></div>
+			<div class="mid-line"></div>
+			<div><div class="text-xl font-black" id="stat-dist-1">--</div><div class="text-gray-400 text-xs">里程 km</div></div>
+			<div class="mid-line"></div>
+			<div><div class="text-xl font-black" id="stat-cp-1">--</div><div class="text-gray-400 text-xs">打点数</div></div>
+			<div class="mid-line"></div>
+			<div><div class="text-xl font-black" id="stat-pace-1">--</div><div class="text-gray-400 text-xs">最快配速</div></div>
+		</div>
+	</div>
+
+	<!-- Main Content: Path List -->
+	<div class="w-full px-4 mt-6">
+		<h3 class="font-bold text-gray-800 mb-3">选择比赛路线</h3>
+		<div id="path-list-container" class="flex flex-col gap-3"></div>
+	</div>
+
+	<!-- Bottom Action Bar -->
+	<div class="fixed bottom-0 w-full bg-white border-t border-gray-100 p-4 z-40">
+		<button onclick="handleStartGame()" class="w-full bg-[#81cd00] text-white font-bold py-3 rounded-full shadow-lg active:scale-95 transition-transform">
+			开始比赛
+		</button>
+	</div>
+
+	<!-- Info Modal -->
+	<div id="infoModal" class="fixed inset-0 z-50 hidden transition-opacity duration-300">
+		<div class="absolute inset-0 bg-slate-900/70 backdrop-blur-sm" onclick="closeModal('infoModal')"></div>
+		<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[90%] bg-white rounded-2xl p-6 shadow-2xl max-h-[80vh] overflow-y-auto">
+			<button onclick="closeModal('infoModal')" class="absolute top-2 right-2 text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
+			<div id="info-modal-content" class="text-sm"></div>
+		</div>
+	</div>
+
+	<script src="./js/utils.js"></script>
+	<script src="./js/bridge.js"></script>
+	<script src="./js/api.js"></script>
+	<script>
+		const STATE = {
+			ecId: 0,
+			token: '',
+			mcId: 0,
+			mcType: 0,
+			mcName: '',
+			coiName: '',
+			beginSecond: 0,
+			endSecond: 0,
+			ocaId: 0,
+			nickName: '',
+			mapList: [],
+			pathList: [],
+			configParam: { subTitle: '', midType: 0 },
+			mapKey: 'rank-tpl-style3-map',
+			mcState: 0,
+			allowMcSignUp: false,
+			stats: {
+				regionTotalNum: 0,
+				regionTotalCp: 0,
+				regionTotalCpRankNum: 0,
+				regionTotalSysPoint: 0,
+				regionTotalDistance: 0,
+				regionFastPace: 0
+			}
+		};
+
+		function injectCss(css) {
+			if (!css) return;
+			const style = document.createElement('style');
+			style.innerHTML = css;
+			document.head.appendChild(style);
+		}
+
+		window.onload = function() {
+			STATE.token = Tools.getQueryParam('token') || '';
+			STATE.ecId = Tools.getQueryParam('id') || 0;
+			STATE.mapKey = `${STATE.mapKey}-${STATE.ecId}`;
+			const cachedMap = localStorage.getItem(STATE.mapKey);
+			if (cachedMap) STATE.ocaId = cachedMap;
+
+			if (window.API) {
+				API.setToken(STATE.token);
+				if (Tools.getQueryParam('env') === 'mock') {
+					API.init({ useMock: true });
+					if (!STATE.ecId) STATE.ecId = 'mock_id';
+				}
+			}
+
+			loadCardConfig();
+			matchRsDetailQuery();
+		};
+
+		function loadCardConfig() {
+			if (!window.API) return;
+			API.getCardConfig(STATE.ecId, 'rankOverview').then(res => {
+				let cfg = res;
+				if (res && res.configJson) {
+					try { cfg = JSON.parse(res.configJson); } catch (e) { console.warn('config parse fail', e); }
+				}
+				if (!cfg) return;
+				if (cfg.common && cfg.common.css) injectCss(cfg.common.css);
+				const pageCfg = cfg.rankOverview || cfg.rank_overview || cfg;
+				if (pageCfg && pageCfg.css) injectCss(pageCfg.css);
+				if (pageCfg && pageCfg.pathList) STATE.pathList = pageCfg.pathList;
+				if (pageCfg && pageCfg.pathListStyle && pageCfg.pathListStyle.showLine === false) {
+					// 保留占位,样式已在 pathList 中体现
+				}
+				if (pageCfg && pageCfg.param) STATE.configParam = Object.assign(STATE.configParam, pageCfg.param);
+				document.getElementById('sub-title').innerText = STATE.configParam.subTitle || '';
+			});
+		}
+
+		function matchRsDetailQuery() {
+			if (!window.API) return;
+			const payload = { ecId: STATE.ecId };
+			if (STATE.ocaId) payload.ocaId = STATE.ocaId; // 仅在有值时传入
+			API.getMatchRsDetail(payload.ecId, payload.ocaId).then(res => {
+				if (!res) return;
+				STATE.mcType = res.mcType;
+				STATE.mcId = res.mcId;
+				STATE.mcName = res.mcName;
+				STATE.coiName = res.coiName;
+				STATE.beginSecond = res.beginSecond;
+				STATE.endSecond = res.endSecond;
+				STATE.nickName = res.nickName;
+				STATE.stats.regionTotalNum = res.regionTotalNum;
+				STATE.stats.regionTotalCp = res.regionTotalCp;
+				STATE.stats.regionTotalCpRankNum = res.regionTotalCpRankNum;
+				STATE.stats.regionTotalSysPoint = res.regionTotalSysPoint;
+				STATE.stats.regionTotalDistance = res.regionTotalDictance;
+				STATE.stats.regionFastPace = res.regionFastPace;
+				STATE.mcState = Tools.checkMcState(STATE.beginSecond, STATE.endSecond);
+				document.getElementById('mc-name').innerText = STATE.mcName || '赛事';
+				document.getElementById('sub-title').innerText = STATE.configParam.subTitle || Tools.fmtMcTime2(STATE.beginSecond, STATE.endSecond);
+				updateMidStats();
+				isAllowMcSignUp();
+				mapListQuery();
+			});
+		}
+
+		function isAllowMcSignUp() {
+			if (!window.API) return;
+			API.isAllowMcSignUp(STATE.ecId).then(res => {
+				if (res) STATE.allowMcSignUp = res.allowSignUp;
+				updateMidStats();
+			});
+		}
+
+		function mapListQuery() {
+			if (!window.API || !STATE.mcId) return;
+			API.getMapList(STATE.mcId).then(res => {
+				if (res && res.length > 0) {
+					STATE.mapList = res;
+					if (!STATE.ocaId) STATE.ocaId = res[0].ocaId || res[0].mapId || 0;
+					renderMapSelect();
+					renderPathList();
+					localStorage.setItem(STATE.mapKey, STATE.ocaId);
+				}
+			});
+		}
+
+		function renderMapSelect() {
+			const midType = STATE.configParam.midType || 0;
+			const select = document.getElementById(`map-select-${midType}`);
+			select.innerHTML = '';
+			STATE.mapList.forEach(map => {
+				const opt = document.createElement('option');
+				opt.value = map.ocaId;
+				opt.innerText = map.mapName || '地图';
+				if (map.ocaId == STATE.ocaId) opt.selected = true;
+				select.appendChild(opt);
+			});
+		}
+
+		function renderPathList() {
+			const container = document.getElementById('path-list-container');
+			container.innerHTML = '';
+			const list = STATE.pathList && Object.keys(STATE.pathList).length > 0
+				? Object.values(STATE.pathList).flat()
+				: STATE.mapList.map(m => ({ path: { ocaId: m.ocaId, mcType: STATE.mcType }, pathImg: m.mapPic, navImg: '', type: 3, text: m.mapName }));
+			list.forEach(item => {
+				const ocaId = item.path?.ocaId || item.ocaId || 0;
+				const name = item.text || item.pathName || item.mapName || '路线';
+				const div = document.createElement('div');
+				div.className = `path-item ${ocaId == STATE.ocaId ? 'selected' : ''}`;
+				div.innerHTML = `
+					<div class="flex items-center gap-3">
+						<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-500 font-bold">
+							<i class="fas fa-map-marker-alt"></i>
+						</div>
+						<div>
+							<div class="font-bold text-gray-800">${name}</div>
+							<div class="text-xs text-gray-400">点击选择此路线</div>
+						</div>
+					</div>
+					${ocaId == STATE.ocaId ? '<i class="fas fa-check-circle text-green-500 text-xl"></i>' : ''}
+				`;
+				div.onclick = () => handleMapChange(ocaId);
+				container.appendChild(div);
+			});
+		}
+
+		function updateMidStats() {
+			const midType = STATE.configParam.midType || 0;
+			document.getElementById('mid-type-0').classList.toggle('hidden', midType !== 0);
+			document.getElementById('mid-type-1').classList.toggle('hidden', midType !== 1);
+			document.getElementById(`nick-name-${midType}`).innerText = STATE.nickName || '昵称';
+			document.getElementById(`coi-name-${midType}`).innerText = STATE.coiName || '组织';
+			if (midType === 0) {
+				document.getElementById('stat-num-0').innerText = STATE.stats.regionTotalNum ?? '--';
+				document.getElementById('stat-cp-0').innerText = STATE.stats.regionTotalCp ?? '--';
+				document.getElementById('stat-rank-0').innerText = STATE.stats.regionTotalCpRankNum ?? '--';
+				const btn = document.getElementById('regroup-btn-0');
+				if (STATE.mcState === 1 && STATE.allowMcSignUp) btn.classList.remove('hidden'); else btn.classList.add('hidden');
+			} else {
+				document.getElementById('stat-num-1').innerText = STATE.stats.regionTotalNum ?? '--';
+				document.getElementById('stat-point-1').innerText = STATE.stats.regionTotalSysPoint ?? '--';
+				document.getElementById('stat-dist-1').innerText = Tools.fmtDistance(STATE.stats.regionTotalDistance) ?? '--';
+				document.getElementById('stat-cp-1').innerText = STATE.stats.regionTotalCp ?? '--';
+				document.getElementById('stat-pace-1').innerText = Tools.convertSecondsToHMS(STATE.stats.regionFastPace, 2);
+				const btn = document.getElementById('regroup-btn-1');
+				if (STATE.mcState === 1 && STATE.allowMcSignUp) btn.classList.remove('hidden'); else btn.classList.add('hidden');
+			}
+		}
+
+		function handleMapChange(newOcaId) {
+			if (newOcaId != STATE.ocaId) {
+				STATE.ocaId = newOcaId;
+				localStorage.setItem(STATE.mapKey, STATE.ocaId);
+				matchRsDetailQuery();
+				renderPathList();
+			}
+		}
+
+		function handleBack() {
+			const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+			Bridge.appAction(`./ranklist.html${qs}`);
+		}
+
+		function handleInfo() {
+			document.getElementById('info-modal-content').innerHTML = `<p>${STATE.configParam.subTitle || '暂无说明'}</p>`;
+			document.getElementById('infoModal').classList.remove('hidden');
+		}
+
+		function handleHelp() {
+			handleInfo();
+		}
+
+		function handleRegroup() {
+			const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+			Bridge.appAction(`./signup.html${qs}&from=rankOverview`);
+		}
+
+		function handleStartGame() {
+			if (STATE.mcState === 1) {
+				Bridge.appAction(`action://to_detail/?id=${STATE.ocaId}&matchType=${STATE.mcType}`);
+			} else if (STATE.mcState === 0) {
+				Tools.showToast('比赛尚未开始');
+			} else {
+				Tools.showToast('比赛已结束');
+			}
+		}
+
+		function closeModal(id) {
+			document.getElementById(id).classList.add('hidden');
+		}
+	</script>
+</body>
+</html>

+ 529 - 0
card/newCards/S3/ranklist - Copy.html

@@ -0,0 +1,529 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>S3 排行榜</title>
+	<script src="https://cdn.tailwindcss.com"></script>
+	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
+	<style>
+		@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&display=swap');
+		@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@500;700&display=swap');
+		body { font-family: 'Noto Sans SC', sans-serif; }
+		.page-top {
+			background-image: url('./static/backgroud/top_bg_sddx.png');
+			background-repeat: no-repeat;
+			background-position: center;
+			background-size: cover;
+			min-height: 220px;
+			border-bottom-left-radius: 20px;
+			border-bottom-right-radius: 20px;
+		}
+		.stat-box {
+			background: #9A300E;
+			border: 1px solid #D3A254;
+			border-radius: 6px;
+			width: 45%;
+			padding: 8px;
+			text-align: center;
+		}
+		.stat-label { color: #f3d809; font-size: 12px; font-weight: 500; }
+		.stat-value { color: #f3d809; font-size: 16px; font-weight: 700; font-family: 'Roboto', sans-serif; }
+		.top-btn { background-color: #9A300E; border: 1px solid #D3A254; border-radius: 6px; padding: 4px 12px; color: white; font-size: 12px; font-weight: 500; }
+		.tab-active { color: var(--tab-color, #81cd00); border-bottom: 2px solid var(--tab-color, #81cd00); font-weight: bold; }
+		.tab-inactive { color: #666; }
+		.sub-tab-active { background-color: var(--tab-color, #81cd00); color: white; font-weight: bold; box-shadow: 0 2px 4px rgba(129, 205, 0, 0.3); }
+		.sub-tab-inactive { background-color: #f3f4f6; color: #666; }
+		.rank-item { transition: transform 0.1s; }
+		.rank-item:active { transform: scale(0.99); }
+		.rank-badge { width: 32px; text-align: center; }
+		.myself-highlight { background-color: #f0f9eb; border: 1px solid #81cd00; }
+		.no-scrollbar::-webkit-scrollbar { display: none; }
+	</style>
+</head>
+<body class="bg-gray-50 min-h-screen flex flex-col pb-20">
+
+	<!-- 1. 顶部区域 -->
+	<div class="page-top w-full relative flex flex-col pt-8 px-4 pb-4">
+		<!-- NavBar -->
+		<div class="flex justify-between items-center text-white mb-4">
+			<button onclick="handleBack()" class="w-8 h-8 flex items-center justify-center bg-black/20 rounded-full active:scale-90 transition">
+				<i class="fas fa-chevron-left"></i>
+			</button>
+			<div class="text-lg font-bold drop-shadow-md" id="mc-name">加载中...</div>
+			<div class="flex gap-3">
+				<button onclick="handleMessage()" class="relative w-8 h-8 flex items-center justify-center bg-black/20 rounded-full active:scale-90 transition">
+					<i class="fas fa-bell"></i>
+					<span id="msg-dot" class="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full hidden"></span>
+				</button>
+				<button onclick="handleInfo()" class="w-8 h-8 flex items-center justify-center bg-black/20 rounded-full active:scale-90 transition">
+					<i class="fas fa-question"></i>
+				</button>
+			</div>
+		</div>
+
+		<!-- Stats Dashboard -->
+		<div class="flex justify-between items-center mb-2">
+			<div class="stat-box flex flex-col">
+				<span class="stat-label">赛事总里程</span>
+				<span class="stat-value" id="total-distance">-- km</span>
+			</div>
+			<div class="stat-box flex flex-col">
+				<span class="stat-label" id="label-answer">文化输出</span>
+				<span class="stat-value" id="total-answer">-- 次</span>
+			</div>
+		</div>
+
+		<!-- Today Date -->
+		<div class="text-center mb-auto">
+			<span id="today-date" class="text-[#751f00] font-black text-lg drop-shadow-sm" style="-webkit-text-stroke: 0.5px #DCA452;">--</span>
+		</div>
+
+		<!-- User Action Bar -->
+		<div class="flex justify-between items-end mt-auto">
+			<button class="top-btn" onclick="handleMyTicket()" id="label-ticket">我的奖券</button>
+			<span class="top-btn bg-opacity-100 font-bold text-sm px-4" id="my-nickname">--</span>
+			<button class="top-btn" onclick="handleExchange()" id="label-exchange">领奖地址</button>
+		</div>
+	</div>
+
+	<!-- 2. Stats Bar (Additional) -->
+	<div class="bg-[#d8e8c6] text-[#3d6706] text-xs py-1 px-2 flex justify-around font-medium">
+		<span>题目: <span id="bar-answer">0</span></span>
+		<span>里程: <span id="bar-distance">0</span>km</span>
+		<span>打点: <span id="bar-cp">0</span></span>
+		<span>百味豆: <span id="bar-point">0</span></span>
+	</div>
+
+	<!-- 3. Tabs Area -->
+	<div class="bg-white sticky top-0 z-30 shadow-sm">
+		<!-- Level 1 Tab -->
+		<div class="flex border-b border-gray-100">
+			<button onclick="switchTab1(0)" id="tab1-0" class="flex-1 py-3 text-sm font-bold tab-active">团体</button>
+			<div class="relative flex-1">
+				<button onclick="toggleMapDropdown()" id="tab1-1" class="w-full h-full py-3 text-sm font-bold tab-inactive flex items-center justify-center gap-1">
+					<span id="current-map-name">个人</span>
+					<i class="fas fa-caret-down text-xs"></i>
+				</button>
+				<div id="map-dropdown" class="absolute top-full left-0 w-full bg-white shadow-lg border border-gray-100 hidden max-h-60 overflow-y-auto z-40 rounded-b-lg no-scrollbar"></div>
+			</div>
+		</div>
+
+		<!-- Level 2 Tab -->
+		<div class="flex overflow-x-auto gap-3 p-3 no-scrollbar" id="metric-tabs-container"></div>
+	</div>
+
+	<!-- 4. List Container -->
+	<div id="rank-list-container" class="px-3 pt-3 flex flex-col gap-2 pb-24 min-h-[300px]">
+		<div class="text-center text-gray-400 py-10 text-sm">加载中...</div>
+	</div>
+
+	<!-- Debug Overlay (仅 debug=1 时展示) -->
+	<div id="debug-panel" class="hidden fixed bottom-0 left-0 w-full max-h-40 overflow-y-auto text-xs bg-black/70 text-green-300 p-2 z-50"></div>
+
+	<!-- 5. Bottom Action -->
+	<div class="fixed bottom-0 w-full bg-white border-t border-gray-100 p-4 z-40 safe-area-bottom">
+		<button onclick="handleStartGame()" id="btn-start" class="w-full bg-[#2e85ec] text-white font-bold py-3 rounded-full shadow-lg active:scale-95 transition-transform text-base">
+			加载中...
+		</button>
+	</div>
+
+	<!-- 简单弹窗 -->
+	<div id="infoModal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
+		<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeModal('infoModal')"></div>
+		<div class="bg-white w-[85%] rounded-xl p-6 relative z-10 max-h-[70vh] overflow-y-auto">
+			<h3 class="text-lg font-bold mb-4 text-center border-b pb-2">规则说明</h3>
+			<div id="info-content" class="text-sm text-gray-600 leading-relaxed space-y-2"></div>
+			<button onclick="closeModal('infoModal')" class="mt-6 w-full py-2 bg-gray-100 rounded-lg font-bold text-gray-600">关闭</button>
+		</div>
+	</div>
+
+	<div id="msgModal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
+		<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeModal('msgModal')"></div>
+		<div class="bg-white w-[85%] rounded-xl p-6 relative z-10">
+			<h3 class="text-lg font-bold mb-4 text-center">消息通知</h3>
+			<div id="msg-content" class="text-sm text-gray-600 mb-4">暂无新消息</div>
+			<button onclick="closeModal('msgModal')" class="w-full py-2 bg-[#2e85ec] text-white rounded-lg font-bold">我知道了</button>
+		</div>
+	</div>
+
+	<script src="./js/utils.js"></script>
+	<script src="./js/bridge.js"></script>
+	<script src="./js/api.js"></script>
+	<script>
+		const STATE = {
+			ecId: 0,
+			token: '',
+			mcId: 0,
+			mcType: 0,
+			mcIdListStr: '',
+			ocaId: 0,
+			isJoin: false,
+			mcState: 0,
+			mapList: [],
+			rankData: {},
+			today: '',
+			tab1Current: 0,
+			tab2Current: 0,
+			mapKey: 'rank-tpl-style3-map',
+			rankKey: 'rank-tpl-style3',
+			messageKey: 'message-tpl-style3',
+			configParam: {
+				subTitle: '',
+				labelRightAnswerNum: '文化输出',
+				labelTicketName: '我的奖券',
+				labelAwardAddress: '领奖地址',
+				labelGoodsList: '',
+				tab1InitActIndex: 0,
+				tab2InitActIndex: 0
+			},
+			dispArrStr: 'teamCp,teamTodayCp,teamDistance,teamRightAnswerPer,teamTodayPace,regionCp,regionTodayCp,regionDistance,regionRightAnswerPer,regionTodayPace',
+			tab2ItemsTeam: ['总积分', '今日积分', '总里程', '校园文化', '今日配速'],
+			tab2ItemsPerson: ['总积分', '今日积分', '总里程', '校园文化', '今日配速'],
+			rankKeysTeam: ['teamCpRs', 'teamTodayCpRs', 'teamDistanceRs', 'teamRightAnswerPerRs', 'teamTodayPaceRs'],
+			rankKeysPerson: ['regionCpRs', 'regionTodayCpRs', 'regionDistanceRs', 'regionRightAnswerPerRs', 'regionTodayPaceRs'],
+			rankTypesTeam: ['totalScore', 'totalScore', 'totalDistance', 'rightAnswerPer', 'fastPace'],
+			rankTypesPerson: ['totalScore', 'totalScore', 'totalDistance', 'rightAnswerPer', 'fastPace']
+		};
+
+		function injectCss(css) {
+			if (!css) return;
+			const style = document.createElement('style');
+			style.innerHTML = css;
+			document.head.appendChild(style);
+		}
+
+		function setListMessage(text, color = 'text-gray-400') {
+			document.getElementById('rank-list-container').innerHTML = `<div class="text-center ${color} py-10 text-sm">${text}</div>`;
+		}
+
+		function normalizeOcaId(val) {
+			if (val === null || val === undefined) return 0;
+			if (typeof val === 'number') return val;
+			if (typeof val === 'string') {
+				const trimmed = val.trim();
+				if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
+					try {
+						const parsed = JSON.parse(trimmed);
+						if (parsed && typeof parsed === 'object' && parsed.ocaId) return parsed.ocaId;
+					} catch (e) {}
+				}
+				const num = Number(trimmed);
+				return Number.isNaN(num) ? 0 : num;
+			}
+			if (typeof val === 'object' && val.ocaId) return val.ocaId;
+			return 0;
+		}
+
+		function initPage() {
+			STATE.today = Tools.timestampToTime(Date.now() / 1000, 2);
+			document.getElementById('today-date').innerText = STATE.today;
+			loadCardConfig();
+			matchRsDetailQuery();
+			getUserJoin();
+			getUnreadMessage();
+		}
+
+		window.onload = function() {
+			STATE.token = Tools.getQueryParam('token') || '';
+			STATE.ecId = Tools.getQueryParam('id') || 112;
+			STATE.ocaId = normalizeOcaId(localStorage.getItem(`${STATE.mapKey}-${STATE.ecId}`) || 0);
+			STATE.rankKey = `${STATE.rankKey}-${STATE.ecId}`;
+			STATE.mapKey = `${STATE.mapKey}-${STATE.ecId}`;
+			STATE.messageKey = `${STATE.messageKey}-${STATE.ecId}`;
+
+			if (window.API) {
+				if (Tools.getQueryParam('env') === 'mock') {
+					API.init({ useMock: true });
+					if (!STATE.ecId) STATE.ecId = 'mock_default_id';
+				}
+				if (STATE.token) {
+					API.setToken(STATE.token);
+				} else if (window.Bridge && Bridge.getToken) {
+					Bridge.onToken((tk) => {
+						STATE.token = tk || '';
+						API.setToken(STATE.token);
+						initPage();
+					});
+					Bridge.getToken();
+					return;
+				}
+			}
+
+			initPage();
+		};
+
+		function loadCardConfig() {
+			if (!window.API) return;
+			API.getCardConfig(STATE.ecId, 'rankList').then(res => {
+				let cfg = res;
+				if (res && res.configJson) {
+					try { cfg = JSON.parse(res.configJson); } catch (e) { console.warn('config parse fail', e); }
+				}
+				if (!cfg) return;
+				if (cfg.common && cfg.common.css) injectCss(cfg.common.css);
+
+				const pageCfg = cfg.rankList || cfg.ranklist || cfg.rank_list || cfg;
+				if (pageCfg && pageCfg.css) injectCss(pageCfg.css);
+
+				if (pageCfg && pageCfg.param) {
+					STATE.configParam = Object.assign(STATE.configParam, pageCfg.param);
+					STATE.tab1Current = STATE.configParam.tab1InitActIndex || 0;
+					STATE.tab2Current = STATE.configParam.tab2InitActIndex || 0;
+				}
+				if (pageCfg && pageCfg.rankParam) {
+					const r = pageCfg.rankParam;
+					if (r.dispArrStr) STATE.dispArrStr = r.dispArrStr;
+					if (r.tab2Items_team) STATE.tab2ItemsTeam = r.tab2Items_team;
+					if (r.tab2Items_person_region) STATE.tab2ItemsPerson = r.tab2Items_person_region;
+					if (r.rank1List) STATE.rankKeysTeam = r.rank1List;
+					if (r.rank2List) STATE.rankKeysPerson = r.rank2List;
+					if (r.rankTypeList_team) STATE.rankTypesTeam = r.rankTypeList_team;
+					if (r.rankTypeList_person_region) STATE.rankTypesPerson = r.rankTypeList_person_region;
+				}
+
+				document.documentElement.style.setProperty('--tab-color', cfg.common?.tabActiveColor || '#81cd00');
+				document.getElementById('label-answer').innerText = STATE.configParam.labelRightAnswerNum;
+				document.getElementById('label-ticket').innerText = STATE.configParam.labelTicketName || '我的奖券';
+				document.getElementById('label-exchange').innerText = STATE.configParam.labelGoodsList || STATE.configParam.labelAwardAddress || '领奖地址';
+				renderTabs();
+			});
+		}
+
+		function matchRsDetailQuery() {
+			if (!window.API) return;
+			API.getMatchRsDetail(STATE.ecId, STATE.ocaId).then(res => {
+				if (!res) return;
+				STATE.mcType = res.mcType;
+				STATE.mcId = res.mcId;
+				STATE.mcIdListStr = res.mcIdListStr || res.mcId || '';
+				STATE.mcName = res.mcName;
+				STATE.beginSecond = res.beginSecond;
+				STATE.endSecond = res.endSecond;
+				STATE.nickName = res.nickName;
+				STATE.mcState = Tools.checkMcState(STATE.beginSecond, STATE.endSecond);
+				document.getElementById('mc-name').innerText = STATE.mcName || '赛事';
+				document.getElementById('my-nickname').innerText = STATE.nickName || '游客';
+				mapListQuery();
+				compStatisticQuery();
+			}).catch(err => {
+				console.error('matchRsDetailQuery failed', err);
+				setListMessage('数据加载失败,请检查网络或参数'+STATE.ecId+'|'+STATE.ocaId, 'text-red-400');
+			});
+		}
+
+		function mapListQuery() {
+			if (!window.API || !STATE.mcId) {
+				setListMessage('缺少赛事信息,无法加载排行榜', 'text-red-400');
+				return;
+			}
+			API.getMapList(STATE.mcId).then(res => {
+				if (res && res.length > 0) {
+					STATE.mapList = res;
+					if (!STATE.ocaId) {
+						STATE.ocaId = res[0].ocaId || res[0].mapId || 0;
+					}
+					renderMapDropdown();
+				}
+				loadRankData();
+			}).catch(err => {
+				console.error('mapListQuery failed', err);
+				loadRankData();
+			});
+		}
+
+		function compStatisticQuery() {
+			if (!window.API || !STATE.mcId) return;
+			API.getCompStatistic(STATE.mcId).then(res => {
+				if (!res) return;
+				const distKm = Tools.fmtDistance(res.totalDistance);
+				document.getElementById('total-distance').innerText = `${distKm} km`;
+				document.getElementById('total-answer').innerText = `${res.totalAnswerNum || 0} 次`;
+				document.getElementById('bar-answer').innerText = res.totalAnswerNum || 0;
+				document.getElementById('bar-distance').innerText = distKm;
+				document.getElementById('bar-cp').innerText = res.totalCp || 0;
+				document.getElementById('bar-point').innerText = res.totalSysPoint || 0;
+			});
+		}
+
+		function loadRankData() {
+			if (!window.API || !STATE.mcId) return;
+			setListMessage('加载中...');
+			const idParam = STATE.mcIdListStr || STATE.mcId;
+			API.getRankDetail(idParam, STATE.mcType, 227, STATE.dispArrStr).then(res => {
+				STATE.rankData = res || {};
+				renderList();
+			}).catch(() => {
+				setListMessage('加载失败', 'text-red-400');
+			});
+		}
+
+		function getUserJoin() {
+			if (!window.API) return;
+			API.getUserJoinStatus(STATE.ecId).then(res => {
+				if (res) {
+					STATE.isJoin = res.isJoin;
+					document.getElementById('btn-start').innerText = res.isJoin ? '选择场地' : '我要报名';
+				}
+			});
+		}
+
+		function getUnreadMessage() {
+			if (!window.API) return;
+			API.getUnReadMessages(STATE.ecId).then(res => {
+				if (res && res.length > 0) document.getElementById('msg-dot').classList.remove('hidden');
+			});
+		}
+
+		function renderTabs() {
+			const container = document.getElementById('metric-tabs-container');
+			container.innerHTML = '';
+			const type = STATE.tab1Current === 0 ? 'team' : 'person';
+			const labels = type === 'team' ? STATE.tab2ItemsTeam : STATE.tab2ItemsPerson;
+			labels.forEach((label, index) => {
+				const btn = document.createElement('button');
+				const isActive = index === STATE.tab2Current;
+				btn.className = `px-4 py-1.5 rounded-full text-xs whitespace-nowrap transition-all ${isActive ? 'sub-tab-active' : 'sub-tab-inactive'}`;
+				btn.innerText = label;
+				btn.onclick = () => switchTab2(index);
+				container.appendChild(btn);
+			});
+
+			document.getElementById('tab1-0').className = `flex-1 py-3 text-sm font-bold ${STATE.tab1Current === 0 ? 'tab-active' : 'tab-inactive'}`;
+			document.getElementById('tab1-1').className = `w-full h-full py-3 text-sm font-bold flex items-center justify-center gap-1 ${STATE.tab1Current === 1 ? 'tab-active' : 'tab-inactive'}`;
+		}
+
+		function renderMapDropdown() {
+			const container = document.getElementById('map-dropdown');
+			container.innerHTML = '';
+			STATE.mapList.forEach(map => {
+				const div = document.createElement('div');
+				div.className = 'p-3 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer border-b border-gray-50 last:border-none';
+				div.innerText = map.mapName || '地图';
+				div.onclick = () => selectMap(map);
+				container.appendChild(div);
+			});
+			const current = STATE.mapList.find(m => m.ocaId == STATE.ocaId);
+			document.getElementById('current-map-name').innerText = current ? `个人(${current.mapName})` : '个人';
+		}
+
+		function renderList() {
+			const container = document.getElementById('rank-list-container');
+			container.innerHTML = '';
+			const isTeam = STATE.tab1Current === 0;
+			const dataKey = (isTeam ? STATE.rankKeysTeam : STATE.rankKeysPerson)[STATE.tab2Current] || '';
+			const rankType = (isTeam ? STATE.rankTypesTeam : STATE.rankTypesPerson)[STATE.tab2Current] || '';
+			const listData = STATE.rankData ? (STATE.rankData[dataKey] || []) : [];
+
+			if (!listData || listData.length === 0) {
+				container.innerHTML = '<div class="text-center text-gray-400 py-10 text-sm">暂无排行数据</div>';
+				return;
+			}
+
+			listData.forEach((item, index) => {
+				const rankNum = item.rankNum || index + 1;
+				let value = item.inRankNum ?? item.score ?? 0;
+				let unit = '';
+				if (rankType === 'totalDistance') {
+					value = Tools.fmtDistance(value);
+					unit = 'km';
+				} else if (rankType === 'fastPace') {
+					value = Tools.convertSecondsToHMS(value, 2);
+				} else if (rankType === 'rightAnswerPer') {
+					unit = '%';
+				}
+				const name = item.teamName || item.userName || '匿名';
+			const badge = rankNum <= 3
+				? `<i class="fas fa-medal ${['text-yellow-500','text-gray-400','text-orange-600'][rankNum-1]} text-xl"></i>`
+				: `<span class="font-bold text-gray-400">${rankNum}</span>`;
+				const row = document.createElement('div');
+				row.className = `rank-item ${item.isSelf ? 'myself-highlight' : 'bg-white'} rounded-xl p-3 flex items-center justify-between shadow-sm border border-gray-100`;
+				row.innerHTML = `
+					<div class="flex items-center gap-3 flex-1 min-w-0">
+						<div class="rank-badge">${badge}</div>
+						<div class="flex flex-col min-w-0">
+							<div class="font-bold text-gray-800 truncate text-sm">
+								${item.isSelf ? '<span class="text-blue-600 mr-1">[我]</span>' : ''}${name}
+							</div>
+						</div>
+					</div>
+					<div class="text-right shrink-0 ml-2">
+						<div class="font-bold text-gray-700 font-mono text-base">${value} <span class="text-xs font-normal text-gray-400">${unit}</span></div>
+					</div>
+				`;
+				container.appendChild(row);
+			});
+		}
+
+		function switchTab1(index) {
+			STATE.tab1Current = index;
+			STATE.tab2Current = 0;
+			renderTabs();
+			renderList();
+		}
+
+		function switchTab2(index) {
+			STATE.tab2Current = index;
+			renderTabs();
+			renderList();
+		}
+
+		function toggleMapDropdown() {
+			if (STATE.tab1Current !== 1) {
+				switchTab1(1);
+			}
+			document.getElementById('map-dropdown').classList.toggle('hidden');
+		}
+
+		function selectMap(map) {
+			STATE.ocaId = map.ocaId || map.mapId || 0;
+			localStorage.setItem(STATE.mapKey, STATE.ocaId);
+			document.getElementById('map-dropdown').classList.add('hidden');
+			renderMapDropdown();
+			loadRankData();
+		}
+
+		function handleBack() {
+			Bridge.appAction('action://to_home/');
+		}
+
+		function handleMessage() {
+			document.getElementById('msgModal').classList.remove('hidden');
+			document.getElementById('msg-dot').classList.add('hidden');
+		}
+
+		function handleInfo() {
+			const info = STATE.configParam.subTitle || '暂无规则描述';
+			document.getElementById('info-content').innerHTML = `<p>${info}</p>`;
+			document.getElementById('infoModal').classList.remove('hidden');
+		}
+
+		function handleMyTicket() {
+			const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+			Bridge.appAction(`/pages/achievement/index2?tabCurrent=2${qs.replace('?', '&')}`, 'uni.navigateTo');
+		}
+
+		function handleExchange() {
+			const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+			if (STATE.configParam.labelGoodsList) {
+				Bridge.appAction(`/pages/exchange/style1/goodsList${qs.replace('?', '&')}`, 'uni.navigateTo');
+			} else {
+				Tools.showToast('请在客户端内查看兑换入口');
+			}
+		}
+
+		function handleStartGame() {
+			const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+			if (STATE.isJoin) {
+				Bridge.appAction(`./rankOverview.html${qs}`);
+			} else {
+				Bridge.appAction(`./signup.html${qs}`);
+			}
+		}
+
+		function closeModal(id) {
+			document.getElementById(id).classList.add('hidden');
+		}
+	</script>
+</body>
+</html>

+ 531 - 0
card/newCards/S3/ranklist.html

@@ -0,0 +1,531 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>S3 排行榜</title>
+	<script src="https://cdn.tailwindcss.com"></script>
+	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
+	<style>
+		@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&display=swap');
+		@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@500;700&display=swap');
+		body { font-family: 'Noto Sans SC', sans-serif; }
+		.page-top {
+			background-image: url('./static/backgroud/top_bg_sddx.png');
+			background-repeat: no-repeat;
+			background-position: center;
+			background-size: cover;
+			min-height: 220px;
+			border-bottom-left-radius: 20px;
+			border-bottom-right-radius: 20px;
+		}
+		.stat-box {
+			background: #9A300E;
+			border: 1px solid #D3A254;
+			border-radius: 6px;
+			width: 45%;
+			padding: 8px;
+			text-align: center;
+		}
+		.stat-label { color: #f3d809; font-size: 12px; font-weight: 500; }
+		.stat-value { color: #f3d809; font-size: 16px; font-weight: 700; font-family: 'Roboto', sans-serif; }
+		.top-btn { background-color: #9A300E; border: 1px solid #D3A254; border-radius: 6px; padding: 4px 12px; color: white; font-size: 12px; font-weight: 500; }
+		.tab-active { color: var(--tab-color, #81cd00); border-bottom: 2px solid var(--tab-color, #81cd00); font-weight: bold; }
+		.tab-inactive { color: #666; }
+		.sub-tab-active { background-color: var(--tab-color, #81cd00); color: white; font-weight: bold; box-shadow: 0 2px 4px rgba(129, 205, 0, 0.3); }
+		.sub-tab-inactive { background-color: #f3f4f6; color: #666; }
+		.rank-item { transition: transform 0.1s; }
+		.rank-item:active { transform: scale(0.99); }
+		.rank-badge { width: 32px; text-align: center; }
+		.myself-highlight { background-color: #f0f9eb; border: 1px solid #81cd00; }
+		.no-scrollbar::-webkit-scrollbar { display: none; }
+	</style>
+</head>
+<body class="bg-gray-50 min-h-screen flex flex-col pb-20">
+
+	<!-- 1. 顶部区域 -->
+	<div class="page-top w-full relative flex flex-col pt-8 px-4 pb-4">
+		<!-- NavBar -->
+		<div class="flex justify-between items-center text-white mb-4">
+			<button onclick="handleBack()" class="w-8 h-8 flex items-center justify-center bg-black/20 rounded-full active:scale-90 transition">
+				<i class="fas fa-chevron-left"></i>
+			</button>
+			<div class="text-lg font-bold drop-shadow-md" id="mc-name">加载中...</div>
+			<div class="flex gap-3">
+				<button onclick="handleMessage()" class="relative w-8 h-8 flex items-center justify-center bg-black/20 rounded-full active:scale-90 transition">
+					<i class="fas fa-bell"></i>
+					<span id="msg-dot" class="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full hidden"></span>
+				</button>
+				<button onclick="handleInfo()" class="w-8 h-8 flex items-center justify-center bg-black/20 rounded-full active:scale-90 transition">
+					<i class="fas fa-question"></i>
+				</button>
+			</div>
+		</div>
+
+		<!-- Stats Dashboard -->
+		<div class="flex justify-between items-center mb-2">
+			<div class="stat-box flex flex-col">
+				<span class="stat-label">赛事总里程</span>
+				<span class="stat-value" id="total-distance">-- km</span>
+			</div>
+			<div class="stat-box flex flex-col">
+				<span class="stat-label" id="label-answer">文化输出</span>
+				<span class="stat-value" id="total-answer">-- 次</span>
+			</div>
+		</div>
+
+		<!-- Today Date -->
+		<div class="text-center mb-auto">
+			<span id="today-date" class="text-[#751f00] font-black text-lg drop-shadow-sm" style="-webkit-text-stroke: 0.5px #DCA452;">--</span>
+		</div>
+
+		<!-- User Action Bar -->
+		<div class="flex justify-between items-end mt-auto">
+			<button class="top-btn" onclick="handleMyTicket()" id="label-ticket">我的奖券</button>
+			<span class="top-btn bg-opacity-100 font-bold text-sm px-4" id="my-nickname">--</span>
+			<button class="top-btn" onclick="handleExchange()" id="label-exchange">领奖地址</button>
+		</div>
+	</div>
+
+	<!-- 2. Stats Bar (Additional) -->
+	<div class="bg-[#d8e8c6] text-[#3d6706] text-xs py-1 px-2 flex justify-around font-medium">
+		<span>题目: <span id="bar-answer">0</span></span>
+		<span>里程: <span id="bar-distance">0</span>km</span>
+		<span>打点: <span id="bar-cp">0</span></span>
+		<span>百味豆: <span id="bar-point">0</span></span>
+	</div>
+
+	<!-- 3. Tabs Area -->
+	<div class="bg-white sticky top-0 z-30 shadow-sm">
+		<!-- Level 1 Tab -->
+		<div class="flex border-b border-gray-100">
+			<button onclick="switchTab1(0)" id="tab1-0" class="flex-1 py-3 text-sm font-bold tab-active">团体</button>
+			<div class="relative flex-1">
+				<button onclick="toggleMapDropdown()" id="tab1-1" class="w-full h-full py-3 text-sm font-bold tab-inactive flex items-center justify-center gap-1">
+					<span id="current-map-name">个人</span>
+					<i class="fas fa-caret-down text-xs"></i>
+				</button>
+				<div id="map-dropdown" class="absolute top-full left-0 w-full bg-white shadow-lg border border-gray-100 hidden max-h-60 overflow-y-auto z-40 rounded-b-lg no-scrollbar"></div>
+			</div>
+		</div>
+
+		<!-- Level 2 Tab -->
+		<div class="flex overflow-x-auto gap-3 p-3 no-scrollbar" id="metric-tabs-container"></div>
+	</div>
+
+	<!-- 4. List Container -->
+	<div id="rank-list-container" class="px-3 pt-3 flex flex-col gap-2 pb-24 min-h-[300px]">
+		<div class="text-center text-gray-400 py-10 text-sm">加载中...</div>
+	</div>
+
+	<!-- Debug Overlay (仅 debug=1 时展示) -->
+	<div id="debug-panel" class="hidden fixed bottom-0 left-0 w-full max-h-40 overflow-y-auto text-xs bg-black/70 text-green-300 p-2 z-50"></div>
+
+	<!-- 5. Bottom Action -->
+	<div class="fixed bottom-0 w-full bg-white border-t border-gray-100 p-4 z-40 safe-area-bottom">
+		<button onclick="handleStartGame()" id="btn-start" class="w-full bg-[#2e85ec] text-white font-bold py-3 rounded-full shadow-lg active:scale-95 transition-transform text-base">
+			加载中...
+		</button>
+	</div>
+
+	<!-- 简单弹窗 -->
+	<div id="infoModal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
+		<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeModal('infoModal')"></div>
+		<div class="bg-white w-[85%] rounded-xl p-6 relative z-10 max-h-[70vh] overflow-y-auto">
+			<h3 class="text-lg font-bold mb-4 text-center border-b pb-2">规则说明</h3>
+			<div id="info-content" class="text-sm text-gray-600 leading-relaxed space-y-2"></div>
+			<button onclick="closeModal('infoModal')" class="mt-6 w-full py-2 bg-gray-100 rounded-lg font-bold text-gray-600">关闭</button>
+		</div>
+	</div>
+
+	<div id="msgModal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
+		<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeModal('msgModal')"></div>
+		<div class="bg-white w-[85%] rounded-xl p-6 relative z-10">
+			<h3 class="text-lg font-bold mb-4 text-center">消息通知</h3>
+			<div id="msg-content" class="text-sm text-gray-600 mb-4">暂无新消息</div>
+			<button onclick="closeModal('msgModal')" class="w-full py-2 bg-[#2e85ec] text-white rounded-lg font-bold">我知道了</button>
+		</div>
+	</div>
+
+	<script src="./js/utils.js"></script>
+	<script src="./js/bridge.js"></script>
+	<script src="./js/api.js"></script>
+	<script>
+		const STATE = {
+			ecId: 0,
+			token: '',
+			mcId: 0,
+			mcType: 0,
+			mcIdListStr: '',
+			ocaId: 0,
+			isJoin: false,
+			mcState: 0,
+			mapList: [],
+			rankData: {},
+			today: '',
+			tab1Current: 0,
+			tab2Current: 0,
+			mapKey: 'rank-tpl-style3-map',
+			rankKey: 'rank-tpl-style3',
+			messageKey: 'message-tpl-style3',
+			configParam: {
+				subTitle: '',
+				labelRightAnswerNum: '文化输出',
+				labelTicketName: '我的奖券',
+				labelAwardAddress: '领奖地址',
+				labelGoodsList: '',
+				tab1InitActIndex: 0,
+				tab2InitActIndex: 0
+			},
+			dispArrStr: 'teamCp,teamTodayCp,teamDistance,teamRightAnswerPer,teamTodayPace,regionCp,regionTodayCp,regionDistance,regionRightAnswerPer,regionTodayPace',
+			tab2ItemsTeam: ['总积分', '今日积分', '总里程', '校园文化', '今日配速'],
+			tab2ItemsPerson: ['总积分', '今日积分', '总里程', '校园文化', '今日配速'],
+			rankKeysTeam: ['teamCpRs', 'teamTodayCpRs', 'teamDistanceRs', 'teamRightAnswerPerRs', 'teamTodayPaceRs'],
+			rankKeysPerson: ['regionCpRs', 'regionTodayCpRs', 'regionDistanceRs', 'regionRightAnswerPerRs', 'regionTodayPaceRs'],
+			rankTypesTeam: ['totalScore', 'totalScore', 'totalDistance', 'rightAnswerPer', 'fastPace'],
+			rankTypesPerson: ['totalScore', 'totalScore', 'totalDistance', 'rightAnswerPer', 'fastPace']
+		};
+
+		function injectCss(css) {
+			if (!css) return;
+			const style = document.createElement('style');
+			style.innerHTML = css;
+			document.head.appendChild(style);
+		}
+
+		function setListMessage(text, color = 'text-gray-400') {
+			document.getElementById('rank-list-container').innerHTML = `<div class="text-center ${color} py-10 text-sm">${text}</div>`;
+		}
+
+		function normalizeOcaId(val) {
+			if (val === null || val === undefined) return 0;
+			if (typeof val === 'number') return val;
+			if (typeof val === 'string') {
+				const trimmed = val.trim();
+				if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
+					try {
+						const parsed = JSON.parse(trimmed);
+						if (parsed && typeof parsed === 'object' && parsed.ocaId) return parsed.ocaId;
+					} catch (e) {}
+				}
+				const num = Number(trimmed);
+				return Number.isNaN(num) ? 0 : num;
+			}
+			if (typeof val === 'object' && val.ocaId) return val.ocaId;
+			return 0;
+		}
+
+		function initPage() {
+			STATE.today = Tools.timestampToTime(Date.now() / 1000, 2);
+			document.getElementById('today-date').innerText = STATE.today;
+			loadCardConfig();
+			matchRsDetailQuery();
+			getUserJoin();
+			getUnreadMessage();
+		}
+
+		window.onload = function() {
+			STATE.token = Tools.getQueryParam('token') || '';
+			STATE.ecId = Tools.getQueryParam('id') || 112;
+			STATE.ocaId = normalizeOcaId(localStorage.getItem(`${STATE.mapKey}-${STATE.ecId}`) || 0);
+			STATE.rankKey = `${STATE.rankKey}-${STATE.ecId}`;
+			STATE.mapKey = `${STATE.mapKey}-${STATE.ecId}`;
+			STATE.messageKey = `${STATE.messageKey}-${STATE.ecId}`;
+
+			if (window.API) {
+				if (Tools.getQueryParam('env') === 'mock') {
+					API.init({ useMock: true });
+					if (!STATE.ecId) STATE.ecId = 'mock_default_id';
+				}
+				if (STATE.token) {
+					API.setToken(STATE.token);
+				} else if (window.Bridge && Bridge.getToken) {
+					Bridge.onToken((tk) => {
+						STATE.token = tk || '';
+						API.setToken(STATE.token);
+						initPage();
+					});
+					Bridge.getToken();
+					return;
+				}
+			}
+
+			initPage();
+		};
+
+		function loadCardConfig() {
+			if (!window.API) return;
+			API.getCardConfig(STATE.ecId, 'rankList').then(res => {
+				let cfg = res;
+				if (res && res.configJson) {
+					try { cfg = JSON.parse(res.configJson); } catch (e) { console.warn('config parse fail', e); }
+				}
+				if (!cfg) return;
+				if (cfg.common && cfg.common.css) injectCss(cfg.common.css);
+
+				const pageCfg = cfg.rankList || cfg.ranklist || cfg.rank_list || cfg;
+				if (pageCfg && pageCfg.css) injectCss(pageCfg.css);
+
+				if (pageCfg && pageCfg.param) {
+					STATE.configParam = Object.assign(STATE.configParam, pageCfg.param);
+					STATE.tab1Current = STATE.configParam.tab1InitActIndex || 0;
+					STATE.tab2Current = STATE.configParam.tab2InitActIndex || 0;
+				}
+				if (pageCfg && pageCfg.rankParam) {
+					const r = pageCfg.rankParam;
+					if (r.dispArrStr) STATE.dispArrStr = r.dispArrStr;
+					if (r.tab2Items_team) STATE.tab2ItemsTeam = r.tab2Items_team;
+					if (r.tab2Items_person_region) STATE.tab2ItemsPerson = r.tab2Items_person_region;
+					if (r.rank1List) STATE.rankKeysTeam = r.rank1List;
+					if (r.rank2List) STATE.rankKeysPerson = r.rank2List;
+					if (r.rankTypeList_team) STATE.rankTypesTeam = r.rankTypeList_team;
+					if (r.rankTypeList_person_region) STATE.rankTypesPerson = r.rankTypeList_person_region;
+				}
+
+				document.documentElement.style.setProperty('--tab-color', cfg.common?.tabActiveColor || '#81cd00');
+				document.getElementById('label-answer').innerText = STATE.configParam.labelRightAnswerNum;
+				document.getElementById('label-ticket').innerText = STATE.configParam.labelTicketName || '我的奖券';
+				document.getElementById('label-exchange').innerText = STATE.configParam.labelGoodsList || STATE.configParam.labelAwardAddress || '领奖地址';
+				renderTabs();
+			});
+		}
+
+		function matchRsDetailQuery() {
+			if (!window.API) return;
+			API.getMatchRsDetail(STATE.ecId, STATE.ocaId).then(res => {
+				if (!res) return;
+				STATE.mcType = res.mcType;
+				STATE.mcId = res.mcId;
+				STATE.mcIdListStr = res.mcIdListStr || res.mcId || '';
+				STATE.mcName = res.mcName;
+				STATE.beginSecond = res.beginSecond;
+				STATE.endSecond = res.endSecond;
+				STATE.nickName = res.nickName;
+				STATE.mcState = Tools.checkMcState(STATE.beginSecond, STATE.endSecond);
+				document.getElementById('mc-name').innerText = STATE.mcName || '赛事';
+				document.getElementById('my-nickname').innerText = STATE.nickName || '游客';
+				mapListQuery();
+				compStatisticQuery();
+			}).catch(err => {
+				console.error('matchRsDetailQuery failed', err);
+				setListMessage('数据加载失败,请检查网络或参数'+STATE.ecId+'|'+STATE.ocaId, 'text-red-400');
+			});
+		}
+
+		function mapListQuery() {
+			if (!window.API || !STATE.mcId) {
+				setListMessage('缺少赛事信息,无法加载排行榜', 'text-red-400');
+				return;
+			}
+			API.getMapList(STATE.mcId).then(res => {
+				if (res && res.length > 0) {
+					STATE.mapList = res;
+					if (!STATE.ocaId) {
+						STATE.ocaId = res[0].ocaId || res[0].mapId || 0;
+					}
+					renderMapDropdown();
+				}
+				loadRankData();
+			}).catch(err => {
+				console.error('mapListQuery failed', err);
+				loadRankData();
+			});
+		}
+
+		function compStatisticQuery() {
+			if (!window.API || !STATE.mcId) return;
+			API.getCompStatistic(STATE.mcId).then(res => {
+				if (!res) return;
+				const distKm = Tools.fmtDistance(res.totalDistance);
+				document.getElementById('total-distance').innerText = `${distKm} km`;
+				document.getElementById('total-answer').innerText = `${res.totalAnswerNum || 0} 次`;
+				document.getElementById('bar-answer').innerText = res.totalAnswerNum || 0;
+				document.getElementById('bar-distance').innerText = distKm;
+				document.getElementById('bar-cp').innerText = res.totalCp || 0;
+				document.getElementById('bar-point').innerText = res.totalSysPoint || 0;
+			});
+		}
+
+		function loadRankData() {
+			if (!window.API || !STATE.mcId) return;
+			setListMessage('加载中...');
+			const idParam = STATE.mcIdListStr || STATE.mcId;
+			API.getRankDetail(idParam, STATE.mcType, 227, STATE.dispArrStr).then(res => {
+				STATE.rankData = res || {};
+				renderList();
+			}).catch(() => {
+				setListMessage('加载失败', 'text-red-400');
+			});
+		}
+
+		function getUserJoin() {
+			if (!window.API) return;
+			API.getUserJoinStatus(STATE.ecId).then(res => {
+				if (res) {
+					STATE.isJoin = res.isJoin;
+					document.getElementById('btn-start').innerText = res.isJoin ? '选择场地' : '我要报名';
+				}
+			});
+		}
+
+		function getUnreadMessage() {
+			if (!window.API) return;
+			API.getUnReadMessages(STATE.ecId).then(res => {
+				if (res && res.length > 0) document.getElementById('msg-dot').classList.remove('hidden');
+			});
+		}
+
+		function renderTabs() {
+			const container = document.getElementById('metric-tabs-container');
+			container.innerHTML = '';
+			const type = STATE.tab1Current === 0 ? 'team' : 'person';
+			const labels = type === 'team' ? STATE.tab2ItemsTeam : STATE.tab2ItemsPerson;
+			labels.forEach((label, index) => {
+				const btn = document.createElement('button');
+				const isActive = index === STATE.tab2Current;
+				btn.className = `px-4 py-1.5 rounded-full text-xs whitespace-nowrap transition-all ${isActive ? 'sub-tab-active' : 'sub-tab-inactive'}`;
+				btn.innerText = label;
+				btn.onclick = () => switchTab2(index);
+				container.appendChild(btn);
+			});
+
+			document.getElementById('tab1-0').className = `flex-1 py-3 text-sm font-bold ${STATE.tab1Current === 0 ? 'tab-active' : 'tab-inactive'}`;
+			document.getElementById('tab1-1').className = `w-full h-full py-3 text-sm font-bold flex items-center justify-center gap-1 ${STATE.tab1Current === 1 ? 'tab-active' : 'tab-inactive'}`;
+		}
+
+		function renderMapDropdown() {
+			const container = document.getElementById('map-dropdown');
+			container.innerHTML = '';
+			STATE.mapList.forEach(map => {
+				const div = document.createElement('div');
+				div.className = 'p-3 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer border-b border-gray-50 last:border-none';
+				div.innerText = map.mapName || '地图';
+				div.onclick = () => selectMap(map);
+				container.appendChild(div);
+			});
+			const current = STATE.mapList.find(m => m.ocaId == STATE.ocaId);
+			document.getElementById('current-map-name').innerText = current ? `个人(${current.mapName})` : '个人';
+		}
+
+		function renderList() {
+			const container = document.getElementById('rank-list-container');
+			container.innerHTML = '';
+			const isTeam = STATE.tab1Current === 0;
+			const dataKey = (isTeam ? STATE.rankKeysTeam : STATE.rankKeysPerson)[STATE.tab2Current] || '';
+			const rankType = (isTeam ? STATE.rankTypesTeam : STATE.rankTypesPerson)[STATE.tab2Current] || '';
+			const label = (isTeam ? STATE.tab2ItemsTeam : STATE.tab2ItemsPerson)[STATE.tab2Current] || '';
+			const isPace = rankType === 'fastPace' || (dataKey && dataKey.toLowerCase().includes('pace')) || (label && label.indexOf('配速') !== -1) || (label && label.indexOf('用时') !== -1);
+			const listData = STATE.rankData ? (STATE.rankData[dataKey] || []) : [];
+
+			if (!listData || listData.length === 0) {
+				container.innerHTML = '<div class="text-center text-gray-400 py-10 text-sm">暂无排行数据</div>';
+				return;
+			}
+
+			listData.forEach((item, index) => {
+				const rankNum = item.rankNum || index + 1;
+				let value = item.inRankNum ?? item.score ?? 0;
+				let unit = '';
+				if (rankType === 'totalDistance') {
+					value = Tools.fmtDistance(value);
+					unit = 'km';
+				} else if (isPace) {
+					value = Tools.convertSecondsToHMS(value, 2);
+				} else if (rankType === 'rightAnswerPer') {
+					unit = '%';
+				}
+				const name = item.teamName || item.userName || '匿名';
+			const badge = rankNum <= 3
+				? `<i class="fas fa-medal ${['text-yellow-500','text-gray-400','text-orange-600'][rankNum-1]} text-xl"></i>`
+				: `<span class="font-bold text-gray-400">${rankNum}</span>`;
+				const row = document.createElement('div');
+				row.className = `rank-item ${item.isSelf ? 'myself-highlight' : 'bg-white'} rounded-xl p-3 flex items-center justify-between shadow-sm border border-gray-100`;
+				row.innerHTML = `
+					<div class="flex items-center gap-3 flex-1 min-w-0">
+						<div class="rank-badge">${badge}</div>
+						<div class="flex flex-col min-w-0">
+							<div class="font-bold text-gray-800 truncate text-sm">
+								${item.isSelf ? '<span class="text-blue-600 mr-1">[我]</span>' : ''}${name}
+							</div>
+						</div>
+					</div>
+					<div class="text-right shrink-0 ml-2">
+						<div class="font-bold text-gray-700 font-mono text-base">${value} <span class="text-xs font-normal text-gray-400">${unit}</span></div>
+					</div>
+				`;
+				container.appendChild(row);
+			});
+		}
+
+		function switchTab1(index) {
+			STATE.tab1Current = index;
+			STATE.tab2Current = 0;
+			renderTabs();
+			renderList();
+		}
+
+		function switchTab2(index) {
+			STATE.tab2Current = index;
+			renderTabs();
+			renderList();
+		}
+
+		function toggleMapDropdown() {
+			if (STATE.tab1Current !== 1) {
+				switchTab1(1);
+			}
+			document.getElementById('map-dropdown').classList.toggle('hidden');
+		}
+
+		function selectMap(map) {
+			STATE.ocaId = map.ocaId || map.mapId || 0;
+			localStorage.setItem(STATE.mapKey, STATE.ocaId);
+			document.getElementById('map-dropdown').classList.add('hidden');
+			renderMapDropdown();
+			loadRankData();
+		}
+
+		function handleBack() {
+			Bridge.appAction('action://to_home/');
+		}
+
+		function handleMessage() {
+			document.getElementById('msgModal').classList.remove('hidden');
+			document.getElementById('msg-dot').classList.add('hidden');
+		}
+
+		function handleInfo() {
+			const info = STATE.configParam.subTitle || '暂无规则描述';
+			document.getElementById('info-content').innerHTML = `<p>${info}</p>`;
+			document.getElementById('infoModal').classList.remove('hidden');
+		}
+
+		function handleMyTicket() {
+			const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+			Bridge.appAction(`/pages/achievement/index2?tabCurrent=2${qs.replace('?', '&')}`, 'uni.navigateTo');
+		}
+
+		function handleExchange() {
+			const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+			if (STATE.configParam.labelGoodsList) {
+				Bridge.appAction(`/pages/exchange/style1/goodsList${qs.replace('?', '&')}`, 'uni.navigateTo');
+			} else {
+				Tools.showToast('请在客户端内查看兑换入口');
+			}
+		}
+
+		function handleStartGame() {
+			const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+			if (STATE.isJoin) {
+				Bridge.appAction(`./rankOverview.html${qs}`);
+			} else {
+				Bridge.appAction(`./signup.html${qs}`);
+			}
+		}
+
+		function closeModal(id) {
+			document.getElementById(id).classList.add('hidden');
+		}
+	</script>
+</body>
+</html>

+ 379 - 0
card/newCards/S3/signup.html

@@ -0,0 +1,379 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>S3 报名</title>
+	<script src="https://cdn.tailwindcss.com"></script>
+	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
+	<style>
+		@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
+		body { font-family: 'Roboto', sans-serif; }
+		.page-top {
+			background-image: url('./static/backgroud/top_bg_sddx.png');
+			background-repeat: no-repeat;
+			background-position: center;
+			background-size: cover;
+			min-height: 220px;
+		}
+		.logo-bg {
+			background-image: url('./static/logo/jbs.png');
+			background-repeat: no-repeat;
+			background-position: center;
+			background-size: contain;
+			width: 80px; height: 80px; margin-top: 10px;
+		}
+		.e-select-wrapper { position: relative; }
+		.e-select-input { width: 100%; padding: 10px; border: 1px solid #dcdfe6; border-radius: 4px; background-color: white; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
+		.e-select-dropdown { position: absolute; width: 100%; background: white; border: 1px solid #dcdfe6; border-radius: 4px; max-height: 200px; overflow-y: auto; z-index: 10; }
+		.e-select-item { padding: 10px; cursor: pointer; }
+		.e-select-item:hover { background-color: #f0f0f0; }
+	</style>
+</head>
+<body class="bg-gray-100 min-h-screen flex flex-col items-center">
+
+	<!-- Top Section -->
+	<div class="page-top w-full flex flex-col justify-between items-center pb-4">
+		<div class="w-full flex justify-between items-center p-4">
+			<button onclick="handleBack()" class="bg-black/20 backdrop-blur-sm p-2 rounded-full w-9 h-9 flex items-center justify-center text-white active:scale-90 transition">
+				<i class="fas fa-chevron-left"></i>
+			</button>
+			<h1 id="mc-name" class="text-white text-lg font-bold">赛事名称</h1>
+			<button onclick="handleInfo()" class="bg-black/20 backdrop-blur-sm px-3 py-1.5 rounded-full text-xs font-semibold flex items-center gap-1 text-white active:scale-90 transition">
+				<i class="fas fa-question-circle"></i> 说明
+			</button>
+		</div>
+
+		<div class="flex flex-col items-center gap-2">
+			<div class="logo-bg"></div>
+			<p id="sub-title" class="text-yellow-400 text-lg font-bold">活动时间</p>
+		</div>
+	</div>
+
+	<!-- Time Bar -->
+	<div class="timebar flex items-center justify-center -mt-4 bg-white px-4 py-2 rounded-full shadow-md z-10 border border-gray-200">
+		<img src="https://img.icons8.com/ios-filled/50/888888/time.png" class="w-4 h-4 mr-2" alt="clock">
+		<span id="act-time" class="text-gray-800 text-sm font-semibold whitespace-nowrap"></span>
+	</div>
+
+	<!-- Main Form Section -->
+	<div class="flex flex-col items-center w-11/12 max-w-sm px-4 py-6 bg-white rounded-lg shadow-lg mt-4">
+		<input type="text" id="nickNameInput" maxlength="12" placeholder="请输入昵称"
+			   class="w-full h-10 px-3 my-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 text-sm" />
+		
+		<div class="e-select-wrapper w-full my-2">
+			<div id="coiSelectInput" class="e-select-input text-gray-700 text-sm" tabindex="0">
+				<span id="selectedCoiName">请选择组织</span>
+				<i class="fas fa-chevron-down text-gray-400"></i>
+			</div>
+			<div id="coiDropdown" class="e-select-dropdown hidden">
+				<input type="text" id="coiSearchInput" placeholder="搜索组织" class="w-full px-3 py-2 border-b border-gray-200 focus:outline-none" />
+				<div id="coiOptionsContainer"></div>
+			</div>
+		</div>
+
+		<div id="introduce-section" class="w-full mt-4 text-gray-700 text-sm leading-relaxed hidden">
+			<h3 id="introduce-title" class="font-bold text-base mb-1"></h3>
+			<div id="introduce-content"></div>
+		</div>
+
+		<div id="rules-section" class="w-full mt-4 p-4 bg-gray-100 rounded-lg hidden">
+			<h3 id="rules-title" class="font-bold text-sm mb-1"></h3>
+			<div id="rules-content" class="text-xs text-gray-600"></div>
+		</div>
+
+		<button id="signup-btn" onclick="handleSignup()" class="w-full h-12 mt-6 text-white text-lg font-bold bg-green-500 rounded-full shadow-lg active:scale-95 transition-transform">
+			我要报名
+		</button>
+		<button id="signup-btn-disabled" class="w-full h-12 mt-6 text-white text-lg font-bold bg-gray-400 rounded-full shadow-lg cursor-not-allowed hidden">
+			活动已结束
+		</button>
+	</div>
+
+	<!-- Info Modal -->
+	<div id="infoModal" class="fixed inset-0 z-50 hidden transition-opacity duration-300">
+		<div class="absolute inset-0 bg-slate-900/70 backdrop-blur-sm" onclick="closeInfoModal()"></div>
+		<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[90%] bg-white rounded-2xl p-6 shadow-2xl">
+			<button onclick="closeInfoModal()" class="absolute -top-10 right-0 text-white/80 hover:text-white w-8 h-8 flex items-center justify-center rounded-full border border-white/30">
+				<i class="fas fa-times"></i>
+			</button>
+			<h3 class="text-center font-bold text-lg mb-4 text-blue-600">活动说明</h3>
+			<div id="info-modal-content" class="text-sm text-gray-700 max-h-80 overflow-y-auto"></div>
+		</div>
+	</div>
+
+	<!-- Alert Dialog -->
+	<div id="alertDialog" class="fixed inset-0 z-50 hidden transition-opacity duration-300">
+		<div class="absolute inset-0 bg-slate-900/70 backdrop-blur-sm" onclick="closeAlertDialog()"></div>
+		<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[90%] max-w-xs bg-white rounded-lg p-6 shadow-2xl text-center">
+			<h3 class="font-bold text-lg mb-4">请确认报名信息</h3>
+			<div class="text-sm text-gray-700 space-y-2">
+				<p id="alert-mc-name" class="font-bold"></p>
+				<p id="alert-nickname-label"></p>
+				<p id="alert-coi-label"></p>
+			</div>
+			<div class="flex justify-around mt-6 space-x-4">
+				<button onclick="closeAlertDialog()" class="flex-1 py-2 rounded-md border border-gray-300 text-gray-600">取消</button>
+				<button onclick="confirmSignup()" class="flex-1 py-2 rounded-md bg-blue-500 text-white">确认</button>
+			</div>
+		</div>
+	</div>
+
+	<script src="./js/utils.js"></script>
+	<script src="./js/bridge.js"></script>
+	<script src="./js/api.js"></script>
+	<script>
+		const STATE = {
+			ecId: 0,
+			token: '',
+			mcId: 0,
+			mcType: 0,
+			mcName: '',
+			beginSecond: 0,
+			endSecond: 0,
+			mcState: 0,
+			nickName: '',
+			coiId: 0,
+			coiName: '',
+			coiOptions: [],
+			fromPage: '',
+			configParam: { labelName: '昵称', labelOrg: '组织', subTitle: '' },
+			introduce: { title: '', content: '' },
+			activityRules: { title: '', content: '' },
+			popupRuleList: []
+		};
+
+		const mcNameEl = document.getElementById('mc-name');
+		const subTitleEl = document.getElementById('sub-title');
+		const actTimeEl = document.getElementById('act-time');
+		const nickNameInput = document.getElementById('nickNameInput');
+		const signupBtn = document.getElementById('signup-btn');
+		const signupBtnDisabled = document.getElementById('signup-btn-disabled');
+		const coiSelectInput = document.getElementById('coiSelectInput');
+		const selectedCoiNameEl = document.getElementById('selectedCoiName');
+		const coiDropdown = document.getElementById('coiDropdown');
+		const coiSearchInput = document.getElementById('coiSearchInput');
+		const coiOptionsContainer = document.getElementById('coiOptionsContainer');
+		const introduceSection = document.getElementById('introduce-section');
+		const introduceTitleEl = document.getElementById('introduce-title');
+		const introduceContentEl = document.getElementById('introduce-content');
+		const rulesSection = document.getElementById('rules-section');
+		const rulesTitleEl = document.getElementById('rules-title');
+		const rulesContentEl = document.getElementById('rules-content');
+		const infoModal = document.getElementById('infoModal');
+		const infoModalContentEl = document.getElementById('info-modal-content');
+		const alertDialog = document.getElementById('alertDialog');
+		const alertMcNameEl = document.getElementById('alert-mc-name');
+		const alertNicknameLabelEl = document.getElementById('alert-nickname-label');
+		const alertCoiLabelEl = document.getElementById('alert-coi-label');
+
+		function injectCss(css) {
+			if (!css) return;
+			const style = document.createElement('style');
+			style.innerHTML = css;
+			document.head.appendChild(style);
+		}
+
+		window.onload = function() {
+			STATE.token = Tools.getQueryParam('token') || '';
+			STATE.ecId = Tools.getQueryParam('id') || 0;
+			STATE.fromPage = Tools.getQueryParam('from') || '';
+
+			if (window.API) {
+				API.setToken(STATE.token);
+				if (Tools.getQueryParam('env') === 'mock') {
+					API.init({ useMock: true });
+					if (!STATE.ecId) STATE.ecId = 'mock_id';
+				}
+			}
+
+			loadCardConfig();
+			getCardDetail();
+			matchRsDetail();
+
+			coiSelectInput.addEventListener('click', toggleCoiDropdown);
+			coiSearchInput.addEventListener('input', filterCoiOptions);
+			document.addEventListener('click', (event) => {
+				if (!coiDropdown.contains(event.target) && !coiSelectInput.contains(event.target)) {
+					coiDropdown.classList.add('hidden');
+				}
+			});
+		};
+
+		function loadCardConfig() {
+			if (!window.API) return;
+			API.getCardConfig(STATE.ecId, 'signup').then(configRes => {
+				let cfg = configRes;
+				if (configRes && configRes.configJson) {
+					try { cfg = JSON.parse(configRes.configJson); } catch (e) { console.warn('config parse fail', e); }
+				}
+				if (!cfg) return;
+				if (cfg.common && cfg.common.css) injectCss(cfg.common.css);
+				const pageCfg = cfg.signup || cfg;
+				if (pageCfg && pageCfg.css) injectCss(pageCfg.css);
+				if (pageCfg && pageCfg.introduce) {
+					STATE.introduce = pageCfg.introduce;
+					introduceTitleEl.innerText = STATE.introduce.title || '';
+					introduceContentEl.innerHTML = STATE.introduce.content || '';
+					introduceSection.classList.remove('hidden');
+				}
+				if (pageCfg && pageCfg.activityRules) {
+					STATE.activityRules = pageCfg.activityRules;
+					rulesTitleEl.innerText = STATE.activityRules.title || '';
+					rulesContentEl.innerHTML = STATE.activityRules.content || '';
+					rulesSection.classList.remove('hidden');
+				}
+				if (pageCfg && pageCfg.param) STATE.configParam = Object.assign(STATE.configParam, pageCfg.param);
+				if (cfg.popupRuleList) STATE.popupRuleList = cfg.popupRuleList;
+				nickNameInput.placeholder = `请输入${STATE.configParam.labelName}`;
+				selectedCoiNameEl.innerText = `请选择${STATE.configParam.labelOrg}`;
+			});
+		}
+
+		function getCardDetail() {
+			if (!window.API) return;
+			API.getCardDetail(STATE.ecId).then(res => {
+				if (!res) return;
+				STATE.mcType = res.mcType;
+				STATE.mcId = res.mcId;
+				STATE.mcName = res.mcName;
+				STATE.beginSecond = res.beginSecond;
+				STATE.endSecond = res.endSecond;
+				STATE.coiId = res.coiId;
+				STATE.coiName = res.coiName;
+				STATE.nickName = res.nickName || '';
+				STATE.mcState = Tools.checkMcState(STATE.beginSecond, STATE.endSecond);
+				updateUI();
+				getOnlineMcSignUpDetail();
+			});
+		}
+
+		function matchRsDetail() {
+			if (!window.API) return;
+			API.getMatchRsDetail(STATE.ecId,0).then(() => {});
+		}
+
+		function getOnlineMcSignUpDetail() {
+			if (!window.API || !STATE.mcId) return;
+			API.getOnlineMcSignUpDetail(STATE.ecId, STATE.mcId).then(res => {
+				if (!res) return;
+				if (res.coiRs) {
+					STATE.coiOptions = res.coiRs.map(item => ({ text: item.coiName, value: item.coiId }));
+					populateCoiOptions(STATE.coiOptions);
+				}
+				if (!STATE.nickName && res.name) STATE.nickName = res.name;
+				nickNameInput.value = STATE.nickName;
+				if (STATE.coiId > 0) {
+					const selected = STATE.coiOptions.find(item => item.value == STATE.coiId);
+					if (selected) {
+						selectedCoiNameEl.innerText = selected.text;
+						STATE.coiName = selected.text;
+					}
+				}
+			});
+		}
+
+		function updateUI() {
+			mcNameEl.innerText = STATE.mcName;
+			subTitleEl.innerText = STATE.configParam.subTitle || Tools.fmtMcTime2(STATE.beginSecond, STATE.endSecond);
+			actTimeEl.innerText = Tools.getActtime(STATE.beginSecond, STATE.endSecond);
+			nickNameInput.value = STATE.nickName;
+			if (STATE.mcState === 2) {
+				signupBtn.classList.add('hidden');
+				signupBtnDisabled.classList.remove('hidden');
+			} else {
+				signupBtn.classList.remove('hidden');
+				signupBtnDisabled.classList.add('hidden');
+			}
+		}
+
+		function toggleCoiDropdown() {
+			coiDropdown.classList.toggle('hidden');
+			coiSearchInput.value = '';
+			populateCoiOptions(STATE.coiOptions);
+		}
+
+		function filterCoiOptions() {
+			const searchTerm = coiSearchInput.value.toLowerCase();
+			const filtered = STATE.coiOptions.filter(item => item.text.toLowerCase().includes(searchTerm));
+			populateCoiOptions(filtered);
+		}
+
+		function populateCoiOptions(options) {
+			coiOptionsContainer.innerHTML = '';
+			options.forEach(option => {
+				const div = document.createElement('div');
+				div.classList.add('e-select-item');
+				div.innerText = option.text;
+				div.dataset.value = option.value;
+				div.addEventListener('click', () => selectCoiOption(option));
+				coiOptionsContainer.appendChild(div);
+			});
+		}
+
+		function selectCoiOption(option) {
+			STATE.coiId = option.value;
+			STATE.coiName = option.text;
+			selectedCoiNameEl.innerText = option.text;
+			coiDropdown.classList.add('hidden');
+		}
+
+		function handleInfo() {
+			let contentHtml = '';
+			if (STATE.popupRuleList && STATE.popupRuleList.length > 0) {
+				STATE.popupRuleList.forEach(rule => {
+					if (rule.data && rule.data.title) contentHtml += `<h4 class="font-bold mt-2">${rule.data.title}</h4>`;
+					if (rule.data && rule.data.content) contentHtml += `<p>${rule.data.content}</p>`;
+				});
+			} else if (STATE.activityRules && STATE.activityRules.content) {
+				contentHtml = `<h4 class="font-bold">${STATE.activityRules.title}</h4><p>${STATE.activityRules.content}</p>`;
+			} else {
+				contentHtml = '<p>暂无说明信息。</p>';
+			}
+			infoModalContentEl.innerHTML = contentHtml;
+			infoModal.classList.remove('hidden');
+		}
+
+		function closeInfoModal() { infoModal.classList.add('hidden'); }
+
+		function handleBack() {
+			const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+			if (STATE.fromPage) {
+				Bridge.appAction(`./${STATE.fromPage}.html${qs}`);
+			} else {
+				Bridge.appAction('action://to_home/');
+			}
+		}
+
+		function handleSignup() {
+			STATE.nickName = nickNameInput.value.trim();
+			if (!STATE.nickName) {
+				Tools.showToast(`请输入${STATE.configParam.labelName}`);
+				return;
+			}
+			if (!STATE.coiId) {
+				Tools.showToast(`请选择${STATE.configParam.labelOrg}`);
+				return;
+			}
+			alertMcNameEl.innerText = STATE.mcName;
+			alertNicknameLabelEl.innerText = `${STATE.configParam.labelName}: ${STATE.nickName}`;
+			alertCoiLabelEl.innerText = `${STATE.configParam.labelOrg}: ${STATE.coiName}`;
+			alertDialog.classList.remove('hidden');
+		}
+
+		function confirmSignup() {
+			if (!window.API) return;
+			API.signUpOnline(STATE.mcId, STATE.coiId, 0, STATE.nickName).then(() => {
+				Tools.showToast('报名成功');
+				const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
+				Bridge.appAction(`./ranklist.html${qs}`);
+			}).catch(() => {
+				Tools.showToast('报名失败,请稍后重试');
+			});
+			closeAlertDialog();
+		}
+
+		function closeAlertDialog() { alertDialog.classList.add('hidden'); }
+	</script>
+</body>
+</html>

BIN
card/newCards/S3/static/backgroud/top_bg_egg2.png


BIN
card/newCards/S3/static/backgroud/top_bg_sddx.png


BIN
card/newCards/S3/static/common/notice.png


BIN
card/newCards/S3/static/logo/jbs.png


BIN
card/newCards/S3/static/logo/sddx.png


+ 36 - 1
card/newCards/nanning/api.js

@@ -105,7 +105,42 @@
             ],
             teamRankRs: [],
             inTeamRs: [],
-            otherRs: []
+            otherRs: [],
+            // Added for ranklist.html mapping
+            teamCpRs: [
+                { teamName: 'Mock飞虎队', inRankNum: 500, rankNum: 1, isSelf: true },
+                { teamName: 'Mock火箭队', inRankNum: 450, rankNum: 2, isSelf: false },
+                { teamName: 'Mock摸鱼队', inRankNum: 400, rankNum: 3, isSelf: false }
+            ],
+            teamDistanceRs: [
+                { teamName: 'Mock火箭队', inRankNum: 120000, rankNum: 1, isSelf: false },
+                { teamName: 'Mock飞虎队', inRankNum: 115000, rankNum: 2, isSelf: true }
+            ],
+            teamRightAnswerPerRs: [
+                { teamName: 'Mock学霸队', inRankNum: 98, rankNum: 1, isSelf: false },
+                { teamName: 'Mock飞虎队', inRankNum: 95, rankNum: 2, isSelf: true }
+            ],
+            teamTodayPaceRs: [
+                { teamName: 'Mock闪电队', inRankNum: 300, rankNum: 1, isSelf: false }, // 5'00"
+                { teamName: 'Mock飞虎队', inRankNum: 330, rankNum: 2, isSelf: true }  // 5'30"
+            ],
+            regionCpRs: [
+                { userName: 'Mock个人A', inRankNum: 50, headUrl: 'https://picsum.photos/40/40?random=6', rankNum: 1, isSelf: false },
+                { userName: 'Mock个人B', inRankNum: 45, headUrl: 'https://picsum.photos/40/40?random=7', rankNum: 2, isSelf: false },
+                { userName: 'Mock我', inRankNum: 40, headUrl: 'https://picsum.photos/40/40?random=8', rankNum: 3, isSelf: true }
+            ],
+            regionDistanceRs: [
+                { userName: 'Mock跑神', inRankNum: 21000, headUrl: 'https://picsum.photos/40/40?random=9', rankNum: 1, isSelf: false },
+                { userName: 'Mock我', inRankNum: 15000, headUrl: 'https://picsum.photos/40/40?random=10', rankNum: 2, isSelf: true }
+            ],
+            regionRightAnswerPerRs: [
+                { userName: 'Mock智多星', inRankNum: 100, headUrl: 'https://picsum.photos/40/40?random=11', rankNum: 1, isSelf: false },
+                { userName: 'Mock我', inRankNum: 90, headUrl: 'https://picsum.photos/40/40?random=12', rankNum: 2, isSelf: true }
+            ],
+            regionTodayPaceRs: [
+                { userName: 'Mock飞人', inRankNum: 240, headUrl: 'https://picsum.photos/40/40?random=13', rankNum: 1, isSelf: false },
+                { userName: 'Mock我', inRankNum: 360, headUrl: 'https://picsum.photos/40/40?random=14', rankNum: 2, isSelf: true }
+            ]
         },
         'UserCurrentRankNumQuery': { rankNum: 5 },
         'UserJoinCardQuery': { isJoin: false }, // 默认未报名

+ 204 - 169
card/newCards/nanning/ranklist.html

@@ -260,75 +260,118 @@
         // --- 核心状态 ---
         let currentTab = 'team';
         let currentMetric = 'score';
+        
+        // --- 全局数据存储 ---
+        let API_DATA = {}; // 存储 API 返回的完整对象
+        let CURRENT_RANK_LIST = []; // 当前显示的列表
 
-        // --- 模拟数据 ---
-        let MOCK_DATA = {
+        // --- 字段映射配置 (参考 rankList.vue) ---
+        // 对应 dispArrStr: "teamCp,teamDistance,teamRightAnswerPer,teamTodayPace,regionCp,regionDistance,regionRightAnswerPer,regionTodayPace"
+        const DATA_MAPPING = {
             team: {
-                score: [
-                    { name: '飞虎队', sub: '兴隆山', val: '98,450', unit: '分' },
-                    { name: '火箭队', sub: '中心校区', val: '85,120', unit: '分' },
-                    { name: '摸鱼队', sub: '快乐组', val: '76,300', unit: '分' },
-                    { name: '汪汪队', sub: '教职工', val: '45,000', unit: '分' },
-                    { name: '喵喵队', sub: '学生会', val: '41,200', unit: '分' },
-                ],
+                score: { key: 'teamCpRs', unit: '分', label: '积分' },     // 对应 rankList.vue tab1Current=0, index=0 (rankType=totalScore) -> teamCpRs
+                mileage: { key: 'teamDistanceRs', unit: 'km', label: '里程' }, // index=2 (rankType=totalDistance) -> teamDistanceRs
+                accuracy: { key: 'teamRightAnswerPerRs', unit: '%', label: '正确率' }, // index=3 -> teamRightAnswerPerRs
+                count: { key: 'teamCpRs', unit: '个', label: '打点' },       // 暂复用 teamCpRs 或 teamTodayCpRs? 这里沿用 score 的源,但在 Vue 里是分开的 Tab
+                lap: { key: 'teamTodayPaceRs', unit: '/km', label: '配速' }   // index=4 -> teamTodayPaceRs
             },
             individual: {
-                score: [
-                    { name: '风一样的男子', sub: '火箭队', val: '3,450', unit: '分', avatar: 33 },
-                    { name: '爱吃西瓜', sub: '摸鱼队', val: '2,980', unit: '分', avatar: 47 },
-                    { name: '晨跑小将', sub: '汪汪队', val: '2,100', unit: '分', avatar: 11 },
-                    { name: '路人甲', sub: '飞虎队', val: '1,800', unit: '分', avatar: 5 },
-                    { name: '学霸李', sub: '火箭队', val: '1,750', unit: '分', avatar: 8 },
-                    // 我在第 45 名
-                    { name: '奔跑的蜗牛', sub: '飞虎队', val: '120', unit: '分', avatar: 12, isMe: true, rank: 45 },
-                    { name: '追风者', sub: '火箭队', val: '110', unit: '分', avatar: 20 },
-                ]
+                score: { key: 'regionCpRs', unit: '分', label: '积分' },    // tab1Current=1, index=0 -> regionCpRs
+                mileage: { key: 'regionDistanceRs', unit: 'km', label: '里程' },
+                accuracy: { key: 'regionRightAnswerPerRs', unit: '%', label: '正确率' },
+                count: { key: 'regionCpRs', unit: '个', label: '打点' },
+                lap: { key: 'regionTodayPaceRs', unit: '/km', label: '配速' }
             }
         };
-        // 补全数据结构
-        ['mileage', 'accuracy', 'count', 'lap'].forEach(key => {
-            MOCK_DATA.team[key] = MOCK_DATA.team.score; 
-            MOCK_DATA.individual[key] = MOCK_DATA.individual.score;
-        });
 
-        // --- 渲染逻辑 (修改了Padding和Avatar大小以变窄) ---
+        // --- 格式化工具 ---
+        const Formatters = {
+            distance: (val) => {
+                if (!val) return '0';
+                if (val < 10000) return (Math.round(val * 100 / 1000) / 100).toFixed(2);
+                return Math.round(val / 1000).toString();
+            },
+            pace: (val) => {
+                if (!val) return "--'--\"";
+                const m = Math.floor(val / 60);
+                const s = Math.floor(val % 60);
+                return `${m}'${s < 10 ? '0' + s : s}"`;
+            },
+            default: (val) => val || '0'
+        };
+
+        // --- 渲染逻辑 ---
         function renderLeaderboard() {
             const container = document.getElementById('leaderboard-container');
-            const fullList = MOCK_DATA[currentTab][currentMetric];
+            container.innerHTML = ''; // Clear existing
+
+            // 1. 获取当前配置
+            const config = DATA_MAPPING[currentTab][currentMetric];
+            if (!config) return;
+
+            // 2. 获取数据列表
+            const rawList = API_DATA[config.key] || [];
+            
+            // 3. 转换数据结构
+            // Vue 组件 item: { rankNum, isSelf, userName, inRankNum, headUrl, ... }
+            const fullList = rawList.map(item => {
+                let valDisplay = item.inRankNum;
+                
+                // 根据 Metric 类型进行格式化
+                if (currentMetric === 'mileage') valDisplay = Formatters.distance(item.inRankNum);
+                else if (currentMetric === 'lap') valDisplay = Formatters.pace(item.inRankNum);
+                
+                return {
+                    name: item.userName || item.teamName || '匿名', // 兼容不同字段
+                    sub: item.additionalName || '', // 副标题
+                    val: valDisplay,
+                    unit: config.unit,
+                    avatar: item.headUrl, // 个人头像
+                    rank: item.rankNum,
+                    isMe: item.isSelf === 1 || item.isSelf === true, // 兼容
+                    raw: item
+                };
+            });
+
+            if (fullList.length === 0) {
+                container.innerHTML = '<div class="text-center text-gray-400 py-10 text-sm">暂无排名数据</div>';
+                return;
+            }
             
             const renderItem = (item, visualIndex, isRealRank = false) => {
-                const rank = (isRealRank && item.rank) ? item.rank : (visualIndex + 1);
-                const isTop3 = visualIndex < 3; 
+                const rank = (isRealRank && item.rank) ? item.rank : (item.rank || visualIndex + 1);
+                const isTop3 = rank <= 3; // 使用真实排名判断样式
 
-                // 图标 - 稍微调小以适应更窄的行 (w-8)
+                // 图标
                 let rankIconHtml = '';
-                if (isTop3 && currentTab === 'team') {
-                    const colors = ['text-yellow-400', 'text-gray-400', 'text-orange-600'];
-                    rankIconHtml = `<div class="w-8 flex justify-center shrink-0 mr-1"><i class="fas fa-trophy ${colors[visualIndex]} text-lg drop-shadow-sm"></i></div>`;
-                } else if (isTop3 && currentTab === 'individual') {
+                if (isTop3) {
                     const colors = ['text-yellow-400', 'text-gray-400', 'text-orange-600'];
-                    rankIconHtml = `<div class="w-8 flex justify-center shrink-0 mr-1"><i class="fas fa-medal ${colors[visualIndex]} text-xl drop-shadow-sm"></i></div>`;
+                    const colorClass = colors[rank - 1] || 'text-gray-400'; // 安全回退
+                    const iconClass = currentTab === 'team' ? 'fa-trophy' : 'fa-medal';
+                    rankIconHtml = `<div class="w-8 flex justify-center shrink-0 mr-1"><i class="fas ${iconClass} ${colorClass} text-lg drop-shadow-sm"></i></div>`;
                 } else {
                     rankIconHtml = `<div class="w-8 text-center font-bold text-gray-400 text-sm mr-1">${rank}</div>`;
                 }
+                
+                // “我”的特殊排位样式
                 if (item.isMe) {
                     rankIconHtml = `<div class="w-8 text-center font-black text-primary text-lg italic mr-1">${rank}</div>`;
                 }
 
-                // 头像 - 调小 (w-9 h-9)
+                // 头像
                 let avatarHtml = '';
                 if (currentTab === 'team') {
                     avatarHtml = `<div class="w-9 h-9 bg-blue-50 rounded-full mr-3 flex items-center justify-center shrink-0 text-primary"><i class="fas fa-user-friends text-base"></i></div>`;
                 } else {
-                    avatarHtml = `<img src="https://i.pravatar.cc/100?img=${item.avatar}" class="w-9 h-9 rounded-full mr-3 border-2 ${isTop3 ? 'border-yellow-400' : 'border-transparent'} shrink-0">`;
+                    const avatarSrc = item.avatar || 'https://i.pravatar.cc/100?img=1'; // 默认头像
+                    avatarHtml = `<img src="${avatarSrc}" class="w-9 h-9 rounded-full mr-3 border-2 ${isTop3 ? 'border-yellow-400' : 'border-transparent'} shrink-0 object-cover">`;
                 }
 
-                // 容器 - 减少 Padding (py-2 px-3)
+                // 容器样式
                 let containerClass = "bg-white rounded-xl py-2 px-3 flex items-center shadow-sm border border-gray-100 relative fade-in-up";
                 let nameClass = "font-bold text-gray-800 text-sm";
-                let valClass = "font-bold text-gray-600 font-mono text-base"; // 稍微减小字号适配窄行
+                let valClass = "font-bold text-gray-600 font-mono text-base";
 
-                // “我”的高亮样式
                 if (item.isMe) {
                     containerClass = "bg-blue-50 rounded-xl py-2 px-3 flex items-center shadow-md border-2 border-primary/30 relative overflow-hidden transform scale-[1.02] fade-in-up z-10 my-2";
                     nameClass = "font-bold text-primary text-sm";
@@ -339,46 +382,32 @@
 
                 const meBadge = item.isMe ? `<div class="absolute right-0 top-0 bg-primary text-white text-[8px] px-1.5 py-0.5 rounded-bl-lg">我</div>` : '';
 
-                return `
-                    <div class="${containerClass}" style="animation-delay: ${visualIndex * 50}ms">
+                return 
+                    `<div class="${containerClass}" style="animation-delay: ${visualIndex * 50}ms">
                         ${meBadge}
                         ${rankIconHtml}
                         ${avatarHtml}
                         <div class="flex-1 min-w-0">
                             <h4 class="${nameClass} truncate">${item.name}</h4>
-                            <p class="text-[10px] text-gray-400">${item.sub}</p>
+                            <p class="text-[10px] text-gray-400 truncate">${item.sub}</p>
                         </div>
                         <div class="text-right">
                             <div class="${valClass}">${item.val}</div>
                             <div class="text-[10px] text-gray-400">${item.unit}</div>
                         </div>
-                    </div>
-                `;
+                    </div>`;
             };
 
             let html = '';
             
-            // 1. 前三名
-            const top3 = fullList.slice(0, 3);
-            top3.forEach((item, index) => {
+            // 逻辑:前3名 + (如果我在后面则插队) + 剩余
+            // 这里简化处理,直接渲染列表,如果 "我" 在列表中会自动高亮。
+            // 如果 API 返回的数据没有包含 "我" (比如分页了),则需要单独处理 getUserCurrentRank 接口,这里暂且假设 rankRs 包含了 top N。
+            
+            fullList.forEach((item, index) => {
                 html += renderItem(item, index);
             });
 
-            // 2. 如果我在三名以外,插队显示在第四位
-            if (currentTab === 'individual') {
-                const me = fullList.find(x => x.isMe);
-                if (me && (!me.rank || me.rank > 3)) {
-                     html += renderItem(me, 3, true);
-                }
-            }
-
-            // 3. 渲染剩余 (从列表的第4个开始,即索引3)
-            const others = fullList.slice(3);
-            others.forEach((item, index) => {
-                if (item.isMe && currentTab === 'individual') return;
-                html += renderItem(item, index + 3); 
-            });
-
             container.innerHTML = html;
         }
 
@@ -402,15 +431,10 @@
 
         function saveProfile() {
             const newName = editNameInput.value;
-            const newTeam = selectedTeamText.innerText.trim();
+            // 这里的 teamName 逻辑仅仅是 UI 演示,实际修改需要调用 API (e.g., signUpOnline)
+            // 目前仅更新 UI
             document.getElementById('profileName').innerText = newName;
-            document.getElementById('profileTeam').innerText = newTeam;
-            
-            ['score', 'mileage', 'accuracy', 'count', 'lap'].forEach(key => {
-                const me = MOCK_DATA.individual[key].find(i => i.isMe);
-                if(me) { me.name = newName; me.sub = newTeam; }
-            });
-            renderLeaderboard();
+            // TODO: Call API to update profile if needed
             closeEditModal();
         }
 
@@ -465,15 +489,12 @@
         function switchMetric(metric) {
             currentMetric = metric;
             document.querySelectorAll('.metric-btn').forEach(btn => {
-                if (btn.id === 'metric-score') {
-                     btn.className = 'metric-btn bg-white text-gray-500 border border-gray-200 px-3 py-1 rounded-full text-xs font-bold shrink-0 active:bg-gray-50 relative overflow-visible transition-colors';
+                if (btn.id === `metric-${metric}`) {
+                     btn.className = 'metric-btn bg-primary text-white px-3 py-1 rounded-full text-xs shadow-md shadow-blue-100 font-bold shrink-0 relative overflow-visible transition-colors';
                 } else {
                      btn.className = 'metric-btn bg-white text-gray-500 border border-gray-200 px-3 py-1 rounded-full text-xs font-bold shrink-0 active:bg-gray-50 transition-colors';
                 }
             });
-
-            const activeBtn = document.getElementById(`metric-${metric}`);
-            activeBtn.className = 'metric-btn bg-primary text-white px-3 py-1 rounded-full text-xs shadow-md shadow-blue-100 font-bold shrink-0 relative overflow-visible transition-colors';
             
             renderLeaderboard();
         }
@@ -494,8 +515,6 @@
         }
 
         window.onload = function() {
-            renderLeaderboard();
-
             // Helper function to get URL query parameters (local to this script)
             function getQueryParam(name) {
                 const params = new URLSearchParams(window.location.search);
@@ -527,7 +546,20 @@
 
             // Get external parameters
             const token = getQueryParam('token');
-            const ecId = getQueryParam('id'); // Use 'id' from URL as ecId
+            let ecId = getQueryParam('id'); // Use 'id' from URL as ecId
+            const env = getQueryParam('env');
+
+            // Initialize Mock if needed
+            if (env === 'mock' && window.API) {
+                console.log('Initializing API in Mock mode...');
+                window.API.init({ useMock: true });
+                
+                // Fallback ecId for mock mode if missing
+                if (!ecId) {
+                    ecId = 'mock_default_id';
+                    console.warn('No "id" parameter found. Using default mock ID:', ecId);
+                }
+            }
 
             // State for Marquee Data
             let marqueeData = {
@@ -550,107 +582,110 @@
                 window.API.setToken(token);
                 console.log('API Token set:', token);
             } else {
-                console.warn('Token not found in URL or API.setToken not available.');
+                if (env !== 'mock') console.warn('Token not found in URL or API.setToken not available.');
             }
 
-            // Call API with ecId and ocaId (using a dummy ocaId for now)
+            // Call API with ecId
             if (window.API && ecId) {
-                // Assuming ocaId is also passed, or a default value for demonstration
-                // For demonstration, using a static ocaId=1. In a real scenario, this might also come from URL or other logic.
-                // const ocaId = 1; // Not needed for getCardDetail
-
-                API.getCardDetail(ecId).then(res => {
-                    console.log('API Response for ecId=' + ecId + ' (getCardDetail):', res);
-
-                    // Update nickname and team from API response
-                    const profileNameElement = document.getElementById('profileName');
-                    const profileTeamElement = document.getElementById('profileTeam');
-
-                    if (profileNameElement && res.nickName) { // Use nickName as per user clarification
-                        profileNameElement.innerText = res.nickName;
-                    }
-                    if (profileTeamElement && res.coiName) {
-                        profileTeamElement.innerText = res.coiName;
+                console.log('Starting Analysis-Based Sequence...');
+
+                // 0. Global State for selections
+                window.CURRENT_SELECTION = {
+                    mcId: 0,
+                    mcType: 0,
+                    ocaId: 0, // Current selected map ID (for individual)
+                    mapList: [] // Store map options
+                };
+                
+                // 1. Match Details (Core)
+                API.getMatchRsDetail(ecId).then(matchRes => {
+                    console.log('API Response (getMatchRsDetail):', matchRes);
+                    if (!matchRes) return;
+
+                    const mcId = matchRes.mcId;
+                    const mcType = matchRes.mcType;
+                    window.CURRENT_SELECTION.mcId = mcId;
+                    window.CURRENT_SELECTION.mcType = mcType;
+
+                    // Update UI Info
+                    if (matchRes.nickName) document.getElementById('profileName').innerText = matchRes.nickName;
+                    if (matchRes.totalSysPoint !== undefined) document.getElementById('currentScoreDisplay').innerText = matchRes.totalSysPoint;
+                    if (matchRes.endSecond !== undefined) {
+                        marqueeData.remainingTimeStr = formatRemainingTime(matchRes.endSecond);
+                        updateMarquee();
                     }
 
-                    // Store mcId and ocaId in local storage, differentiated by ecId
-                    if (res.mcId !== undefined && res.ocaId !== undefined && ecId) {
-                        const storageKey = `cardData_${ecId}`;
-                        const dataToStore = {
-                            mcId: res.mcId,
-                            ocaId: res.ocaId
-                        };
-                        try {
-                            localStorage.setItem(storageKey, JSON.stringify(dataToStore));
-                            console.log(`Stored mcId and ocaId for ecId ${ecId}:`, dataToStore);
-                        } catch (e) {
-                            console.error('Failed to save to localStorage:', e);
+                    // 2. Parallel Calls: MapList & Statistics
+                    const p1 = API.getMapList(mcId).then(mapRes => {
+                        console.log('API Response (getMapList):', mapRes);
+                        if (mapRes && mapRes.length > 0) {
+                            window.CURRENT_SELECTION.mapList = mapRes;
+                            // Default ocaId to the first map
+                            window.CURRENT_SELECTION.ocaId = mapRes[0].ocaId;
+                            
+                            // Update Dropdown/Tab UI if needed (Simulated here)
+                            // In a real app, you'd populate the 'individual' tab dropdown here
+                            console.log('Default ocaId set to:', window.CURRENT_SELECTION.ocaId);
                         }
-                    } else {
-                        console.warn('mcId, ocaId, or ecId missing from API response or URL. Not storing to localStorage.');
-                    }
-
-                    // Call API.getMatchRsDetail if mcId and ocaId are available
-                    if (res.mcId !== undefined && res.ocaId !== undefined) {
-                         
-                        API.getMatchRsDetail(ecId, res.ocaId).then(matchRes => {
-                             console.log('API Response for ecId=' + ecId + ', ocaId=' + res.ocaId + ' (getMatchRsDetail):', matchRes);
-
-                            const currentScoreDisplayElement = document.getElementById('currentScoreDisplay');
-                            if (currentScoreDisplayElement && matchRes && matchRes.totalSysPoint !== undefined) {
-                                currentScoreDisplayElement.innerText = matchRes.totalSysPoint;
-                                console.log('Updated current score with totalSysPoint:', matchRes.totalSysPoint);
-                            } else {
-                                console.warn('totalSysPoint not found in getMatchRsDetail response or element not found.');
-                            }
-
-                            // Update endSecond for marquee
-                            if (matchRes.endSecond !== undefined) {
-                                marqueeData.remainingTimeStr = formatRemainingTime(matchRes.endSecond);
-                                updateMarquee();
-                            }
-
-                        }).catch(matchErr => {
-                             console.error('API Error (getMatchRsDetail):', matchErr);
-                        });
-
-                        // New: Call API.getCompStatistic
-                        if (res.mcId !== undefined) { // Check if mcId is available from getCardDetail response
-                            API.getCompStatistic(res.mcId).then(compStatsRes => {
-                                console.log('API Response for mcId=' + res.mcId + ' (getCompStatistic):', compStatsRes);
-
-                                if (compStatsRes) {
-                                    if (compStatsRes.totalDistance !== undefined) {
-                                        marqueeData.totalDistanceKm = Math.round(compStatsRes.totalDistance / 1000);
-                                    }
-                                    if (compStatsRes.totalAnswerNum !== undefined) {
-                                        marqueeData.totalAnswerNum = compStatsRes.totalAnswerNum;
-                                    }
-                                    if (compStatsRes.totalCp !== undefined) {
-                                        marqueeData.totalCp = compStatsRes.totalCp;
-                                    }
-                                    
-                                    updateMarquee();
-                                    console.log('Updated marquee text with CompStatistic data.');
-                                } else {
-                                    console.warn('CompStatistic data not found.');
-                                }
-                            }).catch(compStatsErr => {
-                                console.error('API Error (getCompStatistic):', compStatsErr);
-                            });
-                        } else {
-                            console.warn('mcId not available for API.getCompStatistic call.');
+                    }).catch(e => console.error('MapList Error:', e));
+
+                    const p2 = API.getCompStatistic(mcId).then(compStatsRes => {
+                        if (compStatsRes) {
+                            if (compStatsRes.totalDistance !== undefined) marqueeData.totalDistanceKm = Math.round(compStatsRes.totalDistance / 1000);
+                            if (compStatsRes.totalAnswerNum !== undefined) marqueeData.totalAnswerNum = compStatsRes.totalAnswerNum;
+                            if (compStatsRes.totalCp !== undefined) marqueeData.totalCp = compStatsRes.totalCp;
+                            updateMarquee();
                         }
-                    }
+                    }).catch(e => console.warn('CompStat Error:', e));
+
+                    // 3. Initial Rank Load (Wait for MapList to ensure ocaId is ready for Individual tab if that's default, 
+                    // but usually Team tab is default which might not need specific ocaId or uses global context)
+                    Promise.all([p1, p2]).then(() => {
+                        loadRankData();
+                    });
+
+                }).catch(err => console.error('MatchRsDetail Error:', err));
 
-                }).catch(err => {
-                    console.error('API Error:', err);
-                });
             } else if (!ecId) {
-                console.warn('URL parameter "id" (ecId) not found. API call skipped.');
-            } else {
-                console.warn('API not loaded. API call skipped.');
+                console.warn('URL parameter "id" (ecId) not found.');
             }
+
+            // Internal function to load rank data based on current state
+            window.loadRankData = function() {
+                const { mcId, mcType, ocaId } = window.CURRENT_SELECTION;
+                // Determine effective ocaId based on current tab
+                // If Team tab, usually we query global or specific team-level data. 
+                // If Individual tab, we MUST use the selected ocaId (mapId).
+                // Note: The Vue component sends 'ocaId' in apiCardRankDetailQuery.
+                // When in Team mode (tab1Current=0), it might still pass the ocaId if the team rank is also segregated by map, 
+                // OR it might rely on the backend ignoring it for team ranks.
+                // Based on analysis: "onSelectChange" updates ocaId and calls query.
+                
+                let effectiveOcaId = ocaId;
+                // If needed: if (currentTab === 'team') effectiveOcaId = 0; // Uncomment if team rank is global
+
+                const dispArrStr = "teamCp,teamTodayCp,teamDistance,teamRightAnswerPer,teamTodayPace,regionCp,regionTodayCp,regionDistance,regionRightAnswerPer,regionTodayPace";
+                
+                console.log(`Fetching Rank: mcId=${mcId}, type=${mcType}, ocaId=${effectiveOcaId}`);
+                
+                API.getRankDetail(mcId, mcType, effectiveOcaId, dispArrStr).then(rankRes => {
+                    console.log('API Response (getRankDetail):', rankRes);
+                    if (rankRes) {
+                        API_DATA = rankRes;
+                        renderLeaderboard();
+                    } else {
+                         document.getElementById('leaderboard-container').innerHTML = '<div class="text-center text-gray-400 py-10 text-sm">暂无排名数据</div>';
+                    }
+                }).catch(e => console.error('RankDetail Error:', e));
+            };
+
+            // Hook into tab switching
+            const originalSwitchMainTab = window.switchMainTab;
+            window.switchMainTab = function(type) {
+                originalSwitchMainTab(type); // Update UI state
+                // Re-load data (ocaId stays same or logic inside loadRankData handles it)
+                loadRankData();
+            };
         };
     </script>
 </body>

+ 0 - 29
card/pages/tpl/style3/new/AGENTS.md

@@ -1,29 +0,0 @@
-# Repository Guidelines
-
-## 项目结构与模块组织
-- 核心库:`bridge.js` 负责原生桥接,`api.js` 封装服务器请求;每个页面都需优先加载。`mock_flutter.js` 仅限桌面调试使用。
-- 根目录的 `index.html` 与 `demo.html` 展示接入方式;接口文档位于 `API.md`、`API_SERVER.md`。
-- `demo_project/` 提供完整示例流程(`index.html`、`rank.html`、`signup.html`、`detail.html`),复用同一 SDK;新增页面可按其目录布局,页面私有资源放在对应 HTML 同目录。
-
-## 构建、测试与开发命令
-- 静态项目;直接在浏览器打开 `index.html` 或 `demo_project/index.html` 即可快速查看。
-- 推荐本地起服务避免 CORS:`python -m http.server 8000`,访问 `http://localhost:8000/demo_project/index.html`。
-- 交付前:移除 `<script src="./mock_flutter.js"></script>`,并在 API 初始化时填入真实 token 后再打包压缩。
-
-## 编码风格与命名约定
-- JavaScript 保持 ES5 兼容:使用 IIFE、`var`、4 空格缩进、分号,文件顶部写 `'use strict'`。
-- 函数/变量用 lowerCamelCase,常量用 UPPER_SNAKE_CASE,HTML/CSS 类名用 kebab-case。
-- 网络与桥接工具函数集中在现有文件内;未经约定不要引入打包器或模块加载器。
-
-## 测试指南
-- 以手动验证为主:开启 `mock_flutter.js` 先跑 Mock 流程,再关闭它并连真实 App Bridge/API 复测。
-- 重点检查 token 处理(URL 参数与 `Bridge.getToken()` 兜底)、`demo_project/rank.html` 与 `signup.html` 的排行榜/报名流程、以及通过 `API.getOssUrl` 拼接的 OSS 链接。
-- 如新增自动化,请在文档中写明运行方式,并与上述手动步骤并行保留。
-
-## 提交与 Pull Request 指南
-- 当前无历史记录,建议沿用 Conventional Commit 前缀(`feat:`、`fix:`、`chore:`),范围简洁。
-- PR 内容需包含:摘要、影响的页面/文件、手动测试记录(浏览器 + mock/真实环境)、UI 变更的截图或录屏,以及关联的文档更新。
-
-## 安全与配置提示
-- 不要在控制台或仓库中暴露 token 和用户数据;本地服务时优先用环境变量注入配置。
-- 交付前确认 `Config.useMock` 设为 `false`,并检查生产接口地址配置正确。

+ 0 - 148
card/pages/tpl/style3/new/API.md

@@ -1,148 +0,0 @@
-# ColorMapRun H5 交互 SDK 开发文档 (Bridge)
-
-本文档描述了 H5 页面如何与彩图奔跑 App (Flutter) 进行交互。所有的交互方法都封装在 `bridge.js` 中。
-
-## 1. 快速开始
-
-### 1.1 文件引入
-
-在您的 `index.html` 或项目入口文件中,引入 `bridge.js`。
-
-```html
-<!-- 引入通信桥梁 -->
-<script src="./bridge.js"></script>
-```
-
-**注意**:本 SDK 内部会自动处理兼容性(优先检测 `uni.webView`,自动降级适配旧版 App URL 拦截),因此您**不需要**关心 App 是新版还是旧版,直接调用 API 即可。
-
-### 1.2 本地调试 (Mock)
-
-在开发阶段(浏览器环境),App 环境不可用。请在 `<head>` 中引入 `mock_flutter.js` 来模拟 App 行为。
-
-```html
-<head>
-    <!-- ... 其他标签 ... -->
-    
-    <!-- 【仅开发环境引入】 模拟 App 通信环境 -->
-    <script src="./mock_flutter.js"></script>
-    
-    <script src="./bridge.js"></script>
-</head>
-```
-
-**⚠️ 重要**:在打包交付给 App 端集成前,**请务必注释或删除对 `mock_flutter.js` 的引用**。
-
----
-
-## 2. API 列表
-
-所有的 API 都挂载在全局对象 `window.Bridge` 上。
-
-### 2.1 基础导航
-
-#### 返回上一页
-通知 App 用户点击了返回按钮 (相当于浏览器的后退)。
-```javascript
-Bridge.back();
-```
-
-#### 返回 App 首页
-强制关闭当前 Webview,返回 App 的原生首页。
-```javascript
-Bridge.toHome();
-```
-
-#### 跳转登录
-当 API 返回 401 未授权时,调用此方法唤起 App 原生登录页。
-```javascript
-Bridge.toLogin();
-```
-
-#### 设置标题
-修改 App 顶部导航栏的标题。
-```javascript
-Bridge.setTitle('我的页面标题');
-```
-
-### 2.2 业务跳转
-
-#### 打开地图导航
-调起 App 原生导航(支持高德、百度、腾讯地图)。
-```javascript
-/**
- * @param {number} latitude 纬度
- * @param {number} longitude 经度
- * @param {string} name 地点名称
- */
-Bridge.openMap(36.666, 117.123, '济南奥体中心');
-```
-
-#### 打开比赛详情 (原生页)
-跳转到 App 内的原生比赛详情页。
-```javascript
-/**
- * @param {string|number} id 活动ID (mcId)
- * @param {string} type 活动类型 (默认 1)
- */
-Bridge.openMatch(101, 1);
-```
-
-#### 打开活动列表 (原生页)
-跳转到 App 内的原生多地图/活动列表页。
-```javascript
-/**
- * @param {string|number} id 关联ID (mapId)
- * @param {string} mapName 地图名称
- */
-Bridge.openActivityList(202, '奥体公园定向');
-```
-
-### 2.3 功能调用
-
-#### 微信分享
-调起 App 的微信分享面板。
-```javascript
-Bridge.shareWx({
-    title: '快来参加彩图奔跑!',
-    url: 'https://colormaprun.com/activity/123',
-    image: 'https://colormaprun.com/logo.png',
-    scene: 'session' // session: 好友, timeline: 朋友圈
-});
-```
-
-#### 跳转微信小程序
-```javascript
-Bridge.launchWxMini('gh_bea09156da8d', 'pages/index/index');
-```
-
-#### 保存图片
-将 Base64 格式的图片保存到手机相册。
-```javascript
-Bridge.saveImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...');
-```
-
-### 2.4 获取数据
-
-#### 获取用户 Token (异步)
-**注意**: 通常 H5 页面加载时 URL 会携带 Token。此方法主要用于特殊情况下的主动获取。
-
-```javascript
-// 1. 注册回调
-Bridge.onToken(function(token) {
-    console.log('收到 Token:', token);
-    // 使用 Token 发起 API 请求...
-});
-
-// 2. 发起获取请求
-Bridge.getToken();
-```
-
----
-
-## 3. 常见问题
-
-**Q: 为什么 `Bridge.back()` 没有反应?**
-A: 请检查是否在浏览器环境中。如果在浏览器中,确保引入了 `mock_flutter.js`。如果在 App 中,请确认 App 的 WebView 能够响应 `javascript:history.back()` 或 `uni.postMessage`。
-
-**Q: 怎么判断当前是在 App 内?**
-A: 推荐通过 UserAgent 判断,或者尝试调用 `Bridge.getToken()` 看是否有回调。但在使用本 SDK 时,建议假设处于 App 环境编写代码,并在本地开启 Mock 进行调试。

+ 0 - 556
card/pages/tpl/style3/new/API_SERVER.md

@@ -1,556 +0,0 @@
-# ColorMapRun 后端 API 接口文档
-
-本文档描述了 H5 页面需要调用的后端接口。
-
-## 1. 基础说明
-
-*   **接口域名**:
-    *   开发环境: `https://t-mapi.colormaprun.com/api/card/`
-    *   生产环境: `https://colormaprun.com/api/card/`
-*   **OSS 资源域名**: `http://oss-card.colormaprun.com/card/`
-*   **请求方式**: `POST`
-*   **Content-Type**: `application/x-www-form-urlencoded`
-*   **鉴权**: 所有接口需要在 Header 中携带 `token`。
-
-### 1.1 请求头 (Headers)
-
-| 参数名 | 必选 | 说明 | 示例 |
-| :--- | :--- | :--- | :--- |
-| `Content-Type` | 是 | 固定值 | `application/x-www-form-urlencoded` |
-| `token` | 是 | 用户身份令牌 | `96ba3c924394934f7d30fa869a94ce0d` |
-
-### 1.2 响应结构 (Response)
-
-所有接口返回 JSON 格式数据:
-
-```json
-{
-  "code": 0,          // 0 表示成功,非 0 表示失败
-  "message": "成功",   // 提示信息
-  "data": { ... }     // 业务数据,下文中的“返回数据”均指此 data 字段的内容
-}
-```
-
-## 2. API 封装方法 (调用 `window.API` 对象)
-
-### 2.1 获取 OSS 资源基础地址
-
-*   **API 方法**: `API.getOssUrl()`
-*   **说明**: 获取图片、视频等静态资源的访问基础 URL。第三方开发者可自行拼接完整资源路径。
-*   **参数**: 无
-*   **返回**: `string` (OSS 基础 URL)
-
----
-
-### 2.2 接口列表
-
-#### 1. 卡片与配置
-
-*   **1.1 卡片基本信息查询**
-    *   **API 方法**: `API.getCardBase(ecId, pageName)`
-    *   **参数**: `ecId` (int), `pageName` (string)
-    *   **返回数据**:
-        ```json
-        {
-            "ecName": "活动名称",
-            "ecDesc": "活动描述",
-            "beginSecond": 1700000000, // 开始时间戳
-            "endSecond": 1700090000,   // 结束时间戳
-            "secondCardName": "跳转页面名称"
-        }
-        ```
-
-*   **1.2 卡片通用配置查询**
-    *   **API 方法**: `API.getCardConfig(ecId, pageName)`
-    *   **参数**:
-        *   `ecId` (int): 卡片/活动 ID
-        *   `pageName` (string): 页面名称 (如 "all", "rankList", "index")
-    *   **返回数据**:
-        ```json
-        {
-            "configJson": "{\"css\": \"string\", \"tabActiveColor\": \"string\", \"popupRuleConfig\": {...}, \"popupRuleList\": [...]}"
-        }
-        ```
-    *   **configJson 字段说明 (示例)**:
-        *   `css`: `string`,动态注入到页面的 CSS 样式。
-        *   `tabActiveColor`: `string`,Tab 栏选中颜色,如 "#FF5733"。
-        *   `teamType`: `int`,队伍类型 (0:默认, 1:学生/家长)。
-        *   `popupRuleConfig`: `object`,规则弹窗组件的样式配置,如 `{ "height": "500px", "theme": "light" }`。
-        *   `popupMessageConfig`: `object`,消息弹窗组件的样式配置。
-        *   `popupHelpConfig`, `popupWarnConfig`, `popupExchgConfig`: `object`,其他类型弹窗的配置。
-        *   `popupRuleList`: `array<object|string>`,规则弹窗的内容列表,元素可以是 `{ "type": 1, "data": { "title": "标题", "content": "HTML 内容", "logo": {"src": "...", "width": "..."} } }`,也可以是字符串 "default" 或 "default2" (表示加载预设内容)。
-        *   `popupExchgList`, `popupHelpList`: `array<object>`,其他类型弹窗的内容列表。
-        *   `popupDataList`: `array<object|string>`,通用弹窗内容列表,用于显示自定义弹窗信息,支持 `default` / `default2` 关键字加载预设内容。
-        *   *注意:`configJson` 结构灵活,字段是动态的,取决于后台配置,开发者应做好判空处理。*
-
-*   **1.3 用户个性化配置查询**
-    *   **API 方法**: `API.getUserConfig(ecId, pageName)`
-    *   **参数**: `ecId` (int), `pageName` (string)
-    *   **返回数据**:
-        ```json
-        {
-            "configJson": "{\"tplInfo\": {...}, \"mapInfo\": [...], \"popupRuleList\": [...]}"
-        }
-        ```
-    *   **configJson 字段说明 (示例)**:
-        *   `tplInfo`: `object`,模板信息,如 `{ "tplTypeId": 1, "ssctId": 1 }`。
-        *   `mapInfo`: `array<object>`,地图相关信息,如 `[{"activityList": [{"showName": "路线名称", "point": {"longitude": 117, "latitude": 36}}]}]`。
-        *   `popupRuleList`: `array<object>`,用户专属规则弹窗内容。
-        *   *注意:结构灵活,字段是动态的,取决于后台配置,开发者应做好判空处理。*
-
-#### 2. 活动与报名
-
-*   **2.1 卡片对应活动或赛事详情查询**
-    *   **API 方法**: `API.getCardDetail(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "mcId": 101,
-            "mcName": "赛事名称",
-            "mcType": 1, // 1:普通 2:线下 3:线上
-            "beginSecond": 1700000000,
-            "endSecond": 1700090000,
-            "teamNum": 0, // 0:个人, >0:团队
-            "coiId": 1, // 已报名单位ID
-            "coiName": "已报名单位名称",
-            "ocaId": 201 // 关联活动详情ID (用于跳转)
-        }
-        ```
-
-*   **2.2 用户是否已报名卡片对应赛事查询**
-    *   **API 方法**: `API.getUserJoinStatus(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "isJoin": true // true: 已报名, false: 未报名
-        }
-        ```
-
-*   **2.3 线上赛报名 (重新分组)**
-    *   **API 方法**: `API.signUpOnline(mcId, coiId, selectTeam, nickName)`
-    *   **参数**: `mcId` (int), `coiId` (opt), `selectTeam` (opt), `nickName` (opt)
-    *   **返回数据**: `{}` (空对象表示成功)
-
-*   **2.4 是否允许重新分组(报名)**
-    *   **API 方法**: `API.isAllowMcSignUp(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "allowSignUp": true
-        }
-        ```
-
-*   **2.5 线上赛报名页面信息详情**
-    *   **API 方法**: `API.getOnlineMcSignUpDetail(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "teamList": [ // 可选的团队列表
-                { "teamId": 1, "teamName": "红队" },
-                { "teamId": 2, "teamName": "蓝队" }
-            ],
-            "signupFields": [ // 报名所需字段,示例
-                { "name": "phone", "label": "手机号", "type": "text", "required": true }
-            ]
-        }
-
-
-*   **2.6 卡片对应线上赛多个活动查询 (用户赛事结果详情)**
-    *   **API 方法**: `API.getMatchRsDetail(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**: (Array) - 返回用户在卡片关联的各个赛事中的具体成绩详情
-        ```json
-        [
-            {
-                "mcId": 101,
-                "mcName": "赛事名称",
-                "mcType": 1,
-                "beginSecond": 1700000000,
-                "endSecond": 1700090000,
-                "status": 1, // 状态
-                "nickName": "用户昵称", // 用户在赛事中的昵称
-                "totalNum": 10, // 总场次/次数
-                "totalDistanct": 5000, // 总距离 (米)
-                "totalDistanctRankNum": 5, // 总距离排名
-                "totalCp": 20, // 总打点数
-                "totalCpRankNum": 3, // 总打点数排名
-                "totalSysPoint": 100, // 总积分/百味豆
-                "totalSysPointRankNum": 10, // 总积分排名
-                "fastPace": 300, // 最快配速 (秒/公里)
-                "fastPaceRankNum": 8 // 最快配速排名
-            }
-        }
-        ```
-
-*   **2.7 用户在卡片对应赛事是否新用户**
-    *   **API 方法**: `API.isNewUserInCardComp(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "isNew": true // true: 是新用户, false: 不是
-        }
-        ```
-
-#### 3. 排名与成就
-
-*   **3.1 排名查询**
-    *   **API 方法**: `API.getRankDetail(mcIdListStr, mcType, dispArrStr)`
-    *   **参数**: `mcIdListStr` (string), `mcType` (int), `dispArrStr` (string)
-    *   **返回数据**:
-        ```json
-        {
-            "totalRankRs": [ // 总榜
-                {
-                    "nickName": "张三",
-                    "score": 10000,
-                    "headUrl": "http://...",
-                    "rankNum": 1
-                }
-            ],
-            "teamRankRs": [], // 队伍榜
-              "inTeamRs": []    // 队内榜
-          }
-          ```
-
-*   **3.1.1 月挑战排名查询 (月榜)**
-    *   **API 方法**: `API.getMonthRankDetail({ year, month, dispArrStr })`
-    *   **参数**:
-        *   `year` (int): 年份,如 2025
-        *   `month` (int): 月份,1-12
-        *   `dispArrStr` (string): 固定传 `grad,mapNum`(分别返回个人榜、场地榜)
-    *   **示例**:
-        ```bash
-        curl -X POST -H "Content-Type: application/x-www-form-urlencoded" \
-          -d "year=2025&month=11&dispArrStr=grad,mapNum" \
-          https://colormaprun.com/api/card/MonthRankDetailQuery
-        ```
-    *   **返回数据**:
-        ```json
-        {
-          "gradRs": [ { "userName": "张三", "rankNum": 1, "inRankNum": 264, "isSelf": 0, "isInGame": 0 }, ... ],
-          "mapNumRs": [ { "userName": "Q", "rankNum": 1, "inRankNum": 1, "isSelf": 0, "isInGame": 0 }, ... ]
-        }
-        ```
-        *说明*: 部分环境允许不带 token 访问月榜;若需鉴权同样在 Header 携带 `token`。
-
-*   **3.2 卡片用户当前排名查询**
-    *   **API 方法**: `API.getUserCurrentRank(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "rankNum": 5 // 当前排名,0表示未上榜
-        }
-        ```
-
-*   **3.3 玩家所有月挑战记录查询**
-    *   **API 方法**: `API.getMonthlyChallenge()`
-    *   **参数**: 无
-    *   **返回数据**: (Array)
-        ```json
-        [
-            {
-                "year": "2023",
-                "monthRs": [
-                    {
-                        "month": 10,
-                        "realNum": 5, // 实际完成
-                        "targetNum": 10 // 目标
-                    }
-                ]
-            }
-        ]
-        ```
-
-*   **3.4 玩家活动成就查询**
-    *   **API 方法**: `API.getAchievement()`
-    *   **参数**: 无
-    *   **返回数据**: (Array)
-        ```json
-        [
-            {
-                "year": "2023",
-                "aiRs": [
-                    {
-                        "aiName": "完赛奖牌",
-                        "aiTime": 1700000000,
-                        "iconUrl": "http://..."
-                    }
-                ]
-            }
-        ]
-        ```
-
-*   **3.5 赛事总成绩统计查询**
-    *   **API 方法**: `API.getCompStatistic(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "totalDistance": 123.45, // 总里程
-            "totalPeople": 1000, // 总参与人数
-            "totalRightAnswerNum": 500, // 总正确答题数
-            "totalAnswerNum": 800, // 总答题数
-            "totalCp": 2000, // 总打点数
-            "totalSysPoint": 5000 // 总积分/百味豆
-        }
-        ```
-
-*   **3.6 玩家当前月挑战记录查询**
-    *   **API 方法**: `API.getCurrentMonthlyChallenge(year, month)`
-    *   **参数**: 
-        *   `year` (int): 可选,默认当年
-        *   `month` (int): 可选,默认当月
-    *   **返回数据**:
-        ```json
-        {
-            "month": 11,
-            "realNum": 10, // 实际完成次数
-            "targetNum": 20 // 目标次数
-        }
-        ```
-
-#### 4. 积分与兑换
-
-*   **4.1 卡片内可用积分查询**
-    *   **API 方法**: `API.getScore(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "score": 880,
-            "extTime": 1700000000 // 过期时间
-        }
-        ```
-
-*   **4.2 积分可兑换商品列表查询**
-    *   **API 方法**: `API.getGoodsList(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**: (Array)
-        ```json
-        [
-            {
-                "goodsId": 1,
-                "goodsName": "商品A",
-                "goodsPic": "/static/...", // 需拼接 OSS Url
-                "goodsLeftNum": 99,
-                "corrScore": 100
-            }
-        ]
-        ```
-
-*   **4.3 积分兑换商品**
-    *   **API 方法**: `API.exchangeGoods(ecId, goodsId, exchNum)`
-    *   **参数**: `ecId` (int), `goodsId` (int), `exchNum` (int, 可选, 默认1)
-    *   **返回数据**: `{}` (成功)
-
-*   **4.4 玩家兑换记录查询**
-    *   **API 方法**: `API.getExchangeList(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**: (Array)
-        ```json
-        [
-            {
-                "exchangeId": 123,
-                "goodsName": "商品A",
-                "createTime": 1700000000,
-                "status": 1 // 1:成功
-            }
-        ]
-        ```
-
-*   **4.5 玩家兑换详情查询**
-    *   **API 方法**: `API.getExchangeDetail(ecId, exchangeId)`
-    *   **参数**: `ecId` (int), `exchangeId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "exchangeId": 1,
-            "goodsName": "商品名称",
-            "createTime": 1700000000,
-            "status": 1,
-            "address": "收货地址",
-            "receiver": "收件人",
-            "phone": "联系电话"
-        }
-        ```
-
-*   **4.6 积分可兑换商品详情**
-    *   **API 方法**: `API.getGoodsDetail(ecId, goodsId)`
-    *   **参数**: `ecId` (int), `goodsId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "goodsId": 1, 
-            "goodsName": "商品名称", 
-            "goodsPic": "http://...", 
-            "corrScore": 500, // 所需积分
-            "goodsLeftNum": 99, // 剩余库存
-            "goodsUnit": "个", // 单位
-            "goodsDesc": "HTML描述内容", 
-            "exchDesc": "HTML兑换说明", 
-            "exchLimit": 3 // 个人兑换上限
-        }
-        ```
-
-#### 5. 消息与通知
-
-*   **5.1 未读消息列表查询**
-    *   **API 方法**: `API.getUnReadMessages(relationId, relationType)`
-    *   **参数**: `relationId` (int), `relationType` (int)
-    *   **返回数据**: (Array)
-        ```json
-        [
-            {
-                "mqId": 1,
-                "mqType": 1, // 1:成就, 3:系统
-                "mqTitle": "标题",
-                "mqMessage": "内容",
-                "iconUrl": "http://..."
-            }
-        ]
-        ```
-
-*   **5.2 警告列表查询**
-    *   **API 方法**: `API.getWarnMessage(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**: (Array)
-        ```json
-        [
-            {
-                "warnType": 1,
-                "warnTitle": "黄牌警告",
-                "warnMessage": "您违规了...",
-                "iconUrl": "http://..."
-            }
-        ]
-        ```
-
-*   **5.3 标记消息已读**
-    *   **API 方法**: `API.readMessage(mqIdListStr)`
-    *   **参数**: `mqIdListStr` (string) - 消息ID列表字符串,如 "1,2,3"
-    *   **返回数据**: `{}` (成功)
-
-#### 6. 网格游戏
-
-*   **6.1 网格卡片信息查询**
-    *   **API 方法**: `API.getGrids(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "compId": 101,
-            "compName": "网格赛",
-            "widthNum": 3,
-            "heightNum": 3,
-            "maskImgPic": "http://...",
-            "actualImgPic": "http://...",
-            "state": 2, // 1:未开始 2:进行中 3:已结束
-            "detailRs": [
-                {
-                    "orderNum": 1, // 格子序号
-                    "isComplete": 1, // 1:点亮 0:未点亮
-                    "showName": "格子1",
-                    "relationType": 1, // 1:活动详情 2:活动列表 3:地图列表
-                    "ocaId": 201, // 关联活动ID (relationType=1时有效)
-                    "mapId": 0, // 关联地图ID (relationType=2时有效)
-                    "description": "描述信息",
-                    "popupImg": "http://...", // 弹窗图片
-                    "longitude": 117.0,
-                    "latitude": 36.0
-                }
-            ]
-        }
-        ```
-
-#### 7. 电子证书
-
-*   **7.1 查询电子证书样式**
-    *   **API 方法**: `API.getCertStyle(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "styleId": 1,
-            "styleName": "默认样式",
-            "templateUrl": "http://...", // 证书模板图片 URL
-            "elements": [
-                { "type": "text", "field": "userName", "x": 100, "y": 200, "fontSize": 20, "color": "#000" }
-                // 更多元素配置,如位置、字体、颜色
-            ]
-        }
-        ```
-
-*   **7.2 查询电子证书成就对应用户基本信息**
-    *   **API 方法**: `API.getUserBaseInCertificate(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**:
-        ```json
-        {
-            "userName": "用户名称",
-            "activityName": "活动名称",
-            "completionTime": 1700000000 // 完成时间戳
-        }
-        ```
-
-*   **7.3 根据成就信息确认生成电子证书**
-    *   **API 方法**: `API.createCertificate(data)`
-    *   **参数**:
-        *   `data` (object): 包含生成证书所需的所有参数,通常包括 `ecId`, `certStyleId`, `userName`, `activityName`, `completionTime` 等。具体结构请参考后端文档或 `api.js` 源码中的请求示例。
-    *   **返回数据**:
-        ```json
-        {
-            "certUrl": "http://example.com/generated_cert.png" // 生成的证书图片 URL
-        }
-        ```
-
-#### 8. 其他辅助接口
-
-*   **8.1 用户基本信息查询**
-    *   **API 方法**: `API.getUserInfo()`
-    *   **参数**: 无
-    *   **返回数据**:
-        ```json
-        {
-            "nickName": "用户昵称",
-            "headUrl": "http://..."
-        }
-        ```
-*   **8.2 卡片对应地图列表详情查询**
-    *   **API 方法**: `API.getMapList(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**: (Array)
-        ```json
-        [
-            {
-                "mapId": 1,
-                "mapName": "奥体中心",
-                "latitude": 36.666,
-                "longitude": 117.123,
-                "activityList": [
-                    { "ocaId": 101, "showName": "迷你跑" }
-                ]
-            }
-        ]
-        ```
-
-*   **8.3 卡片URI查询**
-    *   **API 方法**: `API.getCardUri(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**: `{}` (待定)
-
-*   **8.4 赛事完赛信息查询**
-    *   **API 方法**: `API.getMatchFinishInfo(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**: `{}` (待定)
-
-*   **8.5 Redis 重建 (管理接口)**
-    *   **API 方法**: `API.redisRebuild(ecId)`
-    *   **参数**: `ecId` (int)
-    *   **返回数据**: `{}` (待定)
-

+ 0 - 378
card/pages/tpl/style3/new/HTML_MIGRATION_STRATEGY.md

@@ -1,378 +0,0 @@
-# 纯 HTML 迁移方案分析报告 (更新版)
-
-## 1. 总体评估
-
-用户希望将基于 UniApp/Vue.js 的现有逻辑(主要涉及 `pages/tpl/style3/index.vue`, `pages/tpl/style3/signup.vue`, `pages/tpl/style3/rankList.vue`, `pages/tpl/style3/rankOverview.vue`)迁移到一套新的、纯 HTML/JS/CSS 的 UI 设计中。新的 UI 文件位于 `pages/tpl/style3/new/` 目录下,包括 `index.html` (入口页), `signup.html` (报名页), `ranklist.html` (排行榜/总览页)。
-
-**核心挑战**:
-1.  **UniApp API 依赖**: 原有业务逻辑文件 (`common/api.js`, `common/tools.js`, `common/cardfunc.js`) 大量使用了 `uni.request`、`uni.showToast` 等 UniApp 特有的 API,这些在标准浏览器环境中不可用。
-2.  **Vue 响应式与指令**: 原有 UI 通过 Vue 的数据绑定、指令(`v-if`, `v-for`, `v-model`)实现动态渲染和交互。纯 HTML/JS 环境需要手动操作 DOM。
-3.  **模块化**: 原有 JS 文件使用 ES Module (`export`, `import`) 语法。在纯 HTML 环境中直接使用需要 `<script type="module">` 并且要解决模块路径和依赖顺序问题。
-
-**结论**:迁移完全可行,但需要构建一个“UniApp 兼容层”来适配旧有逻辑,并采用原生 JavaScript 来实现数据与 UI 的绑定及交互。
-
-## 2. 目录结构规划
-
-为了更好地管理代码,建议在 `pages/tpl/style3/new/` 下创建 `js/` 目录来存放所有 JavaScript 文件。
-
-```text
-pages/tpl/style3/new/
-├── index.html          (新UI入口页)
-├── signup.html         (新UI报名页)
-├── ranklist.html       (新UI排行榜/总览页)
-├── bd.png              (新UI图片资源)
-├── gd.png              (新UI图片资源)
-├── css/                (可选:如果需要将 Tailwind/自定义样式提取到单独文件)
-└── js/
-    ├── uni-compat.js   (新建:核心兼容层,模拟 uni.xyz API)
-    ├── define.js       (移植:常量定义,如 defaultPopUpDataList)
-    ├── api.js          (移植:API 接口地址定义)
-    ├── tools.js        (移植:工具函数)
-    ├── cardfunc.js     (移植:配置加载与处理逻辑)
-    ├── logic-index.js  (新建:针对 index.html 的业务逻辑)
-    ├── logic-signup.js (新建:针对 signup.html 的业务逻辑)
-    └── logic-ranklist.js (新建:针对 ranklist.html 的业务逻辑)
-```
-
-## 3. 详细实施方案
-
-### 3.1 构建 UniApp 兼容层 (`js/uni-compat.js`)
-
-这是一个关键的适配器,用于在标准浏览器环境中模拟 UniApp 的部分 API。
-
-**草稿示例 (`js/uni-compat.js`)**:
-
-```javascript
-// pages/tpl/style3/new/js/uni-compat.js
-(function() {
-    window.uni = window.uni || {}; // 确保全局uni对象存在
-
-    // 1. 模拟网络请求 (核心)
-    uni.request = function(options) {
-        let headers = options.header || {};
-        if (options.method === 'POST' && !headers['Content-Type']) {
-            headers['Content-Type'] = 'application/json'; // 默认JSON
-            // 如果是 x-www-form-urlencoded,需要转换data
-            if (options.header && options.header['Content-Type'] === 'application/x-www-form-urlencoded') {
-                headers['Content-Type'] = 'application/x-www-form-urlencoded';
-                options.body = Object.keys(options.data).map(key => `${key}=${encodeURIComponent(options.data[key])}`).join('&');
-            } else if (options.data && typeof options.data === 'object') {
-                options.body = JSON.stringify(options.data);
-            }
-        } else if (options.method === 'GET' && options.data) {
-            const queryString = Object.keys(options.data).map(key => `${key}=${encodeURIComponent(options.data[key])}`).join('&');
-            options.url = `${options.url}?${queryString}`;
-        }
-        
-        fetch(options.url, {
-            method: options.method || 'GET',
-            headers: headers,
-            body: options.body || undefined
-        })
-        .then(response => response.json())
-        .then(data => {
-            if (options.success) {
-                options.success({ statusCode: 200, data: data }); // 适配 uni.request 的回调格式
-            }
-        })
-        .catch(err => {
-            console.error('uni.request error:', err);
-            if (options.fail) options.fail(err);
-        });
-    };
-
-    // 2. 模拟本地存储
-    uni.setStorageSync = function(key, data) {
-        localStorage.setItem(key, JSON.stringify(data));
-    };
-    uni.getStorageSync = function(key) {
-        const val = localStorage.getItem(key);
-        try { return JSON.parse(val); } catch(e) { return val; }
-    };
-    uni.getStorage = function(obj) { // 模拟异步版本
-        try {
-            const data = uni.getStorageSync(obj.key);
-            if(obj.success) obj.success({ data: data });
-        } catch (e) {
-            if(obj.fail) obj.fail(e);
-        }
-    };
-    uni.setStorage = function(obj) { // 模拟异步版本
-        try {
-            uni.setStorageSync(obj.key, obj.data);
-            if(obj.success) obj.success();
-        } catch (e) {
-            if(obj.fail) obj.fail(e);
-        }
-    };
-
-    // 3. 模拟交互反馈
-    uni.showToast = function(options) {
-        // 可替换为更美观的自定义 Toast 实现
-        alert(`Toast: ${options.title}`); // 简单实现
-        // console.log('Toast:', options.title);
-    };
-    
-    // 4. 模拟系统信息
-    uni.getSystemInfoSync = function() {
-        return { appVersion: '1.0.0' }; // 默认值
-    };
-
-    // 5. 模拟导航
-    uni.navigateTo = function(options) {
-        window.location.href = options.url;
-    };
-
-    // 6. 模拟 getApp() 全局对象 (用于 cardfunc.js)
-    window.getApp = function() {
-        return {
-            globalData: { defaultMatchLogo: '' }, // 需根据实际情况填充
-            $cardconfigType: 'remote' // 'remote' 或 'local',根据需要调整
-        };
-    };
-
-    // 7. 模拟原生AppAction (由 tools.js 调用)
-    window.appAction = function(url, actType = "") {
-        if (url.startsWith('action://')) {
-            console.log(`Simulating native app action: ${url}`);
-            // 在纯HTML中,action://通常无法直接处理。
-            // 可以在这里做一些兼容性处理,比如弹出提示或者跳转到特定H5页面。
-            // 例如,如果 action://to_login/,可以跳到H5登录页
-            if (url.includes('to_login/')) {
-                // window.location.href = '/login.html'; // 假设有H5登录页
-                alert('请登录');
-            } else if (url.includes('to_home/')) {
-                window.location.href = 'index.html'; // 假设首页
-            } else if (url.includes('to_detail/')) {
-                // window.location.href = '/game_detail.html?id=' + url.split('id=')[1].split('&')[0];
-                alert(`进入比赛详情: ${url}`);
-            }
-        } else if (url.startsWith('http') || url.startsWith('/')) {
-            window.location.href = url;
-        } else {
-            console.warn(`Unknown app action URL: ${url}`);
-        }
-    };
-
-})();
-```
-
-### 3.2 移植公共模块 (`js/`)
-
-将原 `common/` 目录下的 JS 文件内容复制到 `pages/tpl/style3/new/js/` 相应文件,并进行改造以适应浏览器环境。
-
-*   **`js/define.js`**:
-    *   移除 `export const`。
-    *   直接定义全局变量或使用 `window.define = {...}` 挂载。
-
-    ```javascript
-    // pages/tpl/style3/new/js/define.js
-    window.tplStyleList = [];
-    tplStyleList[0] = 'blue';
-    // ... 其他 tplStyleList 的定义
-
-    window.teamName = [];
-    teamName[0] = [];
-    // ... 其他 teamName 的定义
-
-    window.defaultPopUpDataList = [...];
-    window.defaultPopUpDataList2 = [...];
-    window.defaultPopUpDataList3 = [...];
-    ```
-
-*   **`js/api.js`**:
-    *   移除 `export const`。
-    *   `process.env.OSS_URL` 和 `process.env.API_BASE_URL` 需硬编码。
-    *   `token` 变量可以设为初始空字符串,或从 URL 参数获取。
-    *   `checkResCode`, `checkToken` 函数可以直接定义在全局。
-
-    ```javascript
-    // pages/tpl/style3/new/js/api.js
-    const API_BASE_URL = 'YOUR_ACTUAL_API_SERVER_URL/'; // !!! 替换为实际的 API 地址
-    const OSS_URL = 'YOUR_ACTUAL_OSS_URL/'; // !!! 替换为实际的 OSS 地址
-
-    window.ossUrl = OSS_URL;
-    window.apiServer = API_BASE_URL;
-
-    window.token = ''; // 初始为空,会从 URL 参数中获取或动态设置
-
-    // 所有 API 接口路径
-    window.apiCardBaseQuery = API_BASE_URL + 'CardBaseQuery';
-    // ... 其他 apiXXXQuery 定义
-
-    // 辅助函数直接定义
-    window.checkResCode = function(res, failLabel='') {
-        // ... 原 common/api.js 中的 checkResCode 逻辑,将 uni.showToast 替换为 window.uni.showToast
-        if (res.data.code == 0) {
-            return true;
-        } else if (res.statusCode == 401) {
-            window.uni.showToast({ title: `您尚未登录`, icon: 'none' });
-            window.appAction(`action://to_login/`);
-            return false;
-        } else {
-            window.uni.showToast({ title: `${failLabel}${res.data.message}`, icon: 'none' });
-            return false;
-        }
-    };
-
-    window.checkToken = function(token) {
-        // ... 原 common/api.js 中的 checkToken 逻辑
-        const regex = /^[0-9A-Za-z]{32}$/;
-        if (regex.test(token)) {
-            return true;
-        } else {
-            window.uni.showToast({ title: `您尚未登录`, icon: 'none' });
-            window.appAction(`action://to_login/`);
-            return false;
-        }
-    };
-    ```
-
-*   **`js/tools.js`**:
-    *   移除 `import tools from '/common/tools';` 和 `export default tools;`。
-    *   将 `tools` 对象直接定义在 `window` 上。
-    *   内部对 `uni` 的调用会通过 `uni-compat.js` 提供的 `window.uni` 对象。
-    *   内部对 `appAction` 的调用需要改为 `window.appAction` (或者确保 `appAction` 在全局)。
-
-    ```javascript
-    // pages/tpl/style3/new/js/tools.js
-    // 假设 window.uni 和 window.appAction 已经存在 (由 uni-compat.js 提供)
-    window.tools = {
-        // ... 原 common/tools.js 中的所有方法
-        // 注意:内部对 uni.xyz 的调用会自动映射到 window.uni.xyz
-        // appAction 方法会调用到 window.appAction (由 uni-compat.js 提供模拟)
-    };
-    ```
-
-*   **`js/cardfunc.js`**:
-    *   移除 `import ...` 语句。
-    *   将 `cardfunc` 对象直接定义在 `window` 上。
-    *   确保 `window.tools`, `window.uni`, `window.checkResCode`, `window.apiXXX` 等变量在加载 `cardfunc.js` 之前已定义。
-
-    ```javascript
-    // pages/tpl/style3/new/js/cardfunc.js
-    // 假设 window.tools, window.uni, window.apiXXX, window.checkResCode, window.defaultPopUpDataList 等都已定义
-    window.cardfunc = {
-        caller: null, // 在纯HTML中,caller的概念可能不再适用,或者需要重新定义
-        token: "",
-        ecId: 0,
-        isNewUser: false,
-        cardConfigData: { /* ... */ },
-        userConfigData: { /* ... */ },
-
-        init(token, ecId) { // 调整 init 参数,去除 caller
-            this.token = token;
-            this.ecId = ecId;
-            // this.removeCss(); // 如果纯HTML没有uni.css,则可能不需要
-        },
-        // ... 其他 cardfunc 中的所有方法
-        // 内部对 uni.request 会使用 window.uni.request
-        // 内部对 tools.xxx 会使用 window.tools.xxx
-        // 内部对 apiXXX 会使用 window.apiXXX
-        // 内部对 checkResCode 会使用 window.checkResCode
-        // 内部对 getApp().xxx 会使用 window.getApp().xxx
-    };
-    ```
-
-### 3.3 针对每个页面的业务逻辑 (`js/logic-*.js`)
-
-每个 HTML 页面将有一个专属的 `logic-*.js` 文件来处理其特定的业务逻辑。
-
-#### 3.3.1 `pages/tpl/style3/new/js/logic-index.js` (对应 `index.html`)
-
-**逻辑来源**: `pages/tpl/style3/index.vue`
-
-1.  **全局状态**: 定义页面所需变量 (`token`, `ecId`, `beginSecond`, `endSecond`, `mcState`, `isJoin` 等)。
-2.  **`initPage()` 函数**: 页面加载时执行。
-    *   **解析 URL 参数**: 从 `window.location.search` 获取 `token`, `id` (即 `ecId`)。
-    *   **初始化 `cardfunc`**: `window.cardfunc.init(state.token, state.ecId);`
-    *   **加载配置**: `window.cardfunc.getCardConfig(loadConfigCallback);`
-    *   **数据请求**:
-        *   调用 `apiCardBaseQuery` 获取赛事基本信息(`beginSecond`, `endSecond`)。
-        *   调用 `apiUserJoinCardQuery` 获取用户报名状态 (`isJoin`)。
-    *   **启动倒计时/更新 UI**: 根据 `beginSecond`, `endSecond`, `isJoin` 更新 `action-btn` 的文本、样式,以及倒计时显示。
-    *   **绑定事件**: 为 `action-btn` 绑定点击事件。
-3.  **`updateUI()` 函数**: 根据 `mcState` 和 `isJoin` 动态修改 `action-btn` 和 `timer-container` 的内容和样式。
-4.  **`btnClick()` 事件处理**:
-    *   根据 `mcState` 和 `isJoin` 决定跳转到 `signup.html` 或 `ranklist.html`。
-    *   使用 `window.uni.navigateTo({ url: '...' });` 或 `window.appAction('...');`。
-
-#### 3.3.2 `pages/tpl/style3/new/js/logic-signup.js` (对应 `signup.html`)
-
-**逻辑来源**: `pages/tpl/style3/signup.vue`
-
-1.  **全局状态**: `token`, `ecId`, `mcName`, `beginSecond`, `endSecond`, `coiRs` (组织列表), `nickName`, `coiId` 等。
-2.  **`initPage()` 函数**:
-    *   **解析 URL 参数**: `token`, `id`, `from`。
-    *   **初始化 `cardfunc`**: `window.cardfunc.init(state.token, state.ecId);`
-    *   **加载配置**: `window.cardfunc.getCardConfig(loadConfigCallback);`
-    *   **数据请求**:
-        *   `apiCardDetailQuery` 获取赛事详情和用户已填信息。
-        *   `apiOnlineMcSignUpDetail` 获取组织列表 (`coiRs`),填充到“选择战队”的下拉菜单。
-    *   **更新 UI**: 填充“昵称”输入框,更新“比赛时间”显示,动态生成下拉菜单选项。
-    *   **绑定事件**: 为“立即报名”按钮、下拉菜单、返回按钮、说明按钮等绑定事件。
-3.  **`btnSignup()` 函数**: 验证输入,弹出确认框,然后调用 `apiOnlineMcSignUp`。
-4.  **`selectOption()` 函数**: 更新下拉菜单选中项的显示和隐藏 `coiId` 值。
-5.  **导航**: 报名成功后跳转到 `ranklist.html`。
-
-#### 3.3.3 `pages/tpl/style3/new/js/logic-ranklist.js` (对应 `ranklist.html`)
-
-**逻辑来源**: `pages/tpl/style3/rankList.vue` 和 `pages/tpl/style3/rankOverview.vue`
-
-1.  **全局状态**: `token`, `ecId`, `mcId`, `mcName`, `currentTab`, `currentMetric`, `rankList` (所有榜单数据), `userInfo` (个人信息), `mapList` (地图列表), `ocaId` (当前地图ID) 等。
-2.  **`initPage()` 函数**:
-    *   **解析 URL 参数**: `token`, `id`。
-    *   **初始化 `cardfunc`**: `window.cardfunc.init(state.token, state.ecId);`
-    *   **加载配置**: `window.cardfunc.getCardConfig(loadConfigCallback);`
-    *   **数据请求**:
-        *   `apiMatchRsDetailQuery` 获取赛事和个人概览数据。
-        *   `apiCompStatisticQuery` 获取全局统计数据(用于跑马灯)。
-        *   `apiMapListQuery` 获取地图列表。
-        *   `apiCardRankDetailQuery` 获取榜单数据。
-    *   **更新 UI**: 填充个人信息卡片、跑马灯、默认榜单列表。
-    *   **绑定事件**: Tabs 切换、维度切换、底部“进入比赛”按钮、编辑资料、说明模态框等。
-3.  **`renderLeaderboard()` 函数**: 负责根据 `currentTab`, `currentMetric` 和 `rankList` 数据渲染排行榜列表。
-4.  **`switchMainTab()`, `switchMetric()` 函数**: 切换 Tab 时更新 `state.currentTab`/`state.currentMetric`,然后调用 `renderLeaderboard()`。
-5.  **`openDrawer()`, `closeDrawer()` 函数**: 控制底部抽屉的显示与隐藏。
-6.  **`editProfile()`, `saveProfile()` 函数**: 弹出/关闭编辑资料模态框,保存时调用 `apiOnlineMcSignUp` 更新。
-7.  **导航**: 底部“进入比赛”按钮跳转到 `action://to_detail` 或其他 H5 页面。返回按钮跳转到 `index.html`。
-
-### 3.4 HTML 文件集成
-
-每个 HTML 文件的底部需要引入其所需的 JS 文件,注意顺序:
-
-```html
-<!-- 在所有 HTML 文件中 (例如 index.html, signup.html, ranklist.html) -->
-<!-- 1. Tailwind CSS 和 FontAwesome (已存在) -->
-<!-- 2. 兼容层 -->
-<script src="js/uni-compat.js"></script>
-<!-- 3. 基础定义 -->
-<script src="js/define.js"></script>
-<script src="js/api.js"></script>
-<script src="js/tools.js"></script>
-<!-- 4. 业务模块 -->
-<script src="js/cardfunc.js"></script>
-<!-- 5. 页面特定逻辑 (每个页面引入自己的) -->
-<!-- index.html --> <script src="js/logic-index.js"></script>
-<!-- signup.html --> <script src="js/logic-signup.js"></script>
-<!-- ranklist.html --> <script src="js/logic-ranklist.js"></script>
-
-<script>
-    // 在每个页面的 script 标签底部,调用各自的初始化函数
-    // 例如对于 index.html:
-    document.addEventListener('DOMContentLoaded', function() {
-        initIndexPage(); // logic-index.js 中定义的入口函数
-    });
-</script>
-```
-
-## 4. 潜在风险与注意事项
-
-1.  **样式冲突与 Tailwind**: 新 UI 使用 Tailwind CSS。如果未来与 UniApp 项目的其他部分集成,可能存在样式冲突。建议将新 UI 的 Tailwind 类转换为静态 CSS 文件,或使用 Tailwind 的 `@apply` 规则来集成。
-2.  **API 域名**: `api.js` 中的 `API_BASE_URL` 和 `OSS_URL` 必须替换为真实的服务器地址。
-3.  **跨域问题 (CORS)**: 如果这些 HTML 文件部署在与 API 服务器不同的域,浏览器会遇到 CORS 问题。需要确保服务器端已正确配置 CORS 头部。
-4.  **图片资源路径**: HTML 中引用的图片资源 (`src="https://orienteering.beswell.com/card/nanning/..."`, `./gd.png`, `./bd.png`) 在部署时需要确保路径正确。
-5.  **原生 App 交互 (`action://`)**: `uni-compat.js` 中的 `window.appAction` 只是一个模拟。在实际的混合应用 (Hybrid App) 中,需要由原生 App 拦截并处理 `action://` 协议的 URL。
-6.  **`cardfunc.init` 的 `caller` 参数**: 在 Vue 组件中 `this` 就是 `caller`。在纯 HTML 中,`caller` 的概念不再直接适用。`cardfunc.init(caller, token, ecId)` 可以简化为 `cardfunc.init(token, ecId)`,或者 `caller` 可以是当前页面的一个全局对象或空对象。
-
-这个方案提供了将现有 UniApp/Vue 逻辑迁移到纯 HTML/JS 环境的详细路线图,并考虑了 UniApp 特有 API 的兼容性问题。

+ 0 - 93
card/pages/tpl/style3/new/PROJECT_INSIGHTS.md

@@ -1,93 +0,0 @@
-# ColorMapRun H5 Card 项目开发备忘录 (Project Insights)
-
-这份文档汇总了 AI 助手 (Gemini) 在协助开发过程中对项目的理解、架构分析以及关键技术细节。旨在帮助开发者快速上手或在不同环境间同步上下文。
-
-## 1. 项目概况
-
-*   **项目名称**: ColorMapRun Mobile H5 Card
-*   **核心功能**: 为 ColorMapRun App 提供嵌入式的 H5 卡片页面,用于展示活动详情、排行榜、报名、游戏互动等功能。
-*   **技术栈**:
-    *   **前端框架**: Vue.js (UniApp 风格,但在 SDK 模式下使用了原生 HTML/JS/CSS)。
-    *   **构建环境**: 似乎是基于 UniApp 的工程结构,但在 `sdk/` 目录下维护了一套独立的、无框架依赖的原生 H5 页面,用于嵌入 App。
-    *   **通信协议**: 自定义 JSBridge (`bridge.js`) 与 Native App (Flutter) 交互。
-    *   **样式**: CSS3, Flexbox, FontAwesome (部分页面)。
-
-## 2. 核心目录结构
-
-*   **`sdk/`**: **(核心关注点)** 包含可以直接在浏览器或 WebView 中运行的原生 HTML 页面。
-    *   `detail.html`: 核心页面之一,用于展示月度挑战赛详情、排行榜等。
-    *   `index.html`: 入口卡片页。
-    *   `api.js`: 封装了所有后端 API 请求,包含 Mock 数据机制。
-    *   `bridge.js`: 封装了与 App 的通信逻辑 (JSBridge)。
-    *   `API_SERVER.md`: 详细的后端接口文档(由 AI 维护更新)。
-*   **`pages/`**: UniApp 的页面源码目录,包含各种业务模块的 Vue 组件。
-    *   `tpl/`: 通用模板 (style1, style2, style3...),包含活动首页、排行榜、报名页。
-    *   `mytz/`: 每月挑战 (Monthly Challenge) 模块。
-    *   `game/`: 游戏模块 (如 Grid 网格拼图)。
-    *   `bm/`: 报名 (BaoMing) 相关模块。
-    *   `jbs/`: 锦标赛 (JinBiaoSai) 模块。
-*   **`common/`**: 公共工具库,如 `tools.js` (工具函数), `api.js` (UniApp 版 API 封装)。
-
-## 3. API 交互机制 (`sdk/api.js`)
-
-*   **封装方式**: 所有接口挂载在全局 `window.API` 对象上。
-*   **Mock 模式**:
-    *   通过 URL 参数 `?env=mock` 开启。
-    *   开启后,`API.request` 会拦截请求并返回 `api.js` 内部定义的 `MOCK_DB` 数据。
-    *   日志输出会详细打印 `[API-Mock] Request` 和 `[API-Mock] Response`。
-*   **正式模式**:
-    *   默认请求 `https://colormaprun.com/api/card/` (Base URL)。
-    *   请求头包含 `Content-Type: application/x-www-form-urlencoded` 和 `token`。
-    *   自动处理 `401 Unauthorized`,调用 `Bridge.toLogin()` 跳转登录。
-*   **日志管理**:
-    *   引入了 `Logger` 工具。
-    *   仅在 `env=mock` 时输出 `Logger.log` 和 `Logger.warn`,生产环境静默(`Logger.error` 除外)。
-
-## 4. Bridge 交互机制 (`sdk/bridge.js`)
-
-*   **通信方式**:
-    1.  **优先**: `window.uni.postMessage` (UniApp WebView 标准)。
-    2.  **降级**: URL 拦截协议 (如 `action://to_login/`) 或注入对象 (如 `window.share_wx`)。
-*   **主要功能**:
-    *   页面跳转: `toHome`, `toLogin`, `back`, `appAction` (通用跳转)。
-    *   功能调用: `openMap`, `openMatch`, `shareWx`, `makePhoneCall` 等。
-*   **日志管理**: 同样集成了 `Logger` 工具,生产环境隐藏敏感信息(如 Token)。
-
-## 5. 关键业务模块与逻辑
-
-### 5.1 月度挑战赛 (`sdk/detail.html`)
-*   **功能**: 展示用户当月的挑战进度(仪表盘)、积分/场地排行榜(领奖台 + 列表)。
-*   **逻辑**:
-    *   优先加载当前月份数据。
-    *   如果当前月份无数据,会自动向前回溯 6 个月查找有数据的月份。
-    *   **未登录处理**: 新增了 `renderGuestState`,未登录用户显示随机头像和“去登录”提示。
-    *   **UI 调整**: 针对移动端进行了适配,Header 区域紧凑化以腾出更多列表空间。
-
-### 5.2 游戏/网格挑战 (`pages/game/grid`)
-*   **功能**: 九宫格拼图游戏,点亮格子解锁奖励。
-*   **数据结构**: 依赖 `getGrids` 接口,返回 `detailRs` 包含格子的状态、关联活动 (`ocaId`) 或地图 (`mapId`)。
-
-### 5.3 动态配置 (`CardConfigQuery`)
-*   后端通过 `getCardConfig` 接口返回 `configJson` 字符串。
-*   前端解析 JSON 后,动态应用:
-    *   **CSS**: 注入自定义样式。
-    *   **弹窗**: 配置规则弹窗、消息弹窗的内容。
-    *   **UI 元素**: 如 `tabActiveColor`, `teamType` 等。
-
-## 6. 待办与注意事项 (TODOs)
-
-1.  **API 返回结构待定**:
-    *   `getCardUri`
-    *   `getMatchFinishInfo`
-    *   `redisRebuild`
-    *   以上三个接口已在 `api.js` 和文档中占位,但 Mock 数据目前为空对象 `{}`,需根据后端实际返回进行更新。
-2.  **生产环境部署**:
-    *   上线前请确保 `sdk/detail.html` 等文件中的 `env=mock` 参数已移除或由 App 端正确控制。
-    *   建议服务器开启 Gzip 压缩以优化加载速度。
-3.  **代码同步**:
-    *   `sdk/api.js` (原生版) 和 `common/api.js` (UniApp 版) 维护了两套类似的接口定义,修改时需注意同步(目前主要维护了 `sdk/api.js`)。
-
-## 7. 常用调试指令
-
-*   **开启 Mock**: 在 URL 后追加 `?env=mock`。
-*   **查看完整日志**: 开启 Mock 模式后,控制台会输出详细的 Bridge 调用和 API 请求日志。

BIN
card/pages/tpl/style3/new/bd.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 5
card/pages/tpl/style3/new/css/all.min.css


BIN
card/pages/tpl/style3/new/gd.png


+ 0 - 77
card/pages/tpl/style3/new/index.html

@@ -1,77 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <script src="https://cdn.tailwindcss.com"></script>
-    <style>
-        @import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&display=swap');
-        body, html { 
-            font-family: 'Fredoka', sans-serif; 
-            width: 100%; 
-            height: 100%; 
-            margin: 0; 
-            padding: 0;
-            background-color: transparent; /* 确保倒角外部透明 */
-        }
-    </style>
-</head>
-<body class="flex items-center justify-center overflow-hidden">
-
-    <!-- 卡片容器 
-         1. h-full w-full: 占满屏幕
-         2. rounded-[30px]: 强制圆角
-         3. overflow-hidden: 裁剪图片确保圆角生效
-         4. 无背景色设置(或设为white),确保外部是透的
-    -->
-    <div class="relative w-full h-full bg-white overflow-hidden rounded-[30px] shadow-2xl">
-        
-        <!-- 1. 背景层 -->
-        <img src="https://orienteering.beswell.com/card/nanning/nncardrg.jpg" 
-             alt="活动背景" 
-             class="absolute inset-0 w-full h-full object-cover">
-        
-        <!-- 遮罩层 -->
-        <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-black/20"></div>
-
-        <!-- 2. 倒计时区域 
-             使用 vw 单位实现等比例缩放 
-        -->
-        <div class="absolute top-[5%] right-[5%] bg-black/40 backdrop-blur-md rounded-full border border-white/20 shadow-lg z-10"
-             style="padding: 1.5vw 3vw;">
-            <div class="flex items-center gap-[1vw] text-white leading-none font-medium" id="timer-container">
-                <span class="opacity-90" style="font-size: 3.5vw;">距开始</span>
-                <span class="font-bold text-yellow-300 tabular-nums" style="font-size: 4.5vw;">
-                    <span id="days">02</span>天<span id="hours">14</span>时
-                </span>
-            </div>
-        </div>
-
-        <!-- 3. 进入按钮 
-             使用 vw 单位控制宽度、字体、圆角和边距,实现完全响应式
-        -->
-        <div class="absolute bottom-[8%] left-0 w-full flex justify-center z-10 px-[6vw]">
-            <button id="action-btn" 
-                    class="w-full bg-gradient-to-r from-orange-500 to-red-600 text-white font-bold shadow-xl border border-white/20 active:scale-95 transition-transform tracking-wide flex items-center justify-center gap-[2vw]"
-                    style="font-size: 5vw; padding: 3.5vw 0; border-radius: 9999px;">
-                开始报名 <i class="fas fa-chevron-right opacity-80" style="font-size: 4vw;"></i>
-            </button>
-        </div>
-    </div>
-
-    <!-- FontAwesome for the arrow icon -->
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
-
-    <script src="js/uni-compat.js"></script>
-    <script src="js/define.js"></script>
-    <script src="js/api.js"></script>
-    <script src="js/tools.js"></script>
-    <script src="js/cardfunc.js"></script>
-    <script src="js/logic-index.js"></script>
-    <script>
-        document.addEventListener('DOMContentLoaded', function() {
-            initIndexPage();
-        });
-    </script>
-</body>
-</html>

+ 0 - 68
card/pages/tpl/style3/new/js/api.js

@@ -1,68 +0,0 @@
-(function() {
-    // Configuration
-    // TODO: REPLACE WITH ACTUAL API URL IF NEEDED
-    // Based on ossUrl in vue files, guessing the api url.
-    const API_SERVER = 'https://api.colormaprun.com/card/'; 
-    const OSS_URL = 'https://oss-mbh5.colormaprun.com/';
-
-    window.ossUrl = OSS_URL;
-    window.apiServer = API_SERVER;
-    window.token = ''; // Will be set from logic-*.js via URL params
-
-    // APIs
-    window.apiCardBaseQuery = API_SERVER + 'CardBaseQuery';
-    window.apiCardDetailQuery = API_SERVER + 'CardDetailQuery';
-    window.apiMatchRsDetailQuery = API_SERVER + 'MatchRsDetailQuery';
-    window.apiCardRankDetailQuery = API_SERVER + 'CardRankDetailQuery';
-    window.apiUserCurrentRankNumQuery = API_SERVER + 'UserCurrentRankNumQuery';
-    window.apiUserJoinCardQuery = API_SERVER + 'UserJoinCardQuery';
-    window.apiIsNewUserInCardComp = API_SERVER + 'IsNewUserInCardComp';
-    window.apiOnlineMcSignUpDetail = API_SERVER + 'OnlineMcSignUpDetail';
-    window.apiOnlineMcSignUp = API_SERVER + 'OnlineMcSignUp';
-    window.apiIsAllowMcSignUp = API_SERVER + 'IsAllowMcSignUp';
-    window.apiCardConfigQuery = API_SERVER + 'CardConfigQuery';
-    window.apiUserConfigQuery = API_SERVER + 'UserConfigQuery';
-    window.apiUnReadMessageQuery = API_SERVER + 'UnReadMessageQuery';
-    window.apiMapListQuery = API_SERVER + 'MapListQuery';
-    window.apiCompStatisticQuery = API_SERVER + 'CompStatisticQuery';
-    window.apiWarnMessageQuery = API_SERVER + 'WarnMessageQuery';
-
-    // Helpers
-    window.checkResCode = function(res, failLabel='') {
-        if (res.data && res.data.code == 0) {
-            return true;
-        } else if (res.statusCode == 401) { // Not logged in
-            uni.showToast({
-                title: `您尚未登录`,
-                icon: 'none'
-            });
-            const url = `action://to_login/`;
-            window.appAction(url);
-            return false;
-        } else {
-            const msg = res.data ? res.data.message : 'Unknown Error';
-            uni.showToast({
-                title: `${failLabel}${msg}`,
-                icon: 'none'
-            });
-            return false;
-        }
-    };
-
-    window.checkToken = function(token) {
-        const regex = /^[0-9A-Za-f]{32}$/;
-        if (regex.test(token)) {
-            return true;
-        } else {
-            console.log('checkToken err: ', token);
-            uni.showToast({
-                title: `您尚未登录`,
-                icon: 'none'
-            });
-            const url = `action://to_login/`;
-            window.appAction(url);
-            return false;
-        }
-    };
-
-})();

+ 0 - 401
card/pages/tpl/style3/new/js/cardfunc.js

@@ -1,401 +0,0 @@
-(function() {
-    window.cardfunc = {
-        caller: null,
-        token: "",
-        ecId: 0, // 卡片id
-        isNewUser: false,	// 是否新用户
-        
-        cardConfigData: {
-            tabActiveColor: "#81cd00",
-    
-            popupRuleConfig: {}, // 规则弹窗配置
-            popupRuleList: [], // 规则弹窗数据
-    
-            popupExchgConfig: {}, // 兑换地址弹窗配置
-            popupExchgList: [], // 兑换地址弹窗数据
-    
-            popupHelpConfig: {}, // 帮助弹窗配置
-            popupHelpList: [],
-    
-            popupMessageConfig: {}, // 通知弹窗配置
-            popupMessageList: [], // 通知弹窗数据
-    
-            popupWarnConfig: {}, // 警告弹窗配置
-            popupWarnList: [], // 警告弹窗数据
-        },
-        
-        userConfigData: {
-            
-        },
-        
-        init(token, ecId) {
-            // this.caller = caller; // Not used in pure HTML logic usually
-            this.token = token;
-            this.ecId = ecId;
-            this.removeCss();
-        },
-                
-        // 清除css
-        removeCss() {
-            if(window.tools) {
-                window.tools.removeCssCode("css-common");
-                window.tools.removeCssCode("css-custom");
-                window.tools.removeCssCode("css-user");
-            }
-        },
-    
-        getCardConfig(loadConfig, testconfig) {
-            // Assuming remote config for H5
-            this.cardConfigQuery(loadConfig);
-        },
-    
-        getUserConfig(loadConfig, testconfig) {
-            this.userConfigQuery(loadConfig);
-        },
-        
-        parseCardConfig(cardconfig) {
-            if (cardconfig == undefined || cardconfig == "") {
-                return;
-            }
-            
-            if (typeof cardconfig == "string") {
-                cardconfig = cardconfig.replace(/[\r|\n|\t]/g, "");
-                const config = JSON.parse(cardconfig);
-                return config;
-            } else {
-                return cardconfig;
-            }
-        },
-    
-        // 加载卡片通用配置
-        loadCardCommonConfig(config_common) {
-            config_common = this.parseCardConfig(config_common);
-            if (config_common == undefined || config_common == "") {
-                return;
-            }
-    
-            if (config_common.css != undefined && config_common.css.length > 0) {
-                if(window.tools) window.tools.loadCssCode(config_common.css, "css-common");
-            }
-    
-            if (config_common.tabActiveColor != undefined && config_common.tabActiveColor.length > 0) {
-                this.cardConfigData.tabActiveColor = config_common.tabActiveColor;
-            }
-    
-            // 加载规则弹窗配置
-            if (config_common.popupRuleConfig != undefined) {
-                this.cardConfigData.popupRuleConfig = config_common.popupRuleConfig;
-            }
-    
-            // 加载帮助弹窗配置
-            if (config_common.popupHelpConfig != undefined) {
-                this.cardConfigData.popupHelpConfig = config_common.popupHelpConfig;
-            }
-    
-            // 加载警告弹窗配置
-            if (config_common.popupWarnConfig != undefined) {
-                this.cardConfigData.popupWarnConfig = config_common.popupWarnConfig;
-            }
-    
-            // 加载兑换地址弹窗配置
-            if (config_common.popupExchgConfig != undefined) {
-                this.cardConfigData.popupExchgConfig = config_common.popupExchgConfig;
-            }
-    
-            // 加载通知弹窗配置
-            if (config_common.popupMessageConfig != undefined) {
-                this.cardConfigData.popupMessageConfig = config_common.popupMessageConfig;
-            }
-    
-            // 加载弹窗(规则)数据
-            const popupRuleList = config_common.popupRuleList;
-            if (popupRuleList != undefined && popupRuleList.length > 0) {
-                this.cardConfigData.popupRuleList.length = 0;
-                for (var i = 0; i < popupRuleList.length; i++) {
-                    if (popupRuleList[i] == 'default') {
-                        for (var j = 0; j < defaultPopUpDataList.length; j++) {
-                            this.cardConfigData.popupRuleList.push(defaultPopUpDataList[j]);
-                        }
-                    } else if (popupRuleList[i] == 'default2') {
-                        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]);
-                    }
-                }
-            } else {
-                this.cardConfigData.popupRuleList = defaultPopUpDataList2;
-            }
-    
-            // 加载弹窗(兑换地址)数据
-            const popupExchgList = config_common.popupExchgList;
-            if (popupExchgList != undefined && popupExchgList.length > 0) {
-                this.cardConfigData.popupExchgList.length = 0;
-                for (var i = 0; i < popupExchgList.length; i++) {
-                    this.cardConfigData.popupExchgList.push(popupExchgList[i]);
-                }
-            }
-    
-            // 加载弹窗(帮助)数据
-            const popupHelpList = config_common.popupHelpList;
-            if (popupHelpList != undefined && popupHelpList.length > 0) {
-                this.cardConfigData.popupHelpList.length = 0;
-                for (var i = 0; i < popupHelpList.length; i++) {
-                    this.cardConfigData.popupHelpList.push(popupHelpList[i]);
-                }
-            }
-        },
-        
-        // 加载用户的弹窗数据
-        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 defaultMatchLogo = getApp().globalData.defaultMatchLogo;
-                const logoSrc = (tplInfo.matchLogo != undefined && tplInfo.matchLogo != "") ? tplInfo.matchLogo : defaultMatchLogo;
-                
-                const popupRule = [{
-                    "type": 1,
-                    "data": {
-                        "title": matchInfo.compName,
-                        "logo": {
-                            "src": logoSrc,
-                            "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);
-                }
-                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++) {
-                            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
-                                }
-                            };
-                            row.push(path);
-                        }
-                        pathList["row" + (i + 1)] = row;
-                    }
-                }
-            } else {
-                console.warn("[getUserPathList] mapInfo err:", mapInfo);
-            }
-            return pathList;
-        },
-    
-        // 卡片配置信息查询
-        cardConfigQuery(callback) {
-            uni.request({
-                url: window.apiCardConfigQuery,
-                header: {
-                    "Content-Type": "application/x-www-form-urlencoded",
-                    "token": this.token,
-                },
-                method: "POST",
-                data: {
-                    ecId: this.ecId,
-                    pageName: "all"
-                },
-                success: (res) => {
-                    if(res.data && res.data.data) {
-                        const data = res.data.data;
-                        const config = data.configJson;
-                        callback(config);
-                    }
-                },
-                fail: (err) => {
-                    console.log("[cardConfigQuery] err", err);
-                },
-            });
-        },
-    
-        // 用户自定义配置信息查询
-        userConfigQuery(callback) {
-            uni.request({
-                url: window.apiUserConfigQuery,
-                header: {
-                    "Content-Type": "application/x-www-form-urlencoded",
-                    "token": this.token,
-                },
-                method: "POST",
-                data: {
-                    ecId: this.ecId,
-                    pageName: "all"
-                },
-                success: (res) => {
-                    if(res.data && res.data.data) {
-                        const data = res.data.data;
-                        const config = data.configJson;
-                        callback(config);
-                    }
-                },
-                fail: (err) => {
-                    console.log("[userConfigQuery] err", err);
-                },
-            });
-        },
-        
-        // 警告列表查询
-        warnMessageQuery(callback=null) {
-            uni.request({
-                url: window.apiWarnMessageQuery,
-                header: {
-                    "Content-Type": "application/x-www-form-urlencoded",
-                    "token": this.token,
-                },
-                method: "POST",
-                data: {
-                    ecId: this.ecId
-                },
-                success: (res) => {
-                    if (checkResCode(res)) {
-                        const warnRs = res.data.data;
-                        this.cardConfigData.popupWarnList.length = 0;
-                        for (var i = 0; i < warnRs.length; i++) {
-                            let popupData = {
-                                type: 9, // 9: 警告
-                                data: {}
-                            };
-                            popupData.data.warnType = warnRs[i].warnType;
-                            popupData.data.title = warnRs[i].warnTitle;
-                            popupData.data.iconUrl = warnRs[i].iconUrl;
-                            popupData.data.iconNum = warnRs[i].iconNum;
-                            popupData.data.message = warnRs[i].warnMessage;
-                            popupData.data.qrCodeUrl = warnRs[i].qrCodeUrl;
-                            this.cardConfigData.popupWarnList.push(popupData);
-                        }
-    
-                        if (callback != null) {
-                            callback(this.cardConfigData.popupWarnList);
-                        }
-                    }
-                },
-                fail: (err) => {
-                    console.log("warnMessageQuery err", err)
-                },
-            });
-        },
-        
-        // 未读消息列表查询
-        unReadMessageQuery(callback=null) {
-            uni.request({
-                url: window.apiUnReadMessageQuery,
-                header: {
-                    "Content-Type": "application/x-www-form-urlencoded",
-                    "token": this.token,
-                },
-                method: "POST",
-                data: {
-                    relationType: 2, // 类型 1 成就 2 卡片
-                    relationId: this.ecId
-                },
-                success: (res) => {
-                    if (checkResCode(res)) {
-                        const unReadMessageRs = res.data.data;
-                        this.cardConfigData.popupMessageList.length = 0;
-                        let mqIdListStr = "";
-                        for (var i = 0; i < unReadMessageRs.length; i++) {
-                            let popupData = {
-                                type: 6, // 6: 通知
-                                data: {}
-                            };
-                            mqIdListStr += "-" + unReadMessageRs[i].mqId;
-                            popupData.data.mqType = unReadMessageRs[i].mqType;
-                            popupData.data.title = unReadMessageRs[i].mqTitle;
-                            popupData.data.message = unReadMessageRs[i].mqMessage;
-                            this.cardConfigData.popupMessageList.push(popupData);
-                        }
-        
-                        if (callback != null) {
-                            callback(this.cardConfigData.popupMessageList, mqIdListStr);
-                        }
-                    }
-                },
-                fail: (err) => {
-                    console.log("getUnReadMessageQuery err", err);
-                },
-            });
-        },
-        
-        // 用户是否新用户
-        isNewUserQuery(callback=null) {
-            uni.request({
-                url: window.apiIsNewUserInCardComp,
-                header: {
-                    "Content-Type": "application/x-www-form-urlencoded",
-                    "token": this.token,
-                },
-                method: "POST",
-                data: {
-                    // ecId: this.ecId
-                    ecId: 0	// 0 全部赛事活动
-                },
-                success: (res) => {
-                    if (checkResCode(res)) {
-                        this.isNewUser = res.data.data.isNew;
-                        if (callback != null) {
-                            callback(this.isNewUser);
-                        }
-                    }
-                },
-                fail: (err) => {
-                    console.log("isNewUserInCardComp err", err);
-                },
-            });
-        },
-    
-    }
-})();

+ 0 - 75
card/pages/tpl/style3/new/js/define.js

@@ -1,75 +0,0 @@
-(function() {
-    window.tplStyleList = [];
-    window.tplStyleList[0] = 'blue';
-    window.tplStyleList[1] = 'orange';
-    window.tplStyleList[2] = 'green';
-
-    window.teamName = [];
-
-    window.teamName[0] = [];
-    window.teamName[0][0] = '不组队';
-    window.teamName[0][1] = '红队';
-    window.teamName[0][2] = '黄队';
-    window.teamName[0][3] = '蓝队';
-    window.teamName[0][4] = '紫队';
-
-    window.teamName[1] = [];
-    window.teamName[1][0] = '不组队';
-    window.teamName[1][1] = '学生队';
-    window.teamName[1][2] = '家长队';
-
-    window.defaultPopUpDataList = [{
-            type: 2,
-            data: {
-                title: "活动流程",
-                img: "/static/common/hdlc.png",
-            }
-        },
-        {
-            type: 2,
-            data: {
-                title: "基本标识",
-                img: "/static/common/jbbs.png",
-            }
-        }
-    ];
-
-    window.defaultPopUpDataList2 = [{
-            "type": 7,
-            "data": {
-                "title": "基本标识",
-                "logo": {
-                    "src": "/static/common/jbbs2.png",
-                    "width": "280px",
-                    "height": "250px"
-                },
-                "content": "<span style='display:block; text-align: left; color: #FF870E; margin: 8px 0;'>安全提示</span><li>避免聚集,分散参与 <li>评估自身健康,适时参与 <li>注意交通与场地安全"
-            }
-        },
-        {
-            "type": 7,
-            "data": {
-                "title": "基本图例",
-                "logo": {
-                    "src": "/static/common/jbtl.png",
-                    "width": "280px",
-                    "height": "250px"
-                },
-                "content": "<br><span style='display:block; text-align: left; color: #FF870E; margin: 8px 0;'>安全提示</span><li>避免聚集,分散参与 <li>评估自身健康,适时参与 <li>注意交通与场地安全"
-            }
-        }
-    ];
-
-    window.defaultPopUpDataList3 = [{
-            "type": 7,
-            "data": {
-                "title": "基本标识及图例",
-                "logo": {
-                    "src": "/static/common/jbbs3.png",
-                    "width": "300px",
-                    "height": "380px"
-                }
-            }
-        }
-    ];
-})();

+ 0 - 207
card/pages/tpl/style3/new/js/logic-index.js

@@ -1,207 +0,0 @@
-var state = {
-    token: "",
-    ecId: 0,
-    pageName: "index",
-    
-    ecName: '', 
-    ecDesc: '', 
-    beginSecond: null, 
-    endSecond: null, 
-    secondCardName: '', 
-    
-    isJoin: null, 
-    isFinished: false, 
-    
-    countdown: "", 
-    interval: null,
-    
-    mcState: 0, // 0: Not started, 1: In progress, 2: Finished
-    
-    // Query params
-    type: "锦标赛",
-    btnText: "开始比赛"
-};
-
-function initIndexPage() {
-    const params = new URLSearchParams(window.location.search);
-    state.token = params.get('token') || '';
-    state.ecId = params.get('id') || 0;
-    state.type = params.get('type') || "锦标赛";
-    state.btnText = params.get('btnText') || "开始比赛";
-    
-    if(window.cardfunc) {
-        window.cardfunc.init(state.token, state.ecId);
-        window.cardfunc.getCardConfig(onConfigLoaded);
-    }
-    
-    // Bind events
-    const btn = document.getElementById('action-btn');
-    if(btn) {
-        btn.addEventListener('click', btnClick);
-    }
-}
-
-function onConfigLoaded(config) {
-    // Load CSS if any (cardfunc handles common config, we handle page specific if needed)
-    // For pure HTML with Tailwind, maybe we don't need dynamic CSS injection as much, 
-    // or we rely on cardfunc.loadCardCommonConfig which is already called.
-    
-    // Now fetch data
-    getCardBaseQuery();
-    getUserJoinCardQuery();
-    // matchRsDetailQuery(); // Optional: for red dot
-}
-
-function getCardBaseQuery() {
-    uni.request({
-        url: window.apiCardBaseQuery,
-        header: {
-            "Content-Type": "application/x-www-form-urlencoded",
-            "token": state.token,
-        },
-        method: "POST",
-        data: {
-            ecId: state.ecId,
-            pageName: state.pageName
-        },
-        success: (res) => {
-            const data = res.data.data;
-            if(data) {
-                state.ecName = data.ecName;
-                state.ecDesc = data.ecDesc;
-                state.beginSecond = data.beginSecond;
-                state.endSecond = data.endSecond;
-                state.secondCardName = data.secondCardName;
-                
-                updateCountdown();
-                if(state.interval) clearInterval(state.interval);
-                state.interval = setInterval(updateCountdown, 60000);
-            }
-        }
-    });
-}
-
-function getUserJoinCardQuery() {
-    uni.request({
-        url: window.apiUserJoinCardQuery,
-        header: {
-            "Content-Type": "application/x-www-form-urlencoded",
-            "token": state.token
-        },
-        method: "POST",
-        data: {
-            ecId: state.ecId
-        },
-        success: (res) => {
-            const code = res.data.code;
-            const data = res.data.data;
-            if (code == 0) {
-                state.isJoin = data.isJoin;
-                updateUI();
-            }
-        }
-    });
-}
-
-function updateCountdown() {
-    if (state.endSecond > 0) {
-        const now = Date.now() / 1000;
-        
-        // Calculate state based on time
-        if(state.beginSecond > now) {
-            state.mcState = 0; // Not started
-        } else if (state.endSecond > now) {
-            state.mcState = 1; // In progress
-        } else {
-            state.mcState = 2; // Finished
-            state.isFinished = true;
-        }
-
-        let targetTime = state.mcState === 0 ? state.beginSecond : state.endSecond;
-        let dif = targetTime - now;
-        
-        if(state.mcState === 2) {
-            dif = 0;
-        }
-
-        // Update DOM
-        const timerContainer = document.getElementById('timer-container');
-        if(timerContainer) {
-            let label = state.mcState === 0 ? "距开始" : (state.mcState === 1 ? "距结束" : "已结束");
-            
-            if(state.mcState === 2) {
-                 timerContainer.parentElement.className = "absolute top-[5%] right-[5%] bg-gray-800/80 backdrop-blur-md rounded-full";
-                 timerContainer.parentElement.style.padding = "1.5vw 3vw";
-                 timerContainer.innerHTML = '<span class="text-gray-200 font-bold" style="font-size: 4vw;">已结束</span>';
-            } else {
-                let days = Math.floor(dif / (3600 * 24));
-                let hours = Math.floor((dif % (3600 * 24)) / 3600);
-                // let minutes = Math.floor((dif % 3600) / 60);
-                
-                timerContainer.innerHTML = `
-                    <span class="opacity-90" style="font-size: 3.5vw;">${label}</span>
-                    <span class="font-bold text-yellow-300 tabular-nums" style="font-size: 4.5vw;">
-                        <span id="days">${days.toString().padStart(2,'0')}</span>天<span id="hours">${hours.toString().padStart(2,'0')}</span>时
-                    </span>
-                `;
-            }
-        }
-        updateUI();
-    }
-}
-
-function updateUI() {
-    const btn = document.getElementById('action-btn');
-    if(!btn) return;
-
-    if(state.isJoin) {
-        // Already joined, show "Enter Game" or "View Rank"
-        if (state.mcState === 0) {
-             // Not started but joined? Maybe "Wait for start" or just "View Rank"
-             btn.innerHTML = '查看排名 <i class="fas fa-list-ol opacity-80" style="font-size: 4vw;"></i>';
-             btn.className = "w-full bg-gradient-to-r from-blue-500 to-indigo-600 text-white font-bold shadow-xl border border-white/20 active:scale-95 transition-transform tracking-wide flex items-center justify-center gap-[2vw]";
-        } else if (state.mcState === 1) {
-            btn.innerHTML = '进入比赛 <i class="fas fa-flag-checkered opacity-80" style="font-size: 4vw;"></i>';
-            btn.className = "w-full bg-gradient-to-r from-green-400 to-emerald-600 text-white font-bold shadow-xl border border-white/20 active:scale-95 transition-transform tracking-wide flex items-center justify-center gap-[2vw]";
-        } else {
-            btn.innerHTML = '查看数据';
-            btn.className = "w-full bg-slate-600 text-white font-bold shadow-xl active:scale-95 transition-transform tracking-wide flex items-center justify-center gap-[2vw]";
-        }
-    } else {
-        // Not joined
-         if (state.mcState === 0 || state.mcState === 1) {
-            btn.innerHTML = '开始报名 <i class="fas fa-chevron-right opacity-80" style="font-size: 4vw;"></i>';
-            btn.className = "w-full bg-gradient-to-r from-orange-500 to-red-600 text-white font-bold shadow-xl border border-white/20 active:scale-95 transition-transform tracking-wide flex items-center justify-center gap-[2vw]";
-         } else {
-            btn.innerHTML = '查看排名'; // Event finished
-            btn.className = "w-full bg-slate-600 text-white font-bold shadow-xl active:scale-95 transition-transform tracking-wide flex items-center justify-center gap-[2vw]";
-         }
-    }
-    
-    // Adjust styles based on vw (done in HTML mostly, but classes updated above)
-    btn.style.fontSize = "5vw";
-    btn.style.padding = "3.5vw 0";
-    btn.style.borderRadius = "9999px";
-}
-
-function btnClick() {
-    const queryString = `token=${state.token}&id=${state.ecId}&type=${state.type}&btnText=${state.btnText}`;
-    
-    if (state.isJoin) {	
-        const url = `ranklist.html?${queryString}&full=true`;
-        window.uni.navigateTo({ url: url });
-    } else {	
-        if (!state.isFinished) {
-            if (state.secondCardName == 'rankList') {
-                const url = `ranklist.html?${queryString}&full=true`;
-                 window.uni.navigateTo({ url: url });
-            } else {
-                const url = `signup.html?${queryString}&full=true`;
-                 window.uni.navigateTo({ url: url });
-            }
-        } else {
-            const url = `ranklist.html?${queryString}&full=true`;
-             window.uni.navigateTo({ url: url });
-        }
-    }
-}

+ 0 - 463
card/pages/tpl/style3/new/js/logic-ranklist.js

@@ -1,463 +0,0 @@
-var state = {
-    token: "",
-    ecId: 0,
-    mcId: 0,
-    mcName: "",
-    ocaId: 0, // Current Map/Group ID
-    
-    // Personal Info
-    userInfo: {
-        nickName: '',
-        coiName: '',
-        totalScore: 0, // Points
-        avatar: 12 // Random or from API if available
-    },
-    
-    // Global Stats
-    stats: {
-        totalDistance: 0,
-        totalAnswerNum: 0,
-        totalCp: 0,
-        endSecond: 0
-    },
-    
-    // Leaderboard Data
-    rankList: {}, // Stores raw data from API
-    
-    // UI State
-    currentTab: 'team', // 'team' or 'individual'
-    currentMetric: 'score', // 'score', 'mileage', 'accuracy', 'count', 'lap' (pace)
-    
-    mapList: [], // For drawer
-    
-    mcState: 0
-};
-
-function initRankListPage() {
-    const params = new URLSearchParams(window.location.search);
-    state.token = params.get('token') || '';
-    state.ecId = params.get('id') || 0;
-    
-    const savedOcaId = uni.getStorageSync(`rank-tpl-style3-map-${state.ecId}`);
-    if(savedOcaId) state.ocaId = savedOcaId;
-
-    window.cardfunc.init(state.token, state.ecId);
-    window.cardfunc.getCardConfig(onConfigLoaded);
-    
-    document.getElementById('drawer-backdrop').addEventListener('click', closeDrawer);
-}
-
-function onConfigLoaded(config) {
-    matchRsDetailQuery();
-    compStatisticQuery();
-    mapListQuery(); 
-    window.cardfunc.unReadMessageQuery();
-}
-
-function matchRsDetailQuery() {
-    uni.request({
-        url: window.apiMatchRsDetailQuery,
-        header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
-        method: "POST",
-        data: { ecId: state.ecId, ocaId: state.ocaId },
-        success: (res) => {
-            if(window.checkResCode(res)) {
-                const data = res.data.data;
-                state.mcId = data.mcId;
-                state.mcName = data.mcName;
-                state.userInfo.nickName = data.nickName;
-                state.userInfo.coiName = data.coiName;
-                state.userInfo.totalScore = data.regionTotalSysPoint || 0;
-                state.mcState = window.tools.checkMcState(data.beginSecond, data.endSecond);
-                state.stats.endSecond = data.endSecond;
-                
-                updateHeaderUI();
-                fetchCompStats();
-                fetchMapList();
-            }
-        }
-    });
-}
-
-function fetchCompStats() {
-    uni.request({
-        url: window.apiCompStatisticQuery,
-        header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
-        method: "POST",
-        data: { mcId: state.mcId },
-        success: (res) => {
-            if(res.data.code == 0) {
-                const data = res.data.data;
-                state.stats.totalDistance = data.totalDistance;
-                state.stats.totalAnswerNum = data.totalAnswerNum;
-                state.stats.totalCp = data.totalCp;
-                updateMarquee();
-            }
-        }
-    });
-}
-
-function fetchMapList() {
-    uni.request({
-        url: window.apiMapListQuery,
-        header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
-        method: "POST",
-        data: { mcId: state.mcId },
-        success: (res) => {
-            if(res.data.code == 0) {
-                state.mapList = res.data.data;
-                if(state.ocaId == 0 && state.mapList.length > 0) {
-                    state.ocaId = state.mapList[0].ocaId;
-                    uni.setStorageSync(`rank-tpl-style3-map-${state.ecId}`, state.ocaId);
-                }
-                fetchRankDetail();
-                renderDrawer();
-            }
-        }
-    });
-}
-
-function fetchRankDetail() {
-    const dispArrStr = "teamCp,teamTodayCp,teamDistance,teamRightAnswerPer,teamTodayPace,regionCp,regionTodayCp,regionDistance,regionRightAnswerPer,regionTodayPace";
-    
-    uni.request({
-        url: window.apiCardRankDetailQuery,
-        header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
-        method: "POST",
-        data: {
-            mcIdListStr: state.mcId,
-            mcType: 1,
-            ocaId: state.ocaId,
-            dispArrStr: dispArrStr
-        },
-        success: (res) => {
-            if(res.data.code == 0) {
-                state.rankList = res.data.data;
-                renderLeaderboard();
-            }
-        }
-    });
-}
-
-function updateHeaderUI() {
-    const nameEl = document.getElementById('profileName');
-    if(nameEl) nameEl.innerText = state.userInfo.nickName || '未命名';
-    
-    const teamEl = document.getElementById('profileTeam');
-    if(teamEl) teamEl.innerText = state.userInfo.coiName || '未加入战队';
-    
-    // Score is in the personal card right side.
-    // HTML structure: <div class="text-2xl font-black text-primary font-mono leading-none">120</div>
-    // I'll look for that div or expect user to add ID. 
-    // I'll try to select it by context if ID not present.
-    // "当前积分" is in the next div.
-    const scoreLabel = Array.from(document.querySelectorAll('div')).find(el => el.innerText === '当前积分');
-    if(scoreLabel) {
-        const valEl = scoreLabel.previousElementSibling;
-        if(valEl) valEl.innerText = state.userInfo.totalScore;
-    }
-}
-
-function updateMarquee() {
-    const marquee = document.querySelector('.animate-marquee');
-    if(marquee) {
-        const now = Date.now() / 1000;
-        const dif = state.stats.endSecond - now;
-        let timeStr = "已结束";
-        if(dif > 0) timeStr = window.tools.convertSecondsToDHM(dif);
-        
-        marquee.innerText = `当前总题目: ${state.stats.totalAnswerNum}道 | 总里程: ${window.tools.fmtDistanct(state.stats.totalDistance)}km | 总打点数: ${state.stats.totalCp}个 | 距离比赛结束还有 ${timeStr} | 加油!冲鸭!`;
-    }
-}
-
-const metricMap = {
-    team: {
-        score: 'teamCpRs',
-        mileage: 'teamDistanceRs',
-        accuracy: 'teamRightAnswerPerRs',
-        count: 'teamCpRs',
-        lap: 'teamTodayPaceRs'
-    },
-    individual: {
-        score: 'regionCpRs',
-        mileage: 'regionDistanceRs',
-        accuracy: 'regionRightAnswerPerRs',
-        count: 'regionCpRs',
-        lap: 'regionTodayPaceRs'
-    }
-};
-
-function renderLeaderboard() {
-    const container = document.getElementById('leaderboard-container');
-    const key = metricMap[state.currentTab][state.currentMetric];
-    const list = state.rankList[key] || [];
-    
-    let html = '';
-    
-    list.forEach((item, index) => {
-        const rank = index + 1;
-        const isTop3 = index < 3;
-        const isMe = (item.nickName === state.userInfo.nickName);
-        
-        let rankIconHtml = '';
-        if (isTop3) {
-            const colors = ['text-yellow-400', 'text-gray-400', 'text-orange-600'];
-            const icon = state.currentTab === 'team' ? 'fa-trophy' : 'fa-medal';
-            rankIconHtml = `<div class="w-8 flex justify-center shrink-0 mr-1"><i class="fas ${icon} ${colors[index]} text-lg drop-shadow-sm"></i></div>`;
-        } else {
-            rankIconHtml = `<div class="w-8 text-center font-bold text-gray-400 text-sm mr-1">${rank}</div>`;
-        }
-        
-        let avatarHtml = '';
-        if (state.currentTab === 'team') {
-            avatarHtml = `<div class="w-9 h-9 bg-blue-50 rounded-full mr-3 flex items-center justify-center shrink-0 text-primary"><i class="fas fa-user-friends text-base"></i></div>`;
-        } else {
-             const img = item.avatar || (index % 10 + 1);
-             avatarHtml = `<img src="https://i.pravatar.cc/100?img=${img}" class="w-9 h-9 rounded-full mr-3 border-2 ${isTop3 ? 'border-yellow-400' : 'border-transparent'} shrink-0">`;
-        }
-
-        let containerClass = "bg-white rounded-xl py-2 px-3 flex items-center shadow-sm border border-gray-100 relative fade-in-up";
-        let nameClass = "font-bold text-gray-800 text-sm";
-        let valClass = "font-bold text-gray-600 font-mono text-base";
-
-        if (isMe) {
-            containerClass = "bg-blue-50 rounded-xl py-2 px-3 flex items-center shadow-md border-2 border-primary/30 relative overflow-hidden transform scale-[1.02] fade-in-up z-10 my-2";
-            nameClass = "font-bold text-primary text-sm";
-            valClass = "font-bold text-primary font-mono text-lg";
-        }
-        
-        let val = item.val || item.score || item.value || 0; 
-        if(item.teamCp !== undefined) val = item.teamCp; // Specifics based on API key
-        // Actually API usually returns numeric values in fields like `value` or `score` or same as key name (e.g. teamDistanceRs array contains objects with teamDistance)
-        // Let's try to find the value based on metric.
-        if(state.currentMetric === 'mileage') {
-            val = item.teamDistance || item.regionDistance || item.value || 0;
-            val = window.tools.fmtDistanct(val);
-        } else if (state.currentMetric === 'score') {
-             val = item.teamCp || item.regionCp || item.value || 0;
-        } else if (state.currentMetric === 'accuracy') {
-             val = item.teamRightAnswerPer || item.regionRightAnswerPer || 0;
-        } else if (state.currentMetric === 'lap') {
-             val = item.teamTodayPace || item.regionTodayPace || 0;
-             val = window.tools.fmtPace(val);
-        } else {
-             val = item.teamCp || item.regionCp || 0;
-        }
-
-        let unit = '';
-        if(state.currentMetric === 'mileage') unit = 'km';
-        else if (state.currentMetric === 'score') unit = '分';
-        else if (state.currentMetric === 'accuracy') unit = '%';
-        else if (state.currentMetric === 'count') unit = '个';
-
-        let itemName = item.coiName || item.nickName || item.name || '未知';
-
-        html += `
-            <div class="${containerClass}">
-                ${isMe ? '<div class="absolute right-0 top-0 bg-primary text-white text-[8px] px-1.5 py-0.5 rounded-bl-lg">我</div>' : ''}
-                ${rankIconHtml}
-                ${avatarHtml}
-                <div class="flex-1 min-w-0">
-                    <h4 class="${nameClass} truncate">${itemName}</h4>
-                    <p class="text-[10px] text-gray-400">${item.coiName || ''}</p>
-                </div>
-                <div class="text-right">
-                    <div class="${valClass}">${val}</div>
-                    <div class="text-[10px] text-gray-400">${unit}</div>
-                </div>
-            </div>
-        `;
-    });
-    
-    container.innerHTML = html || '<div class="text-center text-gray-400 py-10">暂无数据</div>';
-}
-
-// Exposed UI Functions
-
-window.switchMainTab = function(type) {
-    state.currentTab = type;
-    updateTabUI();
-    renderLeaderboard();
-};
-
-window.switchMetric = function(metric) {
-    state.currentMetric = metric;
-    updateMetricUI();
-    renderLeaderboard();
-};
-
-function updateTabUI() {
-    const tTeam = document.getElementById('tab-team');
-    const tInd = document.getElementById('tab-ind');
-    const active = "flex-1 py-2 rounded-full text-sm font-bold text-center transition-all duration-300 bg-white text-primary shadow-sm";
-    const inactive = "flex-1 py-2 rounded-full text-sm font-bold text-center transition-all duration-300 text-gray-500 hover:text-gray-700";
-    
-    if(state.currentTab === 'team') {
-        tTeam.className = active;
-        tInd.className = inactive;
-    } else {
-        tTeam.className = inactive;
-        tInd.className = active;
-    }
-}
-
-function updateMetricUI() {
-     document.querySelectorAll('.metric-btn').forEach(btn => {
-         btn.className = 'metric-btn bg-white text-gray-500 border border-gray-200 px-3 py-1 rounded-full text-xs font-bold shrink-0 active:bg-gray-50 transition-colors';
-     });
-     const activeBtn = document.getElementById(`metric-${state.currentMetric}`);
-     if(activeBtn) activeBtn.className = 'metric-btn bg-primary text-white px-3 py-1 rounded-full text-xs shadow-md shadow-blue-100 font-bold shrink-0 relative overflow-visible transition-colors';
-}
-
-// Drawer
-window.openDrawer = function() {
-    document.getElementById('drawer-backdrop').classList.remove('hidden');
-    document.getElementById('drawer').classList.remove('translate-y-full');
-};
-window.closeDrawer = function() {
-    document.getElementById('drawer-backdrop').classList.add('hidden');
-    document.getElementById('drawer').classList.add('translate-y-full');
-};
-function renderDrawer() {
-    const drawerContent = document.querySelector('#drawer .space-y-6');
-    // We can populate map options here
-}
-window.triggerJump = function(target) {
-    const map = state.mapList.find(m => m.mapName === target);
-    if(map) {
-        if(state.ocaId !== map.ocaId) {
-            state.ocaId = map.ocaId;
-            uni.setStorageSync(`rank-tpl-style3-map-${state.ecId}`, state.ocaId);
-            matchRsDetailQuery();
-            fetchRankDetail();
-        }
-    } else {
-        // If it's '高德地图' etc.
-        if(target.includes('地图')) {
-             alert("正在打开导航:" + target);
-        } else {
-             // Assume map name
-             const map2 = state.mapList.find(m => m.mapName === target);
-             if(map2) {
-                  state.ocaId = map2.ocaId;
-                  uni.setStorageSync(`rank-tpl-style3-map-${state.ecId}`, state.ocaId);
-                  matchRsDetailQuery();
-                  fetchRankDetail();
-             }
-        }
-    }
-    closeDrawer();
-};
-
-// Info Modal
-window.openInfoModal = function() { document.getElementById('infoModal').classList.remove('hidden'); }
-window.closeInfoModal = function() { document.getElementById('infoModal').classList.add('hidden'); }
-
-// Edit Modal
-window.openEditModal = function() {
-    const editNameInput = document.getElementById('editNameInput');
-    const selectedTeamText = document.getElementById('selectedTeamText');
-    
-    editNameInput.value = state.userInfo.nickName;
-    selectedTeamText.innerHTML = `<i class="fas fa-user-friends text-primary"></i> ${state.userInfo.coiName || '选择战队'}`;
-    
-    document.getElementById('editProfileModal').classList.remove('hidden');
-    
-    // Populate teams if empty (reuse mapList or fetch teams if different API? 
-    // Actually teams come from apiOnlineMcSignUpDetail which we might need to call if we want to allow changing teams.
-    // For simplicity, assuming mapList contains teams or we need to fetch cois.
-    // The signup page fetches apiOnlineMcSignUpDetail. Let's do that here too if needed.
-    getOnlineMcSignUpDetail(); 
-};
-window.closeEditModal = function() {
-    document.getElementById('editProfileModal').classList.add('hidden');
-    window.closeDropdown();
-};
-
-// Fetch teams for dropdown
-function getOnlineMcSignUpDetail() {
-    uni.request({
-        url: window.apiOnlineMcSignUpDetail,
-        header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
-        method: "POST",
-        data: { mcId: state.mcId },
-        success: (res) => {
-            const data = res.data.data;
-            if(data) {
-                const teams = data.coiRs;
-                renderEditDropdown(teams);
-            }
-        }
-    });
-}
-function renderEditDropdown(teams) {
-    const ul = document.querySelector('#dropdownMenu ul');
-    if(!ul) return;
-    let html = '';
-    teams.forEach(team => {
-        html += `<li onclick="selectEditOption('${team.coiId}', '${team.coiName}')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between border-b border-gray-50"><span class="font-bold flex items-center gap-2"><i class="fas fa-user-friends text-blue-200"></i> ${team.coiName}</span></li>`;
-    });
-    ul.innerHTML = html;
-}
-
-window.toggleDropdown = function(event) {
-    event.stopPropagation();
-    const dropdownMenu = document.getElementById('dropdownMenu');
-    const dropdownArrow = document.getElementById('dropdownArrow');
-    if (dropdownMenu.classList.contains('hidden')) {
-        dropdownMenu.classList.remove('hidden');
-        setTimeout(() => dropdownMenu.classList.add('dropdown-enter-active'), 10);
-        dropdownArrow.style.transform = 'rotate(180deg)';
-    } else { window.closeDropdown(); }
-};
-window.closeDropdown = function() {
-    const dropdownMenu = document.getElementById('dropdownMenu');
-    const dropdownArrow = document.getElementById('dropdownArrow');
-    dropdownMenu.classList.remove('dropdown-enter-active');
-    dropdownArrow.style.transform = 'rotate(0deg)';
-    setTimeout(() => dropdownMenu.classList.add('hidden'), 200);
-};
-window.closeDropdownOnClickOutside = function(event) {
-    if (!document.getElementById('editProfileModal').classList.contains('hidden')) {
-        const btn = document.getElementById('dropdownBtn');
-        const menu = document.getElementById('dropdownMenu');
-        if (menu && !menu.contains(event.target) && btn && !btn.contains(event.target)) {
-            window.closeDropdown();
-        }
-    }
-};
-let editingCoiId = 0;
-window.selectEditOption = function(id, name) {
-    editingCoiId = id;
-    document.getElementById('selectedTeamText').innerHTML = `<i class="fas fa-user-friends text-primary"></i> ${name}`;
-    window.closeDropdown();
-};
-
-window.saveProfile = function() {
-    const newName = document.getElementById('editNameInput').value;
-    if(!newName) { uni.showToast({title:'请输入昵称',icon:'none'}); return; }
-    
-    // Call API
-    uni.request({
-        url: window.apiOnlineMcSignUp,
-        header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
-        method: "POST",
-        data: {
-            mcId: state.mcId,
-            coiId: editingCoiId || 0, // If 0, maybe keep old? API requires valid ID usually.
-            selectTeam: 0,
-            nickName: newName
-        },
-        success: (res) => {
-            if(window.checkResCode(res)) {
-                uni.showToast({title:'修改成功',icon:'none'});
-                window.closeEditModal();
-                matchRsDetailQuery(); // Refresh
-            }
-        }
-    });
-};
-
-document.addEventListener('DOMContentLoaded', () => {
-    initRankListPage();
-});

+ 0 - 264
card/pages/tpl/style3/new/js/logic-signup.js

@@ -1,264 +0,0 @@
-var state = {
-    token: "",
-    ecId: 0,
-    from: "",
-    mcId: 0,
-    mcName: "",
-    mcType: 0,
-    beginSecond: null,
-    endSecond: null,
-    coiId: 0,
-    coiName: "",
-    teamNum: 0,
-    nickName: "",
-    coiRs: [],
-    
-    configParam: {
-        labelName: "昵称",
-        labelOrg: "组织",
-        subTitle: ""
-    },
-    
-    introduce: { title: "", content: "" },
-    activityRules: { title: "", content: "" },
-    
-    mcState: 0
-};
-
-function initSignupPage() {
-    const params = new URLSearchParams(window.location.search);
-    state.token = params.get('token') || '';
-    state.ecId = params.get('id') || 0;
-    state.from = params.get('from') || '';
-    
-    window.cardfunc.init(state.token, state.ecId);
-    window.cardfunc.getCardConfig(onConfigLoaded);
-    
-    // Bind events
-    // Dropdown logic is already in HTML script, but we can enhance or replace it.
-    // For now, I'll assume the HTML script functions are available or I should override them 
-    // if I want to connect them to state.
-    // The HTML script defines `selectOption` globally. I should probably piggyback on that or replace it.
-    
-    // Let's expose necessary functions to window so HTML can call them
-    window.selectOption = selectOption;
-    window.openConfirm = openConfirm;
-}
-
-function onConfigLoaded(config) {
-    // Load logic from signup.vue loadConfig
-    const pageConfig = window.cardfunc.parseCardConfig(config['signup']);
-    if(pageConfig) {
-        // Load params
-        const param = pageConfig.param;
-        if (param) {
-            if (param.labelName) state.configParam.labelName = param.labelName;
-            if (param.labelOrg) state.configParam.labelOrg = param.labelOrg;
-            if (param.subTitle) state.configParam.subTitle = param.subTitle;
-        }
-        
-        // Load introduce/rules if needed to display (HTML structure is static, but content could be dynamic)
-        // The new UI seems to have static "Match Intro". 
-        // If we want to use dynamic intro, we need to inject it.
-        // For now, let's stick to the static UI as per "migration to new UI" request, 
-        // unless the new UI requires dynamic text. The HTML provided has hardcoded intro.
-        // I will leave it static for now to preserve the new design.
-    }
-    
-    // Fetch Data
-    getCardDetailQuery();
-}
-
-function getCardDetailQuery() {
-    uni.request({
-        url: window.apiCardDetailQuery,
-        header: {
-            "Content-Type": "application/x-www-form-urlencoded",
-            "token": state.token
-        },
-        method: "POST",
-        data: { ecId: state.ecId },
-        success: (res) => {
-            const data = res.data.data;
-            if(data) {
-                state.mcType = data.mcType;
-                state.mcId = data.mcId;
-                state.mcName = data.mcName;
-                state.beginSecond = data.beginSecond;
-                state.endSecond = data.endSecond;
-                state.coiId = data.coiId;
-                state.coiName = data.coiName;
-                state.teamNum = data.teamNum;
-                state.nickName = data.nickName;
-                
-                state.mcState = window.tools.checkMcState(state.beginSecond, state.endSecond);
-                
-                // Update Time UI
-                updateTimeUI();
-                
-                // Fetch Dropdown Options
-                getOnlineMcSignUpDetail();
-                
-                // Pre-fill form if data exists
-                if(state.nickName) {
-                    document.getElementById('userName').value = state.nickName;
-                }
-                if(state.coiId > 0 && state.coiName) {
-                    // We will update dropdown after fetching details
-                }
-            }
-        }
-    });
-}
-
-function getOnlineMcSignUpDetail() {
-    uni.request({
-        url: window.apiOnlineMcSignUpDetail,
-        header: {
-            "Content-Type": "application/x-www-form-urlencoded",
-            "token": state.token,
-        },
-        method: "POST",
-        data: { mcId: state.mcId },
-        success: (res) => {
-            const data = res.data.data;
-            if(data) {
-                state.coiRs = data.coiRs;
-                if (!state.nickName && data.name) {
-                    state.nickName = data.name;
-                    document.getElementById('userName').value = state.nickName;
-                }
-                renderDropdown();
-                
-                // If user already selected a team (re-entering page)
-                if(state.coiId > 0) {
-                    // Find team name if not set
-                    const team = state.coiRs.find(t => t.coiId == state.coiId);
-                    if(team) selectOption(team.coiId, team.coiName, true); // true = skip UI toggle
-                }
-            }
-        }
-    });
-}
-
-function updateTimeUI() {
-    // Format: 11.28 08:00 - 11.30 18:00
-    const timeStr = window.tools.fmtMcTime2(state.beginSecond, state.endSecond);
-    // Need to find the element. The HTML has "11.28 08:00 - 11.30 18:00" hardcoded.
-    // I'll search for the element containing the time.
-    // The HTML structure: <span class="font-bold text-gray-800 font-mono truncate">...</span>
-    // I should give it an ID in the HTML update step, but for logic file I'll assume I can access it.
-    // I'll use document.querySelector to find it relative to "比赛时间" text or add an ID in Step 10.
-    // Let's assume I will add ID `matchTimeDisplay` to the span.
-    const el = document.getElementById('matchTimeDisplay');
-    if(el) el.innerText = timeStr;
-}
-
-function renderDropdown() {
-    const ul = document.querySelector('#dropdownMenu ul');
-    if(!ul) return;
-    
-    let html = '';
-    state.coiRs.forEach(team => {
-        html += `
-            <li onclick="selectOption('${team.coiId}', '${team.coiName}')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between border-b border-gray-50 last:border-0 transition-colors group/item">
-                <div class="flex items-center">
-                    <i class="fas fa-user-friends text-blue-200 mr-2 group-hover/item:text-primary transition-colors"></i>
-                    <span class="font-bold">${team.coiName}</span>
-                </div>
-                <i class="fas fa-check text-primary opacity-0 check-icon" data-val="${team.coiId}"></i>
-            </li>
-        `;
-    });
-    ul.innerHTML = html;
-}
-
-function selectOption(value, text, skipToggle = false) {
-    state.coiId = value;
-    state.coiName = text;
-    
-    document.getElementById('teamSelect').value = value;
-    const selectedText = document.getElementById('selectedText');
-    selectedText.innerHTML = '<i class="fas fa-user-friends text-primary mr-2"></i>' + text;
-    selectedText.classList.remove('text-gray-400', 'font-normal');
-    selectedText.classList.add('text-gray-800', 'font-bold');
-    
-    // Update check icons
-    const icons = document.querySelectorAll('.check-icon');
-    icons.forEach(icon => {
-        if(icon.getAttribute('data-val') == value) {
-            icon.classList.remove('opacity-0');
-        } else {
-            icon.classList.add('opacity-0');
-        }
-    });
-
-    if(!skipToggle) {
-        // closeDropdown is defined in HTML script. 
-        // Ideally I should move that logic here or call it.
-        // Since I'm injecting logic.js at the bottom, I can call global functions from HTML.
-        if(window.closeDropdown) window.closeDropdown();
-    }
-}
-
-function openConfirm() {
-    if(!window.checkToken(state.token)) return;
-    
-    const nameInput = document.getElementById('userName');
-    state.nickName = nameInput.value.trim();
-    
-    if (!state.nickName) {
-        uni.showToast({ title: `请填写${state.configParam.labelName}`, icon: 'none' });
-        return;
-    }
-    if (!state.coiId) {
-        uni.showToast({ title: `请选择${state.configParam.labelOrg}`, icon: 'none' });
-        return;
-    }
-    
-    document.getElementById('confirmName').innerText = state.nickName;
-    document.getElementById('confirmTeamText').innerText = state.coiName;
-    document.getElementById('confirmModal').classList.remove('hidden');
-}
-
-// Overwrite the HTML's onclick="location.href='#'" for confirm button
-// I'll bind the event dynamically in init or replace the HTML in Step 10.
-// Better to bind dynamically.
-function confirmSignup() {
-    uni.request({
-        url: window.apiOnlineMcSignUp,
-        header: {
-            "Content-Type": "application/x-www-form-urlencoded",
-            "token": state.token,
-        },
-        method: "POST",
-        data: {
-            mcId: state.mcId,
-            coiId: state.coiId,
-            selectTeam: 0, // Default
-            nickName: state.nickName
-        },
-        success: (res) => {
-            if (window.checkResCode(res)) {
-                uni.showToast({ title: '比赛报名成功!', icon: 'none' });
-                setTimeout(() => {
-                    const queryString = `token=${state.token}&id=${state.ecId}`;
-                    window.location.href = `ranklist.html?${queryString}`;
-                }, 1500);
-            }
-        },
-        fail: () => {
-            uni.showToast({ title: '出错了,报名失败', icon: 'none' });
-        }
-    });
-}
-
-// Hook up confirm button
-document.addEventListener('DOMContentLoaded', () => {
-    // Wait for init
-    const confirmBtn = document.querySelector('#confirmModal button:last-child');
-    if(confirmBtn) {
-        confirmBtn.removeAttribute('onclick'); // Remove inline handler
-        confirmBtn.addEventListener('click', confirmSignup);
-    }
-});

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
card/pages/tpl/style3/new/js/multiavatar.min.js


+ 0 - 205
card/pages/tpl/style3/new/js/tools.js

@@ -1,205 +0,0 @@
-(function() {
-    window.tools = {
-        
-        // 判断对象数组中指定属性是否有某个值的数据
-        objArrHasValue(objArr, key, value) {
-            return objArr.find(obj => obj[key] === value) !== undefined;
-        },
-    
-        // 获取对象数组中指定属性为指定值的对象
-        objArrGetObjByValue(objArr, key, value) {
-            return objArr.find(obj => obj[key] === value);
-        },
-        
-        // 对url追加项目版本号,用于页面更新后用户端的强制刷新
-        urlAddVer(url) {
-            let newUrl = url;
-            // Mock version for H5
-            const version_number = '1.0.0';
-    
-            if (newUrl.indexOf('_v=') !== -1) {
-                return newUrl;
-            }
-    
-            if (newUrl.indexOf('?') !== -1) {
-                newUrl += "&_v=" + version_number;
-            } else {
-                newUrl += "?_v=" + version_number;
-            }
-            console.log("[urlAddVer] newUrl", newUrl);
-            return newUrl;
-        },
-    
-        // 导航到彩图奔跑APP内的某个页面或执行APP内部的某些功能
-        appAction(url, actType = "") {
-            console.log("appAction", url);
-            // Safe guard for getApp audio
-            try {
-                if(window.getApp && window.getApp().$audio) {
-                    window.getApp().$audio.destroy();
-                    window.getApp().$audio.pause();
-                }
-            } catch(e) {}
-    
-            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 {
-                // Native bridge simulation or actual call
-                if(window.appAction) {
-                     window.appAction(url);
-                } else {
-                     window.location.href = url;
-                }
-            }
-        },
-    
-        // 格式化赛事时间 09-09 16:48
-        fmtMcTime(timestamp) {
-            var date = new Date(timestamp * 1000); 
-            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());
-            const timeStr = M + D + h + m;
-            return timeStr;
-        },
-    
-        // 获取活动时间 09-09 16:48 至 09-30 16:48
-        getActtime(beginSecond, endSecond) {
-            const acttime = this.fmtMcTime(beginSecond) + " 至 " + this.fmtMcTime(endSecond);
-            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); 
-            const date2 = new Date(timestamp2 * 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;
-            }
-            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;
-                if (dif1 > 0) {
-                    mcState = 0; // 未开始
-                } else if (dif2 > 0) {
-                    mcState = 1; // 进行中
-                } else {
-                    mcState = 2; // 已结束
-                }
-            }
-            return mcState;
-        },
-    
-        // 动态创建<style>标签
-        loadCssCode(cssCode, styleId = "css-custom") {
-            this.removeCssCode(styleId);
-            var style = window.document.createElement("style");
-            style.type = "text/css";
-            style.id = styleId;
-            if (style.styleSheet) {
-                style.styleSheet.cssText = cssCode;
-            } else {
-                style.appendChild(document.createTextNode(cssCode));
-            }
-            document.getElementsByTagName("head")[0].appendChild(style);
-        },
-    
-        // 删除之前动态创建的<style>标签
-        removeCssCode(styleId = "css-custom") {
-            var oldCss = document.getElementById(styleId);
-            if (oldCss != null) {
-                document.getElementsByTagName("head")[0].removeChild(oldCss);
-            }
-        },
-    
-        // uni-data-select 组件,根据选中的值获取对应的文本 (Mock for pure JS)
-        getSelectedText(obj, value) {
-            if(!obj) return '';
-            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}天${hours.toString().padStart(2, '0')}小时`;
-            else
-                return `${hours.toString().padStart(2, '0')}小时${minutes.toString().padStart(2, '0')}分钟`;
-        },
-    
-        // 秒数转换成时分秒
-        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);
-            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')}″`;
-            }
-        },
-        
-        // ... other simple validation helpers can be kept as is ...
-        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
-            }
-        }
-    }
-})();

+ 0 - 187
card/pages/tpl/style3/new/js/uni-compat.js

@@ -1,187 +0,0 @@
-(function() {
-    window.uni = window.uni || {};
-
-    // 1. Network Requests
-    uni.request = function(options) {
-        let headers = options.header || {};
-        let body = undefined;
-        let url = options.url;
-
-        // Handle Content-Type and Body
-        if (options.method === 'POST') {
-            if (!headers['Content-Type']) {
-                headers['Content-Type'] = 'application/json';
-            }
-
-            if (headers['Content-Type'].includes('application/x-www-form-urlencoded')) {
-                if (options.data) {
-                    body = Object.keys(options.data)
-                        .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options.data[key])}`)
-                        .join('&');
-                }
-            } else {
-                // Default to JSON
-                if (options.data) {
-                    body = JSON.stringify(options.data);
-                }
-            }
-        } else if (options.method === 'GET' && options.data) {
-             // Append query params for GET
-            const queryString = Object.keys(options.data)
-                .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options.data[key])}`)
-                .join('&');
-            url += (url.includes('?') ? '&' : '?') + queryString;
-        }
-
-        fetch(url, {
-            method: options.method || 'GET',
-            headers: headers,
-            body: body
-        })
-        .then(response => {
-            // You might want to handle non-200 status codes here if needed, 
-            // but uni.request success callback usually fires even for 4xx/5xx 
-            // with the statusCode in the result.
-            return response.json().then(data => ({
-                data: data,
-                statusCode: response.status,
-                header: response.headers
-            })).catch(() => ({
-                data: null, // or text
-                statusCode: response.status,
-                header: response.headers
-            }));
-        })
-        .then(res => {
-            if (options.success) {
-                options.success(res);
-            }
-        })
-        .catch(err => {
-            console.error('uni.request error:', err);
-            if (options.fail) {
-                options.fail(err);
-            }
-        });
-    };
-
-    // 2. Storage
-    uni.setStorageSync = function(key, data) {
-        try {
-            // uniApp stores data as is, but localStorage only supports strings.
-            // We wrap it to handle types like numbers or booleans correctly upon retrieval if we wanted to match uni strictly,
-            // but JSON.stringify is the standard web way.
-            window.localStorage.setItem(key, JSON.stringify(data));
-        } catch (e) {
-            console.error('setStorageSync error', e);
-        }
-    };
-
-    uni.getStorageSync = function(key) {
-        try {
-            const value = window.localStorage.getItem(key);
-            if (value === null) return ''; // uniApp returns empty string if not found
-            return JSON.parse(value);
-        } catch (e) {
-            return window.localStorage.getItem(key); // Fallback for non-JSON
-        }
-    };
-    
-    uni.setStorage = function(obj) {
-        try {
-            uni.setStorageSync(obj.key, obj.data);
-            if(obj.success) obj.success();
-        } catch(e) {
-            if(obj.fail) obj.fail(e);
-        }
-    }
-
-    uni.getStorage = function(obj) {
-        try {
-            const res = uni.getStorageSync(obj.key);
-            if(obj.success) obj.success({ data: res });
-        } catch(e) {
-            if(obj.fail) obj.fail(e);
-        }
-    }
-    
-    uni.removeStorageSync = function(key) {
-        window.localStorage.removeItem(key);
-    }
-
-    // 3. Interaction
-    uni.showToast = function(options) {
-        // Simple alert or console log for now. 
-        // In a real implementation, creating a DOM element for toast is better.
-        // const title = options.title || '';
-        // const icon = options.icon || 'none';
-        // console.log(`[Toast] ${title} (${icon})`);
-        // // alert(title); // Alert is too intrusive
-        
-        // Create a simple custom toast
-        const toast = document.createElement('div');
-        toast.style.position = 'fixed';
-        toast.style.top = '50%';
-        toast.style.left = '50%';
-        toast.style.transform = 'translate(-50%, -50%)';
-        toast.style.backgroundColor = 'rgba(0,0,0,0.7)';
-        toast.style.color = '#fff';
-        toast.style.padding = '10px 20px';
-        toast.style.borderRadius = '5px';
-        toast.style.zIndex = '9999';
-        toast.innerText = options.title;
-        document.body.appendChild(toast);
-        setTimeout(() => {
-            document.body.removeChild(toast);
-        }, options.duration || 1500);
-    };
-    
-    uni.showLoading = function(options) {
-        // console.log('[Loading]', options);
-    }
-    
-    uni.hideLoading = function() {
-        // console.log('[HideLoading]');
-    }
-
-    // 4. Navigation
-    uni.navigateTo = function(options) {
-        window.location.href = options.url;
-    };
-    
-    // 5. System Info
-    uni.getSystemInfoSync = function() {
-        return {
-            appVersion: '1.0.0', // Mock
-            platform: 'devtools' // Mock
-        };
-    };
-
-    // 6. Global App Object Mock
-    window.getApp = function() {
-        return {
-            globalData: {
-                defaultMatchLogo: 'https://orienteering.beswell.com/card/nanning/logo.png' // Placeholder
-            },
-            $cardconfigType: 'remote'
-        };
-    };
-
-    // 7. App Action Helper (Mocking native bridge)
-    window.appAction = function(url, actType) {
-        console.log('[appAction]', url);
-        if (url.startsWith('action://')) {
-            if (url.includes('to_login')) {
-                alert('请先登录 (模拟跳转)');
-            } else if (url.includes('to_home')) {
-                window.location.href = 'index.html';
-            } else if (url.includes('to_detail')) {
-                 // Extract ID if possible or just alert
-                 alert('正在进入比赛... (模拟跳转)');
-            }
-        } else {
-             window.location.href = url;
-        }
-    };
-
-})();

+ 0 - 146
card/pages/tpl/style3/new/mock_flutter.js

@@ -1,146 +0,0 @@
-(function (window) {
-    'use strict';
-  
-    // Local Logger for MockFlutter - always enabled
-    const Logger = {
-        log: function() {
-            console.log.apply(console, arguments);
-        },
-        warn: function() {
-            console.warn.apply(console, arguments);
-        },
-        error: function() {
-            console.error.apply(console, arguments);
-        },
-        group: function() {
-            console.group.apply(console, arguments);
-        },
-        groupEnd: function() {
-            console.groupEnd.apply(console, arguments);
-        }
-    };
-  
-    Logger.log('%c [MockFlutter] 已加载,当前处于开发调试模式 ', 'background: #42b983; color: white; font-size: 14px; padding: 4px;');
-  
-    // 1. 模拟 uni.postMessage
-    if (!window.uni) {
-      window.uni = {};
-    }
-  
-    window.uni.postMessage = function (message) {
-      const payload = message.data || {};
-      const action = payload.action;
-      const data = payload.data;
-  
-      Logger.group('%c [MockFlutter] 收到 App 指令 ', 'color: #1aad19; font-weight: bold;');
-      Logger.log('Action:', action);
-      Logger.log('Data:', data);
-      Logger.groupEnd();
-  
-      // 模拟具体行为反馈
-      switch (action) {
-        case 'openMap':
-          alert(`[模拟App] 正在打开地图\n纬度: ${data.latitude}\n经度: ${data.longitude}\n名称: ${data.name}`);
-          break;
-        case 'openMatch':
-          alert(`[模拟App] 正在打开赛事详情\nID: ${data.id}\n类型: ${data.type}`);
-          break;
-        case 'openActivityList':
-          alert(`[模拟App] 正在打开活动列表\nID: ${data.id}\n名称: ${data.mapName}`);
-          break;
-        case 'back':
-          Logger.log('[模拟App] 执行返回操作');
-          // alert('[模拟App] 执行返回操作'); // 弹窗太多会烦,这里只打印
-          break;
-        case 'toHome':
-          alert('[模拟App] 执行返回 App 首页');
-          break;
-        case 'toLogin':
-          alert('[模拟App] 执行跳转登录页');
-          break;
-        case 'setTitle':
-          alert(`[模拟App] 设置标题为: ${data.title}`);
-          break;
-        case 'shareWx':
-          alert(`[模拟App] 正在调起微信分享\n标题: ${data.title}\n链接: ${data.url}`);
-          break;
-        case 'launchWxMini':
-          alert(`[模拟App] 正在打开微信小程序\n原始ID: ${data.username}\n路径: ${data.path}`);
-          break;
-        case 'saveImage':
-          alert(`[模拟App] 正在保存图片 (Base64长度: ${data.base64 ? data.base64.length : '0'})`);
-          break;
-        case 'getToken':
-            Logger.log('[模拟App] 收到获取Token请求,1秒后模拟回调...');
-            setTimeout(function() {
-                if (window.Bridge && window.Bridge.receiveToken) {
-                    window.Bridge.receiveToken('MOCK_TOKEN_FOR_BRIDGE_12345');
-                    Logger.log('[模拟App] 已通过 Bridge.receiveToken 回调 Token');
-                } else {
-                    Logger.warn('[模拟App] 无法回调 Token,Bridge.receiveToken 未定义');
-                }
-            }, 1000);
-            break;
-
-        // --- 新增 Mock 方法 ---
-        case 'previewImage':
-            alert(`[模拟App] 预览图片\n当前: ${data.current}\n列表: ${JSON.stringify(data.urls)}`);
-            break;
-        case 'makePhoneCall':
-            alert(`[模拟App] 拨打电话: ${data.phoneNumber}`);
-            break;
-        case 'setClipboardData':
-            alert(`[模拟App] 剪贴板内容已设置: ${data.data}`);
-            break;
-        case 'showToast':
-            Logger.log(`[模拟App] ShowToast: ${data.title} (icon: ${data.icon})`);
-            break;
-        case 'showLoading':
-            Logger.log(`[模拟App] ShowLoading: ${data.title}`);
-            break;
-        case 'hideLoading':
-            Logger.log(`[模拟App] HideLoading`);
-            break;
-        case 'showModal':
-            const confirmed = confirm(`[模拟App] Modal: ${data.title}\n${data.content || ''}`);
-            Logger.log(`[模拟App] Modal result: ${confirmed ? 'Confirm' : 'Cancel'}`);
-            break;
-
-        default:
-          Logger.log(`[模拟App] 收到未知指令: ${action}, 数据: `, data);
-          // alert(`[模拟App] 收到未知指令: ${action}`);
-      }
-    };
-  
-    // 2. 模拟旧版注入对象 (防止报错,并使用可读中文)
-    if (!window.share_wx) {
-        window.share_wx = {
-            postMessage: function(jsonStr) {
-                Logger.log('[MockFlutter] share_wx.postMessage 收到:', jsonStr);
-                alert('[模拟App/旧通道] 微信分享:\n' + jsonStr);
-            }
-        };
-    }
-    
-    if (!window.wx_launch_mini) {
-        window.wx_launch_mini = {
-            postMessage: function(jsonStr) {
-                Logger.log('[MockFlutter] wx_launch_mini.postMessage 收到:', jsonStr);
-                alert('[模拟App/旧通道] 打开小程序:\n' + jsonStr);
-            }
-        };
-    }
-
-    if (!window.save_base64) {
-        window.save_base64 = {
-            postMessage: function(base64Str) {
-                Logger.log('[MockFlutter] save_base64.postMessage 收到 (Base64长度):', base64Str ? base64Str.length : '0');
-                alert(`[模拟App/旧通道] 保存图片 (Base64长度: ${base64Str ? base64Str.length : '0'})`);
-            }
-        };
-    }
-  
-    // 3. 模拟 UserAgent (可选)
-    // 某些逻辑可能会检查 navigator.userAgent,这里可以根据需要注入,但目前 SDK 不依赖这个判断
-  
-  })(window);

+ 0 - 275
card/pages/tpl/style3/new/ranklist.html

@@ -1,275 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <script src="https://cdn.tailwindcss.com"></script>
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
-    <script>
-        tailwind.config = {
-            theme: {
-                extend: {
-                    colors: {
-                        primary: '#3372ac',
-                    },
-                    animation: {
-                        marquee: 'marquee 25s linear infinite',
-                    },
-                    keyframes: {
-                        marquee: {
-                            '0%': { transform: 'translateX(100%)' },
-                            '100%': { transform: 'translateX(-100%)' },
-                        }
-                    }
-                }
-            }
-        }
-    </script>
-    <style>
-        @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800&display=swap');
-        body { font-family: 'Nunito', sans-serif; }
-        .no-scrollbar::-webkit-scrollbar { display: none; }
-        .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
-        .snap-mandatory { scroll-behavior: smooth; }
-        .fade-in-up { animation: fadeInUp 0.4s ease-out forwards; opacity: 0; transform: translateY(10px); }
-        @keyframes fadeInUp { to { opacity: 1; transform: translateY(0); } }
-        
-        .dropdown-enter { opacity: 0; transform: translateY(-10px) scale(0.95); }
-        .dropdown-enter-active { opacity: 1; transform: translateY(0) scale(1); transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); }
-    </style>
-</head>
-<body class="bg-slate-50 relative pb-24" onclick="closeDropdownOnClickOutside(event)">
-
-    <!-- 1. 顶部 Header -->
-    <div class="relative w-full h-[260px]">
-        <img src="https://orienteering.beswell.com/card/nanning/cardtop1122-2.jpg" 
-             class="w-full h-full object-cover shadow-md" alt="Header BG">
-        <div class="absolute inset-0 bg-gradient-to-b from-primary/60 via-transparent to-black/30"></div>
-
-        <div class="absolute top-0 w-full pt-10 px-5 flex justify-between items-center z-10 text-white">
-            <button class="bg-black/20 backdrop-blur-sm p-2 rounded-full w-9 h-9 flex items-center justify-center active:scale-90 transition">
-                <i class="fas fa-chevron-left"></i>
-            </button>
-            <h1 class="text-lg font-bold tracking-wider shadow-sm">实时赛况</h1>
-            <button onclick="openInfoModal()" class="bg-black/20 backdrop-blur-sm px-3 py-1.5 rounded-full text-xs font-semibold flex items-center gap-1 active:scale-90 transition">
-                <i class="fas fa-question-circle"></i> 说明
-            </button>
-        </div>
-
-        <!-- 底部公告跑马灯 -->
-        <div class="absolute bottom-[35px] w-full z-10 px-4">
-            <div class="bg-black/40 backdrop-blur-md rounded-full py-1.5 px-3 flex items-center gap-2 overflow-hidden border border-white/10 shadow-lg">
-                <i class="fas fa-bullhorn text-yellow-300 text-xs shrink-0"></i>
-                <div class="flex-1 overflow-hidden relative h-4">
-                    <div class="absolute whitespace-nowrap text-[10px] text-white animate-marquee leading-4">
-                        当前总题目: 1500道 | 总里程: 5024km | 总打点数: 8900个 | 距离比赛结束还有 1天 5小时 | 加油!冲鸭!
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- 2. 个人信息卡 -->
-    <div class="px-4 -mt-8 relative z-20">
-        <div class="bg-white rounded-xl shadow-lg p-4 flex justify-between items-center border border-white/50">
-            <div class="flex items-center gap-3">
-                <div class="relative">
-                    <img src="https://i.pravatar.cc/100?img=12" class="w-12 h-12 rounded-full border-2 border-slate-100">
-                    <div class="absolute -bottom-1 -right-1 bg-primary text-white text-[8px] px-1.5 py-0.5 rounded-full border border-white">Lv.3</div>
-                </div>
-                <div>
-                    <!-- 昵称 & 编辑按钮 -->
-                    <h2 class="font-bold text-gray-800 text-sm flex items-center gap-1">
-                        <span id="profileName">奔跑的蜗牛</span>
-                        <button onclick="openEditModal()" class="text-xs text-primary/60 p-1 hover:text-primary transition-colors"><i class="fas fa-pen"></i></button>
-                    </h2>
-                    <!-- 战队 -->
-                    <p class="text-xs text-gray-500 mt-0.5 flex items-center gap-1">
-                        <i class="fas fa-user-friends text-blue-300 text-[10px]"></i>
-                        <span id="profileTeam">飞虎队</span>
-                    </p>
-                </div>
-            </div>
-            <div class="text-center pl-4 border-l border-gray-100">
-                <div id="userScore" class="text-2xl font-black text-primary font-mono leading-none">120</div>
-                <div class="text-[10px] text-gray-400 mt-1">当前积分</div>
-            </div>
-        </div>
-    </div>
-
-    <!-- 3. 排行榜 Tabs -->
-    <div class="mt-6 px-4">
-        
-        <!-- 主榜单切换:轻量化分段控制器风格 -->
-        <div class="bg-slate-100 p-1 rounded-full flex gap-1 mb-4 relative shadow-inner">
-            <button onclick="switchMainTab('team')" id="tab-team" 
-                    class="flex-1 py-2 rounded-full text-sm font-bold text-center transition-all duration-300 bg-white text-primary shadow-sm">
-                团队榜
-            </button>
-            <button onclick="switchMainTab('individual')" id="tab-ind" 
-                    class="flex-1 py-2 rounded-full text-sm font-bold text-center transition-all duration-300 text-gray-500 hover:text-gray-700">
-                个人榜
-            </button>
-        </div>
-        
-        <!-- 维度切换 -->
-        <div class="flex gap-2 justify-center pt-4 overflow-x-auto no-scrollbar pb-2" id="metric-tabs">
-            <button onclick="switchMetric('score')" id="metric-score" class="metric-btn bg-primary text-white px-3 py-1 rounded-full text-xs shadow-md shadow-blue-100 font-bold shrink-0 relative overflow-visible">
-                积分
-                <span id="prize-badge" class="absolute -top-2 -right-2 bg-red-500 text-white text-[9px] px-1.5 py-0.5 rounded-full shadow-sm border border-white z-10 leading-none">奖</span>
-            </button>
-            <button onclick="switchMetric('mileage')" id="metric-mileage" class="metric-btn bg-white text-gray-500 border border-gray-200 px-3 py-1 rounded-full text-xs font-bold shrink-0 active:bg-gray-50 transition-colors">里程</button>
-            <button onclick="switchMetric('accuracy')" id="metric-accuracy" class="metric-btn bg-white text-gray-500 border border-gray-200 px-3 py-1 rounded-full text-xs font-bold shrink-0 active:bg-gray-50 transition-colors">答题正确率</button>
-            <button onclick="switchMetric('count')" id="metric-count" class="metric-btn bg-white text-gray-500 border border-gray-200 px-3 py-1 rounded-full text-xs font-bold shrink-0 active:bg-gray-50 transition-colors">打点数</button>
-            <button onclick="switchMetric('lap')" id="metric-lap" class="metric-btn bg-white text-gray-500 border border-gray-200 px-3 py-1 rounded-full text-xs font-bold shrink-0 active:bg-gray-50 transition-colors">单圈用时</button>
-        </div>
-    </div>
-
-    <!-- 4. 排行榜列表容器 -->
-    <!-- 修改点:space-y-2 (间距变小), 内层渲染 logic 变 p-2 -->
-    <div id="leaderboard-container" class="px-4 mt-1 space-y-2 mb-24 min-h-[300px]"></div>
-
-    <!-- 5. 底部悬浮栏 -->
-    <div class="fixed bottom-0 w-full bg-white border-t border-gray-100 shadow-[0_-5px_20px_rgba(0,0,0,0.05)] px-4 py-3 flex items-center z-40 safe-area-bottom">
-        <button onclick="openDrawer()" class="w-full bg-gradient-to-r from-orange-500 to-red-600 text-white py-3 rounded-full font-bold shadow-lg shadow-orange-100 transform active:scale-[0.98] transition flex items-center justify-center gap-2 text-base">
-            <i class="fas fa-flag-checkered animate-pulse"></i> 进入比赛
-        </button>
-    </div>
-
-    <!-- ================= 模态框区域 ================= -->
-
-    <!-- A. 编辑个人资料模态框 -->
-    <div id="editProfileModal" class="fixed inset-0 z-[60] hidden flex items-center justify-center">
-        <div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onclick="closeEditModal()"></div>
-        <div class="bg-white w-[85%] rounded-2xl p-6 relative z-10 shadow-2xl animate-[fadeInUp_0.3s_ease-out]">
-            <h3 class="font-bold text-lg mb-6 text-gray-800 text-center">修改报名信息</h3>
-            <div class="space-y-5">
-                <div>
-                    <label class="block text-sm font-bold text-gray-500 mb-2">我的昵称</label>
-                    <input type="text" id="editNameInput" value="奔跑的蜗牛" 
-                           class="w-full bg-slate-50 border border-gray-200 rounded-lg px-4 py-2.5 text-sm font-bold text-gray-800 focus:outline-none focus:ring-1 focus:ring-primary focus:bg-white transition-colors">
-                </div>
-                <div>
-                    <label class="block text-sm font-bold text-gray-500 mb-2">所属战队</label>
-                    <div class="relative group">
-                        <button type="button" id="dropdownBtn" onclick="toggleDropdown(event)" 
-                                class="w-full bg-slate-50 border border-gray-200 rounded-lg px-4 py-2.5 text-sm font-bold text-left flex items-center justify-between focus:outline-none focus:ring-1 focus:ring-primary transition-all">
-                            <span id="selectedTeamText" class="text-gray-800 flex items-center gap-2">
-                                <i class="fas fa-user-friends text-primary"></i> 飞虎队
-                            </span>
-                            <i class="fas fa-chevron-circle-down text-gray-400 transition-transform duration-300" id="dropdownArrow"></i>
-                        </button>
-                        <div id="dropdownMenu" class="hidden absolute top-[110%] left-0 w-full bg-white rounded-xl shadow-2xl border border-gray-100 overflow-hidden z-50 dropdown-enter max-h-48 overflow-y-auto">
-                            <ul class="text-sm text-gray-700">
-                                <li onclick="selectOption('飞虎队')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between border-b border-gray-50"><span class="font-bold flex items-center gap-2"><i class="fas fa-user-friends text-blue-200"></i> 飞虎队</span></li>
-                                <li onclick="selectOption('火箭队')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between border-b border-gray-50"><span class="font-bold flex items-center gap-2"><i class="fas fa-user-friends text-blue-200"></i> 火箭队</span></li>
-                                <li onclick="selectOption('摸鱼队')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between border-b border-gray-50"><span class="font-bold flex items-center gap-2"><i class="fas fa-user-friends text-blue-200"></i> 摸鱼队</span></li>
-                                <li onclick="selectOption('汪汪队')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between"><span class="font-bold flex items-center gap-2"><i class="fas fa-user-friends text-blue-200"></i> 汪汪队</span></li>
-                            </ul>
-                        </div>
-                    </div>
-                </div>
-            </div>
-            <div class="flex gap-3 mt-8">
-                <button onclick="closeEditModal()" class="flex-1 py-2.5 rounded-full border border-gray-200 text-gray-500 font-bold text-sm">取消</button>
-                <button onclick="saveProfile()" class="flex-1 py-2.5 rounded-full bg-primary text-white font-bold text-sm shadow-lg shadow-blue-200">保存修改</button>
-            </div>
-        </div>
-    </div>
-
-    <!-- B. 说明模态框 -->
-    <div id="infoModal" class="fixed inset-0 z-50 hidden transition-opacity duration-300">
-        <div class="absolute inset-0 bg-slate-900/70 backdrop-blur-sm" onclick="closeInfoModal()"></div>
-        <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[90%] bg-white rounded-3xl p-6 shadow-2xl">
-            <button onclick="closeInfoModal()" class="absolute -top-12 right-0 text-white/80 hover:text-white w-10 h-10 border border-white/30 rounded-full flex items-center justify-center">
-                <i class="fas fa-times text-lg"></i>
-            </button>
-            <h3 class="text-center font-bold text-xl mb-4 text-primary border-b border-gray-100 pb-3">
-                <i class="fas fa-book-open mr-2"></i>比赛规则说明
-            </h3>
-            <div id="modalSlider" class="overflow-x-auto flex snap-x snap-mandatory no-scrollbar space-x-4 pb-2" onscroll="updateDots()">
-                <div class="snap-center shrink-0 w-full bg-blue-50 rounded-2xl p-5 text-center border border-blue-100">
-                    <div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3"><i class="fas fa-map-marker-alt text-xl text-primary"></i></div>
-                    <h4 class="font-bold text-base text-gray-800 mb-1">1. 定向寻宝</h4>
-                    <p class="text-gray-600 text-xs leading-relaxed">比赛设置12个隐藏打卡点。</p>
-                </div>
-                <div class="snap-center shrink-0 w-full bg-blue-50 rounded-2xl p-5 text-center border border-blue-100">
-                    <div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3"><i class="fas fa-mobile-alt text-xl text-primary"></i></div>
-                    <h4 class="font-bold text-base text-gray-800 mb-1">2. 扫码打卡</h4>
-                    <p class="text-gray-600 text-xs leading-relaxed">到达点位后,使用APP扫描二维码。</p>
-                </div>
-                <div class="snap-center shrink-0 w-full bg-blue-50 rounded-2xl p-5 text-center border border-blue-100">
-                    <div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3"><i class="fas fa-medal text-xl text-primary"></i></div>
-                    <h4 class="font-bold text-base text-gray-800 mb-1">3. 完赛奖励</h4>
-                    <p class="text-gray-600 text-xs leading-relaxed">前50名有精美礼品!加油!</p>
-                </div>
-            </div>
-            <div class="flex justify-center gap-2 mt-4" id="dotsContainer">
-                <div class="dot w-6 h-1.5 rounded-full bg-primary transition-all duration-300"></div>
-                <div class="dot w-1.5 h-1.5 rounded-full bg-gray-300 transition-all duration-300"></div>
-                <div class="dot w-1.5 h-1.5 rounded-full bg-gray-300 transition-all duration-300"></div>
-            </div>
-        </div>
-    </div>
-
-    <!-- C. 上滑抽屉 -->
-    <div id="drawer-backdrop" class="fixed inset-0 bg-black/50 z-50 hidden transition-opacity" onclick="closeDrawer()"></div>
-    <div id="drawer" class="fixed bottom-0 left-0 w-full bg-white rounded-t-3xl p-6 z-50 transform translate-y-full transition-transform duration-300 ease-out safe-area-bottom">
-        <div class="w-12 h-1.5 bg-gray-200 rounded-full mx-auto mb-6"></div>
-        <h3 class="font-bold text-lg mb-5 text-gray-800">前往比赛场地</h3>
-        <div class="space-y-6">
-            <div>
-                <button onclick="triggerJump('兴隆山校区')" class="w-full border-2 border-primary bg-blue-50 text-primary py-4 rounded-xl font-bold text-sm flex items-center justify-between px-6 shadow-sm active:scale-[0.98] transition-transform">
-                    <div class="flex items-center gap-3">
-                        <i class="fas fa-map-marked-alt text-2xl"></i>
-                        <div class="text-left">
-                            <div class="text-base font-bold">兴隆山校区</div>
-                            <div class="text-[10px] opacity-70 font-normal">当前比赛区域</div>
-                        </div>
-                    </div>
-                    <i class="fas fa-chevron-right text-lg"></i>
-                </button>
-            </div>
-            <div>
-                <label class="text-xs text-gray-400 font-bold uppercase mb-2 block pl-1">选择导航</label>
-                <div class="bg-gray-50 rounded-2xl p-2 flex flex-col gap-2 border border-gray-100">
-                    <button onclick="triggerJump('高德地图')" class="flex items-center gap-4 w-full bg-white p-3 rounded-xl border border-gray-100 shadow-sm active:bg-gray-50 active:scale-[0.99] transition-transform">
-                        <img src="./gd.png" class="w-8 h-8 object-contain">
-                        <div class="text-left">
-                            <div class="font-bold text-gray-800 text-sm">高德地图</div>
-                            <div class="text-[10px] text-gray-400">推荐路线</div>
-                        </div>
-                        <i class="fas fa-location-arrow ml-auto text-primary"></i>
-                    </button>
-                    <button onclick="triggerJump('百度地图')" class="flex items-center gap-4 w-full bg-white p-3 rounded-xl border border-gray-100 shadow-sm active:bg-gray-50 active:scale-[0.99] transition-transform">
-                        <img src="./bd.png" class="w-8 h-8 object-contain">
-                        <div class="text-left">
-                            <div class="font-bold text-gray-800 text-sm">百度地图</div>
-                            <div class="text-[10px] text-gray-400">备选方案</div>
-                        </div>
-                        <i class="fas fa-location-arrow ml-auto text-primary"></i>
-                    </button>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <script src="js/uni-compat.js"></script>
-    <script src="js/define.js"></script>
-    <script src="js/api.js"></script>
-    <script src="js/tools.js"></script>
-    <script src="js/cardfunc.js"></script>
-    <script src="js/logic-ranklist.js"></script>
-    <script>
-        // UI Logic that doesn't need state
-        const slider = document.getElementById('modalSlider');
-        const dots = document.querySelectorAll('.dot');
-        function updateDots() {
-            const index = Math.round(slider.scrollLeft / slider.offsetWidth);
-            dots.forEach((dot, i) => {
-                 dot.className = (i === index) ? 'dot w-6 h-1.5 rounded-full bg-primary transition-all duration-300' : 'dot w-1.5 h-1.5 rounded-full bg-gray-300 transition-all duration-300';
-            });
-        }
-    </script>
-</body>
-</html>

+ 0 - 304
card/pages/tpl/style3/new/signup.html

@@ -1,304 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <script src="https://cdn.tailwindcss.com"></script>
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
-    <script>
-        tailwind.config = {
-            theme: {
-                extend: {
-                    colors: {
-                        primary: '#3372ac',
-                    }
-                }
-            }
-        }
-    </script>
-    <style>
-        @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800&display=swap');
-        body { font-family: 'Nunito', sans-serif; }
-        .no-scrollbar::-webkit-scrollbar { display: none; }
-        .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
-        .snap-mandatory { scroll-behavior: smooth; }
-        
-        .dropdown-enter {
-            opacity: 0;
-            transform: translateY(-10px) scale(0.95);
-        }
-        .dropdown-enter-active {
-            opacity: 1;
-            transform: translateY(0) scale(1);
-            transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
-        }
-    </style>
-</head>
-<body class="bg-slate-50 relative pb-24" onclick="closeDropdownOnClickOutside(event)">
-
-    <!-- 1. 顶部 Header -->
-    <div class="relative w-full h-[260px]">
-        <img src="https://orienteering.beswell.com/card/nanning/cardtop1122-2.jpg" 
-             class="w-full h-full object-cover shadow-md" alt="Header BG">
-        <div class="absolute inset-0 bg-gradient-to-b from-primary/60 via-transparent to-black/5"></div>
-        <div class="absolute top-0 w-full pt-10 px-5 flex justify-between items-center z-10 text-white">
-            <button class="bg-black/20 backdrop-blur-sm p-2 rounded-full w-9 h-9 flex items-center justify-center active:scale-90 transition">
-                <i class="fas fa-chevron-left"></i>
-            </button>
-            <h1 class="text-lg font-bold tracking-wider shadow-sm">赛事报名</h1>
-            <button onclick="openModal()" class="bg-black/20 backdrop-blur-sm px-3 py-1.5 rounded-full text-xs font-semibold flex items-center gap-1 active:scale-90 transition">
-                <i class="fas fa-question-circle"></i> 说明
-            </button>
-        </div>
-    </div>
-
-    <!-- 2. 主体内容 -->
-    <div class="relative z-20 px-4">
-        
-        <!-- A. 比赛时间 (字体已放大 text-xs -> text-sm) -->
-        <div class="bg-white rounded-full shadow-lg py-2 px-4 -mt-6 mb-5 mx-2 flex items-center justify-between border border-white/60 relative z-30">
-            <!-- 这里将 text-xs 改为了 text-sm -->
-            <div class="flex items-center gap-2 w-full text-sm overflow-hidden whitespace-nowrap">
-                <div class="bg-blue-50 text-primary w-6 h-6 rounded-full flex items-center justify-center shrink-0">
-                    <i class="fas fa-clock text-xs"></i>
-                </div>
-                <!-- 删除了 scale-90,保持原大 -->
-                <span class="text-gray-400 font-bold uppercase origin-left shrink-0">比赛时间</span>
-                <span class="text-gray-300">|</span>
-                <span id="matchTimeDisplay" class="font-bold text-gray-800 font-mono truncate">11.28 08:00 - 11.30 18:00</span>
-            </div>
-        </div>
-
-        <!-- B. 报名表单 -->
-        <div class="bg-white rounded-xl shadow-sm p-5 mb-5 space-y-5 relative z-40">
-            <!-- 昵称行 -->
-            <div class="flex items-center gap-3">
-                <label class="w-16 text-sm font-bold text-gray-500 text-right">报名昵称</label>
-                <input type="text" id="userName" placeholder="请输入您的昵称" 
-                       class="flex-1 bg-slate-50 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:bg-blue-50 focus:ring-1 focus:ring-primary/50 transition-all text-gray-800 font-bold placeholder-gray-400">
-            </div>
-
-            <!-- 战队行 -->
-            <div class="flex items-center gap-3 relative">
-                <label class="w-16 text-sm font-bold text-gray-500 text-right">选择战队</label>
-                
-                <div class="relative flex-1 group">
-                    <button type="button" id="dropdownBtn" onclick="toggleDropdown(event)" 
-                            class="w-full bg-slate-50 rounded-lg px-3 py-2.5 text-sm font-bold text-left flex items-center justify-between focus:outline-none focus:ring-1 focus:ring-primary/50 transition-all active:bg-blue-50">
-                        <span id="selectedText" class="text-gray-400 font-normal flex items-center">
-                            请选择或搜索...
-                        </span>
-                        <i class="fas fa-chevron-circle-down text-primary/70 transition-transform duration-300" id="dropdownArrow"></i>
-                    </button>
-
-                    <input type="hidden" id="teamSelect" value="">
-
-                    <div id="dropdownMenu" class="hidden absolute top-[110%] left-0 w-full bg-white rounded-xl shadow-2xl border border-gray-100 overflow-hidden origin-top z-50 dropdown-enter">
-                        <ul class="text-sm text-gray-700 max-h-48 overflow-y-auto no-scrollbar">
-                            <li onclick="selectOption('飞虎队', '飞虎队')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between border-b border-gray-50 last:border-0 transition-colors group/item">
-                                <div class="flex items-center">
-                                    <i class="fas fa-user-friends text-blue-200 mr-2 group-hover/item:text-primary transition-colors"></i>
-                                    <span class="font-bold">飞虎队 (兴隆山)</span>
-                                </div>
-                                <i class="fas fa-check text-primary opacity-0 check-icon"></i>
-                            </li>
-                            <li onclick="selectOption('火箭队', '火箭队')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between border-b border-gray-50 last:border-0 transition-colors group/item">
-                                <div class="flex items-center">
-                                    <i class="fas fa-user-friends text-blue-200 mr-2 group-hover/item:text-primary transition-colors"></i>
-                                    <span class="font-bold">火箭队 (中心校区)</span>
-                                </div>
-                                <i class="fas fa-check text-primary opacity-0 check-icon"></i>
-                            </li>
-                            <li onclick="selectOption('摸鱼队', '摸鱼队')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between border-b border-gray-50 last:border-0 transition-colors group/item">
-                                <div class="flex items-center">
-                                    <i class="fas fa-user-friends text-blue-200 mr-2 group-hover/item:text-primary transition-colors"></i>
-                                    <span class="font-bold">摸鱼队 (快乐组)</span>
-                                </div>
-                                <i class="fas fa-check text-primary opacity-0 check-icon"></i>
-                            </li>
-                             <li onclick="selectOption('汪汪队', '汪汪队')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group/item">
-                                <div class="flex items-center">
-                                    <i class="fas fa-user-friends text-blue-200 mr-2 group-hover/item:text-primary transition-colors"></i>
-                                    <span class="font-bold">汪汪队 (教职工)</span>
-                                </div>
-                                <i class="fas fa-check text-primary opacity-0 check-icon"></i>
-                            </li>
-                        </ul>
-                    </div>
-                </div>
-            </div>
-        </div>
-
-        <!-- C. 赛事介绍 -->
-        <div class="bg-white rounded-xl shadow-sm p-5 pb-8 relative z-10">
-            <h3 class="font-bold text-base text-primary mb-4 flex items-center gap-2">
-                <span class="w-1 h-4 bg-primary rounded-full"></span> 赛事介绍
-            </h3>
-            
-            <div class="text-xs text-gray-600 space-y-3 leading-relaxed">
-                <p class="font-bold text-sm text-gray-900 border-b border-gray-100 pb-2">
-                    南宁学院40周年校庆 (暨「智跑南院」数字定向赛)
-                </p>
-                <div class="space-y-2">
-                    <div class="flex flex-col sm:flex-row gap-1">
-                        <span class="font-bold text-primary shrink-0">一、主办单位 /</span>
-                        <span class="text-gray-700">共青团南宁学院委员会、教育学院</span>
-                    </div>
-                    <div class="flex flex-col sm:flex-row gap-1">
-                        <span class="font-bold text-primary shrink-0">二、承办单位 /</span>
-                        <span class="text-gray-700">各二级学院团委</span>
-                    </div>
-                    <div class="flex flex-col sm:flex-row gap-1">
-                        <span class="font-bold text-primary shrink-0">三、协办单位 /</span>
-                        <span class="text-gray-700">南宁学院学生会、青年志愿者协会、教育学院大学生竞技中心、教育学院大学生运动健康促进中心</span>
-                    </div>
-                    <div class="flex flex-col sm:flex-row gap-1">
-                        <span class="font-bold text-primary shrink-0">四、参赛人员 /</span>
-                        <span class="text-gray-700">全校在校学生</span>
-                    </div>
-                    <div class="flex flex-col sm:flex-row gap-1">
-                        <span class="font-bold text-primary shrink-0">五、赞助单位 /</span>
-                        <span class="text-gray-700">一食堂三楼广西桂师傅餐饮管理有限公司</span>
-                    </div>
-                </div>
-                <div class="text-right mt-4 pt-2 font-bold text-primary opacity-90">
-                    共青团南宁学院委员会
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- 底部按钮 -->
-    <div class="fixed bottom-0 w-full bg-white px-4 py-3 shadow-[0_-4px_15px_rgba(0,0,0,0.05)] z-50">
-        <button onclick="openConfirm()" class="w-full bg-gradient-to-r from-orange-500 to-red-600 text-white font-bold text-lg py-2.5 rounded-full shadow-lg shadow-orange-100 active:scale-[0.98] transition-transform flex items-center justify-center gap-2">
-            立即报名 <i class="fas fa-paper-plane text-sm"></i>
-        </button>
-    </div>
-
-    <!-- 说明模态框 -->
-    <div id="infoModal" class="fixed inset-0 z-50 hidden transition-opacity duration-300">
-        <div class="absolute inset-0 bg-slate-900/70 backdrop-blur-sm" onclick="closeModal()"></div>
-        <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[90%] bg-white rounded-3xl p-6 shadow-2xl">
-            <button onclick="closeModal()" class="absolute -top-12 right-0 text-white/80 hover:text-white w-10 h-10 border border-white/30 rounded-full flex items-center justify-center">
-                <i class="fas fa-times text-lg"></i>
-            </button>
-            <h3 class="text-center font-bold text-xl mb-4 text-primary border-b border-gray-100 pb-3">
-                <i class="fas fa-book-open mr-2"></i>比赛规则说明
-            </h3>
-            <div id="modalSlider" class="overflow-x-auto flex snap-x snap-mandatory no-scrollbar space-x-4 pb-2" onscroll="updateDots()">
-                <!-- Slides... -->
-                <div class="snap-center shrink-0 w-full bg-blue-50 rounded-2xl p-5 text-center border border-blue-100">
-                    <div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3">
-                        <i class="fas fa-map-marker-alt text-xl text-primary"></i>
-                    </div>
-                    <h4 class="font-bold text-base text-gray-800 mb-1">1. 定向寻宝</h4>
-                    <p class="text-gray-600 text-xs leading-relaxed">比赛设置12个隐藏打卡点,根据地图指引,规划最佳路线。</p>
-                </div>
-                <div class="snap-center shrink-0 w-full bg-blue-50 rounded-2xl p-5 text-center border border-blue-100">
-                    <div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3">
-                        <i class="fas fa-mobile-alt text-xl text-primary"></i>
-                    </div>
-                    <h4 class="font-bold text-base text-gray-800 mb-1">2. 扫码打卡</h4>
-                    <p class="text-gray-600 text-xs leading-relaxed">到达点位后,使用APP扫描二维码,系统自动记录时间。</p>
-                </div>
-                <div class="snap-center shrink-0 w-full bg-blue-50 rounded-2xl p-5 text-center border border-blue-100">
-                    <div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3">
-                        <i class="fas fa-medal text-xl text-primary"></i>
-                    </div>
-                    <h4 class="font-bold text-base text-gray-800 mb-1">3. 完赛奖励</h4>
-                    <p class="text-gray-600 text-xs leading-relaxed">规定时间内完赛,前50名有精美礼品!加油!</p>
-                </div>
-            </div>
-            <div class="flex justify-center gap-2 mt-4" id="dotsContainer">
-                <div class="dot w-6 h-1.5 rounded-full bg-primary transition-all duration-300"></div>
-                <div class="dot w-1.5 h-1.5 rounded-full bg-gray-300 transition-all duration-300"></div>
-                <div class="dot w-1.5 h-1.5 rounded-full bg-gray-300 transition-all duration-300"></div>
-            </div>
-        </div>
-    </div>
-
-    <!-- 确认模态框 -->
-    <div id="confirmModal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
-        <div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onclick="closeConfirm()"></div>
-        <div class="bg-white w-[80%] rounded-2xl p-6 relative z-10 text-center shadow-xl">
-            <div class="w-14 h-14 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
-                <i class="fas fa-check text-2xl text-green-600"></i>
-            </div>
-            <h3 class="font-bold text-lg mb-2 text-gray-800">确认提交报名?</h3>
-            <div class="bg-slate-50 rounded-lg p-4 mb-6 text-left border border-slate-100">
-                <p class="mb-2"><span class="text-gray-500 text-sm">昵称:</span><span class="font-bold text-gray-800 ml-2" id="confirmName"></span></p>
-                <p><span class="text-gray-500 text-sm">战队:</span><span class="font-bold text-primary ml-2" id="confirmTeamText"></span></p>
-            </div>
-            <div class="flex gap-3">
-                <button onclick="closeConfirm()" class="flex-1 py-2.5 rounded-lg border border-gray-200 text-gray-600 font-bold text-sm">再想想</button>
-                <button onclick="location.href='#'" class="flex-1 py-2.5 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 text-white font-bold text-sm shadow-md">确认提交</button>
-            </div>
-        </div>
-    </div>
-
-    <script src="js/uni-compat.js"></script>
-    <script src="js/define.js"></script>
-    <script src="js/api.js"></script>
-    <script src="js/tools.js"></script>
-    <script src="js/cardfunc.js"></script>
-    <script src="js/logic-signup.js"></script>
-    <script>
-        document.addEventListener('DOMContentLoaded', function() {
-            initSignupPage();
-        });
-
-        const dropdownMenu = document.getElementById('dropdownMenu');
-        const dropdownArrow = document.getElementById('dropdownArrow');
-        const selectedText = document.getElementById('selectedText');
-        const teamInput = document.getElementById('teamSelect');
-        const checkIcons = document.querySelectorAll('.check-icon');
-
-        function toggleDropdown(event) {
-            event.stopPropagation();
-            const isHidden = dropdownMenu.classList.contains('hidden');
-            if (isHidden) {
-                dropdownMenu.classList.remove('hidden');
-                setTimeout(() => dropdownMenu.classList.add('dropdown-enter-active'), 10);
-                dropdownArrow.style.transform = 'rotate(180deg)';
-            } else {
-                closeDropdown();
-            }
-        }
-
-        function closeDropdown() {
-            dropdownMenu.classList.remove('dropdown-enter-active');
-            dropdownArrow.style.transform = 'rotate(0deg)';
-            setTimeout(() => dropdownMenu.classList.add('hidden'), 200);
-        }
-
-        // selectOption is in logic-signup.js
-
-        function closeDropdownOnClickOutside(event) {
-            if (!dropdownMenu.contains(event.target) && event.target.id !== 'dropdownBtn') {
-                closeDropdown();
-            }
-        }
-
-        function openModal() { document.getElementById('infoModal').classList.remove('hidden'); }
-        function closeModal() { document.getElementById('infoModal').classList.add('hidden'); }
-        
-        // openConfirm is in logic-signup.js
-        function closeConfirm() { document.getElementById('confirmModal').classList.add('hidden'); }
-
-        const slider = document.getElementById('modalSlider');
-        const dots = document.querySelectorAll('.dot');
-        function updateDots() {
-            const index = Math.round(slider.scrollLeft / slider.offsetWidth);
-            dots.forEach((dot, i) => {
-                if (i === index) {
-                    dot.classList.remove('w-1.5', 'bg-gray-300');
-                    dot.classList.add('w-6', 'bg-primary');
-                } else {
-                    dot.classList.remove('w-6', 'bg-primary');
-                    dot.classList.add('w-1.5', 'bg-gray-300');
-                }
-            });
-        }
-    </script>
-</body>
-</html>

BIN
card/pages/tpl/style3/new/webfonts/fa-brands-400.woff2


BIN
card/pages/tpl/style3/new/webfonts/fa-regular-400.woff2


BIN
card/pages/tpl/style3/new/webfonts/fa-solid-900.woff2


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác