feat: 培训小程序(Taro+React,OPC AI 移动端)
- 页面:首页/活动/报名/测评/政策/流程/调研/我的 - 自定义 tabBar、骨架屏、后端驱动测评
This commit is contained in:
@@ -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%); }
|
||||
}
|
||||
Reference in New Issue
Block a user