This commit is contained in:
Neo
2026-03-15 10:15:02 +08:00
parent 2dd217522c
commit 72bb11b34f
916 changed files with 65306 additions and 16102803 deletions

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 标识配色演示</title>
<link href="../css/ai-icons.css" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Noto Sans SC', -apple-system, sans-serif; background: #f5f5f5; padding: 20px; }
h1 { font-size: 18px; color: #242424; margin-bottom: 20px; text-align: center; }
h2 { font-size: 15px; color: #5e5e5e; margin: 24px 0 12px; padding-left: 8px; border-left: 3px solid #667eea; }
.demo-row { display: flex; align-items: center; gap: 16px; padding: 14px 16px; background: #fff; border-radius: 12px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
.demo-row .label { font-size: 13px; color: #8b8b8b; min-width: 36px; }
.demo-row .sample-text { font-size: 14px; color: #5e5e5e; display: flex; align-items: center; }
.note { font-size: 12px; color: #a6a6a6; text-align: center; margin-top: 8px; }
</style>
</head>
<body>
<h1>AI 标识配色方案演示</h1>
<!-- ===== 嵌入 Icon ===== -->
<h2>嵌入 Icon行首小图标 · 机器人)</h2>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-red"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-orange"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-yellow"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-blue"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-indigo"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-purple"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<p class="note">每个页面所有嵌入 Icon 统一使用一种配色,刷新后随机分配</p>
<!-- ===== Title AI 标识 ===== -->
<h2>Title AI 标识(标题行右侧 · 机器人 · 浅色背景+边框)</h2>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-red"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="currentColor" opacity="0.12" stroke="currentColor" stroke-width="0.7"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="currentColor"/><circle cx="9" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="15" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="9.4" cy="11.2" r="0.7" fill="currentColor"/><circle cx="15.4" cy="11.2" r="0.7" fill="currentColor"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><rect x="3" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/><rect x="19" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-orange"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="currentColor" opacity="0.12" stroke="currentColor" stroke-width="0.7"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="currentColor"/><circle cx="9" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="15" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="9.4" cy="11.2" r="0.7" fill="currentColor"/><circle cx="15.4" cy="11.2" r="0.7" fill="currentColor"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><rect x="3" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/><rect x="19" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-yellow"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="currentColor" opacity="0.12" stroke="currentColor" stroke-width="0.7"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="currentColor"/><circle cx="9" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="15" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="9.4" cy="11.2" r="0.7" fill="currentColor"/><circle cx="15.4" cy="11.2" r="0.7" fill="currentColor"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><rect x="3" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/><rect x="19" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-blue"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="currentColor" opacity="0.12" stroke="currentColor" stroke-width="0.7"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="currentColor"/><circle cx="9" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="15" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="9.4" cy="11.2" r="0.7" fill="currentColor"/><circle cx="15.4" cy="11.2" r="0.7" fill="currentColor"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><rect x="3" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/><rect x="19" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-indigo"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="currentColor" opacity="0.12" stroke="currentColor" stroke-width="0.7"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="currentColor"/><circle cx="9" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="15" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="9.4" cy="11.2" r="0.7" fill="currentColor"/><circle cx="15.4" cy="11.2" r="0.7" fill="currentColor"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><rect x="3" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/><rect x="19" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-purple"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="currentColor" opacity="0.12" stroke="currentColor" stroke-width="0.7"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="currentColor"/><circle cx="9" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="15" cy="11.5" r="1.8" fill="white" stroke="currentColor" stroke-width="0.6"/><circle cx="9.4" cy="11.2" r="0.7" fill="currentColor"/><circle cx="15.4" cy="11.2" r="0.7" fill="currentColor"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.5"/><rect x="3" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/><rect x="19" y="10" width="2" height="4" rx="1" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/></svg></span>AI智能洞察</span>
</div>
<p class="note">每个页面所有 Title AI 标识统一使用一种配色,刷新后随机分配</p>
</body>
</html>

View File

@@ -0,0 +1,217 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>B 类能力实测对照页</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#0052d9',
'primary-light': '#ecf2fe',
success: '#00a870',
warning: '#ed7b2f',
error: '#e34d59',
'gray-1': '#f3f3f3',
'gray-7': '#8b8b8b',
'gray-9': '#5e5e5e',
'gray-13': '#242424',
}
}
}
}
</script>
<style>
@keyframes pulse-soft { 0%,100%{opacity:1} 50%{opacity:.5} }
@keyframes float-y { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} }
.anim-pulse { animation: pulse-soft 2s ease-in-out infinite; }
.anim-float { animation: float-y 3s ease-in-out infinite; }
</style>
</head>
<body class="bg-gray-1 text-gray-13 pb-8">
<!-- ===== 顶栏 sticky ===== -->
<div class="bg-white px-4 py-3 text-lg font-semibold sticky top-0 z-50 shadow-sm">
B 类能力实测H5 端)
</div>
<!-- ===== 1. Grid 布局 ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">1. Grid 布局</p>
<div class="bg-white rounded-lg p-3">
<p class="text-xs text-gray-7 mb-1">grid-cols-2</p>
<div class="grid grid-cols-2 gap-2 mb-3">
<div class="bg-primary-light rounded-lg p-3 text-center text-sm">A</div>
<div class="bg-primary-light rounded-lg p-3 text-center text-sm">B</div>
</div>
<p class="text-xs text-gray-7 mb-1">grid-cols-3</p>
<div class="grid grid-cols-3 gap-2 mb-3">
<div class="bg-primary-light rounded-lg p-2 text-center text-xs">1</div>
<div class="bg-primary-light rounded-lg p-2 text-center text-xs">2</div>
<div class="bg-primary-light rounded-lg p-2 text-center text-xs">3</div>
</div>
<p class="text-xs text-gray-7 mb-1">grid-cols-4</p>
<div class="grid grid-cols-4 gap-2">
<div class="bg-primary-light rounded-lg p-2 text-center text-xs"></div>
<div class="bg-primary-light rounded-lg p-2 text-center text-xs"></div>
<div class="bg-primary-light rounded-lg p-2 text-center text-xs"></div>
<div class="bg-primary-light rounded-lg p-2 text-center text-xs"></div>
</div>
</div>
</section>
<!-- ===== 2. Sticky 定位 ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">2. Sticky需滚动观察</p>
<div class="bg-white rounded-lg overflow-hidden">
<div class="sticky top-[52px] z-40 bg-primary text-white text-center py-2 text-sm">
sticky 元素 top-52px
</div>
<div class="p-3">
<div class="h-[160px] bg-gray-1 rounded-lg flex items-center justify-center text-xs text-gray-7">
占位区域
</div>
</div>
</div>
</section>
<!-- ===== 3. line-clamp ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">3. line-clamp 多行截断</p>
<div class="bg-white rounded-lg p-3">
<p class="text-xs text-gray-7 mb-1">line-clamp-2</p>
<p class="text-sm line-clamp-2 mb-3">这是一段很长的文本用于测试多行截断效果。微信小程序中 -webkit-line-clamp 属于条件能力,需要配合 display:-webkit-box 和 -webkit-box-orient:vertical 使用。在不同基础库版本上表现可能不一致,需要真机验证确认。</p>
<p class="text-xs text-gray-7 mb-1">line-clamp-3</p>
<p class="text-sm line-clamp-3">这是另一段很长的文本用于测试三行截断。第一行内容开始。第二行内容继续延伸到这里。第三行内容到这里应该被截断了。如果你能看到第四行的内容说明 line-clamp-3 没有生效。这段文字需要足够长才能触发三行截断的效果所以多写一些。</p>
</div>
</section>
<!-- ===== 4. vh 单位 ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">4. vh 单位</p>
<div class="bg-white rounded-lg p-3">
<div class="min-h-[25vh] bg-primary/5 rounded-lg flex items-center justify-center">
<span class="text-sm text-primary">min-height: 25vh</span>
</div>
</div>
</section>
<!-- ===== 5. backdrop-filter ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">5. backdrop-filter: blur</p>
<div class="bg-white rounded-lg p-3">
<div class="relative h-[100px] rounded-lg overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-primary to-success"></div>
<div class="absolute inset-0 flex items-center justify-center">
<div class="bg-white/20 backdrop-blur-md rounded-lg px-6 py-3">
<span class="text-white text-sm font-medium">毛玻璃效果</span>
</div>
</div>
</div>
</div>
</section>
<!-- ===== 6. space-y ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">6. space-y 子元素间距</p>
<div class="bg-white rounded-lg p-3">
<p class="text-xs text-gray-7 mb-1">space-y-2 (8px)</p>
<div class="space-y-2 mb-3">
<div class="bg-primary-light rounded p-2 text-sm">项目 A</div>
<div class="bg-primary-light rounded p-2 text-sm">项目 B</div>
<div class="bg-primary-light rounded p-2 text-sm">项目 C</div>
</div>
<p class="text-xs text-gray-7 mb-1">space-y-4 (16px)</p>
<div class="space-y-4">
<div class="bg-primary-light rounded p-2 text-sm">项目 X</div>
<div class="bg-primary-light rounded p-2 text-sm">项目 Y</div>
</div>
</div>
</section>
<!-- ===== 7. gap-x / gap-y ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">7. gap-x / gap-y 方向性间距</p>
<div class="bg-white rounded-lg p-3">
<p class="text-xs text-gray-7 mb-1">flex-wrap + gap-x-3 gap-y-2</p>
<div class="flex flex-wrap gap-x-3 gap-y-2">
<span class="bg-primary/10 text-primary text-xs px-3 py-1 rounded-full">标签一</span>
<span class="bg-success/10 text-success text-xs px-3 py-1 rounded-full">标签二</span>
<span class="bg-warning/10 text-warning text-xs px-3 py-1 rounded-full">标签三</span>
<span class="bg-error/10 text-error text-xs px-3 py-1 rounded-full">标签四</span>
<span class="bg-primary/10 text-primary text-xs px-3 py-1 rounded-full">标签五</span>
<span class="bg-success/10 text-success text-xs px-3 py-1 rounded-full">标签六</span>
</div>
</div>
</section>
<!-- ===== 8. CSS transition ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">8. CSS transition</p>
<div class="bg-white rounded-lg p-3">
<p class="text-xs text-gray-7 mb-2">点击按钮切换颜色transition-colors 300ms</p>
<button id="btn-transition" class="bg-primary text-white text-sm px-4 py-2 rounded-lg transition-colors duration-300" onclick="this.classList.toggle('bg-success')">
点我变色
</button>
</div>
</section>
<!-- ===== 9. CSS animation ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">9. CSS @keyframes 动画</p>
<div class="bg-white rounded-lg p-3 flex gap-6 items-center">
<div class="flex flex-col items-center gap-1">
<div class="w-10 h-10 rounded-full bg-primary anim-pulse"></div>
<span class="text-xs text-gray-7">pulse</span>
</div>
<div class="flex flex-col items-center gap-1">
<div class="w-10 h-10 rounded-full bg-success anim-float"></div>
<span class="text-xs text-gray-7">float</span>
</div>
</div>
</section>
<!-- ===== 10. overflow-x 横向滚动 ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">10. overflow-x-auto 横向滚动</p>
<div class="bg-white rounded-lg p-3">
<div class="overflow-x-auto">
<div class="flex gap-3" style="width: max-content;">
<div class="w-[120px] h-[80px] bg-primary/10 rounded-lg flex items-center justify-center text-xs text-primary shrink-0">卡片 1</div>
<div class="w-[120px] h-[80px] bg-success/10 rounded-lg flex items-center justify-center text-xs text-success shrink-0">卡片 2</div>
<div class="w-[120px] h-[80px] bg-warning/10 rounded-lg flex items-center justify-center text-xs text-warning shrink-0">卡片 3</div>
<div class="w-[120px] h-[80px] bg-error/10 rounded-lg flex items-center justify-center text-xs text-error shrink-0">卡片 4</div>
<div class="w-[120px] h-[80px] bg-primary/10 rounded-lg flex items-center justify-center text-xs text-primary shrink-0">卡片 5</div>
</div>
</div>
</div>
</section>
<!-- ===== 11. divide-y 分割线 ===== -->
<section class="mx-4 mt-4">
<p class="text-sm font-semibold mb-2">11. divide-y 分割线</p>
<div class="bg-white rounded-lg overflow-hidden">
<div class="divide-y divide-gray-200">
<div class="px-3 py-3 text-sm">列表项 1</div>
<div class="px-3 py-3 text-sm">列表项 2</div>
<div class="px-3 py-3 text-sm">列表项 3</div>
<div class="px-3 py-3 text-sm">列表项 4</div>
</div>
</div>
</section>
<!-- ===== 12. env(safe-area-inset-bottom) ===== -->
<section class="mx-4 mt-4 mb-4">
<p class="text-sm font-semibold mb-2">12. safe-area-inset-bottom</p>
<div class="bg-white rounded-lg p-3">
<div class="bg-primary text-white text-center py-3 rounded-lg text-sm" style="padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));">
底部按钮(含安全区补偿)
</div>
</div>
</section>
</body>
</html>

295
_DEL/Delete/benchmark.html Normal file
View File

