This commit is contained in:
Zyronon
2025-12-18 00:26:42 +08:00
parent 75e956b46d
commit 4199f4b53a
8 changed files with 272 additions and 217 deletions

View File

@@ -22,13 +22,13 @@ const props = withDefaults(defineProps<{
exportLoading?: boolean
importLoading?: boolean
request?: Function
list?: any[]
}>(), {
loading: true,
showToolbar: true,
showPagination: true,
exportLoading: false,
importLoading: false,
request: () => void 0,
})
const emit = defineEmits<{
@@ -59,7 +59,7 @@ function scrollToTop() {
function scrollToItem(index: number) {
nextTick(() => {
listRef?.children[index]?.scrollIntoView({block: 'center', behavior: 'smooth'})
listRef?.children[index]?.scrollIntoView({ block: 'center', behavior: 'smooth' })
})
}
@@ -140,11 +140,15 @@ function cancelSearch() {
}
async function getData() {
loading2 = true
let {list, total} = await props.request(params)
params.list = list
params.total = total
loading2 = false
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) {
@@ -155,177 +159,182 @@ function handlePageNo(e) {
onMounted(async () => {
getData()
})
defineRender(
() => {
const d = (item) => <Checkbox
modelValue={selectIds.includes(item.id)}
onChange={() => toggleSelect(item)}
size="large"/>
() => {
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>
return (
<div class="flex flex-col gap-3">
{
showSearchInput ? (
<div class="flex gap-4">
<BaseInput
clearable
modelValue={params.searchKey}
onUpdate:modelValue={debounce(e => search(e), 500)}
class="flex-1"
autofocus>
{{
subfix: () => <IconFluentSearch24Regular
class="text-lg text-gray"
/>
}}
</BaseInput>
<BaseButton onClick={cancelSearch}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between">
<div class="flex gap-2 items-center">
<Checkbox
disabled={!params.list.length}
onChange={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<span>{selectIds.length} / {params.total}</span>
</div>
props.showToolbar && <div>
{
showSearchInput ? (
<div class="flex gap-4">
<BaseInput
clearable
modelValue={params.searchKey}
onUpdate:modelValue={debounce(e => search(e), 500)}
class="flex-1"
autofocus>
{{
subfix: () => <IconFluentSearch24Regular
class="text-lg text-gray"
/>
}}
</BaseInput>
<BaseButton onClick={cancelSearch}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between">
<div class="flex gap-2 items-center">
<Checkbox
disabled={!params.list.length}
onChange={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<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('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>
}
{
loading2 ?
<div class="h-full w-full center text-4xl">
<IconEosIconsLoading color="gray"/>
</div>
: 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 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={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}/>
}
{
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('import', 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>

View File

@@ -44,20 +44,7 @@ defineExpose({ scrollToBottom, scrollToItem })
<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>
<WordItem :item="item"/>
</template>
<template v-slot:suffix="{ item, index }">
<slot name="suffix" :item="item" :index="index"></slot>

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

View File

@@ -1,44 +1,24 @@
<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";

View File

@@ -468,7 +468,6 @@ function onSort(type: Sort, pageNo: number, pageSize: number) {
}
}
defineRender(() => {
return (
<BasePage>
@@ -543,7 +542,6 @@ defineRender(() => {
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
</div>
)
}}
@@ -688,7 +686,7 @@ defineRender(() => {
}
.word-list-section {
width: 40%;
width: 44%;
}
.edit-section {

View File

@@ -4,12 +4,24 @@ import BaseTable from "@/components/BaseTable.vue";
import WordItem from "@/components/WordItem.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]
}>()
@@ -24,8 +36,7 @@ defineEmits<{
<div class="py-4 h-80vh w-30rem">
<BaseTable
class="h-full"
:list='runtimeStore.editDict.words'
:loading='false'
:request="requestList"
:show-toolbar="false"
>
<template v-slot="item">

View File

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