Adapt to mobile devices
This commit is contained in:
@@ -438,4 +438,26 @@ footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.slide {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition: height .3s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.slide-infinite {
|
||||
z-index: 1;
|
||||
margin-top: 0;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.slide-list {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ function changeSort(v: Sort, notice: boolean = true) {
|
||||
function option(type: string) {
|
||||
show = false
|
||||
setTimeout(() => {
|
||||
router.push({path: '/dict', query: {type: type}})
|
||||
router.push({path: '/pc/dict', query: {type: type}})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
|
||||
106
src/components/slide/SlideHorizontal.vue
Normal file
106
src/components/slide/SlideHorizontal.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref, watch} from "vue";
|
||||
import GM from '@/utils/gm.js'
|
||||
import {
|
||||
getSlideDistance,
|
||||
slideInit,
|
||||
slideReset,
|
||||
slideTouchEnd,
|
||||
slideTouchMove,
|
||||
slideTouchStart
|
||||
} from "./common";
|
||||
import {SlideType} from "@/types.ts";
|
||||
|
||||
const props = defineProps({
|
||||
index: {
|
||||
type: Number,
|
||||
default: () => {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
//改变index,是否使用动画
|
||||
changeActiveIndexUseAnim: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
anim: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:index'])
|
||||
|
||||
const judgeValue = 20
|
||||
const wrapperEl = ref(null)
|
||||
const state = reactive({
|
||||
name: props.name,
|
||||
localIndex: props.index,
|
||||
needCheck: true,
|
||||
next: false,
|
||||
start: {x: 0, y: 0, time: 0},
|
||||
move: {x: 0, y: 0},
|
||||
wrapper: {width: 0, height: 0, childrenLength: 0},
|
||||
slideItemsWidths:[]
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.index,
|
||||
(newVal) => {
|
||||
if (state.localIndex !== newVal) {
|
||||
state.localIndex = newVal
|
||||
if (props.changeActiveIndexUseAnim) {
|
||||
GM.$setCss(wrapperEl.value, 'transition-duration', `300ms`)
|
||||
}
|
||||
GM.$setCss(wrapperEl.value, 'transform', `translate3d(${getSlideDistance(state, SlideType.HORIZONTAL)}px, 0, 0)`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
slideInit(wrapperEl.value, state, SlideType.HORIZONTAL)
|
||||
})
|
||||
|
||||
function touchStart(e) {
|
||||
if (!props.anim) return
|
||||
slideTouchStart(e, wrapperEl.value, state)
|
||||
}
|
||||
|
||||
function touchMove(e) {
|
||||
if (!props.anim) return
|
||||
slideTouchMove(e, wrapperEl.value, state, judgeValue, canNext, null, SlideType.HORIZONTAL)
|
||||
}
|
||||
|
||||
function touchEnd(e) {
|
||||
if (!props.anim) return
|
||||
slideTouchEnd(e, state, canNext, () => {
|
||||
|
||||
})
|
||||
slideReset(wrapperEl.value, state, SlideType.HORIZONTAL, emit)
|
||||
}
|
||||
|
||||
|
||||
function canNext(isNext) {
|
||||
if (isNext){
|
||||
return state.localIndex !== state.wrapper.childrenLength - 1
|
||||
}else {
|
||||
return state.localIndex !== 0
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="slide hhhh">
|
||||
<div class="slide-list"
|
||||
ref="wrapperEl"
|
||||
@touchstart="touchStart"
|
||||
@touchmove="touchMove"
|
||||
@touchend="touchEnd"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
18
src/components/slide/SlideItem.vue
Normal file
18
src/components/slide/SlideItem.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="slide-item">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.slide-item {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
135
src/components/slide/common.js
Normal file
135
src/components/slide/common.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import {emitter as bus} from "@/utils/eventBus.ts";
|
||||
import Utils from '@/utils/gm.js'
|
||||
import {SlideType} from "@/types.ts";
|
||||
import GM from "@/utils/gm.js";
|
||||
|
||||
export function slideInit(el, state, type) {
|
||||
state.wrapper.width = GM.$getCss(el, 'width')
|
||||
state.wrapper.height = GM.$getCss(el, 'height')
|
||||
let els = el.children
|
||||
state.wrapper.childrenLength = els.length
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
let el = els[i]
|
||||
state.slideItemsWidths.push(GM.$getCss(el, 'width'))
|
||||
}
|
||||
|
||||
let t = getSlideDistance(state, type)
|
||||
let dx1 = 0, dx2 = 0
|
||||
if (type === SlideType.HORIZONTAL) dx1 = t
|
||||
else dx2 = t
|
||||
|
||||
console.log('start', dx1)
|
||||
Utils.$setCss(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
|
||||
}
|
||||
|
||||
export function slideTouchStart(e, el, state) {
|
||||
Utils.$setCss(el, 'transition-duration', `0ms`)
|
||||
state.start.x = e.touches[0].pageX
|
||||
state.start.y = e.touches[0].pageY
|
||||
state.start.time = Date.now()
|
||||
}
|
||||
|
||||
export function canSlide(state, judgeValue, type = SlideType.HORIZONTAL) {
|
||||
if (state.needCheck) {
|
||||
if (Math.abs(state.move.x) > judgeValue || Math.abs(state.move.y) > judgeValue) {
|
||||
let angle = (Math.abs(state.move.x) * 10) / (Math.abs(state.move.y) * 10)
|
||||
state.next = type === SlideType.HORIZONTAL ? angle > 1 : angle <= 1;
|
||||
// console.log(angle)
|
||||
state.needCheck = false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return state.next
|
||||
}
|
||||
|
||||
export function slideTouchMove(e, el, state, judgeValue, canNextCb, nextCb, type = SlideType.HORIZONTAL, notNextCb) {
|
||||
state.move.x = e.touches[0].pageX - state.start.x
|
||||
state.move.y = e.touches[0].pageY - state.start.y
|
||||
|
||||
let isNext = type === SlideType.HORIZONTAL ? state.move.x < 0 : state.move.y < 0
|
||||
|
||||
let canSlideRes = canSlide(state, judgeValue, type)
|
||||
|
||||
if (canSlideRes && state.localIndex === 0 && !isNext && type === SlideType.VERTICAL) {
|
||||
bus.emit(state.name + '-moveY', state.move.y)
|
||||
}
|
||||
if (!canNextCb?.(isNext, state.move)) return
|
||||
|
||||
if (canSlideRes) {
|
||||
nextCb?.()
|
||||
if (type === SlideType.HORIZONTAL) {
|
||||
bus.emit(state.name + '-moveX', state.move.x)
|
||||
}
|
||||
Utils.$stopPropagation(e)
|
||||
let t = getSlideDistance(state, type) + (isNext ? judgeValue : -judgeValue)
|
||||
let dx1 = 0
|
||||
let dx2 = 0
|
||||
if (type === SlideType.HORIZONTAL) {
|
||||
dx1 = t + state.move.x
|
||||
} else {
|
||||
dx2 = t + state.move.y
|
||||
}
|
||||
Utils.$setCss(el, 'transition-duration', `0ms`)
|
||||
Utils.$setCss(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
|
||||
} else {
|
||||
notNextCb?.()
|
||||
}
|
||||
}
|
||||
|
||||
export function slideTouchEnd(e, state, canNextCb, nextCb, notNextCb, type = SlideType.HORIZONTAL) {
|
||||
let isHorizontal = type === SlideType.HORIZONTAL;
|
||||
let isNext = isHorizontal ? state.move.x < 0 : state.move.y < 0
|
||||
|
||||
if (!canNextCb?.(isNext)) return notNextCb?.()
|
||||
if (state.next) {
|
||||
Utils.$stopPropagation(e)
|
||||
let endTime = Date.now()
|
||||
let gapTime = endTime - state.start.time
|
||||
let distance = isHorizontal ? state.move.x : state.move.y
|
||||
let judgeValue = isHorizontal ? state.wrapper.width : state.wrapper.height
|
||||
if (Math.abs(distance) < 20) gapTime = 1000
|
||||
if (Math.abs(distance) > (judgeValue / 3)) gapTime = 100
|
||||
if (gapTime < 150) {
|
||||
if (isNext) {
|
||||
state.localIndex++
|
||||
} else {
|
||||
state.localIndex--
|
||||
}
|
||||
return nextCb?.(isNext)
|
||||
}
|
||||
}
|
||||
notNextCb?.()
|
||||
}
|
||||
|
||||
export function slideReset(el, state, type, emit) {
|
||||
Utils.$setCss(el, 'transition-duration', `300ms`)
|
||||
let t = getSlideDistance(state, type)
|
||||
let dx1 = 0
|
||||
let dx2 = 0
|
||||
if (type === SlideType.HORIZONTAL) {
|
||||
bus.emit(state.name + '-end', state.localIndex)
|
||||
dx1 = t
|
||||
} else {
|
||||
bus.emit(state.name + '-end',)
|
||||
dx2 = t
|
||||
}
|
||||
Utils.$setCss(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
|
||||
state.start.x = state.start.y = state.start.time = state.move.x = state.move.y = 0
|
||||
state.next = false
|
||||
state.needCheck = true
|
||||
emit?.('update:index', state.localIndex)
|
||||
}
|
||||
|
||||
export function getSlideDistance(state, type = SlideType.HORIZONTAL) {
|
||||
if (type === SlideType.HORIZONTAL) {
|
||||
return state.slideItemsWidths.reduce((p, c, i) => {
|
||||
if (i <= state.localIndex && i > 0) p -= c
|
||||
return p
|
||||
}, 0)
|
||||
} else {
|
||||
//TODO 这里需要改的和上面一样,不然不能显示半屏的div
|
||||
return -state.localIndex * state.wrapper.height
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
<script setup>
|
||||
import {Icon} from "@iconify/vue";
|
||||
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
|
||||
import SlideItem from "@/components/slide/SlideItem.vue";
|
||||
import Home from "@/pages/mobile/Home.vue";
|
||||
|
||||
let state = $ref({
|
||||
baseIndex: 0
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="page mobile">
|
||||
<div class="content">
|
||||
<router-view/>
|
||||
<SlideHorizontal
|
||||
:anim="false"
|
||||
v-model:index="state.baseIndex">
|
||||
<SlideItem>
|
||||
<Home/>
|
||||
</SlideItem>
|
||||
<SlideItem>2</SlideItem>
|
||||
<SlideItem>3</SlideItem>
|
||||
</SlideHorizontal>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<div class="tab">
|
||||
<div class="tab" @click="state.baseIndex = 0">
|
||||
<Icon width="30" icon="icon-park:word"/>
|
||||
<span>单词</span>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<div class="tab" @click="state.baseIndex = 1">
|
||||
<Icon width="30" icon="icon-park:word"/>
|
||||
<span>词典</span>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<div class="tab" @click="state.baseIndex = 2">
|
||||
<Icon width="30" icon="icon-park:word"/>
|
||||
<span>我的</span>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,13 @@ import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {ShortcutKey, Sort, Word} from "@/types.ts";
|
||||
import {DefaultDisplayStatistics, DictType, ShortcutKey, Sort, Word} from "@/types.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {syncMyDictList, useWordOptions} from "@/hooks/dict.ts";
|
||||
import {nextTick, onMounted, onUnmounted, watch} from "vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import Options from "@/pages/practice/Options.vue";
|
||||
import Options from "@/pages/pc/practice/Options.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import MobilePanel from "@/pages/mobile/components/MobilePanel.vue";
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
@@ -18,20 +18,21 @@ import WordList from "@/components/list/WordList.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import router from "@/router.ts";
|
||||
import Typing from "@/pages/practice/practice-word/Typing.vue";
|
||||
import Typing from "@/pages/pc/practice/practice-word/Typing.vue";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
let wordData = $ref({
|
||||
let data = $ref({
|
||||
words: [],
|
||||
index: -1,
|
||||
wrongWords: [],
|
||||
})
|
||||
|
||||
const word: Word = $computed(() => {
|
||||
return wordData.words[wordData.index] ?? {
|
||||
return data.words[data.index] ?? {
|
||||
trans: [],
|
||||
name: '',
|
||||
usphone: '',
|
||||
@@ -41,8 +42,8 @@ const word: Word = $computed(() => {
|
||||
|
||||
function getCurrentPractice() {
|
||||
if (store.chapter.length) {
|
||||
wordData.words = store.chapter
|
||||
wordData.index = 0
|
||||
data.words = store.chapter
|
||||
data.index = 0
|
||||
|
||||
store.chapter.map((w: Word) => {
|
||||
if (!w.trans.length) {
|
||||
@@ -51,18 +52,18 @@ function getCurrentPractice() {
|
||||
}
|
||||
})
|
||||
|
||||
wordData.words = cloneDeep(store.chapter)
|
||||
data.words = cloneDeep(store.chapter)
|
||||
emitter.emit(EventKey.resetWord)
|
||||
}
|
||||
}
|
||||
|
||||
function sort(list: Word[]) {
|
||||
store.currentDict.chapterWords[store.currentDict.chapterIndex] = wordData.words = list
|
||||
wordData.index = 0
|
||||
store.currentDict.chapterWords[store.currentDict.chapterIndex] = data.words = list
|
||||
data.index = 0
|
||||
syncMyDictList(store.currentDict)
|
||||
}
|
||||
|
||||
function next() {
|
||||
function nextChapter() {
|
||||
if (store.currentDict.chapterIndex >= store.currentDict.chapterWords.length - 1) {
|
||||
store.currentDict.chapterIndex = 0
|
||||
} else store.currentDict.chapterIndex++
|
||||
@@ -70,6 +71,61 @@ function next() {
|
||||
getCurrentPractice()
|
||||
}
|
||||
|
||||
let stat = cloneDeep(DefaultDisplayStatistics)
|
||||
const practiceStore = usePracticeStore()
|
||||
|
||||
function next(isTyping: boolean = true) {
|
||||
if (data.index === data.words.length - 1) {
|
||||
|
||||
//复制当前错词,因为第一遍错词是最多的,后续的练习都是从错词中练习
|
||||
if (stat.total === -1) {
|
||||
let now = Date.now()
|
||||
stat = {
|
||||
startDate: practiceStore.startDate,
|
||||
endDate: now,
|
||||
spend: now - practiceStore.startDate,
|
||||
total: props.words.length,
|
||||
correctRate: -1,
|
||||
inputWordNumber: practiceStore.inputWordNumber,
|
||||
wrongWordNumber: data.wrongWords.length,
|
||||
wrongWords: data.wrongWords,
|
||||
}
|
||||
stat.correctRate = 100 - Math.trunc(((stat.wrongWordNumber) / (stat.total)) * 100)
|
||||
}
|
||||
|
||||
if (data.wrongWords.length) {
|
||||
console.log('当前背完了,但还有错词')
|
||||
data.words = cloneDeep(data.wrongWords)
|
||||
|
||||
practiceStore.total = data.words.length
|
||||
practiceStore.index = data.index = 0
|
||||
practiceStore.inputWordNumber = 0
|
||||
practiceStore.wrongWordNumber = 0
|
||||
practiceStore.repeatNumber++
|
||||
data.wrongWords = []
|
||||
} else {
|
||||
console.log('这章节完了')
|
||||
isTyping && practiceStore.inputWordNumber++
|
||||
|
||||
let now = Date.now()
|
||||
stat.endDate = now
|
||||
stat.spend = now - stat.startDate
|
||||
|
||||
emitter.emit(EventKey.openStatModal, stat)
|
||||
}
|
||||
} else {
|
||||
data.index++
|
||||
isTyping && practiceStore.inputWordNumber++
|
||||
console.log('这个词完了')
|
||||
if ([DictType.word].includes(store.currentDict.type)
|
||||
&& store.skipWordNames.includes(word.name.toLowerCase())) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
watch(() => store.load, n => {
|
||||
getCurrentPractice()
|
||||
})
|
||||
@@ -87,7 +143,6 @@ const {
|
||||
toggleWordCollect,
|
||||
isWordSimple,
|
||||
toggleWordSimple,
|
||||
toggleWordSimpleWrapper
|
||||
} = useWordOptions()
|
||||
|
||||
let showSortOption = $ref(false)
|
||||
@@ -113,7 +168,7 @@ function change(e) {
|
||||
function know() {
|
||||
settingStore.translate = false
|
||||
setTimeout(() => {
|
||||
wordData.index++
|
||||
data.index++
|
||||
}, 300)
|
||||
}
|
||||
|
||||
@@ -207,18 +262,18 @@ function unknow() {
|
||||
v-if="store.currentDict.chapterIndex < store.currentDict.chapterWords.length - 1"/>
|
||||
</div>
|
||||
<div class="right">
|
||||
{{ wordData.words.length }}个单词
|
||||
{{ data.words.length }}个单词
|
||||
</div>
|
||||
</div>
|
||||
<WordList
|
||||
v-if="wordData.words.length"
|
||||
v-if="data.words.length"
|
||||
:is-active="active"
|
||||
:static="false"
|
||||
:show-word="!settingStore.dictation"
|
||||
:show-translate="settingStore.translate"
|
||||
:list="wordData.words"
|
||||
:activeIndex="wordData.index"
|
||||
@click="(val:any) => wordData.index = val.index"
|
||||
:list="data.words"
|
||||
:activeIndex="data.index"
|
||||
@click="(val:any) => data.index = val.index"
|
||||
>
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
@@ -303,6 +358,11 @@ function unknow() {
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
:deep(.word){
|
||||
letter-spacing: 0;
|
||||
font-size: 40rem!important;
|
||||
}
|
||||
|
||||
.options {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
150
src/pages/mobile/practice/index.vue
Normal file
150
src/pages/mobile/practice/index.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, onUnmounted, watch} from "vue";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import PracticeWord from "@/pages/mobile/practice/practice-word/index.vue";
|
||||
import {ShortcutKey} from "@/types.ts";
|
||||
import {useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
|
||||
const practiceStore = usePracticeStore()
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const {toggleTheme} = useTheme()
|
||||
const practiceRef: any = $ref()
|
||||
|
||||
watch(practiceStore, () => {
|
||||
if (practiceStore.inputWordNumber < 1) {
|
||||
return practiceStore.correctRate = -1
|
||||
}
|
||||
if (practiceStore.wrongWordNumber > practiceStore.inputWordNumber) {
|
||||
return practiceStore.correctRate = 0
|
||||
}
|
||||
practiceStore.correctRate = 100 - Math.trunc(((practiceStore.wrongWordNumber) / (practiceStore.inputWordNumber)) * 100)
|
||||
})
|
||||
|
||||
|
||||
function test() {
|
||||
MessageBox.confirm(
|
||||
'2您选择了“本地翻译”,但译文内容却为空白,是否修改为“不需要翻译”并保存?',
|
||||
'1提示',
|
||||
() => {
|
||||
console.log('ok')
|
||||
},
|
||||
() => {
|
||||
console.log('cencal')
|
||||
})
|
||||
}
|
||||
|
||||
function write() {
|
||||
// console.log('write')
|
||||
settingStore.dictation = true
|
||||
repeat()
|
||||
}
|
||||
|
||||
//TODO 需要判断是否已忽略
|
||||
function repeat() {
|
||||
// console.log('repeat')
|
||||
emitter.emit(EventKey.resetWord)
|
||||
practiceRef.getCurrentPractice()
|
||||
}
|
||||
|
||||
function prev() {
|
||||
// console.log('next')
|
||||
if (store.currentDict.chapterIndex === 0) {
|
||||
ElMessage.warning('已经在第一章了~')
|
||||
} else {
|
||||
store.currentDict.chapterIndex--
|
||||
repeat()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleShowTranslate() {
|
||||
settingStore.translate = !settingStore.translate
|
||||
}
|
||||
|
||||
function toggleDictation() {
|
||||
settingStore.dictation = !settingStore.dictation
|
||||
}
|
||||
|
||||
function openSetting() {
|
||||
runtimeStore.showSettingModal = true
|
||||
}
|
||||
|
||||
function openDictDetail() {
|
||||
emitter.emit(EventKey.openDictModal, 'detail')
|
||||
}
|
||||
|
||||
function toggleConciseMode() {
|
||||
settingStore.showToolbar = !settingStore.showToolbar
|
||||
settingStore.showPanel = settingStore.showToolbar
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
settingStore.showPanel = !settingStore.showPanel
|
||||
}
|
||||
|
||||
function jumpSpecifiedChapter(val: number) {
|
||||
store.currentDict.chapterIndex = val
|
||||
repeat()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.write, write)
|
||||
emitter.on(EventKey.repeat, repeat)
|
||||
emitter.on(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
|
||||
|
||||
emitter.on(ShortcutKey.PreviousChapter, prev)
|
||||
emitter.on(ShortcutKey.RepeatChapter, repeat)
|
||||
emitter.on(ShortcutKey.DictationChapter, write)
|
||||
emitter.on(ShortcutKey.ToggleShowTranslate, toggleShowTranslate)
|
||||
emitter.on(ShortcutKey.ToggleDictation, toggleDictation)
|
||||
emitter.on(ShortcutKey.OpenSetting, openSetting)
|
||||
emitter.on(ShortcutKey.OpenDictDetail, openDictDetail)
|
||||
emitter.on(ShortcutKey.ToggleTheme, toggleTheme)
|
||||
emitter.on(ShortcutKey.ToggleConciseMode, toggleConciseMode)
|
||||
emitter.on(ShortcutKey.TogglePanel, togglePanel)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.write, write)
|
||||
emitter.off(EventKey.repeat, repeat)
|
||||
emitter.off(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
|
||||
|
||||
emitter.off(ShortcutKey.PreviousChapter, prev)
|
||||
emitter.off(ShortcutKey.RepeatChapter, repeat)
|
||||
emitter.off(ShortcutKey.DictationChapter, write)
|
||||
emitter.off(ShortcutKey.ToggleShowTranslate, toggleShowTranslate)
|
||||
emitter.off(ShortcutKey.ToggleDictation, toggleDictation)
|
||||
emitter.off(ShortcutKey.OpenSetting, openSetting)
|
||||
emitter.off(ShortcutKey.OpenDictDetail, openDictDetail)
|
||||
emitter.off(ShortcutKey.ToggleTheme, toggleTheme)
|
||||
emitter.off(ShortcutKey.ToggleConciseMode, toggleConciseMode)
|
||||
emitter.off(ShortcutKey.TogglePanel, togglePanel)
|
||||
})
|
||||
|
||||
// useStartKeyboardEventListener()
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="practice-wrapper">
|
||||
<PracticeWord ref="practiceRef"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.practice-wrapper {
|
||||
font-size: 14rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -20,7 +20,7 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
next: [],
|
||||
complete: [],
|
||||
wrong: []
|
||||
}>()
|
||||
|
||||
@@ -110,13 +110,13 @@ async function onTyping(e: KeyboardEvent) {
|
||||
playCorrect()
|
||||
if (settingStore.repeatCount == 100) {
|
||||
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('next'), settingStore.waitTimeForChangeWord)
|
||||
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
} else {
|
||||
if (settingStore.repeatCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('next'), settingStore.waitTimeForChangeWord)
|
||||
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
466
src/pages/mobile/practice/practice-word/TypingWord.vue
Normal file
466
src/pages/mobile/practice/practice-word/TypingWord.vue
Normal file
@@ -0,0 +1,466 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, onUnmounted, watch} from "vue"
|
||||
import {$computed, $ref} from "vue/macros"
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
import {DefaultDisplayStatistics, DictType, ShortcutKey, Sort, Word} from "../../../../types.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts"
|
||||
import {cloneDeep, reverse, shuffle} from "lodash-es"
|
||||
import {usePracticeStore} from "@/stores/practice.ts"
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useOnKeyboardEventListener, useWindowClick} from "@/hooks/event.ts";
|
||||
import Typing from "@/pages/mobile/practice/practice-word/Typing.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useWordOptions} from "@/hooks/dict.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import WordList from "@/components/list/WordList.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
|
||||
import SlideItem from "@/components/slide/SlideItem.vue";
|
||||
import MobilePanel from "@/pages/mobile/components/MobilePanel.vue";
|
||||
import router from "@/router.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
interface IProps {
|
||||
words: Word[],
|
||||
index: number,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
words: [],
|
||||
index: -1
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:words': [val: Word[]],
|
||||
sort: [val: Word[]]
|
||||
}>()
|
||||
|
||||
const typingRef: any = $ref()
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const practiceStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const {
|
||||
isWordCollect,
|
||||
toggleWordCollect,
|
||||
isWordSimple,
|
||||
toggleWordSimple
|
||||
} = useWordOptions()
|
||||
|
||||
let data = $ref({
|
||||
index: props.index,
|
||||
words: props.words,
|
||||
wrongWords: [],
|
||||
})
|
||||
|
||||
let stat = cloneDeep(DefaultDisplayStatistics)
|
||||
let showSortOption = $ref(false)
|
||||
useWindowClick(() => showSortOption = false)
|
||||
|
||||
watch(() => props.words, () => {
|
||||
data.words = props.words
|
||||
data.index = props.index
|
||||
data.wrongWords = []
|
||||
|
||||
practiceStore.wrongWords = []
|
||||
practiceStore.repeatNumber = 0
|
||||
practiceStore.startDate = Date.now()
|
||||
practiceStore.correctRate = -1
|
||||
practiceStore.inputWordNumber = 0
|
||||
practiceStore.wrongWordNumber = 0
|
||||
stat = cloneDeep(DefaultDisplayStatistics)
|
||||
|
||||
}, {immediate: true})
|
||||
|
||||
watch(data, () => {
|
||||
practiceStore.total = data.words.length
|
||||
practiceStore.index = data.index
|
||||
})
|
||||
|
||||
const word = $computed(() => {
|
||||
return data.words[data.index] ?? {
|
||||
trans: [],
|
||||
name: '',
|
||||
usphone: '',
|
||||
ukphone: '',
|
||||
}
|
||||
})
|
||||
|
||||
const prevWord: Word = $computed(() => {
|
||||
return data.words?.[data.index - 1] ?? undefined
|
||||
})
|
||||
|
||||
const nextWord: Word = $computed(() => {
|
||||
return data.words?.[data.index + 1] ?? undefined
|
||||
})
|
||||
|
||||
function next(isTyping: boolean = true) {
|
||||
if (data.index === data.words.length - 1) {
|
||||
|
||||
//复制当前错词,因为第一遍错词是最多的,后续的练习都是从错词中练习
|
||||
if (stat.total === -1) {
|
||||
let now = Date.now()
|
||||
stat = {
|
||||
startDate: practiceStore.startDate,
|
||||
endDate: now,
|
||||
spend: now - practiceStore.startDate,
|
||||
total: props.words.length,
|
||||
correctRate: -1,
|
||||
inputWordNumber: practiceStore.inputWordNumber,
|
||||
wrongWordNumber: data.wrongWords.length,
|
||||
wrongWords: data.wrongWords,
|
||||
}
|
||||
stat.correctRate = 100 - Math.trunc(((stat.wrongWordNumber) / (stat.total)) * 100)
|
||||
}
|
||||
|
||||
if (data.wrongWords.length) {
|
||||
console.log('当前背完了,但还有错词')
|
||||
data.words = cloneDeep(data.wrongWords)
|
||||
|
||||
practiceStore.total = data.words.length
|
||||
practiceStore.index = data.index = 0
|
||||
practiceStore.inputWordNumber = 0
|
||||
practiceStore.wrongWordNumber = 0
|
||||
practiceStore.repeatNumber++
|
||||
data.wrongWords = []
|
||||
} else {
|
||||
console.log('这章节完了')
|
||||
isTyping && practiceStore.inputWordNumber++
|
||||
|
||||
let now = Date.now()
|
||||
stat.endDate = now
|
||||
stat.spend = now - stat.startDate
|
||||
|
||||
emitter.emit(EventKey.openStatModal, stat)
|
||||
}
|
||||
} else {
|
||||
data.index++
|
||||
isTyping && practiceStore.inputWordNumber++
|
||||
console.log('这个词完了')
|
||||
if ([DictType.word].includes(store.currentDict.type)
|
||||
&& store.skipWordNames.includes(word.name.toLowerCase())) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wordWrong() {
|
||||
if (!store.wrong.originWords.find((v: Word) => v.name.toLowerCase() === word.name.toLowerCase())) {
|
||||
store.wrong.originWords.push(word)
|
||||
}
|
||||
if (!data.wrongWords.find((v: Word) => v.name.toLowerCase() === word.name.toLowerCase())) {
|
||||
data.wrongWords.push(word)
|
||||
practiceStore.wrongWordNumber++
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp(e: KeyboardEvent) {
|
||||
typingRef.hideWord()
|
||||
}
|
||||
|
||||
async function onKeyDown(e: KeyboardEvent) {
|
||||
// console.log('e', e)
|
||||
switch (e.key) {
|
||||
case 'Backspace':
|
||||
typingRef.del()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
useOnKeyboardEventListener(onKeyDown, onKeyUp)
|
||||
|
||||
//TODO 略过忽略的单词上
|
||||
function prev() {
|
||||
if (data.index === 0) {
|
||||
ElMessage.warning('已经是第一个了~')
|
||||
} else {
|
||||
data.index--
|
||||
}
|
||||
}
|
||||
|
||||
function skip(e: KeyboardEvent) {
|
||||
next(false)
|
||||
// e.preventDefault()
|
||||
}
|
||||
|
||||
function show(e: KeyboardEvent) {
|
||||
typingRef.showWord()
|
||||
}
|
||||
|
||||
function collect(e: KeyboardEvent) {
|
||||
toggleWordCollect(word)
|
||||
}
|
||||
|
||||
function toggleWordSimpleWrapper() {
|
||||
if (!isWordSimple(word)) {
|
||||
toggleWordSimple(word)
|
||||
//延迟一下,不知道为什么不延迟会导致当前条目不自动定位到列表中间
|
||||
setTimeout(() => next(false))
|
||||
} else {
|
||||
toggleWordSimple(word)
|
||||
}
|
||||
}
|
||||
|
||||
function play() {
|
||||
typingRef.play()
|
||||
}
|
||||
|
||||
function sort(type: Sort) {
|
||||
if (type === Sort.reverse) {
|
||||
ElMessage.success('已翻转排序')
|
||||
emit('sort', reverse(cloneDeep(data.words)))
|
||||
}
|
||||
if (type === Sort.random) {
|
||||
ElMessage.success('已随机排序')
|
||||
emit('sort', shuffle(data.words))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(ShortcutKey.ShowWord, show)
|
||||
emitter.on(ShortcutKey.Previous, prev)
|
||||
emitter.on(ShortcutKey.Next, skip)
|
||||
emitter.on(ShortcutKey.ToggleCollect, collect)
|
||||
emitter.on(ShortcutKey.ToggleSimple, toggleWordSimpleWrapper)
|
||||
emitter.on(ShortcutKey.PlayWordPronunciation, play)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(ShortcutKey.ShowWord, show)
|
||||
emitter.off(ShortcutKey.Previous, prev)
|
||||
emitter.off(ShortcutKey.Next, skip)
|
||||
emitter.off(ShortcutKey.ToggleCollect, collect)
|
||||
emitter.off(ShortcutKey.ToggleSimple, toggleWordSimpleWrapper)
|
||||
emitter.off(ShortcutKey.PlayWordPronunciation, play)
|
||||
})
|
||||
|
||||
let index = $ref(0)
|
||||
watch(() => index, n => {
|
||||
settingStore.showPanel = index === 1
|
||||
})
|
||||
|
||||
let inputRef = $ref<HTMLInputElement>()
|
||||
|
||||
|
||||
function change(e) {
|
||||
console.log('e', e)
|
||||
e.key = e.data
|
||||
emitter.emit(EventKey.onTyping, e)
|
||||
inputRef.value = ''
|
||||
}
|
||||
|
||||
function know() {
|
||||
settingStore.translate = false
|
||||
setTimeout(() => {
|
||||
data.index++
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function unknow() {
|
||||
settingStore.translate = true
|
||||
inputRef.focus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="practice-word">
|
||||
<SlideHorizontal v-model:index="index">
|
||||
<SlideItem>
|
||||
<div class="practice-body" @click.stop="index = 0">
|
||||
<div class="tool-bar">
|
||||
<div class="left">
|
||||
<Icon icon="octicon:arrow-left-24" width="22"
|
||||
@click="router.back()"
|
||||
/>
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseIcon
|
||||
v-if="!isWordCollect(word)"
|
||||
class="collect"
|
||||
@click="toggleWordCollect(word)"
|
||||
icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleWordCollect(word)"
|
||||
icon="ph:star-fill"/>
|
||||
<BaseIcon
|
||||
@click="index = 1"
|
||||
icon="tdesign:menu-unfold"/>
|
||||
</div>
|
||||
</div>
|
||||
<input ref="inputRef"
|
||||
style="position:fixed;top:200vh;"
|
||||
@input="change"
|
||||
type="text">
|
||||
<Typing
|
||||
style="width: 90%;"
|
||||
v-loading="!store.load"
|
||||
ref="typingRef"
|
||||
:word="word"
|
||||
@next="next"
|
||||
/>
|
||||
<div class="options">
|
||||
<div class="wrapper">
|
||||
<BaseButton @click="unknow">不认识</BaseButton>
|
||||
<BaseButton @click="know">认识</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlideItem>
|
||||
<SlideItem style="width: 80vw;">
|
||||
<MobilePanel>
|
||||
<template v-slot="{active}">
|
||||
<div class="panel-page-item"
|
||||
v-loading="!store.load"
|
||||
>
|
||||
<div class="list-header">
|
||||
<div class="left">
|
||||
<div class="title">
|
||||
{{ store.chapterName }}
|
||||
</div>
|
||||
<BaseIcon title="切换词典"
|
||||
@click="emitter.emit(EventKey.openDictModal,'list')"
|
||||
icon="carbon:change-catalog"/>
|
||||
<div style="position:relative;"
|
||||
@click.stop="null">
|
||||
<BaseIcon
|
||||
title="改变顺序"
|
||||
icon="icon-park-outline:sort-two"
|
||||
@click="showSortOption = !showSortOption"
|
||||
/>
|
||||
<MiniDialog
|
||||
v-model="showSortOption"
|
||||
style="width: 130rem;"
|
||||
>
|
||||
<div class="mini-row-title">
|
||||
列表循环设置
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" @click="sort(Sort.reverse)">翻转</BaseButton>
|
||||
<BaseButton size="small" @click="sort(Sort.random)">随机</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
<BaseIcon icon="bi:arrow-right"
|
||||
@click="next"
|
||||
v-if="store.currentDict.chapterIndex < store.currentDict.chapterWords.length - 1"/>
|
||||
</div>
|
||||
<div class="right">
|
||||
{{ data.words.length }}个单词
|
||||
</div>
|
||||
</div>
|
||||
<WordList
|
||||
v-if="data.words.length"
|
||||
:is-active="active"
|
||||
:static="false"
|
||||
:show-word="!settingStore.dictation"
|
||||
:show-translate="settingStore.translate"
|
||||
:list="data.words"
|
||||
:activeIndex="data.index"
|
||||
@click="(val:any) => data.index = val.index"
|
||||
>
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
v-if="!isWordCollect(item)"
|
||||
class="collect"
|
||||
@click="toggleWordCollect(item)"
|
||||
title="收藏" icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleWordCollect(item)"
|
||||
title="取消收藏" icon="ph:star-fill"/>
|
||||
<BaseIcon
|
||||
v-if="!isWordSimple(item)"
|
||||
class="easy"
|
||||
@click="toggleWordSimple(item)"
|
||||
title="标记为简单词"
|
||||
icon="material-symbols:check-circle-outline-rounded"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleWordSimple(item)"
|
||||
title="取消标记简单词"
|
||||
icon="material-symbols:check-circle-rounded"/>
|
||||
</template>
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</template>
|
||||
</MobilePanel>
|
||||
</SlideItem>
|
||||
</SlideHorizontal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.practice-word {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
//display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
font-size: 14rem;
|
||||
color: gray;
|
||||
gap: 6rem;
|
||||
|
||||
.practice-body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
padding: 10rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
.tool-bar {
|
||||
width: 100%;
|
||||
height: 50rem;
|
||||
display: flex;
|
||||
padding: 0 10rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
:deep(.word) {
|
||||
letter-spacing: 0;
|
||||
font-size: 40rem !important;
|
||||
}
|
||||
|
||||
.options {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 20rem;
|
||||
|
||||
.wrapper {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
.base-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
86
src/pages/mobile/practice/practice-word/index.vue
Normal file
86
src/pages/mobile/practice/practice-word/index.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import TypingWord from "@/pages/mobile/practice/practice-word/TypingWord.vue";
|
||||
import {$ref} from "vue/macros";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {ShortcutKey, Word} from "@/types.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {syncMyDictList} from "@/hooks/dict.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
let wordData = $ref({
|
||||
words: [],
|
||||
index: -1
|
||||
})
|
||||
|
||||
function getCurrentPractice() {
|
||||
if (store.chapter.length) {
|
||||
wordData.words = store.chapter
|
||||
wordData.index = 0
|
||||
|
||||
store.chapter.map((w: Word) => {
|
||||
if (!w.trans.length) {
|
||||
let res = runtimeStore.translateWordList.find(a => a.name === w.name)
|
||||
if (res) w = Object.assign(w, res)
|
||||
}
|
||||
})
|
||||
|
||||
wordData.words = cloneDeep(store.chapter)
|
||||
emitter.emit(EventKey.resetWord)
|
||||
}
|
||||
}
|
||||
|
||||
function sort(list: Word[]) {
|
||||
store.currentDict.chapterWords[store.currentDict.chapterIndex] = wordData.words = list
|
||||
wordData.index = 0
|
||||
syncMyDictList(store.currentDict)
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (store.currentDict.chapterIndex >= store.currentDict.chapterWords.length - 1) {
|
||||
store.currentDict.chapterIndex = 0
|
||||
} else store.currentDict.chapterIndex++
|
||||
|
||||
getCurrentPractice()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCurrentPractice()
|
||||
emitter.on(EventKey.changeDict, getCurrentPractice)
|
||||
emitter.on(EventKey.next, next)
|
||||
emitter.on(ShortcutKey.NextChapter, next)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.changeDict, getCurrentPractice)
|
||||
emitter.off(EventKey.next, next)
|
||||
emitter.off(ShortcutKey.NextChapter, next)
|
||||
})
|
||||
|
||||
defineExpose({getCurrentPractice})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="practice">
|
||||
<TypingWord
|
||||
@sort="sort"
|
||||
v-model:words="wordData.words"
|
||||
:index="wordData.index"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.practice {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -8,10 +8,10 @@ import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import Slide from "@/components/Slide.vue";
|
||||
import ArticleDictDetail from "@/pages/dict/components/ArticleDictDetail.vue";
|
||||
import WordDictDetail from "@/pages/dict/components/WordDictDetail.vue";
|
||||
import ArticleDictDetail from "@/pages/pc/dict/components/ArticleDictDetail.vue";
|
||||
import WordDictDetail from "@/pages/pc/dict/components/WordDictDetail.vue";
|
||||
import DictListPanel from "@/components/DictListPanel.vue";
|
||||
import EditDict from "@/pages/dict/components/EditDict.vue";
|
||||
import EditDict from "@/pages/pc/dict/components/EditDict.vue";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const route = useRoute()
|
||||
@@ -11,7 +11,7 @@ import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import EditBatchArticleModal from "@/components/article/EditBatchArticleModal.vue";
|
||||
import {no} from "@/utils";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import EditDict from "@/pages/dict/components/EditDict.vue";
|
||||
import EditDict from "@/pages/pc/dict/components/EditDict.vue";
|
||||
import {nanoid} from "nanoid";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import ChapterWordList from "@/pages/dict/components/ChapterWordList.vue";
|
||||
import ChapterWordList from "@/pages/pc/dict/components/ChapterWordList.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {assign, chunk, cloneDeep, reverse, shuffle} from "lodash-es";
|
||||
@@ -18,7 +18,7 @@ import * as XLSX from "xlsx";
|
||||
import WordListDialog from "@/components/dialog/WordListDialog.vue";
|
||||
import {no} from "@/utils";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import EditDict from "@/pages/dict/components/EditDict.vue";
|
||||
import EditDict from "@/pages/pc/dict/components/EditDict.vue";
|
||||
import {syncMyDictList} from "@/hooks/dict.ts";
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import DictManage from "@/pages/dict/DictManage.vue";
|
||||
import DictManage from "@/pages/pc/dict/DictManage.vue";
|
||||
import {onMounted} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
@@ -20,10 +20,10 @@ onMounted(() => {
|
||||
<Logo/>
|
||||
<div class="nav-list">
|
||||
<nav>
|
||||
<router-link to="/practice">练习</router-link>
|
||||
<router-link to="/pc/practice">练习</router-link>
|
||||
</nav>
|
||||
<nav class="active">
|
||||
<router-link to="/dict">词典</router-link>
|
||||
<router-link to="/pc/dict">词典</router-link>
|
||||
</nav>
|
||||
<nav @click.stop="runtimeStore.showSettingModal = true"><a href="javascript:void(0)">设置</a></nav>
|
||||
</div>
|
||||
@@ -3,16 +3,16 @@
|
||||
import Toolbar from "@/components/toolbar/index.vue"
|
||||
import {onMounted, onUnmounted, watch} from "vue";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import Footer from "@/pages/practice/Footer.vue";
|
||||
import Footer from "@/pages/pc/practice/Footer.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import Statistics from "@/pages/practice/Statistics.vue";
|
||||
import Statistics from "@/pages/pc/practice/Statistics.vue";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import PracticeArticle from "@/pages/practice/practice-article/index.vue";
|
||||
import PracticeWord from "@/pages/practice/practice-word/index.vue";
|
||||
import PracticeArticle from "@/pages/pc/practice/practice-article/index.vue";
|
||||
import PracticeWord from "@/pages/pc/practice/practice-word/index.vue";
|
||||
import {ShortcutKey} from "@/types.ts";
|
||||
import DictModal from "@/components/dialog/DictDiglog.vue";
|
||||
import {useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
@@ -9,7 +9,7 @@ import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} fro
|
||||
import {useOnKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import Options from "@/pages/practice/Options.vue";
|
||||
import Options from "@/pages/pc/practice/Options.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Word
|
||||
} from "@/types.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import TypingWord from "@/pages/practice/practice-word/TypingWord.vue";
|
||||
import TypingWord from "@/pages/pc/practice/practice-word/TypingWord.vue";
|
||||
import Panel from "../Panel.vue";
|
||||
import {onMounted, onUnmounted, watch} from "vue";
|
||||
import {renewSectionTexts, renewSectionTranslates} from "@/hooks/translate.ts";
|
||||
@@ -2,7 +2,7 @@
|
||||
import {onMounted, onUnmounted, watch} from "vue"
|
||||
import {$computed, $ref} from "vue/macros"
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
import {DefaultDisplayStatistics, DictType, ShortcutKey, Sort, Word} from "../../../types.ts";
|
||||
import {DefaultDisplayStatistics, DictType, ShortcutKey, Sort, Word} from "../../../../types.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts"
|
||||
import {cloneDeep, reverse, shuffle} from "lodash-es"
|
||||
import {usePracticeStore} from "@/stores/practice.ts"
|
||||
@@ -10,9 +10,9 @@ import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useOnKeyboardEventListener, useWindowClick} from "@/hooks/event.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import Options from "@/pages/practice/Options.vue";
|
||||
import Typing from "@/pages/practice/practice-word/Typing.vue";
|
||||
import Panel from "@/pages/practice/Panel.vue";
|
||||
import Options from "@/pages/pc/practice/Options.vue";
|
||||
import Typing from "@/pages/pc/practice/practice-word/Typing.vue";
|
||||
import Panel from "@/pages/pc/practice/Panel.vue";
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {syncMyDictList, useWordOptions} from "@/hooks/dict.ts";
|
||||
@@ -268,7 +268,7 @@ onUnmounted(() => {
|
||||
ref="typingRef"
|
||||
:word="word"
|
||||
@wrong="wordWrong"
|
||||
@next="next"
|
||||
@complete="next"
|
||||
/>
|
||||
<div class="options-wrapper">
|
||||
<Options
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import TypingWord from "@/pages/practice/practice-word/TypingWord.vue";
|
||||
import TypingWord from "@/pages/pc/practice/practice-word/TypingWord.vue";
|
||||
import {$ref} from "vue/macros";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
@@ -81,5 +81,6 @@ defineExpose({getCurrentPractice})
|
||||
.practice {
|
||||
//height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -1,28 +1,21 @@
|
||||
import * as VueRouter from 'vue-router'
|
||||
import Practice from "@/pages/practice/index.vue";
|
||||
import Dict from '@/pages/dict/index.vue'
|
||||
import {RouteRecordRaw} from 'vue-router'
|
||||
import Practice from "@/pages/pc/practice/index.vue";
|
||||
import Dict from '@/pages/pc/dict/index.vue'
|
||||
import Mobile from '@/pages/mobile/index.vue'
|
||||
import MobileHome from '@/pages/mobile/home.vue'
|
||||
import MobilePractice from '@/pages/mobile/practice.vue'
|
||||
import MobilePractice from '@/pages/mobile/practice/index.vue'
|
||||
import Test from "@/pages/test.vue";
|
||||
import {RouteRecordRaw} from "vue-router";
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{path: '/practice', component: Practice},
|
||||
{path: '/dict', component: Dict},
|
||||
{path: '/pc/practice', component: Practice},
|
||||
{path: '/pc/dict', component: Dict},
|
||||
{
|
||||
path: '/mobile', component: Mobile,
|
||||
redirect:'/mobile/home',
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
component: MobileHome,
|
||||
},
|
||||
]
|
||||
// redirect:'/mobile/home',
|
||||
},
|
||||
{path: '/mobile-practice', component: MobilePractice,},
|
||||
{path: '/test', component: Test},
|
||||
{path: '/', redirect: '/practice'},
|
||||
{path: '/', redirect: '/pc/practice'},
|
||||
]
|
||||
|
||||
const router = VueRouter.createRouter({
|
||||
|
||||
@@ -262,3 +262,7 @@ export interface WordItem {
|
||||
index: number
|
||||
}
|
||||
|
||||
export const SlideType = {
|
||||
HORIZONTAL: 0,
|
||||
VERTICAL: 1,
|
||||
}
|
||||
113
src/utils/gm.js
Normal file
113
src/utils/gm.js
Normal file
@@ -0,0 +1,113 @@
|
||||
export default {
|
||||
$notice(val) {
|
||||
let div = document.createElement('div')
|
||||
div.classList.add('global-notice')
|
||||
div.textContent = val
|
||||
document.body.append(div)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(div)
|
||||
}, 1000)
|
||||
},
|
||||
$no() {
|
||||
this.$notice('未实现')
|
||||
},
|
||||
$back() {
|
||||
this.$router.back()
|
||||
// window.history.back()
|
||||
},
|
||||
$stopPropagation(e) {
|
||||
e.stopImmediatePropagation()
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
},
|
||||
$getCss(curEle, attr) {
|
||||
let val = null, reg = null
|
||||
if ("getComputedStyle" in window) {
|
||||
val = window.getComputedStyle(curEle, null)[attr]
|
||||
} else { //ie6~8不支持上面属性
|
||||
//不兼容
|
||||
if (attr === "opacity") {
|
||||
val = curEle.currentStyle["filter"] //'alpha(opacity=12,345)'
|
||||
reg = /^alphaopacity=(\d+(?:\.\d+)?)opacity=(\d+(?:\.\d+)?)$/i
|
||||
val = reg.test(val) ? reg.exec(val)[1] / 100 : 1
|
||||
} else {
|
||||
val = curEle.currentStyle[attr]
|
||||
}
|
||||
}
|
||||
// reg = /^(-?\d+(\.\d)?)(px|pt|em|rem)?$/i
|
||||
// return reg.test(val) ? parseFloat(val) : val
|
||||
return parseFloat(val)
|
||||
},
|
||||
$getCss2(curEle, attr) {
|
||||
let val = null, reg = null
|
||||
if ("getComputedStyle" in window) {
|
||||
val = window.getComputedStyle(curEle, null)[attr]
|
||||
} else { //ie6~8不支持上面属性
|
||||
//不兼容
|
||||
if (attr === "opacity") {
|
||||
val = curEle.currentStyle["filter"] //'alpha(opacity=12,345)'
|
||||
reg = /^alphaopacity=(\d+(?:\.\d+)?)opacity=(\d+(?:\.\d+)?)$/i
|
||||
val = reg.test(val) ? reg.exec(val)[1] / 100 : 1
|
||||
} else {
|
||||
val = curEle.currentStyle[attr]
|
||||
}
|
||||
}
|
||||
// reg = /^(-?\d+(\.\d)?)(px|pt|em|rem)?$/i
|
||||
// return reg.test(val) ? parseFloat(val) : val
|
||||
return val
|
||||
},
|
||||
$setCss(el, key, value) {
|
||||
// console.log(value)
|
||||
if (key === 'transform') {
|
||||
//直接设置不生效
|
||||
el.style.webkitTransform = el.style.MsTransform = el.style.msTransform = el.style.MozTransform = el.style.OTransform = el.style.transform = value;
|
||||
} else {
|
||||
el.style[key] = value
|
||||
}
|
||||
},
|
||||
$nav(path, query = {}) {
|
||||
this.$router.push({path, query})
|
||||
},
|
||||
$clone(v) {
|
||||
return JSON.parse(JSON.stringify(v))
|
||||
},
|
||||
$console(v) {
|
||||
return console.log(JSON.stringify(v, null, 4))
|
||||
},
|
||||
$sleep(duration) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, duration)
|
||||
})
|
||||
},
|
||||
$getTransform(el) {
|
||||
let transform = el.style.transform
|
||||
if (!transform) return 0
|
||||
// console.log('transform',transform)
|
||||
let transformY = transform.substring(transform.indexOf('0px') + 5, transform.lastIndexOf('0px') - 4)
|
||||
// console.log('transformY',transformY)
|
||||
//当前的transformY
|
||||
transformY = parseInt(transformY)
|
||||
return transformY
|
||||
},
|
||||
getCenter(a, b) {
|
||||
const x = (a.x + b.x) / 2;
|
||||
const y = (a.y + b.y) / 2;
|
||||
return {x, y}
|
||||
},
|
||||
// 获取坐标之间的举例
|
||||
getDistance(start, stop) {
|
||||
return Math.hypot(stop.x - start.x, stop.y - start.y);
|
||||
},
|
||||
copy(val) {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('readonly', 'readonly');
|
||||
input.setAttribute('value', val);
|
||||
document.body.appendChild(input);
|
||||
input.setSelectionRange(0, 9999);
|
||||
if (document.execCommand('copy')) {
|
||||
document.execCommand('copy');
|
||||
this.$notice('已复制')
|
||||
}
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user