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 安全清单扩展
This commit is contained in:
526
docs/design/mp-05-mall-v2.html
Normal file
526
docs/design/mp-05-mall-v2.html
Normal file
@@ -0,0 +1,526 @@
|
||||
<!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>
|
||||
12
docs/design/mp-05-mall-v2/META.yml
Normal file
12
docs/design/mp-05-mall-v2/META.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
prototype: mp-05-mall-v2.html
|
||||
source: docs/design/mp-05-mall-v2.html
|
||||
variant: patient
|
||||
generated_at: 2026-05-22T12:00:00+08:00
|
||||
tokens:
|
||||
matched: 22
|
||||
unmatched: 2
|
||||
components:
|
||||
total: 12
|
||||
mapped: 8
|
||||
new: 4
|
||||
interactions: 7
|
||||
183
docs/design/mp-05-mall-v2/SPEC.md
Normal file
183
docs/design/mp-05-mall-v2/SPEC.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# 积分商城 V2 设计规格
|
||||
|
||||
> 来源: docs/design/mp-05-mall-v2.html | 平台: 微信小程序 (Taro) | 页面数: 3 | 生成: 2026-05-22
|
||||
|
||||
## 页面索引
|
||||
|
||||
| 页面 | 截图 | 路由 |
|
||||
|------|------|------|
|
||||
| 商城主页 |  | /pages/mall/index |
|
||||
| 商品详情 |  | /pages/pkg-mall/product/index |
|
||||
| 签到成功弹窗 |  | 弹窗 (商城主页内触发) |
|
||||
|
||||
## 一、Token 映射
|
||||
|
||||
| 原型值 | 项目 Token | 状态 |
|
||||
|--------|-----------|------|
|
||||
| #C4623A (T.pri) | `--tk-pri` / `$pri` | ✅ |
|
||||
| #F0DDD4 (T.priL) | `--tk-pri-l` / `$pri-l` | ✅ |
|
||||
| #8B3E1F (T.priD) | `--tk-pri-d` / `$pri-d` | ✅ |
|
||||
| #F5F0EB (T.bg) | `$bg` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #FFFFFF (T.card) | `--tk-card-bg` / `$card` | ✅ |
|
||||
| #EDE8E2 (T.surface) | `$surface-alt` (≈卡片白底) | ⚠️ 近似匹配 |
|
||||
| #2D2A26 (T.tx) | `$tx` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #5A554F (T.tx2) | `$tx2` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #78716C (T.tx3) | `--tk-text-secondary` / `$tx3` | ✅ |
|
||||
| #E8E2DC (T.bd) | `$bd` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #F0EBE5 (T.bdL) | `$bd-l` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #5B7A5E (T.acc) | `$acc` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #E8F0E8 (T.accL) | `$acc-l` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #C4873A (T.wrn) | `$wrn` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #FFF3E0 (T.wrnL) | `$wrn-l` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #B54A4A (T.dan) | `$dan` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| #FDEAEA (T.danL) | `$dan-l` | ⚠️ SCSS 变量,无 CSS Token |
|
||||
| 16 (T.r) | `--tk-card-radius` / `$r` | ✅ |
|
||||
| 12 (T.rSm) | `$r-sm` | ⚠️ SCSS 变量 |
|
||||
| 20 (T.rPill) | `$r-pill` | ⚠️ SCSS 变量 |
|
||||
| serif 字体 | `$serif` + `@mixin serif-number` | ⚠️ SCSS mixin |
|
||||
| sans 字体 | `$sans` | ⚠️ SCSS 变量 |
|
||||
|
||||
> 状态标记: ✅ confirmed 直接使用 | ⚠️ pending 需 SCSS 变量引用 | ❌ unmatched 需硬编码或新建 Token
|
||||
|
||||
## 二、页面结构
|
||||
|
||||
### 1. 商城主页
|
||||
|
||||

