wip
This commit is contained in:
@@ -49,7 +49,9 @@
|
||||
"@iconify-json/qlementine-icons": "^1.2.11",
|
||||
"@iconify-json/ri": "^1.2.5",
|
||||
"@iconify-json/simple-icons": "^1.2.48",
|
||||
"@iconify-json/streamline": "^1.2.5",
|
||||
"@iconify-json/system-uicons": "^1.2.4",
|
||||
"@iconify-json/uiw": "^1.2.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/md5": "^2.1.33",
|
||||
|
||||
@@ -25,6 +25,45 @@ export type LevelBenefits = {
|
||||
}[]
|
||||
}
|
||||
|
||||
export type CouponInfo = {
|
||||
"id": number,
|
||||
"code": string,
|
||||
"name": string,
|
||||
"type": string,
|
||||
"value"?: string,
|
||||
"min_amount"?: string,
|
||||
"max_discount"?: string,
|
||||
"applicable_levels": {
|
||||
code: string,
|
||||
name: string,
|
||||
level: string,
|
||||
}[]
|
||||
"usage_limit": number,
|
||||
"total_usage": number,
|
||||
"start_date": string
|
||||
"end_date": string
|
||||
"is_active": number,
|
||||
"created_at": string
|
||||
"updated_at": string
|
||||
"is_valid": boolean,
|
||||
}
|
||||
|
||||
export function levelBenefits(params) {
|
||||
return http<LevelBenefits>('member/levelBenefits', null, params, 'get')
|
||||
}
|
||||
|
||||
export function orderCreate(params) {
|
||||
return http<{ orderNo: string }>('/member/orderCreate', params, null, 'post')
|
||||
}
|
||||
|
||||
export function orderStatus(params) {
|
||||
return http('/member/orderStatus', null, params, 'get')
|
||||
}
|
||||
|
||||
export function couponInfo(params) {
|
||||
return http<CouponInfo>('/member/couponInfo', null, params, 'get')
|
||||
}
|
||||
|
||||
export function setAutoRenewApi(params) {
|
||||
return http('/member/setAutoRenew', params, null, 'post')
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface User {
|
||||
endDate: number,
|
||||
autoRenew: boolean,
|
||||
plan: string,
|
||||
planDesc: string,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@
|
||||
|
||||
--color-label-bg: whitesmoke;
|
||||
--color-link: #2563EB;
|
||||
|
||||
--color-card-bg: white;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -124,6 +126,8 @@ html.dark {
|
||||
|
||||
--color-label-bg: rgb(10, 10, 10);
|
||||
|
||||
--color-card-bg: rgb(30, 31, 34);
|
||||
|
||||
.footer {
|
||||
&.hide {
|
||||
--color-progress-bar: var(--color-third) !important;
|
||||
@@ -403,6 +407,11 @@ a {
|
||||
background: var(--color-second);
|
||||
}
|
||||
|
||||
.card-white {
|
||||
@extend .card;
|
||||
background: var(--color-card-bg);
|
||||
}
|
||||
|
||||
.inline-center {
|
||||
@apply inline-flex justify-center items-center;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,21 @@ export default {
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
type: [String, Array],
|
||||
default() {
|
||||
return ''
|
||||
},
|
||||
validator(value) {
|
||||
// Validate that array items have the correct structure
|
||||
if (Array.isArray(value)) {
|
||||
return value.every(item =>
|
||||
typeof item === 'object' &&
|
||||
item !== null &&
|
||||
typeof item.text === 'string' &&
|
||||
['normal', 'bold', 'red', 'redBold'].includes(item.type)
|
||||
)
|
||||
}
|
||||
return typeof value === 'string'
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
@@ -23,6 +35,17 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
titleItems() {
|
||||
if (typeof this.title === 'string') {
|
||||
return [{ text: this.title, type: 'normal' }]
|
||||
}
|
||||
if (Array.isArray(this.title)) {
|
||||
return this.title
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false
|
||||
@@ -37,6 +60,27 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
getTextStyle(type) {
|
||||
const styles = {
|
||||
normal: {
|
||||
fontWeight: 'normal',
|
||||
color: 'inherit'
|
||||
},
|
||||
bold: {
|
||||
fontWeight: 'bold',
|
||||
color: 'inherit'
|
||||
},
|
||||
red: {
|
||||
fontWeight: 'normal',
|
||||
color: 'red'
|
||||
},
|
||||
redBold: {
|
||||
fontWeight: 'bold',
|
||||
color: 'red'
|
||||
}
|
||||
}
|
||||
return styles[type] || styles.normal
|
||||
},
|
||||
showPop(e) {
|
||||
if (this.disabled) return this.$emit('confirm')
|
||||
e?.stopPropagation()
|
||||
@@ -68,8 +112,16 @@ export default {
|
||||
{
|
||||
this.show && (
|
||||
<div ref="tip" class="pop-confirm-content shadow-2xl">
|
||||
<div class="w-50">
|
||||
{this.title}
|
||||
<div class="w-52 title-content">
|
||||
{this.titleItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={this.getTextStyle(item.type)}
|
||||
class="title-item"
|
||||
>
|
||||
{item.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="options">
|
||||
<BaseButton type="info" size="small" onClick={() => this.show = false}>取消</BaseButton>
|
||||
@@ -95,6 +147,15 @@ export default {
|
||||
transform: translate(-50%, calc(-100% - .6rem));
|
||||
z-index: 999;
|
||||
|
||||
.title-content {
|
||||
.title-item {
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
margin-top: .9rem;
|
||||
|
||||
@@ -24,6 +24,7 @@ export let AppEnv = {
|
||||
|
||||
AppEnv.IS_LOGIN = !!AppEnv.TOKEN
|
||||
AppEnv.CAN_REQUEST = AppEnv.IS_LOGIN && AppEnv.IS_OFFICIAL
|
||||
// console.log('AppEnv.CAN_REQUEST',AppEnv.CAN_REQUEST)
|
||||
|
||||
export const RESOURCE_PATH = ENV.API + 'static'
|
||||
|
||||
|
||||
@@ -437,15 +437,15 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j, w) {
|
||||
onClick: () => {
|
||||
let word = props.article.sections[i][j].words[w]
|
||||
let text = word.word
|
||||
// let doc = nlp(text)
|
||||
// // 优先判断是不是动词
|
||||
// if (doc.verbs().found) {
|
||||
// text = doc.verbs().toInfinitive().text()
|
||||
// }
|
||||
// // 如果是名词(复数 → 单数)
|
||||
// if (doc.nouns().found) {
|
||||
// text = doc.nouns().toSingular().text()
|
||||
// }
|
||||
let doc = nlp(text)
|
||||
// 优先判断是不是动词
|
||||
if (doc.verbs().found) {
|
||||
text = doc.verbs().toInfinitive().text()
|
||||
}
|
||||
// 如果是名词(复数 → 单数)
|
||||
if (doc.nouns().found) {
|
||||
text = doc.nouns().toSingular().text()
|
||||
}
|
||||
if (!text.length) text = word.word
|
||||
console.log('text', text)
|
||||
toggleWordCollect(getDefaultWord({word: text, id: nanoid()}))
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
|
||||
// Payment method selection
|
||||
const selectedPaymentMethod = ref('')
|
||||
const agreeToTerms = ref(false)
|
||||
|
||||
// Payment methods - WeChat and Alipay
|
||||
const paymentMethods = [
|
||||
{
|
||||
id: 'wechat',
|
||||
name: '微信支付',
|
||||
icon: '💚',
|
||||
description: '使用微信支付'
|
||||
},
|
||||
{
|
||||
id: 'alipay',
|
||||
name: '支付宝',
|
||||
icon: '💙',
|
||||
description: '使用支付宝支付'
|
||||
}
|
||||
]
|
||||
|
||||
// Order data (this would typically come from props or store)
|
||||
const orderData = {
|
||||
planName: '月度会员',
|
||||
price: 9.99,
|
||||
currency: 'US$',
|
||||
unit: '每月',
|
||||
startDate: new Date().toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}),
|
||||
vatText: '(含增值税)'
|
||||
}
|
||||
|
||||
function selectPaymentMethod(methodId: string) {
|
||||
selectedPaymentMethod.value = methodId
|
||||
}
|
||||
|
||||
function handlePayment() {
|
||||
if (!selectedPaymentMethod.value) {
|
||||
alert('请选择支付方式')
|
||||
return
|
||||
}
|
||||
if (!agreeToTerms.value) {
|
||||
alert('请同意服务条款')
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Implement payment processing
|
||||
console.log('Processing payment with:', selectedPaymentMethod.value)
|
||||
}
|
||||
|
||||
function handleChangePlan() {
|
||||
// TODO: Navigate back to plan selection
|
||||
console.log('Change plan clicked')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="pay-page min-h-screen py-8">
|
||||
<!-- Page Header -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-2">安全支付</h1>
|
||||
<p class="text-gray-600">选择支付方式完成订单</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left Card: Payment Method Selection -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">Choose a way to pay</h2>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="method in paymentMethods"
|
||||
:key="method.id"
|
||||
@click="selectPaymentMethod(method.id)"
|
||||
class="flex items-center p-4 border rounded-lg cursor-pointer transition-all duration-200"
|
||||
:class="[
|
||||
selectedPaymentMethod === method.id
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-2xl mr-3">{{ method.icon }}</span>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">{{ method.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ method.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-5 h-5 rounded-full border-2 flex items-center justify-center"
|
||||
:class="[
|
||||
selectedPaymentMethod === method.id
|
||||
? 'border-blue-500 bg-blue-500'
|
||||
: 'border-gray-300'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="selectedPaymentMethod === method.id"
|
||||
class="w-2 h-2 bg-white rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Card: Order Summary -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">订单概要</h2>
|
||||
<button
|
||||
@click="handleChangePlan"
|
||||
class="px-3 py-1 text-sm text-gray-600 border border-gray-300 rounded-full hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
更改
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Plan Info -->
|
||||
<div class="mb-4">
|
||||
<div class="text-purple-600 text-sm mb-2">付费方案(月费)订阅</div>
|
||||
<div class="text-gray-900 mb-4">
|
||||
从 {{ orderData.startDate }} 开始:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="flex items-baseline mb-4">
|
||||
<span class="text-3xl font-semibold text-gray-900">{{ orderData.currency }}{{ orderData.price }}</span>
|
||||
<span class="text-gray-600 ml-2">/ {{ orderData.unit }}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mb-6">{{ orderData.vatText }}</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-6">
|
||||
<p class="text-sm text-gray-600 mb-2">
|
||||
你将于 {{ orderData.startDate }} 付费。
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
在试订期间和订阅开始前的24小时内,你可随时通过 "账户" > "订阅" 页面取消或改订。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Terms Checkbox -->
|
||||
<div class="flex items-start mb-6">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="terms"
|
||||
v-model="agreeToTerms"
|
||||
class="mt-1 w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label for="terms" class="ml-3 text-sm text-gray-700">
|
||||
我同意 Lingvist 的
|
||||
<a href="#" class="text-purple-600 hover:text-purple-700 underline">服务条款</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Payment Button -->
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:type="selectedPaymentMethod && agreeToTerms ? 'primary' : 'default'"
|
||||
:disabled="!selectedPaymentMethod || !agreeToTerms"
|
||||
@click="handlePayment"
|
||||
>
|
||||
选择
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pay-page {
|
||||
@apply min-h-screen;
|
||||
}
|
||||
|
||||
/* Custom radio button style */
|
||||
.payment-method {
|
||||
@apply flex items-center p-4 border rounded-lg cursor-pointer transition-all duration-200;
|
||||
|
||||
&:hover {
|
||||
@apply border-gray-300 bg-gray-50;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@apply border-blue-500 bg-blue-50;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-radio {
|
||||
@apply w-5 h-5 rounded-full border-2 flex items-center justify-center;
|
||||
|
||||
&.selected {
|
||||
@apply border-blue-500 bg-blue-500;
|
||||
}
|
||||
|
||||
&.unselected {
|
||||
@apply border-gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-dot {
|
||||
@apply w-2 h-2 bg-white rounded-full;
|
||||
}
|
||||
</style>
|
||||
@@ -246,13 +246,17 @@ function subscribe() {
|
||||
|
||||
}
|
||||
|
||||
function onFileChange(e) {
|
||||
console.log('e', e)
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<!-- Unauthenticated View -->
|
||||
<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="card-white 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>
|
||||
@@ -279,8 +283,7 @@ function subscribe() {
|
||||
<!-- Authenticated View -->
|
||||
<div v-else class="w-full flex gap-4">
|
||||
<!-- Main Account Settings -->
|
||||
<!-- todo 夜间背景色-->
|
||||
<div class="card bg-reverse-white shadow-lg flex-1 flex flex-col gap-2 px-6">
|
||||
<div class="card-white flex-1 flex flex-col gap-2 px-6">
|
||||
<h1 class="text-2xl font-bold mt-0">帐户</h1>
|
||||
|
||||
<!-- 用户名-->
|
||||
@@ -458,14 +461,14 @@ function subscribe() {
|
||||
|
||||
|
||||
<!-- Password Section -->
|
||||
<div class="item cp" @click="showChangePwdForm">
|
||||
<div class="item">
|
||||
<div class="flex-1">
|
||||
<div class="mb-2">设置密码</div>
|
||||
<div class="text-xs">在此输入密码</div>
|
||||
</div>
|
||||
<IconFluentChevronLeft28Filled
|
||||
class="transition-transform"
|
||||
:class="['rotate-270','rotate-180'][showChangePwd?0:1]"/>
|
||||
<BaseIcon @click="showChangePwdForm">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
<div v-if="showChangePwd">
|
||||
<Form
|
||||
@@ -523,6 +526,20 @@ function subscribe() {
|
||||
</div>
|
||||
<!-- <div class="line"></div>-->
|
||||
|
||||
<!-- 同步进度-->
|
||||
<div class="item cp relative">
|
||||
<div class="flex-1">
|
||||
<div class="">同步进度</div>
|
||||
<!-- <div class="text-xs mt-2">在此输入密码</div>-->
|
||||
</div>
|
||||
<IconFluentChevronLeft28Filled class="rotate-180"/>
|
||||
<input type="file" accept=".json,.zip,application/json,application/zip"
|
||||
@change="onFileChange"
|
||||
class="absolute left-0 top-0 w-full h-full bg-red cp opacity-0"/>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
|
||||
<!-- 去github issue-->
|
||||
<div class="item cp"
|
||||
@click="goIssues">
|
||||
<div class="flex-1">
|
||||
@@ -551,8 +568,7 @@ function subscribe() {
|
||||
</div>
|
||||
|
||||
<!-- Subscription Information -->
|
||||
<!-- todo 夜间背景色-->
|
||||
<div class="card bg-reverse-white shadow-lg w-80">
|
||||
<div class="card-white w-80">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<IconFluentCrown20Regular class="text-2xl text-yellow-500"/>
|
||||
<div class="text-lg font-bold">订阅信息</div>
|
||||
@@ -563,13 +579,13 @@ function subscribe() {
|
||||
<template v-if="userStore.user?.member">
|
||||
<div>
|
||||
<div class="mb-1">当前计划</div>
|
||||
<div class="text-base font-bold">{{ member?.levelDesc }}</div>
|
||||
<div class="text-base font-bold">{{ member?.planDesc }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1">状态</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full" :class="member?.active ?'bg-green-500':'bg-red-500'"></div>
|
||||
<div class="w-2 h-2 rounded-full" :class="member?.active ?'bg-green-500':'bg-red-500'"></div>
|
||||
<span class="text-base font-medium" :class="member?.active ?'text-green-700':'text-red-700'">
|
||||
{{ member?.status }}
|
||||
</span>
|
||||
@@ -609,6 +625,7 @@ function subscribe() {
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item {
|
||||
@apply flex items-center justify-between min-h-14;
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/auth.ts'
|
||||
import { User } from "@/apis/user.ts";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useUserStore} from '@/stores/auth.ts'
|
||||
import {User} from "@/apis/user.ts";
|
||||
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
|
||||
import Header from "@/components/Header.vue";
|
||||
import { LevelBenefits, levelBenefits } from "@/apis/member.ts";
|
||||
import {
|
||||
CouponInfo,
|
||||
couponInfo,
|
||||
LevelBenefits,
|
||||
levelBenefits,
|
||||
orderCreate,
|
||||
orderStatus,
|
||||
setAutoRenewApi
|
||||
} from "@/apis/member.ts";
|
||||
import Radio from "@/components/base/radio/Radio.vue";
|
||||
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
|
||||
import { APP_NAME } from "@/config/env.ts";
|
||||
import {APP_NAME} from "@/config/env.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import { _dateFormat, _nextTick } from "@/utils";
|
||||
import {_dateFormat, _nextTick} from "@/utils";
|
||||
import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import dayjs from "dayjs";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -25,9 +37,10 @@ interface Plan {
|
||||
autoRenew?: boolean
|
||||
}
|
||||
|
||||
|
||||
let loading = $ref(false);
|
||||
let selectedPaymentMethod = $ref('wechat')
|
||||
let selectedSubscribePlan = $ref(undefined)
|
||||
let selectedPlanId = $ref('')
|
||||
let duration = $ref(1)
|
||||
const member = $computed<User['member']>(() => userStore.user?.member ?? {} as any)
|
||||
|
||||
const memberEndDate = $computed(() => {
|
||||
@@ -40,13 +53,13 @@ const plans: Plan[] = $computed(() => {
|
||||
let list = []
|
||||
if (data?.level) {
|
||||
list.push({
|
||||
id: 'monthly',
|
||||
id: 'month',
|
||||
name: '月付',
|
||||
price: data.level.price,
|
||||
unit: '月',
|
||||
},)
|
||||
list.push({
|
||||
id: 'monthly-auto',
|
||||
id: 'month_auto',
|
||||
name: '连续包月',
|
||||
price: data.level.price_auto,
|
||||
unit: '月',
|
||||
@@ -64,41 +77,6 @@ const plans: Plan[] = $computed(() => {
|
||||
return list
|
||||
})
|
||||
|
||||
const currentPlan = $computed(() => {
|
||||
return plans.find(v => v.id === selectedSubscribePlan) ?? null
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
let res = await levelBenefits({levelCode: 'basic'})
|
||||
if (res.success) {
|
||||
data = res.data
|
||||
}
|
||||
})
|
||||
|
||||
// Toggle auto-renewal
|
||||
function toggleAutoRenew() {
|
||||
// TODO: Implement API call to toggle auto-renewal
|
||||
console.log('Toggle auto-renewal:', !member.autoRenew)
|
||||
}
|
||||
|
||||
// Get button text based on current plan
|
||||
function getPlanButtonText(plan: Plan) {
|
||||
if (plan.id === selectedSubscribePlan) return '当前计划'
|
||||
if (!member?.active) return '选择'
|
||||
}
|
||||
|
||||
function goPurchase(plan: Plan) {
|
||||
if (!userStore.isLogin) {
|
||||
router.push({path: '/login', query: {redirect: '/vip'}})
|
||||
return
|
||||
}
|
||||
selectedSubscribePlan = plan.id
|
||||
_nextTick(() => {
|
||||
let el = document.getElementById('pay')
|
||||
el.scrollIntoView({behavior: "smooth"})
|
||||
})
|
||||
}
|
||||
|
||||
// Payment methods - WeChat and Alipay
|
||||
const paymentMethods = [
|
||||
{
|
||||
@@ -113,9 +91,188 @@ const paymentMethods = [
|
||||
}
|
||||
]
|
||||
|
||||
const currentPlan = $computed(() => {
|
||||
return plans.find(v => v.id === member?.plan) ?? null
|
||||
})
|
||||
|
||||
function handlePayment() {
|
||||
console.log('Processing payment with:', selectedPaymentMethod)
|
||||
const selectPlan = $computed(() => {
|
||||
return plans.find(v => v.id === selectedPlanId) ?? null
|
||||
})
|
||||
|
||||
// Calculate original price based on plan type
|
||||
const originalPrice = $computed(() => {
|
||||
return selectPlan?.id === 'month_auto' ? Number(selectPlan?.price) : Number(duration) * Number(selectPlan?.price)
|
||||
})
|
||||
|
||||
// check Is it enough for a discount
|
||||
const enoughDiscount = $computed(() => {
|
||||
if (coupon.is_valid) {
|
||||
if (coupon.min_amount) {
|
||||
const minAmount = Number(coupon.min_amount)
|
||||
return originalPrice > minAmount
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const endPrice = $computed(() => {
|
||||
if (!coupon.is_valid) {
|
||||
return Number(originalPrice.toFixed(2))
|
||||
}
|
||||
|
||||
if (coupon.type === 'free_trial') return 0
|
||||
|
||||
if (!enoughDiscount) {
|
||||
return Number(originalPrice.toFixed(2))
|
||||
}
|
||||
|
||||
let discountAmount = 0
|
||||
if (coupon.type === 'discount') {
|
||||
// Discount coupon: e.g., 0.8 means 20% off
|
||||
const discountRate = Number(coupon.value)
|
||||
discountAmount = originalPrice * (1 - discountRate)
|
||||
|
||||
// Apply max_discount limit if available
|
||||
if (coupon.max_discount) {
|
||||
const maxDiscount = Number(coupon.max_discount)
|
||||
discountAmount = Math.min(discountAmount, maxDiscount)
|
||||
}
|
||||
} else if (coupon.type === 'amount') {
|
||||
// Amount coupon: fixed amount off
|
||||
discountAmount = Number(coupon.value)
|
||||
}
|
||||
|
||||
const finalPrice = Math.max(originalPrice - discountAmount, 0)
|
||||
return finalPrice.toFixed(2)
|
||||
}
|
||||
)
|
||||
|
||||
const startDate = $computed(() => {
|
||||
if (member?.active) {
|
||||
return member.endDate
|
||||
} else {
|
||||
return _dateFormat(Date.now())
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
let res = await levelBenefits({levelCode: 'basic'})
|
||||
if (res.success) {
|
||||
data = res.data
|
||||
}
|
||||
})
|
||||
|
||||
let loading2 = $ref(false);
|
||||
|
||||
async function toggleAutoRenew() {
|
||||
if (loading2) return
|
||||
loading2 = true
|
||||
let res = await setAutoRenewApi({autoRenew: false})
|
||||
if (res.success) {
|
||||
Toast.success('取消成功')
|
||||
userStore.init()
|
||||
} else {
|
||||
Toast.error(res.msg || '取消失败')
|
||||
}
|
||||
loading2 = false
|
||||
}
|
||||
|
||||
// Get button text based on current plan
|
||||
function getPlanButtonText(plan: Plan) {
|
||||
if (plan.id === selectedPlanId) return '已选中'
|
||||
if (plan.id === currentPlan?.id) return '当前计划'
|
||||
return '选择'
|
||||
}
|
||||
|
||||
function goPurchase(plan: Plan) {
|
||||
if (!userStore.isLogin) {
|
||||
router.push({path: '/login', query: {redirect: '/vip'}})
|
||||
return
|
||||
}
|
||||
selectedPlanId = plan.id
|
||||
_nextTick(() => {
|
||||
let el = document.getElementById('pay')
|
||||
el.scrollIntoView({behavior: "smooth"})
|
||||
})
|
||||
}
|
||||
|
||||
let startLoop = $ref(false)
|
||||
let orderNo = $ref('')
|
||||
let timer: number = $ref()
|
||||
let showCouponInput = $ref(false)
|
||||
let coupon = $ref<CouponInfo>({code: ''} as CouponInfo)
|
||||
|
||||
watch(() => startLoop, (n) => {
|
||||
if (n) {
|
||||
clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
orderStatus({orderNo}).then(res => {
|
||||
if (res?.success) {
|
||||
if (res.data?.payment_status === 'paid') {
|
||||
Toast.success('付款成功')
|
||||
userStore.init()
|
||||
startLoop = false
|
||||
selectedPlanId = undefined
|
||||
}
|
||||
} else {
|
||||
startLoop = false
|
||||
Toast.error(res.msg || '付款失败')
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
} else {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
startLoop = false
|
||||
clearInterval(timer)
|
||||
})
|
||||
|
||||
async function handlePayment() {
|
||||
if (loading) return
|
||||
loading = true
|
||||
let data = {
|
||||
plan: selectedPlanId,
|
||||
duration: Number(duration),
|
||||
payment_method: selectedPaymentMethod,
|
||||
couponCode: coupon.is_valid ? coupon.code : undefined
|
||||
}
|
||||
let res = await orderCreate(data)
|
||||
if (res.success) {
|
||||
orderNo = res.data.orderNo
|
||||
startLoop = true
|
||||
} else {
|
||||
Toast.error(res.msg || '付款失败')
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
let couponLoading = $ref(false)
|
||||
|
||||
async function getCouponInfo() {
|
||||
if (showCouponInput) {
|
||||
if (!coupon.code) return
|
||||
if (couponLoading) return
|
||||
couponLoading = true
|
||||
let res = await couponInfo(coupon)
|
||||
if (res.success) {
|
||||
if (res.data.is_valid) {
|
||||
coupon = res.data
|
||||
} else {
|
||||
coupon = {code: coupon.code} as CouponInfo
|
||||
Toast.info('优惠券已失效')
|
||||
}
|
||||
} else {
|
||||
coupon = {code: coupon.code} as CouponInfo
|
||||
Toast.error(res.msg || '优惠券无效')
|
||||
}
|
||||
couponLoading = false
|
||||
} else {
|
||||
showCouponInput = true
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -123,16 +280,42 @@ function handlePayment() {
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<div class="card-white">
|
||||
<Header title="会员介绍"></Header>
|
||||
<div class="center">
|
||||
<div>
|
||||
<div class="text-lg flex items-center" v-for="f in data.benefits" :key="f.name">
|
||||
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
|
||||
<span>
|
||||
<span>{{ f.name }}</span>
|
||||
<span v-if="f.value !== 'true'">{{ `(${f.value}${f.unit ?? ''})`}}</span>
|
||||
</span>
|
||||
<div class="grid grid-cols-3 grid-rows-3 gap-3">
|
||||
<div class="text-lg items-center" v-for="f in data.benefits" :key="f.name">
|
||||
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
|
||||
<span>
|
||||
<span>{{ f.name }}</span>
|
||||
<span v-if="f.value !== 'true'">{{ `(${f.value}${f.unit ?? ''})` }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="member?.active" class="card-white bg-green-50 dark:bg-item border border-green-200 mt-3 mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
|
||||
<div>
|
||||
<div class="font-semibold text-green-800">当前计划:{{ currentPlan?.name }}</div>
|
||||
<div class="text-sm text-green-600">
|
||||
到期时间:{{ memberEndDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-align-end space-y-2">
|
||||
<div v-if="member.autoRenew" class="flex items-center gap-space">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<IconFluentArrowRepeatAll20Regular class="mr-1"/>
|
||||
<span>自动续费已开启</span>
|
||||
</div>
|
||||
<PopConfirm
|
||||
title="确认取消?"
|
||||
@confirm="toggleAutoRenew"
|
||||
>
|
||||
<BaseButton size="small" type="info" :loading="loading2">关闭</BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,8 +328,8 @@ function handlePayment() {
|
||||
|
||||
<div class="plans">
|
||||
<div v-for="p in plans" :key="p.id"
|
||||
class="card bg-reverse-white shadow-lg p-0 shadow-lg overflow-hidden flex flex-col">
|
||||
<div class="plan-name">{{ p.name }}</div>
|
||||
class="card-white p-0 overflow-hidden flex flex-col">
|
||||
<div class="text-2xl font-bold bg-gray-300 dark:bg-third px-6 py-4">{{ p.name }}</div>
|
||||
<div class="p-6 flex flex-col justify-between flex-1">
|
||||
<div class="plan-head">
|
||||
<div class="price">
|
||||
@@ -159,76 +342,79 @@ function handlePayment() {
|
||||
<IconFluentArrowRepeatAll20Regular class="mr-2"/>
|
||||
开启自动续费,可随时关闭
|
||||
</div>
|
||||
<BaseButton
|
||||
class="w-full mt-4"
|
||||
size="large"
|
||||
:type="p.id === selectedSubscribePlan ? 'primary' : 'info'"
|
||||
@click="goPurchase(p)">
|
||||
<BaseButton class="w-full mt-4" size="large"
|
||||
:type="(p.id === currentPlan?.id || p.id === selectedPlanId) ? 'primary' : 'info'"
|
||||
:disabled="p.id === currentPlan?.id" @click="goPurchase(p)">
|
||||
{{ getPlanButtonText(p) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Membership Status Display -->
|
||||
<div v-if="member?.active" class="card bg-green-50 border border-green-200 mt-3 mb-6 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
|
||||
<div>
|
||||
<div class="font-semibold text-green-800">当前计划:{{ member?.levelDesc }}</div>
|
||||
<div class="text-sm text-green-600">
|
||||
到期时间:{{ memberEndDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-align-end space-y-2">
|
||||
<div v-if="member.autoRenew" class="flex items-center gap-space">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<IconFluentArrowRepeatAll20Regular class="mr-1"/>
|
||||
<span>自动续费已开启</span>
|
||||
</div>
|
||||
<BaseButton
|
||||
size="small"
|
||||
type="info"
|
||||
@click="toggleAutoRenew">
|
||||
关闭
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pay" class="mb-50" v-if="member?.plan !== selectedSubscribePlan">
|
||||
<div id="pay" class="mb-50" v-if="selectedPlanId">
|
||||
<!-- Page Header -->
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="text-xl font-semibold mb-2">安全支付</h1>
|
||||
<p class="">选择支付方式完成订单</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="center">
|
||||
<div class="card-white w-7/10">
|
||||
<div class="flex items-center justify-between gap-6 ">
|
||||
<div class="center gap-2" v-if="!showCouponInput">
|
||||
<IconStreamlineDiscountPercentCoupon/>
|
||||
<span>有优惠券?</span>
|
||||
</div>
|
||||
<BaseInput v-else v-model="coupon.code"
|
||||
placeholder="请输入优惠券"
|
||||
autofocus
|
||||
@enter="getCouponInfo"
|
||||
/>
|
||||
<BaseButton size="large"
|
||||
:loading="couponLoading"
|
||||
@click="getCouponInfo">{{ showCouponInput ? '确定' : '在此兑换!' }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg px-4 py-3 mt-4"
|
||||
v-if="coupon.is_valid">
|
||||
<div class="font-medium">优惠券: {{ coupon.name }}</div>
|
||||
<div class="flex justify-between w-full mt-2">
|
||||
<span v-if="coupon.type === 'discount'">折扣券:{{ (Number(coupon.value) * 10).toFixed(1) }}折</span>
|
||||
<span v-else-if="coupon.type === 'amount'">立减券:¥{{ Number(coupon.value).toFixed(2) }}</span>
|
||||
<span v-else-if="coupon.type === 'free_trial'">折扣: -100%</span>
|
||||
|
||||
<!-- Coupon restrictions -->
|
||||
<div v-if="coupon.min_amount || coupon.max_discount">
|
||||
<span v-if="coupon.min_amount">满{{ Number(coupon.min_amount).toFixed(2) }}元可用</span>
|
||||
<span v-if="coupon.max_discount && coupon.type === 'discount'">
|
||||
· 最高减{{ Number(coupon.max_discount).toFixed(2) }}元
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left Card: Payment Method Selection -->
|
||||
<div class="card bg-white shadow-lg">
|
||||
<div class="card-white">
|
||||
<div class="text-lg font-medium mb-4">选择支付方式</div>
|
||||
<RadioGroup v-model="selectedPaymentMethod">
|
||||
<div class="space-y-3 w-full">
|
||||
<div
|
||||
v-for="method in paymentMethods"
|
||||
:key="method.id"
|
||||
@click=" selectedPaymentMethod = method.id"
|
||||
class="flex p-4 border rounded-lg cp transition-all duration-200"
|
||||
:class="[
|
||||
selectedPaymentMethod === method.id
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center flex-1 gap-2">
|
||||
<IconSimpleIconsWechat class="text-xl"/>
|
||||
<div v-for="method in paymentMethods" :key="method.id"
|
||||
@click=" selectedPaymentMethod = method.id"
|
||||
class="flex p-4 border rounded-lg cp transition-all duration-200 hover:bg-item"
|
||||
:class="selectedPaymentMethod === method.id && 'bg-item'">
|
||||
<div class="flex items-center flex-1 gap-4">
|
||||
<IconSimpleIconsWechat class="text-xl color-green-500" v-if="method.id === 'wechat'"/>
|
||||
<IconUiwAlipay class="text-xl color-blue" v-else/>
|
||||
<div>
|
||||
<div class="font-medium">{{ method.name }}</div>
|
||||
<div class="font-medium color-main">{{ method.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ method.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,31 +425,57 @@ function handlePayment() {
|
||||
</div>
|
||||
|
||||
<!-- Right Card: Order Summary -->
|
||||
<div class="card bg-white shadow-lg">
|
||||
<div class="card-white">
|
||||
<div class="text-lg font-semibold mb-4">订单概要</div>
|
||||
|
||||
<!-- Plan Info -->
|
||||
<div class="mb-4">
|
||||
<div class="text-purple-600 text-sm mb-2">付费方案({{ currentPlan.name }})订阅</div>
|
||||
<div class="text-gray-900 mb-4">
|
||||
从 {{ _dateFormat(Date.now()) }} 开始:
|
||||
<div class="text-purple-600 text-sm mb-2">付费方案({{ selectPlan?.name }})订阅</div>
|
||||
<div class="mb-4">从 {{ startDate }} 开始:</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<!-- Price -->
|
||||
<div class="flex items-baseline">
|
||||
<span class="font-semibold"
|
||||
:class="selectPlan?.id === 'month_auto' ? 'text-3xl' : 'text-xl'">
|
||||
¥{{ selectPlan?.price }}
|
||||
</span>
|
||||
<span class="ml-2">/ {{ selectPlan?.unit }}</span>
|
||||
</div>
|
||||
<div v-if="selectPlan?.id !== 'month_auto'">
|
||||
<InputNumber :min="1" v-model="duration"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div v-if="coupon.is_valid" class="mb-4">
|
||||
<div class="flex items-baseline text-gray-500 line-through" v-if="enoughDiscount">
|
||||
<span class="text-lg">原价:¥{{ Number(originalPrice).toFixed(2) }}</span>
|
||||
<span class="ml-2">/ {{ selectPlan?.unit }}</span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<div v-if="enoughDiscount" class="text-green-600 flex items-center">
|
||||
<IconStreamlineDiscountPercentCoupon class="mr-2"/>
|
||||
<span>已优惠:¥{{ (Number(originalPrice) - Number(endPrice)).toFixed(2) }}</span>
|
||||
</div>
|
||||
<span v-else>优惠券不可用:未满足条件</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Final Price -->
|
||||
<div class="flex items-baseline mb-4">
|
||||
<span class="text-3xl font-semibold text-gray-900">¥{{ currentPlan.price }}</span>
|
||||
<span class="text-gray-600 ml-2">/ {{ currentPlan.unit }}</span>
|
||||
<span class="text-2xl font-semibold">总计:</span>
|
||||
<span class="text-3xl font-semibold">¥{{ endPrice }}</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-second text-sm px-4 py-3 rounded-lg mb-4 text-gray-600">
|
||||
会员属于虚拟服务,一经购买激活后不支持退款。请在购买前仔细阅读权益说明,确认符合您的需求再进行支付。
|
||||
</div>
|
||||
|
||||
<!-- Payment Button -->
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:type="!!selectedPaymentMethod ? 'primary' : 'info'"
|
||||
:disabled="!selectedPaymentMethod"
|
||||
@click="handlePayment"
|
||||
>
|
||||
<BaseButton class="w-full" size="large" :loading="loading || startLoop"
|
||||
:type="!!selectedPaymentMethod ? 'primary' : 'info'" :disabled="!selectedPaymentMethod"
|
||||
@click="handlePayment">
|
||||
付款
|
||||
</BaseButton>
|
||||
</div>
|
||||
@@ -273,7 +485,6 @@ function handlePayment() {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.plans {
|
||||
display: grid;
|
||||
gap: 3rem;
|
||||
@@ -284,9 +495,6 @@ function handlePayment() {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
@apply text-2xl font-bold bg-gray-300 px-6 py-4;
|
||||
}
|
||||
|
||||
.price {
|
||||
@apply flex items-end gap-1;
|
||||
@@ -308,4 +516,3 @@ function handlePayment() {
|
||||
@apply text-xs bg-yellow-100 text-yellow-700 px-2 py-1 rounded w-fit;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {APP_NAME} from "@/config/env.ts";
|
||||
import {useUserStore} from "@/stores/auth.ts";
|
||||
import {loginApi, LoginParams, registerApi, resetPasswordApi, sendCode} from "@/apis/user.ts";
|
||||
import {loginApi, LoginParams, registerApi, resetPasswordApi} 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";
|
||||
@@ -15,9 +15,9 @@ 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 {isNewUser, useNav} from "@/utils";
|
||||
import Header from "@/components/Header.vue";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
|
||||
// 状态管理
|
||||
const userStore = useUserStore()
|
||||
@@ -28,13 +28,14 @@ const router = useNav()
|
||||
let currentMode = $ref<'login' | 'register' | 'forgot'>('login')
|
||||
let loginType = $ref<'code' | 'password'>('code') // 默认验证码登录
|
||||
let loading = $ref(false)
|
||||
let codeCountdown = $ref(0)
|
||||
let showWechatQR = $ref(true)
|
||||
let wechatQRUrl = $ref('https://open.weixin.qq.com/connect/qrcode/041GmMJM2wfM0w3D')
|
||||
// 微信二维码状态:idle-正常/等待扫码,scanned-已扫码待确认,expired-已过期,cancelled-已取消
|
||||
let qrStatus = $ref<'idle' | 'scanned' | 'expired' | 'cancelled'>('idle')
|
||||
let qrExpireTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let qrCheckInterval: ReturnType<typeof setInterval> | null = null
|
||||
let waitForImportConfirmation = $ref(true)
|
||||
let isImporting = $ref(true)
|
||||
const QR_EXPIRE_TIME = 5 * 60 * 1000 // 5分钟过期
|
||||
|
||||
|
||||
@@ -153,7 +154,7 @@ async function handleRegister() {
|
||||
let res = await registerApi(registerForm)
|
||||
if (res.success) {
|
||||
userStore.setToken(res.data.token)
|
||||
userStore.setUser(res.data.user)
|
||||
userStore.setUser(res.data.user as any)
|
||||
Toast.success('注册成功')
|
||||
// 跳转到首页或用户中心
|
||||
router.push('/')
|
||||
@@ -284,7 +285,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<div class="center min-h-screen">
|
||||
<div class="rounded-2xl p-2 bg-white shadow-lg">
|
||||
<div class="card-white p-2" v-if="!waitForImportConfirmation">
|
||||
<!-- 登录区域容器 - 弹框形式 -->
|
||||
<div class="flex gap-2">
|
||||
<!-- 左侧登录区域 -->
|
||||
@@ -581,6 +582,44 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="card-white p-6 w-100">
|
||||
<div class="title">同步数据确认</div>
|
||||
<div class="flex flex-col justify-between h-60">
|
||||
<div v-if="!isImporting">
|
||||
<h2>检测到您本地存在使用记录</h2>
|
||||
<h3>是否需要同步到账户中?</h3>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-align-center">正在导入中</h3>
|
||||
<ol class="pl-4">
|
||||
<li>
|
||||
您的用户数据已自动下载到您的电脑中
|
||||
</li>
|
||||
<li>
|
||||
随后将开始数据同步
|
||||
</li>
|
||||
<li>
|
||||
如果您的数据量很大,这将是一个耗时操作
|
||||
</li>
|
||||
<li class="color-red-5 font-bold">
|
||||
请耐心等待,请勿关闭此页面
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="flex gap-space justify-end">
|
||||
<PopConfirm :title="[
|
||||
{text:'您的用户数据将以压缩包自动下载到您的电脑中',type:'normal'},
|
||||
{text:'随后用户数据将被移除',type:'redBold'},
|
||||
{text:'是否确认继续?',type:'normal'},
|
||||
]">
|
||||
<BaseButton type="info">放弃数据</BaseButton>
|
||||
</PopConfirm>
|
||||
|
||||
<BaseButton>确认同步</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as VueRouter from 'vue-router'
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
import {RouteRecordRaw} from 'vue-router'
|
||||
import WordsPage from "@/pages/word/WordsPage.vue";
|
||||
import Layout from "@/pages/layout.vue";
|
||||
import ArticlesPage from "@/pages/article/ArticlesPage.vue";
|
||||
@@ -13,7 +13,6 @@ import Setting from "@/pages/setting/Setting.vue";
|
||||
import Login from "@/pages/user/login.vue";
|
||||
import User from "@/pages/user/User.vue";
|
||||
import VipIntro from "@/pages/user/VipIntro.vue";
|
||||
import Pay from "@/pages/user/Pay.vue";
|
||||
// import { useAuthStore } from "@/stores/auth.ts";
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
@@ -39,7 +38,6 @@ export const routes: RouteRecordRaw[] = [
|
||||
{path: 'login', component: Login},
|
||||
{path: 'user', component: User},
|
||||
{path: 'vip', component: VipIntro},
|
||||
{path: 'pay', component: Pay},
|
||||
]
|
||||
},
|
||||
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getUserInfo, User } from '@/apis/user.ts'
|
||||
import { AppEnv } from "@/config/env.ts";
|
||||
import {defineStore} from 'pinia'
|
||||
import {ref} from 'vue'
|
||||
import {getUserInfo, User} from '@/apis/user.ts'
|
||||
import {AppEnv} from "@/config/env.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
@@ -42,6 +42,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
|
||||
// 获取用户信息
|
||||
async function fetchUserInfo() {
|
||||
if (!AppEnv.CAN_REQUEST) return false
|
||||
try {
|
||||
const res = await getUserInfo()
|
||||
if (res.success) {
|
||||
@@ -58,11 +59,9 @@ export const useUserStore = defineStore('user', () => {
|
||||
|
||||
// 初始化用户状态
|
||||
async function init() {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
const success = await fetchUserInfo()
|
||||
if (!success) {
|
||||
clearToken()
|
||||
}
|
||||
const success = await fetchUserInfo()
|
||||
if (!success) {
|
||||
clearToken()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface BaseState {
|
||||
dictListVersion: number
|
||||
}
|
||||
|
||||
export const DefaultBaseState = (): BaseState => ({
|
||||
export const getDefaultBaseState = (): BaseState => ({
|
||||
simpleWords: [
|
||||
'a', 'an',
|
||||
'i', 'my', 'me', 'you', 'your', 'he', 'his', 'she', 'her', 'it',
|
||||
@@ -51,7 +51,7 @@ export const DefaultBaseState = (): BaseState => ({
|
||||
|
||||
export const useBaseStore = defineStore('base', {
|
||||
state: (): BaseState => {
|
||||
return DefaultBaseState()
|
||||
return getDefaultBaseState()
|
||||
},
|
||||
getters: {
|
||||
collectWord(): Dict {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {BaseState, DefaultBaseState, useBaseStore} from "@/stores/base.ts";
|
||||
import {BaseState, getDefaultBaseState, 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";
|
||||
@@ -28,7 +28,7 @@ export function checkAndUpgradeSaveDict(val: any) {
|
||||
// console.log(configStr)
|
||||
// console.log('s', new Blob([val]).size)
|
||||
// val = ''
|
||||
let defaultState = DefaultBaseState()
|
||||
let defaultState = getDefaultBaseState()
|
||||
if (val) {
|
||||
try {
|
||||
let data: any
|
||||
@@ -450,3 +450,12 @@ export function resourceWrap(resource: string, version?: number) {
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
// check if it is a new user
|
||||
export async function isNewUser() {
|
||||
let isNew = false
|
||||
let base = useBaseStore()
|
||||
console.log(JSON.stringify(base.$state))
|
||||
console.log(JSON.stringify(getDefaultBaseState()))
|
||||
return JSON.stringify(base.$state) === JSON.stringify({...getDefaultBaseState(), ...{load: true}})
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export default defineConfig({
|
||||
'bg-primary': 'bg-[var(--color-primary)]',
|
||||
'bg-second': 'bg-[var(--color-second)]',
|
||||
'bg-third': 'bg-[var(--color-third)]',
|
||||
'bg-fourth': 'bg-[var(--color-fourth)]',
|
||||
'bg-card-active': 'bg-[var(--color-card-active)]',
|
||||
'bg-item': 'bg-[var(--color-item-bg)]',
|
||||
'bg-reverse-white': 'bg-[var(--color-reverse-white)]',
|
||||
|
||||
Reference in New Issue
Block a user