save
This commit is contained in:
30
src/apis/member.ts
Normal file
30
src/apis/member.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import http from '@/utils/http.ts'
|
||||
|
||||
export type LevelBenefits = {
|
||||
"level": {
|
||||
"id": number,
|
||||
"name": string,
|
||||
"code": string,
|
||||
"level": number,
|
||||
"price": string,
|
||||
"price_auto": string,
|
||||
"yearly_price": string,
|
||||
"description": string,
|
||||
"color": string,
|
||||
"icon": string,
|
||||
"is_active": number,
|
||||
"created_at": string,
|
||||
"updated_at": string
|
||||
},
|
||||
"benefits": {
|
||||
"code": string,
|
||||
"name": string,
|
||||
"type": boolean,
|
||||
"unit": null,
|
||||
"value": string
|
||||
}[]
|
||||
}
|
||||
|
||||
export function levelBenefits(params) {
|
||||
return http<LevelBenefits>('member/levelBenefits', null, params, 'get')
|
||||
}
|
||||
@@ -18,13 +18,11 @@ export interface User {
|
||||
avatar?: string,
|
||||
hasPwd?: boolean,
|
||||
member: {
|
||||
level: number,
|
||||
levelDesc: string,
|
||||
status: string,
|
||||
active: boolean,
|
||||
endTime: number,
|
||||
endDate: number,
|
||||
autoRenew: boolean,
|
||||
payMethod: number,
|
||||
payMethodDesc: string,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +65,7 @@ export interface WechatLoginParams {
|
||||
}
|
||||
|
||||
export function loginApi(params: LoginParams) {
|
||||
return http<User>('user/login', params, null, 'post')
|
||||
return http<{ token:string }>('user/login', params, null, 'post')
|
||||
}
|
||||
|
||||
export function registerApi(params: RegisterParams) {
|
||||
|
||||
217
src/pages/user/Pay.vue
Normal file
217
src/pages/user/Pay.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
|
||||
// Payment method selection
|
||||
const selectedPaymentMethod = ref('')
|
||||
const agreeToTerms = ref(false)
|
||||
|
||||
// Payment methods - WeChat and Alipay
|
||||
const paymentMethods = [
|
||||
{
|
||||
id: 'wechat',
|
||||
name: '微信支付',
|
||||
icon: '💚',
|
||||
description: '使用微信支付'
|
||||
},
|
||||
{
|
||||
id: 'alipay',
|
||||
name: '支付宝',
|
||||
icon: '💙',
|
||||
description: '使用支付宝支付'
|
||||
}
|
||||
]
|
||||
|
||||
// Order data (this would typically come from props or store)
|
||||
const orderData = {
|
||||
planName: '月度会员',
|
||||
price: 9.99,
|
||||
currency: 'US$',
|
||||
unit: '每月',
|
||||
startDate: new Date().toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}),
|
||||
vatText: '(含增值税)'
|
||||
}
|
||||
|
||||
function selectPaymentMethod(methodId: string) {
|
||||
selectedPaymentMethod.value = methodId
|
||||
}
|
||||
|
||||
function handlePayment() {
|
||||
if (!selectedPaymentMethod.value) {
|
||||
alert('请选择支付方式')
|
||||
return
|
||||
}
|
||||
if (!agreeToTerms.value) {
|
||||
alert('请同意服务条款')
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Implement payment processing
|
||||
console.log('Processing payment with:', selectedPaymentMethod.value)
|
||||
}
|
||||
|
||||
function handleChangePlan() {
|
||||
// TODO: Navigate back to plan selection
|
||||
console.log('Change plan clicked')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="pay-page min-h-screen py-8">
|
||||
<!-- Page Header -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-2">安全支付</h1>
|
||||
<p class="text-gray-600">选择支付方式完成订单</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left Card: Payment Method Selection -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">Choose a way to pay</h2>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="method in paymentMethods"
|
||||
:key="method.id"
|
||||
@click="selectPaymentMethod(method.id)"
|
||||
class="flex items-center p-4 border rounded-lg cursor-pointer transition-all duration-200"
|
||||
:class="[
|
||||
selectedPaymentMethod === method.id
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-2xl mr-3">{{ method.icon }}</span>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">{{ method.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ method.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-5 h-5 rounded-full border-2 flex items-center justify-center"
|
||||
:class="[
|
||||
selectedPaymentMethod === method.id
|
||||
? 'border-blue-500 bg-blue-500'
|
||||
: 'border-gray-300'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="selectedPaymentMethod === method.id"
|
||||
class="w-2 h-2 bg-white rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Card: Order Summary -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">订单概要</h2>
|
||||
<button
|
||||
@click="handleChangePlan"
|
||||
class="px-3 py-1 text-sm text-gray-600 border border-gray-300 rounded-full hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
更改
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Plan Info -->
|
||||
<div class="mb-4">
|
||||
<div class="text-purple-600 text-sm mb-2">付费方案(月费)订阅</div>
|
||||
<div class="text-gray-900 mb-4">
|
||||
从 {{ orderData.startDate }} 开始:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="flex items-baseline mb-4">
|
||||
<span class="text-3xl font-semibold text-gray-900">{{ orderData.currency }}{{ orderData.price }}</span>
|
||||
<span class="text-gray-600 ml-2">/ {{ orderData.unit }}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mb-6">{{ orderData.vatText }}</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-6">
|
||||
<p class="text-sm text-gray-600 mb-2">
|
||||
你将于 {{ orderData.startDate }} 付费。
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
在试订期间和订阅开始前的24小时内,你可随时通过 "账户" > "订阅" 页面取消或改订。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Terms Checkbox -->
|
||||
<div class="flex items-start mb-6">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="terms"
|
||||
v-model="agreeToTerms"
|
||||
class="mt-1 w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label for="terms" class="ml-3 text-sm text-gray-700">
|
||||
我同意 Lingvist 的
|
||||
<a href="#" class="text-purple-600 hover:text-purple-700 underline">服务条款</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Payment Button -->
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:type="selectedPaymentMethod && agreeToTerms ? 'primary' : 'default'"
|
||||
:disabled="!selectedPaymentMethod || !agreeToTerms"
|
||||
@click="handlePayment"
|
||||
>
|
||||
选择
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pay-page {
|
||||
@apply min-h-screen;
|
||||
}
|
||||
|
||||
/* Custom radio button style */
|
||||
.payment-method {
|
||||
@apply flex items-center p-4 border rounded-lg cursor-pointer transition-all duration-200;
|
||||
|
||||
&:hover {
|
||||
@apply border-gray-300 bg-gray-50;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@apply border-blue-500 bg-blue-50;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-radio {
|
||||
@apply w-5 h-5 rounded-full border-2 flex items-center justify-center;
|
||||
|
||||
&.selected {
|
||||
@apply border-blue-500 bg-blue-500;
|
||||
}
|
||||
|
||||
&.unselected {
|
||||
@apply border-gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-dot {
|
||||
@apply w-2 h-2 bg-white rounded-full;
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/auth.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {onMounted} from 'vue'
|
||||
import {useUserStore} from '@/stores/auth.ts'
|
||||
import {useRouter} from 'vue-router'
|
||||
import BaseInput from '@/components/base/BaseInput.vue'
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { APP_NAME, EMAIL, GITHUB } from "@/config/env.ts";
|
||||
import {APP_NAME, EMAIL, GITHUB} from "@/config/env.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { PASSWORD_CONFIG, PHONE_CONFIG } from "@/config/auth.ts";
|
||||
import { changeEmailApi, changePhoneApi, setPassword, updateUserInfoApi } from "@/apis/user.ts";
|
||||
import {PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts";
|
||||
import {changeEmailApi, changePhoneApi, setPassword, updateUserInfoApi, User} from "@/apis/user.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import { CodeType } from "@/types/types.ts";
|
||||
import {CodeType} from "@/types/types.ts";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import Form from "@/components/base/form/Form.vue";
|
||||
import { FormInstance } from "@/components/base/form/types.ts";
|
||||
import { codeRules, emailRules, passwordRules, phoneRules } from "@/utils/validation.ts";
|
||||
import { _dateFormat, cloneDeep } from "@/utils";
|
||||
import {FormInstance} from "@/components/base/form/types.ts";
|
||||
import {codeRules, emailRules, passwordRules, phoneRules} from "@/utils/validation.ts";
|
||||
import {_dateFormat, cloneDeep} from "@/utils";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import Code from "@/pages/user/Code.vue";
|
||||
import { MessageBox } from "@/utils/MessageBox.tsx";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
@@ -234,14 +234,11 @@ function changePwd() {
|
||||
})
|
||||
}
|
||||
|
||||
const member = $computed<User['member']>(() => userStore.user?.member ?? {} as any)
|
||||
|
||||
// 订阅相关
|
||||
const memberEndtime = $computed(() => {
|
||||
if (userStore.user?.member) {
|
||||
if (userStore.user?.member?.endTime === -1) return '永久'
|
||||
else return _dateFormat(userStore.user?.member?.endTime)
|
||||
}
|
||||
return ''
|
||||
const memberEndDate = $computed(() => {
|
||||
if (member?.endDate === null) return '永久'
|
||||
return member?.endDate
|
||||
})
|
||||
|
||||
function subscribe() {
|
||||
@@ -266,9 +263,9 @@ function subscribe() {
|
||||
<p class="">登录,开启您的学习之旅</p>
|
||||
<div>保存进度、同步数据、解锁个性化内容</div>
|
||||
<BaseButton
|
||||
@click="router.push('/login')"
|
||||
size="large"
|
||||
class="w-full mt-4"
|
||||
@click="router.push('/login')"
|
||||
size="large"
|
||||
class="w-full mt-4"
|
||||
>
|
||||
登录
|
||||
</BaseButton>
|
||||
@@ -302,16 +299,16 @@ function subscribe() {
|
||||
</div>
|
||||
<div v-if="showChangeUsername">
|
||||
<Form
|
||||
ref="changeUsernameFormRef"
|
||||
:rules="changeUsernameFormRules"
|
||||
:model="changeUsernameForm">
|
||||
ref="changeUsernameFormRef"
|
||||
:rules="changeUsernameFormRules"
|
||||
:model="changeUsernameForm">
|
||||
<FormItem prop="username">
|
||||
<BaseInput
|
||||
v-model="changeUsernameForm.username"
|
||||
type="text"
|
||||
size="large"
|
||||
placeholder="请输入用户名"
|
||||
autofocus
|
||||
v-model="changeUsernameForm.username"
|
||||
type="text"
|
||||
size="large"
|
||||
placeholder="请输入用户名"
|
||||
autofocus
|
||||
>
|
||||
<template #preIcon>
|
||||
<IconFluentPerson20Regular class="text-base"/>
|
||||
@@ -342,17 +339,17 @@ function subscribe() {
|
||||
</div>
|
||||
<div v-if="showChangePhone">
|
||||
<Form
|
||||
ref="changePhoneFormRef"
|
||||
:rules="changePhoneFormRules"
|
||||
:model="changePhoneForm">
|
||||
ref="changePhoneFormRef"
|
||||
:rules="changePhoneFormRules"
|
||||
:model="changePhoneForm">
|
||||
<FormItem prop="oldCode" v-if="userStore.user?.phone">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="changePhoneForm.oldCode"
|
||||
type="code"
|
||||
autofocus
|
||||
placeholder="请输入原手机号验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
v-model="changePhoneForm.oldCode"
|
||||
type="code"
|
||||
autofocus
|
||||
placeholder="请输入原手机号验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => true"
|
||||
:type="CodeType.ChangePhoneOld"
|
||||
@@ -361,19 +358,19 @@ function subscribe() {
|
||||
</FormItem>
|
||||
<FormItem prop="phone">
|
||||
<BaseInput
|
||||
v-model="changePhoneForm.phone"
|
||||
type="tel"
|
||||
size="large"
|
||||
placeholder="请输入新手机号"
|
||||
v-model="changePhoneForm.phone"
|
||||
type="tel"
|
||||
size="large"
|
||||
placeholder="请输入新手机号"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="changePhoneForm.code"
|
||||
type="code"
|
||||
placeholder="请输入新手机号验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
v-model="changePhoneForm.code"
|
||||
type="code"
|
||||
placeholder="请输入新手机号验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => changePhoneFormRef.validateField('phone')"
|
||||
:type="CodeType.ChangePhoneNew"
|
||||
@@ -382,10 +379,10 @@ function subscribe() {
|
||||
</FormItem>
|
||||
<FormItem prop="pwd" v-if="!userStore.user?.phone">
|
||||
<BaseInput
|
||||
v-model="changePhoneForm.pwd"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请输入原密码"
|
||||
v-model="changePhoneForm.pwd"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请输入原密码"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
@@ -418,25 +415,25 @@ function subscribe() {
|
||||
</div>
|
||||
<div v-if="showChangeEmail">
|
||||
<Form
|
||||
ref="changeEmailFormRef"
|
||||
:rules="changeEmailFormRules"
|
||||
:model="changeEmailForm">
|
||||
ref="changeEmailFormRef"
|
||||
:rules="changeEmailFormRules"
|
||||
:model="changeEmailForm">
|
||||
<FormItem prop="email">
|
||||
<BaseInput
|
||||
v-model="changeEmailForm.email"
|
||||
type="email"
|
||||
size="large"
|
||||
placeholder="请输入邮箱地址"
|
||||
autofocus
|
||||
v-model="changeEmailForm.email"
|
||||
type="email"
|
||||
size="large"
|
||||
placeholder="请输入邮箱地址"
|
||||
autofocus
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="changeEmailForm.code"
|
||||
type="code"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
v-model="changeEmailForm.code"
|
||||
type="code"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => changeEmailFormRef.validateField('email')"
|
||||
:type="CodeType.ChangeEmail"
|
||||
@@ -445,10 +442,10 @@ function subscribe() {
|
||||
</FormItem>
|
||||
<FormItem prop="pwd" v-if="userStore.user?.hasPwd">
|
||||
<BaseInput
|
||||
v-model="changePwdForm.pwd"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
v-model="changePwdForm.pwd"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
@@ -467,42 +464,42 @@ function subscribe() {
|
||||
<div class="text-xs">在此输入密码</div>
|
||||
</div>
|
||||
<IconFluentChevronLeft28Filled
|
||||
class="transition-transform"
|
||||
:class="['rotate-270','rotate-180'][showChangePwd?0:1]"/>
|
||||
class="transition-transform"
|
||||
:class="['rotate-270','rotate-180'][showChangePwd?0:1]"/>
|
||||
</div>
|
||||
<div v-if="showChangePwd">
|
||||
<Form
|
||||
ref="changePwdFormRef"
|
||||
:rules="changePwdFormRules"
|
||||
:model="changePwdForm">
|
||||
ref="changePwdFormRef"
|
||||
:rules="changePwdFormRules"
|
||||
:model="changePwdForm">
|
||||
<FormItem prop="oldPwd" v-if="userStore.user.hasPwd">
|
||||
<BaseInput
|
||||
v-model="changePwdForm.oldPwd"
|
||||
placeholder="旧密码"
|
||||
type="password"
|
||||
size="large"
|
||||
autofocus
|
||||
v-model="changePwdForm.oldPwd"
|
||||
placeholder="旧密码"
|
||||
type="password"
|
||||
size="large"
|
||||
autofocus
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem prop="newPwd">
|
||||
<BaseInput
|
||||
v-model="changePwdForm.newPwd"
|
||||
type="password"
|
||||
size="large"
|
||||
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength}位)`"
|
||||
:min="PASSWORD_CONFIG.minLength"
|
||||
:max="PASSWORD_CONFIG.maxLength"
|
||||
v-model="changePwdForm.newPwd"
|
||||
type="password"
|
||||
size="large"
|
||||
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength}位)`"
|
||||
:min="PASSWORD_CONFIG.minLength"
|
||||
:max="PASSWORD_CONFIG.maxLength"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="confirmPwd">
|
||||
<BaseInput
|
||||
v-model="changePwdForm.confirmPwd"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请再次输入新密码"
|
||||
:min="PASSWORD_CONFIG.minLength"
|
||||
:max="PASSWORD_CONFIG.maxLength"
|
||||
v-model="changePwdForm.confirmPwd"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请再次输入新密码"
|
||||
:min="PASSWORD_CONFIG.minLength"
|
||||
:max="PASSWORD_CONFIG.maxLength"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
@@ -537,9 +534,9 @@ function subscribe() {
|
||||
<!-- Logout Button -->
|
||||
<div class="center w-full mt-4">
|
||||
<BaseButton
|
||||
@click="handleLogout"
|
||||
size="large"
|
||||
class="w-[80%]"
|
||||
@click="handleLogout"
|
||||
size="large"
|
||||
class="w-[80%]"
|
||||
>
|
||||
登出
|
||||
</BaseButton>
|
||||
@@ -565,15 +562,15 @@ function subscribe() {
|
||||
<template v-if="userStore.user?.member">
|
||||
<div>
|
||||
<div class="mb-1">当前计划</div>
|
||||
<div class="text-base font-bold">{{ userStore.user?.member?.levelDesc }}</div>
|
||||
<div class="text-base font-bold">{{ member?.levelDesc }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1">状态</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span class="text-base font-medium text-green-700">
|
||||
{{ userStore.user?.member?.active ? '使用中' : '已过期' }}
|
||||
<div class="w-2 h-2 rounded-full" :class="member?.active ?'bg-green-500':'bg-red-500'"></div>
|
||||
<span class="text-base font-medium" :class="member?.active ?'text-green-700':'text-red-700'">
|
||||
{{ member?.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -582,7 +579,7 @@ function subscribe() {
|
||||
<div class="mb-1">到期时间</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconFluentCalendarDate20Regular class="text-lg"/>
|
||||
<span class="text-base font-medium">{{ memberEndtime }}</span>
|
||||
<span class="text-base font-medium">{{ memberEndDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -590,27 +587,21 @@ function subscribe() {
|
||||
<div class="mb-1">自动续费</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full"
|
||||
:class="userStore.user?.member?.autoRenew ? 'bg-blue-500' : 'bg-gray-400'"
|
||||
:class="member?.autoRenew ? 'bg-blue-500' : 'bg-gray-400'"
|
||||
></div>
|
||||
<span class="text-base font-medium"
|
||||
:class="userStore.user?.member?.autoRenew ? 'text-blue-700' : 'text-gray-600'">
|
||||
{{ userStore.user?.member?.autoRenew ? '已开启' : '已关闭' }}
|
||||
:class="member?.autoRenew ? 'text-blue-700' : 'text-gray-600'">
|
||||
{{ member?.autoRenew ? '已开启' : '已关闭' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1">付款方式</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconFluentPayment20Regular class="text-lg"/>
|
||||
<span class="text-base font-medium">{{ userStore.user?.member?.payMethodDesc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="text-base" v-else>当前无订阅</div>
|
||||
|
||||
<BaseButton class="w-full" size="large" @click="subscribe">{{ userStore.user?.member ? '管理订阅' : '会员介绍' }}
|
||||
<BaseButton class="w-full" size="large" @click="subscribe">{{
|
||||
userStore.user?.member ? '管理订阅' : '会员介绍'
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/auth.ts'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useUserStore} from '@/stores/auth.ts'
|
||||
import {User} from "@/apis/user.ts";
|
||||
import {onMounted} from "vue";
|
||||
import Header from "@/components/Header.vue";
|
||||
import {LevelBenefits, levelBenefits} from "@/apis/member.ts";
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -18,119 +22,185 @@ interface Plan {
|
||||
features: string[]
|
||||
}
|
||||
|
||||
const plans: Plan[] = [
|
||||
{
|
||||
id: 'monthly',
|
||||
name: '月付',
|
||||
price: 10,
|
||||
unit: '月',
|
||||
desc: '',
|
||||
features: [
|
||||
'同步自定义词典/书籍',
|
||||
'练习功能与进度统计',
|
||||
'词库与文章每日更新',
|
||||
'优先客服支持'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'monthly-auto',
|
||||
name: '连续包月',
|
||||
price: 7,
|
||||
unit: '月',
|
||||
desc: '',
|
||||
highlight: '性价比更高',
|
||||
autoRenew: true,
|
||||
features: [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'yearly',
|
||||
name: '年度会员',
|
||||
price: 100,
|
||||
unit: '年',
|
||||
desc: '',
|
||||
highlight: '年度优惠',
|
||||
features: [
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const member = $computed<User['member']>(() => userStore.user?.member ?? {} as any)
|
||||
|
||||
const memberEndDate = $computed(() => {
|
||||
if (member?.endDate === null) return '永久'
|
||||
return member?.endDate
|
||||
})
|
||||
|
||||
// Get current plan info
|
||||
const currentPlan = $computed(() => {
|
||||
if (!member?.active) return null
|
||||
return plans.find(p => p.id === member.planId) || null
|
||||
})
|
||||
|
||||
// Toggle auto-renewal
|
||||
function toggleAutoRenew() {
|
||||
// TODO: Implement API call to toggle auto-renewal
|
||||
console.log('Toggle auto-renewal:', !member.autoRenew)
|
||||
}
|
||||
|
||||
|
||||
// Get button text based on current plan
|
||||
function getPlanButtonText(plan: Plan) {
|
||||
if (!member?.active) return '选择'
|
||||
if (plan.id === currentPlan?.id) return '当前计划'
|
||||
|
||||
// Compare prices to determine upgrade/downgrade
|
||||
if (plan.price > (currentPlan?.price || 0)) return '升级'
|
||||
return '降级'
|
||||
}
|
||||
|
||||
function goPurchase(plan: Plan) {
|
||||
return router.push('/pay')
|
||||
|
||||
if (!userStore.isLogin) {
|
||||
router.push({path: '/login', query: {redirect: '/vip'}})
|
||||
return
|
||||
}
|
||||
|
||||
if (plan.id === currentPlan?.id) return
|
||||
|
||||
router.push('/user')
|
||||
}
|
||||
|
||||
let data = $ref<LevelBenefits>({} as any)
|
||||
onMounted(async () => {
|
||||
let res = await levelBenefits({levelCode: 'basic'})
|
||||
if (res.success) {
|
||||
data = res.data
|
||||
}
|
||||
})
|
||||
|
||||
const plans: Plan[] = $computed(() => {
|
||||
let list = []
|
||||
if (data?.level) {
|
||||
list.push({
|
||||
id: 'monthly',
|
||||
name: '月付',
|
||||
price: data.level.price,
|
||||
unit: '月',
|
||||
desc: '',
|
||||
},)
|
||||
list.push({
|
||||
id: 'monthly-auto',
|
||||
name: '连续包月',
|
||||
price: data.level.price_auto,
|
||||
unit: '月',
|
||||
desc: '',
|
||||
highlight: '性价比更高',
|
||||
autoRenew: true,
|
||||
},)
|
||||
list.push({
|
||||
id: 'monthly',
|
||||
name: '年度会员',
|
||||
price: data.level.yearly_price,
|
||||
unit: '年',
|
||||
highlight: '年度优惠',
|
||||
},)
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="vip-intro">
|
||||
<div class="my-5 flex justify-between">
|
||||
<div class="flex items-center text-2xl">
|
||||
<IconFluentCrown20Regular class="mr-2 text-yellow-500"/>
|
||||
<span>会员介绍</span>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<Header title="会员介绍"></Header>
|
||||
<div class="center">
|
||||
<div>
|
||||
<div class="text-lg flex items-center" v-for="f in data.benefits" :key="f.name">
|
||||
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
|
||||
<span>{{ f.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<div class="title">选择会员级别并立即开始试用。您可以随时升级、降级或取消。</div>
|
||||
<div class="subtitle">三种方案,按需选择</div>
|
||||
</div>
|
||||
|
||||
<div class="plans">
|
||||
<div v-for="p in plans" :key="p.id" class="card bg-reverse-white shadow-lg plan">
|
||||
<div>
|
||||
<div class="plan-head">
|
||||
<div class="plan-name">{{ p.name }}</div>
|
||||
<div class="price">
|
||||
<span class="amount">¥{{ p.price }}</span>
|
||||
<span class="unit">/{{ p.unit }}</span>
|
||||
</div>
|
||||
<div class="desc">{{ p.desc }}</div>
|
||||
<div v-if="p.highlight" class="tag">{{ p.highlight }}</div>
|
||||
</div>
|
||||
<div class="features">
|
||||
<div class="feature" v-for="f in p.features" :key="f">
|
||||
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
|
||||
<span>{{ f }}</span>
|
||||
</div>
|
||||
<div v-if="p.autoRenew" class="notice">
|
||||
<IconFluentArrowRepeatAll20Regular class="mr-2"/>
|
||||
开启自动续费,可在账户页随时关闭
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="p in plans" :key="p.id"
|
||||
class="card bg-reverse-white shadow-lg p-0 shadow-lg overflow-hidden flex flex-col">
|
||||
<div class="plan-name">{{ p.name }}</div>
|
||||
<div class="p-6 flex flex-col justify-between flex-1">
|
||||
<div class="plan-head">
|
||||
<div class="price">
|
||||
<span class="amount">¥{{ p.price }}</span>
|
||||
<span class="unit">/ 每{{ p.unit }}</span>
|
||||
</div>
|
||||
<div class="desc">{{ p.desc }}</div>
|
||||
<div v-if="p.highlight" class="tag">{{ p.highlight }}</div>
|
||||
</div>
|
||||
<div v-if="p.autoRenew" class="text-sm flex items-center mt-4">
|
||||
<IconFluentArrowRepeatAll20Regular class="mr-2"/>
|
||||
开启自动续费,可随时关闭
|
||||
</div>
|
||||
<BaseButton
|
||||
class="w-full mt-4"
|
||||
size="large"
|
||||
:type="p.id === currentPlan?.id ? 'primary' : 'info'"
|
||||
:disabled="p.id === currentPlan?.id"
|
||||
@click="goPurchase(p)">
|
||||
{{ getPlanButtonText(p) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseButton class="w-full mt-4" size="large" type="info" @click="goPurchase(p)">选择</BaseButton>
|
||||
<!-- Membership Status Display -->
|
||||
<div v-if="member?.active" class="card bg-green-50 border border-green-200 mt-3 mb-6 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
|
||||
<div>
|
||||
<div class="font-semibold text-green-800">当前计划:{{ member?.levelDesc }}</div>
|
||||
<div class="text-sm text-green-600">
|
||||
到期时间:{{ memberEndDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-align-end space-y-2">
|
||||
<div v-if="member.autoRenew" class="flex items-center gap-space">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<IconFluentArrowRepeatAll20Regular class="mr-1"/>
|
||||
<span>自动续费已开启</span>
|
||||
</div>
|
||||
<BaseButton
|
||||
size="small"
|
||||
type="info"
|
||||
@click="toggleAutoRenew">
|
||||
关闭
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.vip-intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.plans {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
gap: 3rem;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.plan {
|
||||
@apply flex flex-col gap-3 px-6 py-5 justify-between;
|
||||
}
|
||||
|
||||
.plan-head {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
@apply text-lg font-bold;
|
||||
@apply text-2xl font-bold bg-gray-300 px-6 py-4;
|
||||
}
|
||||
|
||||
.price {
|
||||
@@ -138,11 +208,11 @@ function goPurchase(plan: Plan) {
|
||||
}
|
||||
|
||||
.amount {
|
||||
@apply text-2xl font-500;
|
||||
@apply text-4xl font-500;
|
||||
}
|
||||
|
||||
.unit {
|
||||
@apply text-sm text-gray-500;
|
||||
@apply text-base text-gray-500;
|
||||
}
|
||||
|
||||
.desc {
|
||||
@@ -152,18 +222,5 @@ function goPurchase(plan: Plan) {
|
||||
.tag {
|
||||
@apply text-xs bg-yellow-100 text-yellow-700 px-2 py-1 rounded w-fit;
|
||||
}
|
||||
|
||||
.features {
|
||||
@apply flex flex-col gap-2 mt-2;
|
||||
}
|
||||
|
||||
.feature {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.notice {
|
||||
@apply text-xs text-gray-600 flex items-center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script setup lang="tsx">
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {onBeforeUnmount, onMounted} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { APP_NAME } from "@/config/env.ts";
|
||||
import { useUserStore } from "@/stores/auth.ts";
|
||||
import { loginApi, LoginParams, registerApi, resetPasswordApi, sendCode } from "@/apis/user.ts";
|
||||
import { accountRules, codeRules, passwordRules, phoneRules } from "@/utils/validation.ts";
|
||||
import {APP_NAME} from "@/config/env.ts";
|
||||
import {useUserStore} from "@/stores/auth.ts";
|
||||
import {loginApi, LoginParams, registerApi, resetPasswordApi, sendCode} from "@/apis/user.ts";
|
||||
import {accountRules, codeRules, passwordRules, phoneRules} from "@/utils/validation.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import Form from "@/components/base/form/Form.vue";
|
||||
import Notice from "@/pages/user/Notice.vue";
|
||||
import { FormInstance } from "@/components/base/form/types.ts";
|
||||
import { PASSWORD_CONFIG, PHONE_CONFIG } from "@/config/auth.ts";
|
||||
import { CodeType } from "@/types/types.ts";
|
||||
import {FormInstance} from "@/components/base/form/types.ts";
|
||||
import {PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts";
|
||||
import {CodeType} from "@/types/types.ts";
|
||||
import Code from "@/pages/user/Code.vue";
|
||||
import BackIcon from "@/components/BackIcon.vue";
|
||||
import { useNav } from "@/utils";
|
||||
import {useNav} from "@/utils";
|
||||
import Header from "@/components/Header.vue";
|
||||
|
||||
// 状态管理
|
||||
@@ -128,12 +128,13 @@ async function handleLogin() {
|
||||
let res = await loginApi(data as LoginParams)
|
||||
if (res.success) {
|
||||
userStore.setToken(res.data.token)
|
||||
userStore.setUser(res.data.user)
|
||||
Toast.success('登录成功')
|
||||
// 跳转到首页或用户中心
|
||||
router.push('/')
|
||||
router.back()
|
||||
} else {
|
||||
Toast.error(res.msg || '登录失败')
|
||||
if (res.code === 499) {
|
||||
loginType = 'code'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.error('登录失败,请重试')
|
||||
@@ -295,28 +296,28 @@ onBeforeUnmount(() => {
|
||||
<!-- Tab切换 -->
|
||||
<div class="center gap-8 mb-6">
|
||||
<div
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'code'"
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'code'"
|
||||
>
|
||||
<div>
|
||||
<span>验证码登录</span>
|
||||
<div
|
||||
v-opacity="loginType === 'code'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
v-opacity="loginType === 'code'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'password'"
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'password'"
|
||||
>
|
||||
<div>
|
||||
<span>密码登录</span>
|
||||
<div
|
||||
v-opacity="loginType === 'password'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
v-opacity="loginType === 'password'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,10 +325,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 验证码登录表单 -->
|
||||
<Form
|
||||
v-if="loginType === 'code'"
|
||||
ref="phoneLoginFormRef"
|
||||
:rules="phoneLoginFormRules"
|
||||
:model="phoneLoginForm">
|
||||
v-if="loginType === 'code'"
|
||||
ref="phoneLoginFormRef"
|
||||
:rules="phoneLoginFormRules"
|
||||
:model="phoneLoginForm">
|
||||
<FormItem prop="phone">
|
||||
<BaseInput v-model="phoneLoginForm.phone"
|
||||
type="tel"
|
||||
@@ -340,11 +341,11 @@ onBeforeUnmount(() => {
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="phoneLoginForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
placeholder="请输入验证码"
|
||||
v-model="phoneLoginForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
placeholder="请输入验证码"
|
||||
/>
|
||||
<Code :validate-field="() => phoneLoginFormRef.validateField('phone')"
|
||||
:type="CodeType.Login"
|
||||
@@ -355,10 +356,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 密码登录表单 -->
|
||||
<Form
|
||||
v-else
|
||||
ref="loginForm2Ref"
|
||||
:rules="loginForm2Rules"
|
||||
:model="loginForm2">
|
||||
v-else
|
||||
ref="loginForm2Ref"
|
||||
:rules="loginForm2Rules"
|
||||
:model="loginForm2">
|
||||
<FormItem prop="account">
|
||||
<BaseInput v-model="loginForm2.account"
|
||||
type="email"
|
||||
@@ -371,12 +372,12 @@ onBeforeUnmount(() => {
|
||||
<FormItem prop="password">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="loginForm2.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
v-model="loginForm2.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
@@ -387,10 +388,10 @@ onBeforeUnmount(() => {
|
||||
</Notice>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</BaseButton>
|
||||
@@ -407,27 +408,27 @@ onBeforeUnmount(() => {
|
||||
<Header @click="switchMode('login')" title="注册新账号"/>
|
||||
|
||||
<Form
|
||||
ref="registerFormRef"
|
||||
:rules="registerFormRules"
|
||||
:model="registerForm">
|
||||
ref="registerFormRef"
|
||||
:rules="registerFormRules"
|
||||
:model="registerForm">
|
||||
<FormItem prop="account">
|
||||
<BaseInput
|
||||
v-model="registerForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
v-model="registerForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="registerForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
v-model="registerForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => registerFormRef.validateField('account')"
|
||||
:type="CodeType.Register"
|
||||
@@ -436,22 +437,22 @@ onBeforeUnmount(() => {
|
||||
</FormItem>
|
||||
<FormItem prop="password">
|
||||
<BaseInput
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="confirmPassword">
|
||||
<BaseInput
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入密码"
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
@@ -459,10 +460,10 @@ onBeforeUnmount(() => {
|
||||
<Notice/>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleRegister"
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleRegister"
|
||||
>
|
||||
注册
|
||||
</BaseButton>
|
||||
@@ -474,27 +475,27 @@ onBeforeUnmount(() => {
|
||||
<Header @click="switchMode('login')" title="重置密码"/>
|
||||
|
||||
<Form
|
||||
ref="forgotFormRef"
|
||||
:rules="forgotFormRules"
|
||||
:model="forgotForm">
|
||||
ref="forgotFormRef"
|
||||
:rules="forgotFormRules"
|
||||
:model="forgotForm">
|
||||
<FormItem prop="account">
|
||||
<BaseInput
|
||||
v-model="forgotForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
v-model="forgotForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="forgotForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
v-model="forgotForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => forgotFormRef.validateField('account')"
|
||||
:type="CodeType.ResetPwd"
|
||||
@@ -503,31 +504,31 @@ onBeforeUnmount(() => {
|
||||
</FormItem>
|
||||
<FormItem prop="newPassword">
|
||||
<BaseInput
|
||||
v-model="forgotForm.newPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
v-model="forgotForm.newPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="confirmPassword">
|
||||
<BaseInput
|
||||
v-model="forgotForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入新密码"
|
||||
v-model="forgotForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<BaseButton
|
||||
class="w-full mt-2"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleForgotPassword"
|
||||
class="w-full mt-2"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleForgotPassword"
|
||||
>
|
||||
重置密码
|
||||
</BaseButton>
|
||||
@@ -538,16 +539,16 @@ onBeforeUnmount(() => {
|
||||
<div v-if="currentMode === 'login'" class="center flex-col bg-gray-100 rounded-xl px-12">
|
||||
<div class="relative w-40 h-40 bg-white rounded-xl overflow-hidden shadow-xl">
|
||||
<img
|
||||
v-if="showWechatQR"
|
||||
:src="wechatQRUrl"
|
||||
alt="微信登录二维码"
|
||||
class="w-full h-full"
|
||||
:class="{ 'opacity-30': qrStatus === 'expired' }"
|
||||
v-if="showWechatQR"
|
||||
:src="wechatQRUrl"
|
||||
alt="微信登录二维码"
|
||||
class="w-full h-full"
|
||||
:class="{ 'opacity-30': qrStatus === 'expired' }"
|
||||
/>
|
||||
<!-- 扫描成功蒙层 -->
|
||||
<div
|
||||
v-if="qrStatus === 'scanned'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
v-if="qrStatus === 'scanned'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
>
|
||||
<IconFluentCheckmarkCircle20Filled class="color-green text-4xl"/>
|
||||
<div class="text-base text-gray-700 font-medium">扫描成功</div>
|
||||
@@ -555,8 +556,8 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<!-- 取消登录蒙层 -->
|
||||
<div
|
||||
v-if="qrStatus === 'cancelled'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
v-if="qrStatus === 'cancelled'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
>
|
||||
<IconFluentErrorCircle20Regular class="color-red text-4xl"/>
|
||||
<div class="text-base text-gray-700 font-medium">你已取消此次登录</div>
|
||||
@@ -565,12 +566,12 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<!-- 过期蒙层 -->
|
||||
<div
|
||||
v-if=" qrStatus === 'expired'"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
|
||||
v-if=" qrStatus === 'expired'"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
|
||||
>
|
||||
<IconFluentArrowClockwise20Regular
|
||||
@click="refreshQRCode"
|
||||
class="cp text-4xl"/>
|
||||
@click="refreshQRCode"
|
||||
class="cp text-4xl"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-4 center gap-space">
|
||||
|
||||
@@ -13,6 +13,7 @@ import Setting from "@/pages/setting/Setting.vue";
|
||||
import Login from "@/pages/user/login.vue";
|
||||
import User from "@/pages/user/User.vue";
|
||||
import VipIntro from "@/pages/user/VipIntro.vue";
|
||||
import Pay from "@/pages/user/Pay.vue";
|
||||
// import { useAuthStore } from "@/stores/auth.ts";
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
@@ -38,6 +39,7 @@ export const routes: RouteRecordRaw[] = [
|
||||
{path: 'login', component: Login},
|
||||
{path: 'user', component: User},
|
||||
{path: 'vip', component: VipIntro},
|
||||
{path: 'pay', component: Pay},
|
||||
]
|
||||
},
|
||||
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},
|
||||
|
||||
@@ -13,6 +13,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
// 设置token
|
||||
const setToken = (newToken: string) => {
|
||||
AppEnv.TOKEN = newToken
|
||||
AppEnv.IS_LOGIN = !!AppEnv.TOKEN
|
||||
AppEnv.CAN_REQUEST = AppEnv.IS_LOGIN && AppEnv.IS_OFFICIAL
|
||||
localStorage.setItem('token', newToken)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user