refactor
This commit is contained in:
66
src/components/user/Code.vue
Normal file
66
src/components/user/Code.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {sendCode} from "@/apis/user.ts";
|
||||
import {PHONE_CONFIG} from "@/config/auth.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import {CodeType} from "@/types/enum.ts";
|
||||
|
||||
let isSendingCode = $ref(false)
|
||||
let codeCountdown = $ref(0)
|
||||
|
||||
interface IProps {
|
||||
validateField: Function,
|
||||
type: CodeType
|
||||
val: any
|
||||
size?: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
size: 'large',
|
||||
})
|
||||
|
||||
// 发送验证码
|
||||
async function sendVerificationCode() {
|
||||
let res = props.validateField()
|
||||
if (res) {
|
||||
try {
|
||||
isSendingCode = true
|
||||
const res = await sendCode({val: props.val, type: props.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseButton
|
||||
@click="sendVerificationCode"
|
||||
:disabled="isSendingCode || codeCountdown > 0"
|
||||
type="info"
|
||||
:size="props.size"
|
||||
style="border: 1px solid var(--color-input-border)"
|
||||
>
|
||||
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '发送验证码') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
15
src/components/user/Notice.vue
Normal file
15
src/components/user/Notice.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-12 text-xs text-gray-400">
|
||||
<span>
|
||||
继续操作即表示你阅读并同意我们的
|
||||
<a href="/user-agreement.html" target="_blank" class="link">用户协议</a>
|
||||
与
|
||||
<a href="/privacy-policy.html" target="_blank" class="link">隐私政策</a>
|
||||
</span>
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,6 +13,7 @@ const common = {
|
||||
const map = {
|
||||
DEV: {
|
||||
API: 'http://localhost/',
|
||||
RESOURCE_URL: 'https://dicts.2study.top/',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ function updateList(e) {
|
||||
:loading="exportLoading"
|
||||
:disabled="!article.id"
|
||||
@click="exportData({ type: 'item', data: article })"
|
||||
>当前
|
||||
>当前
|
||||
</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
@@ -319,7 +319,7 @@ watch([() => displayMode, () => selectArticle.id, () => showTranslate], () => {
|
||||
v-for="(s, n) in w.split(' ').filter(Boolean)"
|
||||
:class="`inline-block word-${i}-${j}-${n}`"
|
||||
:key="`${i}-${j}-${n}`"
|
||||
><span>{{ s }}</span>
|
||||
><span>{{ s }}</span>
|
||||
<span class="space"></span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -38,10 +38,10 @@ const searchList = computed<any[]>(() => {
|
||||
let s = searchKey.toLowerCase()
|
||||
return bookList.value.filter((item) => {
|
||||
return item.id.toLowerCase().includes(s)
|
||||
|| item.name.toLowerCase().includes(s)
|
||||
|| item.category.toLowerCase().includes(s)
|
||||
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|
||||
|| item?.url?.toLowerCase?.().includes?.(s)
|
||||
|| item.name.toLowerCase().includes(s)
|
||||
|| item.category.toLowerCase().includes(s)
|
||||
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|
||||
|| item?.url?.toLowerCase?.().includes?.(s)
|
||||
})
|
||||
}
|
||||
return []
|
||||
@@ -68,20 +68,20 @@ const searchList = computed<any[]>(() => {
|
||||
</div>
|
||||
<div class="mt-4" v-if="searchKey">
|
||||
<DictList
|
||||
v-if="searchList.length "
|
||||
@selectDict="selectDict"
|
||||
:list="searchList"
|
||||
quantifier="篇"
|
||||
:select-id="'-1'"/>
|
||||
v-if="searchList.length "
|
||||
@selectDict="selectDict"
|
||||
:list="searchList"
|
||||
quantifier="篇"
|
||||
:select-id="'-1'"/>
|
||||
<Empty v-else text="没有相关书籍"/>
|
||||
</div>
|
||||
<div class="w-full mt-2" v-else>
|
||||
<DictList
|
||||
v-if="bookList?.length "
|
||||
@selectDict="selectDict"
|
||||
:list="bookList"
|
||||
quantifier="篇"
|
||||
:select-id="'-1'"/>
|
||||
v-if="bookList?.length "
|
||||
@selectDict="selectDict"
|
||||
:list="bookList"
|
||||
quantifier="篇"
|
||||
:select-id="'-1'"/>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
@@ -10,16 +10,15 @@ import { accountRules, codeRules, passwordRules, phoneRules } from '@/utils/vali
|
||||
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 Notice from '@/components/user/Notice.vue'
|
||||
import { FormInstance } from '@/components/base/form/types.ts'
|
||||
import { PASSWORD_CONFIG, PHONE_CONFIG } from '@/config/auth.ts'
|
||||
import Code from '@/pages/user/Code.vue'
|
||||
import { isNewUser, jump2Feedback, sleep, useNav } from '@/utils'
|
||||
import Code from '@/components/user/Code.vue'
|
||||
import { jump2Feedback, sleep, useNav } from '@/utils'
|
||||
import Header from '@/components/Header.vue'
|
||||
import PopConfirm from '@/components/PopConfirm.vue'
|
||||
import { useExport } from '@/hooks/export.ts'
|
||||
import { getProgress, upload, uploadImportData } from '@/apis'
|
||||
import { Exception } from 'sass'
|
||||
import { getProgress, uploadImportData } from '@/apis'
|
||||
import { CodeType, ImportStatus } from '@/types/enum.ts'
|
||||
|
||||
// 状态管理
|
||||
@@ -1,23 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted} from 'vue'
|
||||
import {useUserStore} from '@/stores/user.ts'
|
||||
import {useRouter} from 'vue-router'
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user.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 BaseButton from "@/components/BaseButton.vue";
|
||||
import {PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts";
|
||||
import {changeEmailApi, changePhoneApi, setPassword, updateUserInfoApi, User} from "@/apis/user.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import Form from "@/components/base/form/Form.vue";
|
||||
import {FormInstance} from "@/components/base/form/types.ts";
|
||||
import {codeRules, emailRules, passwordRules, phoneRules} from "@/utils/validation.ts";
|
||||
import {_dateFormat, cloneDeep, jump2Feedback} from "@/utils";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import Code from "@/pages/user/Code.vue";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {CodeType} from "@/types/enum.ts";
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import { APP_NAME, EMAIL } from '@/config/env.ts'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import { PASSWORD_CONFIG, PHONE_CONFIG } from '@/config/auth.ts'
|
||||
import { changeEmailApi, changePhoneApi, setPassword, updateUserInfoApi, User } from '@/apis/user.ts'
|
||||
import BaseIcon from '@/components/BaseIcon.vue'
|
||||
import FormItem from '@/components/base/form/FormItem.vue'
|
||||
import Form from '@/components/base/form/Form.vue'
|
||||
import { FormInstance } from '@/components/base/form/types.ts'
|
||||
import { codeRules, emailRules, passwordRules, phoneRules } from '@/utils/validation.ts'
|
||||
import { cloneDeep, jump2Feedback } from '@/utils'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import Code from '@/components/user/Code.vue'
|
||||
import { MessageBox } from '@/utils/MessageBox.tsx'
|
||||
import { CodeType } from '@/types/enum.ts'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
@@ -45,19 +45,23 @@ onMounted(() => {
|
||||
// 修改手机号
|
||||
// 修改手机号
|
||||
let changePhoneFormRef = $ref<FormInstance>()
|
||||
let defaultFrom = {oldCode: '', phone: '', code: '', pwd: '',}
|
||||
let defaultFrom = { oldCode: '', phone: '', code: '', pwd: '' }
|
||||
let changePhoneForm = $ref(cloneDeep(defaultFrom))
|
||||
let changePhoneFormRules = {
|
||||
oldCode: codeRules,
|
||||
phone: [...phoneRules, {
|
||||
validator: (rule: any, value: any) => {
|
||||
if (userStore.user?.phone && value === userStore.user?.phone) {
|
||||
throw new Error('新手机号与原手机号一致')
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
},],
|
||||
phone: [
|
||||
...phoneRules,
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (userStore.user?.phone && value === userStore.user?.phone) {
|
||||
throw new Error('新手机号与原手机号一致')
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
code: codeRules,
|
||||
pwd: passwordRules
|
||||
pwd: passwordRules,
|
||||
}
|
||||
|
||||
function showChangePhoneForm() {
|
||||
@@ -92,15 +96,15 @@ function changePhone() {
|
||||
// 修改用户名
|
||||
// 修改用户名
|
||||
let changeUsernameFormRef = $ref<FormInstance>()
|
||||
let changeUsernameForm = $ref({username: ''})
|
||||
let changeUsernameForm = $ref({ username: '' })
|
||||
let changeUsernameFormRules = {
|
||||
username: [{required: true, message: '请输入用户名', trigger: 'blur'}],
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
function showChangeUsernameForm() {
|
||||
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
|
||||
showChangeUsername = true
|
||||
changeUsernameForm = cloneDeep({username: userStore.user?.username ?? '',})
|
||||
changeUsernameForm = cloneDeep({ username: userStore.user?.username ?? '' })
|
||||
}
|
||||
|
||||
function changeUsername() {
|
||||
@@ -137,13 +141,15 @@ let changeEmailForm = $ref({
|
||||
})
|
||||
let changeEmailFormRules = {
|
||||
email: [
|
||||
...emailRules, {
|
||||
...emailRules,
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (userStore.user?.email && value === userStore.user?.email) {
|
||||
throw new Error('该邮箱与当前一致')
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
pwd: passwordRules,
|
||||
code: codeRules,
|
||||
@@ -152,7 +158,7 @@ let changeEmailFormRules = {
|
||||
function showChangeEmailForm() {
|
||||
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
|
||||
showChangeEmail = true
|
||||
changeEmailForm = cloneDeep({email: userStore.user?.email ?? '', pwd: '', code: '',})
|
||||
changeEmailForm = cloneDeep({ email: userStore.user?.email ?? '', pwd: '', code: '' })
|
||||
}
|
||||
|
||||
function changeEmail() {
|
||||
@@ -191,13 +197,14 @@ let changePwdFormRules = {
|
||||
oldPwd: passwordRules,
|
||||
newPwd: passwordRules,
|
||||
confirmPwd: [
|
||||
{required: true, message: '请再次输入新密码', trigger: 'blur'},
|
||||
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (value !== changePwdForm.newPwd) {
|
||||
throw new Error('两次密码输入不一致')
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -230,7 +237,7 @@ function changePwd() {
|
||||
})
|
||||
}
|
||||
|
||||
const member = $computed<User['member']>(() => userStore.user?.member ?? {} as any)
|
||||
const member = $computed<User['member']>(() => userStore.user?.member ?? ({} as any))
|
||||
|
||||
const memberEndDate = $computed(() => {
|
||||
if (member?.endDate === null) return '永久'
|
||||
@@ -239,12 +246,10 @@ const memberEndDate = $computed(() => {
|
||||
|
||||
function subscribe() {
|
||||
router.push('/vip')
|
||||
|
||||
}
|
||||
|
||||
function onFileChange(e) {
|
||||
console.log('e', e)
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -254,21 +259,15 @@ function onFileChange(e) {
|
||||
<div v-if="!userStore.isLogin" class="center h-screen">
|
||||
<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"/>
|
||||
<IconFluentPerson20Regular class="text-3xl text-blue-600" />
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold">
|
||||
<IconFluentHandWave20Regular class="text-xl translate-y-1 mr-2 shrink-0"/>
|
||||
<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"
|
||||
>
|
||||
登录
|
||||
</BaseButton>
|
||||
<BaseButton @click="router.push('/login')" size="large" class="w-full mt-4"> 登录 </BaseButton>
|
||||
<p class="text-sm text-gray-500">
|
||||
还没有账户?
|
||||
<router-link to="/login?register=1" class="line">立即注册</router-link>
|
||||
@@ -287,20 +286,17 @@ function onFileChange(e) {
|
||||
<div class="flex-1">
|
||||
<div class="mb-2">用户名</div>
|
||||
<div class="flex items-center gap-2" v-if="userStore.user?.username">
|
||||
<IconFluentPerson20Regular class="text-base"/>
|
||||
<IconFluentPerson20Regular class="text-base" />
|
||||
<span>{{ userStore.user?.username }}</span>
|
||||
</div>
|
||||
<div v-else class="text-xs">在此设置用户名</div>
|
||||
</div>
|
||||
<BaseIcon @click="showChangeUsernameForm">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
<IconFluentTextEditStyle20Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
<div v-if="showChangeUsername">
|
||||
<Form
|
||||
ref="changeUsernameFormRef"
|
||||
:rules="changeUsernameFormRules"
|
||||
:model="changeUsernameForm">
|
||||
<Form ref="changeUsernameFormRef" :rules="changeUsernameFormRules" :model="changeUsernameForm">
|
||||
<FormItem prop="username">
|
||||
<BaseInput
|
||||
v-model="changeUsernameForm.username"
|
||||
@@ -310,7 +306,7 @@ function onFileChange(e) {
|
||||
autofocus
|
||||
>
|
||||
<template #preIcon>
|
||||
<IconFluentPerson20Regular class="text-base"/>
|
||||
<IconFluentPerson20Regular class="text-base" />
|
||||
</template>
|
||||
</BaseInput>
|
||||
</FormItem>
|
||||
@@ -327,20 +323,17 @@ function onFileChange(e) {
|
||||
<div class="flex-1">
|
||||
<div class="mb-2">手机号</div>
|
||||
<div class="flex items-center gap-2" v-if="userStore.user?.phone">
|
||||
<IconFluentMail20Regular class="text-base"/>
|
||||
<IconFluentMail20Regular class="text-base" />
|
||||
<span>{{ userStore.user?.phone }}</span>
|
||||
</div>
|
||||
<div v-else class="text-xs">在此设置手机号</div>
|
||||
</div>
|
||||
<BaseIcon @click="showChangePhoneForm">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
<IconFluentTextEditStyle20Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
<div v-if="showChangePhone">
|
||||
<Form
|
||||
ref="changePhoneFormRef"
|
||||
:rules="changePhoneFormRules"
|
||||
:model="changePhoneForm">
|
||||
<Form ref="changePhoneFormRef" :rules="changePhoneFormRules" :model="changePhoneForm">
|
||||
<FormItem prop="oldCode" v-if="userStore.user?.phone">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
@@ -350,18 +343,11 @@ function onFileChange(e) {
|
||||
placeholder="请输入原手机号验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => true"
|
||||
:type="CodeType.ChangePhoneOld"
|
||||
:val="userStore.user.phone"/>
|
||||
<Code :validate-field="() => true" :type="CodeType.ChangePhoneOld" :val="userStore.user.phone" />
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem prop="phone">
|
||||
<BaseInput
|
||||
v-model="changePhoneForm.phone"
|
||||
type="tel"
|
||||
size="large"
|
||||
placeholder="请输入新手机号"
|
||||
/>
|
||||
<BaseInput v-model="changePhoneForm.phone" type="tel" size="large" placeholder="请输入新手机号" />
|
||||
</FormItem>
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
@@ -371,24 +357,24 @@ function onFileChange(e) {
|
||||
placeholder="请输入新手机号验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => changePhoneFormRef.validateField('phone')"
|
||||
:type="CodeType.ChangePhoneNew"
|
||||
:val="changePhoneForm.phone"/>
|
||||
<Code
|
||||
:validate-field="() => changePhoneFormRef.validateField('phone')"
|
||||
:type="CodeType.ChangePhoneNew"
|
||||
:val="changePhoneForm.phone"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem prop="pwd" v-if="!userStore.user?.phone">
|
||||
<BaseInput
|
||||
v-model="changePhoneForm.pwd"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请输入原密码"
|
||||
/>
|
||||
<BaseInput 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="userStore.user?.phone">原手机号不可用,点此申诉</span>
|
||||
<span
|
||||
class="link text-sm cp"
|
||||
@click="MessageBox.notice(`请提供证明信息发送邮件到 ${EMAIL} 进行申诉`, '人工申诉')"
|
||||
v-if="userStore.user?.phone"
|
||||
>原手机号不可用,点此申诉</span
|
||||
>
|
||||
<span v-else></span>
|
||||
<div>
|
||||
<BaseButton type="info" @click="showChangePhone = false">取消</BaseButton>
|
||||
@@ -403,20 +389,17 @@ function onFileChange(e) {
|
||||
<div class="flex-1">
|
||||
<div class="mb-2">电子邮箱</div>
|
||||
<div class="flex items-center gap-2" v-if="userStore.user?.email">
|
||||
<IconFluentMail20Regular class="text-base"/>
|
||||
<IconFluentMail20Regular class="text-base" />
|
||||
<span>{{ userStore.user?.email }}</span>
|
||||
</div>
|
||||
<div v-else class="text-xs">在此设置邮箱</div>
|
||||
</div>
|
||||
<BaseIcon @click="showChangeEmailForm">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
<IconFluentTextEditStyle20Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
<div v-if="showChangeEmail">
|
||||
<Form
|
||||
ref="changeEmailFormRef"
|
||||
:rules="changeEmailFormRules"
|
||||
:model="changeEmailForm">
|
||||
<Form ref="changeEmailFormRef" :rules="changeEmailFormRules" :model="changeEmailForm">
|
||||
<FormItem prop="email">
|
||||
<BaseInput
|
||||
v-model="changeEmailForm.email"
|
||||
@@ -434,18 +417,15 @@ function onFileChange(e) {
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => changeEmailFormRef.validateField('email')"
|
||||
:type="CodeType.ChangeEmail"
|
||||
:val="changeEmailForm.email"/>
|
||||
<Code
|
||||
:validate-field="() => changeEmailFormRef.validateField('email')"
|
||||
:type="CodeType.ChangeEmail"
|
||||
:val="changeEmailForm.email"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem prop="pwd" v-if="userStore.user?.hasPwd">
|
||||
<BaseInput
|
||||
v-model="changePwdForm.pwd"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
<BaseInput v-model="changePwdForm.pwd" type="password" size="large" placeholder="请输入密码" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div class="text-align-end mb-2">
|
||||
@@ -455,7 +435,6 @@ function onFileChange(e) {
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
|
||||
|
||||
<!-- Password Section -->
|
||||
<div class="item">
|
||||
<div class="flex-1">
|
||||
@@ -463,22 +442,13 @@ function onFileChange(e) {
|
||||
<div class="text-xs">在此输入密码</div>
|
||||
</div>
|
||||
<BaseIcon @click="showChangePwdForm">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
<IconFluentTextEditStyle20Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
<div v-if="showChangePwd">
|
||||
<Form
|
||||
ref="changePwdFormRef"
|
||||
:rules="changePwdFormRules"
|
||||
:model="changePwdForm">
|
||||
<Form 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
|
||||
/>
|
||||
<BaseInput v-model="changePwdForm.oldPwd" placeholder="旧密码" type="password" size="large" autofocus />
|
||||
</FormItem>
|
||||
|
||||
<FormItem prop="newPwd">
|
||||
@@ -510,15 +480,10 @@ function onFileChange(e) {
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
|
||||
|
||||
<!-- Contact Support -->
|
||||
<div class="item cp"
|
||||
v-if="false"
|
||||
@click="contactSupport">
|
||||
<div class="flex-1">
|
||||
联系 {{ APP_NAME }} 客服
|
||||
</div>
|
||||
<IconFluentChevronLeft28Filled class="rotate-180"/>
|
||||
<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>-->
|
||||
|
||||
@@ -528,32 +493,26 @@ function onFileChange(e) {
|
||||
<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"/>
|
||||
<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="jump2Feedback()">
|
||||
<div class="flex-1">
|
||||
给 {{ APP_NAME }} 提交意见
|
||||
</div>
|
||||
<IconFluentChevronLeft28Filled class="rotate-180"/>
|
||||
<div class="item cp" @click="jump2Feedback()">
|
||||
<div class="flex-1">给 {{ APP_NAME }} 提交意见</div>
|
||||
<IconFluentChevronLeft28Filled class="rotate-180" />
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<div class="center w-full mt-4">
|
||||
<BaseButton
|
||||
@click="handleLogout"
|
||||
size="large"
|
||||
class="w-[40%]"
|
||||
>
|
||||
登出
|
||||
</BaseButton>
|
||||
<BaseButton @click="handleLogout" size="large" class="w-[40%]"> 登出 </BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-center mt-2">
|
||||
@@ -566,12 +525,11 @@ function onFileChange(e) {
|
||||
<!-- Subscription Information -->
|
||||
<div class="card-white w-80">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<IconFluentCrown20Regular class="text-2xl text-yellow-500"/>
|
||||
<IconFluentCrown20Regular class="text-2xl text-yellow-500" />
|
||||
<div class="text-lg font-bold">订阅信息</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
<template v-if="userStore.user?.member">
|
||||
<div>
|
||||
<div class="mb-1">当前计划</div>
|
||||
@@ -581,17 +539,17 @@ function onFileChange(e) {
|
||||
<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>
|
||||
<span class="text-base font-medium" :class="member?.active ?'text-green-700':'text-red-700'">
|
||||
{{ member?.status }}
|
||||
</span>
|
||||
<div class="w-2 h-2 rounded-full" :class="member?.active ? 'bg-green-500' : 'bg-red-500'"></div>
|
||||
<span class="text-base font-medium" :class="member?.active ? 'text-green-700' : 'text-red-700'">
|
||||
{{ member?.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1">到期时间</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconFluentCalendarDate20Regular class="text-lg"/>
|
||||
<IconFluentCalendarDate20Regular class="text-lg" />
|
||||
<span class="text-base font-medium">{{ memberEndDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -599,22 +557,18 @@ function onFileChange(e) {
|
||||
<div>
|
||||
<div class="mb-1">自动续费</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full"
|
||||
:class="member?.autoRenew ? 'bg-blue-500' : 'bg-gray-400'"
|
||||
></div>
|
||||
<span class="text-base font-medium"
|
||||
:class="member?.autoRenew ? 'text-blue-700' : 'text-gray-600'">
|
||||
{{ member?.autoRenew ? '已开启' : '已关闭' }}
|
||||
</span>
|
||||
<div class="w-2 h-2 rounded-full" :class="member?.autoRenew ? 'bg-blue-500' : 'bg-gray-400'"></div>
|
||||
<span class="text-base font-medium" :class="member?.autoRenew ? 'text-blue-700' : 'text-gray-600'">
|
||||
{{ member?.autoRenew ? '已开启' : '已关闭' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="text-base" v-else>当前无订阅</div>
|
||||
|
||||
<BaseButton class="w-full" size="large" @click="subscribe">{{
|
||||
userStore.user?.member ? '管理订阅' : '会员介绍'
|
||||
}}
|
||||
<BaseButton class="w-full" size="large" @click="subscribe"
|
||||
>{{ userStore.user?.member ? '管理订阅' : '会员介绍' }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,11 @@
|
||||
<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/user.ts'
|
||||
import {User} from "@/apis/user.ts";
|
||||
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
|
||||
import Header from "@/components/Header.vue";
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { User } from '@/apis/user.ts'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
import {
|
||||
alipayQuery,
|
||||
CouponInfo,
|
||||
@@ -14,17 +14,15 @@ import {
|
||||
levelBenefits,
|
||||
orderCreate,
|
||||
orderStatus,
|
||||
setAutoRenewApi, testPay
|
||||
} 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 Toast from "@/components/base/toast/Toast.ts";
|
||||
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";
|
||||
setAutoRenewApi,
|
||||
} from '@/apis/member.ts'
|
||||
import Radio from '@/components/base/radio/Radio.vue'
|
||||
import RadioGroup from '@/components/base/radio/RadioGroup.vue'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import { _dateFormat, _nextTick } from '@/utils'
|
||||
import InputNumber from '@/components/base/InputNumber.vue'
|
||||
import BaseInput from '@/components/base/BaseInput.vue'
|
||||
import PopConfirm from '@/components/PopConfirm.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -38,11 +36,11 @@ interface Plan {
|
||||
autoRenew?: boolean
|
||||
}
|
||||
|
||||
let loading = $ref(false);
|
||||
let loading = $ref(false)
|
||||
let selectedPaymentMethod = $ref('alipay')
|
||||
let selectedPlanId = $ref('')
|
||||
let duration = $ref(1)
|
||||
const member = $computed<User['member']>(() => userStore.user?.member ?? {} as any)
|
||||
const member = $computed<User['member']>(() => userStore.user?.member ?? ({} as any))
|
||||
|
||||
const memberEndDate = $computed(() => {
|
||||
if (member?.endDate === null) return '永久'
|
||||
@@ -58,7 +56,7 @@ const plans: Plan[] = $computed(() => {
|
||||
name: '月付',
|
||||
price: data.level.price,
|
||||
unit: '月',
|
||||
},)
|
||||
})
|
||||
list.push({
|
||||
id: 'month_auto',
|
||||
name: '连续包月',
|
||||
@@ -66,14 +64,14 @@ const plans: Plan[] = $computed(() => {
|
||||
unit: '月',
|
||||
highlight: '性价比更高',
|
||||
autoRenew: true,
|
||||
},)
|
||||
})
|
||||
list.push({
|
||||
id: 'year',
|
||||
name: '年度会员',
|
||||
price: data.level.yearly_price,
|
||||
unit: '年',
|
||||
highlight: '年度优惠',
|
||||
},)
|
||||
})
|
||||
}
|
||||
return list
|
||||
})
|
||||
@@ -88,8 +86,8 @@ const paymentMethods = [
|
||||
{
|
||||
id: 'alipay',
|
||||
name: '支付宝',
|
||||
description: '使用支付宝支付'
|
||||
}
|
||||
description: '使用支付宝支付',
|
||||
},
|
||||
]
|
||||
|
||||
const currentPlan = $computed(() => {
|
||||
@@ -118,36 +116,35 @@ const enoughDiscount = $computed(() => {
|
||||
})
|
||||
|
||||
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)
|
||||
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) {
|
||||
@@ -158,18 +155,18 @@ const startDate = $computed(() => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
let res = await levelBenefits({levelCode: 'basic'})
|
||||
let res = await levelBenefits({ levelCode: 'basic' })
|
||||
if (res.success) {
|
||||
data = res.data
|
||||
}
|
||||
})
|
||||
|
||||
let loading2 = $ref(false);
|
||||
let loading2 = $ref(false)
|
||||
|
||||
async function toggleAutoRenew() {
|
||||
if (loading2) return
|
||||
loading2 = true
|
||||
let res = await setAutoRenewApi({autoRenew: false})
|
||||
let res = await setAutoRenewApi({ autoRenew: false })
|
||||
if (res.success) {
|
||||
Toast.success('取消成功')
|
||||
userStore.init()
|
||||
@@ -188,13 +185,13 @@ function getPlanButtonText(plan: Plan) {
|
||||
|
||||
function goPurchase(plan: Plan) {
|
||||
if (!userStore.isLogin) {
|
||||
router.push({path: '/login', query: {redirect: '/vip'}})
|
||||
router.push({ path: '/login', query: { redirect: '/vip' } })
|
||||
return
|
||||
}
|
||||
selectedPlanId = plan.id
|
||||
_nextTick(() => {
|
||||
let el = document.getElementById('pay')
|
||||
el.scrollIntoView({behavior: "smooth"})
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -202,36 +199,39 @@ let startLoop = $ref(false)
|
||||
let orderNo = $ref('')
|
||||
let timer: number = $ref()
|
||||
let showCouponInput = $ref(false)
|
||||
let coupon = $ref<CouponInfo>({code: ''} as CouponInfo)
|
||||
let coupon = $ref<CouponInfo>({ code: '' } as CouponInfo)
|
||||
let checkLoading = $ref(false)
|
||||
let showCheckBtn = $ref(false)
|
||||
|
||||
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()
|
||||
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
|
||||
selectedPlanId = undefined
|
||||
Toast.error(res.msg || '付款失败')
|
||||
}
|
||||
} else {
|
||||
startLoop = false
|
||||
Toast.error(res.msg || '付款失败')
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
showCheckBtn = true
|
||||
}, 3000)
|
||||
} else {
|
||||
clearInterval(timer)
|
||||
showCheckBtn = false
|
||||
})
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
showCheckBtn = true
|
||||
}, 3000)
|
||||
} else {
|
||||
clearInterval(timer)
|
||||
showCheckBtn = false
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
startLoop = false
|
||||
@@ -252,21 +252,21 @@ async function handlePayment() {
|
||||
plan: selectedPlanId,
|
||||
duration: Number(duration),
|
||||
payment_method: selectedPaymentMethod,
|
||||
couponCode: coupon.is_valid ? coupon.code : undefined
|
||||
couponCode: coupon.is_valid ? coupon.code : undefined,
|
||||
}
|
||||
let res = await orderCreate(data)
|
||||
console.log('res', res)
|
||||
if (res.success) {
|
||||
_nextTick(() => {
|
||||
const iframe = document.getElementById('payFrame');
|
||||
const iframe = document.getElementById('payFrame')
|
||||
// 强制重置为 about:blank,让 document 可写
|
||||
iframe.src = 'about:blank';
|
||||
iframe.src = 'about:blank'
|
||||
iframe.onload = () => {
|
||||
const doc = iframe.contentWindow.document;
|
||||
doc.open();
|
||||
doc.write(res.data.result); // 写入 form
|
||||
doc.close(); // form 会自动提交
|
||||
};
|
||||
const doc = iframe.contentWindow.document
|
||||
doc.open()
|
||||
doc.write(res.data.result) // 写入 form
|
||||
doc.close() // form 会自动提交
|
||||
}
|
||||
startLoop = true
|
||||
})
|
||||
orderNo = res.data.orderNo
|
||||
@@ -279,7 +279,7 @@ async function handlePayment() {
|
||||
async function checkOrderStatus() {
|
||||
if (checkLoading) return
|
||||
checkLoading = true
|
||||
let res = await alipayQuery({orderNo})
|
||||
let res = await alipayQuery({ orderNo })
|
||||
if (!res.success) {
|
||||
Toast.info(res.msg || '未付款')
|
||||
}
|
||||
@@ -298,11 +298,11 @@ async function getCouponInfo() {
|
||||
if (res.data.is_valid) {
|
||||
coupon = res.data
|
||||
} else {
|
||||
coupon = {code: coupon.code} as CouponInfo
|
||||
coupon = { code: coupon.code } as CouponInfo
|
||||
Toast.info('优惠券已失效')
|
||||
}
|
||||
} else {
|
||||
coupon = {code: coupon.code} as CouponInfo
|
||||
coupon = { code: coupon.code } as CouponInfo
|
||||
Toast.error(res.msg || '优惠券无效')
|
||||
}
|
||||
couponLoading = false
|
||||
@@ -310,7 +310,6 @@ async function getCouponInfo() {
|
||||
showCouponInput = true
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -320,7 +319,7 @@ async function getCouponInfo() {
|
||||
<Header title="会员介绍"></Header>
|
||||
<div class="grid grid-cols-3 grid-rows-3 gap-3">
|
||||
<div class="text-lg flex items-center" v-for="f in data.benefits" :key="f.name">
|
||||
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
|
||||
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600" />
|
||||
<span>
|
||||
<span>{{ f.name }}</span>
|
||||
<span v-if="f.value !== 'true'">{{ `(${f.value}${f.unit ?? ''})` }}</span>
|
||||
@@ -329,27 +328,22 @@ async function getCouponInfo() {
|
||||
</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 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"/>
|
||||
<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 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"/>
|
||||
<IconFluentArrowRepeatAll20Regular class="mr-1" />
|
||||
<span>自动续费已开启</span>
|
||||
</div>
|
||||
<PopConfirm
|
||||
title="确认取消?"
|
||||
@confirm="toggleAutoRenew"
|
||||
>
|
||||
<PopConfirm title="确认取消?" @confirm="toggleAutoRenew">
|
||||
<BaseButton size="small" type="info" :loading="loading2">关闭</BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
@@ -363,8 +357,7 @@ async function getCouponInfo() {
|
||||
</div>
|
||||
|
||||
<div class="plans">
|
||||
<div v-for="p in plans" :key="p.id"
|
||||
class="card-white p-0 overflow-hidden flex flex-col">
|
||||
<div v-for="p in plans" :key="p.id" 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">
|
||||
@@ -375,12 +368,16 @@ async function getCouponInfo() {
|
||||
<div v-if="p.highlight" class="tag">{{ p.highlight }}</div>
|
||||
</div>
|
||||
<div v-if="p.autoRenew" class="text-sm flex items-center mt-4">
|
||||
<IconFluentArrowRepeatAll20Regular class="mr-2"/>
|
||||
<IconFluentArrowRepeatAll20Regular class="mr-2" />
|
||||
开启自动续费,可随时关闭
|
||||
</div>
|
||||
<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)">
|
||||
<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>
|
||||
@@ -397,25 +394,25 @@ async function getCouponInfo() {
|
||||
|
||||
<div class="center">
|
||||
<div class="card-white w-5/10">
|
||||
<div class="flex items-center justify-between gap-6 ">
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<div class="center gap-2" v-if="!showCouponInput">
|
||||
<IconStreamlineDiscountPercentCoupon/>
|
||||
<IconStreamlineDiscountPercentCoupon />
|
||||
<span>有优惠券?</span>
|
||||
</div>
|
||||
<BaseInput v-else v-model="coupon.code"
|
||||
placeholder="请输入优惠券"
|
||||
autofocus
|
||||
clearable
|
||||
@enter="getCouponInfo"
|
||||
<BaseInput
|
||||
v-else
|
||||
v-model="coupon.code"
|
||||
placeholder="请输入优惠券"
|
||||
autofocus
|
||||
clearable
|
||||
@enter="getCouponInfo"
|
||||
/>
|
||||
<BaseButton size="large"
|
||||
:loading="couponLoading"
|
||||
@click="getCouponInfo">{{ showCouponInput ? '确定' : '在此兑换!' }}
|
||||
<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="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>
|
||||
@@ -426,8 +423,8 @@ async function getCouponInfo() {
|
||||
<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>
|
||||
· 最高减{{ Number(coupon.max_discount).toFixed(2) }}元
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -441,13 +438,16 @@ async function getCouponInfo() {
|
||||
<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 hover:bg-item"
|
||||
:class="selectedPaymentMethod === method.id && 'bg-item'">
|
||||
<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/>
|
||||
<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 color-main">{{ method.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ method.description }}</div>
|
||||
@@ -472,14 +472,13 @@ async function getCouponInfo() {
|
||||
<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="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"/>
|
||||
<InputNumber :min="1" v-model="duration" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -490,7 +489,7 @@ async function getCouponInfo() {
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<div v-if="enoughDiscount" class="text-green-600 flex items-center">
|
||||
<IconStreamlineDiscountPercentCoupon class="mr-2"/>
|
||||
<IconStreamlineDiscountPercentCoupon class="mr-2" />
|
||||
<span>已优惠:¥{{ (Number(originalPrice) - Number(endPrice)).toFixed(2) }}</span>
|
||||
</div>
|
||||
<span v-else>优惠券不可用:未满足条件</span>
|
||||
@@ -503,14 +502,19 @@ async function getCouponInfo() {
|
||||
<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 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" :loading="loading || startLoop"
|
||||
: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>
|
||||
@@ -520,31 +524,22 @@ async function getCouponInfo() {
|
||||
<div class="text-lg font-semibold mb-4">扫码支付</div>
|
||||
<div class="center flex-col relative flex-1">
|
||||
<div class="center h-full w-full absolute left-0 top-0 bg-white z-2" v-if="!startLoop">
|
||||
<div class="w-5/10">
|
||||
请点击左侧付款按钮后,支付二维码将自动显示
|
||||
</div>
|
||||
<div class="w-5/10">请点击左侧付款按钮后,支付二维码将自动显示</div>
|
||||
</div>
|
||||
|
||||
<iframe id="payFrame" class="w-[205px] h-[205px] center border-none"></iframe>
|
||||
<div class="text-center my-4">
|
||||
请使用支付宝扫码支付
|
||||
</div>
|
||||
<BaseButton size="large"
|
||||
v-if="showCheckBtn"
|
||||
:loading="checkLoading"
|
||||
@click="checkOrderStatus">
|
||||
<div class="text-center my-4">请使用支付宝扫码支付</div>
|
||||
<BaseButton size="large" v-if="showCheckBtn" :loading="checkLoading" @click="checkOrderStatus">
|
||||
我已付款
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.pay-dialog {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -564,7 +559,6 @@ async function getCouponInfo() {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
|
||||
.price {
|
||||
@apply flex items-end gap-1;
|
||||
}
|
||||
@@ -183,11 +183,11 @@ function word2Str(word) {
|
||||
res.synos = word.synos.map(v => (v.pos + v.cn + '\n' + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
|
||||
res.relWords = word.relWords.root
|
||||
? '词根:' +
|
||||
word.relWords.root +
|
||||
'\n\n' +
|
||||
word.relWords.rels
|
||||
.map(v => (v.pos + '\n' + v.words.map(v => v.c + ':' + v.cn).join('\n')).replaceAll('"', ''))
|
||||
.join('\n\n')
|
||||
word.relWords.root +
|
||||
'\n\n' +
|
||||
word.relWords.rels
|
||||
.map(v => (v.pos + '\n' + v.words.map(v => v.c + ':' + v.cn).join('\n')).replaceAll('"', ''))
|
||||
.join('\n\n')
|
||||
: ''
|
||||
res.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
|
||||
return res
|
||||
@@ -70,10 +70,10 @@ const searchList = computed<any[]>(() => {
|
||||
let s = searchKey.toLowerCase()
|
||||
return dict_list.value.filter((item) => {
|
||||
return item.id.toLowerCase().includes(s)
|
||||
|| item.name.toLowerCase().includes(s)
|
||||
|| item.category.toLowerCase().includes(s)
|
||||
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|
||||
|| item?.url?.toLowerCase?.().includes?.(s)
|
||||
|| item.name.toLowerCase().includes(s)
|
||||
|| item.category.toLowerCase().includes(s)
|
||||
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|
||||
|| item?.url?.toLowerCase?.().includes?.(s)
|
||||
})
|
||||
}
|
||||
return []
|
||||
@@ -125,31 +125,31 @@ watch(dict_list, (val) => {
|
||||
<div class="py-1 flex flex-1 justify-end" v-else>
|
||||
<span class="page-title absolute w-full center">词典列表</span>
|
||||
<BaseIcon
|
||||
title="搜索"
|
||||
@click="showSearchInput = true"
|
||||
class="z-1"
|
||||
icon="fluent:search-24-regular">
|
||||
title="搜索"
|
||||
@click="showSearchInput = true"
|
||||
class="z-1"
|
||||
icon="fluent:search-24-regular">
|
||||
<IconFluentSearch24Regular/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4" v-if="searchKey">
|
||||
<DictList
|
||||
v-if="searchList.length "
|
||||
@selectDict="selectDict"
|
||||
:list="searchList"
|
||||
quantifier="词"
|
||||
:select-id="'-1'"/>
|
||||
v-if="searchList.length "
|
||||
@selectDict="selectDict"
|
||||
:list="searchList"
|
||||
quantifier="词"
|
||||
:select-id="'-1'"/>
|
||||
<Empty v-else text="没有相关词典"/>
|
||||
</div>
|
||||
<div class="w-full" v-else>
|
||||
<DictGroup
|
||||
v-for="item in groupedByCategoryAndTag"
|
||||
:select-id="store.sdict.id"
|
||||
@selectDict="selectDict"
|
||||
quantifier="词"
|
||||
:groupByTag="item[1]"
|
||||
:category="item[0]"
|
||||
v-for="item in groupedByCategoryAndTag"
|
||||
:select-id="store.sdict.id"
|
||||
@selectDict="selectDict"
|
||||
quantifier="词"
|
||||
:groupByTag="item[1]"
|
||||
:category="item[0]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -241,7 +241,7 @@ onMounted(init)
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.option:hover { background: var(--color-second); }
|
||||
@@ -355,7 +355,7 @@ const systemPracticeText = $computed(() => {
|
||||
{{ isSaveData ? '上次任务' : '今日任务' }}
|
||||
</div>
|
||||
<span class="color-link cursor-pointer" v-if="store.sdict.id" @click="showPracticeWordListDialog = true"
|
||||
>词表</span
|
||||
>词表</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex gap-1 items-center" v-if="store.sdict.id">
|
||||
@@ -1,22 +1,24 @@
|
||||
import * as VueRouter 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";
|
||||
import PracticeArticles from "@/pages/article/PracticeArticles.vue";
|
||||
import DictDetail from "@/pages/word/DictDetail.vue";
|
||||
import PracticeWords from "@/pages/word/PracticeWords.vue";
|
||||
import WordTest from "@/pages/word/WordTest.vue";
|
||||
import BookDetail from "@/pages/article/BookDetail.vue";
|
||||
import DictList from "@/pages/word/DictList.vue";
|
||||
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 Feedback from "@/pages/feedback.vue";
|
||||
import Qa from "@/pages/qa.vue";
|
||||
import Doc from "@/pages/doc.vue";
|
||||
import words from "@/pages/(words)/words.vue";
|
||||
import DictDetail from "@/pages/(words)/dict-detail.vue";
|
||||
import DictList from "@/pages/(words)/dict-list.vue";
|
||||
import PracticeWords from "@/pages/(words)/practice-words/[id].vue";
|
||||
import WordTest from "@/pages/(words)/words-test/[id].vue";
|
||||
|
||||
import articles from "@/pages/(articles)/articles.vue";
|
||||
import BookDetail from "@/pages/(articles)/book-detail.vue";
|
||||
import BookList from "@/pages/(articles)/book-list.vue";
|
||||
import PracticeArticles from "@/pages/(articles)/practice-articles/[id].vue";
|
||||
|
||||
import setting from "@/pages/setting/Setting.vue";
|
||||
import login from "@/pages/(user)/login.vue";
|
||||
import user from "@/pages/(user)/user.vue";
|
||||
import vip from "@/pages/(user)/vip.vue";
|
||||
import feedback from "@/pages/feedback.vue";
|
||||
import qa from "@/pages/qa.vue";
|
||||
import doc from "@/pages/doc.vue";
|
||||
// import { useAuthStore } from "@/stores/user.ts";
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
@@ -25,7 +27,7 @@ export const routes: RouteRecordRaw[] = [
|
||||
component: Layout,
|
||||
children: [
|
||||
{path: '/', redirect: '/words'},
|
||||
{path: 'words', component: WordsPage},
|
||||
{path: 'words', component: words},
|
||||
{path: 'word', redirect: '/words'},
|
||||
{path: 'practice-words/:id', component: PracticeWords},
|
||||
{path: 'word-test/:id', component: WordTest},
|
||||
@@ -33,24 +35,24 @@ export const routes: RouteRecordRaw[] = [
|
||||
{path: 'dict-list', component: DictList},
|
||||
{path: 'dict-detail', component: DictDetail},
|
||||
|
||||
{path: 'articles', component: ArticlesPage},
|
||||
{path: 'articles', component: articles},
|
||||
{path: 'article', redirect: '/articles'},
|
||||
{path: 'practice-articles/:id', component: PracticeArticles},
|
||||
{path: 'study-article', redirect: '/articles'},
|
||||
{path: 'book-detail', component: BookDetail},
|
||||
{path: 'book-list', component: BookList},
|
||||
|
||||
{path: 'login', component: Login},
|
||||
{path: 'user', component: User},
|
||||
{path: 'vip', component: VipIntro},
|
||||
{path: 'login', component: login},
|
||||
{path: 'user', component: user},
|
||||
{path: 'vip', component: vip},
|
||||
|
||||
{path: 'setting', component: Setting},
|
||||
{path: 'feedback', component: Feedback},
|
||||
{path: 'qa', component: Qa},
|
||||
{path: 'doc', component: Doc},
|
||||
{path: 'setting', component: setting},
|
||||
{path: 'feedback', component: feedback},
|
||||
{path: 'qa', component: qa},
|
||||
{path: 'doc', component: doc},
|
||||
]
|
||||
},
|
||||
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},
|
||||
{path: '/batch-edit-article', component: () => import("@/pages/(articles)/batch-edit-article.vue")},
|
||||
{path: '/test', component: () => import("@/pages/test/test.vue")},
|
||||
{path: '/:pathMatch(.*)*', redirect: '/words'},
|
||||
]
|
||||
|
||||
@@ -4,12 +4,12 @@ import type { Dict, DictResource } from '@/types/types.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRuntimeStore } from '@/stores/runtime.ts'
|
||||
import dayjs from 'dayjs'
|
||||
import { AppEnv, DictId, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY } from '@/config/env.ts'
|
||||
import { AppEnv, DictId, ENV, 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 duration from 'dayjs/plugin/duration'
|
||||
import {DictType} from "@/types/enum.ts";
|
||||
import { DictType } from '@/types/enum.ts'
|
||||
|
||||
dayjs.extend(duration)
|
||||
|
||||
@@ -61,9 +61,7 @@ export function checkAndUpgradeSaveDict(val: any) {
|
||||
return defaultState
|
||||
} else {
|
||||
// 版本不匹配时,尽量保留数据而不是直接返回默认状态
|
||||
console.warn(
|
||||
`数据版本不匹配: 当前版本 ${version}, 期望版本 ${SAVE_DICT_KEY.version},尝试保留数据`
|
||||
)
|
||||
console.warn(`数据版本不匹配: 当前版本 ${version}, 期望版本 ${SAVE_DICT_KEY.version},尝试保留数据`)
|
||||
try {
|
||||
checkRiskKey(defaultState, state)
|
||||
// 尝试保留 bookList 数据
|
||||
@@ -136,8 +134,7 @@ export function checkAndUpgradeSaveSetting(val: any) {
|
||||
export function shakeCommonDict(n: BaseState): BaseState {
|
||||
let data: BaseState = cloneDeep(n)
|
||||
data.word.bookList.map((v: Dict) => {
|
||||
if (!v.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(v.id))
|
||||
v.words = []
|
||||
if (!v.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(v.id)) v.words = []
|
||||
})
|
||||
data.article.bookList.map((v: Dict) => {
|
||||
if (!v.custom && ![DictId.articleCollect].includes(v.id)) v.articles = []
|
||||
@@ -248,14 +245,11 @@ export async function sleep(time: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, time))
|
||||
}
|
||||
|
||||
export async function _getDictDataByUrl(
|
||||
val: DictResource,
|
||||
type: DictType = DictType.word
|
||||
): Promise<Dict> {
|
||||
export async function _getDictDataByUrl(val: DictResource, type: DictType = DictType.word): Promise<Dict> {
|
||||
// await sleep(2000);
|
||||
let dictResourceUrl = `https://dicts.2study.top/dicts/${val.language}/word/${val.url}`
|
||||
let dictResourceUrl = ENV.RESOURCE_URL + `dicts/${val.language}/word/${val.url}`
|
||||
if (type === DictType.article) {
|
||||
dictResourceUrl = `https://dicts.2study.top/dicts/${val.language}/article/${val.url}`
|
||||
dictResourceUrl = ENV.RESOURCE_URL + `dicts/${val.language}/article/${val.url}`
|
||||
}
|
||||
let s = await fetch(resourceWrap(dictResourceUrl, val.version)).then(r => r.json())
|
||||
if (s) {
|
||||
@@ -271,8 +265,7 @@ export async function _getDictDataByUrl(
|
||||
//从字符串里面转换为Word格式
|
||||
export function convertToWord(raw: any) {
|
||||
const safeString = str => (typeof str === 'string' ? str.trim() : '')
|
||||
const safeSplit = (str, sep) =>
|
||||
safeString(str) ? safeString(str).split(sep).filter(Boolean) : []
|
||||
const safeSplit = (str, sep) => (safeString(str) ? safeString(str).split(sep).filter(Boolean) : [])
|
||||
|
||||
// 1. trans
|
||||
const trans = safeSplit(raw.trans, '\n').map(line => {
|
||||
@@ -508,9 +501,7 @@ export async function isNewUser() {
|
||||
let base = useBaseStore()
|
||||
console.log(JSON.stringify(base.$state))
|
||||
console.log(JSON.stringify(getDefaultBaseState()))
|
||||
return (
|
||||
JSON.stringify(base.$state) === JSON.stringify({ ...getDefaultBaseState(), ...{ load: true } })
|
||||
)
|
||||
return JSON.stringify(base.$state) === JSON.stringify({ ...getDefaultBaseState(), ...{ load: true } })
|
||||
}
|
||||
|
||||
export function jump2Feedback() {
|
||||
|
||||
Reference in New Issue
Block a user