Files

439 lines
9.4 KiB
Vue
Raw Permalink Normal View History

<!--
JQueryVsStateDemo.vue
用可视化方式解释jQuery = 手动改 DOM框架 = State 自动同步
-->
<template>
<div class="jq-demo">
<div class="header">
<div class="title">
什么是 jQuery购物车数量秒懂
</div>
<div class="subtitle">
左边 jQuery 一样手动改页面容易漏右边 Vue/React
一样只改状态
</div>
</div>
<div class="panes">
<!-- jQuery-like -->
<div class="pane">
<div class="pane-title">
jQuery 思路到处改 DOM
</div>
<div class="mock-app">
<div class="topbar">
<span>🛒 角标</span>
<span
class="badge"
:class="{ wrong: jqBadgeWrong }"
>{{
jqBadge
}}</span>
</div>
<div class="content">
<div class="row">
购物车页数量
<span
class="num"
:class="{ wrong: jqPageWrong }"
>{{
jqPage
}}</span>
</div>
<div class="row">
结算按钮
<button class="checkout">
去结算 ({{ jqButtonLabel }})
</button>
</div>
</div>
</div>
<div class="controls">
<div class="control-title">
模拟你写的命令
</div>
<div class="btns">
<button @click="jqIncreaseData">
数据 +1但还没改页面
</button>
<button @click="jqUpdateBadge">
改角标
</button>
<button @click="jqUpdateCartPage">
改购物车页
</button>
<button @click="jqUpdateCheckoutButton">
改结算按钮
</button>
</div>
<div
class="hint"
:class="{ danger: jqInconsistent }"
>
{{ jqHint }}
</div>
<div class="log">
<div class="log-title">
命令日志
</div>
<div
v-if="jqLogs.length === 0"
class="log-empty"
>
还没有操作
</div>
<div
v-else
class="log-list"
>
<div
v-for="(l, idx) in jqLogs"
:key="idx"
class="log-item"
>
{{ l }}
</div>
</div>
</div>
</div>
</div>
<!-- State-driven -->
<div class="pane">
<div class="pane-title">
Vue/React 思路只改 State
</div>
<div class="mock-app">
<div class="topbar">
<span>🛒 角标</span>
<span class="badge">{{ state }}</span>
</div>
<div class="content">
<div class="row">
购物车页数量 <span class="num">{{ state }}</span>
</div>
<div class="row">
结算按钮
<button class="checkout">
去结算 ({{ state }} )
</button>
</div>
</div>
</div>
<div class="controls">
<div class="control-title">
你只需要做一件事
</div>
<div class="btns">
<button
class="primary"
@click="state = state + 1"
>
state +1
</button>
<button
class="secondary"
@click="resetAll"
>
重置
</button>
</div>
<div class="hint ok">
State 变了界面三处会自动同步不需要你手动找 DOM 去改
</div>
<div class="mini">
<div class="mini-title">
这里的两个新词
</div>
<div class="mini-item">
<strong>DOM</strong>浏览器里的页面结构按钮/文字/图片都在里面
</div>
<div class="mini-item">
<strong>State</strong>页面的数据比如购物车数量
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const state = ref(1)
// jQuery side: "real data" + "DOM" values displayed at multiple places
const jqData = ref(1)
const jqBadge = ref(1)
const jqPage = ref(1)
const jqButtonLabel = ref('1 件')
const jqLogs = ref([])
const log = (txt) => {
jqLogs.value.unshift(
`${new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})} - ${txt}`
)
jqLogs.value = jqLogs.value.slice(0, 8)
}
const jqIncreaseData = () => {
jqData.value += 1
log(`数据 +1(现在真实数据 = ${jqData.value}`)
}
const jqUpdateBadge = () => {
jqBadge.value = jqData.value
log(`更新角标 DOM = ${jqBadge.value}`)
}
const jqUpdateCartPage = () => {
jqPage.value = jqData.value
log(`更新购物车页 DOM = ${jqPage.value}`)
}
const jqUpdateCheckoutButton = () => {
jqButtonLabel.value = `${jqData.value}`
log(`更新结算按钮 DOM = ${jqButtonLabel.value}`)
}
const jqInconsistent = computed(() => {
return (
jqBadge.value !== jqData.value ||
jqPage.value !== jqData.value ||
jqButtonLabel.value !== `${jqData.value}`
)
})
const jqBadgeWrong = computed(() => jqBadge.value !== jqData.value)
const jqPageWrong = computed(() => jqPage.value !== jqData.value)
const jqHint = computed(() => {
if (!jqInconsistent.value) return '✅ 三处显示一致(恭喜你都改对了)'
return '⚠️ 数据和页面不一致:你可能漏更新了某一处 DOM(真实项目里这就是 bug)'
})
const resetAll = () => {
state.value = 1
jqData.value = 1
jqBadge.value = 1
jqPage.value = 1
jqButtonLabel.value = '1 件'
jqLogs.value = []
}
</script>
<style scoped>
.jq-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.panes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1rem;
}
.pane {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 12px;
padding: 0.75rem;
}
.pane-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.mock-app {
border: 1px dashed var(--vp-c-divider);
border-radius: 12px;
overflow: hidden;
}
.topbar {
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg-soft);
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2ch;
padding: 0.1rem 0.45rem;
border-radius: 999px;
background: rgba(59, 130, 246, 0.15);
color: #1d4ed8;
font-weight: 700;
}
.content {
padding: 0.75rem;
}
.row {
margin-bottom: 0.6rem;
font-size: 0.92rem;
}
.num {
font-weight: 800;
padding: 0.05rem 0.25rem;
border-radius: 6px;
background: rgba(34, 197, 94, 0.12);
color: #15803d;
}
.checkout {
border: none;
background: var(--vp-c-brand);
color: #fff;
padding: 0.4rem 0.8rem;
border-radius: 10px;
font-size: 0.85rem;
}
.controls {
margin-top: 0.9rem;
}
.control-title {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.btns {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.btns button {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
padding: 0.35rem 0.65rem;
border-radius: 10px;
cursor: pointer;
font-size: 0.85rem;
}
.btns button.primary {
border: none;
background: #22c55e;
color: #fff;
}
.btns button.secondary {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.hint {
margin-top: 0.65rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
border: 1px dashed var(--vp-c-divider);
background: var(--vp-c-bg-soft);
padding: 0.6rem 0.7rem;
border-radius: 10px;
}
.hint.danger {
color: #b91c1c;
border-color: rgba(239, 68, 68, 0.4);
background: rgba(239, 68, 68, 0.08);
}
.hint.ok {
color: #166534;
border-color: rgba(34, 197, 94, 0.35);
background: rgba(34, 197, 94, 0.08);
}
.wrong {
background: rgba(239, 68, 68, 0.12) !important;
color: #b91c1c !important;
}
.log {
margin-top: 0.75rem;
}
.log-title {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
.log-empty {
color: var(--vp-c-text-3);
font-size: 0.85rem;
}
.log-list {
display: grid;
gap: 0.25rem;
}
.log-item {
font-family: var(--vp-font-family-mono);
font-size: 0.78rem;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.35rem 0.5rem;
}
.mini {
margin-top: 0.75rem;
border-top: 1px dashed var(--vp-c-divider);
padding-top: 0.75rem;
}
.mini-title {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.4rem;
}
.mini-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
</style>