Axios 类型封装🚨
目标:改写 Axios 返回值的 TS 类型
Axios 二次封装,让 Axios 和 TS 类型组合使用时更方便。
Axios 内置类型声明解读
// 1. Axios 实例类型
export class Axios {
// ...省略
request<T = any, R = AxiosResponse<T>>(config): Promise<R>;
get<T = any, R = AxiosResponse<T>>(url: string, config): Promise<R>;
}
// 2. AxiosResponse 返回值类型
export interface AxiosResponse<T = any, D = any> {
data: T;
// ...省略
}
TS 类型升级支持
+ interface ApiRes<T> {
+ msg: string;
+ result: T;
+ }
- // 能用,但 res.data 的返回值类型为 any
- const res = await request.get("/home/category/head");
+ // 🎉恭喜已经有 TS 类型提醒了,res.data 能提示 result 和正确的类型
+ const res = await request.get<ApiRes<CategoryList>>("/home/category/head");
TS 类型进阶封装(先使用)
- 📦 课堂中先直接使用,提高开发效率。
- ⏰ 课后大家自行解读,先删除所有 TS 类型读代码,再添加 TS 类型,提升自己 TS 类型处理能力。
参考代码
src\utils\request.ts
- import axios from "axios";
+ import axios, { type Method } from "axios";
const instance = axios.create({
baseURL: "http://pcapi-xiaotuxian-front-devtest.itheima.net/",
timeout: 5000,
});
// 添加请求拦截器
instance.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
return response;
},
function (error) {
// 对响应错误做点什么
return Promise.reject(error);
}
);
+ // 后端返回的接口数据格式
+ interface ApiRes<T> {
+ msg: string;
+ result: T;
+ }
+/**
+ * axios 二次封装,整合 TS 类型
+ * @param url 请求地址
+ * @param method 请求类型
+ * @param submitData 对象类型,提交数据
+ */
+export const http = <T>(method: Method, url: string, submitData?: object) => {
+ return instance.request<ApiRes<T>>({
+ url,
+ method,
+ // 🔔 自动设置合适的 params/data 键名称,如果 method 为 get 用 params 传请求参数,否则用 data
+ [method.toUpperCase() === "GET" ? "params" : "data"]: submitData,
+ });
+};
export default instance;
使用
-import request from "@/utils/request";
+import { http } from "@/utils/request";
- const res = await request.get<ApiRes<CategoryList>>("/home/category/head");
+ const res = await http<CategoryList>("GET", "/home/category/head");
详情模块
目标:界面渲染部分我们快速准备,详情模块的重点都在组件封装。
基础布局和路由
任务目标: 完成商品详情的基础布局和路由配置
1)新建页面组件
src/views/Goods/index.vue
<script setup lang="ts">
//
</script>
<template>
<div class="xtx-goods-page">
<div class="container">
<!-- 商品信息 -->
<div class="goods-info">
<!-- 图片预览区 -->
<div class="media"></div>
<!-- 商品信息区 -->
<div class="spec"></div>
</div>
<!-- 商品详情 -->
<div class="goods-footer">
<div class="goods-article">
<!-- 商品详情 -->
<div class="goods-tabs"></div>
</div>
<!-- 24热榜+专题推荐 -->
<div class="goods-aside"></div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.container {
margin-top: 20px;
}
.goods-info {
min-height: 600px;
background: #fff;
display: flex;
.media {
width: 580px;
height: 600px;
padding: 30px 50px;
}
.spec {
flex: 1;
padding: 30px 30px 30px 0;
}
}
.goods-footer {
display: flex;
margin-top: 20px;
.goods-article {
width: 940px;
margin-right: 20px;
}
.goods-aside {
width: 280px;
min-height: 1000px;
}
}
.goods-tabs {
min-height: 600px;
background: #fff;
}
.goods-warn {
min-height: 600px;
background: #fff;
margin-top: 20px;
}
</style>
2)路由配置
src/router/index.ts
const routes = [
{
path: '/',
component: Layout,
children: [
// ...
{
path: '/goods/:id',
component: () => import('@/views/Goods/index.vue')
}
]
}
]
3)新鲜好物添加路由跳转链接,测试路由跳转
商品详情 Store 和 类型声明
定义 Store
定义新 Store : src\store\modules\goods.ts
import { defineStore } from 'pinia';
export const useGoodsStore = defineStore('goods', () => {
// 记得 return
return {};
});
合并新 Store: src\store\index.ts
export * from './modules/goods';
定义类型声明
新建类型声明文件:src\types\modules\goods.d.ts
// 商品详情类型声明文件
合并类型声明:src\types\index.d.ts
// 统一导出所有类型文件
export * from "./api/goods";
获取商品详情数据
步骤
-
获取路由 id 参数
-
在组件中直接调用 axios 发送请求
-
准备 TS 类型声明文件
-
保存后端返回数据,并指定TS类型(有更好的提示)
商品详情接口
基本信息
Path: /goods
Method: GET
接口描述:
规格集合一定要和 skus 集合下的 specs 顺序保持一致
请求参数
Query
| 参数名称 | 是否必须 | 示例 | 备注 |
|---|---|---|---|
| id | 是 | 3995885 | 商品id |
发送请求
实现步骤
store 中封装 请求方法
// 根据id获取商品详情
const getGoodsDetail = async (id: string) => {
const res = await http('GET', '/goods', { id: id });
console.log('GET', '/goods', res);
};
// 记得 return
在组件 setup 中获取商品详情数据
<script setup lang="ts">
import { useGoodsStore } from '@/store';
import { useRoute } from 'vue-router';
// 获取 goods Store
const goods = useGoodsStore();
// 获取 路由信息
const route = useRoute();
// 调用获取商品详情方法
// 注意事项:
// 路由参数类型是联合类型,需通过 as 断言修正类型
goods.getGoodsDetail(route.params.id as string);
</script>
store 中指定类型
const goodsDetail = ref<GoodsDetail>();
const getGoodsDetail = async (id: string) => {
const res = await http<GoodsDetail>("GET", "/goods", { id: id });
console.log("/goods", res.data.result);
goodsDetail.value = res.data.result;
};
// 记得 return
商品信息渲染
静态结构准备-CV
src\views\Goods\index.vue,替换原本的 template 和 style
<template>
<div class="xtx-goods-page">
<div class="container">
<!-- 商品信息 -->
<div class="goods-info">
<div class="media">
<!-- 图片预览区 -->
<div class="goods-image">
<!-- 图片预览组件 -->
</div>
<!-- 统计数量 -->
<ul class="goods-sales">
<li>
<p>销量人气</p>
<p>100+</p>
<p><i class="iconfont icon-task-filling"></i>销量人气</p>
</li>
<li>
<p>商品评价</p>
<p>200+</p>
<p><i class="iconfont icon-comment-filling"></i>查看评价</p>
</li>
<li>
<p>收藏人气</p>
<p>80+</p>
<p><i class="iconfont icon-favorite-filling"></i>收藏商品</p>
</li>
<li>
<p>品牌信息</p>
<p>90+</p>
<p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p>
</li>
</ul>
</div>
<!-- 商品信息区 -->
<div class="spec">
<!-- 商品主要信息 -->
<div class="goods-main">
<p class="g-name">这是商品标题</p>
<p class="g-desc">这是商品描述</p>
<p class="g-desc">这是选中的商品规格</p>
<p class="g-price">
<span>商品现在的价钱</span>
<span>商品原来的价格</span>
</p>
<div class="g-service">
<dl>
<dt>促销</dt>
<dd>12月好物放送,App领券购买直降120元</dd>
</dl>
<dl>
<dt>配送</dt>
<dd>至</dd>
<dd>
<XtxCity />
</dd>
</dl>
<dl>
<dt>服务</dt>
<dd>
<span>无忧退货</span>
<span>快速退款</span>
<span>免费包邮</span>
<a href="javascript:;">了解详情</a>
</dd>
</dl>
</div>
</div>
<!-- 规格选择组件 -->
<!-- 数量选择组件 -->
<!-- 按钮组件 -->
</div>
</div>
<!-- 商品详情 -->
<div class="goods-footer">
<div class="goods-article">
<!-- 商品详情 -->
<div class="goods-tabs"></div>
</div>
<!-- 24热榜+专题推荐 -->
<div class="goods-aside"></div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.container {
margin-top: 20px;
}
// 商品信息
.goods-info {
min-height: 600px;
background: #fff;
display: flex;
.media {
width: 580px;
height: 600px;
padding: 30px 50px;
}
.spec {
flex: 1;
padding: 30px 30px 30px 0;
}
}
// 图片预览区
.goods-image {
width: 480px;
height: 400px;
background-color: #eee;
}
// 统计数量
.goods-sales {
display: flex;
width: 400px;
align-items: center;
text-align: center;
height: 140px;
li {
flex: 1;
position: relative;
~ li::after {
position: absolute;
top: 10px;
left: 0;
height: 60px;
border-left: 1px solid #e4e4e4;
content: '';
}
p {
&:first-child {
color: #999;
}
&:nth-child(2) {
color: @priceColor;
margin-top: 10px;
}
&:last-child {
color: #666;
margin-top: 10px;
i {
color: @xtxColor;
font-size: 14px;
margin-right: 2px;
}
&:hover {
color: @xtxColor;
cursor: pointer;
}
}
}
}
}
// 商品信息区
.spec {
.g-name {
font-size: 22px;
}
.g-desc {
color: #999;
margin-top: 10px;
}
.g-price {
margin-top: 10px;
span {
&::before {
content: '¥';
font-size: 14px;
}
&:first-child {
color: @priceColor;
margin-right: 10px;
font-size: 22px;
}
&:last-child {
color: #999;
text-decoration: line-through;
font-size: 16px;
}
}
}
.g-service {
background: #f5f5f5;
width: 500px;
padding: 20px 10px 0 10px;
margin-top: 10px;
dl {
padding-bottom: 20px;
display: flex;
align-items: center;
dt {
width: 50px;
color: #999;
}
dd {
color: #666;
&:last-child {
span {
margin-right: 10px;
&::before {
content: '•';
color: @xtxColor;
margin-right: 2px;
}
}
a {
color: @xtxColor;
}
}
}
}
}
}
.goods-footer {
display: flex;
margin-top: 20px;
.goods-article {
width: 940px;
margin-right: 20px;
}
.goods-aside {
width: 280px;
min-height: 1000px;
}
}
.goods-tabs {
min-height: 600px;
background: #fff;
}
.goods-warn {
min-height: 600px;
background: #fff;
margin-top: 20px;
}
</style>
商品信息渲染
任务目标: 按照功能渲染商品信息。
商品信息区
<!-- 商品信息区 -->
<div class="spec">
<div class="goods-main">
<p class="g-name">{{ goods.goodsDetail?.name }}</p>
<p class="g-desc">{{ goods.goodsDetail?.desc }}</p>
<p class="g-desc">这是选中的商品规格</p>
<p class="g-price">
<span>{{ goods.goodsDetail?.price }}</span>
<span>{{ goods.goodsDetail?.oldPrice }}</span>
</p>
...
storeToRefs 应用
<script setup lang="ts">
import { storeToRefs } from 'pinia';
// ...省略其他代码
// 👍解构依旧能保持响应式
const { goodsDetail } = storeToRefs(goods);
</script>
<!-- 商品信息区 -->
<div class="spec">
<div class="goods-main">
<p class="g-name">{{ goodsDetail?.name }}</p>
<p class="g-desc">{{ goodsDetail?.desc }}</p>
<p class="g-desc">这是选中的商品规格</p>
<p class="g-price">
<span>{{ goodsDetail?.price }}</span>
<span>{{ goodsDetail?.oldPrice }}</span>
</p>
...
添加 loading 效果
-
为了使用方便,商品数据在渲染是添加
v-if条件,使用数据时省略大量的?.写法。 -
同时增强用户体验,添加
loading效果。
<template>
<div class="xtx-goods-page">
<div class="container">
<!-- 商品信息 -->
- <div class="goods-info">
+ <div v-if="goodsDetail" class="goods-info">
...
+ <div v-else class="goods-info xtx-loading"></div>
...
</template>
<style scoped lang="less">
// ...
+ .goods-info.xtx-loading {
+ background: #fff url('@/assets/images/loading.gif') no-repeat center;
+ }
</style>
图片预览渲染
任务目标:通过图片预览组件,实现商品图片预览功能。
<!-- 图片预览区 -->
<div class="media">
<!-- 图片预览区 -->
<XtxImageView :image-list="goodsDetail.mainPictures" />
</div>
按钮组件实现🚨
任务目标: 实现按钮组件的封装
组件用法
先直接使用组件库提供的组件,尝试自己实现组件的封装。
<!-- 按钮组件 -->
<XtxButton type="primary" size="middle" style="margin-top: 20px">
加入购物车
</XtxButton>
组件开发步骤
- 准备静态结构
- 分析按钮组件的自定义属性
defineProps定义 Props 接收值- 模板中使用父组件传过来的值设置按钮样式
静态结构-CV
新建组件:src\views\Test\components\MyButton.vue
<script setup lang="ts">
// 步骤:
// 1. 准备静态结构
// 2. 分析按钮组件的自定义属性
// 3. `defineProps` 定义 Props 接收值
// 4. 模板中使用父组件传过来的值设置按钮样式
</script>
<template>
<button class="xtx-button ellipsis" :class="`middle default`">
<slot></slot>
</button>
</template>
<style scoped lang="less">
// 基于类名定义一些和定制样式无关的样式
.xtx-button {
appearance: none;
border: none;
outline: none;
background: #fff;
text-align: center;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
}
// ---------大小类名-------------
// 大
.large {
width: 240px;
height: 50px;
font-size: 16px;
}
// 中
.middle {
width: 180px;
height: 50px;
font-size: 16px;
}
// 小
.small {
width: 100px;
height: 32px;
}
//超小
.mini {
width: 60px;
height: 32px;
}
// ---------颜色类名----------
// 默认色
.default {
border-color: #e4e4e4;
color: #666;
}
// 确认
.primary {
border-color: @xtxColor;
background-color: @xtxColor;
color: #fff;
}
// 普通
.plain {
border-color: @xtxColor;
color: @xtxColor;
background-color: lighten(@xtxColor, 50%);
}
// 灰色
.gray {
border-color: #ccc;
background-color: #ccc;
color: #fff;
}
</style>
Props 默认值
写法1 - 响应性语法糖(实验性最新写法)
- 实验性写法
Vue版本要求vue@3.2.25+,需修改Vite的配置,并且修改EsLint检查规则后才能使用。 - 实验性写法很方便,属于响应性语法糖,但官方还没正式发布。
<script setup lang="ts">
// 响应性语法糖(实验性)写法 `Vue` 版本要求 `vue@3.2.25+`
// 🚨 需修改配置后才能使用:
// 1. vite.config.ts 开启响应性语法糖(实验性)
// 2. .eslintrc.cjs 关闭检查规则
// 注意:实验性写法在 yarn dev 时会有黄色提醒,我们知道自己再干什么,忽略黄色提醒即可
const { type = 'default', size = 'middle' } = defineProps<{
type?: 'default' | 'primary' | 'plain' | 'gray';
size?: 'large' | 'middle' | 'small' | 'mini';
}>();
</script>
vite.config.ts
import { fileURLToPath, URL } from 'url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
+ // 开启<实验性>的响应性语法糖来解决 prop 定义默认值的问题
+ reactivityTransform: true,
}),
vueJsx(),
],
});
关闭 eslint 规则
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: true,
// 自定义规则
rules: {
// 关闭多词组合的组件命名要求: off 关闭
'vue/multi-word-component-names': 'off',
+ // 关闭 props 不能解构的规则: off 关闭
+ 'vue/no-setup-props-destructure': 'off',
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
],
};
写法2 - 类型断言写法(早期写法)
PropType类型断言写法同样适用于选项式API和组合式API
<script setup lang="ts">
// 设置 Props 默认值
// PropType 类型定义写法同样适用于选项式 API
import type { PropType } from "vue";
defineProps({
size: {
type: String as PropType<"mini" | "small" | "middle" | "large">,
default: "small",
},
type: {
type: String as PropType<"gray" | "plain" | "primary" | "default">,
default: "gray",
},
});
</script>
常见问题:
- 问:开发者如何选择设置默认值的写法?
- 答:
- 实验性写法最方便,新项目推荐,但要求
Vue版本较新,目前还没完全定稿,所以终端有提醒。 - 正式项目建议按团队习惯来选择,老项目用类型断言写法较为常见。
- 实验性写法最方便,新项目推荐,但要求
数量组件实现🚨🚨
任务目标: 实现商品的数量操作组件的封装
组件用法
先直接使用组件库提供的组件,尝试自己实现组件的封装。
const count = ref(1);
<!-- 数量选择组件 -->
<XtxCount
is-label
v-model="count"
:min="1"
:max="goodsDetail.inventory"
/>
v-model 语法糖
目标:熟悉 v-model 双向绑定语法糖原理,并实现自定义组件双向绑定。
父组件 v-model 使用
<!-- v-model 语法糖由 :modelValue 和 @update:modelValue 两部分组合而成 -->
<MyCount v-model="count" />
<!-- 语法糖原理 -->
<MyCount :model-value="count" @update:model-value="(val) => {count = val}" />
注意事项:
- 在
Vue2中,组件 v-model 双向绑定语法糖由:value和@input两部分组合而成。 - 在
Vue3中,组件 v-model 双向绑定语法糖由:modelValue和@update:modelValue两部分组合而成。 - 原生表单标签 input、 select 、 textarea 使用不受影响,只影响自定义组件封装。
- vue3文档:cn.vuejs.org/guide/compo…
子组件 v-model 封装
- 子组件处理
v-model需要定义modelValue属性和update:modelValue事件。
<script setup lang="ts">
// 接收 v-model 的自定义属性 modelValue
const { modelValue } = defineProps<{
modelValue: number;
}>();
// 注册 v-model 的自定义事件 update:modelValue
const emit = defineEmits<{
(event: 'update:modelValue', val: number): void;
}>();
</script>
组件开发步骤
- 搭建组件静态结构并实现增减逻辑
- 通过 props 传入控制最大和最小值
- 实现组件的
v-model双向绑定 (难点)
<MyCount v-model="count"><MyCount> 数字框
希望实现 v-model 双向绑定
1. 父组件中,修改了,设置了 num 值, 子组件需要实时更新
2. 子组件中,点击了 + -, 调整了数值, 希望绑定的 num 值也能自动更新
静态结构-CV
新建组件:src\views\Test\components\MyCount.vue
<script setup lang="ts">
// 需求:
// 1. 显示标签和数量
// 2. 加号和最大值处理
// 3. 减号和最小值处理
// 4. props 默认值处理
</script>
<template>
<div class="xtx-numbox">
<div class="label">数量</div>
<div class="numbox">
<a href="javascript:;">-</a>
<input type="text" readonly :value="1" />
<a href="javascript:;">+</a>
</div>
</div>
</template>
<style scoped lang="less">
.xtx-numbox {
display: flex;
align-items: center;
.label {
width: 60px;
color: #999;
padding-left: 10px;
}
.numbox {
width: 120px;
height: 30px;
border: 1px solid #e4e4e4;
display: flex;
> a {
width: 29px;
line-height: 28px;
text-align: center;
background: #f8f8f8;
font-size: 16px;
color: #666;
&:first-of-type {
border-right: 1px solid #e4e4e4;
}
&:last-of-type {
border-left: 1px solid #e4e4e4;
}
}
> input {
width: 60px;
padding: 0 5px;
text-align: center;
color: #666;
}
}
}
</style>
显示标签和数量
<script setup lang="ts">
defineProps<{
// v-model 语法糖的 props
modelValue: number;
// 是否显示标签
isLabel?: boolean;
}>();
</script>
<template>
<div class="xtx-numbox">
<div class="label" v-if="isLabel">数量</div>
<div class="numbox">
<a href="javascript:;">-</a>
<input type="text" readonly :value="modelValue" />
<a href="javascript:;">+</a>
</div>
</div>
</template>
测试
<script setup lang="ts">
import { ref } from 'vue';
import MyCount from './components/MyCount.vue';
const count = ref(1);
</script>
<template>
<MyCount v-model="count" />
</template>
加号和最大值处理
<script setup lang="ts">
const {
modelValue,
isLabel = false,
max = 100,
} = defineProps<{
// v-model 语法糖的 props
modelValue: number;
// 是否显示标签
isLabel?: boolean;
// 最大值
max?: number;
}>();
// v-model 语法糖的 update:modelValue
const emit = defineEmits<{
(event: 'update:modelValue', value: number): void;
}>();
// 加号和最大值处理
const add = () => {
// 临时加
const temp = modelValue + 1;
// 是否超过最大值,超过直接 return 退出,不更新父组件的数据
if (temp > max) return;
// 更新 v-model 绑定的数据
emit('update:modelValue', temp);
};
</script>
<template>
<div class="xtx-numbox">
<div v-if="isLabel" class="label">数量</div>
<div class="numbox">
<a href="javascript:;">-</a>
<input type="text" readonly :value="modelValue" />
<a href="javascript:;" @click="add">+</a>
</div>
</div>
</template>
减号和最小值处理
- 参考加号和最大值处理,点击减号按钮触发减少,最小值默认值为 1。
常见问题
- 问:为什么我组件数量只能加到 2 就加不动了?
- 答:解构 props 需在
vite.config.ts中开启响应性语法糖,同时修改.eslintrc.cjs规则。
商品规格组件使用
目标:商品规格处理是电商网站一个非常核心的功能模块,选择不同规格会变化价格和库存最大值。
测试商品id: 1379052170040578049 和 1369155859933827074
理解基础 sku 概念
官方话术:
-
SPU(Standard Product Unit):标准化产品单元,展示用商品 id 。(
Iphone12=> SPU ) -
SKU(Stock Keeping Unit):库存量单位,下单用 sku id。(
Iphone12 蓝色256g=> SKU )
图解:
基础用法
<script setup lang="ts">
+import type { SkuEmit } from '@/components/XtxUI/Sku/index.vue';
+const changeSku = (val: SkuEmit) => {
+ console.log('当前选择的sku信息为', val);
+};
</script>
<template>
...
<!-- 商品信息区 -->
<div class="spec">
<!-- 商品主要信息 -->
<div class="goods-main">
...
</div>
+ <!-- 📌规格选择组件 -->
+ <XtxSku :goods="goodsDetail" @change="changeSku" />
<!-- 数量选择组件 -->
<!-- 按钮组件 -->
</div>
...
</template>
业务整合
<script setup lang="ts">
// 获取 XtxSku 组件选中的商品信息
const skuId = ref(''); // 🔔存储 skuId 用于加入购物车
const specsText = ref(''); // 🔔商品规格说明
const changeSku = (val: SkuEmit) => {
// 商品规格 和 skuId 更新
skuId.value = val.skuId || '';
specsText.value = val.specsText || '';
// 类型守卫,排除掉 undefined 情况
if (goodsDetail.value && val.skuId) {
// 根据选中规格,更新商品库存,最新价格,原始价格
goodsDetail.value.inventory = val.inventory;
goodsDetail.value.price = val.price;
goodsDetail.value.oldPrice = val.oldPrice;
}
};
</script>
<template>
<!-- 商品描述 -->
<p class="g-desc">规格:{{ specsText }}</p>
</template>
Store 数据缓存问题
store 数据组件卸载不会重置,最终导致商品详情页 v-if="goodsDetail" 工作异常。
src\store\modules\goods.ts
export const useGoodsStore = defineStore('goods', () => {
// 根据id获取商品详情
const goodsDetail = ref<GoodsDetail>();
const getGoodsDetail = async (id: string) => {
+ // 🐛修复BUG,先清空数据,再请求新数据,否则页面中的 v-if="goodsDetail" 工作异常
+ goodsDetail.value = undefined;
const res = await http<GoodsDetail>('GET', '/goods', { id: id });
goodsDetail.value = res.data.result;
};
// 记得 return
return {
goodsDetail,
getGoodsDetail,
};
});
详情展示实现-课后练习
说明:实现商品详情信息的展示,自己课后实现即可。
实现步骤
- 把详情数据通过 props 传入
- 接收数据并渲染模板
代码落地
1)传入数据
<!-- 商品详情 -->
<GoodsDetail :goods="goods"/>
2)接收并渲染数据
<script setup lang="ts">
import type { Goods } from "@/types";
interface Props {
goods: Goods;
}
defineProps<Props>();
</script>
<template>
<div class="goods-tabs">
<nav>
<a>商品详情</a>
</nav>
<div class="goods-detail">
<!-- 属性 -->
<ul class="attrs">
<li v-for="item in goods.details.properties" :key="item.value">
<span class="dt">{{ item.name }}</span>
<span class="dd">{{ item.value }}</span>
</li>
</ul>
<!-- 图片 -->
<img
v-for="item in goods.details.pictures"
:key="item"
:src="item"
alt=""
/>
</div>
</div>
</template>
<style scoped lang="less">
.goods-tabs {
min-height: 600px;
background: #fff;
nav {
height: 70px;
line-height: 70px;
display: flex;
border-bottom: 1px solid #f5f5f5;
a {
padding: 0 40px;
font-size: 18px;
position: relative;
> span {
color: @priceColor;
font-size: 16px;
margin-left: 10px;
}
}
}
}
.goods-detail {
padding: 40px;
.attrs {
display: flex;
flex-wrap: wrap;
margin-bottom: 30px;
li {
display: flex;
margin-bottom: 10px;
width: 50%;
.dt {
width: 100px;
color: #999;
}
.dd {
flex: 1;
color: #666;
}
}
}
> img {
width: 100%;
}
}
</style>
热榜区域实现-课后练习
说明:列表渲染部分,自己课后实现即可。
任务目标: 展示24小时热榜商品,和周热榜商品
实现步骤
- 定义一个组件,完成多个组件展现型态,根据传入组件的类型决定
- 1: 代表24小时热销榜
- 2: 代表周热销榜
- 3: 代表总热销榜
- 根据传入的type获取数据,完成商品展示和标题样式的设置
1. 定义组件
src/views/Goods/components/goods-hot.vue
<script setup lang="ts">
import GoodsItem from "@/views/Category/components/goods-item.vue";
import type { PropType } from "vue";
const { type = 1 } = defineProps<{
type?: number
}>();
// 标题对象
const titleObj = {
1: "24小时热销榜",
2: "周热销榜",
3: "总热销榜",
};
</script>
<template>
<div class="goods-hot">
<h3>{{ titleObj[props.type] }}</h3>
<div class="goods-list">
<!-- 商品区块 -->
<GoodsItem v-for="(item, index) in 4" :key="index" />
</div>
</div>
</template>
<style scoped lang="less">
.goods-hot {
background-color: #fff;
margin-bottom: 20px;
h3 {
height: 70px;
background: @helpColor;
color: #fff;
font-size: 18px;
line-height: 70px;
padding-left: 25px;
margin-bottom: 10px;
font-weight: normal;
}
.goods-list {
display: flex;
flex-direction: column;
align-items: center;
}
}
</style>
2)使用组件
src/views/Goods/index.vue
import GoodsHot from './components/goods-hot.vue'
<!-- 24热榜+专题推荐 -->
<div class="goods-aside">
<GoodsHot :type="1" />
<GoodsHot :type="2" />
<GoodsHot :type="3" />
</div>
2. 获取数据渲染组件
src/views/goods/components/goot-hot.vue
<script setup lang="ts">
// 🚨导入时出现命名冲突,可以通过 as 重命名
import type { GoodsItem as Item } from "@/types";
import { http } from "@/utils/request";
import GoodsItem from "@/views/Category/components/goods-item.vue";
import { onMounted, ref, type PropType } from "vue";
import { useRoute } from "vue-router";
const props = defineProps({
type: {
type: Number as PropType<1 | 2 | 3>,
default: 1,
},
});
// 标题对象
const titleObj = {
1: "24小时热销榜",
2: "周热销榜",
3: "总热销榜",
};
// 发送请求获取数据
const route = useRoute();
const { id } = route.params;
const list = ref<Item[]>([]);
onMounted(async () => {
const res = await http<Item[]>("GET", "/goods/hot", {
id: id,
type: props.type,
});
list.value = res.data.result;
});
</script>
<template>
<div class="goods-hot">
<h3>{{ titleObj[props.type] }}</h3>
<div class="goods-list">
<!-- 商品区块 -->
<GoodsItem v-for="item in list" :key="item.id" :goods="item" />
</div>
</div>
</template>
<style scoped lang="less">
.goods-hot {
background-color: #fff;
margin-bottom: 20px;
h3 {
height: 70px;
background: @helpColor;
color: #fff;
font-size: 18px;
line-height: 70px;
padding-left: 25px;
margin-bottom: 10px;
font-weight: normal;
}
.goods-list {
display: flex;
flex-direction: column;
align-items: center;
}
}
</style>