@@ -0,0 +1,295 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=430, initial-scale=1.0, user-scalable=no">
<title>基准测试页 v3</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;}
html,body{scrollbar-width:none;-ms-overflow-style:none;}
::-webkit-scrollbar{display:none;}
body{width:430px;font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif;background:#f5f5f5;color:#333;overflow-x:hidden;position:relative;}
/* 右侧贯穿彩条 — absolute高度=视口高度752px */
.ruler-strip{position:absolute;top:0;right:0;width:10px;z-index:9999;display:flex;flex-direction:column;}
.ruler-bar{width:10px;height:752px;border-left:1px solid #000;}
.ruler-bar.blue{background:#0052d9;}
.ruler-bar.green{background:#00a870;}
/* 通用 section */
.sec{padding:6px 0 6px 0;border-bottom:1px solid #ddd;margin-right:10px;}
.sec-t{font-size:11px;font-weight:600;color:#0052d9;padding:0 0 4px 4px;}
/* 行 */
.row{display:flex;align-items:center;padding:2px 4px;}
.lbl{font-size:10px;color:#999;width:80px;flex-shrink:0;}
.val{flex:1;}
/* 横条 */
.bar{display:flex;align-items:center;color:#fff;font-size:9px;padding-left:4px;}
</style>
</head>
<body>
<!-- 右侧贯穿彩条 -->
<div class="ruler-strip" id="rulerStrip"></div>
<script>
(function(){
var c=document.getElementById('rulerStrip');
for(var i=0;i<4;i++){
var d=document.createElement('div');
d.className='ruler-bar '+(i%2===0?'blue':'green');
c.appendChild(d);
}
})();
</script>
<!-- 1. font-size -->
<div class="sec">
<div class="sec-t">1. font-size</div>
<div class="row"><span class="lbl">10px</span><span class="val" style="font-size:10px;">中文ABC123</span></div>
<div class="row"><span class="lbl">11px</span><span class="val" style="font-size:11px;">中文ABC123</span></div>
<div class="row"><span class="lbl">12px</span><span class="val" style="font-size:12px;">中文ABC123</span></div>
<div class="row"><span class="lbl">13px</span><span class="val" style="font-size:13px;">中文ABC123</span></div>
<div class="row"><span class="lbl">14px</span><span class="val" style="font-size:14px;">中文ABC123</span></div>
<div class="row"><span class="lbl">16px</span><span class="val" style="font-size:16px;">中文ABC123</span></div>
<div class="row"><span class="lbl">18px</span><span class="val" style="font-size:18px;">中文ABC123</span></div>
<div class="row"><span class="lbl">20px</span><span class="val" style="font-size:20px;">中文ABC123</span></div>
<div class="row"><span class="lbl">24px</span><span class="val" style="font-size:24px;">中文ABC123</span></div>
<div class="row"><span class="lbl">28px</span><span class="val" style="font-size:28px;">中文ABC123</span></div>
<div class="row"><span class="lbl">32px</span><span class="val" style="font-size:32px;">中文ABC123</span></div>
</div>
<!-- 2. font-weight -->
<div class="sec">
<div class="sec-t">2. font-weight</div>
<div class="row"><span class="lbl">300</span><span class="val" style="font-size:16px;font-weight:300;">中文ABC123</span></div>
<div class="row"><span class="lbl">400</span><span class="val" style="font-size:16px;font-weight:400;">中文ABC123</span></div>
<div class="row"><span class="lbl">500</span><span class="val" style="font-size:16px;font-weight:500;">中文ABC123</span></div>
<div class="row"><span class="lbl">600</span><span class="val" style="font-size:16px;font-weight:600;">中文ABC123</span></div>
<div class="row"><span class="lbl">700</span><span class="val" style="font-size:16px;font-weight:700;">中文ABC123</span></div>
</div>
<!-- 3. line-height -->
<div class="sec">
<div class="sec-t">3. line-height (14px)</div>
<div style="font-size:14px;line-height:1.0;background:#ecf2fe;padding:2px 4px;margin-bottom:2px;margin-right:10px;">LH1.0 中文测试文字验证行高</div>
<div style="font-size:14px;line-height:1.25;background:#e8f8f0;padding:2px 4px;margin-bottom:2px;margin-right:10px;">LH1.25 中文测试文字验证行高</div>
<div style="font-size:14px;line-height:1.5;background:#ecf2fe;padding:2px 4px;margin-bottom:2px;margin-right:10px;">LH1.5 中文测试文字验证行高</div>
<div style="font-size:14px;line-height:2.0;background:#e8f8f0;padding:2px 4px;margin-right:10px;">LH2.0 中文测试文字验证行高</div>
</div>
<!-- 4. letter-spacing -->
<div class="sec">
<div class="sec-t">4. letter-spacing</div>
<div class="row"><span class="lbl">0px</span><span class="val" style="font-size:14px;letter-spacing:0px;">中文ABC123</span></div>
<div class="row"><span class="lbl">0.5px</span><span class="val" style="font-size:14px;letter-spacing:0.5px;">中文ABC123</span></div>
<div class="row"><span class="lbl">1px</span><span class="val" style="font-size:14px;letter-spacing:1px;">中文ABC123</span></div>
<div class="row"><span class="lbl">2px</span><span class="val" style="font-size:14px;letter-spacing:2px;">中文ABC123</span></div>
</div>
<!-- 5. width bars (25%/50%/75%/100%) -->
<div class="sec">
<div class="sec-t">5. width bars</div>
<div class="bar" style="width:108px;height:30px;background:#0052d9;margin-bottom:2px;">108 (25%)</div>
<div class="bar" style="width:215px;height:30px;background:#00a870;margin-bottom:2px;">215 (50%)</div>
<div class="bar" style="width:323px;height:30px;background:#ed7b2f;margin-bottom:2px;">323 (75%)</div>
<div class="bar" style="width:420px;height:30px;background:#e34d59;">420 (≈100%-10)</div>
</div>
<!-- 6. height bars -->
<div class="sec">
<div class="sec-t">6. height bars</div>
<div style="display:flex;gap:4px;align-items:flex-end;padding:0 4px;">
<div style="width:40px;height:20px;background:#0052d9;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">20</div>
<div style="width:40px;height:40px;background:#00a870;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">40</div>
<div style="width:40px;height:60px;background:#ed7b2f;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">60</div>
<div style="width:40px;height:80px;background:#e34d59;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">80</div>
<div style="width:40px;height:100px;background:#0052d9;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">100</div>
</div>
</div>
<!-- 7. padding -->
<div class="sec">
<div class="sec-t">7. padding</div>
<div style="display:flex;gap:4px;flex-wrap:wrap;padding:0 4px;">
<div style="padding:4px;background:#ecf2fe;border:1px solid #0052d9;font-size:9px;color:#0052d9;">p4</div>
<div style="padding:8px;background:#ecf2fe;border:1px solid #0052d9;font-size:9px;color:#0052d9;">p8</div>
<div style="padding:12px;background:#ecf2fe;border:1px solid #0052d9;font-size:9px;color:#0052d9;">p12</div>
<div style="padding:16px;background:#ecf2fe;border:1px solid #0052d9;font-size:9px;color:#0052d9;">p16</div>
<div style="padding:20px;background:#ecf2fe;border:1px solid #0052d9;font-size:9px;color:#0052d9;">p20</div>
</div>
</div>
<!-- 8. margin -->
<div class="sec">
<div class="sec-t">8. margin</div>
<div style="background:#f0f0f0;padding:2px 0;">
<div style="margin-left:0;width:80px;height:20px;background:#0052d9;color:#fff;font-size:8px;display:flex;align-items:center;padding-left:2px;margin-bottom:2px;">ml:0</div>
<div style="margin-left:10px;width:80px;height:20px;background:#00a870;color:#fff;font-size:8px;display:flex;align-items:center;padding-left:2px;margin-bottom:2px;">ml:10</div>
<div style="margin-left:20px;width:80px;height:20px;background:#ed7b2f;color:#fff;font-size:8px;display:flex;align-items:center;padding-left:2px;margin-bottom:2px;">ml:20</div>
<div style="margin-left:40px;width:80px;height:20px;background:#e34d59;color:#fff;font-size:8px;display:flex;align-items:center;padding-left:2px;">ml:40</div>
</div>
</div>
<!-- 9. border-radius -->
<div class="sec">
<div class="sec-t">9. border-radius</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;padding:0 4px;">
<div style="width:40px;height:40px;border-radius:2px;background:#00a870;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">r2</div>
<div style="width:40px;height:40px;border-radius:4px;background:#00a870;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">r4</div>
<div style="width:40px;height:40px;border-radius:8px;background:#00a870;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">r8</div>
<div style="width:40px;height:40px;border-radius:12px;background:#00a870;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">r12</div>
<div style="width:40px;height:40px;border-radius:20px;background:#00a870;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">r20</div>
<div style="width:40px;height:40px;border-radius:50%;background:#00a870;color:#fff;font-size:8px;display:flex;align-items:center;justify-content:center;">50%</div>
</div>
</div>
<!-- 10. border-width -->
<div class="sec">
<div class="sec-t">10. border-width</div>
<div style="display:flex;gap:6px;padding:0 4px;">
<div style="width:50px;height:30px;border:1px solid #333;font-size:8px;display:flex;align-items:center;justify-content:center;">1px</div>
<div style="width:50px;height:30px;border:2px solid #333;font-size:8px;display:flex;align-items:center;justify-content:center;">2px</div>
<div style="width:50px;height:30px;border:3px solid #333;font-size:8px;display:flex;align-items:center;justify-content:center;">3px</div>
</div>
</div>
<!-- 11. gap -->
<div class="sec">
<div class="sec-t">11. gap</div>
<div style="padding:0 4px;">
<div style="font-size:9px;color:#999;">gap:4px</div>
<div style="display:flex;gap:4px;margin-bottom:4px;">
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
</div>
<div style="font-size:9px;color:#999;">gap:8px</div>
<div style="display:flex;gap:8px;margin-bottom:4px;">
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
</div>
<div style="font-size:9px;color:#999;">gap:12px</div>
<div style="display:flex;gap:12px;margin-bottom:4px;">
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
<div style="width:24px;height:24px;background:#ed7b2f;"></div>
</div>
</div>
</div>
<!-- 12. box-shadow -->
<div class="sec">
<div class="sec-t">12. box-shadow</div>
<div style="display:flex;gap:10px;padding:4px;">
<div style="width:60px;height:40px;background:#fff;border-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,0.08);font-size:8px;display:flex;align-items:center;justify-content:center;color:#999;">sm</div>
<div style="width:60px;height:40px;background:#fff;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.1);font-size:8px;display:flex;align-items:center;justify-content:center;color:#999;">md</div>
<div style="width:60px;height:40px;background:#fff;border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,0.12);font-size:8px;display:flex;align-items:center;justify-content:center;color:#999;">lg</div>
</div>
</div>
<!-- 13. 色板 -->
<div class="sec">
<div class="sec-t">13. colors</div>
<div style="display:flex;gap:2px;flex-wrap:wrap;padding:0 4px;">
<div style="width:36px;height:20px;background:#0052d9;border-radius:2px;color:#fff;font-size:7px;display:flex;align-items:center;justify-content:center;">pri</div>
<div style="width:36px;height:20px;background:#00a870;border-radius:2px;color:#fff;font-size:7px;display:flex;align-items:center;justify-content:center;">suc</div>
<div style="width:36px;height:20px;background:#ed7b2f;border-radius:2px;color:#fff;font-size:7px;display:flex;align-items:center;justify-content:center;">wrn</div>
<div style="width:36px;height:20px;background:#e34d59;border-radius:2px;color:#fff;font-size:7px;display:flex;align-items:center;justify-content:center;">err</div>
<div style="width:36px;height:20px;background:#f5f5f5;border-radius:2px;color:#333;font-size:7px;display:flex;align-items:center;justify-content:center;">g1</div>
<div style="width:36px;height:20px;background:#eee;border-radius:2px;color:#333;font-size:7px;display:flex;align-items:center;justify-content:center;">g2</div>
<div style="width:36px;height:20px;background:#999;border-radius:2px;color:#fff;font-size:7px;display:flex;align-items:center;justify-content:center;">g6</div>
<div style="width:36px;height:20px;background:#333;border-radius:2px;color:#fff;font-size:7px;display:flex;align-items:center;justify-content:center;">g13</div>
</div>
</div>
<!-- 14. text-align -->
<div class="sec">
<div class="sec-t">14. text-align</div>
<div style="font-size:12px;text-align:left;background:#ecf2fe;padding:2px 4px;margin-bottom:1px;margin-right:10px;">left 左对齐</div>
<div style="font-size:12px;text-align:center;background:#e8f8f0;padding:2px 4px;margin-bottom:1px;margin-right:10px;">center 居中</div>
<div style="font-size:12px;text-align:right;background:#ecf2fe;padding:2px 4px;margin-right:10px;">right 右对齐</div>
</div>
<!-- 15. text-overflow -->
<div class="sec">
<div class="sec-t">15. text-overflow</div>
<div style="width:200px;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background:#ecf2fe;padding:2px 4px;margin-bottom:2px;">这是一段很长的文字用于测试文本溢出省略号效果展示</div>
<div style="width:200px;font-size:12px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;background:#e8f8f0;padding:2px 4px;line-height:1.4;">这是一段很长的文字用于测试多行文本溢出省略号效果展示两行截断</div>
</div>
<!-- 16. flex布局 -->
<div class="sec">
<div class="sec-t">16. flex layout</div>
<div style="padding:0 4px;">
<div style="font-size:9px;color:#999;">space-between</div>
<div style="display:flex;justify-content:space-between;margin-bottom:4px;">
<div style="width:50px;height:24px;background:#0052d9;"></div>
<div style="width:50px;height:24px;background:#00a870;"></div>
<div style="width:50px;height:24px;background:#ed7b2f;"></div>
</div>
<div style="font-size:9px;color:#999;">space-around</div>
<div style="display:flex;justify-content:space-around;margin-bottom:4px;">
<div style="width:50px;height:24px;background:#0052d9;"></div>
<div style="width:50px;height:24px;background:#00a870;"></div>
<div style="width:50px;height:24px;background:#ed7b2f;"></div>
</div>
<div style="font-size:9px;color:#999;">center</div>
<div style="display:flex;justify-content:center;gap:8px;">
<div style="width:50px;height:24px;background:#0052d9;"></div>
<div style="width:50px;height:24px;background:#00a870;"></div>
<div style="width:50px;height:24px;background:#ed7b2f;"></div>
</div>
</div>
</div>
<!-- 17. 综合卡片 -->
<div class="sec">
<div class="sec-t">17. card</div>
<div style="background:#fff;border-radius:8px;padding:10px;margin:0 4px;box-shadow:0 1px 4px rgba(0,0,0,0.06);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
<span style="font-size:14px;font-weight:600;">经营一览</span>
<span style="font-size:9px;background:#ecf2fe;color:#0052d9;padding:2px 6px;border-radius:4px;">本月</span>
</div>
<div style="font-size:22px;font-weight:600;margin-bottom:4px;">¥128,560.00</div>
<div style="font-size:11px;color:#999;">较上月 +12.5%</div>
</div>
</div>
<!-- 18. 列表项 -->
<div class="sec">
<div class="sec-t">18. list items</div>
<div style="padding:0 4px;">
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #eee;">
<span style="font-size:13px;">台费收入</span><span style="font-size:13px;font-weight:600;">¥85,200</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #eee;">
<span style="font-size:13px;">商品收入</span><span style="font-size:13px;font-weight:600;">¥32,100</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;">
<span style="font-size:13px;">助教收入</span><span style="font-size:13px;font-weight:600;">¥11,260</span>
</div>
</div>
</div>
<!-- 19. 全宽色条 -->
<div class="sec" style="border-bottom:none;">
<div class="sec-t">19. full-width bars</div>
<div style="display:flex;height:16px;margin-right:10px;">
<div style="flex:1;background:#0052d9;"></div>
<div style="flex:1;background:#00a870;"></div>
<div style="flex:1;background:#ed7b2f;"></div>
<div style="flex:1;background:#e34d59;"></div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,155 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页设置 - 球房运营助手</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#0052d9',
'primary-light': '#ecf2fe',
success: '#00a870',
warning: '#ed7b2f',
error: '#e34d59',
'gray-1': '#f3f3f3',
'gray-2': '#eeeeee',
'gray-3': '#e7e7e7',
'gray-4': '#dcdcdc',
'gray-5': '#c5c5c5',
'gray-6': '#a6a6a6',
'gray-7': '#8b8b8b',
'gray-8': '#777777',
'gray-9': '#5e5e5e',
'gray-10': '#4b4b4b',
'gray-11': '#393939',
'gray-12': '#2c2c2c',
'gray-13': '#242424',
},
fontFamily: {
sans: ['Noto Sans SC', 'sans-serif'],
}
}
}
}
</script>
<style>
body {
font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, sans-serif;
}
.safe-area-top {
padding-top: env(safe-area-inset-top, 44px);
}
.radio-checked {
border-color: #0052d9;
background: #0052d9;
}
.radio-checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
}
.option-card {
transition: all 0.2s ease;
}
.option-card.selected {
border-color: #0052d9;
background: linear-gradient(135deg, #ecf2fe 0%, #f8faff 100%);
}
</style>
</head>
<body class="bg-gray-1 min-h-screen">
<!-- 顶部导航 -->
<div class="safe-area-top bg-white sticky top-0 z-10">
<div class="h-11 flex items-center relative border-b border-gray-2 px-4">
<button onclick="history.back()" class="absolute left-4 p-1">
<svg class="w-5 h-5 text-gray-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
</button>
<h1 class="flex-1 text-center text-base font-medium text-gray-13">首页设置</h1>
</div>
</div>
<!-- 说明文字 -->
<div class="px-4 py-4">
<p class="text-sm text-gray-7">选择登录后默认显示的首页</p>
</div>
<!-- 选项列表 -->
<div class="px-4 space-y-3">
<div id="option-task" class="option-card bg-white rounded-2xl p-4 border-2 border-transparent cursor-pointer selected" onclick="selectHome('task')">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-gradient-to-br from-primary to-blue-400 rounded-xl flex items-center justify-center shadow-sm">
<svg class="w-6 h-6 text-white" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
</svg>
</div>
<div class="flex-1">
<p class="text-base font-medium text-gray-13 mb-1">任务</p>
<p class="text-xs text-gray-6">查看待办任务和业绩概览</p>
</div>
<div id="radio-task" class="w-5 h-5 border-2 border-gray-4 rounded-full relative radio-checked"></div>
</div>
</div>
<div id="option-board" class="option-card bg-white rounded-2xl p-4 border-2 border-transparent cursor-pointer" onclick="selectHome('board')">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-gradient-to-br from-success to-green-400 rounded-xl flex items-center justify-center shadow-sm">
<svg class="w-6 h-6 text-white" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
<div class="flex-1">
<p class="text-base font-medium text-gray-13 mb-1">看板</p>
<p class="text-xs text-gray-6">查看财务、客户、助教数据</p>
</div>
<div id="radio-board" class="w-5 h-5 border-2 border-gray-4 rounded-full relative"></div>
</div>
</div>
</div>
<!-- 提示信息 -->
<div class="px-4 py-6">
<div class="flex items-start gap-3 p-4 bg-primary/5 rounded-xl">
<svg class="w-5 h-5 text-primary flex-shrink-0 mt-0.5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>
<p class="text-xs text-gray-7 leading-relaxed">设置会自动保存,切换选项后即刻生效。退出登录后重新登录仍会保持您的设置。</p>
</div>
</div>
<script>
function selectHome(type) {
const taskRadio = document.getElementById('radio-task');
const boardRadio = document.getElementById('radio-board');
const taskOption = document.getElementById('option-task');
const boardOption = document.getElementById('option-board');
if (type === 'task') {
taskRadio.classList.add('radio-checked');
boardRadio.classList.remove('radio-checked');
taskOption.classList.add('selected');
boardOption.classList.remove('selected');
} else {
boardRadio.classList.add('radio-checked');
taskRadio.classList.remove('radio-checked');
boardOption.classList.add('selected');
taskOption.classList.remove('selected');
}
console.log('已设置默认首页为:', type);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,856 @@
# H5 原型 → 微信小程序迁移桥接规范(可执行版 v2.0
> 适用对象人工开发、AI 批量迁移、代码审查、验收回归
> 适用范围:本项目上传的 H5 原型页面、配套 CSS/JS、静态资源
> 目标:在微信原生小程序中实现页面与交互的高还原迁移;除字体渲染细微差异外,样式、层级、动效、状态切换、页面流转尽量 1:1 对齐 H5 验收原型
---
## 1. 文档定位
这不是知识笔记,而是执行规程。
使用本规范时AI 或开发者必须做到:
1. 能从单个 H5 页面产出对应的小程序页面文件。
2. 能明确哪些能力可直接迁移,哪些必须降级,哪些禁止直迁。
3. 能在不使用 DOM API 的前提下,把 H5 交互改造成小程序的数据驱动实现。
4. 能输出可交付物,而不是停留在“分析建议”。
本规范优先级高于旧版桥接文档。若旧版文档与本规范冲突,以本规范为准。
---
## 2. 项目级输入与输出契约
### 2.1 输入物
单页迁移时,输入物必须至少包含:
- 对应 H5 页面 HTML 文件
- 该页面引用的 CSS页面内 `<style>`、独立 css 文件、Tailwind utility
- 该页面引用的 JS
- 该页面依赖的图片 / 图标 / SVG / JSON Mock
- 页面跳转关系和入口参数
### 2.2 输出物
每个页面迁移后,必须至少产出:
- `pages/<route>/<route>.wxml`
- `pages/<route>/<route>.wxss`
- `pages/<route>/<route>.js`
- `pages/<route>/<route>.json`
- 该页面使用到的 mock 数据文件或页面内 mock 常量
- 迁移说明(至少包含:状态变量、事件、待确认项、降级项)
### 2.3 不允许的输出
以下结果视为未完成:
- 只给“迁移思路”,不产出页面文件
- 只把 Tailwind class 翻译成 CSS不改 WXML / 交互
- 继续依赖 `document.*``window.*``history.*``localStorage` 等浏览器 API
- 以“兼容性未知”为由保留 H5 写法不处理
- 把高风险 CSS 特性原样搬过去,且不写降级方案
---
## 3. 项目级强制决策
### 3.1 框架与运行时
1. **必须使用微信原生小程序页面体系**WXML + WXSS + JS + JSON。
2. **禁止继续在小程序端运行 Tailwind CDN / 浏览器脚本。**
3. **禁止使用 DOM 驱动交互。** 页面必须改为数据驱动。
4. **不引入 Taro / uni-app / WebView 兜底。** 本规范面向原生小程序页面迁移。
### 3.2 验收基准
1. H5 原型的主要验收基准宽度为 **412 CSS px**
2. 小程序以 412 宽设备的视觉效果为主验收目标。
3. 其他常见宽度设备允许比例差异,但不得出现:文本溢出、遮挡、错层、关键按钮点击区域异常、吸顶/弹层错位。
### 3.3 单位策略
采用 **`rpx` 为主、`px` 为辅** 的混合单位方案。
- **优先 `rpx`**:页面宽度、横向布局、卡片尺寸、主容器 padding / margin、列表间距、栅格、吸顶区高度。
- **优先 `px`**1px 发丝线、阴影 blur/spread、小图标、绝对定位微调、小控件尺寸、状态栏 / 安全区补偿、细描边。
- **按效果决定**:字号、圆角、局部 padding、某些 transform 位移。
换算基准:
```text
rpx = H5_CSS_px × (750 / 412)
≈ H5_CSS_px × 1.8204
```
默认规则:
- 普通布局值:四舍五入到整数 `rpx`
- 视觉敏感值:允许人工微调,不强行套公式
- 不使用“统一取偶数 rpx”的硬规则
### 3.4 页面滚动策略
1. 默认使用 **页面自然滚动**
2. 只有在必须做局部滚动区域时,才使用 `scroll-view`
3. 吸顶、滚动联动、section 感知优先围绕页面滚动实现;不要无必要把整页包进 `scroll-view`
### 3.5 样式组织策略
1. 全局 token、复用组件样式放 `app.wxss` 或公共样式文件。
2. 页面私有样式放页面同名 `.wxss`
3. 样式优先使用 class动态值用内联 style 绑定。
4. 不以 CSS 变量作为唯一运行前提;优先输出静态色值 / 静态 token。若项目后续验证 CSS 变量稳定可用,可作为增强层,不可作为唯一实现路径。
### 3.6 文本与行高策略 ⚠️
**关键发现**:微信小程序的 `<text>` 组件不能直接设置 `line-height`,必须通过外层 `<view>` 设置。
**全局设置(推荐):**
```wxss
page {
line-height: 1.5; /* Tailwind 默认行高 */
}
view {
line-height: inherit; /* view 继承 page 的 line-height */
}
/* text 会自动继承外层 view 的 line-height不需要额外设置 */
```
**局部覆盖:**
```wxss
.section-title {
font-size: 26rpx;
line-height: 36rpx; /* 在 view 的 class 上设置text 会继承 */
font-weight: 600;
}
```
**错误做法:**
```wxss
/* ❌ 直接在 text 上设置 line-height 无效 */
text {
line-height: 36rpx; /* 不会生效 */
}
```
**执行规则:**
1. 所有文本类样式必须同时设置 `font-size``line-height`
2. `line-height` 必须在外层 `<view>` 的 class 上设置,不能在 `<text>` 上设置
3. 标准字号的 `line-height` 换算参考附录 B.3
4. 若发现 Y 轴高度与 H5 端不一致,优先检查 `line-height` 是否正确设置
### 3.7 SVG / 图标策略
1. **禁止把 H5 的内联 `<svg>...</svg>` 直接当 WXML DOM 搬运。**
2. 资源层允许 `svg` 文件进入小程序项目包,但页面显示是否稳定,必须以目标设备实测为准。
3. 默认执行顺序:
- 简单静态图标:优先导出为独立资源文件
-`svg` 渲染不稳定:转为 `png`
- 高频单色图标:允许改为 iconfont / 组件化图标
4. 动态换色的 SVG 不要临时拼接字符串;应改为多份资源、图标组件或样式控制的等价实现。
---
## 4. 技术基线与能力分级
> 说明:本规范不把“经验上可能可用”的能力写成“完全支持”。
> 所有能力分三类A 可直接用B 有条件使用C 禁止直迁。
### 4.1 A 类:可直接使用
以下能力在当前项目中视为稳定迁移能力:
- `app.wxss` / 页面 `wxss` / `@import`
- `rpx`
- `.class``#id`、元素选择器、`::before``::after`
- `flex``position: relative/absolute/fixed`
- `box-shadow``border-radius``linear-gradient`
- `opacity`、常规 `transform`
- `transition``animation`
- 动态内联样式绑定
- `onPageScroll`
- `wx.createAnimation()`
- `wx.createSelectorQuery()`
- `wx.createIntersectionObserver()`
- `navigator` / `wx.navigateTo` / `wx.redirectTo` / `wx.switchTab` / `wx.navigateBack`
- `view` / `navigator``hover-class`
### 4.2 B 类:有条件使用,必须带回退或验证
以下能力允许使用,但不能把它们当成项目硬依赖:
- `position: sticky`
- `grid`
- `filter: blur()`
- `backdrop-filter`
- `clip-path`
- `vh`
- `line-clamp`
- 复杂组合选择器
- 资源型 `svg`
- CSS 变量
执行规则:
1. 使用前必须确认该页面已有回退方案。
2. 若页面对视觉或交互依赖度高,则必须做真机验证。
3. 若 B 类能力失效,不得阻塞交付,必须切换为本规范定义的降级实现。
### 4.3 C 类:禁止直迁
以下能力不得原样从 H5 搬到小程序:
- `document.getElementById/querySelector/querySelectorAll`
- `classList.add/remove/toggle`
- 直接改 `element.style.xxx`
- `innerHTML` / `textContent` 方式渲染视图
- `window.scrollY` / `window.scrollTo`
- `history.back`
- `localStorage` / `sessionStorage`
- `alert` / `confirm`
- CSS `env(safe-area-inset-*)`
- `:hover` / `group-hover:*`
- 依赖浏览器焦点模型的 `:focus` / `:active` 交互
- 依赖 `:first-child/:last-child/:nth-child` 才能成立的视觉规则
---
## 5. 页面转换的标准执行流程
每迁移 1 个页面,都必须按以下顺序执行。
### 步骤 1页面资产盘点
输入HTML / CSS / JS / 图片资源
动作:
- 抽取页面标题、路由名、入口参数、返回路径
- 列出页面块顶部、卡片区、列表区、底部操作区、弹层、浮层、Toast
- 列出页面状态:默认态、空态、加载态、错误态、选中态、展开态、禁用态
- 列出页面动效:淡入、位移、缩放、吸顶、滚动联动、长按、评分拖动
输出:页面迁移卡
### 步骤 2结构改写为 WXML 语义树
动作:
- `div``view`
- 文本优先用 `text`
- 图片用 `image`
- 链接/跳转用 `navigator``bindtap`
- 表单元素替换为原生小程序组件
- 列表一律改为 `wx:for`
- 条件显示一律改为 `wx:if` / `hidden` / 条件 class
禁止:
- 保留 H5 DOM 层级只是把标签名替换一下
-`text` 内嵌套 `view`
- 用字符串拼接 HTML 片段
### 步骤 3Tailwind 与自定义 CSS 拆解
动作:
- 先按视觉功能拆成:布局、尺寸、文本、颜色、装饰、状态、动画
- 再判断每个值使用 `rpx` 还是 `px`
- 统一抽出公共 token不允许每页各自发散命名
要求:
- 不保留“一长串 utility class 直接照搬”的中间态
- 对频繁出现的组合样式抽成语义类,如 `.card``.section-title``.tab--active`
### 步骤 4交互从 DOM 驱动改为数据驱动
动作:
- 把所有 DOM 查询改成状态变量
- 把显隐、激活、展开、选中、校验、加载都改成 `data` 驱动
- 把滚动联动改成 `onPageScroll` + 节流 + 仅在状态变化时 `setData`
输出:状态表
状态表至少包含:
- 状态变量名
- 默认值
- 可选值
- 受哪个事件修改
- 影响哪些视图
### 步骤 5页面级动效实现
执行规则:
- 纯显隐 / 透明度 / 位移 / 简单位移动画:优先 `transition` / `animation`
- 需要顺序编排、组合变换、受事件精确驱动:用 `wx.createAnimation()`
- 高频交互不要每帧 `setData`
### 步骤 6导航与参数改造
动作:
- 普通页面:`wx.navigateTo`
- 替换当前页:`wx.redirectTo`
- tabBar 页面:`wx.switchTab`
- 返回:`wx.navigateBack`
- 页面参数:统一在 `onLoad(query)` 中读取
### 步骤 7安全区与导航栏补偿
动作:
- 禁止使用 `env(safe-area-inset-*)`
- 通过系统信息计算状态栏与底部安全区
- 自定义导航栏页面必须统一实现,不得一页一套
### 步骤 8Mock 数据接入
动作:
- 页面先用 mock 跑通
- 模板静态文案与后端数据字段分开
- 所有列表、徽标、状态标签都要有 mock
### 步骤 9真机回归与像素微调
动作:
- 先看 412 宽主验收设备
- 再看至少一台 375 宽设备
- 出现不对齐时优先微调局部 `px/rpx`,不要推翻整体单位策略
### 步骤 10记录迁移说明
每页必须输出迁移说明,至少包含:
- 页面依赖资源
- 迁移状态变量
- 高风险点
- 已做降级项
- 待后端对接字段
- 待真机复核项
---
## 6. Tailwind → WXSS 的执行规则
## 6.1 间距与尺寸
执行原则:
1. 标准 spacing 先换算为 H5 实际 px再决定 `rpx` / `px`
2. 主容器、卡片、列表间距优先 `rpx`
3. 图标、徽标、定位补偿优先 `px`
4. arbitrary 值不能偷懒统一换 `rpx`,必须逐项判断。
### 建议 token412 基准)
| H5 px | 建议值 |
|---|---|
| 4px | 8rpx |
| 8px | 14rpx |
| 12px | 22rpx |
| 16px | 29~30rpx |
| 20px | 36rpx |
| 24px | 44rpx |
| 32px | 58rpx |
| 40px | 72rpx |
说明:
- 16px 允许落在 29rpx 或 30rpx以真机比对为准。
- 阴影参数不要强制 `rpx` 化;优先保留 `px` 质感。
## 6.2 文本
### 标准字号类
对 Tailwind 标准字号类,迁移时应同时输出 `font-size``line-height`
### arbitrary 字号
规则:
1. `text-[Npx]` 不能直接假设其 line-height = 1.2 倍字号。
2. 若原型页面显式设置了 `leading-*`,以显式值为准。
3. 若未显式设置,则应从原型的实际计算样式、截图观感或上下文排版需求确定行高。
4. 对单行标签/徽章文字,可使用更紧的 line-height对正文、说明文必须按真实视觉高度定。
禁止:
- 在全项目层面硬编码“所有 arbitrary 字号默认 1.2 倍行高”
## 6.3 颜色
执行原则:
1. 颜色优先输出静态十六进制 / `rgba()`
2. Tailwind 透明度修饰符转成 `rgba()`
3. 若项目后续统一做 token 化,可再抽公共常量;首轮迁移不要把颜色系统设计复杂化。
## 6.4 布局
### Flex
默认首选。能用 flex 做出来的布局,不优先上 grid。
### Grid
仅在以下条件同时满足时允许:
- 页面是规则网格
- 列数固定
- 没有复杂跨列
- 真机验证通过
否则改用 flex + wrap + 固定宽度 / 百分比。
## 6.5 组合类
以下 Tailwind 组合迁移时不要机械翻译:
- `space-y-*`
- `divide-y`
- `group-hover:*`
- `peer-*`
- `hover:*`
- `focus:*`
- `active:*`
迁移规则:
- `space-y-*`:优先给循环项单独 class选择器方案仅作受控场景补充
- `divide-y`:优先对列表项显式加边线 class并根据 index 控首项
- `hover:*`:改 `hover-class` 或按压反馈
- `focus:*`:用 `bindfocus/bindblur` + 状态 class
---
## 7. HTML / 事件 / API 的标准映射
## 7.1 标签映射
| H5 | 小程序 |
|---|---|
| `div` | `view` |
| `span` | `text` |
| `p` | `view` / `text` |
| `img` | `image` |
| `a` | `navigator` / `view bindtap` |
| `button` | `button` / `view bindtap` |
| `input` | `input` |
| `textarea` | `textarea` |
| `ul/ol/li` | `view + wx:for` |
| `select` | `picker` |
补充规则:
- `image``mode` 必须按原图展示意图设置,不允许默认留空就交付。
- `object-fit: cover` 常映射为 `aspectFill`;但若原图不能被裁切,则应改为 `aspectFit`
## 7.2 事件映射
| H5 | 小程序 |
|---|---|
| `onclick` | `bindtap` |
| `oninput` | `bindinput` |
| `onchange` | `bindchange` |
| `onscroll` | `bindscroll` / `onPageScroll` |
| `ontouchstart` | `bindtouchstart` |
| `ontouchmove` | `bindtouchmove` |
| `ontouchend` | `bindtouchend` |
| `oncontextmenu` | `bindlongpress` |
注意:这只是常见替换,不代表浏览器事件语义与小程序事件语义完全等价。
## 7.3 冒泡与阻断
- `bind*`:正常冒泡
- `catch*`:阻止事件向上冒泡
弹层、蒙层、内部内容区必须明确设计谁冒泡、谁拦截,不能边做边猜。
## 7.4 常见浏览器 API 替代
| H5 | 小程序 |
|---|---|
| `history.back()` | `wx.navigateBack()` |
| `window.location.href` | `wx.navigateTo()` |
| `window.location.replace` | `wx.redirectTo()` |
| tab 页跳转 | `wx.switchTab()` |
| `window.scrollY` | `onPageScroll().scrollTop` |
| `navigator.clipboard.writeText` | `wx.setClipboardData()` |
| `localStorage` | `wx.setStorageSync/getStorageSync` |
| `alert/confirm` | `wx.showToast/wx.showModal` |
---
## 8. JS 交互模式的标准改写
## 8.1 显隐切换
H5 的 `classList.add/remove('hidden')` 一律改为:
- `wx:if`
-`hidden`
- 或状态 class
默认建议:
- 大块弹层:`wx:if`
- 仅做过渡且结构稳定:状态 class + transition
## 8.2 Tab / 选中态
统一改为单一状态变量:
```js
Page({
data: { activeTab: 'basic' },
onTabChange(e) {
this.setData({ activeTab: e.currentTarget.dataset.tab })
}
})
```
禁止:
- 同时维护多个布尔值表示同一个 tab 组选中态
## 8.3 展开 / 收起
统一改为对象映射或数组项状态,不允许操作 DOM 文本改按钮文案。
## 8.4 滚动联动
执行规则:
1. `onPageScroll` 只在确实需要时声明。
2. 必须节流或去抖。
3. 只在状态变化时 `setData`
4. 吸顶标题、目录高亮、section 感知,优先用 `IntersectionObserver`
## 8.5 长按菜单
原型中的“卡片旁锚点弹出菜单”在小程序中优先改为:
- 底部 action sheet
- 或固定底部操作面板
原因:
- 更稳定
- 更易复用
- 更符合触屏交互
## 8.6 评分拖动 / 拖拽类交互
规则:
1. 触摸起始时缓存容器 rect。
2. `touchmove` 中仅基于缓存坐标计算,不每次重新 query。
3. 评分变化只在值变化时更新。
## 8.7 Toast / 短提示
优先级:
1. 系统 `wx.showToast`
2. 若原型要求强定制视觉,再做自定义 toast 组件
## 8.8 动画
优先级:
1. WXSS transition / animation
2. `wx.createAnimation()`
3. 避免高频 `setData` 驱动逐帧动画
---
## 9. 高风险能力的强制降级规则
## 9.1 `sticky`
执行顺序:
1. 页面自然滚动场景:先尝试 `position: sticky`
2. 若在 `scroll-view` 中:不要硬上 sticky改为滚动监听/观察者驱动
3. 多层 sticky只保留最外层 sticky
## 9.2 `backdrop-filter`
规则:
- 不作为交付必需能力
- 默认必须提供半透明背景降级
- 若失效,不得阻塞上线
## 9.3 `vh`
规则:
- 不把 `100vh` 当成万能全屏高度
- 涉及自定义导航栏、安全区、底部操作栏的页面,必须计算可用高度
- 能用 `min-height` + 内边距解决的,不强依赖 `vh`
## 9.4 复杂选择器
规则:
- WXSS 公开支持的基础选择器以 `.class``#id`、元素、`::before/::after` 为准
- 组合选择器只在局部受控场景下使用
- 不把复杂结构选择器写成项目标准手段
## 9.5 CSS 变量
规则:
- 首轮迁移不强制依赖 `var()`
- 颜色、渐变、阴影优先输出静态值
- 若后续项目验证 CSS 变量稳定可用,可作为增强重构项,不是首轮交付阻塞项
---
## 10. 模板内容与数据内容的边界
每个页面都要把内容拆成三类:
### 10.1 模板固化内容
这些通常写死在页面模板或常量里:
- 页面标题
- 分区标题
- 固定按钮文案
- 空态文案
- Tooltip 固定说明
- 表头 / 标签名
### 10.2 Mock / 后端数据内容
这些必须抽成数据:
- 卡片列表
- 客户信息
- 任务信息
- 看板统计值
- 徽标状态
- 提示数量
- 聊天记录
- 备注内容
- 评分值
- 时间轴 / 历史记录
### 10.3 前端派生内容
这些应由前端根据后端字段计算:
- 状态色 class
- 选中态 class
- 进度条宽度
- 文案格式化
- 百分比显示
- 数字千分位
- 是否显示空态 / 加载态 / 错误态
---
## 11. 页面目录与公共层建议结构
```text
app.js
app.json
app.wxss
/common
/styles
tokens.wxss
mixins.wxss
animation.wxss
/utils
format.js
system.js
nav.js
/mock
xxx.js
/components
app-navbar/
app-tab/
app-card/
app-empty/
app-toast/
app-action-sheet/
/pages
/task-list
/task-detail
/board-finance
/board-customer
/board-coach
/notes
/chat
...
/assets
/images
/icons
```
执行要求:
- 可复用的组件必须抽离
- 不允许把每页都做成孤岛
- 但也禁止首轮过度抽象,影响交付速度
---
## 12. 每页迁移完成的验收清单
以下清单必须全部勾过,页面才算完成。
### 12.1 结构
- [ ] 页面可正常进入
- [ ] 路由与参数正确
- [ ] 结构层级与原型一致
- [ ] 图片、图标、背景都已替换到位
### 12.2 样式
- [ ] 主布局宽度、边距、卡片尺寸与原型接近
- [ ] 文本字号、行高、字重合理
- [ ] 圆角、边框、阴影、渐变已还原
- [ ] 吸顶、弹层、底栏无错位
- [ ] 412 宽设备通过视觉验收
### 12.3 交互
- [ ] 点击反馈存在
- [ ] tab 切换正确
- [ ] 弹层开关正确
- [ ] 长按 / 复制 / 展开 / 收起 / 评分等交互正确
- [ ] 返回和跳转链路正确
### 12.4 性能
- [ ] 滚动时无明显抖动
- [ ] 没有在高频事件中无脑 `setData`
- [ ] 动画不依赖逐帧 JS
### 12.5 风险项
- [ ] 所有降级项已记录
- [ ] 所有待真机复核项已记录
- [ ] 所有待接口对接字段已记录
---
## 13. AI 执行模板(标准提示词契约)
以下模板用于驱动 AI 按页迁移。
```md
你现在负责把指定 H5 页面迁移为微信原生小程序页面。
必须遵守以下约束:
1. 输出原生小程序文件wxml / wxss / js / json。
2. 禁止使用任何 DOM API、window、history、localStorage。
3. 禁止继续依赖 Tailwind 运行时;需将 Tailwind 与页面自定义样式改写为可维护的 WXSS。
4. 视觉目标是最大程度还原 H5默认以 412 宽设备为验收基准。
5. 单位策略为 rpx 为主、px 为辅:布局主尺寸优先 rpx发丝线/小图标/阴影/绝对定位微调优先 px。
6. 所有交互必须改为数据驱动。
7. 对 sticky / backdrop-filter / svg / vh / grid 等高风险能力,必须给出降级或验证说明。
8. 每输出一个页面,必须附带:
- 页面状态变量表
- 事件处理函数表
- mock 数据结构
- 未决问题与降级项
请按以下顺序输出:
A. 页面迁移摘要
B. 目录结构
C. WXML
D. WXSS
E. JS
F. JSON
G. mock 数据
H. 迁移说明与风险项
```
### 13.1 AI 页面状态表模板
```md
| 状态变量 | 类型 | 默认值 | 可选值 | 影响视图 | 触发事件 |
|---|---|---:|---|---|---|
| showFilter | boolean | false | true/false | 筛选面板 | onToggleFilter |
| activeTab | string | overview | overview/detail/... | tab内容区 | onTabChange |
```
### 13.2 AI 风险项模板
```md
- 已降级backdrop-filter 改为半透明背景
- 待验证:顶部 sticky 在真机 Android 上需复核
- 待后端:客户列表、统计卡片、备注列表当前仍为 mock
```
---
## 14. 本项目对旧版桥接文档的修订结论
旧版文档可继续参考,但以下内容不再作为硬规范:
1. 不再把大量现代 CSS 特性写成“完全支持”。
2. 不再把 arbitrary 字号 line-height 的经验值当成定理。
3. 不再把 CSS 变量当成首轮必选依赖。
4. 不再把 `.parent > view + view` 这类技巧写成唯一实现方案。
5. 不再把 `<image src="xxx.svg">` 写成无条件可靠方案。
6. 不再允许“先翻样式,交互以后再说”的半迁移状态作为交付完成。
---
## 15. 参考文档(执行时必须优先查官方)
微信小程序官方 / 腾讯云镜像:
- WXSS 样式:<https://www.tencentcloud.com/zh/document/product/1219/60346>
- 页面与 `onPageScroll`<https://www.tencentcloud.com/zh/document/product/1219/61713>
- 渲染层 / 事件模型:<https://www.tencentcloud.com/zh/document/product/1219/61744>
- `createSelectorQuery` / `createIntersectionObserver`<https://www.tencentcloud.com/document/product/1219/57688>
- 动画 `wx.createAnimation`<https://www.tencentcloud.com/zh/document/product/1219/57764>
- 视图容器 `view` / `hover-class`<https://www.tencentcloud.com/zh/document/product/1219/57773>
- 导航组件 `navigator`<https://www.tencentcloud.com/zh/document/product/1219/57779>
- 小程序目录结构与可上传文件类型:<https://www.tencentcloud.com/zh/document/product/1219/57660>
Tailwind 官方:
- Font Size<https://v3.tailwindcss.com/docs/font-size>
- Line Height<https://v3.tailwindcss.com/docs/line-height>
- Height<https://v3.tailwindcss.com/docs/height>
---
## 16. 最终执行口径
**本项目的小程序迁移,不是“把 Tailwind 翻译成 WXSS”这么简单。**
真正的执行标准是:
1. 页面结构重写为 WXML
2. 样式收敛为可维护的 WXSS
3. 交互全部改为小程序数据驱动;
4. 对高风险 CSS 特性提供可交付降级;
5. 每页都有 mock、状态表、事件表、风险说明和验收清单。
能做到这 5 条,才算“可执行版桥接规范”。

2097
_DEL/MIGRATION-PLAYBOOK.md Normal file

File diff suppressed because it is too large Load Diff

BIN
_DEL/MP.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
_DEL/WEB.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

1568
_DEL/h5-to-mp-bridge.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
# 基准测试历史记录
> 本文件记录 H5→MP 迁移项目的 Benchmark 基准测试历史,仅供参考。
> 完整推导过程见 `docs/miniprogram-dev/03-reference/benchmark-history.md`(本文件即为迁移后的版本,原 `docs/h5_ui/compare/HISTORY.md` 已归档)。
---
## 当前有效参数v3
| 参数 | H5 | MP | 说明 |
|------|-----|-----|------|
| viewport | 430×752 | 430×752 | 统一视口 |
| DPR | 1.5 | 1.5 | 双端一致 |
| 输出尺寸 | 645×1128 | 645×1128 | 零缩放 |
| rpx 系数 | — | 1:1.75 | `rpx = px × 2 × 0.875` |
## v3 基准测试结果2026-03-11
| 组 | 差异率 | 说明 |
|---|---|---|
| g1裁剪后 | 3.87% | 全部来自不可消除的系统性因素 |
差异来源:字体渲染 ~2% + 行内元素高度 ~1% + rpx 取偶 ~0.5% + 抗锯齿 ~0.5%
## 版本演进
| 版本 | 日期 | rpx 系数 | 状态 |
|------|------|---------|------|
| v2 | 2026-03-10 | 1:1.82750/412 | ❌ 废弃视口不一致、有损缩放、DPR 不对等) |
| v3 | 2026-03-11 | 1:1.75750/430 工程近似) | ✅ 当前有效 |
## 结论
1. `rpx = px × 2 × 0.875`1:1.75)是正确的迁移换算公式
2. 双端截图参数必须统一viewport=430×752, DPR=1.5
3. 像素对比的背景噪音基线约 3.87%
4. <5% 通过目标在基线之上仅有 ~1% 修正空间,公式和参数已高度准确

View File

@@ -0,0 +1,53 @@
# 各页面特殊结构速查表
> 基于 H5 源码扫描得出。处理对应页面时直接查表,不需要重新扫描。
---
| 页面 | safe-area-top | bottomNav | ai-float | position:fixed | ::before/::after | 其他风险 |
|---|---|---|---|---|---|---|
| board-finance | ✅ | ✅ | ✅ | ✅(筛选下拉) | ✅tab 指示线) | `backdrop-filter: blur()` |
| board-coach | ✅ | ✅ | ✅ | ✅(筛选下拉) | ✅tab 指示线) | — |
| board-customer | ✅ | ✅ | ✅ | ✅(筛选下拉) | ✅tab 指示线) | CSS 渐变文字 |
| task-list | — | ✅ | ✅ | ✅(筛选下拉) | — | — |
| task-detail | — | — | — | — | ✅(气泡尖角) | — |
| task-detail-callback | — | — | — | — | — | — |
| task-detail-priority | — | — | — | — | — | — |
| task-detail-relationship | — | — | — | — | — | — |
| my-profile | ✅ | ✅ | — | — | — | — |
| coach-detail | — | — | — | — | — | — |
| customer-detail | — | — | — | — | ✅ | — |
| customer-service-records | — | — | ✅ | — | — | — |
| performance | — | — | ✅ | — | — | — |
| performance-records | — | — | ✅ | — | — | — |
| notes | ✅ | — | — | — | — | — |
| chat | ✅ | — | — | — | — | — |
| chat-history | ✅ | — | — | — | — | — |
| reviewing | — | — | — | — | — | `data:image/svg` |
| no-permission | — | — | — | — | — | `data:image/svg` |
✅ = 存在该结构,需按规则处理;— = 不存在。
---
## 处理规则速查
| 结构 | MP 处理方式 |
|------|-----------|
| `.safe-area-top` | 去除 `padding-top: env(safe-area-inset-top, 44px)`MP 由 navigationBar 处理 |
| `#bottomNav` | 不迁移MP 用原生 tabBarH5 截图时隐藏 |
| `.ai-float-btn-container` | 双端截图前隐藏 |
| `<dev-fab />` | MP 截图前 `wx:if="{{false}}"` |
| `.filter-overlay` | 优先用组件遮罩层 |
| `.tab-active::after` | 额外 `<view>` 模拟 |
| `.speech-bubble::after` | 绝对定位 `<view>` + `transform: rotate(45deg)` |
| `data:image/svg+xml` | 导出 PNG/base64 或用 CSS 渐变模拟 |
---
## 页面导航栏模式
| 模式 | 页面 |
|------|------|
| A系统默认 navBar | board-finance, board-coach, board-customer, task-list, my-profile |
| B自定义 navBar | task-detail 系列, coach-detail, customer-detail, performance, notes, chat, chat-history, customer-service-records, performance-records |

View File

@@ -0,0 +1,41 @@
# 版本变更记录
> 记录文档体系的版本演进。旧版本变更见下方「历史版本」章节(原 `docs/h5_ui/compare/CHANGELOG.md` 已归档)。
---
## 文档体系 v5.02026-03-12
`docs/h5_ui/compare/` 单目录体系重构为 `docs/miniprogram-dev/` 五层文档体系:
| 变更 | 说明 |
|------|------|
| 拆分 AGENT-PLAYBOOK.md | → 02-action/ 下 5 个专职代理手册 |
| 拆分 ORCHESTRATION-PLAN.md | → 01-orchestration/ 下 3 种工作模式 |
| 新增半自动模式 | user-guided-playbook.md |
| 新增新页面开发模式 | new-page-playbook.md + page-dev-agent.md |
| 提取参考层 | 03-reference/ 下 5 个规范文档 |
| 新增经验层 | 05-lessons/ 下 2 个经验文档 |
| 旧文档归档 | docs/h5_ui/compare/ 下原文件归档到 _archived/ |
---
## 历史版本AGENT-PLAYBOOK
| 版本 | 日期 | 变更摘要 |
|---|---|---|
| v4.2 | 2026-03-11 | 间距测量子代理;通用工具 measure_gaps.py |
| v4.1 | 2026-03-11 | 4 种专职代理;复杂结构专项识别 |
| v4.0 | 2026-03-11 | 截图参数重写;最大重试 5 次;全流程自动化 |
| v3.0 | 2026-03-10 | H5 viewport 430pxDPR 1.5rpx 1:1.75 |
| v2.0 | 2026-03-10 | Benchmark 分析1:1.82,已废弃) |
| v1.0 | 2026-02 | 初始版 |
## 历史版本ORCHESTRATION-PLAN
| 版本 | 日期 | 变更摘要 |
|---|---|---|
| v2.0 | 2026-03-11 | 4 种专职子代理执行模型 |
| v1.7 | 2026-03-10 | board-finance 2 维度×10 屏 |
| v1.5 | 2026-03-09 | board-customer 扩展至 8 维度 |
| v1.0 | 2026-03-09 | 初始版 59 单元 |

