wip
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 音量控制 -->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,9 +25,6 @@ const simpleWords = $computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 通用练习设置-->
|
||||
<!-- 通用练习设置-->
|
||||
<!-- 通用练习设置-->
|
||||
<div>
|
||||
<SettingItem
|
||||
title="忽略大小写"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import {Article, DictId} from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {_nextTick, cloneDeep, loadJsLib} from "@/utils";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import { Article, DictId } from '@/types/types.ts'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import { _nextTick, cloneDeep, loadJsLib } from '@/utils'
|
||||
import { useBaseStore } from '@/stores/base.ts'
|
||||
|
||||
import List from "@/components/list/List.vue";
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {nanoid} from "nanoid";
|
||||
import EditArticle from "@/pages/article/components/EditArticle.vue";
|
||||
import List from '@/components/list/List.vue'
|
||||
import { useWindowClick } from '@/hooks/event.ts'
|
||||
import { MessageBox } from '@/utils/MessageBox.tsx'
|
||||
import { useRuntimeStore } from '@/stores/runtime.ts'
|
||||
import { nanoid } from 'nanoid'
|
||||
import EditArticle from '@/pages/article/components/EditArticle.vue'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import {getDefaultArticle} from "@/types/func.ts";
|
||||
import BackIcon from "@/components/BackIcon.vue";
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import {onMounted} from "vue";
|
||||
import { LIB_JS_URL, Origin } from "@/config/env.ts";
|
||||
import {syncBookInMyStudyList} from "@/hooks/article.ts";
|
||||
import { getDefaultArticle } from '@/types/func.ts'
|
||||
import BackIcon from '@/components/BackIcon.vue'
|
||||
import MiniDialog from '@/components/dialog/MiniDialog.vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { LIB_JS_URL, Origin } from '@/config/env.ts'
|
||||
import { syncBookInMyStudyList } from '@/hooks/article.ts'
|
||||
|
||||
const base = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
@@ -43,31 +43,31 @@ function checkDataChange() {
|
||||
editArticle.textTranslate = editArticle.textTranslate.trim()
|
||||
|
||||
if (
|
||||
editArticle.title !== article.title ||
|
||||
editArticle.titleTranslate !== article.titleTranslate ||
|
||||
editArticle.text !== article.text ||
|
||||
editArticle.textTranslate !== article.textTranslate
|
||||
editArticle.title !== article.title ||
|
||||
editArticle.titleTranslate !== article.titleTranslate ||
|
||||
editArticle.text !== article.text ||
|
||||
editArticle.textTranslate !== article.textTranslate
|
||||
) {
|
||||
return MessageBox.confirm(
|
||||
'检测到数据有变动,是否保存?',
|
||||
'提示',
|
||||
async () => {
|
||||
let r = await editArticleRef.save('save')
|
||||
if (r) resolve(true)
|
||||
},
|
||||
() => resolve(true),
|
||||
'检测到数据有变动,是否保存?',
|
||||
'提示',
|
||||
async () => {
|
||||
let r = await editArticleRef.save('save')
|
||||
if (r) resolve(true)
|
||||
},
|
||||
() => resolve(true)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (editArticle.title.trim() && editArticle.text.trim()) {
|
||||
return MessageBox.confirm(
|
||||
'检测到数据有变动,是否保存?',
|
||||
'提示',
|
||||
async () => {
|
||||
let r = await editArticleRef.save('save')
|
||||
if (r) resolve(true)
|
||||
},
|
||||
() => resolve(true),
|
||||
'检测到数据有变动,是否保存?',
|
||||
'提示',
|
||||
async () => {
|
||||
let r = await editArticleRef.save('save')
|
||||
if (r) resolve(true)
|
||||
},
|
||||
() => resolve(true)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ function saveAndNext(val: Article) {
|
||||
}
|
||||
|
||||
let showExport = $ref(false)
|
||||
useWindowClick(() => showExport = false)
|
||||
useWindowClick(() => (showExport = false))
|
||||
|
||||
onMounted(() => {
|
||||
if (runtimeStore.editDict.articles.length) {
|
||||
@@ -129,26 +129,28 @@ function importData(e: any) {
|
||||
let file = e.target.files[0]
|
||||
if (!file) return
|
||||
// no()
|
||||
let reader = new FileReader();
|
||||
let reader = new FileReader()
|
||||
reader.onload = async function (s) {
|
||||
importLoading = true
|
||||
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
|
||||
let data = s.target.result;
|
||||
let workbook = XLSX.read(data, {type: 'binary'});
|
||||
let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']);
|
||||
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX)
|
||||
let data = s.target.result
|
||||
let workbook = XLSX.read(data, { type: 'binary' })
|
||||
let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1'])
|
||||
if (res.length) {
|
||||
let articles = res.map(v => {
|
||||
if (v['原文标题'] && v['原文正文']) {
|
||||
return getDefaultArticle({
|
||||
id: nanoid(6),
|
||||
title: String(v['原文标题']),
|
||||
titleTranslate: String(v['译文标题']),
|
||||
text: String(v['原文正文']),
|
||||
textTranslate: String(v['译文正文']),
|
||||
audioSrc: String(v['音频地址']),
|
||||
})
|
||||
}
|
||||
}).filter(v => v)
|
||||
let articles = res
|
||||
.map(v => {
|
||||
if (v['原文标题'] && v['原文正文']) {
|
||||
return getDefaultArticle({
|
||||
id: nanoid(6),
|
||||
title: String(v['原文标题']),
|
||||
titleTranslate: String(v['译文标题']),
|
||||
text: String(v['原文正文']),
|
||||
textTranslate: String(v['译文正文']),
|
||||
audioSrc: String(v['音频地址']),
|
||||
})
|
||||
}
|
||||
})
|
||||
.filter(v => v)
|
||||
|
||||
let repeat = []
|
||||
let noRepeat = []
|
||||
@@ -166,22 +168,22 @@ function importData(e: any) {
|
||||
|
||||
if (repeat.length) {
|
||||
MessageBox.confirm(
|
||||
'文章"' + repeat.map(v => v.title).join(', ') + '" 已存在,是否覆盖原有文章?',
|
||||
'检测到重复文章',
|
||||
() => {
|
||||
repeat.map(v => {
|
||||
runtimeStore.editDict.articles[v.index] = v
|
||||
delete runtimeStore.editDict.articles[v.index]["index"]
|
||||
})
|
||||
setTimeout(listEl?.scrollToBottom, 100)
|
||||
},
|
||||
null,
|
||||
() => {
|
||||
e.target.value = ''
|
||||
importLoading = false
|
||||
syncBookInMyStudyList()
|
||||
Toast.success('导入成功!')
|
||||
}
|
||||
'文章"' + repeat.map(v => v.title).join(', ') + '" 已存在,是否覆盖原有文章?',
|
||||
'检测到重复文章',
|
||||
() => {
|
||||
repeat.map(v => {
|
||||
runtimeStore.editDict.articles[v.index] = v
|
||||
delete runtimeStore.editDict.articles[v.index]['index']
|
||||
})
|
||||
setTimeout(listEl?.scrollToBottom, 100)
|
||||
},
|
||||
null,
|
||||
() => {
|
||||
e.target.value = ''
|
||||
importLoading = false
|
||||
syncBookInMyStudyList()
|
||||
Toast.success('导入成功!')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
syncBookInMyStudyList()
|
||||
@@ -192,14 +194,14 @@ function importData(e: any) {
|
||||
}
|
||||
e.target.value = ''
|
||||
importLoading = false
|
||||
};
|
||||
reader.readAsBinaryString(file);
|
||||
}
|
||||
reader.readAsBinaryString(file)
|
||||
}
|
||||
|
||||
async function exportData(val: { type: string, data?: Article }) {
|
||||
async function exportData(val: { type: string; data?: Article }) {
|
||||
exportLoading = true
|
||||
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
|
||||
const {type, data} = val
|
||||
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX)
|
||||
const { type, data } = val
|
||||
let list = []
|
||||
let filename = ''
|
||||
if (type === 'item') {
|
||||
@@ -224,7 +226,7 @@ async function exportData(val: { type: string, data?: Article }) {
|
||||
})
|
||||
wb.Sheets['Sheet1'] = XLSX.utils.json_to_sheet(sheetData)
|
||||
wb.SheetNames = ['Sheet1']
|
||||
XLSX.writeFile(wb, `${filename}.xlsx`);
|
||||
XLSX.writeFile(wb, `${filename}.xlsx`)
|
||||
Toast.success(filename + ' 导出成功!')
|
||||
showExport = false
|
||||
exportLoading = false
|
||||
@@ -240,51 +242,48 @@ function updateList(e) {
|
||||
<div class="add-article">
|
||||
<div class="aslide">
|
||||
<header class="flex gap-2 items-center">
|
||||
<BackIcon/>
|
||||
<BackIcon />
|
||||
<div class="text-xl">{{ runtimeStore.editDict.name }}</div>
|
||||
</header>
|
||||
<List
|
||||
ref="listEl"
|
||||
:list="runtimeStore.editDict.articles"
|
||||
@update:list="updateList"
|
||||
:select-item="article"
|
||||
@del-select-item="article = getDefaultArticle()"
|
||||
@select-item="selectArticle"
|
||||
ref="listEl"
|
||||
:list="runtimeStore.editDict.articles"
|
||||
@update:list="updateList"
|
||||
:select-item="article"
|
||||
@del-select-item="article = getDefaultArticle()"
|
||||
@select-item="selectArticle"
|
||||
>
|
||||
<template v-slot="{item,index}">
|
||||
<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 v-slot="{ item, index }">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</List>
|
||||
<div class="add" v-if="!article.title">
|
||||
正在添加新文章...
|
||||
</div>
|
||||
<div class="add" v-if="!article.title">正在添加新文章...</div>
|
||||
<div class="footer">
|
||||
<div class="import">
|
||||
<BaseButton :loading="importLoading">导入</BaseButton>
|
||||
<input type="file"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
@change="importData">
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
@change="importData"
|
||||
/>
|
||||
</div>
|
||||
<div class="export"
|
||||
style="position: relative"
|
||||
@click.stop="null">
|
||||
<div class="export" style="position: relative" @click.stop="null">
|
||||
<BaseButton @click="showExport = true">导出</BaseButton>
|
||||
<MiniDialog
|
||||
v-model="showExport"
|
||||
style="width: 8rem;bottom: calc(100% + 1rem);top:unset;"
|
||||
>
|
||||
<div class="mini-row-title">
|
||||
导出选项
|
||||
</div>
|
||||
<MiniDialog v-model="showExport" style="width: 8rem; bottom: calc(100% + 1rem); top: unset">
|
||||
<div class="mini-row-title">导出选项</div>
|
||||
<div class="flex">
|
||||
<BaseButton :loading="exportLoading" @click="exportData({type:'all'})">全部</BaseButton>
|
||||
<BaseButton :loading="exportLoading" :disabled="!article.id"
|
||||
@click="exportData({type:'item',data:article})">当前
|
||||
<BaseButton :loading="exportLoading" @click="exportData({ type: 'all' })">全部</BaseButton>
|
||||
<BaseButton
|
||||
:loading="exportLoading"
|
||||
:disabled="!article.id"
|
||||
@click="exportData({ type: 'item', data: article })"
|
||||
>当前
|
||||
</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
@@ -292,24 +291,18 @@ function updateList(e) {
|
||||
<BaseButton @click="add">新增</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<EditArticle
|
||||
ref="editArticleRef"
|
||||
type="batch"
|
||||
@save="saveArticle"
|
||||
@saveAndNext="saveAndNext"
|
||||
:article="article"/>
|
||||
<EditArticle ref="editArticleRef" type="batch" @save="saveArticle" @saveAndNext="saveAndNext" :article="article" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
|
||||
.add-article {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
color: var(--color-font-1);
|
||||
display: flex;
|
||||
background: var(--color-second);
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
@@ -320,7 +313,7 @@ function updateList(e) {
|
||||
.aslide {
|
||||
width: 14vw;
|
||||
height: 100%;
|
||||
padding: 0 .6rem;
|
||||
padding: 0 0.6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -341,12 +334,12 @@ function updateList(e) {
|
||||
.add {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: .5rem;
|
||||
margin-bottom: .6rem;
|
||||
padding: .6rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.6rem;
|
||||
padding: 0.6rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: all .3s;
|
||||
transition: all 0.3s;
|
||||
color: var(--color-font-active-1);
|
||||
background: var(--color-select-bg);
|
||||
}
|
||||
@@ -354,7 +347,7 @@ function updateList(e) {
|
||||
.footer {
|
||||
height: $height;
|
||||
display: flex;
|
||||
gap: .6rem;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import BackIcon from '@/components/BackIcon.vue'
|
||||
import Empty from '@/components/Empty.vue'
|
||||
import ArticleList from '@/components/list/ArticleList.vue'
|
||||
import { useBaseStore } from '@/stores/base.ts'
|
||||
import { Article, Dict, DictId, DictType, ShortcutKey } from '@/types/types.ts'
|
||||
import { Article, Dict, DictId, DictType } from '@/types/types.ts'
|
||||
import { useRuntimeStore } from '@/stores/runtime.ts'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import EditBook from '@/pages/article/components/EditBook.vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { _dateFormat, _getDictDataByUrl, msToHourMinute, resourceWrap, total, useNav } from '@/utils'
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { _dateFormat, _getDictDataByUrl, isMobile, msToHourMinute, resourceWrap, total, useNav, _nextTick } from '@/utils'
|
||||
import { getDefaultArticle, getDefaultDict } from '@/types/func.ts'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import ArticleAudio from '@/pages/article/components/ArticleAudio.vue'
|
||||
@@ -20,6 +19,7 @@ import { useFetch } from '@vueuse/core'
|
||||
import { AppEnv, DICT_LIST } from '@/config/env.ts'
|
||||
import { detail } from '@/apis'
|
||||
import BaseIcon from '@/components/BaseIcon.vue'
|
||||
import Switch from '@/components/base/Switch.vue'
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -49,12 +49,11 @@ function handleCheckedChange(val) {
|
||||
selectArticle = val.item
|
||||
}
|
||||
|
||||
async function addMyStudyList() {
|
||||
async function startPractice() {
|
||||
let sbook = runtimeStore.editDict
|
||||
if (!sbook.articles.length) {
|
||||
return Toast.warning('没有文章可学习!')
|
||||
}
|
||||
|
||||
studyLoading = true
|
||||
await base.changeBook(sbook)
|
||||
studyLoading = false
|
||||
@@ -108,7 +107,23 @@ async function init() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(init)
|
||||
onMounted(() => {
|
||||
init()
|
||||
if (displayMode === 'typing-style') {
|
||||
positionTranslations()
|
||||
}
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
function handleResize() {
|
||||
if (displayMode === 'typing-style') {
|
||||
positionTranslations()
|
||||
}
|
||||
}
|
||||
|
||||
function formClose() {
|
||||
if (isEdit) isEdit = false
|
||||
@@ -160,6 +175,7 @@ const totalSpend = $computed(() => {
|
||||
|
||||
function next() {
|
||||
if (!settingStore.articleAutoPlayNext) return
|
||||
startPlay = true
|
||||
let index = runtimeStore.editDict.articles.findIndex(v => v.id === selectArticle.id)
|
||||
if (index > -1) {
|
||||
//如果是最后一个
|
||||
@@ -177,8 +193,106 @@ const list = $computed(() => {
|
||||
].concat(runtimeStore.editDict.articles)
|
||||
})
|
||||
|
||||
let showAudio = $ref(false)
|
||||
let showTranslate = $ref(true)
|
||||
let startPlay = $ref(false)
|
||||
let displayMode = $ref<'normal' | 'typing-style'>('normal')
|
||||
let articleWrapperRef = $ref<HTMLElement>()
|
||||
const isMob = isMobile()
|
||||
|
||||
const handleVolumeUpdate = (volume: number) => {
|
||||
settingStore.articleSoundVolume = volume
|
||||
}
|
||||
|
||||
const handleSpeedUpdate = (speed: number) => {
|
||||
settingStore.articleSoundSpeed = speed
|
||||
}
|
||||
|
||||
// 解析文本为段落和句子结构
|
||||
interface ParsedSentence {
|
||||
text: string
|
||||
translate: string
|
||||
}
|
||||
|
||||
interface ParsedParagraph {
|
||||
sentences: ParsedSentence[]
|
||||
}
|
||||
|
||||
function parseTextToSections(text: string, textTranslate: string): ParsedParagraph[] {
|
||||
if (!text) return []
|
||||
|
||||
// 按段落分割(双换行)
|
||||
const textParagraphs = text.split('\n\n').filter(p => p.trim())
|
||||
const translateParagraphs = textTranslate ? textTranslate.split('\n\n').filter(p => p.trim()) : []
|
||||
|
||||
// 句子分割正则:按句号、问号、感叹号分割,但保留标点
|
||||
const sentenceRegex = /([^.!?]+[.!?]+)/g
|
||||
|
||||
return textParagraphs.map((para, paraIndex) => {
|
||||
// 分割句子
|
||||
const sentences = para.match(sentenceRegex) || [para]
|
||||
const translateSentences = translateParagraphs[paraIndex]
|
||||
? (translateParagraphs[paraIndex].match(sentenceRegex) || [translateParagraphs[paraIndex]])
|
||||
: []
|
||||
|
||||
return {
|
||||
sentences: sentences.map((sent, sentIndex) => ({
|
||||
text: sent.trim(),
|
||||
translate: translateSentences[sentIndex]?.trim() || '',
|
||||
})),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算解析后的文章结构
|
||||
const parsedArticle = $computed(() => {
|
||||
if (!selectArticle.text || displayMode !== 'typing-style') return null
|
||||
return parseTextToSections(selectArticle.text, selectArticle.textTranslate || '')
|
||||
})
|
||||
|
||||
// 定位翻译到原文下方
|
||||
function positionTranslations() {
|
||||
if (!parsedArticle || isMob || !articleWrapperRef) return
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
_nextTick(() => {
|
||||
if (!articleWrapperRef) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const articleRect = articleWrapperRef.getBoundingClientRect()
|
||||
|
||||
parsedArticle?.forEach((paragraph, paraIndex) => {
|
||||
paragraph.sentences.forEach((sentence, sentIndex) => {
|
||||
const location = `${paraIndex}-${sentIndex}`
|
||||
const sentenceClassName = `.sentence-${location}`
|
||||
const sentenceEl = articleWrapperRef?.querySelector(sentenceClassName)
|
||||
const translateClassName = `.translate-${location}`
|
||||
const translateEl = articleWrapperRef?.querySelector(translateClassName) as HTMLDivElement
|
||||
|
||||
if (sentenceEl && translateEl && sentence.translate) {
|
||||
const sentenceRect = sentenceEl.getBoundingClientRect()
|
||||
translateEl.style.opacity = '1'
|
||||
translateEl.style.top = sentenceRect.top - articleRect.top + 24 + 'px'
|
||||
const spaceEl = translateEl.firstElementChild as HTMLElement
|
||||
if (spaceEl) {
|
||||
spaceEl.style.width = sentenceRect.left - articleRect.left + 'px'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
resolve()
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
// 监听显示模式和文章变化,重新定位翻译
|
||||
watch([() => displayMode, () => selectArticle.id, () => showTranslate], () => {
|
||||
if (displayMode === 'typing-style') {
|
||||
positionTranslations()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -196,7 +310,7 @@ let showTranslate = $ref(true)
|
||||
</BaseButton>
|
||||
<BaseButton :loading="studyLoading || loading" type="info" @click="isEdit = true">编辑</BaseButton>
|
||||
<BaseButton type="info" @click="router.push('batch-edit-article')">文章管理</BaseButton>
|
||||
<BaseButton :loading="studyLoading || loading" @click="addMyStudyList">学习</BaseButton>
|
||||
<BaseButton :loading="studyLoading || loading" @click="startPractice">学习</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 overflow-hidden mt-3">
|
||||
@@ -211,8 +325,8 @@ let showTranslate = $ref(true)
|
||||
</ArticleList>
|
||||
<Empty v-else />
|
||||
</div>
|
||||
<div class="flex-1 shrink-0 pl-4 overflow-auto">
|
||||
<div v-if="selectArticle.id">
|
||||
<div class="flex-1 shrink-0 pl-4 flex flex-col overflow-hidden">
|
||||
<template v-if="selectArticle.id">
|
||||
<template v-if="selectArticle.id === -1">
|
||||
<div class="flex gap-4 mt-2">
|
||||
<img
|
||||
@@ -226,64 +340,133 @@ let showTranslate = $ref(true)
|
||||
<div class="text-base" v-if="totalSpend">总学习时长:{{ totalSpend }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="">
|
||||
<div class="text-3xl flex justify-between items-center relative">
|
||||
<span>
|
||||
<span class="font-bold">{{ selectArticle.title }}</span>
|
||||
<span class="ml-6 text-2xl" v-if="showTranslate">{{ selectArticle.titleTranslate }}</span>
|
||||
</span>
|
||||
<div>
|
||||
<BaseIcon title="显示音频" @click="showAudio = !showAudio">
|
||||
<IconBxVolumeFull />
|
||||
</BaseIcon>
|
||||
<BaseIcon :title="`开关释义显示`" @click="showTranslate = !showTranslate">
|
||||
<IconFluentTranslate16Regular v-if="showTranslate" />
|
||||
<IconFluentTranslateOff16Regular v-else />
|
||||
</BaseIcon>
|
||||
<div class="flex-1 space-y-10 overflow-auto pb-30">
|
||||
<div>
|
||||
<div class="flex justify-between items-center relative">
|
||||
<span class="text-3xl">
|
||||
<span class="font-bold">{{ selectArticle.title }}</span>
|
||||
<span class="ml-6 text-2xl" v-if="showTranslate">{{ selectArticle.titleTranslate }}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2 mr-4">
|
||||
<BaseIcon :title="`切换显示模式`" @click="displayMode = displayMode === 'normal' ? 'typing-style' : 'normal'">
|
||||
<IconFluentTextParagraph16Regular v-if="displayMode === 'normal'" />
|
||||
<IconFluentTextAlignLeft16Regular v-else />
|
||||
</BaseIcon>
|
||||
<BaseIcon :title="`开关释义显示`" @click="showTranslate = !showTranslate">
|
||||
<IconFluentTranslate16Regular v-if="showTranslate" />
|
||||
<IconFluentTranslateOff16Regular v-else />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-2xl" v-if="selectArticle?.question?.text">
|
||||
Question: {{ selectArticle?.question?.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 普通显示模式 -->
|
||||
<template v-if="displayMode === 'normal'">
|
||||
<div class="text-2xl en-article-family space-y-5" v-if="selectArticle.text">
|
||||
<div v-for="t in selectArticle.text.split('\n\n')">{{ t }}</div>
|
||||
<div class="text-right italic">{{ selectArticle?.quote?.text }}</div>
|
||||
</div>
|
||||
|
||||
<template v-if="showTranslate">
|
||||
<div class="line"></div>
|
||||
<div class="text-xl line-height-normal space-y-5" v-if="selectArticle.textTranslate">
|
||||
<div class="mt-2" v-if="selectArticle?.question?.translate">
|
||||
问题: {{ selectArticle?.question?.translate }}
|
||||
</div>
|
||||
<div v-for="t in selectArticle.textTranslate.split('\n\n')">{{ t }}</div>
|
||||
<div class="text-right italic">{{ selectArticle?.quote?.translate }}</div>
|
||||
</div>
|
||||
<Empty v-else />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 打字式显示模式 -->
|
||||
<template v-else-if="displayMode === 'typing-style' && parsedArticle">
|
||||
<div
|
||||
class="article-content"
|
||||
:class="[showTranslate && 'tall']"
|
||||
ref="articleWrapperRef"
|
||||
>
|
||||
<article>
|
||||
<div class="section" v-for="(paragraph, paraIndex) in parsedArticle" :key="paraIndex">
|
||||
<span
|
||||
class="sentence"
|
||||
:class="`sentence-${paraIndex}-${sentIndex}`"
|
||||
v-for="(sentence, sentIndex) in paragraph.sentences"
|
||||
:key="`${paraIndex}-${sentIndex}`"
|
||||
>
|
||||
{{ sentence.text }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-right italic" v-if="selectArticle?.quote?.text">
|
||||
{{ selectArticle?.quote?.text }}
|
||||
</div>
|
||||
</article>
|
||||
<div class="translate" v-show="showTranslate">
|
||||
<template v-for="(paragraph, paraIndex) in parsedArticle" :key="`t-${paraIndex}`">
|
||||
<div
|
||||
class="row"
|
||||
:class="`translate-${paraIndex}-${sentIndex}`"
|
||||
v-for="(sentence, sentIndex) in paragraph.sentences"
|
||||
:key="`${paraIndex}-${sentIndex}`"
|
||||
>
|
||||
<span class="space"></span>
|
||||
<Transition name="fade">
|
||||
<span class="text" v-if="sentence.translate">{{ sentence.translate }}</span>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
<div class="text-right italic" v-if="selectArticle?.quote?.translate">
|
||||
{{ selectArticle?.quote?.translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 移动端显示翻译 -->
|
||||
<template v-if="isMob && showTranslate">
|
||||
<div class="sentence-translate-mobile" v-for="(paragraph, paraIndex) in parsedArticle" :key="`m-${paraIndex}`">
|
||||
<div v-for="(sentence, sentIndex) in paragraph.sentences" :key="`${paraIndex}-${sentIndex}`">
|
||||
<div v-if="sentence.translate" class="mt-2">{{ sentence.translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="currentPractice.length">
|
||||
<div class="line"></div>
|
||||
<div class="font-family text-base pr-2">
|
||||
<div class="text-2xl font-bold">学习记录</div>
|
||||
<div class="mt-1 mb-3">总学习时长:{{ msToHourMinute(total(currentPractice, 'spend')) }}</div>
|
||||
<div
|
||||
class="item border border-item border-solid mt-2 p-2 bg-[var(--bg-history)] rounded-md flex justify-between"
|
||||
v-for="i in currentPractice"
|
||||
>
|
||||
<span class="color-gray">{{ _dateFormat(i.startDate) }}</span>
|
||||
<span>{{ msToHourMinute(i.spend) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="border-t-1 border-t-gray-300 border-solid border-0 center gap-2 pt-4">
|
||||
<ArticleAudio
|
||||
v-if="showAudio"
|
||||
class="mt-4"
|
||||
:article="selectArticle"
|
||||
:autoplay="settingStore.articleAutoPlayNext"
|
||||
@update-speed="handleSpeedUpdate"
|
||||
@update-volume="handleVolumeUpdate"
|
||||
:autoplay="(settingStore.articleAutoPlayNext && startPlay)"
|
||||
@ended="next"
|
||||
/>
|
||||
<div class="mb-4 mt-4 text-2xl" v-if="selectArticle?.question?.text">
|
||||
Question: {{ selectArticle?.question?.text }}
|
||||
</div>
|
||||
<div class="text-2xl line-height-normal en-article-family" v-if="selectArticle.text">
|
||||
<div class="my-6" v-for="t in selectArticle.text.split('\n\n')">{{ t }}</div>
|
||||
<div class="text-right italic mb-5">{{ selectArticle?.quote?.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line my-10"></div>
|
||||
<div class="mt-6" v-if="showTranslate">
|
||||
<div class="text-xl line-height-normal" v-if="selectArticle.textTranslate">
|
||||
<div class="my-5" v-for="t in selectArticle.textTranslate.split('\n\n')">{{ t }}</div>
|
||||
<div class="text-right italic mb-5">{{ selectArticle?.quote?.translate }}</div>
|
||||
</div>
|
||||
<Empty v-else />
|
||||
</div>
|
||||
<div class="font-family text-base mb-4 pr-2" v-if="currentPractice.length">
|
||||
<div class="line my-10"></div>
|
||||
<div class="text-2xl font-bold">学习记录</div>
|
||||
<div class="mt-1 mb-3">总学习时长:{{ msToHourMinute(total(currentPractice, 'spend')) }}</div>
|
||||
<div
|
||||
class="item border border-item border-solid mt-2 p-2 bg-[var(--bg-history)] rounded-md flex justify-between"
|
||||
v-for="i in currentPractice"
|
||||
>
|
||||
<span class="color-gray">{{ _dateFormat(i.startDate) }}</span>
|
||||
<span>{{ msToHourMinute(i.spend) }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>结束后播放下一篇</span>
|
||||
<Switch v-model="settingStore.articleAutoPlayNext" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<Empty v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-0 dict-detail-card" v-else>
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" @click="isAdd ? $router.back() : (isEdit = false)" />
|
||||
@@ -311,6 +494,76 @@ let showTranslate = $ref(true)
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
// 打字式显示模式样式(复用 TypingArticle 的样式)
|
||||
$translate-lh: 3.2;
|
||||
$article-lh: 2.4;
|
||||
|
||||
.article-content {
|
||||
position: relative;
|
||||
color: var(--color-article);
|
||||
font-size: 1.6rem;
|
||||
|
||||
&.tall {
|
||||
article {
|
||||
line-height: $article-lh;
|
||||
}
|
||||
}
|
||||
|
||||
article {
|
||||
word-break: keep-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--en-article-family);
|
||||
|
||||
.section {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.sentence {
|
||||
transition: all 0.3s;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.translate {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: 1.2rem;
|
||||
line-height: $translate-lh;
|
||||
letter-spacing: 0.2rem;
|
||||
font-family: var(--zh-article-family);
|
||||
font-weight: bold;
|
||||
color: #818181;
|
||||
|
||||
.row {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
|
||||
.space {
|
||||
transition: all 0.3s;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sentence-translate-mobile {
|
||||
display: none;
|
||||
margin-top: 0.4rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
color: var(--color-font-3);
|
||||
font-family: var(--zh-article-family);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dict-detail-card {
|
||||
height: calc(100vh - 2rem);
|
||||
@@ -356,5 +609,49 @@ let showTranslate = $ref(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配 - 打字式显示模式
|
||||
@media (max-width: 768px) {
|
||||
.article-content {
|
||||
article {
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.sentence {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.translate {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sentence-translate-mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.article-content {
|
||||
article {
|
||||
.section {
|
||||
.sentence {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sentence-translate-mobile {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```````` ;
|
||||
|
||||
@@ -145,15 +145,11 @@ const initAudio = () => {
|
||||
}
|
||||
|
||||
const handleVolumeUpdate = (volume: number) => {
|
||||
settingStore.setState({
|
||||
articleSoundVolume: volume,
|
||||
})
|
||||
settingStore.articleSoundVolume = volume
|
||||
}
|
||||
|
||||
const handleSpeedUpdate = (speed: number) => {
|
||||
settingStore.setState({
|
||||
articleSoundSpeed: speed,
|
||||
})
|
||||
settingStore.articleSoundSpeed = speed
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -517,7 +513,7 @@ provide('currentPractice', currentPractice)
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="bottom ">
|
||||
<div class="bottom">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="stat">
|
||||
<div class="row">
|
||||
@@ -551,8 +547,6 @@ provide('currentPractice', currentPractice)
|
||||
<ArticleAudio
|
||||
ref="audioRef"
|
||||
:article="articleData.article"
|
||||
:autoplay="settingStore.articleAutoPlayNext"
|
||||
@ended="settingStore.articleAutoPlayNext && next()"
|
||||
@update-speed="handleSpeedUpdate"
|
||||
@update-volume="handleVolumeUpdate"
|
||||
></ArticleAudio>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { Article } from "@/types/types.ts";
|
||||
import { ref, watch, nextTick } from "vue";
|
||||
import { get } from "idb-keyval";
|
||||
import Audio from "@/components/base/Audio.vue";
|
||||
import { LOCAL_FILE_KEY } from "@/config/env.ts";
|
||||
import { Article } from '@/types/types.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get } from 'idb-keyval'
|
||||
import Audio from '@/components/base/Audio.vue'
|
||||
import { LOCAL_FILE_KEY } from '@/config/env.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
article: Article
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'ended'): [],
|
||||
(e: 'update-volume', volume: number): void,
|
||||
(e: 'ended'): []
|
||||
(e: 'update-volume', volume: number): void
|
||||
(e: 'update-speed', volume: number): void
|
||||
}>();
|
||||
}>()
|
||||
|
||||
let file = $ref(null)
|
||||
let instance = $ref<{ audioRef: HTMLAudioElement }>({ audioRef: null })
|
||||
@@ -31,14 +31,14 @@ const setAudioRefValue = (key: string, value: any) => {
|
||||
if (instance?.audioRef) {
|
||||
switch (key) {
|
||||
case 'currentTime':
|
||||
instance.audioRef.currentTime = value;
|
||||
break;
|
||||
instance.audioRef.currentTime = value
|
||||
break
|
||||
case 'volume':
|
||||
instance.audioRef.volume = value;
|
||||
break;
|
||||
instance.audioRef.volume = value
|
||||
break
|
||||
case 'playbackRate':
|
||||
instance.audioRef.playbackRate = value;
|
||||
break;
|
||||
instance.audioRef.playbackRate = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -48,61 +48,85 @@ const setAudioRefValue = (key: string, value: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.article.audioFileId, async () => {
|
||||
if (!props.article.audioSrc && props.article.audioFileId) {
|
||||
let list = await get(LOCAL_FILE_KEY)
|
||||
if (list) {
|
||||
let rItem = list.find((file) => file.id === props.article.audioFileId)
|
||||
if (rItem) {
|
||||
file = URL.createObjectURL(rItem.file)
|
||||
watch(
|
||||
() => props.article.audioFileId,
|
||||
async () => {
|
||||
if (!props.article.audioSrc && props.article.audioFileId) {
|
||||
let list = await get(LOCAL_FILE_KEY)
|
||||
if (list) {
|
||||
let rItem = list.find(file => file.id === props.article.audioFileId)
|
||||
if (rItem) {
|
||||
file = URL.createObjectURL(rItem.file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
file = null
|
||||
}
|
||||
} else {
|
||||
file = null
|
||||
}
|
||||
}, { immediate: true })
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听instance变化,设置之前pending的值
|
||||
watch(() => instance, (newVal) => {
|
||||
Object.entries(pendingUpdates.value).forEach(([key, value]) => {
|
||||
setAudioRefValue(key, value)
|
||||
});
|
||||
pendingUpdates.value = {};
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
() => instance,
|
||||
newVal => {
|
||||
Object.entries(pendingUpdates.value).forEach(([key, value]) => {
|
||||
setAudioRefValue(key, value)
|
||||
})
|
||||
pendingUpdates.value = {}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
//转发一遍,这里Proxy的默认值不能为{},可能是vue做了什么
|
||||
defineExpose(new Proxy({
|
||||
currentTime: 0,
|
||||
played: false,
|
||||
src: '',
|
||||
volume: 0,
|
||||
playbackRate: 1,
|
||||
play: () => void 0,
|
||||
pause: () => void 0,
|
||||
}, {
|
||||
get(target, key) {
|
||||
if (key === 'currentTime') return instance?.audioRef?.currentTime
|
||||
if (key === 'played') return instance?.audioRef?.played
|
||||
if (key === 'src') return instance?.audioRef?.src
|
||||
if (key === 'volume') return instance?.audioRef?.volume
|
||||
if (key === 'playbackRate') return instance?.audioRef?.playbackRate
|
||||
if (key === 'play') instance?.audioRef?.play()
|
||||
if (key === 'pause') instance?.audioRef?.pause()
|
||||
return target[key]
|
||||
},
|
||||
set(_, key, value) {
|
||||
setAudioRefValue(key as string, value)
|
||||
return true
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
|
||||
defineExpose(
|
||||
new Proxy(
|
||||
{
|
||||
currentTime: 0,
|
||||
played: false,
|
||||
src: '',
|
||||
volume: 0,
|
||||
playbackRate: 1,
|
||||
play: () => void 0,
|
||||
pause: () => void 0,
|
||||
},
|
||||
{
|
||||
get(target, key) {
|
||||
if (key === 'currentTime') return instance?.audioRef?.currentTime
|
||||
if (key === 'played') return instance?.audioRef?.played
|
||||
if (key === 'src') return instance?.audioRef?.src
|
||||
if (key === 'volume') return instance?.audioRef?.volume
|
||||
if (key === 'playbackRate') return instance?.audioRef?.playbackRate
|
||||
if (key === 'play') instance?.audioRef?.play()
|
||||
if (key === 'pause') instance?.audioRef?.pause()
|
||||
return target[key]
|
||||
},
|
||||
set(_, key, value) {
|
||||
setAudioRefValue(key as string, value)
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Audio v-bind="$attrs" ref="instance" v-if="props.article.audioSrc" :src="props.article.audioSrc"
|
||||
@ended="emit('ended')" @update-volume="handleVolumeUpdate" @update-speed="handleSpeedUpdate" />
|
||||
<Audio v-bind="$attrs" ref="instance" v-else-if="file" :src="file" @ended="emit('ended')"
|
||||
@update-volume="handleVolumeUpdate" @update-speed="handleSpeedUpdate" />
|
||||
<Audio
|
||||
v-bind="$attrs"
|
||||
ref="instance"
|
||||
v-if="props.article.audioSrc"
|
||||
:src="props.article.audioSrc"
|
||||
@ended="emit('ended')"
|
||||
@update-volume="handleVolumeUpdate"
|
||||
@update-speed="handleSpeedUpdate"
|
||||
/>
|
||||
<Audio
|
||||
v-bind="$attrs"
|
||||
ref="instance"
|
||||
v-else-if="file"
|
||||
:src="file"
|
||||
@ended="emit('ended')"
|
||||
@update-volume="handleVolumeUpdate"
|
||||
@update-speed="handleSpeedUpdate"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="tsx">
|
||||
import { DictId, Sort } from '@/types/types.ts'
|
||||
|
||||
import { add2MyDict, detail } from '@/apis'
|
||||
import { detail } from '@/apis'
|
||||
import BackIcon from '@/components/BackIcon.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import BaseIcon from '@/components/BaseIcon.vue'
|
||||
@@ -23,23 +23,14 @@ import { useBaseStore } from '@/stores/base.ts'
|
||||
import { useRuntimeStore } from '@/stores/runtime.ts'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { getDefaultDict } from '@/types/func.ts'
|
||||
import {
|
||||
_getDictDataByUrl,
|
||||
_nextTick,
|
||||
convertToWord,
|
||||
isMobile,
|
||||
loadJsLib,
|
||||
reverse,
|
||||
shuffle,
|
||||
useNav,
|
||||
} from '@/utils'
|
||||
import { _getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, reverse, shuffle, useNav } from '@/utils'
|
||||
import { MessageBox } from '@/utils/MessageBox.tsx'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { wordDelete } from '@/apis/words.ts'
|
||||
import { copyOfficialDict } from '@/apis/dict.ts'
|
||||
import {PRACTICE_WORD_CACHE} from "@/utils/cache.ts";
|
||||
import { PRACTICE_WORD_CACHE } from '@/utils/cache.ts'
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
@@ -81,10 +72,7 @@ function syncDictInMyStudyList(study = false) {
|
||||
|
||||
runtimeStore.editDict.words = allList
|
||||
let temp = runtimeStore.editDict
|
||||
if (
|
||||
!temp.custom &&
|
||||
![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(temp.id)
|
||||
) {
|
||||
if (!temp.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(temp.id)) {
|
||||
temp.custom = true
|
||||
if (!temp.id.includes('_custom')) {
|
||||
temp.id += '_custom_' + nanoid(6)
|
||||
@@ -193,17 +181,13 @@ function word2Str(word) {
|
||||
res.trans = word.trans.map(v => (v.pos + v.cn).replaceAll('"', '')).join('\n')
|
||||
res.sentences = word.sentences.map(v => (v.c + '\n' + v.cn).replaceAll('"', '')).join('\n\n')
|
||||
res.phrases = word.phrases.map(v => (v.c + '\n' + v.cn).replaceAll('"', '')).join('\n\n')
|
||||
res.synos = word.synos
|
||||
.map(v => (v.pos + v.cn + '\n' + v.ws.join('/')).replaceAll('"', ''))
|
||||
.join('\n\n')
|
||||
res.synos = word.synos.map(v => (v.pos + v.cn + '\n' + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
|
||||
res.relWords = word.relWords.root
|
||||
? '词根:' +
|
||||
word.relWords.root +
|
||||
'\n\n' +
|
||||
word.relWords.rels
|
||||
.map(v =>
|
||||
(v.pos + '\n' + v.words.map(v => v.c + ':' + v.cn).join('\n')).replaceAll('"', '')
|
||||
)
|
||||
.map(v => (v.pos + '\n' + v.words.map(v => v.c + ':' + v.cn).join('\n')).replaceAll('"', ''))
|
||||
.join('\n\n')
|
||||
: ''
|
||||
res.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
|
||||
@@ -521,10 +505,7 @@ function getLocalList({ pageNo, pageSize, searchKey }) {
|
||||
}
|
||||
|
||||
async function requestList({ pageNo, pageSize, searchKey }) {
|
||||
if (
|
||||
!dict.custom &&
|
||||
![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(dict.en_name || dict.id)
|
||||
) {
|
||||
if (!dict.custom && ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(dict.en_name || dict.id)) {
|
||||
// 非自定义词典,直接请求json
|
||||
|
||||
//如果没数据则请求
|
||||
@@ -582,15 +563,9 @@ defineRender(() => {
|
||||
<div className="card mb-0 dict-detail-card flex flex-col">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" />
|
||||
<div class="dict-title absolute page-title text-align-center w-full">
|
||||
{runtimeStore.editDict.name}
|
||||
</div>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
|
||||
<div class="dict-actions flex">
|
||||
<BaseButton
|
||||
loading={studyLoading || loading}
|
||||
type="info"
|
||||
onClick={() => (isEdit = true)}
|
||||
>
|
||||
<BaseButton loading={studyLoading || loading} type="info" onClick={() => (isEdit = true)}>
|
||||
编辑
|
||||
</BaseButton>
|
||||
<BaseButton loading={studyLoading || loading} type="info" onClick={startTest}>
|
||||
@@ -611,25 +586,17 @@ defineRender(() => {
|
||||
{/* 移动端标签页导航 */}
|
||||
{isMob && isOperate && (
|
||||
<div class="tab-navigation mb-3">
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
|
||||
onClick={() => (activeTab = 'list')}
|
||||
>
|
||||
<div class={`tab-item ${activeTab === 'list' ? 'active' : ''}`} onClick={() => (activeTab = 'list')}>
|
||||
单词列表
|
||||
</div>
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
|
||||
onClick={() => (activeTab = 'edit')}
|
||||
>
|
||||
<div class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`} onClick={() => (activeTab = 'edit')}>
|
||||
{wordForm.id ? '编辑' : '添加'}单词
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex flex-1 overflow-hidden content-area">
|
||||
<div
|
||||
class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}
|
||||
>
|
||||
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
class="h-full"
|
||||
@@ -643,21 +610,14 @@ defineRender(() => {
|
||||
importLoading={importLoading}
|
||||
>
|
||||
{val => (
|
||||
<WordItem
|
||||
showTransPop={false}
|
||||
showCollectIcon={false}
|
||||
showMarkIcon={false}
|
||||
item={val.item}
|
||||
>
|
||||
<WordItem showTransPop={false}
|
||||
onClick={() => editWord(val.item)}
|
||||
index={val.index} showCollectIcon={false} showMarkIcon={false} item={val.item}>
|
||||
{{
|
||||
prefix: () => val.checkbox(val.item),
|
||||
suffix: () => (
|
||||
<div class="flex flex-col">
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
onClick={() => editWord(val.item)}
|
||||
title="编辑"
|
||||
>
|
||||
<BaseIcon class="option-icon" onClick={() => editWord(val.item)} title="编辑">
|
||||
<IconFluentTextEditStyle20Regular />
|
||||
</BaseIcon>
|
||||
<PopConfirm title="确认删除?" onConfirm={() => batchDel([val.item.id])}>
|
||||
@@ -673,9 +633,7 @@ defineRender(() => {
|
||||
</BaseTable>
|
||||
</div>
|
||||
{isOperate ? (
|
||||
<div
|
||||
class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}
|
||||
>
|
||||
<div class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
|
||||
<div class="common-title">{wordForm.id ? '修改' : '添加'}单词</div>
|
||||
<Form
|
||||
class="flex-1 overflow-auto pr-2"
|
||||
@@ -685,22 +643,13 @@ defineRender(() => {
|
||||
label-width="7rem"
|
||||
>
|
||||
<FormItem label="单词" prop="word">
|
||||
<BaseInput
|
||||
modelValue={wordForm.word}
|
||||
onUpdate:modelValue={e => (wordForm.word = e)}
|
||||
></BaseInput>
|
||||
<BaseInput modelValue={wordForm.word} onUpdate:modelValue={e => (wordForm.word = e)}></BaseInput>
|
||||
</FormItem>
|
||||
<FormItem label="英音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic0}
|
||||
onUpdate:modelValue={e => (wordForm.phonetic0 = e)}
|
||||
/>
|
||||
<BaseInput modelValue={wordForm.phonetic0} onUpdate:modelValue={e => (wordForm.phonetic0 = e)} />
|
||||
</FormItem>
|
||||
<FormItem label="美音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic1}
|
||||
onUpdate:modelValue={e => (wordForm.phonetic1 = e)}
|
||||
/>
|
||||
<BaseInput modelValue={wordForm.phonetic1} onUpdate:modelValue={e => (wordForm.phonetic1 = e)} />
|
||||
</FormItem>
|
||||
<FormItem label="翻译">
|
||||
<Textarea
|
||||
@@ -781,12 +730,7 @@ defineRender(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<EditBook
|
||||
isAdd={isAdd}
|
||||
isBook={false}
|
||||
onClose={formClose}
|
||||
onSubmit={() => (isEdit = isAdd = false)}
|
||||
/>
|
||||
<EditBook isAdd={isAdd} isBook={false} onClose={formClose} onSubmit={() => (isEdit = isAdd = false)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface Statistics {
|
||||
new: number //新学单词数量
|
||||
review: number //复习单词数量
|
||||
wrong: number //错误数
|
||||
title: string //文章标题
|
||||
}
|
||||
|
||||
export enum Sort {
|
||||
|
||||
Reference in New Issue
Block a user