This commit is contained in:
Zyronon
2025-11-14 02:00:21 +08:00
parent 0441302f88
commit 066686f024
14 changed files with 581 additions and 389 deletions

15
components.d.ts vendored
View File

@@ -29,11 +29,13 @@ declare module 'vue' {
Empty: typeof import('./src/components/Empty.vue')['default']
Form: typeof import('./src/components/base/form/Form.vue')['default']
FormItem: typeof import('./src/components/base/form/FormItem.vue')['default']
Header: typeof import('./src/components/Header.vue')['default']
IconBxVolume: typeof import('~icons/bx/volume')['default']
IconBxVolumeFull: typeof import('~icons/bx/volume-full')['default']
IconBxVolumeLow: typeof import('~icons/bx/volume-low')['default']
IconBxVolumeMute: typeof import('~icons/bx/volume-mute')['default']
IconEosIconsLoading: typeof import('~icons/eos-icons/loading')['default']
IconFluentAccessibilityQuestionMark20Regular: typeof import('~icons/fluent/accessibility-question-mark20-regular')['default']
IconFluentAdd16Regular: typeof import('~icons/fluent/add16-regular')['default']
IconFluentAdd20Regular: typeof import('~icons/fluent/add20-regular')['default']
IconFluentAddSquare20Regular: typeof import('~icons/fluent/add-square20-regular')['default']
@@ -41,6 +43,8 @@ declare module 'vue' {
IconFluentArrowCircleRight16Regular: typeof import('~icons/fluent/arrow-circle-right16-regular')['default']
IconFluentArrowClockwise20Regular: typeof import('~icons/fluent/arrow-clockwise20-regular')['default']
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
IconFluentArrowRepeatAll20Regular: typeof import('~icons/fluent/arrow-repeat-all20-regular')['default']
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
IconFluentArrowShuffle20Filled: typeof import('~icons/fluent/arrow-shuffle20-filled')['default']
@@ -48,27 +52,38 @@ declare module 'vue' {
IconFluentArrowSwap20Regular: typeof import('~icons/fluent/arrow-swap20-regular')['default']
IconFluentBookLetter20Regular: typeof import('~icons/fluent/book-letter20-regular')['default']
IconFluentBookNumber20Filled: typeof import('~icons/fluent/book-number20-filled')['default']
IconFluentCalendarDate20Regular: typeof import('~icons/fluent/calendar-date20-regular')['default']
IconFluentCalendarEmpty20Regular: typeof import('~icons/fluent/calendar-empty20-regular')['default']
IconFluentCardUi20Regular: typeof import('~icons/fluent/card-ui20-regular')['default']
IconFluentCheckmark20Regular: typeof import('~icons/fluent/checkmark20-regular')['default']
IconFluentCheckmarkCircle16Filled: typeof import('~icons/fluent/checkmark-circle16-filled')['default']
IconFluentCheckmarkCircle16Regular: typeof import('~icons/fluent/checkmark-circle16-regular')['default']
IconFluentCheckmarkCircle20Filled: typeof import('~icons/fluent/checkmark-circle20-filled')['default']
IconFluentCheckmarkCircle20Regular: typeof import('~icons/fluent/checkmark-circle20-regular')['default']
IconFluentChevronLeft20Filled: typeof import('~icons/fluent/chevron-left20-filled')['default']
IconFluentChevronLeft28Filled: typeof import('~icons/fluent/chevron-left28-filled')['default']
IconFluentCrown20Regular: typeof import('~icons/fluent/crown20-regular')['default']
IconFluentDatabasePerson20Regular: typeof import('~icons/fluent/database-person20-regular')['default']
IconFluentDelete20Regular: typeof import('~icons/fluent/delete20-regular')['default']
IconFluentDismiss20Regular: typeof import('~icons/fluent/dismiss20-regular')['default']
IconFluentDismissCircle16Regular: typeof import('~icons/fluent/dismiss-circle16-regular')['default']
IconFluentDismissCircle20Filled: typeof import('~icons/fluent/dismiss-circle20-filled')['default']
IconFluentDocumentSparkle20Regular: typeof import('~icons/fluent/document-sparkle20-regular')['default']
IconFluentErrorCircle20Filled: typeof import('~icons/fluent/error-circle20-filled')['default']
IconFluentErrorCircle20Regular: typeof import('~icons/fluent/error-circle20-regular')['default']
IconFluentEye16Regular: typeof import('~icons/fluent/eye16-regular')['default']
IconFluentEyeOff16Regular: typeof import('~icons/fluent/eye-off16-regular')['default']
IconFluentHandWave20Regular: typeof import('~icons/fluent/hand-wave20-regular')['default']
IconFluentHome20Regular: typeof import('~icons/fluent/home20-regular')['default']
IconFluentKeyboardLayoutFloat20Regular: typeof import('~icons/fluent/keyboard-layout-float20-regular')['default']
IconFluentLockClosed20Regular: typeof import('~icons/fluent/lock-closed20-regular')['default']
IconFluentMail20Regular: typeof import('~icons/fluent/mail20-regular')['default']
IconFluentMyLocation20Regular: typeof import('~icons/fluent/my-location20-regular')['default']
IconFluentNumberSymbol20Regular: typeof import('~icons/fluent/number-symbol20-regular')['default']
IconFluentPaddingLeft20Regular: typeof import('~icons/fluent/padding-left20-regular')['default']
IconFluentPayment20Regular: typeof import('~icons/fluent/payment20-regular')['default']
IconFluentPerson20Regular: typeof import('~icons/fluent/person20-regular')['default']
IconFluentPhone20Regular: typeof import('~icons/fluent/phone20-regular')['default']
IconFluentPlay20Regular: typeof import('~icons/fluent/play20-regular')['default']
IconFluentQuestionCircle20Regular: typeof import('~icons/fluent/question-circle20-regular')['default']
IconFluentReplay20Regular: typeof import('~icons/fluent/replay20-regular')['default']

View File

@@ -12,12 +12,12 @@ import { useRoute } from "vue-router";
import { DictId } from "@/types/types.ts";
import { APP_VERSION, CAN_REQUEST, LOCAL_FILE_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY } from "@/config/env.ts";
import { syncSetting } from "@/apis";
import {useAuthStore} from "@/stores/auth.ts";
import {useUserStore} from "@/stores/auth.ts";
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
const authStore = useAuthStore()
const userStore = useUserStore()
const {setTheme} = useTheme()
let lastAudioFileIdList = []
@@ -59,10 +59,10 @@ watch(settingStore.$state, (n) => {
})
async function init() {
await userStore.init()
await store.init()
await settingStore.init()
store.load = true
await authStore.init()
setTheme(settingStore.theme)

View File

@@ -1,5 +1,5 @@
import http from '@/utils/http.ts'
import {CodeType} from "@/types/types.ts";
import { CodeType } from "@/types/types.ts";
// 用户登录接口
export interface LoginParams {
@@ -10,17 +10,25 @@ export interface LoginParams {
type: 'code' | 'pwd'
}
export interface LoginResponse {
token: string
user: {
id: string
email?: string
phone?: string
nickname?: string
avatar?: string
export interface User {
id: string
email?: string
phone?: string
username?: string
avatar?: string,
hasPwd?: boolean,
member: {
level: number,
levelDesc: string,
active: boolean,
endTime: number,
autoRenew: boolean,
payMethod: number,
payMethodDesc: string,
}
}
// 用户注册接口
export interface RegisterParams {
account: string
@@ -59,7 +67,7 @@ export interface WechatLoginParams {
}
export function loginApi(params: LoginParams) {
return http<LoginResponse>('user/login', params, null, 'post')
return http<User>('user/login', params, null, 'post')
}
export function registerApi(params: RegisterParams) {
@@ -75,7 +83,7 @@ export function resetPasswordApi(params: ResetPasswordParams) {
}
export function wechatLogin(params: WechatLoginParams) {
return http<LoginResponse>('user/wechatLogin', params, null, 'post')
return http<User>('user/wechatLogin', params, null, 'post')
}
export function refreshToken() {
@@ -84,7 +92,7 @@ export function refreshToken() {
// 获取用户信息
export function getUserInfo() {
return http<LoginResponse['user']>('user/userInfo', null, null, 'get')
return http<User>('user/userInfo', null, null, 'get')
}
// 设置密码

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import BaseIcon from "@/components/BaseIcon.vue";
import {useAttrs} from "vue";
import router from "@/router.ts";
import { useAttrs } from "vue";
import { useNav } from "@/utils";
const attrs = useAttrs()
const router = useNav()
function onClick() {
if (!attrs.onClick) {

21
src/components/Header.vue Normal file
View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import BackIcon from "@/components/BackIcon.vue";
import { useAttrs } from "vue";
defineProps<{
title: string;
}>()
const attrs = useAttrs()
</script>
<template>
<div class="mb-3 text-xl font-bold relative">
<BackIcon class="z-2 relative" v-bind="attrs"/>
<span class="absolute text-center w-full left-0" @click.stop>{{ title }}</span>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -225,7 +225,7 @@ function next() {
<div
class="item border border-item border-solid mt-2 p-2 bg-[var(--bg-history)] rounded-md flex justify-between"
v-for="i in currentPractice">
<span class="color-gray">{{ _dateFormat(i.startDate, 'YYYY/MM/DD HH:mm') }}</span>
<span class="color-gray">{{ _dateFormat(i.startDate) }}</span>
<span>{{ msToHourMinute(i.spend) }}</span>
</div>
</div>

View File

@@ -636,7 +636,7 @@ const currentPractice = inject('currentPractice', [])
<span :class="i === currentPractice.length-1 ? 'color-red':'color-gray'"
>{{
i === currentPractice.length - 1 ? '当前' : i + 1
}}.&nbsp;&nbsp;{{ _dateFormat(item.startDate, 'YYYY/MM/DD HH:mm') }}</span>
}}.&nbsp;&nbsp;{{ _dateFormat(item.startDate) }}</span>
<span>{{ msToHourMinute(item.spend) }}</span>
</div>
</div>

View File

@@ -31,7 +31,7 @@ import Textarea from "@/components/base/Textarea.vue";
import SettingItem from "@/pages/setting/SettingItem.vue";
import {get, set} from "idb-keyval";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useAuthStore} from "@/stores/auth.ts";
import {useUserStore} from "@/stores/auth.ts";
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
@@ -41,7 +41,7 @@ const tabIndex = $ref(0)
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
const authStore = useAuthStore()
const userStore = useUserStore()
//@ts-ignore
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
@@ -793,22 +793,22 @@ function importOldData() {
<h1>Type Words</h1>
<!-- 用户信息部分 -->
<div v-if="authStore.isLoggedIn && authStore.user" class="user-info-section mb-6">
<div v-if="userStore.isLoggedIn && userStore.user" class="user-info-section mb-6">
<div class="user-avatar mb-4">
<img v-if="authStore.user.avatar" :src="authStore.user.avatar" alt="头像" class="avatar-img"/>
<img v-if="userStore.user.avatar" :src="userStore.user.avatar" alt="头像" class="avatar-img"/>
<div v-else class="avatar-placeholder">
{{ authStore.user.nickname?.charAt(0) || 'U' }}
{{ userStore.user.nickname?.charAt(0) || 'U' }}
</div>
</div>
<h3 class="mb-2">{{ authStore.user.nickname || '用户' }}</h3>
<p v-if="authStore.user.email" class="text-sm color-gray mb-1">{{ authStore.user.email }}</p>
<p v-if="authStore.user.phone" class="text-sm color-gray">{{ authStore.user.phone }}</p>
<h3 class="mb-2">{{ userStore.user.nickname || '用户' }}</h3>
<p v-if="userStore.user.email" class="text-sm color-gray mb-1">{{ userStore.user.email }}</p>
<p v-if="userStore.user.phone" class="text-sm color-gray">{{ userStore.user.phone }}</p>
<BaseButton
@click="authStore.logout"
@click="userStore.logout"
type="info"
class="mt-4"
:loading="authStore.isLoading"
:loading="userStore.isLoading"
>
退出登录
</BaseButton>

View File

@@ -1,41 +1,27 @@
<script setup lang="ts">
import {computed, ref} from 'vue'
import {Calendar, CreditCard, Crown} from 'lucide-vue-next'
import {useAuthStore} 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 } 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, validatePhone} from "@/utils/validation.ts";
import {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 authStore = useAuthStore()
const userStore = useUserStore()
const router = useRouter()
// Check login state
const isLoggedIn = computed(() => authStore.isLogin)
// Mock subscription data (you can replace with real data from your API)
const subscriptionData = ref({
plan: 'Premium',
status: 'active',
expiresAt: '2025-12-31',
autoRenew: true,
paymentMethod: '信用卡 ****1234'
})
// UI state
let showChangePwd = $ref(false)
let showChangeEmail = $ref(false)
let showChangeUsername = $ref(false)
@@ -43,7 +29,7 @@ let showChangePhone = $ref(false)
let loading = $ref(false)
const handleLogout = () => {
authStore.logout()
userStore.logout()
router.push('/login')
}
@@ -51,10 +37,13 @@ const contactSupport = () => {
console.log('Contact support')
}
const leaveTrustpilotReview = () => {
const goIssues = () => {
window.open(GITHUB + '/issues', '_blank')
}
onMounted(() => {
userStore.fetchUserInfo()
})
// 修改手机号
// 修改手机号
@@ -66,7 +55,7 @@ let changePhoneFormRules = {
oldCode: codeRules,
phone: [...phoneRules, {
validator: (rule: any, value: any) => {
if (authStore.user?.phone && value === authStore.user?.phone) {
if (userStore.user?.phone && value === userStore.user?.phone) {
throw new Error('新手机号与原手机号一致')
}
}, trigger: 'blur'
@@ -89,7 +78,7 @@ function changePhone() {
const res = await changePhoneApi(changePhoneForm)
if (res.success) {
Toast.success('修改成功')
await authStore.fetchUserInfo()
await userStore.fetchUserInfo()
showChangePhone = false
} else {
Toast.error(res.msg || '修改失败')
@@ -115,7 +104,7 @@ let changeUsernameFormRules = {
function showChangeUsernameForm() {
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
showChangeUsername = true
changeUsernameForm = cloneDeep({username: authStore.user?.username ?? '',})
changeUsernameForm = cloneDeep({username: userStore.user?.username ?? '',})
}
function changeUsername() {
@@ -126,7 +115,7 @@ function changeUsername() {
const res = await updateUserInfoApi(changeUsernameForm)
if (res.success) {
Toast.success('修改成功')
await authStore.fetchUserInfo()
await userStore.fetchUserInfo()
showChangeUsername = false
} else {
Toast.error(res.msg || '修改失败')
@@ -154,7 +143,7 @@ let changeEmailFormRules = {
email: [
...emailRules, {
validator: (rule: any, value: any) => {
if (authStore.user?.email && value === authStore.user?.email) {
if (userStore.user?.email && value === userStore.user?.email) {
throw new Error('该邮箱与当前一致')
}
}, trigger: 'blur'
@@ -167,7 +156,7 @@ let changeEmailFormRules = {
function showChangeEmailForm() {
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
showChangeEmail = true
changeEmailForm = cloneDeep({email: authStore.user?.email ?? '', pwd: '', code: '',})
changeEmailForm = cloneDeep({email: userStore.user?.email ?? '', pwd: '', code: '',})
}
function changeEmail() {
@@ -178,7 +167,7 @@ function changeEmail() {
const res = await changeEmailApi(changeEmailForm)
if (res.success) {
Toast.success('修改成功')
await authStore.fetchUserInfo()
await userStore.fetchUserInfo()
showChangeEmail = false
} else {
Toast.error(res.msg || '修改失败')
@@ -232,7 +221,7 @@ function changePwd() {
if (res.success) {
Toast.success('密码设置成功,请重新登录')
showChangePwd = false
authStore.logout()
userStore.logout()
} else {
Toast.error(res.msg || '设置失败')
}
@@ -245,28 +234,47 @@ function changePwd() {
})
}
// 订阅相关
const memberEndtime = $computed(() => {
if (userStore.user?.member) {
if (userStore.user?.member?.endTime === -1) return '永久'
else return _dateFormat(userStore.user?.member?.endTime)
}
return ''
})
function subscribe() {
router.push('/vip')
}
</script>
<template>
<BasePage>
<!-- Unauthenticated View -->
<div v-if="!isLoggedIn" class="center h-screen">
<div class="card shadow-lg text-center flex-col gap-6 w-100 ">
<div v-if="!userStore.isLogin" class="center h-screen">
<div class="card bg-white shadow-lg text-center flex-col gap-6 w-110">
<div class="w-20 h-20 bg-blue-100 rounded-full center mx-auto">
<IconFluentPerson20Regular class="text-3xl text-blue-600"/>
</div>
<h1 class="text-2xl font-bold">欢迎使用</h1>
<p class="">请登录以管理您的账户</p>
<h1 class="text-2xl font-bold">
<IconFluentHandWave20Regular class="text-xl translate-y-1 mr-2 shrink-0"/>
<span>欢迎使用</span>
</h1>
<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>
<p class="text-sm text-gray-500">
还没有账户
<router-link to="/login" class="line">立即注册</router-link>
<router-link to="/login?register=1" class="line">立即注册</router-link>
</p>
</div>
</div>
@@ -274,16 +282,17 @@ function changePwd() {
<!-- Authenticated View -->
<div v-else class="w-full flex gap-4">
<!-- Main Account Settings -->
<div class="card flex-1 flex flex-col gap-2 px-6">
<!-- todo 夜间背景色-->
<div class="card bg-reverse-white shadow-lg flex-1 flex flex-col gap-2 px-6">
<h1 class="text-2xl font-bold mt-0">帐户</h1>
<!-- 用户名-->
<div class="item">
<div class="flex-1">
<div class="mb-2">用户名</div>
<div class="flex items-center gap-2" v-if="authStore.user?.username">
<div class="flex items-center gap-2" v-if="userStore.user?.username">
<IconFluentPerson20Regular class="text-base"/>
<span>{{ authStore.user?.username }}</span>
<span>{{ userStore.user?.username }}</span>
</div>
<div v-else class="text-xs">在此设置用户名</div>
</div>
@@ -293,16 +302,16 @@ function changePwd() {
</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"/>
@@ -321,9 +330,9 @@ function changePwd() {
<div class="item">
<div class="flex-1">
<div class="mb-2">手机号</div>
<div class="flex items-center gap-2" v-if="authStore.user?.phone">
<div class="flex items-center gap-2" v-if="userStore.user?.phone">
<IconFluentMail20Regular class="text-base"/>
<span>{{ authStore.user?.phone }}</span>
<span>{{ userStore.user?.phone }}</span>
</div>
<div v-else class="text-xs">在此设置手机号</div>
</div>
@@ -333,57 +342,57 @@ function changePwd() {
</div>
<div v-if="showChangePhone">
<Form
ref="changePhoneFormRef"
:rules="changePhoneFormRules"
:model="changePhoneForm">
<FormItem prop="oldCode" v-if="authStore.user?.phone">
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"
:val="authStore.user.phone"/>
:val="userStore.user.phone"/>
</div>
</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"
:val="changePhoneForm.phone"/>
</div>
</FormItem>
<FormItem prop="pwd" v-if="!authStore.user?.phone">
<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>
<div class="flex justify-between items-end mb-2">
<span class="link text-sm cp"
@click="MessageBox.notice(`请提供证明信息发送邮件到 ${EMAIL} 进行申诉`,'人工申诉')"
v-if="authStore.user?.phone">原手机号不可用,点此申诉</span>
v-if="userStore.user?.phone">原手机号不可用,点此申诉</span>
<span v-else></span>
<div>
<BaseButton type="info" @click="showChangePhone = false">取消</BaseButton>
@@ -397,9 +406,9 @@ function changePwd() {
<div class="item">
<div class="flex-1">
<div class="mb-2">电子邮箱</div>
<div class="flex items-center gap-2" v-if="authStore.user?.email">
<div class="flex items-center gap-2" v-if="userStore.user?.email">
<IconFluentMail20Regular class="text-base"/>
<span>{{ authStore.user?.email }}</span>
<span>{{ userStore.user?.email }}</span>
</div>
<div v-else class="text-xs">在此设置邮箱</div>
</div>
@@ -409,37 +418,37 @@ function changePwd() {
</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"
:val="changeEmailForm.email"/>
</div>
</FormItem>
<FormItem prop="pwd" v-if="authStore.user?.hasPwd">
<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>
@@ -458,42 +467,42 @@ function changePwd() {
<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">
<FormItem prop="oldPwd" v-if="authStore.user.hasPwd">
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>
@@ -507,17 +516,17 @@ function changePwd() {
<!-- Contact Support -->
<div class="item cp"
v-if="false"
@click="contactSupport">
<div class="flex-1">
联系 {{ APP_NAME }} 客服
</div>
<IconFluentChevronLeft28Filled class="rotate-180"/>
</div>
<div class="line"></div>
<!-- <div class="line"></div>-->
<!-- Trustpilot Review -->
<div class="item cp"
@click="leaveTrustpilotReview">
@click="goIssues">
<div class="flex-1">
给 {{ APP_NAME }} 提交意见
</div>
@@ -528,9 +537,9 @@ function changePwd() {
<!-- 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>
@@ -544,59 +553,65 @@ function changePwd() {
</div>
<!-- Subscription Information -->
<div class="card w-80">
<!-- todo 夜间背景色-->
<div class="card bg-reverse-white shadow-lg w-80">
<div class="flex items-center gap-3 mb-4">
<Crown class="w-6 h-6 text-yellow-500"/>
<h2 class="text-lg font-bold text-gray-900">订阅信息</h2>
<IconFluentCrown20Regular class="text-2xl text-yellow-500"/>
<div class="text-lg font-bold">订阅信息</div>
</div>
<div class="space-y-4">
<div>
<div class="text-sm text-gray-500 mb-1">当前计划</div>
<div class="text-lg font-semibold text-gray-900">{{ subscriptionData.plan }}</div>
</div>
<div>
<div class="text-sm text-gray-500 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-sm font-medium text-green-700">{{
subscriptionData.status === 'active' ? '活跃' : '已过期'
}}</span>
<template v-if="userStore.user?.member">
<div>
<div class="mb-1">当前计划</div>
<div class="text-base font-bold">{{ userStore.user?.member?.levelDesc }}</div>
</div>
</div>
<div>
<div class="text-sm text-gray-500 mb-1">到期时间</div>
<div class="flex items-center gap-2">
<Calendar class="w-4 h-4 text-gray-400"/>
<span class="text-sm font-medium text-gray-900">{{ subscriptionData.expiresAt }}</span>
<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 ? '使用中' : '已过期' }}
</span>
</div>
</div>
</div>
<div>
<div class="text-sm text-gray-500 mb-1">自动续费</div>
<div class="flex items-center gap-2">
<div class="w-2 h-2" :class="subscriptionData.autoRenew ? 'bg-blue-500' : 'bg-gray-400'"
rounded-full></div>
<span class="text-sm font-medium"
:class="subscriptionData.autoRenew ? 'text-blue-700' : 'text-gray-600'">
{{ subscriptionData.autoRenew ? '已开启' : '已关闭' }}
<div>
<div class="mb-1">到期时间</div>
<div class="flex items-center gap-2">
<IconFluentCalendarDate20Regular class="text-lg"/>
<span class="text-base font-medium">{{ memberEndtime }}</span>
</div>
</div>
<div>
<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'"
></div>
<span class="text-base font-medium"
:class="userStore.user?.member?.autoRenew ? 'text-blue-700' : 'text-gray-600'">
{{ userStore.user?.member?.autoRenew ? '已开启' : '已关闭' }}
</span>
</div>
</div>
</div>
<div>
<div class="text-sm text-gray-500 mb-1">付款方式</div>
<div class="flex items-center gap-2">
<CreditCard class="w-4 h-4 text-gray-400"/>
<span class="text-sm font-medium text-gray-900">{{ subscriptionData.paymentMethod }}</span>
<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>
</div>
</template>
<div class="pt-4 border-t border-gray-200">
<BaseButton class="w-full">管理订阅</BaseButton>
</div>
<div class="text-base" v-else>当前无订阅</div>
<BaseButton class="w-full" size="large" @click="subscribe">{{ userStore.user?.member ? '管理订阅' : '会员介绍' }}
</BaseButton>
</div>
</div>
</div>

169
src/pages/user/VipIntro.vue Normal file
View File

@@ -0,0 +1,169 @@
<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'
const router = useRouter()
const userStore = useUserStore()
interface Plan {
id: string
name: string
price: number
unit: '月' | '年'
desc: string
highlight?: string
autoRenew?: boolean
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: [
]
}
]
function goPurchase(plan: Plan) {
if (!userStore.isLogin) {
router.push({path: '/login', query: {redirect: '/vip'}})
return
}
router.push('/user')
}
</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>
<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>
<BaseButton class="w-full mt-4" size="large" type="info" @click="goPurchase(p)">选择</BaseButton>
</div>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
.vip-intro {
display: flex;
flex-direction: column;
gap: 1rem;
}
.plans {
display: grid;
gap: 1rem;
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;
}
.price {
@apply flex items-end gap-1;
}
.amount {
@apply text-2xl font-500;
}
.unit {
@apply text-sm text-gray-500;
}
.desc {
@apply text-sm text-gray-600;
}
.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>

View File

@@ -1,30 +1,32 @@
<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 {useAuthStore} from "@/stores/auth.ts";
import {loginApi, LoginParams, registerApi, resetPasswordApi, sendCode} from "@/apis/user.ts";
import {accountRules, codeRules, passwordRules, phoneRules, validateEmail, validatePhone} 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 router from "@/router.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 Header from "@/components/Header.vue";
// 状态管理
const authStore = useAuthStore()
const userStore = useUserStore()
const route = useRoute()
const router = useNav()
// 页面状态
let currentMode = $ref<'login' | 'register' | 'forgot'>('login')
let loginType = $ref<'code' | 'password'>('code') // 默认验证码登录
let isSendingCode = $ref(false)
let loading = $ref(false)
let codeCountdown = $ref(0)
let showWechatQR = $ref(true)
@@ -109,33 +111,6 @@ const currentFormRef = $computed<FormInstance>(() => {
else return forgotFormRef
})
// 发送验证码
async function sendVerificationCode(val: string, type: CodeType, fileName: string) {
let res = currentFormRef.validateField(fileName)
if (res) {
try {
isSendingCode = true
const res = await sendCode({val, type})
if (res.success) {
codeCountdown = PHONE_CONFIG.sendInterval
const timer = setInterval(() => {
codeCountdown--
if (codeCountdown <= 0) {
clearInterval(timer)
}
}, 1000)
} else {
Toast.error(res.msg || '发送失败')
}
} catch (error) {
console.error('Send code error:', error)
Toast.error('发送验证码失败')
} finally {
isSendingCode = false
}
}
}
// 统一登录处理
async function handleLogin() {
currentFormRef.validate(async (valid) => {
@@ -152,8 +127,8 @@ async function handleLogin() {
}
let res = await loginApi(data as LoginParams)
if (res.success) {
authStore.setToken(res.data.token)
authStore.setUser(res.data.user)
userStore.setToken(res.data.token)
userStore.setUser(res.data.user)
Toast.success('登录成功')
// 跳转到首页或用户中心
router.push('/')
@@ -176,8 +151,8 @@ async function handleRegister() {
loading = true
let res = await registerApi(registerForm)
if (res.success) {
authStore.setToken(res.data.token)
authStore.setUser(res.data.user)
userStore.setToken(res.data.token)
userStore.setUser(res.data.user)
Toast.success('注册成功')
// 跳转到首页或用户中心
router.push('/')
@@ -288,15 +263,15 @@ function switchMode(mode: 'login' | 'register' | 'forgot') {
// 用户主动取消登录(示例:可在需要的地方调用)
function cancelWechatLogin() {
qrStatus = 'cancelled'
qrStatus = 'cancelled'
qrStatus = 'cancelled'
}
// 初始化页面
onMounted(() => {
// 检查是否有重定向地址
const redirect = route.query.redirect as string
if (redirect) {
// 如果有重定向地址,可以显示提示信息
Toast.info('请先登录后再访问该页面')
console.log('route.query', route.query)
if (route.query?.register) {
currentMode = 'register'
}
})
@@ -312,7 +287,7 @@ onBeforeUnmount(() => {
<!-- 登录区域容器 - 弹框形式 -->
<div class="flex gap-2">
<!-- 左侧登录区域 -->
<div class="flex-1 w-80 p-6">
<div class="flex-1 w-80 p-3">
<!-- 登录选项 -->
<div v-if="currentMode === 'login'">
<div class="mb-6 text-center text-2xl font-bold">{{ APP_NAME }}</div>
@@ -320,28 +295,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>
@@ -349,10 +324,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"
@@ -365,11 +340,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"
@@ -380,10 +355,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"
@@ -396,12 +371,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>
@@ -412,10 +387,10 @@ onBeforeUnmount(() => {
</Notice>
<BaseButton
class="w-full"
size="large"
:loading="loading"
@click="handleLogin"
class="w-full"
size="large"
:loading="loading"
@click="handleLogin"
>
登录
</BaseButton>
@@ -429,29 +404,30 @@ onBeforeUnmount(() => {
<!-- 注册模式 -->
<div v-else-if="currentMode === 'register'">
<div class="mb-6 text-xl font-bold text-center">注册新账号</div>
<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"
@@ -460,22 +436,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>
@@ -483,45 +459,42 @@ onBeforeUnmount(() => {
<Notice/>
<BaseButton
class="w-full"
size="large"
:loading="loading"
@click="handleRegister"
class="w-full"
size="large"
:loading="loading"
@click="handleRegister"
>
注册
</BaseButton>
<div class="mt-4 text-center">
<div class="color-link cp hover:opacity-80 text-sm" @click="switchMode('login')">返回登录
</div>
</div>
</div>
<!-- 忘记密码模式 -->
<div v-else-if="currentMode === 'forgot'">
<div class="mb-6 text-xl font-bold text-center">重置密码</div>
<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"
@@ -530,39 +503,34 @@ 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>
<div class="mt-4 text-center">
<div class="color-link cp hover:opacity-80 text-sm" @click="switchMode('login')">返回登录
</div>
</div>
</div>
</div>
@@ -570,16 +538,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>
@@ -587,8 +555,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>
@@ -597,12 +565,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">
@@ -614,5 +582,6 @@ onBeforeUnmount(() => {
</div>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -12,6 +12,7 @@ import BookList from "@/pages/article/BookList.vue";
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 { useAuthStore } from "@/stores/auth.ts";
export const routes: RouteRecordRaw[] = [
@@ -36,6 +37,7 @@ export const routes: RouteRecordRaw[] = [
{path: 'setting', component: Setting},
{path: 'login', component: Login},
{path: 'user', component: User},
{path: 'vip', component: VipIntro},
]
},
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},
@@ -61,7 +63,7 @@ const router = VueRouter.createRouter({
router.beforeEach(async (to: any, from: any) => {
return true
// const authStore = useAuthStore()
// const userStore = useAuthStore()
//
// // 公共路由,不需要登录验证
// const publicRoutes = ['/login', '/wechat/callback', '/user-agreement', '/privacy-policy']
@@ -72,9 +74,9 @@ router.beforeEach(async (to: any, from: any) => {
// }
//
// // 如果用户未登录,跳转到登录页
// if (!authStore.isLoggedIn) {
// if (!userStore.isLoggedIn) {
// // 尝试初始化认证状态
// const isInitialized = await authStore.initAuth()
// const isInitialized = await userStore.initAuth()
// if (!isInitialized) {
// return {path: '/login', query: {redirect: to.fullPath}}
// }

View File

@@ -1,20 +1,12 @@
import {defineStore} from 'pinia'
import {computed, ref} from 'vue'
import {getUserInfo} from '@/apis/user.ts'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { getUserInfo, User } from '@/apis/user.ts'
import Toast from '@/components/base/toast/Toast.ts'
import router from '@/router.ts'
import {AppEnv} from "@/config/env.ts";
import { AppEnv } from "@/config/env.ts";
export interface User {
id: string
email?: string
phone?: string
username?: string
avatar?: string,
hasPwd?: boolean
}
export const useAuthStore = defineStore('auth', () => {
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const isLogin = computed(() => AppEnv.IS_LOGIN)

View File

@@ -1,14 +1,14 @@
import {BaseState, DefaultBaseState, useBaseStore} from "@/stores/base.ts";
import {getDefaultSettingState, SettingState} from "@/stores/setting.ts";
import {Dict, DictId, DictResource, DictType} from "@/types/types.ts";
import {useRouter} from "vue-router";
import {useRuntimeStore} from "@/stores/runtime.ts";
import { BaseState, DefaultBaseState, useBaseStore } from "@/stores/base.ts";
import { getDefaultSettingState, SettingState } from "@/stores/setting.ts";
import { Dict, DictId, DictResource, DictType } from "@/types/types.ts";
import { useRouter } from "vue-router";
import { useRuntimeStore } from "@/stores/runtime.ts";
import dayjs from 'dayjs'
import axios from "axios";
import {ENV, IS_OFFICIAL, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/config/env.ts";
import {nextTick} from "vue";
import { ENV, IS_OFFICIAL, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY } from "@/config/env.ts";
import { nextTick } from "vue";
import Toast from '@/components/base/toast/Toast.ts'
import {getDefaultDict, getDefaultWord} from "@/types/func.ts";
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
import duration from "dayjs/plugin/duration";
dayjs.extend(duration);
@@ -138,10 +138,10 @@ export function useNav() {
router.push({path, query})
}
return {nav, back: router.back}
return {nav, push: nav, back: router.back}
}
export function _dateFormat(val: any, format?: string): string {
export function _dateFormat(val: any, format: string = 'YYYY/MM/DD HH:mm'): string {
if (!val) return
if (String(val).length === 10) {
val = val * 1000