Files
wechat_wc/tools/tabbar-gradient-test.html
T

530 lines
19 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TabBar 指示器调试器</title>
<style>
:root {
--bg-page: #faf7f2;
--bg-card: #ffffff;
--text: #2e2622;
--text-2: #8a7e76;
--border: #ede5dc;
--accent: #d96c6a;
--glass: rgba(255, 255, 255, 0.55);
--glass-line: rgba(255, 255, 255, 0.55);
--shadow: rgba(46, 38, 34, 0.18);
--ind: rgba(0, 0, 0, 0.12);
}
html.theme-dark {
--bg-page: #191919;
--bg-card: #23201d;
--text: #f5efe8;
--text-2: #b3a79c;
--border: #37312c;
--accent: #f09198;
--glass: rgba(25, 25, 25, 0.55);
--glass-line: rgba(255, 255, 255, 0.14);
--shadow: rgba(0, 0, 0, 0.5);
--ind: rgba(255, 255, 255, 0.16);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
background: var(--bg-page);
color: var(--text);
min-height: 100vh;
transition: background .2s, color .2s;
}
.appbar {
position: sticky;
top: 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 16px 24px;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
}
.appbar h1 { margin: 0; font-size: 18px; font-weight: 700; letter-spacing: 0; }
.actions { display: flex; gap: 8px; flex-wrap: wrap; }
button {
font: inherit;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text);
border-radius: 999px;
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
}
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
button:active { transform: scale(0.98); }
.layout {
display: grid;
grid-template-columns: minmax(280px, 380px) 1fr;
gap: 24px;
max-width: 1080px;
margin: 0 auto;
padding: 24px;
}
.controls {
display: flex;
flex-direction: column;
gap: 12px;
align-self: start;
padding: 18px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 16px;
}
.group-title { font-size: 13px; font-weight: 700; margin: 4px 0 8px; letter-spacing: 0; }
.group { border-top: 1px solid var(--border); padding-top: 12px; }
.field { margin-bottom: 12px; }
.field label {
display: flex;
justify-content: space-between;
font-size: 13px;
color: var(--text-2);
margin-bottom: 6px;
}
.field output { color: var(--accent); font-variant-numeric: tabular-nums; }
input[type=range] { width: 100%; accent-color: var(--accent); }
.output { margin-top: 4px; }
.output .group-title { display: flex; justify-content: space-between; align-items: center; }
.output textarea {
width: 100%;
min-height: 118px;
resize: vertical;
font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px;
background: var(--bg-page);
color: var(--text);
}
.hint { font-size: 12px; color: var(--text-2); margin: 6px 0 0; line-height: 1.5; }
.preview { display: flex; align-items: center; justify-content: center; }
.phone {
width: min(100%, 380px);
aspect-ratio: 9 / 18.5;
border-radius: 44px;
background: var(--bg-card);
box-shadow: 0 24px 60px var(--shadow);
padding: 12px;
}
.screen {
position: relative;
width: 100%;
height: 100%;
border-radius: 34px;
overflow: hidden;
background: var(--bg-page);
}
.feed {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
flex-direction: column;
gap: 12px;
padding: 52px 24px 220px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
scrollbar-width: none;
-ms-overflow-style: none;
}
.feed::-webkit-scrollbar { display: none; }
.card {
flex: 0 0 auto;
padding: 10px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
display: flex;
flex-direction: column;
gap: 7px;
}
.img { height: 86px; border-radius: 10px; }
.g1 { background: linear-gradient(145deg, #e08e8b, #7c8bd6); }
.g2 { background: linear-gradient(205deg, #67c58e, #d9a35e); }
.g3 { background: linear-gradient(120deg, #d96c6a, #e6c08a); }
.g4 { background: linear-gradient(160deg, #8e97e8, #5faa8a); }
.ln {
height: 9px;
border-radius: 6px;
background: var(--text-2);
opacity: 0.35;
}
.w90 { width: 90%; }
.w85 { width: 85%; }
.w80 { width: 80%; }
.w75 { width: 75%; }
.w70 { width: 70%; }
.w65 { width: 65%; }
.w60 { width: 60%; }
.w55 { width: 55%; }
.w50 { width: 50%; }
.w45 { width: 45%; }
.w40 { width: 40%; }
.tiles { display: flex; gap: 8px; }
.t { width: 44px; height: 44px; border-radius: 12px; flex: 0 0 auto; }
.t1 { background: #e07a72; }
.t2 { background: #6c7bdb; }
.t3 { background: #4fa477; }
.t4 { background: #d9a35e; }
.t5 { background: #8e97e8; }
.gradient {
position: absolute;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.pill {
position: absolute;
display: flex;
align-items: center;
justify-content: space-around;
border-radius: 28px;
background: var(--glass);
border: 1px solid var(--glass-line);
backdrop-filter: blur(32px) saturate(1.2);
-webkit-backdrop-filter: blur(32px) saturate(1.2);
box-shadow: 0 12px 40px var(--shadow);
}
.tab {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
min-width: 0;
color: var(--text-2);
font-size: 10px;
}
.tab.active { color: var(--accent); font-weight: 700; }
.tab svg, .tab > span { position: relative; z-index: 1; }
.tab svg { width: 22px; height: 22px; }
.ind {
position: absolute;
background: var(--ind);
}
@media (max-width: 760px) {
.layout { grid-template-columns: 1fr; }
}
</style>
</head>
<body class="theme-light">
<header class="appbar">
<h1>TabBar 指示器调试器</h1>
<div class="actions">
<button id="themeBtn">切换到深色</button>
<button id="copyCss" type="button">复制 SCSS</button>
<button id="copyJson" class="primary" type="button">复制参数</button>
</div>
</header>
<main class="layout">
<section class="controls" aria-label="参数调节">
<div class="group">
<div class="group-title">选中指示器</div>
<div class="field">
<label>高度 <output id="vIndHeight">50</output> px</label>
<input id="indHeight" type="range" min="8" max="64" step="1" value="50">
</div>
<div class="field">
<label>左右贴边 <output id="vIndSide">-18</output> px</label>
<input id="indSide" type="range" min="-20" max="30" step="1" value="-18">
</div>
<div class="field">
<label>底部贴边 <output id="vIndBottom">3</output> px</label>
<input id="indBottom" type="range" min="-20" max="20" step="1" value="3">
</div>
<div class="field">
<label>圆角 <output id="vIndRadius">25</output> px</label>
<input id="indRadius" type="range" min="0" max="40" step="1" value="25">
</div>
<div class="field">
<label>宽度 <output id="vIndWidth">60</output> px</label>
<input id="indWidth" type="range" min="0" max="160" step="1" value="60">
</div>
<div class="field">
<label>不透明度 <output id="vIndOpacity">14</output> %</label>
<input id="indOpacity" type="range" min="0" max="100" step="1" value="14">
</div>
<div class="field">
<label>阴影模糊 <output id="vIndBlur">3</output> px</label>
<input id="indBlur" type="range" min="0" max="30" step="1" value="3">
</div>
<div class="field">
<label>底色 <output id="vIndColor">#000000</output></label>
<input id="indColor" type="color" value="#000000">
</div>
</div>
<div class="output">
<div class="group">
<div class="group-title">导出的参数</div>
<textarea id="jsonOut" readonly aria-label="导出的参数"></textarea>
</div>
<p class="hint">点「复制参数」把 JSON 发给我;点「复制 SCSS」得到可直接落地的样式代码。</p>
</div>
</section>
<section class="preview" aria-label="预览">
<div class="phone">
<div class="screen">
<div class="feed">
<div class="card"><div class="img g1"></div><div class="ln w80"></div><div class="ln w55"></div></div>
<div class="card"><div class="img g2"></div><div class="ln w70"></div><div class="ln w45"></div></div>
<div class="card">
<div class="ln w90"></div><div class="ln w60"></div><div class="ln w75"></div>
</div>
<div class="card">
<div class="tiles"><i class="t t1"></i><i class="t t2"></i><i class="t t3"></i></div>
<div class="ln w65"></div>
</div>
<div class="card"><div class="img g3"></div><div class="ln w80"></div><div class="ln w50"></div></div>
<div class="card">
<div class="ln w85"></div><div class="ln w60"></div><div class="ln w40"></div>
</div>
<div class="card"><div class="img g4"></div><div class="ln w75"></div></div>
<div class="card">
<div class="tiles"><i class="t t4"></i><i class="t t5"></i></div>
<div class="ln w70"></div>
</div>
</div>
<div class="gradient" id="gradient"></div>
<div class="pill" id="pill"></div>
</div>
</div>
</section>
</main>
<script>
const TABS = [
['首页', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m3 10 9-7 9 7"/><path d="M5 9v11h14V9"/><path d="M9 20v-6h6v6"/></svg>'],
['商品', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8 12 3 3 8v8l9 5 9-5Z"/><path d="M3.5 7.5 12 12l8.5-4.5"/><path d="M12 22V12"/></svg>'],
['设计', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22a10 10 0 1 1 10-10c0 2.21-1.79 4-4 4h-2a2 2 0 0 0-2 2c0 .53.2 1 .54 1.36C14.78 21.4 13.5 22 12 22Z"/><circle cx="7.5" cy="10.5" r="1"/><circle cx="12" cy="7.5" r="1"/><circle cx="16.5" cy="10.5" r="1"/></svg>'],
['订单', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>'],
['我的', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21s1-5 8-5 8 5 8 5"/></svg>']
]
const ids = {
indHeight: document.getElementById('indHeight'),
indSide: document.getElementById('indSide'),
indBottom: document.getElementById('indBottom'),
indRadius: document.getElementById('indRadius'),
indWidth: document.getElementById('indWidth'),
indOpacity: document.getElementById('indOpacity'),
indBlur: document.getElementById('indBlur'),
indColor: document.getElementById('indColor'),
gradient: document.getElementById('gradient'),
pill: document.getElementById('pill'),
indicator: document.getElementById('indicator')
}
const val = (el) => Number(el.value)
function hexToRgb(hex) {
const h = hex.replace('#', '')
return {
r: parseInt(h.substring(0, 2), 16),
g: parseInt(h.substring(2, 4), 16),
b: parseInt(h.substring(4, 6), 16)
}
}
function themeColors() {
return document.body.classList.contains('theme-dark')
? { r: 25, g: 25, b: 25, label: 'dark' }
: { r: 250, g: 247, b: 242, label: 'light' }
}
function buildPill() {
ids.pill.innerHTML = TABS.map((t, i) =>
'<div class="tab' + (i === 0 ? ' active' : '') + '">' +
(i === 0 ? '<div class="ind" id="indicator"></div>' : '') +
t[1] + '<span>' + t[0] + '</span></div>'
).join('')
ids.indicator = document.getElementById('indicator')
}
function currentParams() {
const c = themeColors()
return {
theme: c.label,
gradient: {
height: 80,
bottomOpacity: 50,
midStop: 50,
midOpacity: 10,
endStop: 86
},
tabBar: {
bottom: 18,
sideMargin: 18,
height: 58,
radius: 32
},
indicator: {
width: val(ids.indWidth),
height: val(ids.indHeight),
sideInset: val(ids.indSide),
bottomInset: val(ids.indBottom),
radius: val(ids.indRadius),
opacity: val(ids.indOpacity),
blur: val(ids.indBlur),
color: ids.indColor.value
}
}
}
function render() {
const c = themeColors()
const p = currentParams()
const g = p.gradient
const tb = p.tabBar
const ind = p.indicator
const mid = Math.max(g.midStop, 1)
const end = Math.max(g.endStop, mid + 1)
const arr = [
'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + (g.bottomOpacity / 100).toFixed(3) + ') 0%',
'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + (g.midOpacity / 100).toFixed(3) + ') ' + mid + '%',
'rgba(' + c.r + ',' + c.g + ',' + c.b + ',0) ' + end + '%'
]
ids.gradient.style.height = g.height + 'px'
ids.gradient.style.background = 'linear-gradient(to top, ' + arr.join(', ') + ')'
ids.pill.style.bottom = tb.bottom + 'px'
ids.pill.style.left = tb.sideMargin + 'px'
ids.pill.style.right = tb.sideMargin + 'px'
ids.pill.style.height = tb.height + 'px'
ids.pill.style.borderRadius = tb.radius + 'px'
const rgb = hexToRgb(ind.color)
const alpha = (ind.opacity / 100).toFixed(3)
ids.indicator.style.bottom = ind.bottomInset + 'px'
ids.indicator.style.height = ind.height + 'px'
ids.indicator.style.borderRadius = ind.radius + 'px'
ids.indicator.style.background = 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + alpha + ')'
ids.indicator.style.boxShadow = ind.blur > 0
? '0 0 ' + ind.blur + 'px rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + (alpha / 2) + ')'
: 'none'
if (ind.width > 0) {
ids.indicator.style.width = ind.width + 'px'
ids.indicator.style.left = '50%'
ids.indicator.style.right = 'auto'
ids.indicator.style.transform = 'translateX(-50%)'
} else {
ids.indicator.style.width = 'auto'
ids.indicator.style.left = ind.sideInset + 'px'
ids.indicator.style.right = ind.sideInset + 'px'
ids.indicator.style.transform = 'none'
}
document.getElementById('vIndHeight').textContent = ind.height
document.getElementById('vIndSide').textContent = ind.sideInset
document.getElementById('vIndBottom').textContent = ind.bottomInset
document.getElementById('vIndRadius').textContent = ind.radius
document.getElementById('vIndWidth').textContent = ind.width
document.getElementById('vIndOpacity').textContent = ind.opacity
document.getElementById('vIndBlur').textContent = ind.blur
document.getElementById('vIndColor').textContent = ind.color
document.getElementById('jsonOut').value = JSON.stringify(p, null, 2)
}
function scssSnippet() {
const c = themeColors()
const p = currentParams()
const col = c.label === 'dark' ? '25, 25, 25' : '250, 247, 242'
const rpx = (v) => Math.round(v * 2)
const ind = p.indicator
const irgb = hexToRgb(ind.color)
const indLines = []
if (ind.width > 0) {
indLines.push(' width: ' + rpx(ind.width) + 'rpx;')
indLines.push(' left: 50%;')
indLines.push(' transform: translateX(-50%);')
} else {
indLines.push(' left: ' + rpx(ind.sideInset) + 'rpx;')
indLines.push(' right: ' + rpx(ind.sideInset) + 'rpx;')
}
indLines.push(' bottom: ' + rpx(ind.bottomInset) + 'rpx;')
indLines.push(' height: ' + rpx(ind.height) + 'rpx;')
indLines.push(' border-radius: ' + rpx(ind.radius) + 'rpx;')
indLines.push(' background: rgba(' + irgb.r + ', ' + irgb.g + ', ' + irgb.b + ', ' + (ind.opacity / 100).toFixed(2) + ');')
if (ind.blur > 0) {
indLines.push(' box-shadow: 0 0 ' + rpx(ind.blur) + 'rpx rgba(' + irgb.r + ', ' + irgb.g + ', ' + irgb.b + ', ' + ((ind.opacity / 100) / 2).toFixed(2) + ');')
}
return [
'/* ' + c.label + ' theme */',
'.tab-bar-root.' + c.label + ' .tab-bar-gradient {',
' height: ' + rpx(p.gradient.height) + 'rpx;',
' background: linear-gradient(to top,',
' rgba(' + col + ', ' + (p.gradient.bottomOpacity / 100).toFixed(2) + ') 0%,',
' rgba(' + col + ', ' + (p.gradient.midOpacity / 100).toFixed(2) + ') ' + p.gradient.midStop + '%,',
' rgba(' + col + ', 0) ' + p.gradient.endStop + '%',
' );',
'}',
'.custom-tab-bar {',
' bottom: ' + rpx(p.tabBar.bottom) + 'rpx;',
' left: ' + rpx(p.tabBar.sideMargin) + 'rpx;',
' right: ' + rpx(p.tabBar.sideMargin) + 'rpx;',
' height: ' + rpx(p.tabBar.height) + 'rpx;',
' border-radius: ' + rpx(p.tabBar.radius) + 'rpx;',
'}',
'.tab-indicator {',
indLines.join('\n'),
'}'
].join('\n')
}
function copy(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text)
}
const ta = document.createElement('textarea')
ta.value = text
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
return Promise.resolve()
}
document.getElementById('themeBtn').addEventListener('click', () => {
const dark = document.body.classList.toggle('theme-dark')
document.getElementById('themeBtn').textContent = dark ? '切换到浅色' : '切换到深色'
render()
})
document.getElementById('copyJson').addEventListener('click', () => {
copy(JSON.stringify(currentParams(), null, 2)).then(() => { alert('参数已复制') })
})
document.getElementById('copyCss').addEventListener('click', () => {
copy(scssSnippet()).then(() => { alert('SCSS 已复制') })
})
Object.keys(ids).forEach((k) => {
if (k === 'gradient' || k === 'pill' || k === 'indicator') return
ids[k].addEventListener('input', render)
})
buildPill()
render()
</script>
</body>
</html>