From 709cf29b6da853d8249199d006b66a2602d08728 Mon Sep 17 00:00:00 2001 From: lvjin Date: Tue, 15 Apr 2025 16:52:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=92=93=E9=B1=BC?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=BB=84=E4=BB=B6=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=92=93=E9=B1=BC=E9=A1=B5=E9=9D=A2=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增钓鱼日志组件,用于展示钓鱼记录。优化钓鱼页面的交互体验,增加动画效果和状态提示。同时,修复了请求拦截器中的错误处理逻辑,并改进了登录和注册页面的UI设计。 --- Dockerfile | 29 +++ nginx.conf | 33 +++ src/assets/fishing-scene.svg | 170 ++++++++++++++++ src/components/FishingLog.vue | 87 ++++++++ src/pages/equipments/Equipments.vue | 2 +- src/pages/fishbaskets/Fishbaskets.vue | 16 +- src/pages/fishing/Fishing.vue | 276 +++++++++++++++++++++----- src/pages/login/Login.vue | 127 ++++++++++-- src/pages/ranking/Ranking.vue | 115 ++++++----- src/pages/shop/Shop.vue | 14 +- src/utils/request.js | 94 ++++++--- 11 files changed, 804 insertions(+), 159 deletions(-) create mode 100644 Dockerfile create mode 100644 nginx.conf create mode 100644 src/assets/fishing-scene.svg create mode 100644 src/components/FishingLog.vue diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7cd6f63 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# 构建阶段 +FROM node:18-alpine as builder + +WORKDIR /app + +# 复制项目文件 +COPY package*.json ./ + +# 安装依赖 +RUN npm install + +# 复制源代码 +COPY . . + +# 构建项目 +RUN npm run build + +# 生产阶段 +FROM nginx:1.25.3-alpine + +# 从构建阶段复制构建结果到nginx目录 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 复制nginx配置 +COPY ./nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..cc8ff66 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,33 @@ +server { + listen 80; + server_name localhost; + + # 启用gzip压缩 + gzip on; + gzip_min_length 1k; + gzip_comp_level 6; + gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml; + gzip_vary on; + gzip_disable "MSIE [1-6]\."; + + # 设置根目录 + root /usr/share/nginx/html; + index index.html; + + # 启用 CORS + location / { + try_files $uri $uri/ /index.html; + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type'; + } + + # 处理 404 页面 + error_page 404 /index.html; + + # 缓存静态资源 + location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|json)$ { + expires max; + access_log off; + } +} \ No newline at end of file diff --git a/src/assets/fishing-scene.svg b/src/assets/fishing-scene.svg new file mode 100644 index 0000000..ca24bd2 --- /dev/null +++ b/src/assets/fishing-scene.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/FishingLog.vue b/src/components/FishingLog.vue new file mode 100644 index 0000000..e531a1e --- /dev/null +++ b/src/components/FishingLog.vue @@ -0,0 +1,87 @@ + + + + + \ No newline at end of file diff --git a/src/pages/equipments/Equipments.vue b/src/pages/equipments/Equipments.vue index 1dc9f46..ad12a83 100644 --- a/src/pages/equipments/Equipments.vue +++ b/src/pages/equipments/Equipments.vue @@ -51,7 +51,7 @@ onMounted(async () => { const handleEquip = async (id) => { try { await equip(id) - ElMessage.success('装备成功!') + ElMessage.success('装备成功!🎯') // 重新加载数据 const res = await getEquipments() equipmentList.value = res || [] diff --git a/src/pages/fishbaskets/Fishbaskets.vue b/src/pages/fishbaskets/Fishbaskets.vue index d3eaef8..4f41b9c 100644 --- a/src/pages/fishbaskets/Fishbaskets.vue +++ b/src/pages/fishbaskets/Fishbaskets.vue @@ -55,7 +55,7 @@ const fetchFishList = async () => { fishList.value = res; } } catch (err) { - ElMessage.warning('请检查网络或重试') + ElMessage.warning('请检查网络或重试 🌐') } } @@ -76,14 +76,14 @@ const handleProcessFish = async () => { try { const res = await autoHandleFish() if (res) { - ElMessage.success(res) + ElMessage.success(res + ' 🎉') fetchFishList() } } catch (err) { console.log('===================================='); console.log(err); console.log('===================================='); - ElMessage.warning('处理失败,请稍后再试') + ElMessage.warning('处理失败,请稍后再试 ⏳') } } // 手动处理鱼 @@ -92,14 +92,14 @@ const handleFish = async (fishId) => { try { const res = await handleFishById(fishId) if (res) { - ElMessage.success(res) + ElMessage.success(res + ' 🎉') fetchFishList() } } catch (err) { console.log('===================================='); console.log(err); console.log('===================================='); - ElMessage.warning('处理失败,请稍后再试') + ElMessage.warning('处理失败,请稍后再试 ⏳') } finally { loading.value = false @@ -119,13 +119,13 @@ const handleSell = async (fish) => { try { const res = await sellFish({ FishBagId: fish.id, Points: price }) if (res.success) { - ElMessage.success(res.message) + ElMessage.success(res.message + ' 🎉') fetchFishList() } else { - ElMessage.error('出售失败 ' + res.message); + ElMessage.error('出售失败 ' + res.message + ' 😢'); } } catch (err) { - ElMessage.error('出售失败,请稍后再试') + ElMessage.error('出售失败,请稍后再试 🛒') } } } diff --git a/src/pages/fishing/Fishing.vue b/src/pages/fishing/Fishing.vue index ec43a7a..a4629c6 100644 --- a/src/pages/fishing/Fishing.vue +++ b/src/pages/fishing/Fishing.vue @@ -1,68 +1,248 @@ diff --git a/src/pages/login/Login.vue b/src/pages/login/Login.vue index e566165..55bc0f3 100644 --- a/src/pages/login/Login.vue +++ b/src/pages/login/Login.vue @@ -37,6 +37,13 @@ + + + + + + + @@ -102,10 +109,10 @@ async function handleLogin () { localStorage.setItem('token', res.token) router.push('/') } else { - ElMessage.error('登录失败,未返回token') + ElMessage.error('登录失败,未返回token 🔑') } } catch (err) { - ElMessage.error('登录失败,请检查网络或重试') + //ElMessage.error('登录失败,请检查网络或重试') console.error('登录错误:', err) } } else { @@ -120,15 +127,17 @@ async function handleRegister () { try { const res = await register({ Name: form.value.Name, + Email: form.value.Email, Password: form.value.Password }) if (res) { - ElMessage.success('注册成功') + ElMessage.success('注册成功 😀😀') + toggleForm() // 注册成功后切换到登录表单 } else { - ElMessage.error('注册失败') + ElMessage.error('注册失败 😢😢') } } catch (err) { - ElMessage.error('注册失败,请检查网络或重试') + ElMessage.error('注册失败,请检查网络或重试 📶') console.error('注册失败:', err) } } else { @@ -139,6 +148,11 @@ async function handleRegister () { function toggleForm () { isLogin.value = !isLogin.value // 切换登录和注册表单 + form.value = { + Name: '', + Password: '' + } + formRef.value?.resetFields() // 重置表单验证状态 } @@ -147,44 +161,119 @@ function toggleForm () { display: flex; justify-content: center; align-items: center; - height: 100vh; - background-color: #f5f5f5; + min-height: 100vh; + background: linear-gradient(135deg, #0d47a1 0%, #1565c0 100%); + padding: 20px; } .login-card { - width: 500px; - padding: 20px; - background-color: #ffffff; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 500px; + padding: 30px; + background-color: rgba(255, 255, 255, 0.92); + border-radius: 20px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25), 0 0 15px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(20px); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.login-card:hover { + transform: translateY(-8px); + box-shadow: 0 15px 50px rgba(0, 0, 0, 0.18), 0 0 15px rgba(0, 0, 0, 0.08); } .title { text-align: center; - margin-bottom: 30px; - font-size: 2rem; - color: #409eff; - font-weight: bold; + margin-bottom: 40px; + font-size: 2.8rem; + background: linear-gradient(45deg, #0d47a1, #1976d2); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: 800; + letter-spacing: 3px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15); } .el-input { width: 100%; } +.el-input :deep(.el-input__wrapper) { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06); + border-radius: 12px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + padding: 8px 15px; +} + +.el-input :deep(.el-input__wrapper:hover) { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); +} + .el-button { width: 100%; + height: 48px; + border-radius: 12px; + font-size: 17px; + font-weight: 600; + letter-spacing: 1.2px; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.el-button:not(.el-button--text) { + background: linear-gradient(45deg, #0d47a1, #1976d2); + border: none; + box-shadow: 0 4px 15px rgba(13, 71, 161, 0.4); +} + +.el-button:not(.el-button--text):hover { + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(30, 136, 229, 0.5); } .toggle-link { - margin-top: 15px; + margin-top: 35px; text-align: center; + padding: 20px 16px 0; + border-top: 1px solid rgba(235, 238, 245, 0.8); + transition: all 0.3s ease; } .toggle-link span { - margin-right: 5px; + margin-right: 8px; + color: #606266; + font-size: 15px; } -.toggle-link el-button { +.toggle-link .el-button { padding: 0; - font-size: 14px; + font-size: 16px; + font-weight: 600; + background: none; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + margin-left: 4px; + height: auto; +} + +.toggle-link .el-button:hover { + color: #1e88e5; + transform: scale(1.08); + text-shadow: 0 0 10px rgba(30, 136, 229, 0.3); +} + +.el-form-item { + margin-bottom: 28px; +} + +@media (max-width: 576px) { + .login-card { + padding: 25px; + } + + .title { + font-size: 2.2rem; + margin-bottom: 35px; + } } diff --git a/src/pages/ranking/Ranking.vue b/src/pages/ranking/Ranking.vue index b4c80cb..57fdb85 100644 --- a/src/pages/ranking/Ranking.vue +++ b/src/pages/ranking/Ranking.vue @@ -8,81 +8,90 @@ * @FilePath: \go_fish_web\src\pages\ranking\Ranking.vue --> diff --git a/src/pages/shop/Shop.vue b/src/pages/shop/Shop.vue index 8967c0f..aefea32 100644 --- a/src/pages/shop/Shop.vue +++ b/src/pages/shop/Shop.vue @@ -37,7 +37,7 @@ const fetchData = async () => { points.value = pointsRes.points items.value = pointsRes.items } catch (err) { - ElMessage.error('加载数据失败,请重试') + ElMessage.error('加载数据失败,请重试 🛍️') } } @@ -52,16 +52,16 @@ const buyItem = async (good) => { if (count) { try { - const res = await await buy({ EquipmentId: good.id, Quantity: 1 }) + const res = await await buy({ EquipmentId: good.id, Quantity: count }) if (res) { - good.myQuantity += 1 - points.value -= good.points - ElMessage.success("购买成功") + good.myQuantity += count + points.value -= count + ElMessage.success("购买成功 😎") } else { - ElMessage.error(res.message) + ElMessage.error(res.message + ' 😞') } } catch (err) { - ElMessage.error('购买失败,请稍后再试') + ElMessage.error('购买失败,请稍后再试 👿') } } } diff --git a/src/utils/request.js b/src/utils/request.js index 7c79616..a38ca93 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -7,58 +7,106 @@ * @LastEditTime: 2025-04-12 14:17:15 * @FilePath: \go_fish_web\src\utils\request.js */ -import axios from 'axios' -import { ElMessage } from 'element-plus' -import router from '@/router' +import axios from "axios"; +import { ElMessage } from "element-plus"; +import router from "@/router"; -let isRefreshing = false +let isRefreshing = false; +let requestQueue = []; // 创建 axios 实例 +// 清理登录状态的函数 +export function clearLoginState() { + localStorage.removeItem("token"); + localStorage.removeItem("fishingLogs"); +} + const request = axios.create({ - baseURL: 'http://49.235.165.171:31001', + baseURL: "http://49.235.165.171:31001", timeout: 5000, -}) +}); // 设置默认 Content-Type -request.defaults.headers['Content-Type'] = 'application/json' +request.defaults.headers["Content-Type"] = "application/json"; // 请求拦截器:添加 token request.interceptors.request.use( (config) => { - const token = localStorage.getItem('token') + const token = localStorage.getItem("token"); if (token) { - config.headers.Authorization = `Bearer ${token}` + config.headers.Authorization = `Bearer ${token}`; + // 登录成功后重试队列中的请求 + if (requestQueue.length > 0) { + requestQueue.forEach(({ resolve, config }) => { + resolve(request(config)); + }); + requestQueue = []; + } } - return config + return config; }, (error) => Promise.reject(error) -) +); // 响应拦截器 request.interceptors.response.use( (response) => response.data, (error) => { - const status = error.response?.status + console.dir(error); + const status = error.response?.status; + const config = error.config; - if (status === 401) { + if (status === 400) { + // 处理400错误,解析并显示具体的验证错误信息 + const errorData = error.response?.data; + if (errorData?.errors) { + // 收集所有错误信息 + const errorMessages = []; + Object.entries(errorData.errors).forEach(([field, messages]) => { + if (Array.isArray(messages)) { + messages.forEach((msg) => errorMessages.push(msg)); + } + }); + // 显示合并后的错误信息 + if (errorMessages.length > 0) { + ElMessage.error(errorMessages.join("\n")); + } else { + ElMessage.error("请求参数验证失败"); + } + } else { + // 如果没有详细的错误信息,显示通用错误消息 + ElMessage.error(errorData?.title || "请求参数错误"); + } + return Promise.reject(error); + } else if (status === 401) { // 避免重复处理 401 if (!isRefreshing) { - isRefreshing = true - ElMessage.warning('登录已过期,请重新登录') - localStorage.removeItem('token') - router.push('/login') + isRefreshing = true; + ElMessage.warning("登录已过期,请重新登录"); + clearLoginState(); + // 清空请求队列 + requestQueue = []; + router.push("/login"); // 重置标志位,避免死锁 setTimeout(() => { - isRefreshing = false - }, 3000) // 避免多次跳转,给 3 秒冷却时间 + isRefreshing = false; + }, 3000); // 避免多次跳转,给 3 秒冷却时间 } + + // 将请求加入队列 + return new Promise((resolve) => { + requestQueue.push({ + resolve, + config, + }); + }); } else { - ElMessage.error(error.response?.data?.message || '请求失败,请稍后再试') + ElMessage.error(error.response?.data?.message || "请求失败,请稍后再试"); } - return Promise.reject(error) + return Promise.reject(error); } -) +); -export default request +export default request;