guzhi/web1/src/views/login/index.vue
2025-11-24 18:01:39 +08:00

668 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page flex-col">
<div class="section_1 flex-col" :style="{ backgroundImage: `url(${loginBgFull})` }">
<div class="box_1 flex-row">
<div class="section_2 flex-col">
<div class="group_1 flex-col" :style="{ backgroundImage: `url(${loginBoxLeft})` }"></div>
</div>
<div class="section_3 flex-col">
<img class="image_1" referrerpolicy="no-referrer" :src="loginTitleImg" />
<n-form ref="formRef" :model="loginInfo" :rules="rules" :show-label="false">
<n-form-item path="phone">
<div class="block_1 flex-row">
<div class="image-text_1 flex-row">
<img class="thumbnail_1" referrerpolicy="no-referrer" :src="iconUserImg" />
<n-input
v-model:value="loginInfo.phone"
class="input-reset text-group_1 custom-n-input"
placeholder="请输入账号"
type="text"
:bordered="false"
@keypress.enter="handleLogin"
/>
</div>
</div>
</n-form-item>
<div class="block_2 flex-row justify-between">
<n-form-item path="verifyCode" class="verify-code-item">
<div class="section_4 flex-row">
<div class="image-text_2 flex-row">
<img class="thumbnail_2" referrerpolicy="no-referrer" :src="iconCodeImg" />
<n-input
v-model:value="loginInfo.verifyCode"
class="input-reset text-group_2 custom-n-input"
placeholder="请输入验证码"
type="text"
:bordered="false"
@keypress.enter="handleLogin"
/>
</div>
</div>
</n-form-item>
<div
class="text-wrapper_1 flex-col cursor-pointer"
@click="handleSendCode"
:class="{ disabled: countdown > 0 }"
>
<span class="text_1">{{
countdown > 0 ? `${countdown}秒后重新获取` : '获取验证码'
}}</span>
</div>
</div>
</n-form>
<div class="block_3 flex-row">
<n-checkbox v-model:checked="isAgreed" class="agreement-checkbox">
<span class="text-group_3">阅读并同意</span>
<span class="text_2" @click.stop="handleOpenAgreement">用户协议</span>
</n-checkbox>
</div>
<!-- User Agreement Modal -->
<n-modal
v-model:show="showAgreement"
preset="card"
title="用户服务协议"
style="width: 800px; max-width: 90vw"
:bordered="false"
>
<div
class="agreement-content"
style="max-height: 60vh; overflow-y: auto; padding: 0 10px"
>
<h3>特别提示</h3>
<p>
在此特别提醒您用户在注册成为用户之前请认真阅读本用户协议以下简称协议确保您充分理解本协议中各条款请您审慎阅读并选择接受或不接受本协议除非您接受本协议所有条款否则您无权注册登录或使用本协议所涉服务您的注册登录使用等行为将视为对本协议的接受并同意接受本协议各项条款的约束
</p>
<p>
本协议约定成都文化产权交易所有限公司以下简称成都文交所与用户之间关于IP文产通平台服务以下简称服务的权利义务用户是指注册登录使用本服务的个人本协议可由成都文交所随时更新更新后的协议条款一旦公布即代替原来的协议条款恕不另行通知用户可在本网站查阅最新版协议条款在成都文交所修改协议条款后如果用户不接受修改后的条款请立即停止使用成都文交所提供的服务用户继续使用成都文交所提供的服务将被视为接受修改后的协议
</p>
<h3>服务内容</h3>
<p>
1.1
本服务的具体内容由成都文交所根据实际情况提供包括但不限于授权用户通过其账号进行相关操作
</p>
<p>
1.2
成都文交所提供的服务仅限于平台上的相关功能用户在使用服务时应遵守相关法律法规及本协议的规定
</p>
<h3>用户账号</h3>
<p>
2.1
用户在使用本服务前需要注册一个账号账号注册过程中用户应提供真实准确完整的个人资料
</p>
<p>
2.2
用户有责任妥善保管注册账号信息及账号密码的安全因用户保管不善可能导致遭受盗号或密码失窃责任由用户自行承担
</p>
<h3>用户个人信息保护</h3>
<p>
3.1
保护用户个人信息是成都文交所的一项基本原则成都文交所将会采取合理的措施保护用户的个人信息除法律法规规定的情形外未经用户许可成都文交所不会向第三方公开透露用户个人信息
</p>
<br />
<!-- <p style="color: #999; text-align: center; font-size: 12px;">(以上内容为示例请替换为完整版用户服务协议暂行)</p> -->
</div>
</n-modal>
<div
class="text-wrapper_2 flex-col cursor-pointer"
@click="handleLogin"
:style="{ backgroundImage: `url(${loginBtnBg})` }"
>
<span class="text_3">立即登录</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onBeforeUnmount } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { setToken } from '@/utils'
import api from '@/api'
// Images
import loginTitleImg from '@/assets/img/login_title.png'
import iconUserImg from '@/assets/img/icon_user.png'
import iconCodeImg from '@/assets/img/icon_code.png'
import iconCheckboxImg from '@/assets/img/icon_checkbox.png'
import loginBgFull from '@/assets/img/login_bg_full.png'
import loginBoxLeft from '@/assets/img/login_box_left.png'
import loginBtnBg from '@/assets/img/login_btn_bg.png'
const router = useRouter()
const { query } = useRoute()
const loginInfo = ref({
phone: '',
verifyCode: '',
requestId: '',
})
const isAgreed = ref(false)
const showAgreement = ref(false)
const countdown = ref(0)
let countdownTimer = null
const loading = ref(false)
// Open Agreement Modal
function handleOpenAgreement() {
showAgreement.value = true
}
// Initialize login info
if (localStorage.getItem('phone')) {
loginInfo.value.phone = localStorage.getItem('phone')
}
const formRef = ref(null)
const rules = {
phone: [
{ required: true, message: '请输入手机号', trigger: ['input', 'blur'] },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: ['input', 'blur'] },
],
verifyCode: [{ required: true, message: '请输入验证码', trigger: ['input', 'blur'] }],
}
// Validate phone
function validatePhone(phone) {
const phoneReg = /^1[3-9]\d{9}$/
return phoneReg.test(phone)
}
// Send verification code
async function handleSendCode() {
if (countdown.value > 0) return
const { phone } = loginInfo.value
if (!phone) {
$message.warning('请输入手机号')
return
}
if (!validatePhone(phone)) {
$message.warning('请输入正确的手机号')
return
}
try {
const res = await api.sendVerifyCode({ phone })
loginInfo.value.requestId = res.data.request_id
$message.success('验证码已发送')
// Start countdown
countdown.value = 60
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(countdownTimer)
countdownTimer = null
}
}, 1000)
} catch (error) {
console.error(error)
}
}
// Login
async function handleLogin() {
const { phone, verifyCode, requestId } = loginInfo.value
if (!isAgreed.value) {
$message.warning('请阅读并同意用户协议')
return
}
formRef.value?.validate(async (errors) => {
if (!errors) {
loading.value = true
try {
const res = await api.loginWithVerifyCode({
phone_number: phone,
verification_code: verifyCode,
device_id: requestId,
})
if (res.data?.token?.access_token) {
setToken(res.data.token.access_token)
localStorage.setItem('phone', phone)
localStorage.setItem('app_user_id', res.data.user.id)
if (query.redirect) {
const path = query.redirect
Reflect.deleteProperty(query, 'redirect')
router.push({ path, query })
} else {
router.push('/home')
}
} else {
$message.error('登录失败未获取到token')
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
})
}
onBeforeUnmount(() => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
})
</script>
<style scoped>
/* Utility Classes */
.flex-col {
display: flex;
flex-direction: column;
}
.flex-row {
display: flex;
flex-direction: row;
}
.justify-between {
justify-content: space-between;
}
.cursor-pointer {
cursor: pointer;
}
.input-reset {
border: none;
outline: none;
background: transparent;
padding: 0;
margin: 0;
width: 100%;
}
.disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Page Styles */
.page {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
background-color: #f0f2f5; /* Fallback */
}
.section_1 {
background-size: cover;
background-position: center;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
}
.box_1 {
box-shadow: 0px 2px 30px 0px rgba(225, 206, 206, 0.22);
background-color: rgba(255, 255, 255, 1);
width: 900px;
height: 500px;
border-radius: 8px;
overflow: hidden;
}
.section_2 {
background-color: rgba(255, 255, 255, 1);
border-radius: 0px 100px 0px 0px;
height: 500px;
width: 400px;
position: relative;
overflow: hidden;
}
.group_1 {
/* Matches reference: width 841px, margin-left -270px inside a 400px container */
width: 841px;
height: 500px;
margin-left: -270px;
background-repeat: no-repeat;
/* Ensure the image covers this specific area if needed, or default */
}
.section_3 {
width: 331px;
height: 298px;
margin: 101px 85px 0 84px;
}
.image_1 {
width: 260px;
height: 42px;
margin-left: 1px;
/* Removed object-fit: contain to match reference behavior */
}
.block_1 {
background-color: rgba(255, 255, 255, 1);
border-radius: 4px;
width: 330px;
height: 42px;
border: 1px solid rgba(229, 229, 229, 1);
margin: 50px 0 0 1px;
align-items: center;
}
.image-text_1 {
width: 100%;
height: 16px;
margin: 0 15px;
align-items: center;
}
.thumbnail_1 {
width: 16px;
height: 16px;
margin-right: 10px;
}
.text-group_1 {
color: rgba(48, 49, 51, 1);
font-size: 14px;
font-family: Alibaba-PuHuiTi-R, sans-serif;
font-weight: normal;
line-height: 14px;
}
.text-group_1::placeholder {
color: rgba(189, 189, 189, 1);
}
.block_2 {
width: 330px;
height: 42px;
margin: 20px 0 0 1px;
align-items: center;
}
.section_4 {
background-color: rgba(255, 255, 255, 1);
border-radius: 4px;
width: 191px;
height: 42px;
border: 1px solid rgba(229, 229, 229, 1);
align-items: center;
}
.image-text_2 {
width: 100%;
height: 16px;
margin: 0 15px;
align-items: center;
}
.thumbnail_2 {
width: 16px;
height: 16px;
margin-right: 10px;
}
.text-group_2 {
color: rgba(48, 49, 51, 1);
font-size: 14px;
font-family: Alibaba-PuHuiTi-R, sans-serif;
font-weight: normal;
line-height: 14px;
}
.text-group_2::placeholder {
color: rgba(189, 189, 189, 1);
}
.text-wrapper_1 {
background-color: rgba(255, 255, 255, 1);
border-radius: 4px;
height: 42px;
border: 1px solid rgba(229, 229, 229, 1);
width: 131px;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
}
.text-wrapper_1:hover:not(.disabled) {
border-color: rgba(163, 1, 19, 1);
background-color: rgba(163, 1, 19, 0.05);
}
.text-wrapper_1:active:not(.disabled) {
transform: scale(0.98);
}
.text_1 {
color: rgba(48, 49, 51, 1);
font-size: 14px;
font-family: Alibaba-PuHuiTi-R, sans-serif;
font-weight: normal;
text-align: center;
white-space: nowrap;
transition: color 0.3s ease;
}
.text-wrapper_1:hover:not(.disabled) .text_1 {
color: rgba(163, 1, 19, 1);
}
.block_3 {
width: 100%;
margin-top: 36px;
align-items: center;
}
.text-group_3 {
color: rgba(136, 136, 136, 1);
font-size: 12px;
letter-spacing: 0.32px;
font-family: Alibaba-PuHuiTi-R, sans-serif;
font-weight: normal;
white-space: nowrap;
line-height: 12px;
margin-right: 4px;
}
.text_2 {
color: rgba(163, 1, 19, 1);
font-size: 12px;
letter-spacing: 0.32px;
font-family: Alibaba-PuHuiTi-M, sans-serif;
font-weight: normal;
white-space: nowrap;
line-height: 12px;
transition: all 0.3s ease;
cursor: pointer;
}
.text_2:hover {
text-decoration: underline;
color: rgba(180, 1, 21, 1);
}
.text-wrapper_2 {
height: 42px;
background-size: 100% 100%;
background-repeat: no-repeat;
width: 330px;
margin: 10px 0 0 1px;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
position: relative;
}
.text-wrapper_2:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(163, 1, 19, 0.3);
}
.text-wrapper_2:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(163, 1, 19, 0.2);
}
.text_3 {
color: rgba(255, 255, 255, 1);
font-size: 18px;
font-family: Alibaba-PuHuiTi-M, sans-serif;
font-weight: normal;
text-align: center;
white-space: nowrap;
transition: all 0.3s ease;
}
.text-wrapper_2:hover .text_3 {
letter-spacing: 1px;
}
/* Custom Naive UI Overrides */
.custom-n-input {
background-color: transparent !important;
font-size: 14px;
line-height: 14px;
}
.custom-n-input :deep(.n-input-wrapper) {
padding: 0 !important;
}
.custom-n-input :deep(.n-input__input-el) {
height: 16px !important;
line-height: 16px !important;
color: rgba(48, 49, 51, 1);
font-family: Alibaba-PuHuiTi-R, sans-serif;
}
.custom-n-input :deep(.n-input__placeholder) {
color: rgba(189, 189, 189, 1) !important;
}
.custom-n-input :deep(.n-input__state-border),
.custom-n-input :deep(.n-input__border) {
display: none !important;
}
/* Fix Form Item Layout to prevent shifts */
:deep(.n-form-item-feedback-wrapper) {
position: absolute;
top: 100%;
left: 0;
padding-top: 2px;
line-height: 1;
}
:deep(.n-form-item) {
display: block;
margin-bottom: 0 !important;
}
/* Ensure the code input item doesn't break flex layout */
.verify-code-item {
flex: 1;
/* The section_4 inside has fixed width, so we just let it be */
max-width: 191px;
}
/* Input Container Interactions */
.block_1,
.section_4 {
transition: all 0.3s ease;
}
.block_1:hover,
.section_4:hover {
border-color: rgba(163, 1, 19, 0.5);
}
.block_1:focus-within,
.section_4:focus-within {
border-color: rgba(163, 1, 19, 1);
box-shadow: 0 0 0 2px rgba(163, 1, 19, 0.1);
}
/* Error State Styling */
:deep(.n-form-item.n-form-item--error) .block_1,
:deep(.n-form-item.n-form-item--error) .section_4 {
border-color: #d03050;
}
:deep(.n-form-item.n-form-item--error) .block_1:focus-within,
:deep(.n-form-item.n-form-item--error) .section_4:focus-within {
border-color: #d03050;
box-shadow: 0 0 0 2px rgba(208, 48, 80, 0.1);
}
/* Checkbox Customization */
:deep(.agreement-checkbox .n-checkbox-box) {
border-radius: 2px;
transition: all 0.3s ease;
}
/* Checked State - Solid Red */
:deep(.agreement-checkbox.n-checkbox--checked .n-checkbox-box) {
background-color: rgba(163, 1, 19, 1) !important;
border: 1px solid rgba(163, 1, 19, 1) !important;
}
/* Hide the separate border element when checked to prevent color overlap/double borders */
:deep(.agreement-checkbox.n-checkbox--checked .n-checkbox-box .n-checkbox-box__border) {
display: none !important;
}
/* Hover State - Red Border */
:deep(.agreement-checkbox:hover .n-checkbox-box .n-checkbox-box__border) {
border-color: rgba(163, 1, 19, 1) !important;
}
/* Focus State - Red Border & Shadow */
:deep(.agreement-checkbox.n-checkbox--focus .n-checkbox-box .n-checkbox-box__border) {
border-color: rgba(163, 1, 19, 1) !important;
}
:deep(.agreement-checkbox.n-checkbox--focus .n-checkbox-box) {
box-shadow: 0 0 0 2px rgba(163, 1, 19, 0.2) !important;
}
/* Ensure checkmark is white */
:deep(.agreement-checkbox .n-checkbox-icon) {
color: #fff !important;
}
/* Agreement Modal Styles */
.agreement-content {
font-family: Alibaba-PuHuiTi-R, sans-serif;
line-height: 1.6;
color: #333;
}
.agreement-content h3 {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.agreement-content p {
margin-bottom: 12px;
text-align: justify;
font-size: 14px;
}
</style>