01_创建项目
请输入项目名称: ... 01_vue_pro
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
正在构建项目 C:\Users\41099\Desktop\飞秋共享\BK_2310_2\三阶段\04_周\01_vue_pro...
项目构建完成,可执行以下命令:
cd 01_vue_pro
npm install
npm run dev
02_项目目录介绍
- .vscode
- 当前编辑器的一些配置文件
- node_modules
- 当前项目需要使用的依赖
- public
- 当前项目需要使用的一些固定的静态资源(不会发生变化, 主要存放别人处理好的第三方文件)
- src (重要)
- assets: 当前项目使用的一些静态资源(这里的静态资源可能会发生变化, 存放自己的静态资源文件)
- components: 当前项目的公共组件
- router: 路由相关内容
- stores: pinia 状态管理相关的内容
- views: 页面文件
- App.vue: 项目根组件
- main.js: 项目入口文件
- .gitignore
- git 的配置文件
- env.d.ts
- ts 类型文件
- index.html
- 项目的基础文件
- package-lock.json
- 依赖包的详细版本与安装地址, 不重要
- package.json
- 依赖包的安装版本与项目的一起启动打包等命令
- README.md
- 项目的说明书
- tsconfig.app.json
- tsconfig.json
- tsconfig.node.json
- vite.config.js
- 项目的配置文件
03_引入 CSS 格式化文件
因为我们在书写基本结构的时候, 很多的标签是具有一些默认样式的, 如果自己去除那么会很麻烦, 所以我们借助一个 第三方包
- 安装: normalize.css
- 安装命令 npm i normalize.css
- 安装完毕后在 main.js 引入
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import "normalize.css/normalize.css";
const app = createApp(App);
app.use(router);
app.mount("#app");
04_引入 sass
需要安装 sass 安装命令 npm i sass
- sass 是 css 的一个预编译语言
- 通过 sass 能够让我们像书写 JS 一样 书写 CSS
- 如果不使用 sass 我们也可以完成页面的样式书写, 只不过语法没有那么便捷
- 借助 sass 我们可以更加简洁的书写 css 样式
- 安装完毕后, 只需要在书写 css 的 style 标签中添加一个属性 'lang=scss'
- 注意: 属性就是 'lang=scss', 不是 sass
05_引入 vant 组件库
安装:
npm i vant
-
按照 按需引入的方式 使用 vant
-
安装插件
npm i @vant/auto-import-resolver unplugin-vue-components -D
-
根据创建的方式配置项目, 当前项目使用 vite
import vue from "@vitejs/plugin-vue"; import Components from "unplugin-vue-components/vite"; import { VantResolver } from "@vant/auto-import-resolver"; export default { plugins: [ vue(), Components({ resolvers: [VantResolver()], }), ], };
-
使用组件
<template> <van-button type="primary" /> </template> -
引入函数组件的样式
// Toast import { showToast } from "vant"; import "vant/es/toast/style"; // Dialog import { showDialog } from "vant"; import "vant/es/dialog/style"; // Notify import { showNotify } from "vant"; import "vant/es/notify/style"; // ImagePreview import { showImagePreview } from "vant"; import "vant/es/image-preview/style";
-
06_引入首页图标
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<!-- 导入 tabBar 上的 4 个小图标 -->
<link
rel="stylesheet"
href="https://at.alicdn.com/t/font_3120366_9utchxxpyz.css"
/>
</head>
<body>
<span class="iconfont icon-shouye"></span>
<span class="iconfont icon-fenlei"></span>
<span class="iconfont icon-gouwuche"></span>
<span class="iconfont icon-My"></span>
</body>
07_处理路由_页面布局
处理路由
import { createRouter, createWebHashHistory } from "vue-router";
import HomeView from "../views/home/index.vue";
const routes = [
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/kind",
name: "kind",
component: () => import("../views/kind/index.vue"),
},
{
path: "/cart",
name: "cart",
component: () => import("../views/cart/index.vue"),
},
{
path: "/my",
name: "my",
component: () => import("../views/my/index.vue"),
},
];
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
});
export default router;
页面布局
<script setup lang="ts"></script>
<template>
<div class="container">
<!-- 4个主页面的展示 -->
<router-view></router-view>
<!-- 底部导航 -->
<footer class="footer">
<router-link to="/">
<span class="iconfont icon-shouye"></span>
<p>首页</p>
</router-link>
<router-link to="/kind">
<span class="iconfont icon-fenlei"></span>
<p>分类</p>
</router-link>
<router-link to="/cart">
<span class="iconfont icon-gouwuche"></span>
<p>购物车</p>
</router-link>
<router-link to="/my">
<span class="iconfont icon-My"></span>
<p>我的</p>
</router-link>
</footer>
</div>
</template>
<style lang="scss">
html,
body,
#app,
.continer {
width: 100%;
height: 100%;
}
html {
font-size: 26.6666666666666vw;
body {
font-size: 14px;
}
.container {
display: flex;
flex-direction: column;
.box {
flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
.header {
width: 100%;
height: 0.44rem;
background-color: #f66;
line-height: 0.44rem;
text-align: center;
color: white;
}
.content {
overflow: auto;
}
}
.footer {
height: 0.5rem;
border-top: 1px solid gray;
display: flex;
a {
flex: 1;
text-decoration: none;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
color: black;
p {
margin: 0;
}
}
a.router-link-active {
color: #f66;
}
}
}
}
</style>
08_引入 axios
// 基于 axios 封装一个我们自己的 'ajax'
// 1. 引入 axios
import axios from "axios";
import type { AxiosInstance } from "axios";
const instance: AxiosInstance = axios.create({
baseURL: "http://121.89.205.189:3000/api",
timeout: 60000,
});
// 添加请求拦截器
instance.interceptors.request.use(
function (config) {
if (config.url !== "/admin/login") {
}
return config;
},
function (error) {
return Promise.reject(error);
}
);
// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
if (response.data.code === "10119") {
}
return response;
},
function (error) {
return Promise.reject(error);
}
);
interface IOptions {
url: string;
method?: string;
data?: any;
}
export default function ajax(options: IOptions) {
const { url, method = "get", data = {} } = options;
const reg = /^get$/i;
if (reg.test(method)) {
return instance.get(url, {
params: data,
});
} else {
return instance.post(url, data);
}
}
09_添加轮播图
<template>
<div class="box">
<div class="header">首页</div>
<div class="content">
<!-- 1. 商品轮播图 -->
<van-swipe
class="my-swipe"
:autoplay="3000"
indicator-color="white"
>
<van-swipe-item v-for="item in bannerList" :key="item.bannerid">
<van-image :src="item.img" />
</van-swipe-item>
</van-swipe>
</div>
</div>
</template>
<script setup lang="ts">
import { getBannerList } from "@/api/home";
import { onMounted, ref } from "vue";
import type { Ref } from "vue";
interface IBannerList {
alt: string;
bannerid: string;
flag: boolean;
img: string;
link: string;
}
const bannerList: Ref<IBannerList[]> = ref([]);
onMounted(async () => {
const { data } = await getBannerList();
bannerList.value = data.data;
});
</script>
<style>
.van-swipe {
height: 1.6rem;
.van-image {
width: 100%;
height: 100%;
}
}
</style>
10_子级路由以及footer展示需要用到路由元信息
1.router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";
import HomeView from "../views/home/index.vue";
const routes = [
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/kind",
name: "kind",
component: () => import("../views/kind/index.vue"),
},
{
path: "/cart",
name: "cart",
component: () => import("../views/cart/index.vue"),
},
{
path: "/my",
name: "my",
component: () => import("../views/my/index.vue"),
},
{
path: "/login",
name: "login",
// 利用 路由元信息 帮助我们区分到底那个页面需要展示 底部导航
meta: {
// 当前的对象内部可以书写我们的路由元信息
hidden: true, // 路由元信息说白了就是给路由添加一些特殊的标识
},
component: () => import("../views/login/index.vue"),
},
{
path: "/register",
name: "register",
meta: {
hidden: true,
},
component: () => import("../views/register/index.vue"),
children: [
{
path: 'no1',
name: 'no1',
component: () => import("../views/register/no1.vue"),
},
{
path: 'no2',
name: 'no2',
component: () => import("../views/register/no2.vue"),
},
{
path: 'no3',
name: 'no3',
component: () => import("../views/register/no3.vue"),
}
]
},
];
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
});
export default router;
2.APP.vue
<template>
<div class="container">
<!-- 4个主页面的展示 -->
<router-view></router-view>
<!-- 底部导航 -->
<!-- <footer class="footer" v-if="!route.meta.hidden"> -->
<footer class="footer" v-if="!$route.meta.hidden">
<router-link to="/">
<span class="iconfont icon-shouye"></span>
<p>首页</p>
</router-link>
<router-link to="/kind">
<span class="iconfont icon-fenlei"></span>
<p>分类</p>
</router-link>
<router-link to="/cart">
<span class="iconfont icon-gouwuche"></span>
<p>购物车</p>
</router-link>
<router-link to="/my">
<span class="iconfont icon-My"></span>
<p>我的</p>
</router-link>
</footer>
</div>
</template>
11_注册_验证手机号
<template>
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="phoneNum"
name="手机号"
label="手机号"
placeholder="手机号"
:rules="[{ required: true, message: '请填写手机号' }]"
/>
</van-cell-group>
<div style="margin: 16px">
<van-button
:disabled="buttonFlag"
round
block
type="primary"
native-type="submit"
>
验证手机号
</van-button>
</div>
</van-form>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import type { Ref } from "vue";
import { docheckphone } from "@/api/register";
import { showSuccessToast, showFailToast } from "vant";
import { useRouter } from "vue-router";
// 创建一个 选项式 API 中的 this.$router
const router = useRouter();
// 存储用户输入的手机号
const phoneNum: Ref<string> = ref("");
// 当前计算属性, 会根据用户输入的手机号 决定 按钮能否被点击
const buttonFlag = computed(() => {
// return true 按钮禁用, 否则按钮恢复使用
// return false
return !/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(phoneNum.value);
});
// 点击 验证手机号 按钮
async function onSubmit() {
// console.log("验证手机号", phoneNum.value);
const { data } = await docheckphone({
tel: phoneNum.value,
});
if (data.code !== "200") {
// 提示用户手机号已被注册
return showFailToast(data.message);
}
// 提醒用户手机号正确
showSuccessToast(data.message);
// 跳转到第二个注册页面
router.push("/register/no2");
}
</script>
12_注册_提交验证码
<template>
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="msg"
name="验证码"
label="验证码"
placeholder="验证码"
:rules="[{ required: true, message: '请填写验证码' }]"
>
<template #button>
<van-button
@click="getMsg"
size="small"
:disabled="btnTime !== 5"
>{{ btnText }}</van-button
>
</template>
</van-field>
</van-cell-group>
<div style="margin: 16px">
<van-button
:disabled="btnFlag"
round
block
type="primary"
native-type="submit"
>
提交验证码
</van-button>
</div>
</van-form>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import type { Ref } from "vue";
import { dosendmsgcode, docheckcode } from "@/api/register";
import { showSuccessToast, showFailToast } from "vant";
import { useRouter } from "vue-router";
// 创建一个 选项式 API 中的 this.$router
const router = useRouter();
// 存储用户输入的验证码
const msg: Ref<string> = ref("");
// 提交验证码
async function onSubmit() {
const { data } = await docheckcode({
tel: window.localStorage.getItem("phoneNum"),
telcode: msg.value,
});
if (data.code !== "200") {
// 提示用户验证码错误
return showFailToast(data.message);
}
// 提醒用户 验证码 通过
showSuccessToast(data.message);
// 跳转到第三个注册页面
router.push("/register/no3");
}
// 获取验证码
const btnTime: Ref<number> = ref(5);
const btnText: Ref<string> = ref("获取验证码");
async function getMsg() {
btnText.value = "5s 后重新获取验证码";
btnTime.value--;
const timerId = setInterval(() => {
btnText.value = `${btnTime.value}s 后重新获取验证码`;
btnTime.value--;
if (btnTime.value === -1) {
clearInterval(timerId);
btnText.value = "获取验证码";
btnTime.value = 5;
}
}, 1000);
const { data } = await dosendmsgcode({
tel: window.localStorage.getItem("phoneNum"),
});
// console.log(data);
msg.value = data.data;
}
// 侦听验证码, 控制 提交验证码 按钮能否使用
const btnFlag: Ref<boolean> = ref(true);
watch(
msg,
() => {
// console.log("验证码: ", msg.value);
btnFlag.value = msg.value.length !== 5;
},
{
immediate: true,
}
);
</script>