This commit is contained in:
Zyronon
2025-12-19 01:45:13 +08:00
parent 36ddd399b6
commit eba448dbd5
22 changed files with 506 additions and 783 deletions

12
.editorconfig Normal file
View 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
View File

@@ -0,0 +1,2 @@
dist
node_modules

9
.prettierrc Normal file
View File

@@ -0,0 +1,9 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "es5",
"arrowParens": "avoid",
"bracketSpacing": true
}

View File

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

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

View File

@@ -325,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 {
@@ -378,6 +378,7 @@ a {
align-items: center;
gap: .5rem;
color: var(--color-main-text);
flex-wrap: wrap;
span {
flex-shrink: 0;
@@ -385,7 +386,6 @@ a {
.word {
font-size: 1.2rem;
display: flex;
}
.phonetic {
@@ -535,4 +535,4 @@ a {
.base-button + .base-button {
margin-left: 0 !important;
}
}
}

View File

@@ -1,313 +0,0 @@
<script setup lang="tsx">
import { nextTick, 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 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')
const props = withDefaults(defineProps<{
loading?: boolean
showToolbar?: boolean
showPagination?: boolean
exportLoading?: boolean
importLoading?: boolean
del?: Function
batchDel?: Function
add?: Function
total: number
}>(), {
loading: true,
showToolbar: true,
showPagination: true,
exportLoading: false,
importLoading: false,
del: () => void 0,
add: () => void 0,
batchDel: () => void 0
})
const emit = defineEmits<{
click: [val: {
item: any,
index: number
}],
importData: [e: Event]
exportData: []
}>()
let listRef: any = $ref()
function scrollToBottom() {
nextTick(() => {
listRef?.scrollTo(0, listRef.scrollHeight)
})
}
function scrollToTop() {
nextTick(() => {
listRef?.scrollTo(0, 0)
})
}
function scrollToItem(index: number) {
nextTick(() => {
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
})
function toggleSelect(item) {
let rIndex = selectIds.findIndex(v => v === item.id)
if (rIndex > -1) {
selectIds.splice(rIndex, 1)
} else {
selectIds.push(item.id)
}
}
function toggleSelectAll() {
if (selectAll) {
selectIds = []
} else {
selectIds = currentList.map(v => v.id)
}
}
let searchKey = $ref('')
let showSortDialog = $ref(false)
let showSearchInput = $ref(false)
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))
}
showSortDialog = false
}
function handleBatchDel() {
props.batchDel(selectIds)
selectIds = []
}
function handlePageNo(e) {
pageNo = e
scrollToTop()
}
const s = useSlots()
defineExpose({
scrollToBottom,
scrollToItem,
closeImportDialog
})
defineRender(
() => {
const d = (item) => <Checkbox
modelValue={selectIds.includes(item.id)}
onChange={() => toggleSelect(item)}
size="large"/>
return (
<div class="flex flex-col gap-3">
{
props.showToolbar && <div>
{
showSearchInput ? (
<div class="flex gap-4">
<BaseInput
clearable
modelValue={searchKey}
onUpdate:modelValue={debounce(e => searchKey = e)}
class="flex-1"
autofocus>
{{
subfix: () => <IconFluentSearch24Regular
class="text-lg text-gray"
/>
}}
</BaseInput>
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between">
<div class="flex gap-2 items-center">
<Checkbox
disabled={!currentList.length}
onChange={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<span>{selectIds.length} / {list.value.length}</span>
</div>
<div class="flex gap-2 relative">
{
selectIds.length ?
<PopConfirm title="确认删除所有选中数据?"
onConfirm={handleBatchDel}
>
<BaseIcon
class="del"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
: null
}
<BaseIcon
onClick={() => showImportDialog = true}
title="导入">
<IconSystemUiconsImport/>
</BaseIcon>
<BaseIcon
onClick={() => emit('exportData')}
title="导出">
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
</BaseIcon>
<BaseIcon
onClick={props.add}
title="添加单词">
<IconFluentAdd20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
title="改变顺序"
onClick={() => showSortDialog = !showSortDialog}
>
<IconFluentArrowSort20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
onClick={() => showSearchInput = !showSearchInput}
title="搜索">
<IconFluentSearch20Regular/>
</BaseIcon>
<MiniDialog
modelValue={showSortDialog}
onUpdate:modelValue={e => showSortDialog = e}
style="width: 8rem;"
>
<div class="mini-row-title">
列表顺序设置
</div>
<div class="mini-row">
<BaseButton size="small" onClick={() => sort(Sort.reverse)}>翻转
</BaseButton>
<BaseButton size="small" onClick={() => sort(Sort.random)}>随机</BaseButton>
</div>
</MiniDialog>
</div>
</div>
)
}
</div>
}
{
props.loading ?
<div class="h-full w-full center text-4xl">
<IconEosIconsLoading color="gray"/>
</div>
: currentList.length ? (
<>
<div class="flex-1 overflow-auto"
ref={e => listRef = e}>
{currentList.map((item, index) => {
return (
<div class="list-item-wrapper"
key={item.word}
>
{s.default({ checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1 })}
</div>
)
})}
</div>
{
props.showPagination && <div class="flex justify-end">
<Pagination
currentPage={pageNo}
onUpdate:current-page={handlePageNo}
pageSize={pageSize}
onUpdate:page-size={(e) => pageSize = e}
pageSizes={[20, 50, 100, 200]}
layout="prev, pager, next"
total={props.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>
</div>
</Dialog>
</div>
)
}
)
</script>
<style scoped lang="scss">
</style>

View File

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

View File

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

View File

@@ -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
},
]
}>()
//虚拟列表长度限制
@@ -41,35 +46,45 @@ function scrollViewToCenter(index: number) {
if (props.list.length > limit) {
listRef?.scrollToItem(index)
} else {
listRef?.children[index]?.scrollIntoView({block: 'center', behavior: 'smooth'})
listRef?.children[index]?.scrollIntoView({ block: 'center', behavior: 'smooth' })
}
})
}
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)
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(() => {
@@ -86,91 +101,51 @@ function scrollToItem(index: number) {
if (props.list.length > limit) {
listRef?.scrollToItem(index)
} else {
listRef?.children[index]?.scrollIntoView({block: 'center', behavior: 'smooth'})
listRef?.children[index]?.scrollIntoView({ block: 'center', behavior: 'smooth' })
}
})
}
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})
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 class="list-item-wrapper" v-for="(item, index) in props.list" :key="item.title">
<div @click="emit('click', { item, index })">
<slot :item="item" :index="index" :active="itemIsActive(item, index)"></slot>
</div>
</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 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 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 @click="emit('click', { item, index })">
<slot :item="item" :index="index" :active="itemIsActive(item, index)"></slot>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.scroller {
flex: 1;
//padding: 0 var(--space);

View File

@@ -1,11 +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 WordItem from "@/components/WordItem.vue";
import { Word } from "@/types/types.ts";
import WordItem from "../WordItem.vue";
withDefaults(defineProps<{
list: Word[],
@@ -19,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)
@@ -32,22 +28,18 @@ function scrollToItem(index: number) {
listRef?.scrollToItem(index)
}
const playWordAudio = usePlayWordAudio()
defineExpose({ scrollToBottom, scrollToItem })
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 }">
<WordItem :item="item"/>
</template>
<template v-slot:suffix="{ item, index }">
<slot name="suffix" :item="item" :index="index"></slot>
</template>
<BaseList
ref="listRef"
@click="(e:any) => emit('click',e)"
:list="list"
v-bind="$attrs">
<template v-slot="{ item, index, active }">
<WordItem :item="item" :index="index" :active="active" />
</template>
</BaseList>
</template>

View File

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

View File

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

View File

@@ -533,18 +533,6 @@ provide('currentPractice', currentPractice)
@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>
</ArticleList>
</div>
</Panel>

View File

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

View File

@@ -1,10 +1,9 @@
<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";
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>
@@ -13,17 +12,20 @@ import ConflictNoticeText from "@/components/ConflictNoticeText.vue";
<div class="card qa w-2/3">
<div class="font-bold text-2xl mb-6">常见问题解答</div>
<div class="list">
<Collapse q="网站是免费的吗?" :a="[
'不完全免费,因为想要长久发展后续收费是必然的,但不会必须付费才可用,我们尽量在免费与收费之间找到一个平衡点',
// '不登录依然可以使用大部分功能,但数据需要自己管理,如需多设备使用则需要自行导入导出',
// '登录后提供官方词典/书籍的数据同步功能,如需要同步自定义词典/书籍,则需要开通会员,同时会提供更多的学习内容和功能',
'项目是开源的,可自行部署'
]"/>
<Collapse
q="网站是免费的吗?"
:a="[
'不完全免费,因为想要长久发展后续收费是必然的,但不会必须付费才能使用,我们尽量在免费与收费之间找一个平衡点',
// '不登录依然可以使用大部分功能,但数据需要自己管理,如需多设备使用则需要自行导入导出',
// '登录后提供官方词典/书籍的数据同步功能,如需要同步自定义词典/书籍,则需要开通会员,同时会提供更多的学习内容和功能',
'项目是开源的,可自行部署',
]"
/>
<div class="line"></div>
<Collapse q="无法输入,按下键盘没有反应?">
<ConflictNoticeText/>
<ConflictNoticeText />
</Collapse>
<div class="line"></div>
@@ -31,103 +33,118 @@ import ConflictNoticeText from "@/components/ConflictNoticeText.vue";
<Collapse q="数据在哪?怎么在多台电脑/设备之间使用?">
<div>
1. 所有用户数据
<b class="text-red">保存在本地浏览器中</b>如果您需要在不同的设备浏览器上使用 {{ APP_NAME }}
您需要手动进行数据导出和导入
<b class="text-red">保存在本地浏览器中</b>如果您需要在不同的设备浏览器上使用
{{ APP_NAME }} 您需要手动进行数据导出和导入
</div>
<p>
2. 设置 -> 数据设置 -> 在原电脑上导出数据 -> 通过社交软件发送给新电脑 -> 在新电脑上导入
</p>
<p>
3. 正在开发账户体系
2. 设置 -> 数据设置 -> 在原电脑上导出数据 -> 通过社交软件发送给新电脑 ->
在新电脑上导入
</p>
<p>3. 正在开发账户体系</p>
</Collapse>
<div class="line"></div>
<Collapse q="网站自动规划的单词数量太多了,怎么修改?">
<p>
1. 默认复习词数量与新词数量是14如果新词40个那么会复习40个上次学习的复习120个之前学习的由近到远
</p>
<p>
2. 您可在通过 设置 -> 单词设置 -> 复习比 修改
1.
默认复习词数量与新词数量是14如果新词40个那么会复习40个上次学习的复习120个之前学习的由近到远
</p>
<p>2. 您可在通过 设置 -> 单词设置 -> 复习比 修改</p>
</Collapse>
<div class="line"></div>
<Collapse q="完成一次学习要很久,流程是不是太冗长了?" :a="[
'这的确是个问题,冗长的流程容易让人失去背单词的积极性,我正在思考如何优化学习流程,如果您有好的建议欢迎反馈',
'错误单词会重新再来如果只是手误按错了后续重新练习时可以按Tab键跳过。无法判断用户是手误还是真的不会所以只能错词统统重来直到正确为止',
'复习时只有选择了不认识的单词才会要求听写与默写这是合理的不过目前会出现同一个单词复习了N遍的问题',
'上线了账户体系之后,会添加艾宾浩斯的记忆曲线功能,到时候规划的复习单词会比现在更智能'
]"
<Collapse
q="完成一次学习要很久,流程是不是太冗长了?"
:a="[
'这的确是个问题,冗长的流程容易让人失去背单词的积极性,我正在思考如何优化学习流程,如果您有好的建议欢迎反馈',
'错误单词会重新再来如果只是手误按错了后续重新练习时可以按Tab键跳过。无法判断用户是手误还是真的不会所以只能错词统统重来直到正确为止',
'复习时只有选择了不认识的单词才会要求听写与默写这是合理的不过目前会出现同一个单词复习了N遍的问题',
'上线了账户体系之后,会添加艾宾浩斯的记忆曲线功能,到时候规划的复习单词会比现在更智能',
]"
/>
<div class="line"></div>
<Collapse q="会添加艾宾浩斯的记忆曲线功能吗?" :a="[
'上线了账户体系之后,会添加艾宾浩斯的记忆曲线功能'
]"
<Collapse
q="会添加艾宾浩斯的记忆曲线功能吗?"
:a="['上线了账户体系之后,会添加艾宾浩斯的记忆曲线功能']"
/>
<div class="line"></div>
<Collapse q="能否 自行添加单词/自定义词典/导入自己的单词/修改单词内容?" :a="[
'可以',
'在单词界面,点击“创建个人词典”',
'创建完成之后,在词典详情页面,点击 “添加单词” 图标,即可添加自己的单词',
'也可以点击 “导入” 图标批量导入需要严格按照模板xlsx格式来'
]"/>
<Collapse
q="能否 自行添加单词/自定义词典/导入自己的单词/修改单词内容?"
:a="[
'可以',
'在单词界面,点击“创建个人词典”',
'创建完成之后,在词典详情页面,点击 “添加单词” 图标,即可添加自己的单词',
'也可以点击 “导入” 图标批量导入需要严格按照模板xlsx格式来',
]"
/>
<div class="line"></div>
<Collapse q="能否 自行添加文章/自定义书籍/导入自己的文章/修改文章内容?" :a="[
'可以,操作步骤基本和添加单词的一样',
'在文章界面,点击“创建个人书籍”',
'创建完成之后,在书籍详情页面,点击顶部的 “文章管理” 按钮,即可添加自己的文章',
]"/>
<Collapse
q="能否 自行添加文章/自定义书籍/导入自己的文章/修改文章内容?"
:a="[
'可以,操作步骤基本和添加单词的一样',
'在文章界面,点击“创建个人书籍”',
'创建完成之后,在书籍详情页面,点击顶部的 “文章管理” 按钮,即可添加自己的文章',
]"
/>
<div class="line"></div>
<Collapse q="怎么关闭按键音?" :a="[
'设置 -> 通用设置 -> 音效 -> 按键音,关闭即可',
]"/>
<Collapse q="怎么关闭按键音?" :a="['设置 -> 通用设置 -> 音效 -> 按键音,关闭即可']" />
<div class="line"></div>
<Collapse q="平板能用吗?" :a="[
'平板可以使用但使用蓝牙键盘体验会更好毕竟系统自带的虚拟键盘占了1/3的屏幕空间比较影响观感',
'连接蓝牙键盘',
'安卓平板,需要开启 “电脑模式”iPad无需此操作',
]"/>
<Collapse
q="平板能用吗?"
:a="[
'平板可以使用但使用蓝牙键盘体验会更好毕竟系统自带的虚拟键盘占了1/3的屏幕空间比较影响观感',
'连接蓝牙键盘',
'安卓平板,需要开启 “电脑模式”iPad无需此操作',
]"
/>
<div class="line"></div>
<Collapse q="手机能用吗?" :a="[
'手机可以使用,但暂时未进行其针对优化,使用起来可能会有不方便的地方,还是建议在电脑或平板上用'
]"/>
<Collapse
q="手机能用吗?"
:a="[
'手机可以使用,但暂时未进行其针对优化,使用起来可能会有不方便的地方,还是建议在电脑或平板上用',
]"
/>
<div class="line"></div>
<Collapse q="有APP、小程序吗" a="无,只有网站"/>
<Collapse q="有APP、小程序吗" a="无,只有网站" />
<div class="line"></div>
<Collapse q="如何向开发团队反馈问题和功能需求?" :a="[
'可以加入我们官方 微信 群, 详细的描述您想要的功能以及告知这个功能想要解决的问题是什么',
'如果您在应用中发现了错误或漏洞,请提供详细的描述和重现问题的步骤,当然最好提供一个小视频',
'也可以给我们提工单',
'也可以去 github/issues 提交'
]">
<div class="flex items-center">微信群
<WeChat/>
<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>
工单反馈<a :href="`https://v.wjx.cn/vm/ev0W7fv.aspx#`" target="_blank"
>https://v.wjx.cn/vm/ev0W7fv.aspx#</a
>
</div>
</Collapse>
</div>
@@ -136,5 +153,4 @@ import ConflictNoticeText from "@/components/ConflictNoticeText.vue";
</BasePage>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -211,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) {
@@ -373,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">
@@ -527,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 {

View File

@@ -1,13 +1,31 @@
<script setup lang="tsx">
import { DictId, Sort } from "@/types/types.ts";
import { detail } from "@/apis";
import BackIcon from "@/components/BackIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import BasePage from "@/components/BasePage.vue";
import { computed, onMounted, reactive, ref, shallowReactive, watch } from "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 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,
cloneDeep,
convertToWord,
isMobile,
loadJsLib,
@@ -15,29 +33,10 @@ import {
shuffle,
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 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 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 DeleteIcon from "@/components/icon/DeleteIcon.vue";
import { getCurrentStudyWord } from "@/hooks/dict.ts";
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
import { useSettingStore } from "@/stores/setting.ts";
import { MessageBox } from "@/utils/MessageBox.tsx";
import { 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()
@@ -45,7 +44,7 @@ const router = useRouter()
const route = useRoute()
const isMob = isMobile()
let loading = $ref(false)
let list2 = $ref([])
let allList = $ref([])
const getDefaultFormWord = () => {
return {
@@ -74,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
@@ -84,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)
@@ -100,7 +102,7 @@ async function onSubmitWord() {
let data: any = convertToWord(wordForm)
//todo 可以检查的更准确些比如json对比
if (data.id) {
let r = list2.find(v => v.id === data.id)
let r = allList.find(v => v.id === data.id)
if (r) {
Object.assign(r, data)
Toast.success('修改成功')
@@ -111,11 +113,11 @@ async function onSubmitWord() {
} else {
data.id = nanoid(6)
data.checked = false
let r = list2.find(v => v.word === wordForm.word)
let r = allList.find(v => v.word === wordForm.word)
if (r) {
Toast.warning('已有相同名称单词!')
return
} else list2.push(data)
} else allList.push(data)
Toast.success('添加成功')
wordForm = getDefaultFormWord()
}
@@ -128,14 +130,15 @@ async function onSubmitWord() {
function batchDel(ids: string[]) {
ids.map(id => {
let rIndex2 = list2.findIndex(v => v.id === id)
let rIndex2 = allList.findIndex(v => v.id === id)
if (rIndex2 > -1) {
if (id === wordForm.id) {
wordForm = getDefaultFormWord()
}
list2.splice(rIndex2, 1)
allList.splice(rIndex2, 1)
}
})
tableRef.value.getData()
syncDictInMyStudyList()
}
@@ -189,7 +192,7 @@ 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
@@ -210,11 +213,12 @@ onMounted(async () => {
}
}
}
list2 = runtimeStore.editDict.words
loading = false
tableRef.value.getData()
}
}
allList = runtimeStore.editDict.words
tableRef.value.getData()
})
function formClose() {
@@ -439,10 +443,10 @@ async function requestList({pageNo, pageSize, searchKey}) {
if (AppEnv.CAN_REQUEST) {
} else {
let list = list2
let total = list2.length
let list = allList
let total = allList.length
if (searchKey.trim()) {
list = list2.filter(v => v.word.toLowerCase().includes(searchKey.trim().toLowerCase()))
list = allList.filter(v => v.word.toLowerCase().includes(searchKey.trim().toLowerCase()))
total = list.length
}
list = list.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
@@ -459,12 +463,13 @@ function onSort(type: Sort, pageNo: number, pageSize: number) {
} else if ([Sort.random, Sort.randomAll].includes(type)) {
fun = shuffle
}
list2 = list2.slice(0, pageSize * (pageNo - 1))
.concat(fun(list2.slice(pageSize * (pageNo - 1), pageSize * (pageNo - 1) + pageSize)))
.concat(list2.slice(pageSize * (pageNo - 1) + pageSize))
runtimeStore.editDict.words = list2
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()
}
}
@@ -522,7 +527,10 @@ defineRender(() => {
(val) =>
<WordItem
showTransPop={false}
item={val.item}>
showCollectIcon={false}
showMarkIcon={false}
item={val.item}
>
{{
prefix: () => val.checkbox(val.item),
suffix: () => (

View File

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

View File

@@ -33,7 +33,7 @@ defineEmits<{
<Dialog v-model="model"
padding
title="修改学习进度">
<div class="py-4 h-80vh w-30rem">
<div class="py-4 h-80vh ">
<BaseTable
class="h-full"
:request="requestList"
@@ -42,11 +42,11 @@ defineEmits<{
<template v-slot="item">
<WordItem
@click="$emit('ok',item.index)"
:item="item.item" :show-translate="false">
<template v-slot:prefix>
{{ item.index }}
</template>
</WordItem>
:item="item.item"
:show-translate="false"
:index="item.index"
:show-option="false"
/>
</template>
</BaseTable>
</div>

View File

@@ -22,7 +22,8 @@ let showTranslate = $ref(false)
<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>

View File

@@ -32,12 +32,12 @@ watch(() => model.value, (n) => {
: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