+
-
+
-
-
注 册
+
+
+ 我已阅读并同意
+ 《用户协议》
+ 和
+ 《隐私政策》
+
-
+
+
+ {{ loading ? '注册中...' : '注 册' }}
+
+
+
+
已有账号?
- 登录
+ 登录
@@ -240,9 +317,17 @@
-
+
+
+
+
+
+
+
+
+
@@ -250,21 +335,31 @@
import Captcha from '@/components/Captcha.vue'
import ResetPass from '@/components/ResetPass.vue'
import SendMsg from '@/components/SendMsg.vue'
+import CustomTabPane from '@/components/ui/CustomTabPane.vue'
+import CustomTabs from '@/components/ui/CustomTabs.vue'
import { getSystemInfo } from '@/store/cache'
import { setUserToken } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
-import { setRoute } from '@/store/system'
import { httpGet, httpPost } from '@/utils/http'
import { arrayContains } from '@/utils/libs'
import { validateEmail, validateMobile } from '@/utils/validate'
import { Checked, Iphone, Lock, Message } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
-import { onMounted, ref, watch } from 'vue'
-import { useRouter } from 'vue-router'
+import { marked } from 'marked'
+import QRCode from 'qrcode'
+import { onMounted, onUnmounted, ref, watch } from 'vue'
// eslint-disable-next-line no-undef
const props = defineProps({
show: Boolean,
+ active: {
+ type: String,
+ default: 'login',
+ },
+ inviteCode: {
+ type: String,
+ default: '',
+ },
})
const showDialog = ref(false)
watch(
@@ -274,7 +369,8 @@ watch(
}
)
-const login = ref(true)
+const login = ref(props.active === 'login')
+const loginActiveName = ref('account') // 新增:登录标签页激活状态
const data = ref({
username: import.meta.env.VITE_USER,
password: import.meta.env.VITE_PASS,
@@ -282,34 +378,47 @@ const data = ref({
email: '',
repass: '',
code: '',
- invite_code: '',
+ invite_code: props.inviteCode,
})
+
+// 微信登录相关变量
+const wechatLoginQRCode = ref('')
+const wechatLoginState = ref('')
+const qrcodeLoading = ref(false)
+const pollingTimer = ref(null)
+const qrcodeExpired = ref(false)
+const qrcodeTimer = ref(null)
+
const enableMobile = ref(false)
const enableEmail = ref(false)
const enableUser = ref(false)
const enableRegister = ref(true)
-const wechatLoginURL = ref('')
+
const activeName = ref('')
const wxImg = ref('/images/wx.png')
const captchaRef = ref(null)
// eslint-disable-next-line no-undef
-const emits = defineEmits(['hide', 'success'])
+const emits = defineEmits(['hide', 'success', 'changeActive'])
const action = ref('login')
-const enableVerify = ref(false)
+const enableCaptcha = ref(false)
+const captchaType = ref('')
const showResetPass = ref(false)
-const router = useRouter()
const store = useSharedStore()
+const loading = ref(false)
+const agreeChecked = ref(false)
+const showAgreement = ref(false)
+const showPrivacy = ref(false)
+const agreementHtml = ref('')
+const privacyHtml = ref('')
+
+watch(
+ () => login.value,
+ (newValue) => {
+ emits('changeActive', newValue)
+ }
+)
onMounted(() => {
- const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
- httpGet('/api/user/clogin?return_url=' + returnURL)
- .then((res) => {
- wechatLoginURL.value = res.data.url
- })
- .catch((e) => {
- console.log(e.message)
- })
-
getSystemInfo()
.then((res) => {
if (res.data) {
@@ -332,14 +441,48 @@ onMounted(() => {
if (res.data['wechat_card_url'] !== '') {
wxImg.value = res.data['wechat_card_url']
}
- enableVerify.value = res.data['enabled_verify']
}
})
.catch((e) => {
ElMessage.error('获取系统配置失败:' + e.message)
})
+
+ httpGet('/api/captcha/config').then((res) => {
+ enableCaptcha.value = res.data['enabled']
+ captchaType.value = res.data['type']
+ })
})
+// 监听登录标签页切换
+watch(loginActiveName, (newValue) => {
+ if (newValue === 'wechat') {
+ getWxLoginURL()
+ } else {
+ // 其他登录方式,清除定时器
+ if (pollingTimer.value) {
+ clearInterval(pollingTimer.value)
+ }
+ if (qrcodeTimer.value) {
+ clearTimeout(qrcodeTimer.value)
+ }
+ }
+})
+
+const handleTabClick = (tab) => {
+ // CustomTabs组件传递的是tab对象,包含paneName属性
+ if (tab.paneName === 'wechat') {
+ getWxLoginURL()
+ } else {
+ // 其他登录方式,清除定时器
+ if (pollingTimer.value) {
+ clearInterval(pollingTimer.value)
+ }
+ if (qrcodeTimer.value) {
+ clearTimeout(qrcodeTimer.value)
+ }
+ }
+}
+
const submit = (verifyData) => {
if (action.value === 'login') {
doLogin(verifyData)
@@ -348,6 +491,107 @@ const submit = (verifyData) => {
}
}
+// 获取微信登录 URL
+const getWxLoginURL = () => {
+ wechatLoginQRCode.value = ''
+ qrcodeLoading.value = true
+ qrcodeExpired.value = false
+
+ // 清除可能存在的旧定时器
+ if (qrcodeTimer.value) {
+ clearTimeout(qrcodeTimer.value)
+ }
+
+ httpGet('/api/user/login/qrcode')
+ .then((res) => {
+ // 生成二维码
+ QRCode.toDataURL(res.data.url, { width: 200, height: 200, margin: 2 }, (error, url) => {
+ if (error) {
+ console.error(error)
+ } else {
+ wechatLoginQRCode.value = url
+ }
+ })
+ wechatLoginState.value = res.data.state
+ // 开始轮询状态
+ startPolling()
+
+ // 设置1分钟后二维码过期
+ qrcodeTimer.value = setTimeout(() => {
+ qrcodeExpired.value = true
+ // 停止轮询
+ if (pollingTimer.value) {
+ clearInterval(pollingTimer.value)
+ }
+ }, 60 * 1000) // 1分钟过期
+ })
+ .catch((e) => {
+ ElMessage.error('获取微信登录 URL 失败,' + e.message)
+ })
+ .finally(() => {
+ qrcodeLoading.value = false
+ })
+}
+
+// 开始轮询
+const startPolling = () => {
+ // 清除可能存在的旧定时器
+ if (pollingTimer.value) {
+ clearInterval(pollingTimer.value)
+ }
+
+ pollingTimer.value = setInterval(() => {
+ checkLoginStatus()
+ }, 1000) // 每1秒检查一次
+}
+
+// 检查登录状态
+const checkLoginStatus = () => {
+ if (!wechatLoginState.value) return
+
+ httpGet(`/api/user/login/status?state=${wechatLoginState.value}`)
+ .then((res) => {
+ const status = res.data.status
+
+ switch (status) {
+ case 'success':
+ // 登录成功
+ clearInterval(pollingTimer.value)
+ clearTimeout(qrcodeTimer.value)
+ setUserToken(res.data.token)
+ store.setIsLogin(true)
+ ElMessage.success('登录成功!')
+ emits('hide')
+ emits('success')
+ break
+
+ case 'expired':
+ // 二维码过期
+ clearInterval(pollingTimer.value)
+ clearTimeout(qrcodeTimer.value)
+ qrcodeExpired.value = true
+ break
+
+ case 'pending':
+ // 继续轮询
+ break
+
+ default:
+ // 其他错误情况
+ clearInterval(pollingTimer.value)
+ clearTimeout(qrcodeTimer.value)
+ ElMessage.error('登录失败,请重试')
+ break
+ }
+ })
+ .catch((e) => {
+ // 发生错误时显示过期状态
+ clearInterval(pollingTimer.value)
+ clearTimeout(qrcodeTimer.value)
+ qrcodeExpired.value = true
+ })
+}
+
// 登录操作
const submitLogin = () => {
if (!data.value.username) {
@@ -356,7 +600,7 @@ const submitLogin = () => {
if (!data.value.password) {
return ElMessage.error('请输入密码')
}
- if (enableVerify.value) {
+ if (enableCaptcha.value) {
captchaRef.value.loadCaptcha()
action.value = 'login'
} else {
@@ -368,6 +612,7 @@ const doLogin = (verifyData) => {
data.value.key = verifyData.key
data.value.dots = verifyData.dots
data.value.x = verifyData.x
+ loading.value = true
httpPost('/api/user/login', data.value)
.then((res) => {
setUserToken(res.data.token)
@@ -379,6 +624,9 @@ const doLogin = (verifyData) => {
.catch((e) => {
ElMessage.error('登录失败,' + e.message)
})
+ .finally(() => {
+ loading.value = false
+ })
}
// 注册操作
@@ -405,7 +653,10 @@ const submitRegister = () => {
if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
return ElMessage.error('请输入验证码')
}
- if (enableVerify.value && activeName.value === 'username') {
+ if (!agreeChecked.value) {
+ return ElMessage.error('请先阅读并同意《用户协议》和《隐私政策》')
+ }
+ if (enableCaptcha.value) {
captchaRef.value.loadCaptcha()
action.value = 'register'
} else {
@@ -418,6 +669,7 @@ const doRegister = (verifyData) => {
data.value.dots = verifyData.dots
data.value.x = verifyData.x
data.value.reg_way = activeName.value
+ loading.value = true
httpPost('/api/user/register', data.value)
.then((res) => {
setUserToken(res.data.token)
@@ -433,80 +685,196 @@ const doRegister = (verifyData) => {
.catch((e) => {
ElMessage.error('注册失败,' + e.message)
})
+ .finally(() => {
+ loading.value = false
+ })
}
+
+// 打开并加载协议
+const openAgreement = () => {
+ if (!agreementHtml.value) {
+ httpGet('/api/config/get?key=agreement')
+ .then((res) => {
+ agreementHtml.value = marked.parse(res.data?.content || '')
+ showAgreement.value = true
+ })
+ .catch((e) => ElMessage.error('加载用户协议失败:' + e.message))
+ } else {
+ showAgreement.value = true
+ }
+}
+
+// 打开并加载隐私政策
+const openPrivacy = () => {
+ if (!privacyHtml.value) {
+ httpGet('/api/config/get?key=privacy')
+ .then((res) => {
+ privacyHtml.value = marked.parse(res.data?.content || '')
+ showPrivacy.value = true
+ })
+ .catch((e) => ElMessage.error('加载隐私政策失败:' + e.message))
+ } else {
+ showPrivacy.value = true
+ }
+}
+
+// 组件卸载时清除定时器
+onUnmounted(() => {
+ if (pollingTimer.value) {
+ clearInterval(pollingTimer.value)
+ }
+ if (qrcodeTimer.value) {
+ clearTimeout(qrcodeTimer.value)
+ }
+})
-
diff --git a/web/src/components/MusicPlayer.vue b/web/src/components/MusicPlayer.vue
index 3b217056..632d6549 100644
--- a/web/src/components/MusicPlayer.vue
+++ b/web/src/components/MusicPlayer.vue
@@ -1,263 +1,273 @@
-
-
-
-
-
-
-
{{title}}
-
- {{ tags }}
- |
- {{ formatTime(currentTime) }}/ {{ formatTime(duration) }}
-
-
+
+
+
+
+
+
+
{{ title }}
+
+ {{ tags }}
+ |
+ {{ formatTime(currentTime) }}/ {{ formatTime(duration) }}
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
-
diff --git a/web/src/components/PasswordDialog.vue b/web/src/components/PasswordDialog.vue
index 79981b09..21f80853 100644
--- a/web/src/components/PasswordDialog.vue
+++ b/web/src/components/PasswordDialog.vue
@@ -1,23 +1,23 @@
-