Преглед на файлове

feat(api): 完善请求拦截与响应处理逻辑

新增 token 自动注入、GET 请求参数过滤及统一错误处理机制,
并导出常用的 HTTP 方法封装(get、post、put、del)。

feat(login): 重构登录弹窗组件实现方式

移除 v-model 双向绑定控制显示逻辑,改为通过 expose 的 open 方法触发显示;
增加图形验证码功能,支持点击刷新;优化账号字段校验逻辑。

feat(auth): 调整登录接口调用方式

将登录请求由模拟改为真实 API 调用,并根据账号类型动态传递参数。

chore(vite): 明确环境变量配置项

显式设置 envDir 和 envPrefix 配置,确保环境变量正确加载。

chore(package): 新增生产构建脚本命令

添加 "build:prod" 脚本用于生产环境打包;
更新 dev 启动命令为 "vite --host" 支持外部访问。
zhangningning преди 2 седмици
родител
ревизия
caa4dde35d
променени са 9 файла, в които са добавени 236 реда и са изтрити 36 реда
  1. 1 0
      .env
  2. 2 1
      package.json
  3. 1 0
      src/.env.development
  4. 0 0
      src/.env.production
  5. 2 3
      src/App.vue
  6. 10 0
      src/api/auth.js
  7. 135 4
      src/api/request.js
  8. 82 28
      src/components/LoginDialog.vue
  9. 3 0
      vite.config.js

+ 1 - 0
.env

@@ -0,0 +1 @@
+VITE_API_BASE_URL=http://192.168.100.134:8080/api

+ 2 - 1
package.json

