feat:save

This commit is contained in:
zyronon
2025-08-03 13:49:03 +08:00
parent 05bf1ac5b4
commit 102f5fbc1f
32 changed files with 3 additions and 3832 deletions

View File

@@ -1,173 +0,0 @@
<script setup lang="ts">
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
import SlideItem from "@/components/slide/SlideItem.vue";
import {useBaseStore} from "@/stores/base.ts";
import {showConfirmDialog, showToast} from "vant";
import 'vant/lib/index.css'
import {onMounted} from "vue";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import {Dict} from "@/types.ts";
import DictPlan from "@/pages/mobile/components/DictPlan.vue";
import BackIcon from "@/components/icon/BackIcon.vue";
import {useRouter} from "vue-router";
const store = useBaseStore()
let index = $ref(0)
const router = useRouter()
onMounted(() => {
})
function handleDel(item: Dict, index: number) {
if (item.id === store.sdict.id) {
//TODO
} else {
showConfirmDialog({title: '确认删除?', message: '删除后无法撤销,确认删除吗?',})
.then(() => {
store.word.bookList.splice(index, 1)
})
}
}
</script>
<template>
<div class="mobile-page">
<header>
<BackIcon @click="router.back()"/>
<div class="tabs">
<div class="tab" :class="index === 0 && 'active'" @click="index = 0">修改计划</div>
<div class="tab" :class="index === 1 && 'active'" @click="index = 1">更换词书</div>
</div>
</header>
<SlideHorizontal v-model:index="index">
<SlideItem>
<DictPlan/>
</SlideItem>
<SlideItem>
<div class="my-dcits">
<div class="list">
<div class="dict" v-for="(item,index) in store.word.bookList">
<div class="title">
<div class="name">{{ item.name }}</div>
<span v-if="item.id === store.sdict.id">当前在学</span>
<template v-else>
<DeleteIcon
v-if="index>=3"
@click="handleDel(item,index)"/>
</template>
</div>
<div class="chapter">每日{{ item.chapterWordNumber }} 剩余100天</div>
<el-progress
:show-text="false"
:percentage="90"
/>
<div class="progress">
<span>已学单词</span>
<span>0/{{ item.length }}</span>
</div>
</div>
</div>
<BaseButton size="large">添加新书</BaseButton>
</div>
</SlideItem>
</SlideHorizontal>
</div>
</template>
<style scoped lang="scss">
header {
height: 60rem;
display: flex;
align-items: center;
position: relative;
padding: 0 var(--space);
.back {
position: absolute;
}
.tabs {
width: 100%;
border-top: 1px solid gray;
height: 100%;
display: flex;
justify-content: center;
.tab {
width: 100rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.active {
border-bottom: 2px solid gray;
}
}
}
.plan {
padding: 10rem;
.dict {
display: flex;
flex-direction: column;
gap: 10rem;
}
.set-plan {
background: white;
.header {
height: 60rem;
color: black;
display: flex;
justify-content: space-around;
align-items: center;
}
.picker-wrapper {
display: flex;
.van-picker {
flex: 1;
}
}
}
}
.my-dcits {
height: 100%;
padding: var(--space);
box-sizing: border-box;
display: flex;
flex-direction: column;
.list {
flex: 1;
overflow: auto;
margin-bottom: 20rem;
}
.dict {
padding: var(--space);
border-radius: var(--radius);
background: var(--color-second-bg);
display: flex;
flex-direction: column;
gap: 6rem;
margin-bottom: 10rem;
.title {
display: flex;
justify-content: space-between;
}
}
}
</style>

View File

@@ -1,186 +0,0 @@
<script setup lang="ts">
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
import SlideItem from "@/components/slide/SlideItem.vue";
import {useBaseStore} from "@/stores/base.ts";
import {groupBy} from "lodash-es";
import {dictionaryResources} from "@/assets/dictionary.ts";
import {Dict, DictResource, languageCategoryOptions} from "@/types.ts";
import {onMounted} from "vue";
import DictGroup from "./components/DictGroup.vue";
import router from "@/router.ts";
let index = $ref(1)
const store = useBaseStore()
let currentLanguage = $ref('my')
let currentTranslateLanguage = $ref('common')
let groupByLanguage = groupBy(dictionaryResources, 'language')
let translateLanguageList = $ref([])
function groupByDictTags(dictList: DictResource[]) {
return dictList.reduce<Record<string, DictResource[]>>((result, dict) => {
dict.tags.forEach((tag) => {
if (Object.prototype.hasOwnProperty.call(result, tag)) {
result[tag].push(dict)
} else {
result[tag] = [dict]
}
})
return result
}, {})
}
const groupByTranslateLanguage = $computed(() => {
let data: any
if (currentLanguage === 'article') {
let articleList = dictionaryResources.filter(v => v.type === 'article')
data = groupBy(articleList, 'translateLanguage')
} else if (currentLanguage === 'my') {
data = {
common: store.word.bookList.concat([{id: '',} as any])
}
} else {
data = groupBy(groupByLanguage[currentLanguage], 'translateLanguage')
}
// console.log('groupByTranslateLanguage', data)
translateLanguageList = Object.keys(data)
currentTranslateLanguage = translateLanguageList[0]
return data
})
const groupedByCategoryAndTag = $computed(() => {
const currentTranslateLanguageDictList = groupByTranslateLanguage[currentTranslateLanguage]
const groupByCategory = groupBy(currentTranslateLanguageDictList, 'category')
let data = []
for (const [key, value] of Object.entries(groupByCategory)) {
data.push([key, groupByDictTags(value)])
}
// console.log('groupedByCategoryAndTag', data)
return data
})
let articleData = $ref({
translateLanguageList: [],
currentTranslateLanguage: '',
dictList: []
})
let wordData = $ref({
translateLanguageList: [],
currentTranslateLanguage: '',
dictList: []
})
function getData(type: string) {
let articleList = dictionaryResources.filter(v => v.type === type)
let data = groupBy(articleList, 'translateLanguage')
let translateLanguageList = Object.keys(data)
let currentTranslateLanguage = translateLanguageList[0]
const currentTranslateLanguageDictList = data[currentTranslateLanguage]
const groupByCategory = groupBy(currentTranslateLanguageDictList, 'category')
let dictList = []
for (const [key, value] of Object.entries(groupByCategory)) {
dictList.push([key, groupByDictTags(value)])
}
return {
translateLanguageList,
currentTranslateLanguage,
dictList,
}
}
onMounted(() => {
let temp = getData('article')
articleData = temp
let temp1 = getData('word')
wordData = temp1
})
function selectDict(val: { dict: DictResource | Dict, index: number }) {
// console.log('val', val)
router.push({
path: '/mobile/set-dict-plan', query: {id: val.dict.id}
})
}
</script>
<template>
<div class="page dict-manage">
<div class="tabs">
<div class="tab"
:class="currentLanguage === item.id && 'active'"
@click="currentLanguage = item.id,index=i"
v-for="(item,i) in languageCategoryOptions">
<img :src='item.flag' alt=""/>
<span>{{ item.name }}</span>
</div>
</div>
<SlideHorizontal
v-model:index="index">
<SlideItem>
<div class="translate">
<span>翻译</span>
<el-radio-group v-model="articleData.currentTranslateLanguage">
<el-radio-button border v-for="i in articleData.translateLanguageList" :value="i">{{
$t(i)
}}
</el-radio-button>
</el-radio-group>
</div>
<DictGroup
v-for="item in articleData.dictList"
:select-id="store.sdict.id"
:groupByTag="item[1]"
:category="item[0]"
/>
</SlideItem>
<SlideItem style="display: flex;">
<div class="scroll">
<div class="translate">
<span>翻译</span>
<el-radio-group v-model="wordData.currentTranslateLanguage">
<el-radio-button border v-for="i in wordData.translateLanguageList" :value="i">{{
$t(i)
}}
</el-radio-button>
</el-radio-group>
</div>
<DictGroup
@select-dict="selectDict"
v-for="item in wordData.dictList"
:select-id="store.sdict.id"
:groupByTag="item[1]"
:category="item[0]"
/>
</div>
</SlideItem>
<SlideItem>3</SlideItem>
<SlideItem>4</SlideItem>
</SlideHorizontal>
</div>
</template>
<style scoped lang="scss">
.dict-manage {
font-size: 18rem;
.tabs {
height: 60rem;
display: flex;
width: 100%;
border-bottom: 1px solid gray;
.tab {
width: 80rem;
display: flex;
justify-content: center;
align-items: center;
}
}
}
</style>

View File

@@ -1,100 +0,0 @@
<script setup lang="ts">
import BaseButton from "@/components/BaseButton.vue";
import router from "@/router.ts";
import {useBaseStore} from "@/stores/base.ts";
import {Icon} from "@iconify/vue";
import {APP_NAME} from "../../utils/const.ts";
const store = useBaseStore()
function goPractice() {
router.push('/mobile/practice')
}
</script>
<template>
<div class="page home">
<div class="header">
<div class="welcome">
我在{{ APP_NAME }}学习
</div>
<div class="day">
<div class="num">151</div>
</div>
</div>
<div class="current-dict">
<div class="top">
<div class="left" @click="router.push('/mobile/dict-detail')">
<div class="name">{{ store.sdict.name }}</div>
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
</div>
<span>词表</span>
</div>
<el-progress
:percentage="90"
/>
</div>
<div class="btn">
<BaseButton size="large" @click="goPractice">开始背单词吧</BaseButton>
</div>
</div>
</template>
<style scoped lang="scss">
.home {
font-size: 18rem;
color: var(--color-font-2);
display: flex;
flex-direction: column;
//align-items: center;
//justify-content: center;
.header {
margin-top: 30rem;
padding: 20rem;
.day {
margin-top: 20rem;
display: flex;
align-items: flex-end;
.num {
font-size: 60rem;
transform: translateY(10rem);
}
}
}
.current-dict {
margin: 20rem;
padding: 20rem;
background: var(--color-third-bg);
border-radius: 8rem;
.top {
display: flex;
gap: 20rem;
align-items: center;
justify-content: space-between;
margin-bottom: 20rem;
.left {
display: flex;
align-items: center;
gap: 4rem;
}
span {
font-size: 14rem;
}
}
}
.btn {
margin: 20rem;
}
}
</style>

View File

@@ -1,258 +0,0 @@
<!--<script setup lang="ts">-->
<!--import {Icon} from "@iconify/vue";-->
<!--import IconWrapper from "@/components/IconWrapper.vue";-->
<!--import useTheme from "@/hooks/theme.ts";-->
<!--import {useSettingStore} from "@/stores/setting.ts";-->
<!---->
<!--import SlideItem from "@/components/slide/SlideItem.vue";-->
<!--import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";-->
<!--import BaseIcon from "@/components/BaseIcon.vue";-->
<!--import WordList from "@/components/list/WordList.vue";-->
<!--import {useRouter} from "vue-router";-->
<!--import {useBaseStore} from "@/stores/base.ts";-->
<!--import {useRuntimeStore} from "@/stores/runtime.ts";-->
<!--const {toggleTheme} = useTheme()-->
<!--const router = useRouter()-->
<!--const store = useBaseStore()-->
<!--const runtimeStore = useRuntimeStore()-->
<!--const settingStore = useSettingStore()-->
<!--let index = $ref(0)-->
<!--let isShowStarCount = $ref(false)-->
<!--function $nav() {-->
<!--}-->
<!--function $no() {-->
<!--}-->
<!--</script>-->
<!--<template>-->
<!-- <div class="page setting">-->
<!-- <div ref="float" class="float">-->
<!-- <div class="right">-->
<!-- <IconWrapper>-->
<!-- <Icon icon="fluent:search-24-regular"/>-->
<!-- </IconWrapper>-->
<!-- <IconWrapper>-->
<!-- <Icon icon="ep:moon"-->
<!-- v-if="settingStore.theme === 'dark'"-->
<!-- @click="toggleTheme"/>-->
<!-- <Icon icon="tabler:sun" v-else @click="toggleTheme"/>-->
<!-- </IconWrapper>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div ref="desc" class="desc">-->
<!-- <header ref="header"></header>-->
<!-- <div class="detail">-->
<!-- <div class="heat">-->
<!-- <div class="text" @click="isShowStarCount = true">-->
<!-- <span>收藏</span>-->
<!-- <span class="num">123</span>-->
<!-- </div>-->
<!-- <div class="text" @click="$nav('/people/follow-and-fans',{type:0})">-->
<!-- <span>错误</span>-->
<!-- <span class="num">123</span>-->
<!-- </div>-->
<!-- <div class="text" @click="$nav('/people/follow-and-fans',{type:1})">-->
<!-- <span>已掌握</span>-->
<!-- <span class="num">123</span>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="description">-->
<!-- <span>您已坚持了164天加油</span>-->
<!-- </div>-->
<!-- <div class="my-buttons">-->
<!-- <div class="button" @click="router.push('/mobile/setting')">-->
<!-- <span>设置</span>-->
<!-- </div>-->
<!-- <div class="button" @click="router.push('/mobile/data-manage')">-->
<!-- <span>数据同步</span>-->
<!-- <div class="not-read"></div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="nav">-->
<!-- <div class="tabs">-->
<!-- <div class="tab" :class="index === 0 && 'active'" @click="index = 0">当前</div>-->
<!-- <div class="tab" :class="index === 1 && 'active'" @click="index = 1">收藏</div>-->
<!-- <div class="tab" :class="index === 2 && 'active'" @click="index = 2">错词本</div>-->
<!-- <div class="tab" :class="index === 3 && 'active'" @click="index = 3">已掌握</div>-->
<!-- </div>-->
<!-- <div class="indicator" :style="{left:index * 25 + '%'}"></div>-->
<!-- </div>-->
<!-- <SlideHorizontal-->
<!-- v-model:index="index">-->
<!-- <SlideItem>-->
<!-- </SlideItem>-->
<!-- <SlideItem>-->
<!-- <div class="panel-page-item">-->
<!-- <div class="list-header">-->
<!-- <div class="left">-->
<!-- <div class="dict-name">总词数{{ store.collect.words.length }}</div>-->
<!-- <BaseIcon icon="fluent:add-12-regular" title="添加" @click="addCollect"/>-->
<!-- </div>-->
<!-- </div>-->
<!-- <WordList-->
<!-- v-if="store.collect.words.length"-->
<!-- class="word-list"-->
<!-- :list="store.collect.words">-->
<!-- <template v-slot:suffix="{item,index}">-->
<!-- <BaseIcon-->
<!-- class="del"-->
<!-- title="移除"-->
<!-- icon="solar:trash-bin-minimalistic-linear"/>-->
<!-- </template>-->
<!-- </WordList>-->
<!-- </div>-->
<!-- </SlideItem>-->
<!-- <SlideItem>4</SlideItem>-->
<!-- <SlideItem>4</SlideItem>-->
<!-- </SlideHorizontal>-->
<!-- </div>-->
<!--</template>-->
<!--<style scoped lang="scss">-->
<!--$main-bg: rgb(21, 23, 36);-->
<!--$second-btn-color: rgb(58, 58, 70);-->
<!--.setting {-->
<!-- font-size: 18rem;-->
<!-- display: flex;-->
<!-- flex-direction: column;-->
<!-- align-items: center;-->
<!-- background: $main-bg;-->
<!-- .float {-->
<!-- position: fixed;-->
<!-- box-sizing: border-box;-->
<!-- width: 100vw;-->
<!-- z-index: 2;-->
<!-- display: flex;-->
<!-- justify-content: flex-end;-->
<!-- align-items: center;-->
<!-- height: 46rem;-->
<!-- padding: 0 15rem;-->
<!-- background: transparent;-->
<!-- transition: all .2s;-->
<!-- .right {-->
<!-- }-->
<!-- }-->
<!-- .desc {-->
<!-- width: 100%;-->
<!-- header {-->
<!-- color: white;-->
<!-- height: 200rem;-->
<!-- background-image: url('../../assets/img/a.jpg');-->
<!-- background-size: cover;-->
<!-- background-position: center;-->
<!-- background-repeat: no-repeat;-->
<!-- box-sizing: border-box;-->
<!-- }-->
<!-- //消息页面-->
<!-- $msg-bg: rgb(22, 22, 22);-->
<!-- $msg-subpage-card-bg: rgb(28, 30, 43); //二级页面,卡片背景-->
<!-- .detail {-->
<!-- transform: translateY(-10rem);-->
<!-- background: $main-bg;-->
<!-- padding: 20rem;-->
<!-- padding-bottom: 0;-->
<!-- border-radius: 10rem 10rem 0 0;-->
<!-- display: flex;-->
<!-- flex-direction: column;-->
<!-- gap: 20rem;-->
<!-- .heat {-->
<!-- color: white;-->
<!-- display: flex;-->
<!-- align-items: center;-->
<!-- font-size: 16rem;-->
<!-- gap: 30rem;-->
<!-- .num {-->
<!-- color: white;-->
<!-- font-weight: bold;-->
<!-- }-->
<!-- .text {-->
<!-- display: flex;-->
<!-- align-items: center;-->
<!-- gap: 10rem;-->
<!-- }-->
<!-- }-->
<!-- .description {-->
<!-- font-size: 16rem;-->
<!-- color: white;-->
<!-- }-->
<!-- .my-buttons {-->
<!-- display: flex;-->
<!-- gap: 20rem;-->
<!-- justify-content: space-between;-->
<!-- .button {-->
<!-- position: relative;-->
<!-- flex: 1;-->
<!-- font-size: 16rem;-->
<!-- display: flex;-->
<!-- align-items: center;-->
<!-- justify-content: center;-->
<!-- border-radius: 6rem;-->
<!-- background: $second-btn-color;-->
<!-- height: 40rem;-->
<!-- color: white;-->
<!-- }-->
<!-- }-->
<!-- }-->
<!-- }-->
<!-- .nav {-->
<!-- font-size: 16rem;-->
<!-- width: 100%;-->
<!-- height: 50rem;-->
<!-- top: 0;-->
<!-- left: 0;-->
<!-- right: 0;-->
<!-- z-index: 1;-->
<!-- background: $main-bg;-->
<!-- .tabs {-->
<!-- width: 100%;-->
<!-- display: flex;-->
<!-- justify-content: space-between;-->
<!-- .tab {-->
<!-- height: 45rem;-->
<!-- width: 45%;-->
<!-- display: flex;-->
<!-- justify-content: center;-->
<!-- align-items: center;-->
<!-- color: gray;-->
<!-- transition: color .3s;-->
<!-- &.active {-->
<!-- font-weight: bold;-->
<!-- color: white;-->
<!-- }-->
<!-- }-->
<!-- }-->
<!-- .indicator {-->
<!-- height: 2px;-->
<!-- background: gold;-->
<!-- width: 25%;-->
<!-- position: relative;-->
<!-- transition: all .3s;-->
<!-- //left: 50%;-->
<!-- }-->
<!-- }-->
<!--}-->
<!--</style>-->

View File

@@ -1,48 +0,0 @@
<script setup lang="ts">
import DictPlan from "@/pages/mobile/components/DictPlan.vue";
import NavBar from "@/pages/mobile/components/NavBar.vue";
import {onMounted} from "vue";
import {useRoute} from "vue-router";
import {Dict, getDefaultDict} from "@/types.ts";
import {cloneDeep} from "lodash-es";
import {nanoid} from "nanoid";
import {dictionaryResources} from "@/assets/dictionary.ts";
import {useBaseStore} from "@/stores/base.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
const route = useRoute()
let load = $ref(false)
onMounted(() => {
// console.log('route', route.query.id)
let item = dictionaryResources.find(v => v.id === route.query.id)
let find: Dict = store.word.bookList.find((v: Dict) => v.id === item.id)
if (find) {
runtimeStore.editDict = cloneDeep(find)
} else {
runtimeStore.editDict = cloneDeep({
...getDefaultDict(),
...item,
})
runtimeStore.editDict.id = nanoid(6)
}
load = true
})
</script>
<template>
<div class="mobile-page">
<NavBar title="设置任务量"/>
<DictPlan v-if="load"/>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,26 +0,0 @@
.setting-list {
background: var(--color-header-bg);
color: var(--color-font-2);
border-radius: 8rem;
width: 100%;
.item {
height: 60rem;
padding-left: 20rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10rem;
.right {
padding-right: 10rem;
height: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f1f1f1;
border-bottom: 1px solid var(--color-item-bg);
}
}
}

View File

@@ -1,84 +0,0 @@
<script setup lang="ts">
import BaseIcon from "@/components/BaseIcon.vue";
import WordList from "@/pages/pc/components/list/WordList.vue";
import BaseButton from "@/components/BaseButton.vue";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
import {Dict, DictType} from "@/types.ts";
import {useRouter} from "vue-router";
import {useBaseStore} from "@/stores/base.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {cloneDeep} from "lodash-es";
import {useWordOptions} from "@/hooks/dict.ts";
const router = useRouter()
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
let practiceType = $ref(DictType.word)
const showCollectToggleButton = $computed(() => {
if (store.sdict.type === DictType.collect) {
// if (store.current.practiceType !== practiceType) {
// return (practiceType === DictType.word && store.collectWord.words.length) ||
// (practiceType === DictType.article && store.collectWord.articles.length)
// }
} else {
return (practiceType === DictType.word && store.collectWord.words.length) ||
(practiceType === DictType.article && store.collectWord.articles.length)
}
return false
})
function changeIndex(dict: Dict) {
store.changeDict(dict, practiceType)
}
function addCollect() {
runtimeStore.editDict = cloneDeep(store.collect)
router.push({path: '/dict', query: {type: 'addWordOrArticle'}})
}
const {
delWrongWord,
delSimpleWord,
toggleWordCollect,
} = useWordOptions()
</script>
<template>
<div class="panel-page-item">
<div class="list-header">
<div class="left">
<div class="dict-name">总词数{{ store.collectWord.words.length }}</div>
<BaseIcon icon="fluent:add-12-regular" title="添加" @click="addCollect"/>
</div>
<template v-if="showCollectToggleButton">
<PopConfirm
:title="`确认切换?`"
@confirm="changeIndex( store.collect)"
>
<BaseButton size="small">切换</BaseButton>
</PopConfirm>
</template>
</div>
<WordList
v-if="store.collectWord.words.length"
class="word-list"
:list="store.collectWord.words">
<template v-slot:suffix="{item,index}">
<BaseIcon
class="del"
@click="toggleWordCollect(item)"
title="移除"
icon="solar:trash-bin-minimalistic-linear"/>
</template>
</WordList>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,74 +0,0 @@
<script setup lang="ts">
import {watch} from "vue";
import {DictResource} from "@/types.ts";
import DictList from "./DictList.vue";
const props = defineProps<{
category: string,
groupByTag: any,
selectId: string
}>()
const emit = defineEmits<{
selectDict: [val: { dict: DictResource, index: number }]
detail: [],
}>()
const tagList = $computed(() => Object.keys(props.groupByTag))
let currentTag = $ref(tagList[0])
let list = $computed(() => {
return props.groupByTag[currentTag]
})
watch(() => props.groupByTag, () => {
currentTag = tagList[0]
})
</script>
<template>
<div class="dict-group">
<div class="category">{{ category }}</div>
<div class="tags">
<div class="tag" :class="i === currentTag &&'active'"
@click="currentTag = i"
v-for="i in Object.keys(groupByTag)">{{ i }}
</div>
</div>
<DictList
@selectDict="e => emit('selectDict',e)"
:list="list"
:select-id="selectId"/>
</div>
</template>
<style scoped lang="scss">
.dict-group {
color: var(--color-font-1);
margin-bottom: 40rem;
//border-bottom: 1px dashed gray;
.category {
font-size: 24rem;
padding-bottom: 10rem;
border-bottom: 1px dashed gray;
}
}
.tags {
display: flex;
flex-wrap: wrap;
margin: 10rem 0;
.tag {
color: var(--color-font-1);
cursor: pointer;
padding: 5rem 10rem;
border-radius: 20rem;
&.active {
color: var(--color-font-active-1);
background: gray;
}
}
}
</style>

View File

@@ -1,138 +0,0 @@
<script setup lang="ts">
import {Dict, DictType} from "@/types.ts";
import {Icon} from "@iconify/vue";
const props = defineProps<{
dict?: Dict,
active?: boolean
}>()
const emit = defineEmits<{
selectDict: [val: { dict: Dict, index: number }]
add: []
}>()
let length = $computed(() => {
let isWord = [DictType.word,DictType.collect,DictType.simple,DictType.wrong].includes(props.dict.type)
let len: any = ''
if (props.dict.length) {
len = props.dict.length
len += (isWord ? '词' : '篇')
} else {
if (isWord) {
len = props.dict.originWords.length + '词'
} else {
len = props.dict.articles.length + '篇'
}
}
return len
})
</script>
<template>
<div
class="dict-item anim"
:class="active && 'active'"
>
<template v-if="dict.id">
<div class="top">
<div class="name">{{ dict.name }}</div>
<div class="desc">{{ dict.description }}</div>
</div>
<div class="bottom">
<div class="num">{{ length }}</div>
</div>
<div class="pin" v-if="dict.type === DictType.article">文章</div>
</template>
<div v-else class="add" @click.stop="emit('add')">
<Icon icon="fluent:add-20-filled" width="38" color="#929596"/>
</div>
</div>
</template>
<style scoped lang="scss">
.dict-item {
cursor: pointer;
box-sizing: border-box;
padding: 10rem;
width: 30%;
height: 165rem;
border-radius: 10rem;
position: relative;
background: var(--color-third-bg);
border: 1px solid var(--color-item-border);
color: var(--color-font-1);
font-size: 14rem;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
.name {
font-size: 16rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box; //作为弹性伸缩盒子模型显示。
-webkit-box-orient: vertical; //设置伸缩盒子的子元素排列方式--从上到下垂直排列
-webkit-line-clamp: 2; //显示的行
}
.desc {
color: var(--color-font-2);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box; //作为弹性伸缩盒子模型显示。
-webkit-box-orient: vertical; //设置伸缩盒子的子元素排列方式--从上到下垂直排列
-webkit-line-clamp: 2; //显示的行
}
.num {
text-align: right;
color: var(--color-font-2);
//font-weight: bold;
}
.go {
position: absolute;
right: 10rem;
bottom: 15rem;
}
&.active {
background: var(--color-item-active);
}
&:hover {
background: var(--color-item-active);
}
.add {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.pin {
position: absolute;
bottom: 0;
left: 0;
height: 55rem;
width: 55rem;
color: white;
//background-color: skyblue;
background-color: var(--color-main-active);
clip-path: polygon(0 10%, 0% 100%, 100% 100%);
font-size: 12rem;
display: flex;
justify-content: flex-start;
align-items: flex-end;
padding: 4rem;
box-sizing: border-box;
}
}
</style>

View File

@@ -1,30 +0,0 @@
<script setup lang="ts">
import {Dict} from "@/types.ts";
defineProps<{
list?: Dict[],
selectId?: string
}>()
const emit = defineEmits<{
selectDict: [val: { dict: any, index: number }]
detail: [],
add: []
}>()
</script>
<template>
<div class="dict-list">
</div>
</template>
<style scoped lang="scss">
.dict-list {
display: flex;
flex-wrap: wrap;
gap: 15rem;
}
</style>

View File

@@ -1,193 +0,0 @@
<script setup lang="ts">
import {useBaseStore} from "@/stores/base.ts";
import {Picker, showToast} from "vant";
import 'vant/lib/index.css'
import {onMounted} from "vue";
import BaseButton from "@/components/BaseButton.vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
import router from "@/router.ts";
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
let columns = $ref([])
let columns2 = $ref([])
let chapterWordNumber = $ref([runtimeStore.editDict.chapterWordNumber])
let length = $ref(runtimeStore.editDict.length)
let completeDay = $ref([Math.ceil(length / chapterWordNumber[0])])
const onChange = ({selectedValues}) => {
chapterWordNumber = selectedValues
completeDay = [Math.ceil(length / chapterWordNumber[0])]
};
const onChange2 = ({selectedValues}) => {
completeDay = selectedValues
for (let i = 0; i < columns.length; i++) {
let v = columns[i]
let s = Math.ceil(length / v.value)
if (s === completeDay[0]) {
chapterWordNumber = [v.value]
break
}
}
};
onMounted(() => {
let list = []
if (length < 50) {
list = Array.from({length: Math.floor(length / 5)}).map((v, i) => (i + 1) * 5)
}
if (length > 50) {
list = Array.from({length: 10}).map((v, i) => (i + 1) * 5)
}
if (length > 100) {
list = list.concat(Array.from({length: 5}).map((v, i) => 50 + (i + 1) * 10))
} else {
list = list.concat(Array.from({length: Math.floor((length - 50) / 10)}).map((v, i) => 50 + (i + 1) * 10))
}
if (length > 200) {
list = list.concat(Array.from({length: 4}).map((v, i) => 100 + (i + 1) * 25))
} else {
list = list.concat(Array.from({length: Math.floor((length - 100) / 25)}).map((v, i) => 100 + (i + 1) * 25))
}
if (length > 500) {
list = list.concat(Array.from({length: 6}).map((v, i) => 200 + (i + 1) * 50))
} else {
list = list.concat(Array.from({length: Math.floor((length - 200) / 50)}).map((v, i) => 200 + (i + 1) * 50))
}
if (length > 1000) {
list = list.concat(Array.from({length: 5}).map((v, i) => 500 + (i + 1) * 100))
} else {
list = list.concat(Array.from({length: Math.floor((length - 500) / 100)}).map((v, i) => 500 + (i + 1) * 100))
}
if (length > 3000) {
list = list.concat(Array.from({length: 8}).map((v, i) => 1000 + (i + 1) * 250))
} else {
list = list.concat(Array.from({length: Math.floor((length - 1000) / 250)}).map((v, i) => 1000 + (i + 1) * 250))
}
if (length > 10000) {
list = list.concat(Array.from({length: 14}).map((v, i) => 3000 + (i + 1) * 500))
} else {
list = list.concat(Array.from({length: Math.floor((length - 3000) / 500)}).map((v, i) => 3000 + (i + 1) * 500))
}
let d = Math.floor((length - 10000) / 1000)
if (d > 0) {
list = list.concat(Array.from({length: d}).map((v, i) => 10000 + (i + 1) * 1000))
}
list.push(length)
columns = list.map(value => {
return {
text: value,
value,
}
})
let days = Array.from(new Set(list.map(v => Math.ceil(length / v)))).sort((a, b) => a - b)
columns2 = days.map(value => {
return {
text: value,
value
}
})
})
function confirm() {
runtimeStore.editDict.chapterWordNumber = chapterWordNumber[0]
store.changeDict(runtimeStore.editDict)
router.back()
}
</script>
<template>
<div class="plan">
<div class="content">
<div class="dict">
<div class="name">{{ runtimeStore.editDict.name }}</div>
<div class="chapter">每日{{ chapterWordNumber[0] }} 剩余{{ completeDay[0] }}</div>
<el-progress
:show-text="false"
:percentage="90"
/>
<div class="progress">
<span>已学单词</span>
<span>0/{{ runtimeStore.editDict.length }}</span>
</div>
</div>
<div class="notice">
<span>完成日期</span>
<span class="date">2023年1月1日</span>
<span>预计每天11分钟</span>
</div>
<div class="set-plan">
<div class="header">
<span>每天背单词</span>
<span>完成天数</span>
</div>
<div class="picker-wrapper">
<Picker
:show-toolbar="false"
:model-value="chapterWordNumber"
:columns="columns"
@change="onChange"
/>
<Picker
:show-toolbar="false"
:model-value="completeDay"
:columns="columns2"
@change="onChange2"
/>
</div>
</div>
</div>
<BaseButton size="large" @click="confirm">确认</BaseButton>
</div>
</template>
<style scoped lang="scss">
.plan {
height: 100%;
padding: 10rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
.content {
flex: 1;
.dict {
display: flex;
flex-direction: column;
gap: 10rem;
}
.set-plan {
background: white;
.header {
height: 60rem;
color: black;
display: flex;
justify-content: space-around;
align-items: center;
}
.picker-wrapper {
display: flex;
.van-picker {
flex: 1;
}
}
}
}
}
</style>

View File

@@ -1,186 +0,0 @@
<script lang="jsx">
import {emitter as bus} from "@/utils/eventBus.ts";
export default {
name: "Indicator",
props: {
activeIndex: {
type: Number,
default: () => 0
},
tabStyleWidth: {
type: String,
default: () => ''
},
tabTexts: {
type: Array,
default: () => []
},
tabRender: {
type: Function,
default: null
},
//用于和slidList绑定因为一个页面可能有多个slidList但只有一个indicator组件
name: {
type: String,
default: () => ''
},
},
data() {
return {
currentSlideItemIndex: this.activeIndex,
tabIndicatorRelationActiveIndexLefts: [],//指标和slideItem的index的对应left,
indicatorSpace: 0,//indicator之间的间距
}
},
computed: {},
render() {
/*
* <div class="tabs" ref="tabs">
<div class="tab"
style="{width : tabStyleWidth}"
v-for="(item,index) in tabTexts"
:class="currentSlideItemIndex === index?'active':''"
@click="changeIndex(index)">
<span>{{ item }}</span></div>
</div>
* */
return (
<div className='indicator-ctn'>
{this.tabRender ?
this.tabRender() :
<div className="tabs" ref="tabs">
{
this.tabTexts.map((item, index) => {
return (
<div className={this.currentSlideItemIndex === index ? 'active tab' : 'tab'}
style={{width: this.tabStyleWidth || 100 / this.tabTexts.length + '%'}}
onClick={e => this.changeIndex(index)}
>
< span> {item}</span>
</div>
)
})
}
</div>
}
<div className="indicator" ref="indicator"
style={{width: this.tabStyleWidth || 100 / this.tabTexts.length + '%'}}/>
</div>
)
},
mounted() {
this.initTabs()
bus.on(this.name + '-moved', this.move)
bus.on(this.name + '-end', this.end)
},
methods: {
changeIndex(index) {
this.currentSlideItemIndex = index
this.$attrs['onUpdate:active-index'] && this.$emit('update:active-index', this.currentSlideItemIndex)
this.$setCss(this.indicatorRef, 'transition-duration', `300ms`)
this.$setCss(this.indicatorRef, 'left', this.tabIndicatorRelationActiveIndexLefts[this.currentSlideItemIndex] + 'px')
},
initTabs() {
let tabs = this.$refs.tabs
this.indicatorRef = this.$refs.indicator
for (let i = 0; i < tabs.children.length; i++) {
let item = tabs.children[i]
this.tabWidth = this.$getCss(item, 'width')
this.tabIndicatorRelationActiveIndexLefts.push(
item.getBoundingClientRect().x - tabs.children[0].getBoundingClientRect().x + (this.indicatorType === 'home' ? this.tabWidth * 0.15 : 0))
}
this.indicatorSpace = this.tabIndicatorRelationActiveIndexLefts[1] - this.tabIndicatorRelationActiveIndexLefts[0]
this.$setCss(this.indicatorRef, 'transition-duration', `0ms`)
this.$setCss(this.indicatorRef, 'left', this.tabIndicatorRelationActiveIndexLefts[this.currentSlideItemIndex] + 'px')
},
move(e) {
this.$setCss(this.indicatorRef, 'left',
this.tabIndicatorRelationActiveIndexLefts[this.currentSlideItemIndex] -
e.x.distance / (this.$store.state.bodyWidth / this.indicatorSpace) + 'px')
},
end(index) {
// console.log(index)
this.currentSlideItemIndex = index
this.$setCss(this.indicatorRef, 'transition-duration', `300ms`)
this.$setCss(this.indicatorRef, 'left',
this.tabIndicatorRelationActiveIndexLefts[this.currentSlideItemIndex] + 'px')
setTimeout(() => {
this.$setCss(this.indicatorRef, 'transition-duration', `0ms`)
}, 300)
}
}
}
</script>
<style scoped lang="scss">
$main-bg: rgb(21, 23, 36);
$active-main-bg: rgb(31, 37, 52);
$second-text-color: rgb(143, 143, 158);
$second-btn-color: rgb(58, 58, 70);
$second-btn-color-tran: rgba(58, 58, 70, .4);
$line-color: rgb(37, 45, 66);
$line-color2: rgb(56, 54, 67);
$footer-color: black;
$primary-btn-color: rgb(252, 47, 86);
$disable-primary-btn-color: rgba(252, 47, 86, .5);
$mask-dark: #000000bb;
$mask-light: transparent;
$mask-white: transparent;
$mask-lightgray: rgba(0, 0, 0, 0.25);
$footer-height: 50rem;
$header-height: 50rem;
$indicator-height: 50rem;
$padding-page: 15rem;
.indicator-ctn {
font-size: 14rem;
width: 100%;
height: $indicator-height;
top: 0;
left: 0;
right: 0;
z-index: 1;
background: $main-bg;
.tabs {
display: flex;
justify-content: space-between;
font-weight: bold;
.tab {
height: 45rem;
width: 45%;
display: flex;
justify-content: center;
align-items: center;
color: gray;
transition: color .3s;
&.active {
color: white;
}
img {
margin-left: 5rem;
$width: 12rem;
width: $width;
height: $width;
}
}
}
.indicator {
height: 2px;
background: gold;
width: 45%;
position: relative;
transition: all .3s;
}
}
</style>

View File

@@ -1,108 +0,0 @@
<script setup lang="ts">
import {useBaseStore} from "@/stores/base.ts"
import {computed, provide, watch} from "vue"
import {useSettingStore} from "@/stores/setting.ts";
import {EventKey, useEvent} from "@/utils/eventBus.ts";
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
import SlideItem from "@/components/slide/SlideItem.vue";
import CollectList from "@/pages/mobile/components/CollectList.vue";
import WrongList from "@/pages/mobile/components/WrongList.vue";
import SimpleList from "@/pages/mobile/components/SimpleList.vue";
const store = useBaseStore()
const settingStore = useSettingStore()
let tabIndex = $ref(0)
provide('tabIndex', computed(() => tabIndex))
watch(() => settingStore.showPanel, n => {
if (n) {
tabIndex = 0
}
})
useEvent(EventKey.changeDict, () => {
tabIndex = 0
})
</script>
<template>
<div class="panel anim">
<header>
<div class="tabs">
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">当前</div>
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">{{ store.collectWord.name }}</div>
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">{{ store.wrong.name }}</div>
<div class="tab" :class="tabIndex === 3 && 'active'" @click="tabIndex = 3">{{ store.known.name }}</div>
</div>
</header>
<SlideHorizontal v-model:index="tabIndex">
<SlideItem>
<slot :active="tabIndex === 0 && settingStore.showPanel"></slot>
</SlideItem>
<SlideItem>
<CollectList/>
</SlideItem>
<SlideItem>
<WrongList/>
</SlideItem>
<SlideItem>
<SimpleList/>
</SlideItem>
</SlideHorizontal>
</div>
</template>
<style scoped lang="scss">
$header-height: 50rem;
.panel {
border-radius: 8rem;
width: 100%;
background: var(--color-second-bg);
height: 100%;
display: flex;
flex-direction: column;
transition: all .3s;
z-index: 1;
border: 1px solid var(--color-item-border);
box-shadow: var(--shadow);
& > header {
min-height: 50rem;
box-sizing: border-box;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10rem 15rem;
border-bottom: 1px solid #e1e1e1;
gap: 15rem;
.close {
cursor: pointer;
}
.tabs {
display: flex;
align-items: center;
gap: 15rem;
font-size: 14rem;
.tab {
cursor: pointer;
word-break: keep-all;
font-size: 16rem;
transition: all .3s;
color: gray;
&.active {
color: var(--color-main-active);
font-weight: bold;
}
}
}
}
}
</style>

View File

@@ -1,36 +0,0 @@
<script setup lang="ts">
import BackIcon from "@/components/icon/BackIcon.vue";
import {useRouter} from "vue-router";
const router = useRouter()
defineProps<{
title?: string
}>()
</script>
<template>
<div class="nav-bar">
<BackIcon @click="router.back()"/>
<div class="title" v-if="title">{{ title }}</div>
</div>
</template>
<style scoped lang="scss">
.nav-bar {
box-sizing: border-box;
width: 100%;
height: 50rem;
padding: 0 var(--space);
display: flex;
align-items: center;
justify-content: center;
position: relative;
font-size: 20rem;
:deep(.back-icon) {
left: var(--space);
position: absolute;
}
}
</style>

View File

@@ -1,71 +0,0 @@
<script setup lang="ts">
import BaseIcon from "@/components/BaseIcon.vue";
import WordList from "@/pages/pc/components/list/WordList.vue";
import BaseButton from "@/components/BaseButton.vue";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
import {Dict, DictType} from "@/types.ts";
import {useBaseStore} from "@/stores/base.ts";
import {useWordOptions} from "@/hooks/dict.ts";
import Empty from "@/components/Empty.vue";
import {cloneDeep} from "lodash-es";
import {useRouter} from "vue-router";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
const router = useRouter()
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
let practiceType = $ref(DictType.word)
function changeIndex(dict: Dict) {
store.changeDict(dict, practiceType)
}
const {
delSimpleWord,
} = useWordOptions()
function addSimple() {
runtimeStore.editDict = cloneDeep(store.simple)
router.push({path: '/dict', query: {type: 'addWordOrArticle'}})
}
</script>
<template>
<div class="panel-page-item">
<div class="list-header">
<div class="left">
<div class="dict-name">总词数{{ store.known.words.length }}</div>
<BaseIcon icon="fluent:add-12-regular" title="添加" @click="addSimple"/>
</div>
<template v-if="store.sdict.type !== DictType.known && store.known.words.length">
<PopConfirm
:title="`确认切换?`"
@confirm="changeIndex( store.known)"
>
<BaseButton size="small">切换</BaseButton>
</PopConfirm>
</template>
</div>
<WordList
v-if="store.known.words.length"
class="word-list"
:list="store.known.words">
<template v-slot:suffix="{item,index}">
<BaseIcon
class="del"
@click="delSimpleWord(item)"
title="移除"
icon="solar:trash-bin-minimalistic-linear"/>
</template>
</WordList>
<Empty v-else/>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,59 +0,0 @@
<script setup lang="ts">
import BaseIcon from "@/components/BaseIcon.vue";
import WordList from "@/pages/pc/components/list/WordList.vue";
import BaseButton from "@/components/BaseButton.vue";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
import {Dict, DictType} from "@/types.ts";
import {useBaseStore} from "@/stores/base.ts";
import {useWordOptions} from "@/hooks/dict.ts";
import Empty from "@/components/Empty.vue";
const store = useBaseStore()
let practiceType = $ref(DictType.word)
function changeIndex(dict: Dict) {
store.changeDict(dict, practiceType)
}
const {
delWrongWord,
} = useWordOptions()
</script>
<template>
<div class="panel-page-item">
<template v-if="store.wrong.words.length">
<div class="list-header">
<div class="dict-name">总词数{{ store.wrong.words.length }}</div>
<template
v-if="store.sdict.type !== DictType.wrong && store.wrong.words.length">
<PopConfirm
:title="`确认切换?`"
@confirm="changeIndex( store.wrong)"
>
<BaseButton size="small">切换</BaseButton>
</PopConfirm>
</template>
</div>
<WordList
class="word-list"
:list="store.wrong.words">
<template v-slot:suffix="{item,index}">
<BaseIcon
class="del"
@click="delWrongWord(item)"
title="移除"
icon="solar:trash-bin-minimalistic-linear"/>
</template>
</WordList>
</template>
<Empty v-else/>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,67 +0,0 @@
<script setup>
import {Icon} from "@iconify/vue";
import Home from "@/pages/mobile/Home.vue";
import DictListManage from "@/pages/mobile/DictListManage.vue";
import My from "@/pages/mobile/my/My.vue";
import {onMounted} from "vue";
defineOptions({
name: 'Practice'
})
let index = $ref(2)
onMounted(() => {
console.log('onMounted')
})
</script>
<template>
<div class="mobile-page mobile">
<div class="content">
<Home v-if="index === 0 "/>
<DictListManage v-if="index === 1"/>
<My v-if="index === 2"/>
</div>
<div class="tabs">
<div class="tab" @click="index = 0">
<Icon width="30" icon="ph:exam"/>
<span>单词</span>
</div>
<div class="tab" @click="index = 1">
<Icon width="30" icon="iconoir:book"/>
<span>词典</span>
</div>
<div class="tab" @click="index = 2">
<Icon width="30" icon="iconoir:user"/>
<span>我的</span>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.mobile {
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.tabs {
border-top: 1px solid var(--color-item-bg);
height: 60rem;
display: flex;
align-items: center;
.tab {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 14rem;
}
}
}
</style>

View File

@@ -1,81 +0,0 @@
<script setup lang="ts">
import NavBar from "@/pages/mobile/components/NavBar.vue";
import {APP_NAME} from "../../../utils/const.ts";
</script>
<template>
<div class="mobile-page">
<NavBar title="关于我们"/>
<div class="page-content">
<div class="name">{{ APP_NAME }}</div>
<div class="desc">可在网页上使用的背单词软件</div>
<div class="git">
Github地址 <a target="_blank" href="https://github.com/zyronon/typing-word">https://github.com/zyronon/typing-word</a>
</div>
<div class="features">功能列表</div>
<ul>
<li>
<div class="title">背单词</div>
<div class="txt">可以选择记忆或默写单词提供了音标显示发音功能均可选美音英音错误统计</div>
</li>
<li>
<div class="title">背文章</div>
<div class="txt">
内置经典教材书籍可以练习和背诵文章逐句输入自动发音也可以自行添加导入文章提供一键翻译译文对照功能
</div>
</li>
<li>
<div class="title">生词本错词本已掌握</div>
<div class="txt">
默写单词时输入错误会自动添加到错词本以便后续复习也可以添加到已掌握之后再遇到这个词便会自动跳过同时也可以将其添加到生词本中以便巩固复习
</div>
</li>
<li>
<div class="title">默写模式</div>
<div class="txt">
在用户完成一个章节的练习后如果有错误词那么会重复练习错误词直到没有错误词为止完成之后弹出选项可选择默写本章重复本章下一章
</div>
</li>
<li>
<div class="title">词库</div>
<div class="txt">内置了常用的 CET-4 CET-6 GMAT GRE IELTS SAT TOEFL
考研英语专业四级英语专业八级英语也有程序员常见英语单词以及多种编程语言
API 等词库 尽可能满足大部分用户对背单词的需求也非常欢迎社区贡献更多的词库
</div>
</li>
</ul>
</div>
</div>
</template>
<style scoped lang="scss">
.mobile-page {
.name {
padding: 30rem 0;
font-size: 46rem;
text-align: center;
border-bottom: 1px solid #c9c9c9;
margin-bottom: 20rem;
}
.desc {
font-size: 22rem;
}
.features {
margin-top: 20rem;
font-weight: bold;
font-size: 22rem;
}
li {
margin: 10rem 0;
.title {
font-weight: bold;
}
}
}
</style>

View File

@@ -1,16 +0,0 @@
<script setup lang="ts">
import NavBar from "@/pages/mobile/components/NavBar.vue";
import CollectList from "@/pages/mobile/components/CollectList.vue";
</script>
<template>
<div class="mobile-page">
<NavBar title="收藏"/>
<CollectList/>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,102 +0,0 @@
<script setup lang="ts">
import {APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
import BaseButton from "@/components/BaseButton.vue";
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, shakeCommonDict} from "@/utils";
import {saveAs} from "file-saver";
import {useSettingStore} from "@/stores/setting.ts";
import {useBaseStore} from "@/stores/base.ts";
import NavBar from "@/pages/mobile/components/NavBar.vue";
const settingStore = useSettingStore()
const store = useBaseStore()
function exportData() {
let data = {
version: EXPORT_DATA_KEY.version,
val: {
setting: {
version: SAVE_SETTING_KEY.version,
val: settingStore.$state
},
dict: {
version: SAVE_DICT_KEY.version,
val: shakeCommonDict(store.$state)
}
}
}
let blob = new Blob([JSON.stringify(data)], {type: "text/plain;charset=utf-8"});
let date = new Date()
let dateStr = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`
saveAs(blob, `${APP_NAME}-User-Data-${dateStr}.json`);
ElMessage.success('导出成功!')
}
function importData(e) {
let file = e.target.files[0]
if (!file) return
// no()
let reader = new FileReader();
reader.onload = function (v) {
let str = v.target.result;
if (str) {
let obj = JSON.parse(str)
if (obj.version === EXPORT_DATA_KEY.version) {
} else {
//TODO
}
let data = obj.val
let settingState = checkAndUpgradeSaveSetting(data.setting)
settingStore.setState(settingState)
let dictState = checkAndUpgradeSaveDict(data.dict)
store.init(dictState)
ElMessage.success('导入成功!')
}
}
reader.readAsText(file);
}
</script>
<template>
<div class="mobile-page">
<NavBar title="数据管理"/>
<div class="page-content">
<div class="row">
<div class="main-title">数据导出</div>
</div>
<div class="row">
<label class="sub-title">
目前用户的所有数据(自定义设置自定义词典练习进度等)
<b>仅保存在本地</b>
如果您需要在不同的设备浏览器或者其他非官方部署上使用 {{ APP_NAME }} 您需要手动进行数据同步和保存
</label>
</div>
<div class="row">
<BaseButton @click="exportData">数据导出</BaseButton>
</div>
<div class="row">
<div class="main-title">数据导入</div>
</div>
<div class="row">
<label class="sub-title">
请注意导入数据将
<b style="color: red"> 完全覆盖 </b>
当前数据请谨慎操作
</label>
</div>
<div class="row">
<div class="import hvr-grow">
<BaseButton>数据导入</BaseButton>
<input type="file"
accept="application/json"
@change="importData">
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,62 +0,0 @@
<script setup lang="ts">
import NavBar from "@/pages/mobile/components/NavBar.vue";
import {GITHUB} from "@/config/ENV.ts";
import BaseButton from "@/components/BaseButton.vue";
</script>
<template>
<div class="mobile-page">
<NavBar title="反馈问题"/>
<div class="page-content">
<div>
给我发Email<a href="mailto:zyronon@163.com">zyronon@163.com</a>
</div>
<p>or</p>
<div class="github">
<span><a :href="GITHUB" target="_blank">Github</a>上给我提一个
<a :href="`${GITHUB}/issues`" target="_blank">Issue</a>
</span>
<div class="options">
<BaseButton>
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=%E5%8D%95%E8%AF%8D%E9%94%99%E8%AF%AF---word-error.md&title=%E5%8D%95%E8%AF%8D%E9%94%99%E8%AF%AF+%7C+Word+error`"
target="_blank">词典错误</a>
</BaseButton>
<BaseButton>
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=问题报告---bug-report-.md&title=问题报告+%7C+Bug+report+`"
target="_blank">反馈BUG</a>
</BaseButton>
<BaseButton>
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=功能请求---feature-request.md&title=功能请求+%7C+Feature+request`"
target="_blank">功能请求</a>
</BaseButton>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.page-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
p {
font-size: 30rem;
}
.github {
display: flex;
align-items: center;
gap: var(--space);
.options {
display: flex;
flex-direction: column;
gap: 10rem;
}
}
</style>

View File

@@ -1,212 +0,0 @@
<script setup lang="ts">
import {Icon} from "@iconify/vue";
import IconWrapper from "@/pages/pc/components/IconWrapper.vue";
import useTheme from "@/hooks/theme.ts";
import {useSettingStore} from "@/stores/setting.ts";
import SlideItem from "@/components/slide/SlideItem.vue";
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import WordList from "@/pages/pc/components/list/WordList.vue";
import {useRouter} from "vue-router";
import {useBaseStore} from "@/stores/base.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
const {toggleTheme} = useTheme()
const router = useRouter()
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
let index = $ref(0)
let isShowStarCount = $ref(false)
function $nav() {
}
function $no() {
}
</script>
<template>
<div class="page my">
<div ref="float" class="float">
<div class="right">
<IconWrapper>
<Icon icon="fluent:search-24-regular"/>
</IconWrapper>
<IconWrapper>
<Icon icon="ep:moon"
v-if="settingStore.theme === 'dark'"
@click="toggleTheme"/>
<Icon icon="tabler:sun" v-else @click="toggleTheme"/>
</IconWrapper>
</div>
</div>
<div ref="desc" class="desc">
<header ref="header"></header>
<div class="detail">
<div class="heat">
<div class="text" @click="router.push('/mobile/collect')">
<span>收藏</span>
<span class="num">123</span>
</div>
<div class="text" @click="router.push('/mobile/wrong')">
<span>错误</span>
<span class="num">123</span>
</div>
<div class="text" @click="router.push('/mobile/simple')">
<span>已掌握</span>
<span class="num">123</span>
</div>
</div>
<div class="description">
<span>您已坚持了164天加油</span>
</div>
<div class="grid">
<div class="item" @click="router.push('/mobile/collect')">
<img src="../../../assets/img/collect.png" alt="">
<span>收藏</span>
</div>
<div class="item" @click="router.push('/mobile/wrong')">
<img src="../../../assets/img/book2.png" alt="">
<span>错词本</span>
</div>
<div class="item" @click="router.push('/mobile/simple')">
<img src="../../../assets/img/complete.png" alt="">
<span>已掌握</span>
</div>
</div>
<div class="setting-list">
<div class="item" @click="router.push('/mobile/setting')">
<Icon icon="uil:setting" width="22"/>
<div class="right">
<span>设置</span>
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
</div>
</div>
<div class="item" @click="router.push('/mobile/data-manage')">
<Icon icon="mdi:database-cog-outline" width="22"/>
<div class="right">
<span>数据同步</span>
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
</div>
</div>
<div class="item" @click="router.push('/mobile/feedback')">
<Icon icon="pepicons-pencil:letter-open" width="22"/>
<div class="right">
<span>反馈问题</span>
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
</div>
</div>
<div class="item" @click="router.push('/mobile/about')">
<Icon icon="mdi:about-circle-outline" width="22"/>
<div class="right" style="border-bottom: none">
<span>关于我们</span>
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@use '../common' as *;
.my {
font-size: 18rem;
display: flex;
flex-direction: column;
align-items: center;
color: var(--color-font-2);
background: var(--color-second-bg);
.float {
position: fixed;
box-sizing: border-box;
width: 100vw;
z-index: 2;
display: flex;
justify-content: flex-end;
align-items: center;
height: 46rem;
padding: 0 15rem;
background: transparent;
transition: all .2s;
.right {
}
}
.desc {
width: 100%;
header {
color: white;
height: 200rem;
background-image: url('../../../assets/img/a.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
box-sizing: border-box;
}
.detail {
transform: translateY(-50rem);
//padding: 20rem;
background: var(--color-second-bg);
padding-top: 30rem;
border-radius: 20rem 20rem 0 0;
display: flex;
flex-direction: column;
gap: 20rem;
padding: 20rem;
.heat {
display: flex;
align-items: center;
font-size: 16rem;
gap: 30rem;
.num {
font-weight: bold;
}
.text {
display: flex;
align-items: center;
gap: 10rem;
}
}
.description {
font-size: 16rem;
}
.grid {
display: flex;
justify-content: space-between;
padding: 20rem;
background: var(--color-header-bg);
border-radius: 8rem;
.item {
height: 60rem;
gap: 10rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
img {
width: 40rem;
}
}
}
}
}
}
</style>

View File

@@ -1,17 +0,0 @@
<script setup lang="ts">
import NavBar from "@/pages/mobile/components/NavBar.vue";
import WrongList from "@/pages/mobile/components/WrongList.vue";
import SimpleList from "@/pages/mobile/components/SimpleList.vue";
</script>
<template>
<div class="mobile-page">
<NavBar title="已掌握"/>
<SimpleList/>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,16 +0,0 @@
<script setup lang="ts">
import NavBar from "@/pages/mobile/components/NavBar.vue";
import WrongList from "@/pages/mobile/components/WrongList.vue";
</script>
<template>
<div class="mobile-page">
<NavBar title="错词本"/>
<WrongList/>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,243 +0,0 @@
<script setup lang="ts">
import NavBar from "@/pages/mobile/components/NavBar.vue";
import {getAudioFileUrl, useChangeAllSound, usePlayAudio} from "@/hooks/sound.ts";
import {SoundFileOptions} from "@/utils/const.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {useBaseStore} from "@/stores/base.ts";
const settingStore = useSettingStore()
const store = useBaseStore()
</script>
<template>
<div class="mobile-page">
<NavBar title="音效设置"/>
<div class="page-content">
<div class="row">
<label class="main-title">所有音效</label>
<div class="wrapper">
<el-switch v-model="settingStore.allSound"
@change="useChangeAllSound"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">单词/句子自动发音</label>
<div class="wrapper">
<el-switch v-model="settingStore.wordSound"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="row">
<label class="sub-title">单词/句子发音口音</label>
<div class="wrapper">
<el-select v-model="settingStore.wordSoundType"
placeholder="请选择"
>
<el-option label="美音" value="us"/>
<el-option label="英音" value="uk"/>
</el-select>
</div>
</div>
<div class="row">
<label class="sub-title">音量</label>
<div class="wrapper">
<el-slider v-model="settingStore.wordSoundVolume"/>
<span>{{ settingStore.wordSoundVolume }}%</span>
</div>
</div>
<div class="row">
<label class="sub-title">倍速</label>
<div class="wrapper">
<el-slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
<span>{{ settingStore.wordSoundSpeed }}</span>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">按键音</label>
<div class="wrapper">
<el-switch v-model="settingStore.keyboardSound"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="row">
<label class="item-title">按键音效</label>
<div class="wrapper">
<el-select v-model="settingStore.keyboardSoundFile"
placeholder="请选择"
>
<el-option
v-for="item in SoundFileOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
<div class="el-option-row">
<span>{{ item.label }}</span>
<VolumeIcon
:time="100"
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
</div>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<label class="sub-title">音量</label>
<div class="wrapper">
<el-slider v-model="settingStore.keyboardSoundVolume"/>
<span>{{ settingStore.keyboardSoundVolume }}%</span>
</div>
</div>
<div class="line"></div>
<!-- <div class="row">-->
<!-- <label class="item-title">释义发音</label>-->
<!-- <div class="wrapper">-->
<!-- <el-switch v-model="settingStore.translateSound"-->
<!-- inline-prompt-->
<!-- active-text="开"-->
<!-- inactive-text="关"-->
<!-- />-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="row">-->
<!-- <label class="sub-title">音量</label>-->
<!-- <div class="wrapper">-->
<!-- <el-slider v-model="settingStore.translateSoundVolume"/>-->
<!-- <span>{{ settingStore.translateSoundVolume }}%</span>-->
<!-- </div>-->
<!-- </div>-->
<div class="line"></div>
<div class="row">
<label class="item-title">效果音(章节结算页烟花音效)</label>
<div class="wrapper">
<el-switch v-model="settingStore.effectSound"
inline-prompt
active-text="开"
inactive-text="关"
/>
</div>
</div>
<div class="row">
<label class="sub-title">音量</label>
<div class="wrapper">
<el-slider v-model="settingStore.effectSoundVolume"/>
<span>{{ settingStore.effectSoundVolume }}%</span>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.page-content {
background: var(--color-header-bg);
flex: 1;
height: 100%;
overflow: auto;
padding: 10rem var(--space);
.row {
min-height: 40rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: calc(var(--space) * 5);
.wrapper {
height: 30rem;
flex: 1;
display: flex;
justify-content: flex-end;
gap: var(--space);
span {
text-align: right;
//width: 30rem;
font-size: 12rem;
color: gray;
}
.set-key {
align-items: center;
input {
width: 150rem;
box-sizing: border-box;
margin-right: 10rem;
height: 28rem;
outline: none;
font-size: 16rem;
border: 1px solid gray;
border-radius: 3rem;
padding: 0 5rem;
background: var(--color-second-bg);
color: var(--color-font-1);
}
}
}
.main-title {
font-size: 22rem;
}
.item-title {
font-size: 16rem;
}
.sub-title {
font-size: 14rem;
}
}
.body {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.scroll {
flex: 1;
padding-right: 10rem;
overflow: auto;
}
.footer {
margin-bottom: 20rem;
}
.desc {
margin-bottom: 10rem;
font-size: 12rem;
}
.line {
border-bottom: 1px solid #c4c3c3;
}
}
.el-option-row {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.icon-wrapper {
transform: translateX(10rem);
}
}
</style>

View File

@@ -1,202 +0,0 @@
<script setup lang="ts">
import NavBar from "@/pages/mobile/components/NavBar.vue";
import {getAudioFileUrl, useChangeAllSound, usePlayAudio} from "@/hooks/sound.ts";
import {SoundFileOptions} from "@/utils/const.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {useBaseStore} from "@/stores/base.ts";
import {ShortcutKey} from "@/types.ts";
const settingStore = useSettingStore()
const store = useBaseStore()
</script>
<template>
<div class="mobile-page">
<NavBar title="其他设置"/>
<div class="page-content">
<div class="row">
<label class="item-title">显示上一个/下一个单词</label>
<div class="wrapper">
<el-switch v-model="settingStore.showNearWord"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="desc">
开启后练习中会在上方显示上一个/下一个单词
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">忽略大小写</label>
<div class="wrapper">
<el-switch v-model="settingStore.ignoreCase"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="desc">
开启后输入时不区分大小写如输入helloHello都会被认为是正确的
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">允许默写模式下显示提示</label>
<div class="wrapper">
<el-switch v-model="settingStore.allowWordTip"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="desc">
开启后可以通过鼠标 hover 单词或者按 {{ settingStore.shortcutKeyMap[ShortcutKey.ShowWord] }} 显示正确答案
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">字体设置(仅可调整单词练习)</label>
</div>
<div class="row">
<label class="sub-title">外语字体</label>
<div class="wrapper">
<el-slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordForeignFontSize"/>
<span>{{ settingStore.fontSize.wordForeignFontSize }}</span>
</div>
</div>
<div class="row">
<label class="sub-title">中文字体</label>
<div class="wrapper">
<el-slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordTranslateFontSize"/>
<span>{{ settingStore.fontSize.wordTranslateFontSize }}</span>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">其他设置</label>
</div>
<div class="row">
<label class="sub-title">切换下一个单词时间</label>
<div class="wrapper">
<el-input-number v-model="settingStore.waitTimeForChangeWord"
:min="6"
:max="100"
type="number"
/>
<span>毫秒</span>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.page-content {
background: var(--color-header-bg);
flex: 1;
height: 100%;
overflow: auto;
padding: 10rem var(--space);
.row {
min-height: 40rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: calc(var(--space) * 5);
.wrapper {
height: 30rem;
flex: 1;
display: flex;
justify-content: flex-end;
gap: var(--space);
span {
text-align: right;
//width: 30rem;
font-size: 12rem;
color: gray;
}
.set-key {
align-items: center;
input {
width: 150rem;
box-sizing: border-box;
margin-right: 10rem;
height: 28rem;
outline: none;
font-size: 16rem;
border: 1px solid gray;
border-radius: 3rem;
padding: 0 5rem;
background: var(--color-second-bg);
color: var(--color-font-1);
}
}
}
.main-title {
font-size: 22rem;
}
.item-title {
font-size: 16rem;
}
.sub-title {
font-size: 14rem;
}
}
.body {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.scroll {
flex: 1;
padding-right: 10rem;
overflow: auto;
}
.footer {
margin-bottom: 20rem;
}
.desc {
margin-bottom: 10rem;
font-size: 12rem;
}
.line {
border-bottom: 1px solid #c4c3c3;
}
}
.el-option-row {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.icon-wrapper {
transform: translateX(10rem);
}
}
</style>

View File

@@ -1,64 +0,0 @@
<script setup lang="ts">
import {Icon} from "@iconify/vue";
import useTheme from "@/hooks/theme.ts";
import {useSettingStore} from "@/stores/setting.ts";
import NavBar from "@/pages/mobile/components/NavBar.vue";
import {ref} from "vue";
import router from "@/router.ts";
const {toggleTheme} = useTheme()
const settingStore = useSettingStore()
// @ts-ignore
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
</script>
<template>
<div class="mobile-page setting">
<NavBar title="设置"/>
<div class="content">
<div class="setting-list">
<div class="item" @click="router.push('music-setting')">
<Icon icon="bx:headphone" width="22"/>
<div class="right">
<span>音效设置</span>
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
</div>
</div>
<div class="item" @click="router.push('other-setting')">
<Icon icon="icon-park-outline:setting-config" width="22"/>
<div class="right" style="border-bottom: none">
<span>其他设置</span>
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
</div>
</div>
</div>
<div class="git-log">
Build {{ gitLastCommitHash }}
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@use '../../common' as *;
.setting {
display: flex;
flex-direction: column;
background: var(--color-second-bg);
.content {
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 20rem;
box-sizing: border-box;
}
}
</style>

View File

@@ -1,113 +0,0 @@
<script setup lang="ts">
import {onDeactivated, onMounted, onUnmounted, watch} from "vue";
import {usePracticeStore} from "@/stores/practice.ts";
import {useBaseStore} from "@/stores/base.ts";
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";
defineOptions({
name: 'PracticeWord'
})
const store = useBaseStore()
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const {toggleTheme} = useTheme()
const practiceRef: any = $ref()
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.sdict.chapterIndex === 0) {
ElMessage.warning('已经在第一章了~')
} else {
store.sdict.chapterIndex--
repeat()
}
}
function toggleShowTranslate() {
settingStore.translate = !settingStore.translate
}
function toggleDictation() {
settingStore.dictation = !settingStore.dictation
}
function toggleConciseMode() {
settingStore.showToolbar = !settingStore.showToolbar
settingStore.showPanel = settingStore.showToolbar
}
function togglePanel() {
settingStore.showPanel = !settingStore.showPanel
}
function jumpSpecifiedChapter(val: number) {
store.sdict.chapterIndex = val
repeat()
}
onMounted(() => {
emitter.on(EventKey.write, write)
emitter.on(EventKey.repeatStudy, 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.ToggleTheme, toggleTheme)
emitter.on(ShortcutKey.ToggleConciseMode, toggleConciseMode)
emitter.on(ShortcutKey.TogglePanel, togglePanel)
})
onUnmounted(() => {
emitter.off(EventKey.write, write)
emitter.off(EventKey.repeatStudy, 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.ToggleTheme, toggleTheme)
emitter.off(ShortcutKey.ToggleConciseMode, toggleConciseMode)
emitter.off(ShortcutKey.TogglePanel, togglePanel)
})
useStartKeyboardEventListener()
</script>
<template>
<div class="mobile-page">
<PracticeWord ref="practiceRef"/>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,352 +0,0 @@
<script setup lang="ts">
import {getDefaultWord, ShortcutKey, Word} from "@/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useSettingStore} from "@/stores/setting.ts";
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {nextTick, onMounted, onUnmounted, watch} from "vue";
import Tooltip from "@/pages/pc/components/Tooltip.vue";
interface IProps {
word: Word,
}
const props = withDefaults(defineProps<IProps>(), {
word: () => getDefaultWord(),
})
const emit = defineEmits<{
complete: [],
wrong: []
}>()
let input = $ref('')
let wrong = $ref('')
let showFullWord = $ref(false)
//输入锁定因为跳转到下一个单词有延时如果重复在延时期间内重复输入导致会跳转N次
let inputLock = false
let wordRepeatCount = 0
const settingStore = useSettingStore()
const playBeep = usePlayBeep()
const playCorrect = usePlayCorrect()
const playKeyboardAudio = usePlayKeyboardAudio()
const playWordAudio = usePlayWordAudio()
const volumeIconRef: any = $ref()
let displayWord = $computed(() => {
return props.word.word.slice(input.length + wrong.length)
})
onMounted(() => {
emitter.on(EventKey.resetWord, () => {
wrong = input = ''
})
emitter.on(EventKey.onTyping, onTyping)
})
onUnmounted(() => {
emitter.off(EventKey.resetWord)
emitter.off(EventKey.onTyping, onTyping)
})
function repeat() {
setTimeout(() => {
wrong = input = ''
wordRepeatCount++
inputLock = false
if (settingStore.wordSound) {
volumeIconRef?.play()
}
}, settingStore.waitTimeForChangeWord)
}
async function onTyping(e: KeyboardEvent) {
if (inputLock) return
inputLock = true
let letter = e.key
let isTypingRight = false
let isWordRight = false
if (settingStore.ignoreCase) {
isTypingRight = letter.toLowerCase() === props.word.word[input.length].toLowerCase()
isWordRight = (input + letter).toLowerCase() === props.word.word.toLowerCase()
} else {
isTypingRight = letter === props.word.word[input.length]
isWordRight = (input + letter) === props.word.word
}
if (isTypingRight) {
input += letter
wrong = ''
playKeyboardAudio()
} else {
emit('wrong')
wrong = letter
playKeyboardAudio()
playBeep()
volumeIconRef?.play()
setTimeout(() => {
wrong = ''
}, 500)
}
if (isWordRight) {
playCorrect()
if (settingStore.repeatCount == 100) {
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
} else {
repeat()
}
} else {
if (settingStore.repeatCount <= wordRepeatCount + 1) {
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
} else {
repeat()
}
}
} else {
inputLock = false
}
}
function del() {
console.log('del')
playKeyboardAudio()
if (wrong) {
wrong = ''
} else {
input = input.slice(0, -1)
}
console.log(input)
}
function showWord() {
if (settingStore.allowWordTip) {
showFullWord = true
}
}
function hideWord() {
showFullWord = false
}
function play() {
volumeIconRef?.play()
}
defineExpose({del, showWord, hideWord, play})
let transHeight = $ref(150)
let transWrapperRef = $ref<HTMLDivElement>()
let showEnd = $ref(true)
const transStyle = $computed(() => {
return {
'justify-content': showEnd ? 'flex-end' : 'unset',
height: transHeight + 'px',
opacity: settingStore.translate ? 1 : 0
}
})
watch(() => props.word, () => {
wrong = input = ''
wordRepeatCount = 0
inputLock = false
if (settingStore.wordSound) {
volumeIconRef?.play(400, true)
}
transHeight = 150
nextTick(() => {
console.log('transWrapperRef.scrollHeight', transWrapperRef.scrollHeight)
let scrollHeight = transWrapperRef.scrollHeight
if (scrollHeight <= 240) {
showEnd = true
if (scrollHeight > transHeight) {
transHeight = scrollHeight
}
} else {
showEnd = scrollHeight <= transHeight
}
})
})
</script>
<template>
<div class="typing-word">
<div class="translate"
:style="transStyle"
>
<div class="wrapper" ref="transWrapperRef">
<div class="translate-item" v-for="(v,i) in word.trans">
<span>{{ (v.pos ? v.pos + '.' : '') + v.cn }}</span>
</div>
</div>
</div>
<div class="word-wrapper"
:style="{marginTop: transHeight + 6 + 'px'}"
>
<div class="word"
:class="wrong && 'is-wrong'"
:style="{fontSize: settingStore.fontSize.wordForeignFontSize +'rem'}"
>
<span class="input" v-if="input">{{ input }}</span>
<span class="wrong" v-if="wrong">{{ wrong }}</span>
<template v-if="settingStore.dictation">
<span class="letter" v-if="!showFullWord"
@mouseenter="settingStore.allowWordTip && (showFullWord = true)">{{
displayWord.split('').map(() => '_').join('')
}}</span>
<span class="letter" v-else @mouseleave="showFullWord = false">{{ displayWord }}</span>
</template>
<span class="letter" v-else>{{ displayWord }}</span>
</div>
<Tooltip
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
>
<VolumeIcon ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
</Tooltip>
</div>
<div class="phonetic" v-if="settingStore.wordSoundType === 'us' && word.phonetic0">[{{ word.phonetic0 }}]</div>
<div class="phonetic" v-if="settingStore.wordSoundType === 'uk' && word.phonetic1">[{{ word.phonetic1 }}]</div>
<transition name="fade">
<div class="other" v-if="settingStore.detail">
<div class="sentences" v-if="word.sentences && word.sentences.length">
<div class="title">例句</div>
<div class="sentence" v-for="item in word.sentences">
<div class="tran">{{ item.tran }}</div>
<div class="v">{{ item.v }}</div>
</div>
</div>
<div class="sentences" v-if="word.phrases && word.phrases.length">
<div class="title">短语</div>
<div class="sentence" v-for="item in word.phrases">
<div class="tran">{{ item.tran }}</div>
<div class="v">{{ item.v }}</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<style scoped lang="scss">
.typing-word {
width: 95%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
word-break: break-word;
position: relative;
color: var(--color-font-2);
overflow: auto;
padding-bottom: 20rem;
&::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.other {
margin-top: 10rem;
width: 100%;
font-size: 18rem;
.sentences {
margin-bottom: 20rem;
.title {
}
.sentence {
margin-bottom: 8rem;
.tran {
color: white;
font-size: 18rem;
margin-bottom: 2rem;
}
.v {
color: var(--color-font-1);
font-size: 14rem;
}
}
}
}
.phonetic, .translate {
transition: opacity .3s;
}
.phonetic {
font-size: 14rem;
margin-top: 5rem;
font-family: var(--word-font-family);
}
.translate {
font-size: 18rem;
width: 100%;
position: absolute;
height: 150px;
display: flex;
align-items: center;
flex-direction: column;
overflow: auto;
.wrapper {
}
&:hover {
.volumeIcon {
opacity: 1;
}
}
.translate-item {
display: flex;
align-items: center;
gap: 10rem;
}
.volumeIcon {
transition: opacity .3s;
opacity: 0;
}
}
.word-wrapper {
margin-top: 150px;
margin-left: 30rem;
display: flex;
align-items: center;
gap: 10rem;
color: var(--color-font-1);
.word {
font-size: 48rem;
line-height: 1;
font-family: var(--word-font-family);
letter-spacing: 5rem;
.input {
color: rgb(22, 163, 74);
}
.wrong {
color: rgba(red, 0.6);
}
&.is-wrong {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
}
}
}
</style>

View File

@@ -1,454 +0,0 @@
<script setup lang="ts">
import {onMounted, onUnmounted, watch} from "vue"
import {useBaseStore} from "@/stores/base.ts"
import {DefaultDisplayStatistics, DictType, getDefaultWord, 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 "@/pages/pc/components/list/WordList.vue";
import Empty from "@/components/Empty.vue";
import MiniDialog from "@/pages/pc/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";
import useTheme from "@/hooks/theme.ts";
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 statisticsStore = usePracticeStore()
const settingStore = useSettingStore()
const {toggleTheme} = useTheme()
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 = []
statisticsStore.startDate = Date.now()
statisticsStore.inputWordNumber = 0
statisticsStore.wrong = 0
stat = cloneDeep(DefaultDisplayStatistics)
}, {immediate: true})
watch(data, () => {
statisticsStore.total = data.words.length
statisticsStore.index = data.index
})
const word: Word = $computed(() => {
return data.words[data.index] ?? getDefaultWord()
})
function next(isTyping: boolean = true) {
if (data.index === data.words.length - 1) {
//复制当前错词,因为第一遍错词是最多的,后续的练习都是从错词中练习
if (stat.total === -1) {
let now = Date.now()
stat = {
startDate: statisticsStore.startDate,
endDate: now,
spend: now - statisticsStore.startDate,
total: props.words.length,
correctRate: -1,
inputWordNumber: statisticsStore.inputWordNumber,
wrong: data.wrongWords.length,
wrongWords: data.wrongWords,
}
stat.correctRate = 100 - Math.trunc(((stat.wrong) / (stat.total)) * 100)
}
if (data.wrongWords.length) {
console.log('当前背完了,但还有错词')
data.words = cloneDeep(data.wrongWords)
statisticsStore.total = data.words.length
statisticsStore.index = data.index = 0
statisticsStore.inputWordNumber = 0
statisticsStore.wrong = 0
data.wrongWords = []
} else {
console.log('这章节完了')
isTyping && statisticsStore.inputWordNumber++
let now = Date.now()
stat.endDate = now
stat.spend = now - stat.startDate
}
} else {
data.index++
isTyping && statisticsStore.inputWordNumber++
console.log('这个词完了')
if ([DictType.word].includes(store.sdict.type)
&& store.knownWords.includes(word.word.toLowerCase())) {
next()
}
}
}
function wordWrong() {
if (!store.wrong.originWords.find((v: Word) => v.word.toLowerCase() === word.word.toLowerCase())) {
store.wrong.originWords.push(word)
}
if (!data.wrongWords.find((v: Word) => v.word.toLowerCase() === word.word.toLowerCase())) {
data.wrongWords.push(word)
statisticsStore.wrong++
}
}
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 nextWord() {
settingStore.translate = false
settingStore.detail = false
setTimeout(() => {
next(true)
}, 300)
}
function complete() {
inputRef.blur()
settingStore.detail = true
}
function unknow() {
settingStore.translate = true
inputRef.focus()
}
let bodyHeight = $ref('100vh')
onMounted(() => {
bodyHeight = document.body.clientHeight + 'px'
})
</script>
<template>
<div class="practice-center" :style="{height:bodyHeight}">
<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
v-loading="!store.load"
ref="typingRef"
:word="word"
@complete="complete"
/>
<div class="options">
<div class="wrapper">
<BaseButton size="large" v-if="settingStore.detail" @click="nextWord">下一个</BaseButton>
<template v-else>
<BaseButton size="large" @click="unknow">不认识</BaseButton>
<BaseButton size="large" @click="nextWord">认识</BaseButton>
</template>
</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">
</div>
<BaseIcon title="切换词典"
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.sdict.chapterIndex < store.sdict.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">
.practice-center {
position: fixed;
z-index: 1;
font-size: 14rem;
color: black;
width: 100%;
left: 0;
top: 0;
height: 100vh;
display: flex;
.practice-body {
width: 100vw;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 0 10rem;
box-sizing: border-box;
.tool-bar {
width: 100%;
height: 50rem;
display: flex;
padding: 0 10rem;
align-items: center;
justify-content: space-between;
.right {
display: flex;
gap: 10rem;
}
}
:deep(.word) {
letter-spacing: 0;
font-size: 36rem !important;
}
.options {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 20rem;
.wrapper {
width: 80%;
display: flex;
gap: 20rem;
}
:deep(.base-button) {
flex: 1;
}
}
}
}
</style>

View File

@@ -1,58 +0,0 @@
<script setup lang="ts">
import TypingWord from "@/pages/mobile/practice/practice-word/TypingWord.vue";
import {useBaseStore} from "@/stores/base.ts";
import {onMounted} from "vue";
import {ShortcutKey, Word} from "@/types.ts";
import {EventKey, useEvents} from "@/utils/eventBus.ts";
const store = useBaseStore()
let wordData = $ref({
words: [],
index: -1
})
function getCurrentPractice() {
}
function sort(list: Word[]) {
wordData.index = 0
// syncMyDictList(store.currentDict)
}
function next() {
getCurrentPractice()
}
onMounted(() => {
getCurrentPractice()
})
useEvents([
[EventKey.changeDict, getCurrentPractice],
[EventKey.continueStudy, next],
[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;
}
</style>

View File

@@ -18,7 +18,7 @@ function pathResolve(dir: string) {
const lifecycle = process.env.npm_lifecycle_event;
async function s() {
async function getConfig() {
const latestCommitHash = await new Promise<string>((resolve) => {
return getLastCommit((err, commit) => (err ? 'unknown' : resolve(commit.shortHash)))
})
@@ -92,7 +92,7 @@ async function s() {
},
server: {
port: 3000,
open: false,
open: true,
host: '0.0.0.0',
fs: {
strict: false,
@@ -105,4 +105,4 @@ async function s() {
}
// https://vitejs.dev/config/
export default defineConfig(s as any)
export default defineConfig(getConfig as any)