文章目录
- 知识回顾
- 前言
- 源码分析
- 1. ref
- 2. 购物车案例分析
- 3. 购物车计算、全选
- 拓展知识
- 数据持久化localStorage
- 总结
知识回顾
前言
元素上使用 ref属性关联响应式数据,获取DOM元素
步骤
● 创建 ref => const hRef = ref(null)
● 模板中建立关联 => <h1 ref="hRef">我是标题</h1>
=> hRef.value
源码分析
1. ref
const inp = ref<null | HTMLInputElement>()
const img = ref<null | HTMLImageElement>()
vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const words = ref('')
const inp = ref<null | HTMLInputElement>()
const img = ref<null | HTMLImageElement>()
onMounted(() => {
console.log('onMounted')
inp.value?.focus()
img.value
console.log(img.value)
console.log(inp.value)
})
</script>
<template>
<div class="container" id="app">
<div class="search-container">
<img ref="img" src="https://" alt="" />
<div class="search-box">
<input ref="inp" type="text" v-model="words" id="inp" />
<button>搜索一下</button>
</div>
</div>
</div>
</template>
<style>
html,
body {
height: 100%;
}
.search-container {
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.search-container .search-box {
display: flex;
}
.search-container img {
margin-bottom: 30px;
}
.search-container .search-box input {
width: 512px;
height: 16px;
padding: 12px 16px;
font-size: 16px;
margin: 0;
vertical-align: top;
outline: 0;
box-shadow: none;
border-radius: 10px 0 0 10px;
border: 2px solid #c4c7ce;
background: #fff;
color: #222;
overflow: hidden;
box-sizing: content-box;
-webkit-tap-highlight-color: transparent;
}
.search-container .search-box button {
cursor: pointer;
width: 112px;
height: 44px;
line-height: 41px;
line-height: 42px;
background-color: #ad2a27;
border-radius: 0 10px 10px 0;
font-size: 17px;
box-shadow: none;
font-weight: 400;
border: 0;
outline: 0;
letter-spacing: normal;
color: white;
}
body {
background: no-repeat center / cover;
background-color: #edf0f5;
}
</style>
2. 购物车案例分析
列表渲染、购物车增加删除
css
.app-container {
padding-bottom: 300px;
width: 800px;
margin: 0 auto;
}
@media screen and (max-width: 800px) {
.app-container {
width: 600px;
}
}
.app-container .banner-box {
border-radius: 20px;
overflow: hidden;
margin-bottom: 10px;
}
.app-container .banner-box img {
width: 100%;
}
.app-container .nav-box {
background: #ddedec;
height: 60px;
border-radius: 10px;
padding-left: 20px;
display: flex;
align-items: center;
}
.app-container .nav-box .my-nav {
display: inline-block;
background: #5fca71;
border-radius: 5px;
width: 90px;
height: 35px;
color: white;
text-align: center;
line-height: 35px;
margin-right: 10px;
}
.breadcrumb {
font-size: 16px;
color: gray;
}
.table {
width: 100%;
text-align: left;
border-radius: 2px 2px 0 0;
border-collapse: separate;
border-spacing: 0;
}
.th {
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
text-align: left;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
transition: background 0.3s ease;
}
.th.num-th {
flex: 1.5;
}
.th {
text-align: center;
}
.th:nth-child(4),
.th:nth-child(5),
.th:nth-child(6),
.th:nth-child(7) {
text-align: center;
}
.th.th-pic {
flex: 1.3;
}
.th:nth-child(6) {
flex: 1.3;
}
.th,
.td {
position: relative;
padding: 16px 16px;
overflow-wrap: break-word;
flex: 1;
}
.pick-td {
font-size: 14px;
}
.main,
.empty {
border: 1px solid #f0f0f0;
margin-top: 10px;
}
.tr {
display: flex;
cursor: pointer;
border-bottom: 1px solid #ebeef5;
}
.tr.active {
background-color: #f5f7fa;
}
.td {
display: flex;
justify-content: center;
align-items: center;
}
.table img {
width: 100px;
height: 100px;
}
button {
outline: 0;
box-shadow: none;
color: #fff;
background: #d9363e;
border-color: #d9363e;
color: #fff;
background: #d9363e;
border-color: #d9363e;
line-height: 1.5715;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
background-image: none;
border: 1px solid transparent;
box-shadow: 0 2px 0 rgb(0 0 0 / 2%);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
touch-action: manipulation;
height: 32px;
padding: 4px 15px;
font-size: 14px;
border-radius: 2px;
}
button.pay {
background-color: #3f85ed;
margin-left: 20px;
}
.bottom {
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 20px;
border: 1px solid #f0f0f0;
border-top: none;
padding-left: 20px;
}
.right-box {
display: flex;
align-items: center;
}
.check-all {
cursor: pointer;
}
.price {
color: hotpink;
font-size: 30px;
font-weight: 700;
}
.price-box {
display: flex;
align-items: center;
}
.empty {
padding: 20px;
text-align: center;
font-size: 30px;
color: #909399;
}
.my-input-number {
display: flex;
}
.my-input-number button {
height: 40px;
color: #333;
border: 1px solid #dcdfe6;
background-color: #f5f7fa;
}
.my-input-number button:disabled {
cursor: not-allowed!important;
}
.my-input-number .my-input__inner {
height: 40px;
width: 50px;
padding: 0;
border: none;
border-top: 1px solid #dcdfe6;
border-bottom: 1px solid #dcdfe6;
}
vue
<script setup lang="ts">
import { ref } from 'vue'
// 定义水果对象的类型
interface Fruit {
id: number
icon: string
isChecked: boolean
num: number
price: number
}
// 水果列表
const fruitList = ref<Fruit[]>([
{
id: 1,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/ll.png',
isChecked: true,
num: 2,
price: 6
},
{
id: 2,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/smt.png',
isChecked: false,
num: 7,
price: 20
},
{
id: 3,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/tg.png',
isChecked: false,
num: 3,
price: 40
},
{
id: 4,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/xg.png',
isChecked: false,
num: 10,
price: 3
},
{
id: 5,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/yl.png',
isChecked: false,
num: 20,
price: 34
}
])
// 删除商品
const delGoods = (id: number) => {
fruitList.value = fruitList.value.filter((item) => item.id !== id)
}
// 添加
const addGoods = (id: number) => {
const fruit = fruitList.value.find((item) => item.id === id)
fruit && fruit.num++
}
// 减少
const subGoods = (id: number) => {
const fruit = fruitList.value.find((item) => item.id === id)
fruit && fruit.num--
}
</script>
<template>
<div class="app-container" id="app">
<!-- 顶部banner -->
<div class="banner-box">
<img src="https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/sg_top.png" alt="" />
</div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div v-if="fruitList.length > 0" class="main">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div
v-for="item in fruitList"
:key="item.id"
class="tr"
:class="{ active: item.isChecked }"
>
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td">
<img :src="item.icon" alt="" />
</div>
<div class="td">{{ item.price }}</div>
<div class="td">
<div class="my-input-number">
<button :disabled="item.num <= 1" class="decrease" @click="subGoods(item.id)">
-
</button>
<span class="my-input__inner">{{ item.num }}</span>
<button class="increase" @click="addGoods(item.id)">+</button>
</div>
</div>
<div class="td">{{ item.price * item.num }}</div>
<div class="td" @click="delGoods(item.id)"><button>删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box"
>总价 : ¥ <span class="price">24</span></span
>
<!-- 结算按钮 -->
<button class="pay">结算( 6 )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div v-else class="empty">🛒空空如也</div>
</div>
</template>
<style scoped>
@import './style/inputnumber.css';
@import './style/index.css';
</style>
3. 购物车计算、全选
<script setup lang="ts">
import { computed, ref } from 'vue'
// 定义水果对象的类型
interface Fruit {
id: number
icon: string
isChecked: boolean
num: number
price: number
}
// 水果列表
const fruitList = ref<Fruit[]>([
{
id: 1,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/ll.png',
isChecked: true,
num: 2,
price: 6
},
{
id: 2,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/smt.png',
isChecked: false,
num: 7,
price: 20
},
{
id: 3,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/tg.png',
isChecked: false,
num: 3,
price: 40
},
{
id: 4,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/xg.png',
isChecked: false,
num: 10,
price: 3
},
{
id: 5,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/yl.png',
isChecked: false,
num: 20,
price: 34
}
])
// 删除商品
const delGoods = (id: number) => {
fruitList.value = fruitList.value.filter((item) => item.id !== id)
}
// 添加
const addGoods = (id: number) => {
const fruit = fruitList.value.find((item) => item.id === id)
fruit && fruit.num++
}
// 减少
const subGoods = (id: number) => {
const fruit = fruitList.value.find((item) => item.id === id)
fruit && fruit.num--
}
// 全选
const checkAll = computed({
get() {
return fruitList.value.every((item) => item.isChecked)
},
set(value) {
fruitList.value.forEach((item) => (item.isChecked = value))
}
})
// 统计选中的总数 reduce
const totalCount = computed(() => {
return fruitList.value.reduce((sum, item) => {
if (item.isChecked) {
// 选中 → 需要累加
return sum + item.num
} else {
// 没选中 → 不需要累加
return sum
}
}, 0)
})
// 总计选中的总价 num * price
const totalPrice = computed(() => {
return fruitList.value.reduce((sum, item) => {
if (item.isChecked) {
return sum + item.num * item.price
} else {
return sum
}
}, 0)
})
</script>
<template>
<div class="app-container" id="app">
<!-- 顶部banner -->
<div class="banner-box">
<img src="https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/sg_top.png" alt="" />
</div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div v-if="fruitList.length > 0" class="main">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div
v-for="item in fruitList"
:key="item.id"
class="tr"
:class="{ active: item.isChecked }"
>
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td">
<img :src="item.icon" alt="" />
</div>
<div class="td">{{ item.price }}</div>
<div class="td">
<div class="my-input-number">
<button :disabled="item.num <= 1" class="decrease" @click="subGoods(item.id)">
-
</button>
<span class="my-input__inner">{{ item.num }}</span>
<button class="increase" @click="addGoods(item.id)">+</button>
</div>
</div>
<div class="td">{{ item.price * item.num }}</div>
<div class="td" @click="delGoods(item.id)"><button>删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" v-model="checkAll" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box"
>总价 : ¥ <span class="price">{{ totalPrice }}</span></span
>
<!-- 结算按钮 -->
<button class="pay">结算( {{ totalCount }} )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div v-else class="empty">🛒空空如也</div>
</div>
</template>
<style scoped>
@import './style/inputnumber.css';
@import './style/index.css';
</style>
拓展知识
数据持久化localStorage
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
// 定义水果对象的类型
interface Fruit {
id: number
icon: string
isChecked: boolean
num: number
price: number
}
const defaultData = [
{
id: 1,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/ll.png',
isChecked: true,
num: 2,
price: 6
},
{
id: 2,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/smt.png',
isChecked: false,
num: 7,
price: 20
},
{
id: 3,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/tg.png',
isChecked: false,
num: 3,
price: 40
},
{
id: 4,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/xg.png',
isChecked: false,
num: 10,
price: 3
},
{
id: 5,
icon: 'https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/yl.png',
isChecked: false,
num: 20,
price: 34
}
]
// 水果列表
let fruitList = ref<Fruit[]>(defaultData)
const localStorageFruitList = localStorage.getItem('fruitList')
if (localStorageFruitList && localStorageFruitList !== '[]') {
fruitList.value = JSON.parse(localStorageFruitList)
}
// 删除商品
const delGoods = (id: number) => {
fruitList.value = fruitList.value.filter((item) => item.id !== id)
}
// 添加
const addGoods = (id: number) => {
const fruit = fruitList.value.find((item) => item.id === id)
fruit && fruit.num++
}
// 减少
const subGoods = (id: number) => {
const fruit = fruitList.value.find((item) => item.id === id)
fruit && fruit.num--
}
// 全选
const checkAll = computed({
get() {
return fruitList.value.every((item) => item.isChecked)
},
set(value) {
fruitList.value.forEach((item) => (item.isChecked = value))
}
})
// 统计选中的总数 reduce
const totalCount = computed(() => {
return fruitList.value.reduce((sum, item) => {
if (item.isChecked) {
// 选中 → 需要累加
return sum + item.num
} else {
// 没选中 → 不需要累加
return sum
}
}, 0)
})
// 总计选中的总价 num * price
const totalPrice = computed(() => {
return fruitList.value.reduce((sum, item) => {
if (item.isChecked) {
return sum + item.num * item.price
} else {
return sum
}
}, 0)
})
// 数据持久化方法
const saveData = () => {
localStorage.setItem('fruitList', JSON.stringify(fruitList.value))
}
const getData = () => {
const fruitListStr = localStorage.getItem('fruitList')
}
// 删除持久化
const delData = () => {
localStorage.removeItem('fruitList')
}
// 清空持久化数据
const clearData = () => {
localStorage.clear()
}
// 监听
watch(() => fruitList.value, saveData, { deep: true, immediate: true })
</script>
<template>
<div class="app-container" id="app">
<!-- 顶部banner -->
<div class="banner-box">
<img src="https://hongm-1312445828.cos.ap-nanjing.myqcloud.com/sg_top.png" alt="" />
</div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div v-if="fruitList.length > 0" class="main">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div
v-for="item in fruitList"
:key="item.id"
class="tr"
:class="{ active: item.isChecked }"
>
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td">
<img :src="item.icon" alt="" />
</div>
<div class="td">{{ item.price }}</div>
<div class="td">
<div class="my-input-number">
<button :disabled="item.num <= 1" class="decrease" @click="subGoods(item.id)">
-
</button>
<span class="my-input__inner">{{ item.num }}</span>
<button class="increase" @click="addGoods(item.id)">+</button>
</div>
</div>
<div class="td">{{ item.price * item.num }}</div>
<div class="td" @click="delGoods(item.id)"><button>删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" v-model="checkAll" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box"
>总价 : ¥ <span class="price">{{ totalPrice }}</span></span
>
<!-- 结算按钮 -->
<button class="pay">结算( {{ totalCount }} )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div v-else class="empty">🛒空空如也</div>
</div>
</template>
<style scoped>
@import './style/inputnumber.css';
@import './style/index.css';
</style>