@@ -4,8 +4,9 @@
   "version": "0.0.0",
   "type": "module",
   "scripts": {
-    "dev": "vite",
+    "dev": "vite --host",
     "build": "vite build",
+    "build:prod": "vite build --mode production",
     "preview": "vite preview"
   },
   "dependencies": {

+ 1 - 0
src/.env.development

@@ -0,0 +1 @@
+VITE_API_BASE_URL=http://192.168.100.134:8080/api

+ 0 - 0
src/.env.production


+ 2 - 3
src/App.vue

@@ -52,7 +52,7 @@
       </el-footer>
     </el-container>
     <!-- 登录弹框组件 -->
-    <LoginDialog v-model="showLoginDialog" ref="loginDialogRef" @login-success="handleLoginSuccess" />
+    <LoginDialog ref="loginDialogRef" @login-success="handleLoginSuccess" />
     </ElConfigProvider>
   </div>
 </template>
@@ -78,11 +78,10 @@ const router = useRouter()
 // 登录弹框引用
 const loginDialogRef = ref(null)
 // 登录弹框显示状态
-const showLoginDialog = ref(false)
 
 // 打开登录弹框
 const openLoginDialog = () => {
-  showLoginDialog.value = true;
+  loginDialogRef.value?.open()
 }
 // 处理登录成功
 const handleLoginSuccess = () => {

+ 10 - 0
src/api/auth.js

@@ -0,0 +1,10 @@
+import request from './request.js'
+
+// 获取验证码
+export function getCaptcha(data = {}) {
+  return request.get('/auth/captcha',data)
+}
+// 用户名登录
+export function loginUsername(data = {}) {
+  return request.post('/auth/login/username',data)
+}

+ 135 - 4
src/api/request.js

@@ -2,14 +2,34 @@ import axios from 'axios'
 
 // 创建 axios 实例
 const request = axios.create({
-  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
-  timeout: 5000
+  baseURL: import.meta.env.VITE_API_BASE_URL,
+  timeout: 10000,
+  headers: {
+    'Content-Type': 'application/json;charset=UTF-8'
+  }
 })
 
 // 请求拦截器
 request.interceptors.request.use(
   config => {
-    // 可以在这里添加 token
+    // 获取 token(假设存储在 localStorage 中)
+    const token = localStorage.getItem('access_token')
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`
+    }
+    
+    // 处理请求参数
+    if (config.method === 'get' && config.params) {
+      // 过滤掉空值参数
+      const filteredParams = {}
+      Object.keys(config.params).forEach(key => {
+        if (config.params[key] !== null && config.params[key] !== undefined && config.params[key] !== '') {
+          filteredParams[key] = config.params[key]
+        }
+      })
+      config.params = filteredParams
+    }
+    
     return config
   },
   error => {
@@ -21,12 +41,123 @@ request.interceptors.request.use(
 // 响应拦截器
 request.interceptors.response.use(
   response => {
-    return response.data
+    // 根据后端约定的响应结构进行状态判断
+    const { code, data, msg } = response.data
+    
+    // 假设 code 为 200 表示成功
+    if (code === 200) {
+      return {code,data,msg}
+    } else {
+      // 处理业务错误
+      console.error('业务错误:', msg)
+      
+      // 特殊状态码处理
+      if (code === 401) {
+        // token 过期或无效,跳转登录页
+        localStorage.removeItem('access_token')
+        window.location.href = '/login'
+      }
+      
+      return {code,data,msg}
+    }
   },
   error => {
     console.error('响应错误:', error)
+    
+    // HTTP 状态码错误处理
+    if (error.response) {
+      const { status, data } = error.response
+      switch (status) {
+        case 401:
+          // 未授权
+          localStorage.removeItem('access_token')
+          window.location.href = '/login'
+          break
+        case 403:
+          // 禁止访问
+          console.error('禁止访问:', data.message)
+          break
+        case 404:
+          // 资源不存在
+          console.error('请求资源不存在')
+          break
+        case 500:
+          // 服务器内部错误
+          console.error('服务器内部错误')
+          break
+        default:
+          console.error(`连接错误:${status}`)
+      }
+    } else if (error.request) {
+      // 请求发出但没有收到响应
+      console.error('网络错误,请检查网络连接')
+    } else {
+      // 其他错误
+      console.error('请求失败:', error.message)
+    }
+    
     return Promise.reject(error)
   }
 )
 
+/**
+ * GET 请求
+ * @param {string} url 请求地址
+ * @param {object} params 请求参数
+ * @param {object} config 其他配置
+ */
+export function get(url, params = {}, config = {}) {
+  return request({
+    method: 'get',
+    url,
+    params,
+    ...config
+  })
+}
+
+/**
+ * POST 请求
+ * @param {string} url 请求地址
+ * @param {object} data 请求数据
+ * @param {object} config 其他配置
+ */
+export function post(url, data = {}, config = {}) {
+  return request({
+    method: 'post',
+    url,
+    data,
+    ...config
+  })
+}
+
+/**
+ * PUT 请求
+ * @param {string} url 请求地址
+ * @param {object} data 请求数据
+ * @param {object} config 其他配置
+ */
+export function put(url, data = {}, config = {}) {
+  return request({
+    method: 'put',
+    url,
+    data,
+    ...config
+  })
+}
+
+/**
+ * DELETE 请求
+ * @param {string} url 请求地址
+ * @param {object} params 请求参数
+ * @param {object} config 其他配置
+ */
+export function del(url, params = {}, config = {}) {
+  return request({
+    method: 'delete',
+    url,
+    params,
+    ...config
+  })
+}
+
 export default request

+ 82 - 28
src/components/LoginDialog.vue

@@ -19,7 +19,7 @@
         <el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="0">
           <el-form-item prop="username" class="login-form-item">
             <el-input
-              v-model="passwordForm.username"
+              v-model="passwordForm.account"
               placeholder="请输入账号"
               prefix-icon="User"
               clearable
@@ -36,6 +36,20 @@
               class="login-input"
             />
           </el-form-item>
+          <!-- 增加动态验证码图片 -->
+          <el-form-item prop="verifyCode" class="login-form-item">
+            <div class="gap10">
+              <el-input
+                v-model="passwordForm.verifyCode"
+                placeholder="请输入验证码"
+                prefix-icon="Lock"
+                show-password
+                class="login-input"
+              />
+              <img :src="captchaImg" alt="验证码" class="captcha-img" @click="getCaptchaFn">
+            </div>
+          </el-form-item>
+
           <el-form-item class="login-form-item remember-item">
             <el-checkbox v-model="passwordForm.remember" class="remember-checkbox">
               <span class="remember-text">记住密码</span>
@@ -181,42 +195,65 @@
 </template>
 
 <script setup>
-import { ref, reactive, watch } from 'vue'
+import { ref, reactive, watch, computed } from 'vue'
 import { ElMessage } from 'element-plus'
 import QQIcon from '@/assets/imgs/QQ.png'
 import WeChatIcon from '@/assets/imgs/WeChat.png'
+import { getCaptcha, loginUsername } from '@/api/auth.js'
+// 正则表达式
+const PHONE_REGEX = /^1[3-9]\d{9}$/;
+const EMAIL_REGEX = /^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$/;
+const isPasswordPhone = computed(() => {
+  return PHONE_REGEX.test(passwordForm.account);
+});
+
+const isPasswordEmail = computed(() => {
+  return EMAIL_REGEX.test(passwordForm.account);
+});
 
-// 定义props和emit
-const props = defineProps({
-  modelValue: {
-    type: Boolean,
-    default: false
-  }
-})
 
-const emit = defineEmits(['update:modelValue', 'login-success'])
+const emit = defineEmits(['login-success'])
 
 // 对话框可见性
-const dialogVisible = ref(props.modelValue)
+const dialogVisible = ref(true)
+// 验证码图片
+const captchaImg = ref('')
 
-// 监听对话框可见性变化
-watch(() => dialogVisible.value, (newVal) => {
-  emit('update:modelValue', newVal)
-})
 
-// 监听props变化
-watch(() => props.modelValue, (newVal) => {
-  dialogVisible.value = newVal
-})
+const open = () => {
+  dialogVisible.value = true;
+  getCaptchaFn()
+}
+
+const getCaptchaFn = async (type) => {
+  try {
+    const res = await getCaptcha()
+    passwordForm.uuid = res.data?.uuid;
+    if (res.data?.img.startsWith('data:image')) {
+      captchaImg.value = res.data?.img;
+    } else {
+      captchaImg.value = 'data:image/jpeg;base64,' + res.data?.img;
+    }
+    console.log(res)
+    // if (res.code === 200) {
+    //   ElMessage.success('验证码获取成功')
+    // } else {
+    //   ElMessage.error(res.msg || '验证码获取失败')
+    // }
+  } catch (error) {
+    ElMessage.error(error.message || '验证码获取失败')
+  }
+}
 
 // 当前激活的标签页
 const activeTab = ref('password')
 
 // 密码登录表单
 const passwordForm = reactive({
-  username: '',
+  account: '',
   password: '',
-  remember: false
+  captcha: '',
+  uuid: ''
 })
 
 // 短信登录表单
@@ -245,7 +282,7 @@ const emailCountdown = ref(0)
 
 // 表单验证规则
 const passwordRules = reactive({
-  username: [
+  account: [
     { required: true, message: '请输入账号', trigger: 'blur' }
   ],
   password: [
@@ -337,16 +374,29 @@ const sendEmailCode = () => {
 
 // 密码登录
 const handlePasswordLogin = () => {
-  passwordFormRef.value?.validate((valid) => {
+  passwordFormRef.value?.validate(async (valid) => {
     if (valid) {
       loading.value = true
-      // 模拟登录请求
-      setTimeout(() => {
-        loading.value = false
+       const params = new URLSearchParams();
+      // 根据账号类型决定参数名
+      if (isPasswordPhone.value) {
+          params.append('phone', passwordForm.account);
+      } else if (isPasswordEmail.value) {
+          params.append('email', passwordForm.account);
+      }
+      
+      params.append('password', passwordForm.password);
+      params.append('captcha', passwordForm.captcha);
+      params.append('uuid', passwordForm.uuid);
+      const res = await loginUsername(params);
+      loading.value = false
+      if (res.code === 200) {
         ElMessage.success('登录成功')
-        emit('login-success', { type: 'password', userInfo: passwordForm })
+        // emit('login-success', { type: 'password', userInfo: passwordForm })
         dialogVisible.value = false
-      }, 1500)
+      } else {
+        ElMessage.error(res.msg || '登录失败')
+      }
     }
   })
 }
@@ -392,6 +442,10 @@ const handleWechatLogin = () => {
 const handleQqLogin = () => {
   ElMessage.info('QQ登录功能开发中')
 }
+
+defineExpose({
+  open
+})
 </script>
 
 <style scoped lang="scss">

+ 3 - 0
vite.config.js

@@ -4,6 +4,9 @@ import path from 'path' //
 
 // https://vite.dev/config/
 export default defineConfig({
+  // 确保没有禁用环境变量加载
+  envDir: '.', // 环境变量文件目录
+  envPrefix: 'VITE_', // 环境变量前缀
   plugins: [vue()],
   resolve: {
     // 配置路径别名(可选,但推荐,避免相对路径层级混乱)