View File

@@ -0,0 +1,243 @@
# H5 → 微信小程序视觉还原 — 进度跟踪
> **主代理必读**:每次会话开始时先读本文件,确认当前状态后再下发任务。
> 每完成一个处理单元后立即更新本文件。
>
> 文档体系入口:[docs/miniprogram-dev/README.md](../README.md)
> 批量自动模式Power `miniprogram-h5-conversion` → `readSteering("batch-auto.md")`
---
## 当前状态(会话开始时填写)
| 项目 | 内容 |
|---|---|
| **当前处理单元** | #54 coach-detail/step-0D 批次开始) |
| **下一个单元** | #54 coach-detail/step-0 |
| **本次会话目标** | D 批次coach-detail + customer-detail + customer-service-records12 单元) |
| **MCP 状态** | ✅ 已连接wsEndpoint, healthy |
| **最后更新** | 2026-03-12 |
| **决策** | 跳过 margin 修正,接受 default 维度现有结果,推进 compare 维度 |
| **MP compare scrollHeight** | board-content: 5030px, maxScroll: 4396px |
### MCP 就绪检查清单(每次会话开始时执行)
```
[ ] mcp_weixin_devtools_mcp_get_connection_status → 已连接
[ ] Playwright MCP → 可用(测试 browser_run_code
[ ] 微信开发者工具已开启并显示目标页面
[ ] pixel-audit Power → 已激活readSteering("measure.md") 获取审计方法论)
```
> image-compare MCP 已移除2026-03-12。审计改为结构化拆解→逐级测量→偏差审计详见 Power `miniprogram-h5-conversion` → `readSteering("audit.md")`。
---
## 总览
| 指标 | 値 |
|------|-----|
| 总单元数 | 89 |
| 已完成 | 0 |
| 跳过 | 0 |
| 进行中 | 0 |
| 未开始 | 89 |
| 整体进度 | 0% |
---
## 前置任务
| # | 任务 | 状态 | 完成日期 | 备注 |
|---|------|------|----------|------|
| P0 | TS 零诊断基线检查 | ✅ 完成 | 2026-03-10 | 17 页面全部通过 |
| P1 | 跨页面共性偏差批量修复 | ✅ 完成 | 2026-03-10 | board-finance/coach/customer 三页 |
| P2 | 截图技术验证 | ✅ 完成 | 2026-03-10 | DPR=1.5 双端 645×1128 已验证 |
| P3 | AGENT-PLAYBOOK.md v4.2 更新 | ✅ 完成 | 2026-03-11 | 4种专职子代理、间距测量代理、裁剪修正 |
## A 批次board-finance/default10 单元)
> H5 scrollHeight=5600maxScroll=484810 步
> 序列0, 600, 1200, 1800, 2400, 3000, 3600, 4200, 4800, 4848
| # | 单元 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|-----------|----------|-----------|------|------|
| 1 | default/step-0 | 6.18% | 3R2回滚 | 6.18% | ✅ 通过 | 剩余为不可消除差异 |
| 2 | default/step-600 | 9.02% | 2均回滚 | 9.02% | ✅ 通过 | 含12px滚动偏移+字体渲染 |
| 3 | default/step-1200 | 11.07% | 1回滚 | 11.07% | ✅ 通过 | 含卡片间距差异(step-0元素)+字体渲染 |
| 4 | default/step-1800 | 4.90% | 0 | 4.90% | ✅ 通过 | 直接通过,无需修正 |
| 5 | default/step-2400 | 17.86% | 2 | 15.84% | ⚠️ 跳过 | 前序板块累积高度差异~103pxmock数据已修正 |
| 6 | default/step-3000 | 14.72% | 0 | 14.72% | ⚠️ 跳过 | 累积高度偏移~153px+TOC浮层mock数据一致 |
| 7 | default/step-3600 | 14.88% | 0 | 14.88% | ⚠️ 跳过 | 累积高度偏移153-260px+TOC浮层mock数据一致 |
| 8 | default/step-4200 | 3.28% | 0 | 3.28% | ✅ 通过 | 页面底部两端均接近maxScroll |
| 9 | default/step-4800 | 3.31% | 0 | 3.31% | ✅ 通过 | H5 clamp到4203MP clamp到3873 |
| 10 | default/step-4848 | 3.31% | 0 | 3.31% | ✅ 通过 | 与step-4800像素级一致 |
## A 批次board-finance/compare10 单元)
> 环比开启后页面高度可能变化scrollTop 序列需实测确认
| # | 单元 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|-----------|----------|-----------|------|------|
| 11 | compare/step-0 | 6.14% | 0 | 6.14% | ✅ 通过 | 剩余为不可消除差异与default维度一致 |
| 12 | compare/step-600 | 10.06% | 0 | 10.06% | ✅ 通过 | 与default 9.02%接近,+1.04%来自环比元素 |
| 13 | compare/step-1200 | 11.06% | 0 | 11.06% | ✅ 通过 | 与default 11.07%几乎一致 |
| 14 | compare/step-1800 | 4.39% | 0 | 4.39% | ✅ 通过 | 与default 4.90%接近,直接通过 |
| 15 | compare/step-2400 | 10.78% | 0 | 10.78% | ⚠️ 跳过 | 累积高度偏移与default维度同因 |
| 16 | compare/step-3000 | 16.32% | 0 | 16.32% | ⚠️ 跳过 | 累积高度偏移与default维度同因 |
| 17 | compare/step-3600 | 7.47% | 0 | 7.47% | ⚠️ 跳过 | 累积高度偏移+MP scrollTop被clamp |
| 18 | compare/step-4200 | 9.34% | 0 | 9.34% | ⚠️ 跳过 | MP maxScroll clamp两端内容窗口偏移327px |
| 19 | compare/step-4800 | 3.41% | 0 | 3.41% | ✅ 通过 | 页面底部两端均clamp到maxScroll |
| 20 | compare/step-4827 | 3.33% | 0 | 3.33% | ✅ 通过 | 与step-4800像素级一致 |
## A 批次board-coach4 单元单屏×4 维度)
| # | 单元 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|-----------|----------|-----------|------|------|
| 21 | perf/step-0 | 11.20% | 4 | 10.92% | ✅ 条件通过 | 扣除白名单(tab-bar+字体)后<3% |
| 22 | salary/step-0 | 11.23% | 0 | 11.23% | ✅ 条件通过 | 与perf基线delta+0.31% |
| 23 | sv/step-0 | 10.75% | 0 | 10.75% | ✅ 条件通过 | 与perf基线delta-0.17% |
| 24 | task/step-0 | 10.48% | 0 | 10.48% | ✅ 条件通过 | 与perf基线delta-0.44% |
## A 批次board-customer8 单元单屏×8 维度)
| # | 单元 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|-----------|----------|-----------|------|------|
| 25 | recall/step-0 | 6.90% | 0 | 6.90% | ✅ 通过 | 白名单完全覆盖净差异≈0% |
| 26 | potential/step-0 | 4.25% | 0 | 4.25% | ✅ 通过 | 白名单完全覆盖 |
| 27 | balance/step-0 | 4.15% | 0 | 4.15% | ✅ 通过 | 白名单完全覆盖 |
| 28 | recharge/step-0 | 4.47% | 0 | 4.47% | ✅ 通过 | 白名单完全覆盖 |
| 29 | recent/step-0 | 4.30% | 0 | 4.30% | ✅ 通过 | 白名单完全覆盖 |
| 30 | spend60/step-0 | 3.87% | 0 | 3.87% | ✅ 通过 | 白名单完全覆盖 |
| 31 | freq60/step-0 | 4.58% | 0 | 4.58% | ✅ 通过 | 白名单完全覆盖 |
| 32 | loyal/step-0 | 4.57% | 0 | 4.57% | ✅ 通过 | 白名单完全覆盖 |
## B 批次task-list + my-profile4 单元)
| # | 单元 | 页面 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|------|-----------|----------|-----------|------|------|
| 33 | step-0 | task-list | 40.82% | 2 | 40.56% | ⚠️ 跳过 | Banner 用户信息区域结构差异+mock 数据+红戳/进度条设计差异 |
| 34 | step-600 | task-list | 8.11% | 0 | 8.11% | ✅ 通过 | 白名单覆盖tab-bar+字体+mock 数据差异) |
| 35 | step-676 | task-list | 7.08% | 0 | 7.08% | ✅ 通过 | 白名单覆盖 |
| 36 | step-0 | my-profile | 1.61% | 0 | 1.61% | ✅ 通过 | 完美还原,零修正 |
## C 批次task-detail 系列17 单元)
| # | 单元 | 页面 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|------|-----------|----------|-----------|------|------|
| 37 | step-0 | task-detail | 22.94% | 3 | 21.13% | ⚠️ 跳过 | mock 数据差异~15%+Banner 纹理~3%+字体渲染~3% |
| 38 | step-600 | task-detail | 9.88% | 4 | 9.31% | ⚠️ 跳过 | 白名单覆盖(字体+Banner纹理+行高偏差+AI图标 |
| 39 | step-1200 | task-detail | 7.39% | 1 | 7.39% | ⚠️ 跳过 | 白名单覆盖(字体+行高偏差+窗口微偏移) |
| 40 | step-1800 | task-detail | 6.98% | 1 | 6.90% | ⚠️ 跳过 | 白名单覆盖(内容窗口错位~3-4%+字体渲染) |
| 41 | step-2243 | task-detail | 6.41% | 0 | 6.41% | ⚠️ 跳过 | 白名单覆盖(内容窗口错位+字体渲染R1回滚 |
| 42 | step-0 | task-detail-callback | 20.07% | 2 | 19.80% | ⚠️ 跳过 | 技术栈实现差异+话术设计变体+mock 数据 |
| 43 | step-600 | task-detail-callback | 15.14% | 1 | 15.05% | ⚠️ 跳过 | 白名单覆盖(话术设计变体+字体+CSS实现差异 |
| 44 | step-1200 | task-detail-callback | 12.73% | 0 | 12.73% | ⚠️ 跳过 | 白名单覆盖(窗口偏移+话术变体+字体渲染) |
| 45 | step-1645 | task-detail-callback | 7.15% | 0 | 7.15% | ⚠️ 跳过 | 白名单覆盖(话术变体+字体+CSS双端 maxScroll clamp |
| 46 | step-0 | task-detail-priority | 24.09% | 0 | 24.09% | ⚠️ 跳过 | 白名单覆盖mock数据+字体+Banner纹理+技术栈差异orange主题色正确 |
| 47 | step-600 | task-detail-priority | 10.78% | 0 | 10.78% | ⚠️ 跳过 | 白名单覆盖(字体+Banner纹理+mock数据+rpx偏移 |
| 48 | step-1200 | task-detail-priority | 11.15% | 0 | 11.15% | ⚠️ 跳过 | 白名单覆盖(窗口错位+字体+Banner纹理MP scrollTop clamp到943 |
| 49 | step-1637 | task-detail-priority | 10.26% | 0 | 10.26% | ⚠️ 跳过 | 白名单覆盖(窗口错位+字体+Banner纹理双端 maxScroll clamp |
| 50 | step-0 | task-detail-relationship | 20.49% | 0 | 20.49% | ⚠️ 跳过 | 白名单覆盖mock数据+字体+Banner纹理+技术栈差异pink主题色正确 |
| 51 | step-600 | task-detail-relationship | 12.84% | 0 | 12.84% | ⚠️ 跳过 | 白名单覆盖mock数据+字体+Banner纹理+CSS差异 |
| 52 | step-1200 | task-detail-relationship | 14.68% | 0 | 14.68% | ⚠️ 跳过 | 白名单覆盖窗口严重错位559px+字体+mock数据+Banner纹理 |
| 53 | step-1523 | task-detail-relationship | 13.36% | 0 | 13.36% | ⚠️ 跳过 | 白名单覆盖窗口错位873px+字体+mock数据双端 maxScroll clamp |
## D 批次详情页12 单元)
| # | 单元 | 页面 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|------|-----------|----------|-----------|------|------|
| 54 | step-0 | coach-detail | — | — | — | 未开始 | |
| 55 | step-600 | coach-detail | — | — | — | 未开始 | |
| 56 | step-1200 | coach-detail | — | — | — | 未开始 | |
| 57 | step-1800 | coach-detail | — | — | — | 未开始 | |
| 58 | step-2166 | coach-detail | — | — | — | 未开始 | |
| 59 | step-0 | customer-detail | — | — | — | 未开始 | |
| 60 | step-600 | customer-detail | — | — | — | 未开始 | |
| 61 | step-1200 | customer-detail | — | — | — | 未开始 | |
| 62 | step-1800 | customer-detail | — | — | — | 未开始 | |
| 63 | step-2318 | customer-detail | — | — | — | 未开始 | |
| 64 | step-0 | customer-service-records | — | — | — | 未开始 | |
| 65 | step-209 | customer-service-records | — | — | — | 未开始 | |
## E 批次绩效页面18 单元)
| # | 单元 | 页面 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|------|-----------|----------|-----------|------|------|
| 66 | step-0 | performance | — | — | — | 未开始 | |
| 67 | step-600 | performance | — | — | — | 未开始 | |
| 68 | step-1200 | performance | — | — | — | 未开始 | |
| 69 | step-1800 | performance | — | — | — | 未开始 | |
| 70 | step-2400 | performance | — | — | — | 未开始 | |
| 71 | step-3000 | performance | — | — | — | 未开始 | |
| 72 | step-3600 | performance | — | — | — | 未开始 | |
| 73 | step-4200 | performance | — | — | — | 未开始 | |
| 74 | step-4800 | performance | — | — | — | 未开始 | |
| 75 | step-5400 | performance | — | — | — | 未开始 | |
| 76 | step-6000 | performance | — | — | — | 未开始 | |
| 77 | step-6600 | performance | — | — | — | 未开始 | |
| 78 | step-6953 | performance | — | — | — | 未开始 | |
| 79 | step-0 | performance-records | — | — | — | 未开始 | |
| 80 | step-600 | performance-records | — | — | — | 未开始 | |
| 81 | step-1200 | performance-records | — | — | — | 未开始 | |
| 82 | step-1800 | performance-records | — | — | — | 未开始 | |
| 83 | step-1925 | performance-records | — | — | — | 未开始 | |
## F 批次对话页面3 单元)
| # | 单元 | 页面 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|------|-----------|----------|-----------|------|------|
| 84 | step-0 | chat | — | — | — | 未开始 | |
| 85 | step-309 | chat | — | — | — | 未开始 | |
| 86 | step-0 | chat-history | — | — | — | 未开始 | |
## G 批次其他3 单元)
| # | 单元 | 页面 | 初始差异率 | 修正轮次 | 最终差异率 | 状态 | 备注 |
|---|------|------|-----------|----------|-----------|------|------|
| 87 | step-0 | notes | — | — | — | 未开始 | |
| 88 | step-600 | notes | — | — | — | 未开始 | |
| 89 | step-957 | notes | — | — | — | 未开始 | |
---
## 主代理会话恢复流程
**每次新会话开始时,主代理必须执行以下步骤:**
```
步骤1读取本文件PROGRESS.md
→ 找到「当前状态」区块,确认「下一个单元」
→ 扫描单元表,找到第一个状态为「进行中」或「未开始」的行
步骤2MCP 就绪检查
→ mcp_weixin_devtools_mcp_get_connection_status
→ 若未连接等待10秒后重试最多3次
→ 3次失败后mcp_weixin_devtools_mcp_recompile 重新编译,再重试
步骤3更新「当前状态」区块
→ 填写「当前处理单元」和「本次会话目标」
步骤4从「下一个单元」开始下发任务
→ 严格按编号顺序,单元完成后立即更新本文件对应行
```
**单元状态说明:**
| 状态 | 含义 |
|------|------|
| 未开始 | 尚未处理 |
| 进行中 | 当前会话正在处理 |
| ✅ 通过 | 差异率 <5%,已收敛 |
| ⚠️ 跳过 | 5轮未收敛已记录差异继续下一单元 |
| 🔁 重写中 | 差异率 >20%,触发结构重写流程 |
---
## 变更日志
| 日期 | 变更 |
|------|------|
| 2026-03-12 | 迁移至 docs/miniprogram-dev/04-audit/,更新内部路径引用 |
| 2026-03-11 | 新增会话恢复机制、MCP就绪检查、重写状态标记 |
| 2026-03-10 | 全部 89 单元初始化,前置任务 P0-P3 完成 |

View File

@@ -0,0 +1,66 @@
# 收敛模式与不可消除差异
> 记录视觉还原过程中的收敛规律和不可消除差异白名单。
> 基于 A-C 批次53 单元)的实际数据总结。
---
## 不可消除差异白名单
以下差异在所有页面中均存在,不计入差异率,审计报告中标注「不可消除,已忽略」:
| 差异类型 | 根因 | 估计贡献 |
|---|---|---|
| 字体渲染(字形、次像素、间距) | ChromiumNoto Sans SCvs 微信 WebView系统字体 | ~2% |
| 行内元素高度系统性偏小 ~7% | WebView 字体度量ascent/descent/leading差异 | ~1% |
| rpx 取偶数四舍五入 | 每个 px 值换算后取偶数,累积微小偏差 | ~0.5% |
| 抗锯齿差异 | 渲染引擎对边缘像素处理不同 | ~0.5% |
| 环比箭头SVG vs 文字 ↑↓) | 已确认可接受 | — |
| CSS 渐变文字 → 纯色文字 | MP 不支持 `-webkit-text-fill-color` + clip | — |
| Banner 纹理差异 | 7 层叠加渐变 + SVG 纹理无法完全复现 | ~3% |
背景噪音基线:~3.87%Benchmark v3 实测)。<5% 通过目标在基线之上仅有 ~1% 修正空间。
---
## 典型收敛曲线
### 单屏页面board-coach/customer 维度)
- 初始差异率4-11%
- 白名单扣除后:<3%
- 通常 0 轮修正直接通过
### 多屏长页面board-finance
- step-06% → 0 轮通过
- step-600~12009-11% → 含滚动偏移 + 字体渲染
- step-2400~360015-18% → 累积高度偏移,跳过
- step-4200+3% → 页面底部,双端 clamp 到 maxScroll
### 任务详情系列task-detail
- step-020-24% → mock 数据差异 ~15% + Banner 纹理 ~3% + 字体 ~3%
- 全部跳过(白名单覆盖)
---
## 跳过原因分类
| 原因 | 出现频率 | 可修复性 |
|------|---------|---------|
| 累积高度偏移rpx 取整导致 MP 页面更短) | 高 | 不可修复(系统性) |
| mock 数据差异 | 中 | 可修复(对齐数据) |
| Banner 纹理差异 | 中 | 部分可修复(简化纹理) |
| 话术设计变体H5 vs MP 实现差异) | 低 | 需重构 |
---
## 高度偏移规律
MP 页面总高度因 rpx 取整累积比 H5 略短。偏移量随页面长度增大:
| 页面 | H5 scrollHeight | MP scrollHeight | 偏移 |
|------|----------------|----------------|------|
| board-finance/default | 5600px | ~5030px | ~570px10% |
| board-finance/compare | 5579px | ~5030px | ~549px |
偏移导致中间屏step-2400~3600的内容窗口错位严重是跳过的主要原因。
页面首尾屏不受影响(首屏 scrollTop=0尾屏双端 clamp 到 maxScroll

View File

@@ -0,0 +1,148 @@
# 踩坑速查与迁移经验
> 经 User 确认后录入的实践经验。AI 发现新踩坑点时先提出建议User 确认后再录入。
> 按类别组织,每条记录包含:场景、问题、解决方案、影响页面。
---
## WXML 模板
### 禁止在 `{{}}` 中调用 JS 方法
- 场景WXML 模板中使用 `.toFixed()``.map()`
- 问题:小程序模板不支持 JS 方法调用
- 方案:用 WXS 模块 `utils/format.wxs`
- 影响:所有页面
### TabBar 页面跳转必须用 `wx.switchTab`
- 场景:从非 tabBar 页面跳转到 task-list、board-finance、my-profile
- 问题:`navigateTo` 会静默失败
- 方案tabBar 页面一律用 `wx.switchTab`
- 影响:所有涉及 tabBar 跳转的页面
---
## 样式与布局
### 一屏页面用 `height: 100vh` 而非 `min-height`
- 场景login、reviewing、no-permission 等单屏页面
- 问题:`min-height: 100vh` 在某些情况下不生效
- 方案:`height: 100vh` + `box-sizing: border-box`
- 影响login, reviewing, no-permission
### 状态栏适配禁止用 `env(safe-area-inset-top)`
- 场景:自定义 navBar 页面的顶部 padding
- 问题:小程序不支持 `env()` CSS 函数
- 方案JS `wx.getSystemInfoSync().statusBarHeight` 动态设置
- 影响:所有自定义 navBar 页面
### `padding-top + 100vh` 必须配合 `box-sizing: border-box`
- 场景:有顶部 padding 的全屏页面
- 问题:不加 box-sizing 会导致底部内容溢出
- 方案:始终添加 `box-sizing: border-box`
- 影响:所有全屏页面
---
## TDesign 组件
### `t-button icon` 只支持内置图标
- 场景:按钮中使用品牌图标(微信 logo 等)
- 问题:`icon` 属性只接受 TDesign 内置图标名
- 方案:用 `<view>` + `<image>` 组合替代
- 影响login 页面微信登录按钮
### TDesign 默认样式优先级高
- 场景:需要大幅覆盖 TDesign 组件样式
- 问题CSS 变量和外部样式类不够用时,`!important` 也可能不生效
- 方案:直接用原生 `<view>` 实现,绕开样式干扰
- 影响:视具体组件
---
## 资源引用
### 所有 H5 内联 SVG 必须导出为独立文件
- 场景H5 中直接写在 HTML 内的 `<svg>` 元素
- 问题:小程序不支持内联 SVG
- 方案:导出为 `.svg` 文件存放 `assets/icons/`,用 `<image>` 引用
- 影响:所有含内联 SVG 的页面
### 引用不存在的图片会 500 错误
- 场景WXML 中 `<image src>` 指向不存在的文件
- 问题:不是 404 而是 500 错误,难以排查
- 方案:用 CSS 渐变、emoji、`<t-icon>` 替代;或确保文件存在
- 影响:所有页面
---
## 截图与对比
### rpx 取整导致累积高度偏移
- 场景:多屏长页面的中间屏截图对比
- 问题MP 页面总高度比 H5 短 ~10%,中间屏内容窗口错位
- 方案:使用锚点对齐法(按元素位置对齐而非 scrollTop 数值)
- 影响board-finance, performance 等长页面
- 发现日期2026-03-10
### AI 浮动按钮动画造成随机差异
- 场景:截图时 ai-float-button 的 gradientShift 动画处于不同帧
- 问题:每次截图差异 ~1%
- 方案双端截图前隐藏H5 用 JSMP 用 `wx:if="{{false}}"`)
- 影响:所有含 ai-float-button 的页面
- 发现日期2026-03-10
### 禁止跨维度套用布局假设H5 原型是唯一视觉真相
- 场景board-customer "最专一"卡片的 `.loyal-table` 布局
- 问题:其他维度(最应召回等)的 `card-mid-row` / `card-assistant-row``ml-11`80rpx 对齐头像右侧),开发时"合理推断"表格也应该加 `margin-left: 80rpx`。但 H5 原型中表格容器没有 `ml-11`,是从卡片 padding 开始的left=16px。Playwright 实测才发现偏差
- 方案:每个维度的每个组件都必须回到 H5 HTML 逐一确认 Tailwind 类,禁止从其他维度"类推"。不确定时用 Playwright `getBoundingClientRect()` 实测 H5 元素位置
- 影响所有多维度页面board-customer 8 维度、board-coach 4 维度)
- 发现日期2026-03-12
### H5 与 MP mock 数据不同,像素级 diff 需结合结构比对
- 场景:用 PIL/numpy 或 image-compare 对比 H5 和 MP 截图
- 问题H5 原型使用硬编码 mock 数据(如孙先生/王先生/李女士MP 使用后端返回的测试数据(王先生/李女士/张先生),文字内容不同导致像素 diff 高达 11%18 个分段覆盖全页面,无法区分"数据差异"和"样式差异"
- 方案:像素级 diff 仅用于发现大面积结构偏移(窗口错位、整块缺失)。样式还原度验证的主要方法是 Tailwind 类→rpx 值逐项比对表,逐一确认每个 CSS 属性的换算结果
- 影响:所有页面的截图对比流程
- 发现日期2026-03-12
### Tailwind→WXSS 映射必须逐项建表验证
- 场景:将 H5 Tailwind 类翻译为小程序 WXSS
- 问题:凭记忆或经验直接写 WXSS 值容易出错——`font-medium` 是 500 不是 600`gap-1` 是 4px 不是 2px`space-y-2` 是 8px 不是 4px。错误在视觉上不明显但会累积
- 方案对每个组件建立完整的对照表H5 Tailwind 类 → 计算值 px → rpx 换算 → MP WXSS 值),逐行核对。参考速查表:`2→4, 4→8, 6→10, 8→14, 10→18, 12→22, 14→24, 16→28, 20→36, 24→42`
- 影响:所有页面的 WXSS 编写
- 发现日期2026-03-12
### 相邻元素间距叠加导致高度翻倍
- 场景:经营一览的 `.overview-grid-2` 底部 padding 32rpx + `.ai-insight-section` margin-top 32rpx间距翻倍为 64rpx
- 问题H5 中 grid 无 margin-bottom间距完全由下方元素的 `mt-4`(16px) 提供。MP 中两端都加了间距,导致块间距翻倍、整体高度偏高
- 方案:校对间距时必须确认「这段间距是谁提供的」——上方元素的 padding-bottom 还是下方元素的 margin-top。用 Playwright `getComputedStyle` 实测 H5 两个相邻元素的 margin/padding确认间距归属后只在一端设置
- 影响:所有包含多个子区块的板块(经营一览、预收资产等)
- 发现日期2026-03-12
### 用 Playwright 实测 H5 比手动解析 Tailwind 更可靠
- 场景H5 原型文件是单行 minified HTML手动读取 Tailwind 类名容易遗漏或误判
- 问题Tailwind 类名组合后的实际渲染值不直观(如 `p-4` 在嵌套容器中可能被覆盖,`space-y-2` 的实际 gap 取决于子元素数量),手动推算容易出错
- 方案:通过 `browser_run_code` 创建 375×812 viewport + DPR 1.5 的 context`getComputedStyle` + `getBoundingClientRect` 直接拿到渲染后的实际 px 值,再按换算规则转 rpx。这个方法应作为所有页面校对的标准流程
- 影响:所有页面的 WXSS 校对流程
- 发现日期2026-03-12
### Tailwind 字号类自带 line-heightWXSS 必须同步补齐
- 场景:校对经营一览的现金流水概览 4 个块H5 明显比 MP 高
- 问题Tailwind 的 `text-xs`/`text-sm`/`text-lg`/`text-xl` 等字号类捆绑了特定的 `line-height`(如 `text-lg` = `font-size: 18px; line-height: 28px`),但 WXSS 只写了 `font-size` 没写 `line-height`,小程序默认行高约 1.2,远小于 Tailwind 的行高,导致每行文字占据的垂直空间不足,块整体矮了一截
- 方案:每个 Tailwind 字号类转 WXSS 时,必须同时写 `font-size``line-height`。完整对照表87.5% 缩放):
| Tailwind | font-size (px) | line-height (px) | WXSS font-size | WXSS line-height |
|----------|---------------|------------------|----------------|-----------------|
| `text-xs` | 12 | 16 | 22rpx | 28rpx |
| `text-sm` | 14 | 20 | 24rpx | 34rpx |
| `text-base` | 16 | 24 | 28rpx | 42rpx |
| `text-lg` | 18 | 28 | 32rpx | 48rpx |
| `text-xl` | 20 | 28 | 36rpx | 48rpx |
| `text-2xl` | 24 | 32 | 42rpx | 56rpx |
- 影响:所有页面的所有文字元素
- 发现日期2026-03-12
---
> 新增经验请按以上格式录入,包含:场景、问题、解决方案、影响页面、发现日期(可选)。

View File

@@ -0,0 +1,74 @@
# 小程序前端页面开发与原型迁移指南
> 操作文档已迁移至 Kiro Power `miniprogram-h5-conversion`,通过关键词自动激活。
> 本目录仅保留实时更新的仓库驻留文件(进度、经验、页面结构等)。
---
## Power 使用方式
操作文档调度手册、子代理手册、转换规则、WXSS 规范、CSS 风险清单、Power 集成指南)已打包为 Kiro Power
- Power 名称:`miniprogram-h5-conversion`
- 激活方式:对话中提及"小程序页面"、"H5转换"、"wxml"、"wxss"、"rpx"等关键词时自动激活
- 手动激活:`kiroPowers action="activate" powerName="miniprogram-h5-conversion"`
- 查看 steering 列表:激活后在返回的 `steeringFiles` 中选择需要的文件
Power 包含 12 个 steering 文件,按职能分三层:
| 层 | 文件 | 说明 |
|----|------|------|
| 调度层 | `batch-auto.md` | 89 单元批量编排 |
| 调度层 | `user-guided.md` | 半自动模式 |
| 调度层 | `new-page.md` | 新页面开发 |
| 行动层 | `screenshot.md` | 截图代理手册 |
| 行动层 | `audit.md` | 审计代理手册 |
| 行动层 | `fix.md` | 修正代理手册 |
| 行动层 | `verify.md` | 验证代理手册 |
| 行动层 | `page-dev.md` | 页面开发代理手册 |
| 参考层 | `conversion-rules.md` | 核心转换规则 |
| 参考层 | `wxss-rules.md` | WXSS 标准值速查 |
| 参考层 | `css-risk.md` | CSS 风险特性清单 |
| 参考层 | `power-integration.md` | Power 调用规范 |
---
## 仓库驻留文件(实时更新,不在 Power 内)
```
docs/miniprogram-dev/
├── README.md ← 你在这里
├── 03-reference/
│ ├── page-structure-map.md # 各页面特殊结构速查(新增页面时更新)
│ └── benchmark-history.md # 基准测试历史记录
├── 04-audit/
│ ├── PROGRESS.md # 89 单元迁移进度(每次会话必读)
│ └── CHANGELOG.md # 文档体系版本变更
└── 05-lessons/
├── pitfalls.md # 踩坑速查(实战经验,持续更新)
└── convergence-patterns.md # 收敛模式 + 不可消除差异白名单
```
---
## Power 依赖清单
| Power | 用途 |
|-------|------|
| `wechat-miniprogram` | 小程序开发规范WXML/WXSS/API/TDesign |
| `pixel-audit` | 像素间距测量 + rpx 换算 |
| `playwright` | H5 端截图 |
| `weixin-devtools` | MP 端截图 + 页面操作 |
---
## 关联资源
| 资源 | 路径 |
|------|------|
| H5 原型页面 | `docs/h5_ui/pages/*.html` |
| 设计 Token | `docs/h5_ui/design-tokens.json` |
| 图标映射表 | `docs/h5_ui/icon-mapping.md` |
| 截图产物 | `docs/h5_ui/compare/<page>/` |
| 小程序源码 | `apps/miniprogram/miniprogram/pages/` |
| 辅助脚本 | `scripts/ops/measure_gaps.py``scripts/ops/anchor_compare.py` |

View File

