别再踩坑了!这份 Vue3+TypeScript 项目教程,赶紧收藏!

功能演示

用户登录、商品增删改查

1.创建基于 Vite 的 Vue3 和 ts 项目

!!首先本地要安装配置 Node 环境

css 复制代码
# 创建项目
npm create vite@latest zhifou-vue3-ts -- --template vue-ts
# 安装依赖
npm install

项目目录:

css 复制代码
src/
├── assets/          # 静态资源
├── components/      # 组件
├── router/          # 路由
│   └── index.ts     # 路由入口文件
├── stores/          # Pinia状态管理
│   ├── index.ts     # Pinia入口配置
│   └── user.ts      # 用户相关状态
├── types/           # ts自定义类型
├── utils/           # 工具函数
│   └── axios.ts     # axios配置
├── views/           # 页面组件
│   ├── Home.vue     # 首页
│   └── Login.vue    # 登录页
├── App.vue          # 根组件
└── main.ts          # 入口文件

配置 vite.config.ts

  • 配置路径别名
  • 代理配置:配置后台接口请求路径
ts 复制代码
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      "@": resolve(__dirname, "src"), // 设置路径别名
    },
  },
  server: {
    port: 3000,
    proxy: {
      // 代理配置,解决跨域问题
      "/api": {
        target: "http://localhost:8083/zhifou-blog",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },
});

2.安装配置 Element Plus

cs 复制代码
npm install element-plus @element-plus/icons-vue

在 main.ts 中配置 ElementPlus

css 复制代码
import { createApp } from "vue";
import App from "./App.vue";

import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import locale from "element-plus/es/locale/lang/zh-cn";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
const app = createApp(App);

// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}
app.use(ElementPlus, { locale }).mount("#app");

3.安装配置 Vue Router

css 复制代码
npm install vue-router@4

在 main.ts 中配置路由

在 router 文件夹下新建 index.ts,然后配置路由:

ts 复制代码
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { useUserStore } from "@/store/user";
const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    redirect: "/login",
  },
  {
    path: "/login",
    name: "Login",
    component: () => import("@/views/Login.vue"),
  },
  {
    path: "/home",
    name: "Home",
    component: () => import("@/views/Home.vue"),
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

// 在之前的路由守卫基础上添加
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore();
  // 判断路由是否需要认证
  if (to.path === "/login") {
    if (userStore.isLoggedIn) {
      next("/home");
    } else {
      next();
    }
  } else {
    const token = localStorage.getItem("token");
    if (token) {
      try {
        // 获取最新用户信息
        // await userStore.getCurrentUser();
        next();
      } catch (error) {
        // 刷新失败,跳转到登录页
        next("/login");
      }
    } else {
      // 没有token,跳转到登录页
      next("/login");
    }
  }
});
export default router;

上面的例子中只配置了登录页面和主页的路由,路由守卫对登录和 token 进行了校验。

4.安装配置配置 Pinia

pinia-plugin-persistedstate 是为 Pinia 设计的持久化存储插件,主要用于解决页面刷新后状态丢失的问题。它通过自动将 Store 数据同步到 localStorage、sessionStorage 或Cookie 中实现持久化,并在应用初始化时从存储中恢复状态。

css 复制代码
npm install pinia pinia-plugin-persistedstate

在 store 文件夹下新建 index.ts 文件,然后配置 pina

ts 复制代码
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export default pinia

在 main.ts 中配置 pina

5. 安装配置 axios

css 复制代码
npm install axios

在 utils 文件夹在新建 axios.ts 文件,配置 axios:

ts 复制代码
import axios, {
  AxiosInstance,
  InternalAxiosRequestConfig,
  AxiosResponse,
} from "axios";
// 创建axios实例
const service: AxiosInstance = axios.create({
  baseURL: "/api", // API基础路径
  timeout: 10000, // 请求超时时间
  headers: {
    "Content-Type": "application/json;charset=utf-8",
  },
});

// 请求拦截器
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    // 在发送请求之前做些什么
    const token = localStorage.getItem("token");
    if (token) {
      config.headers["token"] = token;
    }
    return config;
  },
  (error: any) => {
    // 返回异常
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, message } = response.data;
    if (code === 200) {
      return response;
    } else {
      // 处理业务错误
      return Promise.reject(new Error(message || "Error"));
    }
  },
  (error: any) => {
    if (error.response?.status === 401) {
      localStorage.removeItem("token");
    }
    return Promise.reject(error);
  }
);

export default service;

上面我们主要配置了axios 的请求拦截器和响应拦截器。请求拦截器主要是在请求头中添加了 token。响应拦截器主要是返回响应信息和异常信息。

