专为自由职业、独立开发者提供技能分享交流学习成长的平台,按 Ctrl+D 收藏我们
关于 积分 赞助 社群 投稿

爱网赚i5z.net

  • 首页
  • 发现
    • 有趣产品
    • 项目分享
    • 技能分享
    • 必备工具
    • 苏米杂谈
  • 独立开发者
    • 开发者周刊
    • 开发者故事
  • 实用资源
    • 建站资源
    • 精品教程
    • 域名优惠
    • VPS优惠
  • 独立开发导航
  • 更多
    • 标签云
    • 排行榜
    • 查域名
    • 留言板
    • 小卖铺
  • 登录
  • 首页
  • 发现
    • 有趣产品
    • 项目分享
    • 技能分享
    • 必备工具
    • 苏米杂谈
  • 独立开发者
    • 开发者周刊
    • 开发者故事
  • 实用资源
    • 建站资源
    • 精品教程
    • 域名优惠
    • VPS优惠
  • 独立开发导航
  • 更多
    • 标签云
    • 排行榜
    • 查域名
    • 留言板
    • 小卖铺
当前位置: 首页 » 精品教程

别再踩坑了!手把手教你 Vue3+TypeScript 项目实战(附源码)

2小时前 4 0

功能演示

用户登录、商品增删改查

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

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

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

项目目录:

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

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

exportdefault 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

npm install element-plus @element-plus/icons-vue

在 main.ts 中配置 ElementPlus

import { createApp } from "vue";
importAppfrom "./App.vue";

importElementPlusfrom "element-plus";
import "element-plus/dist/index.css";
importlocalefrom "element-plus/es/locale/lang/zh-cn";
import * asElementPlusIconsVuefrom "@element-plus/icons-vue";
constapp = createApp(App);

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

3.安装配置 Vue Router

npm install vue-router@4

在 main.ts 中配置路由

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

import { createRouter, createWebHistory, RouteRecordRaw } from"vue-router";
import { useUserStore } from"@/store/user";
const routes: Array = [
  {
    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");
    }
  }
});
exportdefault router;

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

4.安装配置配置 Pinia

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

npm install pinia pinia-plugin-persistedstate

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

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

export default pinia

在 main.ts 中配置 pina

5. 安装配置 axios

npm install axios

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

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) => {
    // 返回异常
    returnPromise.reject(error);
  }
);

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

exportdefault service;

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

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

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

6. 封装 http 请求

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

import axios from"../utils/axios";
import { ApiResponse, PageParams, PageResponse } from"../types";

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

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

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

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

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

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

{code:200,data:{username:'zhifou'},messeg:'请求成功'}

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

{username:'zhifou'}

7. 用户登录

7.1 创建用户相关的 API

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

import { get, post } from"./index";
import { LoginParams, LoginResponse, User } from"../types";

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

/**
 * 获取当前用户信息
 * @returns 用户信息
 */
exportconst getCurrentUserInfo = async () => {
returnget("/user/currentUserInfo");
};

7.2 创建用户状态管理

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

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

exportconst 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);
        returntrue;
      } catch (error: any) {
        thrownewError(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();
        returnnull;
      }
    },
  },
  persist: {
    // 存储键名,默认是 store 的 id
    key: "user-store",
    storage: localStorage,
    // paths
    pick: ["userInfo", "token", "isLoggedIn"],
  },
});

7.3 用户登录页面

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

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

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

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

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