|
||||
|
||||
布局层级(从上到下):
|
||||
|
||||
1. **导航栏** — 返回箭头 + 标题「积分商城」(serif 26px 700)
|
||||
2. **积分卡片** — 渐变背景 (`linear-gradient(135deg, $pri, $pri-d)`) + 装饰圆
|
||||
- 左侧:标签「我的积分」13px + 余额数字 serif 42px 700 + 连续签到天数 12px
|
||||
- 右侧:签到按钮 (pill, `rgba(255,255,255,0.2)` bg + border)
|
||||
- 阴影:`0 8px 24px rgba(196,98,58,0.25)`
|
||||
3. **快捷操作栏** — 三等分圆形图标(52×52)+ 文字标签
|
||||
- 签到打卡:$acc 绿底 + `0 4px 12px rgba(91,122,94,0.3)` 阴影
|
||||
- 积分任务:$pri 橙底 + `0 4px 12px rgba(196,98,58,0.3)` 阴影
|
||||
- 兑换记录:$wrn 黄底 + `0 4px 12px rgba(196,135,58,0.3)` 阴影
|
||||
4. **分割线** — 1px $bd
|
||||
5. **分类标签** — Pill tabs(全部/实物/服务券/权益)
|
||||
- 默认:$surface 背景 + $tx2 文字
|
||||
- 激活:$pri 背景 + #fff 文字 + `0 2px 8px rgba(196,98,58,0.25)` 阴影
|
||||
6. **商品网格** — 2列 grid, gap 12px
|
||||
- 商品卡片:白底 + $r-sm 圆角 + `0 2px 8px rgba(0,0,0,0.04)` 阴影
|
||||
- 商品图:1:1 占位(按 type 分色:physical=$priL, service=$accL, privilege=$wrnL)
|
||||
- 标签角标:top:8 left:8, 热门=$dan, 新品=$acc
|
||||
- 已兑完遮罩:`rgba(255,255,255,0.7)` + 「已兑完」文字
|
||||
- 商品信息:名称 (14px 600, 2行截断, 高40px) + 积分价格 (serif 18px 700 $pri) + 原价划线 ($tx3)
|
||||
- 库存紧张标签:$wrnL 背景 + $wrn 文字
|
||||
|
||||
### 2. 商品详情页
|
||||
|
||||

|
||||
|
||||
布局层级(从上到下):
|
||||
|
||||
1. **导航栏** — 返回箭头 + 标题「商品详情」(17px 600)
|
||||
2. **商品大图** — 4:3 比例, 圆角 $r, 按类型分色背景
|
||||
3. **商品信息卡** — 白底 + $r 圆角 + padding 20px
|
||||
- 类型标签行:tag badge ($dan/$acc) + type badge ($accL bg)
|
||||
- 标题:20px 700 $tx
|
||||
- 积分价格:serif 28px 700 $pri + 原价划线 14px $tx3
|
||||
- 描述:13px $tx3, line-height 1.6
|
||||
4. **库存/余额卡** — 白底 + $r 圆角
|
||||
- 库存状态行:标签 $tx2 + 值 (充足=$acc / 紧张=$wrn / 售罄=$dan)
|
||||
- 积分余额行:标签 $tx2 + 值 (充足=$acc / 不足=$dan)
|
||||
5. **温馨提示** — $wrnL 背景 + $wrn 文字, $r-sm 圆角
|
||||
6. **底部操作栏** — 固定底部, 白底 + top border $bd
|
||||
- 左侧:需要积分 (serif 22px 700 $pri)
|
||||
- 右侧:兑换按钮 (pill, 充足=$pri bg+阴影, 不足=$bd bg+$tx3 text, cursor: not-allowed)
|
||||
|
||||
### 3. 签到成功弹窗
|
||||
|
||||

