Merge branch 'zyronon:master' into master
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -42,7 +42,6 @@ declare module 'vue' {
|
||||
IconFluentArrowBounce20Regular: typeof import('~icons/fluent/arrow-bounce20-regular')['default']
|
||||
IconFluentArrowCircleRight16Regular: typeof import('~icons/fluent/arrow-circle-right16-regular')['default']
|
||||
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
|
||||
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
|
||||
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
|
||||
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
|
||||
IconFluentArrowSort20Regular: typeof import('~icons/fluent/arrow-sort20-regular')['default']
|
||||
|
||||
67
package-lock.json
generated
67
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@imengyu/vue3-context-menu": "^1.5.1",
|
||||
"@vueuse/core": "14.0.0-alpha.0",
|
||||
"axios": "^1.10.0",
|
||||
"compromise": "^14.14.4",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
@@ -2082,6 +2083,11 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
|
||||
},
|
||||
"node_modules/@unocss/astro": {
|
||||
"version": "66.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@unocss/astro/-/astro-66.5.1.tgz",
|
||||
@@ -3799,6 +3805,41 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz",
|
||||
"integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw=="
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "14.0.0-alpha.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.0.0-alpha.0.tgz",
|
||||
"integrity": "sha512-y2964MhoYalRdiBX+ilOOwh3EHsZyDhOb/hwZTkLlytcVrtX+Gbz+ffsAomaazJzZ7m/u+KI/5hLEdSHhjelgA==",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.0.0-alpha.0",
|
||||
"@vueuse/shared": "14.0.0-alpha.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "14.0.0-alpha.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.0.0-alpha.0.tgz",
|
||||
"integrity": "sha512-FyQ7kAVMnccJwp0Wt2JuEjCNGl4lpOXKQEqzTbiqhKydQBK0urGaa+v12u8qJxf4XaaPNufLdoCQYQh8ya63JQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "14.0.0-alpha.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.0.0-alpha.0.tgz",
|
||||
"integrity": "sha512-J8cmTJkB0hAwPBXRR5U3N9FJkhPBrhyYiQm21kZ3j/o8W69Pg6JlPSxLOJtkg+AwC/r5x7Gpq2Vglv84vHotwA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
@@ -14413,6 +14454,11 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
|
||||
},
|
||||
"@unocss/astro": {
|
||||
"version": "66.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@unocss/astro/-/astro-66.5.1.tgz",
|
||||
@@ -15730,6 +15776,27 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz",
|
||||
"integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw=="
|
||||
},
|
||||
"@vueuse/core": {
|
||||
"version": "14.0.0-alpha.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.0.0-alpha.0.tgz",
|
||||
"integrity": "sha512-y2964MhoYalRdiBX+ilOOwh3EHsZyDhOb/hwZTkLlytcVrtX+Gbz+ffsAomaazJzZ7m/u+KI/5hLEdSHhjelgA==",
|
||||
"requires": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.0.0-alpha.0",
|
||||
"@vueuse/shared": "14.0.0-alpha.0"
|
||||
}
|
||||
},
|
||||
"@vueuse/metadata": {
|
||||
"version": "14.0.0-alpha.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.0.0-alpha.0.tgz",
|
||||
"integrity": "sha512-FyQ7kAVMnccJwp0Wt2JuEjCNGl4lpOXKQEqzTbiqhKydQBK0urGaa+v12u8qJxf4XaaPNufLdoCQYQh8ya63JQ=="
|
||||
},
|
||||
"@vueuse/shared": {
|
||||
"version": "14.0.0-alpha.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.0.0-alpha.0.tgz",
|
||||
"integrity": "sha512-J8cmTJkB0hAwPBXRR5U3N9FJkhPBrhyYiQm21kZ3j/o8W69Pg6JlPSxLOJtkg+AwC/r5x7Gpq2Vglv84vHotwA==",
|
||||
"requires": {}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@imengyu/vue3-context-menu": "^1.5.1",
|
||||
"@vueuse/core": "14.0.0-alpha.0",
|
||||
"axios": "^1.10.0",
|
||||
"compromise": "^14.14.4",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
|
||||
9558
pnpm-lock.yaml
generated
9558
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -761,7 +761,7 @@
|
||||
"title": "Quick work",
|
||||
"titleTranslate": "破案“神速”",
|
||||
"text": "Dan Robinson has been worried all week. \nLast Tuesday he received a letter from the local police. \nIn the letter he was asked to call at the station. \nDan wondered why he was wanted by the police, \nbut he went to the station yesterday and now he is not worried anymore. \nAt the station, he was told by a smiling policeman that his bicycle had been found. \nFive days ago, the policeman told him, the bicycle was picked up in a small village four hundred miles away. \nIt is now being sent to his home by train. \nDan was most surprised when he heard the news. \nHe was amused too, because he never expected the bicycle to be found. \nIt was stolen twenty years ago when Dan was a boy of fifteen!",
|
||||
"textTranslate": "丹.鲁宾逊焦虑了整整一个星期。 \n上星期二他收到当地警察局的一封信, \n信中要求他到警察局去一趟。 \n丹奇怪警察为什么找他, \n但昨天还是去了,结果他一再担心了。 \n在警察局里,一位面带笑容的警察告诉他,他的自行车找到了。 \n那位警察对他说,那辆自行车是5天前在400英里外的一个小村里发现的, \n现在正用火车给他运回家来。 \n丹听到这个消息后,惊奇万分, \n但又感到非常好笑,因为他从未指望那辆自行车还能找到。 \n这是20年前丹还是一个15岁的孩子时被人偷走的!",
|
||||
"textTranslate": "丹.鲁宾逊焦虑了整整一个星期。 \n上星期二他收到当地警察局的一封信, \n信中要求他到警察局去一趟。 \n丹奇怪警察为什么找他, \n但昨天还是去了,结果他不再担心了。 \n在警察局里,一位面带笑容的警察告诉他,他的自行车找到了。 \n那位警察对他说,那辆自行车是5天前在400英里外的一个小村里发现的, \n现在正用火车给他运回家来。 \n丹听到这个消息后,惊奇万分, \n但又感到非常好笑,因为他从未指望那辆自行车还能找到。 \n这是20年前丹还是一个15岁的孩子时被人偷走的!",
|
||||
"newWords": [],
|
||||
"textAllWords": [],
|
||||
"audioSrc": "/sound/article/nce2-1/Quick work.mp3",
|
||||
|
||||
54
public/list/article.json
Normal file
54
public/list/article.json
Normal file
@@ -0,0 +1,54 @@
|
||||
[
|
||||
{
|
||||
"id": "article_nce1",
|
||||
"name": "新概念英语1-课文",
|
||||
"description": "",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_1.json",
|
||||
"length": 72,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"id": "article_nce2",
|
||||
"name": "新概念英语2-课文",
|
||||
"description": "新概念英语2-课文",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_2.json",
|
||||
"length": 96,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"id": "article_nce3",
|
||||
"name": "新概念英语3-课文",
|
||||
"description": "新概念英语3-课文",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_3.json",
|
||||
"length": 60,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"id": "article_nce4",
|
||||
"name": "新概念英语4-课文",
|
||||
"description": "新概念英语4-课文",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_4.json",
|
||||
"length": 48,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
}
|
||||
]
|
||||
54
public/list/recommend_article.json
Normal file
54
public/list/recommend_article.json
Normal file
@@ -0,0 +1,54 @@
|
||||
[
|
||||
{
|
||||
"id": "article_nce1",
|
||||
"name": "新概念英语1-课文",
|
||||
"description": "",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_1.json",
|
||||
"length": 72,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"id": "article_nce2",
|
||||
"name": "新概念英语2-课文",
|
||||
"description": "新概念英语2-课文",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_2.json",
|
||||
"length": 96,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"id": "article_nce3",
|
||||
"name": "新概念英语3-课文",
|
||||
"description": "新概念英语3-课文",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_3.json",
|
||||
"length": 60,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"id": "article_nce4",
|
||||
"name": "新概念英语4-课文",
|
||||
"description": "新概念英语4-课文",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_4.json",
|
||||
"length": 48,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
}
|
||||
]
|
||||
@@ -1,5 +1,4 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"id": "cet4",
|
||||
"name": "CET-4",
|
||||
@@ -675,9 +674,9 @@
|
||||
"length": 1665,
|
||||
"language": "en",
|
||||
"translateLanguage": "zh-CN"
|
||||
}
|
||||
],
|
||||
[
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "pet-2024",
|
||||
"name": "PET-2024",
|
||||
@@ -1873,9 +1872,9 @@
|
||||
"length": 438,
|
||||
"language": "en",
|
||||
"translateLanguage": "zh-CN"
|
||||
}
|
||||
],
|
||||
[
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "gaokao3500",
|
||||
"name": "高考 3500 词",
|
||||
@@ -3257,5 +3256,4 @@
|
||||
"language": "en",
|
||||
"translateLanguage": "zh-CN"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -1,8 +1,8 @@
|
||||
const {SitemapStream, streamToPromise} = require('sitemap')
|
||||
const {createWriteStream} = require('fs')
|
||||
const {resolve} = require('path')
|
||||
const bookList = require('../src/assets/book-list.json')
|
||||
const dictList = require('../src/assets/dict-list.json')
|
||||
const bookList = require('../public/list/article.json')
|
||||
const dictList = require('../public/list/word.json')
|
||||
// 你的网站域名
|
||||
const SITE_URL = 'https://2study.top'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const fs = require("fs");
|
||||
const bookList = require('../src/assets/book-list.json')
|
||||
const dictList = require('../src/assets/dict-list.json')
|
||||
const bookList = require('../public/list/article.json')
|
||||
const dictList = require('../public/list/word.json')
|
||||
|
||||
async function pushUrls() {
|
||||
// 配置区:改成你的
|
||||
|
||||
@@ -4,13 +4,14 @@ import { BaseState, useBaseStore } from "@/stores/base.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import { APP_VERSION, LOCAL_FILE_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY } from "@/utils/const.ts";
|
||||
import { shakeCommonDict } from "@/utils";
|
||||
import { routes } from "@/router.ts";
|
||||
import { get, set } from 'idb-keyval'
|
||||
|
||||
import { useRoute } from "vue-router";
|
||||
import { DictId } from "@/types/types.ts";
|
||||
import { APP_VERSION, CAN_REQUEST, LOCAL_FILE_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY } from "@/config/env.ts";
|
||||
import { syncSetting } from "@/apis";
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
@@ -50,6 +51,9 @@ watch(store.$state, (n: BaseState) => {
|
||||
|
||||
watch(settingStore.$state, (n) => {
|
||||
set(SAVE_SETTING_KEY.key, JSON.stringify({val: n, version: SAVE_SETTING_KEY.version}))
|
||||
if (CAN_REQUEST) {
|
||||
syncSetting(null, settingStore.$state)
|
||||
}
|
||||
})
|
||||
|
||||
async function init() {
|
||||
|
||||
61
src/apis/index.ts
Normal file
61
src/apis/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import http, { axiosInstance } from "@/utils/http.ts";
|
||||
import { Dict } from "@/types/types.ts";
|
||||
import { cloneDeep } from "@/utils";
|
||||
|
||||
function remove(data?: any) {
|
||||
if (data) {
|
||||
let s = cloneDeep(data)
|
||||
delete s.words
|
||||
delete s.articles
|
||||
delete s.statistics
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
export function dictListVersion() {
|
||||
return http<number>('dict/dictListVersion', null, null, 'get')
|
||||
}
|
||||
|
||||
export function myDictList(params?) {
|
||||
return http('dict/myDictList', null, params, 'get')
|
||||
}
|
||||
|
||||
export function add2MyDict(data) {
|
||||
return http('dict/add2MyDict', remove(data), null, 'post')
|
||||
}
|
||||
|
||||
export function addStat(data) {
|
||||
return http('dict/addStat', data, null, 'post')
|
||||
}
|
||||
|
||||
export function detail(params?, data?) {
|
||||
return http<Dict>('dict/detail', data, params, 'get')
|
||||
}
|
||||
|
||||
export function setDictProp(params?, data?) {
|
||||
return http<Dict>('dict/setDictProp', remove(data), remove(params), 'post')
|
||||
}
|
||||
|
||||
export function syncSetting(params?, data?) {
|
||||
return http<Dict>('dict/syncSetting', remove(data), remove(params), 'post')
|
||||
}
|
||||
|
||||
export function getSetting(params?, data?) {
|
||||
return http<Dict>('dict/getSetting', remove(data), remove(params), 'get')
|
||||
}
|
||||
|
||||
export function addDict(params?, data?) {
|
||||
return http<Dict>('dict/addDict', remove(data), remove(params), 'post')
|
||||
}
|
||||
|
||||
export function uploadImportData(data,onUploadProgress) {
|
||||
return axiosInstance({
|
||||
url: 'dict/uploadImportData',
|
||||
method: 'post',
|
||||
headers: {
|
||||
contentType: 'formdata',
|
||||
},
|
||||
data,
|
||||
onUploadProgress
|
||||
})
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"id": "article_nce1",
|
||||
"name": "新概念英语1-课文",
|
||||
"description": "",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_1.json",
|
||||
"length": 72,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"id": "article_nce2",
|
||||
"name": "新概念英语2-课文",
|
||||
"description": "新概念英语2-课文",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_2.json",
|
||||
"length": 96,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"id": "article_nce3",
|
||||
"name": "新概念英语3-课文",
|
||||
"description": "新概念英语3-课文",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_3.json",
|
||||
"length": 60,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"id": "article_nce4",
|
||||
"name": "新概念英语4-课文",
|
||||
"description": "新概念英语4-课文",
|
||||
"category": "文章学习",
|
||||
"tags": [
|
||||
"新概念英语"
|
||||
],
|
||||
"url": "NCE_4.json",
|
||||
"length": 48,
|
||||
"translateLanguage": "common",
|
||||
"language": "en"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -20,6 +20,7 @@ let list = defineModel('list')
|
||||
const props = withDefaults(defineProps<{
|
||||
loading?: boolean
|
||||
showToolbar?: boolean
|
||||
showPagination?: boolean
|
||||
exportLoading?: boolean
|
||||
importLoading?: boolean
|
||||
del?: Function
|
||||
@@ -28,6 +29,7 @@ const props = withDefaults(defineProps<{
|
||||
}>(), {
|
||||
loading: true,
|
||||
showToolbar: true,
|
||||
showPagination: true,
|
||||
exportLoading: false,
|
||||
importLoading: false,
|
||||
del: () => void 0,
|
||||
@@ -71,6 +73,7 @@ let currentList = $computed(() => {
|
||||
if (searchKey) {
|
||||
return list.value.filter(v => v.word.includes(searchKey))
|
||||
}
|
||||
if (!props.showPagination) return list.value
|
||||
return list.value.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
|
||||
})
|
||||
|
||||
@@ -150,7 +153,8 @@ defineRender(
|
||||
clearable
|
||||
modelValue={searchKey}
|
||||
onUpdate:modelValue={debounce(e => searchKey = e)}
|
||||
class="flex-1">
|
||||
class="flex-1"
|
||||
autofocus>
|
||||
{{
|
||||
subfix: () => <IconFluentSearch24Regular
|
||||
class="text-lg text-gray"
|
||||
@@ -170,134 +174,136 @@ defineRender(
|
||||
<span>{selectIds.length} / {list.value.length}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 relative">
|
||||
{
|
||||
selectIds.length ?
|
||||
<PopConfirm title="确认删除所有选中数据?"
|
||||
onConfirm={handleBatchDel}
|
||||
>
|
||||
<BaseIcon
|
||||
class="del"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
: null
|
||||
}
|
||||
<BaseIcon
|
||||
onClick={() => showImportDialog = true}
|
||||
title="导入">
|
||||
<IconSystemUiconsImport/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={() => emit('exportData')}
|
||||
title="导出">
|
||||
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={props.add}
|
||||
title="添加单词">
|
||||
<IconFluentAdd20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!currentList.length}
|
||||
title="改变顺序"
|
||||
onClick={() => showSortDialog = !showSortDialog}
|
||||
>
|
||||
<IconFluentArrowSort20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!currentList.length}
|
||||
onClick={() => showSearchInput = !showSearchInput}
|
||||
title="搜索">
|
||||
<IconFluentSearch20Regular/>
|
||||
</BaseIcon>
|
||||
<MiniDialog
|
||||
modelValue={showSortDialog}
|
||||
onUpdate:modelValue={e => showSortDialog = e}
|
||||
style="width: 8rem;"
|
||||
>
|
||||
<div class="mini-row-title">
|
||||
列表顺序设置
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" onClick={() => sort(Sort.reverse)}>翻转
|
||||
</BaseButton>
|
||||
<BaseButton size="small" onClick={() => sort(Sort.random)}>随机</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
props.loading ?
|
||||
<div class="h-full w-full center text-4xl">
|
||||
<IconEosIconsLoading color="gray"/>
|
||||
<div class="flex gap-2 relative">
|
||||
{
|
||||
selectIds.length ?
|
||||
<PopConfirm title="确认删除所有选中数据?"
|
||||
onConfirm={handleBatchDel}
|
||||
>
|
||||
<BaseIcon
|
||||
class="del"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
: null
|
||||
}
|
||||
<BaseIcon
|
||||
onClick={() => showImportDialog = true}
|
||||
title="导入">
|
||||
<IconSystemUiconsImport/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={() => emit('exportData')}
|
||||
title="导出">
|
||||
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={props.add}
|
||||
title="添加单词">
|
||||
<IconFluentAdd20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!currentList.length}
|
||||
title="改变顺序"
|
||||
onClick={() => showSortDialog = !showSortDialog}
|
||||
>
|
||||
<IconFluentArrowSort20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!currentList.length}
|
||||
onClick={() => showSearchInput = !showSearchInput}
|
||||
title="搜索">
|
||||
<IconFluentSearch20Regular/>
|
||||
</BaseIcon>
|
||||
<MiniDialog
|
||||
modelValue={showSortDialog}
|
||||
onUpdate:modelValue={e => showSortDialog = e}
|
||||
style="width: 8rem;"
|
||||
>
|
||||
<div class="mini-row-title">
|
||||
列表顺序设置
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" onClick={() => sort(Sort.reverse)}>翻转
|
||||
</BaseButton>
|
||||
<BaseButton size="small" onClick={() => sort(Sort.random)}>随机</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
: currentList.length ? (
|
||||
<>
|
||||
<div class="flex-1 overflow-auto"
|
||||
ref={e => listRef = e}>
|
||||
{currentList.map((item, index) => {
|
||||
return (
|
||||
<div class="list-item-wrapper"
|
||||
key={item.word}
|
||||
>
|
||||
{s.default({checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Pagination
|
||||
currentPage={pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={pageSize}
|
||||
onUpdate:page-size={(e) => pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="prev, pager, next"
|
||||
total={list.value.length}/>
|
||||
</div>
|
||||
</>
|
||||
) : <Empty/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<Dialog modelValue={showImportDialog}
|
||||
onUpdate:modelValue={closeImportDialog}
|
||||
title="导入教程"
|
||||
>
|
||||
<div className="w-100 p-4 pt-0">
|
||||
<div>请按照模板的格式来填写数据</div>
|
||||
<div class="color-red">单词项为必填,其他项可不填</div>
|
||||
<div>翻译:一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行</div>
|
||||
<div>例句:一行原文,一行译文;多个请换<span class="color-red">两</span>行</div>
|
||||
<div>短语:一行原文,一行译文;多个请换<span class="color-red">两</span>行</div>
|
||||
<div>同义词、同根词、词源:请前往官方字典,然后编辑其中某个单词,参考其格式</div>
|
||||
<div class="mt-6">
|
||||
模板下载地址:<a href="https://2study.top/libs/单词导入模板.xlsx">单词导入模板</a>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<BaseButton
|
||||
onClick={() => {
|
||||
let d: HTMLDivElement = document.querySelector('#upload-trigger')
|
||||
d.click()
|
||||
}}
|
||||
loading={props.importLoading}>导入</BaseButton>
|
||||
<input
|
||||
id="upload-trigger"
|
||||
type="file"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
onChange={e => emit('importData', e)}
|
||||
class="w-0 h-0 opacity-0"/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
{
|
||||
props.loading ?
|
||||
<div class="h-full w-full center text-4xl">
|
||||
<IconEosIconsLoading color="gray"/>
|
||||
</div>
|
||||
: currentList.length ? (
|
||||
<>
|
||||
<div class="flex-1 overflow-auto"
|
||||
ref={e => listRef = e}>
|
||||
{currentList.map((item, index) => {
|
||||
return (
|
||||
<div class="list-item-wrapper"
|
||||
key={item.word}
|
||||
>
|
||||
{s.default({checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{
|
||||
props.showPagination && <div class="flex justify-end">
|
||||
<Pagination
|
||||
currentPage={pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={pageSize}
|
||||
onUpdate:page-size={(e) => pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="prev, pager, next"
|
||||
total={list.value.length}/>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
) : <Empty/>
|
||||
}
|
||||
|
||||
<Dialog modelValue={showImportDialog}
|
||||
onUpdate:modelValue={closeImportDialog}
|
||||
title="导入教程"
|
||||
>
|
||||
<div className="w-100 p-4 pt-0">
|
||||
<div>请按照模板的格式来填写数据</div>
|
||||
<div class="color-red">单词项为必填,其他项可不填</div>
|
||||
<div>翻译:一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行</div>
|
||||
<div>例句:一行原文,一行译文;多个请换<span class="color-red">两</span>行</div>
|
||||
<div>短语:一行原文,一行译文;多个请换<span class="color-red">两</span>行</div>
|
||||
<div>同义词、同根词、词源:请前往官方字典,然后编辑其中某个单词,参考其格式</div>
|
||||
<div class="mt-6">
|
||||
模板下载地址:<a href="https://2study.top/libs/单词导入模板.xlsx">单词导入模板</a>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<BaseButton
|
||||
onClick={() => {
|
||||
let d: HTMLDivElement = document.querySelector('#upload-trigger')
|
||||
d.click()
|
||||
}}
|
||||
loading={props.importLoading}>导入</BaseButton>
|
||||
<input
|
||||
id="upload-trigger"
|
||||
type="file"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
onChange={e => emit('importData', e)}
|
||||
class="w-0 h-0 opacity-0"/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -6,7 +6,7 @@ import {watch} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
import {isMobile} from "@/utils";
|
||||
import {ProjectName, Host} from "@/config/ENV.ts";
|
||||
import {ProjectName, Host} from "@/config/env.ts";
|
||||
|
||||
let settingStore = useSettingStore()
|
||||
let showNotice = $ref(false)
|
||||
|
||||
@@ -5,7 +5,7 @@ import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
withDefaults(defineProps<{
|
||||
item: Word,
|
||||
showTranslate?: boolean
|
||||
showWord?: boolean
|
||||
@@ -37,8 +37,8 @@ const playWordAudio = usePlayWordAudio()
|
||||
<div class="item-sub-title flex flex-col gap-2" v-if="item.trans.length && showTranslate">
|
||||
<div v-for="v in item.trans">
|
||||
<Tooltip
|
||||
v-if="v.cn.length > 30 && showTransPop"
|
||||
:title="v.pos + ' ' + v.cn"
|
||||
v-if="v.cn.length > 30 && showTransPop"
|
||||
:title="v.pos + ' ' + v.cn"
|
||||
>
|
||||
<span>{{ v.pos + ' ' + v.cn.slice(0, 30) + '...' }}</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -126,7 +126,6 @@ const vFocus = {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background: var(--color-input-bg);
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@@ -173,6 +172,7 @@ const vFocus = {
|
||||
transition: all .3s;
|
||||
height: 1.5rem;
|
||||
color: var(--color-input-color);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
showText: true,
|
||||
textInside: false,
|
||||
strokeWidth: 6,
|
||||
color: '#409eff',
|
||||
color: '#93ADE3',
|
||||
format: (percentage) => `${percentage}%`,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ import { nextTick, watch } from 'vue'
|
||||
const props = withDefaults(defineProps<{
|
||||
list?: any[],
|
||||
activeIndex?: number,
|
||||
activeId?: string,
|
||||
activeId?: number,
|
||||
isActive?: boolean
|
||||
static?: boolean
|
||||
}>(), {
|
||||
list: [],
|
||||
activeIndex: -1,
|
||||
activeId: '',
|
||||
activeId: null,
|
||||
isActive: false,
|
||||
static: true
|
||||
})
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export const GITHUB = 'https://github.com/zyronon/TypeWords'
|
||||
export const ProjectName = 'Type Words'
|
||||
export const Host = '2study.top'
|
||||
export const Origin = `https://${Host}`
|
||||
|
||||
const common = {
|
||||
word_dict_list_version: 1
|
||||
}
|
||||
const map = {
|
||||
dev: {
|
||||
api: 'http://localhost/index.php',
|
||||
}
|
||||
}
|
||||
export const env = Object.assign(map['dev'], common)
|
||||
68
src/config/env.ts
Normal file
68
src/config/env.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
|
||||
export const GITHUB = 'https://github.com/zyronon/TypeWords'
|
||||
export const ProjectName = 'Type Words'
|
||||
export const Host = '2study.top'
|
||||
export const Origin = `https://${Host}`
|
||||
export const APP_NAME = 'Type Words'
|
||||
|
||||
const common = {
|
||||
word_dict_list_version: 1
|
||||
}
|
||||
const map = {
|
||||
DEV: {
|
||||
API: 'http://localhost/',
|
||||
}
|
||||
}
|
||||
|
||||
export const ENV = Object.assign(map['DEV'], common)
|
||||
// export const IS_OFFICIAL = import.meta.env.DEV
|
||||
// export let IS_LOGIN = true
|
||||
export const IS_OFFICIAL = false
|
||||
export let IS_LOGIN = false
|
||||
export const CAN_REQUEST = IS_LOGIN && IS_OFFICIAL
|
||||
export const RESOURCE_PATH = ENV.API + 'static'
|
||||
|
||||
export const DICT_LIST = {
|
||||
WORD: {
|
||||
ALL: '/list/word.json',
|
||||
RECOMMENDED: '/list/recommend_word.json',
|
||||
},
|
||||
ARTICLE: {
|
||||
ALL: '/list/article.json',
|
||||
RECOMMENDED: '/list/article.json',
|
||||
}
|
||||
}
|
||||
|
||||
export const SoundFileOptions = [
|
||||
{value: '机械键盘', label: '机械键盘'},
|
||||
{value: '机械键盘1', label: '机械键盘1'},
|
||||
{value: '机械键盘2', label: '机械键盘2'},
|
||||
{value: '老式机械键盘', label: '老式机械键盘'},
|
||||
{value: '笔记本键盘', label: '笔记本键盘'},
|
||||
]
|
||||
export const APP_VERSION = {
|
||||
key: 'type-words-app-version',
|
||||
version: 2
|
||||
}
|
||||
export const SAVE_DICT_KEY = {
|
||||
key: 'typing-word-dict',
|
||||
version: 4
|
||||
}
|
||||
export const SAVE_SETTING_KEY = {
|
||||
key: 'typing-word-setting',
|
||||
version: 16
|
||||
}
|
||||
export const EXPORT_DATA_KEY = {
|
||||
key: 'typing-word-export',
|
||||
version: 4
|
||||
}
|
||||
export const LOCAL_FILE_KEY = 'typing-word-files'
|
||||
export const PracticeSaveWordKey = {
|
||||
key: 'PracticeSaveWord',
|
||||
version: 1
|
||||
}
|
||||
export const PracticeSaveArticleKey = {
|
||||
key: 'PracticeSaveArticle',
|
||||
version: 1
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/directives/loading.js
|
||||
import {createApp, h} from 'vue'
|
||||
//@ts-ignore
|
||||
import IconEosIconsLoading from '~icons/eos-icons/loading'
|
||||
|
||||
// 创建一个 Loading 组件
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Article, TaskWords, Word } from "@/types/types.ts";
|
||||
import {Article, TaskWords, Word, WordPracticeMode} from "@/types/types.ts";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { getDefaultWord } from "@/types/func.ts";
|
||||
@@ -144,7 +144,7 @@ export function getCurrentStudyWord(): TaskWords {
|
||||
}
|
||||
|
||||
//如果是自由模式,那么统统设置到new字段里面去
|
||||
if (settingStore.wordPracticeMode === 1) {
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
|
||||
data.new = data.new.length ? data.new : data.review
|
||||
data.review = []
|
||||
return data
|
||||
|
||||
@@ -37,6 +37,7 @@ export function useEventListener(type: string, listener: EventListenerOrEventLis
|
||||
e.ctrlKey = false
|
||||
e.altKey = false
|
||||
e.shiftKey = false
|
||||
//@ts-ignore
|
||||
listener(e)
|
||||
e.target.value = '1'
|
||||
})
|
||||
@@ -53,7 +54,6 @@ export function useEventListener(type: string, listener: EventListenerOrEventLis
|
||||
}
|
||||
})
|
||||
const remove = () => {
|
||||
console.log('onUnmounted')
|
||||
if (isMobile()) {
|
||||
let s = document.querySelector('#typing-listener')
|
||||
if (s) {
|
||||
@@ -71,7 +71,7 @@ export function useEventListener(type: string, listener: EventListenerOrEventLis
|
||||
|
||||
export function getShortcutKey(e: KeyboardEvent) {
|
||||
let shortcutKey = ''
|
||||
if (e.ctrlKey) shortcutKey += 'Ctrl+'
|
||||
if (e.ctrlKey || e.metaKey) shortcutKey += 'Ctrl+'
|
||||
if (e.altKey) shortcutKey += 'Alt+'
|
||||
if (e.shiftKey) shortcutKey += 'Shift+'
|
||||
if (e.key !== 'Control' && e.key !== 'Alt' && e.key !== 'Shift') {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {onMounted, watchEffect} from "vue"
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {PronunciationApi} from "@/types/types.ts";
|
||||
import {SoundFileOptions} from "@/utils/const.ts";
|
||||
|
||||
import { SoundFileOptions } from "@/config/env.ts";
|
||||
|
||||
export function useSound(audioSrcList?: string[], audioFileLength?: number) {
|
||||
let audioList: HTMLAudioElement[] = $ref([])
|
||||
|
||||
@@ -1,33 +1,71 @@
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
// 获取系统主题
|
||||
function getSystemTheme(): Theme {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
} else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
||||
return 'light';
|
||||
}
|
||||
return 'light'; // 默认浅色模式
|
||||
}
|
||||
|
||||
// 交换主题名称
|
||||
function swapTheme(theme: Theme): Theme {
|
||||
return theme === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
// 监听系统主题变化
|
||||
function listenToSystemThemeChange(call: (theme: Theme) => void) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||
if (e.matches) {
|
||||
// console.log('系统已切换到深色模式');
|
||||
call('dark');
|
||||
}
|
||||
});
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', e => {
|
||||
if (e.matches) {
|
||||
// console.log('系统已切换到浅色模式');
|
||||
call('light');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default function useTheme() {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// // 查询当前系统主题颜色
|
||||
// const match: MediaQueryList = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
// // 监听系统主题变化
|
||||
// match.addEventListener('change', followSystem)
|
||||
//
|
||||
// function followSystem() {
|
||||
// document.documentElement.className = match.matches ? 'dark' : 'light'
|
||||
// }
|
||||
|
||||
// 开启监听系统主题变更,后期可以通过用户配置来决定是否开启
|
||||
listenToSystemThemeChange((theme: Theme) => {
|
||||
// 如果系统主题变更后和当前的主题一致,则不需要再重新切换
|
||||
if(settingStore.theme === theme){
|
||||
return;
|
||||
}
|
||||
|
||||
settingStore.theme = theme;
|
||||
setTheme(theme);
|
||||
})
|
||||
|
||||
function toggleTheme() {
|
||||
if (settingStore.theme === 'auto') {
|
||||
settingStore.theme = 'light'
|
||||
} else {
|
||||
settingStore.theme = settingStore.theme === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
setTheme(settingStore.theme)
|
||||
// auto模式下,默认是使用系统主题,切换时应该使用当前系统主题为基础进行切换
|
||||
settingStore.theme = swapTheme(settingStore.theme === 'auto' ? getSystemTheme() : settingStore.theme as Theme);
|
||||
setTheme(settingStore.theme);
|
||||
}
|
||||
|
||||
function setTheme(val) {
|
||||
document.documentElement.className = val
|
||||
function setTheme(val:string) {
|
||||
// auto模式下,则通过查询系统主题来设置主题名称
|
||||
document.documentElement.className = val === 'auto' ? getSystemTheme() : val;
|
||||
}
|
||||
|
||||
// 获取当前具体的主题名称
|
||||
function getTheme():Theme{
|
||||
// auto模式下,则通过查询系统主题来获取当前具体的主题名称
|
||||
return settingStore.theme === 'auto' ? getSystemTheme() : settingStore.theme as Theme;
|
||||
}
|
||||
|
||||
return {
|
||||
toggleTheme,
|
||||
setTheme
|
||||
setTheme,
|
||||
getTheme
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import {createApp} from 'vue'
|
||||
import { createApp } from 'vue'
|
||||
import './assets/css/style.scss'
|
||||
import 'virtual:uno.css';
|
||||
import App from './App.vue'
|
||||
import {createPinia} from "pinia"
|
||||
import { createPinia } from "pinia"
|
||||
import router from "@/router.ts";
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import './types/global.d.ts'
|
||||
import loadingDirective from './directives/loading.tsx'
|
||||
|
||||
|
||||
const pinia = createPinia()
|
||||
const app = createApp(App)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { _getDictDataByUrl, msToHourMinute, total, useNav } from "@/utils";
|
||||
import { _getDictDataByUrl, msToHourMinute, resourceWrap, total, useNav } from "@/utils";
|
||||
import { DictResource, DictType } from "@/types/types.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
@@ -14,11 +14,12 @@ import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import { watch } from "vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
import recommendBookList from "@/assets/book-list.json";
|
||||
import dayjs from "dayjs";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import { PracticeSaveArticleKey } from "@/utils/const.ts";
|
||||
import isoWeek from 'dayjs/plugin/isoWeek'
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { CAN_REQUEST, DICT_LIST, PracticeSaveArticleKey } from "@/config/env.ts";
|
||||
import { myDictList } from "@/apis";
|
||||
|
||||
dayjs.extend(isoWeek)
|
||||
dayjs.extend(isBetween);
|
||||
@@ -35,6 +36,12 @@ watch(() => store.load, n => {
|
||||
}, {immediate: true})
|
||||
|
||||
async function init() {
|
||||
if (CAN_REQUEST) {
|
||||
let res = await myDictList({type: "article"})
|
||||
if (res.success) {
|
||||
store.setState(Object.assign(store.$state, res.data))
|
||||
}
|
||||
}
|
||||
if (store.article.studyIndex >= 1) {
|
||||
if (!store.sbook.custom && !store.sbook.articles.length) {
|
||||
store.article.bookList[store.article.studyIndex] = await _getDictDataByUrl(store.sbook, DictType.article)
|
||||
@@ -152,6 +159,10 @@ const weekList = $computed(() => {
|
||||
});
|
||||
return list
|
||||
})
|
||||
|
||||
const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.ARTICLE.RECOMMENDED)).json()
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -246,7 +257,8 @@ const weekList = $computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col">
|
||||
|
||||
<div class="card flex flex-col min-h-50" v-loading="isFetching">
|
||||
<div class="flex justify-between">
|
||||
<div class="title">推荐</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
@@ -258,7 +270,7 @@ const weekList = $computed(() => {
|
||||
<Book :is-add="false"
|
||||
quantifier="篇"
|
||||
:item="item as any"
|
||||
v-for="(item, j) in recommendBookList[0]" @click="goBookDetail(item as any)"/>
|
||||
v-for="(item, j) in recommendBookList" @click="goBookDetail(item as any)"/>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {getDefaultArticle} from "@/types/func.ts";
|
||||
import BackIcon from "@/components/BackIcon.vue";
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import {onMounted} from "vue";
|
||||
import {Origin} from "@/config/ENV.ts";
|
||||
import {Origin} from "@/config/env.ts";
|
||||
import {syncBookInMyStudyList} from "@/hooks/article.ts";
|
||||
|
||||
const base = useBaseStore()
|
||||
|
||||
@@ -11,15 +11,17 @@ import BaseButton from "@/components/BaseButton.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import EditBook from "@/pages/article/components/EditBook.vue";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { _dateFormat, _getDictDataByUrl, cloneDeep, msToHourMinute, total, useNav } from "@/utils";
|
||||
import { _dateFormat, _getDictDataByUrl, msToHourMinute, resourceWrap, total, useNav } from "@/utils";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import { useArticleOptions } from "@/hooks/dict.ts";
|
||||
import { getDefaultArticle, getDefaultDict } from "@/types/func.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import { MessageBox } from "@/utils/MessageBox.tsx";
|
||||
import book_list from "@/assets/book-list.json";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { CAN_REQUEST, DICT_LIST } from "@/config/env.ts";
|
||||
import { detail } from "@/apis";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -56,7 +58,7 @@ async function addMyStudyList() {
|
||||
}
|
||||
|
||||
studyLoading = true
|
||||
base.changeBook(sbook)
|
||||
await base.changeBook(sbook)
|
||||
studyLoading = false
|
||||
|
||||
window.umami?.track('startStudyArticle', {
|
||||
@@ -82,17 +84,29 @@ async function init() {
|
||||
} else {
|
||||
if (!runtimeStore.editDict?.articles?.length
|
||||
&& !runtimeStore.editDict?.custom
|
||||
&& ![DictId.articleCollect].includes(runtimeStore.editDict.id)
|
||||
&& ![DictId.articleCollect].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
|
||||
&& !runtimeStore.editDict?.is_default
|
||||
) {
|
||||
loading = true
|
||||
let r = await _getDictDataByUrl(runtimeStore.editDict, DictType.article)
|
||||
loading = false
|
||||
runtimeStore.editDict = r
|
||||
}
|
||||
|
||||
if (base.article.bookList.find(book => book.id === runtimeStore.editDict.id)) {
|
||||
if (CAN_REQUEST) {
|
||||
let res = await detail({id: runtimeStore.editDict.id})
|
||||
if (res.success) {
|
||||
runtimeStore.editDict.statistics = res.data.statistics
|
||||
if (res.data.articles.length) {
|
||||
runtimeStore.editDict.articles = res.data.articles
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (runtimeStore.editDict.articles.length) {
|
||||
selectArticle = runtimeStore.editDict.articles[0]
|
||||
}
|
||||
console.log('runtimeStore.editDict', runtimeStore.editDict)
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,12 +123,14 @@ const {
|
||||
toggleArticleCollect
|
||||
} = useArticleOptions()
|
||||
|
||||
const {data: book_list} = useFetch(resourceWrap(DICT_LIST.ARTICLE.ALL)).json()
|
||||
|
||||
function reset() {
|
||||
MessageBox.confirm(
|
||||
'继续此操作会重置所有文章,并从官方书籍获取最新文章列表,学习记录不会被重置。确认恢复默认吗?',
|
||||
'恢复默认',
|
||||
async () => {
|
||||
let dict = book_list.flat().find(v => v.url === runtimeStore.editDict.url) as Dict
|
||||
let dict = book_list.value.find(v => v.url === runtimeStore.editDict.url) as Dict
|
||||
if (dict && dict.id) {
|
||||
dict = await _getDictDataByUrl(dict, DictType.article)
|
||||
let rIndex = base.article.bookList.findIndex(v => v.id === runtimeStore.editDict.id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useNav } from "@/utils";
|
||||
import { resourceWrap, useNav } from "@/utils";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { DictResource } from "@/types/types.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
@@ -9,9 +9,10 @@ import BaseButton from "@/components/BaseButton.vue";
|
||||
import DictList from "@/components/list/DictList.vue";
|
||||
import BackIcon from "@/components/BackIcon.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import book_list from "@/assets/book-list.json";
|
||||
import { computed } from "vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { DICT_LIST } from "@/config/env.ts";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
|
||||
const {nav} = useNav()
|
||||
@@ -30,11 +31,12 @@ async function getDictDetail(val: DictResource) {
|
||||
|
||||
let showSearchInput = $ref(false)
|
||||
let searchKey = $ref('')
|
||||
const {data: bookList, isFetching} = useFetch(resourceWrap(DICT_LIST.ARTICLE.ALL)).json()
|
||||
|
||||
const searchList = computed<any[]>(() => {
|
||||
if (searchKey) {
|
||||
let s = searchKey.toLowerCase()
|
||||
return book_list.flat().filter((item) => {
|
||||
return bookList.value.filter((item) => {
|
||||
return item.id.toLowerCase().includes(s)
|
||||
|| item.name.toLowerCase().includes(s)
|
||||
|| item.category.toLowerCase().includes(s)
|
||||
@@ -49,7 +51,7 @@ const searchList = computed<any[]>(() => {
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="card">
|
||||
<div class="card min-h-50" v-loading="isFetching">
|
||||
<div class="flex items-center relative gap-2">
|
||||
<BackIcon class="z-2" @Click='router.back'/>
|
||||
<div class="flex flex-1 gap-4" v-if="showSearchInput">
|
||||
@@ -75,9 +77,9 @@ const searchList = computed<any[]>(() => {
|
||||
</div>
|
||||
<div class="w-full mt-2" v-else>
|
||||
<DictList
|
||||
v-if="book_list.flat().length "
|
||||
v-if="bookList?.length "
|
||||
@selectDict="selectDict"
|
||||
:list="book_list.flat()"
|
||||
:list="bookList"
|
||||
quantifier="篇"
|
||||
:select-id="'-1'"/>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import { _getDictDataByUrl, _nextTick, cloneDeep, msToMinute, total } from "@/utils";
|
||||
import { _getDictDataByUrl, _nextTick, cloneDeep, msToMinute, resourceWrap, total } from "@/utils";
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import { useArticleOptions } from "@/hooks/dict.ts";
|
||||
import { genArticleSectionData, usePlaySentenceAudio } from "@/hooks/article.ts";
|
||||
@@ -31,13 +31,15 @@ import EditSingleArticleModal from "@/pages/article/components/EditSingleArticle
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import ConflictNotice from "@/components/ConflictNotice.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import book_list from "@/assets/book-list.json";
|
||||
import PracticeLayout from "@/components/PracticeLayout.vue";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import { PracticeSaveArticleKey } from "@/utils/const.ts";
|
||||
import VolumeSetting from "@/pages/article/components/VolumeSetting.vue";
|
||||
import { CAN_REQUEST, DICT_LIST, PracticeSaveArticleKey } from "@/config/env.ts";
|
||||
import { addStat, setDictProp } from "@/apis";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
const statStore = usePracticeStore()
|
||||
const {toggleTheme} = useTheme()
|
||||
@@ -51,7 +53,6 @@ let typingArticleRef = $ref<any>()
|
||||
let loading = $ref<boolean>(false)
|
||||
let allWrongWords = new Set()
|
||||
let editArticle = $ref<Article>(getDefaultArticle())
|
||||
let speedMinute = $ref(0)
|
||||
let timer = $ref(0)
|
||||
let isFocus = true
|
||||
|
||||
@@ -108,8 +109,10 @@ async function init() {
|
||||
let dictId = route.params.id
|
||||
if (dictId) {
|
||||
//先在自己的词典列表里面找,如果没有再在资源列表里面找
|
||||
dict = store.article.bookList.find(v => v.id === dictId)
|
||||
if (!dict) dict = book_list.flat().find(v => v.id === dictId) as Dict
|
||||
dict = store.article.bookList.find(v => v.id == dictId)
|
||||
let r = await fetch(resourceWrap(DICT_LIST.ARTICLE.ALL))
|
||||
let book_list = await r.json()
|
||||
if (!dict) dict = book_list.find(v => v.id === dictId) as Dict
|
||||
if (dict && dict.id) {
|
||||
//如果是不是自定义词典,就请求数据
|
||||
if (!dict.custom) dict = await _getDictDataByUrl(dict, DictType.article)
|
||||
@@ -117,7 +120,7 @@ async function init() {
|
||||
router.push('/articles')
|
||||
return Toast.warning('没有文章可学习!')
|
||||
}
|
||||
store.changeBook(dict)
|
||||
await store.changeBook(dict)
|
||||
articleData.list = cloneDeep(store.sbook.articles)
|
||||
getCurrentPractice()
|
||||
loading = false
|
||||
@@ -143,6 +146,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
runtimeStore.disableEventListener = false
|
||||
clearInterval(timer)
|
||||
savePracticeData(true, false)
|
||||
})
|
||||
@@ -234,21 +238,29 @@ function setArticle(val: Article) {
|
||||
})
|
||||
}
|
||||
|
||||
function complete() {
|
||||
async function complete() {
|
||||
clearInterval(timer)
|
||||
setTimeout(() => {
|
||||
localStorage.removeItem(PracticeSaveArticleKey.key)
|
||||
}, 1500)
|
||||
|
||||
//todo 有空了改成实时保存
|
||||
let data: Partial<Statistics> & { title: string, id: string } = {
|
||||
id: articleData.article.id,
|
||||
let data: Partial<Statistics> & { title: string, articleId: number } = {
|
||||
articleId: articleData.article.id,
|
||||
title: articleData.article.title,
|
||||
spend: statStore.spend,
|
||||
startDate: statStore.startDate,
|
||||
total: statStore.total,
|
||||
wrong: statStore.wrong,
|
||||
}
|
||||
|
||||
if (CAN_REQUEST) {
|
||||
let res = await addStat({...data, type: 'article'})
|
||||
if (!res.success) {
|
||||
Toast.error(res.msg)
|
||||
}
|
||||
}
|
||||
|
||||
let reportData = {
|
||||
name: store.sbook.name,
|
||||
index: store.sbook.lastLearnIndex,
|
||||
@@ -271,7 +283,6 @@ function getCurrentPractice() {
|
||||
emitter.emit(EventKey.resetWord)
|
||||
let currentArticle = articleData.list[store.sbook.lastLearnIndex]
|
||||
let article = getDefaultArticle(currentArticle)
|
||||
// console.log('article', article)
|
||||
if (article.sections.length) {
|
||||
setArticle(article)
|
||||
} else {
|
||||
@@ -320,11 +331,18 @@ function nextWord(word: ArticleWord) {
|
||||
}
|
||||
}
|
||||
|
||||
function changeArticle(val: ArticleItem) {
|
||||
async function changeArticle(val: ArticleItem) {
|
||||
let rIndex = articleData.list.findIndex(v => v.id === val.item.id)
|
||||
if (rIndex > -1) {
|
||||
store.sbook.lastLearnIndex = rIndex
|
||||
getCurrentPractice()
|
||||
|
||||
if (CAN_REQUEST) {
|
||||
let res = await setDictProp(null, store.sbook)
|
||||
if (!res.success) {
|
||||
Toast.error(res.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Article } from "@/types/types.ts";
|
||||
import { watch } from "vue";
|
||||
import { LOCAL_FILE_KEY } from "@/utils/const.ts";
|
||||
import { get } from "idb-keyval";
|
||||
import Audio from "@/components/base/Audio.vue";
|
||||
import { LOCAL_FILE_KEY } from "@/config/env.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
article: Article
|
||||
|
||||
@@ -18,10 +18,10 @@ import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import {nanoid} from "nanoid";
|
||||
import {update} from "idb-keyval";
|
||||
import {LOCAL_FILE_KEY} from "@/utils/const.ts";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import { LOCAL_FILE_KEY } from "@/config/env.ts";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Dict, DictId, DictType} from "@/types/types.ts";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import { Dict, DictId, DictType } from "@/types/types.ts";
|
||||
import { cloneDeep } from "@/utils";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import {onMounted, reactive} from "vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
import {Option, Select} from "@/components/base/select";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import { Option, Select } from "@/components/base/select";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import Form from "@/components/base/form/Form.vue";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import { CAN_REQUEST } from "@/config/env.ts";
|
||||
import { addDict } from "@/apis";
|
||||
|
||||
const props = defineProps<{
|
||||
isAdd: boolean,
|
||||
@@ -35,6 +37,7 @@ const DefaultDictForm = {
|
||||
}
|
||||
let dictForm: any = $ref(cloneDeep(DefaultDictForm))
|
||||
const dictFormRef = $ref()
|
||||
let loading = $ref(false)
|
||||
const dictRules = reactive({
|
||||
name: [
|
||||
{required: true, message: '请输入名称', trigger: 'blur'},
|
||||
@@ -43,9 +46,10 @@ const dictRules = reactive({
|
||||
})
|
||||
|
||||
async function onSubmit() {
|
||||
await dictFormRef.validate((valid) => {
|
||||
await dictFormRef.validate(async (valid) => {
|
||||
if (valid) {
|
||||
let data: Dict = getDefaultDict(dictForm)
|
||||
data.type = props.isBook ? DictType.article : DictType.word
|
||||
let source = [store.article, store.word][props.isBook ? 0 : 1]
|
||||
//todo 可以检查的更准确些,比如json对比
|
||||
if (props.isAdd) {
|
||||
@@ -54,6 +58,16 @@ async function onSubmit() {
|
||||
Toast.warning('已有相同名称!')
|
||||
return
|
||||
} else {
|
||||
if (CAN_REQUEST) {
|
||||
loading = true
|
||||
let res = await addDict(null, data)
|
||||
loading = false
|
||||
if (res.success) {
|
||||
data = getDefaultDict(res.data)
|
||||
} else {
|
||||
return Toast.error(res.msg)
|
||||
}
|
||||
}
|
||||
source.bookList.push(cloneDeep(data))
|
||||
runtimeStore.editDict = data
|
||||
emit('submit')
|
||||
@@ -62,7 +76,7 @@ async function onSubmit() {
|
||||
} else {
|
||||
let rIndex = source.bookList.findIndex(v => v.id === data.id)
|
||||
//任意修改,都将其变为自定义词典
|
||||
if (!data.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect, DictId.articleCollect].includes(data.id)) {
|
||||
if (!data.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect, DictId.articleCollect].includes(data.en_name || data.id)) {
|
||||
data.custom = true
|
||||
data.id += '_custom'
|
||||
}
|
||||
@@ -104,7 +118,7 @@ onMounted(() => {
|
||||
<FormItem label="描述">
|
||||
<BaseInput v-model="dictForm.description" textarea/>
|
||||
</FormItem>
|
||||
<FormItem label="原文语言">
|
||||
<FormItem label="原文语言" v-if="false">
|
||||
<Select v-model="dictForm.language" placeholder="请选择选项">
|
||||
<Option label="英语" value="en"/>
|
||||
<Option label="德语" value="de"/>
|
||||
@@ -112,7 +126,7 @@ onMounted(() => {
|
||||
<Option label="代码" value="code"/>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="译文语言">
|
||||
<FormItem label="译文语言" v-if="false">
|
||||
<Select v-model="dictForm.translateLanguage" placeholder="请选择选项">
|
||||
<Option label="中文" value="zh-CN"/>
|
||||
<Option label="英语" value="en"/>
|
||||
@@ -122,7 +136,7 @@ onMounted(() => {
|
||||
</FormItem>
|
||||
<div class="center">
|
||||
<base-button type="info" @click="emit('close')">关闭</base-button>
|
||||
<base-button type="primary" @click="onSubmit">确定</base-button>
|
||||
<base-button type="primary" :loading="loading" @click="onSubmit">确定</base-button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -18,8 +18,8 @@ import Space from "@/pages/article/components/Space.vue";
|
||||
import { useWordOptions } from "@/hooks/dict.ts";
|
||||
import nlp from "compromise/three";
|
||||
import { nanoid } from "nanoid";
|
||||
import { PracticeSaveArticleKey } from "@/utils/const.ts";
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import { PracticeSaveArticleKey } from "@/config/env.ts";
|
||||
|
||||
interface IProps {
|
||||
article: Article,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {GITHUB, ProjectName} from "@/config/ENV.ts";
|
||||
import {GITHUB, ProjectName} from "@/config/env.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
|
||||
@@ -12,7 +12,7 @@ import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const router = useRouter()
|
||||
const {toggleTheme} = useTheme()
|
||||
const {toggleTheme,getTheme} = useTheme()
|
||||
|
||||
|
||||
</script>
|
||||
@@ -42,6 +42,10 @@ const {toggleTheme} = useTheme()
|
||||
<span v-if="settingStore.sideExpand">设置</span>
|
||||
<div class="red-point" :class="!settingStore.sideExpand && 'top-1 right-0'" v-if="runtimeStore.isNew"></div>
|
||||
</div>
|
||||
<!-- <div class="row" @click="router.push('/user')">-->
|
||||
<!-- <IconFluentPerson20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div class="bottom flex justify-evenly ">
|
||||
<BaseIcon
|
||||
@@ -54,7 +58,7 @@ const {toggleTheme} = useTheme()
|
||||
:title="`切换主题(${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<IconFluentWeatherMoon16Regular v-if="settingStore.theme === 'light'"/>
|
||||
<IconFluentWeatherMoon16Regular v-if="getTheme() === 'light'"/>
|
||||
<IconFluentWeatherSunny16Regular v-else/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
|
||||
@@ -4,22 +4,19 @@ import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { getAudioFileUrl, usePlayAudio } from "@/hooks/sound.ts";
|
||||
import { getShortcutKey, useEventListener } from "@/hooks/event.ts";
|
||||
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, shakeCommonDict } from "@/utils";
|
||||
import { DefaultShortcutKeyMap, ShortcutKey } from "@/types/types.ts";
|
||||
import {DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode} from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {
|
||||
APP_NAME,
|
||||
APP_VERSION,
|
||||
EXPORT_DATA_KEY,
|
||||
LOCAL_FILE_KEY, PracticeSaveArticleKey,
|
||||
PracticeSaveWordKey,
|
||||
SAVE_DICT_KEY,
|
||||
SAVE_SETTING_KEY,
|
||||
SoundFileOptions
|
||||
} from "@/utils/const.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { saveAs } from "file-saver";
|
||||
import { Origin } from "@/config/ENV.ts";
|
||||
import {
|
||||
APP_NAME, APP_VERSION,
|
||||
EXPORT_DATA_KEY,
|
||||
LOCAL_FILE_KEY,
|
||||
Origin,
|
||||
PracticeSaveArticleKey,
|
||||
PracticeSaveWordKey, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions
|
||||
} from "@/config/env.ts";
|
||||
import dayjs from "dayjs";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
@@ -154,7 +151,9 @@ function getShortcutKeyName(key: string): string {
|
||||
'ToggleConciseMode': '切换简洁模式',
|
||||
'TogglePanel': '切换面板',
|
||||
'RandomWrite': '随机默写',
|
||||
'NextRandomWrite': '继续随机默写'
|
||||
'NextRandomWrite': '继续随机默写',
|
||||
'KnowWord': '认识单词',
|
||||
'UnknownWord': '不认识单词',
|
||||
}
|
||||
|
||||
return shortcutKeyNameMap[key] || key
|
||||
@@ -490,8 +489,8 @@ function importOldData() {
|
||||
<div v-if="tabIndex === 1">
|
||||
<SettingItem title="练习模式">
|
||||
<RadioGroup v-model="settingStore.wordPracticeMode" class="flex-col gap-0!">
|
||||
<Radio :value="0" label="智能模式,系统自动计算复习单词与默写单词"/>
|
||||
<Radio :value="1" label="自由模式,系统不强制复习与默写"/>
|
||||
<Radio :value="WordPracticeMode.System" label="智能模式,系统自动计算复习单词与默写单词"/>
|
||||
<Radio :value="WordPracticeMode.Free" label="自由模式,系统不强制复习与默写"/>
|
||||
</RadioGroup>
|
||||
</SettingItem>
|
||||
|
||||
@@ -555,7 +554,7 @@ function importOldData() {
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="自动切换"/>
|
||||
<SettingItem title="自动切换下一个单词"
|
||||
desc="未开启自动切换时,当输入完成后请使用空格键切换下一个"
|
||||
desc="未开启自动切换时,当输入完成后请使用 **空格键** 切换下一个"
|
||||
>
|
||||
<Switch v-model="settingStore.autoNextWord"/>
|
||||
</SettingItem>
|
||||
@@ -683,8 +682,61 @@ function importOldData() {
|
||||
<div class="item p-2">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<span>2025/9/14:</span>
|
||||
<span>完善文章编辑、导入、导出等功能</span>
|
||||
<div>更新日期:2025/10/26</div>
|
||||
<div>更新内容:进一步完善单词练习,解决复习数量太多的问题</div>
|
||||
</div>
|
||||
<div class="text-base mt-1">
|
||||
<ol>
|
||||
<li>
|
||||
<div class="title"><b>智能模式优化</b></div>
|
||||
<div class="desc">练习时新增四种练习模式:学习、复习、听写、默写。</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title"><b>学习模式</b></div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>仅在练习新词时出现。</li>
|
||||
<li>采用「跟写 / 拼写」方式进行学习。</li>
|
||||
<li>每 7 个单词会 <b>强制进行听写</b>,解决原来“一次练太多,听写时已忘记”的问题。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title"><b>复习模式(新增)</b></div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>仅在复习已学单词时出现。</li>
|
||||
<li>不再强制拼写,提供「我认识」与「不认识」选项。</li>
|
||||
<li>选择「我认识」后,该单词在后续听写或默写中将不再出现,<b>显著减少复习数量</b>。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title"><b>听写模式</b></div>
|
||||
<div class="desc">原有逻辑保持不变。</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title"><b>默写模式(新增)</b></div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>仅显示释义,不自动发音,不显示单词长度。</li>
|
||||
<li>适合强化拼写记忆的场景。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<b>说明:</b>
|
||||
<div>本次更新重点解决了“复习单词数量过多、效率偏低”的问题。</div>
|
||||
<div>通过引入「复习」与「默写」两种模式,使复习流程更加灵活、高效。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<div class="item p-2">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>更新日期:2025/9/14</div>
|
||||
<div>更新内容:完善文章编辑、导入、导出等功能</div>
|
||||
</div>
|
||||
<div class="text-base mt-1">
|
||||
<div>1、文章的音频管理功能,目前已可添加音频、设置句子与音频的对应位置</div>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// import origin from './data.json'
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {checkAndUpgradeSaveDict} from "@/utils";
|
||||
import {SAVE_DICT_KEY} from "@/utils/const.ts";
|
||||
import str from './data.json'
|
||||
import {get} from 'idb-keyval'
|
||||
import { SAVE_DICT_KEY } from "@/config/env.ts";
|
||||
|
||||
let data = {}
|
||||
let origin = {}
|
||||
|
||||
29
src/pages/user/index.vue
Normal file
29
src/pages/user/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { onMounted } from "vue";
|
||||
import { IS_LOGIN } from "@/config/env.ts";
|
||||
import router from "@/router.ts";
|
||||
|
||||
onMounted(() => {
|
||||
if (!IS_LOGIN) {
|
||||
}
|
||||
router.push({path: "/login"});
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-between min-h-screen">
|
||||
<div class="center flex-col gap-8">
|
||||
onMounted(() => {
|
||||
if (!IS_LOGIN) {
|
||||
router.push({path: "/login"});
|
||||
}
|
||||
})
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
|
||||
66
src/pages/user/login.vue
Normal file
66
src/pages/user/login.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { APP_NAME } from "@/config/env.ts";
|
||||
import { uploadImportData } from "@/apis";
|
||||
|
||||
function sync() {
|
||||
|
||||
}
|
||||
|
||||
async function handleAudioChange(e) {
|
||||
let uploadFile = e.target?.files?.[0]
|
||||
if (!uploadFile) return
|
||||
let data = new FormData();
|
||||
data.append("file", uploadFile);
|
||||
let res = await uploadImportData(data, e => {
|
||||
console.log('e', e)
|
||||
})
|
||||
console.log('res', res)
|
||||
console.log(uploadFile)
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
async function s() {
|
||||
const taskId = await fetch('/startImport').then(r => r.json()).then(d => d.taskId);
|
||||
|
||||
const timer = setInterval(async () => {
|
||||
const res = await fetch(`/getProgress/${taskId}`).then(r => r.json());
|
||||
console.log(`当前进度: ${res.progress}%`);
|
||||
if (res.progress >= 100) clearInterval(timer);
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center h-screen">
|
||||
<div class=" flex flex-col gap-6 w-100">
|
||||
<h1 class="mb-0 text-align-center">{{ APP_NAME }}</h1>
|
||||
<div class="flex center">
|
||||
<span class="shrink-0">账户:</span>
|
||||
<BaseInput type="text"/>
|
||||
</div>
|
||||
<div class="flex center">
|
||||
<span class="shrink-0">密码:</span>
|
||||
<BaseInput type="password"/>
|
||||
</div>
|
||||
<BaseButton class="w-full">登录</BaseButton>
|
||||
<BaseButton class="w-full" @click="sync">同步</BaseButton>
|
||||
<div class="upload relative">
|
||||
<BaseButton>上传</BaseButton>
|
||||
<input type="file"
|
||||
accept=".zip,.json"
|
||||
@change="handleAudioChange"
|
||||
class="w-full h-full absolute left-0 top-0 opacity-0"/>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-end gap-4">
|
||||
<div>注册</div>
|
||||
<div>忘记密码</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -26,8 +26,8 @@ import { getCurrentStudyWord } from "@/hooks/dict.ts";
|
||||
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { MessageBox } from "@/utils/MessageBox.tsx";
|
||||
import { Origin } from "@/config/ENV.ts";
|
||||
import { PracticeSaveWordKey } from "@/utils/const.ts";
|
||||
import { CAN_REQUEST, Origin, PracticeSaveWordKey } from "@/config/env.ts";
|
||||
import { detail } from "@/apis";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
@@ -178,7 +178,7 @@ const showBookDetail = computed(() => {
|
||||
return !(isAdd || isEdit);
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
if (route.query?.isAdd) {
|
||||
isAdd = true
|
||||
runtimeStore.editDict = getDefaultDict()
|
||||
@@ -188,14 +188,25 @@ onMounted(() => {
|
||||
} else {
|
||||
if (!runtimeStore.editDict.words.length
|
||||
&& !runtimeStore.editDict.custom
|
||||
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.id)
|
||||
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
|
||||
) {
|
||||
loading = true
|
||||
_getDictDataByUrl(runtimeStore.editDict).then(r => {
|
||||
loading = false
|
||||
runtimeStore.editDict = r
|
||||
})
|
||||
let r = await _getDictDataByUrl(runtimeStore.editDict)
|
||||
runtimeStore.editDict = r
|
||||
}
|
||||
|
||||
if (base.word.bookList.find(book => book.id === runtimeStore.editDict.id)) {
|
||||
if (CAN_REQUEST) {
|
||||
let res = await detail({id: runtimeStore.editDict.id})
|
||||
if (res.success) {
|
||||
runtimeStore.editDict.statistics = res.data.statistics
|
||||
if (res.data.words.length) {
|
||||
runtimeStore.editDict.words = res.data.words
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -212,32 +223,27 @@ const settingStore = useSettingStore()
|
||||
const {nav} = useNav()
|
||||
|
||||
//todo 可以和首页合并
|
||||
function startPractice() {
|
||||
if (store.sdict.id) {
|
||||
if (!store.sdict.words.length) {
|
||||
return Toast.warning('没有单词可学习!')
|
||||
}
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
window.umami?.track('startStudyWord', {
|
||||
name: store.sdict.name,
|
||||
index: store.sdict.lastLearnIndex,
|
||||
perDayStudyNumber: store.sdict.perDayStudyNumber,
|
||||
custom: store.sdict.custom,
|
||||
complete: store.sdict.complete,
|
||||
wordPracticeMode: settingStore.wordPracticeMode
|
||||
})
|
||||
let currentStudy = getCurrentStudyWord()
|
||||
nav('practice-words/' + store.sdict.id, {}, currentStudy)
|
||||
} else {
|
||||
window.umami?.track('no-dict')
|
||||
Toast.warning('请先选择一本词典')
|
||||
}
|
||||
}
|
||||
|
||||
async function addMyStudyList() {
|
||||
async function startPractice() {
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
studyLoading = true
|
||||
await base.changeDict(runtimeStore.editDict)
|
||||
studyLoading = false
|
||||
window.umami?.track('startStudyWord', {
|
||||
name: store.sdict.name,
|
||||
index: store.sdict.lastLearnIndex,
|
||||
perDayStudyNumber: store.sdict.perDayStudyNumber,
|
||||
custom: store.sdict.custom,
|
||||
complete: store.sdict.complete,
|
||||
wordPracticeMode: settingStore.wordPracticeMode
|
||||
})
|
||||
let currentStudy = getCurrentStudyWord()
|
||||
nav('practice-words/' + store.sdict.id, {}, currentStudy)
|
||||
}
|
||||
|
||||
async function addMyStudyList() {
|
||||
if (!runtimeStore.editDict.words.length) {
|
||||
return Toast.warning('没有单词可学习!')
|
||||
}
|
||||
if (!settingStore.disableShowPracticeSettingDialog) {
|
||||
showPracticeSettingDialog = true
|
||||
return
|
||||
@@ -245,7 +251,6 @@ async function addMyStudyList() {
|
||||
startPractice()
|
||||
}
|
||||
|
||||
|
||||
let exportLoading = $ref(false)
|
||||
let importLoading = $ref(false)
|
||||
let tableRef = ref()
|
||||
@@ -363,7 +368,7 @@ async function exportData() {
|
||||
}
|
||||
|
||||
function searchWord() {
|
||||
console.log('wordForm.word',wordForm.word)
|
||||
console.log('wordForm.word', wordForm.word)
|
||||
}
|
||||
|
||||
defineRender(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { groupBy, useNav } from "@/utils";
|
||||
import { groupBy, resourceWrap, useNav } from "@/utils";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { DictResource } from "@/types/types.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
@@ -13,7 +13,8 @@ import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed } from "vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import dict_list from "@/assets/dict-list.json";
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { DICT_LIST } from "@/config/env.ts";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
|
||||
const {nav} = useNav()
|
||||
@@ -45,13 +46,17 @@ function groupByDictTags(dictList: DictResource[]) {
|
||||
}, {})
|
||||
}
|
||||
|
||||
const {data: dict_list, isFetching} = useFetch(resourceWrap(DICT_LIST.WORD.ALL)).json()
|
||||
|
||||
const groupedByCategoryAndTag = $computed(() => {
|
||||
const groupByCategory = groupBy(dict_list.flat(), 'category')
|
||||
let data = []
|
||||
if (!dict_list.value) return data
|
||||
const groupByCategory = groupBy(dict_list.value, 'category')
|
||||
for (const [key, value] of Object.entries(groupByCategory)) {
|
||||
data.push([key, groupByDictTags(value)])
|
||||
}
|
||||
[data[2], data[3]] = [data[3], data[2]];
|
||||
console.log('data',data)
|
||||
return data
|
||||
})
|
||||
|
||||
@@ -61,7 +66,7 @@ let searchKey = $ref('')
|
||||
const searchList = computed<any[]>(() => {
|
||||
if (searchKey) {
|
||||
let s = searchKey.toLowerCase()
|
||||
return dict_list.flat().filter((item) => {
|
||||
return dict_list.value.filter((item) => {
|
||||
return item.id.toLowerCase().includes(s)
|
||||
|| item.name.toLowerCase().includes(s)
|
||||
|| item.category.toLowerCase().includes(s)
|
||||
@@ -76,7 +81,7 @@ const searchList = computed<any[]>(() => {
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="card">
|
||||
<div class="card min-h-200" v-loading="isFetching">
|
||||
<div class="flex items-center relative gap-2">
|
||||
<BackIcon class="z-2" @click='router.back'/>
|
||||
<div class="flex flex-1 gap-4" v-if="showSearchInput">
|
||||
@@ -85,9 +90,11 @@ const searchList = computed<any[]>(() => {
|
||||
</div>
|
||||
<div class="py-1 flex flex-1 justify-end" v-else>
|
||||
<span class="page-title absolute w-full center">词典列表</span>
|
||||
<BaseIcon @click="showSearchInput = true"
|
||||
class="z-1"
|
||||
icon="fluent:search-24-regular">
|
||||
<BaseIcon
|
||||
title="搜索"
|
||||
@click="showSearchInput = true"
|
||||
class="z-1"
|
||||
icon="fluent:search-24-regular">
|
||||
<IconFluentSearch24Regular/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { onMounted, provide, watch } from "vue";
|
||||
import {onMounted, provide, ref, watch} from "vue";
|
||||
|
||||
import Statistics from "@/pages/word/Statistics.vue";
|
||||
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { Dict, PracticeData, ShortcutKey, TaskWords, Word } from "@/types/types.ts";
|
||||
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {Dict, PracticeData, WordPracticeType, ShortcutKey, TaskWords, Word, WordPracticeMode} from "@/types/types.ts";
|
||||
import {useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import { getCurrentStudyWord, useWordOptions } from "@/hooks/dict.ts";
|
||||
import { _getDictDataByUrl, cloneDeep, shuffle } from "@/utils";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {getCurrentStudyWord, useWordOptions} from "@/hooks/dict.ts";
|
||||
import {_getDictDataByUrl, cloneDeep, resourceWrap, shuffle} from "@/utils";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import Footer from "@/pages/word/components/Footer.vue";
|
||||
import Panel from "@/components/Panel.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
@@ -19,14 +19,15 @@ import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import WordList from "@/components/list/WordList.vue";
|
||||
import TypeWord from "@/pages/word/components/TypeWord.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
|
||||
import {getDefaultDict, getDefaultWord} from "@/types/func.ts";
|
||||
import ConflictNotice from "@/components/ConflictNotice.vue";
|
||||
import dict_list from "@/assets/dict-list.json";
|
||||
import PracticeLayout from "@/components/PracticeLayout.vue";
|
||||
import { PracticeSaveWordKey } from "@/utils/const.ts";
|
||||
|
||||
import {DICT_LIST, PracticeSaveWordKey} from "@/config/env.ts";
|
||||
import {ToastInstance} from "@/components/base/toast/type.ts";
|
||||
|
||||
const {
|
||||
isWordCollect,
|
||||
@@ -48,14 +49,21 @@ let loading = $ref(false)
|
||||
let taskWords = $ref<TaskWords>({
|
||||
new: [],
|
||||
review: [],
|
||||
write: []
|
||||
write: [],
|
||||
})
|
||||
|
||||
let data = $ref<PracticeData>({
|
||||
index: 0,
|
||||
words: [],
|
||||
wrongWords: [],
|
||||
excludeWords: [],
|
||||
})
|
||||
let isTypingWrongWord = ref(false)
|
||||
|
||||
let practiceMode = ref(WordPracticeType.FollowWrite)
|
||||
provide('isTypingWrongWord', isTypingWrongWord)
|
||||
provide('practiceData', data)
|
||||
provide('practiceMode', practiceMode)
|
||||
|
||||
async function loadDict() {
|
||||
// console.log('load好了开始加载')
|
||||
@@ -64,6 +72,8 @@ async function loadDict() {
|
||||
if (dictId) {
|
||||
//先在自己的词典列表里面找,如果没有再在资源列表里面找
|
||||
dict = store.word.bookList.find(v => v.id === dictId)
|
||||
let r = await fetch(resourceWrap(DICT_LIST.WORD.ALL))
|
||||
let dict_list = await r.json()
|
||||
if (!dict) dict = dict_list.flat().find(v => v.id === dictId) as Dict
|
||||
if (dict && dict.id) {
|
||||
//如果是不是自定义词典,就请求数据
|
||||
@@ -87,7 +97,6 @@ watch(() => store.load, (n) => {
|
||||
if (n && loading) loadDict()
|
||||
}, {immediate: true})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
//如果是从单词学习主页过来的,就直接使用;否则等待加载
|
||||
if (runtimeStore.routeData) {
|
||||
@@ -118,26 +127,27 @@ function initData(initVal: TaskWords, init: boolean = false) {
|
||||
taskWords = initVal
|
||||
if (taskWords.new.length === 0) {
|
||||
if (taskWords.review.length) {
|
||||
settingStore.dictation = false
|
||||
statStore.step = 2
|
||||
settingStore.wordPracticeType = WordPracticeType.Identify
|
||||
statStore.step = 3
|
||||
data.words = taskWords.review
|
||||
} else {
|
||||
if (taskWords.write.length) {
|
||||
settingStore.dictation = true
|
||||
settingStore.wordPracticeType = WordPracticeType.Identify
|
||||
data.words = taskWords.write
|
||||
statStore.step = 4
|
||||
statStore.step = 6
|
||||
} else {
|
||||
Toast.warning('没有可学习的单词!')
|
||||
router.push('/word')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
settingStore.dictation = false
|
||||
settingStore.wordPracticeType = WordPracticeType.FollowWrite
|
||||
data.words = taskWords.new
|
||||
statStore.step = 0
|
||||
}
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
data.excludeWords = []
|
||||
allWrongWords.clear()
|
||||
statStore.startDate = Date.now()
|
||||
statStore.inputWordNumber = 0
|
||||
@@ -147,12 +157,11 @@ function initData(initVal: TaskWords, init: boolean = false) {
|
||||
statStore.reviewWordNumber = taskWords.review.length
|
||||
statStore.writeWordNumber = taskWords.write.length
|
||||
statStore.index = 0
|
||||
isTypingWrongWord.value = false
|
||||
}
|
||||
}
|
||||
|
||||
provide('practiceData', data)
|
||||
|
||||
const word = $computed(() => {
|
||||
const word = $computed<Word>(() => {
|
||||
return data.words[data.index] ?? getDefaultWord()
|
||||
})
|
||||
const prevWord: Word = $computed(() => {
|
||||
@@ -162,94 +171,179 @@ const nextWord: Word = $computed(() => {
|
||||
return data.words?.[data.index + 1] ?? undefined
|
||||
})
|
||||
|
||||
function next(isTyping: boolean = true) {
|
||||
// showStatDialog = true
|
||||
// return
|
||||
if (data.index === data.words.length - 1) {
|
||||
if (data.wrongWords.length) {
|
||||
console.log('当前学完了,但还有错词')
|
||||
data.words = shuffle(cloneDeep(data.wrongWords))
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
watch(() => settingStore.wordPracticeType, (n) => {
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.Free) return
|
||||
switch (n) {
|
||||
case WordPracticeType.Spell:
|
||||
case WordPracticeType.Dictation:
|
||||
settingStore.dictation = true;
|
||||
settingStore.translate = true;
|
||||
break
|
||||
case WordPracticeType.Listen:
|
||||
settingStore.dictation = true;
|
||||
settingStore.translate = false;
|
||||
break
|
||||
case WordPracticeType.FollowWrite:
|
||||
settingStore.dictation = false;
|
||||
settingStore.translate = true;
|
||||
break
|
||||
case WordPracticeType.Identify:
|
||||
settingStore.dictation = false;
|
||||
settingStore.translate = false;
|
||||
break
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
function wordLoop() {
|
||||
// return data.index++
|
||||
let d = Math.floor(data.index / 6) - 1
|
||||
if (data.index > 0 && data.index % 6 === (d < 0 ? 0 : d)) {
|
||||
if (settingStore.wordPracticeType === WordPracticeType.FollowWrite) {
|
||||
settingStore.wordPracticeType = WordPracticeType.Spell
|
||||
data.index -= 6
|
||||
} else {
|
||||
console.log('当前学完了,没错词', statStore.total, statStore.step, data.index)
|
||||
if (isTyping) statStore.inputWordNumber++
|
||||
|
||||
//学完了
|
||||
if (statStore.step === 4) {
|
||||
statStore.spend = Date.now() - statStore.startDate
|
||||
console.log('全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
return;
|
||||
// emit('complete', {})
|
||||
}
|
||||
|
||||
//开始默认所有单词
|
||||
if (statStore.step === 3) {
|
||||
statStore.step++
|
||||
if (taskWords.write.length) {
|
||||
console.log('开始默认所有单词')
|
||||
settingStore.dictation = true
|
||||
data.words = shuffle(taskWords.write)
|
||||
data.index = 0
|
||||
} else {
|
||||
console.log('开始默认所有单词-无单词略过')
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
//开始默写昨日
|
||||
if (statStore.step === 2) {
|
||||
statStore.step++
|
||||
if (taskWords.review.length) {
|
||||
console.log('开始默写昨日')
|
||||
settingStore.dictation = true
|
||||
data.words = shuffle(taskWords.review)
|
||||
data.index = 0
|
||||
} else {
|
||||
console.log('开始默写昨日-无单词略过')
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
//开始复习昨日
|
||||
if (statStore.step === 1) {
|
||||
statStore.step++
|
||||
if (taskWords.review.length) {
|
||||
console.log('开始复习昨日')
|
||||
settingStore.dictation = false
|
||||
data.words = shuffle(taskWords.review)
|
||||
data.index = 0
|
||||
} else {
|
||||
console.log('开始复习昨日-无单词略过')
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
//开始默写新词
|
||||
if (statStore.step === 0) {
|
||||
if (settingStore.wordPracticeMode === 1) {
|
||||
console.log('自由模式,全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
return
|
||||
}
|
||||
statStore.step++
|
||||
console.log('开始默写新词')
|
||||
settingStore.dictation = true
|
||||
data.words = shuffle(taskWords.new)
|
||||
data.index = 0
|
||||
}
|
||||
settingStore.wordPracticeType = WordPracticeType.FollowWrite
|
||||
data.index++
|
||||
}
|
||||
} else {
|
||||
data.index++
|
||||
isTyping && statStore.inputWordNumber++
|
||||
// console.log('这个词完了')
|
||||
}
|
||||
}
|
||||
|
||||
function goNextStep(originList, mode, msg) {
|
||||
//每次都判断,因为每次都可能新增已掌握的单词
|
||||
let list = originList.filter(v => (!data.excludeWords.includes(v.word)))
|
||||
console.log(msg)
|
||||
if (list.length) {
|
||||
if (toastInstance) toastInstance.close()
|
||||
toastInstance = Toast.info('输入完成后按空格键切换下一个', {duration: 5000})
|
||||
data.words = list
|
||||
settingStore.wordPracticeType = mode
|
||||
data.index = 0
|
||||
statStore.step++
|
||||
} else {
|
||||
console.log(msg + ':无单词略过')
|
||||
statStore.step += 3
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
let toastInstance: ToastInstance = null
|
||||
|
||||
async function next(isTyping: boolean = true) {
|
||||
if (isTyping) {
|
||||
statStore.inputWordNumber++
|
||||
}
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
|
||||
if (data.index === data.words.length - 1) {
|
||||
console.log('自由模式,全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
} else {
|
||||
data.index++
|
||||
}
|
||||
} else {
|
||||
if (data.index === data.words.length - 1) {
|
||||
if (statStore.step === 0 || isTypingWrongWord.value) {
|
||||
if (settingStore.wordPracticeType !== WordPracticeType.Spell) {
|
||||
let i = data.index
|
||||
i--
|
||||
let d = Math.floor(i / 6) - 1
|
||||
while (i % 6 !== (d < 0 ? 0 : d)) {
|
||||
i--
|
||||
d = Math.floor(i / 6) - 1
|
||||
}
|
||||
console.log('i', i)
|
||||
if (i <= 0) i = -1
|
||||
if (i + 1 == data.index) {
|
||||
data.index = 0
|
||||
}
|
||||
data.index = i + 1
|
||||
emitter.emit(EventKey.resetWord)
|
||||
settingStore.wordPracticeType = WordPracticeType.Spell
|
||||
return
|
||||
}
|
||||
}
|
||||
data.wrongWords = data.wrongWords.filter(v => (!data.excludeWords.includes(v.word)))
|
||||
if (data.wrongWords.length) {
|
||||
isTypingWrongWord.value = true
|
||||
settingStore.wordPracticeType = WordPracticeType.FollowWrite
|
||||
console.log('当前学完了,但还有错词')
|
||||
data.words = shuffle(cloneDeep(data.wrongWords))
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
} else {
|
||||
isTypingWrongWord.value = false
|
||||
console.log('当前学完了,没错词', statStore.total, statStore.step, data.index)
|
||||
//学完了,这里第 7 步如果无单词,加 3 就是 9 了
|
||||
if (statStore.step >= 8) {
|
||||
statStore.spend = Date.now() - statStore.startDate
|
||||
console.log('全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
return;
|
||||
}
|
||||
|
||||
//开始默写之前
|
||||
if (statStore.step === 7) {
|
||||
return goNextStep(shuffle(taskWords.write), WordPracticeType.Dictation, '开始默写之前')
|
||||
}
|
||||
|
||||
//开始听写之前
|
||||
if (statStore.step === 6) {
|
||||
return goNextStep(shuffle(taskWords.write), WordPracticeType.Listen, '开始听写之前')
|
||||
}
|
||||
|
||||
//开始复写之前
|
||||
if (statStore.step === 5) {
|
||||
return goNextStep(taskWords.write, WordPracticeType.Identify, '开始复写之前')
|
||||
}
|
||||
|
||||
//开始默写上次
|
||||
if (statStore.step === 4) {
|
||||
return goNextStep(shuffle(taskWords.review), WordPracticeType.Dictation, '开始默写上次')
|
||||
}
|
||||
|
||||
//开始听写上次
|
||||
if (statStore.step === 3) {
|
||||
return goNextStep(shuffle(taskWords.review), WordPracticeType.Listen, '开始听写上次')
|
||||
}
|
||||
|
||||
//开始复写昨日
|
||||
if (statStore.step === 2) {
|
||||
return goNextStep(taskWords.review, WordPracticeType.Identify, '开始复写昨日')
|
||||
}
|
||||
|
||||
//开始默写新词
|
||||
if (statStore.step === 1) {
|
||||
return goNextStep(shuffle(taskWords.new), WordPracticeType.Dictation, '开始默写新词')
|
||||
}
|
||||
|
||||
//开始听写新词
|
||||
if (statStore.step === 0) {
|
||||
return goNextStep(shuffle(taskWords.new), WordPracticeType.Listen, '开始听写新词')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (statStore.step === 0) {
|
||||
wordLoop()
|
||||
} else {
|
||||
if (isTypingWrongWord.value) wordLoop()
|
||||
else data.index++
|
||||
}
|
||||
}
|
||||
}
|
||||
savePracticeData()
|
||||
}
|
||||
|
||||
function onWordKnow() {
|
||||
//标记模式时,用户认识的单词加入到排除里面,后续不再复习
|
||||
let rIndex = data.excludeWords.findIndex(v => v === word.word)
|
||||
if (rIndex < 0) {
|
||||
data.excludeWords.push(word.word)
|
||||
}
|
||||
}
|
||||
|
||||
function onTypeWrong() {
|
||||
let temp = word.word.toLowerCase()
|
||||
if (!allWrongWords.has(word.word.toLowerCase())) {
|
||||
@@ -297,7 +391,7 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
|
||||
|
||||
function repeat() {
|
||||
console.log('重学一遍')
|
||||
if (settingStore.wordPracticeMode === 0) settingStore.dictation = false
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
|
||||
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
|
||||
//如果是刚刚完成,那么学习进度要从length减回去,因为lastLearnIndex为0了,同时改complete为false
|
||||
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
|
||||
@@ -329,6 +423,7 @@ function skip(e: KeyboardEvent) {
|
||||
}
|
||||
|
||||
function show(e: KeyboardEvent) {
|
||||
if (![WordPracticeType.FollowWrite].includes(settingStore.wordPracticeType)) onTypeWrong()
|
||||
typingRef.showWord()
|
||||
}
|
||||
|
||||
@@ -342,12 +437,16 @@ function play() {
|
||||
|
||||
function toggleWordSimpleWrapper() {
|
||||
if (!isWordSimple(word)) {
|
||||
toggleWordSimple(word)
|
||||
//延迟一下,不知道为什么不延迟会导致当前条目不自动定位到列表中间
|
||||
setTimeout(() => next(false))
|
||||
} else {
|
||||
toggleWordSimple(word)
|
||||
}
|
||||
let rIndex = data.excludeWords.findIndex(v => v === word.word)
|
||||
if (rIndex > -1) {
|
||||
data.excludeWords.splice(rIndex, 1)
|
||||
} else {
|
||||
data.excludeWords.push(word.word)
|
||||
}
|
||||
toggleWordSimple(word)
|
||||
}
|
||||
|
||||
function toggleTranslate() {
|
||||
@@ -368,7 +467,7 @@ function togglePanel() {
|
||||
}
|
||||
|
||||
function continueStudy() {
|
||||
if (settingStore.wordPracticeMode === 0) settingStore.dictation = false
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
|
||||
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
|
||||
if (!showStatDialog) {
|
||||
console.log('没学完,强行跳过')
|
||||
@@ -386,6 +485,7 @@ function randomWrite() {
|
||||
data.index = 0
|
||||
settingStore.dictation = true
|
||||
}
|
||||
|
||||
function nextRandomWrite() {
|
||||
console.log('继续随机默写')
|
||||
initData(getCurrentStudyWord())
|
||||
@@ -454,6 +554,7 @@ useEvents([
|
||||
:word="word"
|
||||
@wrong="onTypeWrong"
|
||||
@complete="next"
|
||||
@know="onWordKnow"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -89,8 +89,8 @@ useEvents([
|
||||
])
|
||||
|
||||
function options(emitType: string) {
|
||||
close()
|
||||
emitter.emit(EventKey[emitType])
|
||||
close()
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -111,11 +111,11 @@ function options(emitType: string) {
|
||||
<div class="text-4xl font-bold">{{ statStore.newWordNumber }}</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">复习数</div>
|
||||
<div class="text-sm color-gray">复习上次</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.reviewWordNumber }}</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">默写数</div>
|
||||
<div class="text-sm color-gray">复习之前</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.writeWordNumber }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import { _getAccomplishDate, _getDictDataByUrl, useNav } from "@/utils";
|
||||
import { _getAccomplishDate, _getDictDataByUrl, resourceWrap, useNav } from "@/utils";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { DictResource } from "@/types/types.ts";
|
||||
import {DictResource, WordPracticeMode} from "@/types/types.ts";
|
||||
import { watch } from "vue";
|
||||
import { getCurrentStudyWord } from "@/hooks/dict.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
@@ -18,9 +18,11 @@ import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
|
||||
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import recommendDictList from "@/assets/recommend-dict-list.json";
|
||||
import CollectNotice from "@/components/CollectNotice.vue";
|
||||
import { PracticeSaveWordKey } from "@/utils/const.ts";
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { CAN_REQUEST, DICT_LIST, PracticeSaveWordKey } from "@/config/env.ts";
|
||||
import { myDictList } from "@/apis";
|
||||
import PracticeWordListDialog from "@/pages/word/components/PracticeWordListDialog.vue";
|
||||
|
||||
|
||||
const store = useBaseStore()
|
||||
@@ -41,6 +43,12 @@ watch(() => store.load, n => {
|
||||
}, {immediate: true})
|
||||
|
||||
async function init() {
|
||||
if (CAN_REQUEST) {
|
||||
let res = await myDictList({type: "word"})
|
||||
if (res.success) {
|
||||
store.setState(Object.assign(store.$state, res.data))
|
||||
}
|
||||
}
|
||||
if (store.word.studyIndex >= 3) {
|
||||
if (!store.sdict.custom && !store.sdict.words.length) {
|
||||
store.word.bookList[store.word.studyIndex] = await _getDictDataByUrl(store.sdict)
|
||||
@@ -86,6 +94,7 @@ function startPractice() {
|
||||
|
||||
let showPracticeSettingDialog = $ref(false)
|
||||
let showChangeLastPracticeIndexDialog = $ref(false)
|
||||
let showPracticeWordListDialog = $ref(false)
|
||||
|
||||
async function goDictDetail(val: DictResource) {
|
||||
runtimeStore.editDict = getDefaultDict(val)
|
||||
@@ -130,30 +139,38 @@ const progressTextRight = $computed(() => {
|
||||
return store.sdict?.lastLearnIndex
|
||||
})
|
||||
|
||||
|
||||
function check(cb: Function) {
|
||||
if (!store.sdict.id) {
|
||||
Toast.warning('请先选择一本词典')
|
||||
} else {
|
||||
runtimeStore.editDict = getDefaultDict(store.sdict)
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
function savePracticeSetting() {
|
||||
async function savePracticeSetting() {
|
||||
Toast.success('修改成功')
|
||||
isSaveData = false
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
await store.changeDict(runtimeStore.editDict)
|
||||
currentStudy = getCurrentStudyWord()
|
||||
}
|
||||
|
||||
function saveLastPracticeIndex(e) {
|
||||
store.sdict.lastLearnIndex = e
|
||||
showChangeLastPracticeIndexDialog = false
|
||||
async function saveLastPracticeIndex(e) {
|
||||
Toast.success('修改成功')
|
||||
runtimeStore.editDict.lastLearnIndex = e
|
||||
showChangeLastPracticeIndexDialog = false
|
||||
isSaveData = false
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
await store.changeDict(runtimeStore.editDict)
|
||||
currentStudy = getCurrentStudyWord()
|
||||
}
|
||||
|
||||
const {
|
||||
data: recommendDictList,
|
||||
isFetching
|
||||
} = useFetch(resourceWrap(DICT_LIST.WORD.RECOMMENDED)).json()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -167,7 +184,6 @@ function saveLastPracticeIndex(e) {
|
||||
<BaseIcon title="切换词典"
|
||||
class="ml-4"
|
||||
@click="router.push('/dict-list')"
|
||||
|
||||
>
|
||||
<IconFluentArrowSort20Regular v-if="store.sdict.name"/>
|
||||
<IconFluentAdd20Filled v-else/>
|
||||
@@ -196,21 +212,24 @@ function saveLastPracticeIndex(e) {
|
||||
</div>
|
||||
|
||||
<div class="w-3/10 flex flex-col justify-evenly">
|
||||
<div class="center text-xl">{{ isSaveData ? '上次学习任务' : '今日任务' }}</div>
|
||||
<div class="center gap-2">
|
||||
<span class="text-xl">{{ isSaveData ? '上次学习任务' : '今日任务' }}</span>
|
||||
<span class="color-blue cursor-pointer" @click="showPracticeWordListDialog = true">词表</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-4xl font-bold">{{ currentStudy.new.length }}</div>
|
||||
<div class="text">新词</div>
|
||||
</div>
|
||||
<template v-if="settingStore.wordPracticeMode === 0">
|
||||
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-4xl font-bold">{{ currentStudy.review.length }}</div>
|
||||
<div class="text">复习</div>
|
||||
<div class="text">复习上次</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-4xl font-bold">{{ currentStudy.write.length }}
|
||||
</div>
|
||||
<div class="text">默写</div>
|
||||
<div class="text">复习之前</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -219,8 +238,8 @@ function saveLastPracticeIndex(e) {
|
||||
<div class="flex flex-col items-end justify-around ">
|
||||
<div class="flex gap-1 items-center">
|
||||
每日目标
|
||||
<div style="color:#ac6ed1;" @click="check(()=>showPracticeSettingDialog = true)"
|
||||
class="bg-third px-2 h-10 flex center text-2xl rounded cursor-pointer">
|
||||
<div style="color:#ac6ed1;"
|
||||
class="bg-third px-2 h-10 flex center text-2xl rounded">
|
||||
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
|
||||
</div>
|
||||
个单词
|
||||
@@ -266,7 +285,7 @@ function saveLastPracticeIndex(e) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col">
|
||||
<div class="card flex flex-col overflow-hidden" v-loading="isFetching">
|
||||
<div class="flex justify-between">
|
||||
<div class="title">推荐</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
@@ -274,7 +293,7 @@ function saveLastPracticeIndex(e) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
<div class="flex gap-4 flex-wrap mt-4 min-h-50">
|
||||
<Book :is-add="false"
|
||||
quantifier="个词"
|
||||
:item="item as any"
|
||||
@@ -293,6 +312,11 @@ function saveLastPracticeIndex(e) {
|
||||
@ok="saveLastPracticeIndex"
|
||||
/>
|
||||
|
||||
<PracticeWordListDialog
|
||||
:data="currentStudy"
|
||||
v-model="showPracticeWordListDialog"
|
||||
/>
|
||||
|
||||
<CollectNotice/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import BaseTable from "@/components/BaseTable.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
const model = defineModel()
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
defineEmits<{
|
||||
ok: [number]
|
||||
}>()
|
||||
@@ -21,7 +23,7 @@ defineEmits<{
|
||||
<div class="px-4 pb-4 h-80vh w-30rem">
|
||||
<BaseTable
|
||||
class="h-full"
|
||||
:list='store.sdict.words'
|
||||
:list='runtimeStore.editDict.words'
|
||||
:loading='false'
|
||||
:show-toolbar="false"
|
||||
>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { inject, watch } from "vue"
|
||||
import { inject, Ref, watch } from "vue"
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { ShortcutKey, PracticeData } from "@/types/types.ts";
|
||||
import { PracticeData, WordPracticeType, ShortcutKey } from "@/types/types.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import Progress from '@/components/base/Progress.vue'
|
||||
@@ -25,28 +25,42 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
let practiceData = inject<PracticeData>('practiceData')
|
||||
let isTypingWrongWord = inject<Ref<boolean>>('isTypingWrongWord')
|
||||
|
||||
function format(val: number, suffix: string = '', check: number = -1) {
|
||||
return val === check ? '-' : (val + suffix)
|
||||
}
|
||||
|
||||
const status = $computed(() => {
|
||||
let str = '正在'
|
||||
if (isTypingWrongWord.value) return '复习错词'
|
||||
let str = ''
|
||||
switch (statisticsStore.step) {
|
||||
case 0:
|
||||
str += `学习新词`
|
||||
break
|
||||
case 1:
|
||||
str += `默写新词`
|
||||
str += `听写新词`
|
||||
break
|
||||
case 2:
|
||||
str += `复习上次`
|
||||
str += `默写新词`
|
||||
break
|
||||
case 3:
|
||||
str += `默写上次`
|
||||
str += `复习上次学习`
|
||||
break
|
||||
case 4:
|
||||
str += '默写之前'
|
||||
str += '听写上次学习'
|
||||
break
|
||||
case 5:
|
||||
str += '默写上次学习'
|
||||
break
|
||||
case 6:
|
||||
str += '复习之前学习'
|
||||
break
|
||||
case 7:
|
||||
str += '听写之前学习'
|
||||
break
|
||||
case 8:
|
||||
str += '默写之前学习'
|
||||
break
|
||||
}
|
||||
return str
|
||||
@@ -77,7 +91,7 @@ const progress = $computed(() => {
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="stat">
|
||||
<div class="row">
|
||||
<div class="num">{{ `${practiceData.index}/${practiceData.words.length}` }}</div>
|
||||
<div class="num">{{ `${practiceData.index + 1}/${practiceData.words.length}` }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">{{ status }}</div>
|
||||
</div>
|
||||
|
||||
@@ -12,11 +12,12 @@ import {useSettingStore} from "@/stores/setting.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
const store = useBaseStore()
|
||||
const settings = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
const model = defineModel()
|
||||
|
||||
@@ -36,8 +37,8 @@ let tempDisableShowPracticeSettingDialog = $ref(false)
|
||||
|
||||
|
||||
function changePerDayStudyNumber() {
|
||||
store.sdict.perDayStudyNumber = tempPerDayStudyNumber
|
||||
store.sdict.lastLearnIndex = tempLastLearnIndex
|
||||
runtimeStore.editDict.perDayStudyNumber = tempPerDayStudyNumber
|
||||
runtimeStore.editDict.lastLearnIndex = tempLastLearnIndex
|
||||
settings.wordPracticeMode = temPracticeMode
|
||||
settings.disableShowPracticeSettingDialog = tempDisableShowPracticeSettingDialog
|
||||
emit('ok')
|
||||
@@ -45,9 +46,9 @@ function changePerDayStudyNumber() {
|
||||
|
||||
watch(() => model.value, (n) => {
|
||||
if (n) {
|
||||
if (store.sdict.id) {
|
||||
tempPerDayStudyNumber = store.sdict.perDayStudyNumber
|
||||
tempLastLearnIndex = store.sdict.lastLearnIndex
|
||||
if (runtimeStore.editDict.id) {
|
||||
tempPerDayStudyNumber = runtimeStore.editDict.perDayStudyNumber
|
||||
tempLastLearnIndex = runtimeStore.editDict.lastLearnIndex
|
||||
temPracticeMode = settings.wordPracticeMode
|
||||
tempDisableShowPracticeSettingDialog = settings.disableShowPracticeSettingDialog
|
||||
} else {
|
||||
@@ -61,12 +62,25 @@ watch(() => model.value, (n) => {
|
||||
<Dialog v-model="model" title="学习设置" :footer="true"
|
||||
@ok="changePerDayStudyNumber">
|
||||
<div class="target-modal color-main">
|
||||
<div class="center">
|
||||
<div class="flex gap-4 text-center h-30 w-85">
|
||||
<div class="mode-item" :class="temPracticeMode == 0 && 'active'" @click=" temPracticeMode = 0">
|
||||
<div class="title text-align-center">智能模式</div>
|
||||
<div class="desc mt-2">自动规划学习、复习、听写、默写</div>
|
||||
</div>
|
||||
<div class="mode-item" :class="temPracticeMode == 1 && 'active'" @click=" temPracticeMode = 1">
|
||||
<div class="title">自由模式</div>
|
||||
<div class="desc mt-2">自由练习,系统不强制复习与默写</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-2 mb-8">
|
||||
<span>从<span class="text-3xl mx-2 lh">{{ tempLastLearnIndex }}</span>个开始,</span>
|
||||
<span>从第<span class="text-3xl mx-2 lh">{{ tempLastLearnIndex }}</span>个开始,</span>
|
||||
<span>每日<span class="text-3xl mx-2 lh">{{ tempPerDayStudyNumber }}</span>个,</span>
|
||||
<span>预计<span
|
||||
class="text-3xl mx-2 lh">{{
|
||||
_getAccomplishDays(store.sdict.length - tempLastLearnIndex, tempPerDayStudyNumber)
|
||||
class="text-3xl mx-2 lh">{{
|
||||
_getAccomplishDays(runtimeStore.editDict.length - tempLastLearnIndex, tempPerDayStudyNumber)
|
||||
}}</span>天完成</span>
|
||||
</div>
|
||||
<div class="flex mb-4 gap-space">
|
||||
@@ -84,17 +98,10 @@ watch(() => model.value, (n) => {
|
||||
:step="10"
|
||||
show-text
|
||||
class="my-1"
|
||||
:max="store.sdict.words.length" v-model="tempLastLearnIndex"/>
|
||||
:max="runtimeStore.editDict.words.length" v-model="tempLastLearnIndex"/>
|
||||
<BaseButton @click="show = true">从词典选起始位置</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gap-space">
|
||||
<RadioGroup v-model="temPracticeMode" class="flex-col gap-0!">
|
||||
<Radio :value="0" label="智能模式,系统自动计算复习单词与默写单词"/>
|
||||
<Radio :value="1" label="自由模式,系统不强制复习与默写"/>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<template v-slot:footer-left v-if="showLeftOption">
|
||||
<div class="flex items-center">
|
||||
@@ -106,8 +113,8 @@ watch(() => model.value, (n) => {
|
||||
</template>
|
||||
</Dialog>
|
||||
<ChangeLastPracticeIndexDialog
|
||||
v-model="show"
|
||||
@ok="e => {
|
||||
v-model="show"
|
||||
@ok="e => {
|
||||
tempLastLearnIndex = e
|
||||
show = false
|
||||
}"
|
||||
@@ -123,5 +130,13 @@ watch(() => model.value, (n) => {
|
||||
.lh {
|
||||
color: rgb(176, 116, 211)
|
||||
}
|
||||
|
||||
.mode-item{
|
||||
@apply w-50% border border-blue border-solid p-2 rounded-lg cursor-pointer;
|
||||
}
|
||||
|
||||
.active{
|
||||
@apply bg-blue color-white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
96
src/pages/word/components/PracticeWordListDialog.vue
Normal file
96
src/pages/word/components/PracticeWordListDialog.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseTable from "@/components/BaseTable.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
import {TaskWords} from "@/types/types.ts";
|
||||
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
const model = defineModel()
|
||||
defineProps<{
|
||||
data: TaskWords
|
||||
}>()
|
||||
|
||||
let showTranslate = $ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model="model" title="任务">
|
||||
<div class="px-4 pb-4 h-80vh flex gap-4">
|
||||
<div class="h-full flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="title">新词 {{data.new.length}}</span>
|
||||
</div>
|
||||
<BaseTable
|
||||
class="overflow-auto flex-1 w-85"
|
||||
:list='data.new'
|
||||
:loading='false'
|
||||
:show-toolbar="false"
|
||||
:showPagination="false"
|
||||
>
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
:item="item.item"
|
||||
:show-translate="showTranslate">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
<div class="h-full flex flex-col gap-2" v-if="data.review.length">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="title">复习上次 {{data.review.length}}</span>
|
||||
</div>
|
||||
<BaseTable
|
||||
class="overflow-auto flex-1 w-85"
|
||||
:list='data.review'
|
||||
:loading='false'
|
||||
:show-toolbar="false"
|
||||
:showPagination="false"
|
||||
>
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
:item="item.item"
|
||||
:show-translate="showTranslate">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
<div class="h-full flex flex-col gap-2" v-if="data.write.length">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="title">复习之前 {{data.write.length}}</span>
|
||||
<Checkbox v-model="showTranslate">翻译</Checkbox>
|
||||
</div>
|
||||
<BaseTable
|
||||
class="overflow-auto flex-1 w-85"
|
||||
:list='data.write'
|
||||
:loading='false'
|
||||
:show-toolbar="false"
|
||||
:showPagination="false"
|
||||
>
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
:item="item.item"
|
||||
:show-translate="showTranslate">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import {ShortcutKey, Word} from "@/types/types.ts";
|
||||
import {WordPracticeType, ShortcutKey, Word, WordPracticeMode} from "@/types/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio, useTTsPlayAudio} from "@/hooks/sound.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {nextTick, onMounted, onUnmounted, watch} from "vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import {inject, onMounted, onUnmounted, Ref, watch} from "vue";
|
||||
import SentenceHightLightWord from "@/pages/word/components/SentenceHightLightWord.vue";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {getDefaultWord} from "@/types/func.ts";
|
||||
import {_nextTick, sleep} from "@/utils";
|
||||
import {_nextTick, last, sleep} from "@/utils";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import Space from "@/pages/article/components/Space.vue";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
|
||||
interface IProps {
|
||||
word: Word,
|
||||
@@ -21,7 +24,8 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
|
||||
const emit = defineEmits<{
|
||||
complete: [],
|
||||
wrong: []
|
||||
wrong: [],
|
||||
know: [],
|
||||
}>()
|
||||
|
||||
let input = $ref('')
|
||||
@@ -60,17 +64,21 @@ function updateCurrentWordInfo() {
|
||||
};
|
||||
}
|
||||
|
||||
watch(() => props.word, () => {
|
||||
watch(() => props.word, reset, {deep: true})
|
||||
|
||||
function reset() {
|
||||
wrong = input = ''
|
||||
wordRepeatCount = 0
|
||||
inputLock = false
|
||||
showWordResult = inputLock = false
|
||||
if (settingStore.wordSound) {
|
||||
volumeIconRef?.play(400, true)
|
||||
if (settingStore.wordPracticeType !== WordPracticeType.Dictation) {
|
||||
volumeIconRef?.play(400, true)
|
||||
}
|
||||
}
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
checkCursorPosition()
|
||||
}, {deep: true})
|
||||
}
|
||||
|
||||
// 监听输入变化,更新当前单词信息
|
||||
watch(() => input, () => {
|
||||
@@ -81,11 +89,7 @@ onMounted(() => {
|
||||
// 初始化当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
|
||||
emitter.on(EventKey.resetWord, () => {
|
||||
wrong = input = ''
|
||||
updateCurrentWordInfo();
|
||||
})
|
||||
|
||||
emitter.on(EventKey.resetWord, reset)
|
||||
emitter.on(EventKey.onTyping, onTyping)
|
||||
})
|
||||
|
||||
@@ -106,95 +110,168 @@ function repeat() {
|
||||
}, settingStore.waitTimeForChangeWord)
|
||||
}
|
||||
|
||||
async function onTyping(e: KeyboardEvent) {
|
||||
if (inputLock) {
|
||||
//如果是锁定状态,说明要么输入太快;要么就是设置了不自动跳转,然后输入完了
|
||||
//当单词全部输入完成后,空格键用于切换到下一个单词
|
||||
if (e.code === 'Space' && input.toLowerCase() === props.word.word.toLowerCase()) {
|
||||
return emit('complete')
|
||||
}
|
||||
return
|
||||
}
|
||||
let letter = e.key
|
||||
inputLock = true
|
||||
let showWordResult = $ref(false)
|
||||
let pressNumber = 0
|
||||
|
||||
// 检查当前单词是否包含空格
|
||||
const wordContainsSpace = props.word.word.includes(' ')
|
||||
|
||||
// 如果是空格键,需要判断是作为输入还是切换单词
|
||||
if (letter === ' ' || e.code === 'Space') {
|
||||
// 如果当前单词包含空格
|
||||
if (wordContainsSpace && props.word.word[input.length] === ' ') {
|
||||
letter = ' '
|
||||
}
|
||||
// 如果当前单词不包含空格,且已经输入完成,则视为切换单词的信号
|
||||
else if (!wordContainsSpace && input.toLowerCase() === props.word.word.toLowerCase()) {
|
||||
return emit('complete')
|
||||
}
|
||||
}
|
||||
|
||||
let isTypingRight = false
|
||||
const right = $computed(() => {
|
||||
if (settingStore.ignoreCase) {
|
||||
isTypingRight = letter.toLowerCase() === props.word.word[input.length].toLowerCase()
|
||||
return input.toLowerCase() === props.word.word.toLowerCase()
|
||||
} else {
|
||||
isTypingRight = letter === props.word.word[input.length]
|
||||
}
|
||||
if (isTypingRight) {
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
} else {
|
||||
emit('wrong')
|
||||
wrong = letter
|
||||
playBeep()
|
||||
volumeIconRef?.play()
|
||||
await sleep(500)
|
||||
if (settingStore.inputWrongClear) input = ''
|
||||
wrong = ''
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
return input === props.word.word
|
||||
}
|
||||
})
|
||||
|
||||
if (input.toLowerCase() === props.word.word.toLowerCase()) {
|
||||
playCorrect()
|
||||
//不需要把inputLock设为false,输入完成不能再输入了,只能删除,删除会打开锁
|
||||
if (settingStore.autoNextWord) {
|
||||
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()
|
||||
let showNotice = false
|
||||
|
||||
function know(e) {
|
||||
if (settingStore.wordPracticeType === WordPracticeType.Identify) {
|
||||
if (!showWordResult) {
|
||||
inputLock = showWordResult = true
|
||||
input = props.word.word
|
||||
emit('know')
|
||||
if (!showNotice) {
|
||||
Toast.info('若误选“我认识”,可按删除键重新选择!', {duration: 5000})
|
||||
showNotice = true
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
onTyping(e)
|
||||
}
|
||||
|
||||
function unknown(e) {
|
||||
if (settingStore.wordPracticeType === WordPracticeType.Identify) {
|
||||
if (!showWordResult) {
|
||||
showWordResult = true
|
||||
emit('wrong')
|
||||
return
|
||||
}
|
||||
}
|
||||
onTyping(e)
|
||||
}
|
||||
|
||||
async function onTyping(e: KeyboardEvent) {
|
||||
debugger
|
||||
let word = props.word.word
|
||||
if (inputLock) {
|
||||
// 因为输入完成会锁死不能再输入,所以在这里判断空格键切换到下一个单词
|
||||
if (e.code === 'Space' && input.toLowerCase() === word.toLowerCase()) {
|
||||
showWordResult = inputLock = false
|
||||
emit('complete')
|
||||
} else {
|
||||
//当显示单词时,提示用户正确按键
|
||||
if (showWordResult) {
|
||||
pressNumber++
|
||||
if (pressNumber >= 3) {
|
||||
Toast.info(right ? '请按空格键切换' : '请按删除键重新输入', {duration: 2000})
|
||||
pressNumber = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
inputLock = true
|
||||
let letter = e.key
|
||||
//默写特殊逻辑
|
||||
if (settingStore.wordPracticeType === WordPracticeType.Dictation) {
|
||||
if (e.code === 'Space') {
|
||||
//如果输入长度大于单词长度/单词不包含空格,并且输入不为空(开始直接输入空格不行),则显示单词;
|
||||
// 这里inputLock 不设为 false,不能再输入了,只能删除(删除会重置 inputLock)或按空格切下一格
|
||||
if (input.length && (input.length >= word.length || !word.includes(' '))) {
|
||||
//比对是否一致
|
||||
if (input.toLowerCase() === word.toLowerCase()) {
|
||||
//如果已显示单词,则发射完成事件,并 return
|
||||
if (showWordResult) {
|
||||
return emit('complete')
|
||||
} else {
|
||||
//未显示单词,则播放正确音乐,并在后面设置为 showWordResult 为 true 来显示单词
|
||||
playCorrect()
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
} else {
|
||||
//错误处理
|
||||
playBeep()
|
||||
volumeIconRef?.play()
|
||||
emit('wrong')
|
||||
}
|
||||
showWordResult = true
|
||||
return
|
||||
}
|
||||
}
|
||||
//默写途中不判断是否正确,在按空格再判断
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
updateCurrentWordInfo();
|
||||
inputLock = false
|
||||
} else {
|
||||
let right = false
|
||||
if (settingStore.ignoreCase) {
|
||||
right = letter.toLowerCase() === word[input.length].toLowerCase()
|
||||
} else {
|
||||
right = letter === props.word.word[input.length]
|
||||
}
|
||||
if (right) {
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
} else {
|
||||
emit('wrong')
|
||||
wrong = letter
|
||||
playBeep()
|
||||
volumeIconRef?.play()
|
||||
await sleep(500)
|
||||
if (settingStore.inputWrongClear) input = ''
|
||||
wrong = ''
|
||||
}
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
//不需要把inputLock设为false,输入完成不能再输入了,只能删除,删除会打开锁
|
||||
if (input.toLowerCase() === word.toLowerCase()) {
|
||||
playCorrect()
|
||||
if ([WordPracticeType.Listen, WordPracticeType.Identify].includes(settingStore.wordPracticeType) && !showWordResult) {
|
||||
showWordResult = true
|
||||
}
|
||||
if ([WordPracticeType.FollowWrite, WordPracticeType.Spell].includes(settingStore.wordPracticeType)) {
|
||||
if (settingStore.autoNextWord) {
|
||||
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() {
|
||||
playKeyboardAudio()
|
||||
inputLock = false
|
||||
|
||||
if (wrong) {
|
||||
wrong = ''
|
||||
if (showWordResult) {
|
||||
input = ''
|
||||
showWordResult = false
|
||||
} else {
|
||||
input = input.slice(0, -1)
|
||||
if (wrong) {
|
||||
wrong = ''
|
||||
} else {
|
||||
input = input.slice(0, -1)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
}
|
||||
|
||||
|
||||
function showWord() {
|
||||
if (settingStore.allowWordTip) {
|
||||
showFullWord = true
|
||||
@@ -262,39 +339,49 @@ function checkCursorPosition() {
|
||||
_nextTick(() => {
|
||||
// 选中目标元素
|
||||
const cursorEl = document.querySelector(`.cursor`);
|
||||
const input = document.querySelector(`.input`);
|
||||
const inputList = document.querySelectorAll(`.l`);
|
||||
const typingWordRect = typingWordRef.getBoundingClientRect();
|
||||
|
||||
if (input) {
|
||||
let inputRect = input.getBoundingClientRect();
|
||||
if (inputList.length) {
|
||||
let inputRect = last(Array.from(inputList)).getBoundingClientRect();
|
||||
cursor = {
|
||||
top: inputRect.top + inputRect.height - cursorEl.clientHeight - typingWordRect.top,
|
||||
left: inputRect.right - typingWordRect.left - 3,
|
||||
};
|
||||
} else {
|
||||
const letter = document.querySelector(`.letter`);
|
||||
let letterRect = letter.getBoundingClientRect();
|
||||
const dictation = document.querySelector(`.dictation`);
|
||||
let elRect
|
||||
if (dictation) {
|
||||
elRect = dictation.getBoundingClientRect();
|
||||
} else {
|
||||
const letter = document.querySelector(`.letter`);
|
||||
elRect = letter.getBoundingClientRect();
|
||||
}
|
||||
cursor = {
|
||||
top: letterRect.top + letterRect.height - cursorEl.clientHeight - typingWordRect.top,
|
||||
left: letterRect.left - typingWordRect.left - 3,
|
||||
top: elRect.top + elRect.height - cursorEl.clientHeight - typingWordRect.top,
|
||||
left: elRect.left - typingWordRect.left - 3,
|
||||
};
|
||||
}
|
||||
},)
|
||||
}
|
||||
|
||||
useEvents([
|
||||
[ShortcutKey.KnowWord, know],
|
||||
[ShortcutKey.UnknownWord, unknown],
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="typing-word" ref="typingWordRef" v-if="props.word.word.length">
|
||||
<div class="typing-word" ref="typingWordRef" v-if="word.word.length">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex gap-1 mt-26">
|
||||
<div class="phonetic" v-if="settingStore.soundType === 'us' && word.phonetic0">[{{
|
||||
(settingStore.dictation && !showFullWord) ? '_'.repeat(word.phonetic0.length) : word.phonetic0
|
||||
}}]
|
||||
<div class="phonetic"
|
||||
:class="!(!settingStore.dictation || showFullWord || showWordResult) && 'word-shadow'"
|
||||
v-if="settingStore.soundType === 'us' && word.phonetic0">[{{ word.phonetic0 }}]
|
||||
</div>
|
||||
<div class="phonetic" v-if="settingStore.soundType === 'uk' && word.phonetic1">[{{
|
||||
(settingStore.dictation && !showFullWord) ? '_'.repeat(word.phonetic1.length) : word.phonetic1
|
||||
}}]
|
||||
<div class="phonetic"
|
||||
:class="((settingStore.dictation || [WordPracticeType.Spell,WordPracticeType.Listen,WordPracticeType.Dictation].includes(settingStore.wordPracticeType)) && !showFullWord && !showWordResult) && 'word-shadow'"
|
||||
v-if="settingStore.soundType === 'uk' && word.phonetic1">[{{ word.phonetic1 }}]
|
||||
</div>
|
||||
<VolumeIcon
|
||||
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
@@ -307,58 +394,89 @@ function checkCursorPosition() {
|
||||
@mouseenter="showWord"
|
||||
@mouseleave="mouseleave"
|
||||
>
|
||||
<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">{{ displayWord.split('').map(() => '_').join('') }}</span>
|
||||
<div v-if="settingStore.wordPracticeType === WordPracticeType.Dictation">
|
||||
<div class="letter text-align-center w-full inline-block"
|
||||
v-opacity="!settingStore.dictation || showWordResult || showFullWord">
|
||||
{{ word.word }}
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 w-120 dictation"
|
||||
:style="{minHeight: settingStore.fontSize.wordForeignFontSize +'px'}"
|
||||
:class="showWordResult ? (right ? 'right' : 'wrong') : ''">
|
||||
<template v-for="i in input">
|
||||
<span class="l" v-if="i !== ' '">{{ i }}</span>
|
||||
<Space class="l" v-else :is-wrong="showWordResult ? (!right) : false" :is-wait="!showWordResult"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<span class="input" v-if="input">{{ input }}</span>
|
||||
<span class="wrong" v-if="wrong">{{ wrong }}</span>
|
||||
<span class="letter" v-if="settingStore.dictation && !showFullWord">
|
||||
{{ displayWord.split('').map((v) => (v === ' ' ? ' ' : '_')).join('') }}
|
||||
</span>
|
||||
<span class="letter" v-else>{{ displayWord }}</span>
|
||||
</template>
|
||||
<span class="letter" v-else>{{ displayWord }}</span>
|
||||
</div>
|
||||
|
||||
<div class="translate anim flex flex-col gap-2 my-3"
|
||||
v-opacity="settingStore.translate || showFullWord"
|
||||
<div class="mt-4 flex gap-4"
|
||||
v-if="settingStore.wordPracticeType === WordPracticeType.Identify && !showWordResult">
|
||||
<BaseButton
|
||||
:keyboard="`快捷键(${settingStore.shortcutKeyMap[ShortcutKey.KnowWord]})`"
|
||||
size="large" @click="know">我认识
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:keyboard="`快捷键(${settingStore.shortcutKeyMap[ShortcutKey.UnknownWord]})`"
|
||||
size="large" @click="unknown">不认识
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="translate flex flex-col gap-2 my-3"
|
||||
v-opacity="settingStore.translate || showWordResult || showFullWord"
|
||||
:style="{
|
||||
fontSize: settingStore.fontSize.wordTranslateFontSize +'px',
|
||||
}"
|
||||
>
|
||||
<div class="flex" v-for="(v,i) in word.trans">
|
||||
<div class="flex" v-for="v in word.trans">
|
||||
<div class="shrink-0" :class="v.pos ? 'w-12 en-article-family' : '-ml-3'">{{ v.pos }}</div>
|
||||
<span v-if="settingStore.dictation && !showFullWord" v-html="hideWordInTranslation(v.cn, word.word)"></span>
|
||||
<span v-else>{{ v.cn }}</span>
|
||||
<span v-if="!settingStore.dictation || showWordResult || showFullWord">{{ v.cn }}</span>
|
||||
<span v-else v-html="hideWordInTranslation(v.cn, word.word)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="other">
|
||||
<div class="other anim"
|
||||
v-opacity="![WordPracticeType.Listen,WordPracticeType.Dictation,WordPracticeType.Identify].includes(settingStore.wordPracticeType) || showFullWord || showWordResult">
|
||||
<div class="line-white my-2"></div>
|
||||
|
||||
<template v-if="word?.sentences?.length">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="sentence" v-for="item in word.sentences">
|
||||
<SentenceHightLightWord class="text-xl" :text="item.c" :word="word.word"
|
||||
:dictation="(settingStore.dictation && !showFullWord)"/>
|
||||
<div class="text-base anim" v-opacity="settingStore.translate || showFullWord">{{ item.cn }}</div>
|
||||
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"/>
|
||||
<div class="text-base anim" v-opacity="settingStore.translate || showFullWord || showWordResult">
|
||||
{{ item.cn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line-white my-2 mb-5 anim" v-opacity="settingStore.translate || showFullWord"></div>
|
||||
<div class="line-white my-2 mb-5"></div>
|
||||
</template>
|
||||
|
||||
|
||||
<div class="anim" v-opacity="settingStore.translate || showFullWord">
|
||||
<template v-if="word?.phrases?.length">
|
||||
<div class="flex">
|
||||
<div class="label">短语</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-4" v-for="item in word.phrases">
|
||||
<SentenceHightLightWord class="en" :text="item.c" :word="word.word"
|
||||
:dictation="(settingStore.dictation && !showFullWord)"/>
|
||||
<div class="cn anim" v-opacity="settingStore.translate">{{ item.cn }}</div>
|
||||
<template v-if="word?.phrases?.length">
|
||||
<div class="flex">
|
||||
<div class="label">短语</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-4" v-for="item in word.phrases">
|
||||
<SentenceHightLightWord class="en" :text="item.c" :word="word.word"
|
||||
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"/>
|
||||
<div class="cn anim" v-opacity="settingStore.translate || showFullWord || showWordResult">
|
||||
{{ item.cn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line-white mt-3 mb-2"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="line-white mt-3 mb-2"></div>
|
||||
</template>
|
||||
|
||||
<template v-if="(settingStore.translate || !settingStore.dictation)">
|
||||
<template v-if="word?.synos?.length">
|
||||
<div class="flex">
|
||||
<div class='label'>同近义词</div>
|
||||
@@ -366,11 +484,13 @@ function checkCursorPosition() {
|
||||
<div class="flex" v-for="item in word.synos">
|
||||
<div class="pos line-height-1.4rem!">{{ item.pos }}</div>
|
||||
<div>
|
||||
<div class="cn">{{ item.cn }}</div>
|
||||
<div>
|
||||
<span class="en" v-for="(i,j) in item.ws">{{ i }} {{
|
||||
j !== item.ws.length - 1 ? ' / ' : ''
|
||||
}} </span>
|
||||
<div class="cn anim" v-opacity="settingStore.translate || showFullWord || showWordResult">
|
||||
{{ item.cn }}
|
||||
</div>
|
||||
<div class="anim" v-opacity="!settingStore.dictation || showFullWord || showWordResult">
|
||||
<span class="en" v-for="(i,j) in item.ws">
|
||||
{{ i }} {{ j !== item.ws.length - 1 ? ' / ' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -378,7 +498,10 @@ function checkCursorPosition() {
|
||||
</div>
|
||||
<div class="line-white my-2"></div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div class="anim"
|
||||
v-opacity="(settingStore.translate && !settingStore.dictation) || showFullWord || showWordResult">
|
||||
<template v-if="word?.etymology?.length">
|
||||
<div class="flex">
|
||||
<div class="label">词源</div>
|
||||
@@ -419,6 +542,10 @@ function checkCursorPosition() {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dictation {
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
|
||||
.typing-word {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
@@ -442,7 +569,8 @@ function checkCursorPosition() {
|
||||
font-family: var(--en-article-family);
|
||||
letter-spacing: .3rem;
|
||||
|
||||
.input {
|
||||
|
||||
.input, .right {
|
||||
color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import DictList from "@/pages/word/DictList.vue";
|
||||
import BookList from "@/pages/article/BookList.vue";
|
||||
import Setting from "@/pages/setting/Setting.vue";
|
||||
import Home from "@/pages/home/index.vue";
|
||||
import Login from "@/pages/user/login.vue";
|
||||
import User from "@/pages/user/index.vue";
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -33,6 +35,8 @@ export const routes: RouteRecordRaw[] = [
|
||||
{path: 'book-detail', component: BookDetail},
|
||||
{path: 'book-list', component: BookList},
|
||||
{path: 'setting', component: Setting},
|
||||
{path: 'login', component: Login},
|
||||
{path: 'user', component: User},
|
||||
]
|
||||
},
|
||||
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {Dict, DictId, Word} from "../types/types.ts"
|
||||
import {_getAccomplishDate, _getStudyProgress, checkAndUpgradeSaveDict} from "@/utils";
|
||||
import {SAVE_DICT_KEY} from "@/utils/const.ts";
|
||||
import {shallowReactive} from "vue";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
import {get, set} from 'idb-keyval'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Dict, DictId, Word } from "../types/types.ts"
|
||||
import { _getStudyProgress, checkAndUpgradeSaveDict, shakeCommonDict } from "@/utils";
|
||||
import { shallowReactive } from "vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import { get, set } from 'idb-keyval'
|
||||
import { CAN_REQUEST, IS_LOGIN, IS_OFFICIAL, SAVE_DICT_KEY } from "@/config/env.ts";
|
||||
import { add2MyDict, dictListVersion, myDictList } from "@/apis";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
|
||||
export interface BaseState {
|
||||
simpleWords: string[],
|
||||
@@ -16,7 +18,8 @@ export interface BaseState {
|
||||
article: {
|
||||
bookList: Dict[],
|
||||
studyIndex: number,
|
||||
}
|
||||
},
|
||||
dictListVersion: number
|
||||
}
|
||||
|
||||
export const DefaultBaseState = (): BaseState => ({
|
||||
@@ -33,19 +36,7 @@ export const DefaultBaseState = (): BaseState => ({
|
||||
bookList: [
|
||||
getDefaultDict({id: DictId.wordCollect, name: '收藏'}),
|
||||
getDefaultDict({id: DictId.wordWrong, name: '错词'}),
|
||||
getDefaultDict({id: DictId.wordKnown, name: '已掌握',description:'已掌握后的单词不会出现在练习中'}),
|
||||
// getDefaultDict({
|
||||
// id: 'nce-new-2',
|
||||
// name: '新概念英语(新版)-2',
|
||||
// description: '新概念英语新版第二册',
|
||||
// category: '青少年英语',
|
||||
// tags: ['新概念英语'],
|
||||
// url: 'nce-new-2_v2.json',
|
||||
// length: 862,
|
||||
// translateLanguage: 'common',
|
||||
// language: 'en',
|
||||
// type: DictType.word
|
||||
// }),
|
||||
getDefaultDict({id: DictId.wordKnown, name: '已掌握', description: '已掌握后的单词不会出现在练习中'}),
|
||||
],
|
||||
studyIndex: -1,
|
||||
},
|
||||
@@ -54,7 +45,8 @@ export const DefaultBaseState = (): BaseState => ({
|
||||
getDefaultDict({id: DictId.articleCollect, name: '收藏'})
|
||||
],
|
||||
studyIndex: -1,
|
||||
}
|
||||
},
|
||||
dictListVersion: 1
|
||||
})
|
||||
|
||||
export const useBaseStore = defineStore('base', {
|
||||
@@ -128,17 +120,25 @@ export const useBaseStore = defineStore('base', {
|
||||
})
|
||||
this.$patch(obj)
|
||||
},
|
||||
async init(outData?: any) {
|
||||
async init() {
|
||||
return new Promise(async resolve => {
|
||||
try {
|
||||
if (outData) {
|
||||
this.setState(outData)
|
||||
} else {
|
||||
let configStr: string = await get(SAVE_DICT_KEY.key)
|
||||
let data = checkAndUpgradeSaveDict(configStr)
|
||||
this.setState(data)
|
||||
let configStr: string = await get(SAVE_DICT_KEY.key)
|
||||
let data = checkAndUpgradeSaveDict(configStr)
|
||||
if (IS_OFFICIAL) {
|
||||
let r = await dictListVersion()
|
||||
if (r.success) {
|
||||
data.dictListVersion = r.data
|
||||
}
|
||||
}
|
||||
set(SAVE_DICT_KEY.key, JSON.stringify({val: this.$state, version: SAVE_DICT_KEY.version}))
|
||||
if (CAN_REQUEST) {
|
||||
let res = await myDictList()
|
||||
if (res.success) {
|
||||
Object.assign(data, res.data)
|
||||
}
|
||||
}
|
||||
this.setState(data)
|
||||
set(SAVE_DICT_KEY.key, JSON.stringify({val: shakeCommonDict(this.$state), version: SAVE_DICT_KEY.version}))
|
||||
} catch (e) {
|
||||
console.error('读取本地dict数据失败', e)
|
||||
}
|
||||
@@ -146,7 +146,13 @@ export const useBaseStore = defineStore('base', {
|
||||
})
|
||||
},
|
||||
//改变词典
|
||||
changeDict(val: Dict) {
|
||||
async changeDict(val: Dict) {
|
||||
if (CAN_REQUEST) {
|
||||
let r = await add2MyDict(val)
|
||||
if (!r.success) {
|
||||
return Toast.error(r.msg)
|
||||
}
|
||||
}
|
||||
//把其他的词典的单词数据都删掉,全保存在内存里太卡了
|
||||
this.word.bookList.slice(3).map(v => {
|
||||
if (!v.custom) {
|
||||
@@ -161,13 +167,20 @@ export const useBaseStore = defineStore('base', {
|
||||
this.word.studyIndex = rIndex
|
||||
this.word.bookList[this.word.studyIndex].words = shallowReactive(val.words)
|
||||
this.word.bookList[this.word.studyIndex].perDayStudyNumber = val.perDayStudyNumber
|
||||
this.word.bookList[this.word.studyIndex].lastLearnIndex = val.lastLearnIndex
|
||||
} else {
|
||||
this.word.bookList.push(getDefaultDict(val))
|
||||
this.word.studyIndex = this.word.bookList.length - 1
|
||||
}
|
||||
},
|
||||
//改变书籍
|
||||
changeBook(val: Dict) {
|
||||
async changeBook(val: Dict) {
|
||||
if (CAN_REQUEST) {
|
||||
let r = await add2MyDict(val)
|
||||
if (!r.success) {
|
||||
return Toast.error(r.msg)
|
||||
}
|
||||
}
|
||||
//把其他的书籍里面的文章数据都删掉,全保存在内存里太卡了
|
||||
this.article.bookList.slice(1).map(v => {
|
||||
if (!v.custom) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {defineStore} from "pinia"
|
||||
import {checkAndUpgradeSaveSetting, cloneDeep} from "@/utils";
|
||||
import {DefaultShortcutKeyMap} from "@/types/types.ts";
|
||||
import {APP_VERSION, SAVE_SETTING_KEY} from "@/utils/const.ts";
|
||||
import {get} from "idb-keyval";
|
||||
import { defineStore } from "pinia"
|
||||
import { checkAndUpgradeSaveSetting, cloneDeep } from "@/utils";
|
||||
import {DefaultShortcutKeyMap, WordPracticeMode, WordPracticeType} from "@/types/types.ts";
|
||||
import { get } from "idb-keyval";
|
||||
import { CAN_REQUEST, SAVE_SETTING_KEY } from "@/config/env.ts";
|
||||
import { getSetting } from "@/apis";
|
||||
|
||||
export interface SettingState {
|
||||
soundType: string,
|
||||
@@ -47,7 +48,8 @@ export interface SettingState {
|
||||
load: boolean
|
||||
conflictNotice: boolean // 其他脚本/插件冲突提示
|
||||
ignoreSimpleWord: boolean // 忽略简单词
|
||||
wordPracticeMode: number // 单词练习模式,0:智能模式,1:自由模式
|
||||
wordPracticeMode: WordPracticeMode // 单词练习模式
|
||||
wordPracticeType: WordPracticeType // 单词练习类型
|
||||
disableShowPracticeSettingDialog: boolean // 不默认显示练习设置弹框
|
||||
autoNextWord: boolean //自动切换下一个单词
|
||||
inputWrongClear: boolean //单词输入错误,清空已输入内容
|
||||
@@ -96,7 +98,8 @@ export const getDefaultSettingState = (): SettingState => ({
|
||||
load: false,
|
||||
conflictNotice: true,
|
||||
ignoreSimpleWord: false,
|
||||
wordPracticeMode: 0,
|
||||
wordPracticeMode: WordPracticeMode.System,
|
||||
wordPracticeType: WordPracticeType.FollowWrite,
|
||||
disableShowPracticeSettingDialog: false,
|
||||
autoNextWord: true,
|
||||
inputWrongClear: false,
|
||||
@@ -120,6 +123,12 @@ export const useSettingStore = defineStore('setting', {
|
||||
configStr = configStr2
|
||||
}
|
||||
let data = checkAndUpgradeSaveSetting(configStr)
|
||||
if (CAN_REQUEST) {
|
||||
let res = await getSetting()
|
||||
if (res.success) {
|
||||
Object.assign(data, res.data)
|
||||
}
|
||||
}
|
||||
this.setState({...data, load: true})
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@ export function getDefaultArticleWord(val: Partial<ArticleWord> = {}): ArticleWo
|
||||
|
||||
export function getDefaultArticle(val: Partial<Article> = {}): Article {
|
||||
return {
|
||||
id: '',
|
||||
id: null,
|
||||
title: '',
|
||||
titleTranslate: '',
|
||||
text: '',
|
||||
@@ -66,9 +66,16 @@ export function getDefaultDict(val: Partial<Dict> = {}): Dict {
|
||||
perDayStudyNumber: 20,
|
||||
custom: false,
|
||||
complete: false,
|
||||
|
||||
createdBy: '',
|
||||
en_name: '',
|
||||
category_id: null,
|
||||
is_default: false,
|
||||
|
||||
...val,
|
||||
words: shallowReactive(val.words ?? []),
|
||||
articles: shallowReactive(val.articles ?? []),
|
||||
statistics: shallowReactive(val.statistics ?? [])
|
||||
statistics: shallowReactive(val.statistics ?? []),
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface Sentence {
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
id: string,
|
||||
id?: number,
|
||||
title: string,
|
||||
titleTranslate: string,
|
||||
text: string,
|
||||
@@ -117,7 +117,9 @@ export enum ShortcutKey {
|
||||
ToggleConciseMode = 'ToggleConciseMode',
|
||||
TogglePanel = 'TogglePanel',
|
||||
RandomWrite = 'RandomWrite',
|
||||
NextRandomWrite = 'NextRandomWrite'
|
||||
NextRandomWrite = 'NextRandomWrite',
|
||||
KnowWord = 'KnowWord',
|
||||
UnknownWord = 'UnknownWord',
|
||||
}
|
||||
|
||||
export const DefaultShortcutKeyMap = {
|
||||
@@ -139,6 +141,8 @@ export const DefaultShortcutKeyMap = {
|
||||
[ShortcutKey.TogglePanel]: 'Ctrl+L',
|
||||
[ShortcutKey.RandomWrite]: 'Ctrl+R',
|
||||
[ShortcutKey.NextRandomWrite]: 'Ctrl+Shift+R',
|
||||
[ShortcutKey.KnowWord]: '1',
|
||||
[ShortcutKey.UnknownWord]: '2',
|
||||
}
|
||||
|
||||
export enum TranslateEngine {
|
||||
@@ -156,6 +160,7 @@ export type DictResource = {
|
||||
translateLanguage: TranslateLanguageType
|
||||
//todo 可以考虑删除了
|
||||
type?: DictType
|
||||
version?: number
|
||||
language: LanguageType
|
||||
}
|
||||
|
||||
@@ -167,6 +172,11 @@ export interface Dict extends DictResource {
|
||||
statistics: Statistics[],
|
||||
custom: boolean,//是否是自定义词典
|
||||
complete: boolean,//是否学习完成,学完了设为true,然后lastLearnIndex重置
|
||||
//后端字段
|
||||
en_name?: string
|
||||
createdBy?: string
|
||||
category_id?: number
|
||||
is_default?: boolean
|
||||
}
|
||||
|
||||
export interface ArticleItem {
|
||||
@@ -181,8 +191,9 @@ export const SlideType = {
|
||||
|
||||
export interface PracticeData {
|
||||
index: number,
|
||||
words: any[],
|
||||
wrongWords: any[],
|
||||
words: Word[],
|
||||
wrongWords: Word[],
|
||||
excludeWords: string[],
|
||||
}
|
||||
|
||||
export interface TaskWords {
|
||||
@@ -202,4 +213,19 @@ export enum PracticeArticleWordType {
|
||||
Symbol,
|
||||
Number,
|
||||
Word
|
||||
}
|
||||
|
||||
//练习模式
|
||||
export enum WordPracticeMode {
|
||||
System = 0,
|
||||
Free = 1
|
||||
}
|
||||
|
||||
//练习类型
|
||||
export enum WordPracticeType {
|
||||
FollowWrite,//跟写
|
||||
Spell,
|
||||
Identify,
|
||||
Listen,
|
||||
Dictation
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import {Dict, DictResource} from "@/types/types.ts";
|
||||
import {getDictFile} from "@/utils/index.ts";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import {nanoid} from "nanoid";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
|
||||
export async function getArticleBookDataByUrl(val: DictResource) {
|
||||
let dictResourceUrl = `./dicts/${val.language}/${val.type}/${val.translateLanguage}/${val.url}`;
|
||||
let s = await getDictFile(dictResourceUrl)
|
||||
let articles = cloneDeep(s.map(v => {
|
||||
v.id = nanoid(6)
|
||||
return v
|
||||
}))
|
||||
return cloneDeep({
|
||||
...getDefaultDict(),
|
||||
...val,
|
||||
articles
|
||||
})
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
export const SoundFileOptions = [
|
||||
{value: '机械键盘', label: '机械键盘'},
|
||||
{value: '机械键盘1', label: '机械键盘1'},
|
||||
{value: '机械键盘2', label: '机械键盘2'},
|
||||
{value: '老式机械键盘', label: '老式机械键盘'},
|
||||
{value: '笔记本键盘', label: '笔记本键盘'},
|
||||
]
|
||||
|
||||
export const APP_NAME = 'Type Words'
|
||||
export const APP_VERSION = {
|
||||
key: 'type-words-app-version',
|
||||
version: 1
|
||||
}
|
||||
|
||||
export const SAVE_DICT_KEY = {
|
||||
key: 'typing-word-dict',
|
||||
version: 4
|
||||
}
|
||||
export const SAVE_SETTING_KEY = {
|
||||
key: 'typing-word-setting',
|
||||
version: 15
|
||||
}
|
||||
export const EXPORT_DATA_KEY = {
|
||||
key: 'typing-word-export',
|
||||
version: 4
|
||||
}
|
||||
|
||||
export const LOCAL_FILE_KEY = 'typing-word-files'
|
||||
|
||||
export const PracticeSaveWordKey = {
|
||||
key: 'PracticeSaveWord',
|
||||
version: 1
|
||||
}
|
||||
|
||||
export const PracticeSaveArticleKey = {
|
||||
key: 'PracticeSaveArticle',
|
||||
version: 1
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import {SAVE_SETTING_KEY} from "@/utils/const";
|
||||
|
||||
export default {
|
||||
$notice(val) {
|
||||
|
||||
@@ -1,114 +1,100 @@
|
||||
// import axios, {AxiosInstance} from 'axios'
|
||||
// // import globalMethods from './global-methods'
|
||||
// // import Config from '../config/index'
|
||||
// // import CONSTANT from './const_var'
|
||||
// // import store from '../store'
|
||||
// // import Storage from './storage'
|
||||
//
|
||||
// export const axiosInstance: AxiosInstance = axios.create({
|
||||
// // baseURL: process.env.NODE_ENV === 'production' ? Config.PRODUCT_API_URL : Config.API_URL,
|
||||
// // baseURL: 'http://testtestgp.com',
|
||||
// timeout: 15000,
|
||||
// })
|
||||
//
|
||||
// // request 拦截器
|
||||
// axiosInstance.interceptors.request.use(
|
||||
// (config) => {
|
||||
// // console.log('config', config)
|
||||
// if (config.url === 'https://api.fanyi.baidu.com/api/trans/vip/translate') {
|
||||
// config.url = '/baidu'
|
||||
// }
|
||||
// return config
|
||||
// },
|
||||
// error => Promise.reject(error),
|
||||
// )
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { ENV } from "@/config/env.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
|
||||
// respone 拦截器
|
||||
// instance.interceptors.response.use(
|
||||
// // 响应正常的处理
|
||||
// (response) => {
|
||||
// // console.log(response)
|
||||
// // console.log(response.data)
|
||||
// const { data } = response
|
||||
// if (response.status !== 200) {
|
||||
// globalMethods.$warning(response.statusText)
|
||||
// return Promise.reject(data)
|
||||
// }
|
||||
// if (data === null) {
|
||||
// return Promise.resolve({
|
||||
// code: '009900',
|
||||
// msg: '系统出现错误',
|
||||
// data: {},
|
||||
// })
|
||||
// }
|
||||
// return Promise.resolve(data)
|
||||
// },
|
||||
// // 请求出错的处理
|
||||
// (error) => {
|
||||
// console.log(error)
|
||||
// if (error.response === undefined && error.status === undefined) {
|
||||
// return Promise.resolve({
|
||||
// code: '009900',
|
||||
// msg: '服务器响应超时',
|
||||
// data: null,
|
||||
// })
|
||||
// }
|
||||
// if (error.response.status >= 500) {
|
||||
// return Promise.resolve({
|
||||
// code: '009900',
|
||||
// msg: '服务器出现错误',
|
||||
// data: null,
|
||||
// })
|
||||
// }
|
||||
// if (error.response.status === 401) {
|
||||
// return Promise.resolve({
|
||||
// code: '009900',
|
||||
// msg: '用户名或密码不正确',
|
||||
// data: null,
|
||||
// })
|
||||
// }
|
||||
// const { data } = error.response
|
||||
// if (data.code !== undefined) {
|
||||
// return Promise.resolve({
|
||||
// code: data.code,
|
||||
// msg: data.msg,
|
||||
// })
|
||||
// }
|
||||
// return Promise.resolve({
|
||||
// code: '009900',
|
||||
// msg: data.msg,
|
||||
// data: null,
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
export const axiosInstance: AxiosInstance = axios.create({
|
||||
baseURL: ENV.API,
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
// console.log('config', config)
|
||||
// if (config.url === 'https://api.fanyi.baidu.com/api/trans/vip/translate') {
|
||||
// config.url = '/baidu'
|
||||
// }
|
||||
return config
|
||||
},
|
||||
error => Promise.reject(error),
|
||||
)
|
||||
|
||||
/**
|
||||
* @apiDescription 封装的网络请求方法
|
||||
* @apiGroup
|
||||
* @apiName request
|
||||
* @apiParam url 地址
|
||||
* @apiParam data 请求数据
|
||||
* @apiParam params 请求参数
|
||||
* @apiParam method 方法类型:get或者post
|
||||
* @apiParam version 接口版本号
|
||||
* @apiParamExample
|
||||
* request('Appointment/appointmentList', data, params, CONSTANT.GET)
|
||||
* @apiReturn Promise
|
||||
*/
|
||||
// async function request(url, data = {}, params = {}, method = CONSTANT.POST, version = Config.API_VERSION) {
|
||||
// // console.log(url)
|
||||
// if (method === CONSTANT.POST) {
|
||||
// data.userId = store.state.user.userInfo === null ? '' : store.state.user.userInfo.id
|
||||
// } else {
|
||||
// params.userId = store.state.user.userInfo === null ? '' : store.state.user.userInfo.id
|
||||
// }
|
||||
// return instance({
|
||||
// url: version + url,
|
||||
// method,
|
||||
// data,
|
||||
// params,
|
||||
// })
|
||||
// }
|
||||
axiosInstance.interceptors.response.use(
|
||||
// 响应正常的处理
|
||||
(response) => {
|
||||
// console.log(response.data)
|
||||
const {data} = response
|
||||
if (response.status !== 200) {
|
||||
Toast.warning(response.statusText)
|
||||
return Promise.reject(data)
|
||||
}
|
||||
if (data === null) {
|
||||
return Promise.resolve({
|
||||
code: 500,
|
||||
msg: '系统出现错误',
|
||||
data: {},
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
if (typeof data !== 'object') {
|
||||
return Promise.resolve({
|
||||
data,
|
||||
success: true,
|
||||
code: 200
|
||||
})
|
||||
}
|
||||
return Promise.resolve(data)
|
||||
},
|
||||
// 请求出错的处理
|
||||
(error) => {
|
||||
if (error.response === undefined && error.status === undefined) {
|
||||
return Promise.resolve({
|
||||
code: 500,
|
||||
msg: '服务器响应超时',
|
||||
data: null,
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
if (error.response.status >= 500) {
|
||||
return Promise.resolve({
|
||||
code: 500,
|
||||
msg: '服务器出现错误',
|
||||
data: null,
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
if (error.response.status === 401) {
|
||||
return Promise.resolve({
|
||||
code: 500,
|
||||
msg: '用户名或密码不正确',
|
||||
data: null,
|
||||
})
|
||||
}
|
||||
const {data} = error.response
|
||||
if (data.code !== undefined) {
|
||||
return Promise.resolve({
|
||||
code: data.code,
|
||||
msg: data.msg,
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
code: 500,
|
||||
success: false,
|
||||
msg: data.msg,
|
||||
data: null,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// export default request
|
||||
type AxiosResponse<T> = { code: number, data: T, success: boolean, msg: string }
|
||||
|
||||
async function request<T>(url, data = {}, params = {}, method): Promise<AxiosResponse<T>> {
|
||||
return axiosInstance({
|
||||
url: url,
|
||||
method,
|
||||
data,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export default request
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { SAVE_DICT_KEY, SAVE_SETTING_KEY } from "@/utils/const.ts";
|
||||
import { BaseState, DefaultBaseState } from "@/stores/base.ts";
|
||||
import { getDefaultSettingState, SettingState } from "@/stores/setting.ts";
|
||||
import { Dict, DictId, DictResource, DictType } from "@/types/types.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import {BaseState, DefaultBaseState, useBaseStore} from "@/stores/base.ts";
|
||||
import {getDefaultSettingState, SettingState} from "@/stores/setting.ts";
|
||||
import {Dict, DictId, DictResource, DictType} from "@/types/types.ts";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import dayjs from 'dayjs'
|
||||
import axios from "axios";
|
||||
import { env } from "@/config/ENV.ts";
|
||||
import { nextTick } from "vue";
|
||||
import {ENV, IS_OFFICIAL, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/config/env.ts";
|
||||
import {nextTick} from "vue";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import { getDefaultArticle, getDefaultDict, getDefaultWord } from "@/types/func.ts";
|
||||
import { set } from "idb-keyval";
|
||||
import book_list from "@/assets/book-list.json";
|
||||
import dict_list from "@/assets/dict-list.json";
|
||||
import {getDefaultDict, getDefaultWord} from "@/types/func.ts";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
|
||||
dayjs.extend(duration);
|
||||
@@ -58,141 +54,6 @@ export function checkAndUpgradeSaveDict(val: any) {
|
||||
})
|
||||
return defaultState
|
||||
} else {
|
||||
if (version === 3) {
|
||||
localStorage.setItem('type-word-dict-v3', JSON.stringify(state))
|
||||
set('type-word-dict-v3', JSON.stringify(state))
|
||||
|
||||
let studyDictId = ''
|
||||
if (state.current.index >= 0) {
|
||||
let dict = state.myDictList[state.current.index]
|
||||
if (dict) {
|
||||
studyDictId = dict.id
|
||||
}
|
||||
}
|
||||
|
||||
const safeString = (str) => (typeof str === 'string' ? str.trim() : '');
|
||||
|
||||
function formatWord(dict) {
|
||||
dict.words = dict.words?.map?.(v => {
|
||||
return getDefaultWord({
|
||||
word: v.name,
|
||||
phonetic0: v.usphone,
|
||||
phonetic1: v.ukphone,
|
||||
trans: v.trans.map(line => {
|
||||
const match = line.match(/^([^\s.]+\.?)\s*(.*)$/);
|
||||
if (match) {
|
||||
let pos = safeString(match[1]);
|
||||
let cn = safeString(match[2]);
|
||||
|
||||
// 如果 pos 不是常规词性(不以字母开头),例如 "【名】"
|
||||
if (!/^[a-zA-Z]+\.?$/.test(pos)) {
|
||||
cn = safeString(line); // 整行放到 cn
|
||||
pos = ''; // pos 置空
|
||||
}
|
||||
|
||||
return {pos, cn};
|
||||
}
|
||||
return {pos: '', cn: safeString(line)};
|
||||
})
|
||||
})
|
||||
}) || []
|
||||
dict.statistics = dict.statistics?.map?.(v => {
|
||||
return {
|
||||
startDate: v.startDate,
|
||||
spend: v.endDate - v.startDate,
|
||||
total: v.total,
|
||||
new: v.total,
|
||||
wrong: v.wrongWordNumber
|
||||
}
|
||||
}) || []
|
||||
dict.articles = dict.articles?.map?.(v => {
|
||||
let r = getDefaultArticle({
|
||||
textTranslate: v.textCustomTranslate
|
||||
})
|
||||
checkRiskKey(r, v)
|
||||
return r
|
||||
}) || []
|
||||
}
|
||||
|
||||
state.myDictList.map((v: any) => {
|
||||
try {
|
||||
let currentDictId = v.id
|
||||
let currentType = v.type
|
||||
delete v.type
|
||||
if (['collect', 'simple', 'wrong'].includes(currentType)) {
|
||||
formatWord(v)
|
||||
delete v.id
|
||||
delete v.name
|
||||
if (currentType === 'collect') {
|
||||
if (v.words.length) {
|
||||
if (currentDictId === studyDictId) defaultState.word.studyIndex = 0
|
||||
checkRiskKey(defaultState.word.bookList[0], cloneDeep(v))
|
||||
defaultState.word.bookList[0].length = v.words.length
|
||||
}
|
||||
if (v.articles.length) {
|
||||
if (currentDictId === studyDictId) defaultState.article.studyIndex = 0
|
||||
checkRiskKey(defaultState.article.bookList[0], cloneDeep(v))
|
||||
defaultState.article.bookList[0].length = v.articles.length
|
||||
}
|
||||
}
|
||||
if (currentType === 'simple' || currentType === 'skip') {
|
||||
if (currentDictId === studyDictId) defaultState.word.studyIndex = 2
|
||||
checkRiskKey(defaultState.word.bookList[2], v)
|
||||
defaultState.word.bookList[2].length = v.words.length
|
||||
}
|
||||
if (currentType === 'wrong') {
|
||||
if (currentDictId === studyDictId) defaultState.word.studyIndex = 1
|
||||
checkRiskKey(defaultState.word.bookList[1], v)
|
||||
defaultState.word.bookList[1].length = v.words.length
|
||||
}
|
||||
}
|
||||
if (currentType === 'word') {
|
||||
if (v.isCustom) {
|
||||
formatWord(v)
|
||||
let dict = getDefaultDict({custom: true})
|
||||
checkRiskKey(dict, v)
|
||||
dict.length = dict.words.length
|
||||
defaultState.word.bookList.push(dict)
|
||||
if (currentDictId === studyDictId) defaultState.word.studyIndex = defaultState.word.bookList.length - 1
|
||||
} else {
|
||||
//当时把选中的词典的id设为随机了,导致通过id找不到
|
||||
let r: any = dict_list.flat().find(a => a.name === v.name)
|
||||
if (r) {
|
||||
formatWord(v)
|
||||
let dict = getDefaultDict(r)
|
||||
checkRiskKey(dict, v)
|
||||
dict.id = r.id
|
||||
defaultState.word.bookList.push(dict)
|
||||
if (currentDictId === studyDictId) defaultState.word.studyIndex = defaultState.word.bookList.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentType === 'article') {
|
||||
if (v.isCustom) {
|
||||
formatWord(v)
|
||||
let dict = getDefaultDict({custom: true})
|
||||
checkRiskKey(dict, v)
|
||||
dict.length = dict.articles.length
|
||||
defaultState.article.bookList.push(dict)
|
||||
if (currentDictId === studyDictId) defaultState.article.studyIndex = defaultState.article.bookList.length - 1
|
||||
} else {
|
||||
//当时把选中的词典的id设为随机了
|
||||
let r: any = book_list.flat().find(a => a.name === v.name)
|
||||
if (r) {
|
||||
formatWord(v)
|
||||
let dict = getDefaultDict(r)
|
||||
checkRiskKey(dict, v)
|
||||
dict.id = r.id
|
||||
defaultState.article.bookList.push(dict)
|
||||
if (currentDictId === studyDictId) defaultState.article.studyIndex = defaultState.article.bookList.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('升级数据失败!', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
checkRiskKey(defaultState, state)
|
||||
return defaultState
|
||||
}
|
||||
@@ -263,22 +124,7 @@ export function shakeCommonDict(n: BaseState): BaseState {
|
||||
}
|
||||
|
||||
export function isMobile(): boolean {
|
||||
return /Mobi|Android|iPhone/i.test(navigator.userAgent)
|
||||
return (
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
);
|
||||
}
|
||||
|
||||
export async function getDictFile(url: string) {
|
||||
try {
|
||||
const r = await fetch(url);
|
||||
return await r.json();
|
||||
} catch (err) {
|
||||
console.log('getDictFile_error', err);
|
||||
return null;
|
||||
}
|
||||
return /Mobi|iPhone|Android|ipad|tablet/i.test(window.navigator.userAgent)
|
||||
}
|
||||
|
||||
export function useNav() {
|
||||
@@ -316,81 +162,6 @@ export function msToMinute(ms) {
|
||||
return `${Math.floor(dayjs.duration(ms).asMinutes())}分钟`;
|
||||
}
|
||||
|
||||
|
||||
export function _fetch(url: string) {
|
||||
return new Promise<any[]>(async (resolve, reject) => {
|
||||
await fetch(url).then(async r => {
|
||||
let v = await r.json()
|
||||
resolve(v)
|
||||
}).catch(r => {
|
||||
console.log('err', r)
|
||||
reject(r)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function _checkDictWords(dict: Dict) {
|
||||
if ([DictType.collect,
|
||||
DictType.known,
|
||||
DictType.wrong].includes(dict.dictType)) {
|
||||
} else {
|
||||
//TODO 需要和其他需要下载的地方统一
|
||||
//如果不是自定义词典,并且有url地址才去下载
|
||||
if (!dict.custom && dict.fileName) {
|
||||
// let rrr = await axios('http://localhost/static/dict/en/zh/Top50Prepositions-v1.json')
|
||||
// console.log('r', rrr)
|
||||
// return
|
||||
let url = `http://localhost/index.php/v1/support/getDictFile?id=${dict.id}&v=${dict.version}`
|
||||
// let res: any = await axios(`http://localhost/index.php/v1/support/getDictFile?id=2`)
|
||||
let res: any
|
||||
try {
|
||||
res = await axios(url)
|
||||
} catch (err) {
|
||||
console.log('err', err)
|
||||
}
|
||||
console.log('res', res)
|
||||
//说明重定向了
|
||||
let r
|
||||
if (res && res.request.responseURL !== url) {
|
||||
r = res.data
|
||||
} else {
|
||||
let dictLocalUrl = `./dicts/${dict.language}/${dict.type}/${dict.translateLanguage}/${dict.url}`;
|
||||
let r3 = await fetch(dictLocalUrl)
|
||||
try {
|
||||
r = await r3.json()
|
||||
} catch (e) {
|
||||
}
|
||||
console.log('r', r)
|
||||
}
|
||||
// // dict.words = Object.freeze(v)
|
||||
// dict.words = v
|
||||
dict = Object.assign(dict, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWordDictList() {
|
||||
let url = `${env.api}/v1/support/getWordDictListFile?v=${env.word_dict_list_version}`
|
||||
let res: any = await axios(url)
|
||||
// let res: any = await axios(`http://localhost/index.php/v1/support/getDictFile?id=2`)
|
||||
console.log('res', res)
|
||||
//说明重定向了
|
||||
let r
|
||||
if (res.request.responseURL !== url) {
|
||||
r = res.data
|
||||
} else {
|
||||
let dictLocalUrl = `./word_dict_list.json`;
|
||||
let r3 = await fetch(dictLocalUrl)
|
||||
try {
|
||||
let r1 = await r3.json()
|
||||
r = r1.data
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
//获取完成天数
|
||||
export function _getAccomplishDays(total: number, dayNumber: number) {
|
||||
return Math.ceil(total / dayNumber)
|
||||
@@ -453,7 +224,7 @@ export async function _getDictDataByUrl(val: DictResource, type: DictType = Dict
|
||||
if (type === DictType.article) {
|
||||
dictResourceUrl = `/dicts/${val.language}/article/${val.url}`;
|
||||
}
|
||||
let s = await getDictFile(dictResourceUrl)
|
||||
let s = await fetch(resourceWrap(dictResourceUrl, val.version)).then(r => r.json())
|
||||
if (s) {
|
||||
if (type === DictType.word) {
|
||||
return getDefaultDict({...val, words: s})
|
||||
@@ -667,3 +438,16 @@ export function total(arr, key) {
|
||||
return a
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function resourceWrap(resource: string, version?: number) {
|
||||
if (IS_OFFICIAL) {
|
||||
if (resource.includes('.json')) resource = resource.replace('.json', '');
|
||||
if (!resource.includes('http')) resource = RESOURCE_PATH + resource
|
||||
if (version === undefined) {
|
||||
const store = useBaseStore()
|
||||
return `${resource}_v${store.dictListVersion}.json`
|
||||
}
|
||||
return `${resource}_v${version}.json`
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
Reference in New Issue
Block a user