This commit is contained in:
Zyronon
2026-01-07 19:36:19 +08:00
committed by GitHub
parent ce2063f54c
commit 11606dfb68
18 changed files with 418 additions and 396 deletions

View 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>

View 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>

View File

@@ -13,6 +13,7 @@ const common = {
const map = {
DEV: {
API: 'http://localhost/',
RESOURCE_URL: 'https://dicts.2study.top/',
},
}

View File

@@ -283,7 +283,7 @@ function updateList(e) {
:loading="exportLoading"
:disabled="!article.id"
@click="exportData({ type: 'item', data: article })"
>当前
>当前
</BaseButton>
</div>
</MiniDialog>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'
//

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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>

View File

@@ -241,7 +241,7 @@ onMounted(init)
</div>
</div>
</BasePage>
</template>
</template>
<style scoped>
.option:hover { background: var(--color-second); }

View File

@@ -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">

View File

@@ -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'},
]

View File

@@ -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() {