如果vue使用redis会报出Uncaught ReferenceError: Buffer is not defined错误,但是使用在node.js中就没问题
This commit is contained in:
parent
ce92712a30
commit
d5ee474b09
9
package-lock.json
generated
9
package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue-tournament-bracket": "^3.0.0",
|
"vue-tournament-bracket": "^3.0.0",
|
||||||
@ -2472,6 +2473,14 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue-tournament-bracket": "^3.0.0",
|
"vue-tournament-bracket": "^3.0.0",
|
||||||
|
@ -12,3 +12,83 @@ export const getMapEditors = async () => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户提交地图评分
|
||||||
|
* @param {string} mapName - 地图名称
|
||||||
|
* @param {number} reward - 评分(1~5)
|
||||||
|
* @returns {Promise<any>} 返回提交评分的响应数据
|
||||||
|
*/
|
||||||
|
export const submitMapReward = async (mapName, reward) => {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
mapname: mapName,
|
||||||
|
reward: reward
|
||||||
|
};
|
||||||
|
const response = await axiosInstance.post('/user/map/reward', payload);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交地图评分失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户地图信息
|
||||||
|
* @param {string} mapName - 地图名称
|
||||||
|
* @returns {Promise<any>} 返回用户地图信息的Promise对象
|
||||||
|
*/
|
||||||
|
export const getUserMapInfo = async (mapName) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(`/user/map/${mapName}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户地图信息失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户下载地图信息
|
||||||
|
* @param {string} mapName - 地图名称
|
||||||
|
* @returns {Promise<any>} 返回更新下载信息的响应数据
|
||||||
|
*/
|
||||||
|
export const updateUserDownloadMapInfo = async (mapName) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post(`/map/download/${mapName}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用户下载地图信息失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定地图评分
|
||||||
|
* @param {string} mapName - 地图名称
|
||||||
|
* @returns {Promise<any>} 返回一个包含指定地图评分信息的Promise对象
|
||||||
|
*/
|
||||||
|
export const getAppointMapRating = async (mapName) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(`/map/reward/${mapName}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取指定地图评分失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有地图的评分
|
||||||
|
* @returns {Promise<any>} 返回一个包含所有地图评分信息的Promise对象
|
||||||
|
*/
|
||||||
|
export const getAllMapRating = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(`/map/reward`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取所有地图评分失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ export const userLogin = async (username, password, token, captcha) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.access_token) {
|
if (response.data.access_token) {
|
||||||
loginSuccess(response.data.access_token);
|
loginSuccess(response.data.access_token, username);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
128
src/components/SuccessDialog.vue
Normal file
128
src/components/SuccessDialog.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="success-dialog-overlay" @click.self="handleClose">
|
||||||
|
<div class="success-dialog">
|
||||||
|
<div class="success-dialog-content">
|
||||||
|
<div class="success-icon">✓</div>
|
||||||
|
<div class="success-message">{{ message }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="success-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
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.success-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-dialog-content {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #67c23a;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-dialog-footer {
|
||||||
|
padding: 10px 20px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-button {
|
||||||
|
background-color: #67c23a;
|
||||||
|
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: #85ce61;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialog-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.success-dialog {
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-button {
|
||||||
|
padding: 6px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -45,7 +45,7 @@
|
|||||||
<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('register')">注册账号</a>
|
<a @click.prevent="$emit('register')">注册账号</a>
|
||||||
@ -83,16 +83,6 @@ const showError = ref(false)
|
|||||||
const errorTitle = ref('错误提示')
|
const errorTitle = ref('错误提示')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
|
||||||
return !usernameError.value &&
|
|
||||||
!passwordError.value &&
|
|
||||||
!captchaError.value &&
|
|
||||||
username.value &&
|
|
||||||
password.value &&
|
|
||||||
captcha.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const showErrorMessage = (message, title = '错误提示') => {
|
const showErrorMessage = (message, title = '错误提示') => {
|
||||||
errorMessage.value = message
|
errorMessage.value = message
|
||||||
errorTitle.value = title
|
errorTitle.value = title
|
||||||
@ -108,8 +98,14 @@ const refreshCaptcha = async () => {
|
|||||||
captcha.value = '' // 清空验证码输入
|
captcha.value = '' // 清空验证码输入
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取验证码失败:', error)
|
console.error('获取验证码失败:', error)
|
||||||
|
// 优先使用API返回的detail字段
|
||||||
|
const apiError = error.response?.data?.detail || error.response?.data?.message
|
||||||
|
if (apiError) {
|
||||||
|
showErrorMessage(apiError)
|
||||||
|
} else {
|
||||||
showErrorMessage('获取验证码失败,请刷新页面重试')
|
showErrorMessage('获取验证码失败,请刷新页面重试')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -128,23 +124,18 @@ const handleLogin = async () => {
|
|||||||
captcha.value
|
captcha.value
|
||||||
)
|
)
|
||||||
|
|
||||||
// 登录成功,跳转到首页
|
// 登录成功,API中的loginSuccess函数会处理跳转
|
||||||
if (response.access_token) {
|
// 不需要在这里再次设置localStorage或跳转路由
|
||||||
localStorage.setItem('access_token', response.access_token)
|
console.log('登录成功')
|
||||||
localStorage.setItem('user_id', username.value)
|
|
||||||
router.push('/')
|
|
||||||
} else {
|
|
||||||
throw new Error('登录失败:未获取到访问令牌')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error)
|
console.error('登录失败:', error)
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
switch (error.response.status) {
|
// 优先使用API返回的detail字段
|
||||||
case 401:
|
const apiError = error.response?.data?.detail || error.response?.data?.message
|
||||||
showErrorMessage('错误的用户名或密码')
|
if (apiError) {
|
||||||
break
|
showErrorMessage(apiError)
|
||||||
default:
|
} else {
|
||||||
showErrorMessage(error.response?.data?.message || error.message || '登录失败,请稍后重试')
|
showErrorMessage('登录失败,请稍后重试')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showErrorMessage(error.message || '登录失败,请稍后重试')
|
showErrorMessage(error.message || '登录失败,请稍后重试')
|
||||||
@ -165,7 +156,8 @@ const validateForm = () => {
|
|||||||
usernameError.value = '请输入QQ号码'
|
usernameError.value = '请输入QQ号码'
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!/^[/\d/g]+$/.test(username.value)) {
|
// 修正正则表达式:只允许纯数字
|
||||||
|
if (!/^\d+$/.test(username.value)) {
|
||||||
usernameError.value = 'QQ号码只能包含数字'
|
usernameError.value = 'QQ号码只能包含数字'
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -201,6 +193,22 @@ const validateForm = () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置表单数据
|
||||||
|
const resetForm = () => {
|
||||||
|
username.value = ''
|
||||||
|
password.value = ''
|
||||||
|
captcha.value = ''
|
||||||
|
usernameError.value = ''
|
||||||
|
passwordError.value = ''
|
||||||
|
captchaError.value = ''
|
||||||
|
refreshCaptcha()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露resetForm方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
resetForm
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshCaptcha()
|
refreshCaptcha()
|
||||||
})
|
})
|
||||||
|
@ -53,6 +53,11 @@
|
|||||||
:message="errorMessage"
|
:message="errorMessage"
|
||||||
@close="showError = false"
|
@close="showError = false"
|
||||||
/>
|
/>
|
||||||
|
<SuccessDialog
|
||||||
|
:visible="showSuccess"
|
||||||
|
:message="successMessage"
|
||||||
|
@close="showSuccess = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -61,6 +66,7 @@ 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'
|
import ErrorDialog from './ErrorDialog.vue'
|
||||||
|
import SuccessDialog from './SuccessDialog.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
@ -84,12 +90,21 @@ const showError = ref(false)
|
|||||||
const errorTitle = ref('错误提示')
|
const errorTitle = ref('错误提示')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
// 成功提示相关
|
||||||
|
const showSuccess = ref(false)
|
||||||
|
const successMessage = ref('')
|
||||||
|
|
||||||
const showErrorMessage = (message, title = '错误提示') => {
|
const showErrorMessage = (message, title = '错误提示') => {
|
||||||
errorMessage.value = message
|
errorMessage.value = message
|
||||||
errorTitle.value = title
|
errorTitle.value = title
|
||||||
showError.value = true
|
showError.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showSuccessMessage = (message) => {
|
||||||
|
successMessage.value = message
|
||||||
|
showSuccess.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const refreshCaptcha = async () => {
|
const refreshCaptcha = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await getCaptcha()
|
const response = await getCaptcha()
|
||||||
@ -98,8 +113,14 @@ const refreshCaptcha = async () => {
|
|||||||
captcha.value = '' // 清空验证码输入
|
captcha.value = '' // 清空验证码输入
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取验证码失败:', error)
|
console.error('获取验证码失败:', error)
|
||||||
|
// 优先使用API返回的detail字段
|
||||||
|
const apiError = error.response?.data?.detail || error.response?.data?.message
|
||||||
|
if (apiError) {
|
||||||
|
showErrorMessage(apiError)
|
||||||
|
} else {
|
||||||
showErrorMessage('获取验证码失败,请刷新页面重试')
|
showErrorMessage('获取验证码失败,请刷新页面重试')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -121,21 +142,31 @@ const handleRegister = async () => {
|
|||||||
captcha.value
|
captcha.value
|
||||||
)
|
)
|
||||||
|
|
||||||
showErrorMessage('注册成功', '提示')
|
// 显示成功提示
|
||||||
// 切换到登录模块
|
showSuccessMessage('注册成功!正在切换到登录页面...')
|
||||||
|
|
||||||
|
// 清空表单数据
|
||||||
|
username.value = ''
|
||||||
|
password.value = ''
|
||||||
|
confirmPassword.value = ''
|
||||||
|
captcha.value = ''
|
||||||
|
|
||||||
|
// 刷新验证码
|
||||||
|
refreshCaptcha()
|
||||||
|
|
||||||
|
// 延迟1.5秒后切换到登录模块,让用户看到成功提示
|
||||||
|
setTimeout(() => {
|
||||||
emit('login')
|
emit('login')
|
||||||
|
}, 1500)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('注册失败:', error)
|
console.error('注册失败:', error)
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
switch (error.response.status) {
|
// 优先使用API返回的detail字段
|
||||||
case 400:
|
const apiError = error.response?.data?.detail || error.response?.data?.message
|
||||||
showErrorMessage('该QQ号已被注册')
|
if (apiError) {
|
||||||
break
|
showErrorMessage(apiError)
|
||||||
case 409:
|
} else {
|
||||||
showErrorMessage('用户名已存在')
|
showErrorMessage('注册失败,请稍后重试')
|
||||||
break
|
|
||||||
default:
|
|
||||||
showErrorMessage(error.response?.data?.message || error.message || '注册失败,请稍后重试')
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showErrorMessage(error.message || '注册失败,请稍后重试')
|
showErrorMessage(error.message || '注册失败,请稍后重试')
|
||||||
@ -281,7 +312,7 @@ const validateForm = () => {
|
|||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-button button:hover:not(:disabled) {
|
.login-button button:hover {
|
||||||
background: linear-gradient(90deg, #66b1ff 0%, #6dd5fa 100%);
|
background: linear-gradient(90deg, #66b1ff 0%, #6dd5fa 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,16 +72,24 @@ export const logoutUser = () => { // 不再是 async,因为它不执行异步
|
|||||||
* @param {string} userId - 用户ID。
|
* @param {string} userId - 用户ID。
|
||||||
*/
|
*/
|
||||||
export const loginSuccess = (accessToken, userId) => {
|
export const loginSuccess = (accessToken, userId) => {
|
||||||
// console.log('jwt.js: loginSuccess. Storing token and user ID.');
|
// 清除可能存在的旧数据
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('user_id');
|
||||||
|
sessionStorage.removeItem('currentUser');
|
||||||
|
|
||||||
|
// 存储新的认证信息
|
||||||
localStorage.setItem('access_token', accessToken);
|
localStorage.setItem('access_token', accessToken);
|
||||||
if (userId) {
|
if (userId) {
|
||||||
localStorage.setItem('user_id', userId);
|
localStorage.setItem('user_id', userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
justLoggedIn.value = true; // Set the flag before navigation
|
// 设置登录标志
|
||||||
|
justLoggedIn.value = true;
|
||||||
|
|
||||||
// 登录成功后重定向
|
// 获取重定向路径,优先使用查询参数中的redirect,否则跳转到首页
|
||||||
// 尝试获取之前尝试访问的路径,否则重定向到默认路径(例如仪表盘)
|
const currentRoute = router.currentRoute.value;
|
||||||
const redirectPath = router.currentRoute.value.query.redirect || '/backend/dashboard';
|
const redirectPath = currentRoute.query.redirect || '/';
|
||||||
|
|
||||||
|
// 使用replace而不是push,避免在历史记录中留下登录页面
|
||||||
router.replace(redirectPath);
|
router.replace(redirectPath);
|
||||||
};
|
};
|
@ -11,16 +11,17 @@
|
|||||||
返回主界面
|
返回主界面
|
||||||
</button>
|
</button>
|
||||||
<div class="form-content">
|
<div class="form-content">
|
||||||
<LoginModule v-if="!showRegister" @register="showRegister = true" />
|
<LoginModule ref="loginModuleRef" v-if="!showRegister" @register="showRegister = true" />
|
||||||
<RegisterModule v-else @login="showRegister = false" />
|
<RegisterModule v-else @login="handleSwitchToLogin" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { hasValidToken } from '@/utils/jwt'
|
||||||
import loginBg from '@/assets/login_1.jpg'
|
import loginBg from '@/assets/login_1.jpg'
|
||||||
import loginBg1 from '@/assets/login_2.jpg'
|
import loginBg1 from '@/assets/login_2.jpg'
|
||||||
import loginBg3 from '@/assets/login_3.jpg'
|
import loginBg3 from '@/assets/login_3.jpg'
|
||||||
@ -33,10 +34,29 @@ const bgImg = ref(images[randomIndex])
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const showRegister = ref(false)
|
const showRegister = ref(false)
|
||||||
|
const loginModuleRef = ref(null)
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理从注册模块切换到登录模块
|
||||||
|
const handleSwitchToLogin = () => {
|
||||||
|
showRegister.value = false
|
||||||
|
// 在下一个tick中重置登录表单,确保组件已经渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
if (loginModuleRef.value) {
|
||||||
|
loginModuleRef.value.resetForm()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态,如果已登录则跳转到首页
|
||||||
|
onMounted(() => {
|
||||||
|
if (hasValidToken()) {
|
||||||
|
router.replace('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted } from 'vue'
|
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { getUserInfo } from '@/utils/jwt'
|
import { getUserInfo } from '@/utils/jwt'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ const isAdmin = computed(() => {
|
|||||||
const showMobileMenu = ref(false)
|
const showMobileMenu = ref(false)
|
||||||
const currentUserData = ref(null)
|
const currentUserData = ref(null)
|
||||||
const showDropdown = ref(false)
|
const showDropdown = ref(false)
|
||||||
|
const userInfoNavRef = ref(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const userAvatarUrl = computed(() => {
|
const userAvatarUrl = computed(() => {
|
||||||
@ -38,6 +39,16 @@ const handleLogout = () => {
|
|||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleClickOutside = (event: Event) => {
|
||||||
|
if (userInfoNavRef.value && !userInfoNavRef.value.contains(event.target as Node)) {
|
||||||
|
showDropdown.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
showDropdown.value = !showDropdown.value
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (localStorage.getItem('access_token')) {
|
if (localStorage.getItem('access_token')) {
|
||||||
getUserInfo().then(userInfo => {
|
getUserInfo().then(userInfo => {
|
||||||
@ -48,6 +59,14 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加点击外部关闭下拉菜单的监听器
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 移除事件监听器
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -72,16 +91,16 @@ onMounted(() => {
|
|||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
登录
|
登录
|
||||||
</router-link>
|
</router-link>
|
||||||
<div v-if="isLoggedIn && currentUserData" class="user-info-nav" @click="showDropdown = !showDropdown">
|
<div v-if="isLoggedIn && currentUserData" class="user-info-nav" ref="userInfoNavRef" @click="toggleDropdown">
|
||||||
<img v-if="userAvatarUrl" :src="userAvatarUrl" alt="User Avatar" class="nav-avatar" />
|
<img v-if="userAvatarUrl" :src="userAvatarUrl" alt="User Avatar" class="nav-avatar" />
|
||||||
<span class="nav-username">{{ currentUserData.username }}</span>
|
<span class="nav-username">{{ currentUserData.username }}</span>
|
||||||
<i class="fas fa-chevron-down dropdown-icon"></i>
|
<i class="fas fa-chevron-down dropdown-icon"></i>
|
||||||
<div v-show="showDropdown" class="dropdown-menu">
|
<div v-show="showDropdown" class="dropdown-menu">
|
||||||
<div v-if="isAdmin" class="dropdown-item" @click="router.push('/backend/dashboard'); showDropdown = false">
|
<div v-if="isAdmin" class="dropdown-item" @click.stop="router.push('/backend/dashboard'); showDropdown = false">
|
||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-cog"></i>
|
||||||
管理后台
|
管理后台
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item" @click="handleLogout">
|
<div class="dropdown-item" @click.stop="handleLogout">
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
退出登录
|
退出登录
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,6 +77,10 @@
|
|||||||
v-model="addForm.author_contact"
|
v-model="addForm.author_contact"
|
||||||
class="input"
|
class="input"
|
||||||
placeholder="请输入您的QQ号"
|
placeholder="请输入您的QQ号"
|
||||||
|
type="text"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
inputmode="numeric"
|
||||||
|
readonly
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -388,10 +392,12 @@ const closeReplyModal = () => {
|
|||||||
|
|
||||||
// 重置回复表单
|
// 重置回复表单
|
||||||
const resetReplyForm = () => {
|
const resetReplyForm = () => {
|
||||||
|
// 获取用户存储的qq_code
|
||||||
|
const user = getStoredUser()
|
||||||
addForm.value = {
|
addForm.value = {
|
||||||
sendcontent: '',
|
sendcontent: '',
|
||||||
author: '',
|
author: '',
|
||||||
author_contact: '',
|
author_contact: user && user.qq_code ? user.qq_code : '',
|
||||||
replyTo: ''
|
replyTo: ''
|
||||||
};
|
};
|
||||||
addError.value = '';
|
addError.value = '';
|
||||||
@ -496,11 +502,6 @@ const submitReply = async () => {
|
|||||||
addError.value = 'QQ号不能为空';
|
addError.value = 'QQ号不能为空';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 验证QQ号是否为纯数字
|
|
||||||
if (!/^\d+$/.test(addForm.value.author_contact)) {
|
|
||||||
addError.value = 'QQ号必须为纯数字';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addLoading.value = true;
|
addLoading.value = true;
|
||||||
addError.value = '';
|
addError.value = '';
|
||||||
|
@ -182,6 +182,8 @@ const fetchAuthorMaps = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
authorName.value = author
|
authorName.value = author
|
||||||
|
|
||||||
|
// 使用原有的API调用方式
|
||||||
const cacheKey = 'allMaps'
|
const cacheKey = 'allMaps'
|
||||||
let allMaps = []
|
let allMaps = []
|
||||||
const cached = sessionStorage.getItem(cacheKey)
|
const cached = sessionStorage.getItem(cacheKey)
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
<div class="map-image">
|
<div class="map-image">
|
||||||
<img :src="map.img_file" :alt="map.chinese_name">
|
<img :src="map.img_file" :alt="map.chinese_name">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="map-info">
|
<div class="map-info">
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
@ -33,6 +32,10 @@
|
|||||||
<span class="info-label">创建时间</span>
|
<span class="info-label">创建时间</span>
|
||||||
<span class="info-value">{{ formatDate(map.create_time) }}</span>
|
<span class="info-value">{{ formatDate(map.create_time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="info-label">地图评分</span>
|
||||||
|
<span class="info-value">{{ mapRating ? mapRating.toFixed(1) : '暂无评分' }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
@ -44,7 +47,13 @@
|
|||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a :href="map.zip_file" class="download-btn" download @click="handleDownload">下载地图</a>
|
<a :href="map.zip_file" class="download-btn" download @click="handleDownload">下载地图</a>
|
||||||
<a class="score-btn" @click="handleScoreClick">地图评分</a>
|
<a
|
||||||
|
class="score-btn"
|
||||||
|
:class="{ 'disabled': userMapInfo && userMapInfo.is_rewarded }"
|
||||||
|
@click="handleScoreClick"
|
||||||
|
>
|
||||||
|
{{ userMapInfo && userMapInfo.is_rewarded ? '已评分' : '地图评分' }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -70,19 +79,6 @@
|
|||||||
>★</span>
|
>★</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="score-section">
|
|
||||||
<h3>作者评分</h3>
|
|
||||||
<div class="rating">
|
|
||||||
<span
|
|
||||||
v-for="star in 5"
|
|
||||||
:key="'author-'+star"
|
|
||||||
class="star"
|
|
||||||
:class="{ active: star <= authorScore }"
|
|
||||||
@click="authorScore = star"
|
|
||||||
>★</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button class="cancel-btn" @click="showScoreDialog = false">取消</button>
|
<button class="cancel-btn" @click="showScoreDialog = false">取消</button>
|
||||||
@ -104,12 +100,14 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
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 { submitMapReward, updateUserDownloadMapInfo, getUserMapInfo, getAppointMapRating } from '../../api/centre_maps.js'
|
||||||
import { hasValidToken } from '../../utils/jwt'
|
import { hasValidToken } from '../../utils/jwt'
|
||||||
import ErrorDialog from '@/components/ErrorDialog.vue'
|
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 mapRating = ref(null)
|
||||||
|
|
||||||
// 错误弹窗相关
|
// 错误弹窗相关
|
||||||
const showError = ref(false)
|
const showError = ref(false)
|
||||||
@ -135,18 +133,52 @@ const handleErrorClose = () => {
|
|||||||
// 评分相关的响应式变量
|
// 评分相关的响应式变量
|
||||||
const showScoreDialog = ref(false)
|
const showScoreDialog = ref(false)
|
||||||
const mapScore = ref(0)
|
const mapScore = ref(0)
|
||||||
const authorScore = ref(0)
|
|
||||||
const mapComment = ref('')
|
|
||||||
const authorComment = ref('')
|
|
||||||
const hasDownloaded = ref(false)
|
const hasDownloaded = ref(false)
|
||||||
|
const userMapInfo = ref(null)
|
||||||
|
|
||||||
|
// 获取地图评分
|
||||||
|
const fetchMapRating = async () => {
|
||||||
|
try {
|
||||||
|
const ratingData = await getAppointMapRating(map.value.name)
|
||||||
|
mapRating.value = ratingData.rewards
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取地图评分失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理下载点击
|
// 处理下载点击
|
||||||
const handleDownload = () => {
|
const handleDownload = async () => {
|
||||||
|
try {
|
||||||
|
await updateUserDownloadMapInfo(map.value.name)
|
||||||
hasDownloaded.value = true
|
hasDownloaded.value = true
|
||||||
|
// 重新获取用户地图信息
|
||||||
|
await fetchUserMapInfo()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新下载信息失败:', error)
|
||||||
|
// 即使更新失败,也允许用户继续操作
|
||||||
|
hasDownloaded.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户地图信息
|
||||||
|
const fetchUserMapInfo = async () => {
|
||||||
|
if (!hasValidToken()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
userMapInfo.value = await getUserMapInfo(map.value.name)
|
||||||
|
hasDownloaded.value = userMapInfo.value.is_download
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户地图信息失败:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理评分点击
|
// 处理评分点击
|
||||||
const handleScoreClick = () => {
|
const handleScoreClick = () => {
|
||||||
|
// 如果已经评分,直接返回
|
||||||
|
if (userMapInfo.value && userMapInfo.value.is_rewarded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasValidToken()) {
|
if (!hasValidToken()) {
|
||||||
showErrorMessage('请先登录后再进行评分', '', () => {
|
showErrorMessage('请先登录后再进行评分', '', () => {
|
||||||
router.push({
|
router.push({
|
||||||
@ -168,6 +200,11 @@ const handleScoreClick = () => {
|
|||||||
const fetchMapDetail = async () => {
|
const fetchMapDetail = async () => {
|
||||||
try {
|
try {
|
||||||
map.value = await getMapDetail(route.params.id)
|
map.value = await getMapDetail(route.params.id)
|
||||||
|
// 获取地图详情后,获取用户地图信息和地图评分
|
||||||
|
await Promise.all([
|
||||||
|
fetchUserMapInfo(),
|
||||||
|
fetchMapRating()
|
||||||
|
])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取地图详情失败:', error)
|
console.error('获取地图详情失败:', error)
|
||||||
showErrorMessage('获取地图详情失败,请稍后重试')
|
showErrorMessage('获取地图详情失败,请稍后重试')
|
||||||
@ -184,26 +221,24 @@ const formatDate = (dateString) => {
|
|||||||
|
|
||||||
// 提交评分
|
// 提交评分
|
||||||
const submitScores = async () => {
|
const submitScores = async () => {
|
||||||
if (mapScore.value === 0 || authorScore.value === 0) {
|
if (mapScore.value === 0) {
|
||||||
showErrorMessage('请为地图和作者都进行评分')
|
showErrorMessage('请为地图进行评分')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: 调用评分 API
|
// 调用评分 API
|
||||||
console.log('提交评分:', {
|
await submitMapReward(map.value.name, mapScore.value)
|
||||||
mapScore: mapScore.value,
|
|
||||||
authorScore: authorScore.value,
|
|
||||||
mapComment: mapComment.value,
|
|
||||||
authorComment: authorComment.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 关闭弹窗并重置表单
|
// 关闭弹窗并重置表单
|
||||||
showScoreDialog.value = false
|
showScoreDialog.value = false
|
||||||
mapScore.value = 0
|
mapScore.value = 0
|
||||||
authorScore.value = 0
|
|
||||||
mapComment.value = ''
|
// 重新获取用户地图信息和地图评分
|
||||||
authorComment.value = ''
|
await Promise.all([
|
||||||
|
fetchUserMapInfo(),
|
||||||
|
fetchMapRating()
|
||||||
|
])
|
||||||
|
|
||||||
showErrorMessage('评分成功!')
|
showErrorMessage('评分成功!')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -380,6 +415,16 @@ onMounted(() => {
|
|||||||
background: #6c8bb9;
|
background: #6c8bb9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.score-btn.disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-btn.disabled:hover {
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user