@@ -0,0 +1,67 @@
---
name: "miniprogram-h5-conversion"
displayName: "H5 → 微信小程序转换规范"
description: "H5 原型转微信小程序页面的完整开发规范体系。覆盖截图、审计、修正、验证四阶段流水线89 单元批量编排,半自动/新页面开发模式。核心硬性规则在 always-on steering 中始终生效,本 Power 提供操作手册。"
keywords: ["微信小程序", "miniprogram", "wxml", "wxss", "H5", "转换", "迁移", "rpx", "截图", "审计", "修正", "验证", "视觉还原", "TDesign", "Tailwind"]
author: "NeoZQYY"
---
# H5 → 微信小程序转换规范
核心硬性规则已提取到 always-on steering `mp-h5-rules.md`(每次对话自动生效),详细开发参考值在 fileMatch steering `mp-h5-dev.md`(读取 wxml/wxss/h5_ui 文件时自动加载)。
本 Power 提供操作手册——截图、审计、修正、验证的具体执行流程。
## 使用方式
### 确认工作模式后加载对应 steering
| 我要做什么 | 加载的 steering |
|-----------|----------------|
| 批量执行 89 单元视觉还原 | `batch-auto.md` |
| 用户指定页面/区域做开发或修正 | `user-guided.md` |
| 从零开发新页面 | `user-guided.md`(场景二) |
| 执行截图+审计+修正+验证流程 | `action-manual.md` |
| 查 Power/MCP 调用方式 | `power-integration.md` |
### 实时数据(仓库内,不在 Power 中)
| 资源 | 路径 |
|------|------|
| 迁移进度 | `docs/miniprogram-dev/04-audit/PROGRESS.md` |
| 踩坑速查 | `docs/miniprogram-dev/05-lessons/pitfalls.md` |
| 收敛模式 | `docs/miniprogram-dev/05-lessons/convergence-patterns.md` |
| 页面结构速查 | `docs/miniprogram-dev/03-reference/page-structure-map.md` |
| 基准测试历史 | `docs/miniprogram-dev/03-reference/benchmark-history.md` |
### 关联资源
| 资源 | 路径 |
|------|------|
| H5 原型 | `docs/h5_ui/pages/*.html` |
| 设计 Token | `docs/h5_ui/design-tokens.json` |
| 图标映射 | `docs/h5_ui/icon-mapping.md` |
| 截图产物 | `docs/h5_ui/compare/<page>/` |
| MP 源码 | `apps/miniprogram/miniprogram/pages/` |
## Steering 文件清单
本 Power 包含 4 个 steering 文件:
### 调度层(做什么、什么顺序)
- `batch-auto.md` — 89 单元批量编排 + 4 代理流水线
- `user-guided.md` — 半自动/新页面开发模式
### 行动层(怎么做)
- `action-manual.md` — 截图+审计+修正+验证的完整执行手册(合并原 screenshot/audit/fix/verify 四个文件)
### 集成层
- `power-integration.md` — 4 个外部 Power 的调用方式 + MCP 连接规范
## 与 Steering 的分工
| 内容 | 位置 | 加载方式 |
|------|------|---------|
| 硬性规则标签映射、rpx、颜色、截图禁令 | `mp-h5-rules.md` | always-on |
| 开发参考值灰阶全表、圆角、阴影、CSS 风险) | `mp-h5-dev.md` | fileMatch 自动 |
| 操作手册(截图流程、审计方法、修正策略) | 本 Power steering | 按需 readSteering |

View File

@@ -0,0 +1,210 @@
# 截图+审计+修正+验证 执行手册
> 合并原 screenshot.md / audit.md / fix.md / verify.md 四个文件。
> 硬性规则rpx、标签映射、颜色已在 always-on steering `mp-h5-rules.md` 中,此处不重复。
---
## 一、截图
### H5 截图Playwright Power → `browser_run_code`
```javascript
async (page) => {
const browser = page.context().browser();
const ctx = await browser.newContext({
viewport: { width: 430, height: 752 },
deviceScaleFactor: 1.5
});
const p = await ctx.newPage();
await p.goto('file:///C:/NeoZQYY/docs/h5_ui/pages/<page>.html',
{ waitUntil: 'domcontentloaded', timeout: 30000 });
await p.waitForTimeout(3000); // Tailwind CDN JIT
await p.evaluate(() => {
const nav = document.getElementById('bottomNav');
if (nav) nav.style.display = 'none';
const safeArea = document.querySelector('.safe-area-top');
if (safeArea) safeArea.style.paddingTop = '0px';
const aiFab = document.querySelector('.ai-float-btn-container');
if (aiFab) aiFab.style.display = 'none';
const s = document.createElement('style');
s.textContent = '::-webkit-scrollbar{display:none!important}*{scrollbar-width:none!important}';
document.head.appendChild(s);
});
await p.evaluate((scrollTop) => window.scrollTo(0, scrollTop), <scrollTop>);
await p.waitForTimeout(300);
await p.screenshot({
path: 'C:/NeoZQYY/docs/h5_ui/compare/<page>/h5--step-<scrollTop>.png',
type: 'png', scale: 'device'
});
await ctx.close();
return { saved: true };
}
```
步数 ≥3 的页面可在同一 context 中循环截图。
### MP 截图weixin-devtools Power
```
1. relaunch → url: "/pages/<page>/<page>"(禁止 switch_tab/navigate_to
2. waitFor → delay: 2000
3. 前置WXML 中 dev-fab/ai-float-button 加 wx:if="{{false}}"
4. 滚动到目标 scrollTop
5. screenshot
6. 模式 B 页面裁剪头部 96pxPIL: img.crop((0, 96, 645, 1224))
```
### 滚动策略
标准:直接使用 H5 相同 scrollTop。
窗口错位判断:上下大块红绿(>200px)=整体偏移→锚点对齐法 | 散布多区域=样式差异→继续修正 | 底部全黑/白>100px=内容更短→锚点对齐法
锚点对齐法:确定锚点元素 → `wx.createSelectorQuery` 获取绝对 top → `MP_target = absoluteTop - sticky高度` → 滚动到该位置
### 多维度切换
board-coach/customer获取快照 → 点击筛选下拉 → 选择维度 → 等待刷新
board-financeH5 `toggleCompare()` / MP 点击环比开关
### 截图后行为(严格遵守)
- 直接进入审计流程,禁止用 PIL/pixelmatch/image-compare 检查尺寸或做像素 diff
- 截图参数已固定,尺寸必然正确
---
## 二、审计
### 三阶段流程
```
阶段 0结构拆解自顶向下
H5 源码 → 首层容器 → 递归到叶子 → 建立 H5↔MP 映射表
结构完整性校验(缺失/多余/顺序错误 → P0
阶段 1逐级测量自底向上
叶子:自身尺寸 + 样式属性
容器:内边距 + 子元素间距 + 总尺寸
页面级:全局内边距 + 区块间距
工具pixel-audit Power → readSteering("measure.md")
阶段 2偏差审计
逐属性对比 H5 rpx 理论值 vs MP 实测值 → 按偏差量分级
```
### 迁移前置准备step-0 必做)
1. H5 页面结构预扫描全局结构、CSS 风险特性、字体确认
2. MP 骨架检查根容器、navigationBar、safe-area 移除、背景色、浮动按钮隐藏
3. mock 数据一致性预核对(不一致 = P0
4. 多屏页面:测量 MP scrollHeight与编排值差异 >50px 则重算序列
### 审计报告格式
产出 `docs/h5_ui/compare/<page>/audit.md`
| 章节 | 内容 |
|------|------|
| A. 结构对照 | 区域完整性、顺序 |
| B. CSS 风险点 | 不支持的 CSS |
| C. 关键样式映射 | Tailwind→computed→WXSS 现值→是否一致 |
| D. 图标处理 | 每个图标迁移状态 |
| E. 偏差清单 | P0-P7 排序,含 H5 值→rpx→MP 现值→差异 |
| F. 复杂结构专项 | Banner/AI 图标/盖戳/inline SVG/渐变文字 |
关键原则:不能只看 diff 图猜测,必须回溯 H5 源码。Tailwind 类名是唯一可信样式来源。
---
## 三、修正
### 优先级定义
| 级别 | 类型 | 判定标准 | 阶段 |
|------|------|---------|------|
| P0 | 区域缺失/顺序错误 | 整块缺失 | 阶段一 |
| P1 | 背景色/渐变色不匹配 | 大面积色差 | 阶段一 |
| P2 | 字号明显偏差 | font-size 差 ≥4rpx | 阶段一 |
| P3 | 间距明显偏差 | padding/margin/gap 差 ≥4rpx | 阶段一 |
| P4 | 圆角偏差 | border-radius 差 ±2rpx | 阶段二 |
| P5 | 颜色微调 | 灰阶偏差、透明度差 0.1 | 阶段二 |
| P6 | 行高/字重微调 | line-height 缺失或偏差 | 阶段二 |
| P7 | 阴影微调 | box-shadow 偏差 | 阶段二 |
### 两阶段收敛
```
P0-P3 >10 → 结构级重写(每轮 3-8 处)→ 循环
P0-P3 >0 → 阶段一:结构级修正(每轮 2-5 处)→ 循环直到 P0-P3=0
P4-P7 >0 → 阶段二:像素级精调(每轮 1-3 处)→ 循环直到可接受
全部 ≤2rpx → ✅ 通过
```
### 回滚规则(强制)
每轮修正后重新截图对比,差异率上升(哪怕 0.01%)→ 立即回滚 → 逐项排查 → 每次只改一处
### 最大重试限制
差异率下降 >0.5% → 重试计数归零 | 下降 ≤0.5% → 计数+1 | 计数=5 且仍 ≥5% → 跳过(记录到 report.md
### 首屏 >20% 触发结构重写
| 页面类型 | 重写范围 | 禁止修改 |
|---|---|---|
| 单屏 | 当前维度完整 WXML/WXSS | 其他维度 |
| 多屏(step-0) | 页面顶部结构 | step-600 以下 |
| 多屏(step-N>0) | 当前视口内节点 | 其他屏和全局样式 |
| 变体 | 与主页面差异部分 | 共享布局 |
### 修改范围约束
- 只改当前屏可见元素
- 偏差根因在前序屏 → 标注"需回退到 step-X"返回主代理
- 偏差根因是全局样式 → 标注"全局样式变更"返回主代理
- 修改后 `getDiagnostics` 确认零 TS 错误,有错误立即回退
### 收敛停滞
连续 3 轮差异率未下降 >0.5%:分析是否为不可消除结构性差异 → 是则标注可接受 → 否则缩小粒度
---
## 四、验证
### 修正后验证
```
1. 重新截图 → 重新审计
2. P0-P3=0 → ✅ 通过
3. P0-P3 减少 → 继续修正
4. P0-P3 未减少或增加 → 回滚 + 重试计数+1
5. 重试=5 → 跳过
```
### 全量回归校验(多屏页面所有屏通过后强制执行)
```
1. 逐屏截取双端截图
2. 每屏结构化审计
3. 禁止修改任何源码
4. P0-P3 增加 → 标记"回归"
5. 返回主代理决定处理方式
```
### Sticky 元素专项
step-0 完成后先检测 sticky 差异 → 优先修复(一次修复所有屏受益)→ 再继续后续步骤
### 异常处理
| 故障 | 处理 | 升级 |
|------|------|------|
| MCP 断开 | reconnect_devtools3次失败→mcp_recompile+5s | 重编译后仍失败→暂停 |
| Playwright 不可用 | 用已有截图继续审计 | 无截图→报告主代理 |
| 截图异常(白屏) | 暂停检查页面状态 | 禁止使用异常截图 |
| MCP 超时 | 重试最多 2 次 | 3次均超时→跳过 |

View File

@@ -0,0 +1,122 @@
# 批量自动模式89 单元视觉还原编排
> 每次会话开始:读 `docs/miniprogram-dev/04-audit/PROGRESS.md` 确认进度。
> 执行手册:`readSteering("action-manual.md")`
> Power 调用:`readSteering("power-integration.md")`
---
## 执行模型4 种专职子代理
```
主代理(调度)
→ 截图代理 → 审计代理 → 修正代理 ⇆ 验证代理 → 主代理汇总
```
全程严格串行——同一时刻只有一个单元在执行。禁止预先批量截图。
---
## 主代理调度职责
1. 读 PROGRESS.md 确认进度
2. 检查 MCP 就绪
3. 新页面:隐藏 dev-fab/ai-float-button
4. 逐屏下发:截图→审计→修正/验证循环
5. 通过→更新 PROGRESS.md→下一屏
6. 需回退→回退到指定 step
7. 跳过→备注原因,继续
8. 页面所有屏通过→全量回归校验
---
## 单元内流程
```
Step 1截图代理 → H5/MP 截图
Step 2审计代理 → audit.md + 偏差清单
Step 3修正代理 → 修正源码
├─ P0-P3 >0 且未触发跳过 → 验证代理 → 循环
├─ P0-P3=0 → Step 4
├─ 连续 5 轮无改善 → 跳过
└─ P0-P3 >10 且连续 3 轮无法突破 → 结构重写
Step 4主代理汇总 → 更新 PROGRESS.md
```
---
## 前置任务
P0. TS 零诊断基线17 页面 `.ts` 全部 `getDiagnostics` 零诊断
P1. 跨页面共性偏差批量修复
---
## 批次编排
### A 批次看板32 单元)
board-finance20 单元default 10 屏(step-0~4848) + compare 10 屏(step-0~4827)
board-coach4 单元perf/salary/sv/task 各 step-0
board-customer8 单元recall/potential/balance/recharge/recent/spend60/freq60/loyal 各 step-0
### B 批次核心4 单元)
task-list3 屏(#33-35) | my-profile1 屏(#36)
### C 批次任务详情17 单元)
task-detail5 屏(#37-41)
task-detail-callback4 屏(#42-45teal 主题色)
task-detail-priority4 屏(#46-49orange 主题色)
task-detail-relationship4 屏(#50-53pink 主题色)
### D 批次详情12 单元)
coach-detail5 屏(#54-58) | customer-detail5 屏(#59-63) | customer-service-records2 屏(#64-65)
### E 批次绩效18 单元)
performance13 屏(#66-78) | performance-records5 屏(#79-83)
### F 批次对话3 单元)
chat2 屏(#84-85) | chat-history1 屏(#86)
### G 批次其他3 单元)
notes3 屏(#87-89)
---
## 子代理下发模板
### 标准单元
```
执行视觉还原对照单元:<单元 ID>
源码H5 docs/h5_ui/pages/<page>.html | MP apps/miniprogram/.../pages/<page>/
输出docs/h5_ui/compare/<page>/
当前步骤step-<N>(第 M / 共 T 步)
前序屏状态:<首屏"无前序" / 后续"step-X 已通过">
执行:截图→审计→修正→验证(见 action-manual.md
约束:只改当前屏 | 偏差在前序屏→标注"需回退" | 全局样式→标注"全局变更"
完成后:更新 PROGRESS.md
```
### 变体单元C 批次)
```
执行视觉还原(变体简化):<单元 ID>
与 task-detail 共享布局,仅 Banner 主题色不同。
主题色callback=teal / priority=orange / relationship=pink
step-0 重点验证 Banner 渐变色和按钮配色,其余快速对比。
```
### 回归校验
```
执行全量回归校验:<page>
逐屏截取+审计禁止修改源码。P0-P3 增加→标记"回归"。
```

View File

@@ -0,0 +1,63 @@
# Power 集成指南
> 4 个外部 Power 的调用方式 + MCP 连接规范。
---
## Power 清单与激活
| Power | 用途 | 激活 |
|-------|------|------|
| `wechat-miniprogram` | 小程序开发规范 | `kiroPowers activate wechat-miniprogram``view-layer.md` / `tdesign.md` / `builtin-components.md` |
| `pixel-audit` | 像素间距测量 | `kiroPowers activate pixel-audit``readSteering("measure.md")` |
| `power-playwright` | H5 截图 | `kiroPowers activate power-playwright``browser_run_code` |
| `weixin-devtools` | MP 截图+操作 | `kiroPowers activate weixin-devtools``mcp_weixin_devtools_mcp_*` |
---
## 各阶段调用
### 页面开发
wechat-miniprogram → `view-layer.md` + `tdesign.md` + `builtin-components.md`
### 截图
H5power-playwright → `browser_run_code`DPR=1.5 context详见 action-manual.md §一)
MPweixin-devtools → `relaunch``waitFor``screenshot`+ `evaluate_script` / `get_page_snapshot` / `click`
### 审计
pixel-audit → `readSteering("measure.md")`rpx 换算精确公式 + 五种间距类型 + Tailwind→WXSS 映射表)
测量工具:`uv run python scripts/ops/measure_gaps.py`
锚点对比:`uv run python scripts/ops/anchor_compare.py`
> `image-compare` MCP 已移除2026-03-12禁止调用。
---
## MCP 连接规范
### weixin-devtools
- 只能用 wsEndpoint`ws://127.0.0.1:9420`
- 禁止 auto/launch/connect/discover 策略
- 断开:`reconnect_devtools` → 3 次失败 → `mcp_recompile` + 5s
- 单次调用最大等待 10 分钟
### Playwright
- 必须 `browser_run_code` 创建 DPR=1.5 context
- 禁止 `browser_take_screenshot`DPR=1
- 禁止 `browser_navigate` + `browser_evaluate` 分步流程
---
## 会话开始 MCP 就绪检查
```
[ ] weixin-devtools → get_connection_status → 已连接
[ ] Playwright → browser_run_code 可用
[ ] 微信开发者工具已开启
[ ] pixel-audit → 已激活
```

View File

@@ -0,0 +1,122 @@
# 半自动模式 & 新页面开发
> 合并原 user-guided.md / new-page.md / page-dev.md。
> 硬性规则和开发参考值已在 steering `mp-h5-rules.md` + `mp-h5-dev.md` 中。
---
## 一、半自动模式
用户直接指定页面/区域/任务AI 按需执行。
### 意图解析
解析用户输入,确定:目标页面 → 目标范围(整页/step/区域/组件)→ 任务类型(开发/修正/审计/验证)
意图不明确时追问一轮即可(不进入完整审问模式)。
### 按需加载
| 任务类型 | 加载 |
|---------|------|
| 视觉还原(截图+审计+修正) | `readSteering("action-manual.md")` |
| 仅审计 | `readSteering("action-manual.md")` §二 |
| 仅修正(已有审计报告) | `readSteering("action-manual.md")` §三+§四 |
| 仅截图 | `readSteering("action-manual.md")` §一 |
| 新页面开发 | 本文件 §二/§三 |
始终加载:`readSteering("power-integration.md")`
### 与批量模式的区别
- 不需要按批次顺序
- 可跳过不相关步骤
- 修正范围由用户指定
- 完成后提示是否更新 PROGRESS.md
### 经验沉淀
发现新踩坑点 → 向用户提出建议 → 确认后录入 `docs/miniprogram-dev/05-lessons/pitfalls.md`
---
## 二、新页面开发(有 H5 原型)
### 流程
```
1. 前置加载
H5 源码 + design-tokens.json + icon-mapping.md + 交互说明
激活 wechat-miniprogram Powerview-layer.md + tdesign.md
2. H5 结构分析
全局结构识别safe-area/bottomNav/ai-float 等)
CSS 风险特性扫描
组件清单TDesign 可覆盖 vs 自定义)
3. MP 页面骨架搭建(见 §四)
4. 视觉比对审计(推荐)
→ readSteering("action-manual.md")
```
### 必须遵守
- mock 数据必须与 H5 原型完全一致
- TDesign 优先原则
- 所有规则见 always-on steering `mp-h5-rules.md`
---
## 三、新页面开发(无 H5 原型)
从 spec/PRD 直接开发,无视觉比对。
```
1. 需求确认(用途、数据来源、导航关系、是否 tabBar
2. 设计参考design-tokens.json + 现有页面风格 + TDesign 选型)
3. 页面开发(见 §四)
4. 功能验证(微信开发者工具预览 + 交互流程 + 空/加载/错误态)
```
---
## 四、页面骨架搭建
### 文件结构
```
apps/miniprogram/miniprogram/pages/<page>/
├── <page>.wxml # 结构
├── <page>.wxss # 样式
├── <page>.ts # 逻辑 + mock
└── <page>.json # 配置navigationBarTitleText + usingComponents
```
### .json 配置模板
```json
{
"navigationBarTitleText": "页面标题",
"usingComponents": {
"t-button": "tdesign-miniprogram/button/button",
"t-icon": "tdesign-miniprogram/icon/icon"
}
}
```
### TS mock 数据
```typescript
Page({
data: { /* mock 数据,结构贴近真实 API */ },
onLoad() {
// TODO: 替换为真实 API 调用
this.setData({ /* ... */ });
}
});
```
### 完成检查
见 fileMatch steering `mp-h5-dev.md` 中的「新页面 Checklist」。

130
_DEL/mp-h5-dev.md Normal file
View File

@@ -0,0 +1,130 @@
---
inclusion: fileMatch
fileMatchPattern: "**/*.wxml,**/*.wxss,**/h5_ui/**,**/miniprogram/miniprogram/pages/**,**/miniprogram-dev/**"
name: mp-h5-dev
description: 小程序页面开发详细规范。读取 wxml/wxss/h5_ui/miniprogram 页面文件时自动加载。
---
# 小程序页面开发详细规范
硬性规则见 always-on steering `mp-h5-rules.md`。本文件补充开发流程和详细参考值。
## 完整颜色灰阶13 级)
| Token | Hex | 用途 |
|-------|-----|------|
| gray-1 | #f3f3f3 | 页面背景 |
| gray-2 | #eeeeee | 分隔线 |
| gray-3 | #e7e7e7 | 边框 |
| gray-4 | #dcdcdc | 禁用态 |
| gray-5 | #c5c5c5 | — |
| gray-6 | #a6a6a6 | 辅助文字 |
| gray-7 | #8b8b8b | TabBar 默认色 |
| gray-8 | #777777 | — |
| gray-9 | #5e5e5e | 次要文字 |
| gray-10 | #4b4b4b | — |
| gray-11 | #393939 | — |
| gray-12 | #2c2c2c | — |
| gray-13 | #242424 | 主文字 |
## border-radius 标准值
sm 8rpx(rounded) | md 16rpx(rounded-lg) | lg 24rpx(rounded-xl) | xl 32rpx(rounded-2xl) | 3xl 48rpx(rounded-3xl)
## 阴影标准值
lg: `0 8rpx 32rpx rgba(0,0,0,0.06)` | xl: `0 16rpx 48rpx rgba(0,0,0,0.08)`
## 固定高度元素(切断 rpx 取整累积漂移)
Tab 栏 84rpx | 筛选栏 120rpx | 列表项 96rpx(标准)/112rpx(含副标题) | 按钮 80rpx | 底部操作区 112rpx
## 页面模式(截图裁剪)
模式 A系统 navBar无需裁剪board-finance, board-coach, board-customer, task-list, my-profile
模式 B自定义 navBar裁剪头部 96pxtask-detail 系列, coach-detail, customer-detail, performance, notes, chat, chat-history, customer-service-records, performance-records
## Sticky 高度
board-finance 116px | board-coach 110px | board-customer 110px | 其他 0
## 全局结构处理
| H5 元素 | MP 处理 |
|---------|---------|
| `.safe-area-top` | 去除 padding-topMP 由 navigationBar 处理 |
| `#bottomNav` | 不迁移MP 用原生 tabBar |
| `.ai-float-btn-container` | `wx:if="{{false}}"` |
| `<dev-fab />` | `wx:if="{{false}}"` |
| `::before/::after` 视觉元素 | 额外 `<view>` 绝对定位模拟 |
| Tab 指示线 `::after` | 额外 `<view>` 模拟 |
| 气泡尖角 `::after` | 绝对定位 `<view>` + `transform: rotate(45deg)` |
## 复杂视觉元素分层策略
| 复杂度 | 示例 | 方案 |
|--------|------|------|
| L1 简单 | 纯色标签、单色图标 | WXSS 原生 |
| L2 中等 | 渐变标签、多层 gradient | WXSS 多层渐变 |
| L3 复杂 | 伪元素纹理、SVG data URI | 导出为图片/SVG 文件 |
| L4 不可迁移 | `background-clip:text``conic-gradient` | 降级为近似纯色 |
## Banner 迁移模板
```xml
<view class="banner-bg">
<view class="banner-texture"></view> <!-- 替代 ::before -->
<view class="banner-glow"></view> <!-- 替代 ::after -->
<view class="banner-content"><!-- 内容 --></view>
</view>
```
## SVG 处理决策树
1. TDesign 有等价图标 → `<t-icon name="xxx">`
2. 品牌/自定义图标 → 导出到 `assets/icons/<name>.svg``<image>` 引用
3. CSS 背景 `data:image/svg+xml` → 导出独立文件
4. 简单几何图形 → WXSS border/transform 替代
## 不可消除差异白名单
字体渲染差异(Chromium vs WebView) | 行内元素高度偏小~7% | 抗锯齿差异 | 环比箭头(SVG vs 文字↑↓) | CSS 渐变文字→纯色
## CSS 风险特性速查
| 特性 | 出现页面 | 替代方案 |
|------|---------|---------|
| `backdrop-filter: blur()` | board-finance | `rgba(255,255,255,0.95)` |
| `-webkit-background-clip: text` | board-customer | 纯色文字 |
| `data:image/svg+xml` | reviewing, no-permission | 导出 PNG/SVG |
| `position: fixed` | board-finance/coach/customer, task-list | `position: sticky` 或页面级 fixed |
| `@font-face` 远程 | 所有页面 | 系统字体(推荐) |
## 新页面 Checklist
- [ ] 无 HTML 标签残留div/span/p/a/img
- [ ] 内联 SVG 已提取为文件
- [ ] 样式全部 WXSSrpx 单位)
- [ ] 不支持的 CSS 已替代
- [ ] 事件 `bindtap`,传参 `data-*`
- [ ] TDesign 组件在 .json 注册
- [ ] 安全区适配JS statusBarHeight
- [ ] `getDiagnostics` 零错误
- [ ] `wx:key` 已添加
- [ ] 布尔属性用 `{{}}` 包裹
- [ ] mock 数据与 H5 一致
## 关联资源路径
| 资源 | 路径 |
|------|------|
| H5 原型 | `docs/h5_ui/pages/*.html` |
| 设计 Token | `docs/h5_ui/design-tokens.json` |
| 图标映射 | `docs/h5_ui/icon-mapping.md` |
| 交互说明 | `docs/h5_ui/interactions/<page>.md` |
| 截图产物 | `docs/h5_ui/compare/<page>/` |
| MP 源码 | `apps/miniprogram/miniprogram/pages/` |
| 迁移进度 | `docs/miniprogram-dev/04-audit/PROGRESS.md` |
| 踩坑速查 | `docs/miniprogram-dev/05-lessons/pitfalls.md` |
| 收敛模式 | `docs/miniprogram-dev/05-lessons/convergence-patterns.md` |
| 页面结构速查 | `docs/miniprogram-dev/03-reference/page-structure-map.md` |

57
_DEL/mp-h5-rules.md Normal file
View File

@@ -0,0 +1,57 @@
---
name: mp-h5-rules
description: H5→微信小程序转换硬性规则核心约束始终生效
---
# H5 → 微信小程序转换硬性规则
本规则在所有涉及小程序前端开发的 session 中始终生效。详细操作手册见 Power `miniprogram-h5-conversion`
## 视觉真相
- `docs/h5_ui/pages/*.html` 是唯一视觉参考,任何偏离需用户确认
- 颜色/间距/字号标准值见 `docs/h5_ui/design-tokens.json`,禁止凭记忆硬编码
## 标签映射(严禁在 WXML 中使用 HTML 标签)
`<div>``<view>` | `<span>/<p>``<text>` | `<img>``<image mode="">` | `<svg>`内联→`<image src="xx.svg">` | `<button>``<t-button>` | `<input>``<t-input>`
## rpx 换算
- 精确公式:`rpx = px × 2 × 0.875`(取偶数)
- 圆角例外:`border-radius = px × 2`
- Tailwind 字号必须同步 line-heighttext-xs→22/28rpx | text-sm→24/34rpx | text-base→28/42rpx | text-lg→32/48rpx | text-xl→36/48rpx | text-2xl→42/56rpx
## 颜色灰阶(禁止使用 #333/#666/#999 等非标准值)
gray-1 #f3f3f3(页面背景) | gray-6 #a6a6a6(辅助文字) | gray-9 #5e5e5e(次要文字) | gray-13 #242424(主文字) | primary #0052d9 | success #00a870 | warning #ed7b2f | error #e34d59
## TDesign 优先
凡 TDesign 组件能覆盖的 UI 元素必须使用 TDesign。`t-button icon="xxx"` 只支持内置图标,品牌图标用 `<view>+<image>` 组合。
## 不支持的 CSS
`backdrop-filter``rgba()` | `::before/::after`→额外`<view>`绝对定位 | `background-clip:text`→纯色 | `data:image/svg+xml`→导出独立文件 | `*`通配符→逐个设置
## 截图规则
- H5必须 `browser_run_code` 创建 DPR=1.5 context禁止 `browser_take_screenshot`/分步流程)
- MP导航一律 `relaunch`(禁止 `switch_tab`/`navigate_to`);模式 B自定义 navBar裁剪头部 96px
- 双端对比基准645×1128
- 截图后禁止用 PIL/pixelmatch/image-compare 做像素 diff改用结构化拆解+逐级测量+偏差审计
- `image-compare` MCP 已移除2026-03-12禁止调用
## 偏差判定
≤2rpx 通过 | >2rpx且≤4rpx 警告(P4-P7) | >4rpx 不通过(P0-P3)
## 事件与数据
- 传参用 `data-*`,取值 `e.detail.value`,阻止冒泡用 `catchtap`
- `this.setData()` 驱动视图,布尔属性用 `{{}}`,列表加 `wx:key`
- mock 数据必须与 H5 硬编码值完全一致,标记 `// TODO: 替换为真实 API 调用`
## 安全区
JS `wx.getSystemInfoSync().statusBarHeight` 动态 padding-top禁止 `env(safe-area-inset-top)`

148
_DEL/pitfalls.md Normal file
View File

@@ -0,0 +1,148 @@
# 踩坑速查与迁移经验
> 经 User 确认后录入的实践经验。AI 发现新踩坑点时先提出建议User 确认后再录入。
> 按类别组织,每条记录包含:场景、问题、解决方案、影响页面。
---
## WXML 模板
### 禁止在 `{{}}` 中调用 JS 方法
- 场景WXML 模板中使用 `.toFixed()``.map()`
- 问题:小程序模板不支持 JS 方法调用
- 方案:用 WXS 模块 `utils/format.wxs`
- 影响:所有页面
### TabBar 页面跳转必须用 `wx.switchTab`
- 场景:从非 tabBar 页面跳转到 task-list、board-finance、my-profile
- 问题:`navigateTo` 会静默失败
- 方案tabBar 页面一律用 `wx.switchTab`
- 影响:所有涉及 tabBar 跳转的页面
---
## 样式与布局
### 一屏页面用 `height: 100vh` 而非 `min-height`
- 场景login、reviewing、no-permission 等单屏页面
- 问题:`min-height: 100vh` 在某些情况下不生效
- 方案:`height: 100vh` + `box-sizing: border-box`
- 影响login, reviewing, no-permission
### 状态栏适配禁止用 `env(safe-area-inset-top)`
- 场景:自定义 navBar 页面的顶部 padding
- 问题:小程序不支持 `env()` CSS 函数
- 方案JS `wx.getSystemInfoSync().statusBarHeight` 动态设置
- 影响:所有自定义 navBar 页面
### `padding-top + 100vh` 必须配合 `box-sizing: border-box`
- 场景:有顶部 padding 的全屏页面
- 问题:不加 box-sizing 会导致底部内容溢出
- 方案:始终添加 `box-sizing: border-box`
- 影响:所有全屏页面
---
## TDesign 组件
### `t-button icon` 只支持内置图标
- 场景:按钮中使用品牌图标(微信 logo 等)
- 问题:`icon` 属性只接受 TDesign 内置图标名
- 方案:用 `<view>` + `<image>` 组合替代
- 影响login 页面微信登录按钮
### TDesign 默认样式优先级高
- 场景:需要大幅覆盖 TDesign 组件样式
- 问题CSS 变量和外部样式类不够用时,`!important` 也可能不生效
- 方案:直接用原生 `<view>` 实现,绕开样式干扰
- 影响:视具体组件
---
## 资源引用
### 所有 H5 内联 SVG 必须导出为独立文件
- 场景H5 中直接写在 HTML 内的 `<svg>` 元素
- 问题:小程序不支持内联 SVG
- 方案:导出为 `.svg` 文件存放 `assets/icons/`,用 `<image>` 引用
- 影响:所有含内联 SVG 的页面
### 引用不存在的图片会 500 错误
- 场景WXML 中 `<image src>` 指向不存在的文件
- 问题:不是 404 而是 500 错误,难以排查
- 方案:用 CSS 渐变、emoji、`<t-icon>` 替代;或确保文件存在
- 影响:所有页面
---
## 截图与对比
### rpx 取整导致累积高度偏移
- 场景:多屏长页面的中间屏截图对比
- 问题MP 页面总高度比 H5 短 ~10%,中间屏内容窗口错位
- 方案:使用锚点对齐法(按元素位置对齐而非 scrollTop 数值)
- 影响board-finance, performance 等长页面
- 发现日期2026-03-10
### AI 浮动按钮动画造成随机差异
- 场景:截图时 ai-float-button 的 gradientShift 动画处于不同帧
- 问题:每次截图差异 ~1%
- 方案双端截图前隐藏H5 用 JSMP 用 `wx:if="{{false}}"`)
- 影响:所有含 ai-float-button 的页面
- 发现日期2026-03-10
### 禁止跨维度套用布局假设H5 原型是唯一视觉真相
- 场景board-customer "最专一"卡片的 `.loyal-table` 布局
- 问题:其他维度(最应召回等)的 `card-mid-row` / `card-assistant-row``ml-11`80rpx 对齐头像右侧),开发时"合理推断"表格也应该加 `margin-left: 80rpx`。但 H5 原型中表格容器没有 `ml-11`,是从卡片 padding 开始的left=16px。Playwright 实测才发现偏差
- 方案:每个维度的每个组件都必须回到 H5 HTML 逐一确认 Tailwind 类,禁止从其他维度"类推"。不确定时用 Playwright `getBoundingClientRect()` 实测 H5 元素位置
- 影响所有多维度页面board-customer 8 维度、board-coach 4 维度)
- 发现日期2026-03-12
### H5 与 MP mock 数据不同,像素级 diff 需结合结构比对
- 场景:用 PIL/numpy 或 image-compare 对比 H5 和 MP 截图
- 问题H5 原型使用硬编码 mock 数据(如孙先生/王先生/李女士MP 使用后端返回的测试数据(王先生/李女士/张先生),文字内容不同导致像素 diff 高达 11%18 个分段覆盖全页面,无法区分"数据差异"和"样式差异"
- 方案:像素级 diff 仅用于发现大面积结构偏移(窗口错位、整块缺失)。样式还原度验证的主要方法是 Tailwind 类→rpx 值逐项比对表,逐一确认每个 CSS 属性的换算结果
- 影响:所有页面的截图对比流程
- 发现日期2026-03-12
### Tailwind→WXSS 映射必须逐项建表验证
- 场景:将 H5 Tailwind 类翻译为小程序 WXSS
- 问题:凭记忆或经验直接写 WXSS 值容易出错——`font-medium` 是 500 不是 600`gap-1` 是 4px 不是 2px`space-y-2` 是 8px 不是 4px。错误在视觉上不明显但会累积
- 方案对每个组件建立完整的对照表H5 Tailwind 类 → 计算值 px → rpx 换算 → MP WXSS 值),逐行核对。参考速查表:`2→4, 4→8, 6→10, 8→14, 10→18, 12→22, 14→24, 16→28, 20→36, 24→42`
- 影响:所有页面的 WXSS 编写
- 发现日期2026-03-12
### 相邻元素间距叠加导致高度翻倍
- 场景:经营一览的 `.overview-grid-2` 底部 padding 32rpx + `.ai-insight-section` margin-top 32rpx间距翻倍为 64rpx
- 问题H5 中 grid 无 margin-bottom间距完全由下方元素的 `mt-4`(16px) 提供。MP 中两端都加了间距,导致块间距翻倍、整体高度偏高
- 方案:校对间距时必须确认「这段间距是谁提供的」——上方元素的 padding-bottom 还是下方元素的 margin-top。用 Playwright `getComputedStyle` 实测 H5 两个相邻元素的 margin/padding确认间距归属后只在一端设置
- 影响:所有包含多个子区块的板块(经营一览、预收资产等)
- 发现日期2026-03-12
### 用 Playwright 实测 H5 比手动解析 Tailwind 更可靠
- 场景H5 原型文件是单行 minified HTML手动读取 Tailwind 类名容易遗漏或误判
- 问题Tailwind 类名组合后的实际渲染值不直观(如 `p-4` 在嵌套容器中可能被覆盖,`space-y-2` 的实际 gap 取决于子元素数量),手动推算容易出错
- 方案:通过 `browser_run_code` 创建 375×812 viewport + DPR 1.5 的 context`getComputedStyle` + `getBoundingClientRect` 直接拿到渲染后的实际 px 值,再按换算规则转 rpx。这个方法应作为所有页面校对的标准流程
- 影响:所有页面的 WXSS 校对流程
- 发现日期2026-03-12
### Tailwind 字号类自带 line-heightWXSS 必须同步补齐
- 场景:校对经营一览的现金流水概览 4 个块H5 明显比 MP 高
- 问题Tailwind 的 `text-xs`/`text-sm`/`text-lg`/`text-xl` 等字号类捆绑了特定的 `line-height`(如 `text-lg` = `font-size: 18px; line-height: 28px`),但 WXSS 只写了 `font-size` 没写 `line-height`,小程序默认行高约 1.2,远小于 Tailwind 的行高,导致每行文字占据的垂直空间不足,块整体矮了一截
- 方案:每个 Tailwind 字号类转 WXSS 时,必须同时写 `font-size``line-height`。完整对照表87.5% 缩放):
| Tailwind | font-size (px) | line-height (px) | WXSS font-size | WXSS line-height |
|----------|---------------|------------------|----------------|-----------------|
| `text-xs` | 12 | 16 | 22rpx | 28rpx |
| `text-sm` | 14 | 20 | 24rpx | 34rpx |
| `text-base` | 16 | 24 | 28rpx | 42rpx |
| `text-lg` | 18 | 28 | 32rpx | 48rpx |
| `text-xl` | 20 | 28 | 36rpx | 48rpx |
| `text-2xl` | 24 | 32 | 42rpx | 56rpx |
- 影响:所有页面的所有文字元素
- 发现日期2026-03-12
---
> 新增经验请按以上格式录入,包含:场景、问题、解决方案、影响页面、发现日期(可选)。

43
_DEL/pixel-audit/POWER.md Normal file
View File

@@ -0,0 +1,43 @@
---
name: "pixel-audit"
displayName: "H5→MP 像素测量与 rpx 换算"
description: "H5 页面结构化拆解、逐级元素测量、px→rpx 换算、偏差审计方法论。用于迁移前 H5 审计和迁移后双端对比。"
keywords: ["rpx", "间距测量", "measure", "像素", "pixel", "px", "换算", "Tailwind", "wxss", "间距", "gap", "spacing", "审计", "拆解", "映射表", "偏差"]
author: "NeoZQYY"
---
# H5→MP 像素测量与 rpx 换算
精确测量 H5 页面元素尺寸与间距,换算为小程序 rpx 值。
提供结构化拆解→逐级测量→偏差审计的完整方法论,指导 H5→MP 迁移和双端对比。
核心工具:
- `scripts/ops/measure_gaps.py`Playwright headless 测量)
- Playwright MCP `browser_evaluate`(等效 JS 测量)
## rpx 速查
```
精确公式rpx = ceil(px × 1.7442 / 2) × 2
简化心算rpx ≈ px × 2 × 0.875 取偶6px/14px 处有 2rpx 偏差)
圆角border-radius = px × 2
2→4 4→8 6→12 8→14 10→18 12→22
14→26 16→28 20→36 24→42 28→50 32→56
```
## 偏差判定标准
| rpx 偏差 | 级别 | 处理 |
|----------|------|------|
| ≤ 2rpx | 通过 | 无需修正 |
| > 2rpx 且 ≤ 4rpx | 警告P4-P7 | 像素级精调 |
| > 4rpx | 不通过P0-P3 | 必须修正 |
## Steering
| 文件 | 内容 |
|------|------|
| `measure.md` | 结构化拆解方法论、H5 审计映射表、逐级测量流程、偏差审计规则、rpx 换算公式与速查表、measure_gaps.py 用法、MP 反向验证 |
加载:激活 Power → `readSteering("measure.md")`

View File

@@ -0,0 +1,345 @@
# 间距测量与 rpx 换算
## 核心换算公式
```
小程序 viewport 宽 = 750rpx = 430px
RPX_FACTOR = 750 / 430 = 1.7442
精确公式rpx = ceil(H5_px × 1.7442 / 2) × 2
简化心算rpx ≈ H5_px × 2 × 0.875 取偶6px、14px 处有 2rpx 偏差,以精确公式为准)
圆角例外border-radius = px × 2不乘系数数值更整洁
```
---
## 结构化审计方法论(三阶段)
本方法论适用于两个场景:
- **迁移前**:对 H5 页面做详细审计,产出映射表,指导 MP 开发
- **迁移后**:基于映射表做双端对比,逐元素验证偏差
### 阶段 0结构拆解自顶向下
目标:将页面拆解为树状元素清单,建立 H5↔MP 映射表。
#### 0.1 拆解流程
1. 读取 H5 源码(`docs/h5_ui/pages/<page>.html`),识别页面首层容器
2. 对每个首层容器,递归拆解子元素,直到到达叶子节点
3. 叶子节点定义:**单一样式属性可描述**的最小视觉单元
- 文字 spanfont-size + color + weight 即可描述)
- 图标 image / SVG
- 纯色/渐变 viewbackground 即可描述)
- 分隔线border 即可描述)
4. 为每个节点记录层级深度、CSS 选择器、Tailwind 类名、角色描述
#### 0.2 H5 审计映射表(迁移前必做)
拆解完成后,产出结构化映射表。此表是后续所有工作的基准:
```markdown
## <page> H5 审计映射表
| # | 层级 | 元素描述 | H5 选择器 | Tailwind 类名 | 理论 rpx 值 | MP 选择器 | 备注 |
|---|------|---------|-----------|--------------|------------|-----------|------|
| 1 | L0 | 页面根容器 | .page | px-4 bg-gray-1 | padding: 0 14rpx | .page | — |
| 2 | L1 | Banner 区域 | .banner-bg | h-40 rounded-2xl | height: 140rpx, radius: 32rpx | .banner-bg | 渐变需简化 |
| 3 | L1 | 统计卡片 | .summary-card | p-4 rounded-xl | padding: 28rpx, radius: 24rpx | .summary-card | — |
| 4 | L2 | 卡片标题 | .summary-card h3 | text-base font-semibold | font-size: 28rpx, weight: 600 | .card-title | — |
| 5 | L2 | 金额数值 | .amount | text-2xl font-semibold | font-size: 42rpx, weight: 600 | .amount | — |
| ... | ... | ... | ... | ... | ... | ... | ... |
```
列说明:
- **层级**L0=页面根, L1=首层区块, L2=区块内组件, L3=组件内元素...
- **理论 rpx 值**:从 Tailwind 类名查速查表换算,不是实测值
- **MP 选择器**:迁移前留空,迁移后填入对应的 WXML 选择器
- **备注**CSS 风险特性、不可消除差异、特殊处理方案
#### 0.3 结构完整性校验
映射表同时用于校验 MP 端结构完整性:
| 校验项 | 方法 | 判定 |
|--------|------|------|
| 元素缺失 | H5 映射表中有、MP 选择器为空或不存在 | P0 |
| 元素多余 | MP 中存在映射表未列出的视觉元素 | 检查是否为调试元素,否则标注 |
| 顺序错误 | MP 元素 DOM 顺序与映射表不一致 | P0 |
| 嵌套层级不一致 | MP 的父子关系与 H5 不同 | P1可能影响间距计算 |
校验时机:
- **迁移前**映射表产出后MP 选择器列全部留空,无需校验
- **迁移后**:填入 MP 选择器,逐行校验存在性和顺序
- **对比审计时**:先跑结构完整性校验,通过后再进入测量阶段
### 阶段 1逐级测量自底向上
目标:从叶子节点开始,逐级向上测量每个元素的尺寸和间距。
#### 1.1 测量顺序
```
叶子节点L3/L4→ 组件容器L2→ 区块容器L1→ 页面根L0
```
自底向上的原因:内层元素的实测尺寸决定了外层容器的实际高度。先测内层,才能判断外层的 padding/gap 是否正确。
#### 1.2 每个元素的标准测量维度
| 维度 | 测量方法 | 适用节点 |
|------|---------|---------|
| width / height | `getBoundingClientRect()` | 所有 |
| padding四方向 | `getComputedStyle().paddingTop/Right/Bottom/Left` | 容器节点 |
| margin四方向 | `getComputedStyle().marginTop/Right/Bottom/Left` | 所有 |
| font-size | `getComputedStyle().fontSize` | 文字节点 |
| line-height | `getComputedStyle().lineHeight` | 文字节点 |
| border-radius | `getComputedStyle().borderRadius` | 有圆角的节点 |
| 与父容器的 offset | `child.rect.top - parent.rect.top - parent.paddingTop` | 所有子节点 |
| 与相邻兄弟的 gap | `next.rect.top - current.rect.bottom` | 同级相邻节点 |
#### 1.3 H5 端测量Playwright MCP / measure_gaps.py
方式 APlaywright MCP `browser_evaluate`
```javascript
// 测量单个元素的完整属性
async (selector) => {
const el = document.querySelector(selector);
if (!el) return { error: 'not found' };
const rect = el.getBoundingClientRect();
const cs = getComputedStyle(el);
return {
selector,
width: rect.width, height: rect.height,
top: rect.top, left: rect.left,
paddingTop: parseFloat(cs.paddingTop),
paddingRight: parseFloat(cs.paddingRight),
paddingBottom: parseFloat(cs.paddingBottom),
paddingLeft: parseFloat(cs.paddingLeft),
marginTop: parseFloat(cs.marginTop),
marginBottom: parseFloat(cs.marginBottom),
fontSize: parseFloat(cs.fontSize),
lineHeight: cs.lineHeight === 'normal' ? 'normal' : parseFloat(cs.lineHeight),
fontWeight: cs.fontWeight,
borderRadius: cs.borderRadius,
color: cs.color,
backgroundColor: cs.backgroundColor
};
}
```
> Playwright MCP 不支持 `file://` 协议。需先起本地 HTTP 服务:
> `python -m http.server 8765 --directory docs/h5_ui/pages`
> 然后用 `browser_navigate("http://localhost:8765/<page>.html")` 访问。
方式 B`scripts/ops/measure_gaps.py`(批量测量)
```bash
uv run python scripts/ops/measure_gaps.py <page> --selectors "<sel1>" "<sel2>" ...
```
#### 1.4 MP 端测量SelectorQuery
```javascript
// 微信开发者工具 MCP evaluate_script
async (selector) => {
return new Promise(resolve => {
const query = wx.createSelectorQuery();
query.select(selector).boundingClientRect(rect => {
query.select(selector).fields({
computedStyle: ['paddingTop','paddingRight','paddingBottom','paddingLeft',
'marginTop','marginBottom','fontSize','lineHeight',
'fontWeight','borderRadius','color','backgroundColor']
}, style => {
resolve({ rect, style });
}).exec();
}).exec();
});
}
```
#### 1.5 测量结果记录格式
每个元素的测量结果追加到映射表:
```markdown
| # | 元素 | 属性 | H5 实测(px) | 理论 rpx | MP 实测(rpx) | 偏差 | 级别 |
|---|------|------|------------|---------|-------------|------|------|
| 4 | 卡片标题 | font-size | 16px | 28rpx | 28rpx | 0 | ✅ |
| 4 | 卡片标题 | line-height | 24px | 42rpx | 36rpx | -6rpx | P3 |
| 4 | 卡片标题 | font-weight | 600 | 600 | 400 | — | P6 |
| 3 | 统计卡片 | padding-top | 16px | 28rpx | 30rpx | +2rpx | ✅ |
| 3→4 | 卡片→标题 gap | margin-top | 0px | 0rpx | 0rpx | 0 | ✅ |
```
### 阶段 2偏差审计
目标:基于测量结果,逐属性判定偏差级别,输出结构化审计报告。
#### 2.1 偏差判定标准
| rpx 偏差 | 级别 | 说明 |
|----------|------|------|
| 0 | ✅ 通过 | 完全匹配 |
| ≤ 2rpx | ✅ 通过 | rpx 取偶导致的不可避免误差 |
| > 2rpx 且 ≤ 4rpx | ⚠️ 警告P4-P7 | 像素级精调可修复 |
| > 4rpx | ❌ 不通过P0-P3 | 必须修正 |
特殊属性判定:
- **font-weight**:不用 rpx 偏差直接比对数值400/500/600/700不一致即 P6
- **color**:不用 rpx 偏差,比对 hex 值,不在标准色表中即 P5
- **border-radius**:使用 `px × 2` 公式(不乘 1.7442),偏差 > 2rpx 即 P4
- **结构缺失/顺序错误**:直接 P0不进入测量
#### 2.2 审计报告模板
```markdown
# <page> 结构化审计报告
## 基本信息
- 页面:<page>
- 审计日期YYYY-MM-DD
- H5 元素总数N
- MP 元素总数M
- 结构完整性:✅ 通过 / ❌ 缺失 K 个元素
## 结构完整性校验
| # | H5 元素 | MP 状态 | 问题 |
|---|---------|---------|------|
| 7 | .env-arrow svg | ❌ 缺失 | MP 不支持 inline SVG需导出 |
## 偏差汇总
| 级别 | 数量 | 占比 |
|------|------|------|
| ✅ 通过 | XX | XX% |
| ⚠️ P4-P7 | XX | XX% |
| ❌ P0-P3 | XX | XX% |
## 逐元素偏差明细
(按映射表 # 排序,仅列出有偏差的属性)
| # | 元素 | 属性 | H5 值 | 理论 rpx | MP 值 | 偏差 | 级别 | 修正建议 |
|---|------|------|-------|---------|-------|------|------|---------|
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
## 不可消除差异(不计入偏差)
- 字体渲染差异Noto Sans SC vs 系统字体)
- 行内元素高度系统性偏小 ~7%
- 抗锯齿差异
```
#### 2.3 迁移前审计 vs 迁移后对比
| 维度 | 迁移前审计 | 迁移后对比 |
|------|-----------|-----------|
| 输入 | H5 源码 | H5 源码 + MP 源码 |
| 映射表 MP 列 | 留空 | 填入实际选择器 |
| 测量端 | 仅 H5 | H5 + MP 双端 |
| 产出 | 映射表 + 理论 rpx 值 | 偏差审计报告 |
| 用途 | 指导 WXSS 编写 | 验证迁移质量 |
迁移前审计产出的映射表直接作为迁移后对比的输入——MP 开发完成后,填入 MP 选择器列,跑结构完整性校验 + 逐级测量,即可得到偏差报告。
---
## Tailwind → WXSS 完整速查表
### 字号与行高
| Tailwind | H5 px | rpx | 说明 |
|----------|-------|-----|------|
| text-xs | 12px / 16px | 22rpx / 28rpx | 字号 / 行高 |
| text-sm | 14px / 20px | 26rpx / 36rpx | 字号 / 行高 |
| text-base | 16px / 24px | 28rpx / 42rpx | 字号 / 行高 |
| text-lg | 18px / 28px | 32rpx / 50rpx | 字号 / 行高 |
| text-xl | 20px / 28px | 36rpx / 50rpx | 字号 / 行高 |
| text-2xl | 24px / 32px | 42rpx / 56rpx | 字号 / 行高 |
⚠️ Tailwind `text-*` 同时设置 font-size 和 line-height。MP 迁移时必须同时迁移两个属性,遗漏 line-height 会导致行高塌陷。
### 间距
| Tailwind | H5 px | rpx |
|----------|-------|-----|
| p-1 / gap-1 | 4px | 8rpx |
| p-1.5 / gap-1.5 | 6px | 12rpx |
| p-2 / gap-2 | 8px | 14rpx |
| p-3 / gap-3 / space-y-3 | 12px | 22rpx |
| p-4 / gap-4 | 16px | 28rpx |
| p-5 / gap-5 | 20px | 36rpx |
| p-6 / gap-6 | 24px | 42rpx |
| p-8 / gap-8 | 32px | 56rpx |
⚠️ `space-y-N` 换算陷阱:严格按精确公式 `ceil(px × 1.7442 / 2) × 2`,不要心算跳步。
例:`space-y-3` = 12px → 12×1.7442=20.93 → ceil(20.93/2)×2 = ceil(10.47)×2 = 22rpx不是 20rpx
### 常用 px → rpx 对照
```
2px → 4rpx 3px → 6rpx 4px → 8rpx
5px → 10rpx 6px → 12rpx 8px → 14rpx
10px → 18rpx 11px → 20rpx 12px → 22rpx
14px → 26rpx 16px → 28rpx 18px → 32rpx
20px → 36rpx 24px → 42rpx 28px → 50rpx
32px → 56rpx 36px → 64rpx 40px → 70rpx
44px → 78rpx 48px → 84rpx
```
---
## 附录 Ameasure_gaps.py 详细用法
路径:`scripts/ops/measure_gaps.py`
### 基本用法
```bash
# 测量页面内所有 .task-card 元素的尺寸和间距
uv run python scripts/ops/measure_gaps.py task-list --selectors ".task-card"
# 测量多个选择器(按 DOM 顺序,计算相邻间距)
uv run python scripts/ops/measure_gaps.py board-finance --selectors ".summary-header" ".summary-content" ".grid-cols-3"
# 指定两个元素的直接间距
uv run python scripts/ops/measure_gaps.py task-list --pairs ".sticky-header" ".task-card:first-child"
# 页面中下方元素(需 scrollTop
uv run python scripts/ops/measure_gaps.py performance --selectors ".perf-section" --scroll 1200
```
### 输出内容
- 元素尺寸表top_px, h_px, paddingT/B, marginT/B, gap, fontSize, lineHeight, h_rpx
- 相邻元素垂直间距表gap_px 和 gap_rpx
- audit.md 可直接粘贴的 Markdown 表格
### 常用 CSS 选择器快查
| 元素类型 | 选择器示例 |
|---|---|
| 页面内边距容器 | `.px-4`, `.px-6`, `[class*="px-"]` |
| 卡片 | `.task-card`, `[class*="card"]` |
| 列表项间距 | `.list-item`, `li`, `[class*="item"]` |
| Sticky 头部 | `.sticky`, `.filter-bar`, `[class*="sticky"]` |
| Banner | `.banner-bg`, `[class*="banner"]` |
| 标签/徽章 | `.tag`, `.badge`, `[class*="tag"]` |
---
## 附录 B图像反推验证截图偏差定位
当 diff 图显示某元素位置偏移时,用截图像素反推实际间距:
```
实际间距(px) = diff 图中偏移像素数 ÷ DPR
DPR = 1.5
示例diff 图中元素 A 比 H5 下移 9 像素
实际偏差 = 9 / 1.5 = 6px → 查速查表 → 12rpx
应调整 WXSS 中对应 margin/padding
```

