392 lines
9.2 KiB
HTML
392 lines
9.2 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>uvue 动效改写验证 · 红点呼吸 / 扫描线 / 手风琴</title>
|
||
<style>
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
padding: 32px 24px 64px;
|
||
background: #F3F4F6;
|
||
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
color: #1F2937;
|
||
}
|
||
|
||
.wrap {
|
||
max-width: 1040px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 22px;
|
||
margin: 0 0 6px;
|
||
}
|
||
|
||
.sub {
|
||
font-size: 13px;
|
||
color: #6B7280;
|
||
margin: 0 0 8px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.warn {
|
||
background: #FFFBEB;
|
||
border: 1px solid #FDE68A;
|
||
color: #92400E;
|
||
border-radius: 12px;
|
||
padding: 12px 16px;
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
margin-bottom: 26px;
|
||
}
|
||
|
||
.grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 20px;
|
||
}
|
||
|
||
.card {
|
||
flex: 1 1 300px;
|
||
min-width: 300px;
|
||
background: #FFF;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 16px;
|
||
padding: 18px 20px 20px;
|
||
}
|
||
|
||
.card h2 {
|
||
font-size: 15px;
|
||
margin: 0 0 4px;
|
||
}
|
||
|
||
.src {
|
||
font-size: 11px;
|
||
color: #9CA3AF;
|
||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||
margin: 0 0 16px;
|
||
}
|
||
|
||
.stage {
|
||
height: 210px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ---- 1. 红点 ---- */
|
||
.evt-card {
|
||
width: 210px;
|
||
background: #FFFBFB;
|
||
border: 1px solid #FECACA;
|
||
border-radius: 13px;
|
||
padding: 12px 13px;
|
||
}
|
||
|
||
.evt-top {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.evt-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
margin: 0 8px;
|
||
background-color: #DC2626;
|
||
}
|
||
|
||
.evt-name {
|
||
flex: 1;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.pri {
|
||
background: #DC2626;
|
||
color: #FFF;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
border-radius: 6px;
|
||
padding: 2px 8px;
|
||
}
|
||
|
||
/* ---- 2. 扫描线 ---- */
|
||
.sc-box {
|
||
width: 100%;
|
||
height: 178px;
|
||
background: #0F172A;
|
||
border-radius: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
|
||
.sc-frame {
|
||
width: 93px;
|
||
height: 93px;
|
||
position: relative;
|
||
}
|
||
|
||
.cf {
|
||
position: absolute;
|
||
width: 13px;
|
||
height: 13px;
|
||
border: 3px solid #10B981;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.cf-tl {
|
||
top: 0;
|
||
left: 0;
|
||
border-right-width: 0;
|
||
border-bottom-width: 0;
|
||
}
|
||
|
||
.cf-tr {
|
||
top: 0;
|
||
right: 0;
|
||
border-left-width: 0;
|
||
border-bottom-width: 0;
|
||
}
|
||
|
||
.cf-bl {
|
||
bottom: 0;
|
||
left: 0;
|
||
border-right-width: 0;
|
||
border-top-width: 0;
|
||
}
|
||
|
||
.cf-br {
|
||
bottom: 0;
|
||
right: 0;
|
||
border-left-width: 0;
|
||
border-top-width: 0;
|
||
}
|
||
|
||
.sc-line {
|
||
position: absolute;
|
||
left: 8.4px;
|
||
right: 8.4px;
|
||
height: 2px;
|
||
background: #10B981;
|
||
box-shadow: 0 0 8px rgba(16, 185, 129, .9);
|
||
}
|
||
|
||
.sc-tip {
|
||
position: absolute;
|
||
bottom: 12px;
|
||
left: 0;
|
||
right: 0;
|
||
text-align: center;
|
||
font-size: 11px;
|
||
color: rgba(255, 255, 255, .78);
|
||
}
|
||
|
||
/* ---- 3. 手风琴 ---- */
|
||
.qa-card {
|
||
width: 250px;
|
||
background: #FFF;
|
||
border: 1px solid #E5E7EB;
|
||
border-left: 4px solid #2563EB;
|
||
border-radius: 10px;
|
||
padding: 11px 12px;
|
||
box-shadow: 0 2px 10px rgba(15, 23, 42, .06);
|
||
}
|
||
|
||
.qa-stem {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.qa-fold {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.qa-btn {
|
||
font-size: 12px;
|
||
color: #2563EB;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.qa-analysis {
|
||
margin-top: 10px;
|
||
padding: 9px 10px;
|
||
border-radius: 9px;
|
||
background: #F9FAFB;
|
||
font-size: 12px;
|
||
color: #6B7280;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.readout {
|
||
margin-top: 12px;
|
||
font-size: 11px;
|
||
color: #9CA3AF;
|
||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="wrap">
|
||
<h1>uvue App 端动效改写 · 效果验证</h1>
|
||
<p class="sub">下方三个动效与 App 里 <b>完全相同</b> 的算法在跑(定时器逐帧推进 → 下发内联 style),
|
||
只是把 rpx 折算成 px(1rpx = 0.5px @375)。</p>
|
||
<div class="warn">
|
||
<b>背景</b>:App 端 uvue 的 CSS 编译器不接受 <code>@keyframes</code>(0%/75%/from/to 选择器直接报
|
||
ERROR)与 <code>animation</code> 属性,
|
||
因此这 3 处动效已全部改为 <code>setInterval</code> + <code>ref</code> 驱动内联样式的等价实现。
|
||
</div>
|
||
|
||
<div class="grid">
|
||
<!-- 1 -->
|
||
<div class="card">
|
||
<h2>① 待接收红点「呼吸」</h2>
|
||
<p class="src">pages/event/index.uvue · evtPulse</p>
|
||
<div class="stage" style="background:#F9FAFB">
|
||
<div class="evt-card">
|
||
<div class="evt-top">
|
||
<div class="evt-dot" id="dot"></div>
|
||
<span class="evt-name">车祸伤 · 多人受伤</span>
|
||
<span class="pri">P0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="readout" id="dotOut">半径 0rpx · 透明度 0.55</div>
|
||
</div>
|
||
|
||
<!-- 2 -->
|
||
<div class="card">
|
||
<h2>② 取景框扫描线</h2>
|
||
<p class="src">pages/warehouse/scan.uvue · hrScanMove</p>
|
||
<div class="stage">
|
||
<div class="sc-box">
|
||
<div class="sc-frame">
|
||
<div class="cf cf-tl"></div>
|
||
<div class="cf cf-tr"></div>
|
||
<div class="cf cf-bl"></div>
|
||
<div class="cf cf-br"></div>
|
||
<div class="sc-line" id="line"></div>
|
||
</div>
|
||
<div class="sc-tip">将条码对准框内 · 自动识别</div>
|
||
</div>
|
||
</div>
|
||
<div class="readout" id="lineOut">top 60rpx → 313rpx · 32 帧 × 68ms = 2.18s</div>
|
||
</div>
|
||
|
||
<!-- 3 -->
|
||
<div class="card">
|
||
<h2>③ 手风琴展开入场</h2>
|
||
<p class="src">pages/study/center.uvue · qa-acc-in</p>
|
||
<div class="stage" style="background:#F9FAFB">
|
||
<div class="qa-card">
|
||
<div class="qa-stem">成人胸外按压的深度应为?</div>
|
||
<div class="qa-fold"><span class="qa-btn" id="accBtn">点击展开解析</span></div>
|
||
<div id="accHost"></div>
|
||
</div>
|
||
</div>
|
||
<div class="readout" id="accOut">opacity 0 → 1 · translateY -16rpx → 0 · 240ms</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ============ ① 红点呼吸:与 pages/event/index.uvue 完全同算法 ============ */
|
||
const PULSE_FRAMES = 25
|
||
const PULSE_RAMP = 19
|
||
let pulseF = 0
|
||
const dotEl = document.getElementById('dot')
|
||
const dotOut = document.getElementById('dotOut')
|
||
|
||
/* 改写前的 CSS 版本,用作「应该长什么样」的参照 */
|
||
const RING_MAX_PX = 16 * 0.5 // 16rpx
|
||
function renderPulse() {
|
||
const i = pulseF
|
||
let r = 0, a = 0
|
||
if (i > PULSE_RAMP) { r = 0; a = 0 } else {
|
||
const k = i / PULSE_RAMP
|
||
r = Math.round(k * 16)
|
||
a = Math.round(55 * (1 - k))
|
||
}
|
||
dotEl.style.boxShadow = '0 0 0 ' + (r * 0.5).toFixed(1) + 'px rgba(220,38,38,0.' + a + ')'
|
||
dotOut.textContent = '半径 ' + r + 'rpx · 透明度 0.' + a + ' · 第 ' + i + '/25 帧'
|
||
const nf = pulseF + 1
|
||
pulseF = nf >= PULSE_FRAMES ? 0 : nf
|
||
}
|
||
setInterval(renderPulse, 68)
|
||
renderPulse()
|
||
|
||
/* ============ ② 扫描线:与 pages/warehouse/scan.uvue 完全同算法 ============ */
|
||
const LASER_MIN = 60, LASER_SPAN = 253
|
||
let laserT = 0.0
|
||
const lineEl = document.getElementById('line')
|
||
const lineOut = document.getElementById('lineOut')
|
||
function renderLaser() {
|
||
laserT = laserT + 0.03125
|
||
if (laserT > 1) { laserT = laserT - 1 }
|
||
const k = laserT < 0.5 ? laserT * 2 : (1 - laserT) * 2
|
||
const top = Math.round(LASER_MIN + k * LASER_SPAN)
|
||
lineEl.style.top = (top * 0.5).toFixed(1) + 'px'
|
||
lineOut.textContent = 'top ' + top + 'rpx (区间 60 → 313) · 32 帧 × 68ms = 2.18s'
|
||
}
|
||
setInterval(renderLaser, 68)
|
||
renderLaser()
|
||
|
||
/* ============ ③ 手风琴:与 pages/study/center.uvue 完全同算法 ============ */
|
||
let accIdx = -1, accT = 1, accTimer = 0, accOpen = false
|
||
const accHost = document.getElementById('accHost')
|
||
const accBtn = document.getElementById('accBtn')
|
||
const accOut = document.getElementById('accOut')
|
||
|
||
function stopAcc() {
|
||
if (accTimer != 0) { clearInterval(accTimer); accTimer = 0 }
|
||
}
|
||
function runAcc() {
|
||
stopAcc()
|
||
accIdx = 0
|
||
accT = 0
|
||
accTimer = setInterval(() => {
|
||
const t = accT + 0.16
|
||
if (t >= 1) { accT = 1; stopAcc(); renderAcc(); return }
|
||
accT = t
|
||
renderAcc()
|
||
}, 30)
|
||
}
|
||
function renderAcc() {
|
||
if (!accOpen) { accHost.innerHTML = ''; accOut.textContent = '已收起'; return }
|
||
if (accT >= 1) {
|
||
accHost.innerHTML = '<div class="qa-analysis">按压深度 5~6 cm,频率 100~120 次/分;每次按压后让胸廓完全回弹。</div>'
|
||
accOut.textContent = 'opacity 1 · translateY 0 · 动画完成'
|
||
return
|
||
}
|
||
const t = accT
|
||
const y = Math.round(-16 * (1 - t))
|
||
accHost.innerHTML = '<div class="qa-analysis" style="opacity:' + t +
|
||
';transform:translateY(' + (y * 0.5).toFixed(1) + 'px)">按压深度 5~6 cm,频率 100~120 次/分;每次按压后让胸廓完全回弹。</div>'
|
||
accOut.textContent = 'opacity ' + t.toFixed(2) + ' · translateY ' + y + 'rpx'
|
||
}
|
||
accBtn.addEventListener('click', () => {
|
||
accOpen = !accOpen
|
||
accBtn.textContent = accOpen ? '收起解析' : '点击展开解析'
|
||
if (accOpen) { runAcc() } else { stopAcc(); accIdx = -1; accT = 1; renderAcc() }
|
||
})
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|