Vue3 + Ts 项目

155 阅读5分钟

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_项目目录介绍

  1. .vscode
    • 当前编辑器的一些配置文件
  2. node_modules
    • 当前项目需要使用的依赖
  3. public
    • 当前项目需要使用的一些固定的静态资源(不会发生变化, 主要存放别人处理好的第三方文件)
  4. src (重要)
    • assets: 当前项目使用的一些静态资源(这里的静态资源可能会发生变化, 存放自己的静态资源文件)
    • components: 当前项目的公共组件
    • router: 路由相关内容
    • stores: pinia 状态管理相关的内容
    • views: 页面文件
    • App.vue: 项目根组件
    • main.js: 项目入口文件
  5. .gitignore
    • git 的配置文件
  6. env.d.ts
    • ts 类型文件
  7. index.html
    • 项目的基础文件
  8. package-lock.json
    • 依赖包的详细版本与安装地址, 不重要
  9. package.json
    • 依赖包的安装版本与项目的一起启动打包等命令
  10. README.md
    • 项目的说明书
  11. tsconfig.app.json
  12. tsconfig.json
  13. tsconfig.node.json
  14. 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>

13_注册_提交注册