优化部分代码

This commit is contained in:
Kunagisa 2025-06-13 23:56:10 +08:00
parent 3bd286f50e
commit e222de1850
5 changed files with 446 additions and 69 deletions

View File

@ -0,0 +1,150 @@
<template>
<div v-if="visible" class="error-dialog-overlay" @click.self="handleClose">
<div class="error-dialog">
<div class="error-dialog-content">
{{ message }}
</div>
<div class="error-dialog-footer">
<button class="confirm-button" @click="handleClose">确定</button>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '错误提示'
},
message: {
type: String,
required: true
}
})
const emit = defineEmits(['close'])
const handleClose = () => {
emit('close')
}
</script>
<style scoped>
.error-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.error-dialog {
background: white;
border-radius: 8px;
width: 90%;
max-width: 400px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
animation: dialog-fade-in 0.3s ease;
}
.error-dialog-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-dialog-header h3 {
margin: 0;
color: #f56c6c;
font-size: 18px;
font-weight: 500;
}
.close-button {
background: none;
border: none;
font-size: 24px;
color: #909399;
cursor: pointer;
padding: 0;
line-height: 1;
}
.close-button:hover {
color: #f56c6c;
}
.error-dialog-content {
padding: 20px;
color: #606266;
font-size: 16px;
line-height: 1.5;
text-align: center;
}
.error-dialog-footer {
padding: 10px 20px 20px;
text-align: center;
}
.confirm-button {
background-color: #f56c6c;
color: white;
border: none;
padding: 8px 24px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.confirm-button:hover {
background-color: #f78989;
}
@keyframes dialog-fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 移动端适配 */
@media screen and (max-width: 480px) {
.error-dialog {
width: 85%;
}
.error-dialog-header h3 {
font-size: 16px;
}
.error-dialog-content {
font-size: 14px;
padding: 16px;
}
.confirm-button {
padding: 6px 20px;
font-size: 13px;
}
}
</style>

View File

@ -9,7 +9,6 @@
id="username" id="username"
v-model="username" v-model="username"
placeholder="请输入QQ号" placeholder="请输入QQ号"
@blur="validateUsername"
:class="{ 'error': usernameError }" :class="{ 'error': usernameError }"
/> />
<span class="error-message" v-if="usernameError">{{ usernameError }}</span> <span class="error-message" v-if="usernameError">{{ usernameError }}</span>
@ -21,7 +20,6 @@
id="password" id="password"
v-model="password" v-model="password"
placeholder="请输入密码" placeholder="请输入密码"
@blur="validatePassword"
:class="{ 'error': passwordError }" :class="{ 'error': passwordError }"
/> />
<span class="error-message" v-if="passwordError">{{ passwordError }}</span> <span class="error-message" v-if="passwordError">{{ passwordError }}</span>
@ -34,7 +32,6 @@
id="captcha" id="captcha"
v-model="captcha" v-model="captcha"
placeholder="请输入验证码" placeholder="请输入验证码"
@blur="validateCaptcha"
:class="{ 'error': captchaError }" :class="{ 'error': captchaError }"
/> />
<img <img
@ -54,6 +51,12 @@
<a @click.prevent="$emit('register')">注册账号</a> <a @click.prevent="$emit('register')">注册账号</a>
</div> </div>
</form> </form>
<ErrorDialog
:visible="showError"
:title="errorTitle"
:message="errorMessage"
@close="showError = false"
/>
</div> </div>
</template> </template>
@ -61,6 +64,7 @@
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { userLogin, getCaptcha } from '../api/login' import { userLogin, getCaptcha } from '../api/login'
import ErrorDialog from './ErrorDialog.vue'
const router = useRouter() const router = useRouter()
const username = ref('') const username = ref('')
@ -74,6 +78,11 @@ const usernameError = ref('')
const passwordError = ref('') const passwordError = ref('')
const captchaError = ref('') const captchaError = ref('')
//
const showError = ref(false)
const errorTitle = ref('错误提示')
const errorMessage = ref('')
const validateUsername = () => { const validateUsername = () => {
if (!username.value) { if (!username.value) {
usernameError.value = '请输入QQ号码' usernameError.value = '请输入QQ号码'
@ -130,6 +139,12 @@ const isFormValid = computed(() => {
captcha.value captcha.value
}) })
const showErrorMessage = (message, title = '错误提示') => {
errorMessage.value = message
errorTitle.value = title
showError.value = true
}
const refreshCaptcha = async () => { const refreshCaptcha = async () => {
try { try {
const response = await getCaptcha() const response = await getCaptcha()
@ -139,7 +154,7 @@ const refreshCaptcha = async () => {
captcha.value = '' // captcha.value = '' //
} catch (error) { } catch (error) {
console.error('获取验证码失败:', error) console.error('获取验证码失败:', error)
alert('获取验证码失败,请刷新页面重试') showErrorMessage('获取验证码失败,请刷新页面重试')
} }
} }
@ -172,10 +187,16 @@ const handleLogin = async () => {
} }
} catch (error) { } catch (error) {
console.error('登录失败:', error) console.error('登录失败:', error)
if (error.response && error.response.status === 401) { if (error.response) {
alert('错误的用户名或密码') switch (error.response.status) {
case 401:
showErrorMessage('错误的用户名或密码')
break
default:
showErrorMessage(error.response?.data?.message || error.message || '登录失败,请稍后重试')
}
} else { } else {
alert(error.response?.data?.message || error.message || '登录失败,请稍后重试') showErrorMessage(error.message || '登录失败,请稍后重试')
} }
// //
refreshCaptcha() refreshCaptcha()
@ -183,7 +204,50 @@ const handleLogin = async () => {
} }
const validateForm = () => { const validateForm = () => {
return validateUsername() && validatePassword() && validateCaptcha() //
usernameError.value = ''
passwordError.value = ''
captchaError.value = ''
//
if (!username.value) {
usernameError.value = '请输入QQ号码'
return false
}
if (!/^[/\d/g]+$/.test(username.value)) {
usernameError.value = 'QQ号码只能包含数字'
return false
}
if (username.value.length < 4) {
usernameError.value = 'QQ号码长度不能小于4个字符'
return false
}
//
if (!password.value) {
passwordError.value = '请输入密码'
return false
}
if (password.value.length < 4) {
passwordError.value = '密码长度不能小于4个字符'
return false
}
if (password.value.length > 20) {
passwordError.value = '密码长度不能超过20个字符'
return false
}
//
if (!captcha.value) {
captchaError.value = '请输入验证码'
return false
}
if (captcha.value.length !== 4) {
captchaError.value = '验证码长度不正确'
return false
}
return true
} }
</script> </script>

View File

@ -4,20 +4,20 @@
<form class="login-form-container" @submit.prevent="handleRegister"> <form class="login-form-container" @submit.prevent="handleRegister">
<div class="input-container"> <div class="input-container">
<label for="username">QQ号</label> <label for="username">QQ号</label>
<input type="text" id="username" v-model="username" placeholder="请输入QQ号" @blur="validateUsername" <input type="text" id="username" v-model="username" placeholder="请输入QQ号"
:class="{ 'error': usernameError }" /> :class="{ 'error': usernameError }" />
<span class="error-message" v-if="usernameError">{{ usernameError }}</span> <span class="error-message" v-if="usernameError">{{ usernameError }}</span>
</div> </div>
<div class="input-container"> <div class="input-container">
<label for="password">密码</label> <label for="password">密码</label>
<input type="password" id="password" v-model="password" placeholder="请输入密码" @blur="validatePassword" <input type="password" id="password" v-model="password" placeholder="请输入密码"
:class="{ 'error': passwordError }" /> :class="{ 'error': passwordError }" />
<span class="error-message" v-if="passwordError">{{ passwordError }}</span> <span class="error-message" v-if="passwordError">{{ passwordError }}</span>
</div> </div>
<div class="input-container"> <div class="input-container">
<label for="confirmPassword">再次输入密码</label> <label for="confirmPassword">再次输入密码</label>
<input type="password" id="confirmPassword" v-model="confirmPassword" placeholder="请输入密码" <input type="password" id="confirmPassword" v-model="confirmPassword" placeholder="请输入密码"
@blur="validateConfirmPassword" :class="{ 'error': confirmPasswordError }" /> :class="{ 'error': confirmPasswordError }" />
<span class="error-message" v-if="confirmPasswordError">{{ confirmPasswordError }}</span> <span class="error-message" v-if="confirmPasswordError">{{ confirmPasswordError }}</span>
</div> </div>
<div class="input-container captcha-container"> <div class="input-container captcha-container">
@ -28,7 +28,6 @@
id="captcha" id="captcha"
v-model="captcha" v-model="captcha"
placeholder="请输入验证码" placeholder="请输入验证码"
@blur="validateCaptcha"
:class="{ 'error': captchaError }" :class="{ 'error': captchaError }"
/> />
<img <img
@ -42,12 +41,18 @@
<span class="error-message" v-if="captchaError">{{ captchaError }}</span> <span class="error-message" v-if="captchaError">{{ captchaError }}</span>
</div> </div>
<div class="login-button"> <div class="login-button">
<button type="submit" :disabled="!isFormValid">注册</button> <button type="submit">注册</button>
</div> </div>
<div class="register-link"> <div class="register-link">
<a @click.prevent="$emit('login')">返回登陆</a> <a @click.prevent="$emit('login')">返回登陆</a>
</div> </div>
</form> </form>
<ErrorDialog
:visible="showError"
:title="errorTitle"
:message="errorMessage"
@close="showError = false"
/>
</div> </div>
</template> </template>
@ -55,6 +60,7 @@
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { userRegister, getCaptcha } from "@/api/login.js"; import { userRegister, getCaptcha } from "@/api/login.js";
import ErrorDialog from './ErrorDialog.vue'
const router = useRouter() const router = useRouter()
const username = ref('') const username = ref('')
@ -73,6 +79,17 @@ const passwordError = ref('')
const confirmPasswordError = ref('') const confirmPasswordError = ref('')
const captchaError = ref('') const captchaError = ref('')
//
const showError = ref(false)
const errorTitle = ref('错误提示')
const errorMessage = ref('')
const showErrorMessage = (message, title = '错误提示') => {
errorMessage.value = message
errorTitle.value = title
showError.value = true
}
// //
const validateUsername = () => { const validateUsername = () => {
if (!username.value) { if (!username.value) {
@ -127,18 +144,6 @@ const validateCaptcha = () => {
return true return true
} }
//
const isFormValid = computed(() => {
return !usernameError.value &&
!passwordError.value &&
!confirmPasswordError.value &&
!captchaError.value &&
username.value &&
password.value &&
confirmPassword.value &&
captcha.value
})
const refreshCaptcha = async () => { const refreshCaptcha = async () => {
try { try {
const response = await getCaptcha() const response = await getCaptcha()
@ -147,7 +152,7 @@ const refreshCaptcha = async () => {
captcha.value = '' // captcha.value = '' //
} catch (error) { } catch (error) {
console.error('获取验证码失败:', error) console.error('获取验证码失败:', error)
alert('获取验证码失败,请刷新页面重试') showErrorMessage('获取验证码失败,请刷新页面重试')
} }
} }
@ -158,7 +163,7 @@ onMounted(() => {
const handleRegister = async () => { const handleRegister = async () => {
try { try {
// //
if (!validateUsername() || !validatePassword() || !validateConfirmPassword() || !validateCaptcha()) { if (!validateForm()) {
return return
} }
@ -170,20 +175,80 @@ const handleRegister = async () => {
captcha.value captcha.value
) )
alert('注册成功') showErrorMessage('注册成功', '提示')
// //
emit('login') emit('login')
} catch (error) { } catch (error) {
console.error('注册失败:', error) console.error('注册失败:', error)
if (error.response && error.response.status === 400) { if (error.response) {
alert('该QQ号已被注册') switch (error.response.status) {
case 400:
showErrorMessage('该QQ号已被注册')
break
case 409:
showErrorMessage('用户名已存在')
break
default:
showErrorMessage(error.response?.data?.message || error.message || '注册失败,请稍后重试')
}
} else { } else {
alert(error.response?.data?.message || error.message || '注册失败,请稍后重试') showErrorMessage(error.message || '注册失败,请稍后重试')
} }
// //
refreshCaptcha() refreshCaptcha()
} }
} }
const validateForm = () => {
//
usernameError.value = ''
passwordError.value = ''
confirmPasswordError.value = ''
captchaError.value = ''
//
if (!username.value) {
usernameError.value = '请输入QQ号码'
return false
}
//
if (!/^\d+$/.test(username.value)) {
usernameError.value = 'QQ号只能包含数字'
return false
}
//
if (!password.value) {
passwordError.value = '请输入密码'
return false
}
if (password.value.length < 6) {
passwordError.value = '密码长度不能小于6个字符'
return false
}
//
if (!confirmPassword.value) {
confirmPasswordError.value = '请再次输入密码'
return false
}
if (confirmPassword.value !== password.value) {
confirmPasswordError.value = '两次输入的密码不一致'
return false
}
//
if (!captcha.value) {
captchaError.value = '请输入验证码'
return false
}
if (captcha.value.length !== 4) {
captchaError.value = '验证码长度不正确'
return false
}
return true
}
</script> </script>
<style scoped> <style scoped>

View File

@ -6,7 +6,13 @@
<div class="page-header"> <div class="page-header">
<h1>{{ authorName }}</h1> <h1>{{ authorName }}</h1>
</div> </div>
<div class="table-container"> <div v-if="isLoading" class="loading-container">
<div class="loading-bar">
<div class="loading-progress"></div>
</div>
<div class="loading-text">加载中...</div>
</div>
<div v-else class="table-container">
<table class="maps-table"> <table class="maps-table">
<thead> <thead>
<tr> <tr>
@ -39,7 +45,7 @@
</table> </table>
</div> </div>
<!-- 分页控件 --> <!-- 分页控件 -->
<div class="pagination"> <div v-if="!isLoading" class="pagination">
<button <button
class="page-btn" class="page-btn"
:disabled="currentPage === 1" :disabled="currentPage === 1"
@ -73,6 +79,13 @@
<button class="jump-btn" @click="handleJumpPage">跳转</button> <button class="jump-btn" @click="handleJumpPage">跳转</button>
</div> </div>
</div> </div>
<!-- 添加错误弹窗 -->
<ErrorDialog
:visible="showError"
:title="errorTitle"
:message="errorMessage"
@close="showError = false"
/>
</div> </div>
</template> </template>
@ -80,6 +93,7 @@
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { getMaps } from '@/api/maps.js' import { getMaps } from '@/api/maps.js'
import ErrorDialog from '@/components/ErrorDialog.vue'
import '../../assets/styles/common.css' import '../../assets/styles/common.css'
import '../../assets/styles/Maps.css' import '../../assets/styles/Maps.css'
@ -87,6 +101,12 @@ const route = useRoute()
const router = useRouter() const router = useRouter()
const authorMaps = ref([]) const authorMaps = ref([])
const authorName = ref('') const authorName = ref('')
const isLoading = ref(true)
//
const showError = ref(false)
const errorTitle = ref('')
const errorMessage = ref('')
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = 20 const pageSize = 20
@ -146,9 +166,16 @@ const formatDate = (dateString) => {
return date.toLocaleDateString() return date.toLocaleDateString()
} }
const showErrorMessage = (message, title = '') => {
errorMessage.value = message
errorTitle.value = title
showError.value = true
}
// //
const fetchAuthorMaps = async () => { const fetchAuthorMaps = async () => {
try { try {
isLoading.value = true
const author = route.query.author const author = route.query.author
if (!author) { if (!author) {
router.push('/author') router.push('/author')
@ -180,6 +207,9 @@ const fetchAuthorMaps = async () => {
console.log('作者地图列表:', authorMaps.value) console.log('作者地图列表:', authorMaps.value)
} catch (error) { } catch (error) {
console.error('获取作者地图列表失败:', error) console.error('获取作者地图列表失败:', error)
showErrorMessage('获取作者地图列表失败,请稍后重试')
} finally {
isLoading.value = false
} }
} }
@ -352,4 +382,57 @@ onMounted(() => {
.jump-btn:hover { .jump-btn:hover {
background: #1a237e; background: #1a237e;
} }
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
}
.loading-bar {
width: 300px;
height: 4px;
background-color: #f0f0f0;
border-radius: 2px;
overflow: hidden;
position: relative;
}
.loading-progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 30%;
background: linear-gradient(90deg, #2563eb, #1a237e);
border-radius: 2px;
animation: loading 1.5s ease-in-out infinite;
}
.loading-text {
margin-top: 12px;
color: #666;
font-size: 14px;
}
@keyframes loading {
0% {
left: -30%;
}
100% {
left: 100%;
}
}
@media screen and (max-width: 480px) {
.loading-bar {
width: 80%;
}
.loading-text {
font-size: 12px;
}
}
</style> </style>

View File

@ -50,7 +50,7 @@
</div> </div>
<!-- 评分弹窗 --> <!-- 评分弹窗 -->
<div class="score-dialog" v-if="showScoreDialog"> <div v-if="showScoreDialog" class="score-dialog">
<div class="dialog-overlay" @click="showScoreDialog = false"></div> <div class="dialog-overlay" @click="showScoreDialog = false"></div>
<div class="dialog-content"> <div class="dialog-content">
<div class="dialog-header"> <div class="dialog-header">
@ -69,11 +69,6 @@
@click="mapScore = star" @click="mapScore = star"
></span> ></span>
</div> </div>
<!-- <textarea
v-model="mapComment"
placeholder="请输入对地图的评价(选填)"
class="comment-input"
></textarea> -->
</div> </div>
<div class="score-section"> <div class="score-section">
@ -87,11 +82,6 @@
@click="authorScore = star" @click="authorScore = star"
></span> ></span>
</div> </div>
<!-- <textarea
v-model="authorComment"
placeholder="请输入对作者的评价(选填)"
class="comment-input"
></textarea> -->
</div> </div>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
@ -100,6 +90,13 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 添加错误弹窗 -->
<ErrorDialog
:visible="showError"
:title="errorTitle"
:message="errorMessage"
@close="handleErrorClose"
/>
</div> </div>
</template> </template>
@ -108,11 +105,33 @@ import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { getMapDetail } from '../../api/maps.js' import { getMapDetail } from '../../api/maps.js'
import { hasValidToken } from '../../utils/jwt' import { hasValidToken } from '../../utils/jwt'
import ErrorDialog from '@/components/ErrorDialog.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const map = ref(null) const map = ref(null)
//
const showError = ref(false)
const errorTitle = ref('')
const errorMessage = ref('')
const pendingAction = ref(null)
const showErrorMessage = (message, title = '', action = null) => {
errorMessage.value = message
errorTitle.value = title
showError.value = true
pendingAction.value = action
}
const handleErrorClose = () => {
showError.value = false
if (pendingAction.value) {
pendingAction.value()
pendingAction.value = null
}
}
// //
const showScoreDialog = ref(false) const showScoreDialog = ref(false)
const mapScore = ref(0) const mapScore = ref(0)
@ -129,16 +148,17 @@ const handleDownload = () => {
// //
const handleScoreClick = () => { const handleScoreClick = () => {
if (!hasValidToken()) { if (!hasValidToken()) {
alert('请先登录后再进行评分') showErrorMessage('请先登录后再进行评分', '', () => {
router.push({ router.push({
path: '/backend/login', path: '/backend/login',
query: { redirect: route.fullPath } query: { redirect: route.fullPath }
})
}) })
return return
} }
if (!hasDownloaded.value) { if (!hasDownloaded.value) {
alert('请先下载地图后再进行评分') showErrorMessage('请先下载地图后再进行评分')
return return
} }
@ -150,6 +170,7 @@ const fetchMapDetail = async () => {
map.value = await getMapDetail(route.params.id) map.value = await getMapDetail(route.params.id)
} catch (error) { } catch (error) {
console.error('获取地图详情失败:', error) console.error('获取地图详情失败:', error)
showErrorMessage('获取地图详情失败,请稍后重试')
} }
} }
@ -164,7 +185,7 @@ const formatDate = (dateString) => {
// //
const submitScores = async () => { const submitScores = async () => {
if (mapScore.value === 0 || authorScore.value === 0) { if (mapScore.value === 0 || authorScore.value === 0) {
alert('请为地图和作者都进行评分') showErrorMessage('请为地图和作者都进行评分')
return return
} }
@ -184,10 +205,10 @@ const submitScores = async () => {
mapComment.value = '' mapComment.value = ''
authorComment.value = '' authorComment.value = ''
alert('评分成功!') showErrorMessage('评分成功!')
} catch (error) { } catch (error) {
console.error('评分失败:', error) console.error('评分失败:', error)
alert('评分失败,请稍后重试') showErrorMessage('评分失败,请稍后重试')
} }
} }
@ -380,6 +401,15 @@ onMounted(() => {
} }
/* 评分弹窗样式 */ /* 评分弹窗样式 */
.score-dialog {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
}
.dialog-overlay { .dialog-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@ -387,7 +417,7 @@ onMounted(() => {
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
z-index: 100; z-index: 1001;
} }
.dialog-content { .dialog-content {
@ -400,7 +430,7 @@ onMounted(() => {
border-radius: 8px; border-radius: 8px;
width: 90%; width: 90%;
max-width: 500px; max-width: 500px;
z-index: 101; z-index: 1002;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
} }
@ -456,17 +486,6 @@ onMounted(() => {
color: #ffd700; color: #ffd700;
} }
.comment-input {
width: 100%;
height: 80px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
font-size: 14px;
margin-top: 10px;
}
.dialog-footer { .dialog-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@ -515,9 +534,5 @@ onMounted(() => {
.star { .star {
font-size: 20px; font-size: 20px;
} }
.comment-input {
height: 60px;
}
} }
</style> </style>