Merge branch 'zyronon:master' into master

This commit is contained in:
SMGoro
2025-10-28 17:48:10 +08:00
committed by GitHub
63 changed files with 6096 additions and 6311 deletions

1
components.d.ts vendored
View File

@@ -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
View File

@@ -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",

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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"
}
]

View 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"
}
]

View File

@@ -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"
}
]
]

View File

@@ -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'

View File

@@ -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() {
// 配置区:改成你的

View File

@@ -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
View 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
})
}

View File

@@ -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"
}
]
]

View File

@@ -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">

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -14,7 +14,7 @@ const props = withDefaults(defineProps<IProps>(), {
showText: true,
textInside: false,
strokeWidth: 6,
color: '#409eff',
color: '#93ADE3',
format: (percentage) => `${percentage}%`,
});

View File

@@ -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
})

View File

@@ -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
View 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
}

View File

@@ -1,5 +1,6 @@
// src/directives/loading.js
import {createApp, h} from 'vue'
//@ts-ignore
import IconEosIconsLoading from '~icons/eos-icons/loading'
// 创建一个 Loading 组件

View File

@@ -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

View File

@@ -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') {

View File

@@ -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([])

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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()

View File

@@ -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)

View File

@@ -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>

View File

@@ -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)
}
}
}
}

View File

@@ -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

View File

@@ -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'))

View File

@@ -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>

View File

@@ -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,

View File

@@ -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";

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
View 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
View 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>

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"
>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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 === ' ' ? '&nbsp;' : '_')).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);
}

View File

@@ -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")},

View File

@@ -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) {

View File

@@ -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)
})

View File

@@ -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 ?? []),
}
}

View File

@@ -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
}

View File

@@ -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
})
}

View File

@@ -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
}

View File

@@ -1,4 +1,3 @@
import {SAVE_SETTING_KEY} from "@/utils/const";
export default {
$notice(val) {

View File

@@ -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

View File

@@ -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;
}