效果图:

开始

# 新建项目
npm create vite@latest my-admin -- --template vue-ts
cd my-admin
# 安装依赖
npm install
# 安装Element Plus+图标(指定兼容版本)
npm install element-plus@2.4.3 @element-plus/icons-vue@2.3.2
# 安装vue-router
npm install vue-router@4

配置vite.config.ts@别名:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
})

配置tsconfig.json的paths:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

配置src/env.d.ts

// src/env.d.ts
/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}

App.vue

<template>
  <router-view />
</template>

<style>
/* 重置默认样式,确保全屏 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
html, body, #app {
  width: 100%;
  height: 100%;
  max-width: none; /* 取消最大宽度限制 */
  padding: 0;
  text-align: left;
}
/* 修复Element Plus组件的默认间距 */
.el-form-item {
  margin-right: 15px;
  margin-bottom: 15px;
}
.el-table {
  margin-top: 10px;
}
</style>

main.ts

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'

// 引入Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 引入Element Plus图标
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(router)
app.use(ElementPlus)

app.mount('#app')

src/router/index.ts

// src/router/index.ts 顶部修改为:
import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router' // 单独用type导入类型


// 定义路由类型(RouteRecordRaw是Vue Router提供的路由类型)
const routes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue')
  },
  {
    path: '/',
    redirect: '/home',
    component: () => import('@/layout/Layout.vue'),
    children: [
      { path: '/home', name: 'Home', component: () => import('@/views/Home.vue') },
      { path: '/order', name: 'Order', component: () => import('@/views/Order.vue') },
      { path: '/user', name: 'User', component: () => import('@/views/User.vue') }
    ]
  }
]

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

// 路由守卫也可以加类型
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')
  if (to.path !== '/login' && !token) {
    next('/login')
  } else {
    next()
  }
})

export default router

src/layout/Layout.vue

<script setup>
// 引入图标
import { House, ShoppingCart, User } from '@element-plus/icons-vue'
</script>

<template>
  <el-container style="height: 100vh;">
    <!-- 侧边栏 -->
    <el-aside width="200px" style="background-color: #2E3B4E;">
      <el-menu
        default-active="1"
        class="el-menu-vertical-demo"
        background-color="#2E3B4E"
        text-color="#fff"
        active-text-color="#ffd04b"
        router
      >
        <!-- 首页:index绑定路由路径 -->
        <el-menu-item index="/home">
          <el-icon><House /></el-icon>
          <span>首页</span>
        </el-menu-item>

        <!-- 订单管理:子菜单的index也绑定路径 -->
        <el-sub-menu index="/order">
          <template #title>
            <el-icon><ShoppingCart /></el-icon>
            <span>订单管理</span>
          </template>
          <el-menu-item index="/order">
            <span>订单列表</span>
          </el-menu-item>
        </el-sub-menu>

        <!-- 用户管理:index绑定路由路径 -->
        <el-menu-item index="/user">
          <el-icon><User /></el-icon>
          <span>用户管理</span>
        </el-menu-item>
      </el-menu>
    </el-aside>

    <el-container>
      <!-- 顶部导航 -->
      <el-header style="text-align: right; font-size: 12px">
        <el-dropdown>
          <i class="el-icon-setting" style="margin-right: 15px"></i>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item>退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
        <span>管理员</span>
      </el-header>

      <!-- 内容区 -->
      <el-main>
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>



<style scoped>
/* 补充全局样式,确保body/html占满屏幕 */
:root, body, html {
  height: 100%;
  margin: 0;
}
.el-container {
  height: 100%; /* 确保Element Plus的容器继承高度 */
}
.el-header {
  background-color: #fff;
  color: #333;
  line-height: 60px;
}
.el-aside {
  color: #333;
}
</style>

src/views/Home.vue

<template>
  <el-card shadow="hover">
    <h1>欢迎进入后台管理系统</h1>
    <el-statistic title="今日订单数" :value="128" />
    <el-statistic title="用户总数" :value="2048" style="margin-left: 20px" />
  </el-card>
</template>

src/views/Login.vue

<template>
  <div class="login-container">
    <el-card class="login-card" shadow="hover">
      <h2 style="text-align: center; margin-bottom: 20px">后台管理系统</h2>
      <el-form :model="loginForm" label-width="80px">
        <el-form-item label="账号">
          <el-input v-model="loginForm.username" placeholder="请输入账号" />
        </el-form-item>
        <el-form-item label="密码">
          <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleLogin" style="width: 100%">登录</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'

