Compare commits
2 Commits
36891a901a
...
3cf483c422
| Author | SHA1 | Date |
|---|---|---|
|
|
3cf483c422 | |
|
|
eb4010bef0 |
|
|
@ -1,5 +1,6 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
],
|
||||
plugins: ["@babel/plugin-transform-private-methods"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"boxicons": "^2.1.4",
|
||||
"core-js": "^3.8.3",
|
||||
"crypto-js": "^4.1.1",
|
||||
"marked": "^11.1.1",
|
||||
"node-uuid": "^1.4.8",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.2.5",
|
||||
|
|
@ -25,7 +26,8 @@
|
|||
"@vue/cli-service": "~5.0.0",
|
||||
"element-plus": "^2.3.14",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3"
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"raw-loader": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
|
@ -7695,6 +7697,17 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-11.1.1.tgz",
|
||||
"integrity": "sha512-EgxRjgK9axsQuUa/oKMx5DEY8oXpKJfk61rT5iY3aRlgU6QJtUcxU5OAymdhCvWvhYcd9FKmO5eQoX8m9VGJXg==",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
|
|
@ -9318,6 +9331,58 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-loader": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
|
||||
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-loader/node_modules/loader-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-loader/node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"boxicons": "^2.1.4",
|
||||
"core-js": "^3.8.3",
|
||||
"crypto-js": "^4.1.1",
|
||||
"marked": "^11.1.1",
|
||||
"node-uuid": "^1.4.8",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.2.5",
|
||||
|
|
@ -25,7 +26,8 @@
|
|||
"@vue/cli-service": "~5.0.0",
|
||||
"element-plus": "^2.3.14",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3"
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"raw-loader": "^4.0.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
|
|
|||
80
src/App.vue
80
src/App.vue
|
|
@ -1,8 +1,18 @@
|
|||
<template>
|
||||
<div id="navibar" class="pconly" >
|
||||
<div class="navi-item"><el-link @click="go('/')" id="logo">wordIn</el-link></div>
|
||||
<div class="navi-item"><el-link @click="go('/select')" class="link">背诵</el-link></div>
|
||||
<div class="navi-item"><el-link @click="go('/manage')" class="link">编辑单词本</el-link></div>
|
||||
<div class="navi-item"><el-link @click="go('/manual')" class="link">使用说明</el-link></div>
|
||||
<div class="navi-item"><el-link @click="go('/about')" class="link">关于</el-link></div>
|
||||
</div>
|
||||
<router-view style="margin-top: 10px;"></router-view>
|
||||
<!-- <div id="navibar" class="mbonly"></div> -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _wordsets from './js/wordsets'
|
||||
import _history from './js/history';
|
||||
|
||||
window.htmlClasses = [];
|
||||
window.addHtmlclasses = (classname) => {
|
||||
|
|
@ -30,19 +40,20 @@ export default {
|
|||
methods: {
|
||||
change(index) {
|
||||
this.index = index;
|
||||
},
|
||||
go(path) {
|
||||
this.$router.push(path);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
let wordsets = localStorage.getItem("wordsets");
|
||||
if (wordsets) {
|
||||
window.wordsets = JSON.parse(wordsets);
|
||||
} else window.wordsets = {};
|
||||
let res = localStorage.getItem("bgimg");
|
||||
if (res) {
|
||||
document.body.style.backgroundImage = `url("${res}")`;
|
||||
document.body.style.backgroundSize = "cover";
|
||||
window.addHtmlclasses("bgimged");
|
||||
}
|
||||
this.$store.state.wordsets = new _wordsets();
|
||||
this.$store.state.history = new _history();
|
||||
// let res = localStorage.getItem("bgimg");
|
||||
// if (res) {
|
||||
// document.body.style.backgroundImage = `url("${res}")`;
|
||||
// document.body.style.backgroundSize = "cover";
|
||||
// window.addHtmlclasses("bgimged");
|
||||
// }
|
||||
const isDarkTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
if (isDarkTheme.matches) window.addHtmlclasses("dark");
|
||||
isDarkTheme.addEventListener('change', (event) => {
|
||||
|
|
@ -84,13 +95,13 @@ export default {
|
|||
0% {
|
||||
translate: 100px 0;
|
||||
opacity: 0;
|
||||
filter: blur(20px);
|
||||
/* filter: blur(20px); */
|
||||
}
|
||||
|
||||
100% {
|
||||
translate: 0 0;
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
/* filter: blur(0px); */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,19 +109,20 @@ export default {
|
|||
html {
|
||||
--bg-color: #ffffffae;
|
||||
--text-color: #464646;
|
||||
--bd-color: #bbbbbb99;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--bg-color: #2a2a2aae;
|
||||
--bg-color: #2a2a2a88;
|
||||
--text-color: #c0c0c0;
|
||||
--bd-color: #7f7f7f7c;
|
||||
--navi-bg-color: #131313e2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.container {
|
||||
height: calc(100% - 20px);
|
||||
width: calc(100% - 20px);
|
||||
padding: 20px;
|
||||
/* padding: 20px; */
|
||||
animation: enter ease-out .6s backwards;
|
||||
}
|
||||
|
||||
|
|
@ -126,10 +138,14 @@ html.dark {
|
|||
|
||||
.card {
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--el-box-shadow);
|
||||
border: solid 1px var(--bd-color);
|
||||
padding: 20px;
|
||||
margin: 10px;
|
||||
color: var(--text-color);
|
||||
transition: .5s;
|
||||
}
|
||||
.card:hover{
|
||||
box-shadow: var(--el-box-shadow);
|
||||
}
|
||||
|
||||
html.bgimged .card {
|
||||
|
|
@ -153,3 +169,33 @@ html.bgimged .card {
|
|||
margin: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
#navibar {
|
||||
height: 60px;
|
||||
border-bottom: solid 1px var(--bd-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: var(--navi-bg-color);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.navi-item {
|
||||
height: 100%;
|
||||
font-size: 22px;
|
||||
display: flex;
|
||||
justify-content: baseline;
|
||||
margin-left: 20px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#logo {
|
||||
font-size: 28px;
|
||||
margin-right: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,17 +1,29 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div id="ball" class="pconly"></div>
|
||||
<div id="main">
|
||||
<div id="title"> WordIn</div>
|
||||
<div class="colbox">
|
||||
<router-link to="/recite" class="button">
|
||||
背诵
|
||||
<div class="container rowbox" style="height: calc(100% - 65px); align-items: center;width: 100%;overflow: auto;">
|
||||
<div class="colbox" style="width: 95%;margin-top: 10px;">
|
||||
<div style="flex-grow: 2;margin:30px;" class="rowbox">
|
||||
<div style="font-size: 25px;color: var(--text-color);font-weight: 600;">欢迎使用</div>
|
||||
<div id="title">wordIn</div>
|
||||
<div>当前版本: 1.01 更新时间:2024年1月11日 10:43 AM</div>
|
||||
</div>
|
||||
<div style="flex-grow: 1;align-items: center;" class="colbox card">
|
||||
<router-link to="/select" class="button">
|
||||
开始新背诵
|
||||
</router-link>
|
||||
<router-link to="/manage" class="button">
|
||||
编辑单词本
|
||||
查看单词本
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div id="history" class="wrapper">
|
||||
<div class="title" style="margin-left: 20px;margin-top: 10px;width: 95%;">历史记录</div>
|
||||
<div class="card colbox history-item" v-for="(history, index) in $store.state.history._content" :key="index">
|
||||
<div style="width: 25px;">{{ index + 1 }}</div>
|
||||
<div style="width: 200px;">背诵进度: {{ history.current }}/{{ history.total || "Unknown" }}</div>
|
||||
<div style="flex-grow: 1;"> {{ (new Date(history.modified)) }}</div>
|
||||
<el-button @click="restart(index)">继续</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="setting">
|
||||
<box-icon class="btn" color="var(--text-color)" name='cog' @click="open_setting_dialog"></box-icon>
|
||||
|
|
@ -52,6 +64,12 @@ export default {
|
|||
} else {
|
||||
window.delHtmlclasses("dark");
|
||||
}
|
||||
},
|
||||
restart(index){
|
||||
this.$router.push({
|
||||
path: "./recite",
|
||||
query: { index}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
|
@ -92,6 +110,7 @@ export default {
|
|||
.button {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -99,17 +118,18 @@ export default {
|
|||
|
||||
@media screen and (min-width: 500px) {
|
||||
#title {
|
||||
font-size: 180px;
|
||||
font-size: 100px;
|
||||
color: var(--text-color);
|
||||
text-shadow: #00000057 5px 5px 20px;
|
||||
line-height: 100px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 800;
|
||||
color: var(--text-color);
|
||||
font-size: 35px;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -121,6 +141,7 @@ export default {
|
|||
.button {
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
#ball {
|
||||
width: 1350px;
|
||||
height: 1350px;
|
||||
|
|
@ -129,6 +150,21 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
#history{
|
||||
width: calc(95% - 20px);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border: 1px solid var(--bd-color);
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
margin: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#setting {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -1,647 +0,0 @@
|
|||
<template>
|
||||
<div v-if="mode === 0" class="wrapper">
|
||||
<el-page-header style="margin-top:20px;margin-left:20px" @back="back_home">
|
||||
<template #content>
|
||||
<span class="text-large font-600 mr-3"> 选择范围 </span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
<div id="range" class="container colbox">
|
||||
<div class="rowbox item">
|
||||
<div id="lastrecite" class="card">
|
||||
<el-button style="margin-bottom: 20px;" class="tbtn" type="primary" @click="init">开始背诵</el-button><br />
|
||||
<el-button style="margin:0;" class="tbtn" type="success" @click="last_recite">继续上一次的背诵</el-button>
|
||||
</div>
|
||||
<div id="online-set-container" class="card">
|
||||
<div class="title">选择测验范围:</div>
|
||||
<el-checkbox v-model="local.checkAll" :indeterminate="local.isIndeterminate"
|
||||
@change="(res) => { handleCheckAllChange(local, res) }" size="large">全选</el-checkbox>
|
||||
<el-checkbox-group v-model="local.checkedSets" @change="(res) => { handleChange(local, res) }">
|
||||
<div class="set_radios" v-for="(set_class, set_class_name) in wordsets" :key="set_class_name">
|
||||
<p>{{ set_class_name }}</p>
|
||||
<el-checkbox class="checkbox" v-for="set in set_class" :key="set.id" :label="set.id"
|
||||
size="large" border>
|
||||
<div style="font-size: 18px;font-weight: 500;">
|
||||
{{ set.name }}
|
||||
</div>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rowbox item">
|
||||
<div id="rangeselect" class="card">
|
||||
<div class="title">在线词库</div>
|
||||
<el-checkbox v-model="online.checkAll" :indeterminate="online.isIndeterminate"
|
||||
@change="(res) => { handleCheckAllChange(online, res) }" size="large">全选</el-checkbox>
|
||||
<el-checkbox-group v-model="online.checkedSets" @change="(res) => { handleChange(online, res) }">
|
||||
<div class="set_radios" v-for="(set, set_name) in online_sets" :key="set_name">
|
||||
<div class="subtitle">{{ set_name }}</div>
|
||||
<div v-for="(book, book_name) in set" :key="book_name">
|
||||
<p>{{ book_name }}</p>
|
||||
<el-checkbox class="checkbox" v-for="(config, id) in book" :key="id" :label="id"
|
||||
size="large" border>
|
||||
<div style="font-size: 18px;font-weight: 500;">
|
||||
{{ config.name }}
|
||||
</div>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mode === 1">
|
||||
<el-page-header style="margin-top:20px;margin-left:20px" @back="mode = 0">
|
||||
<template #content>
|
||||
<span class="text-large font-600 mr-3"> 背诵 </span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
<div id="test" class="colbox">
|
||||
<div style="flex-grow: 1;" class="card">
|
||||
<div id="status">背诵进度: {{ current + 1 }}/{{ total }}</div>
|
||||
<div>
|
||||
<textarea autofocus id="input" v-if="current === answered" @input="change" class="answer"
|
||||
spellcheck="false">
|
||||
</textarea>
|
||||
<div v-if="current < answered" class="answer">{{ word.word }}</div>
|
||||
</div>
|
||||
<div id="explain">
|
||||
<el-button v-if="current > 0" @click="prev">上一个</el-button>
|
||||
<el-button v-if="current < answered" @click="next">下一个</el-button>
|
||||
<el-button v-if="current === answered" @click="showAnswer">显示答案</el-button>
|
||||
<el-button v-if="current === answered" type="warning" @click="skip">跳过</el-button>
|
||||
<el-button @click="terminate" type="danger">停止背诵</el-button>
|
||||
<box-icon color="var(--text-color)" class="btn" style="margin-left: 10px;translate: 0 4px;"
|
||||
@click="audio_play" name='volume-full'></box-icon>
|
||||
</div>
|
||||
<div id="trans">{{ word.type }} {{ word.trans }}</div>
|
||||
</div>
|
||||
<div id="add-to-box" class="card">
|
||||
<el-text class="mx-1 title">加入至</el-text>
|
||||
<div class="colbox para">
|
||||
<div class="mid-text">分组:</div>
|
||||
<el-select v-model="set_class" class="m-2" placeholder="Select">
|
||||
<el-option v-for="item in Object.keys(wordsets)" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="colbox para">
|
||||
<div class="mid-text">单词本:</div>
|
||||
<el-select v-model="set_id" class="m-2" placeholder="Select">
|
||||
<el-option v-for="item in wordsets[set_class]" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button @click="add_to" type="primary">添加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { ElMessage, ElNotification, ElMessageBox } from 'element-plus';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
export default {
|
||||
name: "WordRecite",
|
||||
data() {
|
||||
return {
|
||||
mode: 0,
|
||||
wordsets: window.wordsets,
|
||||
local: {
|
||||
allsets: [],
|
||||
checkedSets: [],
|
||||
isIndeterminate: false,
|
||||
checkAll: false,
|
||||
},
|
||||
online: {
|
||||
allsets: [],
|
||||
checkedSets: [],
|
||||
isIndeterminate: false,
|
||||
checkAll: false,
|
||||
},
|
||||
testWords: [],
|
||||
seq: [],
|
||||
total: 0,
|
||||
current: 0,
|
||||
answered: 0,
|
||||
word: {},
|
||||
set_class: "",
|
||||
set_id: "",
|
||||
online_sets: {},
|
||||
online_ids: {}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
key_listener(e) {
|
||||
if (this.mode === 1) {
|
||||
let ctrlKey = e.ctrlKey || e.metaKey;
|
||||
if (ctrlKey) {
|
||||
switch (e.key) {
|
||||
case 'b':
|
||||
this.showAnswer();
|
||||
break;
|
||||
case 'm':
|
||||
this.skip();
|
||||
break;
|
||||
case 'i':
|
||||
this.add_to();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
this.prev();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.next();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
},
|
||||
handleCheckAllChange(range, val) {
|
||||
range.checkedSets = val ? range.allsets : [];
|
||||
range.isIndeterminate = false;
|
||||
},
|
||||
handleChange(range, value) {
|
||||
range.checkAll = value.length === range.allsets.length;
|
||||
range.isIndeterminate = value.length > 0 && value.length < range.allsets.length;
|
||||
},
|
||||
async init() {
|
||||
if (this.local.checkedSets.length + this.online.checkedSets.length <= 0) {
|
||||
ElMessage({
|
||||
message: "请至少选择一个单词本",
|
||||
type: "error"
|
||||
})
|
||||
return;
|
||||
}
|
||||
let seed = (new Date()).getTime();
|
||||
this.testWords = [];
|
||||
for (let i of this.local.checkedSets) {
|
||||
let words = JSON.parse(localStorage.getItem(i));
|
||||
this.testWords = this.testWords.concat(...words);
|
||||
}
|
||||
for (let i of this.online.checkedSets) {
|
||||
let config = this.online_ids[i];
|
||||
let res = await axios.get("/wordset/detail", {
|
||||
params: {
|
||||
set: config.set,
|
||||
book: config.book,
|
||||
id: i
|
||||
}
|
||||
})
|
||||
if (res.status != 200) {
|
||||
ElMessage({
|
||||
message: "获取单词本时出现错误",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.testWords = this.testWords.concat(...res.data);
|
||||
}
|
||||
localStorage.setItem("lastrecite", JSON.stringify({
|
||||
checkedSets: this.local.checkedSets,
|
||||
onlineSets: this.online.checkedSets,
|
||||
seed: seed
|
||||
}))
|
||||
this.total = this.testWords.length;
|
||||
if (this.total === 0) {
|
||||
ElMessage({
|
||||
message: "单词本为空",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
let arr = Array.from(new Array(this.total).keys());
|
||||
for (let i = this.total - 1; i > 0; i--) {
|
||||
[arr[i], arr[seed % i]] = [arr[seed % i], arr[i]];
|
||||
}
|
||||
this.seq = arr;
|
||||
this.current = 0;
|
||||
this.answered = 0;
|
||||
this.mode = 1;
|
||||
nextTick(() => {
|
||||
this.show();
|
||||
});
|
||||
},
|
||||
async last_recite() {
|
||||
let lr = JSON.parse(localStorage.getItem("lastrecite"));
|
||||
if (!lr) {
|
||||
ElMessage({
|
||||
message: "没有记录",
|
||||
type: "error"
|
||||
})
|
||||
return;
|
||||
}
|
||||
this.testWords = [];
|
||||
for (let i of lr.checkedSets) {
|
||||
let content = localStorage.getItem(i);
|
||||
if (content) this.testWords = this.testWords.concat(...JSON.parse(content));
|
||||
else {
|
||||
ElMessage({
|
||||
message: "单词本不存在",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (let i of lr.onlineSets) {
|
||||
let config = this.online_ids[i];
|
||||
if (config) {
|
||||
let res = await axios.get("/wordset/detail", {
|
||||
params: {
|
||||
set: config.set,
|
||||
book: config.book,
|
||||
id: i
|
||||
}
|
||||
})
|
||||
if (res.status != 200) {
|
||||
ElMessage({
|
||||
message: "获取单词本时出现错误",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.testWords = this.testWords.concat(...res.data);
|
||||
}
|
||||
else {
|
||||
ElMessage({
|
||||
message: "在线单词本不存在",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
this.total = this.testWords.length;
|
||||
let arr = Array.from(new Array(this.total).keys());
|
||||
for (let i = this.total - 1; i > 0; i--) {
|
||||
[arr[i], arr[lr.seed % i]] = [arr[lr.seed % i], arr[i]];
|
||||
}
|
||||
this.seq = arr;
|
||||
let cur = Number(localStorage.getItem("lastcurrent"));
|
||||
this.current = cur;
|
||||
this.answered = cur;
|
||||
this.mode = 1;
|
||||
nextTick(() => {
|
||||
this.show();
|
||||
});
|
||||
},
|
||||
next() {
|
||||
if (this.current < this.answered) {
|
||||
this.current++;
|
||||
if (this.current === this.total) {
|
||||
this.mode = 0;
|
||||
ElNotification({
|
||||
message: "您已完成所有单词的背诵",
|
||||
title: "Congratulations!",
|
||||
type: "success"
|
||||
})
|
||||
localStorage.removeItem("lastrecite");
|
||||
localStorage.removeItem("lastcurrent");
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
else ElMessage({
|
||||
type: "error",
|
||||
message: "您还未答过该词,跳过请使用ctrl+M",
|
||||
})
|
||||
},
|
||||
skip() {
|
||||
ElMessage({
|
||||
message: "已经跳过该词",
|
||||
type: "info"
|
||||
});
|
||||
this.answered++;
|
||||
localStorage.setItem("lastcurrent", this.answered.toString());
|
||||
this.next();
|
||||
return;
|
||||
},
|
||||
prev() {
|
||||
if (this.current > 0) {
|
||||
this.current--;
|
||||
this.show();
|
||||
}
|
||||
else ElMessage({
|
||||
type: 'error',
|
||||
message: "已经是第一个单词"
|
||||
})
|
||||
},
|
||||
showAnswer() {
|
||||
ElMessage(`答案:${this.word.word}`);
|
||||
},
|
||||
show() {
|
||||
this.word = this.testWords[this.seq[this.current]];
|
||||
if (this.current === this.answered) {
|
||||
let e = document.getElementById("input");
|
||||
e.value = "_".repeat(this.word.word.length);
|
||||
e.setSelectionRange(0, 0);
|
||||
e.style.height = "0";
|
||||
setTimeout(() => {
|
||||
e.style.height = `${e.scrollHeight}px`;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
change() {
|
||||
let e = document.getElementById("input");
|
||||
let t = e.value, n = t.indexOf("_"), o;
|
||||
if (n == this.word.word.length) {
|
||||
if (t.substring(0, t.length - 1).toLowerCase() == this.word.word.toLowerCase()) {
|
||||
ElMessage({
|
||||
title: "Success",
|
||||
message: "正确",
|
||||
type: "success"
|
||||
});
|
||||
this.answered++;
|
||||
localStorage.setItem("lastcurrent", this.answered.toString());
|
||||
this.next();
|
||||
return;
|
||||
}
|
||||
ElMessage({
|
||||
title: "Error",
|
||||
message: "错误",
|
||||
type: "error"
|
||||
});
|
||||
o = "_".repeat(this.word.word.length);
|
||||
n = 0;
|
||||
e.style.height = "0";
|
||||
setTimeout(() => {
|
||||
e.style.height = `${e.scrollHeight}px`;
|
||||
}, 200);
|
||||
} else
|
||||
n == -1 ? o = "_".repeat(this.word.word.length) : (o = t.substring(0, n),
|
||||
o += "_".repeat(this.word.word.length - n));
|
||||
e.value = o;
|
||||
e.setSelectionRange(n, n);
|
||||
setTimeout(() => {
|
||||
if (parseInt(e.style.height) < e.scrollHeight) {
|
||||
e.style.height = `${e.scrollHeight}px`;
|
||||
}
|
||||
},0);
|
||||
e.focus();
|
||||
},
|
||||
audio_play() {
|
||||
var t = new Audio("https://dict.youdao.com/dictvoice?audio=" + this.word.word);
|
||||
t.play();
|
||||
},
|
||||
terminate() {
|
||||
ElMessageBox.confirm("确定要终止吗?")
|
||||
.then(() => {
|
||||
this.mode = 0;
|
||||
ElNotification({
|
||||
message: "背诵已终止",
|
||||
type: "info",
|
||||
title: "Terminate"
|
||||
});
|
||||
});
|
||||
},
|
||||
back_home() {
|
||||
this.$router.push("/");
|
||||
},
|
||||
add_to() {
|
||||
if (this.set_id) {
|
||||
let data = JSON.parse(localStorage.getItem(this.set_id));
|
||||
for (let i of data) {
|
||||
if (i.word === this.word.word) {
|
||||
ElMessage({
|
||||
message: '已经添加过该词',
|
||||
type: 'error',
|
||||
})
|
||||
return;
|
||||
}
|
||||
}
|
||||
data.push(this.word);
|
||||
localStorage.setItem(this.set_id, JSON.stringify(data));
|
||||
ElMessage({
|
||||
message: `已添加 ${this.word.word} (${this.word.type})`,
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
else ElMessage({
|
||||
message: '请先选择单词本',
|
||||
type: 'error',
|
||||
})
|
||||
return;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
for (let i of Object.values(window.wordsets)) {
|
||||
for (let j of i) {
|
||||
this.local.allsets.push(j.id);
|
||||
}
|
||||
}
|
||||
document.addEventListener('keyup', this.key_listener);
|
||||
axios.get("/wordset/list").then((res, err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
ElMessage({
|
||||
message: "在线词库加载失败",
|
||||
type: "error"
|
||||
})
|
||||
return;
|
||||
}
|
||||
this.online_sets = res.data;
|
||||
for (let i in this.online_sets) {
|
||||
for (let j in this.online_sets[i]) {
|
||||
for (let k in this.online_sets[i][j]) {
|
||||
this.online.allsets.push(k);
|
||||
this.online_ids[k] = {
|
||||
set: i,
|
||||
book: j
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener("keyup", this.key_listener);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (max-width: 500px) {
|
||||
#range {
|
||||
max-height: calc(100% - 40px);
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#rangeselect {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
/* background-color: var(--bg-color); */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#test {
|
||||
flex-direction: column;
|
||||
animation: enter .5s ease-out;
|
||||
}
|
||||
|
||||
#trans {
|
||||
font-size: 25px;
|
||||
transition: .5s;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#explain {
|
||||
font-size: 18px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 55px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tbtn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.answer {
|
||||
font-size: 55px;
|
||||
letter-spacing: 15px;
|
||||
min-height: 50px;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 500px) {
|
||||
#range {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#rangeselect {
|
||||
max-height: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-bottom: 20px;
|
||||
/* background-color: var(--bg-color); */
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.tbtn {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 35px;
|
||||
font-weight: 800;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#test {
|
||||
padding: 30px;
|
||||
animation: enter .5s ease-out;
|
||||
}
|
||||
|
||||
#trans {
|
||||
font-size: 43px;
|
||||
transition: .5s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#add-to-box {
|
||||
width: 30%;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
#explain {
|
||||
font-size: 25px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.answer {
|
||||
font-size: 70px;
|
||||
letter-spacing: 30px;
|
||||
min-height: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
height: calc(100% - 10px);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 23px;
|
||||
line-height: 35px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
#status {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.answer {
|
||||
margin-top: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-color);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
border: none;
|
||||
outline-style: none;
|
||||
padding: 0;
|
||||
transition: .5s;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.set_radios {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.set_radios p {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#lastrecite {
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
#online-set-container {
|
||||
height: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div style="flex-grow: 1;" class="card">
|
||||
<el-table :data="table" style="max-height: 550px;overflow: auto;border-radius: 10px;">
|
||||
<el-table :data="table" style="height: calc(100% - 10px);overflow: auto;border-radius: 10px;">
|
||||
<el-table-column type="index" />
|
||||
<el-table-column prop="word" label="单词" />
|
||||
<el-table-column prop="type" label="词性" />
|
||||
|
|
@ -75,7 +75,6 @@ export default {
|
|||
name: "NoteEditor",
|
||||
data() {
|
||||
return {
|
||||
wordsets: window.wordsets,
|
||||
new_word: {
|
||||
word: "",
|
||||
trans: "",
|
||||
|
|
@ -91,6 +90,8 @@ export default {
|
|||
table: [],
|
||||
word_editing: false,
|
||||
new_name: "",
|
||||
name:"",
|
||||
id:""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -112,7 +113,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
this.table.push(Object.assign({}, this.new_word));
|
||||
localStorage.setItem(this.editing.id, JSON.stringify(this.table));
|
||||
this.$store.state.wordsets.saveSet(this.id,this.table);
|
||||
ElMessage({
|
||||
message: `已添加 ${this.new_word.word} (${this.new_word.type})`,
|
||||
type: 'success',
|
||||
|
|
@ -142,7 +143,7 @@ export default {
|
|||
type: this.editing_word.type,
|
||||
trans: this.editing_word.trans,
|
||||
}
|
||||
localStorage.setItem(this.editing.id, JSON.stringify(this.table));
|
||||
this.$store.state.wordsets.saveSet(this.id,this.table);
|
||||
ElMessage({
|
||||
message: `已保存更改`,
|
||||
type: 'success',
|
||||
|
|
@ -150,11 +151,11 @@ export default {
|
|||
this.word_editing = false;
|
||||
},
|
||||
change_name() {
|
||||
let old_name = this.editing.name;
|
||||
this.editing.name = this.new_name;
|
||||
localStorage.setItem("wordsets", JSON.stringify(this.wordsets));
|
||||
let old_name = this.name;
|
||||
this.name = this.new_name;
|
||||
this.$store.state.wordsets.renameSet(this.class_name,this.id,this.new_name);
|
||||
ElMessage({
|
||||
message: `已更改 ${old_name} 为 ${this.new_name}`,
|
||||
message: `已更改 ${old_name} 为 ${this.name}`,
|
||||
type: 'success',
|
||||
});
|
||||
return;
|
||||
|
|
@ -162,7 +163,7 @@ export default {
|
|||
del_word(index) {
|
||||
let word = this.table[index].word.concat();
|
||||
this.table.splice(index, 1);
|
||||
localStorage.setItem(this.editing.id, JSON.stringify(this.table));
|
||||
this.$store.state.wordsets.saveSet(this.id,this.table);
|
||||
ElMessage({
|
||||
message: `已删除 ${word}`,
|
||||
type: 'warning',
|
||||
|
|
@ -171,10 +172,14 @@ export default {
|
|||
|
||||
},
|
||||
created() {
|
||||
this.name = this.$route.query.name;
|
||||
this.class_name = this.$route.query.classname;
|
||||
this.id = this.$route.query.id;
|
||||
if(this.name && this.id){
|
||||
this.table = JSON.parse(localStorage.getItem(this.id));
|
||||
if(this.id){
|
||||
let status = this.$store.state.wordsets.getSetStatus(this.class_name,this.id);
|
||||
if(status){
|
||||
this.name = status.name;
|
||||
}
|
||||
this.table = this.$store.state.wordsets.getSet(this.id);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -201,7 +206,8 @@ export default {
|
|||
|
||||
.container{
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
height: calc(100% - 70px);
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
<div class="mid-text">名称:</div>
|
||||
<el-input v-model="set_name"></el-input>
|
||||
<div class="mid-text">分组:</div>
|
||||
<el-select allow-create filterable v-model="new_set_class" class="m-2" placeholder="Select">
|
||||
<el-option v-for="item in Object.keys(wordsets)" :key="item" :label="item" :value="item" />
|
||||
<el-select allow-create filterable default-first-option v-model="new_set_class" class="m-2" placeholder="输入新单词本类后后请按回车">
|
||||
<el-option v-for="item in $store.state.wordsets._allclass" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button style="width: 50%;margin-top: 20px;" type="primary" @click="new_wordset()">创建</el-button>
|
||||
|
|
@ -20,14 +20,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import uuid from 'node-uuid';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
export default {
|
||||
name: "NewSet",
|
||||
data() {
|
||||
return {
|
||||
wordsets: window.wordsets,
|
||||
set_name: "",
|
||||
new_set_class: "",
|
||||
}
|
||||
|
|
@ -41,17 +39,7 @@ export default {
|
|||
})
|
||||
return;
|
||||
}
|
||||
if (!this.wordsets[this.new_set_class]) {
|
||||
this.wordsets[this.new_set_class] = [];
|
||||
}
|
||||
let id = uuid.v1();
|
||||
this.wordsets[this.new_set_class].push({
|
||||
name: this.set_name,
|
||||
created: (new Date()).getTime(),
|
||||
id: id
|
||||
});
|
||||
localStorage.setItem("wordsets", JSON.stringify(this.wordsets));
|
||||
localStorage.setItem(id, JSON.stringify([]));
|
||||
this.$store.state.wordsets.addSet(this.new_set_class,this.set_name);
|
||||
this.$router.push('/manage');
|
||||
return;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,13 +23,12 @@
|
|||
name='plus'></box-icon>新建单词本</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-container style="height:calc(100% - 40px);">
|
||||
|
||||
<el-container style="height:calc(100% - 200px);position: relative;">
|
||||
<el-aside id="sidebar">
|
||||
<div class="sidebar-title">
|
||||
|本地
|
||||
</div>
|
||||
<div v-for="(set_class, class_name) in wordsets" :title="class_name" :key="class_name"
|
||||
<div v-for="class_name in $store.state.wordsets._allclass" :title="class_name" :key="class_name"
|
||||
@click="view(class_name)" class="sidebar-item">
|
||||
{{ class_name }}
|
||||
</div>
|
||||
|
|
@ -46,7 +45,7 @@
|
|||
<el-main id="wordsets-container">
|
||||
<div id="sets-container">
|
||||
<div v-if="mode === 0" class="colbox wordclass">
|
||||
<div v-for="(wordset, index) in view_wordsets" :key="index" class="wordset rowbox">
|
||||
<div v-for="(wordset, index) in $store.state.wordsets.getClass(view_wordsets)" :key="index" class="wordset rowbox">
|
||||
<div class="no">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
|
|
@ -58,9 +57,9 @@
|
|||
</div>
|
||||
<div class="option">
|
||||
<box-icon class="btn" name='edit' color="var(--text-color)"
|
||||
@click="edit(wordset.id, wordset.name)"></box-icon>
|
||||
@click="edit(wordset.id)"></box-icon>
|
||||
<box-icon class="btn" name='trash' color="var(--text-color)"
|
||||
@click="del(view_class, wordset.id, index)"></box-icon>
|
||||
@click="del(view_wordsets, wordset.id, index)"></box-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -95,8 +94,7 @@ export default {
|
|||
name: "SetList",
|
||||
data() {
|
||||
return {
|
||||
wordsets: window.wordsets,
|
||||
view_wordsets: [],
|
||||
view_wordsets: "",
|
||||
viewing: {
|
||||
set: "",
|
||||
book: "",
|
||||
|
|
@ -108,7 +106,7 @@ export default {
|
|||
methods: {
|
||||
view(classname) {
|
||||
this.mode = 0;
|
||||
this.view_wordsets = this.wordsets[classname];
|
||||
this.view_wordsets = classname;
|
||||
let node = document.getElementById("wordsets-container").style;
|
||||
node.animation = "";
|
||||
setTimeout(() => {
|
||||
|
|
@ -127,22 +125,17 @@ export default {
|
|||
manage_online_wordsets() {
|
||||
window.location.href = '/dashboard';
|
||||
},
|
||||
del(set_class_name, id, index) {
|
||||
del(class_name, id, index) {
|
||||
ElMessageBox.confirm("确定要删除吗?")
|
||||
.then(() => {
|
||||
localStorage.removeItem(id);
|
||||
this.wordsets[set_class_name].splice(index, 1);
|
||||
if (!this.wordsets[set_class_name].length) {
|
||||
delete this.wordsets[set_class_name];
|
||||
}
|
||||
localStorage.setItem("wordsets", JSON.stringify(this.wordsets));
|
||||
ElMessage(`已经删除 ${set_class_name} `);
|
||||
this.$store.state.wordsets.delSet(class_name,index);
|
||||
ElMessage(`已经删除 ${class_name} ${id}`);
|
||||
});
|
||||
},
|
||||
edit(id, name) {
|
||||
edit(id) {
|
||||
this.$router.push({
|
||||
path: "./manage/edit",
|
||||
query: { id, name }
|
||||
query: { id,classname:this.view_wordsets}
|
||||
})
|
||||
},
|
||||
show(id) {
|
||||
|
|
@ -152,60 +145,22 @@ export default {
|
|||
})
|
||||
},
|
||||
async export_set() {
|
||||
let fileHandle = await window.showSaveFilePicker({
|
||||
types: [
|
||||
{
|
||||
description: "JSON file",
|
||||
accept: {
|
||||
'text/json': ['.json'],
|
||||
},
|
||||
},
|
||||
],
|
||||
this.$store.state.wordsets.export_set((cnt) => {
|
||||
ElNotification({
|
||||
type: "success",
|
||||
title: "导出成功",
|
||||
message: `已导出 ${cnt} 本单词本`
|
||||
});
|
||||
let writestream = await fileHandle.createWritable();
|
||||
let data = {
|
||||
wordsets: this.wordsets,
|
||||
words: {}
|
||||
};
|
||||
for (let i of Object.values(this.wordsets)) {
|
||||
for (let j of i) {
|
||||
let temp = localStorage.getItem(j.id);
|
||||
data.words[j.id] = temp;
|
||||
}
|
||||
}
|
||||
writestream.write(JSON.stringify(data));
|
||||
writestream.close();
|
||||
})
|
||||
},
|
||||
async import_set() {
|
||||
let [fileHandle] = await window.showOpenFilePicker({
|
||||
types: [
|
||||
{
|
||||
description: "JSON file",
|
||||
accept: {
|
||||
'text/json': ['.json'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
let file = await fileHandle.getFile();
|
||||
let data = JSON.parse(await file.text());
|
||||
let cnt = 0;
|
||||
for (let i of Object.keys(data.wordsets)) {
|
||||
for (let j of data.wordsets[i]) {
|
||||
if (!localStorage.getItem(j.id)) {
|
||||
if (!this.wordsets[i]) this.wordsets[i] = [];
|
||||
this.wordsets[i].push(j);
|
||||
localStorage.setItem(j.id, data.words[j.id]);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
localStorage.setItem("wordsets", JSON.stringify(this.wordsets));
|
||||
this.$store.state.wordsets.import_set((cnt) => {
|
||||
ElNotification({
|
||||
type: "success",
|
||||
title: "添加成功",
|
||||
message: `已添加 ${cnt} 本单词本`
|
||||
});
|
||||
})
|
||||
},
|
||||
taggle_sidebar() {
|
||||
let node = document.getElementById("sidebar");
|
||||
|
|
@ -215,7 +170,7 @@ export default {
|
|||
}
|
||||
},
|
||||
created() {
|
||||
let first_set = Object.keys(this.wordsets)[0];
|
||||
let first_set = this.$store.state.wordsets._firstClass;
|
||||
if (first_set) {
|
||||
setTimeout(() => {
|
||||
this.view(first_set);
|
||||
|
|
@ -258,10 +213,7 @@ export default {
|
|||
padding: 10px;
|
||||
height: 180px;
|
||||
width: 40%;
|
||||
box-shadow: var(--el-box-shadow);
|
||||
margin: 5px;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.no {
|
||||
|
|
@ -323,13 +275,11 @@ export default {
|
|||
0% {
|
||||
translate: -100px 0;
|
||||
opacity: 0;
|
||||
filter: blur(20px);
|
||||
}
|
||||
|
||||
100% {
|
||||
translate: 0 0;
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -346,10 +296,7 @@ export default {
|
|||
padding: 20px;
|
||||
height: 220px;
|
||||
width: 170px;
|
||||
box-shadow: var(--el-box-shadow);
|
||||
margin: 20px;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.no {
|
||||
|
|
@ -378,14 +325,29 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.wordset{
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
transition: .5s;
|
||||
border: solid 1px var(--bd-color);
|
||||
}
|
||||
|
||||
.wordset:hover{
|
||||
box-shadow: var(--el-box-shadow);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#sidebar {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 0 5px 5px 0;
|
||||
border-radius: 5px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
animation: sidebar-enter ease-out .6s backwards;
|
||||
height: calc(100% - 100px);
|
||||
padding-left: 10px;
|
||||
margin: 10px;
|
||||
border: 1px solid var(--bd-color);
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
|
|
@ -434,7 +396,7 @@ html.bgimged .wordset {
|
|||
|
||||
#wordsets-container {
|
||||
overflow-x: auto;
|
||||
height: 100%;
|
||||
height: calc(100% - 60px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div id="wrapper" class="rowbox container">
|
||||
<Post id="post"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Post from '@/md/about.md'
|
||||
export default {
|
||||
name: "ManualPage",
|
||||
components:{
|
||||
Post
|
||||
},
|
||||
created() {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#post {
|
||||
height:auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
padding-top:0;
|
||||
padding:30px;
|
||||
border-radius:10px;
|
||||
width: 80%;
|
||||
border:solid 1px var(--bd-color);
|
||||
color:var(--text-color);
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
height:calc(100% - 80px);
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div id="wrapper" class="rowbox container">
|
||||
<Post id="post"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Post from '@/md/manual.md'
|
||||
export default {
|
||||
name: "ManualPage",
|
||||
components:{
|
||||
Post
|
||||
},
|
||||
created() {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#post {
|
||||
height:auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom:20px;
|
||||
padding-top:0;
|
||||
padding:30px;
|
||||
border-radius:10px;
|
||||
width: 80%;
|
||||
border:solid 1px var(--bd-color);
|
||||
color:var(--text-color);
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
height:calc(100% - 90px);
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-page-header style="margin-top:20px;margin-left:20px" @back="$router.push('/')">
|
||||
<template #content>
|
||||
<span class="text-large font-600 mr-3"> 背诵 </span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
<div id="test" class="colbox">
|
||||
<div style="flex-grow: 1;" class="card">
|
||||
<div id="status">背诵进度: {{ current + 1 }}/{{ total }}</div>
|
||||
<div>
|
||||
<textarea autofocus id="input" v-if="current === answered" @input="change" class="answer"
|
||||
spellcheck="false">
|
||||
</textarea>
|
||||
<div v-if="current < answered" class="answer">{{ word.word }}</div>
|
||||
</div>
|
||||
<div id="explain">
|
||||
<el-button v-if="current > 0" @click="prev">上一个</el-button>
|
||||
<el-button v-if="current < answered" @click="next">下一个</el-button>
|
||||
<el-button v-if="current === answered" @click="showAnswer">显示答案</el-button>
|
||||
<el-button v-if="current === answered" type="warning" @click="skip">跳过</el-button>
|
||||
<el-button @click="terminate" type="danger">停止背诵</el-button>
|
||||
<box-icon color="var(--text-color)" class="btn" style="margin-left: 10px;translate: 0 4px;"
|
||||
@click="audio_play" name='volume-full'></box-icon>
|
||||
</div>
|
||||
<div id="trans">{{ word.type }} {{ word.trans }}</div>
|
||||
</div>
|
||||
<div id="add-to-box" class="card">
|
||||
<el-text class="mx-1 title">加入至</el-text>
|
||||
<div class="colbox para">
|
||||
<div class="mid-text">分组:</div>
|
||||
<el-select v-model="set_class" class="m-2" placeholder="Select">
|
||||
<el-option v-for="item in $store.state.wordsets._allclass" :key="item" :label="item"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="colbox para">
|
||||
<div class="mid-text">单词本:</div>
|
||||
<el-select v-model="set_id" class="m-2" placeholder="Select">
|
||||
<el-option v-for="item in $store.state.wordsets.getClass(set_class)" :key="item.id"
|
||||
:label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button @click="add_to" type="primary">添加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElMessage, ElNotification, ElMessageBox } from 'element-plus';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
export default {
|
||||
name: "WordRecite",
|
||||
data() {
|
||||
return {
|
||||
wordsets: this.$store.state.wordsets,
|
||||
testWords: [],
|
||||
seq: [],
|
||||
total: 0,
|
||||
current: 0,
|
||||
answered: 0,
|
||||
word: {},
|
||||
set_class: "",
|
||||
set_id: "",
|
||||
online_sets: {},
|
||||
online_ids: {},
|
||||
settings: {
|
||||
direct_answer: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
key_listener(e) {
|
||||
let ctrlKey = e.ctrlKey || e.metaKey;
|
||||
if (ctrlKey) {
|
||||
switch (e.key) {
|
||||
case 'b':
|
||||
this.showAnswer();
|
||||
break;
|
||||
case 'm':
|
||||
this.skip();
|
||||
break;
|
||||
case 'i':
|
||||
this.add_to();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
this.prev();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.next();
|
||||
break;
|
||||
case 'y':
|
||||
this.audio_play();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
return;
|
||||
},
|
||||
next() {
|
||||
if (this.current < this.answered) {
|
||||
this.current++;
|
||||
if (this.current === this.total) {
|
||||
ElNotification({
|
||||
message: "您已完成所有单词的背诵",
|
||||
title: "Congratulations!",
|
||||
type: "success"
|
||||
})
|
||||
this.$router.push('/');
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
else ElMessage({
|
||||
type: "error",
|
||||
message: "您还未答过该词,跳过请使用ctrl+M",
|
||||
})
|
||||
},
|
||||
skip() {
|
||||
ElMessage({
|
||||
message: "已经跳过该词",
|
||||
type: "info"
|
||||
});
|
||||
this.answered++;
|
||||
this.$store.state.history.count(this.answered);
|
||||
this.next();
|
||||
return;
|
||||
},
|
||||
prev() {
|
||||
if (this.current > 0) {
|
||||
this.current--;
|
||||
this.show();
|
||||
}
|
||||
else ElMessage({
|
||||
type: 'error',
|
||||
message: "已经是第一个单词"
|
||||
})
|
||||
},
|
||||
showAnswer() {
|
||||
if (!this.settings.direct_answer) {
|
||||
let e = document.getElementById("input");
|
||||
if (e.value === "_".repeat(this.word.word.length)) {
|
||||
e.value = this.word.word[0] + "_".repeat(this.word.word.length - 1);
|
||||
e.setSelectionRange(1, 1);
|
||||
ElMessage(`首字母为:${this.word.word[0]}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ElMessage(`答案:${this.word.word}`);
|
||||
return;
|
||||
},
|
||||
show() {
|
||||
this.word = this.testWords[this.seq[this.current]];
|
||||
if (this.current === this.answered) {
|
||||
let e = document.getElementById("input");
|
||||
e.value = "_".repeat(this.word.word.length);
|
||||
e.setSelectionRange(0, 0);
|
||||
e.style.height = "0";
|
||||
setTimeout(() => {
|
||||
e.style.height = `${e.scrollHeight}px`;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
change() {
|
||||
let e = document.getElementById("input");
|
||||
let cur = e.selectionStart;
|
||||
let t = e.value, n = t.indexOf("_"), o;
|
||||
if (n == this.word.word.length) {
|
||||
if (t.substring(0, t.length - 1).toLowerCase() == this.word.word.toLowerCase()) {
|
||||
ElMessage({
|
||||
title: "Success",
|
||||
message: "正确",
|
||||
type: "success"
|
||||
});
|
||||
this.answered++;
|
||||
this.$store.state.history.count(this.answered);
|
||||
this.next();
|
||||
return;
|
||||
}
|
||||
ElMessage({
|
||||
title: "Error",
|
||||
message: "错误",
|
||||
type: "error"
|
||||
});
|
||||
o = "_".repeat(this.word.word.length);
|
||||
e.value = o;
|
||||
e.setSelectionRange(0, 0);
|
||||
e.style.height = "0";
|
||||
setTimeout(() => {
|
||||
e.style.height = `${e.scrollHeight}px`;
|
||||
}, 200);
|
||||
return;
|
||||
} else {
|
||||
n == -1 ? o = "_".repeat(this.word.word.length) : (o = t.substring(0, n),
|
||||
o += "_".repeat(this.word.word.length - n));
|
||||
e.value = o;
|
||||
if (cur > n) {
|
||||
e.setSelectionRange(n, n);
|
||||
}
|
||||
else e.setSelectionRange(cur, cur);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (parseInt(e.style.height) < e.scrollHeight) {
|
||||
e.style.height = `${e.scrollHeight}px`;
|
||||
}
|
||||
}, 0);
|
||||
e.focus();
|
||||
},
|
||||
audio_play() {
|
||||
var t = new Audio("https://dict.youdao.com/dictvoice?audio=" + this.word.word);
|
||||
t.play();
|
||||
},
|
||||
terminate() {
|
||||
ElMessageBox.confirm("确定要终止吗?")
|
||||
.then(() => {
|
||||
this.$router.push('/');
|
||||
ElNotification({
|
||||
message: "背诵已终止",
|
||||
type: "info",
|
||||
title: "Terminate"
|
||||
});
|
||||
});
|
||||
},
|
||||
add_to() {
|
||||
if (this.set_id) {
|
||||
let data = JSON.parse(localStorage.getItem(this.set_id));
|
||||
for (let i of data) {
|
||||
if (i.word === this.word.word) {
|
||||
ElMessage({
|
||||
message: '已经添加过该词',
|
||||
type: 'error',
|
||||
})
|
||||
return;
|
||||
}
|
||||
}
|
||||
data.push(this.word);
|
||||
localStorage.setItem(this.set_id, JSON.stringify(data));
|
||||
ElMessage({
|
||||
message: `已添加 ${this.word.word} (${this.word.type})`,
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
else ElMessage({
|
||||
message: '请先选择单词本',
|
||||
type: 'error',
|
||||
})
|
||||
return;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
document.addEventListener('keyup', this.key_listener);
|
||||
let index = this.$route.query.index;
|
||||
if (index) {
|
||||
this.$store.state.history.use(Number(index));
|
||||
}
|
||||
this.$store.state.history.init_recite((words, current, seq, settings) => {
|
||||
this.testWords = words;
|
||||
this.current = current;
|
||||
this.total = words.length;
|
||||
this.answered = current;
|
||||
this.settings = settings;
|
||||
this.seq = seq;
|
||||
if (this.answered >= this.total) {
|
||||
this.answered = 0;
|
||||
this.current = 0;
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: "进度已经重置"
|
||||
})
|
||||
this.$store.state.history.count(this.answered);
|
||||
}
|
||||
nextTick(() => {
|
||||
this.show();
|
||||
})
|
||||
}, (msg) => {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: msg
|
||||
})
|
||||
this.$router.push('/select');
|
||||
})
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener("keyup", this.key_listener);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (max-width: 500px) {
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#test {
|
||||
flex-direction: column;
|
||||
animation: enter .5s ease-out;
|
||||
}
|
||||
|
||||
#trans {
|
||||
font-size: 25px;
|
||||
transition: .5s;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#explain {
|
||||
font-size: 18px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 55px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tbtn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.answer {
|
||||
font-size: 55px;
|
||||
letter-spacing: 15px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 500px) {
|
||||
.item {
|
||||
margin-bottom: 20px;
|
||||
/* background-color: var(--bg-color); */
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.tbtn {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 35px;
|
||||
font-weight: 800;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#test {
|
||||
padding: 30px;
|
||||
animation: enter .5s ease-out;
|
||||
}
|
||||
|
||||
#trans {
|
||||
font-size: 43px;
|
||||
transition: .5s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#add-to-box {
|
||||
width: 30%;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
#explain {
|
||||
font-size: 25px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.answer {
|
||||
font-size: 70px;
|
||||
letter-spacing: 25px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
height: calc(100% - 10px);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
|
||||
#status {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.answer {
|
||||
margin-top: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-color);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
border: none;
|
||||
outline-style: none;
|
||||
padding: 0;
|
||||
transition: .5s;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
<template>
|
||||
<div class="container" id="page">
|
||||
<el-page-header @back="this.$router.push('/');" style="width: calc(100% - 30px);margin-left: 30px;margin-top: 5px;">
|
||||
<template #content>
|
||||
<span class="text-large font-600 mr-3">开始新背诵</span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
<el-steps style="width: 100%;" align-center :active="step">
|
||||
<el-step title="选择单词本" description="选择在线或本地单词本以开始背诵"></el-step>
|
||||
<el-step title="自定义背诵" description="您可以在此处更改背诵设置"></el-step>
|
||||
</el-steps>
|
||||
<div id="main" class="card" style="overflow: hidden;">
|
||||
<div class="item" id="select-area" v-show="step === 1">
|
||||
<el-tabs style="height: calc(100% - 40px);position: relative;overflow: hidden;">
|
||||
<el-tab-pane label="本地">
|
||||
<el-checkbox v-model="local.checkAll" :indeterminate="local.isIndeterminate"
|
||||
@change="(res) => { handleCheckAllChange(local, res) }" size="large">全选</el-checkbox>
|
||||
<el-checkbox-group v-model="local.checkedSets" @change="(res) => { handleChange(local, res) }">
|
||||
<div class="set_radios" v-for="(set_class, set_class_name) in $store.state.wordsets._inner"
|
||||
:key="set_class_name">
|
||||
<p>{{ set_class_name }}</p>
|
||||
<el-checkbox class="checkbox" v-for="set in set_class" :key="set.id" :label="set.id"
|
||||
size="large" border>
|
||||
<div style="font-size: 18px;font-weight: 500;">
|
||||
{{ set.name }}
|
||||
</div>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="在线">
|
||||
<el-checkbox v-model="online.checkAll" :indeterminate="online.isIndeterminate"
|
||||
@change="(res) => { handleCheckAllChange(online, res) }" size="large">全选</el-checkbox>
|
||||
<el-checkbox-group v-model="online.checkedSets" @change="(res) => { handleChange(online, res) }">
|
||||
<div class="set_radios" v-for="(set, set_name) in online_sets" :key="set_name">
|
||||
<div class="subtitle">{{ set_name }}</div>
|
||||
<div v-for="(book, book_name) in set" :key="book_name">
|
||||
<p>{{ book_name }}</p>
|
||||
<el-checkbox class="checkbox" v-for="(config, id) in book" :key="id" :label="id"
|
||||
size="large" border>
|
||||
<div style="font-size: 18px;font-weight: 500;">
|
||||
{{ config.name }}
|
||||
</div>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-button style="position: absolute;margin-top: 10px;right: 30px;" type="primary"
|
||||
@click="completeSelect">下一步</el-button>
|
||||
</div>
|
||||
<div class="item" id="setting-area" v-show="step === 2">
|
||||
<div style="margin: 20px;">
|
||||
<div class="setting-item">
|
||||
<div class="setting-header">
|
||||
<div style="font-size: 20px;">随机顺序</div>
|
||||
<div style="font-size: 14px;">开启后单词顺序将打乱</div>
|
||||
</div>
|
||||
<el-switch @change="update" v-model="settings.shuffle" active-color="#13ce66"></el-switch>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-header">
|
||||
<div style="font-size: 20px;">直接显示答案</div>
|
||||
<div style="font-size: 14px;">开启后答案提示不会先提示首字母</div>
|
||||
</div>
|
||||
<el-switch @change="update" v-model="settings.direct_answer" active-color="#13ce66"></el-switch>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-header">
|
||||
<div style="font-size: 20px;">忽略词组</div>
|
||||
<div style="font-size: 14px;">开启后不背诵词组</div>
|
||||
</div>
|
||||
<el-switch @change="update" v-model="settings.ignore_phrases" active-color="#13ce66"></el-switch>
|
||||
</div>
|
||||
<el-button style="position: absolute;left: 30px;bottom: 30px;" @click="backSelect">上一步</el-button>
|
||||
<el-button style="position: absolute;right: 30px;bottom: 30px;" type="primary"
|
||||
@click="startRecite">下一步</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
export default {
|
||||
name: "SelectPage",
|
||||
data() {
|
||||
return {
|
||||
step: 1,
|
||||
local: {
|
||||
allsets: [],
|
||||
checkedSets: [],
|
||||
isIndeterminate: false,
|
||||
checkAll: false,
|
||||
},
|
||||
online: {
|
||||
allsets: [],
|
||||
checkedSets: [],
|
||||
isIndeterminate: false,
|
||||
checkAll: false,
|
||||
},
|
||||
online_sets: {},
|
||||
online_ids: {},
|
||||
settings: {
|
||||
shuffle: true,
|
||||
direct_answer: false,
|
||||
ignore_phrases: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCheckAllChange(range, val) {
|
||||
range.checkedSets = val ? range.allsets : [];
|
||||
range.isIndeterminate = false;
|
||||
},
|
||||
handleChange(range, value) {
|
||||
range.checkAll = value.length === range.allsets.length;
|
||||
range.isIndeterminate = value.length > 0 && value.length < range.allsets.length;
|
||||
},
|
||||
completeSelect() {
|
||||
if (this.online.checkedSets.length + this.local.checkedSets.length > 0) {
|
||||
document.getElementById("select-area").style.animation = "exitLeft .25s ease-in forwards";
|
||||
setTimeout(() => {
|
||||
document.getElementById("setting-area").style.animation = "enterRight .25s ease-out forwards";
|
||||
this.step++;
|
||||
}, 250)
|
||||
return;
|
||||
}
|
||||
ElMessage({
|
||||
type:'error',
|
||||
message:"请选择单词本",
|
||||
})
|
||||
},
|
||||
backSelect() {
|
||||
document.getElementById("setting-area").style.animation = "exitRight .25s ease-in forwards";
|
||||
setTimeout(() => {
|
||||
this.step--;
|
||||
setTimeout(() => {
|
||||
document.getElementById("select-area").style.animation = "enterLeft .25s ease-out forwards";
|
||||
}, 0);
|
||||
}, 250)
|
||||
},
|
||||
startRecite() {
|
||||
let onlinesets = [];
|
||||
for(let i of this.online.checkedSets){
|
||||
onlinesets.push(Object.assign({id:i},this.online_ids[i]))
|
||||
}
|
||||
this.$store.state.history.add(this.local.checkedSets,onlinesets,this.settings,()=>{
|
||||
this.$router.push('/recite');
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
for (let i of this.$store.state.wordsets._allsets) {
|
||||
for (let j of i) {
|
||||
this.local.allsets.push(j.id);
|
||||
}
|
||||
}
|
||||
document.addEventListener('keyup', this.key_listener);
|
||||
axios.get("/wordset/list").then((res, err) => {
|
||||
if (err) {
|
||||
ElMessage({
|
||||
message: "在线词库加载失败",
|
||||
type: "error"
|
||||
})
|
||||
return;
|
||||
}
|
||||
this.online_sets = res.data;
|
||||
for (let i in this.online_sets) {
|
||||
for (let j in this.online_sets[i]) {
|
||||
for (let k in this.online_sets[i][j]) {
|
||||
this.online.allsets.push(k);
|
||||
this.online_ids[k] = {
|
||||
set: i,
|
||||
book: j
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (max-width: 500px) {
|
||||
.checkbox {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 500px) {
|
||||
.checkbox {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#page{
|
||||
width: 100%;
|
||||
/* margin: 20px; */
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 23px;
|
||||
line-height: 35px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.set_radios {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.set_radios p {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#main {
|
||||
margin-top: 10px;
|
||||
height: calc(100% - 250px);
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.el-tab-pane {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.setting-header {
|
||||
min-width: 25%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: self-start;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.el-tabs__content {
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
|
||||
@keyframes exitLeft {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes exitRight {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(50px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes enterLeft {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes enterRight {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(50px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import axios from 'axios'
|
||||
const limit = 10;
|
||||
export default class _history {
|
||||
constructor() {
|
||||
this.histories = [];
|
||||
let histories = localStorage.getItem("histories");
|
||||
if (histories) {
|
||||
this.histories = JSON.parse(histories);
|
||||
}
|
||||
}
|
||||
save() {
|
||||
localStorage.setItem('histories', JSON.stringify(this.histories));
|
||||
}
|
||||
get length() {
|
||||
return this.histories.length;
|
||||
}
|
||||
get top() {
|
||||
return this.histories[0];
|
||||
}
|
||||
get _content(){
|
||||
return this.histories;
|
||||
}
|
||||
use(index) {
|
||||
if (this.histories.length > index) {
|
||||
console.log(this.histories);
|
||||
let history = this.histories.splice(index, 1);
|
||||
history.modified = (new Date()).getTime();
|
||||
this.histories = history.concat(this.histories);
|
||||
console.log(this.histories);
|
||||
this.save();
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
del(index) {
|
||||
this.histories.splice(index, 1);
|
||||
this.save();
|
||||
}
|
||||
add(localsets, onlinesets, settings, callback) {
|
||||
let time = (new Date()).getTime();
|
||||
let data = {
|
||||
localsets, onlinesets, settings,
|
||||
current: 0,
|
||||
modified: time
|
||||
};
|
||||
if (settings.shuffle) {
|
||||
data.settings.seed = time;
|
||||
}
|
||||
this.histories = [data].concat(this.histories);
|
||||
if(this.histories.length > limit){
|
||||
this.histories = this.histories.slice(0,limit);
|
||||
}
|
||||
this.save();
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
count(cnt) {
|
||||
if (this.histories.length > 0) {
|
||||
this.histories[0].current = cnt;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
async init_recite(callback, err) {
|
||||
let history = this.histories[0];
|
||||
let words = [];
|
||||
for (let i of history.localsets) {
|
||||
let content = localStorage.getItem(i);
|
||||
if (content){
|
||||
let temp = JSON.parse(content);
|
||||
if(history.settings.ignore_phrases){
|
||||
for(let j of temp){
|
||||
console.log(j);
|
||||
if(j.type != 'phr.'){
|
||||
words.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
else words = words.concat(...temp);
|
||||
}
|
||||
else return this.handle_err("单词本不存在",err);
|
||||
}
|
||||
for (let i of history.onlinesets) {
|
||||
let res = await axios.get("/wordset/detail", {
|
||||
params: i
|
||||
})
|
||||
if (res.status != 200) {
|
||||
return this.handle_err("获取单词本时出现错误",err);
|
||||
}
|
||||
let temp = res.data;
|
||||
if(history.settings.ignore_phrases){
|
||||
for(let j of temp){
|
||||
if(j.type != 'phr.'){
|
||||
words.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
else words = words.concat(...temp);
|
||||
}
|
||||
let arr = Array.from(new Array(words.length).keys());
|
||||
if(history.settings.shuffle){
|
||||
let seed = history.settings.seed;
|
||||
for (let i = this.total - 1; i > 0; i--) {
|
||||
[arr[i], arr[seed % i]] = [arr[seed % i], arr[i]];
|
||||
}
|
||||
}
|
||||
if(words.length <= 0){
|
||||
return this.handle_err("单词本为空",err);
|
||||
}
|
||||
history.total = words.length;
|
||||
this.save();
|
||||
if (typeof callback === 'function') {
|
||||
callback(words,history.current,arr,history.settings);
|
||||
}
|
||||
}
|
||||
handle_err(msg,err){
|
||||
this.histories.splice(0,1);
|
||||
this.save();
|
||||
if (typeof err === 'function') {
|
||||
err(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import uuid from 'node-uuid';
|
||||
|
||||
export default class _wordset {
|
||||
constructor() {
|
||||
this.sets = {};
|
||||
let wordsets = localStorage.getItem("wordsets");
|
||||
if (wordsets) {
|
||||
this.sets = JSON.parse(wordsets);
|
||||
}
|
||||
}
|
||||
save() {
|
||||
localStorage.setItem("wordsets", JSON.stringify(this.sets));
|
||||
}
|
||||
get _inner() {
|
||||
return this.sets;
|
||||
}
|
||||
get _firstClass(){
|
||||
return Object.keys(this.sets)[0] || null;
|
||||
}
|
||||
get _allclass(){
|
||||
return Object.keys(this.sets);
|
||||
}
|
||||
get _allsets(){
|
||||
return Object.values(this.sets);
|
||||
}
|
||||
getClass(class_name) {
|
||||
if (this.sets[class_name]) {
|
||||
return this.sets[class_name];
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
getSetStatus(class_name,id){
|
||||
let set_class = this.getClass(class_name);
|
||||
if(set_class){
|
||||
for(let i of set_class){
|
||||
if(i.id === id){
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
getSet(id) {
|
||||
let sets = JSON.parse(localStorage.getItem(id));
|
||||
if (sets) {
|
||||
return sets;
|
||||
}
|
||||
else return [];
|
||||
}
|
||||
delSet(class_name, index) {
|
||||
let set_class = this.sets[class_name];
|
||||
if (set_class) {
|
||||
if (set_class[index]) {
|
||||
localStorage.removeItem(set_class[index].id);
|
||||
this.sets[class_name].splice(index, 1);
|
||||
if (!this.sets[class_name].length) {
|
||||
delete this.sets[class_name];
|
||||
}
|
||||
localStorage.setItem("wordsets", JSON.stringify(this.sets));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
addSet(class_name,name) {
|
||||
if (!this.sets[class_name]) {
|
||||
this.sets[class_name] = [];
|
||||
}
|
||||
let id = uuid.v1();
|
||||
let time = (new Date()).getTime()
|
||||
this.sets[class_name].push({
|
||||
name: name,
|
||||
id: id,
|
||||
created: time,
|
||||
});
|
||||
localStorage.setItem(id, JSON.stringify([]));
|
||||
this.save();
|
||||
return;
|
||||
}
|
||||
saveSet(id,data){
|
||||
localStorage.setItem(id, JSON.stringify(data));
|
||||
}
|
||||
renameSet(class_name,id,new_name){
|
||||
let set_class = this.getClass(class_name);
|
||||
if(set_class){
|
||||
for(let i of set_class){
|
||||
if(i.id === id){
|
||||
i.name = new_name;
|
||||
this.save();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async import_set(callback){
|
||||
let [fileHandle] = await window.showOpenFilePicker({
|
||||
types: [
|
||||
{
|
||||
description: "JSON file",
|
||||
accept: {
|
||||
'text/json': ['.json'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
let file = await fileHandle.getFile();
|
||||
let data = JSON.parse(await file.text());
|
||||
let cnt = 0;
|
||||
for (let i of Object.keys(data.wordsets)) {
|
||||
for (let j of data.wordsets[i]) {
|
||||
if (!localStorage.getItem(j.id)) {
|
||||
if (!this.sets[i]) this.sets[i] = [];
|
||||
this.sets[i].push(j);
|
||||
localStorage.setItem(j.id, data.words[j.id]);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.save();
|
||||
if(typeof callback === "function"){
|
||||
callback(cnt);
|
||||
}
|
||||
}
|
||||
async export_set(callback) {
|
||||
let fileHandle = await window.showSaveFilePicker({
|
||||
types: [
|
||||
{
|
||||
description: "JSON file",
|
||||
accept: {
|
||||
'text/json': ['.json'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
let writestream = await fileHandle.createWritable();
|
||||
let data = {
|
||||
wordsets: this.sets,
|
||||
words: {}
|
||||
};
|
||||
let cnt = 0;
|
||||
for (let i of Object.values(this.sets)) {
|
||||
for (let j of i) {
|
||||
let temp = localStorage.getItem(j.id);
|
||||
data.words[j.id] = temp;
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
writestream.write(JSON.stringify(data));
|
||||
writestream.close();
|
||||
if(typeof callback === "function"){
|
||||
callback(cnt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
const md = require('marked')
|
||||
|
||||
module.exports = function (source) {
|
||||
this.cacheable()
|
||||
// source 是原始文件内容,html 是用 markdown-it 编译后的 html 内容
|
||||
const html = md.parse(source);
|
||||
const template = (
|
||||
`<template>
|
||||
<div class="markdown-body">
|
||||
${html}
|
||||
</div>
|
||||
</template>`
|
||||
)
|
||||
return template
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# 关于 wordIn
|
||||
|
||||
---
|
||||
|
||||
## 1. wordIn 是什么
|
||||
|
||||
wordIn 是一款基于 vue.js 的单词默写器,致力于为用户提供便捷有效的单词记忆方式
|
||||
|
||||
## 2.获取源代码
|
||||
|
||||
wordIn 的前端代码已经开源 [Git Repository](https://git.zjueva.net/cast1e/wordIn)
|
||||
!!需要从浙江大学内网访问
|
||||
|
||||
如果有获取后端代码的需求请联系作者
|
||||
|
||||
## 3.联系作者
|
||||
|
||||
- 通过电子邮件
|
||||
- <rooger@zju.edu.cn>
|
||||
- <rooger233@gmail.com>
|
||||
- 通过QQ
|
||||
- 1176075089
|
||||
|
||||
## 3.更新日志
|
||||
|
||||
### 1.0.1 Release
|
||||
|
||||
#### 1.0.1 是wordIn第一个正式版
|
||||
|
||||
1. 更新内容
|
||||
|
||||
- 暂时禁用了自定义背景功能(预计1.0.2恢复)
|
||||
- 增加了背诵选择界面
|
||||
- 增加了历史记录功能,最多可以存储十条记录
|
||||
- 重写了主页
|
||||
- 优化了过渡动画
|
||||
- 优化了全局变量管理
|
||||
|
||||
2. Bug修复进度
|
||||
|
||||
- (已修复)背诵时插入导致指针归位
|
||||
- (已修复)查看之前的单词时溢出的错误
|
||||
- (解决中)手机端适配问题
|
||||
|
||||
### 1.0
|
||||
|
||||
#### 1.0 及之前的版本汇总
|
||||
|
||||
1. 内容
|
||||
|
||||
- 1.0是整个项目的开始,项目包括前端界面(/),运维管理界面(/manage),后端服务器。
|
||||
|
||||
## 4.开发者的话
|
||||
|
||||
整个项目的开发已经有半年了,在这期间我为 wordIn 花费了很多时间和精力,也收获了很多。希望 wordIn 能够帮助到大家的英语学习,祝各位大英都能满绩!
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# wordIn 使用说明
|
||||
|
||||
---
|
||||
|
||||
- 近期发现ZJUWLAN无法访问unpkg.com导致部分图标无法显示,请使用ZJUWLAN-Secure访问
|
||||
|
||||
## 1. 背诵单词
|
||||
|
||||
### 开始新的背诵
|
||||
|
||||
- 在主页点击“开始新背诵”按钮
|
||||
- 选择在线或者本地单词本(可以多选)
|
||||
- 调整设置开始背诵
|
||||
|
||||
### 背诵中
|
||||
|
||||
- 快捷键说明
|
||||
- ctrl+B 显示答案
|
||||
- ctrl+M 跳过单词
|
||||
- ctrl+I 插入到单词本
|
||||
- ctrl+Y 播放音频
|
||||
- ctrl+方向键左键 上一个单词
|
||||
- ctrl+方向键右键 下一个单词
|
||||
- 错题本功能
|
||||
- wordIn内置丰富的单词管理功能,首先新建一个单词本
|
||||
- 在背诵过程中选择新建的单词本
|
||||
- 点击插入到单词本即可将现在显示的单词插入到单词本中
|
||||
- 进度记忆功能
|
||||
- 一下子没有背完?不必担心,wordIn会自动记忆您的背诵进度,您可以从主页随时返回到背诵
|
||||
|
||||
## 2. 单词本管理
|
||||
|
||||
### 新建单词本
|
||||
|
||||
- 单击新建单词本
|
||||
- 输入单词本的名称
|
||||
- 选择单词本的分类
|
||||
- 选择现有的分类
|
||||
- 在选择框中输入新的分类并 ***回车*** 创建新的分类
|
||||
- 创建完毕
|
||||
|
||||
### 管理单词本
|
||||
|
||||
- 在界面左侧选择单词本分类
|
||||
- 将鼠标移至单词本,选项会自动出现
|
||||
- 手动添加单词输入完毕后按下回车键会自动focus下一个输入框
|
||||
|
||||
### 备份本地单词本
|
||||
|
||||
- 暂不支持Andriod & iOS等系统的设备
|
||||
- 选择导出单词本将单词本导出为json格式文件
|
||||
- 选择导入单词本导入备份的单词本(不会覆盖已经存在的单词本)
|
||||
- 如果没反应,请确认您是否使用了支持的浏览器(推荐Edge,Chrome等基于Chromium的浏览器) [MDN Reference](https://developer.mozilla.org/zh-CN/docs/Web/API/File_System_API)
|
||||
|
|
@ -2,13 +2,17 @@ import {createRouter,createWebHashHistory} from 'vue-router'
|
|||
const manage = ()=>import('./components/Manage.vue');
|
||||
const editor = ()=>import('./components/manage/Editor.vue');
|
||||
const home = ()=>import("./components/Home.vue");
|
||||
const recite = ()=>import('./components/Recite.vue');
|
||||
const recite = ()=>import('./components/recite/recite.vue');
|
||||
const setlist = ()=>import('./components/manage/SetList.vue');
|
||||
const newset = ()=>import('./components/manage/NewSet.vue');
|
||||
const display = ()=>import('./components/manage/Display.vue');
|
||||
const select = ()=>import('./components/recite/select.vue');
|
||||
const manual = ()=>import('./components/post/manual.vue');
|
||||
const about = ()=>import('./components/post/about.vue');
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: home },
|
||||
{ path: '/select',component:select},
|
||||
{ path:'/recite',component:recite},
|
||||
{ path: '/manage', component: manage,
|
||||
children:[
|
||||
|
|
@ -17,6 +21,8 @@ const routes = [
|
|||
{path: 'edit',component:editor},
|
||||
{path: 'show',component:display}
|
||||
]},
|
||||
{ path: '/manual',component:manual},
|
||||
{ path: '/about',component:about},
|
||||
]
|
||||
|
||||
export default createRouter({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,20 @@
|
|||
const { defineConfig } = require('@vue/cli-service')
|
||||
const path = require('path');
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
publicPath:"./",
|
||||
devServer: {
|
||||
proxy: 'https://localhost:443'
|
||||
},
|
||||
chainWebpack: config => {
|
||||
config.module
|
||||
.rule('markdown')
|
||||
.test(/\.md$/)
|
||||
.use('vue-loader')
|
||||
.loader('vue-loader')
|
||||
.end()
|
||||
.use('md-loader')
|
||||
.loader(path.resolve(__dirname, 'src/loader/md-loader.js'))
|
||||
.end()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue