save
This commit is contained in:
15
components.d.ts
vendored
15
components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
// 设置密码
|
||||
|
||||
@@ -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
21
src/components/Header.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -636,7 +636,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
<span :class="i === currentPractice.length-1 ? 'color-red':'color-gray'"
|
||||
>{{
|
||||
i === currentPractice.length - 1 ? '当前' : i + 1
|
||||
}}. {{ _dateFormat(item.startDate, 'YYYY/MM/DD HH:mm') }}</span>
|
||||
}}. {{ _dateFormat(item.startDate) }}</span>
|
||||
<span>{{ msToHourMinute(item.spend) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
169
src/pages/user/VipIntro.vue
Normal 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}
|
||||
// }
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user