feat:save
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>-->
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的
|
||||
</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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user