View File

@@ -0,0 +1,145 @@
---
name: "wechat-miniprogram"
displayName: "微信小程序官方文档"
description: "微信小程序开发官方文档查询工具覆盖框架、组件、API、服务端、自定义组件等全部开发知识。当需要查阅小程序开发细节时按需加载对应领域的 steering 文件获取权威参考。"
keywords: ["微信小程序", "miniprogram", "wxml", "wxss", "wx.request", "小程序组件", "小程序API", "Page", "Component", "getApp", "小程序生命周期", "小程序登录", "小程序支付", "weixin", "wechat", "tdesign", "t-button", "t-cell", "t-dialog", "t-toast", "t-navbar", "t-tabs", "t-popup", "t-input", "t-picker", "t-tag", "t-search", "t-empty", "t-loading", "t-skeleton", "t-tab-bar", "t-avatar", "t-badge", "t-image"]
author: "NeoZQYY"
---
# 微信小程序官方文档
## Overview
本 Power 是微信小程序官方开发文档的结构化知识库,覆盖框架核心概念、视图层、逻辑层、自定义组件、内置组件、前端 API、服务端 API 等全部开发领域。
当 AI 需要回答小程序相关问题时,应根据问题领域加载对应的 steering 文件,获取官方文档的权威内容,而非依赖可能过时的训练数据。
官方文档入口https://developers.weixin.qq.com/miniprogram/dev/framework/
## 文档结构与 Steering 文件索引
以下 steering 文件按领域组织,按需加载:
### framework-core.md — 框架核心
- 小程序框架概述MINA 架构:逻辑层 + 视图层)
- 小程序配置app.json / page.json / sitemap.json
- 目录结构与文件类型(.wxml / .wxss / .js / .json
- 场景值、兼容性、基础库版本
### app-service.md — 逻辑层App Service
- App() 注册程序、生命周期
- Page() 注册页面、页面生命周期onLoad/onShow/onReady/onHide/onUnload
- 页面路由navigateTo/redirectTo/switchTab/reLaunch/navigateBack
- 模块化require/module.exports、文件作用域
- API 调用方式(回调/Promise
- 页面间通信、EventChannel
### view-layer.md — 视图层WXML / WXSS / WXS
- WXML 语法:数据绑定、列表渲染(wx:for)、条件渲染(wx:if)、模板(template)、引用(import/include)
- WXSS 样式rpx 单位、样式导入(@import)、选择器支持范围、内联样式
- WXSWeiXin Script语法、模块、与 WXML 配合使用
- 事件系统:冒泡/非冒泡事件、bind/catch/mut-bind、事件对象、dataset
### custom-component.md — 自定义组件
- 创建自定义组件json/wxml/wxss/js 四文件)
- Component() 构造器properties / data / methods / lifetimes / pageLifetimes / observers
- 组件模板和样式slot、样式隔离 styleIsolation
- 组件间通信properties 传值、triggerEvent 事件、selectComponent
- behaviors代码复用类似 mixins
- 数据监听器observers
- 纯数据字段pureDataPattern
- 组件间关系relations
- 抽象节点componentGenerics
- 用 Component 构造器构造页面
### builtin-components.md — 内置组件
- 视图容器view / scroll-view / swiper / movable-view / cover-view
- 基础内容text / rich-text / progress / icon
- 表单组件button / input / textarea / picker / slider / switch / checkbox / radio / form
- 导航navigator
- 媒体image / video / camera / live-player / live-pusher
- 地图map
- 画布canvas
- 开放能力open-data / web-view / ad
- 无障碍访问
### frontend-api.md — 前端 API
- 基础wx.canIUse / wx.env / 系统信息
- 路由wx.navigateTo / wx.redirectTo / wx.switchTab / wx.reLaunch / wx.navigateBack
- 界面wx.showToast / wx.showModal / wx.showLoading / wx.showActionSheet / 导航栏 / TabBar / 下拉刷新
- 网络wx.request / wx.uploadFile / wx.downloadFile / WebSocket / TCP/UDP
- 数据缓存wx.setStorage / wx.getStorage / wx.removeStorage
- 媒体:图片(wx.chooseImage/wx.previewImage) / 录音 / 音频 / 视频 / 相机
- 位置wx.getLocation / wx.openLocation / wx.chooseLocation
- 文件wx.saveFile / FileSystemManager
- 开放接口:登录(wx.login) / 用户信息 / 支付(wx.requestPayment) / 授权 / 设置 / 收货地址 / 发票 / 生物认证 / 微信运动 / 订阅消息
- 设备:蓝牙 / NFC / Wi-Fi / 电话 / 加速计 / 罗盘 / 陀螺仪 / 剪贴板 / 屏幕亮度 / 振动
- Worker / 第三方平台 / WXML 节点查询(SelectorQuery / IntersectionObserver)
- 画布 Canvas API
### server-api.md — 服务端 API
- 登录code2Sessioncode 换 session_key + openid
- access_token 获取与管理
- 用户信息解密(手机号、用户信息)
- 消息推送(订阅消息、客服消息、模板消息)
- 小程序码与二维码生成
- 内容安全(文本/图片审核)
- 数据分析
- 物流助手
- OCR / 直播 / 安全风控
### login-auth.md — 登录与鉴权(重点)
- wx.login() 获取 code 的完整流程
- 服务端 code2Session 换取 openid / session_key / unionid
- 自定义登录态设计token 方案)
- wx.checkSession() 检查 session_key 有效性
- 手机号快速验证getPhoneNumber
- 用户信息获取getUserProfile 已废弃 → 头像昵称填写能力)
- 授权流程wx.authorize / wx.getSetting / wx.openSetting
- 常见登录架构与安全注意事项
### best-practices.md — 开发最佳实践与常见坑
- setData 性能优化(减少数据量、避免频繁调用)
- 分包加载subpackages / 独立分包 / 分包预下载)
- 图片优化与懒加载
- 页面栈管理(最多 10 层)
- 小程序与 H5 差异(无 DOM/BOM、不支持 window/document
- TypeScript 支持
- npm 支持与构建
- 自定义 tabBar
- 骨架屏
- 常见审核被拒原因与规避
### tdesign.md — TDesign 小程序组件库
- TDesign 安装与配置npm 构建、TS 配置、基础库要求)
- 完整组件列表60+ 组件,按基础/导航/输入/数据展示/反馈分类)
- 常用组件用法示例Button / Input / Cell / Dialog / Toast / Popup / Tabs / Navbar / TabBar / Search / Empty / Loading / Skeleton
- 样式覆盖 4 种方式style 属性 / 解除隔离 / 外部样式类 / CSS 变量)
- 自定义主题(全局 Design Token / CSS Variables
- 深色模式适配
## 使用方式
当遇到小程序相关问题时:
1. 根据问题领域,使用 `readSteering` 加载对应的 steering 文件
2. 如果不确定属于哪个领域,先加载 `framework-core.md` 了解整体架构
3. 登录/鉴权问题优先加载 `login-auth.md`
4. 组件用法问题加载 `builtin-components.md``custom-component.md`
5. API 调用问题加载 `frontend-api.md``server-api.md`
6. TDesign 组件用法/样式定制加载 `tdesign.md`
## 在线查询
如果 steering 文件中的信息不够详细,可以直接访问官方文档页面获取最新内容:
- 框架https://developers.weixin.qq.com/miniprogram/dev/framework/
- 组件https://developers.weixin.qq.com/miniprogram/dev/component/
- APIhttps://developers.weixin.qq.com/miniprogram/dev/api/
- 服务端 APIhttps://developers.weixin.qq.com/miniprogram/dev/api-backend/
- 配置参考https://developers.weixin.qq.com/miniprogram/dev/reference/
- 开发者工具https://developers.weixin.qq.com/miniprogram/dev/devtools/devtools.html
- TDesign 组件总览https://tdesign.tencent.com/miniprogram/overview
- TDesign 具体组件https://tdesign.tencent.com/miniprogram/components/{组件名}
使用 `webFetch` 工具抓取对应页面即可获取最新文档内容。

View File

@@ -0,0 +1,255 @@
# 逻辑层App Service
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/
逻辑层使用 JavaScript 引擎运行,不在浏览器中,没有 `window``document` 等 Web API。
## App() — 注册小程序
```javascript
App({
onLaunch(options) {
// 小程序初始化时触发,全局只触发一次
// options.scene — 场景值
// options.query — 启动参数
// options.path — 启动页面路径
},
onShow(options) {
// 小程序启动或从后台进入前台时触发
},
onHide() {
// 小程序从前台进入后台时触发
},
onError(msg) {
// 小程序发生脚本错误或 API 调用失败时触发
console.error(msg)
},
onUnhandledRejection(res) {
// 未处理的 Promise 拒绝事件
console.warn(res.reason, res.promise)
},
onPageNotFound(res) {
// 页面不存在时触发
wx.redirectTo({ url: 'pages/...' })
},
onThemeChange({ theme }) {
// 系统主题变更dark / light
},
globalData: {
userInfo: null
}
})
```
获取 App 实例:
```javascript
const app = getApp()
console.log(app.globalData)
```
**注意**
- 不要在 `App()` 内调用 `getApp()`,使用 `this` 即可
- 不要在 `onLaunch` 时调用 `getCurrentPages()`,此时 page 还没有生成
## Page() — 注册页面
```javascript
Page({
data: {
text: 'Hello',
array: [{ msg: '1' }, { msg: '2' }]
},
// ===== 生命周期 =====
onLoad(options) {
// 页面加载时触发options 为页面路由参数
// 一个页面只会调用一次
},
onShow() {
// 页面显示/切入前台时触发
},
onReady() {
// 页面初次渲染完成时触发
// 一个页面只会调用一次,代表页面已可以和视图层交互
},
onHide() {
// 页面隐藏/切入后台时触发(如 navigateTo 或底部 tab 切换)
},
onUnload() {
// 页面卸载时触发(如 redirectTo 或 navigateBack
},
// ===== 页面事件处理 =====
onPullDownRefresh() {
// 下拉刷新(需在 json 中开启 enablePullDownRefresh
// 处理完后调用 wx.stopPullDownRefresh()
},
onReachBottom() {
// 上拉触底(可在 json 中设置 onReachBottomDistance
},
onShareAppMessage(res) {
// 用户点击右上角转发
// res.from: 'button' 或 'menu'
return {
title: '自定义转发标题',
path: '/pages/index/index',
imageUrl: '' // 自定义图片路径
}
},
onShareTimeline() {
// 分享到朋友圈(基础库 2.11.3+
return { title: '', query: '', imageUrl: '' }
},
onPageScroll(res) {
// 页面滚动时触发res.scrollTop 为垂直滚动距离(px)
// 注意:频繁触发,避免在此做复杂操作
},
onResize(res) {
// 页面尺寸变化时触发(如屏幕旋转)
},
onTabItemTap(item) {
// 当前是 tab 页时,点击 tab 时触发
// item.index / item.pagePath / item.text
},
// ===== 自定义方法 =====
viewTap() {
this.setData({
text: 'Set some data for updating view.'
})
}
})
```
### 页面生命周期顺序
```
onLoad → onShow → onReady → [onHide → onShow] → onUnload
```
### setData 详解
```javascript
// 基本用法
this.setData({
text: 'changed data'
})
// 修改数组某一项
this.setData({
'array[0].msg': 'changed'
})
// 修改对象某个属性
this.setData({
'object.key': 'value'
})
// 带回调
this.setData({ text: 'new' }, function() {
// setData 引起的界面更新渲染完毕后的回调
})
```
**setData 性能注意事项**
- 数据量不宜过大(单次 setData 不超过 1MB建议不超过 256KB
- 不要频繁调用(如 onPageScroll 中不要每次都 setData
- 只传需要更新的数据,不要整个 data 都传
- 后台页面不要 setData页面 onHide 后避免 setData
## 页面路由
框架以栈的形式维护当前所有页面,最多 10 层。
| 路由方式 | 触发时机 | 路由前页面 | 路由后页面 |
|----------|----------|-----------|-----------|
| 初始化 | 小程序打开第一个页面 | | onLoad, onShow |
| 打开新页面 | wx.navigateTo / `<navigator open-type="navigate">` | onHide | onLoad, onShow |
| 页面重定向 | wx.redirectTo / `<navigator open-type="redirect">` | onUnload | onLoad, onShow |
| 页面返回 | wx.navigateBack / 用户左上角返回 | onUnload | onShow |
| Tab 切换 | wx.switchTab / `<navigator open-type="switchTab">` / 用户切换 Tab | | 各种情况 |
| 重启动 | wx.reLaunch / `<navigator open-type="reLaunch">` | onUnload | onLoad, onShow |
```javascript
// 保留当前页面,跳转到新页面(页面栈 +1
wx.navigateTo({ url: '/pages/detail/detail?id=1' })
// 关闭当前页面,跳转到新页面(页面栈不变)
wx.redirectTo({ url: '/pages/detail/detail?id=1' })
// 关闭所有页面,打开某个页面
wx.reLaunch({ url: '/pages/index/index' })
// 跳转到 tabBar 页面,关闭其他所有非 tabBar 页面
wx.switchTab({ url: '/pages/index/index' })
// 返回上一页delta 为返回的页面数)
wx.navigateBack({ delta: 1 })
```
### 页面间通信EventChannel
```javascript
// 页面 A
wx.navigateTo({
url: '/pages/B/B',
events: {
// 监听来自 B 页面的事件
acceptDataFromOpenedPage(data) {
console.log(data)
}
},
success(res) {
// 向 B 页面发送数据
res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' })
}
})
// 页面 B
Page({
onLoad() {
const eventChannel = this.getOpenerEventChannel()
// 向 A 页面发送数据
eventChannel.emit('acceptDataFromOpenedPage', { data: 'from B' })
// 监听来自 A 页面的数据
eventChannel.on('acceptDataFromOpenerPage', (data) => {
console.log(data)
})
}
})
```
## 模块化
```javascript
// common.js
function sayHello(name) {
console.log(`Hello ${name}!`)
}
module.exports.sayHello = sayHello
// 使用
const common = require('common.js')
common.sayHello('MINA')
```
**注意**
- 小程序不支持直接引入 `node_modules`,需使用 npm 构建或手动拷贝
- 每个文件有独立作用域,不同文件中可声明同名变量
- 通过 `getApp()` 获取全局数据
## getCurrentPages()
```javascript
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] // 当前页面
const prevPage = pages[pages.length - 2] // 上一个页面
// 可以通过 prevPage.setData() 修改上一页数据(返回时生效)
```
## 在线查询
如需更详细信息,可抓取:
- App 参考https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html
- Page 参考https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html
- 路由https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/route.html

View File

@@ -0,0 +1,304 @@
# 开发最佳实践与常见坑
> 综合官方文档与社区经验
## setData 性能优化
setData 是小程序性能的关键瓶颈因为数据需要从逻辑层JS 线程)序列化后传输到视图层(渲染线程)。
### 原则
1. **减少数据量**:只传需要更新的字段
```javascript
// ❌ 错误:传整个列表
this.setData({ list: this.data.list })
// ✅ 正确:只更新变化的项
this.setData({ 'list[2].name': 'new name' })
```
2. **减少调用频率**:合并多次 setData
```javascript
// ❌ 错误:多次调用
this.setData({ a: 1 })
this.setData({ b: 2 })
this.setData({ c: 3 })
// ✅ 正确:合并为一次
this.setData({ a: 1, b: 2, c: 3 })
```
3. **后台页面不要 setData**
```javascript
// ❌ 错误:页面隐藏后仍在 setData如定时器
onShow() {
this._timer = setInterval(() => {
this.setData({ time: Date.now() })
}, 1000)
},
// ✅ 正确:页面隐藏时停止
onHide() {
clearInterval(this._timer)
}
```
4. **避免在 onPageScroll 中 setData**
```javascript
// ❌ 错误
onPageScroll(e) {
this.setData({ scrollTop: e.scrollTop })
}
// ✅ 正确:用 WXS 响应事件或节流
onPageScroll: throttle(function(e) {
if (this._needUpdate) {
this.setData({ isTop: e.scrollTop < 100 })
}
}, 100)
```
5. **大列表用纯数据字段**
```javascript
Component({
options: { pureDataPattern: /^_/ },
data: {
displayList: [], // 用于渲染
_rawList: [] // 纯数据,不传输到视图层
}
})
```
## 分包加载
### 基本分包
```json
// app.json
{
"pages": [
"pages/index/index",
"pages/logs/logs"
],
"subpackages": [
{
"root": "packageA",
"name": "pack-a",
"pages": [
"pages/cat/cat",
"pages/dog/dog"
]
},
{
"root": "packageB",
"name": "pack-b",
"pages": [
"pages/apple/apple"
]
}
]
}
```
**限制**
- 整个小程序所有分包大小不超过 20MB使用分包时
- 单个分包/主包大小不超过 2MB
- tabBar 页面必须在主包
### 独立分包
不依赖主包即可运行,适合独立功能页面(如活动页)。
```json
{
"subpackages": [
{
"root": "packageIndependent",
"pages": ["pages/activity/activity"],
"independent": true
}
]
}
```
**注意**独立分包中不能使用主包的公共资源js/组件/样式)。
### 分包预下载
```json
{
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["packageA"]
},
"pages/logs/logs": {
"network": "wifi",
"packages": ["packageB"]
}
}
}
```
## 小程序与 H5 的关键差异
| 特性 | H5 | 小程序 |
|------|-----|--------|
| DOM 操作 | 支持 document/window | ❌ 不支持 |
| BOM | 支持 | ❌ 不支持 |
| 路由 | URL hash/history | 页面栈(最多 10 层) |
| 样式 | 完整 CSS | WXSS部分 CSS 不支持) |
| 脚本 | 完整 JS + Web API | JS无 DOM API |
| 渲染 | 单线程 | 双线程(逻辑层 + 视图层) |
| 网络请求 | fetch/XMLHttpRequest | wx.request需配置域名 |
| 本地存储 | localStorage | wx.setStorage10MB |
| Cookie | 支持 | ❌ 不支持(需自行管理) |
| 动态创建元素 | 支持 | ❌ 不支持 |
| eval / new Function | 支持 | ❌ 不支持 |
| SVG | 支持 | 部分支持image src 可用) |
### 常见迁移坑
1. **没有 Cookie**:登录态需要自行通过 header 传递 token
2. **没有 DOM**:不能用 jQuery、不能 `document.getElementById`
3. **不支持动态执行代码**`eval()``new Function()` 都不可用
4. **样式差异**
- 不支持 `*` 通配符选择器
- 不支持 `>` `+` `~` 等关系选择器(部分版本已支持)
- 不支持 `@media` 的部分写法
- 不支持 `position: fixed` 在某些场景下的表现
5. **页面栈限制**:最多 10 层,超过后 navigateTo 会失败
6. **包大小限制**:主包 2MB总包 20MB
7. **网络请求域名白名单**:必须在管理后台配置
8. **不支持 npm 直接引入**:需要通过开发者工具构建 npm
## TypeScript 支持
小程序原生支持 TypeScript
```json
// tsconfig.json项目根目录
{
"compilerOptions": {
"strict": true,
"target": "ES2017",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["ES2017"],
"typeRoots": ["./typings"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
```
```typescript
// 页面 .ts 文件
Page({
data: {
msg: 'Hello' as string,
list: [] as Array<{ id: number; name: string }>
},
onLoad(options: Record<string, string | undefined>) {
const id = options.id
}
})
// 组件 .ts 文件
Component({
properties: {
title: { type: String, value: '' }
},
data: {
count: 0 as number
},
methods: {
increment() {
this.setData({ count: this.data.count + 1 })
}
}
})
```
## npm 支持
1. 在小程序根目录执行 `npm install`
2. 在开发者工具中:工具 → 构建 npm
3. 构建后会生成 `miniprogram_npm` 目录
4. 使用:`const dayjs = require('dayjs')`
**注意**
- 不是所有 npm 包都能在小程序中使用(不能依赖 Node.js 内置模块或浏览器 API
- 每次 `npm install` 后都需要重新构建 npm
## 自定义 tabBar
```json
// app.json
{
"tabBar": {
"custom": true,
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/mine/mine", "text": "我的" }
]
}
}
```
在根目录创建 `custom-tab-bar/` 组件:
```
custom-tab-bar/
├── index.js
├── index.json
├── index.wxml
└── index.wxss
```
## 骨架屏
开发者工具支持自动生成骨架屏:
1. 在模拟器中预览页面
2. 点击模拟器右下角「...」→「生成骨架屏」
3. 会自动生成 `页面名.skeleton.wxml``页面名.skeleton.wxss`
```json
// 页面 json 中引用
{
"initialRenderingCache": "static"
}
```
## 常见审核被拒原因
1. **功能不完整**:提交审核时确保所有功能可用
2. **测试账号未提供**:需要登录的小程序必须提供测试账号
3. **类目不符**:选择的服务类目与实际功能不匹配
4. **诱导分享/关注**:不能强制用户分享或关注公众号才能使用
5. **虚拟支付**iOS 不允许虚拟商品使用微信支付(需走 IAP
6. **内容违规**UGC 内容需要内容安全检测
7. **隐私协议**:需要配置隐私保护指引
8. **授权滥用**:不能在首页就弹出授权请求,需要在使用时才请求
## 调试技巧
```javascript
// 真机调试日志
const log = wx.getRealtimeLogManager()
log.info('info message')
log.warn('warn message')
log.error('error message')
// 性能监控
const performance = wx.getPerformance()
const observer = performance.createObserver((entryList) => {
console.log(entryList.getEntries())
})
observer.observe({ entryTypes: ['render', 'script', 'navigation'] })
```
## 在线查询
- 性能优化https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html
- 分包加载https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages.html
- npm 支持https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html
- 自定义 tabBarhttps://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html

View File

@@ -0,0 +1,364 @@
# 内置组件
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/component/
## 视图容器
### view
基础视图容器,类似 HTML 的 `<div>`
```xml
<view class="container" hover-class="hover" hover-stay-time="400">
内容
</view>
```
| 属性 | 类型 | 说明 |
|------|------|------|
| hover-class | string | 按下去的样式类none 为不设置 |
| hover-stop-propagation | boolean | 阻止祖先节点出现点击态 |
| hover-start-time | number | 按住后多久出现点击态(ms),默认 50 |
| hover-stay-time | number | 松开后点击态保留时间(ms),默认 400 |
### scroll-view
可滚动视图区域。
```xml
<!-- 纵向滚动需设置固定高度 -->
<scroll-view
scroll-y
style="height: 300px;"
bindscrolltolower="loadMore"
bindscroll="onScroll"
scroll-into-view="{{toView}}"
scroll-top="{{scrollTop}}"
refresher-enabled="{{true}}"
bindrefresherrefresh="onRefresh"
>
<view id="item1">A</view>
<view id="item2">B</view>
<view id="item3">C</view>
</scroll-view>
```
| 属性 | 类型 | 说明 |
|------|------|------|
| scroll-x | boolean | 允许横向滚动 |
| scroll-y | boolean | 允许纵向滚动 |
| upper-threshold | number | 距顶部/左边多远时触发 scrolltoupper(px),默认 50 |
| lower-threshold | number | 距底部/右边多远时触发 scrolltolower(px),默认 50 |
| scroll-top | number | 设置竖向滚动条位置 |
| scroll-into-view | string | 滚动到某子元素id |
| scroll-with-animation | boolean | 滚动动画过渡 |
| enable-back-to-top | boolean | iOS 点击状态栏回到顶部 |
| refresher-enabled | boolean | 开启自定义下拉刷新2.10.1+ |
| refresher-triggered | boolean | 设置刷新状态 |
| enhanced | boolean | 增强模式2.12.0+,支持 scroll-into-view 动画等) |
### swiper / swiper-item
滑块视图容器(轮播图)。
```xml
<swiper
indicator-dots="{{true}}"
autoplay="{{true}}"
interval="{{3000}}"
duration="{{500}}"
circular="{{true}}"
bindchange="swiperChange"
>
<swiper-item wx:for="{{imgUrls}}" wx:key="*this">
<image src="{{item}}" mode="aspectFill"/>
</swiper-item>
</swiper>
```
### movable-area / movable-view
可拖拽区域。
```xml
<movable-area style="height: 200px; width: 200px; background: red;">
<movable-view direction="all" style="height: 50px; width: 50px; background: blue;">
text
</movable-view>
</movable-area>
```
### cover-view / cover-image
覆盖在原生组件map、video、canvas、camera、live-player、live-pusher之上的视图。
## 基础内容
### text
```xml
<text selectable="{{true}}" space="ensp" decode="{{true}}">
Hello &nbsp; World
</text>
```
- `selectable`:文本是否可选
- `space`显示连续空格ensp/emsp/nbsp
- `decode`:是否解码(`&nbsp;` `&lt;` 等)
- **注意**`<text>` 组件内只支持嵌套 `<text>`
### rich-text
```xml
<rich-text nodes="{{htmlNodes}}"></rich-text>
```
```javascript
data: {
htmlNodes: '<div class="div_class"><h5>标题</h5><p>段落</p></div>'
// 也支持 nodes 数组格式
}
```
### icon
```xml
<icon type="success" size="23" color="green"/>
```
type 可选success, success_no_circle, info, warn, waiting, cancel, download, search, clear
### progress
```xml
<progress percent="80" show-info stroke-width="3" activeColor="#00CC00"/>
```
## 表单组件
### button
```xml
<button
type="primary"
size="default"
loading="{{isLoading}}"
disabled="{{isDisabled}}"
open-type="getPhoneNumber"
bindgetphonenumber="getPhoneNumber"
>
获取手机号
</button>
```
| open-type | 说明 |
|-----------|------|
| contact | 打开客服会话 |
| share | 触发转发 |
| getPhoneNumber | 获取手机号(需配合 bindgetphonenumber |
| getUserInfo | 已废弃2.27.1+ |
| launchApp | 打开 APP |
| openSetting | 打开授权设置页 |
| chooseAvatar | 获取用户头像(基础库 2.21.2+ |
| agreePrivacyAuthorization | 同意隐私协议2.33.2+ |
### input
```xml
<input
type="text"
placeholder="请输入"
value="{{inputValue}}"
bindinput="onInput"
bindfocus="onFocus"
bindblur="onBlur"
bindconfirm="onConfirm"
maxlength="140"
confirm-type="send"
adjust-position="{{true}}"
/>
```
| type | 说明 |
|------|------|
| text | 文本 |
| number | 数字 |
| idcard | 身份证 |
| digit | 带小数点数字 |
| nickname | 昵称输入(基础库 2.21.2+ |
| safe-password | 密码安全输入 |
### textarea
```xml
<textarea
value="{{content}}"
placeholder="请输入内容"
maxlength="-1"
auto-height
bindinput="onInput"
bindblur="onBlur"
show-confirm-bar="{{false}}"
/>
```
### picker
```xml
<!-- 普通选择器 -->
<picker mode="selector" range="{{array}}" bindchange="pickerChange">
<view>当前选择:{{array[index]}}</view>
</picker>
<!-- 多列选择器 -->
<picker mode="multiSelector" range="{{multiArray}}" bindchange="multiChange" bindcolumnchange="columnChange">
<view>{{multiArray[0][multiIndex[0]]}} - {{multiArray[1][multiIndex[1]]}}</view>
</picker>
<!-- 时间选择器 -->
<picker mode="time" value="{{time}}" start="09:00" end="21:00" bindchange="timeChange">
<view>{{time}}</view>
</picker>
<!-- 日期选择器 -->
<picker mode="date" value="{{date}}" start="2020-01-01" end="2030-12-31" bindchange="dateChange">
<view>{{date}}</view>
</picker>
<!-- 省市区选择器 -->
<picker mode="region" value="{{region}}" bindchange="regionChange">
<view>{{region[0]}} - {{region[1]}} - {{region[2]}}</view>
</picker>
```
### form
```xml
<form bindsubmit="formSubmit" bindreset="formReset">
<input name="username" placeholder="用户名"/>
<switch name="agree" checked/>
<slider name="age" show-value/>
<button form-type="submit">提交</button>
<button form-type="reset">重置</button>
</form>
```
```javascript
formSubmit(e) {
e.detail.value // { username: '...', agree: true, age: 50 }
}
```
### 其他表单组件
- `checkbox-group` + `checkbox`
- `radio-group` + `radio`
- `slider`
- `switch`
- `label`(绑定表单控件)
## 导航
### navigator
```xml
<navigator url="/pages/detail/detail?id=1" open-type="navigate">
跳转到详情
</navigator>
<navigator url="/pages/index/index" open-type="switchTab">
切换到首页 Tab
</navigator>
<navigator open-type="navigateBack" delta="1">
返回上一页
</navigator>
```
## 媒体组件
### image
```xml
<image
src="{{imgUrl}}"
mode="aspectFill"
lazy-load
show-menu-by-longpress
binderror="imgError"
bindload="imgLoad"
/>
```
| mode | 说明 |
|------|------|
| scaleToFill | 不保持比例缩放,填满 |
| aspectFit | 保持比例,完整显示(可能留白) |
| aspectFill | 保持比例,填满(可能裁剪) |
| widthFix | 宽度不变,高度自适应 |
| heightFix | 高度不变宽度自适应2.10.3+ |
| top/bottom/center/left/right | 不缩放,显示对应区域 |
**注意**image 默认宽 320px、高 240px。
### video
```xml
<video
src="{{videoUrl}}"
controls
autoplay="{{false}}"
loop="{{false}}"
muted="{{false}}"
initial-time="0"
show-fullscreen-btn
show-play-btn
enable-progress-gesture
bindplay="onPlay"
bindpause="onPause"
bindended="onEnded"
binderror="onError"
/>
```
### camera
```xml
<camera
device-position="back"
flash="auto"
bindscancode="onScanCode"
style="width: 100%; height: 300px;"
/>
```
## 地图
### map
```xml
<map
longitude="{{longitude}}"
latitude="{{latitude}}"
scale="16"
markers="{{markers}}"
polyline="{{polyline}}"
show-location
style="width: 100%; height: 300px;"
bindmarkertap="onMarkerTap"
bindregionchange="onRegionChange"
/>
```
## 画布
### canvas
```xml
<!-- 新版 Canvas 2D推荐基础库 2.9.0+ -->
<canvas type="2d" id="myCanvas" style="width: 300px; height: 200px;"/>
```
```javascript
const query = wx.createSelectorQuery()
query.select('#myCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
const dpr = wx.getWindowInfo().pixelRatio
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
ctx.fillRect(0, 0, 100, 100)
})
```
## 开放能力组件
### web-view
```xml
<!-- 承载网页的容器,会自动铺满整个小程序页面 -->
<web-view src="https://mp.weixin.qq.com/"></web-view>
```
- 需要在小程序管理后台配置业务域名
- 个人类型小程序暂不支持
### open-data已限制
```xml
<!-- 基础库 2.0.1+ 起,大部分 type 已不再返回真实数据 -->
<open-data type="userAvatarUrl"></open-data>
<open-data type="userNickName"></open-data>
```
## 在线查询
组件文档非常详细,如需查看某个具体组件的完整属性和事件,可抓取:
- 组件总览https://developers.weixin.qq.com/miniprogram/dev/component/
- 具体组件:`https://developers.weixin.qq.com/miniprogram/dev/component/{组件名}.html`
例如https://developers.weixin.qq.com/miniprogram/dev/component/scroll-view.html

View File

@@ -0,0 +1,443 @@
# 自定义组件
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/
基础库 1.6.3+ 支持。
## 创建自定义组件
一个自定义组件由 `json` `wxml` `wxss` `js` 四个文件组成。
```json
// my-component.json — 声明为组件
{ "component": true }
```
```xml
<!-- my-component.wxml -->
<view class="inner">
{{innerText}}
<slot></slot>
</view>
```
```css
/* my-component.wxss — 样式只作用于本组件 */
.inner { color: red; }
```
```javascript
// my-component.js
Component({
properties: {
innerText: {
type: String,
value: 'default value'
}
},
data: {
someData: {}
},
methods: {
customMethod() {}
}
})
```
### 使用自定义组件
```json
// 页面或组件的 .json
{
"usingComponents": {
"my-component": "/components/my-component/my-component"
}
}
```
```xml
<!-- 页面 wxml -->
<my-component inner-text="Some text">
<view>这里是插入到 slot 中的内容</view>
</my-component>
```
**注意事项**
- 标签名只能是小写字母、中划线、下划线的组合
- 组件和页面所在项目根目录名不能以 "wx-" 为前缀
- 使用 `usingComponents` 会使页面的 `this` 原型稍有差异(多了 `selectComponent` 等方法)
- 使用 `usingComponents` 时,`setData` 内容不会被深复制(性能优化)
## Component() 构造器
```javascript
Component({
// ===== 组件属性(外部传入) =====
properties: {
myProperty: {
type: String, // 类型String, Number, Boolean, Object, Array, null(任意)
value: '', // 默认值
observer(newVal, oldVal) {
// 属性变化时触发(已不推荐,建议用 observers
}
},
myProperty2: String // 简化定义
},
// ===== 组件内部数据 =====
data: {
someData: 'initial'
},
// ===== 生命周期(推荐写在 lifetimes 中) =====
lifetimes: {
created() {
// 组件实例刚被创建时
// 此时不能调用 setData通常用于给 this 添加自定义属性
},
attached() {
// 组件实例进入页面节点树时
// 大多数初始化工作在此进行
},
ready() {
// 组件在视图层布局完成后
},
moved() {
// 组件实例被移动到节点树另一个位置时
},
detached() {
// 组件实例被从页面节点树移除时
// 清理工作(如清除定时器)
},
error(err) {
// 组件方法抛出错误时(基础库 2.4.1+
}
},
// ===== 组件所在页面的生命周期 =====
pageLifetimes: {
show() {
// 页面被展示时
},
hide() {
// 页面被隐藏时
},
resize(size) {
// 页面尺寸变化时
},
routeDone() {
// 页面路由动画完成时(基础库 2.31.2+
}
},
// ===== 数据监听器(基础库 2.6.1+ =====
observers: {
'numberA, numberB'(numberA, numberB) {
// numberA 或 numberB 变化时触发
this.setData({ sum: numberA + numberB })
},
'some.subfield'(subfield) {
// 监听子数据字段
},
'arr[12]'(val) {
// 监听数组某一项
},
'some.field.**'(field) {
// 使用通配符监听所有子数据字段
},
'**'() {
// 监听所有 setData每次 setData 都触发,慎用)
}
},
// ===== 方法 =====
methods: {
onMyButtonTap() {
this.setData({ someData: 'new value' })
},
// 内部方法建议以下划线开头
_myPrivateMethod() {
this.setData({ 'A.B': 'myPrivateData' })
}
},
// ===== behaviors =====
behaviors: [],
// ===== 其他选项 =====
options: {
multipleSlots: true, // 启用多 slot默认只能一个
styleIsolation: 'isolated', // 样式隔离模式
pureDataPattern: /^_/, // 纯数据字段正则
virtualHost: true // 虚拟化组件节点(基础库 2.11.2+
},
// ===== 外部样式类 =====
externalClasses: ['my-class'],
// ===== 组件间关系 =====
relations: {},
// ===== 导出(配合 wx://component-export =====
export() {
return { myField: 'myValue' }
}
})
```
## 组件模板和样式
### 多 slot
```javascript
// 组件 js
Component({
options: { multipleSlots: true }
})
```
```xml
<!-- 组件 wxml -->
<view>
<slot name="before"></slot>
<view>组件内部内容</view>
<slot name="after"></slot>
</view>
<!-- 使用 -->
<my-component>
<view slot="before">before 内容</view>
<view slot="after">after 内容</view>
</my-component>
```
### 样式隔离 styleIsolation
```javascript
Component({
options: {
styleIsolation: 'isolated'
// 'isolated'(默认):组件样式完全隔离
// 'apply-shared':页面 wxss 样式会影响组件,但组件不影响页面
// 'shared':页面和组件样式互相影响
}
})
```
也可在 json 中配置:
```json
{ "styleIsolation": "isolated" }
```
### 外部样式类
```javascript
// 组件
Component({
externalClasses: ['my-class']
})
```
```xml
<!-- 组件 wxml -->
<view class="my-class">这段文本的颜色由外部决定</view>
<!-- 使用时 -->
<my-component my-class="red-text"/>
```
## 组件间通信
### 父 → 子properties
```xml
<my-component prop-a="{{dataA}}" prop-b="staticValue"/>
```
### 子 → 父triggerEvent
```javascript
// 子组件
Component({
methods: {
onTap() {
this.triggerEvent('myevent', { value: 'data' }, {
bubbles: false, // 是否冒泡
composed: false, // 是否穿越组件边界
capturePhase: false // 是否有捕获阶段
})
}
}
})
```
```xml
<!-- 父组件/页面 -->
<my-component bind:myevent="onMyEvent"/>
<!-- 或 bindmyevent="onMyEvent" -->
```
```javascript
// 父组件/页面
Page({
onMyEvent(e) {
e.detail // { value: 'data' }
}
})
```
### 父获取子实例selectComponent
```xml
<my-component id="the-id" class="the-class"/>
```
```javascript
const child = this.selectComponent('#the-id')
// 或 this.selectComponent('.the-class')
child.setData({ ... })
child.someMethod()
```
## behaviors代码复用
类似 mixins / traits。
```javascript
// my-behavior.js
module.exports = Behavior({
behaviors: [], // 可以引用其他 behavior
properties: {
myBehaviorProperty: { type: String }
},
data: {
myBehaviorData: {}
},
attached() {},
methods: {
myBehaviorMethod() {}
}
})
```
```javascript
// 组件中使用
const myBehavior = require('my-behavior')
Component({
behaviors: [myBehavior],
// 组件自身的 properties/data/methods 会与 behavior 合并
// 同名字段:组件 > behavior > 更早的 behavior
// 同名生命周期都会执行behavior 先于组件)
})
```
### 内置 behaviors
| behavior | 说明 |
|----------|------|
| `wx://form-field` | 使组件像表单控件form 可识别 |
| `wx://form-field-group` | form 识别组件内部所有表单控件2.10.2+ |
| `wx://form-field-button` | form 识别组件内部 button2.10.3+ |
| `wx://component-export` | 自定义 selectComponent 返回值2.2.3+ |
## 纯数据字段
不用于渲染的数据,不会参与 setData 传输,提升性能。
```javascript
Component({
options: {
pureDataPattern: /^_/ // 以 _ 开头的字段为纯数据
},
data: {
a: true, // 普通数据,参与渲染
_b: true // 纯数据,不参与渲染
}
})
```
## 组件间关系 relations
```javascript
// custom-ul
Component({
relations: {
'./custom-li': {
type: 'child',
linked(target) {}, // 子组件 attached 时
linkChanged(target) {},
unlinked(target) {} // 子组件 detached 时
}
}
})
// custom-li
Component({
relations: {
'./custom-ul': {
type: 'parent',
linked(target) {},
linkChanged(target) {},
unlinked(target) {}
}
}
})
```
**type 可选值**`parent` / `child` / `ancestor` / `descendant`
## 抽象节点 componentGenerics
```json
// selectable-group.json
{
"componentGenerics": {
"selectable": {
"default": "path/to/default"
}
}
}
```
```xml
<!-- selectable-group.wxml -->
<view wx:for="{{labels}}">
<selectable disabled="{{false}}"></selectable>
</view>
<!-- 使用时指定具体组件 -->
<selectable-group generic:selectable="custom-radio"/>
```
## 用 Component 构造器构造页面
```javascript
Component({
properties: {
paramA: Number, // 接收页面参数 ?paramA=123
paramB: String
},
methods: {
onLoad() {
this.data.paramA // 123
},
onShow() {},
onPullDownRefresh() {}
// 页面生命周期写在 methods 中
}
})
```
对应 json 需包含 `usingComponents`
```json
{ "usingComponents": {} }
```
好处:可以使用 behaviors 提取所有页面公用代码。
## 在线查询
如需更详细信息,可抓取:
- Component 参考https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html
- Behavior 参考https://developers.weixin.qq.com/miniprogram/dev/reference/api/Behavior.html
- 组件生命周期https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html

View File

@@ -0,0 +1,188 @@
# 框架核心Framework Core
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/
## 架构概述
小程序框架MINA分为两部分
- **逻辑层App Service**:运行 JavaScript处理数据和业务逻辑
- **视图层View**:渲染 WXML + WXSS展示 UI
逻辑层和视图层分别运行在不同的线程中,通过 Native 层进行数据传输和事件通信。
**关键限制**:逻辑层不运行在浏览器中,没有 `window``document` 等 Web API。
## 目录结构
一个小程序主体部分由三个文件组成(必须放在项目根目录):
| 文件 | 必需 | 作用 |
|------|------|------|
| `app.js` | 是 | 小程序逻辑App() 注册) |
| `app.json` | 是 | 小程序公共配置 |
| `app.wxss` | 否 | 小程序公共样式表 |
一个小程序页面由四个文件组成:
| 文件 | 必需 | 作用 |
|------|------|------|
| `页面.js` | 是 | 页面逻辑Page() 注册) |
| `页面.wxml` | 是 | 页面结构(模板) |
| `页面.wxss` | 否 | 页面样式 |
| `页面.json` | 否 | 页面配置 |
## app.json 全局配置
```json
{
"pages": [
"pages/index/index",
"pages/logs/logs"
],
"window": {
"navigationBarTitleText": "小程序",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#eeeeee",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false
},
"tabBar": {
"color": "#999",
"selectedColor": "#333",
"backgroundColor": "#fff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "images/tab/home.png",
"selectedIconPath": "images/tab/home-active.png"
}
]
},
"networkTimeout": {
"request": 10000,
"downloadFile": 10000
},
"subpackages": [],
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
"lazyCodeLoading": "requiredComponents"
}
```
### pages 配置
- 数组第一项为小程序初始页面(首页)
- 不需要写文件后缀,框架会自动寻找 `.json` `.js` `.wxml` `.wxss` 四个文件
- 新增/减少页面需要修改 pages 数组
### window 配置
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| navigationBarBackgroundColor | HexColor | #000000 | 导航栏背景颜色 |
| navigationBarTextStyle | string | white | 导航栏标题颜色,仅支持 black / white |
| navigationBarTitleText | string | | 导航栏标题文字 |
| navigationStyle | string | default | 导航栏样式custom 为自定义导航栏(只保留右上角胶囊按钮) |
| backgroundColor | HexColor | #ffffff | 窗口背景色 |
| backgroundTextStyle | string | dark | 下拉 loading 样式,仅支持 dark / light |
| enablePullDownRefresh | boolean | false | 是否开启全局下拉刷新 |
| onReachBottomDistance | number | 50 | 页面上拉触底事件触发时距页面底部距离px |
### tabBar 配置
- `list` 数组最少 2 个、最多 5 个 tab
- tabBar 页面必须在 pages 数组中
- `position` 可选 `bottom`(默认)或 `top`(顶部时不显示 icon
## 页面配置page.json
每个页面可以有自己的 `.json` 文件,覆盖 `app.json``window` 的配置:
```json
{
"navigationBarTitleText": "页面标题",
"enablePullDownRefresh": true,
"usingComponents": {
"my-component": "/components/my-component/my-component"
}
}
```
## sitemap.json
配置小程序及其页面是否允许被微信索引:
```json
{
"rules": [
{
"action": "allow",
"page": "*"
}
]
}
```
## 场景值
场景值用于描述用户进入小程序的路径,常见场景值:
| 场景值 | 说明 |
|--------|------|
| 1001 | 发现栏小程序主入口 |
| 1007 | 单人聊天会话中的小程序消息卡片 |
| 1008 | 群聊会话中的小程序消息卡片 |
| 1011 | 扫描二维码 |
| 1012 | 长按识别二维码 |
| 1020 | 公众号 profile 页相关小程序列表 |
| 1035 | 公众号自定义菜单 |
| 1036 | App 分享消息卡片 |
| 1037 | 小程序打开小程序 |
| 1038 | 从另一个小程序返回 |
| 1043 | 公众号模板消息 |
| 1047 | 扫描小程序码 |
| 1048 | 长按识别小程序码 |
| 1089 | 微信聊天主界面下拉 |
可在 `App.onLaunch` / `App.onShow` 中通过 `options.scene` 获取。
## 基础库版本兼容
```javascript
// 方式一wx.canIUse
if (wx.canIUse('openBluetoothAdapter')) {
wx.openBluetoothAdapter()
} else {
wx.showModal({ title: '提示', content: '当前微信版本过低,无法使用该功能' })
}
// 方式二:比较版本号
function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) v1.push('0')
while (v2.length < len) v2.push('0')
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i])
const num2 = parseInt(v2[i])
if (num1 > num2) return 1
else if (num1 < num2) return -1
}
return 0
}
const SDKVersion = wx.getSystemInfoSync().SDKVersion
if (compareVersion(SDKVersion, '1.1.0') >= 0) {
wx.openBluetoothAdapter()
}
```
## 在线查询
如需更详细的配置项说明,可直接抓取:
- 全局配置https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html
- 页面配置https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/page.html
- 场景值列表https://developers.weixin.qq.com/miniprogram/dev/reference/scene-list.html

View File

@@ -0,0 +1,431 @@
# 前端 API
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/api/
## API 调用约定
小程序 API 分三类:
- **事件监听 API**:以 `on` 开头,如 `wx.onSocketOpen`
- **同步 API**:以 `Sync` 结尾,如 `wx.setStorageSync`
- **异步 API**:大多数 API支持回调和 Promise
```javascript
// 回调风格
wx.request({
url: 'https://example.com/api',
success(res) { console.log(res.data) },
fail(err) { console.error(err) },
complete() { /* 无论成功失败都执行 */ }
})
// Promise 风格(基础库 2.10.2+,除部分 API 外均支持)
const res = await wx.request({ url: 'https://example.com/api' })
// 注意:部分 API 不支持 Promise如 wx.downloadFile、wx.connectSocket 等
```
## 路由
```javascript
// 保留当前页面,跳转(栈 +1最多 10 层)
wx.navigateTo({
url: '/pages/detail/detail?id=1&name=test',
events: { /* EventChannel 监听 */ },
success(res) { res.eventChannel.emit('data', {}) }
})
// 关闭当前页面,跳转
wx.redirectTo({ url: '/pages/other/other' })
// 关闭所有页面,打开
wx.reLaunch({ url: '/pages/index/index' })
// 跳转到 tabBar 页面(关闭其他非 tabBar 页面)
wx.switchTab({ url: '/pages/index/index' })
// ⚠️ switchTab 不支持带参数
// 返回
wx.navigateBack({ delta: 1 })
```
## 网络
### wx.request — HTTP 请求
```javascript
wx.request({
url: 'https://example.com/api/data',
method: 'POST', // GET, POST, PUT, DELETE, OPTIONS, HEAD, TRACE, CONNECT
data: { key: 'value' },
header: {
'content-type': 'application/json',
'Authorization': 'Bearer token'
},
timeout: 60000, // 超时时间(ms)
dataType: 'json', // 返回数据自动 JSON.parse
responseType: 'text', // text 或 arraybuffer
enableHttp2: false,
enableQuic: false,
enableCache: false,
success(res) {
res.statusCode // HTTP 状态码
res.data // 响应数据
res.header // 响应头
},
fail(err) {
err.errMsg // 错误信息
}
})
```
**注意事项**
- 需在小程序管理后台配置合法域名request 合法域名)
- 默认超时 60s可在 `app.json``networkTimeout.request` 配置
- 最大并发限制 10 个
- HTTPS 只支持 TLS 1.2+
- `data` 为 Object 时GET 请求会序列化为 query stringPOST 请求 header 为 `application/json` 时序列化为 JSON`application/x-www-form-urlencoded` 时序列化为 query string
### wx.uploadFile — 上传文件
```javascript
wx.chooseImage({
success(res) {
wx.uploadFile({
url: 'https://example.com/upload',
filePath: res.tempFilePaths[0],
name: 'file',
formData: { user: 'test' },
success(uploadRes) {
console.log(uploadRes.data)
}
})
}
})
```
### wx.downloadFile — 下载文件
```javascript
wx.downloadFile({
url: 'https://example.com/file.pdf',
success(res) {
if (res.statusCode === 200) {
wx.openDocument({ filePath: res.tempFilePath })
}
}
})
```
### WebSocket
```javascript
const socketTask = wx.connectSocket({
url: 'wss://example.com/ws',
header: { 'Authorization': 'Bearer token' }
})
socketTask.onOpen(() => {
socketTask.send({ data: 'hello' })
})
socketTask.onMessage((res) => {
console.log(res.data)
})
socketTask.onClose(() => {})
socketTask.onError((err) => {})
socketTask.close()
```
## 数据缓存
```javascript
// 异步
wx.setStorage({ key: 'key', data: 'value' })
wx.getStorage({
key: 'key',
success(res) { console.log(res.data) }
})
wx.removeStorage({ key: 'key' })
wx.clearStorage()
wx.getStorageInfo({
success(res) {
res.keys // 所有 key
res.currentSize // 当前占用(KB)
res.limitSize // 限制大小(KB)
}
})
// 同步
wx.setStorageSync('key', 'value')
const value = wx.getStorageSync('key')
wx.removeStorageSync('key')
wx.clearStorageSync()
```
**注意**
- 单个 key 上限 1MB
- 总上限 10MB
- 隔离策略:同一小程序不同用户数据隔离
## 界面
### 交互反馈
```javascript
// Toast
wx.showToast({ title: '成功', icon: 'success', duration: 2000 })
wx.showToast({ title: '加载中', icon: 'loading' })
wx.showToast({ title: '自定义图标', icon: 'none' }) // 无图标,可显示两行文字
wx.hideToast()
// Loading
wx.showLoading({ title: '加载中', mask: true })
wx.hideLoading()
// Modal
wx.showModal({
title: '提示',
content: '确定删除?',
confirmText: '确定',
cancelText: '取消',
success(res) {
if (res.confirm) { /* 确定 */ }
else if (res.cancel) { /* 取消 */ }
}
})
// ActionSheet
wx.showActionSheet({
itemList: ['选项A', '选项B', '选项C'],
success(res) {
console.log(res.tapIndex) // 0, 1, 2
}
})
```
### 导航栏
```javascript
wx.setNavigationBarTitle({ title: '新标题' })
wx.setNavigationBarColor({
frontColor: '#ffffff', // 仅支持 #ffffff 和 #000000
backgroundColor: '#ff0000',
animation: { duration: 400, timingFunc: 'easeIn' }
})
wx.showNavigationBarLoading()
wx.hideNavigationBarLoading()
```
### 下拉刷新
```javascript
wx.startPullDownRefresh() // 触发下拉刷新
wx.stopPullDownRefresh() // 停止下拉刷新
```
### 滚动
```javascript
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
```
### TabBar
```javascript
wx.showTabBar()
wx.hideTabBar()
wx.setTabBarBadge({ index: 0, text: '1' })
wx.removeTabBarBadge({ index: 0 })
wx.showTabBarRedDot({ index: 0 })
wx.hideTabBarRedDot({ index: 0 })
wx.setTabBarItem({
index: 0,
text: '新文字',
iconPath: '/images/new-icon.png',
selectedIconPath: '/images/new-icon-active.png'
})
wx.setTabBarStyle({
color: '#000',
selectedColor: '#ff0000',
backgroundColor: '#ffffff'
})
```
## 媒体
### 图片
```javascript
// 选择图片(从相册或拍照)
wx.chooseMedia({
count: 9,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['original', 'compressed'],
success(res) {
res.tempFiles // [{ tempFilePath, size, ... }]
}
})
// 预览图片
wx.previewImage({
current: 'url', // 当前显示图片的链接
urls: ['url1', 'url2', 'url3']
})
// 获取图片信息
wx.getImageInfo({
src: 'path/or/url',
success(res) {
res.width
res.height
res.path
res.orientation
res.type
}
})
// 保存图片到相册
wx.saveImageToPhotosAlbum({
filePath: 'tempFilePath',
success() {}
})
```
## 位置
```javascript
// 获取位置(需要用户授权 scope.userLocation
wx.getLocation({
type: 'gcj02', // wgs84 或 gcj02
success(res) {
res.latitude
res.longitude
res.speed
res.accuracy
}
})
// 打开地图选择位置
wx.chooseLocation({
success(res) {
res.name
res.address
res.latitude
res.longitude
}
})
// 打开内置地图查看位置
wx.openLocation({
latitude: 23.099994,
longitude: 113.324520,
name: '位置名',
address: '详细地址',
scale: 18
})
```
## 文件系统
```javascript
const fs = wx.getFileSystemManager()
// 读文件
fs.readFile({
filePath: `${wx.env.USER_DATA_PATH}/hello.txt`,
encoding: 'utf8',
success(res) { console.log(res.data) }
})
// 写文件
fs.writeFile({
filePath: `${wx.env.USER_DATA_PATH}/hello.txt`,
data: 'some text',
encoding: 'utf8',
success() {}
})
// 其他appendFile, mkdir, rmdir, readdir, stat, unlink, rename, copyFile, access
```
## WXML 节点查询
```javascript
// SelectorQuery
const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect((rect) => {
rect.top // 节点上边界坐标
rect.right
rect.bottom
rect.left
rect.width
rect.height
})
query.selectViewport().scrollOffset((res) => {
res.scrollTop
res.scrollLeft
})
query.exec()
// 在组件中使用
const query = this.createSelectorQuery()
query.select('#the-id').boundingClientRect().exec(callback)
// IntersectionObserver监听元素与视口交叉
const observer = wx.createIntersectionObserver(this, { thresholds: [0, 0.5, 1] })
observer.relativeToViewport({ bottom: 100 })
observer.observe('.target-class', (res) => {
res.intersectionRatio // 交叉比例
res.intersectionRect // 交叉区域
})
// 停止监听
observer.disconnect()
```
## 系统信息
```javascript
// 同步获取(推荐用新 API
const windowInfo = wx.getWindowInfo()
// windowInfo.windowWidth / windowInfo.windowHeight / windowInfo.pixelRatio
// windowInfo.statusBarHeight / windowInfo.safeArea
const appBaseInfo = wx.getAppBaseInfo()
// appBaseInfo.SDKVersion / appBaseInfo.language / appBaseInfo.theme
const deviceInfo = wx.getDeviceInfo()
// deviceInfo.platform / deviceInfo.brand / deviceInfo.model / deviceInfo.system
// 旧 API仍可用但不推荐
const sysInfo = wx.getSystemInfoSync()
```
## 转发分享
```javascript
// 页面中定义 onShareAppMessage 即可开启转发
Page({
onShareAppMessage(res) {
if (res.from === 'button') {
// 来自页面内转发按钮
}
return {
title: '自定义转发标题',
path: '/pages/index/index?id=123',
imageUrl: '/images/share.png'
}
},
onShareTimeline() {
// 分享到朋友圈(基础库 2.11.3+
return {
title: '朋友圈标题',
query: 'id=123',
imageUrl: '/images/share.png'
}
}
})
```
```xml
<!-- 页面内转发按钮 -->
<button open-type="share">转发</button>
```
## 在线查询
API 文档非常庞大,如需查看某个具体 API 的完整参数,可抓取:
- API 总览https://developers.weixin.qq.com/miniprogram/dev/api/
- 具体 API`https://developers.weixin.qq.com/miniprogram/dev/api/{分类}/{api名}.html`
例如https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html

View File

@@ -0,0 +1,362 @@
# 登录与鉴权
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
## 登录流程概览
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 小程序 │ │ 开发者服务器 │ │ 微信服务器 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. wx.login() │ │
│ ──────────────>│ │
│ 返回 code │ │
│ │ │
│ 2. wx.request │ │
│ 发送 code │ │
│ ──────────────>│ │
│ │ 3. code2Session│
│ │ ──────────────>│
│ │ 返回 openid + │
│ │ session_key │
│ │ <──────────────│
│ │ │
│ │ 4. 生成自定义 │
│ │ 登录态 token │
│ │ │
│ 5. 返回 token │ │
│ <──────────────│ │
│ │ │
│ 6. 后续请求 │ │
│ 携带 token │ │
│ ──────────────>│ │
│ │ 7. 校验 token │
│ │ 查 openid │
```
## 前端登录实现
### 基本登录
```javascript
// app.js 或 utils/auth.js
async function login() {
// 1. 调用 wx.login 获取 code
const { code } = await wx.login()
// 2. 发送 code 到后端换取 token
const res = await wx.request({
url: 'https://your-server.com/api/auth/login',
method: 'POST',
data: { code }
})
if (res.data.token) {
// 3. 存储 token
wx.setStorageSync('token', res.data.token)
return res.data
} else {
throw new Error('登录失败')
}
}
```
### 检查登录态
```javascript
// 检查 session_key 是否过期
function checkSession() {
return new Promise((resolve, reject) => {
wx.checkSession({
success: () => resolve(true), // session_key 未过期
fail: () => resolve(false) // session_key 已过期,需重新 login
})
})
}
// 启动时检查
async function checkAndLogin() {
const token = wx.getStorageSync('token')
if (!token) {
return await login()
}
const isSessionValid = await checkSession()
if (!isSessionValid) {
// session_key 过期,重新登录
return await login()
}
return { token }
}
```
### 封装请求(自动携带 token
```typescript
// utils/request.ts
interface RequestOptions {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
data?: any
header?: Record<string, string>
}
function request(options: RequestOptions): Promise<any> {
const token = wx.getStorageSync('token')
return new Promise((resolve, reject) => {
wx.request({
...options,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...options.header
},
success(res) {
if (res.statusCode === 401) {
// token 过期,重新登录
wx.removeStorageSync('token')
login().then(() => {
// 重试原请求
request(options).then(resolve).catch(reject)
})
return
}
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else {
reject(res)
}
},
fail: reject
})
})
}
```
## 后端登录实现Python / FastAPI 示例)
```python
import httpx
from fastapi import APIRouter, HTTPException, Depends
from datetime import datetime, timedelta
import jwt
router = APIRouter()
APPID = "your_appid"
SECRET = "your_secret"
JWT_SECRET = "your_jwt_secret"
@router.post("/auth/login")
async def login(code: str):
# 1. 用 code 换取 openid + session_key
url = "https://api.weixin.qq.com/sns/jscode2session"
params = {
"appid": APPID,
"secret": SECRET,
"js_code": code,
"grant_type": "authorization_code"
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
data = resp.json()
if "errcode" in data and data["errcode"] != 0:
raise HTTPException(400, f"微信登录失败: {data.get('errmsg')}")
openid = data["openid"]
session_key = data["session_key"]
unionid = data.get("unionid")
# 2. 查找或创建用户
user = await find_or_create_user(openid, unionid)
# 3. 生成 JWT token
token = jwt.encode({
"sub": str(user.id),
"openid": openid,
"exp": datetime.utcnow() + timedelta(days=7)
}, JWT_SECRET, algorithm="HS256")
# 4. 缓存 session_key用于后续解密
await cache_session_key(openid, session_key)
return {"token": token, "user": user.to_dict()}
```
## 获取手机号
### 前端
```xml
<!-- 基础库 2.21.2+ 推荐用法 -->
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">
授权手机号
</button>
```
```javascript
Page({
getPhoneNumber(e) {
if (e.detail.errMsg === 'getPhoneNumber:ok') {
const code = e.detail.code // 动态令牌(新版)
// 发送 code 到后端
wx.request({
url: 'https://your-server.com/api/auth/phone',
method: 'POST',
data: { code },
success(res) {
console.log('手机号:', res.data.phoneNumber)
}
})
} else {
console.log('用户拒绝授权')
}
}
})
```
### 后端
```python
@router.post("/auth/phone")
async def get_phone(code: str, token: str = Depends(get_current_token)):
access_token = await get_access_token()
url = f"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}"
async with httpx.AsyncClient() as client:
resp = await client.post(url, json={"code": code})
data = resp.json()
if data.get("errcode") != 0:
raise HTTPException(400, f"获取手机号失败: {data.get('errmsg')}")
phone_info = data["phone_info"]
phone_number = phone_info["phoneNumber"]
# 更新用户手机号
await update_user_phone(token.openid, phone_number)
return {"phoneNumber": phone_number}
```
## 用户信息获取(当前方案)
`wx.getUserProfile` 已于基础库 2.27.1 废弃。当前获取用户头像和昵称的方式:
### 头像昵称填写能力
```xml
<!-- 头像选择 -->
<button open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image src="{{avatarUrl}}" class="avatar"/>
</button>
<!-- 昵称填写type="nickname" 自动弹出微信昵称) -->
<input type="nickname" placeholder="请输入昵称" bindchange="onNicknameChange"/>
```
```javascript
Page({
data: {
avatarUrl: '/images/default-avatar.png',
nickname: ''
},
onChooseAvatar(e) {
const { avatarUrl } = e.detail
// avatarUrl 是临时路径,需上传到自己服务器
this.setData({ avatarUrl })
this.uploadAvatar(avatarUrl)
},
onNicknameChange(e) {
this.setData({ nickname: e.detail.value })
},
async uploadAvatar(tempPath) {
wx.uploadFile({
url: 'https://your-server.com/api/upload/avatar',
filePath: tempPath,
name: 'file',
success(res) {
const data = JSON.parse(res.data)
// 保存头像 URL
}
})
}
})
```
## 授权管理
```javascript
// 查看当前授权状态
wx.getSetting({
success(res) {
res.authSetting['scope.userLocation'] // true/false/undefined
res.authSetting['scope.writePhotosAlbum']
// undefined = 未请求过true = 已授权false = 已拒绝
}
})
// 提前请求授权
wx.authorize({
scope: 'scope.userLocation',
success() { /* 授权成功 */ },
fail() { /* 用户拒绝 */ }
})
// 打开设置页(用户之前拒绝后,引导重新授权)
wx.openSetting({
success(res) {
res.authSetting // 最新的授权状态
}
})
```
### 常用 scope
| scope | 说明 | 对应 API |
|-------|------|----------|
| scope.userLocation | 精确地理位置 | wx.getLocation |
| scope.userFuzzyLocation | 模糊地理位置 | wx.getFuzzyLocation |
| scope.record | 麦克风 | wx.startRecord |
| scope.camera | 摄像头 | camera 组件 |
| scope.writePhotosAlbum | 保存到相册 | wx.saveImageToPhotosAlbum |
| scope.bluetooth | 蓝牙 | wx.openBluetoothAdapter |
| scope.addPhoneContact | 添加到联系人 | wx.addPhoneContact |
| scope.addPhoneCalendar | 添加到日历 | wx.addPhoneCalendar |
| scope.werun | 微信运动步数 | wx.getWeRunData |
| scope.userInfo | 已废弃 | - |
## 安全注意事项
1. **session_key 绝不能下发到前端**
2. **code 只能使用一次**,且有效期很短(约 5 分钟)
3. **不要在前端存储 openid**,通过 token 在后端关联
4. **JWT token 设置合理过期时间**(建议 7 天,配合 refresh token
5. **HTTPS 是强制要求**,所有网络请求必须 HTTPS
6. **敏感数据解密**要在服务端进行,不要在前端解密
7. **access_token 要在服务端缓存**,不要每次请求都重新获取
8. **unionid 需要绑定开放平台**才能获取,用于跨应用用户关联
## 常见问题
### Q: wx.login 的 code 可以多次使用吗?
A: 不可以,每个 code 只能使用一次,且有效期约 5 分钟。
### Q: session_key 什么时候会过期?
A: 微信不会通知过期时间,需要通过 `wx.checkSession` 检查。用户越频繁使用小程序session_key 有效期越长。
### Q: 如何获取 unionid
A: 需要在微信开放平台绑定小程序。绑定后code2Session 会返回 unionid。
### Q: getUserProfile 废弃后怎么获取用户信息?
A: 使用头像昵称填写能力(`<button open-type="chooseAvatar">` + `<input type="nickname">`),让用户主动填写。
## 在线查询
- 登录流程https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
- 手机号快速验证https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
- 用户信息https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html
- 授权https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html

View File

@@ -0,0 +1,302 @@
# 服务端 API
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/api-backend/
服务端 API 是小程序后端服务器调用微信服务器的 HTTP 接口。
## access_token
几乎所有服务端 API 都需要 access_token。
```
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
```
响应:
```json
{
"access_token": "ACCESS_TOKEN",
"expires_in": 7200
}
```
**注意事项**
- 有效期 2 小时,需要定时刷新并缓存
- 每日调用上限有限制
- 多服务器部署时需要中控服务器统一管理 access_token
- 刷新 access_token 会使旧的立即失效
## 登录 — code2Session
```
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
```
响应:
```json
{
"openid": "OPENID",
"session_key": "SESSION_KEY",
"unionid": "UNIONID",
"errcode": 0,
"errmsg": "ok"
}
```
| 参数 | 说明 |
|------|------|
| openid | 用户唯一标识(同一小程序内唯一) |
| session_key | 会话密钥(用于解密用户数据) |
| unionid | 用户在开放平台的唯一标识(需绑定开放平台) |
**安全要求**
- `session_key` 不能下发到前端
- `js_code` 只能使用一次
- 该接口不需要 access_token
## 手机号
### 获取手机号(新版,推荐)
前端通过 `<button open-type="getPhoneNumber">` 获取 code后端用 code 换取手机号:
```
POST https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=ACCESS_TOKEN
{
"code": "动态令牌code"
}
```
响应:
```json
{
"errcode": 0,
"errmsg": "ok",
"phone_info": {
"phoneNumber": "13800138000",
"purePhoneNumber": "13800138000",
"countryCode": "86",
"watermark": {
"timestamp": 1637744274,
"appid": "APPID"
}
}
}
```
### 旧版解密方式(不推荐)
使用 session_key + iv 解密 encryptedData获取手机号。
## 小程序码
### 获取不限制的小程序码(推荐)
```
POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
{
"scene": "id=123",
"page": "pages/index/index",
"check_path": true,
"env_version": "release",
"width": 430,
"auto_color": false,
"line_color": {"r": 0, "g": 0, "b": 0},
"is_hyaline": false
}
```
- `scene` 最大 32 个可见字符
- 返回二进制图片数据Content-Type: image/jpeg 或 image/png
- 数量不限制
### 获取小程序码(有限制)
```
POST https://api.weixin.qq.com/wxa/getwxacode?access_token=ACCESS_TOKEN
{
"path": "pages/index/index?id=123",
"width": 430
}
```
- `path` 可带参数,最大 128 字节
- 总数限制 10 万个
### 获取小程序二维码(有限制)
```
POST https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=ACCESS_TOKEN
{
"path": "pages/index/index?id=123",
"width": 430
}
```
- 总数限制 10 万个
## 订阅消息
### 发送订阅消息
```
POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN
{
"touser": "OPENID",
"template_id": "TEMPLATE_ID",
"page": "pages/index/index",
"miniprogram_state": "formal",
"lang": "zh_CN",
"data": {
"thing1": { "value": "订单已发货" },
"time2": { "value": "2025-01-01 12:00" },
"character_string3": { "value": "SF1234567890" }
}
}
```
**前端需先请求用户授权**
```javascript
wx.requestSubscribeMessage({
tmplIds: ['TEMPLATE_ID'],
success(res) {
// res[TEMPLATE_ID] === 'accept' 表示用户同意
}
})
```
## 客服消息
### 发送客服消息
```
POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
// 文本消息
{
"touser": "OPENID",
"msgtype": "text",
"text": { "content": "Hello" }
}
// 图片消息
{
"touser": "OPENID",
"msgtype": "image",
"image": { "media_id": "MEDIA_ID" }
}
// 小程序卡片
{
"touser": "OPENID",
"msgtype": "miniprogrampage",
"miniprogrampage": {
"title": "标题",
"pagepath": "pages/index/index",
"thumb_media_id": "MEDIA_ID"
}
}
```
## 内容安全
### 文本内容安全检测
```
POST https://api.weixin.qq.com/wxa/msg_sec_check?access_token=ACCESS_TOKEN
{
"content": "待检测文本",
"version": 2,
"scene": 1,
"openid": "OPENID"
}
```
scene 值1-社交日志 2-评论 3-论坛 4-社交日志
### 图片内容安全检测
```
POST https://api.weixin.qq.com/wxa/img_sec_check?access_token=ACCESS_TOKEN
// multipart/form-data 上传图片
```
## 数据分析
```
// 获取日访问数据
POST https://api.weixin.qq.com/datacube/getweanalysisappiddailyvisittrend?access_token=ACCESS_TOKEN
{ "begin_date": "20250101", "end_date": "20250101" }
// 获取用户画像
POST https://api.weixin.qq.com/datacube/getweanalysisappiduserportrait?access_token=ACCESS_TOKEN
{ "begin_date": "20250101", "end_date": "20250107" }
```
## URL Scheme / URL Link
### 生成 URL Scheme用于短信/邮件等外部跳转)
```
POST https://api.weixin.qq.com/wxa/generatescheme?access_token=ACCESS_TOKEN
{
"jump_wxa": {
"path": "pages/index/index",
"query": "id=123",
"env_version": "release"
},
"is_expire": true,
"expire_type": 0,
"expire_time": 1672502400
}
```
### 生成 URL Link用于短信/邮件等外部跳转)
```
POST https://api.weixin.qq.com/wxa/generate_urllink?access_token=ACCESS_TOKEN
{
"path": "pages/index/index",
"query": "id=123",
"is_expire": true,
"expire_type": 0,
"expire_time": 1672502400,
"env_version": "release"
}
```
## 常见错误码
| errcode | 说明 |
|---------|------|
| -1 | 系统繁忙 |
| 0 | 请求成功 |
| 40001 | access_token 无效或过期 |
| 40013 | 不合法的 AppID |
| 40029 | 不合法的 code已使用或过期 |
| 40125 | 不合法的 appsecret |
| 41002 | 缺少 appid |
| 41004 | 缺少 appsecret |
| 42001 | access_token 过期 |
| 45009 | 调用超过频率限制 |
| 45011 | API 调用太频繁 |
| 48001 | API 未授权 |
| 61024 | 该 code 已被使用 |
## 在线查询
如需查看某个具体服务端 API 的完整参数,可抓取:
- 服务端 API 总览https://developers.weixin.qq.com/miniprogram/dev/api-backend/
- 登录https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
- 手机号https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html
- 小程序码https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html
- 订阅消息https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html

View File

@@ -0,0 +1,511 @@
# TDesign 小程序组件库
> 官方文档https://tdesign.tencent.com/miniprogram/overview
> GitHubhttps://github.com/Tencent/tdesign-miniprogram
TDesign 是腾讯出品的企业级设计体系,提供微信小程序组件库,包含 60+ 高质量组件。
## 安装
```bash
npm i tdesign-miniprogram -S --production
```
安装后在微信开发者工具中构建 npm`工具 → 构建 npm`
构建时若出现 `NPM packages not found`,在 `project.config.json` 补充:
```json
{
"setting": {
"packNpmManually": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./miniprogram/"
}
]
}
}
```
构建成功后勾选 `将 JS 编译成 ES5`
## 必要配置
### 移除 style: v2
`app.json` 中的 `"style": "v2"` 移除,否则会导致 TDesign 组件样式错乱。
### TypeScript 配置
如果使用 TypeScript 开发,修改 `tsconfig.json`
```json
{
"compilerOptions": {
"paths": {
"tdesign-miniprogram/*": ["./miniprogram/miniprogram_npm/tdesign-miniprogram/*"]
}
}
}
```
### 最低基础库版本
`^2.12.0`
## 使用组件
### 引入
在页面或组件的 `.json` 中注册:
```json
{
"usingComponents": {
"t-button": "tdesign-miniprogram/button/button"
}
}
```
全局引入则在 `app.json` 中配置 `usingComponents`
### 使用
```xml
<t-button theme="primary">按钮</t-button>
```
### 引入路径规则
所有组件路径格式:`tdesign-miniprogram/{组件名}/{组件名}`
```json
{
"usingComponents": {
"t-button": "tdesign-miniprogram/button/button",
"t-input": "tdesign-miniprogram/input/input",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-toast": "tdesign-miniprogram/toast/toast",
"t-navbar": "tdesign-miniprogram/navbar/navbar",
"t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
"t-popup": "tdesign-miniprogram/popup/popup",
"t-picker": "tdesign-miniprogram/picker/picker",
"t-picker-item": "tdesign-miniprogram/picker-item/picker-item",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-avatar": "tdesign-miniprogram/avatar/avatar",
"t-badge": "tdesign-miniprogram/badge/badge",
"t-search": "tdesign-miniprogram/search/search",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell",
"t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
"t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item"
}
}
```
## 完整组件列表
### 基础6
| 组件 | 标签 | 说明 |
|------|------|------|
| Button 按钮 | `t-button` | 主按钮、次按钮、文字按钮、图标按钮 |
| Divider 分割线 | `t-divider` | 内容分隔 |
| Fab 悬浮按钮 | `t-fab` | 浮动操作按钮 |
| Icon 图标 | `t-icon` | 内置图标库 |
| Layout 布局 | `t-row` / `t-col` | 栅格布局 |
| Link 链接 | `t-link` | 文字链接 |
### 导航8
| 组件 | 标签 | 说明 |
|------|------|------|
| BackTop 返回顶部 | `t-back-top` | 长页面返回顶部 |
| Drawer 抽屉 | `t-drawer` | 侧边滑出面板 |
| Indexes 索引 | `t-indexes` | 字母索引列表 |
| Navbar 导航条 | `t-navbar` | 自定义顶部导航栏 |
| SideBar 侧边导航栏 | `t-side-bar` / `t-side-bar-item` | 侧边分类导航 |
| Steps 步骤条 | `t-steps` / `t-step-item` | 流程步骤展示 |
| TabBar 底部标签栏 | `t-tab-bar` / `t-tab-bar-item` | 底部导航 |
| Tabs 选项卡 | `t-tabs` / `t-tab-panel` | 顶部选项卡切换 |
### 输入16
| 组件 | 标签 | 说明 |
|------|------|------|
| Calendar 日历 | `t-calendar` | 日期选择 |
| Cascader 级联选择器 | `t-cascader` | 多级联动选择 |
| CheckBox 多选框 | `t-checkbox` / `t-checkbox-group` | 多选 |
| ColorPicker 颜色选择器 | `t-color-picker` | 颜色选取 |
| DateTimePicker 日期选择器 | `t-date-time-picker` | 日期时间选择 |
| Input 输入框 | `t-input` | 文本输入 |
| Picker 选择器 | `t-picker` / `t-picker-item` | 滚动选择 |
| Radio 单选框 | `t-radio` / `t-radio-group` | 单选 |
| Rate 评分 | `t-rate` | 星级评分 |
| Search 搜索框 | `t-search` | 搜索输入 |
| Slider 滑动选择器 | `t-slider` | 滑块选择 |
| Stepper 步进器 | `t-stepper` | 数量加减 |
| Switch 开关 | `t-switch` | 开关切换 |
| Textarea 多行文本框 | `t-textarea` | 多行输入 |
| TreeSelect 树形选择器 | `t-tree-select` | 树形多级选择 |
| Upload 上传 | `t-upload` | 文件/图片上传 |
### 数据展示18
| 组件 | 标签 | 说明 |
|------|------|------|
| Avatar 头像 | `t-avatar` / `t-avatar-group` | 用户头像 |
| Badge 徽章 | `t-badge` | 消息提示红点/数字 |
| Cell 单元格 | `t-cell` / `t-cell-group` | 列表项 |
| Collapse 折叠面板 | `t-collapse` / `t-collapse-panel` | 可展开/收起内容 |
| CountDown 倒计时 | `t-count-down` | 倒计时显示 |
| Empty 空状态 | `t-empty` | 无数据提示 |
| Footer 页脚 | `t-footer` | 页面底部信息 |
| Grid 宫格 | `t-grid` / `t-grid-item` | 宫格布局 |
| Image 图片 | `t-image` | 增强图片(懒加载、加载状态) |
| ImageViewer 图片预览 | `t-image-viewer` | 图片放大预览 |
| Progress 进度条 | `t-progress` | 进度展示 |
| QRCode 二维码 | `t-qrcode` | 二维码生成 |
| Result 结果 | `t-result` | 操作结果反馈 |
| Skeleton 骨架屏 | `t-skeleton` | 加载占位 |
| Sticky 吸顶容器 | `t-sticky` | 滚动吸顶 |
| Swiper 轮播图 | `t-swiper` / `t-swiper-nav` | 轮播展示 |
| Tag 标签 | `t-tag` / `t-check-tag` | 标签展示/可选标签 |
| Watermark 水印 | `t-watermark` | 页面水印 |
### 反馈13
| 组件 | 标签 | 说明 |
|------|------|------|
| ActionSheet 动作面板 | `t-action-sheet` | 底部弹出操作列表 |
| Dialog 对话框 | `t-dialog` | 模态对话框 |
| DropdownMenu 下拉菜单 | `t-dropdown-menu` / `t-dropdown-item` | 下拉筛选 |
| Guide 引导 | `t-guide` | 新手引导 |
| Loading 加载 | `t-loading` | 加载中状态 |
| Message 全局提示 | `t-message` | 顶部消息提示 |
| NoticeBar 消息提醒 | `t-notice-bar` | 通知栏 |
| Overlay 遮罩层 | `t-overlay` | 背景遮罩 |
| Popover 弹出气泡 | `t-popover` | 气泡提示/菜单 |
| Popup 弹出层 | `t-popup` | 通用弹出层 |
| PullDownRefresh 下拉刷新 | `t-pull-down-refresh` | 下拉刷新 |
| SwipeCell 滑动操作 | `t-swipe-cell` | 左右滑动操作 |
| Toast 轻提示 | `t-toast` | 轻量提示 |
## 常用组件用法示例
### Button 按钮
```xml
<!-- 主题 -->
<t-button theme="primary">主按钮</t-button>
<t-button theme="default">次按钮</t-button>
<t-button theme="danger">危险按钮</t-button>
<t-button theme="light">浅色按钮</t-button>
<!-- 变体 -->
<t-button variant="base">填充</t-button>
<t-button variant="outline">描边</t-button>
<t-button variant="dashed">虚框</t-button>
<t-button variant="text">文字</t-button>
<!-- 尺寸 -->
<t-button size="large">大按钮</t-button>
<t-button size="medium">中按钮</t-button>
<t-button size="small">小按钮</t-button>
<t-button size="extra-small">超小按钮</t-button>
<!-- 块级 -->
<t-button block theme="primary">块级按钮</t-button>
<!-- 图标按钮 -->
<t-button theme="primary" icon="app">带图标</t-button>
<!-- 加载状态 -->
<t-button theme="primary" loading>加载中</t-button>
<!-- 禁用 -->
<t-button theme="primary" disabled>禁用</t-button>
```
### Input 输入框
```xml
<t-input
label="标签"
placeholder="请输入"
value="{{value}}"
bind:change="onChange"
maxlength="20"
type="text"
clearable
/>
<!-- 带前缀图标 -->
<t-input
prefixIcon="search"
placeholder="搜索"
bind:change="onSearch"
/>
<!-- 密码输入 -->
<t-input
label="密码"
type="password"
placeholder="请输入密码"
clearable
/>
```
### Cell 单元格
```xml
<t-cell-group>
<t-cell title="单行标题" arrow />
<t-cell title="单行标题" note="辅助信息" arrow />
<t-cell title="单行标题" description="描述信息" arrow />
<t-cell title="单行标题" left-icon="user" arrow />
</t-cell-group>
```
### Dialog 对话框
```xml
<t-dialog
visible="{{showDialog}}"
title="对话框标题"
content="对话框内容"
confirm-btn="确认"
cancel-btn="取消"
bind:confirm="onConfirm"
bind:cancel="onCancel"
/>
```
```javascript
// 命令式调用
const dialog = this.selectComponent('#t-dialog')
dialog.open()
// 或
dialog.close()
```
### Toast 轻提示
```xml
<t-toast id="t-toast" />
```
```javascript
import Toast from 'tdesign-miniprogram/toast/index'
Toast({
context: this,
selector: '#t-toast',
message: '提示信息',
theme: 'success', // success / warning / error / loading
duration: 2000
})
```
### Popup 弹出层
```xml
<t-popup visible="{{visible}}" placement="bottom" bind:visible-change="onVisibleChange">
<view class="popup-content">弹出内容</view>
</t-popup>
```
### Tabs 选项卡
```xml
<t-tabs defaultValue="{{0}}" bind:change="onTabChange">
<t-tab-panel label="标签1" value="0">内容1</t-tab-panel>
<t-tab-panel label="标签2" value="1">内容2</t-tab-panel>
<t-tab-panel label="标签3" value="2">内容3</t-tab-panel>
</t-tabs>
```
### Navbar 导航条
```xml
<t-navbar
title="页面标题"
left-arrow
bind:go-back="onGoBack"
/>
<!-- 自定义导航栏(需在 page.json 设置 navigationStyle: custom -->
<t-navbar title="自定义" left-arrow fixed>
<view slot="capsule">胶囊区域</view>
</t-navbar>
```
### TabBar 底部标签栏
```xml
<t-tab-bar value="{{activeTab}}" bind:change="onTabBarChange">
<t-tab-bar-item value="home" icon="home">首页</t-tab-bar-item>
<t-tab-bar-item value="user" icon="user">我的</t-tab-bar-item>
</t-tab-bar>
```
### Search 搜索框
```xml
<t-search
placeholder="搜索"
value="{{searchValue}}"
bind:change="onSearchChange"
bind:submit="onSearchSubmit"
bind:clear="onSearchClear"
/>
```
### Empty 空状态
```xml
<t-empty icon="folder-open" description="暂无数据" />
```
### Loading 加载
```xml
<t-loading theme="circular" size="40rpx" text="加载中..." />
```
### Skeleton 骨架屏
```xml
<t-skeleton loading="{{loading}}" row-col="{{rowCol}}">
<view>实际内容</view>
</t-skeleton>
```
```javascript
data: {
loading: true,
rowCol: [
{ width: '100%', height: '340rpx' },
[{ width: '45%' }, { width: '45%' }],
{ width: '100%' },
{ width: '60%' }
]
}
```
## 样式覆盖4 种方式)
### 1. style / custom-style 属性
```xml
<t-button style="color: red">按钮</t-button>
<t-button custom-style="color: red">按钮</t-button>
```
开启 virtualHost 时两者效果一致;未开启时只能用 `custom-style`
### 2. 解除样式隔离
TDesign 全体组件开启了 `addGlobalClass`,页面样式可直接覆盖:
```css
/* 页面 wxss */
.t-button--primary {
background-color: navy;
}
```
在自定义组件中使用需开启 `styleIsolation: 'shared'`
### 3. 外部样式类
```xml
<t-button t-class="my-btn-class">按钮</t-button>
```
```css
.my-btn-class {
color: red !important;
}
```
每个组件支持的外部样式类见组件文档(如 `t-class``t-class-icon``t-class-content` 等)。
### 4. CSS 变量
```css
page {
--td-brand-color: navy; /* 主题色 */
--td-success-color: #00a870; /* 成功色 */
--td-warning-color: #ed7b2f; /* 警告色 */
--td-error-color: #e34d59; /* 错误色 */
}
```
每个组件都有独立的 CSS 变量,见组件文档的 CSS Variables 部分。
## 自定义主题
全局 Design Token 变量定义:[_variables.less](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/common/style/_variables.less)
```css
/* app.wxss — 全局主题定制 */
page {
--td-brand-color: #0052d9;
--td-brand-color-light: #d9e1ff;
--td-success-color: #00a870;
--td-warning-color: #ed7b2f;
--td-error-color: #e34d59;
/* 文字颜色 */
--td-text-color-primary: rgba(0, 0, 0, 0.9);
--td-text-color-secondary: rgba(0, 0, 0, 0.6);
--td-text-color-placeholder: rgba(0, 0, 0, 0.26);
--td-text-color-disabled: rgba(0, 0, 0, 0.26);
/* 背景颜色 */
--td-bg-color-container: #fff;
--td-bg-color-page: #f3f3f3;
/* 圆角 */
--td-radius-default: 12rpx;
--td-radius-large: 18rpx;
--td-radius-round: 999px;
/* 字体 */
--td-font-size-s: 24rpx;
--td-font-size-base: 28rpx;
--td-font-size-m: 32rpx;
--td-font-size-l: 36rpx;
}
```
## 深色模式
TDesign 1.3.0+ 支持深色模式。
### 开启步骤
1. `app.json` 添加 `"darkmode": true`
2. `app.wxss` 引入主题变量:
```css
@import 'miniprogram_npm/tdesign-miniprogram/common/style/theme/_index.wxss';
```
3. 页面样式使用 CSS Variable
```css
.text {
color: var(--td-text-color-secondary);
}
```
### 特殊组件适配
自定义 TabBar 和 root-portal 内的组件需手动添加 `.page` 类名:
```xml
<view class="page">
<t-tab-bar />
</view>
```
## 在线查询
如需查看某个具体组件的完整 APIProps / Events / Slots / CSS Variables可抓取
- 组件总览https://tdesign.tencent.com/miniprogram/overview
- 具体组件:`https://tdesign.tencent.com/miniprogram/components/{组件名}`
例如https://tdesign.tencent.com/miniprogram/components/button
- 快速开始https://tdesign.tencent.com/miniprogram/getting-started
- 样式覆盖https://tdesign.tencent.com/miniprogram/custom-style
- 自定义主题https://tdesign.tencent.com/miniprogram/custom-theme
- 深色模式https://tdesign.tencent.com/miniprogram/dark-mode
- CSS 变量定义https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/common/style/_variables.less

View File

@@ -0,0 +1,308 @@
# 视图层WXML / WXSS / WXS
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/view/
## WXML — 模板语法
### 数据绑定
```xml
<!-- 简单绑定 -->
<view>{{ message }}</view>
<!-- 组件属性绑定(需在双引号内) -->
<view id="item-{{id}}"></view>
<!-- 控制属性绑定 -->
<view wx:if="{{condition}}"></view>
<!-- 关键字绑定(注意 true/false 需要在 {{}} 内) -->
<checkbox checked="{{false}}"></checkbox>
<!-- ⚠️ 错误写法checked="false" 会被当作字符串 "false",结果为 true -->
<!-- 运算 -->
<view>{{ a + b }} + {{ c }} + d</view>
<view>{{"hello " + name}}</view>
<view>{{object.key}} {{array[0]}}</view>
<!-- 三元运算 -->
<view hidden="{{flag ? true : false}}">Hidden</view>
<!-- 组合(数组) -->
<view wx:for="{{[zero, 1, 2, 3, 4]}}">{{item}}</view>
<!-- 组合(对象) -->
<template is="objectCombine" data="{{for: a, bar: b}}"></template>
<!-- 展开运算符 -->
<template is="objectCombine" data="{{...obj1, ...obj2, e: 5}}"></template>
```
### 列表渲染 wx:for
```xml
<!-- 基本用法:默认 item 和 index -->
<view wx:for="{{array}}">
{{index}}: {{item.message}}
</view>
<!-- 自定义变量名 -->
<view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">
{{idx}}: {{itemName.message}}
</view>
<!-- 嵌套 -->
<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="i">
<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="j">
<view wx:if="{{i <= j}}">
{{i}} * {{j}} = {{i * j}}
</view>
</view>
</view>
<!-- ⚠️ wx:key 非常重要,用于列表项的唯一标识 -->
<!-- 值为字符串item 的某个 property 名(该 property 值需唯一) -->
<switch wx:for="{{objectArray}}" wx:key="unique">{{item.id}}</switch>
<!-- 值为 *thisitem 本身是唯一字符串或数字 -->
<switch wx:for="{{numberArray}}" wx:key="*this">{{item}}</switch>
```
**wx:key 的作用**:当数据改变触发重新渲染时,带有 key 的组件会被重新排序而非重新创建,保持自身状态(如 `<input>` 的输入内容、`<switch>` 的选中状态)。
### 条件渲染
```xml
<!-- wx:if / wx:elif / wx:else -->
<view wx:if="{{length > 5}}">1</view>
<view wx:elif="{{length > 2}}">2</view>
<view wx:else>3</view>
<!-- block wx:if不会渲染为真实 DOM -->
<block wx:if="{{true}}">
<view>view1</view>
<view>view2</view>
</block>
```
**wx:if vs hidden**
- `wx:if` 是惰性的,条件为 false 时不渲染,切换时销毁/重建
- `hidden` 始终渲染,只是切换显示/隐藏(类似 CSS display:none
- 频繁切换用 `hidden`,运行时条件不大可能改变用 `wx:if`
### 模板 template
```xml
<!-- 定义模板 -->
<template name="msgItem">
<view>
<text>{{index}}: {{msg}}</text>
<text>Time: {{time}}</text>
</view>
</template>
<!-- 使用模板 -->
<template is="msgItem" data="{{...item}}"/>
<!-- 动态模板名 -->
<template is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
```
### 引用
```xml
<!-- import引入目标文件中定义的 template -->
<import src="item.wxml"/>
<template is="item" data="{{text: 'forbar'}}"/>
<!-- ⚠️ import 有作用域:只会引入目标文件中定义的 template不会引入目标文件 import 的 template不递归 -->
<!-- include将目标文件除 <template/> <wxs/> 外的整个代码引入 -->
<include src="header.wxml"/>
<view>body</view>
<include src="footer.wxml"/>
```
## WXSS — 样式
### rpx 单位
rpxresponsive pixel可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx。
| 设备 | rpx 换算 px | px 换算 rpx |
|------|------------|------------|
| iPhone5 | 1rpx = 0.42px | 1px = 2.34rpx |
| iPhone6 | 1rpx = 0.5px | 1px = 2rpx |
| iPhone6 Plus | 1rpx = 0.552px | 1px = 1.81rpx |
**建议**:开发时以 iPhone6 为视觉稿标准750px 宽1px = 1rpx。
### 样式导入
```css
/* common.wxss */
.small-p { padding: 5px; }
/* app.wxss */
@import "common.wxss";
.middle-p { padding: 15px; }
```
### 选择器支持
| 选择器 | 示例 | 说明 |
|--------|------|------|
| .class | `.intro` | 类选择器 |
| #id | `#firstname` | ID 选择器 |
| element | `view` | 元素选择器 |
| element, element | `view, checkbox` | 群组选择器 |
| ::after | `view::after` | 伪元素 |
| ::before | `view::before` | 伪元素 |
### 内联样式
```xml
<!-- style动态样式运行时解析尽量避免静态样式写在 style 中 -->
<view style="color:{{color}};"/>
<!-- class静态样式写在 class 中 -->
<view class="normal_view"/>
```
### 全局样式与局部样式
- `app.wxss` 为全局样式,作用于每个页面
- 页面的 `.wxss` 只对当前页面生效,会覆盖 `app.wxss` 中相同的选择器
## WXSWeiXin Script
WXS 是小程序的一套脚本语言,可以在 WXML 中使用。**WXS 运行在视图层**,比 JS 逻辑层快(不需要跨线程通信)。
```xml
<!-- 内联 WXS -->
<wxs module="m1">
var msg = "hello world";
module.exports.message = msg;
</wxs>
<view>{{m1.message}}</view>
<!-- 外部 WXS 文件 -->
<wxs src="./tools.wxs" module="tools"/>
<view>{{tools.msg}}</view>
<view>{{tools.bar(tools.FOO)}}</view>
```
```javascript
// tools.wxs
var foo = "'hello world' from tools.wxs"
var bar = function(d) {
return d
}
module.exports = {
FOO: foo,
bar: bar,
}
// ⚠️ WXS 不支持 ES6 语法箭头函数、let/const、解构等
// ⚠️ WXS 中不能调用小程序 APIwx.xxx
```
**WXS 典型用途**
- 格式化数据(日期、金额、文本截断)
- 在视图层做简单计算,避免 setData 开销
- 响应事件WXS 事件响应iOS 上性能更好)
## 事件系统
### 事件分类
| 类型 | 说明 | 示例 |
|------|------|------|
| 冒泡事件 | 向父节点传递 | tap, longpress, touchstart, touchmove, touchend, touchcancel |
| 非冒泡事件 | 不向父节点传递 | submit, input, scroll 等组件特有事件 |
### 事件绑定
```xml
<!-- bind不阻止冒泡 -->
<view bindtap="handleTap">Click me</view>
<view bind:tap="handleTap">Click me</view>
<!-- catch阻止冒泡 -->
<view catchtap="handleTap">Click me</view>
<!-- mut-bind互斥事件绑定基础库 2.8.2+ -->
<view mut-bind:tap="handleTap">
<button mut-bind:tap="handleButtonTap">按钮</button>
</view>
<!-- 同一冒泡路径上的 mut-bind 只会有一个被触发 -->
<!-- capture-bind捕获阶段绑定 -->
<view capture-bind:tap="handleCapture">
<view bindtap="handleTap">inner</view>
</view>
<!-- capture-catch捕获阶段中断 -->
<view capture-catch:tap="handleCapture">
<view bindtap="handleTap">inner不会触发</view>
</view>
```
### 事件对象
```javascript
Page({
handleTap(e) {
e.type // 事件类型,如 "tap"
e.timeStamp // 事件生成时的时间戳
e.target // 触发事件的源组件(可能是子组件)
e.currentTarget // 事件绑定的当前组件
e.detail // 额外信息,如 tap 的 { x, y }
e.touches // 触摸事件的触摸点信息数组
e.changedTouches // 变化的触摸点信息数组
e.mark // 事件标记(基础库 2.7.1+
// target 和 currentTarget 的区别
e.target.id // 触发事件的组件 id
e.target.dataset // 触发事件的组件的 data-xxx 属性集合
e.currentTarget.id // 绑定事件的组件 id
e.currentTarget.dataset
}
})
```
### dataset
```xml
<!-- data-xxx 属性传递数据 -->
<view data-alpha-beta="1" data-alphaBeta="2" bindtap="handleTap">
Click
</view>
```
```javascript
handleTap(e) {
e.currentTarget.dataset.alphaBeta // "1"(连字符转驼峰)
e.currentTarget.dataset.alphabeta // "2"(大写转小写)
}
```
### mark基础库 2.7.1+
```xml
<!-- mark 可以在冒泡路径上所有节点收集 -->
<view mark:myMark="last" bindtap="bindViewTap">
<button mark:anotherMark="leaf" bindtap="bindButtonTap">按钮</button>
</view>
```
```javascript
bindButtonTap(e) {
e.mark // { myMark: "last", anotherMark: "leaf" }
// mark 会合并冒泡路径上所有的 mark
}
```
## 在线查询
如需更详细信息,可抓取:
- WXML 语法https://developers.weixin.qq.com/miniprogram/dev/reference/wxml/
- WXSShttps://developers.weixin.qq.com/miniprogram/dev/framework/view/wxss.html
- WXShttps://developers.weixin.qq.com/miniprogram/dev/reference/wxs/
- 事件https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html

View File

@@ -0,0 +1,29 @@
---
inclusion: manual
---
# 微信开发者工具 MCP 连接规范
使用 `weixin-devtools-mcp` 操作小程序时,必须遵循以下规则:
## 连接方式
1. 启动自动化端口:
```powershell
& "C:\dev\WechatDevtools\cli.bat" auto --project "C:\NeoZQYY\apps\miniprogram" --auto-port 9420
```
2. AI 使用 `connect_devtools` 时,只能用 `wsEndpoint` 策略:
- `strategy`: `wsEndpoint`
- `wsEndpoint`: `ws://127.0.0.1:9420`
- `projectPath`: `C:\NeoZQYY\apps\miniprogram`
## 禁止事项
- 禁止使用 `connect`、`discover`、`auto`、`launch` 策略(会导致开发者工具重启)
- 禁止在未确认自动化端口已启动的情况下调用 `connect_devtools`
- 如果连接失败,提示用户检查终端中自动化端口是否在运行,不要自行重试其他策略
## 详细文档
参见 `docs/mcp/WEIXIN-DEVTOOLS-MCP.md`

View File

@@ -0,0 +1,116 @@
---
name: "weixin-devtools"
displayName: "微信开发者工具自动化"
description: "通过 weixin-devtools-mcp 控制微信开发者工具支持页面导航、元素快照、截图、表单操作、JS 执行、网络监控、Console 日志等小程序自动化操作"
keywords: ["微信开发者工具", "小程序调试", "devtools", "wechat", "miniprogram", "截图", "screenshot", "页面快照", "snapshot", "元素点击", "click", "表单", "form", "navigate", "console", "网络请求", "network", "evaluate", "自动化测试", "UI验证"]
author: "NeoZQYY"
---
# 微信开发者工具自动化 Power
通过 weixin-devtools-mcp 控制微信开发者工具,用于小程序 UI 调试、页面验证、自动化测试。
## 可用 Server
### weixin-devtools
连接本地微信开发者工具实例,提供完整的小程序自动化能力。
前置条件:
- 微信开发者工具已打开并加载项目
- 自动化端口 9420 已启用(设置 → 安全 → 服务端口)
## 工具分类速查
### 连接管理
| 工具 | 说明 |
|------|------|
| `connect_devtools` | 连接开发者工具(支持 auto/launch/connect 策略) |
| `disconnect_devtools` | 断开连接并清理状态 |
| `reconnect_devtools` | 重连(可复用上次参数) |
| `get_connection_status` | 查看连接状态 |
| `check_environment` | 检查自动化环境配置 |
| `diagnose_connection` | 诊断连接问题 |
| `debug_connection_flow` | 调试连接流程 |
### 页面导航
| 工具 | 说明 |
|------|------|
| `navigate_to` | 跳转到指定页面(支持参数) |
| `navigate_back` | 返回上一页 |
| `switch_tab` | 切换 TabBar 页面 |
| `relaunch` | 重启小程序并跳转 |
| `get_current_page` | 获取当前页面信息 |
### 元素操作
| 工具 | 说明 |
|------|------|
| `get_page_snapshot` | 获取页面元素快照(含 uid |
| `$` | CSS 选择器查找元素 |
| `click` | 点击元素(支持双击) |
| `input_text` | 输入文本 |
| `set_form_control` | 设置表单控件值picker/switch/slider |
| `get_value` | 获取元素值或文本 |
| `debug_page_elements` | 调试元素获取问题 |
### 断言验证
| 工具 | 说明 |
|------|------|
| `assert_text` | 断言元素文本(精确/包含/正则) |
| `assert_state` | 断言元素状态(可见/启用/选中/聚焦) |
| `assert_attribute` | 断言元素属性值 |
### JS 执行
| 工具 | 说明 |
|------|------|
| `evaluate_script` | 在 AppService 上下文执行 JS |
### 监控与调试
| 工具 | 说明 |
|------|------|
| `screenshot` | 页面截图base64 或保存文件) |
| `list_console_messages` | 列表查询 console 消息 |
| `get_console_message` | 获取单条 console 详情 |
| `get_network_requests` | 获取网络请求记录 |
| `clear_network_requests` | 清空网络请求记录 |
| `stop_network_monitoring` | 停止网络监听 |
### 等待
| 工具 | 说明 |
|------|------|
| `waitFor` | 等待条件满足(元素出现/消失/文本匹配/延时) |
## 典型工作流
### 1. 连接 → 导航 → 验证
```
connect_devtools → navigate_to → get_page_snapshot → assert_text
```
### 2. 表单填写 → 提交 → 检查结果
```
navigate_to → get_page_snapshot → input_text → click → waitFor → assert_text
```
### 3. 截图对比(配合 pixel-audit
```
navigate_to → screenshot → 用 image-compare 对比 H5 截图
```
### 4. 数据验证
```
evaluate_script读取页面 data→ 对比预期值
```
## Steering
| 文件 | 内容 |
|------|------|
| `workflow.md` | 连接策略、页面导航模式、元素操作最佳实践、常见问题排查 |
加载:激活 Power → `readSteering("workflow.md")`
## 注意事项
- 操作元素前必须先 `get_page_snapshot` 获取 uid
- `navigate_to` 不能跳转 TabBar 页面,用 `switch_tab`
- `evaluate_script` 在 AppService 上下文执行,可访问 `wx``getApp()``getCurrentPages()`
- 截图默认返回 base64`path` 参数可保存到文件

View File

@@ -0,0 +1,18 @@
{
"mcpServers": {
"weixin-devtools": {
"command": "npx",
"args": [
"-y",
"weixin-devtools-mcp",
"--tools-profile=full",
"--ws-endpoint=ws://127.0.0.1:9420"
],
"env": {
"WECHAT_DEVTOOLS_CLI": "C:\\dev\\WechatDevtools\\cli.bat",
"WECHAT_DEVTOOLS_PROJECT": "C:\\NeoZQYY\\apps\\miniprogram"
},
"autoApprove": ["*"]
}
}
}

View File

@@ -0,0 +1,179 @@
# 微信开发者工具自动化工作流
## 连接策略
### 首次连接
```
connect_devtoolsstrategy: "auto"
```
auto 策略会依次尝试:发现已运行实例 → 通过 CLI 启动 → ws 直连。
### 连接失败排查
1. `check_environment` — 检查 CLI 路径、项目路径是否正确
2. `diagnose_connection` — 检查端口、进程状态
3. 确认开发者工具「设置 → 安全 → 服务端口」已开启
### 重连
```
reconnect_devtools // 复用上次参数
```
## 页面导航模式
### 普通页面跳转
```
navigate_to(url: "/pages/task/task-list/index", params: { status: "pending" })
```
### TabBar 页面
```
switch_tab(url: "/pages/index/index")
```
注意:`navigate_to` 无法跳转 TabBar 页面,必须用 `switch_tab`
### 重启并跳转
```
relaunch(url: "/pages/index/index")
```
清空页面栈,适合需要干净状态的场景。
### 返回
```
navigate_back(delta: 1) // 返回上一页
navigate_back(delta: 2) // 返回两层
```
## 元素操作最佳实践
### 操作流程(必须遵循)
1. `get_page_snapshot` 获取当前页面所有元素的 uid
2. 从快照中找到目标元素的 uid
3. 用 uid 执行操作click / input_text / get_value / assert_*
### 快照格式选择
- `compact`(默认推荐):含 uid、标签、文本、位置、尺寸token 用量减少 60-70%
- `minimal`:只含 uid、标签、文本最省 token
- `json`:完整 JSON需要属性详情时用
### CSS 选择器查找
```
$("view.container") // 类名
$("#myId") // ID
$("text=按钮") // 文本匹配
```
返回匹配元素的详细信息,适合精确定位。
### 输入文本
```
input_text(uid: "xxx", text: "测试内容")
input_text(uid: "xxx", text: "追加内容", append: true)
input_text(uid: "xxx", text: "新内容", clear: true) // 先清空再输入
```
### 表单控件
```
set_form_control(uid: "xxx", value: 2) // picker 选第3项
set_form_control(uid: "xxx", value: true) // switch 开启
set_form_control(uid: "xxx", value: 50) // slider 设为50
```
## 断言验证
### 文本断言
```
assert_text(uid: "xxx", text: "精确匹配")
assert_text(uid: "xxx", textContains: "包含")
assert_text(uid: "xxx", textMatches: "\\d{4}-\\d{2}-\\d{2}") // 正则
```
### 状态断言
```
assert_state(uid: "xxx", visible: true)
assert_state(uid: "xxx", enabled: false)
assert_state(uid: "xxx", checked: true)
```
### 属性断言
```
assert_attribute(uid: "xxx", attributeKey: "class", attributeValue: "active")
```
## JS 执行
### 读取页面数据
```javascript
evaluate_script({
function: "() => { const pages = getCurrentPages(); return pages[pages.length-1].data; }"
})
```
### 读取全局数据
```javascript
evaluate_script({
function: "() => { return getApp().globalData; }"
})
```
### 调用 wx API
```javascript
evaluate_script({
function: "() => { return wx.getSystemInfoSync(); }"
})
```
### 带参数执行
```javascript
evaluate_script({
function: "(key) => { return wx.getStorageSync(key); }",
args: ["userToken"]
})
```
## 截图与视觉验证
### 保存截图到文件
```
screenshot(path: "C:/NeoZQYY/export/screenshots/task-list.png")
```
### 配合 pixel-audit Power 做视觉对比
1. H5 页面用 Playwright 截图
2. MP 页面用 `screenshot` 截图
3.`image-compare`(已集成在 pixel-audit对比差异
## 网络请求监控
### 查看请求
```
get_network_requests(urlPattern: "api/v1/tasks", limit: 10)
get_network_requests(type: "request", successOnly: true)
```
### 清空并重新监控
```
clear_network_requests → 执行操作 → get_network_requests
```
## 等待策略
### 等待元素出现
```
waitFor(selector: ".loading", disappear: true, timeout: 10000) // 等 loading 消失
waitFor(selector: ".task-card", timeout: 5000) // 等元素出现
waitFor(text: "加载完成", timeout: 5000) // 等文本出现
```
### 固定延时
```
waitFor(delay: 2000) // 等待 2 秒
```
## 常见问题
| 问题 | 解决方案 |
|------|----------|
| 连接超时 | 检查 9420 端口是否开启,`diagnose_connection` |
| 元素找不到 | 先 `get_page_snapshot` 确认页面已加载完成 |
| navigate_to 失败 | TabBar 页面必须用 `switch_tab` |
| evaluate_script 报错 | 函数不能引用外部闭包变量,必须自包含 |
| 截图空白 | 页面可能还在加载,先 `waitFor` |

File diff suppressed because it is too large Load Diff