wip
This commit is contained in:
@@ -1,51 +1,46 @@
|
||||
<script setup lang="tsx">
|
||||
|
||||
import { nextTick, useSlots } from "vue";
|
||||
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')
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
loading?: boolean
|
||||
showToolbar?: boolean
|
||||
showPagination?: boolean
|
||||
exportLoading?: boolean
|
||||
importLoading?: boolean
|
||||
del?: Function
|
||||
batchDel?: Function
|
||||
add?: Function
|
||||
total: number
|
||||
request?: Function
|
||||
}>(), {
|
||||
loading: true,
|
||||
showToolbar: true,
|
||||
showPagination: true,
|
||||
exportLoading: false,
|
||||
importLoading: false,
|
||||
del: () => void 0,
|
||||
add: () => void 0,
|
||||
batchDel: () => void 0
|
||||
request: () => 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()
|
||||
@@ -64,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
|
||||
@@ -97,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)
|
||||
@@ -109,205 +92,240 @@ 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() {
|
||||
loading2 = true
|
||||
let {list, total} = await props.request(params)
|
||||
params.list = list
|
||||
params.total = total
|
||||
loading2 = false
|
||||
}
|
||||
|
||||
function handlePageNo(e) {
|
||||
params.pageNo = e
|
||||
getData()
|
||||
scrollToTop()
|
||||
}
|
||||
|
||||
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">
|
||||
return (
|
||||
<div class="flex flex-col gap-3">
|
||||
{
|
||||
props.showToolbar && <div>
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
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>
|
||||
}
|
||||
{
|
||||
props.loading ?
|
||||
<div class="h-full w-full center text-4xl">
|
||||
<IconEosIconsLoading color="gray"/>
|
||||
) : (
|
||||
<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>
|
||||
: 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 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 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
{
|
||||
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>
|
||||
{
|
||||
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>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="tsx">
|
||||
|
||||
import { nextTick, onMounted, useSlots } from "vue";
|
||||
import { nextTick, useSlots } from "vue";
|
||||
import { Sort } from "@/types/types.ts";
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -26,7 +27,6 @@ const props = withDefaults(defineProps<{
|
||||
del?: Function
|
||||
batchDel?: Function
|
||||
add?: Function
|
||||
request?: Function
|
||||
total: number
|
||||
}>(), {
|
||||
loading: true,
|
||||
@@ -36,19 +36,16 @@ const props = withDefaults(defineProps<{
|
||||
importLoading: false,
|
||||
del: () => void 0,
|
||||
add: () => void 0,
|
||||
request: () => void 0,
|
||||
batchDel: () => void 0
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
add: []
|
||||
click: [val: {
|
||||
item: any,
|
||||
index: number
|
||||
}],
|
||||
importData: [e: Event]
|
||||
exportData: []
|
||||
sort: [type: Sort,pageNo: number,pageSize: number]
|
||||
}>()
|
||||
|
||||
let listRef: any = $ref()
|
||||
@@ -71,15 +68,22 @@ function scrollToItem(index: number) {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
@@ -93,10 +97,11 @@ function toggleSelectAll() {
|
||||
if (selectAll) {
|
||||
selectIds = []
|
||||
} else {
|
||||
selectIds = list2.map(v => v.id)
|
||||
selectIds = currentList.map(v => v.id)
|
||||
}
|
||||
}
|
||||
|
||||
let searchKey = $ref('')
|
||||
let showSortDialog = $ref(false)
|
||||
let showSearchInput = $ref(false)
|
||||
let showImportDialog = $ref(false)
|
||||
@@ -104,10 +109,13 @@ let showImportDialog = $ref(false)
|
||||
const closeImportDialog = () => showImportDialog = false
|
||||
|
||||
function sort(type: Sort) {
|
||||
if ([Sort.reverse, Sort.random].includes(type)) {
|
||||
emit('sort', type,params.pageNo,params.pageSize)
|
||||
}else{
|
||||
emit('sort', type,1,params.total)
|
||||
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
|
||||
}
|
||||
@@ -118,8 +126,7 @@ function handleBatchDel() {
|
||||
}
|
||||
|
||||
function handlePageNo(e) {
|
||||
params.pageNo = e
|
||||
getData()
|
||||
pageNo = e
|
||||
scrollToTop()
|
||||
}
|
||||
|
||||
@@ -128,223 +135,179 @@ const s = useSlots()
|
||||
defineExpose({
|
||||
scrollToBottom,
|
||||
scrollToItem,
|
||||
closeImportDialog,
|
||||
getData
|
||||
closeImportDialog
|
||||
})
|
||||
|
||||
|
||||
let list2 = $ref([])
|
||||
let loading2 = $ref(false)
|
||||
|
||||
let params = $ref({
|
||||
pageNo: 1,
|
||||
pageSize: 50,
|
||||
total: 0,
|
||||
sortType: null,
|
||||
searchKey: ''
|
||||
})
|
||||
|
||||
function search(key: string) {
|
||||
console.log('key',key)
|
||||
if(!params.searchKey) {
|
||||
params.pageNo = 1
|
||||
}
|
||||
params.searchKey = key
|
||||
getData()
|
||||
}
|
||||
|
||||
function cancelSearch() {
|
||||
params.searchKey = ''
|
||||
showSearchInput = false
|
||||
getData()
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
loading2 = true
|
||||
console.log('params',params);
|
||||
let {list, total} = await props.request(params)
|
||||
console.log('list',list)
|
||||
list2 = list
|
||||
params.total = total
|
||||
loading2 = false
|
||||
}
|
||||
|
||||
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">
|
||||
return (
|
||||
<div class="flex flex-col gap-3">
|
||||
{
|
||||
props.showToolbar && <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={!list2.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('exportData')}
|
||||
title="导出">
|
||||
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={() => emit('add')}
|
||||
title="添加单词">
|
||||
<IconFluentAdd20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!list2.length}
|
||||
title="改变顺序"
|
||||
onClick={() => showSortDialog = !showSortDialog}
|
||||
>
|
||||
<IconFluentArrowSort20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!list2.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>
|
||||
)
|
||||
}
|
||||
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>
|
||||
}
|
||||
{
|
||||
loading2 ?
|
||||
<div class="h-full w-full center text-4xl">
|
||||
<IconEosIconsLoading color="gray"/>
|
||||
) : (
|
||||
<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>
|
||||
: list2.length ? (
|
||||
<>
|
||||
<div class="flex-1 overflow-auto"
|
||||
ref={e => listRef = e}>
|
||||
{list2.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={params.pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={params.pageSize}
|
||||
onUpdate:page-size={(e) => params.pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="prev, pager, next, total"
|
||||
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 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 class="mt-4">
|
||||
<BaseButton
|
||||
onClick={() => {
|
||||
let d: HTMLDivElement = document.querySelector('#upload-trigger')
|
||||
d.click()
|
||||
}}
|
||||
loading={props.importLoading}>导入</BaseButton>
|
||||
<input
|
||||
id="upload-trigger"
|
||||
type="file"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
onChange={e => emit('importData', e)}
|
||||
class="w-0 h-0 opacity-0"/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
{
|
||||
props.loading ?
|
||||
<div class="h-full w-full center text-4xl">
|
||||
<IconEosIconsLoading color="gray"/>
|
||||
</div>
|
||||
: currentList.length ? (
|
||||
<>
|
||||
<div class="flex-1 overflow-auto"
|
||||
ref={e => listRef = e}>
|
||||
{currentList.map((item, index) => {
|
||||
return (
|
||||
<div class="list-item-wrapper"
|
||||
key={item.word}
|
||||
>
|
||||
{s.default({ checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1 })}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{
|
||||
props.showPagination && <div class="flex justify-end">
|
||||
<Pagination
|
||||
currentPage={pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={pageSize}
|
||||
onUpdate:page-size={(e) => pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="prev, pager, next"
|
||||
total={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>
|
||||
<style scoped lang="scss">
|
||||
|
||||
</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>
|
||||
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>
|
||||
|
||||
@@ -24,15 +24,15 @@ let show = $ref(false)
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1" v-if="type === 'word'">
|
||||
<IconFluentTextUnderlineDouble20Regular width="20"/>
|
||||
<span>单词</span>
|
||||
<span>单词设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2" v-if="type === 'article'">
|
||||
<IconFluentBookLetter20Regular width="20"/>
|
||||
<span>文章</span>
|
||||
<span>文章设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
|
||||
<IconFluentSettings20Regular width="20"/>
|
||||
<span>通用</span>
|
||||
<span>通用设置</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user