|
||||
|
||||
布局层级:
|
||||
|
||||
1. **半透明遮罩** — `rgba(0,0,0,0.4)`, 全屏覆盖
|
||||
2. **弹窗卡片** — 居中, 宽 320px, 白底 + $r 圆角 + `0 20px 60px rgba(0,0,0,0.15)` 阴影
|
||||
- **顶部装饰区** — 渐变背景 ($pri→$priD) + 装饰圆
|
||||
- 「签到成功」16px rgba(255,255,255,0.8)
|
||||
- 积分数 serif 36px 700 #fff + 「积分」14px
|
||||
- 连续天数 13px rgba(255,255,255,0.65)
|
||||
- **7天日历** — 7 列等宽 (36×36 圆形)
|
||||
- 已签到:$accL bg + 绿色勾 (今日=$pri bg + 2px border + 阴影)
|
||||
- 未签到:$surface bg + 6px 灰色圆点
|
||||
- 下方标签:周X (11px)
|
||||
- **激励提示** — $accL bg + $acc 勾图标 + 文字
|
||||
- **关闭按钮** — 全宽 pill, $pri bg + #fff text + 阴影
|
||||
|
||||
## 三、组件映射
|
||||
|
||||
| 原型元素 | 推荐组件 | 来源 | 备注 |
|
||||
|----------|---------|------|------|
|
||||
| 页面容器 | `PageShell` | @components/ui | padding="none" safeBottom=false |
|
||||
| 商品卡片 | `ContentCard` | @components/ui | variant="elevated" activeFeedback="opacity" |
|
||||
| 分类标签 | `TabFilter` | @components/ui | variant="pill" tabs=[全部,实物,服务券,权益] |
|
||||
| 兑换按钮 | `PrimaryButton` | @components/ui | disabled 时灰显 |
|
||||
| 库存标签 | `StatusTag` | @components/ui | warning/error/default 状态 |
|
||||
| 列表项 | `ListItem` | @components/ui | 商品详情信息行 |
|
||||
| 积分卡片区域 | `GradientHeader` | @components/ui | 包裹积分余额展示 |
|
||||
| 空数据/加载 | `EmptyState` / `LoadingCard` | @components | 已有 |
|
||||
|
||||
> ⚠️ **需新建**:
|
||||
> - `PointsCard` — 积分余额展示卡(渐变背景 + 装饰圆 + 签到按钮)
|
||||
> - `ProductCard` — 商品卡片(图片占位 + 信息区 + 标签 + 库存提示)
|
||||
> - `CheckinCalendar` — 7天签到日历(圆形进度点 + 今日高亮)
|
||||
> - `CheckinModal` — 签到成功弹窗(遮罩 + 卡片 + 日历 + 关闭)
|
||||
|
||||
## 四、交互规格
|
||||
|
||||
| 元素 | 交互 | 触发 | 反馈 | 备注 |
|
||||
|------|------|------|------|------|
|
||||
| 分类标签 | 切换筛选 | tap tab | 高亮样式切换 + 列表重新渲染 | 调用 `fetchProducts(1, type, true)` |
|
||||
| 商品卡片 | 跳转详情 | tap card | opacity 反馈 → `safeNavigateTo` | 库存=0 时 toast「已兑完」 |
|
||||
| 签到按钮 | 触发签到 | tap | loading → 成功/失败 toast | 成功后弹出 CheckinModal |
|
||||
| 快捷操作 | 跳转页面 | tap icon | opacity 反馈 | 签到打卡 / 积分任务(TODO) / 兑换记录 |
|
||||
| 兑换按钮 | 提交兑换 | tap button | loading → 确认弹窗 → 结果 | 积分不足时 disabled |
|
||||
| 签到弹窗 | 关闭 | tap「我知道了」 | 弹窗消失 + 刷新积分数据 | 动画 fadeOut |
|
||||
| 列表加载 | 上拉加载 | scroll to bottom | Loading 组件追加 | `useReachBottom` + 分页 |
|
||||
|
||||
## 五、状态变体
|
||||
|
||||
- **无档案状态**: 显示 EmptyState 组件,提示去建档,隐藏积分卡片和商品列表
|
||||
- **空商品列表**: EmptyState icon='礼' text='暂无商品'
|
||||
- **已签到**: 签到按钮变为低对比度「已签到」,不可再次点击
|
||||
- **库存紧张**: 商品卡片底部显示 $wrnL 标签「仅剩N件」
|
||||
- **已兑完**: 商品图区域叠加半透明遮罩 + 「已兑完」文字,点击 toast 提示
|
||||
- **积分不足**: 详情页兑换按钮灰显 disabled,余额显示红色「不足」
|
||||
|
||||
## 六、样式清单
|
||||
|
||||
### 颜色用法汇总
|
||||
- 渐变卡片: `linear-gradient(135deg, $pri, $priD)`
|
||||
- 商品类型色: physical=$priL, service=$accL, privilege=$wrnL
|
||||
- 标签色: 热门=$dan, 新品=$acc
|
||||
- 库存色: 充足=$acc, 紧张=$wrn, 售罄=$dan
|
||||
|
||||
### 尺寸规格
|
||||
- 积分卡片: padding 24px, borderRadius $r (16px)
|
||||
- 快捷图标: 52×52 圆形, 间距 space-around
|
||||
- 商品卡片: grid 2列, gap 12px, borderRadius $r-sm (12px)
|
||||
- 商品图: 1:1 (主页) / 4:3 (详情), borderRadius $r-sm
|
||||
- Pill 按钮: borderRadius $r-pill (20px), padding 7px 18px (tabs) / 14px 32px (CTA)
|
||||
- 弹窗: 宽 320px, 7天日历项 36×36
|
||||
|
||||
### 字号规格
|
||||
- 积分余额: serif 42px 700 (主页) / 36px 700 (弹窗) / 28px 700 (详情)
|
||||
- 页面标题: serif 26px 700
|
||||
- 商品名: 14px 600, 2行截断
|
||||
- 商品价格: serif 18px 700 $pri
|
||||
- 分类标签: 14px (激活 600 / 默认 400)
|
||||
- 快捷标签: 12px 500
|
||||
- 辅助文字: 13px / 12px / 11px
|
||||
|
||||
---
|
||||
|
||||
> 此规格由 design-handoff skill 自动生成。实施时请:
|
||||
> 1. 先阅读截图建立视觉印象
|
||||
> 2. 按 Token 映射表使用项目 Token(✅ 标记的直接用)
|
||||
> 3. 优先使用"组件映射"中列出的已有组件
|
||||
> 4. 参考"交互规格"实现对应的交互逻辑
|
||||
> 5. "需新建"的组件参考截图和布局描述从头实现
|
||||
319
docs/design/mp-05-mall-v2/tokens.json
Normal file
319
docs/design/mp-05-mall-v2/tokens.json
Normal file
@@ -0,0 +1,319 @@
|
||||
{
|
||||
"source": null,
|
||||
"variant": "patient",
|
||||
"matched": {
|
||||
"T.pri": {
|
||||
"method": "alias",
|
||||
"confidence": "confirmed",
|
||||
"token": "--tk-pri",
|
||||
"scssVar": null,
|
||||
"variant": "patient",
|
||||
"prototypeValue": "#C4623A",
|
||||
"tokenValue": "#C4623A"
|
||||
},
|
||||
"T.priL": {
|
||||
"method": "alias",
|
||||
"confidence": "confirmed",
|
||||
"token": "--tk-pri-l",
|
||||
"scssVar": null,
|
||||
"variant": "patient",
|
||||
"prototypeValue": "#F0DDD4",
|
||||
"tokenValue": "#F0DDD4"
|
||||
},
|
||||
"T.priD": {
|
||||
"method": "alias",
|
||||
"confidence": "confirmed",
|
||||
"token": "--tk-pri-d",
|
||||
"scssVar": null,
|
||||
"variant": "patient",
|
||||
"prototypeValue": "#8B3E1F",
|
||||
"tokenValue": "#8B3E1F"
|
||||
},
|
||||
"T.bg": {
|
||||
"method": "alias",
|
||||
"confidence": "pending",
|
||||
"token": null,
|
||||
"scssVar": "$bg",
|
||||
"tokenValue": "#F5F0EB",
|
||||
"note": "原型页面背景色,tokens.scss 未声明为 CSS 变量,直接用 $bg SCSS 变量",
|
||||
"prototypeValue": "#F5F0EB"
|
||||
},
|
||||
"T.card": {
|
||||
"method": "alias",
|
||||
"confidence": "confirmed",
|
||||
"token": "--tk-card-bg",
|
||||
"scssVar": null,
|
||||
"prototypeValue": "#FFFFFF",
|
||||
"tokenValue": "#FFFFFF"
|
||||
},
|
||||
"T.surface": {
|
||||
"method": "alias",
|
||||
"confidence": "approximate",
|
||||
"token": "--tk-card-bg",
|
||||
"scssVar": null,
|
||||
"note": "原型中 surface ≈ 卡片白底",
|
||||
"prototypeValue": "#EDE8E2",
|
||||
"tokenValue": "#FFFFFF"
|
||||
},
|
||||
"T.tx": {
|
||||
"method": "alias",
|
||||
"confidence": "pending",
|
||||
"token": null,
|
||||
"scssVar": "$tx",
|
||||
"tokenValue": "#2D2A26",
|
||||
"note": "主文字色,tokens.scss 未声明为 CSS 变量,直接用 $tx SCSS 变量",
|
||||
"prototypeValue": "#2D2A26"
|
||||
},
|
||||
"T.tx2": {
|
||||
"method": "alias",
|
||||
"confidence": "pending",
|
||||
"token": null,
|
||||
"scssVar": "$tx2",
|
||||
"tokenValue": "#5A554F",
|
||||
"note": "次文字色,tokens.scss 未声明,elder-mode 下 --tk-text-secondary 覆盖为此值",
|
||||
"prototypeValue": "#5A554F"
|
||||
},
|
||||
"T.tx3": {
|
||||
"method": "alias",
|
||||
"confidence": "confirmed",
|
||||
"token": "--tk-text-secondary",
|
||||
"scssVar": "$tx3",
|
||||
"prototypeValue": "#78716C",
|
||||
"tokenValue": "#78716C"
|
||||
},
|
||||
"T.bd": {
|
||||
"method": "alias",
|
||||
"confidence": "pending",
|
||||
"token": null,
|
||||
"scssVar": "$bd",
|
||||
"tokenValue": "#E8E2DC",
|
||||
"note": "边框色(不是圆角),tokens.scss 未声明为 CSS 变量",
|
||||
"prototypeValue": "#E8E2DC"
|
||||
},
|
||||
"T.bdL": {
|
||||
"token": null,
|
||||
"scssVar": "$bd-l",
|
||||
"prototypeValue": "#F0EBE5",
|
||||
"tokenValue": "#F0EBE5",
|
||||
"method": "value_exact",
|
||||
"confidence": "pending",
|
||||
"role": "浅边框色",
|
||||
"note": "匹配到 SCSS 变量 $bd-l,但无对应 CSS Token"
|
||||
},
|
||||
"T.acc": {
|
||||
"token": null,
|
||||
"scssVar": "$acc",
|
||||
"prototypeValue": "#5B7A5E",
|
||||
"tokenValue": "#5B7A5E",
|
||||
"method": "value_exact",
|
||||
"confidence": "pending",
|
||||
"role": "鼠尾草绿/成功色",
|
||||
"note": "匹配到 SCSS 变量 $acc,但无对应 CSS Token"
|
||||
},
|
||||
"T.accL": {
|
||||
"token": null,
|
||||
"scssVar": "$acc-l",
|
||||
"prototypeValue": "#E8F0E8",
|
||||
"tokenValue": "#E8F0E8",
|
||||
"method": "value_exact",
|
||||
"confidence": "pending",
|
||||
"role": "成功浅色",
|
||||
"note": "匹配到 SCSS 变量 $acc-l,但无对应 CSS Token"
|
||||
},
|
||||
"T.wrn": {
|
||||
"token": null,
|
||||
"scssVar": "$wrn",
|
||||
"prototypeValue": "#C4873A",
|
||||
"tokenValue": "#C4873A",
|
||||
"method": "value_exact",
|
||||
"confidence": "pending",
|
||||
"role": "警告色/暖琥珀",
|
||||
"note": "匹配到 SCSS 变量 $wrn,但无对应 CSS Token"
|
||||
},
|
||||
"T.wrnL": {
|
||||
"token": null,
|
||||
"scssVar": "$wrn-l",
|
||||
"prototypeValue": "#FFF3E0",
|
||||
"tokenValue": "#FFF3E0",
|
||||
"method": "value_exact",
|
||||
"confidence": "pending",
|
||||
"role": "警告浅色",
|
||||
"note": "匹配到 SCSS 变量 $wrn-l,但无对应 CSS Token"
|
||||
},
|
||||
"T.dan": {
|
||||
"token": null,
|
||||
"scssVar": "$dan",
|
||||
"prototypeValue": "#B54A4A",
|
||||
"tokenValue": "#B54A4A",
|
||||
"method": "value_exact",
|
||||
"confidence": "pending",
|
||||
"role": "危险色/柔红",
|
||||
"note": "匹配到 SCSS 变量 $dan,但无对应 CSS Token"
|
||||
},
|
||||
"T.danL": {
|
||||
"token": null,
|
||||
"scssVar": "$dan-l",
|
||||
"prototypeValue": "#FDEAEA",
|
||||
"tokenValue": "#FDEAEA",
|
||||
"method": "value_exact",
|
||||
"confidence": "pending",
|
||||
"role": "危险浅色",
|
||||
"note": "匹配到 SCSS 变量 $dan-l,但无对应 CSS Token"
|
||||
},
|
||||
"T.white": {
|
||||
"token": "--tk-card-bg",
|
||||
"scssVar": "$card",
|
||||
"prototypeValue": "#FFFFFF",
|
||||
"tokenValue": "#FFFFFF",
|
||||
"method": "value_exact",
|
||||
"confidence": "confirmed",
|
||||
"role": "卡片白底"
|
||||
},
|
||||
"T.r": {
|
||||
"method": "alias",
|
||||
"confidence": "confirmed",
|
||||
"token": "--tk-card-radius",
|
||||
"scssVar": "$r",
|
||||
"prototypeValue": "16",
|
||||
"tokenValue": "16px"
|
||||
},
|
||||
"T.rSm": {
|
||||
"method": "alias",
|
||||
"confidence": "pending",
|
||||
"token": null,
|
||||
"scssVar": "$r-sm",
|
||||
"tokenValue": "12px",
|
||||
"note": "tokens.scss 未声明,需添加 --tk-radius-sm 或直接用 $r-sm SCSS 变量",
|
||||
"prototypeValue": "12"
|
||||
},
|
||||
"T.rXs": {
|
||||
"token": null,
|
||||
"scssVar": "$r-xs",
|
||||
"prototypeValue": "8",
|
||||
"tokenValue": "8px",
|
||||
"method": "value_exact",
|
||||
"confidence": "pending",
|
||||
"note": "匹配到 SCSS 变量 $r-xs,但无对应 CSS Token"
|
||||
},
|
||||
"T.rPill": {
|
||||
"token": "--tk-section-gap",
|
||||
"scssVar": "$sp-section",
|
||||
"prototypeValue": "20",
|
||||
"tokenValue": "20px",
|
||||
"method": "value_exact",
|
||||
"confidence": "confirmed"
|
||||
}
|
||||
},
|
||||
"unmatched": [
|
||||
"T.serif",
|
||||
"T.sans"
|
||||
],
|
||||
"inlineTokenMap": {
|
||||
"fontSize:28": {
|
||||
"token": "--tk-font-h1",
|
||||
"tokenValue": "28px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"fontSize:22": {
|
||||
"token": "--tk-font-h2",
|
||||
"tokenValue": "22px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"fontSize:18": {
|
||||
"token": "--tk-font-body-lg",
|
||||
"tokenValue": "18px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"fontSize:16": {
|
||||
"token": "--tk-font-body",
|
||||
"tokenValue": "16px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"fontSize:14": {
|
||||
"token": "--tk-font-body-sm",
|
||||
"tokenValue": "14px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"fontSize:13": {
|
||||
"token": "--tk-font-cap",
|
||||
"tokenValue": "13px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"fontSize:11": {
|
||||
"token": "--tk-font-micro",
|
||||
"tokenValue": "11px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"padding:20": {
|
||||
"token": "--tk-section-gap",
|
||||
"scssVar": "$sp-section",
|
||||
"tokenValue": "20px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"padding:12": {
|
||||
"token": "--tk-gap-sm",
|
||||
"scssVar": "$sp-sm",
|
||||
"tokenValue": "12px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"borderRadius:20": {
|
||||
"token": null,
|
||||
"scssVar": "$r-lg",
|
||||
"tokenValue": "20px",
|
||||
"confidence": "pending",
|
||||
"method": "value_exact",
|
||||
"note": "匹配到 SCSS 变量 $r-lg,但无对应 CSS Token"
|
||||
},
|
||||
"gap:12": {
|
||||
"token": "--tk-gap-sm",
|
||||
"scssVar": "$sp-sm",
|
||||
"tokenValue": "12px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"gap:8": {
|
||||
"token": "--tk-gap-xs",
|
||||
"scssVar": "$sp-xs",
|
||||
"tokenValue": "8px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"gap:4": {
|
||||
"token": "--tk-gap-2xs",
|
||||
"scssVar": "$sp-2xs",
|
||||
"tokenValue": "4px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"width:52": {
|
||||
"token": "--tk-btn-primary-h",
|
||||
"tokenValue": "52px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
},
|
||||
"height:52": {
|
||||
"token": "--tk-btn-primary-h",
|
||||
"tokenValue": "52px",
|
||||
"confidence": "confirmed",
|
||||
"method": "value_exact"
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"total": 111,
|
||||
"aliasTokens": 24,
|
||||
"inlineValues": 87,
|
||||
"confirmed": 22,
|
||||
"pending": 14,
|
||||
"approximate": 1,
|
||||
"unmatched": 2
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user