新增:用户名密码登录
This commit is contained in:
parent
c87099de2a
commit
93090738e1
75
src/api/auth.ts
Normal file
75
src/api/auth.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { post } from './request'
|
||||
import type { UserInfo } from '@/stores/user'
|
||||
|
||||
export interface CaptchaData {
|
||||
captchaId: string
|
||||
picPath: string
|
||||
captchaLength: number
|
||||
openCaptcha: boolean
|
||||
}
|
||||
|
||||
export interface CaptchaResponse {
|
||||
code: number
|
||||
data?: CaptchaData
|
||||
msg?: string
|
||||
}
|
||||
|
||||
export interface LoginResponseData {
|
||||
token: string
|
||||
expiresAt?: number
|
||||
user?: UserInfo
|
||||
}
|
||||
|
||||
export interface AuthApiResponse {
|
||||
code: number
|
||||
data?: LoginResponseData
|
||||
msg?: string
|
||||
}
|
||||
|
||||
export interface LoginByPasswordParams {
|
||||
username: string
|
||||
password: string
|
||||
captcha: string
|
||||
captchaId: string
|
||||
}
|
||||
|
||||
export interface RegisterByPasswordParams {
|
||||
userName: string
|
||||
passWord: string
|
||||
nickName?: string
|
||||
captcha: string
|
||||
captchaId: string
|
||||
promotionCode?: string
|
||||
}
|
||||
|
||||
/** POST /base/captcha */
|
||||
export async function getCaptcha(): Promise<CaptchaResponse> {
|
||||
return post<CaptchaResponse>('/base/captcha')
|
||||
}
|
||||
|
||||
/** POST /base/login */
|
||||
export async function loginByPassword(params: LoginByPasswordParams): Promise<AuthApiResponse> {
|
||||
return post<AuthApiResponse>('/base/login', params)
|
||||
}
|
||||
|
||||
/** POST /base/register */
|
||||
export async function registerByPassword(
|
||||
params: RegisterByPasswordParams,
|
||||
): Promise<AuthApiResponse> {
|
||||
return post<AuthApiResponse>('/base/register', params)
|
||||
}
|
||||
|
||||
/** 从登录/注册响应中解析 token 与 user,供各登录方式复用 */
|
||||
export function parseLoginResponse(data: LoginResponseData | Record<string, unknown>): {
|
||||
token: string | undefined
|
||||
user: UserInfo | undefined
|
||||
} {
|
||||
const record = data as Record<string, unknown>
|
||||
const token = record.token as string | undefined
|
||||
const user =
|
||||
(record.user as UserInfo | undefined) ??
|
||||
(record.ID != null || record.id != null || record.userName != null
|
||||
? (record as unknown as UserInfo)
|
||||
: undefined)
|
||||
return { token, user }
|
||||
}
|
||||
@ -142,14 +142,43 @@
|
||||
},
|
||||
"login": {
|
||||
"title": "Sign In",
|
||||
"desc": "Connect your wallet to sign in",
|
||||
"descAccount": "Sign in with your username and password",
|
||||
"descWallet": "Connect your wallet to sign in",
|
||||
"tabAccount": "Account",
|
||||
"tabWallet": "Wallet",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"captcha": "Captcha",
|
||||
"refreshCaptcha": "Refresh captcha",
|
||||
"loginBtn": "Sign In",
|
||||
"connecting": "Connecting...",
|
||||
"connectBtn": "Connect with Wallet",
|
||||
"signUp": "Don't have an account? Sign Up",
|
||||
"walletNotInstalled": "Ethereum wallet is not installed. Please install MetaMask, TokenPocket or another Ethereum wallet to continue.",
|
||||
"invalidAddress": "Invalid wallet address format. Please check your wallet connection.",
|
||||
"connectFailed": "Failed to connect with wallet. Please check your wallet and try again.",
|
||||
"siweStatement": "Sign in to Alpha Market"
|
||||
"loginFailed": "Sign in failed. Please check your credentials and try again.",
|
||||
"credentialsRequired": "Please enter username and password.",
|
||||
"captchaRequired": "Please enter the captcha.",
|
||||
"captchaLoadFailed": "Failed to load captcha. Please refresh and try again.",
|
||||
"siweStatement": "Sign in to Alpha Market"
|
||||
},
|
||||
"register": {
|
||||
"title": "Sign Up",
|
||||
"desc": "Create an account to start trading",
|
||||
"username": "Username",
|
||||
"nickName": "Nickname (optional)",
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"captcha": "Captcha",
|
||||
"refreshCaptcha": "Refresh captcha",
|
||||
"submitBtn": "Create Account",
|
||||
"signIn": "Already have an account? Sign In",
|
||||
"passwordMismatch": "Passwords do not match.",
|
||||
"credentialsRequired": "Please enter username and password.",
|
||||
"captchaRequired": "Please enter the captcha.",
|
||||
"captchaLoadFailed": "Failed to load captcha. Please refresh and try again.",
|
||||
"failed": "Registration failed. Please try again."
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key Management",
|
||||
|
||||
@ -142,14 +142,43 @@
|
||||
},
|
||||
"login": {
|
||||
"title": "サインイン",
|
||||
"desc": "ウォレットを接続してサインイン",
|
||||
"descAccount": "ユーザー名とパスワードでサインイン",
|
||||
"descWallet": "ウォレットを接続してサインイン",
|
||||
"tabAccount": "アカウント",
|
||||
"tabWallet": "ウォレット",
|
||||
"username": "ユーザー名",
|
||||
"password": "パスワード",
|
||||
"captcha": "認証コード",
|
||||
"refreshCaptcha": "認証コードを更新",
|
||||
"loginBtn": "サインイン",
|
||||
"connecting": "接続中...",
|
||||
"connectBtn": "ウォレットで接続",
|
||||
"signUp": "アカウントをお持ちでないですか?新規登録",
|
||||
"walletNotInstalled": "イーサリアムウォレットがインストールされていません。MetaMask、TokenPocket などのウォレットをインストールしてください。",
|
||||
"invalidAddress": "ウォレットアドレスの形式が無効です。接続を確認してください。",
|
||||
"connectFailed": "ウォレットの接続に失敗しました。ウォレットを確認して再試行してください。",
|
||||
"siweStatement": "Alpha Market にサインイン"
|
||||
"loginFailed": "サインインに失敗しました。認証情報を確認してください。",
|
||||
"credentialsRequired": "ユーザー名とパスワードを入力してください。",
|
||||
"captchaRequired": "認証コードを入力してください。",
|
||||
"captchaLoadFailed": "認証コードの読み込みに失敗しました。更新して再試行してください。",
|
||||
"siweStatement": "Alpha Market にサインイン"
|
||||
},
|
||||
"register": {
|
||||
"title": "新規登録",
|
||||
"desc": "アカウントを作成して取引を開始",
|
||||
"username": "ユーザー名",
|
||||
"nickName": "ニックネーム(任意)",
|
||||
"password": "パスワード",
|
||||
"confirmPassword": "パスワード確認",
|
||||
"captcha": "認証コード",
|
||||
"refreshCaptcha": "認証コードを更新",
|
||||
"submitBtn": "アカウント作成",
|
||||
"signIn": "すでにアカウントをお持ちですか?サインイン",
|
||||
"passwordMismatch": "パスワードが一致しません。",
|
||||
"credentialsRequired": "ユーザー名とパスワードを入力してください。",
|
||||
"captchaRequired": "認証コードを入力してください。",
|
||||
"captchaLoadFailed": "認証コードの読み込みに失敗しました。更新して再試行してください。",
|
||||
"failed": "登録に失敗しました。再試行してください。"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 管理",
|
||||
|
||||
@ -142,15 +142,44 @@
|
||||
},
|
||||
"login": {
|
||||
"title": "로그인",
|
||||
"desc": "지갑을 연결하여 로그인",
|
||||
"descAccount": "사용자 이름과 비밀번호로 로그인",
|
||||
"descWallet": "지갑을 연결하여 로그인",
|
||||
"tabAccount": "계정",
|
||||
"tabWallet": "지갑",
|
||||
"username": "사용자 이름",
|
||||
"password": "비밀번호",
|
||||
"captcha": "인증 코드",
|
||||
"refreshCaptcha": "인증 코드 새로고침",
|
||||
"loginBtn": "로그인",
|
||||
"connecting": "연결 중...",
|
||||
"connectBtn": "지갑 연결",
|
||||
"signUp": "계정이 없으신가요? 회원가입",
|
||||
"walletNotInstalled": "이더리움 지갑이 설치되어 있지 않습니다. MetaMask, TokenPocket 등 지갑을 설치해 주세요.",
|
||||
"invalidAddress": "지갑 주소 형식이 올바르지 않습니다. 지갑 연결을 확인해 주세요.",
|
||||
"connectFailed": "지갑 연결에 실패했습니다. 지갑을 확인하고 다시 시도해 주세요.",
|
||||
"loginFailed": "로그인에 실패했습니다. 계정 정보를 확인해 주세요.",
|
||||
"credentialsRequired": "사용자 이름과 비밀번호를 입력해 주세요.",
|
||||
"captchaRequired": "인증 코드를 입력해 주세요.",
|
||||
"captchaLoadFailed": "인증 코드를 불러오지 못했습니다. 새로고침 후 다시 시도해 주세요.",
|
||||
"siweStatement": "Alpha Market 로그인"
|
||||
},
|
||||
"register": {
|
||||
"title": "회원가입",
|
||||
"desc": "계정을 만들고 거래를 시작하세요",
|
||||
"username": "사용자 이름",
|
||||
"nickName": "닉네임 (선택)",
|
||||
"password": "비밀번호",
|
||||
"confirmPassword": "비밀번호 확인",
|
||||
"captcha": "인증 코드",
|
||||
"refreshCaptcha": "인증 코드 새로고침",
|
||||
"submitBtn": "계정 만들기",
|
||||
"signIn": "이미 계정이 있으신가요? 로그인",
|
||||
"passwordMismatch": "비밀번호가 일치하지 않습니다.",
|
||||
"credentialsRequired": "사용자 이름과 비밀번호를 입력해 주세요.",
|
||||
"captchaRequired": "인증 코드를 입력해 주세요.",
|
||||
"captchaLoadFailed": "인증 코드를 불러오지 못했습니다. 새로고침 후 다시 시도해 주세요.",
|
||||
"failed": "회원가입에 실패했습니다. 다시 시도해 주세요."
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 관리",
|
||||
"createBtn": "Key 생성",
|
||||
|
||||
@ -142,15 +142,44 @@
|
||||
},
|
||||
"login": {
|
||||
"title": "登录",
|
||||
"desc": "连接钱包以登录",
|
||||
"descAccount": "使用用户名和密码登录",
|
||||
"descWallet": "连接钱包以登录",
|
||||
"tabAccount": "账号密码",
|
||||
"tabWallet": "钱包登录",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"captcha": "验证码",
|
||||
"refreshCaptcha": "刷新验证码",
|
||||
"loginBtn": "登录",
|
||||
"connecting": "连接中...",
|
||||
"connectBtn": "连接钱包",
|
||||
"signUp": "没有账号?立即注册",
|
||||
"walletNotInstalled": "未检测到以太坊钱包。请安装 MetaMask、TokenPocket 或其他以太坊钱包后重试。",
|
||||
"invalidAddress": "钱包地址格式无效,请检查钱包连接。",
|
||||
"connectFailed": "连接钱包失败,请检查钱包后重试。",
|
||||
"loginFailed": "登录失败,请检查账号密码后重试。",
|
||||
"credentialsRequired": "请输入用户名和密码。",
|
||||
"captchaRequired": "请输入验证码。",
|
||||
"captchaLoadFailed": "验证码加载失败,请刷新后重试。",
|
||||
"siweStatement": "登录 Alpha Market"
|
||||
},
|
||||
"register": {
|
||||
"title": "注册",
|
||||
"desc": "创建账号开始交易",
|
||||
"username": "用户名",
|
||||
"nickName": "昵称(可选)",
|
||||
"password": "密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"captcha": "验证码",
|
||||
"refreshCaptcha": "刷新验证码",
|
||||
"submitBtn": "注册",
|
||||
"signIn": "已有账号?立即登录",
|
||||
"passwordMismatch": "两次输入的密码不一致。",
|
||||
"credentialsRequired": "请输入用户名和密码。",
|
||||
"captchaRequired": "请输入验证码。",
|
||||
"captchaLoadFailed": "验证码加载失败,请刷新后重试。",
|
||||
"failed": "注册失败,请稍后重试。"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 管理",
|
||||
"createBtn": "创建 Key",
|
||||
|
||||
@ -142,15 +142,44 @@
|
||||
},
|
||||
"login": {
|
||||
"title": "登入",
|
||||
"desc": "連接錢包以登入",
|
||||
"descAccount": "使用使用者名稱和密碼登入",
|
||||
"descWallet": "連接錢包以登入",
|
||||
"tabAccount": "帳號密碼",
|
||||
"tabWallet": "錢包登入",
|
||||
"username": "使用者名稱",
|
||||
"password": "密碼",
|
||||
"captcha": "驗證碼",
|
||||
"refreshCaptcha": "刷新驗證碼",
|
||||
"loginBtn": "登入",
|
||||
"connecting": "連接中...",
|
||||
"connectBtn": "連接錢包",
|
||||
"signUp": "沒有帳號?立即註冊",
|
||||
"walletNotInstalled": "未偵測到以太坊錢包。請安裝 MetaMask、TokenPocket 或其他以太坊錢包後重試。",
|
||||
"invalidAddress": "錢包地址格式無效,請檢查錢包連接。",
|
||||
"connectFailed": "連接錢包失敗,請檢查錢包後重試。",
|
||||
"loginFailed": "登入失敗,請檢查帳號密碼後重試。",
|
||||
"credentialsRequired": "請輸入使用者名稱和密碼。",
|
||||
"captchaRequired": "請輸入驗證碼。",
|
||||
"captchaLoadFailed": "驗證碼載入失敗,請刷新後重試。",
|
||||
"siweStatement": "登入 Alpha Market"
|
||||
},
|
||||
"register": {
|
||||
"title": "註冊",
|
||||
"desc": "建立帳號開始交易",
|
||||
"username": "使用者名稱",
|
||||
"nickName": "暱稱(可選)",
|
||||
"password": "密碼",
|
||||
"confirmPassword": "確認密碼",
|
||||
"captcha": "驗證碼",
|
||||
"refreshCaptcha": "刷新驗證碼",
|
||||
"submitBtn": "註冊",
|
||||
"signIn": "已有帳號?立即登入",
|
||||
"passwordMismatch": "兩次輸入的密碼不一致。",
|
||||
"credentialsRequired": "請輸入使用者名稱和密碼。",
|
||||
"captchaRequired": "請輸入驗證碼。",
|
||||
"captchaLoadFailed": "驗證碼載入失敗,請刷新後重試。",
|
||||
"failed": "註冊失敗,請稍後重試。"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 管理",
|
||||
"createBtn": "創建 Key",
|
||||
|
||||
@ -3,6 +3,7 @@ import { HOME_MAIN_SCROLL_STORAGE_KEY } from './scrollKeys'
|
||||
import Home from '../views/Home.vue'
|
||||
import Trade from '../views/Trade.vue'
|
||||
import Login from '../views/Login.vue'
|
||||
import Register from '../views/Register.vue'
|
||||
import TradeDetail from '../views/TradeDetail.vue'
|
||||
import EventMarkets from '../views/EventMarkets.vue'
|
||||
import Wallet from '../views/Wallet.vue'
|
||||
@ -39,6 +40,11 @@ const router = createRouter({
|
||||
name: 'login',
|
||||
component: Login,
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: Register,
|
||||
},
|
||||
{
|
||||
path: '/trade-detail/:id',
|
||||
name: 'trade-detail',
|
||||
|
||||
@ -5,20 +5,82 @@
|
||||
<v-card class="login-card" elevation="0" rounded="lg">
|
||||
<v-card-title class="login-title">{{ t('login.title') }}</v-card-title>
|
||||
<v-card-text class="login-body">
|
||||
<p class="login-desc">{{ t('login.desc') }}</p>
|
||||
<v-btn
|
||||
class="wallet-btn"
|
||||
color="success"
|
||||
size="large"
|
||||
block
|
||||
rounded="lg"
|
||||
@click="connectWithWallet"
|
||||
:loading="isConnecting"
|
||||
:disabled="isConnecting"
|
||||
>
|
||||
<v-icon start>mdi-wallet</v-icon>
|
||||
{{ isConnecting ? t('login.connecting') : t('login.connectBtn') }}
|
||||
</v-btn>
|
||||
<v-tabs v-model="activeTab" class="login-tabs" color="success" grow>
|
||||
<v-tab value="account">{{ t('login.tabAccount') }}</v-tab>
|
||||
<v-tab value="wallet">{{ t('login.tabWallet') }}</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeTab" class="login-window">
|
||||
<v-window-item value="account">
|
||||
<p class="login-desc">{{ t('login.descAccount') }}</p>
|
||||
<v-form @submit.prevent="submitPasswordLogin">
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
:label="t('login.username')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
autocomplete="username"
|
||||
class="login-field"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:label="t('login.password')"
|
||||
type="password"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
autocomplete="current-password"
|
||||
class="login-field"
|
||||
/>
|
||||
<div v-if="openCaptcha" class="captcha-row">
|
||||
<v-text-field
|
||||
v-model="captcha"
|
||||
:label="t('login.captcha')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="captcha-input"
|
||||
inputmode="numeric"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="captcha-img-wrap"
|
||||
:aria-label="t('login.refreshCaptcha')"
|
||||
@click="loadCaptcha"
|
||||
>
|
||||
<img v-if="captchaImg" :src="captchaImg" alt="" class="captcha-img" />
|
||||
</button>
|
||||
</div>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="success"
|
||||
size="large"
|
||||
block
|
||||
rounded="lg"
|
||||
class="submit-btn"
|
||||
:loading="isSubmitting"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
{{ t('login.loginBtn') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="wallet">
|
||||
<p class="login-desc">{{ t('login.descWallet') }}</p>
|
||||
<v-btn
|
||||
class="wallet-btn"
|
||||
color="success"
|
||||
size="large"
|
||||
block
|
||||
rounded="lg"
|
||||
@click="connectWithWallet"
|
||||
:loading="isConnecting"
|
||||
:disabled="isConnecting"
|
||||
>
|
||||
<v-icon start>mdi-wallet</v-icon>
|
||||
{{ isConnecting ? t('login.connecting') : t('login.connectBtn') }}
|
||||
</v-btn>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
@ -35,21 +97,87 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { BrowserProvider } from 'ethers'
|
||||
import { SiweMessage } from 'siwe'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import type { UserInfo } from '@/stores/user'
|
||||
import { post } from '@/api/request'
|
||||
import { getCaptcha, loginByPassword, parseLoginResponse, type LoginResponseData } from '@/api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const activeTab = ref('account')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const captcha = ref('')
|
||||
const captchaId = ref('')
|
||||
const captchaImg = ref('')
|
||||
const openCaptcha = ref(true)
|
||||
const isSubmitting = ref(false)
|
||||
const isConnecting = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
async function completeLogin(data: LoginResponseData | Record<string, unknown>) {
|
||||
const { token, user } = parseLoginResponse(data)
|
||||
userStore.setUser({ token, user })
|
||||
await userStore.fetchUserInfo()
|
||||
await userStore.fetchUsdcBalance()
|
||||
await router.replace('/')
|
||||
}
|
||||
|
||||
async function loadCaptcha() {
|
||||
try {
|
||||
const res = await getCaptcha()
|
||||
if (res.code === 0 && res.data) {
|
||||
captchaId.value = res.data.captchaId
|
||||
captchaImg.value = res.data.picPath.startsWith('data:')
|
||||
? res.data.picPath
|
||||
: `data:image/png;base64,${res.data.picPath}`
|
||||
openCaptcha.value = res.data.openCaptcha
|
||||
}
|
||||
} catch {
|
||||
errorMessage.value = t('login.captchaLoadFailed')
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPasswordLogin() {
|
||||
isSubmitting.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
if (!username.value.trim() || !password.value) {
|
||||
errorMessage.value = t('login.credentialsRequired')
|
||||
return
|
||||
}
|
||||
if (openCaptcha.value && !captcha.value.trim()) {
|
||||
errorMessage.value = t('login.captchaRequired')
|
||||
return
|
||||
}
|
||||
const res = await loginByPassword({
|
||||
username: username.value.trim(),
|
||||
password: password.value,
|
||||
captcha: captcha.value.trim(),
|
||||
captchaId: captchaId.value,
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
await completeLogin(res.data)
|
||||
return
|
||||
}
|
||||
errorMessage.value = res.msg || t('login.loginFailed')
|
||||
await loadCaptcha()
|
||||
captcha.value = ''
|
||||
} catch (error: unknown) {
|
||||
errorMessage.value = error instanceof Error ? error.message : t('login.loginFailed')
|
||||
await loadCaptcha()
|
||||
captcha.value = ''
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const connectWithWallet = async () => {
|
||||
isConnecting.value = true
|
||||
errorMessage.value = ''
|
||||
@ -85,7 +213,7 @@ const connectWithWallet = async () => {
|
||||
scheme,
|
||||
domain,
|
||||
address: signer.address,
|
||||
statement: 'Sign in to Alpha Market',
|
||||
statement: t('login.siweStatement'),
|
||||
uri: origin,
|
||||
version: '1',
|
||||
chainId: chainId,
|
||||
@ -99,7 +227,7 @@ const connectWithWallet = async () => {
|
||||
params: [message1, signer.address],
|
||||
})
|
||||
|
||||
const loginData = await post<{ code: number; data?: { token: string; user?: UserInfo } }>(
|
||||
const loginData = await post<{ code: number; data?: Record<string, unknown>; msg?: string }>(
|
||||
'/base/walletLogin',
|
||||
{
|
||||
Message: message1,
|
||||
@ -110,19 +238,10 @@ const connectWithWallet = async () => {
|
||||
)
|
||||
|
||||
if (loginData.code === 0 && loginData.data) {
|
||||
const data = loginData.data as Record<string, unknown>
|
||||
const token = data.token as string | undefined
|
||||
const user =
|
||||
(data.user as UserInfo | undefined) ??
|
||||
(data.ID != null || data.id != null || data.userName != null
|
||||
? (data as unknown as UserInfo)
|
||||
: undefined)
|
||||
userStore.setUser({ token, user })
|
||||
await userStore.fetchUserInfo()
|
||||
await userStore.fetchUsdcBalance()
|
||||
await completeLogin(loginData.data)
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/')
|
||||
errorMessage.value = loginData.msg || t('login.connectFailed')
|
||||
} catch (error: unknown) {
|
||||
console.error('Error connecting to wallet:', error)
|
||||
const msg = error instanceof Error ? error.message : ''
|
||||
@ -137,6 +256,14 @@ const connectWithWallet = async () => {
|
||||
isConnecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (userStore.isLoggedIn) {
|
||||
router.replace('/')
|
||||
return
|
||||
}
|
||||
loadCaptcha()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -167,19 +294,63 @@ const connectWithWallet = async () => {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.login-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-window {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
font-size: 0.95rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
margin: 0 0 28px 0;
|
||||
margin: 0 0 20px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.login-field {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.captcha-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.captcha-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.captcha-img-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f3f4f6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.submit-btn,
|
||||
.wallet-btn {
|
||||
height: 48px;
|
||||
font-weight: 600;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.02em;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
|
||||
285
src/views/Register.vue
Normal file
285
src/views/Register.vue
Normal file
@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<v-container class="register-container">
|
||||
<v-row justify="center" align="center" class="register-row">
|
||||
<v-col cols="12" sm="10" md="6" lg="4">
|
||||
<v-card class="register-card" elevation="0" rounded="lg">
|
||||
<v-card-title class="register-title">{{ t('register.title') }}</v-card-title>
|
||||
<v-card-text class="register-body">
|
||||
<p class="register-desc">{{ t('register.desc') }}</p>
|
||||
<v-form @submit.prevent="submitRegister">
|
||||
<v-text-field
|
||||
v-model="userName"
|
||||
:label="t('register.username')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
autocomplete="username"
|
||||
class="register-field"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="nickName"
|
||||
:label="t('register.nickName')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="register-field"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="passWord"
|
||||
:label="t('register.password')"
|
||||
type="password"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
autocomplete="new-password"
|
||||
class="register-field"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="confirmPassword"
|
||||
:label="t('register.confirmPassword')"
|
||||
type="password"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
autocomplete="new-password"
|
||||
class="register-field"
|
||||
/>
|
||||
<div class="captcha-row">
|
||||
<v-text-field
|
||||
v-model="captcha"
|
||||
:label="t('register.captcha')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="captcha-input"
|
||||
inputmode="numeric"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="captcha-img-wrap"
|
||||
:aria-label="t('register.refreshCaptcha')"
|
||||
@click="loadCaptcha"
|
||||
>
|
||||
<img v-if="captchaImg" :src="captchaImg" alt="" class="captcha-img" />
|
||||
</button>
|
||||
</div>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="success"
|
||||
size="large"
|
||||
block
|
||||
rounded="lg"
|
||||
class="submit-btn"
|
||||
:loading="isSubmitting"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
{{ t('register.submitBtn') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<div class="register-links">
|
||||
<router-link to="/login">{{ t('register.signIn') }}</router-link>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getCaptcha, registerByPassword, parseLoginResponse } from '@/api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userName = ref('')
|
||||
const nickName = ref('')
|
||||
const passWord = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const captcha = ref('')
|
||||
const captchaId = ref('')
|
||||
const captchaImg = ref('')
|
||||
const promotionCode = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
async function loadCaptcha() {
|
||||
try {
|
||||
const res = await getCaptcha()
|
||||
if (res.code === 0 && res.data) {
|
||||
captchaId.value = res.data.captchaId
|
||||
captchaImg.value = res.data.picPath.startsWith('data:')
|
||||
? res.data.picPath
|
||||
: `data:image/png;base64,${res.data.picPath}`
|
||||
}
|
||||
} catch {
|
||||
errorMessage.value = t('register.captchaLoadFailed')
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRegister() {
|
||||
isSubmitting.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
if (!userName.value.trim() || !passWord.value) {
|
||||
errorMessage.value = t('register.credentialsRequired')
|
||||
return
|
||||
}
|
||||
if (passWord.value !== confirmPassword.value) {
|
||||
errorMessage.value = t('register.passwordMismatch')
|
||||
return
|
||||
}
|
||||
if (!captcha.value.trim()) {
|
||||
errorMessage.value = t('register.captchaRequired')
|
||||
return
|
||||
}
|
||||
const res = await registerByPassword({
|
||||
userName: userName.value.trim(),
|
||||
passWord: passWord.value,
|
||||
nickName: nickName.value.trim() || undefined,
|
||||
captcha: captcha.value.trim(),
|
||||
captchaId: captchaId.value,
|
||||
promotionCode: promotionCode.value.trim() || undefined,
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
const { token, user } = parseLoginResponse(res.data)
|
||||
userStore.setUser({ token, user })
|
||||
await userStore.fetchUserInfo()
|
||||
await userStore.fetchUsdcBalance()
|
||||
await router.replace('/')
|
||||
return
|
||||
}
|
||||
errorMessage.value = res.msg || t('register.failed')
|
||||
await loadCaptcha()
|
||||
captcha.value = ''
|
||||
} catch (error: unknown) {
|
||||
errorMessage.value = error instanceof Error ? error.message : t('register.failed')
|
||||
await loadCaptcha()
|
||||
captcha.value = ''
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (userStore.isLoggedIn) {
|
||||
router.replace('/')
|
||||
return
|
||||
}
|
||||
const refCode = route.query.ref
|
||||
if (typeof refCode === 'string' && refCode.trim()) {
|
||||
promotionCode.value = refCode.trim()
|
||||
}
|
||||
loadCaptcha()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.register-container {
|
||||
padding: 40px 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.register-row {
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
.register-card {
|
||||
border-radius: 16px;
|
||||
padding: 40px 32px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.register-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.register-body {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.register-desc {
|
||||
font-size: 0.95rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
margin: 0 0 20px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.register-field {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.captcha-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.captcha-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.captcha-img-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f3f4f6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
height: 48px;
|
||||
font-weight: 600;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.02em;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.register-links {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.register-links a {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.register-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user