This commit is contained in:
Zyronon
2026-01-05 19:34:32 +08:00
committed by GitHub
parent 4fd3e51961
commit 0c86906b4d
15 changed files with 819 additions and 3883 deletions

View File

@@ -18,6 +18,7 @@ const props = withDefaults(
defineProps<{
loading?: boolean
showToolbar?: boolean
showCheckbox?: boolean
showPagination?: boolean
exportLoading?: boolean
importLoading?: boolean
@@ -26,6 +27,7 @@ const props = withDefaults(
}>(),
{
loading: true,
showCheckbox: false,
showToolbar: true,
showPagination: true,
exportLoading: false,
@@ -48,6 +50,7 @@ const emit = defineEmits<{
}>()
let listRef: any = $ref()
let showCheckbox = $ref(false)
function scrollToBottom() {
nextTick(() => {
@@ -167,11 +170,7 @@ onMounted(async () => {
defineRender(() => {
const d = item => (
<Checkbox
modelValue={selectIds.includes(item.id)}
onChange={() => toggleSelect(item)}
size="large"
/>
<Checkbox modelValue={selectIds.includes(item.id)} onChange={() => toggleSelect(item)} size="large" />
)
return (
@@ -194,27 +193,33 @@ defineRender(() => {
<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>
<div class="flex justify-between items-center">
{showCheckbox ? (
<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>
) : <div>{params.total}</div>}
<div class="flex gap-2 relative">
{selectIds.length ? (
{selectIds.length && showCheckbox ? (
<PopConfirm title="确认删除所有选中数据?" onConfirm={handleBatchDel}>
<BaseIcon class="del" title="删除">
<DeleteIcon />
</BaseIcon>
<BaseButton type="info">确认</BaseButton>
</PopConfirm>
) : null}
<BaseIcon onClick={() => (showCheckbox = !showCheckbox)} title="批量删除">
<DeleteIcon />
</BaseIcon>
<BaseIcon onClick={() => (showImportDialog = true)} title="导入">
<IconSystemUiconsImport />
</BaseIcon>
@@ -265,7 +270,7 @@ defineRender(() => {
return (
<div class="list-item-wrapper" key={item.word}>
{s.default({
checkbox: d,
checkbox: showCheckbox ? d : () => void 0,
item,
index: params.pageSize * (params.pageNo - 1) + index + 1,
})}
@@ -297,11 +302,7 @@ defineRender(() => {
</div>
)}
<Dialog
modelValue={showImportDialog}
onUpdate:modelValue={closeImportDialog}
title="导入教程"
>
<Dialog modelValue={showImportDialog} onUpdate:modelValue={closeImportDialog} title="导入教程">
<div className="w-100 p-4 pt-0">
<div>请按照模板的格式来填写数据</div>
<div class="color-red">单词项为必填其他项可不填</div>

View File

@@ -418,8 +418,6 @@ defineExpose({ audioRef })
<!-- 进度条区域 -->
<div class="progress-section">
<!-- 时间显示 -->
<span class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
<!-- 进度条 -->
<div class="progress-container" @mousedown="handleProgressMouseDown" ref="progressBarRef">
<div class="progress-track">
@@ -427,7 +425,8 @@ defineExpose({ audioRef })
<div class="progress-thumb" :style="{ left: progress + '%' }"></div>
</div>
</div>
<!-- 时间显示 -->
<span class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
</div>
<!-- 音量控制 -->

View File

@@ -1,14 +1,14 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
import BaseInput from "@/components/base/BaseInput.vue";
import { computed, onMounted, onUnmounted, ref } from 'vue'
import BaseInput from '@/components/base/BaseInput.vue'
interface IProps {
currentPage?: number;
pageSize?: number;
pageSizes?: number[];
layout?: string;
total: number;
hideOnSinglePage?: boolean;
currentPage?: number
pageSize?: number
pageSizes?: number[]
layout?: string
total: number
hideOnSinglePage?: boolean
}
const props = withDefaults(defineProps<IProps>(), {
@@ -17,55 +17,68 @@ const props = withDefaults(defineProps<IProps>(), {
pageSizes: () => [10, 20, 30, 40, 50, 100],
layout: 'prev, pager, next',
hideOnSinglePage: false,
});
})
const emit = defineEmits<{
'update:currentPage': [val: number];
'update:pageSize': [val: number];
'size-change': [val: number];
'current-change': [val: number];
}>();
'update:currentPage': [val: number]
'update:pageSize': [val: number]
'size-change': [val: number]
'current-change': [val: number]
}>()
const internalCurrentPage = ref(props.currentPage);
const internalPageSize = ref(props.pageSize);
const internalCurrentPage = ref(props.currentPage)
const jumpTarget = $ref('')
const internalPageSize = ref(props.pageSize)
// 计算总页数
const pageCount = computed(() => {
return Math.max(1, Math.ceil(props.total / internalPageSize.value));
});
return Math.max(1, Math.ceil(props.total / internalPageSize.value))
})
// 可用于显示的页码数量,会根据容器宽度动态计算
const availablePagerCount = ref(5); // 默认值
const availablePagerCount = ref(5) // 默认值
// 是否显示分页
const shouldShow = computed(() => {
return props.hideOnSinglePage ? pageCount.value > 1 : true;
});
return props.hideOnSinglePage ? pageCount.value > 1 : true
})
// 处理页码变化
function jumpPage(val: number) {
if (Number(val) > pageCount.value) val = pageCount.value;
if (Number(val) <= 0) val = 1;
internalCurrentPage.value = val;
emit('update:currentPage', Number(val));
emit('current-change', Number(val));
if (Number(val) > pageCount.value) val = pageCount.value
if (Number(val) <= 0) val = 1
internalCurrentPage.value = val
emit('update:currentPage', Number(val))
emit('current-change', Number(val))
}
function jumpToTarget() {
let d = Number(jumpTarget)
if (d > pageCount.value) {
// 这里如果目标值大于页码,那么将目标值作为下标计算,计算出对应的页码再跳转
// 按目标值-1整除每页数量定位属于第几页
let page = Math.floor((d - 1) / internalPageSize.value) + 1
jumpPage(page)
} else {
jumpPage(d)
}
}
// 处理每页条数变化
function handleSizeChange(val: number) {
internalPageSize.value = val;
emit('update:pageSize', val);
emit('size-change', val);
internalPageSize.value = val
emit('update:pageSize', val)
emit('size-change', val)
// 重新计算可用页码数量
calculateAvailablePagerCount();
calculateAvailablePagerCount()
// 重新计算当前页,确保当前页在有效范围内
const newPageCount = Math.ceil(props.total / val);
const newPageCount = Math.ceil(props.total / val)
if (internalCurrentPage.value > newPageCount) {
internalCurrentPage.value = newPageCount;
emit('update:currentPage', newPageCount);
emit('current-change', newPageCount);
internalCurrentPage.value = newPageCount
emit('update:currentPage', newPageCount)
emit('current-change', newPageCount)
}
}
@@ -73,97 +86,85 @@ function handleSizeChange(val: number) {
function calculateAvailablePagerCount() {
// 在下一个渲染周期执行确保DOM已更新
setTimeout(() => {
const paginationEl = document.querySelector('.pagination') as HTMLElement;
if (!paginationEl) return;
const paginationEl = document.querySelector('.pagination') as HTMLElement
if (!paginationEl) return
const containerWidth = paginationEl.offsetWidth;
const buttonWidth = 38; // 按钮宽度包括margin
const availableWidth = containerWidth - 120; // 减去其他元素占用的空间(前后按钮等)
const containerWidth = paginationEl.offsetWidth
const buttonWidth = 38 // 按钮宽度包括margin
const availableWidth = containerWidth - 120 // 减去其他元素占用的空间(前后按钮等)
// 计算可以显示多少个页码按钮
const maxPagers = Math.max(3, Math.floor(availableWidth / buttonWidth) - 2); // 减2是因为第一页和最后一页始终显示
availablePagerCount.value = maxPagers;
}, 0);
const maxPagers = Math.max(3, Math.floor(availableWidth / buttonWidth) - 2) // 减2是因为第一页和最后一页始终显示
availablePagerCount.value = maxPagers
}, 0)
}
// 监听窗口大小变化
onMounted(() => {
window.addEventListener('resize', calculateAvailablePagerCount);
window.addEventListener('resize', calculateAvailablePagerCount)
// 初始计算
calculateAvailablePagerCount();
});
calculateAvailablePagerCount()
})
// 组件卸载时移除监听器
onUnmounted(() => {
window.removeEventListener('resize', calculateAvailablePagerCount);
window.removeEventListener('resize', calculateAvailablePagerCount)
})
// 上一页
function prev() {
const newPage = internalCurrentPage.value - 1;
const newPage = internalCurrentPage.value - 1
if (newPage >= 1) {
jumpPage(newPage);
jumpPage(newPage)
}
}
// 下一页
function next() {
const newPage = internalCurrentPage.value + 1;
const newPage = internalCurrentPage.value + 1
if (newPage <= pageCount.value) {
jumpPage(newPage);
jumpPage(newPage)
}
}
</script>
<template>
<div class="pagination" v-if="shouldShow">
<div class="pagination-container">
<!-- 总数 -->
<span v-if="layout.includes('total')" class="total text-base"> {{ total }} </span>
<!-- 上一页 -->
<button
class="btn-prev"
:disabled="internalCurrentPage <= 1"
@click="prev"
>
<IconFluentChevronLeft20Filled/>
<button class="btn-prev" :disabled="internalCurrentPage <= 1" @click="prev">
<IconFluentChevronLeft20Filled />
</button>
<!-- 页码 -->
<div class="flex items-center">
<div class="w-12">
<BaseInput v-model="internalCurrentPage"
@enter="jumpPage(internalCurrentPage)"
class="text-center"/>
<BaseInput v-model="internalCurrentPage" @enter="jumpPage(internalCurrentPage)" class="text-center" />
</div>
<span class="mx-2">/</span>
<span class="text-base">{{ pageCount }}</span>
</div>
<!-- 下一页 -->
<button
class="btn-next"
:disabled="internalCurrentPage >= pageCount"
@click="next"
>
<IconFluentChevronLeft20Filled class="transform-rotate-180"/>
<button class="btn-next" :disabled="internalCurrentPage >= pageCount" @click="next">
<IconFluentChevronLeft20Filled class="transform-rotate-180" />
</button>
<!-- 每页条数选择器 -->
<div v-if="layout.includes('sizes')" class="sizes">
<select
:value="internalPageSize"
@change="handleSizeChange(Number($event.target.value))"
>
<option v-for="item in pageSizes" :key="item" :value="item">
{{ item }}/
</option>
<select :value="internalPageSize" @change="handleSizeChange(Number($event.target.value))">
<option v-for="item in pageSizes" :key="item" :value="item">{{ item }}/</option>
</select>
</div>
<!-- 总数 -->
<span v-if="layout.includes('total')" class="total text-base">
{{ total }}
</span>
<div class="flex items-center gap-1 ml-2">
跳至
<div class="w-15">
<BaseInput placeholder="页/序号" v-model="jumpTarget" @enter="jumpToTarget" class="text-center" />
</div>
</div>
</div>
</div>
</template>
@@ -186,7 +187,8 @@ function next() {
justify-content: flex-end;
}
.btn-prev, .btn-next {
.btn-prev,
.btn-next {
display: inline-flex;
justify-content: center;
align-items: center;
@@ -200,7 +202,7 @@ function next() {
padding: 0 0.375rem;
margin: 0.25rem 0.25rem;
background-color: transparent;
transition: all .3s;
transition: all 0.3s;
&:disabled {
opacity: 0.3;
@@ -216,7 +218,7 @@ function next() {
.sizes {
border: 1px solid var(--color-input-border);
border-radius: 0.25rem;
padding-right: .2rem;
padding-right: 0.2rem;
background-color: var(--color-bg);
overflow: hidden;
@@ -242,7 +244,6 @@ function next() {
}
.total {
margin: 0.25rem 0.5rem;
color: var(--color-main-text);
}
}

View File

@@ -71,44 +71,29 @@ const ballSize = computed(() => switchHeight.value - 4);
<style scoped lang="scss">
.switch {
display: inline-flex;
align-items: center;
cursor: pointer;
user-select: none;
outline: none;
background-color: #DCDFE6;
position: relative;
transition: background-color 0.3s;
@apply inline-flex items-center cursor-pointer user-select-none outline-none bg-gray-200 position-relative transition-all duration-300;
&.disabled {
cursor: not-allowed;
opacity: 0.6;
@apply cursor-not-allowed opacity-60;
}
&.checked {
background-color: #409eff;
@apply bg-blue-500;
}
.ball {
background-color: #fff;
border-radius: 50%;
transition: transform 0.3s;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
position: absolute;
@apply bg-white rounded-full transition-transform duration-300 box-shadow-sm absolute;
}
.text {
position: absolute;
@apply absolute text-xs text-white user-select-none;
font-size: 0.75rem;
color: #fff;
user-select: none;
&.left {
margin-left: 6px;
@apply ml-1.5;
}
&.right {
right: 6px;
@apply right-1.5;
}
}
}

View File

@@ -111,8 +111,8 @@ defineExpose({scrollBottom})
</template>
</BaseInput>
</div>
<transition-group name="drag" class="list" tag="div">
<div class="item"
<transition-group name="drag" class="space-y-3" tag="div">
<div class="common-list-item"
:class="[
(selectItem.id === item.id) && 'active',
draggable && 'draggable',
@@ -178,42 +178,5 @@ defineExpose({scrollBottom})
.search {
margin: .6rem 0;
}
.list {
.item {
box-sizing: border-box;
background: var(--color-second);
color: var(--color-font-1);
border-radius: .5rem;
margin-bottom: .6rem;
padding: .6rem;
display: flex;
justify-content: space-between;
transition: all .3s;
.right {
display: flex;
flex-direction: column;
transition: all .3s;
opacity: 0;
}
&:hover {
background: var(--color-third);
.right {
opacity: 1;
}
}
&.active {
background: var(--color-fourth);
color: var(--color-font-1);
}
&.draggable {
cursor: move;
}
}
}
}
</style>

View File

@@ -1,43 +1,33 @@
<script setup lang="ts">
import Switch from '@/components/base/Switch.vue'
import Slider from '@/components/base/Slider.vue'
import SettingItem from '@/pages/setting/SettingItem.vue'
import { useSettingStore } from '@/stores/setting.ts'
import Switch from "@/components/base/Switch.vue";
import Slider from "@/components/base/Slider.vue";
import SettingItem from "@/pages/setting/SettingItem.vue";
import { useSettingStore } from "@/stores/setting.ts";
const settingStore = useSettingStore()
</script>
<template>
<!-- 文章练习设置-->
<!-- 文章练习设置-->
<!-- 文章练习设置-->
<div>
<!-- 发音-->
<!-- 发音-->
<!-- 发音-->
<SettingItem mainTitle="音效"/>
<SettingItem mainTitle="音效" />
<SettingItem title="自动播放句子">
<Switch v-model="settingStore.articleSound"/>
<Switch v-model="settingStore.articleSound" />
</SettingItem>
<SettingItem title="自动播放下一篇">
<Switch v-model="settingStore.articleAutoPlayNext"/>
<SettingItem title="结束后播放下一篇">
<Switch v-model="settingStore.articleAutoPlayNext" />
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.articleSoundVolume" showText showValue unit="%"/>
<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/>
<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"/>
<Switch v-model="settingStore.ignoreSymbol" />
</SettingItem>
</div>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -25,9 +25,6 @@ const simpleWords = $computed({
</script>
<template>
<!-- 通用练习设置-->
<!-- 通用练习设置-->
<!-- 通用练习设置-->
<div>
<SettingItem
title="忽略大小写"

View File

@@ -12,17 +12,7 @@ const settingStore = useSettingStore()
</script>
<template>
<!-- 单词练习设置-->
<!-- 单词练习设置-->
<!-- 单词练习设置-->
<div>
<!-- <SettingItem title="练习模式">-->
<!-- <RadioGroup v-model="settingStore.wordPracticeMode" class="flex-col gap-0!">-->
<!-- <Radio :value="WordPracticeMode.System" label="智能模式:自动规划学习、复习、听写、默写"/>-->
<!-- <Radio :value="WordPracticeMode.Free" label="自由模式:系统不强制复习与默写"/>-->
<!-- </RadioGroup>-->
<!-- </SettingItem>-->
<SettingItem title="显示上一个/下一个单词"
desc="开启后,练习中会在上方显示上一个/下一个单词"
>
@@ -134,4 +124,4 @@ const settingStore = useSettingStore()
<style scoped lang="scss">
</style>
</style>