const router = useRouter()
const loginForm = ref({
  username: 'admin',
  password: '123456'
})

const handleLogin = () => {
  // 模拟登录逻辑
  if (loginForm.value.username && loginForm.value.password) {
    localStorage.setItem('token', 'fake-token')
    ElMessage.success('登录成功')
    router.push('/home')
  } else {
    ElMessage.error('请输入账号密码')
  }
}
</script>

<style scoped>
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f5f7fa;
}
.login-card {
  width: 400px;
  padding: 20px;
}
</style>

src/views/Order.vue

<template>
  <div class="order-container">
    <!-- 搜索栏 -->
    <el-card shadow="hover" style="margin-bottom: 20px;">
      <el-form inline @submit.prevent>
        <el-form-item label="订单编号">
          <el-input v-model="searchForm.orderNo" placeholder="请输入订单编号" />
        </el-form-item>
        <el-form-item label="用户名称">
          <el-input v-model="searchForm.userName" placeholder="请输入用户名称" />
        </el-form-item>
        <el-form-item label="订单状态">
          <el-select v-model="searchForm.status" placeholder="全部">
            <el-option label="全部" value="" />
            <el-option label="待支付" value="1" />
            <el-option label="已支付" value="2" />
            <el-option label="已发货" value="3" />
            <el-option label="已完成" value="4" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 订单表格 -->
    <el-card shadow="hover">
      <el-table :data="orderList" border style="width: 100%">
        <el-table-column prop="orderNo" label="订单编号" width="180" />
        <el-table-column prop="userName" label="用户名称" width="120" />
        <el-table-column prop="amount" label="订单金额" width="100" align="right">
          <template #default="scope">¥{{ scope.row.amount.toFixed(2) }}</template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" width="180" />
        <el-table-column prop="status" label="订单状态" width="100">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.status)">
              {{ statusMap[scope.row.status] }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="220" align="center">
          <template #default="scope">
            <el-button size="small" type="primary" @click="handleView(scope.row)">查看</el-button>
            <el-button size="small" type="danger" @click="handleCancel(scope.row)" v-if="scope.row.status === '1'">取消</el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页 -->
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="currentPage"
        :page-sizes="[10, 20, 50]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        style="margin-top: 20px; text-align: right;"
      />
    </el-card>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'

// 搜索表单
const searchForm = ref({
  orderNo: '',
  userName: '',
  status: ''
})

// 订单列表数据(模拟)
const orderList = ref([
  {
    orderNo: 'OD20251202001',
    userName: '张三',
    amount: 299.00,
    createTime: '2025-12-02 10:30:00',
    status: '2'
  },
  {
    orderNo: 'OD20251202002',
    userName: '李四',
    amount: 599.00,
    createTime: '2025-12-02 11:15:00',
    status: '3'
  },
  {
    orderNo: 'OD20251202003',
    userName: '王五',
    amount: 129.00,
    createTime: '2025-12-02 14:20:00',
    status: '1'
  }
])

// 状态映射
const statusMap = {
  '1': '待支付',
  '2': '已支付',
  '3': '已发货',
  '4': '已完成'
}
// 状态标签类型
const statusTagType = (status) => {
  switch (status) {
    case '1': return 'warning'
    case '2': return 'primary'
    case '3': return 'success'
    case '4': return 'info'
    default: return ''
  }
}

// 分页参数
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(3)

// 搜索
const handleSearch = () => {
  ElMessage.info('执行搜索逻辑')
}
// 重置
const resetSearch = () => {
  searchForm.value = { orderNo: '', userName: '', status: '' }
}
// 查看订单
const handleView = (row) => {
  ElMessage.info(`查看订单:${row.orderNo}`)
}
// 取消订单
const handleCancel = (row) => {
  ElMessage.success(`取消订单:${row.orderNo}`)
}

// 分页事件
const handleSizeChange = (val) => {
  pageSize.value = val
}
const handleCurrentChange = (val) => {
  currentPage.value = val
}
</script>

<style scoped>
.order-container {
  padding: 0 10px;
}
</style>

src/views/User.vue

<template>
  <div class="user-container">
    <!-- 操作栏 -->
    <el-card shadow="hover" style="margin-bottom: 20px;">
      <el-button type="primary" @click="openUserModal">新增用户</el-button>
    </el-card>

    <!-- 用户表格 -->
    <el-card shadow="hover">
      <el-table :data="userList" border style="width: 100%">
        <!-- 原有表格列不变 -->
        <el-table-column type="selection" width="55" />
        <el-table-column prop="id" label="用户ID" width="80" align="center" />
        <el-table-column prop="userName" label="用户名" width="120" />
        <el-table-column prop="nickName" label="昵称" width="120" />
        <el-table-column prop="phone" label="手机号" width="150" />
        <el-table-column prop="role" label="角色" width="100">
          <template #default="scope">
            <el-tag type="success">{{ scope.row.role }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态" width="100" align="center">
          <template #default="scope">
            <el-switch
              v-model="scope.row.status"
              active-value="1"
              inactive-value="0"
              @change="handleStatusChange(scope.row)"
            />
          </template>
        </el-table-column>
        <el-table-column label="操作" width="220" align="center">
          <template #default="scope">
            <el-button size="small" type="primary" @click="openUserModal(scope.row)">编辑</el-button>
            <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页(原有不变) -->
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="currentPage"
        :page-sizes="[10, 20, 50]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        style="margin-top: 20px; text-align: right;"
      />
    </el-card>

    <!-- 新增/编辑用户弹窗 -->
    <el-dialog
      v-model="userModalVisible"
      :title="isEdit ? '编辑用户' : '新增用户'"
      width="500px"
    >
      <el-form
        ref="userFormRef"
        :model="userForm"
        label-width="80px"
        :rules="userFormRules"
      >
        <el-form-item label="用户名" prop="userName">
          <el-input
            v-model="userForm.userName"
            placeholder="请输入用户名"
          />
        </el-form-item>
        <el-form-item label="昵称" prop="nickName">
          <el-input v-model="userForm.nickName" placeholder="请输入昵称" />
        </el-form-item>
        <el-form-item label="手机号" prop="phone">
          <el-input v-model="userForm.phone" placeholder="请输入手机号" />
        </el-form-item>
        <el-form-item label="角色" prop="role">
          <el-select v-model="userForm.role" placeholder="请选择角色">
            <el-option label="管理员" value="管理员" />
            <el-option label="普通用户" value="普通用户" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="userForm.status">
            <el-radio label="1">启用</el-radio>
            <el-radio label="0">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item v-if="!isEdit" label="密码" prop="password">
          <el-input v-model="userForm.password" type="password" placeholder="请输入密码" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="userModalVisible = false">取消</el-button>
          <el-button type="primary" @click="submitUserForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage, ElMessageBox, ElForm } from 'element-plus'

// 用户列表数据(原有不变)
const userList = ref([
  {
    id: 1,
    userName: 'admin',
    nickName: '管理员',
    phone: '13800138000',
    role: '管理员',
    status: '1'
  },
  {
    id: 2,
    userName: 'user001',
    nickName: '普通用户1',
    phone: '13800138001',
    role: '普通用户',
    status: '1'
  },
  {
    id: 3,
    userName: 'user002',
    nickName: '普通用户2',
    phone: '13800138002',
    role: '普通用户',
    status: '0'
  }
])

// 分页参数(原有不变)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(3)


// -------- 新增/编辑弹窗相关 --------
// 弹窗显隐
const userModalVisible = ref(false)
// 是否为编辑模式
const isEdit = ref(false)
// 表单实例
const userFormRef = ref(null)
// 表单数据
const userForm = reactive({
  id: '',
  userName: '',
  nickName: '',
  phone: '',
  role: '',
  status: '1',
  password: ''
})
// 表单校验规则
const userFormRules = reactive({
  userName: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
  nickName: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
  ],
  role: [{ required: true, message: '请选择角色', trigger: 'change' }],
  password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
})

// 打开弹窗(新增/编辑)
const openUserModal = (row = null) => {
  // 重置表单
  userFormRef.value?.resetFields()
  // 区分新增/编辑
  if (row) {
    isEdit.value = true
    // 赋值给表单(编辑)
    Object.assign(userForm, row)
  } else {
    isEdit.value = false
    // 清空表单(新增)
    Object.keys(userForm).forEach(key => userForm[key] = '')
    userForm.status = '1'
  }
  userModalVisible.value = true
}

// 提交表单
const submitUserForm = async () => {
  try {
    // 表单校验
    await userFormRef.value.validate()

    if (isEdit.value) {
      // 编辑逻辑:更新列表中对应数据
      const index = userList.value.findIndex(item => item.id === userForm.id)
      if (index > -1) {
        userList.value[index] = { ...userForm }
      }
      ElMessage.success('编辑用户成功!')
    } else {
      // 新增逻辑:生成新ID,添加到列表
      const newId = Math.max(...userList.value.map(item => item.id)) + 1
      userList.value.unshift({
        ...userForm,
        id: newId
      })
      total.value++
      ElMessage.success('新增用户成功!')
    }

    // 关闭弹窗
    userModalVisible.value = false
  } catch (err) {
    // 校验失败不处理
  }
}


// -------- 原有操作方法(部分调整) --------
// 删除用户(原有不变)
const handleDelete = async (row) => {
  try {
    await ElMessageBox.confirm(
      '此操作将永久删除该用户,是否继续?',
      '提示',
      { type: 'warning' }
    )
    userList.value = userList.value.filter(item => item.id !== row.id)
    total.value--
    ElMessage.success('删除成功!')
  } catch (err) {
    ElMessage.info('已取消删除')
  }
}

// 状态切换(原有不变)
const handleStatusChange = (row) => {
  ElMessage.info(`${row.userName}的状态已切换为:${row.status === '1' ? '启用' : '禁用'}`)
}

// 分页事件(原有不变)
const handleSizeChange = (val) => {
  pageSize.value = val
}
const handleCurrentChange = (val) => {
  currentPage.value = val
}
</script>

<style scoped>
.user-container {
  padding: 0 10px;
}
</style>

运行项目

npm run dev

以上就完成了一个简单的后台管理项目的搭建。但数据都是模拟的,接下来我们增加环境区分、接口请求、打包发布

区分开发 / 测试 / 生产环境(Vite 通过.env文件区分环境,在项目根目录创建以下文件:)

开发环境:.env.development
# 开发环境
NODE_ENV=development
VITE_BASE_API=http://localhost:3000/api  # 本地后端接口
测试环境:.env.test
# 测试环境
NODE_ENV=test
VITE_BASE_API=http://test.example.com/api  # 测试服接口
生产环境:.env.production
# 生产环境
NODE_ENV=production
VITE_BASE_API=http://prod.example.com/api  # 正式服接口

配置package.json的脚本命令

{
  "name": "my-admin",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc -b && vite build",
    "build:test": "vite build --mode test --outDir dist-test",
    "build:prod": "vite build --mode production --outDir dist-prod",
    "preview": "vite preview"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.2",
    "axios": "^1.13.2",
    "element-plus": "^2.4.3",
    "vue": "^3.5.24",
    "vue-router": "^4.6.3"
  },
  "devDependencies": {
    "@types/node": "^24.10.1",
    "@vitejs/plugin-vue": "^6.0.1",
    "@vue/runtime-core": "^3.5.25",
    "@vue/tsconfig": "^0.8.1",
    "typescript": "~5.9.3",
    "vite": "^7.2.4",
    "vue-tsc": "^3.1.4"
  }
}