这里我们要注意一点,axios 的响应拦截器类型是 AxiosResponse,返回的数据格式是:

也就是说后台返回的数据实际在 res 的 data 里面。

6. 封装 http 请求

在 /src/api 文件夹下面新建 index.ts 文件:

ts 复制代码
import axios from "../utils/axios";
import { ApiResponse, PageParams, PageResponse } from "../types";

/**
 * 通用GET请求
 * @param url 请求地址
 * @param params 请求参数
 * @returns 响应数据
 */
export const get = async <T>(url: string, params?: any): Promise<T> => {
  const res = await axios.get<ApiResponse<T>>(url, { params });
  return res.data.data;
};

/**
 * 通用POST请求
 * @param url 请求地址
 * @param data 请求体数据
 * @returns 响应数据
 */
export const post = async <T>(url: string, data?: any): Promise<T> => {
  const res = await axios.post<ApiResponse<T>>(url, data);
  return res.data.data;
};

/**
 * 通用PUT请求
 * @param url 请求地址
 * @param data 请求体数据
 * @returns 响应数据
 */
export const put = async <T>(url: string, data?: any): Promise<T> => {
  const res = await axios.put<ApiResponse<T>>(url, data);
  return res.data.data;
};

/**
 * 通用DELETE请求
 * @param url 请求地址
 * @param params 请求参数
 * @returns 响应数据
 */
export const del = async <T>(url: string, params?: any): Promise<T> => {
  const res = await axios.delete<ApiResponse<T>>(url, { params });
  return res.data.data;
};

/**
 * 分页请求
 * @param url 请求地址
 * @param params 分页参数
 * @returns 分页响应数据
 */
export const getPage = async <T>(
  url: string,
  params: PageParams
): Promise<PageResponse<T>> => {
  return get<PageResponse<T>>(url, params);
};

这里我们拿 get 请求举例,前面 axios 响应拦截器中返回的是 response,这里封装之后的 get 请求返回的是 res.data.data。如果后台返回的数据是:

json 复制代码
{code:200,data:{username:'zhifou'},messeg:'请求成功'}

那么经过封装之后的 get 请求取到的就是:

json 复制代码
{username:'zhifou'}

7. 用户登录

7.1 创建用户相关的 API

在 /src/api 文件夹下新建 user.ts 文件

ts 复制代码
import { get, post } from "./index";
import { LoginParams, LoginResponse, User } from "../types";

/**
 * 用户登录
 * @param params 登录参数
 * @returns 登录结果
 */
export const userLogin = async (params: LoginParams) => {
  return post<LoginResponse>("/user/login", params);
};

/**
 * 获取当前用户信息
 * @returns 用户信息
 */
export const getCurrentUserInfo = async () => {
  return get<User>("/user/currentUserInfo");
};

7.2 创建用户状态管理

在用户的 store 里面我们主要存储用户信息、token、用户登录状态。

ts 复制代码
import { defineStore } from "pinia";
import { User, LoginParams, UserState } from "../types";
import { userLogin, getCurrentUserInfo } from "../api/user";
import router from "../router";

export const useUserStore = defineStore("user", {
  state: (): UserState => ({
    userInfo: null,
    token: null,
    isLoggedIn: false,
  }),

  getters: {
    // 获取用户名
    getUsername: (state) => state.userInfo?.username || "",
  },

  actions: {
    // 登录
    async login(params: LoginParams) {
      try {
        const res = await userLogin(params);
        this.userInfo = res.userInfo;
        this.token = res.token;
        this.isLoggedIn = true;
        // 存储token到localStorage
        localStorage.setItem("token", res.token);
        return true;
      } catch (error: any) {
        throw new Error(error.message);
      }
    },

    // 退出
    logout() {
      this.userInfo = null;
      this.token = null;
      this.isLoggedIn = false;
      // 清除localStorage
      localStorage.removeItem("token");
      localStorage.removeItem("user-store");
      // 跳转到登录页
      router.push("/login");
    },

    // 获取当前用户信息
    async getCurrentUser() {
      try {
        const res = await getCurrentUserInfo();
        this.userInfo = res;
        this.isLoggedIn = true;
        return res;
      } catch (error) {
        console.error("获取用户信息失败:", error);
        this.logout();
        return null;
      }
    },
  },
  persist: {
    // 存储键名,默认是 store 的 id
    key: "user-store",
    storage: localStorage,
    // paths
    pick: ["userInfo", "token", "isLoggedIn"],
  },
});

7.3 用户登录页面

在 views 文件夹下面新建 Login.vue:

登录页面很简单,这里我们使用 el-card、el-form、el-button 完成登录页面的设计:

