Files
hms/docs/design/mp-05-mall-v2.html
iven 09013ab94a feat(mp): 积分商城 V2 重设计 — design-handoff 全流程
- 新增 4 个 UI 组件: PointsCard/ProductCard/CheckinCalendar/CheckinModal
- 商城首页 V2: 积分卡 + 快捷操作 + 分类标签 + 商品网格
- 商品详情 V2: 大图 + 信息卡 + 库存/余额状态 + 底部操作栏
- TabBar 新增商城入口(5 Tab: 首页/健康/商城/助手/我的)
- 设计原型 docs/design/mp-05-mall-v2.html + SPEC.md 交付包
- CLAUDE.md 安全规范加固: 新增 §3.7 安全规范 6 条 + Feature DoD 安全清单扩展
2026-05-22 19:15:41 +08:00

527 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMS 小程序 — 积分商城 V2</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a1a; font-family: -apple-system, 'PingFang SC', sans-serif; display: flex; flex-direction: column; align-items: center; padding: 40px 20px; gap: 24px; }
.page-title { color: #999; font-size: 13px; letter-spacing: 0.15em; text-transform: uppercase; }
.note { color: #666; font-size: 12px; max-width: 800px; text-align: center; line-height: 1.8; }
.screens { display: flex; gap: 48px; flex-wrap: wrap; justify-content: center; align-items: flex-start; }
.screen-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.screen-label { color: #888; font-size: 12px; font-style: italic; }
</style>
</head>
<body>
<div class="page-title">HMS 小程序 · 积分商城 V2</div>
<div class="note">V2 重设计 — 增强积分卡片层级感、新增商品详情页和签到弹窗。温润东方风设计系统。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框(来自 assets/ios_frame.jsx ───
const iosFrameStyles = {
wrapper: { display: 'inline-block', padding: 12, background: '#000', borderRadius: 60, boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)', position: 'relative' },
screen: { position: 'relative', borderRadius: 48, overflow: 'hidden', background: '#fff' },
statusBar: { position: 'absolute', top: 0, left: 0, right: 0, height: 54, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 32px', fontSize: 16, fontWeight: 600, fontFamily: '-apple-system, "SF Pro Text", sans-serif', zIndex: 20, pointerEvents: 'none' },
dynamicIsland: { position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', width: 124, height: 36, background: '#000', borderRadius: 999, zIndex: 30 },
content: { position: 'absolute', top: 54, left: 0, right: 0, bottom: 34, overflow: 'auto' },
homeIndicator: { position: 'absolute', bottom: 10, left: '50%', transform: 'translateX(-50%)', width: 140, height: 5, background: 'rgba(0,0,0,0.3)', borderRadius: 999, zIndex: 10 },
};
function IosFrame({ children, width = 393, height = 852, time = '9:41', battery = 85, darkStatus = false }) {
const c = darkStatus ? '#fff' : '#000';
return (
<div style={iosFrameStyles.wrapper}>
<div style={{ ...iosFrameStyles.screen, width, height }}>
<div style={{ ...iosFrameStyles.statusBar, color: c }}>
<span>{time}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<svg width="16" height="12" viewBox="0 0 16 12" fill="none"><path d="M8 11.5a1 1 0 100-2 1 1 0 000 2z" fill={c}/><path d="M3 7.5a7 7 0 0110 0" stroke={c} strokeWidth="1.3" fill="none" strokeLinecap="round"/><path d="M1 4.5a11 11 0 0114 0" stroke={c} strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7"/></svg>
<div style={{ width: 26, height: 12, border: `1.5px solid ${c}`, borderRadius: 3, padding: 1, position: 'relative' }}>
<div style={{ width: `${battery}%`, height: '100%', background: c, borderRadius: 1 }} />
</div>
</div>
</div>
<div style={iosFrameStyles.dynamicIsland} />
<div style={iosFrameStyles.content}>{children}</div>
<div style={iosFrameStyles.homeIndicator} />
</div>
</div>
);
}
// ─── 设计 Token ───
const T = {
pri: '#C4623A', priL: '#F0DDD4', priD: '#8B3E1F',
bg: '#F5F0EB', card: '#FFFFFF', surface: '#EDE8E2',
tx: '#2D2A26', tx2: '#5A554F', tx3: '#78716C',
bd: '#E8E2DC', bdL: '#F0EBE5',
acc: '#5B7A5E', accL: '#E8F0E8',
wrn: '#C4873A', wrnL: '#FFF3E0',
dan: '#B54A4A', danL: '#FDEAEA',
white: '#FFFFFF',
serif: "Georgia, 'Times New Roman', serif",
sans: "-apple-system, 'PingFang SC', sans-serif",
r: 16, rSm: 12, rXs: 8, rPill: 20,
};
// ─── 图标组件 ───
function IconSvg({ paths, size = 24, color = '#fff' }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{paths.map((d, i) => typeof d === 'string' ? <path key={i} d={d}/> : d)}
</svg>
);
}
function IconCheckin(props) { return <IconSvg {...props} paths={['M9 11l3 3L22 4','M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11']}/>; }
function IconTask(props) { return <IconSvg {...props} paths={[<rect x="3" y="3" width="18" height="18" rx="2" key="r"/>, 'M9 12l2 2 4-4']}/>; }
function IconHistory(props) { return <IconSvg {...props} paths={[<circle cx="12" cy="12" r="10" key="c"/>, 'M12 6v6l4 2']}/>; }
function IconArrowLeft(props) { return <IconSvg {...props} paths={['M15 18l-6-6 6-6']}/>; }
function IconChevronRight(props) { return <IconSvg {...props} size={14} paths={['M9 18l6-6-6-6']}/>; }
function IconClose(props) { return <IconSvg {...props} size={20} paths={['M18 6L6 18','M6 6l12 12']}/>; }
function IconCheck(props) { return <IconSvg {...props} size={14} paths={['M20 6L9 17l-5-5']}/>; }
function IconGift(props) { return <IconSvg {...props} paths={['M20 12v10H4V12','M2 7h20v5H2z','M12 22V7','M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7z','M12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z']}/>; }
// ─── 商品数据 ───
const PRODUCTS = [
{ id: 1, name: '健康体检套餐', points: 800, price: 299, type: 'service', tag: '热门', stock: 15 },
{ id: 2, name: '血压计', points: 1200, price: 199, type: 'physical', stock: 8 },
{ id: 3, name: '维生素D3', points: 300, price: 89, type: 'physical', stock: 50 },
{ id: 4, name: '专属健康顾问', points: 2000, price: 500, type: 'privilege', tag: '新品', stock: 3 },
{ id: 5, name: '运动手环', points: 2000, price: 399, type: 'physical', stock: 0 },
{ id: 6, name: '保温杯', points: 600, price: 128, type: 'physical', tag: '新品', stock: 20 },
];
const TYPE_LABELS = { physical: '实物', service: '服务券', privilege: '权益' };
const TYPE_COLORS = { physical: T.pri, service: T.acc, privilege: T.wrn };
const TYPE_BG = { physical: T.priL, service: T.accL, privilege: T.wrnL };
// ─── 商品占位图 ───
function ProductThumb({ name, type, large = false }) {
const sz = large ? 48 : 28;
return (
<div style={{
width: '100%', aspectRatio: large ? '4/3' : '1',
background: TYPE_BG[type] || T.surface,
borderRadius: large ? T.r : T.rSm,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
position: 'relative', gap: 4,
}}>
<IconGift size={sz} color={TYPE_COLORS[type] || T.pri}/>
<span style={{ fontSize: large ? 12 : 9, color: T.tx3, fontWeight: 500 }}>{TYPE_LABELS[type]}</span>
</div>
);
}
// ─────────────────────────────────────
// Screen 1: 商城主页
// ─────────────────────────────────────
function MallPage() {
const [activeCat, setActiveCat] = React.useState(0);
const cats = ['全部', '实物', '服务券', '权益'];
const catKeys = ['', 'physical', 'service', 'privilege'];
const filtered = activeCat === 0 ? PRODUCTS : PRODUCTS.filter(p => p.type === catKeys[activeCat]);
const balance = 1280;
const checkedIn = false;
const consecutiveDays = 5;
const actions = [
{ icon: <IconCheckin size={22}/>, label: '签到打卡', bg: T.acc, shadow: 'rgba(91,122,94,0.3)' },
{ icon: <IconTask size={22}/>, label: '积分任务', bg: T.pri, shadow: 'rgba(196,98,58,0.3)' },
{ icon: <IconHistory size={22}/>, label: '兑换记录', bg: T.wrn, shadow: 'rgba(196,135,58,0.3)' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
{/* 导航栏 */}
<div style={{ padding: '12px 20px 8px', display: 'flex', alignItems: 'center', gap: 8, background: T.bg }}>
<IconArrowLeft size={24} color={T.tx}/>
<div style={{ flex: 1, textAlign: 'center', fontFamily: T.serif, fontSize: 26, fontWeight: 700, color: T.tx, paddingRight: 24 }}>积分商城</div>
</div>
{/* 可滚动区域 */}
<div style={{ flex: 1, overflow: 'auto', padding: '8px 20px 24px' }}>
{/* 积分卡片 */}
<div style={{
background: `linear-gradient(135deg, ${T.pri} 0%, ${T.priD} 100%)`,
borderRadius: T.r, padding: '24px 24px 20px', marginBottom: 20,
boxShadow: `0 8px 24px rgba(196,98,58,0.25)`, position: 'relative', overflow: 'hidden',
}}>
{/* 装饰元素 */}
<div style={{ position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: 50, background: 'rgba(255,255,255,0.08)' }}/>
<div style={{ position: 'absolute', bottom: -30, right: 40, width: 80, height: 80, borderRadius: 40, background: 'rgba(255,255,255,0.05)' }}/>
<div style={{ position: 'absolute', top: 20, right: 20, width: 40, height: 40, borderRadius: 20, background: 'rgba(255,255,255,0.06)' }}/>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', position: 'relative', zIndex: 1 }}>
<div>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)', marginBottom: 8, letterSpacing: 1 }}>我的积分</div>
<div style={{ fontFamily: T.serif, fontSize: 42, fontWeight: 700, color: '#fff', lineHeight: 1, letterSpacing: 2 }}>{balance.toLocaleString()}</div>
{consecutiveDays > 0 && (
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.65)', marginTop: 8 }}>
已连续签到 {consecutiveDays}
</div>
)}
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 4,
background: checkedIn ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.2)',
border: `1px solid rgba(255,255,255,${checkedIn ? '0.2' : '0.4'})`,
borderRadius: T.rPill, padding: '8px 16px',
fontSize: 13, color: '#fff', fontWeight: 500, cursor: 'pointer',
}}>
{checkedIn ? '已签到' : '签到'}
</div>
</div>
</div>
{/* 快捷操作 */}
<div style={{ display: 'flex', justifyContent: 'space-around', marginBottom: 24 }}>
{actions.map((a, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
<div style={{
width: 52, height: 52, borderRadius: 26, background: a.bg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: `0 4px 12px ${a.shadow}`,
}}>
{a.icon}
</div>
<span style={{ fontSize: 12, color: T.tx2, fontWeight: 500 }}>{a.label}</span>
</div>
))}
</div>
{/* 分割线 */}
<div style={{ height: 1, background: T.bd, marginBottom: 16 }}/>
{/* 分类标签 */}
<div style={{ display: 'flex', gap: 10, marginBottom: 20, overflowX: 'auto' }}>
{cats.map((cat, i) => (
<div
key={i}
onClick={() => setActiveCat(i)}
style={{
padding: '7px 18px', borderRadius: T.rPill,
fontSize: 14, fontWeight: activeCat === i ? 600 : 400,
background: activeCat === i ? T.pri : T.surface,
color: activeCat === i ? '#fff' : T.tx2,
whiteSpace: 'nowrap', transition: 'all 0.2s', cursor: 'pointer',
boxShadow: activeCat === i ? '0 2px 8px rgba(196,98,58,0.25)' : 'none',
}}
>
{cat}
</div>
))}
</div>
{/* 商品网格 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{filtered.map(p => (
<div key={p.id} style={{
background: T.card, borderRadius: T.rSm, overflow: 'hidden',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)', cursor: 'pointer',
}}>
<div style={{ position: 'relative' }}>
<ProductThumb name={p.name} type={p.type}/>
{p.tag && (
<div style={{
position: 'absolute', top: 8, left: 8,
background: p.tag === '热门' ? T.dan : T.acc,
color: '#fff', fontSize: 10, fontWeight: 600,
padding: '2px 8px', borderRadius: 6,
}}>
{p.tag}
</div>
)}
{p.stock === 0 && (
<div style={{
position: 'absolute', inset: 0, background: 'rgba(255,255,255,0.7)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 14, color: T.tx3, fontWeight: 600,
}}>
已兑完
</div>
)}
</div>
<div style={{ padding: '10px 12px 14px' }}>
<div style={{
fontSize: 14, fontWeight: 600, color: T.tx, lineHeight: 1.4,
marginBottom: 8, height: 40,
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
}}>
{p.name}
</div>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 2 }}>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.pri }}>{p.points}</span>
<span style={{ fontSize: 11, color: T.pri, fontWeight: 500 }}>积分</span>
</div>
<span style={{ fontSize: 12, color: T.tx3, textDecoration: 'line-through' }}>¥{p.price}</span>
</div>
{p.stock > 0 && p.stock <= 10 && (
<div style={{
fontSize: 11, color: T.wrn, fontWeight: 500, marginTop: 4,
background: T.wrnL, padding: '2px 8px', borderRadius: 6, display: 'inline-block',
}}>
仅剩{p.stock}
</div>
)}
</div>
</div>
))}
</div>
<div style={{ height: 16 }}/>
</div>
</div>
);
}
// ─────────────────────────────────────
// Screen 2: 商品详情页
// ─────────────────────────────────────
function ProductDetailPage() {
const product = PRODUCTS[0]; // 健康体检套餐
const balance = 1280;
const canAfford = balance >= product.points;
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
{/* 导航栏 */}
<div style={{ padding: '12px 20px 8px', display: 'flex', alignItems: 'center', gap: 8, background: T.bg }}>
<IconArrowLeft size={24} color={T.tx}/>
<div style={{ flex: 1, textAlign: 'center', fontSize: 17, fontWeight: 600, color: T.tx, paddingRight: 24 }}>商品详情</div>
</div>
{/* 内容 */}
<div style={{ flex: 1, overflow: 'auto' }}>
{/* 商品大图 */}
<div style={{ padding: '0 20px', marginBottom: 16 }}>
<ProductThumb name={product.name} type={product.type} large/>
</div>
{/* 商品信息卡 */}
<div style={{ background: T.card, margin: '0 20px', borderRadius: T.r, padding: 20, marginBottom: 16, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
{product.tag && (
<span style={{
fontSize: 11, fontWeight: 600, color: '#fff',
background: product.tag === '热门' ? T.dan : T.acc,
padding: '2px 8px', borderRadius: 6,
}}>
{product.tag}
</span>
)}
<span style={{
fontSize: 11, fontWeight: 500, color: T.acc,
background: T.accL, padding: '2px 8px', borderRadius: 6,
}}>
{TYPE_LABELS[product.type]}
</span>
</div>
<div style={{ fontSize: 20, fontWeight: 700, color: T.tx, lineHeight: 1.4, marginBottom: 12 }}>
{product.name}
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginBottom: 8 }}>
<span style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.pri }}>{product.points}</span>
<span style={{ fontSize: 14, color: T.pri, fontWeight: 500 }}>积分</span>
<span style={{ fontSize: 14, color: T.tx3, textDecoration: 'line-through', marginLeft: 8 }}>¥{product.price}</span>
</div>
<div style={{ fontSize: 13, color: T.tx3, lineHeight: 1.6 }}>
由专业医疗团队提供的全面健康体检服务包含基础检查血液分析心电图等多项检查项目
</div>
</div>
{/* 库存状态 */}
<div style={{ background: T.card, margin: '0 20px', borderRadius: T.r, padding: '16px 20px', marginBottom: 16, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 14, color: T.tx2 }}>库存状态</span>
<span style={{ fontSize: 14, fontWeight: 600, color: product.stock > 10 ? T.acc : product.stock > 0 ? T.wrn : T.dan }}>
{product.stock > 10 ? '充足' : product.stock > 0 ? `仅剩 ${product.stock}` : '已兑完'}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 12 }}>
<span style={{ fontSize: 14, color: T.tx2 }}>您的积分</span>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: canAfford ? T.acc : T.dan }}>
{balance.toLocaleString()} {canAfford ? '(充足)' : '(不足)'}
</span>
</div>
</div>
{/* 温馨提示 */}
<div style={{ margin: '0 20px', padding: '12px 16px', background: T.wrnL, borderRadius: T.rSm, marginBottom: 24 }}>
<div style={{ fontSize: 12, color: T.wrn, lineHeight: 1.6 }}>
兑换后积分将立即扣除请在 30 天内到院核销过期未核销的订单将自动取消并退还积分
</div>
</div>
</div>
{/* 底部操作栏 */}
<div style={{
padding: '12px 20px', background: T.card,
borderTop: `1px solid ${T.bd}`, display: 'flex', alignItems: 'center', gap: 12,
}}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 12, color: T.tx3 }}>需要</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 2 }}>
<span style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.pri }}>{product.points}</span>
<span style={{ fontSize: 12, color: T.pri }}>积分</span>
</div>
</div>
<div style={{
padding: '14px 32px', borderRadius: T.rPill,
background: canAfford ? T.pri : T.bd,
color: canAfford ? '#fff' : T.tx3,
fontSize: 16, fontWeight: 600,
cursor: canAfford ? 'pointer' : 'not-allowed',
boxShadow: canAfford ? '0 4px 12px rgba(196,98,58,0.3)' : 'none',
}}>
{canAfford ? '立即兑换' : '积分不足'}
</div>
</div>
</div>
);
}
// ─────────────────────────────────────
// Screen 3: 签到弹窗
// ─────────────────────────────────────
function CheckinPopupPage() {
const consecutiveDays = 5;
const earnedPoints = 10;
const days = ['一', '二', '三', '四', '五', '六', '日'];
const checkedDays = [true, true, true, true, true, false, false]; // 前5天已签
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
{/* 导航栏 */}
<div style={{ padding: '12px 20px 8px', display: 'flex', alignItems: 'center', gap: 8, background: T.bg }}>
<IconArrowLeft size={24} color={T.tx}/>
<div style={{ flex: 1, textAlign: 'center', fontFamily: T.serif, fontSize: 26, fontWeight: 700, color: T.tx, paddingRight: 24 }}>积分商城</div>
</div>
<div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
{/* 半透明遮罩 */}
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 5 }}/>
{/* 弹窗卡片 */}
<div style={{
position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
width: 320, background: T.card, borderRadius: T.r, zIndex: 10,
boxShadow: '0 20px 60px rgba(0,0,0,0.15)', overflow: 'hidden',
}}>
{/* 顶部装饰 */}
<div style={{
background: `linear-gradient(135deg, ${T.pri} 0%, ${T.priD} 100%)`,
padding: '24px 24px 20px', textAlign: 'center', position: 'relative', overflow: 'hidden',
}}>
<div style={{ position: 'absolute', top: -15, right: -15, width: 60, height: 60, borderRadius: 30, background: 'rgba(255,255,255,0.08)' }}/>
<div style={{ fontSize: 16, color: 'rgba(255,255,255,0.8)', marginBottom: 4 }}>签到成功</div>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 4 }}>
<span style={{ fontFamily: T.serif, fontSize: 36, fontWeight: 700, color: '#fff' }}>+{earnedPoints}</span>
<span style={{ fontSize: 14, color: 'rgba(255,255,255,0.8)' }}>积分</span>
</div>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.65)', marginTop: 8 }}>
已连续签到 {consecutiveDays}
</div>
</div>
{/* 7 天日历 */}
<div style={{ padding: '20px 20px 16px' }}>
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx, marginBottom: 12, textAlign: 'center' }}>
本周签到
</div>
<div style={{ display: 'flex', gap: 6, justifyContent: 'center' }}>
{days.map((d, i) => {
const isChecked = checkedDays[i];
const isToday = i === consecutiveDays - 1 && isChecked;
return (
<div key={i} style={{
width: 36, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
}}>
<div style={{
width: 36, height: 36, borderRadius: 18,
background: isChecked ? (isToday ? T.pri : T.accL) : T.surface,
display: 'flex', alignItems: 'center', justifyContent: 'center',
border: isToday ? `2px solid ${T.pri}` : 'none',
boxShadow: isToday ? `0 2px 8px rgba(196,98,58,0.3)` : 'none',
}}>
{isChecked ? (
<IconCheck size={14} color={isToday ? '#fff' : T.acc}/>
) : (
<div style={{ width: 6, height: 6, borderRadius: 3, background: T.bd }}/>
)}
</div>
<span style={{ fontSize: 11, color: isChecked ? T.tx : T.tx3, fontWeight: isToday ? 600 : 400 }}>
{d}
</span>
</div>
);
})}
</div>
{/* 连续签到奖励提示 */}
<div style={{
marginTop: 16, padding: '10px 14px', background: T.accL, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', gap: 8,
}}>
<IconCheck size={16} color={T.acc}/>
<span style={{ fontSize: 12, color: T.acc, fontWeight: 500 }}>
再坚持 2 连续 7 天签到额外奖励 50 积分
</span>
</div>
</div>
{/* 关闭按钮 */}
<div style={{ padding: '0 20px 20px', display: 'flex', justifyContent: 'center' }}>
<div style={{
width: '100%', padding: '12px 0', borderRadius: T.rPill,
background: T.pri, color: '#fff', fontSize: 15, fontWeight: 600,
textAlign: 'center', cursor: 'pointer',
boxShadow: '0 4px 12px rgba(196,98,58,0.3)',
}}>
我知道了
</div>
</div>
</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">商城主页</span>
<IosFrame time="9:41" battery={85} darkStatus={false}>
<MallPage />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">商品详情</span>
<IosFrame time="9:42" battery={85}>
<ProductDetailPage />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">签到成功弹窗</span>
<IosFrame time="9:43" battery={84}>
<CheckinPopupPage />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>