import { ElMessage } from"element-plus";
importtype { 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();
// 加载状态
const loading = ref<boolean>(false);
// 登录表单数据
const loginForm = reactive({
  username: "",
  password: "",
});
// 表单验证规则
const loginRules = reactive({
  username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
  password: [
    { required: true, message: "请输入密码", trigger: "blur" },
    { min: 6, message: "密码长度不能少于6位", trigger: "blur" },
  ],
});

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

// 处理登录
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 信息,否则抛出异常。

用户登录成功之后,userStore里面的数据被存储到了浏览器里面:

8. 商品的增删改查

8.1 创建商品相关的 API

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

import { get, post, del, getPage } from"./index";
import { Product, PageParams } from"../types";

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

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

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

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

8.2 商品查询

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

    <!-- 搜索区域 -->
    <el-cardclass="search-card">
      <el-form:model="searchForm" inline>
        <el-form-itemlabel="商品名称">
          <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-columnprop="stock" label="库存"></el-table-column>
        <el-table-columnprop="createTime" label="创建时间"></el-table-column>
        <el-table-columnlabel="操作" width="200">
          <template#default="scope">
            <el-buttontype="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>

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

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 删除商品

// 删除商品
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 新增/修改商品

// 表单数据
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. 完整代码

前端:

git@gitee.com:zhifou-tech/zhifou-vue3-ts.git

后端

通过网盘分享的文件:zhifou-vue3-ts-springboot.zip
链接: https://pan.baidu.com/s/1_42KmE68ucoCAuTMyg8iXQ?pwd=6666 提取码: 6666 
声明:本站原创文章文字版权归本站所有,转载务必注明作者和出处;本站转载文章仅仅代表原作者观点,不代表本站立场,图文版权归原作者所有。如有侵权,请联系我们删除。
未经允许不得转载:别再踩坑了!手把手教你 Vue3+TypeScript 项目实战(附源码)
#Vue3 #TypeScript #项目实战 
收藏 1
推荐阅读
  • 10分钟教会你使用Echarts柱状图!真的一点都不难!
  • 独立开发建站必备:邮箱注册功能,全流程手把手教你开通QQ邮箱SMTP
  • 教育优惠必备 | 国内如何5分钟快速申请 “edu邮箱” 实用操作教程(liberty)
  • 独立开发者必备:Google邮箱100%通过的注册指南(支持无限注册)
  • Nginx史上最强教程:独立开发者从入门到上线的实战心法(看完真醍醐灌顶)
评论 (0)
请登录后发表评论
分类精选
美国ASU大学EDU教育邮箱免费注册教程(2025年最新版)
3910 6月前
独立开发者必备:Google邮箱100%通过的注册指南(支持无限注册)
1047 4月前
教育优惠必备 | 国内如何5分钟快速申请 “edu邮箱” 实用操作教程(liberty)
929 6月前
Umami 一款开源的网站统计工具!安装使用教程(源码安装、Docker安装、BT宝塔/1Panel一键安装)
744 7月前
2025美区Apple ID注册保姆级教程,5分钟搞定美区 Apple ID,支付下载付费应用!
656 4月前
手把手教你安装这款Windows容器化开发神器:Docker Desktop
629 4月前
把域名托管到Cloudflare并申请15年免费证书
502 3月前
独立开发建站必备:邮箱注册功能,全流程手把手教你开通QQ邮箱SMTP
497 4月前
手慢无!限时免费白嫖谷歌Gemini 3 Pro 和 NanaBanana Pro 会员
125 1周前
Nginx史上最强教程:独立开发者从入门到上线的实战心法(看完真醍醐灌顶)
60 1周前

文章目录

分类排行
1 别再踩坑了!手把手教你 Vue3+TypeScript 项目实战(附源码)
2 手把手教你在Vue3 项目中手搓一个useTable 表格 Hooks!大大提高代码的复用性!
3 10分钟教会你使用Echarts柱状图!真的一点都不难!
4 手慢无!限时免费白嫖谷歌Gemini 3 Pro 和 NanaBanana Pro 会员
5 Nginx史上最强教程:独立开发者从入门到上线的实战心法(看完真醍醐灌顶)
6 把域名托管到Cloudflare并申请15年免费证书
7 2025美区Apple ID注册保姆级教程,5分钟搞定美区 Apple ID,支付下载付费应用!
8 手把手教你安装这款Windows容器化开发神器:Docker Desktop
9 独立开发建站必备:邮箱注册功能,全流程手把手教你开通QQ邮箱SMTP
10 独立开发者必备:Google邮箱100%通过的注册指南(支持无限注册)
©2015-2024 i5z爱网赚出海分享 版权所有 · www. i5z.net 闽ICP备15002536号-6
免费影视导航 花式玩客 免费字体下载 产品经理导航 Axure RP 10 免费Axure模板 网赚分享 跨境数研所 聚玩盒子 申请友联