ini 复制代码
 <el-card class="login-card" shadow="hover">
  <el-form
    :model="loginForm"
    :rules="loginRules"
    ref="loginFormRef"
    class="login-form"
  >
    <el-form-item prop="username">
      <el-input
        v-model="loginForm.username"
        placeholder="请输入用户名"
        prefix-icon="User"
      ></el-input>
    </el-form-item>

    <el-form-item prop="password">
      <el-input
        v-model="loginForm.password"
        type="password"
        placeholder="请输入密码"
        prefix-icon="Lock"
      ></el-input>
    </el-form-item>
    <el-form-item>
      <el-button
        type="primary"
        class="login-button"
        @click="handleLogin"
        :loading="loading"
      >
        登录
      </el-button>
    </el-form-item>
  </el-form>
</el-card>

在 js 代码中,我们要定义以下变量:

ts 复制代码
import { ElMessage } from "element-plus";
import type { FormInstance, FormRules } from "element-plus";
import { ref, reactive } from "vue";
import { useRouter } from "vue-router";
import { useUserStore } from "../store/user";
import { LoginParams } from "../types";
// 路由实例
const router = useRouter();
// 用户状态管理
const userStore = useUserStore();
// 表单引用
const loginFormRef = ref<FormInstance>();
// 加载状态
const loading = ref<boolean>(false);
// 登录表单数据
const loginForm = reactive<LoginParams>({
  username: "",
  password: "",
});
// 表单验证规则
const loginRules = reactive<FormRules>({
  username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
  password: [
    { required: true, message: "请输入密码", trigger: "blur" },
    { min: 6, message: "密码长度不能少于6位", trigger: "blur" },
  ],
});

点击登录按钮,首先要进行表单校验,然后调用 userStore 的登录方法,登录成功之后进入主页页面,否则捕获异常信息。

ts 复制代码
// 处理登录
const handleLogin = async () => {
  if (!loginFormRef.value) return;
  try {
    // 表单验证
    await loginFormRef.value.validate();
    // 显示加载状态
    loading.value = true;
    // 调用登录方法
    const success = await userStore.login(loginForm);
    if (success) {
      ElMessage.success("登录成功");
      // 跳转到首页
      router.push("/home");
    }
  } catch (error: any) {
    if (error.message) {
      ElMessage.error(error.message);
    }
  } finally {
    // 隐藏加载状态
    loading.value = false;
  }
};

在 useStore 的登录方法中,如果登录成功存储 store 信息,否则抛出异常。

8. 商品的增删改查

8.1 创建商品相关的 API

在 /src/api 文件夹下面新建 product.ts 文件

ts 复制代码
import { get, post, del, getPage } from "./index";
import { Product, PageParams } from "../types";

/**
 * 获取商品列表(分页)
 * @param params 分页查询参数
 * @returns 分页商品列表
 */
export const getProductList = (params: PageParams) => {
  return getPage<Product>("/product/page", params);
};

/**
 * 获取商品详情
 * @param id 商品ID
 * @returns 商品详情
 */
export const getProductDetail = (id: number) => {
  return get<Product>(`/product/info/${id}`);
};

/**
 * 新增/修改商品
 * @param data 商品数据
 * @returns
 */
export const createUpdateProduct = (data: Product) => {
  return post<Product>("/product/saveUpdate", data);
};

/**
 * 删除商品
 * @param id 商品ID
 * @returns 删除结果
 */
export const deleteProduct = (id: number) => {
  return del<{ success: boolean }>(`/product/delete/${id}`);
};

8.2 商品查询

商品查询页面包含搜索区域、列表区域、分页区域

css 复制代码
    <!-- 搜索区域 -->
    <el-card class="search-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="商品名称">
          <el-input
            v-model="searchForm.name"
            clearable
            @clear="handleSearch"
            placeholder="请输入商品名称"
            style="width: 200px"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
          <el-button type="success" @click="handleAddProduct"> 添加商品 </el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 商品列表 -->
    <el-card class="table-card">
      <el-table :data="productList" border style="width: 100%" v-loading="loading">
        <el-table-column type="index" label="序号" width="55" />
        <el-table-column prop="name" label="商品名称"></el-table-column>
        <el-table-column prop="price" label="价格">
          <template #default="scope"> ¥{{ scope.row.price.toFixed(2) }} </template>
        </el-table-column>
        <el-table-column prop="stock" label="库存"></el-table-column>
        <el-table-column prop="createTime" label="创建时间"></el-table-column>
        <el-table-column label="操作" width="200">
          <template #default="scope">
            <el-button type="primary" size="small" @click="handleEdit(scope.row)">
              编辑
            </el-button>
            <el-button type="danger" size="small" @click="handleDelete(scope.row.id)">
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页 -->
      <div class="pagination">
        <el-pagination
          :current-page="pageParams.current"
          :page-size="pageParams.size"
          :page-sizes="[10, 20, 50]"
          :total="total"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        ></el-pagination>
      </div>
    </el-card>

