diff --git a/docker-compose.yml b/docker-compose.yml index 6cc8dbf..d2dc58d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: build: context: . dockerfile: Dockerfile - image: tradus-web:1.0.16 + image: tradus-web:1.0.17 container_name: tradus-web ports: - '6000:80' diff --git a/src/App.vue b/src/App.vue index c4f8796..5caa5f0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,12 +2,179 @@ import { RouterView } from 'vue-router' import { computed, ref, onMounted, onUnmounted } from 'vue' import { useUserStore } from './stores/user' +import { authApi } from './services/api' const userStore = useUserStore() const isAuthenticated = computed(() => userStore.isAuthenticated) const userInfo = computed(() => userStore.userInfo) const showMobileMenu = ref(false) const showUserMenu = ref(false) +const showLoginModal = ref(false) +const loginMode = ref('login') // 'login' 或 'register' + +// 表单数据 +const formData = ref({ + email: '', + password: '', + confirmPassword: '', + nickname: '', + verificationCode: '', +}) + +// 表单错误信息 +const formErrors = ref({ + email: '', + password: '', + confirmPassword: '', + nickname: '', + verificationCode: '', +}) + +// 验证码相关状态 +const sendingCode = ref(false) +const countdown = ref(0) + +// 加载状态 +const isLoading = ref(false) + +// 发送验证码 +const sendVerificationCode = async () => { + if (!formData.value.email) { + formErrors.value.email = '请输入邮箱' + return + } + + try { + sendingCode.value = true + formErrors.value.email = '' + + await authApi.sendVerificationCode(formData.value.email) + + countdown.value = 60 + + // 倒计时 + const timer = setInterval(() => { + countdown.value-- + if (countdown.value <= 0) { + clearInterval(timer) + } + }, 1000) + } catch (err) { + formErrors.value.email = err instanceof Error ? err.message : '发送验证码失败,请重试' + console.error('发送验证码失败:', err) + } finally { + sendingCode.value = false + } +} + +// 验证表单 +const validateForm = () => { + let isValid = true + formErrors.value = { + email: '', + password: '', + confirmPassword: '', + nickname: '', + verificationCode: '', + } + + // 验证邮箱 + if (!formData.value.email) { + formErrors.value.email = '请输入邮箱' + isValid = false + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.value.email)) { + formErrors.value.email = '请输入有效的邮箱地址' + isValid = false + } + + // 验证密码 + if (!formData.value.password) { + formErrors.value.password = '请输入密码' + isValid = false + } else if (formData.value.password.length < 6) { + formErrors.value.password = '密码长度不能小于6位' + isValid = false + } + + // 注册模式下的额外验证 + if (loginMode.value === 'register') { + // 验证确认密码 + if (!formData.value.confirmPassword) { + formErrors.value.confirmPassword = '请确认密码' + isValid = false + } else if (formData.value.confirmPassword !== formData.value.password) { + formErrors.value.confirmPassword = '两次输入的密码不一致' + isValid = false + } + + // 验证昵称 + if (!formData.value.nickname) { + formErrors.value.nickname = '请输入昵称' + isValid = false + } + + // 验证验证码 + if (!formData.value.verificationCode) { + formErrors.value.verificationCode = '请输入验证码' + isValid = false + } + } + + return isValid +} + +// 处理表单提交 +const handleSubmit = async () => { + if (!validateForm()) return + + isLoading.value = true + try { + if (loginMode.value === 'login') { + await authApi.login({ + mail: formData.value.email, + password: formData.value.password, + }) + } else { + await authApi.register({ + mail: formData.value.email, + nickname: formData.value.nickname, + password: formData.value.password, + verification_code: formData.value.verificationCode, + }) + // 注册成功后自动登录 + await authApi.login({ + mail: formData.value.email, + password: formData.value.password, + }) + } + closeAuthModal() + } catch (error) { + console.error('认证失败:', error) + formErrors.value.email = error instanceof Error ? error.message : '认证失败,请检查输入信息' + } finally { + isLoading.value = false + } +} + +const closeAuthModal = () => { + showLoginModal.value = false + loginMode.value = 'login' + // 重置表单和错误信息 + formData.value = { + email: '', + password: '', + confirmPassword: '', + nickname: '', + verificationCode: '', + } + formErrors.value = { + email: '', + password: '', + confirmPassword: '', + nickname: '', + verificationCode: '', + } +} const handleLogout = () => { userStore.logout() @@ -38,6 +205,11 @@ const closeMenus = (event: MouseEvent) => { } } +const openAuthModal = (mode: 'login' | 'register') => { + loginMode.value = mode + showLoginModal.value = true +} + onMounted(() => { document.addEventListener('click', closeMenus) }) @@ -147,8 +319,8 @@ onUnmounted(() => {
- 登录 - 注册 + +
@@ -191,12 +363,8 @@ onUnmounted(() => {
- 登录 - 注册 + +
@@ -207,9 +375,142 @@ onUnmounted(() => {
- +
+ + + @@ -912,4 +1213,278 @@ body { display: block; } } + +/* 认证按钮样式 */ +.auth-button { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--color-accent); + border-radius: var(--border-radius); + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + margin: 0.5rem 0; + background: none; + color: var(--color-accent); +} + +.auth-button:hover { + background-color: rgba(51, 85, 255, 0.04); +} + +.auth-button.register { + background-color: var(--color-accent); + color: white; +} + +.auth-button.register:hover { + background-color: var(--color-accent-hover); +} + +/* 模态框样式 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; + backdrop-filter: blur(4px); +} + +.modal-container { + background-color: var(--color-bg-primary); + border-radius: var(--border-radius); + width: 90%; + max-width: 400px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); + animation: modalSlideIn 0.3s ease; +} + +.modal-header { + padding: 1.5rem; + border-bottom: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-header h2 { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0; +} + +.close-button { + background: none; + border: none; + color: var(--color-text-secondary); + cursor: pointer; + padding: 0.5rem; + border-radius: 50%; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.close-button:hover { + background-color: var(--color-bg-hover); + color: var(--color-text-primary); +} + +.modal-content { + padding: 1.5rem; +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-group label { + font-size: 0.95rem; + font-weight: 500; + color: var(--color-text-primary); +} + +.form-group input { + padding: 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + font-size: 1rem; + transition: all 0.2s ease; + background-color: var(--color-bg-secondary); + color: var(--color-text-primary); +} + +.form-group input:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 2px rgba(51, 85, 255, 0.1); +} + +.submit-button { + margin-top: 1rem; + padding: 0.875rem; + background-color: var(--color-accent); + color: white; + border: none; + border-radius: var(--border-radius); + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.submit-button:hover { + background-color: var(--color-accent-hover); +} + +.auth-switch { + margin-top: 1.5rem; + text-align: center; + color: var(--color-text-secondary); + font-size: 0.95rem; +} + +.switch-button { + background: none; + border: none; + color: var(--color-accent); + cursor: pointer; + font-weight: 500; + padding: 0; + margin-left: 0.5rem; +} + +.switch-button:hover { + text-decoration: underline; +} + +@keyframes modalSlideIn { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* 移动端适配 */ +@media (max-width: 768px) { + .modal-container { + width: 95%; + } + + .modal-header { + padding: 1.25rem; + } + + .modal-content { + padding: 1.25rem; + } +} + +/* 表单错误样式 */ +.form-group input.error { + border-color: #ff4d4f; +} + +.form-group input.error:focus { + box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.1); +} + +.error-message { + color: #ff4d4f; + font-size: 0.85rem; + margin-top: 0.25rem; +} + +/* 加载动画 */ +.loading-spinner { + display: inline-block; + width: 1.25rem; + height: 1.25rem; + border: 2px solid #ffffff; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* 禁用状态 */ +.submit-button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +/* 输入框聚焦状态 */ +.form-group input:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 2px rgba(51, 85, 255, 0.1); +} + +/* 验证码按钮样式 */ +.input-with-button { + display: flex; + gap: 0.5rem; +} + +.input-with-button input { + flex: 1; +} + +.code-button { + padding: 0 1rem; + border-radius: var(--border-radius); + background-color: var(--color-bg-elevated); + border: 1px solid var(--color-border); + color: var(--color-text-secondary); + font-size: 0.9rem; + white-space: nowrap; + cursor: pointer; + transition: all 0.2s ease; +} + +.code-button:hover:not(:disabled) { + background-color: var(--color-bg-secondary); + color: var(--color-text-primary); +} + +.code-button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +@media (max-width: 480px) { + .input-with-button { + flex-direction: column; + } + + .code-button { + padding: 0.8rem; + } +} diff --git a/src/views/AIAgentView.vue b/src/views/AIAgentView.vue index 96f8267..1ba2880 100644 --- a/src/views/AIAgentView.vue +++ b/src/views/AIAgentView.vue @@ -347,10 +347,6 @@ const getIconPath = (agent: Agent) => {

需要登录

请登录或注册账号以使用AI Agent功能

-
- - 注册 -
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 924b0ae..6a5461a 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -5,6 +5,12 @@ import { useRouter } from 'vue-router' const userStore = useUserStore() const isAuthenticated = computed(() => userStore.isAuthenticated) + +const emit = defineEmits(['openAuth']) + +const openAuthModal = (mode: 'login' | 'register') => { + emit('openAuth', mode) +}