This commit is contained in:
Zyronon
2025-11-13 02:19:42 +08:00
parent 9585031e64
commit 04136db975
9 changed files with 220 additions and 22897 deletions

1
components.d.ts vendored
View File

@@ -65,6 +65,7 @@ declare module 'vue' {
IconFluentEyeOff16Regular: typeof import('~icons/fluent/eye-off16-regular')['default']
IconFluentHome20Regular: typeof import('~icons/fluent/home20-regular')['default']
IconFluentKeyboardLayoutFloat20Regular: typeof import('~icons/fluent/keyboard-layout-float20-regular')['default']
IconFluentMail20Regular: typeof import('~icons/fluent/mail20-regular')['default']
IconFluentMyLocation20Regular: typeof import('~icons/fluent/my-location20-regular')['default']
IconFluentPaddingLeft20Regular: typeof import('~icons/fluent/padding-left20-regular')['default']
IconFluentPerson20Regular: typeof import('~icons/fluent/person20-regular')['default']

22687
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"dayjs": "^1.11.13",
"file-saver": "^2.0.5",
"idb-keyval": "^6.2.2",
"lucide-vue-next": "^0.553.0",
"md5": "^2.2.1",
"mitt": "^3.0.1",
"nanoid": "^5.1.5",

12
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
idb-keyval:
specifier: ^6.2.2
version: 6.2.2
lucide-vue-next:
specifier: ^0.553.0
version: 0.553.0(vue@3.5.18(typescript@5.9.2))
md5:
specifier: ^2.2.1
version: 2.3.0
@@ -2594,6 +2597,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-vue-next@0.553.0:
resolution: {integrity: sha512-0tg9XT+VCElTT+7EXXbBRhWe1nU7Doa32Xv/dHP5/LCleFVgV6cAqziM3C7AetqmsYIsfAtNwRYdtvs4Ds7aUg==}
peerDependencies:
vue: '>=3.0.1'
magic-string-ast@0.7.1:
resolution: {integrity: sha512-ub9iytsEbT7Yw/Pd29mSo/cNQpaEu67zR1VVcXDiYjSFwzeBxNdTd0FMnSslLQXiRj8uGPzwsaoefrMD5XAmdw==}
engines: {node: '>=16.14.0'}
@@ -6504,6 +6512,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-vue-next@0.553.0(vue@3.5.18(typescript@5.9.2)):
dependencies:
vue: 3.5.18(typescript@5.9.2)
magic-string-ast@0.7.1:
dependencies:
magic-string: 0.30.17

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 {
@@ -86,3 +86,8 @@ export function refreshToken() {
export function getUserInfo() {
return http<LoginResponse['user']>('user/userInfo', null, null, 'get')
}
// 设置密码
export function setPassword(password: string) {
return http('user/setPassword', {password}, null, 'post')
}

View File

@@ -219,12 +219,12 @@ a {
text-decoration: none;
}
.link{
.link {
color: var(--color-link);
@apply hover:opacity-80;
}
.cp{
.cp {
@apply cursor-pointer;
}
@@ -422,6 +422,8 @@ a {
.line {
width: 100%;
border-bottom: 1px solid var(--color-item-border);
@apply hover:text-blue-700;
}
.line-white {

View File

@@ -62,7 +62,7 @@ defineEmits(['click'])
color: white;
& + .base-button {
margin-left: var(--space);
margin-left: 1rem;
}
.loading {

View File

@@ -33,7 +33,7 @@ const props = defineProps({
},
});
const emit = defineEmits(['update:modelValue', 'input', 'change', 'focus', 'blur', 'validation']);
const emit = defineEmits(['update:modelValue', 'input', 'change', 'focus', 'blur', 'validation', 'enter']);
const attrs = useAttrs();
const inputValue = ref(props.modelValue);
@@ -78,6 +78,10 @@ const onBlur = (e: FocusEvent) => {
emit('blur', e);
};
const onEnter = (e: KeyboardEvent) => {
emit('enter', e);
};
const clearInput = () => {
inputValue.value = '';
emit('update:modelValue', '');
@@ -112,6 +116,7 @@ const vFocus = {
@change="onChange"
@focus="onFocus"
@blur="onBlur"
@keydown.enter="onEnter"
class="inner"
v-focus="autofocus"
:maxlength="maxLength"

View File

@@ -1,12 +1,14 @@
<script setup lang="ts">
import {computed, ref} from 'vue'
import {Calendar, ChevronRight, CreditCard, Crown, Mail, User} from 'lucide-vue-next'
import {useAuthStore} from '@/stores/auth.ts'
import {useRouter} from 'vue-router'
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 BaseInput from '@/components/base/BaseInput.vue'
import BasePage from "@/components/BasePage.vue";
import {APP_NAME, GITHUB} from "@/config/env.ts";
import { APP_NAME, GITHUB } from "@/config/env.ts";
import BaseButton from "@/components/BaseButton.vue";
import { PASSWORD_CONFIG } from "@/config/auth.ts";
import { setPassword } from "@/apis/user.ts";
const authStore = useAuthStore()
const router = useRouter()
@@ -33,11 +35,6 @@ const isEditingUsername = ref(false)
const isEditingEmail = ref(false)
const showPasswordSection = ref(false)
// Handlers
const handleLogin = () => {
router.push('/login')
}
const handleLogout = () => {
authStore.logout()
router.push('/login')
@@ -82,227 +79,214 @@ const contactSupport = () => {
const leaveTrustpilotReview = () => {
window.open(GITHUB + '/issues', '_blank')
}
async function changePassword(e) {
let res = await setPassword(e.target.value)
//todo
}
</script>
<template>
<BasePage>
<!-- Unauthenticated View -->
<div v-if="!isLoggedIn" class="w-full max-w-md">
<div class="bg-white rounded-2xl shadow-lg p-8 text-center">
<div class="mb-8">
<div class="w-20 h-20 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
<User class="w-10 h-10 text-blue-600"/>
</div>
<h1 class="text-2xl font-bold text-gray-900 mb-2">欢迎使用</h1>
<p class="text-gray-600">请登录以管理您的账户</p>
<div v-if="isLoggedIn" class="center h-screen">
<div class="card shadow-lg text-center flex-col gap-6 w-100 ">
<div class="w-20 h-20 bg-blue-100 rounded-full center mx-auto">
<IconFluentPerson20Regular class="text-3xl text-blue-600"/>
</div>
<button
@click="handleLogin"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-6 rounded-xl transition-colors duration-200 mb-4"
<h1 class="text-2xl font-bold">欢迎使用</h1>
<p class="">请登录以管理您的账户</p>
<BaseButton
@click="router.push('/login')"
size="large"
class="w-full mt-4"
>
登录
</button>
</BaseButton>
<p class="text-sm text-gray-500">
还没有账户
<a href="#" class="text-blue-600 hover:text-blue-700 font-medium">立即注册</a>
<router-link to="/login" class="line">立即注册</router-link>
</p>
</div>
</div>
<!-- Authenticated View -->
<div v-else class="w-full max-w-4xl">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Account Settings -->
<div class="lg:col-span-2">
<div class="card">
<!-- Header -->
<div class="px-6 border-b border-gray-200">
<h1 class="text-xl font-bold text-gray-900">帐户</h1>
</div>
<!-- Username Section -->
<div class="px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">用户名</div>
<div class="flex items-center gap-3">
<User class="w-4 h-4 text-gray-500"/>
<BaseInput
v-if="isEditingUsername"
v-model="username"
type="text"
size="normal"
@blur="saveUsername"
@keyup.enter="saveUsername"
class="flex-1 max-w-xs"
autofocus
/>
<span v-else class="text-gray-900">{{ username }}</span>
</div>
</div>
<IconFluentTextEditStyle20Regular
@click="isEditingUsername ? saveUsername() : editUsername()"
class="text-xl"/>
</div>
<div class="border-t border-gray-200"></div>
<!-- Email Section -->
<div class="px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">电子邮箱</div>
<div class="flex items-center gap-3">
<Mail class="w-4 h-4 text-gray-500"/>
<BaseInput
v-if="isEditingEmail"
v-model="email"
type="email"
size="normal"
@blur="saveEmail"
@keyup.enter="saveEmail"
class="flex-1 max-w-xs"
autofocus
/>
<span v-else class="text-gray-900">{{ email }}</span>
</div>
</div>
<IconFluentTextEditStyle20Regular
@click="isEditingEmail ? saveEmail() : editEmail()"
class="text-xl"/>
</div>
<div class="border-t border-gray-200"></div>
<!-- Password Section -->
<div class="px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">设置密码</div>
<div class="text-sm text-gray-500">在此输入密码</div>
</div>
<IconFluentChevronLeft28Filled @click="showPasswordSection = !showPasswordSection"
class="transition-transform"
:class="['rotate-270','rotate-180'][showPasswordSection?0:1]"/>
</div>
<div class="border-t border-gray-200"></div>
<!-- Notification Toggle -->
<div class="px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">同意接收优惠信息</div>
<div class="text-sm text-gray-500">第一时间掌握 Lingvist 的各种优惠及最新消息</div>
</div>
<button
@click="toggleNotifications"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="receiveNotifications ? 'bg-blue-600' : 'bg-gray-200'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="receiveNotifications ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<div class="border-t border-gray-200"></div>
<!-- Contact Support -->
<div class="px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors cursor-pointer"
@click="contactSupport">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">联系{{ APP_NAME }}客服</div>
</div>
<ChevronRight class="w-5 h-5 text-gray-400"/>
</div>
<div class="border-t border-gray-200"></div>
<!-- Trustpilot Review -->
<div class="px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors cursor-pointer"
@click="leaveTrustpilotReview">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1"> {{ APP_NAME }} 上留下评论</div>
</div>
<ChevronRight class="w-5 h-5 text-gray-400"/>
</div>
<!-- Logout Button -->
<div class="px-6 py-6 border-t border-gray-200">
<button
@click="handleLogout"
class="w-full bg-gray-800 hover:bg-gray-900 text-white font-semibold py-3 px-6 rounded-xl transition-colors duration-200"
>
登出
</button>
</div>
<!-- Footer Links -->
<div class="px-6 py-4 border-t border-gray-200 text-center">
<div class="text-sm text-gray-500">
<a href="/user-agreement.html" class="text-gray-500 hover:text-gray-700 underline">用户协议</a>
<a href="/privacy-policy.html" class="text-gray-500 hover:text-gray-700 underline">隐私政策</a>
</div>
<div v-else class="w-full flex items-start gap-4">
<!-- Main Account Settings -->
<div class="card flex-1 flex flex-col gap-2 px-8">
<h1 class="text-xl font-bold">帐户</h1>
<!-- Username Section -->
<div class="flex items-center justify-between ">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">用户名</div>
<div class="flex items-center gap-2">
<IconFluentPerson20Regular class="text-base text-gray-500"/>
<BaseInput
v-if="isEditingUsername"
v-model="username"
type="text"
size="normal"
@blur="saveUsername"
@keyup.enter="saveUsername"
class="flex-1 max-w-xs"
autofocus
/>
<span v-else class="text-gray-900">{{ username }}</span>
</div>
</div>
<IconFluentTextEditStyle20Regular
@click="isEditingUsername ? saveUsername() : editUsername()"
class="text-xl"/>
</div>
<div class="line"></div>
<!-- Email Section -->
<div class="flex items-center justify-between ">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">电子邮箱</div>
<div class="flex items-center gap-2">
<IconFluentMail20Regular class="text-base text-gray-500"/>
<BaseInput
v-if="isEditingEmail"
v-model="email"
type="email"
size="normal"
@blur="saveEmail"
@keyup.enter="saveEmail"
class="flex-1 max-w-xs"
autofocus
/>
<span v-else class="text-gray-900">{{ email }}</span>
</div>
</div>
<IconFluentTextEditStyle20Regular
@click="isEditingEmail ? saveEmail() : editEmail()"
class="text-xl"/>
</div>
<!-- Subscription Information -->
<div class="lg:col-span-1">
<div class="card">
<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>
<div class="line"></div>
<!-- Password Section -->
<div class="flex items-center justify-between cp"
@click="showPasswordSection = !showPasswordSection"
>
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">设置密码</div>
<div class="text-xs text-gray-500">在此输入密码</div>
</div>
<IconFluentChevronLeft28Filled
class="transition-transform"
:class="['rotate-270','rotate-180'][showPasswordSection?0:1]"/>
</div>
<div v-if="showPasswordSection">
<BaseInput placeholder="新密码"
type="password"
autofocus
:min="PASSWORD_CONFIG.minLength"
:max="PASSWORD_CONFIG.maxLength"/>
<div class="text-align-end mt-4">
<BaseButton type="info" @click="showPasswordSection = !showPasswordSection">取消</BaseButton>
<BaseButton @click="changePassword">保存</BaseButton>
</div>
</div>
<div class="line"></div>
<!-- Contact Support -->
<div class="flex py-2 items-center justify-between cp"
@click="contactSupport">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">联系 {{ APP_NAME }} 客服</div>
</div>
<IconFluentChevronLeft28Filled class="rotate-180"/>
</div>
<div class="line"></div>
<!-- Trustpilot Review -->
<div class="flex py-2 items-center justify-between cp"
@click="leaveTrustpilotReview">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1"> {{ APP_NAME }} 上留下评论</div>
</div>
<IconFluentChevronLeft28Filled class="rotate-180"/>
</div>
<div class="line"></div>
<!-- Logout Button -->
<div class="center w-full">
<BaseButton
@click="handleLogout"
size="large"
class="w-[80%]"
>
登出
</BaseButton>
</div>
<div class="text-xs text-center">
<a href="/user-agreement.html" target="_blank" class="text-gray-500 hover:text-gray-700">用户协议</a>
<a href="/privacy-policy.html" target="_blank" class="text-gray-500 hover:text-gray-700">隐私政策</a>
</div>
</div>
<!-- Subscription Information -->
<div class="card 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>
</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>
</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">
<Calendar class="w-4 h-4 text-gray-400"/>
<span class="text-sm font-medium text-gray-900">{{ subscriptionData.expiresAt }}</span>
</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>
</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>
<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'">
<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 ? '已开启' : '已关闭' }}
</span>
</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>
<div class="pt-4 border-t border-gray-200">
<BaseButton class="w-full">管理订阅</BaseButton>
</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>
<div class="pt-4 border-t border-gray-200">
<BaseButton class="w-full">管理订阅</BaseButton>
</div>
</div>
</div>
</div>