商品查询相关变量和方法:

ts 复制代码
import { ref, reactive, onMounted } from "vue";
import { ElForm, FormInstance, ElMessage, ElMessageBox, FormRules } from "element-plus";
import { Product, PageParams } from "../types";
import { getProductList, createUpdateProduct, deleteProduct } from "../api/product";
import { useUserStore } from "../store/user";
// 用户状态管理
const userStore = useUserStore();
// 初始化时加载数据
onMounted(() => {
  fetchProductList();
});

// 加载状态
const loading = ref<boolean>(false);
// 商品列表数据
const productList = ref<Product[]>([]);
const total = ref<number>(0);
// 搜索表单
const searchForm = reactive({
  name: "",
});
// 分页参数
const pageParams = reactive<PageParams>({
  current: 1,
  size: 10,
  name: "",
});
// 获取商品列表
const fetchProductList = async () => {
  try {
    loading.value = true;
    const res = await getProductList(pageParams);
    productList.value = res.records;
    total.value = res.total;
  } catch (error) {
    ElMessage.error("获取商品列表失败");
  } finally {
    loading.value = false;
  }
};

// 搜索
const handleSearch = () => {
  pageParams.current = 1;
  pageParams.name = searchForm.name;
  fetchProductList();
};

// 重置搜索
const resetSearch = () => {
  pageParams.current = 1;
  pageParams.name = "";
  fetchProductList();
};
// 分页大小变化
const handleSizeChange = (size: number) => {
  pageParams.size = size;
  fetchProductList();
};
// 当前页变化
const handleCurrentChange = (page: number) => {
  pageParams.current = page;
  fetchProductList();
};

8.3 删除商品

ts 复制代码
// 删除商品
const handleDelete = async (id: number) => {
  try {
    const confirmResult = await ElMessageBox.confirm(
      "确定要删除这个商品吗?",
      "删除确认",
      {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }
    );
    if (confirmResult === "confirm") {
      await deleteProduct(id);
      ElMessage.success("商品删除成功");
      fetchProductList();
    }
  } catch (error: any) {
    // 如果是取消操作,不显示错误信息
    if (error != "cancel") {
      ElMessage.error("商品删除失败");
    }
  }
};

8.4 新增编辑商品

ts 复制代码
// 表单数据
let formData = reactive<Product>({
  id: "",
  name: "",
  price: 0,
  stock: 0,
  description: "",
});

// 打开添加商品弹窗
const handleAddProduct = () => {
  dialogTitle.value = "添加商品";
  dialogVisible.value = true;
};
// 打开编辑商品弹窗
const handleEdit = (product: Product) => {
  dialogTitle.value = "修改商品";
  // 填充表单数据
  formData = product;
  dialogVisible.value = true;
};
// 提交表单
const handleSubmit = async () => {
  if (!formRef.value) return;
  try {
    await formRef.value.validate();
    // 创建商品
    await createUpdateProduct(formData);
    ElMessage.success(`商品${formData.id ? "修改成功" : "添加成功"}`);
    // 重置提交
    resetSubmit(formRef.value);
  } catch (error: any) {
    if (error.message) {
      ElMessage.error(error.message);
    }
  }
};

9. 完整代码

前端:

css 复制代码
git@gitee.com:zhifou-tech/zhifou-vue3-ts.git

后端

css 复制代码
通过网盘分享的文件:zhifou-vue3-ts-springboot.zip
链接: https://pan.baidu.com/s/1_42KmE68ucoCAuTMyg8iXQ?pwd=6666 提取码: 6666 
相关推荐
IT_陈寒2 小时前
JavaScript 2024:10个颠覆你认知的ES新特性实战解析
前端·人工智能·后端
meng半颗糖2 小时前
JavaScript 性能优化实战指南
前端·javascript·servlet·性能优化
EndingCoder2 小时前
离线应用开发:Service Worker 与缓存
前端·javascript·缓存·性能优化·electron·前端框架
遗憾随她而去.3 小时前
css3的 --自定义属性, 变量
前端·css·css3
haogexiaole5 小时前
vue知识点总结
前端·javascript·vue.js
哆啦A梦15887 小时前
[前台小程序] 01 项目初始化
前端·vue.js·uni-app
小周同学@9 小时前
谈谈对this的理解
开发语言·前端·javascript
Wiktok9 小时前
Pyside6加载本地html文件并实现与Javascript进行通信
前端·javascript·html·pyside6
@AfeiyuO9 小时前
分类别柱状图(Vue3)
typescript·vue·echarts