新增:用户名密码登录
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": {
|
"login": {
|
||||||
"title": "Sign In",
|
"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...",
|
"connecting": "Connecting...",
|
||||||
"connectBtn": "Connect with Wallet",
|
"connectBtn": "Connect with Wallet",
|
||||||
"signUp": "Don't have an account? Sign Up",
|
"signUp": "Don't have an account? Sign Up",
|
||||||
"walletNotInstalled": "Ethereum wallet is not installed. Please install MetaMask, TokenPocket or another Ethereum wallet to continue.",
|
"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.",
|
"invalidAddress": "Invalid wallet address format. Please check your wallet connection.",
|
||||||
"connectFailed": "Failed to connect with wallet. Please check your wallet and try again.",
|
"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": {
|
"apiKey": {
|
||||||
"title": "API Key Management",
|
"title": "API Key Management",
|
||||||
|
|||||||
@ -142,14 +142,43 @@
|
|||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "サインイン",
|
"title": "サインイン",
|
||||||
"desc": "ウォレットを接続してサインイン",
|
"descAccount": "ユーザー名とパスワードでサインイン",
|
||||||
|
"descWallet": "ウォレットを接続してサインイン",
|
||||||
|
"tabAccount": "アカウント",
|
||||||
|
"tabWallet": "ウォレット",
|
||||||
|
"username": "ユーザー名",
|
||||||
|
"password": "パスワード",
|
||||||
|
"captcha": "認証コード",
|
||||||
|
"refreshCaptcha": "認証コードを更新",
|
||||||
|
"loginBtn": "サインイン",
|
||||||
"connecting": "接続中...",
|
"connecting": "接続中...",
|
||||||
"connectBtn": "ウォレットで接続",
|
"connectBtn": "ウォレットで接続",
|
||||||
"signUp": "アカウントをお持ちでないですか?新規登録",
|
"signUp": "アカウントをお持ちでないですか?新規登録",
|
||||||
"walletNotInstalled": "イーサリアムウォレットがインストールされていません。MetaMask、TokenPocket などのウォレットをインストールしてください。",
|
"walletNotInstalled": "イーサリアムウォレットがインストールされていません。MetaMask、TokenPocket などのウォレットをインストールしてください。",
|
||||||
"invalidAddress": "ウォレットアドレスの形式が無効です。接続を確認してください。",
|
"invalidAddress": "ウォレットアドレスの形式が無効です。接続を確認してください。",
|
||||||
"connectFailed": "ウォレットの接続に失敗しました。ウォレットを確認して再試行してください。",
|
"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": {
|
"apiKey": {
|
||||||
"title": "API Key 管理",
|
"title": "API Key 管理",
|
||||||
|
|||||||
@ -142,15 +142,44 @@
|
|||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "로그인",
|
"title": "로그인",
|
||||||
"desc": "지갑을 연결하여 로그인",
|
"descAccount": "사용자 이름과 비밀번호로 로그인",
|
||||||
|
"descWallet": "지갑을 연결하여 로그인",
|
||||||
|
"tabAccount": "계정",
|
||||||
|
"tabWallet": "지갑",
|
||||||
|
"username": "사용자 이름",
|
||||||
|
"password": "비밀번호",
|
||||||
|
"captcha": "인증 코드",
|
||||||
|
"refreshCaptcha": "인증 코드 새로고침",
|
||||||
|
"loginBtn": "로그인",
|
||||||
"connecting": "연결 중...",
|
"connecting": "연결 중...",
|
||||||
"connectBtn": "지갑 연결",
|
"connectBtn": "지갑 연결",
|
||||||
"signUp": "계정이 없으신가요? 회원가입",
|
"signUp": "계정이 없으신가요? 회원가입",
|
||||||
"walletNotInstalled": "이더리움 지갑이 설치되어 있지 않습니다. MetaMask, TokenPocket 등 지갑을 설치해 주세요.",
|
"walletNotInstalled": "이더리움 지갑이 설치되어 있지 않습니다. MetaMask, TokenPocket 등 지갑을 설치해 주세요.",
|
||||||
"invalidAddress": "지갑 주소 형식이 올바르지 않습니다. 지갑 연결을 확인해 주세요.",
|
"invalidAddress": "지갑 주소 형식이 올바르지 않습니다. 지갑 연결을 확인해 주세요.",
|
||||||
"connectFailed": "지갑 연결에 실패했습니다. 지갑을 확인하고 다시 시도해 주세요.",
|
"connectFailed": "지갑 연결에 실패했습니다. 지갑을 확인하고 다시 시도해 주세요.",
|
||||||
|
"loginFailed": "로그인에 실패했습니다. 계정 정보를 확인해 주세요.",
|
||||||
|
"credentialsRequired": "사용자 이름과 비밀번호를 입력해 주세요.",
|
||||||
|
"captchaRequired": "인증 코드를 입력해 주세요.",
|
||||||
|
"captchaLoadFailed": "인증 코드를 불러오지 못했습니다. 새로고침 후 다시 시도해 주세요.",
|
||||||
"siweStatement": "Alpha Market 로그인"
|
"siweStatement": "Alpha Market 로그인"
|
||||||
},
|
},
|
||||||
|
"register": {
|
||||||
|
"title": "회원가입",
|
||||||
|
"desc": "계정을 만들고 거래를 시작하세요",
|
||||||
|
"username": "사용자 이름",
|
||||||
|
"nickName": "닉네임 (선택)",
|
||||||
|
"password": "비밀번호",
|
||||||
|
"confirmPassword": "비밀번호 확인",
|
||||||
|
"captcha": "인증 코드",
|
||||||
|
"refreshCaptcha": "인증 코드 새로고침",
|
||||||
|
"submitBtn": "계정 만들기",
|
||||||
|
"signIn": "이미 계정이 있으신가요? 로그인",
|
||||||
|
"passwordMismatch": "비밀번호가 일치하지 않습니다.",
|
||||||
|
"credentialsRequired": "사용자 이름과 비밀번호를 입력해 주세요.",
|
||||||
|
"captchaRequired": "인증 코드를 입력해 주세요.",
|
||||||
|
"captchaLoadFailed": "인증 코드를 불러오지 못했습니다. 새로고침 후 다시 시도해 주세요.",
|
||||||
|
"failed": "회원가입에 실패했습니다. 다시 시도해 주세요."
|
||||||
|
},
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"title": "API Key 관리",
|
"title": "API Key 관리",
|
||||||
"createBtn": "Key 생성",
|
"createBtn": "Key 생성",
|
||||||
|
|||||||
@ -142,15 +142,44 @@
|
|||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "登录",
|
"title": "登录",
|
||||||
"desc": "连接钱包以登录",
|
"descAccount": "使用用户名和密码登录",
|
||||||
|
"descWallet": "连接钱包以登录",
|
||||||
|
"tabAccount": "账号密码",
|
||||||
|
"tabWallet": "钱包登录",
|
||||||
|
"username": "用户名",
|
||||||
|
"password": "密码",
|
||||||
|
"captcha": "验证码",
|
||||||
|
"refreshCaptcha": "刷新验证码",
|
||||||
|
"loginBtn": "登录",
|
||||||
"connecting": "连接中...",
|
"connecting": "连接中...",
|
||||||
"connectBtn": "连接钱包",
|
"connectBtn": "连接钱包",
|
||||||
"signUp": "没有账号?立即注册",
|
"signUp": "没有账号?立即注册",
|
||||||
"walletNotInstalled": "未检测到以太坊钱包。请安装 MetaMask、TokenPocket 或其他以太坊钱包后重试。",
|
"walletNotInstalled": "未检测到以太坊钱包。请安装 MetaMask、TokenPocket 或其他以太坊钱包后重试。",
|
||||||
"invalidAddress": "钱包地址格式无效,请检查钱包连接。",
|
"invalidAddress": "钱包地址格式无效,请检查钱包连接。",
|
||||||
"connectFailed": "连接钱包失败,请检查钱包后重试。",
|
"connectFailed": "连接钱包失败,请检查钱包后重试。",
|
||||||
|
"loginFailed": "登录失败,请检查账号密码后重试。",
|
||||||
|
"credentialsRequired": "请输入用户名和密码。",
|
||||||
|
"captchaRequired": "请输入验证码。",
|
||||||
|
"captchaLoadFailed": "验证码加载失败,请刷新后重试。",
|
||||||
"siweStatement": "登录 Alpha Market"
|
"siweStatement": "登录 Alpha Market"
|
||||||
},
|
},
|
||||||
|
"register": {
|
||||||
|
"title": "注册",
|
||||||
|
"desc": "创建账号开始交易",
|
||||||
|
"username": "用户名",
|
||||||
|
"nickName": "昵称(可选)",
|
||||||
|
"password": "密码",
|
||||||
|
"confirmPassword": "确认密码",
|
||||||
|
"captcha": "验证码",
|
||||||
|
"refreshCaptcha": "刷新验证码",
|
||||||
|
"submitBtn": "注册",
|
||||||
|
"signIn": "已有账号?立即登录",
|
||||||
|
"passwordMismatch": "两次输入的密码不一致。",
|
||||||
|
"credentialsRequired": "请输入用户名和密码。",
|
||||||
|
"captchaRequired": "请输入验证码。",
|
||||||
|
"captchaLoadFailed": "验证码加载失败,请刷新后重试。",
|
||||||
|
"failed": "注册失败,请稍后重试。"
|
||||||
|
},
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"title": "API Key 管理",
|
"title": "API Key 管理",
|
||||||
"createBtn": "创建 Key",
|
"createBtn": "创建 Key",
|
||||||
|
|||||||
@ -142,15 +142,44 @@
|
|||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "登入",
|
"title": "登入",
|
||||||
"desc": "連接錢包以登入",
|
"descAccount": "使用使用者名稱和密碼登入",
|
||||||
|
"descWallet": "連接錢包以登入",
|
||||||
|
"tabAccount": "帳號密碼",
|
||||||
|
"tabWallet": "錢包登入",
|
||||||
|
"username": "使用者名稱",
|
||||||
|
"password": "密碼",
|
||||||
|
"captcha": "驗證碼",
|
||||||
|
"refreshCaptcha": "刷新驗證碼",
|
||||||
|
"loginBtn": "登入",
|
||||||
"connecting": "連接中...",
|
"connecting": "連接中...",
|
||||||
"connectBtn": "連接錢包",
|
"connectBtn": "連接錢包",
|
||||||
"signUp": "沒有帳號?立即註冊",
|
"signUp": "沒有帳號?立即註冊",
|
||||||
"walletNotInstalled": "未偵測到以太坊錢包。請安裝 MetaMask、TokenPocket 或其他以太坊錢包後重試。",
|
"walletNotInstalled": "未偵測到以太坊錢包。請安裝 MetaMask、TokenPocket 或其他以太坊錢包後重試。",
|
||||||
"invalidAddress": "錢包地址格式無效,請檢查錢包連接。",
|
"invalidAddress": "錢包地址格式無效,請檢查錢包連接。",
|
||||||
"connectFailed": "連接錢包失敗,請檢查錢包後重試。",
|
"connectFailed": "連接錢包失敗,請檢查錢包後重試。",
|
||||||
|
"loginFailed": "登入失敗,請檢查帳號密碼後重試。",
|
||||||
|
"credentialsRequired": "請輸入使用者名稱和密碼。",
|
||||||
|
"captchaRequired": "請輸入驗證碼。",
|
||||||
|
"captchaLoadFailed": "驗證碼載入失敗,請刷新後重試。",
|
||||||
"siweStatement": "登入 Alpha Market"
|
"siweStatement": "登入 Alpha Market"
|
||||||
},
|
},
|
||||||
|
"register": {
|
||||||
|
"title": "註冊",
|
||||||
|
"desc": "建立帳號開始交易",
|
||||||
|
"username": "使用者名稱",
|
||||||
|
"nickName": "暱稱(可選)",
|
||||||
|
"password": "密碼",
|
||||||
|
"confirmPassword": "確認密碼",
|
||||||
|
"captcha": "驗證碼",
|
||||||
|
"refreshCaptcha": "刷新驗證碼",
|
||||||
|
"submitBtn": "註冊",
|
||||||
|
"signIn": "已有帳號?立即登入",
|
||||||
|
"passwordMismatch": "兩次輸入的密碼不一致。",
|
||||||
|
"credentialsRequired": "請輸入使用者名稱和密碼。",
|
||||||
|
"captchaRequired": "請輸入驗證碼。",
|
||||||
|
"captchaLoadFailed": "驗證碼載入失敗,請刷新後重試。",
|
||||||
|
"failed": "註冊失敗,請稍後重試。"
|
||||||
|
},
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"title": "API Key 管理",
|
"title": "API Key 管理",
|
||||||
"createBtn": "創建 Key",
|
"createBtn": "創建 Key",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { HOME_MAIN_SCROLL_STORAGE_KEY } from './scrollKeys'
|
|||||||
import Home from '../views/Home.vue'
|
import Home from '../views/Home.vue'
|
||||||
import Trade from '../views/Trade.vue'
|
import Trade from '../views/Trade.vue'
|
||||||
import Login from '../views/Login.vue'
|
import Login from '../views/Login.vue'
|
||||||
|
import Register from '../views/Register.vue'
|
||||||
import TradeDetail from '../views/TradeDetail.vue'
|
import TradeDetail from '../views/TradeDetail.vue'
|
||||||
import EventMarkets from '../views/EventMarkets.vue'
|
import EventMarkets from '../views/EventMarkets.vue'
|
||||||
import Wallet from '../views/Wallet.vue'
|
import Wallet from '../views/Wallet.vue'
|
||||||
@ -39,6 +40,11 @@ const router = createRouter({
|
|||||||
name: 'login',
|
name: 'login',
|
||||||
component: Login,
|
component: Login,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'register',
|
||||||
|
component: Register,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/trade-detail/:id',
|
path: '/trade-detail/:id',
|
||||||
name: 'trade-detail',
|
name: 'trade-detail',
|
||||||
|
|||||||
@ -5,20 +5,82 @@
|
|||||||
<v-card class="login-card" elevation="0" rounded="lg">
|
<v-card class="login-card" elevation="0" rounded="lg">
|
||||||
<v-card-title class="login-title">{{ t('login.title') }}</v-card-title>
|
<v-card-title class="login-title">{{ t('login.title') }}</v-card-title>
|
||||||
<v-card-text class="login-body">
|
<v-card-text class="login-body">
|
||||||
<p class="login-desc">{{ t('login.desc') }}</p>
|
<v-tabs v-model="activeTab" class="login-tabs" color="success" grow>
|
||||||
<v-btn
|
<v-tab value="account">{{ t('login.tabAccount') }}</v-tab>
|
||||||
class="wallet-btn"
|
<v-tab value="wallet">{{ t('login.tabWallet') }}</v-tab>
|
||||||
color="success"
|
</v-tabs>
|
||||||
size="large"
|
|
||||||
block
|
<v-window v-model="activeTab" class="login-window">
|
||||||
rounded="lg"
|
<v-window-item value="account">
|
||||||
@click="connectWithWallet"
|
<p class="login-desc">{{ t('login.descAccount') }}</p>
|
||||||
:loading="isConnecting"
|
<v-form @submit.prevent="submitPasswordLogin">
|
||||||
:disabled="isConnecting"
|
<v-text-field
|
||||||
>
|
v-model="username"
|
||||||
<v-icon start>mdi-wallet</v-icon>
|
:label="t('login.username')"
|
||||||
{{ isConnecting ? t('login.connecting') : t('login.connectBtn') }}
|
variant="outlined"
|
||||||
</v-btn>
|
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">
|
<div v-if="errorMessage" class="error-message">
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
@ -35,21 +97,87 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { BrowserProvider } from 'ethers'
|
import { BrowserProvider } from 'ethers'
|
||||||
import { SiweMessage } from 'siwe'
|
import { SiweMessage } from 'siwe'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import type { UserInfo } from '@/stores/user'
|
|
||||||
import { post } from '@/api/request'
|
import { post } from '@/api/request'
|
||||||
|
import { getCaptcha, loginByPassword, parseLoginResponse, type LoginResponseData } from '@/api/auth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const userStore = useUserStore()
|
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 isConnecting = ref(false)
|
||||||
const errorMessage = ref('')
|
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 () => {
|
const connectWithWallet = async () => {
|
||||||
isConnecting.value = true
|
isConnecting.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
@ -85,7 +213,7 @@ const connectWithWallet = async () => {
|
|||||||
scheme,
|
scheme,
|
||||||
domain,
|
domain,
|
||||||
address: signer.address,
|
address: signer.address,
|
||||||
statement: 'Sign in to Alpha Market',
|
statement: t('login.siweStatement'),
|
||||||
uri: origin,
|
uri: origin,
|
||||||
version: '1',
|
version: '1',
|
||||||
chainId: chainId,
|
chainId: chainId,
|
||||||
@ -99,7 +227,7 @@ const connectWithWallet = async () => {
|
|||||||
params: [message1, signer.address],
|
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',
|
'/base/walletLogin',
|
||||||
{
|
{
|
||||||
Message: message1,
|
Message: message1,
|
||||||
@ -110,19 +238,10 @@ const connectWithWallet = async () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (loginData.code === 0 && loginData.data) {
|
if (loginData.code === 0 && loginData.data) {
|
||||||
const data = loginData.data as Record<string, unknown>
|
await completeLogin(loginData.data)
|
||||||
const token = data.token as string | undefined
|
return
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
errorMessage.value = loginData.msg || t('login.connectFailed')
|
||||||
router.push('/')
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('Error connecting to wallet:', error)
|
console.error('Error connecting to wallet:', error)
|
||||||
const msg = error instanceof Error ? error.message : ''
|
const msg = error instanceof Error ? error.message : ''
|
||||||
@ -137,6 +256,14 @@ const connectWithWallet = async () => {
|
|||||||
isConnecting.value = false
|
isConnecting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (userStore.isLoggedIn) {
|
||||||
|
router.replace('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadCaptcha()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -167,19 +294,63 @@ const connectWithWallet = async () => {
|
|||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-tabs {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-window {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.login-desc {
|
.login-desc {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0 0 28px 0;
|
margin: 0 0 20px 0;
|
||||||
line-height: 1.5;
|
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 {
|
.wallet-btn {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.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