打包后dist文件放服务器目录时,需在vite.config.ts里配置base目录路径(比如服务域名根目示下的文件目录路径:/oss/prod/myadmin/),以免找不到相关文件:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  base: '/oss/prod/myadmin/',
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  build: {
    sourcemap: false // 禁用打包后的SourceMap文件(默认是false,生产环境建议关闭)
  }
})

配置接口请求工具(以 Axios 为例)

npm install axios
创建接口请求封装文件 src/utils/request.ts
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router' // 引入路由实例

// 创建Axios实例
const request = axios.create({
  baseURL: import.meta.env.VITE_BASE_API, // 自动读取对应环境的接口地址
  timeout: 5000
})

// 请求拦截器(添加Token等)
request.interceptors.request.use(
  (config) => {
    // 从localStorage中获取Token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器(统一处理错误)
request.interceptors.response.use(
  (response) => {
    const res = response.data
    if (res.code !== 200) {
      // 处理Token过期(假设后端返回code=401表示未授权)
      if (res.code === 401) {
        ElMessage.error('登录已过期,请重新登录')
        // 清除过期的Token
        localStorage.removeItem('token')
        // 跳转到登录页,并携带当前页面路径(方便登录后返回)
        router.push({
          path: '/login',
          query: { redirect: router.currentRoute.value.fullPath }
        })
      } else {
        ElMessage.error(res.msg || '请求失败')
      }
      return Promise.reject(new Error(res.msg || 'Error'))
    } else {
      return res.data
    }
  },
  (error) => {
    // 处理网络请求层面的401(比如后端直接返回401状态码)
    if (error.response?.status === 401) {
      ElMessage.error('登录已过期,请重新登录')
      localStorage.removeItem('token')
      router.push({
        path: '/login',
        query: { redirect: router.currentRoute.value.fullPath }
      })
    } else {
      ElMessage.error(error.message || '网络异常')
    }
    return Promise.reject(error)
  }
)

export default request
登录成功后跳回原页面
<template>
  <div class="login-container">
    <el-card class="login-card" shadow="hover">
      <h2 style="text-align: center; margin-bottom: 20px">后台管理系统</h2>
      <el-form :model="loginForm" label-width="80px">
        <el-form-item label="账号">
          <el-input v-model="loginForm.username" placeholder="请输入账号" />
        </el-form-item>
        <el-form-item label="密码">
          <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleLogin" style="width: 100%">登录</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router' // 引入路由和当前路由实例
import { ElMessage } from 'element-plus'
import { loginApi } from '@/api/index' // 假设这是登录接口

const router = useRouter()
const route = useRoute() // 获取当前路由信息
const loginFormRef = ref()
const loginForm = ref({
  username: 'admin',
  password: '123456'
})

const handleLogin = async () => {
  try {
    // 调用登录接口,获取Token
    const res = await loginApi(loginForm.value)
    localStorage.setItem('token', res.data.token) // 存储Token

    // 关键:读取redirect参数,决定跳转路径
    const redirect = route.query.redirect as string
    // 如果有redirect,跳转到该页面;否则跳首页
    router.push(redirect ? redirect : '/home')

    ElMessage.success('登录成功')
  } catch (error) {
    ElMessage.error('登录失败,请检查账号密码')
  }
}

/* const handleLogin = () => {
  // 模拟登录逻辑
  if (loginForm.value.username && loginForm.value.password) {
    localStorage.setItem('token', 'fake-token')
    ElMessage.success('登录成功')
    router.push('/home')
  } else {
    ElMessage.error('请输入账号密码')
  }
} */
</script>

<style scoped>
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f5f7fa;
}
.login-card {
  width: 400px;
  padding: 20px;
}
</style>
创建接口模块(以订单接口为例)src/api/api.ts
import request from '@/utils/request'

// 获取订单列表
export const getOrderList = (params: any) => {
  return request({
    url: '/order/list',
    method: 'get',
    params
  })
}

// 取消订单
export const cancelOrder = (orderNo: string) => {
  return request({
    url: `/order/cancel/${orderNo}`,
    method: 'post'
  })
}

// 登录接口
export const loginApi = (data: { username: string; password: string }) => {
  return request({
    url: '/user/login',
    method: 'post',
    data
  })
}

有些模块语法配置不兼容,修改tsconfig.json

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "vue": ["node_modules/vue/dist/vue.esm-bundler.js"], // 强制指定Vue的模块路径
      "vue-router": ["node_modules/vue-router/dist/vue-router.esm-bundler.js"],
      "element-plus": ["node_modules/element-plus/dist/index.es.mjs"]
    },
    "module": "ESNext", // 或 "NodeNext"
    "moduleResolution": "Bundler", // Vite项目推荐用Bundler
    "types": ["vite/client", "@vue/runtime-core", "vue-router"] // 引入Vite的类型声明(包含import.meta.env)
  }
}

src/env.d.ts

// src/env.d.ts
/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}

打包发现ts、vue生成了.js文件。通过tsconfig.json禁止 TS 自动编译

// tsconfig.json
{
  "compilerOptions": {
    "noEmit": true, // 禁止TS输出JS文件
    "emitDeclarationOnly": false
  }
}

Vite 中配置代理解决跨域问题,在 Vite 项目的 vite.config.js 文件中配置代理。

export default {
    server: {
        proxy: {
            '/api': {
                target: 'http://localhost:3000', // 目标服务器地址
                changeOrigin: true,
                rewrite: (path) => path.replace(/^\/api/, '')
            }
        }
    }
};

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