feat: 培训小程序(Taro+React,OPC AI 移动端)

- 页面:首页/活动/报名/测评/政策/流程/调研/我的
- 自定义 tabBar、骨架屏、后端驱动测评
This commit is contained in:
2026-08-23 22:32:39 +08:00
commit 1f65a1a7a8
51 changed files with 21830 additions and 0 deletions
@@ -0,0 +1,30 @@
import { View, Text } from '@tarojs/components';
import { useState, useEffect } from 'react';
import { parseIso } from '@/utils/booking';
/**
* 倒计时:距目标时间的 天/时/分/秒,每秒刷新。
* props: target (ISO 字符串)。用于活动页「距下一场」与个人中心「下一场活动」。
*/
export default function CountdownBox({ target }) {
const [now, setNow] = useState(Date.now());
useEffect(() => {
const t = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(t);
}, []);
const targetDate = parseIso(target);
const diff = Math.max(0, targetDate ? targetDate.getTime() - now : 0);
const d = Math.floor(diff / 86400000);
const h = Math.floor((diff % 86400000) / 3600000);
const m = Math.floor((diff % 3600000) / 60000);
const s = Math.floor((diff % 60000) / 1000);
const pad = (n) => String(n).padStart(2, '0');
return (
<View className="mk-count-nums">
<View className="mk-count-cell"><Text className="num">{d}</Text><Text className="unit"></Text></View>
<View className="mk-count-cell"><Text className="num">{pad(h)}</Text><Text className="unit"></Text></View>
<View className="mk-count-cell"><Text className="num">{pad(m)}</Text><Text className="unit"></Text></View>
<View className="mk-count-cell"><Text className="num">{pad(s)}</Text><Text className="unit"></Text></View>
</View>
);
}
@@ -0,0 +1,33 @@
import { View } from '@tarojs/components';
import './index.scss';
/**
* 骨架屏组件(纯本地占位,不依赖网络)。
* 用法:<Skeleton.Rows n={3} />、<Skeleton.Box h={140} />、<Skeleton.Line w="60%" />、<Skeleton.Circle s={44} />
* 样式见 index.scss 的 .sk 系列(暗色 + 流光)。
*/
const Skeleton = ({ className = '', style = {}, children }) => (
<View className={className} style={style} aria-hidden="true">{children}</View>
);
Skeleton.Line = ({ w = '100%', h = 12, className = '', style = {} }) => (
<View className={`sk sk-line ${className}`} style={{ height: h, width: w, ...style }} />
);
Skeleton.Box = ({ h = 120, className = '', style = {}, children }) => (
<View className={`sk ${className}`} style={{ height: h, ...style }}>{children}</View>
);
Skeleton.Circle = ({ s = 44, className = '', style = {} }) => (
<View className={`sk sk-round ${className}`} style={{ width: s, height: s, ...style }} />
);
Skeleton.Rows = ({ n = 3, gap = 12, last = '70%', className = '', style = {} }) => (
<View className={className} style={{ display: 'flex', flexDirection: 'column', gap, ...style }}>
{Array.from({ length: n }).map((_, i) => (
<Skeleton.Line key={i} w={i === n - 1 ? last : '100%'} />
))}
</View>
);
export default Skeleton;
@@ -0,0 +1,20 @@
.sk {
position: relative;
overflow: hidden;
background: #1d1d24;
border-radius: 8rpx;
}
.sk::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.06), transparent);
animation: sk-shimmer 1.4s ease-in-out infinite;
}
.sk-round { border-radius: 50%; }
.sk-line { height: 18rpx; }
@keyframes sk-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}