Merge branch 'dev' into master
This commit is contained in:
@@ -157,9 +157,7 @@ const sentence = $computed(() => {
|
||||
</div>
|
||||
|
||||
<!-- 学习总结分享图片生成对话框 -->
|
||||
<Dialog
|
||||
v-model="showShareDialog"
|
||||
title="分享" :close-on-click-bg="true" custom-class="!max-w-4xl !w-auto">
|
||||
<Dialog v-model="showShareDialog" title="分享">
|
||||
<div class="flex min-w-160 max-w-200 p-6 pt-0 gap-space">
|
||||
<!-- 左侧:海报预览区域 -->
|
||||
<div ref="posterEl" class="flex-1 border-r border-gray-200 bg-gray-100 rounded-xl overflow-hidden relative">
|
||||
|
||||
@@ -82,7 +82,6 @@ async function transfer() {
|
||||
Toast.success('迁移完成')
|
||||
model.value = false
|
||||
emit('ok')
|
||||
|
||||
} catch (e) {
|
||||
Toast.error('迁移失败:' + e)
|
||||
console.error('迁移失败', e);
|
||||
@@ -91,21 +90,21 @@ async function transfer() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model="model" title="迁移数据">
|
||||
<div class="px-4 flex-col center text-align-center w-100">
|
||||
<h2>
|
||||
<Dialog v-model="model"
|
||||
:footer="true"
|
||||
@ok="transfer"
|
||||
confirmButtonText="迁移数据"
|
||||
title="迁移数据">
|
||||
<div class="px-4 flex-col center w-100">
|
||||
<h2 class="text-align-center">
|
||||
本网站已启用新域名 <span class="color-blue">{{ Origin }}</span>
|
||||
</h2>
|
||||
<h3>
|
||||
老域名即将停用,由于浏览器安全限制,新老网站数据无法互通,需要您手动点击转移数据
|
||||
</h3>
|
||||
<h3>
|
||||
<BaseButton
|
||||
size="large"
|
||||
@click="transfer">
|
||||
转移数据
|
||||
</BaseButton>
|
||||
</h3>
|
||||
<div>
|
||||
如果您不想此时迁移,关闭弹窗后,您可随时在“设置” -> “数据管理” 里面再次进行
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -251,7 +251,7 @@ onBeforeUnmount(() => {
|
||||
.select__dropdown {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
background-color: var(--color-input-bg);
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
@@ -269,13 +269,13 @@ onBeforeUnmount(() => {
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
background-color: var(--color-item-hover);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: var(--color-select-bg);
|
||||
font-weight: bold;
|
||||
background-color: #f5f7fa;
|
||||
background-color: var(--color-item-active);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { onMounted, onUnmounted, watch, onDeactivated } from "vue";
|
||||
import { onDeactivated, onMounted, onUnmounted, watch } from "vue";
|
||||
import { emitter, EventKey } from "@/utils/eventBus.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { ShortcutKey } from "@/types/types.ts";
|
||||
import { isMobile } from "@/utils";
|
||||
|
||||
export function useWindowClick(cb: (e: PointerEvent) => void) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import dayjs from "dayjs";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import isoWeek from 'dayjs/plugin/isoWeek'
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { AppEnv, DICT_LIST, PracticeSaveArticleKey } from "@/config/env.ts";
|
||||
import {AppEnv, DICT_LIST, Host, PracticeSaveArticleKey} from "@/config/env.ts";
|
||||
import { myDictList } from "@/apis";
|
||||
|
||||
dayjs.extend(isoWeek)
|
||||
@@ -162,11 +162,16 @@ const weekList = $computed(() => {
|
||||
|
||||
const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.ARTICLE.RECOMMENDED)).json()
|
||||
|
||||
let isNewHost = $ref(window.location.host === Host)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="mb-4" v-if="!isNewHost">
|
||||
新域名已启用,后续请访问 <a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前 2study.top 域名将在不久后停止使用
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col md:flex-row justify-between gap-space p-4 md:p-6">
|
||||
<div class="">
|
||||
<Book
|
||||
|
||||
@@ -274,7 +274,7 @@ function next() {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-detail-card {
|
||||
min-height: calc(100vh - 3rem);
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
|
||||
.dict-header {
|
||||
@@ -287,7 +287,7 @@ function next() {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dict-detail-card {
|
||||
min-height: calc(100vh - 2rem);
|
||||
height: calc(100vh - 2rem);
|
||||
}
|
||||
|
||||
.dict-header {
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Article, Sentence, TranslateEngine} from "@/types/types.ts";
|
||||
import { Article, Sentence, TranslateEngine } from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import EditAbleText from "@/components/EditAbleText.vue";
|
||||
import {getNetworkTranslate, getSentenceAllText, getSentenceAllTranslateText} from "@/hooks/translate.ts";
|
||||
import {genArticleSectionData, splitCNArticle2, splitEnArticle2, usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
import {_nextTick, _parseLRC, cloneDeep, last} from "@/utils";
|
||||
import {defineAsyncComponent, watch} from "vue";
|
||||
import { getNetworkTranslate, getSentenceAllText, getSentenceAllTranslateText } from "@/hooks/translate.ts";
|
||||
import { genArticleSectionData, splitCNArticle2, splitEnArticle2, usePlaySentenceAudio } from "@/hooks/article.ts";
|
||||
import { _nextTick, _parseLRC, cloneDeep, last } from "@/utils";
|
||||
import { defineAsyncComponent, watch } from "vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import * as Comparison from "string-comparison"
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {getDefaultArticle} from "@/types/func.ts";
|
||||
import { getDefaultArticle } from "@/types/func.ts";
|
||||
import copy from "copy-to-clipboard";
|
||||
import {Option, Select} from "@/components/base/select";
|
||||
import { Option, Select } from "@/components/base/select";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import {nanoid} from "nanoid";
|
||||
import {update} from "idb-keyval";
|
||||
import { nanoid } from "nanoid";
|
||||
import { update } from "idb-keyval";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import { LOCAL_FILE_KEY } from "@/config/env.ts";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
@@ -52,7 +53,7 @@ const TranslateEngineOptions = [
|
||||
let editArticle = $ref<Article>(getDefaultArticle())
|
||||
|
||||
watch(() => props.article, val => {
|
||||
editArticle = cloneDeep(val)
|
||||
editArticle = getDefaultArticle(val)
|
||||
progress = 0
|
||||
failCount = 0
|
||||
apply(false)
|
||||
@@ -230,9 +231,31 @@ let editSentence = $ref<Sentence>({} as any)
|
||||
let preSentence = $ref<Sentence>({} as any)
|
||||
let showEditAudioDialog = $ref(false)
|
||||
let showAudioDialog = $ref(false)
|
||||
let showNameDialog = $ref(false)
|
||||
let sentenceAudioRef = $ref<HTMLAudioElement>()
|
||||
let audioRef = $ref<HTMLAudioElement>()
|
||||
|
||||
let nameListRef = $ref<string[]>([])
|
||||
watch(() => showNameDialog, (v) => {
|
||||
if (v) {
|
||||
nameListRef = cloneDeep(Array.isArray(editArticle.nameList) ? editArticle.nameList : [])
|
||||
nameListRef.push('')
|
||||
}
|
||||
})
|
||||
|
||||
function addName() {
|
||||
nameListRef.push('')
|
||||
}
|
||||
|
||||
function removeName(i: number) {
|
||||
nameListRef.splice(i, 1)
|
||||
}
|
||||
|
||||
function saveNameList() {
|
||||
const cleaned = Array.from(new Set(nameListRef.map(s => (s ?? '').trim()).filter(Boolean)))
|
||||
editArticle.nameList = cleaned as any
|
||||
}
|
||||
|
||||
function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
|
||||
showEditAudioDialog = true
|
||||
currentSentence = val
|
||||
@@ -307,7 +330,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
val.audioPosition[0] = Number(Number(audioRef.currentTime).toFixed(2))
|
||||
}
|
||||
if (val.audioPosition[0] > val.audioPosition[1] && val.audioPosition[1] !== 0) {
|
||||
val.audioPosition[1] = val.audioPosition[0]
|
||||
val.audioPosition[1] = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +349,15 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
placeholder="请填写原文标题"
|
||||
/>
|
||||
</div>
|
||||
<div class="">正文:<span class="text-sm color-gray">一行一句,段落间空一行</span></div>
|
||||
<div class="flex justify-between">
|
||||
<span>正文:<span class="text-sm color-gray">一行一句,段落间空一行</span></span>
|
||||
<Tooltip title="配置人名之后,在练习时自动忽略(可选,默认开启)">
|
||||
<div @click="showNameDialog = true" class="center gap-1 cp">
|
||||
<span>人名配置</span>
|
||||
<IconFluentSettings20Regular/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Textarea v-model="editArticle.text"
|
||||
class="h-full"
|
||||
:disabled="![100,0].includes(progress)"
|
||||
@@ -609,6 +640,31 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog title="人名管理"
|
||||
v-model="showNameDialog"
|
||||
:footer="true"
|
||||
@close="showNameDialog = false"
|
||||
@ok="saveNameList"
|
||||
>
|
||||
<div class="p-4 pt-0 color-main w-150 flex flex-col gap-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-base">配置需要忽略的人名,练习时自动忽略这些名称(可选,默认开启)</div>
|
||||
<BaseButton type="info" @click="addName">添加名称</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2" v-for="(name,i) in nameListRef" :key="i">
|
||||
<BaseInput v-model="nameListRef[i]"
|
||||
placeholder="输入名称"
|
||||
size="large"
|
||||
:autofocus="i===nameListRef.length-1"/>
|
||||
<BaseButton type="info" @click="removeName(i)">删除</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -703,65 +759,65 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
flex-direction: column;
|
||||
padding: 0.5rem;
|
||||
gap: 1rem;
|
||||
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
|
||||
|
||||
&:nth-child(3) {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
|
||||
// 表单元素优化
|
||||
.base-input, .base-textarea {
|
||||
width: 100%;
|
||||
font-size: 16px; // 防止iOS自动缩放
|
||||
}
|
||||
|
||||
|
||||
.base-textarea {
|
||||
min-height: 150px;
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
|
||||
// 按钮组优化
|
||||
.flex.gap-2 {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
|
||||
.base-button {
|
||||
min-height: 44px;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 文章翻译区域优化
|
||||
.article-translate {
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
|
||||
.sentence {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem;
|
||||
|
||||
|
||||
.flex-\[7\] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.flex-\[2\] {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
|
||||
|
||||
.flex.justify-end.gap-2 {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
@@ -771,17 +827,17 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 选项区域优化
|
||||
.options {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
|
||||
|
||||
.status {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
.warning, .success {
|
||||
font-size: 1rem;
|
||||
}
|
||||
@@ -793,12 +849,12 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
@media (max-width: 480px) {
|
||||
.content {
|
||||
padding: 0.3rem;
|
||||
|
||||
|
||||
.row {
|
||||
.base-textarea {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
|
||||
.flex.gap-2 {
|
||||
.base-button {
|
||||
min-width: 100px;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio} from "@/hooks/sound.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import { _dateFormat, _nextTick, isMobile, msToHourMinute, total } from "@/utils";
|
||||
import {_dateFormat, _nextTick, isMobile, msToHourMinute, total} from "@/utils";
|
||||
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
|
||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
@@ -19,6 +19,7 @@ import nlp from "compromise/three";
|
||||
import {nanoid} from "nanoid";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {PracticeSaveArticleKey} from "@/config/env.ts";
|
||||
import {retry} from "ali-oss/lib/common/utils/retry";
|
||||
|
||||
interface IProps {
|
||||
article: Article,
|
||||
@@ -150,6 +151,10 @@ function init() {
|
||||
})
|
||||
typeArticleRef?.scrollTo({top: 0, behavior: "smooth"})
|
||||
}
|
||||
_nextTick(() => {
|
||||
emit('play', {sentence: props.article.sections[sectionIndex][sentenceIndex], handle: false})
|
||||
if (isNameWord()) next()
|
||||
})
|
||||
checkTranslateLocation().then(() => checkCursorPosition())
|
||||
focusMobileInput()
|
||||
}
|
||||
@@ -222,8 +227,10 @@ function processMobileCharacter(char: string) {
|
||||
const fakeEvent = {
|
||||
key: char,
|
||||
code,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
preventDefault() {
|
||||
},
|
||||
stopPropagation() {
|
||||
},
|
||||
} as unknown as KeyboardEvent
|
||||
onTyping(fakeEvent)
|
||||
}
|
||||
@@ -247,6 +254,21 @@ function handleMobileBeforeInput(event: InputEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const normalize = (s: string) => s.toLowerCase().trim()
|
||||
const namePatterns = $computed(() => {
|
||||
return Array.from(new Set((props.article?.nameList ?? []).map(normalize).filter(Boolean).map(s => s.split(/\s+/).filter(Boolean)).flat().concat([
|
||||
'Mr', 'Mrs', 'Ms', 'Dr', 'Miss',
|
||||
].map(normalize))))
|
||||
})
|
||||
|
||||
const isNameWord = () => {
|
||||
let currentSection = props.article.sections[sectionIndex]
|
||||
let currentSentence = currentSection[sentenceIndex]
|
||||
let w: ArticleWord = currentSentence.words[wordIndex]
|
||||
return w?.type === PracticeArticleWordType.Word && namePatterns.length > 0 && namePatterns.includes(normalize(w.word))
|
||||
}
|
||||
|
||||
let isTyping = false
|
||||
//专用锁,因为这个方法父级要调用
|
||||
let lock = false
|
||||
@@ -279,15 +301,47 @@ function nextSentence() {
|
||||
isEnd = true
|
||||
emit('complete')
|
||||
} else {
|
||||
if (isNameWord()) next()
|
||||
emit('play', {sentence: props.article.sections[sectionIndex][0], handle: false})
|
||||
}
|
||||
} else {
|
||||
if (isNameWord()) next()
|
||||
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
|
||||
|
||||
}
|
||||
lock = false
|
||||
focusMobileInput()
|
||||
}
|
||||
|
||||
|
||||
const next = () => {
|
||||
isSpace = false;
|
||||
input = wrong = ''
|
||||
stringIndex = 0;
|
||||
|
||||
let currentSection = props.article.sections[sectionIndex]
|
||||
let currentSentence = currentSection[sentenceIndex]
|
||||
let currentWord: ArticleWord = currentSentence.words[wordIndex]
|
||||
|
||||
// 检查下一个单词是否存在
|
||||
if (wordIndex + 1 < currentSentence.words.length) {
|
||||
wordIndex++;
|
||||
currentWord = currentSentence.words[wordIndex]
|
||||
//这里把未输入的单词补全,因为删除时会用到input
|
||||
currentSentence.words.slice(0, wordIndex).forEach((word, i) => {
|
||||
word.input = word.input + word.word.slice(word.input?.length ?? 0)
|
||||
})
|
||||
if ([PracticeArticleWordType.Symbol, PracticeArticleWordType.Number].includes(currentWord.type) && settingStore.ignoreSymbol) {
|
||||
next()
|
||||
} else if (isNameWord()) {
|
||||
next()
|
||||
} else {
|
||||
emit('nextWord', currentWord);
|
||||
}
|
||||
} else {
|
||||
nextSentence()
|
||||
}
|
||||
}
|
||||
|
||||
function onTyping(e: KeyboardEvent) {
|
||||
debugger
|
||||
if (!props.article.sections.length) return
|
||||
@@ -300,24 +354,6 @@ function onTyping(e: KeyboardEvent) {
|
||||
let currentWord: ArticleWord = currentSentence.words[wordIndex]
|
||||
wrong = ''
|
||||
|
||||
const next = () => {
|
||||
isSpace = false;
|
||||
input = wrong = ''
|
||||
stringIndex = 0;
|
||||
// 检查下一个单词是否存在
|
||||
if (wordIndex + 1 < currentSentence.words.length) {
|
||||
wordIndex++;
|
||||
currentWord = currentSentence.words[wordIndex]
|
||||
if ([PracticeArticleWordType.Symbol,PracticeArticleWordType.Number].includes(currentWord.type) && settingStore.ignoreSymbol){
|
||||
next()
|
||||
}else {
|
||||
emit('nextWord', currentWord);
|
||||
}
|
||||
} else {
|
||||
nextSentence()
|
||||
}
|
||||
}
|
||||
|
||||
if (isSpace) {
|
||||
if (e.code === 'Space') {
|
||||
next()
|
||||
@@ -334,10 +370,13 @@ function onTyping(e: KeyboardEvent) {
|
||||
// }, 500)
|
||||
}
|
||||
} else {
|
||||
//如果是首句首词
|
||||
if (sectionIndex === 0 && sentenceIndex === 0 && wordIndex === 0 && stringIndex === 0) {
|
||||
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
|
||||
}
|
||||
|
||||
// if (isNameWord(currentWord)) {
|
||||
// isSpace = false
|
||||
// next()
|
||||
// isTyping = false
|
||||
// return onTyping(e)
|
||||
// }
|
||||
let letter = e.key
|
||||
let key = currentWord.word[stringIndex]
|
||||
// console.log('key', key,)
|
||||
@@ -376,7 +415,7 @@ function onTyping(e: KeyboardEvent) {
|
||||
//todo 上报
|
||||
localStorage.removeItem(PracticeSaveArticleKey.key)
|
||||
init()
|
||||
}finally {
|
||||
} finally {
|
||||
isTyping = false
|
||||
}
|
||||
}
|
||||
@@ -599,19 +638,22 @@ const currentPractice = inject('currentPractice', [])
|
||||
<template>
|
||||
<div class="typing-article" ref="typeArticleRef" @click="focusMobileInput">
|
||||
<input
|
||||
v-if="isMob"
|
||||
ref="mobileInputRef"
|
||||
class="mobile-input"
|
||||
type="text"
|
||||
inputmode="text"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
@beforeinput="handleMobileBeforeInput"
|
||||
@input="handleMobileInput"
|
||||
v-if="isMob"
|
||||
ref="mobileInputRef"
|
||||
class="mobile-input"
|
||||
type="text"
|
||||
inputmode="text"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
@beforeinput="handleMobileBeforeInput"
|
||||
@input="handleMobileInput"
|
||||
/>
|
||||
<header class="mb-4">
|
||||
<div class="title word"><span class="font-family text-3xl">{{ store.sbook.lastLearnIndex + 1 }}.</span>{{ props.article.title }}</div>
|
||||
<div class="title word"><span class="font-family text-3xl">{{
|
||||
store.sbook.lastLearnIndex + 1
|
||||
}}.</span>{{ props.article.title }}
|
||||
</div>
|
||||
<div class="titleTranslate" v-if="settingStore.translate">{{ props.article.titleTranslate }}</div>
|
||||
</header>
|
||||
|
||||
@@ -624,10 +666,10 @@ const currentPractice = inject('currentPractice', [])
|
||||
<span class="sentence"
|
||||
v-for="(sentence,indexJ) in section">
|
||||
<span
|
||||
v-for="(word,indexW) in sentence.words"
|
||||
@contextmenu="e=>onContextMenu(e,sentence,indexI,indexJ,indexW)"
|
||||
class="word"
|
||||
:class="[(sectionIndex>indexI
|
||||
v-for="(word,indexW) in sentence.words"
|
||||
@contextmenu="e=>onContextMenu(e,sentence,indexI,indexJ,indexW)"
|
||||
class="word"
|
||||
:class="[(sectionIndex>indexI
|
||||
?'wrote':
|
||||
(sectionIndex>=indexI &&sentenceIndex>indexJ)
|
||||
?'wrote' :
|
||||
@@ -654,16 +696,16 @@ const currentPractice = inject('currentPractice', [])
|
||||
<span class="border-bottom" v-if="settingStore.dictation"></span>
|
||||
</span>
|
||||
<Space
|
||||
v-if="word.nextSpace"
|
||||
class="word-end"
|
||||
:is-wrong="false"
|
||||
:is-wait="isCurrent(indexI,indexJ,indexW) && isSpace"
|
||||
:is-shake="isCurrent(indexI,indexJ,indexW) && isSpace && wrong !== ''"
|
||||
v-if="word.nextSpace"
|
||||
class="word-end"
|
||||
:is-wrong="false"
|
||||
:is-wait="isCurrent(indexI,indexJ,indexW) && isSpace"
|
||||
:is-shake="isCurrent(indexI,indexJ,indexW) && isSpace && wrong !== ''"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="sentence-translate-mobile"
|
||||
v-if="isMob && settingStore.translate && sentence.translate">
|
||||
class="sentence-translate-mobile"
|
||||
v-if="isMob && settingStore.translate && sentence.translate">
|
||||
{{ sentence.translate }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -693,11 +735,11 @@ const currentPractice = inject('currentPractice', [])
|
||||
|
||||
<div class="options flex justify-center" v-if="isEnd">
|
||||
<BaseButton
|
||||
@click="emit('replay')">重新练习
|
||||
@click="emit('replay')">重新练习
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="store.currentBook.lastLearnIndex < store.currentBook.articles.length - 1"
|
||||
@click="emit('next')">下一篇
|
||||
v-if="store.currentBook.lastLearnIndex < store.currentBook.articles.length - 1"
|
||||
@click="emit('next')">下一篇
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
@@ -881,7 +923,7 @@ $article-lh: 2.4;
|
||||
width: 100vw;
|
||||
max-width: 100%;
|
||||
padding: 1rem 0.5rem;
|
||||
|
||||
|
||||
// 标题优化
|
||||
header {
|
||||
.title {
|
||||
@@ -889,31 +931,31 @@ $article-lh: 2.4;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
|
||||
.font-family {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.titleTranslate {
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 句子显示优化
|
||||
.article-content {
|
||||
article {
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
|
||||
.sentence {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
|
||||
.word {
|
||||
.word-wrap {
|
||||
padding: 0.1rem 0.05rem;
|
||||
@@ -936,16 +978,16 @@ $article-lh: 2.4;
|
||||
font-family: var(--zh-article-family);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
// 翻译区域优化
|
||||
.translate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
// 问答表单优化
|
||||
.question-form {
|
||||
padding: 0.5rem;
|
||||
|
||||
|
||||
.base-button {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
@@ -958,21 +1000,21 @@ $article-lh: 2.4;
|
||||
@media (max-width: 480px) {
|
||||
.typing-article {
|
||||
padding: 0.5rem 0.3rem;
|
||||
|
||||
|
||||
header {
|
||||
.title {
|
||||
font-size: 1rem;
|
||||
|
||||
|
||||
.font-family {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.titleTranslate {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.article-content {
|
||||
article {
|
||||
.section {
|
||||
@@ -983,7 +1025,7 @@ $article-lh: 2.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.sentence-translate-mobile {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.35;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {useRouter} from "vue-router";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import { jump2Feedback } from "@/utils";
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
@@ -44,10 +45,14 @@ function goHome() {
|
||||
<span v-if="settingStore.sideExpand">设置</span>
|
||||
<div class="red-point" :class="!settingStore.sideExpand && 'top-1 right-0'" v-if="runtimeStore.isNew"></div>
|
||||
</div>
|
||||
<!-- <div class="row" @click="router.push('/user')">-->
|
||||
<!-- <IconFluentPerson20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
|
||||
<!-- </div>-->
|
||||
<div class="row" @click="jump2Feedback">
|
||||
<IconFluentCommentEdit20Regular/>
|
||||
<span v-if="settingStore.sideExpand">建议反馈</span>
|
||||
</div>
|
||||
<div class="row" @click="router.push('/user')">
|
||||
<IconFluentPerson20Regular/>
|
||||
<span v-if="settingStore.sideExpand">用户</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom flex justify-evenly ">
|
||||
<BaseIcon
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, watch } from "vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { getAudioFileUrl, usePlayAudio } from "@/hooks/sound.ts";
|
||||
import { getShortcutKey, useEventListener } from "@/hooks/event.ts";
|
||||
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, shakeCommonDict } from "@/utils";
|
||||
import { DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode } from "@/types/types.ts";
|
||||
import {nextTick, onMounted, ref, watch} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {getAudioFileUrl, usePlayAudio} from "@/hooks/sound.ts";
|
||||
import {getShortcutKey, useEventListener} from "@/hooks/event.ts";
|
||||
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, shakeCommonDict} from "@/utils";
|
||||
import {DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode} from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { saveAs } from "file-saver";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {saveAs} from "file-saver";
|
||||
import {
|
||||
APP_NAME, APP_VERSION, EMAIL,
|
||||
EXPORT_DATA_KEY, GITHUB,
|
||||
EXPORT_DATA_KEY, GITHUB, Host,
|
||||
LOCAL_FILE_KEY,
|
||||
Origin,
|
||||
PracticeSaveArticleKey,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import dayjs from "dayjs";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import { Option, Select } from "@/components/base/select";
|
||||
import {Option, Select} from "@/components/base/select";
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
|
||||
@@ -29,10 +29,11 @@ import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import { get, set } from "idb-keyval";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { useUserStore } from "@/stores/user.ts";
|
||||
import { useExport } from "@/hooks/export.ts";
|
||||
import {get, set} from "idb-keyval";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useUserStore} from "@/stores/user.ts";
|
||||
import {useExport} from "@/hooks/export.ts";
|
||||
import MigrateDialog from "@/components/MigrateDialog.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleDisabledDialogEscKey: [val: boolean]
|
||||
@@ -42,7 +43,6 @@ const tabIndex = $ref(0)
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const store = useBaseStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
//@ts-ignore
|
||||
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
|
||||
@@ -99,7 +99,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
} else {
|
||||
// 忽略单独的修饰键
|
||||
if (shortcutKey === 'Ctrl+' || shortcutKey === 'Alt+' || shortcutKey === 'Shift+' ||
|
||||
e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift') {
|
||||
e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,6 +300,15 @@ function importOldData() {
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
let isNewHost = $ref(window.location.host === Host)
|
||||
|
||||
let showTransfer = $ref(false)
|
||||
function transferOk() {
|
||||
setTimeout(() => {
|
||||
window.location.href = '/words'
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -372,8 +381,8 @@ function importOldData() {
|
||||
v-if="settingStore.ignoreSimpleWord"
|
||||
>
|
||||
<Textarea
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 音效-->
|
||||
@@ -401,16 +410,16 @@ function importOldData() {
|
||||
class="w-50!"
|
||||
>
|
||||
<Option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
@@ -528,16 +537,16 @@ function importOldData() {
|
||||
<SettingItem mainTitle="字体设置"/>
|
||||
<SettingItem title="外语字体">
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
<span class="w-10 pl-5">{{ settingStore.fontSize.wordForeignFontSize }}px</span>
|
||||
</SettingItem>
|
||||
<SettingItem title="中文字体">
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
<span class="w-10 pl-5">{{ settingStore.fontSize.wordTranslateFontSize }}px</span>
|
||||
</SettingItem>
|
||||
</div>
|
||||
@@ -550,7 +559,6 @@ function importOldData() {
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="音效"/>
|
||||
<SettingItem title="自动播放句子">
|
||||
<Switch v-model="settingStore.articleSound"/>
|
||||
@@ -568,7 +576,7 @@ function importOldData() {
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="输入时忽略符号/数字">
|
||||
<SettingItem title="输入时忽略符号/数字/人名">
|
||||
<Switch v-model="settingStore.ignoreSymbol"/>
|
||||
</SettingItem>
|
||||
</div>
|
||||
@@ -586,7 +594,7 @@ function importOldData() {
|
||||
<input ref="shortcutInput" :value="item[1]?item[1]:'未设置快捷键'" readonly type="text"
|
||||
@blur="handleInputBlur">
|
||||
<span @click.stop="editShortcutKey = ''">按键盘进行设置,<span
|
||||
class="text-red!">设置完成点击这里</span></span>
|
||||
class="text-red!">设置完成点击这里</span></span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="item[1]">{{ item[1] }}</div>
|
||||
@@ -623,14 +631,39 @@ function importOldData() {
|
||||
@change="importData">
|
||||
</div>
|
||||
<PopConfirm
|
||||
title="导入老版本数据前,请先备份当前数据,确定要导入老版本数据吗?"
|
||||
@confirm="importOldData">
|
||||
title="导入老版本数据前,请先备份当前数据,确定要导入老版本数据吗?"
|
||||
@confirm="importOldData">
|
||||
<BaseButton>老版本数据导入</BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
|
||||
<template v-if="isNewHost">
|
||||
<div class="line my-3"></div>
|
||||
<div>请注意,如果本地已有使用记录,请先备份当前数据,迁移数据后将<b class="text-red"> 完全覆盖 </b>当前所有数据,请谨慎操作。
|
||||
</div>
|
||||
<div class="flex gap-space mt-3">
|
||||
<BaseButton @click="showTransfer = true">迁移 2study.top 网站数据</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="tabIndex === 5">
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>日期:2025/11/25</div>
|
||||
<div>内容:文章练习新增人名忽略功能(新概念一已全部适配),上传了新概念(一)1-18 音频</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>日期:2025/11/23</div>
|
||||
<div>内容:优化练习完成结算界面,新增分享功能</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
@@ -782,29 +815,6 @@ function importOldData() {
|
||||
|
||||
<div v-if="tabIndex === 6" class="center flex-col">
|
||||
<h1>Type Words</h1>
|
||||
|
||||
<!-- 用户信息部分 -->
|
||||
<div v-if="userStore.isLoggedIn && userStore.user" class="user-info-section mb-6">
|
||||
<div class="user-avatar mb-4">
|
||||
<img v-if="userStore.user.avatar" :src="userStore.user.avatar" alt="头像" class="avatar-img"/>
|
||||
<div v-else class="avatar-placeholder">
|
||||
{{ userStore.user.nickname?.charAt(0) || 'U' }}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mb-2">{{ userStore.user.nickname || '用户' }}</h3>
|
||||
<p v-if="userStore.user.email" class="text-sm color-gray mb-1">{{ userStore.user.email }}</p>
|
||||
<p v-if="userStore.user.phone" class="text-sm color-gray">{{ userStore.user.phone }}</p>
|
||||
|
||||
<BaseButton
|
||||
@click="userStore.logout"
|
||||
type="info"
|
||||
class="mt-4"
|
||||
:loading="userStore.isLoading"
|
||||
>
|
||||
退出登录
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<p class="w-100 text-xl">
|
||||
感谢使用本项目!本项目是开源项目,如果觉得有帮助,请在 GitHub 点个 Star,您的支持是我持续改进的动力。
|
||||
</p>
|
||||
@@ -821,10 +831,14 @@ function importOldData() {
|
||||
Build {{ gitLastCommitHash }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
<MigrateDialog
|
||||
v-model="showTransfer"
|
||||
@ok="transferOk"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -1038,85 +1052,85 @@ function importOldData() {
|
||||
@media (max-width: 768px) {
|
||||
.setting {
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
.left {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 2px solid gainsboro;
|
||||
|
||||
|
||||
.tabs {
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
padding: 0.5rem;
|
||||
gap: 0.3rem;
|
||||
|
||||
|
||||
.tab {
|
||||
white-space: nowrap;
|
||||
padding: 0.4rem 0.6rem;
|
||||
font-size: 0.9rem;
|
||||
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.content {
|
||||
padding: 0 1rem;
|
||||
|
||||
|
||||
.row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
min-height: auto;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
|
||||
|
||||
.set-key {
|
||||
width: 100%;
|
||||
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 补充:选择器和输入框优化
|
||||
.base-select, .base-input {
|
||||
width: 100% !important;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
|
||||
// 单选按钮组优化
|
||||
.radio-group {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
|
||||
.radio {
|
||||
min-height: 44px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 滑块优化
|
||||
.slider {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.main-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.item-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.body {
|
||||
height: auto;
|
||||
max-height: 60vh;
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script setup lang="tsx">
|
||||
import {onBeforeUnmount, onMounted} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {APP_NAME} from "@/config/env.ts";
|
||||
import {useUserStore} from "@/stores/user.ts";
|
||||
import {loginApi, LoginParams, registerApi, resetPasswordApi} from "@/apis/user.ts";
|
||||
import {accountRules, codeRules, passwordRules, phoneRules} from "@/utils/validation.ts";
|
||||
import { APP_NAME } from "@/config/env.ts";
|
||||
import { useUserStore } from "@/stores/user.ts";
|
||||
import { loginApi, LoginParams, registerApi, resetPasswordApi } from "@/apis/user.ts";
|
||||
import { accountRules, codeRules, passwordRules, phoneRules } from "@/utils/validation.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import Form from "@/components/base/form/Form.vue";
|
||||
import Notice from "@/pages/user/Notice.vue";
|
||||
import {FormInstance} from "@/components/base/form/types.ts";
|
||||
import {PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts";
|
||||
import {CodeType, ImportStatus} from "@/types/types.ts";
|
||||
import { FormInstance } from "@/components/base/form/types.ts";
|
||||
import { PASSWORD_CONFIG, PHONE_CONFIG } from "@/config/auth.ts";
|
||||
import { CodeType, ImportStatus } from "@/types/types.ts";
|
||||
import Code from "@/pages/user/Code.vue";
|
||||
import {isNewUser, sleep, useNav} from "@/utils";
|
||||
import { isNewUser, 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 { useExport } from "@/hooks/export.ts";
|
||||
import { getProgress, upload, uploadImportData } from "@/apis";
|
||||
import { Exception } from "sass";
|
||||
|
||||
// 状态管理
|
||||
const userStore = useUserStore()
|
||||
@@ -295,7 +295,7 @@ enum ImportStep {
|
||||
}
|
||||
|
||||
const {exportData} = useExport()
|
||||
let importStep = $ref<ImportStep>(ImportStep.CONFIRMATION)
|
||||
let importStep = $ref<ImportStep>(ImportStep.SUCCESS)
|
||||
let isImporting = $ref(false)
|
||||
let reason = $ref('')
|
||||
let timer = $ref(-1)
|
||||
@@ -358,6 +358,10 @@ function logout() {
|
||||
|
||||
function forgetData() {
|
||||
|
||||
}
|
||||
|
||||
function goHome(){
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -375,28 +379,28 @@ function forgetData() {
|
||||
<!-- Tab切换 -->
|
||||
<div class="center gap-8 mb-6">
|
||||
<div
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'code'"
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'code'"
|
||||
>
|
||||
<div>
|
||||
<span>验证码登录</span>
|
||||
<div
|
||||
v-opacity="loginType === 'code'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
v-opacity="loginType === 'code'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'password'"
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'password'"
|
||||
>
|
||||
<div>
|
||||
<span>密码登录</span>
|
||||
<div
|
||||
v-opacity="loginType === 'password'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
v-opacity="loginType === 'password'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -404,10 +408,10 @@ function forgetData() {
|
||||
|
||||
<!-- 验证码登录表单 -->
|
||||
<Form
|
||||
v-if="loginType === 'code'"
|
||||
ref="phoneLoginFormRef"
|
||||
:rules="phoneLoginFormRules"
|
||||
:model="phoneLoginForm">
|
||||
v-if="loginType === 'code'"
|
||||
ref="phoneLoginFormRef"
|
||||
:rules="phoneLoginFormRules"
|
||||
:model="phoneLoginForm">
|
||||
<FormItem prop="phone">
|
||||
<BaseInput v-model="phoneLoginForm.phone"
|
||||
type="tel"
|
||||
@@ -420,11 +424,11 @@ function forgetData() {
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="phoneLoginForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
placeholder="请输入验证码"
|
||||
v-model="phoneLoginForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
placeholder="请输入验证码"
|
||||
/>
|
||||
<Code :validate-field="() => phoneLoginFormRef.validateField('phone')"
|
||||
:type="CodeType.Login"
|
||||
@@ -435,10 +439,10 @@ function forgetData() {
|
||||
|
||||
<!-- 密码登录表单 -->
|
||||
<Form
|
||||
v-else
|
||||
ref="loginForm2Ref"
|
||||
:rules="loginForm2Rules"
|
||||
:model="loginForm2">
|
||||
v-else
|
||||
ref="loginForm2Ref"
|
||||
:rules="loginForm2Rules"
|
||||
:model="loginForm2">
|
||||
<FormItem prop="account">
|
||||
<BaseInput v-model="loginForm2.account"
|
||||
type="email"
|
||||
@@ -451,12 +455,12 @@ function forgetData() {
|
||||
<FormItem prop="password">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="loginForm2.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
v-model="loginForm2.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
@@ -467,10 +471,10 @@ function forgetData() {
|
||||
</Notice>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</BaseButton>
|
||||
@@ -487,27 +491,27 @@ function forgetData() {
|
||||
<Header @click="switchMode('login')" title="注册新账号"/>
|
||||
|
||||
<Form
|
||||
ref="registerFormRef"
|
||||
:rules="registerFormRules"
|
||||
:model="registerForm">
|
||||
ref="registerFormRef"
|
||||
:rules="registerFormRules"
|
||||
:model="registerForm">
|
||||
<FormItem prop="account">
|
||||
<BaseInput
|
||||
v-model="registerForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
v-model="registerForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="registerForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
v-model="registerForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => registerFormRef.validateField('account')"
|
||||
:type="CodeType.Register"
|
||||
@@ -516,22 +520,22 @@ function forgetData() {
|
||||
</FormItem>
|
||||
<FormItem prop="password">
|
||||
<BaseInput
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="confirmPassword">
|
||||
<BaseInput
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入密码"
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
@@ -539,10 +543,10 @@ function forgetData() {
|
||||
<Notice/>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleRegister"
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleRegister"
|
||||
>
|
||||
注册
|
||||
</BaseButton>
|
||||
@@ -554,27 +558,27 @@ function forgetData() {
|
||||
<Header @click="switchMode('login')" title="重置密码"/>
|
||||
|
||||
<Form
|
||||
ref="forgotFormRef"
|
||||
:rules="forgotFormRules"
|
||||
:model="forgotForm">
|
||||
ref="forgotFormRef"
|
||||
:rules="forgotFormRules"
|
||||
:model="forgotForm">
|
||||
<FormItem prop="account">
|
||||
<BaseInput
|
||||
v-model="forgotForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
v-model="forgotForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="forgotForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
v-model="forgotForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => forgotFormRef.validateField('account')"
|
||||
:type="CodeType.ResetPwd"
|
||||
@@ -583,31 +587,31 @@ function forgetData() {
|
||||
</FormItem>
|
||||
<FormItem prop="newPassword">
|
||||
<BaseInput
|
||||
v-model="forgotForm.newPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
v-model="forgotForm.newPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="confirmPassword">
|
||||
<BaseInput
|
||||
v-model="forgotForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入新密码"
|
||||
v-model="forgotForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<BaseButton
|
||||
class="w-full mt-2"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleForgotPassword"
|
||||
class="w-full mt-2"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleForgotPassword"
|
||||
>
|
||||
重置密码
|
||||
</BaseButton>
|
||||
@@ -618,16 +622,16 @@ function forgetData() {
|
||||
<div v-if="currentMode === 'login'" class="center flex-col bg-gray-100 rounded-xl px-12">
|
||||
<div class="relative w-40 h-40 bg-white rounded-xl overflow-hidden shadow-xl">
|
||||
<img
|
||||
v-if="showWechatQR"
|
||||
:src="wechatQRUrl"
|
||||
alt="微信登录二维码"
|
||||
class="w-full h-full"
|
||||
:class="{ 'opacity-30': qrStatus === 'expired' }"
|
||||
v-if="showWechatQR"
|
||||
:src="wechatQRUrl"
|
||||
alt="微信登录二维码"
|
||||
class="w-full h-full"
|
||||
:class="{ 'opacity-30': qrStatus === 'expired' }"
|
||||
/>
|
||||
<!-- 扫描成功蒙层 -->
|
||||
<div
|
||||
v-if="qrStatus === 'scanned'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
v-if="qrStatus === 'scanned'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
>
|
||||
<IconFluentCheckmarkCircle20Filled class="color-green text-4xl"/>
|
||||
<div class="text-base text-gray-700 font-medium">扫描成功</div>
|
||||
@@ -635,8 +639,8 @@ function forgetData() {
|
||||
</div>
|
||||
<!-- 取消登录蒙层 -->
|
||||
<div
|
||||
v-if="qrStatus === 'cancelled'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
v-if="qrStatus === 'cancelled'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
>
|
||||
<IconFluentErrorCircle20Regular class="color-red text-4xl"/>
|
||||
<div class="text-base text-gray-700 font-medium">你已取消此次登录</div>
|
||||
@@ -645,12 +649,12 @@ function forgetData() {
|
||||
</div>
|
||||
<!-- 过期蒙层 -->
|
||||
<div
|
||||
v-if=" qrStatus === 'expired'"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
|
||||
v-if=" qrStatus === 'expired'"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
|
||||
>
|
||||
<IconFluentArrowClockwise20Regular
|
||||
@click="refreshQRCode"
|
||||
class="cp text-4xl"/>
|
||||
@click="refreshQRCode"
|
||||
class="cp text-4xl"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-4 center gap-space">
|
||||
@@ -662,7 +666,7 @@ function forgetData() {
|
||||
</div>
|
||||
|
||||
<div v-else class="card-white p-6 w-100">
|
||||
<div class="title">同步数据确认</div>
|
||||
<Header @click="logout" title="同步数据"></Header>
|
||||
<div class="flex flex-col justify-between h-60">
|
||||
<template v-if="importStep === ImportStep.CONFIRMATION">
|
||||
<div>
|
||||
@@ -671,8 +675,6 @@ function forgetData() {
|
||||
</div>
|
||||
<div class="flex gap-space justify-end">
|
||||
<template v-if="importStep === ImportStep.CONFIRMATION">
|
||||
<BaseButton type="info" @click="logout">退出登录</BaseButton>
|
||||
|
||||
<PopConfirm :title="[
|
||||
{text:'您的用户数据将以压缩包自动下载到您的电脑中,以便您随时恢复',type:'normal'},
|
||||
{text:'随后网站的用户数据将被删除',type:'redBold'},
|
||||
@@ -680,15 +682,15 @@ function forgetData() {
|
||||
]"
|
||||
@confirm="forgetData"
|
||||
>
|
||||
<BaseButton type="info">放弃数据</BaseButton>
|
||||
<BaseButton type="info">不同步</BaseButton>
|
||||
</PopConfirm>
|
||||
</template>
|
||||
<BaseButton @click="startSync">确认同步</BaseButton>
|
||||
<BaseButton @click="startSync">同步</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="importStep === ImportStep.PROCESSING">
|
||||
<div>
|
||||
<h3 class="text-align-center">正在导入中</h3>
|
||||
<div class="title text-align-center">正在导入中</div>
|
||||
<ol class="pl-4">
|
||||
<li>
|
||||
您的用户数据已自动下载到您的电脑中,以便随时恢复
|
||||
@@ -711,31 +713,28 @@ function forgetData() {
|
||||
</template>
|
||||
<template v-if="importStep === ImportStep.FAIL">
|
||||
<div>
|
||||
<h3 class="text-align-center">同步失败</h3>
|
||||
<div class="title text-align-center">同步失败</div>
|
||||
<div class="mt-10">
|
||||
<span>{{ reason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-space justify-end">
|
||||
<BaseButton type="info" @click="logout">退出登录</BaseButton>
|
||||
|
||||
<PopConfirm :title="[
|
||||
{text:'您的用户数据将以压缩包自动下载到您的电脑中,以便您随时恢复',type:'normal'},
|
||||
{text:'随后网站的用户数据将被删除',type:'redBold'},
|
||||
{text:'是否确认继续?',type:'normal'},
|
||||
]"
|
||||
@confirm="forgetData"
|
||||
>
|
||||
<BaseButton type="info">放弃数据</BaseButton>
|
||||
</PopConfirm>
|
||||
<BaseButton @click="startSync">再次同步</BaseButton>
|
||||
<div class="flex justify-end">
|
||||
<BaseButton type="info" @click="jump2Feedback">反馈</BaseButton>
|
||||
<BaseButton @click="goHome">进入网站</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="importStep === ImportStep.SUCCESS">
|
||||
<div>
|
||||
<div class="title text-align-center">同步成功</div>
|
||||
<div class="mt-10">
|
||||
<span>稍后将自动进入网站...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<BaseButton @click="goHome">进入网站</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
</template>]
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="tsx">
|
||||
import { DictId } from "@/types/types.ts";
|
||||
import {DictId} from "@/types/types.ts";
|
||||
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { computed, onMounted, reactive, ref, shallowReactive } from "vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { _getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, useNav } from "@/utils";
|
||||
import { nanoid } from "nanoid";
|
||||
import {computed, onMounted, reactive, ref, shallowReactive} from "vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {_getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, useNav} from "@/utils";
|
||||
import {nanoid} from "nanoid";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import BaseTable from "@/components/BaseTable.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
@@ -13,21 +13,21 @@ import Toast from '@/components/base/toast/Toast.ts'
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import BackIcon from "@/components/BackIcon.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import EditBook from "@/pages/article/components/EditBook.vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import Form from "@/components/base/form/Form.vue";
|
||||
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
import { getCurrentStudyWord } from "@/hooks/dict.ts";
|
||||
import {getCurrentStudyWord} from "@/hooks/dict.ts";
|
||||
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { MessageBox } from "@/utils/MessageBox.tsx";
|
||||
import { AppEnv, Origin, PracticeSaveWordKey } from "@/config/env.ts";
|
||||
import { detail } from "@/apis";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {AppEnv, Origin, PracticeSaveWordKey} from "@/config/env.ts";
|
||||
import {detail} from "@/apis";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
@@ -151,7 +151,7 @@ function word2Str(word) {
|
||||
res.phrases = word.phrases.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
|
||||
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.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
|
||||
}
|
||||
@@ -192,8 +192,8 @@ onMounted(async () => {
|
||||
router.push("/word")
|
||||
} else {
|
||||
if (!runtimeStore.editDict.words.length
|
||||
&& !runtimeStore.editDict.custom
|
||||
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
|
||||
&& !runtimeStore.editDict.custom
|
||||
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
|
||||
) {
|
||||
loading = true
|
||||
let r = await _getDictDataByUrl(runtimeStore.editDict)
|
||||
@@ -242,7 +242,7 @@ async function startPractice() {
|
||||
wordPracticeMode: settingStore.wordPracticeMode
|
||||
})
|
||||
let currentStudy = getCurrentStudyWord()
|
||||
nav('practice-words/' + store.sdict.id, {}, {taskWords:currentStudy})
|
||||
nav('practice-words/' + store.sdict.id, {}, {taskWords: currentStudy})
|
||||
}
|
||||
|
||||
async function addMyStudyList() {
|
||||
@@ -322,22 +322,22 @@ function importData(e) {
|
||||
|
||||
if (repeat.length) {
|
||||
MessageBox.confirm(
|
||||
'单词"' + repeat.map(v => v.word).join(', ') + '" 已存在,是否覆盖原单词?',
|
||||
'检测到重复单词',
|
||||
() => {
|
||||
repeat.map(v => {
|
||||
runtimeStore.editDict.words[v.index] = v
|
||||
delete runtimeStore.editDict.words[v.index]["index"]
|
||||
})
|
||||
},
|
||||
null,
|
||||
() => {
|
||||
tableRef.value.closeImportDialog()
|
||||
e.target.value = ''
|
||||
importLoading = false
|
||||
syncDictInMyStudyList()
|
||||
Toast.success('导入成功!')
|
||||
}
|
||||
'单词"' + repeat.map(v => v.word).join(', ') + '" 已存在,是否覆盖原单词?',
|
||||
'检测到重复单词',
|
||||
() => {
|
||||
repeat.map(v => {
|
||||
runtimeStore.editDict.words[v.index] = v
|
||||
delete runtimeStore.editDict.words[v.index]["index"]
|
||||
})
|
||||
},
|
||||
null,
|
||||
() => {
|
||||
tableRef.value.closeImportDialog()
|
||||
e.target.value = ''
|
||||
importLoading = false
|
||||
syncDictInMyStudyList()
|
||||
Toast.success('导入成功!')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
tableRef.value.closeImportDialog()
|
||||
@@ -389,213 +389,214 @@ function searchWord() {
|
||||
|
||||
defineRender(() => {
|
||||
return (
|
||||
<BasePage>
|
||||
{
|
||||
showBookDetail.value ? <div className="card mb-0 dict-detail-card flex flex-col">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2"/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
|
||||
<div class="dict-actions flex gap-2">
|
||||
<BaseButton loading={studyLoading || loading} type="info"
|
||||
onClick={() => isEdit = true}>编辑</BaseButton>
|
||||
<BaseButton loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
|
||||
<BaseButton loading={studyLoading || loading} onClick={startTest}>测试</BaseButton>
|
||||
</div>
|
||||
<BasePage>
|
||||
{
|
||||
showBookDetail.value ? <div className="card mb-0 dict-detail-card flex flex-col">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2"/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
|
||||
<div class="dict-actions flex gap-2">
|
||||
<BaseButton loading={studyLoading || loading} type="info"
|
||||
onClick={() => isEdit = true}>编辑</BaseButton>
|
||||
<BaseButton loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
|
||||
<BaseButton loading={studyLoading || loading} onClick={startTest}>测试</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-lg ">介绍:{runtimeStore.editDict.description}</div>
|
||||
<div class="line my-3"></div>
|
||||
|
||||
{/* 移动端标签页导航 */}
|
||||
{isMob && isOperate && (
|
||||
<div class="tab-navigation mb-3">
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'list'}
|
||||
>
|
||||
单词列表
|
||||
</div>
|
||||
<div class="text-lg ">介绍:{runtimeStore.editDict.description}</div>
|
||||
<div class="line my-3"></div>
|
||||
|
||||
{/* 移动端标签页导航 */}
|
||||
{isMob && isOperate && (
|
||||
<div class="tab-navigation mb-3">
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'list'}
|
||||
>
|
||||
单词列表
|
||||
</div>
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'edit'}
|
||||
>
|
||||
{wordForm.id ? '编辑' : '添加'}单词
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex flex-1 overflow-hidden content-area">
|
||||
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
class="h-full"
|
||||
list={list}
|
||||
loading={loading}
|
||||
onUpdate:list={e => list = e}
|
||||
del={delWord}
|
||||
batchDel={batchDel}
|
||||
add={addWord}
|
||||
onImportData={importData}
|
||||
onExportData={exportData}
|
||||
exportLoading={exportLoading}
|
||||
importLoading={importLoading}
|
||||
>
|
||||
{
|
||||
(val) =>
|
||||
<WordItem
|
||||
showTransPop={false}
|
||||
item={val.item}>
|
||||
{{
|
||||
prefix: () => val.checkbox(val.item),
|
||||
suffix: () => (
|
||||
<div class='flex flex-col'>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
onClick={() => editWord(val.item)}
|
||||
title="编辑">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
</BaseIcon>
|
||||
<PopConfirm title="确认删除?"
|
||||
onConfirm={() => delWord(val.item.id)}
|
||||
>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</WordItem>
|
||||
}
|
||||
</BaseTable>
|
||||
</div>
|
||||
{
|
||||
isOperate ? (
|
||||
<div class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
|
||||
<div class="common-title">
|
||||
{wordForm.id ? '修改' : '添加'}单词
|
||||
</div>
|
||||
<Form
|
||||
class="flex-1 overflow-auto pr-2"
|
||||
ref={e => wordFormRef = e}
|
||||
rules={wordRules}
|
||||
model={wordForm}
|
||||
label-width="7rem">
|
||||
<FormItem label="单词" prop="word">
|
||||
<BaseInput
|
||||
modelValue={wordForm.word}
|
||||
onUpdate:modelValue={e => wordForm.word = e}
|
||||
>
|
||||
|
||||
</BaseInput>
|
||||
</FormItem>
|
||||
<FormItem label="英音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic0}
|
||||
onUpdate:modelValue={e => wordForm.phonetic0 = e}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="美音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic1}
|
||||
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
|
||||
</FormItem>
|
||||
<FormItem label="翻译">
|
||||
<Textarea
|
||||
modelValue={wordForm.trans}
|
||||
onUpdate:modelValue={e => wordForm.trans = e}
|
||||
placeholder="一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="例句">
|
||||
<Textarea
|
||||
modelValue={wordForm.sentences}
|
||||
onUpdate:modelValue={e => wordForm.sentences = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="短语">
|
||||
<Textarea
|
||||
modelValue={wordForm.phrases}
|
||||
onUpdate:modelValue={e => wordForm.phrases = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同义词">
|
||||
<Textarea
|
||||
modelValue={wordForm.synos}
|
||||
onUpdate:modelValue={e => wordForm.synos = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同根词">
|
||||
<Textarea
|
||||
modelValue={wordForm.relWords}
|
||||
onUpdate:modelValue={e => wordForm.relWords = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="词源">
|
||||
<Textarea
|
||||
modelValue={wordForm.etymology}
|
||||
onUpdate:modelValue={e => wordForm.etymology = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div class="center">
|
||||
<BaseButton
|
||||
type="info"
|
||||
onClick={closeWordForm}>关闭
|
||||
</BaseButton>
|
||||
<BaseButton type="primary"
|
||||
onClick={onSubmitWord}>保存
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
<div class="card mb-0 dict-detail-card">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" onClick={() => {
|
||||
if (isAdd) {
|
||||
router.back()
|
||||
} else {
|
||||
isEdit = false
|
||||
}
|
||||
}}/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">
|
||||
{runtimeStore.editDict.id ? '修改' : '创建'}词典
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<EditBook
|
||||
isAdd={isAdd}
|
||||
isBook={false}
|
||||
onClose={formClose}
|
||||
onSubmit={() => isEdit = isAdd = false}
|
||||
/>
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'edit'}
|
||||
>
|
||||
{wordForm.id ? '编辑' : '添加'}单词
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
<PracticeSettingDialog
|
||||
showLeftOption
|
||||
modelValue={showPracticeSettingDialog}
|
||||
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
|
||||
onOk={startPractice}/>
|
||||
</BasePage>
|
||||
<div class="flex flex-1 overflow-hidden content-area">
|
||||
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
class="h-full"
|
||||
list={list}
|
||||
loading={loading}
|
||||
onUpdate:list={e => list = e}
|
||||
del={delWord}
|
||||
batchDel={batchDel}
|
||||
add={addWord}
|
||||
onImportData={importData}
|
||||
onExportData={exportData}
|
||||
exportLoading={exportLoading}
|
||||
importLoading={importLoading}
|
||||
>
|
||||
{
|
||||
(val) =>
|
||||
<WordItem
|
||||
showTransPop={false}
|
||||
item={val.item}>
|
||||
{{
|
||||
prefix: () => val.checkbox(val.item),
|
||||
suffix: () => (
|
||||
<div class='flex flex-col'>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
onClick={() => editWord(val.item)}
|
||||
title="编辑">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
</BaseIcon>
|
||||
<PopConfirm title="确认删除?"
|
||||
onConfirm={() => delWord(val.item.id)}
|
||||
>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</WordItem>
|
||||
}
|
||||
</BaseTable>
|
||||
</div>
|
||||
{
|
||||
isOperate ? (
|
||||
<div
|
||||
class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
|
||||
<div class="common-title">
|
||||
{wordForm.id ? '修改' : '添加'}单词
|
||||
</div>
|
||||
<Form
|
||||
class="flex-1 overflow-auto pr-2"
|
||||
ref={e => wordFormRef = e}
|
||||
rules={wordRules}
|
||||
model={wordForm}
|
||||
label-width="7rem">
|
||||
<FormItem label="单词" prop="word">
|
||||
<BaseInput
|
||||
modelValue={wordForm.word}
|
||||
onUpdate:modelValue={e => wordForm.word = e}
|
||||
>
|
||||
|
||||
</BaseInput>
|
||||
</FormItem>
|
||||
<FormItem label="英音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic0}
|
||||
onUpdate:modelValue={e => wordForm.phonetic0 = e}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="美音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic1}
|
||||
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
|
||||
</FormItem>
|
||||
<FormItem label="翻译">
|
||||
<Textarea
|
||||
modelValue={wordForm.trans}
|
||||
onUpdate:modelValue={e => wordForm.trans = e}
|
||||
placeholder="一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="例句">
|
||||
<Textarea
|
||||
modelValue={wordForm.sentences}
|
||||
onUpdate:modelValue={e => wordForm.sentences = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="短语">
|
||||
<Textarea
|
||||
modelValue={wordForm.phrases}
|
||||
onUpdate:modelValue={e => wordForm.phrases = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同义词">
|
||||
<Textarea
|
||||
modelValue={wordForm.synos}
|
||||
onUpdate:modelValue={e => wordForm.synos = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同根词">
|
||||
<Textarea
|
||||
modelValue={wordForm.relWords}
|
||||
onUpdate:modelValue={e => wordForm.relWords = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="词源">
|
||||
<Textarea
|
||||
modelValue={wordForm.etymology}
|
||||
onUpdate:modelValue={e => wordForm.etymology = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div class="center">
|
||||
<BaseButton
|
||||
type="info"
|
||||
onClick={closeWordForm}>关闭
|
||||
</BaseButton>
|
||||
<BaseButton type="primary"
|
||||
onClick={onSubmitWord}>保存
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
<div class="card mb-0 dict-detail-card">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" onClick={() => {
|
||||
if (isAdd) {
|
||||
router.back()
|
||||
} else {
|
||||
isEdit = false
|
||||
}
|
||||
}}/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">
|
||||
{runtimeStore.editDict.id ? '修改' : '创建'}词典
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<EditBook
|
||||
isAdd={isAdd}
|
||||
isBook={false}
|
||||
onClose={formClose}
|
||||
onSubmit={() => isEdit = isAdd = false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<PracticeSettingDialog
|
||||
showLeftOption
|
||||
modelValue={showPracticeSettingDialog}
|
||||
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
|
||||
onOk={startPractice}/>
|
||||
</BasePage>
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-detail-card {
|
||||
min-height: calc(100vh - 3rem);
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
|
||||
.dict-header {
|
||||
@@ -625,6 +626,7 @@ defineRender(() => {
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.dict-detail-card {
|
||||
height: unset;
|
||||
min-height: calc(100vh - 2rem);
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
@@ -702,6 +704,7 @@ defineRender(() => {
|
||||
// 超小屏幕适配
|
||||
@media (max-width: 480px) {
|
||||
.dict-detail-card {
|
||||
height: unset;
|
||||
min-height: calc(100vh - 1rem);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,8 +138,7 @@ calcWeekList(); // 新增:计算本周学习记录
|
||||
:close-on-click-bg="false"
|
||||
:header="false"
|
||||
:keyboard="false"
|
||||
:show-close="false"
|
||||
class="statistics-modal">
|
||||
:show-close="false">
|
||||
<div class="p-8 pr-3 bg-[var(--bg-card-primary)] rounded-2xl space-y-6">
|
||||
<!-- Header Section -->
|
||||
<div class="text-center relative">
|
||||
|
||||
@@ -19,7 +19,7 @@ import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog
|
||||
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { AppEnv, DICT_LIST, PracticeSaveWordKey } from "@/config/env.ts";
|
||||
import {AppEnv, DICT_LIST, Host, PracticeSaveWordKey} from "@/config/env.ts";
|
||||
import { myDictList } from "@/apis";
|
||||
import PracticeWordListDialog from "@/pages/word/components/PracticeWordListDialog.vue";
|
||||
import ShufflePracticeSettingDialog from "@/pages/word/components/ShufflePracticeSettingDialog.vue";
|
||||
@@ -32,6 +32,7 @@ const {nav} = useNav()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
let loading = $ref(true)
|
||||
let isSaveData = $ref(false)
|
||||
|
||||
let currentStudy = $ref({
|
||||
new: [],
|
||||
review: [],
|
||||
@@ -196,11 +197,15 @@ const {
|
||||
isFetching
|
||||
} = useFetch(resourceWrap(DICT_LIST.WORD.RECOMMENDED)).json()
|
||||
|
||||
|
||||
let isNewHost = $ref(window.location.host === Host)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="mb-4" v-if="!isNewHost">
|
||||
新域名已启用,后续请访问 <a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前 2study.top 域名将在不久后停止使用
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col md:flex-row gap-8">
|
||||
<div class="flex-1 w-full flex flex-col justify-between">
|
||||
<div class="flex gap-3">
|
||||
|
||||
@@ -652,14 +652,10 @@ useEvents([
|
||||
@apply text-lg w-12;
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏光标
|
||||
.cursor {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.typing-word {
|
||||
padding: 0 0.5rem 12rem;
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ export function getDefaultArticle(val: Partial<Article> = {}): Article {
|
||||
audioFileId: '',
|
||||
lrcPosition: [],
|
||||
questions: [],
|
||||
nameList:[],
|
||||
...cloneDeep(val)
|
||||
}
|
||||
}
|
||||
|
||||
13
src/types/global.d.ts
vendored
13
src/types/global.d.ts
vendored
@@ -19,18 +19,25 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.json = function (v: any, space = 0) {
|
||||
const json = JSON.stringify(
|
||||
v,
|
||||
(key, value) => {
|
||||
if (Array.isArray(value)) {
|
||||
if (Array.isArray(value) && key !== 'nameList') {
|
||||
return `__ARRAY__${JSON.stringify(value)}`;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
space
|
||||
).replace(/"__ARRAY__(\[.*?\])"/g, (_, arr) => arr);
|
||||
)
|
||||
.replace(/"__ARRAY__(\[.*?\])"/g, (_, arr) => arr)
|
||||
// 专门处理 nameList,将其压缩成一行
|
||||
.replace(/"nameList": \[\s*([^\]]+)\s*\]/g, (match, content) => {
|
||||
// 移除数组内部的换行和多余空格,但保留字符串间的空格
|
||||
const compressed = content.replace(/\s*\n\s*/g, ' ').trim();
|
||||
return `"nameList": [${compressed}]`;
|
||||
});
|
||||
|
||||
console.log(json);
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface Article {
|
||||
audioSrc: string,
|
||||
audioFileId: string,
|
||||
lrcPosition: number[][],
|
||||
nameList: string[],
|
||||
questions: {
|
||||
stem: string,
|
||||
options: string[],
|
||||
|
||||
@@ -458,4 +458,8 @@ export async function isNewUser() {
|
||||
console.log(JSON.stringify(base.$state))
|
||||
console.log(JSON.stringify(getDefaultBaseState()))
|
||||
return JSON.stringify(base.$state) === JSON.stringify({...getDefaultBaseState(), ...{load: true}})
|
||||
}
|
||||
|
||||
export function jump2Feedback(){
|
||||
window.open('https://v.wjx.cn/vm/ev0W7fv.aspx#', '_blank');
|
||||
}
|
||||
Reference in New Issue
Block a user